aether-colony 5.1.0 → 5.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/.aether/aether-utils.sh +122 -42
  2. package/.aether/commands/colonize.yaml +4 -0
  3. package/.aether/commands/council.yaml +205 -0
  4. package/.aether/commands/init.yaml +46 -13
  5. package/.aether/commands/insert-phase.yaml +4 -0
  6. package/.aether/commands/plan.yaml +53 -2
  7. package/.aether/commands/quick.yaml +104 -0
  8. package/.aether/commands/resume-colony.yaml +6 -4
  9. package/.aether/commands/resume.yaml +9 -0
  10. package/.aether/commands/run.yaml +37 -1
  11. package/.aether/commands/seal.yaml +9 -0
  12. package/.aether/commands/status.yaml +45 -1
  13. package/.aether/docs/command-playbooks/build-full.md +2 -1
  14. package/.aether/docs/command-playbooks/build-prep.md +2 -1
  15. package/.aether/docs/command-playbooks/continue-full.md +1 -0
  16. package/.aether/docs/command-playbooks/continue-verify.md +1 -0
  17. package/.aether/utils/council.sh +425 -0
  18. package/.aether/utils/error-handler.sh +3 -3
  19. package/.aether/utils/flag.sh +23 -12
  20. package/.aether/utils/hive.sh +2 -2
  21. package/.aether/utils/immune.sh +508 -0
  22. package/.aether/utils/learning.sh +2 -2
  23. package/.aether/utils/midden.sh +178 -0
  24. package/.aether/utils/queen.sh +29 -17
  25. package/.aether/utils/session.sh +264 -0
  26. package/.aether/utils/spawn-tree.sh +7 -7
  27. package/.aether/utils/spawn.sh +2 -2
  28. package/.aether/utils/state-api.sh +191 -1
  29. package/.claude/commands/ant/colonize.md +2 -0
  30. package/.claude/commands/ant/council.md +205 -0
  31. package/.claude/commands/ant/init.md +46 -13
  32. package/.claude/commands/ant/insert-phase.md +4 -0
  33. package/.claude/commands/ant/plan.md +27 -1
  34. package/.claude/commands/ant/quick.md +100 -0
  35. package/.claude/commands/ant/resume-colony.md +3 -2
  36. package/.claude/commands/ant/resume.md +9 -0
  37. package/.claude/commands/ant/run.md +37 -1
  38. package/.claude/commands/ant/seal.md +9 -0
  39. package/.claude/commands/ant/status.md +45 -1
  40. package/.opencode/commands/ant/colonize.md +2 -0
  41. package/.opencode/commands/ant/council.md +143 -0
  42. package/.opencode/commands/ant/init.md +46 -13
  43. package/.opencode/commands/ant/insert-phase.md +4 -0
  44. package/.opencode/commands/ant/plan.md +26 -1
  45. package/.opencode/commands/ant/quick.md +91 -0
  46. package/.opencode/commands/ant/resume-colony.md +3 -2
  47. package/.opencode/commands/ant/resume.md +9 -0
  48. package/.opencode/commands/ant/run.md +37 -1
  49. package/.opencode/commands/ant/status.md +2 -0
  50. package/CHANGELOG.md +90 -0
  51. package/README.md +23 -0
  52. package/package.json +10 -2
