aether-colony 5.0.0 → 5.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (317) hide show
  1. package/.aether/aether-utils.sh +3226 -3345
  2. package/.aether/agents-claude/aether-ambassador.md +265 -0
  3. package/.aether/agents-claude/aether-archaeologist.md +327 -0
  4. package/.aether/agents-claude/aether-architect.md +236 -0
  5. package/.aether/agents-claude/aether-auditor.md +271 -0
  6. package/.aether/agents-claude/aether-builder.md +224 -0
  7. package/.aether/agents-claude/aether-chaos.md +269 -0
  8. package/.aether/agents-claude/aether-chronicler.md +305 -0
  9. package/.aether/agents-claude/aether-gatekeeper.md +330 -0
  10. package/.aether/agents-claude/aether-includer.md +374 -0
  11. package/.aether/agents-claude/aether-keeper.md +272 -0
  12. package/.aether/agents-claude/aether-measurer.md +322 -0
  13. package/.aether/agents-claude/aether-oracle.md +237 -0
  14. package/.aether/agents-claude/aether-probe.md +211 -0
  15. package/.aether/agents-claude/aether-queen.md +330 -0
  16. package/.aether/agents-claude/aether-route-setter.md +178 -0
  17. package/.aether/agents-claude/aether-sage.md +418 -0
  18. package/.aether/agents-claude/aether-scout.md +179 -0
  19. package/.aether/agents-claude/aether-surveyor-disciplines.md +417 -0
  20. package/.aether/agents-claude/aether-surveyor-nest.md +355 -0
  21. package/.aether/agents-claude/aether-surveyor-pathogens.md +289 -0
  22. package/.aether/agents-claude/aether-surveyor-provisions.md +360 -0
  23. package/.aether/agents-claude/aether-tracker.md +270 -0
  24. package/.aether/agents-claude/aether-watcher.md +280 -0
  25. package/.aether/agents-claude/aether-weaver.md +248 -0
  26. package/.aether/commands/archaeology.yaml +653 -0
  27. package/.aether/commands/build.yaml +1221 -0
  28. package/.aether/commands/chaos.yaml +653 -0
  29. package/.aether/commands/colonize.yaml +442 -0
  30. package/.aether/commands/continue.yaml +1484 -0
  31. package/.aether/commands/council.yaml +509 -0
  32. package/.aether/commands/data-clean.yaml +80 -0
  33. package/.aether/commands/dream.yaml +275 -0
  34. package/.aether/commands/entomb.yaml +863 -0
  35. package/.aether/commands/export-signals.yaml +64 -0
  36. package/.aether/commands/feedback.yaml +158 -0
  37. package/.aether/commands/flag.yaml +160 -0
  38. package/.aether/commands/flags.yaml +177 -0
  39. package/.aether/commands/focus.yaml +112 -0
  40. package/.aether/commands/help.yaml +167 -0
  41. package/.aether/commands/history.yaml +137 -0
  42. package/.aether/commands/import-signals.yaml +79 -0
  43. package/.aether/commands/init.yaml +502 -0
  44. package/.aether/commands/insert-phase.yaml +102 -0
  45. package/.aether/commands/interpret.yaml +285 -0
  46. package/.aether/commands/lay-eggs.yaml +224 -0
  47. package/.aether/commands/maturity.yaml +122 -0
  48. package/.aether/commands/memory-details.yaml +74 -0
  49. package/.aether/commands/migrate-state.yaml +174 -0
  50. package/.aether/commands/oracle.yaml +1224 -0
  51. package/.aether/commands/organize.yaml +446 -0
  52. package/.aether/commands/patrol.yaml +621 -0
  53. package/.aether/commands/pause-colony.yaml +424 -0
  54. package/.aether/commands/phase.yaml +124 -0
  55. package/.aether/commands/pheromones.yaml +153 -0
  56. package/.aether/commands/plan.yaml +1364 -0
  57. package/.aether/commands/preferences.yaml +63 -0
  58. package/.aether/commands/quick.yaml +104 -0
  59. package/.aether/commands/redirect.yaml +123 -0
  60. package/.aether/commands/resume-colony.yaml +375 -0
  61. package/.aether/commands/resume.yaml +407 -0
  62. package/.aether/commands/run.yaml +229 -0
  63. package/.aether/commands/seal.yaml +1214 -0
  64. package/.aether/commands/skill-create.yaml +337 -0
  65. package/.aether/commands/status.yaml +408 -0
  66. package/.aether/commands/swarm.yaml +352 -0
  67. package/.aether/commands/tunnels.yaml +814 -0
  68. package/.aether/commands/update.yaml +131 -0
  69. package/.aether/commands/verify-castes.yaml +159 -0
  70. package/.aether/commands/watch.yaml +454 -0
  71. package/.aether/docs/INCIDENT_TEMPLATE.md +32 -0
  72. package/.aether/docs/QUEEN-SYSTEM.md +11 -11
  73. package/.aether/docs/README.md +32 -2
  74. package/.aether/docs/command-playbooks/README.md +23 -0
  75. package/.aether/docs/command-playbooks/build-complete.md +349 -0
  76. package/.aether/docs/command-playbooks/build-context.md +282 -0
  77. package/.aether/docs/command-playbooks/build-full.md +1683 -0
  78. package/.aether/docs/command-playbooks/build-prep.md +284 -0
  79. package/.aether/docs/command-playbooks/build-verify.md +405 -0
  80. package/.aether/docs/command-playbooks/build-wave.md +749 -0
  81. package/.aether/docs/command-playbooks/continue-advance.md +524 -0
  82. package/.aether/docs/command-playbooks/continue-finalize.md +447 -0
  83. package/.aether/docs/command-playbooks/continue-full.md +1725 -0
  84. package/.aether/docs/command-playbooks/continue-gates.md +686 -0
  85. package/.aether/docs/command-playbooks/continue-verify.md +407 -0
  86. package/.aether/docs/context-continuity.md +84 -0
  87. package/.aether/docs/disciplines/DISCIPLINES.md +9 -7
  88. package/.aether/docs/error-codes.md +1 -1
  89. package/.aether/docs/known-issues.md +34 -173
  90. package/.aether/docs/pheromones.md +86 -6
  91. package/.aether/docs/plans/pheromone-display-plan.md +257 -0
  92. package/.aether/docs/queen-commands.md +10 -9
  93. package/.aether/docs/source-of-truth-map.md +132 -0
  94. package/.aether/docs/xml-utilities.md +47 -0
  95. package/.aether/rules/aether-colony.md +23 -13
  96. package/.aether/scripts/incident-test-add.sh +47 -0
  97. package/.aether/scripts/weekly-audit.sh +79 -0
  98. package/.aether/skills/.index.json +649 -0
  99. package/.aether/skills/colony/.manifest.json +16 -0
  100. package/.aether/skills/colony/build-discipline/SKILL.md +78 -0
  101. package/.aether/skills/colony/colony-interaction/SKILL.md +56 -0
  102. package/.aether/skills/colony/colony-lifecycle/SKILL.md +77 -0
  103. package/.aether/skills/colony/colony-visuals/SKILL.md +112 -0
  104. package/.aether/skills/colony/context-management/SKILL.md +80 -0
  105. package/.aether/skills/colony/error-presentation/SKILL.md +99 -0
  106. package/.aether/skills/colony/pheromone-protocol/SKILL.md +79 -0
  107. package/.aether/skills/colony/pheromone-visibility/SKILL.md +81 -0
  108. package/.aether/skills/colony/state-safety/SKILL.md +84 -0
  109. package/.aether/skills/colony/worker-priming/SKILL.md +82 -0
  110. package/.aether/skills/domain/.manifest.json +24 -0
  111. package/.aether/skills/domain/README.md +33 -0
  112. package/.aether/skills/domain/django/SKILL.md +49 -0
  113. package/.aether/skills/domain/docker/SKILL.md +52 -0
  114. package/.aether/skills/domain/golang/SKILL.md +52 -0
  115. package/.aether/skills/domain/graphql/SKILL.md +51 -0
  116. package/.aether/skills/domain/html-css/SKILL.md +48 -0
  117. package/.aether/skills/domain/nextjs/SKILL.md +45 -0
  118. package/.aether/skills/domain/nodejs/SKILL.md +53 -0
  119. package/.aether/skills/domain/postgresql/SKILL.md +53 -0
  120. package/.aether/skills/domain/prisma/SKILL.md +59 -0
  121. package/.aether/skills/domain/python/SKILL.md +50 -0
  122. package/.aether/skills/domain/rails/SKILL.md +52 -0
  123. package/.aether/skills/domain/react/SKILL.md +45 -0
  124. package/.aether/skills/domain/rest-api/SKILL.md +58 -0
  125. package/.aether/skills/domain/svelte/SKILL.md +47 -0
  126. package/.aether/skills/domain/tailwind/SKILL.md +45 -0
  127. package/.aether/skills/domain/testing/SKILL.md +53 -0
  128. package/.aether/skills/domain/typescript/SKILL.md +58 -0
  129. package/.aether/skills/domain/vue/SKILL.md +49 -0
  130. package/.aether/templates/QUEEN.md.template +23 -41
  131. package/.aether/templates/colony-state-reset.jq.template +1 -0
  132. package/.aether/templates/colony-state.template.json +4 -0
  133. package/.aether/templates/learning-observations.template.json +6 -0
  134. package/.aether/templates/midden.template.json +13 -0
  135. package/.aether/templates/pheromones.template.json +6 -0
  136. package/.aether/templates/session.template.json +9 -0
  137. package/.aether/utils/atomic-write.sh +63 -17
  138. package/.aether/utils/chamber-utils.sh +145 -2
  139. package/.aether/utils/council.sh +425 -0
  140. package/.aether/utils/emoji-audit.sh +166 -0
  141. package/.aether/utils/error-handler.sh +21 -7
  142. package/.aether/utils/file-lock.sh +182 -27
  143. package/.aether/utils/flag.sh +278 -0
  144. package/.aether/utils/hive.sh +572 -0
  145. package/.aether/utils/immune.sh +508 -0
  146. package/.aether/utils/learning.sh +1928 -0
  147. package/.aether/utils/midden.sh +520 -0
  148. package/.aether/utils/oracle/oracle.md +168 -0
  149. package/.aether/utils/oracle/oracle.sh +1023 -0
  150. package/.aether/utils/pheromone.sh +2029 -0
  151. package/.aether/utils/queen.sh +1710 -0
  152. package/.aether/utils/scan.sh +860 -0
  153. package/.aether/utils/semantic-cli.sh +10 -8
  154. package/.aether/utils/session.sh +816 -0
  155. package/.aether/utils/skills.sh +509 -0
  156. package/.aether/utils/spawn-tree.sh +103 -271
  157. package/.aether/utils/spawn.sh +260 -0
  158. package/.aether/utils/state-api.sh +389 -0
  159. package/.aether/utils/state-loader.sh +8 -6
  160. package/.aether/utils/suggest.sh +611 -0
  161. package/.aether/utils/swarm-display.sh +10 -1
  162. package/.aether/utils/swarm.sh +1004 -0
  163. package/.aether/utils/watch-spawn-tree.sh +11 -2
  164. package/.aether/utils/xml-compose.sh +2 -2
  165. package/.aether/utils/xml-convert.sh +9 -5
  166. package/.aether/utils/xml-core.sh +5 -9
  167. package/.aether/utils/xml-query.sh +4 -4
  168. package/.aether/workers.md +86 -67
  169. package/.claude/agents/ant/aether-ambassador.md +2 -1
  170. package/.claude/agents/ant/aether-archaeologist.md +6 -1
  171. package/.claude/agents/ant/aether-architect.md +236 -0
  172. package/.claude/agents/ant/aether-auditor.md +6 -1
  173. package/.claude/agents/ant/aether-builder.md +38 -1
  174. package/.claude/agents/ant/aether-chaos.md +2 -1
  175. package/.claude/agents/ant/aether-chronicler.md +1 -0
  176. package/.claude/agents/ant/aether-gatekeeper.md +6 -1
  177. package/.claude/agents/ant/aether-includer.md +1 -0
  178. package/.claude/agents/ant/aether-keeper.md +1 -0
  179. package/.claude/agents/ant/aether-measurer.md +6 -1
  180. package/.claude/agents/ant/aether-oracle.md +237 -0
  181. package/.claude/agents/ant/aether-probe.md +2 -1
  182. package/.claude/agents/ant/aether-queen.md +6 -1
  183. package/.claude/agents/ant/aether-route-setter.md +6 -1
  184. package/.claude/agents/ant/aether-sage.md +68 -3
  185. package/.claude/agents/ant/aether-scout.md +38 -1
  186. package/.claude/agents/ant/aether-surveyor-disciplines.md +2 -1
  187. package/.claude/agents/ant/aether-surveyor-nest.md +2 -1
  188. package/.claude/agents/ant/aether-surveyor-pathogens.md +2 -1
  189. package/.claude/agents/ant/aether-surveyor-provisions.md +2 -1
  190. package/.claude/agents/ant/aether-tracker.md +6 -1
  191. package/.claude/agents/ant/aether-watcher.md +37 -1
  192. package/.claude/agents/ant/aether-weaver.md +2 -1
  193. package/.claude/commands/ant/archaeology.md +1 -8
  194. package/.claude/commands/ant/build.md +43 -1159
  195. package/.claude/commands/ant/chaos.md +1 -14
  196. package/.claude/commands/ant/colonize.md +3 -14
  197. package/.claude/commands/ant/continue.md +40 -1026
  198. package/.claude/commands/ant/council.md +213 -15
  199. package/.claude/commands/ant/data-clean.md +81 -0
  200. package/.claude/commands/ant/dream.md +12 -9
  201. package/.claude/commands/ant/entomb.md +62 -87
  202. package/.claude/commands/ant/export-signals.md +57 -0
  203. package/.claude/commands/ant/feedback.md +18 -0
  204. package/.claude/commands/ant/flag.md +12 -0
  205. package/.claude/commands/ant/flags.md +22 -8
  206. package/.claude/commands/ant/focus.md +18 -0
  207. package/.claude/commands/ant/help.md +40 -8
  208. package/.claude/commands/ant/history.md +3 -0
  209. package/.claude/commands/ant/import-signals.md +71 -0
  210. package/.claude/commands/ant/init.md +349 -191
  211. package/.claude/commands/ant/insert-phase.md +105 -0
  212. package/.claude/commands/ant/interpret.md +11 -0
  213. package/.claude/commands/ant/lay-eggs.md +167 -158
  214. package/.claude/commands/ant/maturity.md +22 -11
  215. package/.claude/commands/ant/memory-details.md +77 -0
  216. package/.claude/commands/ant/migrate-state.md +6 -0
  217. package/.claude/commands/ant/oracle.md +317 -62
  218. package/.claude/commands/ant/organize.md +10 -5
  219. package/.claude/commands/ant/patrol.md +620 -0
  220. package/.claude/commands/ant/pause-colony.md +8 -22
  221. package/.claude/commands/ant/phase.md +26 -37
  222. package/.claude/commands/ant/pheromones.md +156 -0
  223. package/.claude/commands/ant/plan.md +199 -50
  224. package/.claude/commands/ant/preferences.md +65 -0
  225. package/.claude/commands/ant/quick.md +100 -0
  226. package/.claude/commands/ant/redirect.md +18 -0
  227. package/.claude/commands/ant/resume-colony.md +37 -22
  228. package/.claude/commands/ant/resume.md +60 -7
  229. package/.claude/commands/ant/run.md +231 -0
  230. package/.claude/commands/ant/seal.md +506 -78
  231. package/.claude/commands/ant/skill-create.md +286 -0
  232. package/.claude/commands/ant/status.md +171 -1
  233. package/.claude/commands/ant/swarm.md +11 -23
  234. package/.claude/commands/ant/tunnels.md +1 -0
  235. package/.claude/commands/ant/update.md +58 -135
  236. package/.claude/commands/ant/verify-castes.md +90 -42
  237. package/.claude/commands/ant/watch.md +1 -0
  238. package/.opencode/agents/aether-ambassador.md +1 -1
  239. package/.opencode/agents/aether-architect.md +133 -0
  240. package/.opencode/agents/aether-builder.md +3 -3
  241. package/.opencode/agents/aether-oracle.md +137 -0
  242. package/.opencode/agents/aether-queen.md +1 -1
  243. package/.opencode/agents/aether-route-setter.md +1 -1
  244. package/.opencode/agents/aether-scout.md +1 -1
  245. package/.opencode/agents/aether-surveyor-disciplines.md +6 -1
  246. package/.opencode/agents/aether-surveyor-nest.md +6 -1
  247. package/.opencode/agents/aether-surveyor-pathogens.md +6 -1
  248. package/.opencode/agents/aether-surveyor-provisions.md +6 -1
  249. package/.opencode/agents/aether-tracker.md +1 -1
  250. package/.opencode/agents/aether-watcher.md +1 -1
  251. package/.opencode/agents/aether-weaver.md +1 -1
  252. package/.opencode/commands/ant/archaeology.md +7 -14
  253. package/.opencode/commands/ant/build.md +54 -88
  254. package/.opencode/commands/ant/chaos.md +7 -24
  255. package/.opencode/commands/ant/colonize.md +10 -17
  256. package/.opencode/commands/ant/continue.md +595 -66
  257. package/.opencode/commands/ant/council.md +150 -18
  258. package/.opencode/commands/ant/data-clean.md +77 -0
  259. package/.opencode/commands/ant/dream.md +15 -17
  260. package/.opencode/commands/ant/entomb.md +28 -18
  261. package/.opencode/commands/ant/export-signals.md +54 -0
  262. package/.opencode/commands/ant/feedback.md +24 -5
  263. package/.opencode/commands/ant/flag.md +16 -4
  264. package/.opencode/commands/ant/flags.md +24 -10
  265. package/.opencode/commands/ant/focus.md +22 -5
  266. package/.opencode/commands/ant/help.md +41 -8
  267. package/.opencode/commands/ant/history.md +9 -0
  268. package/.opencode/commands/ant/import-signals.md +68 -0
  269. package/.opencode/commands/ant/init.md +396 -154
  270. package/.opencode/commands/ant/insert-phase.md +111 -0
  271. package/.opencode/commands/ant/interpret.md +16 -0
  272. package/.opencode/commands/ant/lay-eggs.md +184 -112
  273. package/.opencode/commands/ant/maturity.md +18 -2
  274. package/.opencode/commands/ant/memory-details.md +83 -0
  275. package/.opencode/commands/ant/migrate-state.md +12 -0
  276. package/.opencode/commands/ant/oracle.md +322 -67
  277. package/.opencode/commands/ant/organize.md +14 -12
  278. package/.opencode/commands/ant/patrol.md +626 -0
  279. package/.opencode/commands/ant/pause-colony.md +12 -29
  280. package/.opencode/commands/ant/phase.md +30 -40
  281. package/.opencode/commands/ant/pheromones.md +162 -0
  282. package/.opencode/commands/ant/plan.md +210 -57
  283. package/.opencode/commands/ant/preferences.md +71 -0
  284. package/.opencode/commands/ant/quick.md +91 -0
  285. package/.opencode/commands/ant/redirect.md +22 -5
  286. package/.opencode/commands/ant/resume-colony.md +41 -29
  287. package/.opencode/commands/ant/resume.md +80 -20
  288. package/.opencode/commands/ant/run.md +237 -0
  289. package/.opencode/commands/ant/seal.md +230 -25
  290. package/.opencode/commands/ant/skill-create.md +63 -0
  291. package/.opencode/commands/ant/status.md +125 -30
  292. package/.opencode/commands/ant/swarm.md +3 -345
  293. package/.opencode/commands/ant/tunnels.md +3 -9
  294. package/.opencode/commands/ant/update.md +63 -127
  295. package/.opencode/commands/ant/verify-castes.md +96 -42
  296. package/.opencode/commands/ant/watch.md +7 -0
  297. package/CHANGELOG.md +368 -1
  298. package/README.md +195 -324
  299. package/bin/cli.js +236 -429
  300. package/bin/generate-commands.js +186 -0
  301. package/bin/generate-commands.sh +128 -89
  302. package/bin/lib/spawn-logger.js +0 -15
  303. package/bin/lib/update-transaction.js +285 -35
  304. package/bin/npx-install.js +178 -0
  305. package/bin/validate-package.sh +85 -3
  306. package/package.json +16 -4
  307. package/.aether/CONTEXT.md +0 -160
  308. package/.aether/docs/QUEEN.md +0 -84
  309. package/.aether/exchange/colony-registry.xml +0 -11
  310. package/.aether/exchange/pheromones.xml +0 -87
  311. package/.aether/exchange/queen-wisdom.xml +0 -14
  312. package/.aether/model-profiles.yaml +0 -100
  313. package/.aether/utils/spawn-with-model.sh +0 -56
  314. package/bin/lib/model-profiles.js +0 -445
  315. package/bin/lib/model-verify.js +0 -288
  316. package/bin/lib/proxy-health.js +0 -253
  317. package/bin/lib/telemetry.js +0 -441
