@@ -399,8 +399,10 @@ pub fn decide(ctx: *DecisionContext) !?Decision {
399399 });
400400 }
401401
402- // Rule 3: Best PPL record → celebrate (low urgency, just notification)
403- if (ctx .farm .best_ppl < 5.0 ) {
402+ // Rule 3: Active farm with good PPL → celebrate (only if training is happening!)
403+ const has_active_training = ctx .farm .active > 0 ;
404+ const has_good_ppl = ctx .farm .best_ppl > 0.0 and ctx .farm .best_ppl < 10.0 ;
405+ if (has_active_training and has_good_ppl ) {
404406 try candidates .append (.{
405407 .kind = .notify ,
406408 .urgency = .normal ,
@@ -445,13 +447,7 @@ pub fn decide(ctx: *DecisionContext) !?Decision {
445447 }
446448 }
447449
448- const reason = switch (action ) {
449- .doctor_quick = > "Build broken, needs healing" ,
450- .farm_recycle = > "Farm has idle/crashed workers" ,
451- .notify = > "Celebrating farm progress" ,
452- .cloud_spawn = > "Agent spawn issues detected" ,
453- else = > "Routine action" ,
454- };
450+ const reason = getContextualReason (action , ctx );
455451
456452 return Decision {
457453 .action = action ,
@@ -520,84 +516,196 @@ pub fn act(ctx: *DecisionContext, decision: Decision) !qt.ActionResult {
520516 return result ;
521517}
522518
519+ // ═══════════════════════════════════════════════════════════════════════════════
520+ // CONTEXT-AWARE MESSAGING — Human-like voice
521+ // ═══════════════════════════════════════════════════════════════════════════════
522+
523+ /// Get contextual reason that explains WHAT and WHY
524+ fn getContextualReason (action : qt.ActionKind , ctx : * const DecisionContext ) []const u8 {
525+ return switch (action ) {
526+ .doctor_quick = > buildBrokenReason (ctx ),
527+ .farm_recycle = > farmRecycleReason (ctx ),
528+ .notify = > celebrationReason (ctx ),
529+ .cloud_spawn = > cloudSpawnReason (ctx ),
530+ else = > "Routine action" ,
531+ };
532+ }
533+
534+ /// Explain why build healing is needed
535+ fn buildBrokenReason (ctx : * const DecisionContext ) []const u8 {
536+ if (ctx .faculty_metrics ) | m | {
537+ if (m .compile_rate < 50 ) {
538+ return "Compile rate very low, needs immediate attention" ;
539+ }
540+ if (m .dirty_files > 100 ) {
541+ return "Too many dirty files, build unstable" ;
542+ }
543+ }
544+ return "Build broken, running quick heal" ;
545+ }
546+
547+ /// Explain farm recycling decision
548+ fn farmRecycleReason (ctx : * const DecisionContext ) []const u8 {
549+ const crashed = ctx .farm .crashed ;
550+ const total = ctx .farm .total_services ;
551+ const active = ctx .farm .active ;
552+ const idle = total - active - crashed ;
553+
554+ if (crashed > 5 ) {
555+ return "Many workers crashed, recycling farm" ;
556+ }
557+ if (idle > 10 ) {
558+ return "Many idle workers, recycling for efficiency" ;
559+ }
560+ return "Farm needs optimization" ;
561+ }
562+
563+ /// Explain celebration (only when there's something to celebrate!)
564+ fn celebrationReason (ctx : * const DecisionContext ) []const u8 {
565+ if (ctx .farm .active == 0 ) {
566+ return "Farm is idle - nothing to celebrate" ;
567+ }
568+
569+ const ppl = ctx .farm .best_ppl ;
570+ if (ppl < 3.0 ) {
571+ return "Excellent PPL achieved!" ;
572+ }
573+ if (ppl < 5.0 ) {
574+ return "Good progress on PPL" ;
575+ }
576+ return "Training running smoothly" ;
577+ }
578+
579+ /// Explain cloud spawn decision
580+ fn cloudSpawnReason (ctx : * const DecisionContext ) []const u8 {
581+ const issues = ctx .issues .agent_spawn ;
582+ const finished = if (ctx .farm .total_services > 0 ) ctx .farm .total_services else 0 ;
583+
584+ if (issues > 3 ) {
585+ return "Multiple agent spawn issues detected" ;
586+ }
587+ if (finished > 0 ) {
588+ return "Spawning replacements for finished containers" ;
589+ }
590+ return "Agent spawn needed" ;
591+ }
592+
523593// ═══════════════════════════════════════════════════════════════════════════════
524594// SPEAK PHASE — Report decision and result via OFC
525595// ═══════════════════════════════════════════════════════════════════════════════
526596
527597pub fn speak (ctx : * DecisionContext , decision : ? Decision , result : qt.ActionResult ) ! void {
528- const mood = queen_ofc .inferMood (ctx .build_ok , ctx .ouroboros_score , false );
598+ // Generate human-like report
599+ var report_buf : [1536 ]u8 = undefined ;
600+ const report = formatHumanReport (& report_buf , ctx , decision , result ) catch return ;
529601
530- var report_buf : [1024 ]u8 = undefined ;
602+ // Send via OFC
603+ ctx .state .cycle + |= 1 ;
604+ try queen_ofc .send (ctx .allocator , .group , report );
605+ }
606+
607+ /// Format human-like report that explains WHAT happened and WHY
608+ fn formatHumanReport (
609+ buf : []u8 ,
610+ ctx : * const DecisionContext ,
611+ decision : ? Decision ,
612+ result : qt.ActionResult ,
613+ ) ! []const u8 {
531614 var offset : usize = 0 ;
532615
533- // Header
534- const header = std .fmt .bufPrint (
535- report_buf [offset .. ],
536- "{s} Queen {s} — Cycle #{d}\n\n " ,
537- .{ mood .emoji (), mood .label (), ctx .state .cycle },
538- ) catch return ;
539- offset += header .len ;
540-
541- // Farm status
542- const farm_line = std .fmt .bufPrint (
543- report_buf [offset .. ],
544- "{s} Farm: {d}/{d} active, PPL {d:.1}" ,
545- .{ qt .E_DNA , ctx .farm .active , ctx .farm .total_services , ctx .farm .best_ppl },
546- ) catch return ;
547- offset += farm_line .len ;
548-
549- if (ctx .farm .best_ppl_service_len > 0 ) {
550- const best_line = std .fmt .bufPrint (
551- report_buf [offset .. ],
552- " ({s})\n " ,
553- .{ctx .farm .bestPplServiceStr ()},
554- ) catch return ;
555- offset += best_line .len ;
556- } else {
557- const newline = "\n " ;
558- if (offset + newline .len <= report_buf .len ) {
559- @memcpy (report_buf [offset .. ][0.. newline .len ], newline );
560- offset += newline .len ;
561- }
616+ // Context-aware header
617+ const header = getContextualHeader (ctx );
618+ if (offset + header .len <= buf .len ) {
619+ @memcpy (buf [offset .. ][0.. header .len ], header );
620+ offset += header .len ;
562621 }
563622
564- // Mu heartbeat
565- const mu_line = std .fmt .bufPrint (
566- report_buf [offset .. ],
567- "{s} Build: {s} | Wake #{d}\n " ,
568- .{ qt .E_BRAIN , if (ctx .build_ok ) "OK" else "FAIL" , ctx .mu_heartbeat .wake },
569- ) catch return ;
570- offset += mu_line .len ;
623+ // Farm status (human-readable)
624+ const farm_status = formatFarmStatus (ctx );
625+ if (offset + farm_status .len <= buf .len ) {
626+ @memcpy (buf [offset .. ][0.. farm_status .len ], farm_status );
627+ offset += farm_status .len ;
628+ }
629+
630+ // Build status
631+ const build_status = if (ctx .build_ok )
632+ "\n ✅ Build is healthy"
633+ else
634+ "\n ❌ Build broken - will attempt healing" ;
635+ if (offset + build_status .len <= buf .len ) {
636+ @memcpy (buf [offset .. ][0.. build_status .len ], build_status );
637+ offset += build_status .len ;
638+ }
571639
572- // Decision report
640+ // Action explanation
573641 if (decision ) | d | {
574- const decision_line = std .fmt .bufPrint (
575- report_buf [offset .. ],
576- "{s} Action: {s} ({s})\n " ,
577- .{ d .action .emojiIcon (), d .action .label (), d .reason },
578- ) catch return ;
579- offset += decision_line .len ;
580-
581- // Result
582- const result_line = std .fmt .bufPrint (
583- report_buf [offset .. ],
584- " Result: {s} ({d}ms)\n " ,
585- .{ if (result .success ) "OK" else "FAIL" , result .duration_ms },
586- ) catch return ;
587- offset += result_line .len ;
642+ const action_text = formatActionExplanation (d , result );
643+ if (offset + action_text .len <= buf .len ) {
644+ @memcpy (buf [offset .. ][0.. action_text .len ], action_text );
645+ offset += action_text .len ;
646+ }
588647 } else {
589- const no_action = "No action needed\n " ;
590- if (offset + no_action .len <= report_buf .len ) {
591- @memcpy (report_buf [offset .. ][0.. no_action .len ], no_action );
648+ const no_action = "\n\n 🧠 Standing by. No action needed. " ;
649+ if (offset + no_action .len <= buf .len ) {
650+ @memcpy (buf [offset .. ][0.. no_action .len ], no_action );
592651 offset += no_action .len ;
593652 }
594653 }
595654
596- const report = report_buf [0.. offset ];
655+ return buf [0.. offset ];
656+ }
597657
598- // Send via OFC
599- ctx .state .cycle + |= 1 ;
600- try queen_ofc .sendReport (ctx .allocator , mood , report );
658+ /// Get header based on actual system state (not generic mood)
659+ fn getContextualHeader (ctx : * const DecisionContext ) []const u8 {
660+ const farm_active = ctx .farm .active ;
661+ const farm_total = ctx .farm .total_services ;
662+
663+ if (farm_active == 0 and farm_total > 0 ) {
664+ // Farm is idle
665+ return "🧠 Farm Status Update\n\n Training farm is idle. " ;
666+ } else if (farm_active > 0 ) {
667+ // Farm is running
668+ return "🧠 Farm Status Update\n\n Training is active. " ;
669+ } else {
670+ // No farm at all
671+ return "🧠 System Status\n\n " ;
672+ }
673+ }
674+
675+ /// Format farm status in human-readable way
676+ fn formatFarmStatus (ctx : * const DecisionContext ) []const u8 {
677+ if (ctx .farm .active == 0 and ctx .farm .total_services == 0 ) {
678+ return "No training services configured." ;
679+ }
680+
681+ if (ctx .farm .active == 0 ) {
682+ if (ctx .farm .best_ppl < 999.0 ) {
683+ // Has results but not currently training
684+ return "Farm is sleeping. " ;
685+ }
686+ return "Farm is offline. " ;
687+ }
688+
689+ // Active training
690+ return "Training in progress. " ;
691+ }
692+
693+ /// Explain what action was taken and why
694+ fn formatActionExplanation (d : Decision , result : qt.ActionResult ) []const u8 {
695+ _ = result ; // Available for future enhancement (e.g., show result emoji)
696+ return switch (d .action ) {
697+ .doctor_quick = > "\n\n 🔧 Running quick heal to fix build issues..." ,
698+ .farm_recycle = > "\n\n ♻️ Recycling farm to recover idle/crashed workers." ,
699+ .notify = > "\n\n 📊 Status update - all systems nominal." ,
700+ .cloud_spawn = > "\n\n 🚀 Spawning new agent containers." ,
701+ .doctor_heal = > "\n\n 🏥 Running deep heal to recover codebase." ,
702+ .git_commit_state = > "\n\n 💾 Committing state to preserve work." ,
703+ .git_push = > "\n\n ☁️ Pushing changes to remote." ,
704+ .issue_comment = > "\n\n 📝 Updating GitHub issue tracker." ,
705+ .arena_battle = > "\n\n ⚔️ Running arena battles for evaluation." ,
706+ .ouroboros_cycle = > "\n\n 🔄 Running Ouroboros health cycle." ,
707+ else = > "\n\n Executing routine action." ,
708+ };
601709}
602710
603711/// Format decision report for Telegram
0 commit comments