aether-colony 3.1.16 → 5.0.0

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 (183) hide show
  1. package/{runtime → .aether}/CONTEXT.md +1 -1
  2. package/{runtime → .aether}/aether-utils.sh +2087 -142
  3. package/.aether/docs/QUEEN-SYSTEM.md +211 -0
  4. package/.aether/docs/QUEEN.md +84 -0
  5. package/.aether/docs/README.md +68 -0
  6. package/.aether/docs/caste-system.md +48 -0
  7. package/{runtime → .aether/docs/disciplines}/DISCIPLINES.md +8 -8
  8. package/.aether/docs/error-codes.md +268 -0
  9. package/{runtime → .aether}/docs/known-issues.md +42 -26
  10. package/.aether/docs/queen-commands.md +97 -0
  11. package/.aether/exchange/colony-registry.xml +11 -0
  12. package/{runtime → .aether}/exchange/pheromone-xml.sh +2 -1
  13. package/.aether/exchange/pheromones.xml +87 -0
  14. package/.aether/exchange/queen-wisdom.xml +14 -0
  15. package/{runtime → .aether}/exchange/registry-xml.sh +7 -3
  16. package/{runtime → .aether}/exchange/wisdom-xml.sh +11 -4
  17. package/.aether/rules/aether-colony.md +134 -0
  18. package/.aether/schemas/example-prompt-builder.xml +234 -0
  19. package/.aether/templates/colony-state-reset.jq.template +22 -0
  20. package/.aether/templates/colony-state.template.json +35 -0
  21. package/.aether/templates/constraints.template.json +9 -0
  22. package/.aether/templates/crowned-anthill.template.md +36 -0
  23. package/.aether/templates/handoff-build-error.template.md +30 -0
  24. package/.aether/templates/handoff-build-success.template.md +39 -0
  25. package/.aether/templates/handoff.template.md +40 -0
  26. package/{runtime → .aether}/utils/atomic-write.sh +5 -5
  27. package/{runtime → .aether}/utils/chamber-compare.sh +23 -10
  28. package/{runtime → .aether}/utils/chamber-utils.sh +32 -20
  29. package/{runtime → .aether}/utils/error-handler.sh +13 -1
  30. package/{runtime → .aether}/utils/file-lock.sh +49 -13
  31. package/.aether/utils/semantic-cli.sh +413 -0
  32. package/{runtime → .aether}/utils/xml-compose.sh +7 -1
  33. package/.aether/utils/xml-convert.sh +273 -0
  34. package/.aether/utils/xml-query.sh +201 -0
  35. package/.aether/utils/xml-utils.sh +110 -0
  36. package/{runtime → .aether}/workers.md +14 -17
  37. package/.claude/agents/ant/aether-ambassador.md +264 -0
  38. package/.claude/agents/ant/aether-archaeologist.md +322 -0
  39. package/.claude/agents/ant/aether-auditor.md +266 -0
  40. package/.claude/agents/ant/aether-builder.md +187 -0
  41. package/.claude/agents/ant/aether-chaos.md +268 -0
  42. package/.claude/agents/ant/aether-chronicler.md +304 -0
  43. package/.claude/agents/ant/aether-gatekeeper.md +325 -0
  44. package/.claude/agents/ant/aether-includer.md +373 -0
  45. package/.claude/agents/ant/aether-keeper.md +271 -0
  46. package/.claude/agents/ant/aether-measurer.md +317 -0
  47. package/.claude/agents/ant/aether-probe.md +210 -0
  48. package/.claude/agents/ant/aether-queen.md +325 -0
  49. package/.claude/agents/ant/aether-route-setter.md +173 -0
  50. package/.claude/agents/ant/aether-sage.md +353 -0
  51. package/.claude/agents/ant/aether-scout.md +142 -0
  52. package/.claude/agents/ant/aether-surveyor-disciplines.md +416 -0
  53. package/.claude/agents/ant/aether-surveyor-nest.md +354 -0
  54. package/.claude/agents/ant/aether-surveyor-pathogens.md +288 -0
  55. package/.claude/agents/ant/aether-surveyor-provisions.md +359 -0
  56. package/.claude/agents/ant/aether-tracker.md +265 -0
  57. package/.claude/agents/ant/aether-watcher.md +244 -0
  58. package/.claude/agents/ant/aether-weaver.md +247 -0
  59. package/.claude/commands/ant/archaeology.md +17 -8
  60. package/.claude/commands/ant/build.md +415 -284
  61. package/.claude/commands/ant/chaos.md +20 -11
  62. package/.claude/commands/ant/colonize.md +58 -24
  63. package/.claude/commands/ant/continue.md +155 -145
  64. package/.claude/commands/ant/council.md +15 -5
  65. package/.claude/commands/ant/dream.md +16 -7
  66. package/.claude/commands/ant/entomb.md +274 -157
  67. package/.claude/commands/ant/feedback.md +33 -29
  68. package/.claude/commands/ant/flag.md +18 -10
  69. package/.claude/commands/ant/flags.md +14 -6
  70. package/.claude/commands/ant/focus.md +29 -21
  71. package/.claude/commands/ant/help.md +11 -1
  72. package/.claude/commands/ant/history.md +10 -0
  73. package/.claude/commands/ant/init.md +91 -65
  74. package/.claude/commands/ant/interpret.md +15 -4
  75. package/.claude/commands/ant/lay-eggs.md +55 -7
  76. package/.claude/commands/ant/maturity.md +11 -1
  77. package/.claude/commands/ant/migrate-state.md +14 -2
  78. package/.claude/commands/ant/oracle.md +23 -15
  79. package/.claude/commands/ant/organize.md +29 -20
  80. package/.claude/commands/ant/pause-colony.md +17 -7
  81. package/.claude/commands/ant/phase.md +17 -8
  82. package/.claude/commands/ant/plan.md +20 -9
  83. package/.claude/commands/ant/redirect.md +29 -32
  84. package/.claude/commands/ant/resume-colony.md +19 -9
  85. package/.claude/commands/ant/resume.md +272 -96
  86. package/.claude/commands/ant/seal.md +201 -191
  87. package/.claude/commands/ant/status.md +71 -32
  88. package/.claude/commands/ant/swarm.md +26 -44
  89. package/.claude/commands/ant/tunnels.md +279 -105
  90. package/.claude/commands/ant/update.md +81 -20
  91. package/.claude/commands/ant/verify-castes.md +14 -4
  92. package/.claude/commands/ant/watch.md +13 -12
  93. package/.opencode/agents/aether-ambassador.md +63 -20
  94. package/.opencode/agents/aether-archaeologist.md +29 -12
  95. package/.opencode/agents/aether-auditor.md +51 -18
  96. package/.opencode/agents/aether-builder.md +69 -19
  97. package/.opencode/agents/aether-chaos.md +29 -12
  98. package/.opencode/agents/aether-chronicler.md +60 -18
  99. package/.opencode/agents/aether-gatekeeper.md +27 -18
  100. package/.opencode/agents/aether-includer.md +27 -18
  101. package/.opencode/agents/aether-keeper.md +89 -18
  102. package/.opencode/agents/aether-measurer.md +27 -18
  103. package/.opencode/agents/aether-probe.md +60 -18
  104. package/.opencode/agents/aether-queen.md +172 -24
  105. package/.opencode/agents/aether-route-setter.md +57 -12
  106. package/.opencode/agents/aether-sage.md +26 -18
  107. package/.opencode/agents/aether-scout.md +27 -19
  108. package/.opencode/agents/aether-surveyor-disciplines.md +53 -1
  109. package/.opencode/agents/aether-surveyor-nest.md +53 -1
  110. package/.opencode/agents/aether-surveyor-pathogens.md +51 -1
  111. package/.opencode/agents/aether-surveyor-provisions.md +53 -1
  112. package/.opencode/agents/aether-tracker.md +64 -18
  113. package/.opencode/agents/aether-watcher.md +66 -19
  114. package/.opencode/agents/aether-weaver.md +61 -18
  115. package/.opencode/commands/ant/build.md +406 -192
  116. package/.opencode/commands/ant/continue.md +66 -76
  117. package/.opencode/commands/ant/entomb.md +106 -45
  118. package/.opencode/commands/ant/init.md +46 -48
  119. package/.opencode/commands/ant/organize.md +5 -5
  120. package/.opencode/commands/ant/resume.md +334 -0
  121. package/.opencode/commands/ant/seal.md +33 -24
  122. package/.opencode/commands/ant/status.md +11 -0
  123. package/.opencode/commands/ant/tunnels.md +149 -0
  124. package/.opencode/commands/ant/update.md +59 -16
  125. package/CHANGELOG.md +79 -0
  126. package/README.md +135 -353
  127. package/bin/cli.js +243 -122
  128. package/bin/generate-commands.sh +2 -2
  129. package/bin/lib/init.js +13 -3
  130. package/bin/lib/update-transaction.js +119 -117
  131. package/bin/sync-to-runtime.sh +5 -137
  132. package/bin/validate-package.sh +84 -0
  133. package/package.json +9 -6
  134. package/.opencode/agents/aether-architect.md +0 -66
  135. package/.opencode/agents/aether-guardian.md +0 -107
  136. package/.opencode/agents/workers.md +0 -1034
  137. package/runtime/QUEEN_ANT_ARCHITECTURE.md +0 -402
  138. package/runtime/data/signatures.json +0 -41
  139. package/runtime/docs/AETHER-2.0-IMPLEMENTATION-PLAN.md +0 -1343
  140. package/runtime/docs/AETHER-PHEROMONE-SYSTEM-MASTER-SPEC.md +0 -2642
  141. package/runtime/docs/PHEROMONE-INJECTION.md +0 -240
  142. package/runtime/docs/PHEROMONE-INTEGRATION.md +0 -192
  143. package/runtime/docs/PHEROMONE-SYSTEM-DESIGN.md +0 -426
  144. package/runtime/docs/README.md +0 -94
  145. package/runtime/docs/VISUAL-OUTPUT-SPEC.md +0 -219
  146. package/runtime/docs/biological-reference.md +0 -272
  147. package/runtime/docs/codebase-review.md +0 -399
  148. package/runtime/docs/command-sync.md +0 -164
  149. package/runtime/docs/constraints.md +0 -116
  150. package/runtime/docs/implementation-learnings.md +0 -89
  151. package/runtime/docs/namespace.md +0 -148
  152. package/runtime/docs/pathogen-schema-example.json +0 -36
  153. package/runtime/docs/pathogen-schema.md +0 -111
  154. package/runtime/docs/planning-discipline.md +0 -159
  155. package/runtime/docs/progressive-disclosure.md +0 -184
  156. package/runtime/lib/queen-utils.sh +0 -729
  157. package/runtime/planning.md +0 -159
  158. package/runtime/recover.sh +0 -136
  159. package/runtime/utils/xml-utils.sh +0 -2196
  160. package/runtime/workers-new-castes.md +0 -516
  161. /package/{runtime → .aether/docs/disciplines}/coding-standards.md +0 -0
  162. /package/{runtime → .aether/docs/disciplines}/debugging.md +0 -0
  163. /package/{runtime → .aether/docs/disciplines}/learning.md +0 -0
  164. /package/{runtime → .aether/docs/disciplines}/tdd.md +0 -0
  165. /package/{runtime → .aether/docs/disciplines}/verification-loop.md +0 -0
  166. /package/{runtime → .aether/docs/disciplines}/verification.md +0 -0
  167. /package/{runtime → .aether}/docs/pheromones.md +0 -0
  168. /package/{runtime → .aether}/model-profiles.yaml +0 -0
  169. /package/{runtime → .aether}/schemas/aether-types.xsd +0 -0
  170. /package/{runtime → .aether}/schemas/colony-registry.xsd +0 -0
  171. /package/{runtime → .aether}/schemas/pheromone.xsd +0 -0
  172. /package/{runtime → .aether}/schemas/prompt.xsd +0 -0
  173. /package/{runtime → .aether}/schemas/queen-wisdom.xsd +0 -0
  174. /package/{runtime → .aether}/schemas/worker-priming.xsd +0 -0
  175. /package/{runtime → .aether}/templates/QUEEN.md.template +0 -0
  176. /package/{runtime → .aether}/utils/colorize-log.sh +0 -0
  177. /package/{runtime → .aether}/utils/queen-to-md.xsl +0 -0
  178. /package/{runtime → .aether}/utils/spawn-tree.sh +0 -0
  179. /package/{runtime → .aether}/utils/spawn-with-model.sh +0 -0
  180. /package/{runtime → .aether}/utils/state-loader.sh +0 -0
  181. /package/{runtime → .aether}/utils/swarm-display.sh +0 -0
  182. /package/{runtime → .aether}/utils/watch-spawn-tree.sh +0 -0
  183. /package/{runtime → .aether}/utils/xml-core.sh +0 -0
@@ -16,7 +16,7 @@ trap 'if type error_handler &>/dev/null; then error_handler ${LINENO} "$BASH_COM
16
16
 
17
17
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
18
18
  AETHER_ROOT="$(cd "$SCRIPT_DIR/.." && pwd 2>/dev/null || echo "$SCRIPT_DIR")"
19
- DATA_DIR="$AETHER_ROOT/.aether/data"
19
+ DATA_DIR="${DATA_DIR:-$AETHER_ROOT/.aether/data}"
20
20
 
21
21
  # Initialize lock state before sourcing (file-lock.sh trap needs these)
22
22
  LOCK_ACQUIRED=${LOCK_ACQUIRED:-false}
@@ -28,6 +28,7 @@ CURRENT_LOCK=${CURRENT_LOCK:-""}
28
28
  [[ -f "$SCRIPT_DIR/utils/error-handler.sh" ]] && source "$SCRIPT_DIR/utils/error-handler.sh"
29
29
  [[ -f "$SCRIPT_DIR/utils/chamber-utils.sh" ]] && source "$SCRIPT_DIR/utils/chamber-utils.sh"
30
30
  [[ -f "$SCRIPT_DIR/utils/xml-utils.sh" ]] && source "$SCRIPT_DIR/utils/xml-utils.sh"
31
+ [[ -f "$SCRIPT_DIR/utils/semantic-cli.sh" ]] && source "$SCRIPT_DIR/utils/semantic-cli.sh"
31
32
 
32
33
  # Fallback error constants if error-handler.sh wasn't sourced
33
34
  # This prevents "unbound variable" errors in older installations
@@ -37,26 +38,13 @@ CURRENT_LOCK=${CURRENT_LOCK:-""}
37
38
  : "${E_FILE_NOT_FOUND:=E_FILE_NOT_FOUND}"
38
39
  : "${E_JSON_INVALID:=E_JSON_INVALID}"
39
40
  : "${E_LOCK_FAILED:=E_LOCK_FAILED}"
41
+ : "${E_LOCK_STALE:=E_LOCK_STALE}"
40
42
  : "${E_GIT_ERROR:=E_GIT_ERROR}"
41
43
  : "${E_VALIDATION_FAILED:=E_VALIDATION_FAILED}"
42
44
  : "${E_FEATURE_UNAVAILABLE:=E_FEATURE_UNAVAILABLE}"
43
45
  : "${E_BASH_ERROR:=E_BASH_ERROR}"
44
-
45
- # Feature detection for graceful degradation
46
- # These checks run silently - failures are logged but don't block operation
47
- if type feature_disable &>/dev/null; then
48
- # Check if DATA_DIR is writable for activity logging
49
- [[ -w "$DATA_DIR" ]] 2>/dev/null || feature_disable "activity_log" "DATA_DIR not writable"
50
-
51
- # Check if git is available for git integration
52
- command -v git &>/dev/null || feature_disable "git_integration" "git not installed"
53
-
54
- # Check if jq is available for JSON processing
55
- command -v jq &>/dev/null || feature_disable "json_processing" "jq not installed"
56
-
57
- # Check if lock utilities are available
58
- [[ -f "$SCRIPT_DIR/utils/file-lock.sh" ]] || feature_disable "file_locking" "lock utilities not available"
59
- fi
46
+ : "${E_DEPENDENCY_MISSING:=E_DEPENDENCY_MISSING}"
47
+ : "${E_RESOURCE_NOT_FOUND:=E_RESOURCE_NOT_FOUND}"
60
48
 
61
49
  # Fallback atomic_write if not sourced (uses temp file + mv for true atomicity)
62
50
  if ! type atomic_write &>/dev/null; then
@@ -77,14 +65,62 @@ json_ok() { printf '{"ok":true,"result":%s}\n' "$1"; }
77
65
  # Error: JSON to stderr, exit 1
78
66
  # Use enhanced json_err from error-handler.sh if available, otherwise fallback
79
67
  if ! type json_err &>/dev/null; then
80
- # Fallback: simple error format for backward compatibility
68
+ # Fallback: error-handler.sh failed to load. Emits minimal but parseable JSON.
69
+ # Diagnostic note tells the user their installation may be incomplete.
81
70
  json_err() {
82
- local message="${2:-$1}"
83
- printf '{"ok":false,"error":"%s"}\n' "$message" >&2
71
+ local code="${1:-E_UNKNOWN}"
72
+ local message="${2:-An unknown error occurred}"
73
+ printf '[aether] Warning: error-handler.sh not loaded — using minimal fallback\n' >&2
74
+ printf '{"ok":false,"error":{"code":"%s","message":"%s"}}\n' "$code" "$message" >&2
84
75
  exit 1
85
76
  }
86
77
  fi
87
78
 
79
+ # Feature detection for graceful degradation
80
+ # ARCH-09: runs AFTER all fallback definitions (atomic_write, json_ok, json_err)
81
+ # so feature_disable is never called before those functions exist.
82
+ # These checks run silently - failures are logged but don't block operation
83
+ if type feature_disable &>/dev/null; then
84
+ # Check if DATA_DIR is writable for activity logging
85
+ [[ -w "$DATA_DIR" ]] 2>/dev/null || feature_disable "activity_log" "DATA_DIR not writable"
86
+
87
+ # Check if git is available for git integration
88
+ command -v git &>/dev/null || feature_disable "git_integration" "git not installed"
89
+
90
+ # Check if jq is available for JSON processing
91
+ command -v jq &>/dev/null || feature_disable "json_processing" "jq not installed"
92
+
93
+ # Check if lock utilities are available
94
+ [[ -f "$SCRIPT_DIR/utils/file-lock.sh" ]] || feature_disable "file_locking" "lock utilities not available"
95
+ fi
96
+
97
+ # Composed exit cleanup — replaces individual traps from file-lock.sh and atomic-write.sh
98
+ # ARCH-10: bash traps are single-valued per signal — last trap set wins.
99
+ # This function ensures both lock and temp cleanup run on every exit path.
100
+ # Must be set AFTER file-lock.sh is sourced so it overrides the individual
101
+ # 'trap cleanup_locks EXIT TERM INT HUP' set by file-lock.sh.
102
+ _aether_exit_cleanup() {
103
+ cleanup_locks 2>/dev/null || true
104
+ cleanup_temp_files 2>/dev/null || true
105
+ }
106
+ trap '_aether_exit_cleanup' EXIT TERM INT HUP
107
+
108
+ # Startup cleanup — remove temp files from dead sessions (PID-based orphan detection)
109
+ # ARCH-10: runs once at startup, silent (matches lock cleanup behavior)
110
+ _cleanup_orphaned_temp_files() {
111
+ local temp_dir="${TEMP_DIR:-$AETHER_ROOT/.aether/temp}"
112
+ [[ -d "$temp_dir" ]] || return 0
113
+ while IFS= read -r -d '' tmp_file; do
114
+ local file_pid
115
+ file_pid=$(basename "$tmp_file" | awk -F'.' '{print $(NF-2)}')
116
+ if [[ "$file_pid" =~ ^[0-9]+$ ]] && ! kill -0 "$file_pid" 2>/dev/null; then
117
+ rm -f "$tmp_file" 2>/dev/null || true
118
+ fi
119
+ done < <(find "$temp_dir" -maxdepth 1 -name "*.tmp" -print0 2>/dev/null)
120
+ }
121
+ # Run orphan cleanup on startup (silent — matches cleanup_locks behavior)
122
+ type cleanup_temp_files &>/dev/null && _cleanup_orphaned_temp_files
123
+
88
124
  # --- Caste emoji helper ---