@@ -488,8 +488,9 @@ _queen_promote() {
488
488
  ev_separator=$(grep -n "^|------|" "$tmp_file" | tail -1 | cut -d: -f1 || true)
489
489
 
490
490
  # Use awk for cross-platform insertion (only if separator found)
491
+ # Use ENVIRON instead of -v for user content to avoid C-escape interpretation (\n, \t, \\)
491
492
  if [[ -n "$ev_separator" ]]; then
492
- awk -v line="$ev_separator" -v entry="$ev_entry" 'NR==line{print; print entry; next}1' "$tmp_file" > "${tmp_file}.ev" && mv "${tmp_file}.ev" "$tmp_file"
493
+ AETHER_EV_ENTRY="$ev_entry" awk -v line="$ev_separator" 'NR==line{print; print ENVIRON["AETHER_EV_ENTRY"]; next}1' "$tmp_file" > "${tmp_file}.ev" && mv "${tmp_file}.ev" "$tmp_file"
493
494
  fi
494
495
 
495
496
  # Update METADATA stats in temp file
@@ -528,9 +529,10 @@ _queen_promote() {
528
529
  ev_log_entry="{\"timestamp\": \"$ts\", \"action\": \"promote\", \"wisdom_type\": \"$wisdom_type\", \"content_hash\": \"$content_hash\", \"colony\": \"$colony_name\"}"
529
530
 
530
531
  # Check if evolution_log exists in metadata, add if not
532
+ # Use ENVIRON instead of -v for user content to avoid C-escape interpretation (\n, \t, \\)
531
533
  if ! grep -q '"evolution_log"' "$tmp_file"; then
532
534
  # Add evolution_log array after stats
533
- awk -v entry="$ev_log_entry" '
535
+ AETHER_EV_LOG_ENTRY="$ev_log_entry" awk '
534
536
  /"stats": \{/ {
535
537
  print
536
538
  # Read until closing brace of stats
@@ -540,19 +542,19 @@ _queen_promote() {
540
542
  }
541
543
  # Add comma and evolution_log
542
544
  print ","
543
- print " \"evolution_log\": [" entry "]"
545
+ print " \"evolution_log\": [" ENVIRON["AETHER_EV_LOG_ENTRY"] "]"
544
546
  next
545
547
  }
546
548
  { print }
547
549
  ' "$tmp_file" > "${tmp_file}.evlog" && mv "${tmp_file}.evlog" "$tmp_file"
548
550
  else
549
551
  # Append to existing evolution_log array
550
- awk -v entry="$ev_log_entry" '
552
+ AETHER_EV_LOG_ENTRY="$ev_log_entry" awk '
551
553
  /"evolution_log": \[/ {
552
554
  # Check if array is empty or has items
553
555
  if (/\]/) {
554
556
  # Empty array - replace with entry
555
- gsub(/"evolution_log": \[\]/, "\"evolution_log\": [" entry "]")
557
+ gsub(/"evolution_log": \[\]/, "\"evolution_log\": [" ENVIRON["AETHER_EV_LOG_ENTRY"] "]")
556
558
  } else {
557
559
  # Has items - need to add before closing bracket
558
560
  # For now, just print and handle in next iteration
@@ -566,7 +568,7 @@ _queen_promote() {
566
568
  getline
567
569
  if (/\]/) {
568
570
  # Was empty, now add entry
569
- print entry
571
+ print ENVIRON["AETHER_EV_LOG_ENTRY"]
570
572
  print "]"
571
573
  } else {
572
574
  # Has items, add comma and entry before closing
@@ -574,7 +576,7 @@ _queen_promote() {
574
576
  while (getline > 0) {
575
577
  if (/^\s*\]/) {
576
578
  print ","
577
- print entry
579
+ print ENVIRON["AETHER_EV_LOG_ENTRY"]
578
580
  print "]"
579
581
  break
580
582
  }
@@ -628,12 +630,18 @@ _queen_promote() {
628
630
  if [[ -n "$meta_section" ]]; then
629
631
  # SUPPRESS:OK -- read-default: returns fallback on failure
630
632
  updated_meta=$(echo "$meta_section" | jq --arg hash "$content_hash" --argjson cols "$colonies_json" '.colonies_contributed[$hash] = $cols' 2>/dev/null || echo "$meta_section")
631
- # Replace metadata section
632
- new_comment="<!-- METADATA"
633
- new_comment="$new_comment
634
- $updated_meta
635
- -->"
636
- awk -v new="$new_comment" '/<!-- METADATA/,/-->/{ if (/<!-- METADATA/) print new; next }1' "$tmp_file" > "${tmp_file}.metaupd" && mv "${tmp_file}.metaupd" "$tmp_file"
633
+ # Replace metadata section using head/tail to handle multi-line content safely
634
+ # awk -v cannot handle embedded newlines in variable values (C-escape interpretation)
635
+ local meta_start_line meta_end_line
636
+ meta_start_line=$(grep -n "^<!-- METADATA$" "$tmp_file" | head -1 | cut -d: -f1)
637
+ meta_end_line=$(grep -n "^-->$" "$tmp_file" | head -1 | cut -d: -f1)
638
+ if [[ -n "$meta_start_line" && -n "$meta_end_line" ]]; then
639
+ {
640
+ head -n $((meta_start_line - 1)) "$tmp_file"
641
+ printf '<!-- METADATA\n%s\n-->\n' "$updated_meta"
642
+ tail -n +$((meta_end_line + 1)) "$tmp_file"
643
+ } > "${tmp_file}.metaupd" && mv "${tmp_file}.metaupd" "$tmp_file"
644
+ fi
637
645
  fi
638
646
  fi
639
647
 
@@ -837,7 +845,8 @@ _queen_write_learnings() {
837
845
  local ev_separator
838
846
  ev_separator=$(grep -n "^|------|" "$tmp_file" | tail -1 | cut -d: -f1 || true)
839
847
  if [[ -n "$ev_separator" ]]; then
840
- awk -v line="$ev_separator" -v entry="$ev_entry" 'NR==line{print; print entry; next}1' "$tmp_file" > "${tmp_file}.ev" && mv "${tmp_file}.ev" "$tmp_file"
848
+ # Use ENVIRON instead of -v for user content to avoid C-escape interpretation (\n, \t, \\)
849
+ AETHER_EV_ENTRY="$ev_entry" awk -v line="$ev_separator" 'NR==line{print; print ENVIRON["AETHER_EV_ENTRY"]; next}1' "$tmp_file" > "${tmp_file}.ev" && mv "${tmp_file}.ev" "$tmp_file"
841
850
  fi
842
851
 
843
852
  # Update METADATA stats: increment total_build_learnings
@@ -967,7 +976,8 @@ _queen_promote_instinct() {
967
976
  local ev_separator
968
977
  ev_separator=$(grep -n "^|------|" "$tmp_file" | tail -1 | cut -d: -f1 || true)
969
978
  if [[ -n "$ev_separator" ]]; then
970
- awk -v line="$ev_separator" -v entry="$ev_entry" 'NR==line{print; print entry; next}1' "$tmp_file" > "${tmp_file}.ev" && mv "${tmp_file}.ev" "$tmp_file"
979
+ # Use ENVIRON instead of -v for user content to avoid C-escape interpretation (\n, \t, \\)
980
+ AETHER_EV_ENTRY="$ev_entry" awk -v line="$ev_separator" 'NR==line{print; print ENVIRON["AETHER_EV_ENTRY"]; next}1' "$tmp_file" > "${tmp_file}.ev" && mv "${tmp_file}.ev" "$tmp_file"
971
981
  fi
972
982
 
973
983
  # Update METADATA stats: increment total_instincts
@@ -1110,7 +1120,8 @@ _queen_seed_from_hive() {
1110
1120
  local ev_separator
1111
1121
  ev_separator=$(grep -n "^|------|" "$tmp_file" | tail -1 | cut -d: -f1 || true)
1112
1122
  if [[ -n "$ev_separator" ]]; then
1113
- awk -v line="$ev_separator" -v entry="$ev_entry" 'NR==line{print; print entry; next}1' "$tmp_file" > "${tmp_file}.ev" && mv "${tmp_file}.ev" "$tmp_file"
1123
+ # Use ENVIRON instead of -v for user content to avoid C-escape interpretation (\n, \t, \\)
1124
+ AETHER_EV_ENTRY="$ev_entry" awk -v line="$ev_separator" 'NR==line{print; print ENVIRON["AETHER_EV_ENTRY"]; next}1' "$tmp_file" > "${tmp_file}.ev" && mv "${tmp_file}.ev" "$tmp_file"
1114
1125
  fi
1115
1126
 
1116
1127
  # Update METADATA stats: increment total_codebase_patterns
@@ -1601,7 +1612,8 @@ _queen_write_charter() {
1601
1612
  local ev_separator
1602
1613
  ev_separator=$(grep -n "^|------|" "$tmp_file" | tail -1 | cut -d: -f1 || true)
1603
1614
  if [[ -n "$ev_separator" ]]; then
1604
- awk -v line="$ev_separator" -v entry="$ev_entry" 'NR==line{print; print entry; next}1' "$tmp_file" > "${tmp_file}.ev" && mv "${tmp_file}.ev" "$tmp_file"
1615
+ # Use ENVIRON instead of -v for user content to avoid C-escape interpretation (\n, \t, \\)
1616
+ AETHER_EV_ENTRY="$ev_entry" awk -v line="$ev_separator" 'NR==line{print; print ENVIRON["AETHER_EV_ENTRY"]; next}1' "$tmp_file" > "${tmp_file}.ev" && mv "${tmp_file}.ev" "$tmp_file"
1605
1617
  fi
1606
1618
 
1607
1619
  # Update METADATA stats -- count non-charter list items in each section, add charter entries
@@ -550,3 +550,267 @@ _session_summary() {
550
550
  [[ "$cleared" == "true" ]] && echo "Status: Context was cleared"
551
551
  fi
552
552
  }
553
+
554
+ # ============================================================================
555
+ # _pending_decision_add
556
+ # Add a decision to the pending decisions queue
557
+ # Usage: pending-decision-add --type <type> --description <desc> [--phase N] [--source <src>]
558
+ # Types: visual_checkpoint, replan, escalation, runtime_verification, user_input
559
+ # ============================================================================
560
+ _pending_decision_add() {
561
+ local pd_type=""
562
+ local pd_description=""
563
+ local pd_phase="null"
564
+ local pd_source=""
565
+
566
+ while [[ $# -gt 0 ]]; do
567
+ case "$1" in
568
+ --type) pd_type="$2"; shift 2 ;;
569
+ --description) pd_description="$2"; shift 2 ;;
570
+ --phase) pd_phase="$2"; shift 2 ;;
571
+ --source) pd_source="$2"; shift 2 ;;
572
+ *) shift ;;
573
+ esac
574
+ done
575
+
576
+ [[ -z "$pd_type" ]] && json_err "$E_VALIDATION_FAILED" "pending-decision-add requires --type"
577
+ [[ -z "$pd_description" ]] && json_err "$E_VALIDATION_FAILED" "pending-decision-add requires --description"
578
+
579
+ local pd_file="$COLONY_DATA_DIR/pending-decisions.json"
580
+ local pd_id="pd_$(date +%s)_$$"
581
+ local pd_now
582
+ pd_now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
583
+
584
+ # Acquire lock for concurrent access
585
+ if type acquire_lock &>/dev/null; then
586
+ acquire_lock "$pd_file" || json_err "$E_LOCK_FAILED" "Failed to acquire lock on pending-decisions.json"
587
+ trap 'release_lock 2>/dev/null || true' EXIT # SUPPRESS:OK -- cleanup: lock may not be held
588
+ fi
589
+
590
+ # Initialize file if missing
591
+ if [[ ! -f "$pd_file" ]]; then
592
+ echo '{"version":"1.0","decisions":[]}' > "$pd_file"
593
+ fi
594
+
595
+ local pd_current
596
+ pd_current=$(cat "$pd_file" 2>/dev/null || echo '{"version":"1.0","decisions":[]}') # SUPPRESS:OK -- read-default: file may not exist yet
597
+
598
+ # Build new decision entry
599
+ local pd_phase_val
600
+ if [[ "$pd_phase" == "null" ]]; then
601
+ pd_phase_val="null"
602
+ else
603
+ pd_phase_val="$pd_phase"
604
+ fi
605
+
606
+ local pd_updated
607
+ pd_updated=$(echo "$pd_current" | jq \
608
+ --arg id "$pd_id" \
609
+ --arg type "$pd_type" \
610
+ --arg description "$pd_description" \
611
+ --argjson phase "${pd_phase_val}" \
612
+ --arg source "$pd_source" \
613
+ --arg created_at "$pd_now" \
614
+ '.decisions += [{
615
+ id: $id,
616
+ type: $type,
617
+ description: $description,
618
+ phase: $phase,
619
+ source: $source,
620
+ created_at: $created_at,
621
+ resolved: false
622
+ }]' 2>/dev/null) || {
623
+ type release_lock &>/dev/null && release_lock 2>/dev/null || true # SUPPRESS:OK -- cleanup: lock may not be held
624
+ json_err "$E_JSON_INVALID" "Failed to append decision to pending-decisions.json"
625
+ }
626
+
627
+ atomic_write "$pd_file" "$pd_updated" || {
628
+ type release_lock &>/dev/null && release_lock 2>/dev/null || true # SUPPRESS:OK -- cleanup: lock may not be held
629
+ json_err "$E_JSON_INVALID" "Failed to write pending-decisions.json"
630
+ }
631
+
632
+ type release_lock &>/dev/null && { release_lock 2>/dev/null || true; trap - EXIT; } # SUPPRESS:OK -- cleanup: lock may not be held
633
+
634
+ local pd_count
635
+ pd_count=$(echo "$pd_updated" | jq '.decisions | length')
636
+
637
+ json_ok "$(jq -n --arg id "$pd_id" --argjson count "$pd_count" \
638
+ '{id: $id, decision_count: $count}')"
639
+ }
640
+
641
+ # ============================================================================
642
+ # _pending_decision_list
643
+ # List decisions from the pending decisions queue
644
+ # Usage: pending-decision-list [--unresolved] [--type <type>]
645
+ # Default: show only unresolved
646
+ # ============================================================================
647
+ _pending_decision_list() {
648
+ local pd_unresolved_only="true"
649
+ local pd_filter_type=""
650
+
651
+ while [[ $# -gt 0 ]]; do
652
+ case "$1" in
653
+ --unresolved) pd_unresolved_only="true"; shift ;;
654
+ --type) pd_filter_type="$2"; shift 2 ;;
655
+ *) shift ;;
656
+ esac
657
+ done
658
+
659
+ local pd_file="$COLONY_DATA_DIR/pending-decisions.json"
660
+
661
+ if [[ ! -f "$pd_file" ]]; then
662
+ json_ok '{"total":0,"unresolved":0,"decisions":[]}'
663
+ exit 0
664
+ fi
665
+
666
+ local pd_data
667
+ pd_data=$(cat "$pd_file" 2>/dev/null || echo '{"version":"1.0","decisions":[]}') # SUPPRESS:OK -- read-default: file may not exist yet
668
+
669
+ # Build jq filter
670
+ local pd_filter='.decisions'
671
+
672
+ # Apply type filter if provided
673
+ if [[ -n "$pd_filter_type" ]]; then
674
+ pd_filter="$pd_filter | map(select(.type == \"$pd_filter_type\"))"
675
+ fi
676
+
677
+ # Apply resolved filter (default: only unresolved)
678
+ if [[ "$pd_unresolved_only" == "true" ]]; then
679
+ pd_filter="$pd_filter | map(select(.resolved == false))"
680
+ fi
681
+
682
+ local pd_total pd_unresolved pd_decisions
683
+ pd_total=$(echo "$pd_data" | jq '.decisions | length')
684
+ pd_unresolved=$(echo "$pd_data" | jq '[.decisions[] | select(.resolved == false)] | length')
685
+ pd_decisions=$(echo "$pd_data" | jq "$pd_filter")
686
+
687
+ json_ok "$(jq -n --argjson total "$pd_total" --argjson unresolved "$pd_unresolved" \
688
+ --argjson decisions "$pd_decisions" \
689
+ '{total: $total, unresolved: $unresolved, decisions: $decisions}')"
690
+ }
691
+
692
+ # ============================================================================
693
+ # _pending_decision_resolve
694
+ # Mark a pending decision as resolved
695
+ # Usage: pending-decision-resolve --id <id> --resolution <text>
696
+ # ============================================================================
697
+ _pending_decision_resolve() {
698
+ local pd_id=""
699
+ local pd_resolution=""
700
+
701
+ while [[ $# -gt 0 ]]; do
702
+ case "$1" in
703
+ --id) pd_id="$2"; shift 2 ;;
704
+ --resolution) pd_resolution="$2"; shift 2 ;;
705
+ *) shift ;;
706
+ esac
707
+ done
708
+
709
+ [[ -z "$pd_id" ]] && json_err "$E_VALIDATION_FAILED" "pending-decision-resolve requires --id"
710
+ [[ -z "$pd_resolution" ]] && json_err "$E_VALIDATION_FAILED" "pending-decision-resolve requires --resolution"
711
+
712
+ local pd_file="$COLONY_DATA_DIR/pending-decisions.json"
713
+
714
+ if [[ ! -f "$pd_file" ]]; then
715
+ json_err "$E_RESOURCE_NOT_FOUND" "No pending decisions file found"
716
+ fi
717
+
718
+ # Acquire lock for concurrent access
719
+ if type acquire_lock &>/dev/null; then
720
+ acquire_lock "$pd_file" || json_err "$E_LOCK_FAILED" "Failed to acquire lock on pending-decisions.json"
721
+ trap 'release_lock 2>/dev/null || true' EXIT # SUPPRESS:OK -- cleanup: lock may not be held
722
+ fi
723
+
724
+ local pd_data
725
+ pd_data=$(cat "$pd_file" 2>/dev/null || echo '{"version":"1.0","decisions":[]}') # SUPPRESS:OK -- read-default: file may not exist yet
726
+
727
+ # Check if ID exists
728
+ local pd_exists
729
+ pd_exists=$(echo "$pd_data" | jq --arg id "$pd_id" '[.decisions[] | select(.id == $id)] | length')
730
+ if [[ "$pd_exists" -eq 0 ]]; then
731
+ type release_lock &>/dev/null && release_lock 2>/dev/null || true # SUPPRESS:OK -- cleanup: lock may not be held
732
+ json_err "$E_RESOURCE_NOT_FOUND" "Decision not found: $pd_id"
733
+ fi
734
+
735
+ local pd_now
736
+ pd_now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
737
+
738
+ local pd_updated
739
+ pd_updated=$(echo "$pd_data" | jq \
740
+ --arg id "$pd_id" \
741
+ --arg resolution "$pd_resolution" \
742
+ --arg resolved_at "$pd_now" \
743
+ '(.decisions[] | select(.id == $id)) |= (. + {resolved: true, resolution: $resolution, resolved_at: $resolved_at})' 2>/dev/null) || {
744
+ type release_lock &>/dev/null && release_lock 2>/dev/null || true # SUPPRESS:OK -- cleanup: lock may not be held
745
+ json_err "$E_JSON_INVALID" "Failed to resolve decision in pending-decisions.json"
746
+ }
747
+
748
+ atomic_write "$pd_file" "$pd_updated" || {
749
+ type release_lock &>/dev/null && release_lock 2>/dev/null || true # SUPPRESS:OK -- cleanup: lock may not be held
750
+ json_err "$E_JSON_INVALID" "Failed to write pending-decisions.json"
751
+ }
752
+
753
+ type release_lock &>/dev/null && { release_lock 2>/dev/null || true; trap - EXIT; } # SUPPRESS:OK -- cleanup: lock may not be held
754
+
755
+ json_ok "$(jq -n --arg id "$pd_id" '{resolved: true, id: $id}')"
756
+ }
757
+
758
+ # ============================================================================
759
+ # _autopilot_headless_check
760
+ # Check whether headless mode is active in run-state.json
761
+ # Usage: autopilot-headless-check
762
+ # Returns: {"ok":true,"result":{"headless":true|false}}
763
+ # ============================================================================
764
+ _autopilot_headless_check() {
765
+ local ah_state_file="$COLONY_DATA_DIR/run-state.json"
766
+
767
+ if [[ ! -f "$ah_state_file" ]]; then
768
+ json_ok '{"headless":false}'
769
+ exit 0
770
+ fi
771
+
772
+ local ah_headless
773
+ ah_headless=$(jq -r '.headless // false' "$ah_state_file" 2>/dev/null || echo "false") # SUPPRESS:OK -- read-default: field may not exist
774
+
775
+ # Normalize to boolean
776
+ if [[ "$ah_headless" == "true" ]]; then
777
+ json_ok '{"headless":true}'
778
+ else
779
+ json_ok '{"headless":false}'
780
+ fi
781
+ }
782
+
783
+ # ============================================================================
784
+ # _autopilot_set_headless
785
+ # Set the headless flag in run-state.json
786
+ # Usage: autopilot-set-headless <true|false>
787
+ # Returns: {"ok":true,"result":{"headless":true|false,"updated":true}}
788
+ # ============================================================================
789
+ _autopilot_set_headless() {
790
+ local ah_value="${1:-}"
791
+
792
+ if [[ "$ah_value" != "true" && "$ah_value" != "false" ]]; then
793
+ json_err "$E_VALIDATION_FAILED" "autopilot-set-headless requires true or false argument"
794
+ fi
795
+
796
+ local ah_state_file="$COLONY_DATA_DIR/run-state.json"
797
+
798
+ if [[ ! -f "$ah_state_file" ]]; then
799
+ json_err "$E_FILE_NOT_FOUND" "run-state.json not found — autopilot not active"
800
+ fi
801
+
802
+ local ah_headless_bool
803
+ [[ "$ah_value" == "true" ]] && ah_headless_bool=true || ah_headless_bool=false
804
+
805
+ local ah_current ah_updated
806
+ ah_current=$(cat "$ah_state_file" 2>/dev/null || echo '{}') # SUPPRESS:OK -- read-default: file may not exist yet
807
+ ah_updated=$(echo "$ah_current" | jq --argjson headless "$ah_headless_bool" '.headless = $headless' 2>/dev/null) || {
808
+ json_err "$E_JSON_INVALID" "Failed to update headless flag in run-state.json"
809
+ }
810
+
811
+ atomic_write "$ah_state_file" "$ah_updated" || {
812
+ json_err "$E_JSON_INVALID" "Failed to write run-state.json"
813
+ }
814
+
815
+ json_ok "$(jq -n --argjson headless "$ah_headless_bool" '{headless: $headless, updated: true}')"
816
+ }
@@ -51,9 +51,9 @@ parse_spawn_tree() {
51
51
  printf "\"spawns\":["
52
52
  for (i = 0; i < n; i++) {
53
53
  if (i > 0) printf ","
54
- nm = names[i]; gsub(/\\/, "\\\\", nm); gsub(/"/, "\\\"", nm); gsub(/\t/, "\\t", nm)
55
- pr = parents[i]; gsub(/\\/, "\\\\", pr); gsub(/"/, "\\\"", pr); gsub(/\t/, "\\t", pr)
56
- tk = tasks[i]; gsub(/\\/, "\\\\", tk); gsub(/"/, "\\\"", tk); gsub(/\t/, "\\t", tk)
54
+ nm = names[i]; gsub(/\\/, "\\\\", nm); gsub(/"/, "\\\"", nm); gsub(/\t/, "\\t", nm); gsub(/\n/, "\\n", nm); gsub(/\r/, "\\r", nm)
55
+ pr = parents[i]; gsub(/\\/, "\\\\", pr); gsub(/"/, "\\\"", pr); gsub(/\t/, "\\t", pr); gsub(/\n/, "\\n", pr); gsub(/\r/, "\\r", pr)
56
+ tk = tasks[i]; gsub(/\\/, "\\\\", tk); gsub(/"/, "\\\"", tk); gsub(/\t/, "\\t", tk); gsub(/\n/, "\\n", tk); gsub(/\r/, "\\r", tk)
57
57
  printf "{\"name\":\"%s\",\"parent\":\"%s\",\"caste\":\"%s\",", nm, pr, castes[i]
58
58
  printf "\"task\":\"%s\",\"status\":\"%s\",", tk, statuses[i]
59
59
  printf "\"spawned_at\":\"%s\",\"completed_at\":\"%s\",", timestamps[i], completed_at[i]
@@ -63,7 +63,7 @@ parse_spawn_tree() {
63
63
  for (j = 1; j <= length(cidxs); j++) {
64
64
  if (j > 1) printf ","
65
65
  cn = names[cidxs[j]+0]
66
- gsub(/\\/, "\\\\", cn); gsub(/"/, "\\\"", cn); gsub(/\t/, "\\t", cn)
66
+ gsub(/\\/, "\\\\", cn); gsub(/"/, "\\\"", cn); gsub(/\t/, "\\t", cn); gsub(/\n/, "\\n", cn); gsub(/\r/, "\\r", cn)
67
67
  printf "\"%s\"", cn
68
68
  }
69
69
  }
@@ -149,9 +149,9 @@ get_active_spawns() {
149
149
  if (!(spawn_names[i] in done_set)) {
150
150
  if (!first) printf ","
151
151
  first = 0
152
- nm = spawn_names[i]; gsub(/\\/, "\\\\", nm); gsub(/"/, "\\\"", nm); gsub(/\t/, "\\t", nm)
153
- pr = spawn_parents[i]; gsub(/\\/, "\\\\", pr); gsub(/"/, "\\\"", pr); gsub(/\t/, "\\t", pr)
154
- tk = spawn_tasks[i]; gsub(/\\/, "\\\\", tk); gsub(/"/, "\\\"", tk); gsub(/\t/, "\\t", tk)
152
+ nm = spawn_names[i]; gsub(/\\/, "\\\\", nm); gsub(/"/, "\\\"", nm); gsub(/\t/, "\\t", nm); gsub(/\n/, "\\n", nm); gsub(/\r/, "\\r", nm)
153
+ pr = spawn_parents[i]; gsub(/\\/, "\\\\", pr); gsub(/"/, "\\\"", pr); gsub(/\t/, "\\t", pr); gsub(/\n/, "\\n", pr); gsub(/\r/, "\\r", pr)
154
+ tk = spawn_tasks[i]; gsub(/\\/, "\\\\", tk); gsub(/"/, "\\\"", tk); gsub(/\t/, "\\t", tk); gsub(/\n/, "\\n", tk); gsub(/\r/, "\\r", tk)
155
155
  printf "{\"name\":\"%s\",\"caste\":\"%s\",\"parent\":\"%s\",\"task\":\"%s\",\"spawned_at\":\"%s\"}", nm, spawn_castes[i], pr, tk, spawn_ts[i]
156
156
  }
157
157
  }
@@ -25,7 +25,7 @@ _spawn_log() {
25
25
  emoji=$(get_caste_emoji "$child_caste")
26
26
  parent_emoji=$(get_caste_emoji "$parent_id")
27
27
  # Log to activity log with spawn format, emojis, and model info
28
- echo "[$ts] ⚡ SPAWN $parent_emoji $parent_id -> $emoji $child_name ($child_caste): $task_summary [model: $model]" >> "$COLONY_DATA_DIR/activity.log"
28
+ [[ "${AETHER_TESTING:-}" != "1" ]] && echo "[$ts] ⚡ SPAWN $parent_emoji $parent_id -> $emoji $child_name ($child_caste): $task_summary [model: $model]" >> "$COLONY_DATA_DIR/activity.log"
29
29
  # Log to spawn tree file for visualization (NEW FORMAT: includes model field)
30
30
  echo "$ts_full|$parent_id|$child_caste|$child_name|$task_summary|$model|$status" >> "$COLONY_DATA_DIR/spawn-tree.txt"
31
31
  # Return emoji-formatted result for display (jq-safe: child_name may contain JSON-special chars)
@@ -46,7 +46,7 @@ _spawn_complete() {
46
46
  status_icon="✅"
47
47
  [[ "$status" == "failed" ]] && status_icon="❌"
48
48
  [[ "$status" == "blocked" ]] && status_icon="🚫"
49
- echo "[$ts] $status_icon $emoji $ant_name: $status${summary:+ - $summary}" >> "$COLONY_DATA_DIR/activity.log"
49
+ [[ "${AETHER_TESTING:-}" != "1" ]] && echo "[$ts] $status_icon $emoji $ant_name: $status${summary:+ - $summary}" >> "$COLONY_DATA_DIR/activity.log"
50
50
  # Update spawn tree
51
51
  echo "$ts_full|$ant_name|$status|$summary" >> "$COLONY_DATA_DIR/spawn-tree.txt"
52
52
  # Log failed spawns to events array as pipe-delimited strings (matching template format)
@@ -1,6 +1,7 @@
1
1
  #!/bin/bash
2
2
  # State API facade -- centralized COLONY_STATE.json access
3
- # Provides: _state_read, _state_write, _state_read_field, _state_mutate, _state_migrate
3
+ # Provides: _state_read, _state_write, _state_read_field, _state_mutate, _state_migrate,
4
+ # _colony_vital_signs
4
5
  #
5
6
  # These functions are sourced by aether-utils.sh at startup.
6
7
  # All shared infrastructure (json_ok, json_err, atomic_write, acquire_lock,
@@ -197,3 +198,192 @@ _state_migrate() {
197
198
  [[ "$sm_lock_held" == "true" ]] && release_lock 2>/dev/null || true # SUPPRESS:OK -- cleanup: lock may not be held
198
199
  fi
199
200
  }
201
+
202
+ # ============================================================================
203
+ # _colony_vital_signs
204
+ # Compute colony health metrics from existing data files
205
+ # Usage: colony-vital-signs
206
+ # Returns: JSON with build_velocity, error_rate, signal_health, memory_pressure,
207
+ # colony_age_hours, and overall_health (0-100)
208
+ # Gracefully degrades: missing files produce zero/default values
209
+ # ============================================================================
210
+ _colony_vital_signs() {
211
+ local cvs_state_file="$COLONY_DATA_DIR/COLONY_STATE.json"
212
+ local cvs_midden_file="$COLONY_DATA_DIR/midden/midden.json"
213
+ local cvs_phero_file="$COLONY_DATA_DIR/pheromones.json"
214
+ local cvs_session_file="$COLONY_DATA_DIR/session.json"
215
+
216
+ # --- Compute 24h window boundary ---
217
+ local cvs_now
218
+ cvs_now=$(date -u +%s 2>/dev/null || echo "0")
219
+ local cvs_window_start=$(( cvs_now - 86400 ))
220
+
221
+ # ---- build_velocity: count phase_completed events in last 24h ----
222
+ local cvs_phases_per_day=0
223
+ if [[ -f "$cvs_state_file" ]]; then
224
+ cvs_phases_per_day=$(jq --argjson win "$cvs_window_start" '
225
+ [.events[]? |
226
+ select(. != null) |
227
+ select(test("\\|phase_completed\\|")) |
228
+ capture("^(?P<ts>[^|]+)\\|") |
229
+ .ts |
230
+ gsub("[TZ:-]"; " ") |
231
+ split(" ") |
232
+ if length >= 6 then
233
+ (.[0:6] | join(" ")) |
234
+ # convert to comparable string for ordering -- full ISO compare
235
+ . as $s | $s
236
+ else . end
237
+ ] | length
238
+ ' "$cvs_state_file" 2>/dev/null || echo "0")
239
+
240
+ # Simpler approach: use string comparison on ISO timestamps
241
+ # Compute the 24h-ago timestamp as ISO string
242
+ local cvs_cutoff_iso
243
+ cvs_cutoff_iso=$(date -u -r "$cvs_window_start" '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null \
244
+ || date -u -d "@$cvs_window_start" '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null \
245
+ || echo "")
246
+
247
+ if [[ -n "$cvs_cutoff_iso" ]]; then
248
+ cvs_phases_per_day=$(jq --arg cutoff "$cvs_cutoff_iso" '
249
+ [.events[]? |
250
+ select(. != null and (type == "string")) |
251
+ select(test("\\|phase_completed\\|")) |
252
+ split("|") | .[0] |
253
+ select(. >= $cutoff)
254
+ ] | length
255
+ ' "$cvs_state_file" 2>/dev/null || echo "0")
256
+ fi
257
+ fi
258
+ # Normalize: ensure integer
259
+ cvs_phases_per_day=$(( cvs_phases_per_day + 0 )) 2>/dev/null || cvs_phases_per_day=0
260
+
261
+ # Determine trend (simple heuristic: any builds = steady, 0 = idle)
262
+ local cvs_bv_trend="idle"
263
+ [[ "$cvs_phases_per_day" -ge 1 ]] && cvs_bv_trend="steady"
264
+ [[ "$cvs_phases_per_day" -ge 3 ]] && cvs_bv_trend="accelerating"
265
+
266
+ # ---- error_rate: unreviewed midden entries in last 24h ----
267
+ local cvs_errors_per_day=0
268
+ local cvs_err_status="clean"
269
+ if [[ -f "$cvs_midden_file" ]]; then
270
+ local cvs_cutoff_iso_err
271
+ cvs_cutoff_iso_err=$(date -u -r "$cvs_window_start" '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null \
272
+ || date -u -d "@$cvs_window_start" '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null \
273
+ || echo "")
274
+
275
+ if [[ -n "$cvs_cutoff_iso_err" ]]; then
276
+ cvs_errors_per_day=$(jq --arg cutoff "$cvs_cutoff_iso_err" '
277
+ [(.entries // [])[]? |
278
+ select(.reviewed == false or .reviewed == null) |
279
+ select((.timestamp // "") >= $cutoff)
280
+ ] | length
281
+ ' "$cvs_midden_file" 2>/dev/null || echo "0")
282
+ else
283
+ # Fallback: count all unreviewed
284
+ cvs_errors_per_day=$(jq '
285
+ [(.entries // [])[]? | select(.reviewed == false or .reviewed == null)] | length
286
+ ' "$cvs_midden_file" 2>/dev/null || echo "0")
287
+ fi
288
+ fi
289
+ cvs_errors_per_day=$(( cvs_errors_per_day + 0 )) 2>/dev/null || cvs_errors_per_day=0
290
+
291
+ if [[ "$cvs_errors_per_day" -eq 0 ]]; then
292
+ cvs_err_status="clean"
293
+ elif [[ "$cvs_errors_per_day" -le 2 ]]; then
294
+ cvs_err_status="nominal"
295
+ elif [[ "$cvs_errors_per_day" -le 5 ]]; then
296
+ cvs_err_status="elevated"
297
+ else
298
+ cvs_err_status="critical"
299
+ fi
300
+
301
+ # ---- signal_health: count active pheromones ----
302
+ local cvs_active_count=0
303
+ local cvs_sig_status="dormant"
304
+ if [[ -f "$cvs_phero_file" ]]; then
305
+ cvs_active_count=$(jq '
306
+ [.signals[]? | select(.active == true)] | length
307
+ ' "$cvs_phero_file" 2>/dev/null || echo "0")
308
+ fi
309
+ cvs_active_count=$(( cvs_active_count + 0 )) 2>/dev/null || cvs_active_count=0
310
+
311
+ if [[ "$cvs_active_count" -eq 0 ]]; then
312
+ cvs_sig_status="dormant"
313
+ elif [[ "$cvs_active_count" -le 3 ]]; then
314
+ cvs_sig_status="guided"
315
+ else
316
+ cvs_sig_status="active"
317
+ fi
318
+
319
+ # ---- memory_pressure: count instincts ----
320
+ local cvs_instinct_count=0
321
+ local cvs_mem_status="empty"
322
+ if [[ -f "$cvs_state_file" ]]; then
323
+ # instincts may be a JSON string (serialized array) or a real array
324
+ local cvs_raw_instincts
325
+ cvs_raw_instincts=$(jq -r '.memory.instincts // "[]"' "$cvs_state_file" 2>/dev/null || echo "[]")
326
+ # Handle both string-encoded and native array
327
+ cvs_instinct_count=$(echo "$cvs_raw_instincts" | jq -r 'if type == "string" then (. | fromjson | length) elif type == "array" then length else 0 end' 2>/dev/null || echo "0")
328
+ fi
329
+ cvs_instinct_count=$(( cvs_instinct_count + 0 )) 2>/dev/null || cvs_instinct_count=0
330
+
331
+ if [[ "$cvs_instinct_count" -eq 0 ]]; then
332
+ cvs_mem_status="empty"
333
+ elif [[ "$cvs_instinct_count" -le 5 ]]; then
334
+ cvs_mem_status="growing"
335
+ elif [[ "$cvs_instinct_count" -le 15 ]]; then
336
+ cvs_mem_status="healthy"
337
+ else
338
+ cvs_mem_status="rich"
339
+ fi
340
+
341
+ # ---- colony_age_hours: hours since initialized_at ----
342
+ local cvs_age_hours=0
343
+ if [[ -f "$cvs_state_file" ]]; then
344
+ local cvs_init_at
345
+ cvs_init_at=$(jq -r '.initialized_at // empty' "$cvs_state_file" 2>/dev/null || echo "")
346
+ if [[ -n "$cvs_init_at" ]]; then
347
+ local cvs_init_ts
348
+ cvs_init_ts=$(date -u -j -f '%Y-%m-%dT%H:%M:%SZ' "$cvs_init_at" '+%s' 2>/dev/null \
349
+ || date -u -d "$cvs_init_at" '+%s' 2>/dev/null \
350
+ || echo "0")
351
+ if [[ "$cvs_init_ts" -gt 0 && "$cvs_now" -gt "$cvs_init_ts" ]]; then
352
+ cvs_age_hours=$(( (cvs_now - cvs_init_ts) / 3600 ))
353
+ fi
354
+ fi
355
+ fi
356
+
357
+ # ---- overall_health: weighted 0-100 score ----
358
+ # Components (max points each):
359
+ # recent builds (+30): has at least one phase_completed in 24h
360
+ # low errors (+30): zero unreviewed errors in 24h
361
+ # signals exist (+20): at least one active pheromone
362
+ # instincts growing (+20): at least one instinct
363
+ local cvs_score=0
364
+ [[ "$cvs_phases_per_day" -ge 1 ]] && cvs_score=$(( cvs_score + 30 ))
365
+ [[ "$cvs_errors_per_day" -eq 0 ]] && cvs_score=$(( cvs_score + 30 ))
366
+ [[ "$cvs_active_count" -ge 1 ]] && cvs_score=$(( cvs_score + 20 ))
367
+ [[ "$cvs_instinct_count" -ge 1 ]] && cvs_score=$(( cvs_score + 20 ))
368
+ [[ "$cvs_score" -gt 100 ]] && cvs_score=100
369
+
370
+ json_ok "$(jq -n \
371
+ --argjson phases_per_day "$cvs_phases_per_day" \
372
+ --arg bv_trend "$cvs_bv_trend" \
373
+ --argjson errors_per_day "$cvs_errors_per_day" \
374
+ --arg err_status "$cvs_err_status" \
375
+ --argjson active_count "$cvs_active_count" \
376
+ --arg sig_status "$cvs_sig_status" \
377
+ --argjson instinct_count "$cvs_instinct_count" \
378
+ --arg mem_status "$cvs_mem_status" \
379
+ --argjson age_hours "$cvs_age_hours" \
380
+ --argjson overall_health "$cvs_score" \
381
+ '{
382
+ build_velocity: {phases_per_day: $phases_per_day, trend: $bv_trend},
383
+ error_rate: {errors_per_day: $errors_per_day, status: $err_status},
384
+ signal_health: {active_count: $active_count, status: $sig_status},
385
+ memory_pressure: {instinct_count: $instinct_count, status: $mem_status},
386
+ colony_age_hours: $age_hours,
387
+ overall_health: $overall_health
388
+ }')"
389
+ }
@@ -69,6 +69,8 @@ Read `.aether/data/COLONY_STATE.json`.
69
69
 
70
70
  **If the file exists:** continue.
71
71
 
72
+ **If `milestone` == `"Crowned Anthill"`:** output "This colony has been sealed. Start a new colony with `/ant:init \"new goal\"`.", stop.
73
+
72
74
  **If `plan.phases` is not empty:** output "Colony already has phases. Use /ant:continue.", stop.
73
75
 
74
76
  ### Step 2: Quick Surface Scan (for session context)