@@ -0,0 +1,260 @@
1
+ #!/bin/bash
2
+ # Spawn utility functions — extracted from aether-utils.sh
3
+ # Provides: _spawn_log, _spawn_complete, _spawn_can_spawn, _spawn_get_depth, _spawn_can_spawn_swarm, _spawn_tree_load, _spawn_tree_active, _spawn_tree_depth, _spawn_efficiency
4
+ #
5
+ # These functions are sourced by aether-utils.sh at startup.
6
+ # All shared infrastructure (json_ok, json_err, json_warn, atomic_write, acquire_lock,
7
+ # release_lock, feature_enabled, LOCK_DIR, DATA_DIR, SCRIPT_DIR, error constants) is available.
8
+ # Note: get_caste_emoji is defined in the main file and available to this module at call time.
9
+
10
+ _spawn_log() {
11
+ # Usage: spawn-log <parent_id> <child_caste> <child_name> <task_summary> [model] [status]
12
+ parent_id="${1:-}"
13
+ child_caste="${2:-}"
14
+ child_name="${3:-}"
15
+ task_summary="${4:-}"
16
+ model="${5:-default}"
17
+ status="${6:-spawned}"
18
+ # Model slot resolution removed (archived: .aether/archive/model-routing/)
19
+ # Agent frontmatter model: fields handle routing natively via Claude Code.
20
+ [[ "$model" == "default" ]] && model="inherit"
21
+ [[ -z "$parent_id" || -z "$child_caste" || -z "$task_summary" ]] && json_err "$E_VALIDATION_FAILED" "Usage: spawn-log <parent_id> <child_caste> <child_name> <task_summary> [model] [status]"
22
+ mkdir -p "$COLONY_DATA_DIR"
23
+ ts=$(date -u +"%H:%M:%S")
24
+ ts_full=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
25
+ emoji=$(get_caste_emoji "$child_caste")
26
+ parent_emoji=$(get_caste_emoji "$parent_id")
27
+ # Log to activity log with spawn format, emojis, and model info
28
+ [[ "${AETHER_TESTING:-}" != "1" ]] && echo "[$ts] ⚡ SPAWN $parent_emoji $parent_id -> $emoji $child_name ($child_caste): $task_summary [model: $model]" >> "$COLONY_DATA_DIR/activity.log"
29
+ # Log to spawn tree file for visualization (NEW FORMAT: includes model field)
30
+ echo "$ts_full|$parent_id|$child_caste|$child_name|$task_summary|$model|$status" >> "$COLONY_DATA_DIR/spawn-tree.txt"
31
+ # Return emoji-formatted result for display (jq-safe: child_name may contain JSON-special chars)
32
+ json_ok "$(jq -n --arg msg "⚡ $emoji $child_name spawned" '$msg')"
33
+ }
34
+
35
+ _spawn_complete() {
36
+ # Migrated to state-api facade: uses _state_mutate for failed spawn event logging
37
+ # Usage: spawn-complete <ant_name> <status> [summary]
38
+ ant_name="${1:-}"
39
+ status="${2:-completed}"
40
+ summary="${3:-}"
41
+ [[ -z "$ant_name" ]] && json_err "$E_VALIDATION_FAILED" "Usage: spawn-complete <ant_name> <status> [summary]"
42
+ mkdir -p "$COLONY_DATA_DIR"
43
+ ts=$(date -u +"%H:%M:%S")
44
+ ts_full=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
45
+ emoji=$(get_caste_emoji "$ant_name")
46
+ status_icon="✅"
47
+ [[ "$status" == "failed" ]] && status_icon="❌"
48
+ [[ "$status" == "blocked" ]] && status_icon="🚫"
49
+ [[ "${AETHER_TESTING:-}" != "1" ]] && echo "[$ts] $status_icon $emoji $ant_name: $status${summary:+ - $summary}" >> "$COLONY_DATA_DIR/activity.log"
50
+ # Update spawn tree
51
+ echo "$ts_full|$ant_name|$status|$summary" >> "$COLONY_DATA_DIR/spawn-tree.txt"
52
+ # Log failed spawns to events array as pipe-delimited strings (matching template format)
53
+ if [[ "$status" == "failed" ]] || [[ "$status" == "error" ]]; then
54
+ if [[ -f "$DATA_DIR/COLONY_STATE.json" ]]; then
55
+ SC_EVENT="$ts_full|spawn_failed|$ant_name|${summary:-unknown}" \
56
+ _state_mutate '
57
+ .events += [env.SC_EVENT]
58
+ ' >/dev/null 2>&1 || _aether_log_error "Failed to log spawn failure to colony state"
59
+ fi
60
+ fi
61
+ # Return emoji-formatted result for display (jq-safe: ant_name/summary may contain JSON-special chars)
62
+ json_ok "$(jq -n --arg msg "$status_icon $emoji $ant_name: ${summary:-$status}" '$msg')"
63
+ }
64
+
65
+ _spawn_can_spawn() {
66
+ # Check if spawning is allowed at given depth
67
+ # Usage: spawn-can-spawn [depth] [--enforce]
68
+ # Returns: {can_spawn: bool, depth: N, max_spawns: N, current_total: N, global_cap: N}
69
+ # --enforce: fail with non-zero exit when spawning is not allowed
70
+ depth=""
71
+ enforce_mode=false
72
+ for arg in "$@"; do
73
+ case "$arg" in
74
+ --enforce) enforce_mode=true ;;
75
+ *)
76
+ if [[ -z "$depth" ]]; then
77
+ depth="$arg"
78
+ else
79
+ json_err "$E_VALIDATION_FAILED" "Usage: spawn-can-spawn [depth] [--enforce]"
80
+ fi
81
+ ;;
82
+ esac
83
+ done
84
+ [[ -z "$depth" ]] && depth=1
85
+ [[ "$depth" =~ ^[0-9]+$ ]] || json_err "$E_VALIDATION_FAILED" "Depth must be a non-negative integer" "{\"provided\":\"$depth\"}"
86
+
87
+ # Depth limits: 1→4 spawns, 2→2 spawns, 3+→0 spawns
88
+ if [[ $depth -eq 1 ]]; then
89
+ max_for_depth=4
90
+ elif [[ $depth -eq 2 ]]; then
91
+ max_for_depth=2
92
+ else
93
+ max_for_depth=0
94
+ fi
95
+
96
+ # Count current spawns in this session (from spawn-tree.txt)
97
+ current=0
98
+ if [[ -f "$COLONY_DATA_DIR/spawn-tree.txt" ]]; then
99
+ current=$(grep -c "|spawned$" "$COLONY_DATA_DIR/spawn-tree.txt" 2>/dev/null || echo 0) # SUPPRESS:OK -- read-default: count defaults to 0 if file missing
100
+ fi
101
+
102
+ # Global cap of 10 workers per phase
103
+ global_cap=10
104
+
105
+ # Can spawn if: depth < 3 AND under global cap
106
+ if [[ $depth -lt 3 && $current -lt $global_cap ]]; then
107
+ can="true"
108
+ else
109
+ can="false"
110
+ fi
111
+
112
+ if [[ "$enforce_mode" == "true" && "$can" == "false" ]]; then
113
+ json_err "$E_VALIDATION_FAILED" "Spawn cap exceeded: depth=$depth current=$current max=$global_cap"
114
+ fi
115
+
116
+ json_ok "{\"can_spawn\":$can,\"depth\":$depth,\"max_spawns\":$max_for_depth,\"current_total\":$current,\"global_cap\":$global_cap}"
117
+ }
118
+
119
+ _spawn_get_depth() {
120
+ # Return depth for a given ant name by tracing spawn tree
121
+ # Usage: spawn-get-depth <ant_name>
122
+ # Queen = depth 0, Queen's spawns = depth 1, their spawns = depth 2, etc.
123
+ ant_name="${1:-Queen}"
124
+
125
+ if [[ "$ant_name" == "Queen" ]]; then
126
+ json_ok '{"ant":"Queen","depth":0}'
127
+ exit 0
128
+ fi
129
+
130
+ # Check if spawn tree exists
131
+ if [[ ! -f "$COLONY_DATA_DIR/spawn-tree.txt" ]]; then
132
+ json_ok "$(jq -n --arg ant "$ant_name" '{ant: $ant, depth: 1, found: false}')"
133
+ exit 0
134
+ fi
135
+
136
+ # Check if ant exists in spawn tree (gracefully handle missing ants)
137
+ if ! grep -qF "|$ant_name|" "$COLONY_DATA_DIR/spawn-tree.txt" 2>/dev/null; then # SUPPRESS:OK -- existence-test: file may not exist; -F: ant_name may contain regex metacharacters
138
+ json_ok "$(jq -n --arg ant "$ant_name" '{ant: $ant, depth: 1, found: false}')"
139
+ exit 0
140
+ fi
141
+
142
+ # Find the spawn record for this ant and trace parents
143
+ depth=1
144
+ current_ant="$ant_name"
145
+
146
+ # Find who spawned this ant (look for lines with |spawned)
147
+ while true; do
148
+ # Format: timestamp|parent|caste|child_name|task|spawned
149
+ # SUPPRESS:OK -- read-default: returns fallback on failure
150
+ parent=$(grep -F "|$current_ant|" "$COLONY_DATA_DIR/spawn-tree.txt" 2>/dev/null | grep "|spawned$" | head -1 | cut -d'|' -f2 || echo "")
151
+
152
+ if [[ -z "$parent" || "$parent" == "Queen" ]]; then
153
+ break
154
+ fi
155
+
156
+ depth=$((depth + 1))
157
+ current_ant="$parent"
158
+
159
+ # Safety limit
160
+ if [[ $depth -gt 5 ]]; then
161
+ break
162
+ fi
163
+ done
164
+
165
+ json_ok "$(jq -n --arg ant "$ant_name" --argjson depth "$depth" '{ant: $ant, depth: $depth, found: true}')"
166
+ }
167
+
168
+ _spawn_can_spawn_swarm() {
169
+ # Check if swarm can spawn more scouts (separate from phase workers)
170
+ # Usage: spawn-can-spawn-swarm <swarm_id>
171
+ # Swarm has its own cap of 6 (4 scouts + 2 sub-scouts max)
172
+ swarm_id="${1:-swarm}"
173
+ swarm_cap=6
174
+
175
+ current=0
176
+ if [[ -f "$COLONY_DATA_DIR/spawn-tree.txt" ]]; then
177
+ # SUPPRESS:OK -- existence-test: grep returns 1 when no matches
178
+ # -F: swarm_id may contain regex metacharacters; anchor $ dropped (swarm_id is unique, no substring collision risk)
179
+ current=$(grep -cF "|swarm:$swarm_id" "$COLONY_DATA_DIR/spawn-tree.txt" 2>/dev/null) || current=0
180
+ fi
181
+
182
+ if [[ $current -lt $swarm_cap ]]; then
183
+ can="true"
184
+ remaining=$((swarm_cap - current))
185
+ else
186
+ can="false"
187
+ remaining=0
188
+ fi
189
+
190
+ json_ok "$(jq -n --argjson can_spawn "$can" --argjson current "$current" \
191
+ --argjson cap "$swarm_cap" --argjson remaining "$remaining" --arg swarm_id "$swarm_id" \
192
+ '{can_spawn: $can_spawn, current: $current, cap: $cap, remaining: $remaining, swarm_id: $swarm_id}')"
193
+ }
194
+
195
+ _spawn_tree_load() {
196
+ source "$SCRIPT_DIR/utils/spawn-tree.sh" 2>/dev/null || { # SUPPRESS:OK -- read-default: utility may not be installed
197
+ json_err "$E_FILE_NOT_FOUND" "spawn-tree.sh not found"
198
+ exit 1
199
+ }
200
+ tree_json=$(reconstruct_tree_json)
201
+ if echo "$tree_json" | jq -e . >/dev/null 2>&1; then
202
+ json_ok "$tree_json"
203
+ else
204
+ json_err "$E_VALIDATION_FAILED" "spawn tree reconstruction produced invalid JSON"
205
+ return 1
206
+ fi
207
+ }
208
+
209
+ _spawn_tree_active() {
210
+ source "$SCRIPT_DIR/utils/spawn-tree.sh" 2>/dev/null || { # SUPPRESS:OK -- read-default: utility may not be installed
211
+ json_err "$E_FILE_NOT_FOUND" "spawn-tree.sh not found"
212
+ exit 1
213
+ }
214
+ active=$(get_active_spawns)
215
+ if echo "$active" | jq -e . >/dev/null 2>&1; then
216
+ json_ok "$active"
217
+ else
218
+ json_err "$E_VALIDATION_FAILED" "spawn-tree active produced invalid JSON"
219
+ return 1
220
+ fi
221
+ }
222
+
223
+ _spawn_tree_depth() {
224
+ ant_name="${1:-}"
225
+ [[ -z "$ant_name" ]] && json_err "$E_VALIDATION_FAILED" "Usage: spawn-tree-depth <ant_name>"
226
+ source "$SCRIPT_DIR/utils/spawn-tree.sh" 2>/dev/null || { # SUPPRESS:OK -- read-default: utility may not be installed
227
+ json_err "$E_FILE_NOT_FOUND" "spawn-tree.sh not found"
228
+ exit 1
229
+ }
230
+ depth=$(get_spawn_depth "$ant_name")
231
+ if echo "$depth" | jq -e . >/dev/null 2>&1; then
232
+ json_ok "$depth"
233
+ else
234
+ json_err "$E_VALIDATION_FAILED" "spawn-tree depth produced invalid JSON"
235
+ return 1
236
+ fi
237
+ }
238
+
239
+ _spawn_efficiency() {
240
+ # Calculate spawn efficiency metrics from spawn-tree.txt
241
+ # Usage: spawn-efficiency
242
+ spawn_tree_file="$COLONY_DATA_DIR/spawn-tree.txt"
243
+ total=0
244
+ completed=0
245
+ failed=0
246
+
247
+ if [[ -f "$spawn_tree_file" ]]; then
248
+ total=$(grep -c "|spawned$" "$spawn_tree_file" 2>/dev/null || echo 0) # SUPPRESS:OK -- read-default: count defaults to 0 if file missing
249
+ completed=$(grep -c "|completed$" "$spawn_tree_file" 2>/dev/null || echo 0) # SUPPRESS:OK -- read-default: count defaults to 0 if file missing
250
+ failed=$(grep -c "|failed$" "$spawn_tree_file" 2>/dev/null || echo 0) # SUPPRESS:OK -- read-default: count defaults to 0 if file missing
251
+ fi
252
+
253
+ if [[ "$total" -gt 0 ]]; then
254
+ efficiency=$(( completed * 100 / total ))
255
+ else
256
+ efficiency=0
257
+ fi
258
+
259
+ json_ok "{\"total\":$total,\"completed\":$completed,\"failed\":$failed,\"efficiency_pct\":$efficiency}"
260
+ }
@@ -0,0 +1,389 @@
1
+ #!/bin/bash
2
+ # State API facade -- centralized COLONY_STATE.json access
3
+ # Provides: _state_read, _state_write, _state_read_field, _state_mutate, _state_migrate,
4
+ # _colony_vital_signs
5
+ #
6
+ # These functions are sourced by aether-utils.sh at startup.
7
+ # All shared infrastructure (json_ok, json_err, atomic_write, acquire_lock,
8
+ # release_lock, LOCK_DIR, DATA_DIR, SCRIPT_DIR, error constants) is available.
9
+
10
+ _state_read() {
11
+ # Read full COLONY_STATE.json and return via json_ok
12
+ # Usage: state-read
13
+ # No lock needed for reads (jq is atomic on single files)
14
+ # Returns: json_ok with full state, or json_err on missing/invalid file
15
+
16
+ sr_state_file="$DATA_DIR/COLONY_STATE.json"
17
+
18
+ if [[ ! -f "$sr_state_file" ]]; then
19
+ json_err "$E_FILE_NOT_FOUND" "COLONY_STATE.json not found" '{"file":"COLONY_STATE.json"}'
20
+ fi
21
+
22
+ sr_content=$(cat "$sr_state_file" 2>/dev/null) || {
23
+ json_err "$E_FILE_NOT_FOUND" "Failed to read COLONY_STATE.json"
24
+ }
25
+
26
+ if ! echo "$sr_content" | jq -e . >/dev/null 2>&1; then
27
+ json_err "$E_JSON_INVALID" "COLONY_STATE.json contains invalid JSON"
28
+ fi
29
+
30
+ json_ok "$sr_content"
31
+ }
32
+
33
+ _state_read_field() {
34
+ # Read a specific jq field path from COLONY_STATE.json
35
+ # Usage: state-read-field <jq_path>
36
+ # For internal callers: outputs raw value to stdout (no json_ok wrapper)
37
+ # For subcommand entry: case block wraps in json_ok
38
+ # Returns empty string + exit 0 for missing field (callers check emptiness)
39
+
40
+ srf_field="${1:-}"
41
+
42
+ if [[ -z "$srf_field" ]]; then
43
+ json_err "$E_VALIDATION_FAILED" "state-read-field requires a jq field path argument"
44
+ fi
45
+
46
+ srf_state_file="$DATA_DIR/COLONY_STATE.json"
47
+
48
+ if [[ ! -f "$srf_state_file" ]]; then
49
+ json_err "$E_FILE_NOT_FOUND" "COLONY_STATE.json not found" '{"file":"COLONY_STATE.json"}'
50
+ fi
51
+
52
+ # Extract the field value (raw output, no quotes around strings)
53
+ srf_value=$(jq -r "$srf_field // empty" "$srf_state_file" 2>/dev/null) || srf_value=""
54
+
55
+ echo "$srf_value"
56
+ }
57
+
58
+ _state_write() {
59
+ # Write COLONY_STATE.json through a locked, validated, atomic path
60
+ # Usage: state-write '<json>'
61
+ # or: cat state.json | state-write
62
+ # Refactored from inline state-write case block for reuse
63
+ # Validates JSON, acquires lock, creates backup, writes atomically
64
+
65
+ sw_content="${1:-}"
66
+ if [[ -z "$sw_content" ]]; then
67
+ sw_content=$(cat)
68
+ fi
69
+
70
+ # Validate JSON
71
+ if ! echo "$sw_content" | jq -e . >/dev/null 2>&1; then # SUPPRESS:OK -- validation: testing JSON validity
72
+ json_err "$E_JSON_INVALID" "state-write received invalid JSON"
73
+ fi
74
+
75
+ sw_state_file="$DATA_DIR/COLONY_STATE.json"
76
+
77
+ # Acquire lock (colony-level, not hub-level)
78
+ acquire_lock "$sw_state_file" || json_err "$E_LOCK_FAILED" "Failed to acquire lock on COLONY_STATE.json"
79
+
80
+ # Create backup before writing
81
+ if [[ -f "$sw_state_file" ]]; then
82
+ if ! create_backup "$sw_state_file"; then
83
+ _aether_log_error "Could not create backup of colony state before writing"
84
+ fi
85
+ fi
86
+
87
+ # Write atomically; release lock on failure
88
+ atomic_write "$sw_state_file" "$sw_content" || {
89
+ release_lock 2>/dev/null || true # SUPPRESS:OK -- cleanup: lock may not be held
90
+ json_err "$E_UNKNOWN" "Failed to write COLONY_STATE.json"
91
+ }
92
+ release_lock 2>/dev/null || true # SUPPRESS:OK -- cleanup: lock may not be held
93
+
94
+ json_ok '{"written":true}'
95
+ }
96
+
97
+ _state_mutate() {
98
+ # Read-modify-write COLONY_STATE.json with a jq expression
99
+ # Usage: state-mutate '<jq_expression>'
100
+ # Acquires lock, creates backup, applies jq, validates, writes atomically
101
+ # Returns: json_ok with mutated:true, or json_err on failure
102
+
103
+ sm_expr="${1:-}"
104
+
105
+ if [[ -z "$sm_expr" ]]; then
106
+ json_err "$E_VALIDATION_FAILED" "state-mutate requires a jq expression argument"
107
+ fi
108
+
109
+ sm_state_file="$DATA_DIR/COLONY_STATE.json"
110
+
111
+ if [[ ! -f "$sm_state_file" ]]; then
112
+ json_err "$E_FILE_NOT_FOUND" "COLONY_STATE.json not found" '{"file":"COLONY_STATE.json"}'
113
+ fi
114
+
115
+ # Acquire lock for safe read-modify-write
116
+ acquire_lock "$sm_state_file" || json_err "$E_LOCK_FAILED" "Failed to acquire lock on COLONY_STATE.json"
117
+
118
+ # Create backup before mutation
119
+ if type create_backup &>/dev/null; then
120
+ if ! create_backup "$sm_state_file"; then
121
+ _aether_log_error "Could not create backup of colony state before mutation"
122
+ fi
123
+ fi
124
+
125
+ # Apply jq expression to current state
126
+ sm_updated=$(jq "$sm_expr" "$sm_state_file" 2>/dev/null) || {
127
+ release_lock 2>/dev/null || true # SUPPRESS:OK -- cleanup: lock may not be held
128
+ json_err "$E_JSON_INVALID" "jq expression failed: $sm_expr"
129
+ }
130
+
131
+ # Validate the result is valid JSON
132
+ if [[ -z "$sm_updated" ]] || ! echo "$sm_updated" | jq -e . >/dev/null 2>&1; then
133
+ release_lock 2>/dev/null || true # SUPPRESS:OK -- cleanup: lock may not be held
134
+ json_err "$E_JSON_INVALID" "state-mutate produced invalid JSON"
135
+ fi
136
+
137
+ # Write atomically
138
+ atomic_write "$sm_state_file" "$sm_updated" || {
139
+ release_lock 2>/dev/null || true # SUPPRESS:OK -- cleanup: lock may not be held
140
+ json_err "$E_UNKNOWN" "Failed to write mutated COLONY_STATE.json"
141
+ }
142
+
143
+ release_lock 2>/dev/null || true # SUPPRESS:OK -- cleanup: lock may not be held
144
+
145
+ json_ok '{"mutated":true}'
146
+ }
147
+
148
+ _state_migrate() {
149
+ # Schema migration helper: auto-upgrades pre-3.0 state files to v3.0
150
+ # Additive only (never removes fields) -- idempotent and safe for concurrent access
151
+ # Moved from validate-state case block for reuse
152
+
153
+ sm_state_file="${1:-}"
154
+ [[ -f "$sm_state_file" ]] || return 0
155
+
156
+ # First: verify file is parseable JSON at all
157
+ if ! jq -e . "$sm_state_file" >/dev/null 2>&1; then # SUPPRESS:OK -- validation: testing JSON validity
158
+ # Corrupt state file -- backup and error
159
+ if type create_backup &>/dev/null; then
160
+ if ! create_backup "$sm_state_file"; then
161
+ _aether_log_error "Could not create backup of corrupted COLONY_STATE.json"
162
+ fi
163
+ fi
164
+ json_err "$E_JSON_INVALID" \
165
+ "COLONY_STATE.json is corrupted (invalid JSON). A backup was saved in .aether/data/backups/. Try: run /ant:init to reset colony state."
166
+ fi
167
+
168
+ sm_current_version=$(jq -r '.version // "1.0"' "$sm_state_file" 2>/dev/null) # SUPPRESS:OK -- read-default: file may not exist yet
169
+
170
+ if [[ "$sm_current_version" != "3.0" ]]; then
171
+ sm_lock_held=false
172
+ # Skip lock acquisition when caller already holds the state lock
173
+ if [[ "${AETHER_STATE_LOCKED:-false}" != "true" ]] && type acquire_lock &>/dev/null; then
174
+ acquire_lock "$sm_state_file" || json_err "$E_LOCK_FAILED" "Failed to acquire lock on COLONY_STATE.json for migration"
175
+ sm_lock_held=true
176
+ fi
177
+
178
+ # Add missing v3.0 fields (additive only -- idempotent and safe for concurrent access)
179
+ sm_updated=$(jq '
180
+ .version = "3.0" |
181
+ if .signals == null then .signals = [] else . end |
182
+ if .graveyards == null then .graveyards = [] else . end |
183
+ if .events == null then .events = [] else . end
184
+ ' "$sm_state_file" 2>/dev/null) || { # SUPPRESS:OK -- read-default: file may not exist yet
185
+ [[ "$sm_lock_held" == "true" ]] && release_lock 2>/dev/null || true # SUPPRESS:OK -- cleanup: lock may not be held
186
+ json_err "$E_JSON_INVALID" "Failed to migrate COLONY_STATE.json"
187
+ }
188
+
189
+ if [[ -n "$sm_updated" ]]; then
190
+ atomic_write "$sm_state_file" "$sm_updated" || {
191
+ [[ "$sm_lock_held" == "true" ]] && release_lock 2>/dev/null || true # SUPPRESS:OK -- cleanup: lock may not be held
192
+ json_err "$E_JSON_INVALID" "Failed to write migrated COLONY_STATE.json"
193
+ }
194
+ # Notify user of migration (auto-migrate + notify pattern)
195
+ printf '{"ok":true,"warning":"W_MIGRATED","message":"Migrated colony state from v%s to v3.0"}\n' "$sm_current_version" >&2
196
+ fi
197
+
198
+ [[ "$sm_lock_held" == "true" ]] && release_lock 2>/dev/null || true # SUPPRESS:OK -- cleanup: lock may not be held
199
+ fi
200
+ }
201
+
202
+ # ============================================================================
203
+ # _colony_vital_signs
204
+ # Compute colony health metrics from existing data files
205
+ # Usage: colony-vital-signs
206
+ # Returns: JSON with build_velocity, error_rate, signal_health, memory_pressure,
207
+ # colony_age_hours, and overall_health (0-100)
208
+ # Gracefully degrades: missing files produce zero/default values
209
+ # ============================================================================
210
+ _colony_vital_signs() {
211
+ local cvs_state_file="$COLONY_DATA_DIR/COLONY_STATE.json"
212
+ local cvs_midden_file="$COLONY_DATA_DIR/midden/midden.json"
213
+ local cvs_phero_file="$COLONY_DATA_DIR/pheromones.json"
214
+ local cvs_session_file="$COLONY_DATA_DIR/session.json"
215
+
216
+ # --- Compute 24h window boundary ---
217
+ local cvs_now
218
+ cvs_now=$(date -u +%s 2>/dev/null || echo "0")
219
+ local cvs_window_start=$(( cvs_now - 86400 ))
220
+
221
+ # ---- build_velocity: count phase_completed events in last 24h ----
222
+ local cvs_phases_per_day=0
223
+ if [[ -f "$cvs_state_file" ]]; then
224
+ cvs_phases_per_day=$(jq --argjson win "$cvs_window_start" '
225
+ [.events[]? |
226
+ select(. != null) |
227
+ select(test("\\|phase_completed\\|")) |
228
+ capture("^(?P<ts>[^|]+)\\|") |
229
+ .ts |
230
+ gsub("[TZ:-]"; " ") |
231
+ split(" ") |
232
+ if length >= 6 then
233
+ (.[0:6] | join(" ")) |
234
+ # convert to comparable string for ordering -- full ISO compare
235
+ . as $s | $s
236
+ else . end
237
+ ] | length
238
+ ' "$cvs_state_file" 2>/dev/null || echo "0")
239
+
240
+ # Simpler approach: use string comparison on ISO timestamps
241
+ # Compute the 24h-ago timestamp as ISO string
242
+ local cvs_cutoff_iso
243
+ cvs_cutoff_iso=$(date -u -r "$cvs_window_start" '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null \
244
+ || date -u -d "@$cvs_window_start" '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null \
245
+ || echo "")
246
+
247
+ if [[ -n "$cvs_cutoff_iso" ]]; then
248
+ cvs_phases_per_day=$(jq --arg cutoff "$cvs_cutoff_iso" '
249
+ [.events[]? |
250
+ select(. != null and (type == "string")) |
251
+ select(test("\\|phase_completed\\|")) |
252
+ split("|") | .[0] |
253
+ select(. >= $cutoff)
254
+ ] | length
255
+ ' "$cvs_state_file" 2>/dev/null || echo "0")
256
+ fi
257
+ fi
258
+ # Normalize: ensure integer
259
+ cvs_phases_per_day=$(( cvs_phases_per_day + 0 )) 2>/dev/null || cvs_phases_per_day=0
260
+
261
+ # Determine trend (simple heuristic: any builds = steady, 0 = idle)
262
+ local cvs_bv_trend="idle"
263
+ [[ "$cvs_phases_per_day" -ge 1 ]] && cvs_bv_trend="steady"
264
+ [[ "$cvs_phases_per_day" -ge 3 ]] && cvs_bv_trend="accelerating"
265
+
266
+ # ---- error_rate: unreviewed midden entries in last 24h ----
267
+ local cvs_errors_per_day=0
268
+ local cvs_err_status="clean"
269
+ if [[ -f "$cvs_midden_file" ]]; then
270
+ local cvs_cutoff_iso_err
271
+ cvs_cutoff_iso_err=$(date -u -r "$cvs_window_start" '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null \
272
+ || date -u -d "@$cvs_window_start" '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null \
273
+ || echo "")
274
+
275
+ if [[ -n "$cvs_cutoff_iso_err" ]]; then
276
+ cvs_errors_per_day=$(jq --arg cutoff "$cvs_cutoff_iso_err" '
277
+ [(.entries // [])[]? |
278
+ select(.reviewed == false or .reviewed == null) |
279
+ select((.timestamp // "") >= $cutoff)
280
+ ] | length
281
+ ' "$cvs_midden_file" 2>/dev/null || echo "0")
282
+ else
283
+ # Fallback: count all unreviewed
284
+ cvs_errors_per_day=$(jq '
285
+ [(.entries // [])[]? | select(.reviewed == false or .reviewed == null)] | length
286
+ ' "$cvs_midden_file" 2>/dev/null || echo "0")
287
+ fi
288
+ fi
289
+ cvs_errors_per_day=$(( cvs_errors_per_day + 0 )) 2>/dev/null || cvs_errors_per_day=0
290
+
291
+ if [[ "$cvs_errors_per_day" -eq 0 ]]; then
292
+ cvs_err_status="clean"
293
+ elif [[ "$cvs_errors_per_day" -le 2 ]]; then
294
+ cvs_err_status="nominal"
295
+ elif [[ "$cvs_errors_per_day" -le 5 ]]; then
296
+ cvs_err_status="elevated"
297
+ else
298
+ cvs_err_status="critical"
299
+ fi
300
+
301
+ # ---- signal_health: count active pheromones ----
302
+ local cvs_active_count=0
303
+ local cvs_sig_status="dormant"
304
+ if [[ -f "$cvs_phero_file" ]]; then
305
+ cvs_active_count=$(jq '
306
+ [.signals[]? | select(.active == true)] | length
307
+ ' "$cvs_phero_file" 2>/dev/null || echo "0")
308
+ fi
309
+ cvs_active_count=$(( cvs_active_count + 0 )) 2>/dev/null || cvs_active_count=0
310
+
311
+ if [[ "$cvs_active_count" -eq 0 ]]; then
312
+ cvs_sig_status="dormant"
313
+ elif [[ "$cvs_active_count" -le 3 ]]; then
314
+ cvs_sig_status="guided"
315
+ else
316
+ cvs_sig_status="active"
317
+ fi
318
+
319
+ # ---- memory_pressure: count instincts ----
320
+ local cvs_instinct_count=0
321
+ local cvs_mem_status="empty"
322
+ if [[ -f "$cvs_state_file" ]]; then
323
+ # instincts may be a JSON string (serialized array) or a real array
324
+ local cvs_raw_instincts
325
+ cvs_raw_instincts=$(jq -r '.memory.instincts // "[]"' "$cvs_state_file" 2>/dev/null || echo "[]")
326
+ # Handle both string-encoded and native array
327
+ cvs_instinct_count=$(echo "$cvs_raw_instincts" | jq -r 'if type == "string" then (. | fromjson | length) elif type == "array" then length else 0 end' 2>/dev/null || echo "0")
328
+ fi
329
+ cvs_instinct_count=$(( cvs_instinct_count + 0 )) 2>/dev/null || cvs_instinct_count=0
330
+
331
+ if [[ "$cvs_instinct_count" -eq 0 ]]; then
332
+ cvs_mem_status="empty"
333
+ elif [[ "$cvs_instinct_count" -le 5 ]]; then
334
+ cvs_mem_status="growing"
335
+ elif [[ "$cvs_instinct_count" -le 15 ]]; then
336
+ cvs_mem_status="healthy"
337
+ else
338
+ cvs_mem_status="rich"
339
+ fi
340
+
341
+ # ---- colony_age_hours: hours since initialized_at ----
342
+ local cvs_age_hours=0
343
+ if [[ -f "$cvs_state_file" ]]; then
344
+ local cvs_init_at
345
+ cvs_init_at=$(jq -r '.initialized_at // empty' "$cvs_state_file" 2>/dev/null || echo "")
346
+ if [[ -n "$cvs_init_at" ]]; then
347
+ local cvs_init_ts
348
+ cvs_init_ts=$(date -u -j -f '%Y-%m-%dT%H:%M:%SZ' "$cvs_init_at" '+%s' 2>/dev/null \
349
+ || date -u -d "$cvs_init_at" '+%s' 2>/dev/null \
350
+ || echo "0")
351
+ if [[ "$cvs_init_ts" -gt 0 && "$cvs_now" -gt "$cvs_init_ts" ]]; then
352
+ cvs_age_hours=$(( (cvs_now - cvs_init_ts) / 3600 ))
353
+ fi
354
+ fi
355
+ fi
356
+
357
+ # ---- overall_health: weighted 0-100 score ----
358
+ # Components (max points each):
359
+ # recent builds (+30): has at least one phase_completed in 24h
360
+ # low errors (+30): zero unreviewed errors in 24h
361
+ # signals exist (+20): at least one active pheromone
362
+ # instincts growing (+20): at least one instinct
363
+ local cvs_score=0
364
+ [[ "$cvs_phases_per_day" -ge 1 ]] && cvs_score=$(( cvs_score + 30 ))
365
+ [[ "$cvs_errors_per_day" -eq 0 ]] && cvs_score=$(( cvs_score + 30 ))
366
+ [[ "$cvs_active_count" -ge 1 ]] && cvs_score=$(( cvs_score + 20 ))
367
+ [[ "$cvs_instinct_count" -ge 1 ]] && cvs_score=$(( cvs_score + 20 ))
368
+ [[ "$cvs_score" -gt 100 ]] && cvs_score=100
369
+
370
+ json_ok "$(jq -n \
371
+ --argjson phases_per_day "$cvs_phases_per_day" \
372
+ --arg bv_trend "$cvs_bv_trend" \
373
+ --argjson errors_per_day "$cvs_errors_per_day" \
374
+ --arg err_status "$cvs_err_status" \
375
+ --argjson active_count "$cvs_active_count" \
376
+ --arg sig_status "$cvs_sig_status" \
377
+ --argjson instinct_count "$cvs_instinct_count" \
378
+ --arg mem_status "$cvs_mem_status" \
379
+ --argjson age_hours "$cvs_age_hours" \
380
+ --argjson overall_health "$cvs_score" \
381
+ '{
382
+ build_velocity: {phases_per_day: $phases_per_day, trend: $bv_trend},
383
+ error_rate: {errors_per_day: $errors_per_day, status: $err_status},
384
+ signal_health: {active_count: $active_count, status: $sig_status},
385
+ memory_pressure: {instinct_count: $instinct_count, status: $mem_status},
386
+ colony_age_hours: $age_hours,
387
+ overall_health: $overall_health
388
+ }')"
389
+ }
@@ -10,11 +10,13 @@
10
10
  #
11
11
  # Provides: load_colony_state, unload_colony_state, get_handoff_summary, display_resumption_context
12
12
 
13
- # Aether root detection - use git root if available, otherwise use current directory
14
- if git rev-parse --show-toplevel >/dev/null 2>&1; then
15
- AETHER_ROOT="$(git rev-parse --show-toplevel)"
16
- else
17
- AETHER_ROOT="$(pwd)"
13
+ # Aether root detection - respect existing AETHER_ROOT, or use git root, or use current directory
14
+ if [[ -z "${AETHER_ROOT:-}" ]]; then
15
+ if git rev-parse --show-toplevel >/dev/null 2>&1; then
16
+ AETHER_ROOT="$(git rev-parse --show-toplevel)"
17
+ else
18
+ AETHER_ROOT="$(pwd)"
19
+ fi
18
20
  fi
19
21
 
20
22
  SCRIPT_DIR="${SCRIPT_DIR:-$AETHER_ROOT/.aether}"
@@ -70,7 +72,7 @@ load_colony_state() {
70
72
 
71
73
  # Validate state before loading
72
74
  local validation
73
- validation=$(bash "$SCRIPT_DIR/aether-utils.sh" validate-state colony 2>/dev/null)
75
+ validation=$(AETHER_STATE_LOCKED=true bash "$SCRIPT_DIR/aether-utils.sh" validate-state colony 2>/dev/null)
74
76
  if ! echo "$validation" | jq -e '.result.pass' >/dev/null 2>&1; then
75
77
  # Validation failed - release lock and report error
76
78
  if type release_lock &>/dev/null; then