89
125
  get_caste_emoji() {
90
126
  case "$1" in
@@ -110,10 +146,88 @@ get_caste_emoji() {
110
146
  *Probe*|*probe*|*Test*|*Excavat*|*Uncover*|*Edge*|*Case*|*Mutant*) echo "🧪🐜" ;;
111
147
  *Tracker*|*tracker*|*Debug*|*Trace*|*Follow*|*Bug*|*Hunt*|*Root*) echo "🐛🐜" ;;
112
148
  *Weaver*|*weaver*|*Refactor*|*Restruct*|*Transform*|*Clean*|*Pattern*|*Weave*) echo "🔄🐜" ;;
149
+ *Dreamer*|*dreamer*|*Dream*|*Muse*|*Imagine*|*Wonder*|*Ponder*|*Reverie*) echo "💭🐜" ;;
113
150
  *) echo "🐜" ;;
114
151
  esac
115
152
  }
116
153
 
154
+ # --- Progress bar helper ---
155
+ # Usage: generate-progress-bar <current> <total> [width]
156
+ # Returns: "[████████░░░░░░░░] 8/20" format string
157
+ generate-progress-bar() {
158
+ local current="${1:-0}"
159
+ local total="${2:-1}"
160
+ local width="${3:-20}"
161
+
162
+ # Prevent division by zero
163
+ [[ "$total" -lt 1 ]] && total=1
164
+ [[ "$current" -lt 0 ]] && current=0
165
+ [[ "$current" -gt "$total" ]] && current="$total"
166
+
167
+ # Calculate filled/empty segments
168
+ local filled=$(( (current * width) / total ))
169
+ local empty=$(( width - filled ))
170
+
171
+ # Build bar with Unicode block characters
172
+ local bar=""
173
+ for ((i=0; i<filled; i++)); do bar+="█"; done
174
+ for ((i=0; i<empty; i++)); do bar+="░"; done
175
+
176
+ echo "[$bar] $current/$total"
177
+ }
178
+
179
+ # --- Standard banner helper ---
180
+ # Usage: print-standard-banner <title>
181
+ # Outputs a standardized banner with heavy horizontal lines (U+2501)
182
+ print-standard-banner() {
183
+ local title="$1"
184
+
185
+ # Convert title to spaced uppercase
186
+ local spaced_title
187
+ spaced_title=$(echo "$title" | tr '[:lower:]' '[:upper:]' | sed 's/./& /g' | sed 's/ $//')
188
+
189
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
190
+ echo " $spaced_title"
191
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
192
+ }
193
+
194
+ # --- Next Up block helper ---
195
+ # Usage: print-next-up <state> [current_phase] [total_phases]
196
+ # Outputs a Next Up block with state-based suggestions
197
+ print-next-up() {
198
+ local state="${1:-IDLE}"
199
+ local current_phase="${2:-0}"
200
+ local total_phases="${3:-0}"
201
+ local next_phase=$((current_phase + 1))
202
+
203
+ echo "──────────────────────────────────────────────────"
204
+ echo "🐜 Next Up"
205
+ echo "──────────────────────────────────────────────────"
206
+
207
+ case "$state" in
208
+ IDLE)
209
+ echo " /ant:init 🌱 Start a new colony"
210
+ echo " /ant:status 📊 Check current state"
211
+ ;;
212
+ READY)
213
+ echo " /ant:build $next_phase 🔨 Build phase $next_phase"
214
+ echo " /ant:phase $next_phase 📋 Review phase details"
215
+ echo " /ant:focus 🎯 Guide colony attention"
216
+ ;;
217
+ EXECUTING)
218
+ echo " /ant:continue ➡️ Continue current build"
219
+ echo " /ant:status 📊 Check build progress"
220
+ ;;
221
+ PLANNING)
222
+ echo " /ant:plan 📝 Create execution plan"
223
+ echo " /ant:status 📊 Check current state"
224
+ ;;
225
+ *)
226
+ echo " /ant:status 📊 Check colony state"
227
+ ;;
228
+ esac
229
+ }
230
+
117
231
  # ============================================
118
232
  # CONTEXT UPDATE HELPER FUNCTION
119
233
  # (Defined outside case block to fix SC2168: local outside function)
