@@ -26,10 +26,7 @@ public static function register(): void {
2626 }
2727
2828 $ registry_class = '\DataMachine\Engine\AI\MemoryFileRegistry ' ;
29- if ( ! is_callable (array ( $ registry_class , 'register ' )) ) {
30- return ;
31- }
32- $ register = array ( $ registry_class , 'register ' );
29+ $ register = array ( $ registry_class , 'register ' );
3330 /** @var callable $register */
3431
3532 call_user_func (
@@ -149,6 +146,7 @@ private static function render_workspace_policy_intro( string $workspace_path ):
149146 * @param string $default Default policy markdown.
150147 * @param string $workspace_path Resolved DMC workspace root.
151148 */
149+ /** @var mixed $filtered */
152150 $ filtered = apply_filters ('datamachine_code_workspace_policy_intro ' , $ default , $ workspace_path );
153151 if ( ! is_string ($ filtered ) ) {
154152 return $ default ;
@@ -175,6 +173,7 @@ private static function render_workspace_policy_section( string $workspace_path,
175173 * @param string $workspace_path Resolved DMC workspace root.
176174 * @param string $wp WP-CLI command prefix.
177175 */
176+ /** @var mixed $filtered */
178177 $ filtered = apply_filters ('datamachine_code_workspace_policy_section ' , $ default , $ workspace_path , $ wp );
179178 if ( ! is_string ($ filtered ) ) {
180179 return $ default ;
@@ -288,10 +287,7 @@ private static function resolve_wp_cli_cmd(): string {
288287 */
289288 private static function register_section ( string $ file , string $ section , int $ priority , callable $ callback , array $ metadata ): void {
290289 $ registry_class = '\DataMachine\Engine\AI\SectionRegistry ' ;
291- if ( ! is_callable (array ( $ registry_class , 'register ' )) ) {
292- return ;
293- }
294- $ register = array ( $ registry_class , 'register ' );
290+ $ register = array ( $ registry_class , 'register ' );
295291 /** @var callable $register */
296292
297293 call_user_func ($ register , $ file , $ section , $ priority , $ callback , $ metadata );
@@ -374,21 +370,32 @@ private static function render_workspace_inventory_section( string $wp ): string
374370 $ mode = 'compact ' ;
375371 }
376372
377- $ lines = array ();
373+ $ lines = array ();
374+ $ attention_lines = array ();
378375 foreach ( $ by_repo as $ repo => $ bucket ) {
379- $ primary = $ bucket ['primary ' ];
376+ $ primary = is_array ( $ bucket ['primary ' ]) ? $ bucket [ ' primary ' ] : array () ;
380377 $ worktrees = $ bucket ['worktrees ' ];
381378 $ wt_count = count ($ worktrees );
382379 $ branch = $ primary ['branch ' ] ?? null ;
383380 $ remote = $ primary ['remote ' ] ?? null ;
384381 $ branch_str = ( null !== $ branch && '' !== $ branch ) ? sprintf (' (`%s`) ' , $ branch ) : '' ;
382+ $ freshness = is_array ($ primary ['primary_freshness ' ] ?? null ) ? $ primary ['primary_freshness ' ] : null ;
383+
384+ $ attention = self ::format_primary_freshness_attention ($ repo , $ freshness );
385+ if ( '' !== $ attention ) {
386+ $ attention_lines [] = $ attention ;
387+ }
385388
386389 if ( 'compact ' === $ mode ) {
387390 $ suffix_parts = array ();
388391 $ suffix_parts [] = sprintf ('%d %s ' , $ wt_count , 1 === $ wt_count ? 'worktree ' : 'worktrees ' );
389392 if ( null !== $ remote && '' !== $ remote ) {
390393 $ suffix_parts [] = $ remote ;
391394 }
395+ $ freshness_badge = self ::format_primary_freshness_badge ($ freshness );
396+ if ( '' !== $ freshness_badge ) {
397+ $ suffix_parts [] = $ freshness_badge ;
398+ }
392399 $ lines [] = sprintf ('- **%s**%s — %s ' , $ repo , $ branch_str , implode (' · ' , $ suffix_parts ));
393400 continue ;
394401 }
@@ -397,6 +404,10 @@ private static function render_workspace_inventory_section( string $wp ): string
397404 if ( null !== $ remote && '' !== $ remote ) {
398405 $ header .= ' — ' . $ remote ;
399406 }
407+ $ freshness_badge = self ::format_primary_freshness_badge ($ freshness );
408+ if ( '' !== $ freshness_badge ) {
409+ $ header .= ' · ' . $ freshness_badge ;
410+ }
400411 $ lines [] = $ header ;
401412
402413 usort (
@@ -415,11 +426,12 @@ private static function render_workspace_inventory_section( string $wp ): string
415426 }
416427 }
417428
418- $ body = implode ("\n" , $ lines );
419- $ generated_at = gmdate ('c ' );
420- $ workspace_path = $ listing ['path ' ];
421- $ agent_slug = self ::resolve_agent_slug ();
422- $ agent_suffix = '' !== $ agent_slug ? ' --agent= ' . $ agent_slug : '' ;
429+ $ body = implode ("\n" , $ lines );
430+ $ attention_block = self ::render_primary_freshness_attention_block ($ attention_lines );
431+ $ generated_at = gmdate ('c ' );
432+ $ workspace_path = $ listing ['path ' ];
433+ $ agent_slug = self ::resolve_agent_slug ();
434+ $ agent_suffix = '' !== $ agent_slug ? ' --agent= ' . $ agent_slug : '' ;
423435
424436 return <<<MD
425437## Workspace Inventory
@@ -428,6 +440,101 @@ private static function render_workspace_inventory_section( string $wp ): string
428440
429441Refresh this file with ` {$ wp } datamachine memory compose AGENTS.md {$ agent_suffix }` after workspace changes if the inventory looks stale.
430442
443+ {$ attention_block }
444+
445+ {$ body }
446+ MD ;
447+ }
448+
449+ private static function primary_freshness_needs_attention ( ?array $ freshness ): bool {
450+ if ( null === $ freshness ) {
451+ return false ;
452+ }
453+
454+ $ status = (string ) ( $ freshness ['status ' ] ?? '' );
455+ return in_array ($ status , array ( 'stale ' , 'diverged ' , 'detached ' , 'unknown ' , 'no_upstream ' , 'ahead ' ), true );
456+ }
457+
458+ private static function primary_freshness_needs_refresh ( ?array $ freshness ): bool {
459+ if ( null === $ freshness ) {
460+ return false ;
461+ }
462+
463+ $ status = (string ) ( $ freshness ['status ' ] ?? '' );
464+ return in_array ($ status , array ( 'stale ' , 'diverged ' ), true );
465+ }
466+
467+ private static function format_primary_freshness_badge ( ?array $ freshness ): string {
468+ if ( ! self ::primary_freshness_needs_attention ($ freshness ) ) {
469+ return '' ;
470+ }
471+
472+ $ status = (string ) ( $ freshness ['status ' ] ?? 'unknown ' );
473+ $ parts = array ( 'primary ' . $ status );
474+ if ( isset ($ freshness ['behind ' ]) && is_numeric ($ freshness ['behind ' ]) && (int ) $ freshness ['behind ' ] > 0 ) {
475+ $ parts [] = sprintf ('behind %d ' , (int ) $ freshness ['behind ' ]);
476+ }
477+ if ( isset ($ freshness ['ahead ' ]) && is_numeric ($ freshness ['ahead ' ]) && (int ) $ freshness ['ahead ' ] > 0 ) {
478+ $ parts [] = sprintf ('ahead %d ' , (int ) $ freshness ['ahead ' ]);
479+ }
480+
481+ return implode (', ' , $ parts );
482+ }
483+
484+ private static function format_primary_freshness_attention ( string $ repo , ?array $ freshness ): string {
485+ if ( ! self ::primary_freshness_needs_attention ($ freshness ) ) {
486+ return '' ;
487+ }
488+
489+ $ status = (string ) ( $ freshness ['status ' ] ?? 'unknown ' );
490+ $ branch = (string ) ( $ freshness ['branch ' ] ?? '' );
491+ $ upstream = (string ) ( $ freshness ['upstream ' ] ?? '' );
492+ $ details = array ();
493+ if ( '' !== $ branch ) {
494+ $ details [] = sprintf ('branch `%s` ' , $ branch );
495+ }
496+ if ( '' !== $ upstream ) {
497+ $ details [] = sprintf ('upstream `%s` ' , $ upstream );
498+ }
499+ if ( isset ($ freshness ['behind ' ]) && is_numeric ($ freshness ['behind ' ]) ) {
500+ $ details [] = sprintf ('behind %d ' , (int ) $ freshness ['behind ' ]);
501+ }
502+ if ( isset ($ freshness ['ahead ' ]) && is_numeric ($ freshness ['ahead ' ]) ) {
503+ $ details [] = sprintf ('ahead %d ' , (int ) $ freshness ['ahead ' ]);
504+ }
505+
506+ $ line = sprintf ('- **%s** primary is `%s` ' , $ repo , $ status );
507+ if ( ! empty ($ details ) ) {
508+ $ line .= ' ( ' . implode (', ' , $ details ) . ') ' ;
509+ }
510+
511+ $ command = (string ) ( $ freshness ['suggested_command ' ] ?? '' );
512+ if ( '' !== $ command && self ::primary_freshness_needs_refresh ($ freshness ) ) {
513+ $ line .= sprintf ('. Refresh: `%s` ' , $ command );
514+ }
515+
516+ return $ line . '. ' ;
517+ }
518+
519+ private static function render_primary_freshness_attention_block ( array $ attention_lines ): string {
520+ if ( empty ($ attention_lines ) ) {
521+ return '' ;
522+ }
523+
524+ $ max_lines = 20 ;
525+ $ shown = array_slice ($ attention_lines , 0 , $ max_lines );
526+ $ omitted = count ($ attention_lines ) - count ($ shown );
527+ if ( $ omitted > 0 ) {
528+ $ shown [] = sprintf ('- %d more primary checkout(s) need attention; run `wp datamachine-code workspace list` for the full set. ' , $ omitted );
529+ }
530+
531+ $ body = implode ("\n" , $ shown );
532+
533+ return <<<MD
534+ **Primary Checkout Attention**
535+
536+ These primary checkouts may be stale or unsafe to read. Refresh them or create a worktree from an explicit remote ref before using them as source evidence.
537+
431538{$ body }
432539MD ;
433540 }
0 commit comments