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,816 @@
1
+ #!/usr/bin/env bash
2
+ # Session utility functions -- extracted from aether-utils.sh
3
+ # Provides: _session_verify_fresh, _session_clear, _session_init, _session_update,
4
+ # _session_read, _session_is_stale, _session_clear_context,
5
+ # _session_mark_resumed, _session_summary
6
+ # Also includes: _rotate_spawn_tree (helper used only by _session_init)
7
+
8
+ # ============================================================================
9
+ # _session_verify_fresh
10
+ # Generic session freshness verification
11
+ # Usage: _session_verify_fresh [args...] (same args as before: --command <name> [--force] <session_start_unixtime>)
12
+ # Returns: JSON with pass/fail status and file details
13
+ # ============================================================================
14
+ _session_verify_fresh() {
15
+ # Parse arguments
16
+ local command_name=""
17
+ local force_mode=""
18
+ local session_start_time=""
19
+
20
+ while [[ $# -gt 0 ]]; do
21
+ case "$1" in
22
+ --command) command_name="$2"; shift 2 ;;
23
+ --force) force_mode="--force"; shift ;;
24
+ *) session_start_time="$1"; shift ;;
25
+ esac
26
+ done
27
+
28
+ # Validate command name
29
+ [[ -z "$command_name" ]] && json_err "$E_VALIDATION_FAILED" "Usage: session-verify-fresh --command <name> [--force] <session_start>"
30
+
31
+ # Map command to directory and files (using env var override pattern)
32
+ local session_dir required_docs
33
+ case "$command_name" in
34
+ survey)
35
+ session_dir="${SURVEY_DIR:-.aether/data/survey}"
36
+ required_docs="PROVISIONS.md TRAILS.md BLUEPRINT.md CHAMBERS.md DISCIPLINES.md SENTINEL-PROTOCOLS.md PATHOGENS.md"
37
+ ;;
38
+ oracle)
39
+ session_dir="${ORACLE_DIR:-.aether/oracle}"
40
+ required_docs="state.json plan.json gaps.md synthesis.md research-plan.md"
41
+ ;;
42
+ watch)
43
+ session_dir="${WATCH_DIR:-.aether/data}"
44
+ required_docs="watch-status.txt watch-progress.txt"
45
+ ;;
46
+ swarm)
47
+ session_dir="${SWARM_DIR:-.aether/data/swarm}"
48
+ required_docs="findings.json"
49
+ ;;
50
+ init)
51
+ session_dir="${INIT_DIR:-.aether/data}"
52
+ required_docs="COLONY_STATE.json constraints.json"
53
+ ;;
54
+ seal|entomb)
55
+ session_dir="${ARCHIVE_DIR:-.aether/data/archive}"
56
+ required_docs="manifest.json"
57
+ ;;
58
+ *)
59
+ json_err "$E_VALIDATION_FAILED" "Unknown command: $command_name" '{"commands":["survey","oracle","watch","swarm","init","seal","entomb"]}'
60
+ ;;
61
+ esac
62
+
63
+ # Initialize result arrays
64
+ local fresh_docs=""
65
+ local stale_docs=""
66
+ local missing_docs=""
67
+ local total_lines=0
68
+
69
+ for doc in $required_docs; do
70
+ local doc_path="$session_dir/$doc"
71
+
72
+ if [[ ! -f "$doc_path" ]]; then
73
+ missing_docs="${missing_docs:+$missing_docs }$doc"
74
+ continue
75
+ fi
76
+
77
+ # Get line count
78
+ local lines
79
+ lines=$(wc -l < "$doc_path" 2>/dev/null | tr -d ' ' || echo "0") # SUPPRESS:OK -- read-default: file may not exist
80
+ total_lines=$((total_lines + lines))
81
+
82
+ # In force mode, accept any existing file
83
+ if [[ "$force_mode" == "--force" ]]; then
84
+ fresh_docs="${fresh_docs:+$fresh_docs }$doc"
85
+ continue
86
+ fi
87
+
88
+ # Check timestamp if session_start_time provided
89
+ if [[ -n "$session_start_time" ]]; then
90
+ # Cross-platform stat: macOS uses -f %m, Linux uses -c %Y
91
+ local file_mtime
92
+ file_mtime=$(stat -f %m "$doc_path" 2>/dev/null || stat -c %Y "$doc_path" 2>/dev/null || echo "0") # SUPPRESS:OK -- cross-platform: macOS stat syntax
93
+
94
+ if [[ "$file_mtime" -ge "$session_start_time" ]]; then
95
+ fresh_docs="${fresh_docs:+$fresh_docs }$doc"
96
+ else
97
+ stale_docs="${stale_docs:+$stale_docs }$doc"
98
+ fi
99
+ else
100
+ # No start time provided - accept existing file (backward compatible)
101
+ fresh_docs="${fresh_docs:+$fresh_docs }$doc"
102
+ fi
103
+ done
104
+
105
+ # Determine pass/fail
106
+ # pass = true if: no stale files (fresh files can coexist with missing files)
107
+ # missing files are ok - they will be created during the session
108
+ local pass=false
109
+ if [[ "$force_mode" == "--force" ]] || [[ -z "$stale_docs" ]]; then
110
+ pass=true
111
+ fi
112
+
113
+ # Build JSON response
114
+ local fresh_json=""
115
+ for item in $fresh_docs; do fresh_json="$fresh_json\"$item\","; done
116
+ fresh_json="[${fresh_json%,}]"
117
+
118
+ local stale_json=""
119
+ for item in $stale_docs; do stale_json="$stale_json\"$item\","; done
120
+ stale_json="[${stale_json%,}]"
121
+
122
+ local missing_json=""
123
+ for item in $missing_docs; do missing_json="$missing_json\"$item\","; done
124
+ missing_json="[${missing_json%,}]"
125
+
126
+ echo "$(jq -n --argjson ok "$pass" --arg command "$command_name" \
127
+ --argjson fresh "$fresh_json" --argjson stale "$stale_json" \
128
+ --argjson missing "$missing_json" --argjson total_lines "$total_lines" \
129
+ '{ok: $ok, command: $command, fresh: $fresh, stale: $stale, missing: $missing, total_lines: $total_lines}')"
130
+ exit 0
131
+ }
132
+
133
+ # ============================================================================
134
+ # _session_clear
135
+ # Generic session file clearing
136
+ # Usage: _session_clear [args...] (same args as before: --command <name> [--dry-run])
137
+ # ============================================================================
138
+ _session_clear() {
139
+ # Parse arguments
140
+ local command_name=""
141
+ local dry_run=""
142
+
143
+ while [[ $# -gt 0 ]]; do
144
+ case "$1" in
145
+ --command) command_name="$2"; shift 2 ;;
146
+ --dry-run) dry_run="--dry-run"; shift ;;
147
+ *) shift ;;
148
+ esac
149
+ done
150
+
151
+ [[ -z "$command_name" ]] && json_err "$E_VALIDATION_FAILED" "Usage: session-clear --command <name> [--dry-run]"
152
+
153
+ # Map command to directory and files
154
+ local session_dir="" files="" subdir_files=""
155
+ case "$command_name" in
156
+ survey)
157
+ session_dir="${SURVEY_DIR:-.aether/data/survey}"
158
+ files="PROVISIONS.md TRAILS.md BLUEPRINT.md CHAMBERS.md DISCIPLINES.md SENTINEL-PROTOCOLS.md PATHOGENS.md"
159
+ ;;
160
+ oracle)
161
+ session_dir="${ORACLE_DIR:-.aether/oracle}"
162
+ files="state.json plan.json gaps.md synthesis.md research-plan.md .stop .last-topic"
163
+ # Also clear discoveries subdirectory
164
+ subdir_files="discoveries/*"
165
+ ;;
166
+ watch)
167
+ session_dir="${WATCH_DIR:-.aether/data}"
168
+ files="watch-status.txt watch-progress.txt"
169
+ ;;
170
+ swarm)
171
+ session_dir="${SWARM_DIR:-.aether/data/swarm}"
172
+ files="findings.json display.json timing.json"
173
+ ;;
174
+ init)
175
+ # Init clear is destructive - blocked for auto-clear
176
+ json_err "$E_VALIDATION_FAILED" "Command 'init' is protected and cannot be auto-cleared. Use manual removal of COLONY_STATE.json if absolutely necessary."
177
+ ;;
178
+ seal|entomb)
179
+ # Archive operations should never be auto-cleared
180
+ json_err "$E_VALIDATION_FAILED" "Command '$command_name' is protected and cannot be auto-cleared. Archives and chambers must be managed manually."
181
+ ;;
182
+ *)
183
+ json_err "$E_VALIDATION_FAILED" "Unknown command: $command_name"
184
+ ;;
185
+ esac
186
+
187
+ local cleared=""
188
+ local errors=""
189
+
190
+ if [[ -d "$session_dir" && -n "$files" ]]; then
191
+ for doc in $files; do
192
+ local doc_path="$session_dir/$doc"
193
+ if [[ -f "$doc_path" ]]; then
194
+ if [[ "$dry_run" == "--dry-run" ]]; then
195
+ cleared="$cleared $doc"
196
+ else
197
+ if rm -f "$doc_path" 2>/dev/null; then # SUPPRESS:OK -- cleanup: file may not exist
198
+ cleared="$cleared $doc"
199
+ else
200
+ errors="$errors $doc"
201
+ fi
202
+ fi
203
+ fi
204
+ done
205
+
206
+ # Handle oracle discoveries subdirectory
207
+ if [[ "$command_name" == "oracle" && -d "$session_dir/discoveries" ]]; then
208
+ if [[ "$dry_run" == "--dry-run" ]]; then
209
+ cleared="$cleared discoveries/"
210
+ else
211
+ # SUPPRESS:OK -- cleanup: file may not exist
212
+ rm -rf "$session_dir/discoveries" 2>/dev/null && cleared="$cleared discoveries/" || errors="$errors discoveries/"
213
+ fi
214
+ fi
215
+ fi
216
+
217
+ local dry_run_bool=$([[ "$dry_run" == "--dry-run" ]] && echo "true" || echo "false")
218
+ json_ok "$(jq -n --arg command "$command_name" --arg cleared "${cleared// /}" \
219
+ --arg errors "${errors// /}" --argjson dry_run "$dry_run_bool" \
220
+ '{command: $command, cleared: $cleared, errors: $errors, dry_run: $dry_run}')"
221
+ }
222
+
223
+ # ============================================================================
224
+ # _rotate_spawn_tree (helper -- used only by _session_init)
225
+ # ARCH-03: Rotate spawn-tree.txt at session start to prevent unbounded growth.
226
+ # Archives previous session's tree to a timestamped file; caps archive count at 5.
227
+ # ============================================================================
228
+ _rotate_spawn_tree() {
229
+ local tree_file="$COLONY_DATA_DIR/spawn-tree.txt"
230
+ [[ -f "$tree_file" ]] && [[ -s "$tree_file" ]] || return 0
231
+ mkdir -p "$COLONY_DATA_DIR/spawn-tree-archive"
232
+ local archive_ts
233
+ archive_ts=$(date +%Y%m%d_%H%M%S)
234
+ if ! cp "$tree_file" "$COLONY_DATA_DIR/spawn-tree-archive/spawn-tree.${archive_ts}.txt" 2>/dev/null; then # SUPPRESS:OK -- cleanup: backup copy is best-effort
235
+ _aether_log_error "Could not archive spawn-tree before rotation"
236
+ fi
237
+ > "$tree_file" # Truncate in-place — preserves file handle for tail -f watchers
238
+ # Keep only 5 archives
239
+ # SUPPRESS:OK -- read-default: directory may not exist
240
+ # SUPPRESS:OK -- cleanup: rotation cleanup is best-effort
241
+ ls -t "$COLONY_DATA_DIR/spawn-tree-archive"/spawn-tree.*.txt 2>/dev/null \
242
+ | tail -n +6 | while IFS= read -r file; do rm -f "$file"; done 2>/dev/null || true # SUPPRESS:OK -- cleanup: file may not exist
243
+ }
244
+
245
+ # ============================================================================
246
+ # _session_init
247
+ # Initialize a new session tracking file
248
+ # Usage: _session_init [session_id] [goal]
249
+ # ============================================================================
250
+ _session_init() {
251
+ local session_id="${1:-$(date +%s)_$(openssl rand -hex 4 2>/dev/null || echo $$)}" # SUPPRESS:OK -- read-default: openssl may not be available
252
+ local goal="${2:-}"
253
+
254
+ _rotate_spawn_tree
255
+
256
+ local session_file="$COLONY_DATA_DIR/session.json"
257
+ local baseline
258
+ baseline=$(git rev-parse HEAD 2>/dev/null || echo "") # SUPPRESS:OK -- read-default: may not have commits yet
259
+
260
+ jq -n --arg sid "$session_id" --arg started "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
261
+ --arg goal "$goal" --arg baseline "$baseline" \
262
+ '{
263
+ session_id: $sid,
264
+ started_at: $started,
265
+ last_command: null,
266
+ last_command_at: null,
267
+ colony_goal: $goal,
268
+ current_phase: 0,
269
+ current_milestone: "First Mound",
270
+ suggested_next: "/ant:plan",
271
+ context_cleared: false,
272
+ baseline_commit: $baseline,
273
+ resumed_at: null,
274
+ active_todos: [],
275
+ summary: "Session initialized"
276
+ }' > "$session_file.tmp"
277
+ mv "$session_file.tmp" "$session_file"
278
+ json_ok "$(jq -n --arg sid "$session_id" --arg goal "$goal" --arg file "$session_file" \
279
+ '{session_id: $sid, goal: $goal, file: $file}')"
280
+ }
281
+
282
+ # ============================================================================
283
+ # _session_update
284
+ # Update session with latest activity
285
+ # Usage: _session_update <command> [suggested_next] [summary]
286
+ # ============================================================================
287
+ _session_update() {
288
+ local cmd_run="${1:-}"
289
+ local suggested="${2:-}"
290
+ local summary="${3:-}"
291
+
292
+ local session_file="$COLONY_DATA_DIR/session.json"
293
+
294
+ if [[ ! -f "$session_file" ]]; then
295
+ # Auto-initialize if doesn't exist
296
+ bash "$SCRIPT_DIR/aether-utils.sh" session-init "auto_$(date +%s)" ""
297
+ fi
298
+
299
+ # Read current session
300
+ local current_session
301
+ current_session=$(cat "$session_file" 2>/dev/null || echo '{}') # SUPPRESS:OK -- read-default: file may not exist yet
302
+
303
+ # Extract current values for preservation
304
+ local current_goal current_phase current_milestone
305
+ current_goal=$(sanitize_read_value "$(echo "$current_session" | jq -r '.colony_goal // empty')")
306
+ current_phase=$(echo "$current_session" | jq -r '.current_phase // 0')
307
+ current_milestone=$(echo "$current_session" | jq -r '.current_milestone // "First Mound"')
308
+
309
+ # Get top 3 TODOs if TO-DOs.md exists
310
+ local todos="[]"
311
+ if [[ -f "TO-DOs.md" ]]; then
312
+ todos=$(grep "^### " TO-DOs.md 2>/dev/null | head -3 | sed 's/^### //' | jq -R . | jq -s .) # SUPPRESS:OK -- existence-test: file may not exist
313
+ fi
314
+
315
+ # MIGRATE: direct COLONY_STATE.json access -- use _state_read_field instead
316
+ # Get colony state if exists
317
+ if [[ -f "$DATA_DIR/COLONY_STATE.json" ]]; then
318
+ # SUPPRESS:OK -- read-default: query may return empty
319
+ current_goal=$(sanitize_read_value "$(jq -r '.goal // empty' "$DATA_DIR/COLONY_STATE.json" 2>/dev/null || echo "$current_goal")")
320
+ # SUPPRESS:OK -- read-default: query may return empty
321
+ current_phase=$(jq -r '.current_phase // 0' "$DATA_DIR/COLONY_STATE.json" 2>/dev/null || echo "$current_phase")
322
+ # SUPPRESS:OK -- read-default: query may return empty
323
+ current_milestone=$(jq -r '.milestone // "First Mound"' "$DATA_DIR/COLONY_STATE.json" 2>/dev/null || echo "$current_milestone")
324
+ fi
325
+
326
+ # Capture current git HEAD for drift detection
327
+ local baseline
328
+ baseline=$(git rev-parse HEAD 2>/dev/null || echo "") # SUPPRESS:OK -- read-default: may not have commits yet
329
+
330
+ # Build updated session
331
+ echo "$current_session" | jq --arg cmd "$cmd_run" \
332
+ --arg ts "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
333
+ --arg suggested "$suggested" \
334
+ --arg summary "$summary" \
335
+ --arg goal "$current_goal" \
336
+ --argjson phase "$current_phase" \
337
+ --arg milestone "$current_milestone" \
338
+ --argjson todos "$todos" \
339
+ --arg baseline "$baseline" \
340
+ '.last_command = $cmd |
341
+ .last_command_at = $ts |
342
+ .suggested_next = $suggested |
343
+ .summary = $summary |
344
+ .colony_goal = $goal |
345
+ .current_phase = $phase |
346
+ .current_milestone = $milestone |
347
+ .active_todos = $todos |
348
+ .baseline_commit = $baseline' > "$session_file.tmp" || {
349
+ _aether_log_error "Could not process session update"
350
+ rm -f "$session_file.tmp"
351
+ json_err "$E_UNKNOWN" "Failed to update session file"
352
+ }
353
+ [[ -s "$session_file.tmp" ]] || {
354
+ _aether_log_error "Session update produced empty result -- not overwriting"
355
+ rm -f "$session_file.tmp"
356
+ json_err "$E_JSON_INVALID" "Session update produced empty result"
357
+ }
358
+ mv "$session_file.tmp" "$session_file" || {
359
+ _aether_log_error "Could not finalize session file update"
360
+ rm -f "$session_file.tmp"
361
+ json_err "$E_UNKNOWN" "Failed to rename temporary session file"
362
+ }
363
+
364
+ json_ok "$(jq -n --arg cmd "$cmd_run" '{updated: true, command: $cmd}')"
365
+ }
366
+
367
+ # ============================================================================
368
+ # _session_read
369
+ # Read and return current session state
370
+ # ============================================================================
371
+ _session_read() {
372
+ local session_file="$COLONY_DATA_DIR/session.json"
373
+
374
+ if [[ ! -f "$session_file" ]]; then
375
+ json_ok "{\"exists\":false,\"session\":null}"
376
+ exit 0
377
+ fi
378
+
379
+ local session_data
380
+ session_data=$(cat "$session_file" 2>/dev/null || echo '{}') # SUPPRESS:OK -- read-default: file may not exist yet
381
+
382
+ # Check if stale (> 24 hours)
383
+ local last_cmd_ts="" is_stale="" age_hours=""
384
+ last_cmd_ts=$(echo "$session_data" | jq -r '.last_command_at // .started_at // empty')
385
+ if [[ -n "$last_cmd_ts" ]]; then
386
+ local last_epoch=0 now_epoch=0
387
+ # SUPPRESS:OK -- cross-platform: macOS date syntax
388
+ # SUPPRESS:OK -- cross-platform: macOS vs Linux date/stat flags
389
+ last_epoch=$(date -j -f "%Y-%m-%dT%H:%M:%SZ" "$last_cmd_ts" +%s 2>/dev/null \
390
+ || date -d "$last_cmd_ts" +%s 2>/dev/null \
391
+ || echo 0)
392
+ now_epoch=$(date +%s)
393
+ age_hours=$(( (now_epoch - last_epoch) / 3600 ))
394
+ [[ $age_hours -gt 24 ]] && is_stale=true || is_stale=false
395
+ else
396
+ is_stale="false"
397
+ age_hours="unknown"
398
+ fi
399
+
400
+ json_ok "$(jq -n --argjson is_stale "$is_stale" --argjson age "$age_hours" \
401
+ --argjson session "$session_data" \
402
+ '{exists: true, is_stale: $is_stale, age_hours: $age, session: $session}')"
403
+ }
404
+
405
+ # ============================================================================
406
+ # _session_is_stale
407
+ # Check if session is stale (returns JSON with is_stale boolean)
408
+ # ============================================================================
409
+ _session_is_stale() {
410
+ _deprecation_warning "session-is-stale"
411
+ local session_file="$COLONY_DATA_DIR/session.json"
412
+
413
+ if [[ ! -f "$session_file" ]]; then
414
+ json_ok '{"is_stale":true}'
415
+ exit 0
416
+ fi
417
+
418
+ local last_cmd_ts
419
+ last_cmd_ts=$(jq -r '.last_command_at // .started_at // empty' "$session_file" 2>/dev/null) # SUPPRESS:OK -- read-default: file may not exist yet
420
+
421
+ if [[ -z "$last_cmd_ts" ]]; then
422
+ json_ok '{"is_stale":true}'
423
+ exit 0
424
+ fi
425
+
426
+ # macOS uses -j -f, Linux uses -d
427
+ # SUPPRESS:OK -- cross-platform: macOS date syntax
428
+ # SUPPRESS:OK -- cross-platform: macOS vs Linux date/stat flags
429
+ local last_epoch now_epoch age_hours
430
+ last_epoch=$(date -j -f "%Y-%m-%dT%H:%M:%SZ" "$last_cmd_ts" +%s 2>/dev/null \
431
+ || date -d "$last_cmd_ts" +%s 2>/dev/null \
432
+ || echo 0)
433
+ now_epoch=$(date +%s)
434
+ age_hours=$(( (now_epoch - last_epoch) / 3600 ))
435
+
436
+ if [[ $age_hours -gt 24 ]]; then
437
+ json_ok '{"is_stale":true}'
438
+ else
439
+ json_ok '{"is_stale":false}'
440
+ fi
441
+ }
442
+
443
+ # ============================================================================
444
+ # _session_clear_context
445
+ # Mark session context as cleared (preserves file but marks context_cleared)
446
+ # ============================================================================
447
+ _session_clear_context() {
448
+ _deprecation_warning "session-clear-context"
449
+ local preserve="${1:-false}"
450
+ local session_file="$COLONY_DATA_DIR/session.json"
451
+
452
+ if [[ -f "$session_file" ]]; then
453
+ if [[ "$preserve" == "true" ]]; then
454
+ # Just mark as cleared
455
+ jq '.context_cleared = true' "$session_file" > "$session_file.tmp" || {
456
+ _aether_log_error "Could not mark session as cleared"
457
+ rm -f "$session_file.tmp"
458
+ }
459
+ if [[ -s "$session_file.tmp" ]]; then
460
+ mv "$session_file.tmp" "$session_file" || _aether_log_error "Could not finalize session clear"
461
+ fi
462
+ json_ok "{\"cleared\":true,\"preserved\":true}"
463
+ else
464
+ # Remove file entirely
465
+ rm -f "$session_file"
466
+ json_ok "{\"cleared\":true,\"preserved\":false}"
467
+ fi
468
+ else
469
+ json_ok "{\"cleared\":false,\"reason\":\"no_session_exists\"}"
470
+ fi
471
+ }
472
+
473
+ # ============================================================================
474
+ # _session_mark_resumed
475
+ # Mark session as resumed
476
+ # ============================================================================
477
+ _session_mark_resumed() {
478
+ local session_file="$COLONY_DATA_DIR/session.json"
479
+
480
+ if [[ -f "$session_file" ]]; then
481
+ jq --arg ts "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
482
+ '.resumed_at = $ts | .context_cleared = false' "$session_file" > "$session_file.tmp" || {
483
+ _aether_log_error "Could not process session resume update"
484
+ rm -f "$session_file.tmp"
485
+ }
486
+ if [[ -s "$session_file.tmp" ]]; then
487
+ mv "$session_file.tmp" "$session_file" || _aether_log_error "Could not finalize session resume"
488
+ fi
489
+ json_ok "$(jq -n --arg ts "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" '{resumed: true, timestamp: $ts}')"
490
+ else
491
+ json_err "$E_RESOURCE_NOT_FOUND" "No active session to mark as resumed. Try: run /ant:init to start a new session."
492
+ fi
493
+ }
494
+
495
+ # ============================================================================
496
+ # _session_summary
497
+ # Get session summary (human-readable or JSON)
498
+ # ============================================================================
499
+ _session_summary() {
500
+ _deprecation_warning "session-summary"
501
+ local session_file="$COLONY_DATA_DIR/session.json"
502
+ local json_mode="false"
503
+
504
+ # Parse --json flag (command name already shifted by main dispatch)
505
+ while [[ $# -gt 0 ]]; do
506
+ case "$1" in
507
+ --json)
508
+ json_mode="true"
509
+ shift
510
+ ;;
511
+ *)
512
+ shift
513
+ ;;
514
+ esac
515
+ done
516
+
517
+ if [[ ! -f "$session_file" ]]; then
518
+ if [[ "$json_mode" == "true" ]]; then
519
+ json_ok '{"exists":false,"goal":null,"phase":0}'
520
+ else
521
+ echo "No active session found."
522
+ fi
523
+ exit 0
524
+ fi
525
+
526
+ local goal phase milestone last_cmd last_at suggested cleared
527
+ goal=$(sanitize_read_value "$(jq -r '.colony_goal // "No goal set"' "$session_file")")
528
+ phase=$(jq -r '.current_phase // 0' "$session_file")
529
+ milestone=$(jq -r '.current_milestone // "First Mound"' "$session_file")
530
+ last_cmd=$(jq -r '.last_command // "None"' "$session_file")
531
+ last_at=$(jq -r '.last_command_at // "Unknown"' "$session_file")
532
+ suggested=$(jq -r '.suggested_next // "None"' "$session_file")
533
+ cleared=$(jq -r '.context_cleared // false' "$session_file")
534
+
535
+ if [[ "$json_mode" == "true" ]]; then
536
+ json_ok "$(jq -n --arg goal "$goal" --argjson phase "$phase" \
537
+ --arg milestone "$milestone" --arg last_cmd "$last_cmd" \
538
+ --arg last_at "$last_at" --arg suggested "$suggested" \
539
+ --argjson cleared "$cleared" \
540
+ '{exists: true, goal: $goal, phase: $phase, milestone: $milestone, last_command: $last_cmd, last_active: $last_at, suggested_next: $suggested, context_cleared: $cleared}')"
541
+ else
542
+ echo "Session Summary"
543
+ echo "=================="
544
+ echo "Goal: $goal"
545
+ [[ "$phase" != "0" ]] && echo "Phase: $phase"
546
+ echo "Milestone: $milestone"
547
+ echo "Last Command: $last_cmd"
548
+ echo "Last Active: $last_at"
549
+ [[ "$suggested" != "None" ]] && echo "Suggested Next: $suggested"
550
+ [[ "$cleared" == "true" ]] && echo "Status: Context was cleared"
551
+ fi
552
+ }
553
+
554
+ # ============================================================================
555
+ # _pending_decision_add
556
+ # Add a decision to the pending decisions queue
557
+ # Usage: pending-decision-add --type <type> --description <desc> [--phase N] [--source <src>]
558
+ # Types: visual_checkpoint, replan, escalation, runtime_verification, user_input
559
+ # ============================================================================
560
+ _pending_decision_add() {
561
+ local pd_type=""
562
+ local pd_description=""
563
+ local pd_phase="null"
564
+ local pd_source=""
565
+
566
+ while [[ $# -gt 0 ]]; do
567
+ case "$1" in
568
+ --type) pd_type="$2"; shift 2 ;;
569
+ --description) pd_description="$2"; shift 2 ;;
570
+ --phase) pd_phase="$2"; shift 2 ;;
571
+ --source) pd_source="$2"; shift 2 ;;
572
+ *) shift ;;
573
+ esac
574
+ done
575
+
576
+ [[ -z "$pd_type" ]] && json_err "$E_VALIDATION_FAILED" "pending-decision-add requires --type"
577
+ [[ -z "$pd_description" ]] && json_err "$E_VALIDATION_FAILED" "pending-decision-add requires --description"
578
+
579
+ local pd_file="$COLONY_DATA_DIR/pending-decisions.json"
580
+ local pd_id="pd_$(date +%s)_$$"
581
+ local pd_now
582
+ pd_now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
583
+
584
+ # Acquire lock for concurrent access
585
+ if type acquire_lock &>/dev/null; then
586
+ acquire_lock "$pd_file" || json_err "$E_LOCK_FAILED" "Failed to acquire lock on pending-decisions.json"
587
+ trap 'release_lock 2>/dev/null || true' EXIT # SUPPRESS:OK -- cleanup: lock may not be held
588
+ fi
589
+
590
+ # Initialize file if missing
591
+ if [[ ! -f "$pd_file" ]]; then
592
+ echo '{"version":"1.0","decisions":[]}' > "$pd_file"
593
+ fi
594
+
595
+ local pd_current
596
+ pd_current=$(cat "$pd_file" 2>/dev/null || echo '{"version":"1.0","decisions":[]}') # SUPPRESS:OK -- read-default: file may not exist yet
597
+
598
+ # Build new decision entry
599
+ local pd_phase_val
600
+ if [[ "$pd_phase" == "null" ]]; then
601
+ pd_phase_val="null"
602
+ else
603
+ pd_phase_val="$pd_phase"
604
+ fi
605
+
606
+ local pd_updated
607
+ pd_updated=$(echo "$pd_current" | jq \
608
+ --arg id "$pd_id" \
609
+ --arg type "$pd_type" \
610
+ --arg description "$pd_description" \
611
+ --argjson phase "${pd_phase_val}" \
612
+ --arg source "$pd_source" \
613
+ --arg created_at "$pd_now" \
614
+ '.decisions += [{
615
+ id: $id,
616
+ type: $type,
617
+ description: $description,
618
+ phase: $phase,
619
+ source: $source,
620
+ created_at: $created_at,
621
+ resolved: false
622
+ }]' 2>/dev/null) || {
623
+ type release_lock &>/dev/null && release_lock 2>/dev/null || true # SUPPRESS:OK -- cleanup: lock may not be held
624
+ json_err "$E_JSON_INVALID" "Failed to append decision to pending-decisions.json"
625
+ }
626
+
627
+ atomic_write "$pd_file" "$pd_updated" || {
628
+ type release_lock &>/dev/null && release_lock 2>/dev/null || true # SUPPRESS:OK -- cleanup: lock may not be held
629
+ json_err "$E_JSON_INVALID" "Failed to write pending-decisions.json"
630
+ }
631
+
632
+ type release_lock &>/dev/null && { release_lock 2>/dev/null || true; trap - EXIT; } # SUPPRESS:OK -- cleanup: lock may not be held
633
+
634
+ local pd_count
635
+ pd_count=$(echo "$pd_updated" | jq '.decisions | length')
636
+
637
+ json_ok "$(jq -n --arg id "$pd_id" --argjson count "$pd_count" \
638
+ '{id: $id, decision_count: $count}')"
639
+ }
640
+
641
+ # ============================================================================
642
+ # _pending_decision_list
643
+ # List decisions from the pending decisions queue
644
+ # Usage: pending-decision-list [--unresolved] [--type <type>]
645
+ # Default: show only unresolved
646
+ # ============================================================================
647
+ _pending_decision_list() {
648
+ local pd_unresolved_only="true"
649
+ local pd_filter_type=""
650
+
651
+ while [[ $# -gt 0 ]]; do
652
+ case "$1" in
653
+ --unresolved) pd_unresolved_only="true"; shift ;;
654
+ --type) pd_filter_type="$2"; shift 2 ;;
655
+ *) shift ;;
656
+ esac
657
+ done
658
+
659
+ local pd_file="$COLONY_DATA_DIR/pending-decisions.json"
660
+
661
+ if [[ ! -f "$pd_file" ]]; then
662
+ json_ok '{"total":0,"unresolved":0,"decisions":[]}'
663
+ exit 0
664
+ fi
665
+
666
+ local pd_data
667
+ pd_data=$(cat "$pd_file" 2>/dev/null || echo '{"version":"1.0","decisions":[]}') # SUPPRESS:OK -- read-default: file may not exist yet
668
+
669
+ # Build jq filter
670
+ local pd_filter='.decisions'
671
+
672
+ # Apply type filter if provided
673
+ if [[ -n "$pd_filter_type" ]]; then
674
+ pd_filter="$pd_filter | map(select(.type == \"$pd_filter_type\"))"
675
+ fi
676
+
677
+ # Apply resolved filter (default: only unresolved)
678
+ if [[ "$pd_unresolved_only" == "true" ]]; then
679
+ pd_filter="$pd_filter | map(select(.resolved == false))"
680
+ fi
681
+
682
+ local pd_total pd_unresolved pd_decisions
683
+ pd_total=$(echo "$pd_data" | jq '.decisions | length')
684
+ pd_unresolved=$(echo "$pd_data" | jq '[.decisions[] | select(.resolved == false)] | length')
685
+ pd_decisions=$(echo "$pd_data" | jq "$pd_filter")
686
+
687
+ json_ok "$(jq -n --argjson total "$pd_total" --argjson unresolved "$pd_unresolved" \
688
+ --argjson decisions "$pd_decisions" \
689
+ '{total: $total, unresolved: $unresolved, decisions: $decisions}')"
690
+ }
691
+
692
+ # ============================================================================
693
+ # _pending_decision_resolve
694
+ # Mark a pending decision as resolved
695
+ # Usage: pending-decision-resolve --id <id> --resolution <text>
696
+ # ============================================================================
697
+ _pending_decision_resolve() {
698
+ local pd_id=""
699
+ local pd_resolution=""
700
+
701
+ while [[ $# -gt 0 ]]; do
702
+ case "$1" in
703
+ --id) pd_id="$2"; shift 2 ;;
704
+ --resolution) pd_resolution="$2"; shift 2 ;;
705
+ *) shift ;;
706
+ esac
707
+ done
708
+
709
+ [[ -z "$pd_id" ]] && json_err "$E_VALIDATION_FAILED" "pending-decision-resolve requires --id"
710
+ [[ -z "$pd_resolution" ]] && json_err "$E_VALIDATION_FAILED" "pending-decision-resolve requires --resolution"
711
+
712
+ local pd_file="$COLONY_DATA_DIR/pending-decisions.json"
713
+
714
+ if [[ ! -f "$pd_file" ]]; then
715
+ json_err "$E_RESOURCE_NOT_FOUND" "No pending decisions file found"
716
+ fi
717
+
718
+ # Acquire lock for concurrent access
719
+ if type acquire_lock &>/dev/null; then
720
+ acquire_lock "$pd_file" || json_err "$E_LOCK_FAILED" "Failed to acquire lock on pending-decisions.json"
721
+ trap 'release_lock 2>/dev/null || true' EXIT # SUPPRESS:OK -- cleanup: lock may not be held
722
+ fi
723
+
724
+ local pd_data
725
+ pd_data=$(cat "$pd_file" 2>/dev/null || echo '{"version":"1.0","decisions":[]}') # SUPPRESS:OK -- read-default: file may not exist yet
726
+
727
+ # Check if ID exists
728
+ local pd_exists
729
+ pd_exists=$(echo "$pd_data" | jq --arg id "$pd_id" '[.decisions[] | select(.id == $id)] | length')
730
+ if [[ "$pd_exists" -eq 0 ]]; then
731
+ type release_lock &>/dev/null && release_lock 2>/dev/null || true # SUPPRESS:OK -- cleanup: lock may not be held
732
+ json_err "$E_RESOURCE_NOT_FOUND" "Decision not found: $pd_id"
733
+ fi
734
+
735
+ local pd_now
736
+ pd_now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
737
+
738
+ local pd_updated
739
+ pd_updated=$(echo "$pd_data" | jq \
740
+ --arg id "$pd_id" \
741
+ --arg resolution "$pd_resolution" \
742
+ --arg resolved_at "$pd_now" \
743
+ '(.decisions[] | select(.id == $id)) |= (. + {resolved: true, resolution: $resolution, resolved_at: $resolved_at})' 2>/dev/null) || {
744
+ type release_lock &>/dev/null && release_lock 2>/dev/null || true # SUPPRESS:OK -- cleanup: lock may not be held
745
+ json_err "$E_JSON_INVALID" "Failed to resolve decision in pending-decisions.json"
746
+ }
747
+
748
+ atomic_write "$pd_file" "$pd_updated" || {
749
+ type release_lock &>/dev/null && release_lock 2>/dev/null || true # SUPPRESS:OK -- cleanup: lock may not be held
750
+ json_err "$E_JSON_INVALID" "Failed to write pending-decisions.json"
751
+ }
752
+
753
+ type release_lock &>/dev/null && { release_lock 2>/dev/null || true; trap - EXIT; } # SUPPRESS:OK -- cleanup: lock may not be held
754
+
755
+ json_ok "$(jq -n --arg id "$pd_id" '{resolved: true, id: $id}')"
756
+ }
757
+
758
+ # ============================================================================
759
+ # _autopilot_headless_check
760
+ # Check whether headless mode is active in run-state.json
761
+ # Usage: autopilot-headless-check
762
+ # Returns: {"ok":true,"result":{"headless":true|false}}
763
+ # ============================================================================
764
+ _autopilot_headless_check() {
765
+ local ah_state_file="$COLONY_DATA_DIR/run-state.json"
766
+
767
+ if [[ ! -f "$ah_state_file" ]]; then
768
+ json_ok '{"headless":false}'
769
+ exit 0
770
+ fi
771
+
772
+ local ah_headless
773
+ ah_headless=$(jq -r '.headless // false' "$ah_state_file" 2>/dev/null || echo "false") # SUPPRESS:OK -- read-default: field may not exist
774
+
775
+ # Normalize to boolean
776
+ if [[ "$ah_headless" == "true" ]]; then
777
+ json_ok '{"headless":true}'
778
+ else
779
+ json_ok '{"headless":false}'
780
+ fi
781
+ }
782
+
783
+ # ============================================================================
784
+ # _autopilot_set_headless
785
+ # Set the headless flag in run-state.json
786
+ # Usage: autopilot-set-headless <true|false>
787
+ # Returns: {"ok":true,"result":{"headless":true|false,"updated":true}}
788
+ # ============================================================================
789
+ _autopilot_set_headless() {
790
+ local ah_value="${1:-}"
791
+
792
+ if [[ "$ah_value" != "true" && "$ah_value" != "false" ]]; then
793
+ json_err "$E_VALIDATION_FAILED" "autopilot-set-headless requires true or false argument"
794
+ fi
795
+
796
+ local ah_state_file="$COLONY_DATA_DIR/run-state.json"
797
+
798
+ if [[ ! -f "$ah_state_file" ]]; then
799
+ json_err "$E_FILE_NOT_FOUND" "run-state.json not found — autopilot not active"
800
+ fi
801
+
802
+ local ah_headless_bool
803
+ [[ "$ah_value" == "true" ]] && ah_headless_bool=true || ah_headless_bool=false
804
+
805
+ local ah_current ah_updated
806
+ ah_current=$(cat "$ah_state_file" 2>/dev/null || echo '{}') # SUPPRESS:OK -- read-default: file may not exist yet
807
+ ah_updated=$(echo "$ah_current" | jq --argjson headless "$ah_headless_bool" '.headless = $headless' 2>/dev/null) || {
808
+ json_err "$E_JSON_INVALID" "Failed to update headless flag in run-state.json"
809
+ }
810
+
811
+ atomic_write "$ah_state_file" "$ah_updated" || {
812
+ json_err "$E_JSON_INVALID" "Failed to write run-state.json"
813
+ }
814
+
815
+ json_ok "$(jq -n --argjson headless "$ah_headless_bool" '{headless: $headless, updated: true}')"
816
+ }