@@ -125,6 +239,19 @@ _cmd_context_update() {
125
239
  local ctx_ts
126
240
  ctx_ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
127
241
 
242
+ # Check for empty action first - show usage message
243
+ if [[ -z "$ctx_action" ]]; then
244
+ json_err "$E_VALIDATION_FAILED" "No action specified. Suggestion: Use one of: init, update-phase, activity, constraint, decision, safe-to-clear, build-start, worker-spawn, worker-complete, build-progress, build-complete"
245
+ fi
246
+
247
+ # Acquire lock for context-update operations (LOCK-04: prevent concurrent corruption)
248
+ local _ctx_lock_held=false
249
+ if type acquire_lock &>/dev/null && type feature_enabled &>/dev/null && feature_enabled "file_locking"; then
250
+ acquire_lock "$ctx_file" || json_err "$E_LOCK_FAILED" "Failed to acquire CONTEXT.md lock for context-update"
251
+ _ctx_lock_held=true
252
+ trap 'release_lock 2>/dev/null || true' EXIT
253
+ fi
254
+
128
255
  ensure_context_dir() {
129
256
  local dir
130
257
  dir=$(dirname "$ctx_file")
@@ -186,7 +313,7 @@ Colony initialization in progress...
186
313
 
187
314
  | Constraint | Source | Date Set |
188
315
  |------------|--------|----------|
189
- | In the Aether repo, \`.aether/\` IS the source of truth — \`runtime/\` is auto-populated on publish | CLAUDE.md | Permanent |
316
+ | In the Aether repo, \`.aether/\` IS the source of truth — published directly via npm (private dirs excluded by .npmignore) | CLAUDE.md | Permanent |
190
317
  | Never push without explicit user approval | CLAUDE.md Safety | Permanent |
191
318
 
192
319
  ---
@@ -268,7 +395,7 @@ EOF
268
395
  local safe_clear="${4:-NO}"
269
396
  local safe_reason="${5:-Phase in progress}"
270
397
 
271
- [[ -f "$ctx_file" ]] || { json_err "CONTEXT.md not found. Run context-update init first."; }
398
+ [[ -f "$ctx_file" ]] || { json_err "$E_FILE_NOT_FOUND" "Couldn't find CONTEXT.md. Try: run context-update init first."; }
272
399
 
273
400
  sed -i.bak "s/| \*\*Last Updated\*\* | .*/| **Last Updated** | $ctx_ts |/" "$ctx_file" && rm -f "$ctx_file.bak"
274
401
  sed -i.bak "s/| \*\*Current Phase\*\* | .*/| **Current Phase** | $new_phase |/" "$ctx_file" && rm -f "$ctx_file.bak"
@@ -283,7 +410,7 @@ EOF
283
410
  local result="${3:-}"
284
411
  local files_changed="${4:-—}"
285
412
 
286
- [[ -f "$ctx_file" ]] || { json_err "CONTEXT.md not found"; }
413
+ [[ -f "$ctx_file" ]] || { json_err "$E_FILE_NOT_FOUND" "Couldn't find CONTEXT.md. Try: run context-update init first."; }
287
414
 
288
415
  sed -i.bak "s/| \*\*Last Updated\*\* | .*/| **Last Updated** | $ctx_ts |/" "$ctx_file" && rm -f "$ctx_file.bak"
289
416
 
@@ -309,7 +436,7 @@ EOF
309
436
  local safe="${2:-NO}"
310
437
  local reason="${3:-Unknown state}"
311
438
 
312
- [[ -f "$ctx_file" ]] || { json_err "CONTEXT.md not found"; }
439
+ [[ -f "$ctx_file" ]] || { json_err "$E_FILE_NOT_FOUND" "Couldn't find CONTEXT.md. Try: run context-update init first."; }
313
440
 
314
441
  sed -i.bak "s/| \*\*Last Updated\*\* | .*/| **Last Updated** | $ctx_ts |/" "$ctx_file" && rm -f "$ctx_file.bak"
315
442
  sed -i.bak "s/| \*\*Safe to Clear?\*\* | .*/| **Safe to Clear?** | $safe — $reason |/" "$ctx_file" && rm -f "$ctx_file.bak"
@@ -322,7 +449,7 @@ EOF
322
449
  local c_message="${3:-}"
323
450
  local c_source="${4:-User}"
324
451
 
325
- [[ -f "$ctx_file" ]] || { json_err "CONTEXT.md not found"; }
452
+ [[ -f "$ctx_file" ]] || { json_err "$E_FILE_NOT_FOUND" "Couldn't find CONTEXT.md. Try: run context-update init first."; }
326
453
 
327
454
  sed -i.bak "s/| \*\*Last Updated\*\* | .*/| **Last Updated** | $ctx_ts |/" "$ctx_file" && rm -f "$ctx_file.bak"
328
455
 
@@ -344,7 +471,7 @@ EOF
344
471
  local rationale="${3:-}"
345
472
  local made_by="${4:-Colony}"
346
473
 
347
- [[ -f "$ctx_file" ]] || { json_err "CONTEXT.md not found"; }
474
+ [[ -f "$ctx_file" ]] || { json_err "$E_FILE_NOT_FOUND" "Couldn't find CONTEXT.md. Try: run context-update init first."; }
348
475
 
349
476
  sed -i.bak "s/| \*\*Last Updated\*\* | .*/| **Last Updated** | $ctx_ts |/" "$ctx_file" && rm -f "$ctx_file.bak"
350
477
 
@@ -376,7 +503,7 @@ EOF
376
503
  local worker_count="${3:-0}"
377
504
  local tasks_count="${4:-0}"
378
505
 
379
- [[ -f "$ctx_file" ]] || { json_err "CONTEXT.md not found"; }
506
+ [[ -f "$ctx_file" ]] || { json_err "$E_FILE_NOT_FOUND" "Couldn't find CONTEXT.md. Try: run context-update init first."; }
380
507
 
381
508
  sed -i.bak "s/| \*\*Last Updated\*\* | .*/| **Last Updated** | $ctx_ts |/" "$ctx_file" && rm -f "$ctx_file.bak"
382
509
  sed -i.bak "s/## 📍 What's In Progress/## 📍 What's In Progress\n\n**Phase $phase_id Build IN PROGRESS**\n- Workers: $worker_count | Tasks: $tasks_count\n- Started: $ctx_ts/" "$ctx_file" && rm -f "$ctx_file.bak"
@@ -390,7 +517,7 @@ EOF
390
517
  local caste="${3:-}"
391
518
  local task="${4:-}"
392
519
 
393
- [[ -f "$ctx_file" ]] || { json_err "CONTEXT.md not found"; }
520
+ [[ -f "$ctx_file" ]] || { json_err "$E_FILE_NOT_FOUND" "Couldn't find CONTEXT.md. Try: run context-update init first."; }
394
521
 
395
522
  awk -v ant="$ant_name" -v caste="$caste" -v task="$task" -v ts="$ctx_ts" '
396
523
  /^## 📍 What'\''s In Progress/ { in_progress=1 }
@@ -410,7 +537,7 @@ EOF
410
537
  local ant_name="${2:-}"
411
538
  local status="${3:-completed}"
412
539
 
413
- [[ -f "$ctx_file" ]] || { json_err "CONTEXT.md not found"; }
540
+ [[ -f "$ctx_file" ]] || { json_err "$E_FILE_NOT_FOUND" "Couldn't find CONTEXT.md. Try: run context-update init first."; }
414
541
 
415
542
  sed -i.bak "s/- .*$ant_name .*$/- $ant_name: $status (updated $ctx_ts)/" "$ctx_file" && rm -f "$ctx_file.bak"
416
543
 
@@ -422,7 +549,7 @@ EOF
422
549
  local total="${3:-1}"
423
550
  local percentage=$(( completed * 100 / total ))
424
551
 
425
- [[ -f "$ctx_file" ]] || { json_err "CONTEXT.md not found"; }
552
+ [[ -f "$ctx_file" ]] || { json_err "$E_FILE_NOT_FOUND" "Couldn't find CONTEXT.md. Try: run context-update init first."; }
426
553
 
427
554
  sed -i.bak "s/Build IN PROGRESS/Build IN PROGRESS ($percentage% complete)/" "$ctx_file" && rm -f "$ctx_file.bak"
428
555
 
@@ -433,7 +560,7 @@ EOF
433
560
  local status="${2:-completed}"
434
561
  local result="${3:-success}"
435
562
 
436
- [[ -f "$ctx_file" ]] || { json_err "CONTEXT.md not found"; }
563
+ [[ -f "$ctx_file" ]] || { json_err "$E_FILE_NOT_FOUND" "Couldn't find CONTEXT.md. Try: run context-update init first."; }
437
564
 
438
565
  sed -i.bak "s/| \*\*Last Updated\*\* | .*/| **Last Updated** | $ctx_ts |/" "$ctx_file" && rm -f "$ctx_file.bak"
439
566
 
@@ -456,9 +583,20 @@ EOF
456
583
  ;;
457
584
 
458
585
  *)
459
- json_err "$E_VALIDATION_FAILED" "Unknown context action: $ctx_action"
586
+ json_err "$E_VALIDATION_FAILED" "Unknown context action: '$ctx_action'. Suggestion: Use one of: init, update-phase, activity, constraint, decision, safe-to-clear, build-start, worker-spawn, worker-complete, build-progress, build-complete"
460
587
  ;;
461
588
  esac
589
+
590
+ # Release lock on success (LOCK-04)
591
+ # NOTE: Do NOT clear the EXIT trap here. This function RETURNS (it does not
592
+ # call exit), so clearing the trap would remove the safety net without benefit.
593
+ # The EXIT trap remains active as a true safety net for unexpected exit calls
594
+ # elsewhere in the process. The _ctx_lock_held variable is the primary gate
595
+ # for this function's own cleanup.
596
+ if [[ "$_ctx_lock_held" == "true" ]]; then
597
+ release_lock 2>/dev/null || true
598
+ _ctx_lock_held=false
599
+ fi
462
600
  }
463
601
 
464
602
  # --- Subcommand dispatch ---
@@ -467,17 +605,140 @@ shift 2>/dev/null || true
467
605
 
468
606
  case "$cmd" in
469
607
  help)
470
- cat <<'EOF'
471
- {"ok":true,"commands":["help","version","validate-state","load-state","unload-state","error-add","error-pattern-check","error-summary","activity-log","activity-log-init","activity-log-read","learning-promote","learning-inject","generate-ant-name","spawn-log","spawn-complete","spawn-can-spawn","spawn-get-depth","spawn-tree-load","spawn-tree-active","spawn-tree-depth","update-progress","check-antipattern","error-flag-pattern","signature-scan","signature-match","flag-add","flag-check-blockers","flag-resolve","flag-acknowledge","flag-list","flag-auto-resolve","autofix-checkpoint","autofix-rollback","spawn-can-spawn-swarm","swarm-findings-init","swarm-findings-add","swarm-findings-read","swarm-solution-set","swarm-cleanup","swarm-activity-log","swarm-display-init","swarm-display-update","swarm-display-get","swarm-timing-start","swarm-timing-get","swarm-timing-eta","view-state-init","view-state-get","view-state-set","view-state-toggle","view-state-expand","view-state-collapse","grave-add","grave-check","generate-commit-message","version-check","registry-add","bootstrap-system","model-profile","model-get","model-list","chamber-create","chamber-verify","chamber-list","milestone-detect","queen-init","queen-read","queen-promote","survey-load","survey-verify","pheromone-export"],"description":"Aether Colony Utility Layer deterministic ops for the ant colony"}
472
- EOF
608
+ # Build help JSON with sections for discoverability.
609
+ # The flat 'commands' array is kept for backward compatibility
610
+ # (callers use: jq '.commands[]')
611
+ cat <<'HELP_EOF'
612
+ {
613
+ "ok": true,
614
+ "commands": ["help","version","validate-state","load-state","unload-state","error-add","error-pattern-check","error-summary","activity-log","activity-log-init","activity-log-read","learning-promote","learning-inject","generate-ant-name","spawn-log","spawn-complete","spawn-can-spawn","spawn-get-depth","spawn-tree-load","spawn-tree-active","spawn-tree-depth","update-progress","check-antipattern","error-flag-pattern","signature-scan","signature-match","flag-add","flag-check-blockers","flag-resolve","flag-acknowledge","flag-list","flag-auto-resolve","autofix-checkpoint","autofix-rollback","spawn-can-spawn-swarm","swarm-findings-init","swarm-findings-add","swarm-findings-read","swarm-solution-set","swarm-cleanup","swarm-activity-log","swarm-display-init","swarm-display-update","swarm-display-get","swarm-display-text","swarm-timing-start","swarm-timing-get","swarm-timing-eta","view-state-init","view-state-get","view-state-set","view-state-toggle","view-state-expand","view-state-collapse","grave-add","grave-check","generate-commit-message","version-check","registry-add","bootstrap-system","model-profile","model-get","model-list","chamber-create","chamber-verify","chamber-list","milestone-detect","queen-init","queen-read","queen-promote","survey-load","survey-verify","pheromone-export","pheromone-write","pheromone-count","pheromone-read","instinct-read","pheromone-prime","pheromone-expire","eternal-init","pheromone-export-xml","pheromone-import-xml","pheromone-validate-xml","wisdom-export-xml","wisdom-import-xml","registry-export-xml","registry-import-xml","force-unlock"],
615
+ "sections": {
616
+ "Core": [
617
+ {"name": "help", "description": "List all available commands with sections"},
618
+ {"name": "version", "description": "Show installed version"}
619
+ ],
620
+ "Colony State": [
621
+ {"name": "validate-state", "description": "Validate COLONY_STATE.json or constraints.json"},
622
+ {"name": "load-state", "description": "Load and lock COLONY_STATE.json"},
623
+ {"name": "unload-state", "description": "Release COLONY_STATE.json lock"}
624
+ ],
625
+ "Queen Commands": [
626
+ {"name": "queen-init", "description": "Initialize a new colony QUEEN.md from template"},
627
+ {"name": "queen-read", "description": "Read QUEEN.md wisdom as JSON for worker priming"},
628
+ {"name": "queen-promote", "description": "Promote a validated learning to QUEEN.md wisdom"}
629
+ ],
630
+ "Model Routing": [
631
+ {"name": "model-profile", "description": "Manage caste-to-model assignments"},
632
+ {"name": "model-get", "description": "Get model assignment for a caste"},
633
+ {"name": "model-list", "description": "List all model assignments"}
634
+ ],
635
+ "Spawn Management": [
636
+ {"name": "spawn-log", "description": "Log a spawn event to spawn-tree.txt"},
637
+ {"name": "spawn-complete", "description": "Record spawn completion in spawn-tree.txt"},
638
+ {"name": "spawn-can-spawn", "description": "Check if spawn budget allows another worker"},
639
+ {"name": "spawn-get-depth", "description": "Get spawn depth for an ant name"},
640
+ {"name": "spawn-tree-load", "description": "Load spawn-tree.txt as JSON"},
641
+ {"name": "spawn-tree-active", "description": "List currently active spawns"},
642
+ {"name": "spawn-tree-depth", "description": "Get depth for a named ant"}
643
+ ],
644
+ "Flag Management": [
645
+ {"name": "flag-add", "description": "Add a flag to flags.json"},
646
+ {"name": "flag-check-blockers", "description": "Check for flags blocking a task"},
647
+ {"name": "flag-resolve", "description": "Mark a flag as resolved"},
648
+ {"name": "flag-acknowledge", "description": "Acknowledge a flag without resolving"},
649
+ {"name": "flag-list", "description": "List all flags"},
650
+ {"name": "flag-auto-resolve", "description": "Auto-resolve flags matching criteria"}
651
+ ],
652
+ "Chamber Management": [
653
+ {"name": "chamber-create", "description": "Entomb a colony into a named chamber"},
654
+ {"name": "chamber-verify", "description": "Verify chamber integrity"},
655
+ {"name": "chamber-list", "description": "List all available chambers"}
656
+ ],
657
+ "Swarm Operations": [
658
+ {"name": "swarm-findings-init", "description": "Initialize swarm findings file"},
659
+ {"name": "swarm-findings-add", "description": "Add a finding to swarm results"},
660
+ {"name": "swarm-findings-read", "description": "Read all swarm findings"},
661
+ {"name": "swarm-solution-set", "description": "Set the chosen swarm solution"},
662
+ {"name": "swarm-cleanup", "description": "Clean up swarm state files"},
663
+ {"name": "swarm-display-init", "description": "Initialize swarm progress display"},
664
+ {"name": "swarm-display-update", "description": "Update swarm display for an ant"},
665
+ {"name": "swarm-timing-start", "description": "Start timing for a swarm operation"},
666
+ {"name": "swarm-timing-get", "description": "Get elapsed time for a swarm"},
667
+ {"name": "swarm-timing-eta", "description": "Estimate remaining time for a swarm"}
668
+ ],
669
+ "Pheromone System": [
670
+ {"name": "pheromone-write", "description": "Write a pheromone signal"},
671
+ {"name": "pheromone-read", "description": "Read pheromone signals"},
672
+ {"name": "pheromone-count", "description": "Count active pheromone signals"},
673
+ {"name": "pheromone-prime", "description": "Prime the pheromone system"},
674
+ {"name": "pheromone-expire", "description": "Expire old pheromone signals"},
675
+ {"name": "pheromone-export", "description": "Export pheromone data to JSON"},
676
+ {"name": "pheromone-export-xml", "description": "Export pheromone data to XML"},
677
+ {"name": "pheromone-import-xml", "description": "Import pheromone data from XML"},
678
+ {"name": "pheromone-validate-xml", "description": "Validate pheromone XML against schema"}
679
+ ],
680
+ "Utilities": [
681
+ {"name": "generate-ant-name", "description": "Generate a unique ant name with caste prefix"},
682
+ {"name": "activity-log", "description": "Append an entry to the activity log"},
683
+ {"name": "activity-log-init", "description": "Initialize the activity log file"},
684
+ {"name": "activity-log-read", "description": "Read recent activity log entries"},
685
+ {"name": "generate-commit-message", "description": "Generate a commit message from git diff"},
686
+ {"name": "version-check", "description": "Check if Aether version meets requirement"},
687
+ {"name": "registry-add", "description": "Register a repo with Aether"},
688
+ {"name": "bootstrap-system", "description": "Bootstrap minimal system files if missing"},
689
+ {"name": "force-unlock", "description": "Emergency unlock — remove stale lock files"}
690
+ ]
691
+ },
692
+ "description": "Aether Colony Utility Layer — deterministic ops for the ant colony"
693
+ }
694
+ HELP_EOF
473
695
  ;;
474
696
  version)
475
697
  json_ok '"1.0.0"'
476
698
  ;;
477
699
  validate-state)
700
+ # Schema migration helper: auto-upgrades pre-3.0 state files to v3.0
701
+ # Additive only (never removes fields) — idempotent and safe for concurrent access
702
+ _migrate_colony_state() {
703
+ local state_file="$1"
704
+ [[ -f "$state_file" ]] || return 0
705
+
706
+ # First: verify file is parseable JSON at all
707
+ if ! jq -e . "$state_file" >/dev/null 2>&1; then
708
+ # Corrupt state file — backup and error
709
+ if type create_backup &>/dev/null; then
710
+ create_backup "$state_file" 2>/dev/null || true
711
+ fi
712
+ json_err "$E_JSON_INVALID" \
713
+ "COLONY_STATE.json is corrupted (invalid JSON). A backup was saved in .aether/data/backups/. Try: run /ant:init to reset colony state."
714
+ fi
715
+
716
+ local current_version
717
+ current_version=$(jq -r '.version // "1.0"' "$state_file" 2>/dev/null)
718
+
719
+ if [[ "$current_version" != "3.0" ]]; then
720
+ # Add missing v3.0 fields (additive only — idempotent and safe for concurrent access)
721
+ local updated
722
+ updated=$(jq '
723
+ .version = "3.0" |
724
+ if .signals == null then .signals = [] else . end |
725
+ if .graveyards == null then .graveyards = [] else . end |
726
+ if .events == null then .events = [] else . end
727
+ ' "$state_file" 2>/dev/null)
728
+
729
+ if [[ -n "$updated" ]]; then
730
+ atomic_write "$state_file" "$updated"
731
+ # Notify user of migration (auto-migrate + notify pattern)
732
+ printf '{"ok":true,"warning":"W_MIGRATED","message":"Migrated colony state from v%s to v3.0"}\n' "$current_version" >&2
733
+ fi
734
+ fi
735
+ }
736
+
478
737
  case "${1:-}" in
479
738
  colony)
480
739
  [[ -f "$DATA_DIR/COLONY_STATE.json" ]] || json_err "$E_FILE_NOT_FOUND" "COLONY_STATE.json not found" '{"file":"COLONY_STATE.json"}'
740
+ # Run schema migration before field validation (ensures v3.0 fields always present)
741
+ _migrate_colony_state "$DATA_DIR/COLONY_STATE.json"
481
742
  json_ok "$(jq '
482
743
  def chk(f;t): if has(f) then (if (.[f]|type) as $a | t | any(. == $a) then "pass" else "fail: \(f) is \(.[f]|type), expected \(t|join("|"))" end) else "fail: missing \(f)" end;
483
744
  def opt(f;t): if has(f) then (if (.[f]|type) as $a | t | any(. == $a) then "pass" else "fail: \(f) is \(.[f]|type), expected \(t|join("|"))" end) else "pass" end;
@@ -492,7 +753,7 @@ EOF
492
753
  opt("session_id";["string","null"]),
493
754
  opt("initialized_at";["string","null"]),
494
755
  opt("build_started_at";["string","null"])
495
- ]} | . + {pass: ([.checks[] | select(. == "pass")] | length) == (.checks | length)}
756
+ ]} | . + {pass: (([.checks[] | select(. == "pass")] | length) == (.checks | length))}
496
757
  ' "$DATA_DIR/COLONY_STATE.json")"
497
758
  ;;
498
759
  constraints)
@@ -502,7 +763,7 @@ EOF
502
763
  {file:"constraints.json", checks:[
503
764
  arr("focus"),
504
765
  arr("constraints")
505
- ]} | . + {pass: ([.checks[] | select(. == "pass")] | length) == (.checks | length)}
766
+ ]} | . + {pass: (([.checks[] | select(. == "pass")] | length) == (.checks | length))}
506
767
  ' "$DATA_DIR/constraints.json")"
507
768
  ;;
508
769
  all)
@@ -727,6 +988,18 @@ EOF
727
988
  echo "[$ts] $status_icon $emoji $ant_name: $status${summary:+ - $summary}" >> "$DATA_DIR/activity.log"
728
989
  # Update spawn tree
729
990
  echo "$ts_full|$ant_name|$status|$summary" >> "$DATA_DIR/spawn-tree.txt"
991
+ # Log failed spawns to COLONY_STATE.json events array for audit trail (ARCH-04)
992
+ if [[ "$status" == "failed" ]] || [[ "$status" == "error" ]]; then
993
+ spawn_complete_state_file="$DATA_DIR/COLONY_STATE.json"
994
+ if [[ -f "$spawn_complete_state_file" ]]; then
995
+ spawn_complete_updated=$(jq --arg ts "$ts_full" --arg name "$ant_name" --arg st "$status" --arg sum "${summary:-unknown}" \
996
+ '.events += [{"type":"spawn_failed","ant":$name,"status":$st,"summary":$sum,"timestamp":$ts}]' \
997
+ "$spawn_complete_state_file" 2>/dev/null)
998
+ if [[ -n "$spawn_complete_updated" ]]; then
999
+ atomic_write "$spawn_complete_state_file" "$spawn_complete_updated"
1000
+ fi
1001
+ fi
1002
+ fi
730
1003
  # Return emoji-formatted result for display
731
1004
  json_ok "\"$status_icon $emoji $ant_name: ${summary:-$status}\""
732
1005
  ;;
@@ -1172,7 +1445,6 @@ EOF
1172
1445
  ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
1173
1446
 
1174
1447
  # Acquire lock for atomic flag update (degrade gracefully if locking unavailable)
1175
- lock_acquired=false
1176
1448
  if type feature_enabled &>/dev/null && ! feature_enabled "file_locking"; then
1177
1449
  json_warn "W_DEGRADED" "File locking disabled - proceeding without lock: $(type _feature_reason &>/dev/null && _feature_reason file_locking || echo 'unknown')"
1178
1450
  else
@@ -1184,9 +1456,8 @@ EOF
1184
1456
  exit 1
1185
1457
  fi
1186
1458
  }
1187
- lock_acquired=true
1188
1459
  # Ensure lock is always released on exit (BUG-002 fix)
1189
- trap 'release_lock "$flags_file" 2>/dev/null || true' EXIT
1460
+ trap 'release_lock 2>/dev/null || true' EXIT
1190
1461
  fi
1191
1462
 
1192
1463
  # Map type to severity
@@ -1224,8 +1495,8 @@ EOF
1224
1495
  ' "$flags_file") || { json_err "$E_JSON_INVALID" "Failed to add flag"; }
1225
1496
 
1226
1497
  atomic_write "$flags_file" "$updated"
1227
- # Lock released by trap on exit (BUG-002 fix)
1228
1498
  trap - EXIT
1499
+ release_lock 2>/dev/null || true
1229
1500
  json_ok "{\"id\":\"$id\",\"type\":\"$type\",\"severity\":\"$severity\"}"
1230
1501
  ;;
1231
1502
  flag-check-blockers)
@@ -1274,6 +1545,7 @@ EOF
1274
1545
  json_warn "W_DEGRADED" "File locking disabled - proceeding without lock"
1275
1546
  else
1276
1547
  acquire_lock "$flags_file" || json_err "$E_LOCK_FAILED" "Failed to acquire lock on flags.json"
1548
+ trap 'release_lock 2>/dev/null || true' EXIT
1277
1549
  fi
1278
1550
 
1279
1551
  updated=$(jq --arg id "$flag_id" --arg res "$resolution" --arg ts "$ts" '
@@ -1282,14 +1554,12 @@ EOF
1282
1554
  .resolution = $res
1283
1555
  else . end]
1284
1556
  ' "$flags_file") || {
1285
- if type feature_enabled &>/dev/null && feature_enabled "file_locking"; then
1286
- release_lock "$flags_file"
1287
- fi
1288
1557
  json_err "$E_JSON_INVALID" "Failed to resolve flag"
1289
1558
  }
1290
1559
 
1291
1560
  atomic_write "$flags_file" "$updated"
1292
- release_lock "$flags_file"
1561
+ trap - EXIT
1562
+ release_lock 2>/dev/null || true
1293
1563
  json_ok "{\"resolved\":\"$flag_id\"}"
1294
1564
  ;;
1295
1565
  flag-acknowledge)
@@ -1308,6 +1578,7 @@ EOF
1308
1578
  json_warn "W_DEGRADED" "File locking disabled - proceeding without lock"
1309
1579
  else
1310
1580
  acquire_lock "$flags_file" || json_err "$E_LOCK_FAILED" "Failed to acquire lock on flags.json"
1581
+ trap 'release_lock 2>/dev/null || true' EXIT
1311
1582
  fi
1312
1583
 
1313
1584
  updated=$(jq --arg id "$flag_id" --arg ts "$ts" '
@@ -1315,14 +1586,12 @@ EOF
1315
1586
  .acknowledged_at = $ts
1316
1587
  else . end]
1317
1588
  ' "$flags_file") || {
1318
- if type feature_enabled &>/dev/null && feature_enabled "file_locking"; then
1319
- release_lock "$flags_file"
1320
- fi
1321
1589
  json_err "$E_JSON_INVALID" "Failed to acknowledge flag"
1322
1590
  }
1323
1591
 
1324
1592
  atomic_write "$flags_file" "$updated"
1325
- release_lock "$flags_file"
1593
+ trap - EXIT
1594
+ release_lock 2>/dev/null || true
1326
1595
  json_ok "{\"acknowledged\":\"$flag_id\"}"
1327
1596
  ;;
1328
1597
  flag-list)
@@ -1376,14 +1645,12 @@ EOF
1376
1645
  ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
1377
1646
 
1378
1647
  # Acquire lock for atomic flag update (degrade gracefully if locking unavailable)
1379
- lock_acquired=false
1380
1648
  if type feature_enabled &>/dev/null && ! feature_enabled "file_locking"; then
1381
1649
  json_warn "W_DEGRADED" "File locking disabled - proceeding without lock"
1382
1650
  else
1383
1651
  acquire_lock "$flags_file" || json_err "$E_LOCK_FAILED" "Failed to acquire lock on flags.json"
1384
- lock_acquired=true
1385
1652
  # Ensure lock is always released on exit (BUG-005/BUG-011 fix)
1386
- trap 'release_lock "$flags_file" 2>/dev/null || true' EXIT
1653
+ trap 'release_lock 2>/dev/null || true' EXIT
1387
1654
  fi
1388
1655
 
1389
1656
  # Count how many will be resolved
@@ -1404,10 +1671,8 @@ EOF
1404
1671
  }
1405
1672
 
1406
1673
  atomic_write "$flags_file" "$updated"
1407
- # Lock released by trap on exit (BUG-005/BUG-011 fix)
1408
- if [[ "$lock_acquired" == "true" ]]; then
1409
- trap - EXIT
1410
- fi
1674
+ trap - EXIT
1675
+ release_lock 2>/dev/null || true
1411
1676
  json_ok "{\"resolved\":$count,\"trigger\":\"$trigger\"}"
1412
1677
  ;;
1413
1678
  generate-ant-name)
@@ -1456,7 +1721,7 @@ EOF
1456
1721
  if git rev-parse --git-dir >/dev/null 2>&1; then
1457
1722
  # Check if there are changes to Aether-managed files only
1458
1723
  # Target directories that Aether is allowed to modify
1459
- target_dirs=".aether .claude/commands/ant .claude/commands/st .opencode runtime bin"
1724
+ target_dirs=".aether .claude/commands/ant .claude/commands/st .opencode bin"
1460
1725
  has_changes=false
1461
1726
 
1462
1727
  for dir in $target_dirs; do
@@ -1535,7 +1800,7 @@ EOF
1535
1800
 
1536
1801
  current=0
1537
1802
  if [[ -f "$DATA_DIR/spawn-tree.txt" ]]; then
1538
- current=$(grep -c "|swarm:$swarm_id$" "$DATA_DIR/spawn-tree.txt" 2>/dev/null || echo 0)
1803
+ current=$(grep -c "|swarm:$swarm_id$" "$DATA_DIR/spawn-tree.txt" 2>/dev/null) || current=0
1539
1804
  fi
1540
1805
 
1541
1806
  if [[ $current -lt $swarm_cap ]]; then
@@ -1870,6 +2135,33 @@ Files: ${files_changed} files changed"
1870
2135
  fi
1871
2136
  ;;
1872
2137
 
2138
+ version-check-cached)
2139
+ # Cached version of version-check — skips if checked within TTL (3600s = 1 hour)
2140
+ # Usage: version-check-cached
2141
+ cache_file="$AETHER_ROOT/.aether/data/.version-check-cache"
2142
+ now=$(date +%s)
2143
+
2144
+ if [[ -f "$cache_file" ]]; then
2145
+ cached_at=$(cat "$cache_file" 2>/dev/null || echo "0")
2146
+ age=$((now - cached_at))
2147
+ if [[ $age -lt 3600 ]]; then
2148
+ # Within TTL — skip silently
2149
+ json_ok '""'
2150
+ exit 0
2151
+ fi
2152
+ fi
2153
+
2154
+ # Cache miss or stale — run actual check
2155
+ mkdir -p "$(dirname "$cache_file")" 2>/dev/null || true
2156
+ result=$("$0" version-check 2>/dev/null) || true
2157
+ echo "$now" > "$cache_file" 2>/dev/null || true
2158
+ if [[ -n "$result" ]]; then
2159
+ echo "$result"
2160
+ else
2161
+ json_ok '""'
2162
+ fi
2163
+ ;;
2164
+
1873
2165
  registry-add)
1874
2166
  # Add or update a repo entry in ~/.aether/registry.json
1875
2167
  # Usage: registry-add <repo_path> <version>
@@ -1924,15 +2216,14 @@ Files: ${files_changed} files changed"
1924
2216
  # Allowlist of system files to copy (relative to system/)
1925
2217
  allowlist=(
1926
2218
  "aether-utils.sh"
1927
- "coding-standards.md"
1928
- "debugging.md"
1929
- "DISCIPLINES.md"
1930
- "learning.md"
1931
- "planning.md"
1932
- "QUEEN_ANT_ARCHITECTURE.md"
1933
- "tdd.md"
1934
- "verification-loop.md"
1935
- "verification.md"
2219
+ "docs/disciplines/coding-standards.md"
2220
+ "docs/disciplines/debugging.md"
2221
+ "docs/disciplines/DISCIPLINES.md"
2222
+ "docs/disciplines/learning.md"
2223
+ "docs/disciplines/tdd.md"
2224
+ "docs/disciplines/verification-loop.md"
2225
+ "docs/disciplines/verification.md"
2226
+ "docs/QUEEN_ANT_ARCHITECTURE.md"
1936
2227
  "workers.md"
1937
2228
  "docs/constraints.md"
1938
2229
  "docs/pathogen-schema-example.json"
@@ -2143,15 +2434,29 @@ NODESCRIPT
2143
2434
  model-get)
2144
2435
  # Shortcut: model-get <caste>
2145
2436
  caste="${1:-}"
2146
- [[ -z "$caste" ]] && json_err "$E_VALIDATION_FAILED" "Usage: model-get <caste>"
2147
-
2148
- # Delegate to model-profile get
2149
- exec bash "$0" model-profile get "$caste"
2437
+ [[ -z "$caste" ]] && json_err "$E_VALIDATION_FAILED" "Usage: model-get <caste>. Try: provide a caste name (e.g., builder, scout, surveyor)."
2438
+
2439
+ # Delegate to model-profile get via subprocess (not exec) so errors can be captured
2440
+ set +e
2441
+ result=$(bash "$0" model-profile get "$caste" 2>&1)
2442
+ exit_code=$?
2443
+ set -e
2444
+ if [[ $exit_code -ne 0 ]]; then
2445
+ json_err "$E_BASH_ERROR" "Couldn't get model assignment for caste '$caste'. Try: check that .aether/model-profiles.yaml exists and is valid YAML."
2446
+ fi
2447
+ echo "$result"
2150
2448
  ;;
2151
2449
 
2152
2450
  model-list)
2153
- # Shortcut: list all models
2154
- exec bash "$0" model-profile list
2451
+ # Shortcut: list all models via subprocess (not exec) so errors can be captured
2452
+ set +e
2453
+ result=$(bash "$0" model-profile list 2>&1)
2454
+ exit_code=$?
2455
+ set -e
2456
+ if [[ $exit_code -ne 0 ]]; then
2457
+ json_err "$E_BASH_ERROR" "Couldn't list model assignments. Try: run 'aether verify-models' to check model configuration."
2458
+ fi
2459
+ echo "$result"
2155
2460
  ;;
2156
2461
 
2157
2462
  # ============================================
@@ -2426,6 +2731,377 @@ NODESCRIPT
2426
2731
  fi
2427
2732
  ;;
2428
2733
 
2734
+ swarm-display-inline)
2735
+ # Inline swarm display for Claude Code (no loop, no clear)
2736
+ # Usage: swarm-display-inline [swarm_id]
2737
+ swarm_id="${1:-default-swarm}"
2738
+ display_file="$DATA_DIR/swarm-display.json"
2739
+
2740
+ # ANSI colors
2741
+ BLUE='\033[34m'
2742
+ GREEN='\033[32m'
2743
+ YELLOW='\033[33m'
2744
+ RED='\033[31m'
2745
+ MAGENTA='\033[35m'
2746
+ BOLD='\033[1m'
2747
+ DIM='\033[2m'
2748
+ RESET='\033[0m'
2749
+
2750
+ # Caste colors
2751
+ get_caste_color() {
2752
+ case "$1" in
2753
+ builder) echo "$BLUE" ;;
2754
+ watcher) echo "$GREEN" ;;
2755
+ scout) echo "$YELLOW" ;;
2756
+ chaos) echo "$RED" ;;
2757
+ prime) echo "$MAGENTA" ;;
2758
+ oracle) echo "$MAGENTA" ;;
2759
+ route_setter) echo "$MAGENTA" ;;
2760
+ *) echo "$RESET" ;;
2761
+ esac
2762
+ }
2763
+
2764
+ # Caste emojis with ant
2765
+ get_caste_emoji() {
2766
+ case "$1" in
2767
+ builder) echo "🔨🐜" ;;
2768
+ watcher) echo "👁️🐜" ;;
2769
+ scout) echo "🔍🐜" ;;
2770
+ chaos) echo "🎲🐜" ;;
2771
+ prime) echo "👑🐜" ;;
2772
+ oracle) echo "🔮🐜" ;;
2773
+ route_setter) echo "🧭🐜" ;;
2774
+ archaeologist) echo "🏺🐜" ;;
2775
+ chronicler) echo "📝🐜" ;;
2776
+ gatekeeper) echo "📦🐜" ;;
2777
+ guardian) echo "🛡️🐜" ;;
2778
+ includer) echo "♿🐜" ;;
2779
+ keeper) echo "📚🐜" ;;
2780
+ measurer) echo "⚡🐜" ;;
2781
+ probe) echo "🧪🐜" ;;
2782
+ sage) echo "📜🐜" ;;
2783
+ tracker) echo "🐛🐜" ;;
2784
+ weaver) echo "🔄🐜" ;;
2785
+ colonizer) echo "🌱🐜" ;;
2786
+ dreamer) echo "💭🐜" ;;
2787
+ *) echo "🐜" ;;
2788
+ esac
2789
+ }
2790
+
2791
+ # Status phrases
2792
+ get_status_phrase() {
2793
+ case "$1" in
2794
+ builder) echo "excavating..." ;;
2795
+ watcher) echo "observing..." ;;
2796
+ scout) echo "exploring..." ;;
2797
+ chaos) echo "testing..." ;;
2798
+ prime) echo "coordinating..." ;;
2799
+ oracle) echo "researching..." ;;
2800
+ route_setter) echo "planning..." ;;
2801
+ *) echo "working..." ;;
2802
+ esac
2803
+ }
2804
+
2805
+ # Excavation phrase based on progress
2806
+ get_excavation_phrase() {
2807
+ local progress="${1:-0}"
2808
+ if [[ "$progress" -lt 25 ]]; then
2809
+ echo "🚧 Starting excavation..."
2810
+ elif [[ "$progress" -lt 50 ]]; then
2811
+ echo "⛏️ Digging deeper..."
2812
+ elif [[ "$progress" -lt 75 ]]; then
2813
+ echo "🪨 Moving earth..."
2814
+ elif [[ "$progress" -lt 100 ]]; then
2815
+ echo "🏗️ Almost there..."
2816
+ else
2817
+ echo "✅ Excavation complete!"
2818
+ fi
2819
+ }
2820
+
2821
+ # Format tools: "📖5 🔍3 ✏️2 ⚡1"
2822
+ format_tools() {
2823
+ local read="${1:-0}"
2824
+ local grep="${2:-0}"
2825
+ local edit="${3:-0}"
2826
+ local bash="${4:-0}"
2827
+ local result=""
2828
+ [[ "$read" -gt 0 ]] && result="${result}📖${read} "
2829
+ [[ "$grep" -gt 0 ]] && result="${result}🔍${grep} "
2830
+ [[ "$edit" -gt 0 ]] && result="${result}✏️${edit} "
2831
+ [[ "$bash" -gt 0 ]] && result="${result}⚡${bash}"
2832
+ echo "$result"
2833
+ }
2834
+
2835
+ # Render progress bar (green when working)
2836
+ render_progress_bar() {
2837
+ local percent="${1:-0}"
2838
+ local width="${2:-20}"
2839
+ [[ "$percent" -lt 0 ]] && percent=0
2840
+ [[ "$percent" -gt 100 ]] && percent=100
2841
+ local filled=$((percent * width / 100))
2842
+ local empty=$((width - filled))
2843
+ local bar=""
2844
+ for ((i=0; i<filled; i++)); do bar+="█"; done
2845
+ for ((i=0; i<empty; i++)); do bar+="░"; done
2846
+ echo -e "${GREEN}[$bar]${RESET} ${percent}%"
2847
+ }
2848
+
2849
+ # Format duration
2850
+ format_duration() {
2851
+ local seconds="${1:-0}"
2852
+ if [[ "$seconds" -lt 60 ]]; then
2853
+ echo "${seconds}s"
2854
+ else
2855
+ local mins=$((seconds / 60))
2856
+ local secs=$((seconds % 60))
2857
+ echo "${mins}m${secs}s"
2858
+ fi
2859
+ }
2860
+
2861
+ # Check for display file
2862
+ if [[ ! -f "$display_file" ]]; then
2863
+ echo -e "${DIM}🐜 No active swarm data${RESET}"
2864
+ json_ok '{"displayed":false,"reason":"no_data"}'
2865
+ exit 0
2866
+ fi
2867
+
2868
+ # Check for jq
2869
+ if ! command -v jq >/dev/null 2>&1; then
2870
+ echo -e "${DIM}🐜 Swarm active (jq not available for details)${RESET}"
2871
+ json_ok '{"displayed":true,"warning":"jq_missing"}'
2872
+ exit 0
2873
+ fi
2874
+
2875
+ # Read swarm data
2876
+ total_active=$(jq -r '.summary.total_active // 0' "$display_file" 2>/dev/null || echo "0")
2877
+
2878
+ if [[ "$total_active" -eq 0 ]]; then
2879
+ echo -e "${DIM}🐜 Colony idle${RESET}"
2880
+ json_ok '{"displayed":true,"ants":0}'
2881
+ exit 0
2882
+ fi
2883
+
2884
+ # Render header with ant logo
2885
+ echo ""
2886
+ cat << 'ANTLOGO'
2887
+
2888
+
2889
+ ▁▐▖ ▁
2890
+ ▗▇▇███▆▇▃▅████▆▇▆▅▟██▛▇
2891
+ ▝▜▅▛██████████████▜▅██
2892
+ ▁▂▀▇▆██▙▜██████████▛▟███▛▁▃▁
2893
+ ▕▂▁▉▅████▙▞██████▜█▚▟████▅▊ ▐
2894
+ ▗▁▐█▀▜████▛▃▝▁████▍▘▟▜████▛▀█▂ ▖
2895
+ ▁▎▝█▁▝▍▆▜████▊▐▀▏▀▍▂▂▝▀▕▀▌█████▀▅▐▚ █▏▁▁
2896
+ ▂▚▃▇▙█▟████▛▏ ▝▜▐▛▀▍▛▘ ▕█████▆▊▐▂▃▞▂▔
2897
+ ▚▔█▛██████▙▟▍▜▍▜▃▃▖▟▛▐██████▛▛▜▔▔▞
2898
+ ▋▖▍▊▖██████▇▃▁▝██▘▝▃████▜█▜ ▋▐▐▗
2899
+ ▍▌▇█▅▂▜██████████████████▉▃▄▋▖ ▝
2900
+ ▁▎▍▁▜▟███▀▀▜████████████▛▀▀███▆▂ ▁▁
2901
+ ██ ▆▇▌▁▕▚▅▆███▛████████▜███▆▄▞▁▁▐▅▎ █▉
2902
+ ▆█████▛▃▟█▀████████████████▛█▙▙▜▉▟▛▜█▌▗
2903
+ ▅▆▋ ▁▁▁▔▕▁▁▁▇█████▛▀▀▀▁▜▇▇▁▁▁▁▁▁▁▁ ▐▊▗
2904
+ ▗▆▃▃▃▔███▖▔██▀▀▝▀██▀▍█▛▁▐█▏█▛▀▀▏█▛▀▜█▆▃▃▆▖
2905
+ ▝▗▖ ▟█▟█▙ █▛▀▘ █▊ ▕█▛▀▜█▏█▛▀▘ █▋▆█▛ ▗▖
2906
+ ▘ ▘ ▟▛ ▝▀▘▀▀▀▀▘ ▀▀▂▂█▙▂▐▀▏▀▀▀▀▘▀▘ ▝▀▅▂▝ ▕▏
2907
+ ▕▕ ▃▗▄▔▗▄▄▗▗▗▔▄▄▄▄▗▄▄▗▔▃▃▃▗▄▂▄▃▗▄▂▖▖ ▏▁
2908
+ ▝▘▏ ▔▔ ▁▔▁▔▔▁▔▔▔▔▔▔▔▁▁ ▔▔ ▔▔▔▔
2909
+ ▀ ▀▝▘▀▀▔▘▘▀▝▕▀▀▝▝▀▔▀ ▀▔▘
2910
+ ▘ ▗▅▁▝▚▃▀▆▟██▙▆▝▃ ▘ ▁▗▌
2911
+ ▔▀▔▝ ▔▀▟▜▛▛▀▔ ▀
2912
+
2913
+
2914
+ ANTLOGO
2915
+ echo -e "${BOLD}AETHER COLONY :: Colony Activity${RESET}"
2916
+ echo -e "${DIM}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
2917
+ echo ""
2918
+
2919
+ # Render each active ant (limit to 5)
2920
+ jq -r '.active_ants[0:5][] | "\(.name)|\(.caste)|\(.status // "")|\(.task // "")|\(.tools.read // 0)|\(.tools.grep // 0)|\(.tools.edit // 0)|\(.tools.bash // 0)|\(.tokens // 0)|\(.started_at // "")|\(.parent // "Queen")|\(.progress // 0)"' "$display_file" 2>/dev/null | while IFS='|' read -r ant_name ant_caste ant_status ant_task read_ct grep_ct edit_ct bash_ct tokens started_at parent progress; do
2921
+ color=$(get_caste_color "$ant_caste")
2922
+ emoji=$(get_caste_emoji "$ant_caste")
2923
+ phrase=$(get_status_phrase "$ant_caste")
2924
+
2925
+ # Format tools
2926
+ tools_str=$(format_tools "$read_ct" "$grep_ct" "$edit_ct" "$bash_ct")
2927
+
2928
+ # Truncate task if too long
2929
+ display_task="$ant_task"
2930
+ [[ ${#display_task} -gt 35 ]] && display_task="${display_task:0:32}..."
2931
+
2932
+ # Calculate elapsed time
2933
+ elapsed_str=""
2934
+ started_ts="${started_at:-}"
2935
+ if [[ -n "$started_ts" ]] && [[ "$started_ts" != "null" ]]; then
2936
+ started_ts=$(date -j -f "%Y-%m-%dT%H:%M:%SZ" "$started_ts" +%s 2>/dev/null)
2937
+ if [[ -z "$started_ts" ]] || [[ "$started_ts" == "null" ]]; then
2938
+ started_ts=$(date -d "$started_ts" +%s 2>/dev/null) || started_ts=0
2939
+ fi
2940
+ now_ts=$(date +%s)
2941
+ elapsed=0
2942
+ if [[ -n "$started_ts" ]] && [[ "$started_ts" -gt 0 ]] 2>/dev/null; then
2943
+ elapsed=$((now_ts - started_ts))
2944
+ fi
2945
+ if [[ ${elapsed:-0} -gt 0 ]]; then
2946
+ elapsed_str="($(format_duration $elapsed))"
2947
+ fi
2948
+ fi
2949
+
2950
+ # Token indicator
2951
+ token_str=""
2952
+ if [[ -n "$tokens" ]] && [[ "$tokens" -gt 0 ]]; then
2953
+ token_str="🍯${tokens}"
2954
+ fi
2955
+
2956
+ # Output ant line: "🐜 Builder: excavating... Implement auth 📖5 🔍3 (2m3s) 🍯1250"
2957
+ echo -e "${color}${emoji} ${BOLD}${ant_name}${RESET}${color}: ${phrase}${RESET} ${display_task}"
2958
+ echo -e " ${tools_str} ${DIM}${elapsed_str}${RESET} ${token_str}"
2959
+
2960
+ # Show progress bar if progress > 0
2961
+ if [[ -n "$progress" ]] && [[ "$progress" -gt 0 ]]; then
2962
+ progress_bar=$(render_progress_bar "$progress" 15)
2963
+ excavation_phrase=$(get_excavation_phrase "$progress")
2964
+ echo -e " ${DIM}${progress_bar}${RESET}"
2965
+ echo -e " ${DIM}${excavation_phrase}${RESET}"
2966
+ fi
2967
+
2968
+ echo ""
2969
+ done
2970
+
2971
+ # Chamber activity map
2972
+ echo -e "${DIM}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
2973
+ echo ""
2974
+ echo -e "${BOLD}Chamber Activity:${RESET}"
2975
+
2976
+ # Show active chambers with fire intensity
2977
+ has_chamber_activity=0
2978
+ jq -r '.chambers | to_entries[] | "\(.key)|\(.value.activity)|\(.value.icon)"' "$display_file" 2>/dev/null | \
2979
+ while IFS='|' read -r chamber activity icon; do
2980
+ if [[ -n "$activity" ]] && [[ "$activity" -gt 0 ]]; then
2981
+ has_chamber_activity=1
2982
+ if [[ "$activity" -ge 5 ]]; then
2983
+ fires="🔥🔥🔥"
2984
+ elif [[ "$activity" -ge 3 ]]; then
2985
+ fires="🔥🔥"
2986
+ else
2987
+ fires="🔥"
2988
+ fi
2989
+ chamber_name="${chamber//_/ }"
2990
+ echo -e " ${icon} ${chamber_name} ${fires} (${activity} ants)"
2991
+ fi
2992
+ done
2993
+
2994
+ if [[ "$has_chamber_activity" -eq 0 ]]; then
2995
+ echo -e "${DIM} (no chamber activity)${RESET}"
2996
+ fi
2997
+
2998
+ # Summary
2999
+ echo ""
3000
+ echo -e "${DIM}${total_active} forager$([[ "$total_active" -eq 1 ]] || echo "s") excavating...${RESET}"
3001
+
3002
+ json_ok "{\"displayed\":true,\"ants\":$total_active}"
3003
+ ;;
3004
+
3005
+ swarm-display-text)
3006
+ # Plain-text swarm display for Claude conversation (no ANSI codes)
3007
+ # Usage: swarm-display-text [swarm_id]
3008
+ swarm_id="${1:-default-swarm}"
3009
+ display_file="$DATA_DIR/swarm-display.json"
3010
+
3011
+ # Check for display file
3012
+ if [[ ! -f "$display_file" ]]; then
3013
+ echo "🐜 Colony idle"
3014
+ json_ok '{"displayed":false,"reason":"no_data"}'
3015
+ exit 0
3016
+ fi
3017
+
3018
+ # Check for jq
3019
+ if ! command -v jq >/dev/null 2>&1; then
3020
+ echo "🐜 Swarm active (details unavailable)"
3021
+ json_ok '{"displayed":true,"warning":"jq_missing"}'
3022
+ exit 0
3023
+ fi
3024
+
3025
+ # Read swarm data — handle both flat total_active and nested .summary.total_active
3026
+ total_active=$(jq -r '(.total_active // .summary.total_active // 0)' "$display_file" 2>/dev/null || echo "0")
3027
+
3028
+ if [[ "$total_active" -eq 0 ]]; then
3029
+ echo "🐜 Colony idle"
3030
+ json_ok '{"displayed":true,"ants":0}'
3031
+ exit 0
3032
+ fi
3033
+
3034
+ # Compact header
3035
+ echo "🐜 COLONY ACTIVITY"
3036
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
3037
+
3038
+ # Caste emoji lookup
3039
+ get_emoji() {
3040
+ case "$1" in
3041
+ builder) echo "🔨🐜" ;;
3042
+ watcher) echo "👁️🐜" ;;
3043
+ scout) echo "🔍🐜" ;;
3044
+ chaos) echo "🎲🐜" ;;
3045
+ prime) echo "👑🐜" ;;
3046
+ oracle) echo "🔮🐜" ;;
3047
+ route_setter) echo "🧭🐜" ;;
3048
+ archaeologist) echo "🏺🐜" ;;
3049
+ surveyor) echo "📊🐜" ;;
3050
+ *) echo "🐜" ;;
3051
+ esac
3052
+ }
3053
+
3054
+ # Format tool counts (only non-zero)
3055
+ format_tools_text() {
3056
+ local r="${1:-0}" g="${2:-0}" e="${3:-0}" b="${4:-0}"
3057
+ local result=""
3058
+ [[ "$r" -gt 0 ]] && result="${result}📖${r} "
3059
+ [[ "$g" -gt 0 ]] && result="${result}🔍${g} "
3060
+ [[ "$e" -gt 0 ]] && result="${result}✏️${e} "
3061
+ [[ "$b" -gt 0 ]] && result="${result}⚡${b}"
3062
+ echo "$result"
3063
+ }
3064
+
3065
+ # Progress bar using block characters (no ANSI)
3066
+ render_bar_text() {
3067
+ local pct="${1:-0}" w="${2:-10}"
3068
+ [[ "$pct" -lt 0 ]] && pct=0
3069
+ [[ "$pct" -gt 100 ]] && pct=100
3070
+ local filled=$((pct * w / 100))
3071
+ local empty=$((w - filled))
3072
+ local bar=""
3073
+ for ((i=0; i<filled; i++)); do bar+="█"; done
3074
+ for ((i=0; i<empty; i++)); do bar+="░"; done
3075
+ echo "[$bar] ${pct}%"
3076
+ }
3077
+
3078
+ # Render each ant (max 5)
3079
+ jq -r '.active_ants[0:5][] | "\(.name)|\(.caste)|\(.task // "")|\(.tools.read // 0)|\(.tools.grep // 0)|\(.tools.edit // 0)|\(.tools.bash // 0)|\(.progress // 0)"' "$display_file" 2>/dev/null | while IFS='|' read -r name caste task r g e b progress; do
3080
+ emoji=$(get_emoji "$caste")
3081
+ tools=$(format_tools_text "$r" "$g" "$e" "$b")
3082
+ bar=$(render_bar_text "${progress:-0}" 10)
3083
+
3084
+ # Truncate task to 25 chars
3085
+ [[ ${#task} -gt 25 ]] && task="${task:0:22}..."
3086
+
3087
+ echo "${emoji} ${name} ${bar} ${task}"
3088
+ [[ -n "$tools" ]] && echo " ${tools}"
3089
+ echo ""
3090
+ done
3091
+
3092
+ # Overflow indicator
3093
+ if [[ "$total_active" -gt 5 ]]; then
3094
+ echo " +$((total_active - 5)) more ants..."
3095
+ echo ""
3096
+ fi
3097
+
3098
+ # Footer
3099
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
3100
+ echo "${total_active} ants active"
3101
+
3102
+ json_ok "{\"displayed\":true,\"ants\":$total_active}"
3103
+ ;;
3104
+
2429
3105
  swarm-timing-start)
2430
3106
  # Record start time for an ant
2431
3107
  # Usage: swarm-timing-start <ant_name>
@@ -2699,12 +3375,12 @@ NODESCRIPT
2699
3375
  queen_file="$AETHER_ROOT/.aether/docs/QUEEN.md"
2700
3376
 
2701
3377
  # Check multiple locations for template
2702
- # Order: dev (runtime/) -> npm install (hub) -> legacy
3378
+ # Order: hub (system/) -> dev (.aether/) -> repo local -> legacy
2703
3379
  template_file=""
2704
3380
  for path in \
2705
- "$AETHER_ROOT/runtime/templates/QUEEN.md.template" \
2706
- "$HOME/.aether/templates/QUEEN.md.template" \
2707
- "$AETHER_ROOT/.aether/templates/QUEEN.md.template"; do
3381
+ "$HOME/.aether/system/templates/QUEEN.md.template" \
3382
+ "$AETHER_ROOT/.aether/templates/QUEEN.md.template" \
3383
+ "$HOME/.aether/templates/QUEEN.md.template"; do
2708
3384
  if [[ -f "$path" ]]; then
2709
3385
  template_file="$path"
2710
3386
  break
@@ -2722,7 +3398,9 @@ NODESCRIPT
2722
3398
 
2723
3399
  # Check if template was found
2724
3400
  if [[ -z "$template_file" ]]; then
2725
- json_err "$E_FILE_NOT_FOUND" "Template not found" '{"templates_checked":["runtime/templates/QUEEN.md.template","~/.aether/templates/QUEEN.md.template",".aether/templates/QUEEN.md.template"]}'
3401
+ json_err "$E_FILE_NOT_FOUND" \
3402
+ "Template not found. Run: npm install -g aether && aether install to restore it." \
3403
+ '{"templates_checked":["~/.aether/system/templates/QUEEN.md.template",".aether/templates/QUEEN.md.template","~/.aether/templates/QUEEN.md.template"]}'
2726
3404
  exit 1
2727
3405
  fi
2728
3406
 
@@ -2757,6 +3435,12 @@ NODESCRIPT
2757
3435
  metadata='{"version":"unknown","last_evolved":null,"colonies_contributed":[],"promotion_thresholds":{},"stats":{}}'
2758
3436
  fi
2759
3437
 
3438
+ # Gate 1: Validate metadata is parseable JSON BEFORE using as --argjson
3439
+ if ! echo "$metadata" | jq -e . >/dev/null 2>&1; then
3440
+ json_err "$E_JSON_INVALID" \
3441
+ "QUEEN.md has a malformed METADATA block — the JSON between <!-- METADATA and --> is invalid. Try: fix the JSON in .aether/docs/QUEEN.md or run queen-init to reset."
3442
+ fi
3443
+
2760
3444
  # Extract sections content for worker priming
2761
3445
  # Use awk to parse markdown sections - remove header line and trailing section header
2762
3446
  philosophies=$(awk '/^## 📜 Philosophies$/,/^## /' "$queen_file" | tail -n +2 | sed '$d' | sed '/^$/d' | jq -Rs '.')
@@ -2791,6 +3475,41 @@ NODESCRIPT
2791
3475
  }
2792
3476
  }')
2793
3477
 
3478
+ # Gate 2: Validate assembled result before returning
3479
+ if [[ -z "$result" ]] || ! echo "$result" | jq -e . >/dev/null 2>&1; then
3480
+ json_err "$E_JSON_INVALID" \
3481
+ "Couldn't assemble queen-read output. QUEEN.md may have formatting issues. Try: run queen-init to reset."
3482
+ fi
3483
+ json_ok "$result"
3484
+ ;;
3485
+
3486
+ pheromone-read)
3487
+ # Read active pheromones (FOCUS/REDIRECT) from constraints.json
3488
+ # Used to inject active signals into worker prompts
3489
+ constraints_file="$AETHER_ROOT/.aether/data/constraints.json"
3490
+
3491
+ # Initialize defaults (no local - script-level)
3492
+ priorities='[]'
3493
+ avoid='[]'
3494
+
3495
+ # Check if constraints file exists
3496
+ if [[ -f "$constraints_file" ]]; then
3497
+ # Read focus array as priorities
3498
+ priorities=$(jq -c '.focus // []' "$constraints_file" 2>/dev/null || echo '[]')
3499
+
3500
+ # Read constraints array, extract content and source
3501
+ avoid=$(jq -c '[.constraints[]? | {content: .content, source: .source}] // []' "$constraints_file" 2>/dev/null || echo '[]')
3502
+ fi
3503
+
3504
+ # Build JSON output
3505
+ result=$(jq -n \
3506
+ --argjson priorities "$priorities" \
3507
+ --argjson avoid "$avoid" \
3508
+ '{
3509
+ priorities: $priorities,
3510
+ avoid: $avoid
3511
+ }')
3512
+
2794
3513
  json_ok "$result"
2795
3514
  ;;
2796
3515
 
@@ -3068,8 +3787,6 @@ ${entry}" "$queen_file" > "$tmp_file"
3068
3787
  is_system=true
3069
3788
  elif [[ "$file" == .opencode/agents/*.md ]] || [[ "$file" == .opencode/agents/**/*.md ]]; then
3070
3789
  is_system=true
3071
- elif [[ "$file" == runtime/* ]]; then
3072
- is_system=true
3073
3790
  elif [[ "$file" == bin/* ]]; then
3074
3791
  is_system=true
3075
3792
  fi
@@ -3399,64 +4116,1129 @@ ${entry}" "$queen_file" > "$tmp_file"
3399
4116
  if type pheromone-export &>/dev/null; then
3400
4117
  pheromone-export "$input_json" "$output_xml" "$schema_file"
3401
4118
  else
3402
- json_err "$E_DEPENDENCY_MISSING" "xml-utils.sh not available for pheromone export"
4119
+ json_err "$E_DEPENDENCY_MISSING" "xml-utils.sh not available. Try: run aether update to restore utility scripts."
3403
4120
  fi
3404
4121
  ;;
3405
4122
 
3406
- # ============================================================================
3407
- # Session Continuity Commands
3408
- # ============================================================================
4123
+ pheromone-write)
4124
+ # Write a pheromone signal to pheromones.json
4125
+ # Usage: pheromone-write <type> <content> [--strength N] [--ttl TTL] [--source SOURCE] [--reason REASON]
4126
+ # type: FOCUS, REDIRECT, or FEEDBACK
4127
+ # content: signal text (required, max 500 chars)
4128
+ # --strength: 0.0-1.0 (defaults: REDIRECT=0.9, FOCUS=0.8, FEEDBACK=0.7)
4129
+ # --ttl: phase_end (default), 2h, 1d, 7d, 30d, etc.
4130
+ # --source: user (default), worker:builder, system
4131
+ # --reason: human-readable explanation
3409
4132
 
3410
- session-init)
3411
- # Initialize a new session tracking file
3412
- # Usage: session-init [session_id] [goal]
3413
- session_id="${2:-$(date +%s)_$(openssl rand -hex 4 2>/dev/null || echo $$)}"
3414
- goal="${3:-}"
4133
+ pw_type="${1:-}"
4134
+ pw_content="${2:-}"
3415
4135
 
3416
- session_file="$DATA_DIR/session.json"
4136
+ # Validate type
4137
+ if [[ -z "$pw_type" ]]; then
4138
+ json_err "$E_VALIDATION_FAILED" "pheromone-write requires <type> argument (FOCUS, REDIRECT, or FEEDBACK)"
4139
+ fi
3417
4140
 
3418
- cat > "$session_file" << EOF
3419
- {
3420
- "session_id": "$session_id",
3421
- "started_at": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")",
3422
- "last_command": null,
3423
- "last_command_at": null,
3424
- "colony_goal": "$goal",
3425
- "current_phase": 0,
3426
- "current_milestone": "First Mound",
3427
- "suggested_next": "/ant:plan",
3428
- "context_cleared": false,
3429
- "resumed_at": null,
3430
- "active_todos": [],
3431
- "summary": "Session initialized"
3432
- }
3433
- EOF
3434
- json_ok "{\"session_id\":\"$session_id\",\"goal\":\"$goal\",\"file\":\"$session_file\"}"
3435
- ;;
4141
+ pw_type=$(echo "$pw_type" | tr '[:lower:]' '[:upper:]')
4142
+ case "$pw_type" in
4143
+ FOCUS|REDIRECT|FEEDBACK) ;;
4144
+ *) json_err "$E_VALIDATION_FAILED" "Invalid pheromone type: $pw_type. Must be FOCUS, REDIRECT, or FEEDBACK" ;;
4145
+ esac
3436
4146
 
3437
- session-update)
3438
- # Update session with latest activity
3439
- # Usage: session-update <command> [suggested_next] [summary]
3440
- cmd_run="${2:-}"
3441
- suggested="${3:-}"
3442
- summary="${4:-}"
4147
+ if [[ -z "$pw_content" ]]; then
4148
+ json_err "$E_VALIDATION_FAILED" "pheromone-write requires <content> argument"
4149
+ fi
3443
4150
 
3444
- session_file="$DATA_DIR/session.json"
4151
+ # Parse optional flags from remaining args (after type and content)
4152
+ pw_strength=""
4153
+ pw_ttl="phase_end"
4154
+ pw_source="user"
4155
+ pw_reason=""
3445
4156
 
3446
- if [[ ! -f "$session_file" ]]; then
3447
- # Auto-initialize if doesn't exist
3448
- bash "$0" session-init "auto_$(date +%s)" ""
4157
+ shift 2 # shift past type and content
4158
+ while [[ $# -gt 0 ]]; do
4159
+ case "$1" in
4160
+ --strength) pw_strength="$2"; shift 2 ;;
4161
+ --ttl) pw_ttl="$2"; shift 2 ;;
4162
+ --source) pw_source="$2"; shift 2 ;;
4163
+ --reason) pw_reason="$2"; shift 2 ;;
4164
+ *) shift ;;
4165
+ esac
4166
+ done
4167
+
4168
+ # Apply default strength by type
4169
+ if [[ -z "$pw_strength" ]]; then
4170
+ case "$pw_type" in
4171
+ REDIRECT) pw_strength="0.9" ;;
4172
+ FOCUS) pw_strength="0.8" ;;
4173
+ FEEDBACK) pw_strength="0.7" ;;
4174
+ esac
3449
4175
  fi
3450
4176
 
3451
- # Read current session
3452
- current_session=$(cat "$session_file" 2>/dev/null || echo '{}')
4177
+ # Apply default reason by type
4178
+ if [[ -z "$pw_reason" ]]; then
4179
+ pw_type_lower_r=$(echo "$pw_type" | tr '[:upper:]' '[:lower:]')
4180
+ pw_reason="User emitted via /ant:${pw_type_lower_r}"
4181
+ fi
3453
4182
 
3454
- # Extract current values for preservation
3455
- current_goal=$(echo "$current_session" | jq -r '.colony_goal // empty')
3456
- current_phase=$(echo "$current_session" | jq -r '.current_phase // 0')
3457
- current_milestone=$(echo "$current_session" | jq -r '.current_milestone // "First Mound"')
4183
+ # Set priority by type
4184
+ case "$pw_type" in
4185
+ REDIRECT) pw_priority="high" ;;
4186
+ FOCUS) pw_priority="normal" ;;
4187
+ FEEDBACK) pw_priority="low" ;;
4188
+ esac
3458
4189
 
3459
- # Get top 3 TODOs if TO-DOs.md exists
4190
+ # Generate ID and timestamps
4191
+ pw_epoch=$(date +%s)
4192
+ pw_epoch_ms="${pw_epoch}000"
4193
+ pw_type_lower=$(echo "$pw_type" | tr '[:upper:]' '[:lower:]')
4194
+ pw_id="sig_${pw_type_lower}_${pw_epoch_ms}"
4195
+ pw_created=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
4196
+
4197
+ # Compute expires_at from TTL
4198
+ if [[ "$pw_ttl" == "phase_end" ]]; then
4199
+ pw_expires="phase_end"
4200
+ else
4201
+ pw_ttl_secs=0
4202
+ if [[ "$pw_ttl" =~ ^([0-9]+)m$ ]]; then
4203
+ pw_ttl_secs=$(( ${BASH_REMATCH[1]} * 60 ))
4204
+ elif [[ "$pw_ttl" =~ ^([0-9]+)h$ ]]; then
4205
+ pw_ttl_secs=$(( ${BASH_REMATCH[1]} * 3600 ))
4206
+ elif [[ "$pw_ttl" =~ ^([0-9]+)d$ ]]; then
4207
+ pw_ttl_secs=$(( ${BASH_REMATCH[1]} * 86400 ))
4208
+ fi
4209
+ if [[ $pw_ttl_secs -gt 0 ]]; then
4210
+ pw_expires_epoch=$(( pw_epoch + pw_ttl_secs ))
4211
+ pw_expires=$(date -u -r "$pw_expires_epoch" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || \
4212
+ date -u -d "@$pw_expires_epoch" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || \
4213
+ echo "phase_end")
4214
+ else
4215
+ pw_expires="phase_end"
4216
+ fi
4217
+ fi
4218
+
4219
+ pw_file="$DATA_DIR/pheromones.json"
4220
+
4221
+ # Initialize pheromones.json if missing
4222
+ if [[ ! -f "$pw_file" ]]; then
4223
+ pw_colony_id="aether-dev"
4224
+ if [[ -f "$DATA_DIR/COLONY_STATE.json" ]]; then
4225
+ pw_colony_id=$(jq -r '.session_id // "aether-dev"' "$DATA_DIR/COLONY_STATE.json" 2>/dev/null || echo "aether-dev")
4226
+ fi
4227
+ printf '{\n "version": "1.0.0",\n "colony_id": "%s",\n "generated_at": "%s",\n "signals": []\n}\n' \
4228
+ "$pw_colony_id" "$pw_created" > "$pw_file"
4229
+ fi
4230
+
4231
+ # Build signal object and append to pheromones.json
4232
+ pw_signal=$(jq -n \
4233
+ --arg id "$pw_id" \
4234
+ --arg type "$pw_type" \
4235
+ --arg priority "$pw_priority" \
4236
+ --arg source "$pw_source" \
4237
+ --arg created_at "$pw_created" \
4238
+ --arg expires_at "$pw_expires" \
4239
+ --argjson active true \
4240
+ --argjson strength "$pw_strength" \
4241
+ --arg reason "$pw_reason" \
4242
+ --arg content "$pw_content" \
4243
+ '{id: $id, type: $type, priority: $priority, source: $source, created_at: $created_at, expires_at: $expires_at, active: $active, strength: ($strength | tonumber), reason: $reason, content: {text: $content}}')
4244
+
4245
+ pw_updated=$(jq --argjson sig "$pw_signal" '.signals += [$sig]' "$pw_file" 2>/dev/null)
4246
+ if [[ -z "$pw_updated" ]]; then
4247
+ json_err "${E_JSON_INVALID:-E_JSON_INVALID}" "Failed to update pheromones.json — jq parse error"
4248
+ fi
4249
+ echo "$pw_updated" > "$pw_file"
4250
+
4251
+ # Backward compatibility: also write to constraints.json
4252
+ pw_cfile="$DATA_DIR/constraints.json"
4253
+ if [[ "$pw_type" == "FOCUS" ]]; then
4254
+ if [[ ! -f "$pw_cfile" ]]; then
4255
+ echo '{"version":"1.0","focus":[],"constraints":[]}' > "$pw_cfile"
4256
+ fi
4257
+ pw_cfile_updated=$(jq --arg txt "$pw_content" '
4258
+ .focus += [$txt] |
4259
+ if (.focus | length) > 5 then .focus = .focus[-5:] else . end
4260
+ ' "$pw_cfile" 2>/dev/null)
4261
+ [[ -n "$pw_cfile_updated" ]] && echo "$pw_cfile_updated" > "$pw_cfile"
4262
+ elif [[ "$pw_type" == "REDIRECT" ]]; then
4263
+ if [[ ! -f "$pw_cfile" ]]; then
4264
+ echo '{"version":"1.0","focus":[],"constraints":[]}' > "$pw_cfile"
4265
+ fi
4266
+ pw_constraint=$(jq -n \
4267
+ --arg id "c_${pw_epoch}" \
4268
+ --arg content "$pw_content" \
4269
+ --arg source "user:redirect" \
4270
+ --arg created_at "$pw_created" \
4271
+ '{id: $id, type: "AVOID", content: $content, source: $source, created_at: $created_at}')
4272
+ pw_cfile_updated=$(jq --argjson c "$pw_constraint" '
4273
+ .constraints += [$c] |
4274
+ if (.constraints | length) > 10 then .constraints = .constraints[-10:] else . end
4275
+ ' "$pw_cfile" 2>/dev/null)
4276
+ [[ -n "$pw_cfile_updated" ]] && echo "$pw_cfile_updated" > "$pw_cfile"
4277
+ fi
4278
+
4279
+ # Get active signal count
4280
+ pw_active_count=$(jq '[.signals[] | select(.active == true)] | length' "$pw_file" 2>/dev/null || echo "0")
4281
+
4282
+ json_ok "{\"signal_id\":\"$pw_id\",\"type\":\"$pw_type\",\"active_count\":$pw_active_count}"
4283
+ ;;
4284
+
4285
+ pheromone-count)
4286
+ # Count active pheromone signals by type
4287
+ # Usage: pheromone-count
4288
+ # Returns: JSON with per-type counts
4289
+
4290
+ pc_file="$DATA_DIR/pheromones.json"
4291
+
4292
+ if [[ ! -f "$pc_file" ]]; then
4293
+ json_ok '{"focus":0,"redirect":0,"feedback":0,"total":0}'
4294
+ else
4295
+ pc_result=$(jq -c '{
4296
+ focus: ([.signals[] | select(.active == true and .type == "FOCUS")] | length),
4297
+ redirect: ([.signals[] | select(.active == true and .type == "REDIRECT")] | length),
4298
+ feedback: ([.signals[] | select(.active == true and .type == "FEEDBACK")] | length),
4299
+ total: ([.signals[] | select(.active == true)] | length)
4300
+ }' "$pc_file" 2>/dev/null)
4301
+ if [[ -z "$pc_result" ]]; then
4302
+ json_ok '{"focus":0,"redirect":0,"feedback":0,"total":0}'
4303
+ else
4304
+ json_ok "$pc_result"
4305
+ fi
4306
+ fi
4307
+ ;;
4308
+
4309
+ pheromone-read)
4310
+ # Read pheromones from colony data with decay calculation
4311
+ # Usage: pheromone-read [type]
4312
+ # type: Filter by pheromone type (focus, redirect, feedback) or 'all' (default: all)
4313
+ # Returns: JSON object with pheromones array including effective_strength
4314
+
4315
+ pher_type="${1:-all}"
4316
+ pher_file="$DATA_DIR/pheromones.json"
4317
+
4318
+ # Check if file exists
4319
+ if [[ ! -f "$pher_file" ]]; then
4320
+ json_err "$E_FILE_NOT_FOUND" "Pheromones file not found. Run /ant:colonize first to initialize the colony."
4321
+ fi
4322
+
4323
+ # Get current epoch for decay calculation
4324
+ pher_now=$(date +%s)
4325
+
4326
+ # Apply decay and expiry at read time
4327
+ # Decay rates: FOCUS=30d, REDIRECT=60d, FEEDBACK/PATTERN=90d
4328
+ # effective_strength = original_strength * (1 - elapsed_days / decay_days)
4329
+ # If effective_strength < 0.1, mark inactive
4330
+ # Also check expires_at: if not "phase_end" and past expiry, mark inactive
4331
+ pher_type_upper=$(echo "$pher_type" | tr '[:lower:]' '[:upper:]')
4332
+
4333
+ pher_result=$(jq -c \
4334
+ --argjson now "$pher_now" \
4335
+ --arg type_filter "$pher_type_upper" \
4336
+ '
4337
+ # Rough ISO-8601 to epoch: accumulate years*365d + month*30d + days + time
4338
+ def to_epoch(ts):
4339
+ if ts == null or ts == "" or ts == "phase_end" then null
4340
+ else
4341
+ (ts | split("T")) as $parts |
4342
+ ($parts[0] | split("-")) as $d |
4343
+ ($parts[1] | rtrimstr("Z") | split(":")) as $t |
4344
+ (($d[0] | tonumber) - 1970) * 365 * 86400 +
4345
+ (($d[1] | tonumber) - 1) * 30 * 86400 +
4346
+ (($d[2] | tonumber) - 1) * 86400 +
4347
+ ($t[0] | tonumber) * 3600 +
4348
+ ($t[1] | tonumber) * 60 +
4349
+ ($t[2] | rtrimstr("Z") | tonumber)
4350
+ end;
4351
+
4352
+ def decay_days(t):
4353
+ if t == "FOCUS" then 30
4354
+ elif t == "REDIRECT" then 60
4355
+ else 90
4356
+ end;
4357
+
4358
+ .signals | map(
4359
+ (to_epoch(.created_at)) as $created_epoch |
4360
+ (if $created_epoch != null then ($now - $created_epoch) / 86400 else 0 end) as $elapsed_days |
4361
+ (decay_days(.type)) as $dd |
4362
+ ((.strength // 0.8) * (1 - ($elapsed_days / $dd))) as $eff_raw |
4363
+ (if $eff_raw < 0 then 0 else $eff_raw end) as $eff |
4364
+ (to_epoch(.expires_at)) as $exp_epoch |
4365
+ ($exp_epoch != null and $exp_epoch <= $now) as $expired |
4366
+ ($eff < 0.1 or $expired) as $deactivate |
4367
+ . + {
4368
+ effective_strength: (($eff * 100 | round) / 100),
4369
+ active: (if $deactivate then false else (.active // true) end)
4370
+ }
4371
+ ) |
4372
+ map(select(.active == true)) |
4373
+ if $type_filter != "ALL" then
4374
+ map(select(.type == $type_filter))
4375
+ else
4376
+ .
4377
+ end
4378
+ ' "$pher_file" 2>/dev/null)
4379
+
4380
+ if [[ -z "$pher_result" || "$pher_result" == "null" ]]; then
4381
+ json_ok '{"version":"1.0.0","signals":[]}'
4382
+ else
4383
+ pher_version=$(jq -r '.version // "1.0.0"' "$pher_file" 2>/dev/null || echo "1.0.0")
4384
+ pher_colony=$(jq -r '.colony_id // "unknown"' "$pher_file" 2>/dev/null || echo "unknown")
4385
+ json_ok "{\"version\":\"$pher_version\",\"colony_id\":\"$pher_colony\",\"signals\":$pher_result}"
4386
+ fi
4387
+ ;;
4388
+
4389
+ instinct-read)
4390
+ # Read learned instincts from COLONY_STATE.json memory
4391
+ # Usage: instinct-read [--min-confidence N] [--max N] [--domain DOMAIN]
4392
+ # Returns: JSON with filtered, confidence-sorted instincts
4393
+
4394
+ ir_min_confidence="0.5"
4395
+ ir_max="5"
4396
+ ir_domain=""
4397
+
4398
+ # Parse flags from positional args
4399
+ ir_shift=1
4400
+ while [[ $ir_shift -le $# ]]; do
4401
+ eval "ir_arg=\${$ir_shift}"
4402
+ ir_shift=$((ir_shift + 1))
4403
+ case "$ir_arg" in
4404
+ --min-confidence)
4405
+ eval "ir_min_confidence=\${$ir_shift}"
4406
+ ir_shift=$((ir_shift + 1))
4407
+ ;;
4408
+ --max)
4409
+ eval "ir_max=\${$ir_shift}"
4410
+ ir_shift=$((ir_shift + 1))
4411
+ ;;
4412
+ --domain)
4413
+ eval "ir_domain=\${$ir_shift}"
4414
+ ir_shift=$((ir_shift + 1))
4415
+ ;;
4416
+ esac
4417
+ done
4418
+
4419
+ ir_state_file="$DATA_DIR/COLONY_STATE.json"
4420
+
4421
+ if [[ ! -f "$ir_state_file" ]]; then
4422
+ json_err "$E_FILE_NOT_FOUND" "COLONY_STATE.json not found. Run /ant:init first."
4423
+ fi
4424
+
4425
+ # Check if memory.instincts exists
4426
+ ir_has_instincts=$(jq 'if .memory.instincts then "yes" else "no" end' "$ir_state_file" 2>/dev/null || echo "no")
4427
+ if [[ "$ir_has_instincts" != '"yes"' ]]; then
4428
+ json_ok '{"instincts":[],"total":0,"filtered":0}'
4429
+ fi
4430
+
4431
+ ir_result=$(jq -c \
4432
+ --argjson min_conf "$ir_min_confidence" \
4433
+ --argjson max_count "$ir_max" \
4434
+ --arg domain_filter "$ir_domain" \
4435
+ '
4436
+ (.memory.instincts // []) as $all |
4437
+ ($all | length) as $total |
4438
+ $all
4439
+ | map(select(
4440
+ (.confidence // 0) >= $min_conf
4441
+ and (.status // "hypothesis") != "disproven"
4442
+ and (if $domain_filter != "" then (.domain // "") == $domain_filter else true end)
4443
+ ))
4444
+ | sort_by(-.confidence)
4445
+ | .[:$max_count]
4446
+ | {
4447
+ instincts: .,
4448
+ total: $total,
4449
+ filtered: (. | length)
4450
+ }
4451
+ ' "$ir_state_file" 2>/dev/null)
4452
+
4453
+ if [[ -z "$ir_result" || "$ir_result" == "null" ]]; then
4454
+ json_ok '{"instincts":[],"total":0,"filtered":0}'
4455
+ else
4456
+ json_ok "$ir_result"
4457
+ fi
4458
+ ;;
4459
+
4460
+ pheromone-prime)
4461
+ # Combine active pheromone signals and learned instincts into a prompt-ready block
4462
+ # Usage: pheromone-prime
4463
+ # Returns: JSON with signal_count, instinct_count, prompt_section, log_line
4464
+
4465
+ pp_pher_file="$DATA_DIR/pheromones.json"
4466
+ pp_state_file="$DATA_DIR/COLONY_STATE.json"
4467
+ pp_now=$(date +%s)
4468
+
4469
+ # Read active signals (same decay logic as pheromone-read)
4470
+ pp_signals="[]"
4471
+ if [[ -f "$pp_pher_file" ]]; then
4472
+ pp_signals=$(jq -c \
4473
+ --argjson now "$pp_now" \
4474
+ '
4475
+ def to_epoch(ts):
4476
+ if ts == null or ts == "" or ts == "phase_end" then null
4477
+ else
4478
+ (ts | split("T")) as $parts |
4479
+ ($parts[0] | split("-")) as $d |
4480
+ ($parts[1] | rtrimstr("Z") | split(":")) as $t |
4481
+ (($d[0] | tonumber) - 1970) * 365 * 86400 +
4482
+ (($d[1] | tonumber) - 1) * 30 * 86400 +
4483
+ (($d[2] | tonumber) - 1) * 86400 +
4484
+ ($t[0] | tonumber) * 3600 +
4485
+ ($t[1] | tonumber) * 60 +
4486
+ ($t[2] | rtrimstr("Z") | tonumber)
4487
+ end;
4488
+
4489
+ def decay_days(t):
4490
+ if t == "FOCUS" then 30
4491
+ elif t == "REDIRECT" then 60
4492
+ else 90
4493
+ end;
4494
+
4495
+ .signals | map(
4496
+ (to_epoch(.created_at)) as $created_epoch |
4497
+ (if $created_epoch != null then ($now - $created_epoch) / 86400 else 0 end) as $elapsed_days |
4498
+ (decay_days(.type)) as $dd |
4499
+ ((.strength // 0.8) * (1 - ($elapsed_days / $dd))) as $eff_raw |
4500
+ (if $eff_raw < 0 then 0 else $eff_raw end) as $eff |
4501
+ (to_epoch(.expires_at)) as $exp_epoch |
4502
+ ($exp_epoch != null and $exp_epoch <= $now) as $expired |
4503
+ ($eff < 0.1 or $expired) as $deactivate |
4504
+ . + {
4505
+ effective_strength: (($eff * 100 | round) / 100),
4506
+ active: (if $deactivate then false else (.active // true) end)
4507
+ }
4508
+ ) |
4509
+ map(select(.active == true))
4510
+ ' "$pp_pher_file" 2>/dev/null || echo "[]")
4511
+ fi
4512
+
4513
+ if [[ -z "$pp_signals" || "$pp_signals" == "null" ]]; then
4514
+ pp_signals="[]"
4515
+ fi
4516
+
4517
+ # Read instincts (confidence >= 0.5, not disproven, max 5)
4518
+ pp_instincts="[]"
4519
+ if [[ -f "$pp_state_file" ]]; then
4520
+ pp_instincts=$(jq -c \
4521
+ '
4522
+ (.memory.instincts // [])
4523
+ | map(select(
4524
+ (.confidence // 0) >= 0.5
4525
+ and (.status // "hypothesis") != "disproven"
4526
+ ))
4527
+ | sort_by(-.confidence)
4528
+ | .[:5]
4529
+ ' "$pp_state_file" 2>/dev/null || echo "[]")
4530
+ fi
4531
+
4532
+ if [[ -z "$pp_instincts" || "$pp_instincts" == "null" ]]; then
4533
+ pp_instincts="[]"
4534
+ fi
4535
+
4536
+ pp_signal_count=$(echo "$pp_signals" | jq 'length' 2>/dev/null || echo "0")
4537
+ pp_instinct_count=$(echo "$pp_instincts" | jq 'length' 2>/dev/null || echo "0")
4538
+
4539
+ # Build prompt section
4540
+ if [[ "$pp_signal_count" -eq 0 && "$pp_instinct_count" -eq 0 ]]; then
4541
+ pp_section=""
4542
+ pp_log_line="Primed: 0 signals, 0 instincts"
4543
+ else
4544
+ pp_section="--- ACTIVE SIGNALS (Colony Guidance) ---"$'\n'
4545
+
4546
+ # FOCUS signals
4547
+ pp_focus=$(echo "$pp_signals" | jq -r 'map(select(.type == "FOCUS")) | .[] | "[" + ((.effective_strength * 10 | round) / 10 | tostring) + "] " + (.content.text // (if (.content | type) == "string" then .content else "" end))' 2>/dev/null || echo "")
4548
+ if [[ -n "$pp_focus" ]]; then
4549
+ pp_section+=$'\n'"FOCUS (Pay attention to):"$'\n'"$pp_focus"$'\n'
4550
+ fi
4551
+
4552
+ # REDIRECT signals
4553
+ pp_redirect=$(echo "$pp_signals" | jq -r 'map(select(.type == "REDIRECT")) | .[] | "[" + ((.effective_strength * 10 | round) / 10 | tostring) + "] " + (.content.text // (if (.content | type) == "string" then .content else "" end))' 2>/dev/null || echo "")
4554
+ if [[ -n "$pp_redirect" ]]; then
4555
+ pp_section+=$'\n'"REDIRECT (HARD CONSTRAINTS - MUST follow):"$'\n'"$pp_redirect"$'\n'
4556
+ fi
4557
+
4558
+ # FEEDBACK signals
4559
+ pp_feedback=$(echo "$pp_signals" | jq -r 'map(select(.type == "FEEDBACK")) | .[] | "[" + ((.effective_strength * 10 | round) / 10 | tostring) + "] " + (.content.text // (if (.content | type) == "string" then .content else "" end))' 2>/dev/null || echo "")
4560
+ if [[ -n "$pp_feedback" ]]; then
4561
+ pp_section+=$'\n'"FEEDBACK (Flexible guidance):"$'\n'"$pp_feedback"$'\n'
4562
+ fi
4563
+
4564
+ # Instincts section
4565
+ if [[ "$pp_instinct_count" -gt 0 ]]; then
4566
+ pp_section+=$'\n'"--- INSTINCTS (Learned Behaviors) ---"$'\n'
4567
+ pp_section+="Weight by confidence - higher = stronger guidance:"$'\n'
4568
+ pp_instinct_lines=$(echo "$pp_instincts" | jq -r '.[] | "[" + ((.confidence * 10 | round) / 10 | tostring) + "] When " + .trigger + " -> " + .action + " (" + (.domain // "general") + ")"' 2>/dev/null || echo "")
4569
+ if [[ -n "$pp_instinct_lines" ]]; then
4570
+ pp_section+=$'\n'"$pp_instinct_lines"$'\n'
4571
+ fi
4572
+ fi
4573
+
4574
+ pp_section+=$'\n'"--- END COLONY CONTEXT ---"
4575
+
4576
+ pp_log_line="Primed: ${pp_signal_count} signals, ${pp_instinct_count} instincts"
4577
+ fi
4578
+
4579
+ # Escape section for JSON embedding (use printf to avoid appending extra newline)
4580
+ pp_section_json=$(printf '%s' "$pp_section" | jq -Rs '.' 2>/dev/null || echo '""')
4581
+ pp_log_json=$(printf '%s' "$pp_log_line" | jq -Rs '.' 2>/dev/null || echo '"Primed: 0 signals, 0 instincts"')
4582
+
4583
+ json_ok "{\"signal_count\":$pp_signal_count,\"instinct_count\":$pp_instinct_count,\"prompt_section\":$pp_section_json,\"log_line\":$pp_log_json}"
4584
+ ;;
4585
+
4586
+ pheromone-expire)
4587
+ # Archive expired pheromone signals to midden
4588
+ # Usage: pheromone-expire [--phase-end-only]
4589
+ #
4590
+ # Two modes:
4591
+ # --phase-end-only Only expire signals where expires_at == "phase_end"
4592
+ # (no flag) Expire signals where expires_at is an ISO-8601 timestamp
4593
+ # <= now, AND signals where effective_strength < 0.1
4594
+
4595
+ phe_phase_end_only="false"
4596
+ while [[ $# -gt 0 ]]; do
4597
+ case "$1" in
4598
+ --phase-end-only) phe_phase_end_only="true"; shift ;;
4599
+ *) shift ;;
4600
+ esac
4601
+ done
4602
+
4603
+ phe_pheromones_file="$DATA_DIR/pheromones.json"
4604
+ phe_midden_dir="$DATA_DIR/midden"
4605
+ phe_midden_file="$phe_midden_dir/midden.json"
4606
+
4607
+ # Handle missing pheromones.json gracefully
4608
+ if [[ ! -f "$phe_pheromones_file" ]]; then
4609
+ json_ok '{"expired_count":0,"remaining_active":0,"midden_total":0}'
4610
+ exit 0
4611
+ fi
4612
+
4613
+ # Ensure midden directory and file exist
4614
+ mkdir -p "$phe_midden_dir"
4615
+ if [[ ! -f "$phe_midden_file" ]]; then
4616
+ printf '%s\n' '{"version":"1.0.0","archived_at_count":0,"signals":[]}' > "$phe_midden_file"
4617
+ fi
4618
+
4619
+ phe_now_epoch=$(date +%s)
4620
+ phe_archived_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
4621
+
4622
+ # Compute pause_duration from COLONY_STATE.json (pause-aware TTL)
4623
+ phe_pause_duration=0
4624
+ if [[ -f "$DATA_DIR/COLONY_STATE.json" ]]; then
4625
+ phe_paused_at=$(jq -r '.paused_at // empty' "$DATA_DIR/COLONY_STATE.json" 2>/dev/null || true)
4626
+ phe_resumed_at=$(jq -r '.resumed_at // empty' "$DATA_DIR/COLONY_STATE.json" 2>/dev/null || true)
4627
+ if [[ -n "$phe_paused_at" && -n "$phe_resumed_at" ]]; then
4628
+ phe_paused_epoch=$(date -j -f "%Y-%m-%dT%H:%M:%SZ" "$phe_paused_at" +%s 2>/dev/null || date -d "$phe_paused_at" +%s 2>/dev/null || echo 0)
4629
+ phe_resumed_epoch=$(date -j -f "%Y-%m-%dT%H:%M:%SZ" "$phe_resumed_at" +%s 2>/dev/null || date -d "$phe_resumed_at" +%s 2>/dev/null || echo 0)
4630
+ if [[ "$phe_resumed_epoch" -gt "$phe_paused_epoch" ]]; then
4631
+ phe_pause_duration=$(( phe_resumed_epoch - phe_paused_epoch ))
4632
+ fi
4633
+ fi
4634
+ fi
4635
+
4636
+ # Identify expired signal IDs
4637
+ # We'll use jq to find signals to expire, then update in bash
4638
+ if [[ "$phe_phase_end_only" == "true" ]]; then
4639
+ # Only expire signals where expires_at == "phase_end"
4640
+ phe_expired_ids=$(jq -r '.signals[] | select(.active == true and .expires_at == "phase_end") | .id' "$phe_pheromones_file" 2>/dev/null || true)
4641
+ else
4642
+ # Expire time-based expired signals (pause-aware) AND decay-expired signals
4643
+ phe_expired_ids=$(jq -r --argjson now "$phe_now_epoch" --argjson pause_secs "$phe_pause_duration" '
4644
+ .signals[] |
4645
+ select(.active == true) |
4646
+ select(
4647
+ (.expires_at != "phase_end" and .expires_at != null and .expires_at != "") and
4648
+ (
4649
+ # ISO-8601 timestamp expiry (pause-aware: add pause_duration to expires_at before comparing)
4650
+ (
4651
+ .expires_at |
4652
+ # Convert ISO-8601 to approximate epoch via string parsing
4653
+ (
4654
+ (split("T")[0] | split("-")) as $d |
4655
+ (split("T")[1] | split(":")) as $t |
4656
+ ($d[0] | tonumber) as $y |
4657
+ ($d[1] | tonumber) as $mo |
4658
+ ($d[2] | tonumber) as $day |
4659
+ ($t[0] | tonumber) as $h |
4660
+ ($t[1] | tonumber) as $m |
4661
+ (($t[2] // "0") | gsub("[^0-9]";"") | if . == "" then 0 else tonumber end) as $s |
4662
+ # Rough epoch: years*365.25*86400 + months*30.44*86400 + day*86400 + time
4663
+ (($y - 1970) * 31557600) + (($mo - 1) * 2629800) + (($day - 1) * 86400) + ($h * 3600) + ($m * 60) + $s
4664
+ )
4665
+ ) + $pause_secs <= $now
4666
+ )
4667
+ ) |
4668
+ .id
4669
+ ' "$phe_pheromones_file" 2>/dev/null || true)
4670
+ fi
4671
+
4672
+ # Count expired signals
4673
+ phe_expired_count=0
4674
+ if [[ -n "$phe_expired_ids" ]]; then
4675
+ phe_expired_count=$(echo "$phe_expired_ids" | grep -c . 2>/dev/null || echo 0)
4676
+ fi
4677
+
4678
+ # If nothing to expire, return counts
4679
+ if [[ "$phe_expired_count" -eq 0 ]]; then
4680
+ phe_remaining=$(jq '[.signals[] | select(.active == true)] | length' "$phe_pheromones_file" 2>/dev/null || echo 0)
4681
+ phe_midden_total=$(jq '.signals | length' "$phe_midden_file" 2>/dev/null || echo 0)
4682
+ json_ok "{\"expired_count\":0,\"remaining_active\":$phe_remaining,\"midden_total\":$phe_midden_total}"
4683
+ exit 0
4684
+ fi
4685
+
4686
+ # Build jq args for IDs to expire
4687
+ phe_id_array=$(echo "$phe_expired_ids" | jq -R . | jq -s . 2>/dev/null || echo '[]')
4688
+
4689
+ # Extract expired signal objects (with archived_at added)
4690
+ phe_expired_objects=$(jq --argjson ids "$phe_id_array" --arg archived_at "$phe_archived_at" '
4691
+ [.signals[] | select(.id as $id | $ids | any(. == $id)) | . + {"archived_at": $archived_at, "active": false}]
4692
+ ' "$phe_pheromones_file" 2>/dev/null || echo '[]')
4693
+
4694
+ # Update pheromones.json: set active=false for expired signals (do NOT remove them)
4695
+ phe_updated_pheromones=$(jq --argjson ids "$phe_id_array" '
4696
+ .signals = [.signals[] | if (.id as $id | $ids | any(. == $id)) then .active = false else . end]
4697
+ ' "$phe_pheromones_file" 2>/dev/null)
4698
+
4699
+ if [[ -n "$phe_updated_pheromones" ]]; then
4700
+ printf '%s\n' "$phe_updated_pheromones" > "$phe_pheromones_file"
4701
+ fi
4702
+
4703
+ # Append expired signals to midden.json
4704
+ phe_midden_updated=$(jq --argjson new_signals "$phe_expired_objects" '
4705
+ .signals += $new_signals |
4706
+ .archived_at_count = (.signals | length)
4707
+ ' "$phe_midden_file" 2>/dev/null)
4708
+
4709
+ if [[ -n "$phe_midden_updated" ]]; then
4710
+ printf '%s\n' "$phe_midden_updated" > "$phe_midden_file"
4711
+ fi
4712
+
4713
+ phe_remaining_active=$(jq '[.signals[] | select(.active == true)] | length' "$phe_pheromones_file" 2>/dev/null || echo 0)
4714
+ phe_midden_total=$(jq '.signals | length' "$phe_midden_file" 2>/dev/null || echo 0)
4715
+
4716
+ json_ok "{\"expired_count\":$phe_expired_count,\"remaining_active\":$phe_remaining_active,\"midden_total\":$phe_midden_total}"
4717
+ ;;
4718
+
4719
+ eternal-init)
4720
+ # Initialize the ~/.aether/eternal/ directory and memory.json schema
4721
+ # Usage: eternal-init
4722
+ # Idempotent: safe to call multiple times
4723
+
4724
+ ei_eternal_dir="$HOME/.aether/eternal"
4725
+ ei_memory_file="$ei_eternal_dir/memory.json"
4726
+ ei_already_existed="false"
4727
+
4728
+ mkdir -p "$ei_eternal_dir"
4729
+
4730
+ if [[ -f "$ei_memory_file" ]]; then
4731
+ ei_already_existed="true"
4732
+ else
4733
+ ei_created_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
4734
+ printf '%s\n' "{
4735
+ \"version\": \"1.0.0\",
4736
+ \"created_at\": \"$ei_created_at\",
4737
+ \"colonies\": [],
4738
+ \"high_value_signals\": [],
4739
+ \"cross_session_patterns\": []
4740
+ }" > "$ei_memory_file"
4741
+ fi
4742
+
4743
+ json_ok "{\"dir\":\"$ei_eternal_dir\",\"initialized\":true,\"already_existed\":$ei_already_existed}"
4744
+ ;;
4745
+
4746
+ # ============================================================================
4747
+ # XML Exchange Commands
4748
+ # ============================================================================
4749
+
4750
+ pheromone-export-xml)
4751
+ # Export pheromones.json to XML format
4752
+ # Usage: pheromone-export-xml [output_file]
4753
+ # Default output: .aether/exchange/pheromones.xml
4754
+
4755
+ pex_output="${1:-$SCRIPT_DIR/exchange/pheromones.xml}"
4756
+ pex_pheromones="$DATA_DIR/pheromones.json"
4757
+
4758
+ # Graceful degradation: check for xmllint
4759
+ if ! command -v xmllint >/dev/null 2>&1; then
4760
+ json_err "$E_FEATURE_UNAVAILABLE" "xmllint is not installed. Try: xcode-select --install on macOS."
4761
+ fi
4762
+
4763
+ # Check pheromones.json exists
4764
+ if [[ ! -f "$pex_pheromones" ]]; then
4765
+ json_err "$E_FILE_NOT_FOUND" "Couldn't find pheromones.json. Try: run /ant:init first."
4766
+ fi
4767
+
4768
+ # Ensure output directory exists
4769
+ mkdir -p "$(dirname "$pex_output")"
4770
+
4771
+ # Source the exchange script
4772
+ source "$SCRIPT_DIR/exchange/pheromone-xml.sh"
4773
+
4774
+ # Call the export function
4775
+ xml-pheromone-export "$pex_pheromones" "$pex_output"
4776
+ ;;
4777
+
4778
+ pheromone-import-xml)
4779
+ # Import pheromone signals from XML into pheromones.json
4780
+ # Usage: pheromone-import-xml <xml_file> [colony_prefix]
4781
+ # When colony_prefix is provided, imported signal IDs are tagged with "${prefix}:" before merge
4782
+
4783
+ pix_xml="${1:-}"
4784
+ pix_colony_prefix="${2:-}"
4785
+ pix_pheromones="$DATA_DIR/pheromones.json"
4786
+
4787
+ if [[ -z "$pix_xml" ]]; then
4788
+ json_err "$E_VALIDATION_FAILED" "Missing XML file argument. Try: pheromone-import-xml <xml_file> [colony_prefix]."
4789
+ fi
4790
+
4791
+ if [[ ! -f "$pix_xml" ]]; then
4792
+ json_err "$E_FILE_NOT_FOUND" "XML file not found: $pix_xml. Try: check the file path."
4793
+ fi
4794
+
4795
+ # Graceful degradation: check for xmllint
4796
+ if ! command -v xmllint >/dev/null 2>&1; then
4797
+ json_err "$E_FEATURE_UNAVAILABLE" "xmllint is not installed. Try: xcode-select --install on macOS."
4798
+ fi
4799
+
4800
+ # Source the exchange script
4801
+ source "$SCRIPT_DIR/exchange/pheromone-xml.sh"
4802
+
4803
+ # Import XML to get JSON signals
4804
+ pix_imported=$(xml-pheromone-import "$pix_xml")
4805
+
4806
+ # Extract actual signal array from result.json | fromjson | .signals
4807
+ # (result.signals is an integer count — must unpack result.json to get the array)
4808
+ pix_raw_signals=$(echo "$pix_imported" | jq -r '.result.json // "{}"' | jq -c '.signals // []' 2>/dev/null || echo '[]')
4809
+
4810
+ # Apply colony prefix to imported signal IDs (when provided)
4811
+ # This prevents ID collisions and tags signals with their source colony
4812
+ if [[ -n "$pix_colony_prefix" ]]; then
4813
+ pix_prefixed_signals=$(echo "$pix_raw_signals" | jq --arg prefix "$pix_colony_prefix" '[.[] | .id = ($prefix + ":" + .id)]' 2>/dev/null || echo '[]')
4814
+ else
4815
+ pix_prefixed_signals="$pix_raw_signals"
4816
+ fi
4817
+
4818
+ # If pheromones.json exists, merge; otherwise create
4819
+ if [[ -f "$pix_pheromones" ]]; then
4820
+ # Merge: imported signals first, existing signals last
4821
+ # map(last) keeps current colony's version on ID collision — current colony always wins
4822
+ pix_merged=$(jq -s --argjson new_signals "$pix_prefixed_signals" '
4823
+ .[0] as $existing |
4824
+ {
4825
+ signals: ([$new_signals[], $existing.signals[]] | group_by(.id) | map(last)),
4826
+ version: $existing.version,
4827
+ colony_id: $existing.colony_id
4828
+ }
4829
+ ' "$pix_pheromones" 2>/dev/null)
4830
+
4831
+ if [[ -n "$pix_merged" ]]; then
4832
+ printf '%s\n' "$pix_merged" > "$pix_pheromones"
4833
+ fi
4834
+ fi
4835
+
4836
+ pix_count=$(echo "$pix_raw_signals" | jq 'length' 2>/dev/null || echo 0)
4837
+ json_ok "{\"imported\":true,\"signal_count\":$pix_count,\"source\":\"$pix_xml\"}"
4838
+ ;;
4839
+
4840
+ pheromone-validate-xml)
4841
+ # Validate pheromone XML against XSD schema
4842
+ # Usage: pheromone-validate-xml <xml_file>
4843
+
4844
+ pvx_xml="${1:-}"
4845
+ pvx_xsd="$SCRIPT_DIR/schemas/pheromone.xsd"
4846
+
4847
+ if [[ -z "$pvx_xml" ]]; then
4848
+ json_err "$E_VALIDATION_FAILED" "Missing XML file argument. Try: pheromone-validate-xml <xml_file>."
4849
+ fi
4850
+
4851
+ if [[ ! -f "$pvx_xml" ]]; then
4852
+ json_err "$E_FILE_NOT_FOUND" "XML file not found: $pvx_xml. Try: check the file path."
4853
+ fi
4854
+
4855
+ # Graceful degradation: check for xmllint
4856
+ if ! command -v xmllint >/dev/null 2>&1; then
4857
+ json_err "$E_FEATURE_UNAVAILABLE" "xmllint is not installed. Try: xcode-select --install on macOS."
4858
+ fi
4859
+
4860
+ # Source the exchange script
4861
+ source "$SCRIPT_DIR/exchange/pheromone-xml.sh"
4862
+
4863
+ # Call validate function
4864
+ xml-pheromone-validate "$pvx_xml" "$pvx_xsd"
4865
+ ;;
4866
+
4867
+ wisdom-export-xml)
4868
+ # Export queen wisdom to XML format
4869
+ # Usage: wisdom-export-xml [input_json] [output_xml]
4870
+ # Default input: .aether/data/queen-wisdom.json
4871
+ # Default output: .aether/exchange/queen-wisdom.xml
4872
+
4873
+ wex_input="${1:-$DATA_DIR/queen-wisdom.json}"
4874
+ wex_output="${2:-$SCRIPT_DIR/exchange/queen-wisdom.xml}"
4875
+
4876
+ # Graceful degradation: check for xmllint
4877
+ if ! command -v xmllint >/dev/null 2>&1; then
4878
+ json_err "$E_FEATURE_UNAVAILABLE" "xmllint is not installed. Try: xcode-select --install on macOS."
4879
+ fi
4880
+
4881
+ # Look for wisdom data: check specified file, then COLONY_STATE memory
4882
+ if [[ ! -f "$wex_input" ]]; then
4883
+ # Try to extract from COLONY_STATE.json memory field
4884
+ if [[ -f "$DATA_DIR/COLONY_STATE.json" ]]; then
4885
+ wex_memory=$(jq '.memory // {}' "$DATA_DIR/COLONY_STATE.json" 2>/dev/null || echo '{}')
4886
+ if [[ "$wex_memory" != "{}" && "$wex_memory" != "null" ]]; then
4887
+ # Create minimal wisdom JSON from colony memory
4888
+ wex_created_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
4889
+ printf '%s\n' "{
4890
+ \"version\": \"1.0.0\",
4891
+ \"metadata\": {\"created\": \"$wex_created_at\", \"colony_id\": \"$(jq -r '.goal // \"unknown\"' "$DATA_DIR/COLONY_STATE.json" 2>/dev/null)\"},
4892
+ \"philosophies\": [],
4893
+ \"patterns\": $(echo "$wex_memory" | jq '[.instincts // [] | .[] | {\"id\": (. | @base64), \"content\": ., \"confidence\": 0.7, \"domain\": \"general\", \"source\": \"colony_memory\"}]' 2>/dev/null || echo '[]')
4894
+ }" > "$wex_input"
4895
+ fi
4896
+ fi
4897
+ fi
4898
+
4899
+ # If still no wisdom data, create minimal skeleton
4900
+ if [[ ! -f "$wex_input" ]]; then
4901
+ wex_created_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
4902
+ mkdir -p "$(dirname "$wex_input")"
4903
+ printf '%s\n' "{
4904
+ \"version\": \"1.0.0\",
4905
+ \"metadata\": {\"created\": \"$wex_created_at\", \"colony_id\": \"unknown\"},
4906
+ \"philosophies\": [],
4907
+ \"patterns\": []
4908
+ }" > "$wex_input"
4909
+ fi
4910
+
4911
+ # Ensure output directory exists
4912
+ mkdir -p "$(dirname "$wex_output")"
4913
+
4914
+ # Source the exchange script
4915
+ source "$SCRIPT_DIR/exchange/wisdom-xml.sh"
4916
+
4917
+ # Call the export function
4918
+ xml-wisdom-export "$wex_input" "$wex_output"
4919
+ ;;
4920
+
4921
+ wisdom-import-xml)
4922
+ # Import wisdom from XML into JSON format
4923
+ # Usage: wisdom-import-xml <xml_file> [output_json]
4924
+
4925
+ wix_xml="${1:-}"
4926
+ wix_output="${2:-$DATA_DIR/queen-wisdom.json}"
4927
+
4928
+ if [[ -z "$wix_xml" ]]; then
4929
+ json_err "$E_VALIDATION_FAILED" "Missing XML file argument. Try: wisdom-import-xml <xml_file> [output_json]."
4930
+ fi
4931
+
4932
+ if [[ ! -f "$wix_xml" ]]; then
4933
+ json_err "$E_FILE_NOT_FOUND" "XML file not found: $wix_xml. Try: check the file path."
4934
+ fi
4935
+
4936
+ # Graceful degradation: check for xmllint
4937
+ if ! command -v xmllint >/dev/null 2>&1; then
4938
+ json_err "$E_FEATURE_UNAVAILABLE" "xmllint is not installed. Try: xcode-select --install on macOS."
4939
+ fi
4940
+
4941
+ # Ensure output directory exists
4942
+ mkdir -p "$(dirname "$wix_output")"
4943
+
4944
+ # Source the exchange script
4945
+ source "$SCRIPT_DIR/exchange/wisdom-xml.sh"
4946
+
4947
+ # Call the import function
4948
+ xml-wisdom-import "$wix_xml" "$wix_output"
4949
+ ;;
4950
+
4951
+ registry-export-xml)
4952
+ # Export colony registry to XML format
4953
+ # Usage: registry-export-xml [input_json] [output_xml]
4954
+ # Default input: .aether/data/colony-registry.json
4955
+ # Default output: .aether/exchange/colony-registry.xml
4956
+
4957
+ rex_input="${1:-$DATA_DIR/colony-registry.json}"
4958
+ rex_output="${2:-$SCRIPT_DIR/exchange/colony-registry.xml}"
4959
+
4960
+ # Graceful degradation: check for xmllint
4961
+ if ! command -v xmllint >/dev/null 2>&1; then
4962
+ json_err "$E_FEATURE_UNAVAILABLE" "xmllint is not installed. Try: xcode-select --install on macOS."
4963
+ fi
4964
+
4965
+ # If no registry file exists, generate from chambers
4966
+ if [[ ! -f "$rex_input" ]]; then
4967
+ rex_chambers_dir="$AETHER_ROOT/.aether/chambers"
4968
+ rex_generated_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
4969
+ rex_colonies="[]"
4970
+
4971
+ if [[ -d "$rex_chambers_dir" ]]; then
4972
+ # Scan chambers for manifest.json files
4973
+ rex_colonies=$(
4974
+ for manifest in "$rex_chambers_dir"/*/manifest.json; do
4975
+ [[ -f "$manifest" ]] || continue
4976
+ jq -c '{
4977
+ id: (.colony_id // .goal // "unknown"),
4978
+ name: (.goal // "Unnamed Colony"),
4979
+ created_at: (.created_at // "unknown"),
4980
+ sealed_at: (.sealed_at // null),
4981
+ status: (if .sealed_at then "sealed" else "active" end),
4982
+ chamber: input_filename
4983
+ }' "$manifest" 2>/dev/null || true
4984
+ done | jq -s '.' 2>/dev/null || echo '[]'
4985
+ )
4986
+ fi
4987
+
4988
+ mkdir -p "$(dirname "$rex_input")"
4989
+ printf '%s\n' "{
4990
+ \"version\": \"1.0.0\",
4991
+ \"generated_at\": \"$rex_generated_at\",
4992
+ \"colonies\": $rex_colonies
4993
+ }" > "$rex_input"
4994
+ fi
4995
+
4996
+ # Ensure output directory exists
4997
+ mkdir -p "$(dirname "$rex_output")"
4998
+
4999
+ # Source the exchange script
5000
+ source "$SCRIPT_DIR/exchange/registry-xml.sh"
5001
+
5002
+ # Call the export function
5003
+ xml-registry-export "$rex_input" "$rex_output"
5004
+ ;;
5005
+
5006
+ registry-import-xml)
5007
+ # Import colony registry from XML into JSON format
5008
+ # Usage: registry-import-xml <xml_file> [output_json]
5009
+
5010
+ rix_xml="${1:-}"
5011
+ rix_output="${2:-$DATA_DIR/colony-registry.json}"
5012
+
5013
+ if [[ -z "$rix_xml" ]]; then
5014
+ json_err "$E_VALIDATION_FAILED" "Missing XML file argument. Try: registry-import-xml <xml_file> [output_json]."
5015
+ fi
5016
+
5017
+ if [[ ! -f "$rix_xml" ]]; then
5018
+ json_err "$E_FILE_NOT_FOUND" "XML file not found: $rix_xml. Try: check the file path."
5019
+ fi
5020
+
5021
+ # Graceful degradation: check for xmllint
5022
+ if ! command -v xmllint >/dev/null 2>&1; then
5023
+ json_err "$E_FEATURE_UNAVAILABLE" "xmllint is not installed. Try: xcode-select --install on macOS."
5024
+ fi
5025
+
5026
+ # Ensure output directory exists
5027
+ mkdir -p "$(dirname "$rix_output")"
5028
+
5029
+ # Source the exchange script
5030
+ source "$SCRIPT_DIR/exchange/registry-xml.sh"
5031
+
5032
+ # Call the import function
5033
+ xml-registry-import "$rix_xml" "$rix_output"
5034
+ ;;
5035
+
5036
+ colony-archive-xml)
5037
+ # Export combined colony archive XML containing pheromones, wisdom, and registry
5038
+ # Usage: colony-archive-xml [output_file]
5039
+ # Default output: .aether/exchange/colony-archive.xml
5040
+ # Always filters to active-only pheromone signals
5041
+
5042
+ # Graceful degradation: check for xmllint
5043
+ if ! command -v xmllint >/dev/null 2>&1; then
5044
+ json_err "$E_FEATURE_UNAVAILABLE" "xmllint is not installed. Try: xcode-select --install on macOS."
5045
+ fi
5046
+
5047
+ cax_output="${1:-$SCRIPT_DIR/exchange/colony-archive.xml}"
5048
+ mkdir -p "$(dirname "$cax_output")"
5049
+
5050
+ # Step 1: Filter active-only pheromone signals to a temp file
5051
+ cax_tmp_pheromones=$(mktemp)
5052
+ if [[ -f "$DATA_DIR/pheromones.json" ]]; then
5053
+ jq '{
5054
+ version: .version,
5055
+ colony_id: .colony_id,
5056
+ generated_at: .generated_at,
5057
+ signals: [.signals[] | select(.active == true)]
5058
+ }' "$DATA_DIR/pheromones.json" > "$cax_tmp_pheromones" 2>/dev/null
5059
+ else
5060
+ printf '%s\n' '{"version":"1.0","colony_id":"unknown","generated_at":"","signals":[]}' > "$cax_tmp_pheromones"
5061
+ fi
5062
+
5063
+ # Step 2: Export each section to temp XML files
5064
+ cax_tmp_dir=$(mktemp -d)
5065
+
5066
+ # Pheromone section (using filtered active-only)
5067
+ source "$SCRIPT_DIR/exchange/pheromone-xml.sh"
5068
+ xml-pheromone-export "$cax_tmp_pheromones" "$cax_tmp_dir/pheromones.xml" 2>/dev/null || true
5069
+
5070
+ # Wisdom section — reuse wisdom-export-xml fallback logic
5071
+ source "$SCRIPT_DIR/exchange/wisdom-xml.sh"
5072
+ cax_wisdom_input="$DATA_DIR/queen-wisdom.json"
5073
+ if [[ ! -f "$cax_wisdom_input" ]]; then
5074
+ # Try extracting from COLONY_STATE.json memory field
5075
+ if [[ -f "$DATA_DIR/COLONY_STATE.json" ]]; then
5076
+ cax_wex_memory=$(jq '.memory // {}' "$DATA_DIR/COLONY_STATE.json" 2>/dev/null || echo '{}')
5077
+ if [[ "$cax_wex_memory" != "{}" && "$cax_wex_memory" != "null" ]]; then
5078
+ cax_wex_created_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
5079
+ cax_wisdom_input="$cax_tmp_dir/wisdom-input.json"
5080
+ printf '%s\n' "{
5081
+ \"version\": \"1.0.0\",
5082
+ \"metadata\": {\"created\": \"$cax_wex_created_at\", \"colony_id\": \"$(jq -r '.goal // \"unknown\"' "$DATA_DIR/COLONY_STATE.json" 2>/dev/null)\"},
5083
+ \"philosophies\": [],
5084
+ \"patterns\": $(echo "$cax_wex_memory" | jq '[.instincts // [] | .[] | {"id": (. | @base64), "content": ., "confidence": 0.7, "domain": "general", "source": "colony_memory"}]' 2>/dev/null || echo '[]')
5085
+ }" > "$cax_wisdom_input"
5086
+ fi
5087
+ fi
5088
+ fi
5089
+ if [[ -f "$cax_wisdom_input" ]]; then
5090
+ xml-wisdom-export "$cax_wisdom_input" "$cax_tmp_dir/wisdom.xml" 2>/dev/null || true
5091
+ fi
5092
+
5093
+ # Registry section — reuse registry-export-xml on-demand generation logic
5094
+ source "$SCRIPT_DIR/exchange/registry-xml.sh"
5095
+ cax_registry_input="$DATA_DIR/colony-registry.json"
5096
+ if [[ ! -f "$cax_registry_input" ]]; then
5097
+ cax_rex_chambers_dir="$AETHER_ROOT/.aether/chambers"
5098
+ cax_rex_generated_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
5099
+ cax_rex_colonies="[]"
5100
+ if [[ -d "$cax_rex_chambers_dir" ]]; then
5101
+ cax_rex_colonies=$(
5102
+ for manifest in "$cax_rex_chambers_dir"/*/manifest.json; do
5103
+ [[ -f "$manifest" ]] || continue
5104
+ jq -c '{
5105
+ id: (.colony_id // .goal // "unknown"),
5106
+ name: (.goal // "Unnamed Colony"),
5107
+ created_at: (.created_at // "unknown"),
5108
+ sealed_at: (.sealed_at // null),
5109
+ status: (if .sealed_at then "sealed" else "active" end),
5110
+ chamber: input_filename
5111
+ }' "$manifest" 2>/dev/null || true
5112
+ done | jq -s '.' 2>/dev/null || echo '[]'
5113
+ )
5114
+ fi
5115
+ cax_registry_input="$cax_tmp_dir/registry-input.json"
5116
+ printf '%s\n' "{
5117
+ \"version\": \"1.0.0\",
5118
+ \"generated_at\": \"$cax_rex_generated_at\",
5119
+ \"colonies\": $cax_rex_colonies
5120
+ }" > "$cax_registry_input"
5121
+ fi
5122
+ xml-registry-export "$cax_registry_input" "$cax_tmp_dir/registry.xml" 2>/dev/null || true
5123
+
5124
+ # Step 3: Build combined XML
5125
+ cax_colony_id=$(jq -r '.goal // "unknown"' "$DATA_DIR/COLONY_STATE.json" 2>/dev/null | tr '[:upper:]' '[:lower:]' | tr -cs '[:alnum:]' '-' | sed 's/^-//;s/-$//')
5126
+ [[ -z "$cax_colony_id" || "$cax_colony_id" == "unknown" ]] && cax_colony_id="unknown"
5127
+ cax_sealed_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
5128
+ cax_pheromone_count=$(jq '.signals | length' "$cax_tmp_pheromones" 2>/dev/null || echo 0)
5129
+
5130
+ {
5131
+ printf '<?xml version="1.0" encoding="UTF-8"?>\n'
5132
+ printf '<colony-archive\n'
5133
+ printf ' xmlns="http://aether.colony/schemas/archive/1.0"\n'
5134
+ printf ' colony_id="%s"\n' "$cax_colony_id"
5135
+ printf ' sealed_at="%s"\n' "$cax_sealed_at"
5136
+ printf ' version="1.0.0"\n'
5137
+ printf ' pheromone_count="%s">\n' "$cax_pheromone_count"
5138
+
5139
+ # Append pheromone section (strip XML declaration)
5140
+ if [[ -f "$cax_tmp_dir/pheromones.xml" ]]; then
5141
+ sed '1{/^<?xml/d;}' "$cax_tmp_dir/pheromones.xml"
5142
+ fi
5143
+
5144
+ # Append wisdom section (strip XML declaration)
5145
+ if [[ -f "$cax_tmp_dir/wisdom.xml" ]]; then
5146
+ sed '1{/^<?xml/d;}' "$cax_tmp_dir/wisdom.xml"
5147
+ fi
5148
+
5149
+ # Append registry section (strip XML declaration)
5150
+ if [[ -f "$cax_tmp_dir/registry.xml" ]]; then
5151
+ sed '1{/^<?xml/d;}' "$cax_tmp_dir/registry.xml"
5152
+ fi
5153
+
5154
+ printf '</colony-archive>\n'
5155
+ } > "$cax_output"
5156
+
5157
+ # Step 4: Validate well-formedness
5158
+ if xmllint --noout "$cax_output" 2>/dev/null; then
5159
+ cax_valid=true
5160
+ else
5161
+ cax_valid=false
5162
+ fi
5163
+
5164
+ # Step 5: Cleanup temp files
5165
+ rm -rf "$cax_tmp_dir" "$cax_tmp_pheromones"
5166
+
5167
+ json_ok "{\"path\":\"$cax_output\",\"valid\":$cax_valid,\"colony_id\":\"$cax_colony_id\",\"pheromone_count\":$cax_pheromone_count}"
5168
+ ;;
5169
+
5170
+ # ============================================================================
5171
+ # Session Continuity Commands
5172
+ # ============================================================================
5173
+
5174
+ session-init)
5175
+ # Initialize a new session tracking file
5176
+ # Usage: session-init [session_id] [goal]
5177
+ session_id="${2:-$(date +%s)_$(openssl rand -hex 4 2>/dev/null || echo $$)}"
5178
+ goal="${3:-}"
5179
+
5180
+ # ARCH-03: Rotate spawn-tree.txt at session start to prevent unbounded growth.
5181
+ # Archives previous session's tree to a timestamped file; caps archive count at 5.
5182
+ _rotate_spawn_tree() {
5183
+ local tree_file="$DATA_DIR/spawn-tree.txt"
5184
+ [[ -f "$tree_file" ]] && [[ -s "$tree_file" ]] || return 0
5185
+ mkdir -p "$DATA_DIR/spawn-tree-archive"
5186
+ local archive_ts
5187
+ archive_ts=$(date +%Y%m%d_%H%M%S)
5188
+ cp "$tree_file" "$DATA_DIR/spawn-tree-archive/spawn-tree.${archive_ts}.txt" 2>/dev/null || true
5189
+ > "$tree_file" # Truncate in-place — preserves file handle for tail -f watchers
5190
+ # Keep only 5 archives
5191
+ ls -t "$DATA_DIR/spawn-tree-archive"/spawn-tree.*.txt 2>/dev/null \
5192
+ | tail -n +6 | xargs rm -f 2>/dev/null || true
5193
+ }
5194
+ _rotate_spawn_tree
5195
+
5196
+ session_file="$DATA_DIR/session.json"
5197
+ baseline=$(git rev-parse HEAD 2>/dev/null || echo "")
5198
+
5199
+ cat > "$session_file" << EOF
5200
+ {
5201
+ "session_id": "$session_id",
5202
+ "started_at": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")",
5203
+ "last_command": null,
5204
+ "last_command_at": null,
5205
+ "colony_goal": "$goal",
5206
+ "current_phase": 0,
5207
+ "current_milestone": "First Mound",
5208
+ "suggested_next": "/ant:plan",
5209
+ "context_cleared": false,
5210
+ "baseline_commit": "$baseline",
5211
+ "resumed_at": null,
5212
+ "active_todos": [],
5213
+ "summary": "Session initialized"
5214
+ }
5215
+ EOF
5216
+ json_ok "{\"session_id\":\"$session_id\",\"goal\":\"$goal\",\"file\":\"$session_file\"}"
5217
+ ;;
5218
+
5219
+ session-update)
5220
+ # Update session with latest activity
5221
+ # Usage: session-update <command> [suggested_next] [summary]
5222
+ cmd_run="${2:-}"
5223
+ suggested="${3:-}"
5224
+ summary="${4:-}"
5225
+
5226
+ session_file="$DATA_DIR/session.json"
5227
+
5228
+ if [[ ! -f "$session_file" ]]; then
5229
+ # Auto-initialize if doesn't exist
5230
+ bash "$0" session-init "auto_$(date +%s)" ""
5231
+ fi
5232
+
5233
+ # Read current session
5234
+ current_session=$(cat "$session_file" 2>/dev/null || echo '{}')
5235
+
5236
+ # Extract current values for preservation
5237
+ current_goal=$(echo "$current_session" | jq -r '.colony_goal // empty')
5238
+ current_phase=$(echo "$current_session" | jq -r '.current_phase // 0')
5239
+ current_milestone=$(echo "$current_session" | jq -r '.current_milestone // "First Mound"')
5240
+
5241
+ # Get top 3 TODOs if TO-DOs.md exists
3460
5242
  todos="[]"
3461
5243
  if [[ -f "TO-DOs.md" ]]; then
3462
5244
  todos=$(grep "^### " TO-DOs.md 2>/dev/null | head -3 | sed 's/^### //' | jq -R . | jq -s .)
@@ -3469,6 +5251,9 @@ EOF
3469
5251
  current_milestone=$(jq -r '.milestone // "First Mound"' "$DATA_DIR/COLONY_STATE.json" 2>/dev/null || echo "$current_milestone")
3470
5252
  fi
3471
5253
 
5254
+ # Capture current git HEAD for drift detection
5255
+ baseline=$(git rev-parse HEAD 2>/dev/null || echo "")
5256
+
3472
5257
  # Build updated session
3473
5258
  echo "$current_session" | jq --arg cmd "$cmd_run" \
3474
5259
  --arg ts "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
@@ -3478,6 +5263,7 @@ EOF
3478
5263
  --argjson phase "$current_phase" \
3479
5264
  --arg milestone "$current_milestone" \
3480
5265
  --argjson todos "$todos" \
5266
+ --arg baseline "$baseline" \
3481
5267
  '.last_command = $cmd |
3482
5268
  .last_command_at = $ts |
3483
5269
  .suggested_next = $suggested |
@@ -3485,7 +5271,8 @@ EOF
3485
5271
  .colony_goal = $goal |
3486
5272
  .current_phase = $phase |
3487
5273
  .current_milestone = $milestone |
3488
- .active_todos = $todos' > "$session_file"
5274
+ .active_todos = $todos |
5275
+ .baseline_commit = $baseline' > "$session_file"
3489
5276
 
3490
5277
  json_ok "{\"updated\":true,\"command\":\"$cmd_run\"}"
3491
5278
  ;;
@@ -3519,18 +5306,18 @@ EOF
3519
5306
  ;;
3520
5307
 
3521
5308
  session-is-stale)
3522
- # Check if session is stale (returns true/false)
5309
+ # Check if session is stale (returns JSON with is_stale boolean)
3523
5310
  session_file="$DATA_DIR/session.json"
3524
5311
 
3525
5312
  if [[ ! -f "$session_file" ]]; then
3526
- echo "true"
5313
+ json_ok '{"is_stale":true}'
3527
5314
  exit 0
3528
5315
  fi
3529
5316
 
3530
5317
  last_cmd_ts=$(jq -r '.last_command_at // .started_at // empty' "$session_file" 2>/dev/null)
3531
5318
 
3532
5319
  if [[ -z "$last_cmd_ts" ]]; then
3533
- echo "true"
5320
+ json_ok '{"is_stale":true}'
3534
5321
  exit 0
3535
5322
  fi
3536
5323
 
@@ -3538,7 +5325,11 @@ EOF
3538
5325
  now_epoch=$(date +%s)
3539
5326
  age_hours=$(( (now_epoch - last_epoch) / 3600 ))
3540
5327
 
3541
- [[ $age_hours -gt 24 ]] && echo "true" || echo "false"
5328
+ if [[ $age_hours -gt 24 ]]; then
5329
+ json_ok '{"is_stale":true}'
5330
+ else
5331
+ json_ok '{"is_stale":false}'
5332
+ fi
3542
5333
  ;;
3543
5334
 
3544
5335
  session-clear)
@@ -3570,16 +5361,34 @@ EOF
3570
5361
  '.resumed_at = $ts | .context_cleared = false' "$session_file" > "$session_file.tmp" && mv "$session_file.tmp" "$session_file"
3571
5362
  json_ok "{\"resumed\":true,\"timestamp\":\"$(date -u +"%Y-%m-%dT%H:%M:%SZ")\"}"
3572
5363
  else
3573
- json_err "$E_RESOURCE_NOT_FOUND" "No session to mark as resumed"
5364
+ json_err "$E_RESOURCE_NOT_FOUND" "No active session to mark as resumed. Try: run /ant:init to start a new session."
3574
5365
  fi
3575
5366
  ;;
3576
5367
 
3577
5368
  session-summary)
3578
- # Get human-readable session summary
5369
+ # Get session summary (human-readable or JSON)
3579
5370
  session_file="$DATA_DIR/session.json"
5371
+ json_mode="false"
5372
+
5373
+ # Parse --json flag (command name already shifted by main dispatch)
5374
+ while [[ $# -gt 0 ]]; do
5375
+ case "$1" in
5376
+ --json)
5377
+ json_mode="true"
5378
+ shift
5379
+ ;;
5380
+ *)
5381
+ shift
5382
+ ;;
5383
+ esac
5384
+ done
3580
5385
 
3581
5386
  if [[ ! -f "$session_file" ]]; then
3582
- echo "No active session found."
5387
+ if [[ "$json_mode" == "true" ]]; then
5388
+ json_ok '{"exists":false,"goal":null,"phase":0}'
5389
+ else
5390
+ echo "No active session found."
5391
+ fi
3583
5392
  exit 0
3584
5393
  fi
3585
5394
 
@@ -3591,15 +5400,151 @@ EOF
3591
5400
  suggested=$(jq -r '.suggested_next // "None"' "$session_file")
3592
5401
  cleared=$(jq -r '.context_cleared // false' "$session_file")
3593
5402
 
3594
- echo "📋 Session Summary"
3595
- echo "=================="
3596
- echo "Goal: $goal"
3597
- [[ "$phase" != "0" ]] && echo "Phase: $phase"
3598
- echo "Milestone: $milestone"
3599
- echo "Last Command: $last_cmd"
3600
- echo "Last Active: $last_at"
3601
- [[ "$suggested" != "None" ]] && echo "Suggested Next: $suggested"
3602
- [[ "$cleared" == "true" ]] && echo "Status: Context was cleared"
5403
+ if [[ "$json_mode" == "true" ]]; then
5404
+ # Escape goal for JSON
5405
+ goal_escaped=$(echo "$goal" | jq -Rs . | tr -d '\n')
5406
+ milestone_escaped=$(echo "$milestone" | jq -Rs . | tr -d '\n')
5407
+ last_cmd_escaped=$(echo "$last_cmd" | jq -Rs . | tr -d '\n')
5408
+ last_at_escaped=$(echo "$last_at" | jq -Rs . | tr -d '\n')
5409
+ suggested_escaped=$(echo "$suggested" | jq -Rs . | tr -d '\n')
5410
+ json_ok "{\"exists\":true,\"goal\":$goal_escaped,\"phase\":$phase,\"milestone\":$milestone_escaped,\"last_command\":$last_cmd_escaped,\"last_active\":$last_at_escaped,\"suggested_next\":$suggested_escaped,\"context_cleared\":$cleared}"
5411
+ else
5412
+ echo "Session Summary"
5413
+ echo "=================="
5414
+ echo "Goal: $goal"
5415
+ [[ "$phase" != "0" ]] && echo "Phase: $phase"
5416
+ echo "Milestone: $milestone"
5417
+ echo "Last Command: $last_cmd"
5418
+ echo "Last Active: $last_at"
5419
+ [[ "$suggested" != "None" ]] && echo "Suggested Next: $suggested"
5420
+ [[ "$cleared" == "true" ]] && echo "Status: Context was cleared"
5421
+ fi
5422
+ ;;
5423
+
5424
+ generate-progress-bar)
5425
+ generate-progress-bar "$@"
5426
+ ;;
5427
+ print-standard-banner)
5428
+ print-standard-banner "$@"
5429
+ ;;
5430
+ print-next-up)
5431
+ print-next-up "$@"
5432
+ ;;
5433
+
5434
+ # ============================================
5435
+ # LOCK MANAGEMENT
5436
+ # ============================================
5437
+
5438
+ force-unlock)
5439
+ # Emergency lock cleanup — removes all lock files
5440
+ # Usage: force-unlock [--yes]
5441
+ # Without --yes, lists locks and asks for confirmation in interactive mode
5442
+ lock_dir="${AETHER_ROOT:-.}/.aether/locks"
5443
+ auto_yes=false
5444
+ [[ "${1:-}" == "--yes" ]] && auto_yes=true
5445
+
5446
+ if [[ ! -d "$lock_dir" ]]; then
5447
+ json_ok '{"removed":0,"message":"No locks directory found"}'
5448
+ exit 0
5449
+ fi
5450
+
5451
+ lock_files=$(find "$lock_dir" -name "*.lock" -o -name "*.lock.pid" 2>/dev/null)
5452
+
5453
+ if [[ -z "$lock_files" ]]; then
5454
+ json_ok '{"removed":0,"message":"No lock files found"}'
5455
+ exit 0
5456
+ fi
5457
+
5458
+ lock_count=$(echo "$lock_files" | grep -c '\.lock$' || echo "0")
5459
+
5460
+ if [[ "$auto_yes" != "true" ]]; then
5461
+ if [[ -t 2 ]]; then
5462
+ echo "" >&2
5463
+ echo "Lock files found in $lock_dir:" >&2
5464
+ echo "$lock_files" | while read -r f; do
5465
+ [[ "$f" == *.pid ]] && continue
5466
+ pid_content=$(cat "${f}.pid" 2>/dev/null || echo "unknown")
5467
+ echo " $f (PID: $pid_content)" >&2
5468
+ done
5469
+ printf "Remove all %d lock(s)? [y/N] " "$lock_count" >&2
5470
+ read -r response < /dev/tty
5471
+ if [[ ! "$response" =~ ^[Yy]$ ]]; then
5472
+ json_ok '{"removed":0,"message":"Cancelled by user"}'
5473
+ exit 0
5474
+ fi
5475
+ else
5476
+ json_err "$E_VALIDATION_FAILED" "force-unlock requires --yes flag in non-interactive mode"
5477
+ fi
5478
+ fi
5479
+
5480
+ rm -f "$lock_dir"/*.lock "$lock_dir"/*.lock.pid 2>/dev/null || true
5481
+ export LOCK_ACQUIRED=false
5482
+ export CURRENT_LOCK=""
5483
+ json_ok "{\"removed\":$lock_count,\"message\":\"All locks cleared\"}"
5484
+ ;;
5485
+
5486
+ #=============================================================================
5487
+ # SEMANTIC COMMANDS
5488
+ #=============================================================================
5489
+
5490
+ semantic-init)
5491
+ # Initialize semantic store
5492
+ semantic-init
5493
+ ;;
5494
+
5495
+ semantic-index)
5496
+ # Index text for semantic search
5497
+ # Usage: semantic-index <text> <source> [entry_id]
5498
+ text="${2:-}"
5499
+ source="${3:-unknown}"
5500
+ entry_id="${4:-}"
5501
+
5502
+ if [[ -z "$text" ]]; then
5503
+ json_err "$E_VALIDATION_FAILED" "semantic-index requires text argument"
5504
+ exit 1
5505
+ fi
5506
+
5507
+ semantic-index "$text" "$source" "$entry_id"
5508
+ ;;
5509
+
5510
+ semantic-search)
5511
+ # Search for similar entries
5512
+ # Usage: semantic-search <query> [top_k] [threshold] [source_filter]
5513
+ query="${2:-}"
5514
+ top_k="${3:-5}"
5515
+ threshold="${4:-0.5}"
5516
+ source_filter="${5:-}"
5517
+
5518
+ if [[ -z "$query" ]]; then
5519
+ json_err "$E_VALIDATION_FAILED" "semantic-search requires query argument"
5520
+ exit 1
5521
+ fi
5522
+
5523
+ semantic-search "$query" "$top_k" "$threshold" "$source_filter"
5524
+ ;;
5525
+
5526
+ semantic-rebuild)
5527
+ # Rebuild semantic index from all data sources
5528
+ semantic-rebuild
5529
+ ;;
5530
+
5531
+ semantic-status)
5532
+ # Get semantic layer status
5533
+ semantic-status
5534
+ ;;
5535
+
5536
+ semantic-context)
5537
+ # Get context for task (for worker injection)
5538
+ # Usage: semantic-context <task_description> [max_results]
5539
+ task="${2:-}"
5540
+ max_results="${3:-3}"
5541
+
5542
+ if [[ -z "$task" ]]; then
5543
+ json_ok "[]" "No task provided"
5544
+ exit 0
5545
+ fi
5546
+
5547
+ semantic-get-context "$task" "$max_results"
3603
5548
  ;;
3604
5549
 
3605
5550
  *)