@@ -349,9 +349,6 @@ public static function find_duplicate_task_ownership( array $worktrees ): array
349349 $ buckets = array ();
350350
351351 foreach ( $ worktrees as $ row ) {
352- if ( ! is_array ( $ row ) ) {
353- continue ;
354- }
355352 $ handle = (string ) ( $ row ['handle ' ] ?? '' );
356353 if ( '' === $ handle ) {
357354 continue ;
@@ -369,7 +366,7 @@ public static function find_duplicate_task_ownership( array $worktrees ): array
369366
370367 $ duplicates = array ();
371368 foreach ( $ buckets as $ bucket ) {
372- $ handles = array_values ( array_unique ( ( array ) ( $ bucket ['handles ' ] ?? array () ) ) );
369+ $ handles = array_values ( array_unique ( $ bucket ['handles ' ] ) );
373370 if ( count ( $ handles ) < 2 ) {
374371 continue ;
375372 }
@@ -510,14 +507,14 @@ private static function resolve_primary_id( array $session, array $ids ): ?strin
510507
511508 // Pass 1: session_id across registered runtimes, in registration order.
512509 foreach ( array_keys ( $ signatures ) as $ runtime_id ) {
513- if ( isset ( $ ids [ $ runtime_id ]['session_id ' ] ) && null !== $ ids [ $ runtime_id ][ ' session_id ' ] ) {
510+ if ( isset ( $ ids [ $ runtime_id ]['session_id ' ] ) ) {
514511 return $ ids [ $ runtime_id ]['session_id ' ];
515512 }
516513 }
517514
518515 // Pass 2: any subkey across registered runtimes, in registration order.
519516 foreach ( array_keys ( $ signatures ) as $ runtime_id ) {
520- if ( ! isset ( $ ids [ $ runtime_id ] ) || ! is_array ( $ ids [ $ runtime_id ] ) ) {
517+ if ( ! isset ( $ ids [ $ runtime_id ] ) ) {
521518 continue ;
522519 }
523520 foreach ( $ ids [ $ runtime_id ] as $ value ) {
@@ -592,7 +589,7 @@ private static function migrate_legacy_origin_session( array $session ): array {
592589 'ids ' => true ,
593590 );
594591 foreach ( $ session as $ key => $ value ) {
595- if ( ! is_string ( $ key ) || isset ( $ canonical_top_level [ $ key ] ) ) {
592+ if ( isset ( $ canonical_top_level [ $ key ] ) ) {
596593 continue ;
597594 }
598595 $ underscore = strpos ( $ key , '_ ' );
@@ -601,9 +598,6 @@ private static function migrate_legacy_origin_session( array $session ): array {
601598 }
602599 $ runtime_id = substr ( $ key , 0 , $ underscore );
603600 $ subkey = substr ( $ key , $ underscore + 1 );
604- if ( '' === $ runtime_id || '' === $ subkey ) {
605- continue ;
606- }
607601 if ( ! isset ( $ ids [ $ runtime_id ] ) || ! is_array ( $ ids [ $ runtime_id ] ) ) {
608602 $ ids [ $ runtime_id ] = array ();
609603 }
@@ -840,12 +834,23 @@ public static function build_payload(): ?array {
840834 $ user_id = $ dm ->get_effective_user_id ( 0 );
841835 $ agent_slug = $ dm ->resolve_agent_slug ( array ( 'user_id ' => $ user_id ) );
842836
843- $ files = array ();
837+ $ files = array ();
838+ $ memory_class = '\\DataMachine \\Core \\FilesRepository \\AgentMemory ' ;
844839 foreach ( self ::MEMORY_FILES as $ filename ) {
845- $ memory = new \DataMachine \Core \FilesRepository \AgentMemory ( $ user_id , 0 , $ filename );
846- $ result = $ memory ->get_all ();
847- if ( ! empty ( $ result ['success ' ] ) && is_string ( $ result ['content ' ] ?? null ) && '' !== trim ( $ result ['content ' ] ) ) {
848- $ files [ $ filename ] = $ result ['content ' ];
840+ $ memory = new $ memory_class ( $ user_id , 0 , $ filename );
841+ $ content = null ;
842+ if ( is_callable ( array ( $ memory , 'get_all ' ) ) ) {
843+ $ result = call_user_func ( array ( $ memory , 'get_all ' ) );
844+ $ content = is_array ( $ result ) && ! empty ( $ result ['success ' ] ) && is_string ( $ result ['content ' ] ?? null ) ? $ result ['content ' ] : null ;
845+ } elseif ( is_callable ( array ( $ memory , 'get_file_path ' ) ) ) {
846+ $ file_path = call_user_func ( array ( $ memory , 'get_file_path ' ) );
847+ if ( is_string ( $ file_path ) && is_readable ( $ file_path ) ) {
848+ $ content = file_get_contents ( $ file_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- AgentMemory returns a validated local file path, not a remote URL.
849+ }
850+ }
851+
852+ if ( is_string ( $ content ) && '' !== trim ( $ content ) ) {
853+ $ files [ $ filename ] = $ content ;
849854 }
850855 }
851856
@@ -1029,12 +1034,10 @@ public static function inject( string $worktree_path, array $payload ): array|\W
10291034 if ( is_wp_error ( $ agents_projection ) ) {
10301035 return $ agents_projection ;
10311036 }
1032- if ( is_array ( $ agents_projection ) ) {
1033- $ written = array_merge ( $ written , $ agents_projection );
1034- }
1037+ $ written = array_merge ( $ written , $ agents_projection );
10351038
10361039 $ exclude_entries = self ::INJECTED_PATHS ;
1037- if ( is_array ( $ agents_projection ) && ! empty ( $ agents_projection ) ) {
1040+ if ( ! empty ( $ agents_projection ) ) {
10381041 $ exclude_entries [] = self ::PROJECTED_AGENTS_PATH ;
10391042 $ exclude_entries [] = self ::PROJECTED_AGENTS_MARKER_PATH ;
10401043 $ exclude_entries [] = self ::PROJECTED_OPENCODE_CONFIG_PATH ;
@@ -1065,7 +1068,7 @@ public static function inject( string $worktree_path, array $payload ): array|\W
10651068 */
10661069 private static function project_site_agents_md ( string $ worktree_path , array $ payload ): array |\WP_Error {
10671070 $ source = isset ( $ payload ['agents_md_path ' ] ) ? (string ) $ payload ['agents_md_path ' ] : '' ;
1068- if ( '' === $ source || ! is_file ( $ source ) ) {
1071+ if ( '' === $ source || ( ! is_file ( $ source ) && self :: can_symlink_site_agents_md ( $ source ) ) ) {
10691072 return array ();
10701073 }
10711074
@@ -1087,17 +1090,30 @@ private static function project_site_agents_md( string $worktree_path, array $pa
10871090 }
10881091 }
10891092
1090- // phpcs:ignore WordPress.WP.AlternativeFunctions.symlink_symlink -- Local checkout projection to a DMC-owned generated file.
1091- if ( ! symlink ( $ source , $ target ) ) {
1092- return new \WP_Error (
1093- 'agents_md_projection_failed ' ,
1094- sprintf ( 'Failed to symlink site AGENTS.md into worktree: %s ' , $ target ),
1095- array ( 'status ' => 500 )
1096- );
1093+ $ projection_kind = 'symlink ' ;
1094+ if ( self ::can_symlink_site_agents_md ( $ source ) ) {
1095+ // phpcs:ignore WordPress.WP.AlternativeFunctions.symlink_symlink -- Local checkout projection to a DMC-owned generated file.
1096+ if ( ! symlink ( $ source , $ target ) ) {
1097+ return new \WP_Error (
1098+ 'agents_md_projection_failed ' ,
1099+ sprintf ( 'Failed to symlink site AGENTS.md into worktree: %s ' , $ target ),
1100+ array ( 'status ' => 500 )
1101+ );
1102+ }
1103+ } else {
1104+ $ projection_kind = 'inline ' ;
1105+ // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
1106+ if ( false === file_put_contents ( $ target , self ::render ( $ payload ) ) ) {
1107+ return new \WP_Error (
1108+ 'agents_md_projection_failed ' ,
1109+ sprintf ( 'Failed to write inline site AGENTS.md into worktree: %s ' , $ target ),
1110+ array ( 'status ' => 500 )
1111+ );
1112+ }
10971113 }
10981114
10991115 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
1100- if ( false === file_put_contents ( $ marker , $ source . "\n" ) ) {
1116+ if ( false === file_put_contents ( $ marker , $ projection_kind . "\n" . $ source . "\n" ) ) {
11011117 // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink -- Roll back the symlink if the ownership marker cannot be written.
11021118 unlink ( $ target );
11031119 return new \WP_Error (
@@ -1110,6 +1126,20 @@ private static function project_site_agents_md( string $worktree_path, array $pa
11101126 return array ( $ target , $ marker );
11111127 }
11121128
1129+ /**
1130+ * Determine whether the site AGENTS.md path can safely be symlinked into a host checkout.
1131+ *
1132+ * Studio's WP-CLI runtime exposes the mounted WordPress tree as `/wordpress` inside
1133+ * PHP-WASM. Symlinking that virtual path into `/Users/.../Developer` creates a
1134+ * broken host symlink and can crash the Studio CLI while syncing filesystem state.
1135+ *
1136+ * @param string $source Absolute AGENTS.md source path as seen by PHP.
1137+ * @return bool Whether a host-visible symlink should be used.
1138+ */
1139+ private static function can_symlink_site_agents_md ( string $ source ): bool {
1140+ return ! str_starts_with ( $ source , '/wordpress/ ' );
1141+ }
1142+
11131143 /**
11141144 * Add the site AGENTS.md to a local OpenCode instructions array.
11151145 *
@@ -1124,8 +1154,8 @@ private static function project_site_agents_md_via_opencode_config( string $work
11241154
11251155 $ config_exists = is_file ( $ config );
11261156 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Path is a marker file within a controlled worktree.
1127- $ previous = $ config_exists ? (string ) file_get_contents ( $ config ) : '' ;
1128- $ data = array (
1157+ $ previous = $ config_exists ? (string ) file_get_contents ( $ config ) : '' ;
1158+ $ data = array (
11291159 '$schema ' => 'https://opencode.ai/config.json ' ,
11301160 'instructions ' => array (),
11311161 );
@@ -1205,7 +1235,15 @@ public static function uninject( string $worktree_path ): array {
12051235 $ projected_agents = rtrim ( $ worktree_path , '/ ' ) . '/ ' . self ::PROJECTED_AGENTS_PATH ;
12061236 $ projection_marker = rtrim ( $ worktree_path , '/ ' ) . '/ ' . self ::PROJECTED_AGENTS_MARKER_PATH ;
12071237 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Path is a marker file within a controlled worktree.
1208- $ marked_source = is_file ( $ projection_marker ) ? trim ( (string ) file_get_contents ( $ projection_marker ) ) : '' ;
1238+ $ marked_source = '' ;
1239+ $ projection_kind = 'symlink ' ;
1240+ if ( is_file ( $ projection_marker ) ) {
1241+ // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Path is a marker file within a controlled worktree.
1242+ $ marker_lines = preg_split ( '/\r\n|\r|\n/ ' , trim ( (string ) file_get_contents ( $ projection_marker ) ) );
1243+ $ marker_lines = false === $ marker_lines ? array () : $ marker_lines ;
1244+ $ projection_kind = in_array ( $ marker_lines [0 ] ?? '' , array ( 'symlink ' , 'inline ' ), true ) ? (string ) $ marker_lines [0 ] : 'symlink ' ;
1245+ $ marked_source = 'symlink ' === $ projection_kind && isset ( $ marker_lines [1 ] ) ? (string ) $ marker_lines [1 ] : (string ) ( $ marker_lines [0 ] ?? '' );
1246+ }
12091247 if (
12101248 is_link ( $ projected_agents ) &&
12111249 '' !== $ marked_source &&
@@ -1214,6 +1252,10 @@ public static function uninject( string $worktree_path ): array {
12141252 // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink -- Removes DMC-injected local-only context symlink from a worktree.
12151253 unlink ( $ projected_agents );
12161254 $ removed [] = $ projected_agents ;
1255+ } elseif ( 'inline ' === $ projection_kind && is_file ( $ projected_agents ) ) {
1256+ // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink -- Removes DMC-injected local-only inline context from a worktree.
1257+ unlink ( $ projected_agents );
1258+ $ removed [] = $ projected_agents ;
12171259 }
12181260 if ( is_file ( $ projection_marker ) ) {
12191261 // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink -- Removes DMC-injected local-only projection marker from a worktree.
@@ -1343,8 +1385,8 @@ private static function resolve_origin_agent(): ?string {
13431385
13441386 try {
13451387 $ dm = new \DataMachine \Core \FilesRepository \DirectoryManager ();
1346- $ user_id = method_exists ( $ dm, ' get_effective_user_id ' ) ? $ dm ->get_effective_user_id ( 0 ) : 0 ;
1347- $ agent_slug = method_exists ( $ dm, ' resolve_agent_slug ' ) ? $ dm ->resolve_agent_slug ( array ( 'user_id ' => $ user_id ) ) : '' ;
1388+ $ user_id = $ dm ->get_effective_user_id ( 0 );
1389+ $ agent_slug = $ dm ->resolve_agent_slug ( array ( 'user_id ' => $ user_id ) );
13481390 return '' !== (string ) $ agent_slug ? (string ) $ agent_slug : null ;
13491391 } catch ( \Throwable $ e ) {
13501392 return null ;
0 commit comments