aether-colony 5.1.0 → 5.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/.aether/aether-utils.sh +157 -42
  2. package/.aether/agents/aether-ambassador.md +140 -0
  3. package/.aether/agents/aether-archaeologist.md +108 -0
  4. package/.aether/agents/aether-architect.md +133 -0
  5. package/.aether/agents/aether-auditor.md +144 -0
  6. package/.aether/agents/aether-builder.md +184 -0
  7. package/.aether/agents/aether-chaos.md +115 -0
  8. package/.aether/agents/aether-chronicler.md +122 -0
  9. package/.aether/agents/aether-gatekeeper.md +116 -0
  10. package/.aether/agents/aether-includer.md +117 -0
  11. package/.aether/agents/aether-keeper.md +177 -0
  12. package/.aether/agents/aether-measurer.md +128 -0
  13. package/.aether/agents/aether-oracle.md +137 -0
  14. package/.aether/agents/aether-probe.md +133 -0
  15. package/.aether/agents/aether-queen.md +286 -0
  16. package/.aether/agents/aether-route-setter.md +130 -0
  17. package/.aether/agents/aether-sage.md +106 -0
  18. package/.aether/agents/aether-scout.md +101 -0
  19. package/.aether/agents/aether-surveyor-disciplines.md +391 -0
  20. package/.aether/agents/aether-surveyor-nest.md +329 -0
  21. package/.aether/agents/aether-surveyor-pathogens.md +264 -0
  22. package/.aether/agents/aether-surveyor-provisions.md +334 -0
  23. package/.aether/agents/aether-tracker.md +137 -0
  24. package/.aether/agents/aether-watcher.md +174 -0
  25. package/.aether/agents/aether-weaver.md +130 -0
  26. package/.aether/commands/claude/archaeology.md +334 -0
  27. package/.aether/commands/claude/build.md +65 -0
  28. package/.aether/commands/claude/chaos.md +336 -0
  29. package/.aether/commands/claude/colonize.md +259 -0
  30. package/.aether/commands/claude/continue.md +60 -0
  31. package/.aether/commands/claude/council.md +507 -0
  32. package/.aether/commands/claude/data-clean.md +81 -0
  33. package/.aether/commands/claude/dream.md +268 -0
  34. package/.aether/commands/claude/entomb.md +498 -0
  35. package/.aether/commands/claude/export-signals.md +57 -0
  36. package/.aether/commands/claude/feedback.md +96 -0
  37. package/.aether/commands/claude/flag.md +151 -0
  38. package/.aether/commands/claude/flags.md +169 -0
  39. package/.aether/commands/claude/focus.md +76 -0
  40. package/.aether/commands/claude/help.md +154 -0
  41. package/.aether/commands/claude/history.md +140 -0
  42. package/.aether/commands/claude/import-signals.md +71 -0
  43. package/.aether/commands/claude/init.md +505 -0
  44. package/.aether/commands/claude/insert-phase.md +105 -0
  45. package/.aether/commands/claude/interpret.md +278 -0
  46. package/.aether/commands/claude/lay-eggs.md +210 -0
  47. package/.aether/commands/claude/maturity.md +113 -0
  48. package/.aether/commands/claude/memory-details.md +77 -0
  49. package/.aether/commands/claude/migrate-state.md +171 -0
  50. package/.aether/commands/claude/oracle.md +642 -0
  51. package/.aether/commands/claude/organize.md +232 -0
  52. package/.aether/commands/claude/patrol.md +620 -0
  53. package/.aether/commands/claude/pause-colony.md +233 -0
  54. package/.aether/commands/claude/phase.md +115 -0
  55. package/.aether/commands/claude/pheromones.md +156 -0
  56. package/.aether/commands/claude/plan.md +693 -0
  57. package/.aether/commands/claude/preferences.md +65 -0
  58. package/.aether/commands/claude/quick.md +100 -0
  59. package/.aether/commands/claude/redirect.md +76 -0
  60. package/.aether/commands/claude/resume-colony.md +197 -0
  61. package/.aether/commands/claude/resume.md +388 -0
  62. package/.aether/commands/claude/run.md +231 -0
  63. package/.aether/commands/claude/seal.md +774 -0
  64. package/.aether/commands/claude/skill-create.md +286 -0
  65. package/.aether/commands/claude/status.md +410 -0
  66. package/.aether/commands/claude/swarm.md +349 -0
  67. package/.aether/commands/claude/tunnels.md +426 -0
  68. package/.aether/commands/claude/update.md +132 -0
  69. package/.aether/commands/claude/verify-castes.md +143 -0
  70. package/.aether/commands/claude/watch.md +239 -0
  71. package/.aether/commands/colonize.yaml +4 -0
  72. package/.aether/commands/council.yaml +205 -0
  73. package/.aether/commands/init.yaml +46 -13
  74. package/.aether/commands/insert-phase.yaml +4 -0
  75. package/.aether/commands/opencode/archaeology.md +331 -0
  76. package/.aether/commands/opencode/build.md +1168 -0
  77. package/.aether/commands/opencode/chaos.md +329 -0
  78. package/.aether/commands/opencode/colonize.md +195 -0
  79. package/.aether/commands/opencode/continue.md +1436 -0
  80. package/.aether/commands/opencode/council.md +437 -0
  81. package/.aether/commands/opencode/data-clean.md +77 -0
  82. package/.aether/commands/opencode/dream.md +260 -0
  83. package/.aether/commands/opencode/entomb.md +377 -0
  84. package/.aether/commands/opencode/export-signals.md +54 -0
  85. package/.aether/commands/opencode/feedback.md +99 -0
  86. package/.aether/commands/opencode/flag.md +149 -0
  87. package/.aether/commands/opencode/flags.md +167 -0
  88. package/.aether/commands/opencode/focus.md +73 -0
  89. package/.aether/commands/opencode/help.md +157 -0
  90. package/.aether/commands/opencode/history.md +136 -0
  91. package/.aether/commands/opencode/import-signals.md +68 -0
  92. package/.aether/commands/opencode/init.md +518 -0
  93. package/.aether/commands/opencode/insert-phase.md +111 -0
  94. package/.aether/commands/opencode/interpret.md +272 -0
  95. package/.aether/commands/opencode/lay-eggs.md +213 -0
  96. package/.aether/commands/opencode/maturity.md +108 -0
  97. package/.aether/commands/opencode/memory-details.md +83 -0
  98. package/.aether/commands/opencode/migrate-state.md +165 -0
  99. package/.aether/commands/opencode/oracle.md +593 -0
  100. package/.aether/commands/opencode/organize.md +226 -0
  101. package/.aether/commands/opencode/patrol.md +626 -0
  102. package/.aether/commands/opencode/pause-colony.md +203 -0
  103. package/.aether/commands/opencode/phase.md +113 -0
  104. package/.aether/commands/opencode/pheromones.md +162 -0
  105. package/.aether/commands/opencode/plan.md +684 -0
  106. package/.aether/commands/opencode/preferences.md +71 -0
  107. package/.aether/commands/opencode/quick.md +91 -0
  108. package/.aether/commands/opencode/redirect.md +84 -0
  109. package/.aether/commands/opencode/resume-colony.md +190 -0
  110. package/.aether/commands/opencode/resume.md +394 -0
  111. package/.aether/commands/opencode/run.md +237 -0
  112. package/.aether/commands/opencode/seal.md +452 -0
  113. package/.aether/commands/opencode/skill-create.md +63 -0
  114. package/.aether/commands/opencode/status.md +307 -0
  115. package/.aether/commands/opencode/swarm.md +15 -0
  116. package/.aether/commands/opencode/tunnels.md +400 -0
  117. package/.aether/commands/opencode/update.md +127 -0
  118. package/.aether/commands/opencode/verify-castes.md +139 -0
  119. package/.aether/commands/opencode/watch.md +227 -0
  120. package/.aether/commands/plan.yaml +53 -2
  121. package/.aether/commands/quick.yaml +104 -0
  122. package/.aether/commands/resume-colony.yaml +6 -4
  123. package/.aether/commands/resume.yaml +9 -0
  124. package/.aether/commands/run.yaml +37 -1
  125. package/.aether/commands/seal.yaml +9 -0
  126. package/.aether/commands/status.yaml +45 -1
  127. package/.aether/docs/command-playbooks/build-full.md +3 -2
  128. package/.aether/docs/command-playbooks/build-prep.md +12 -4
  129. package/.aether/docs/command-playbooks/build-verify.md +51 -0
  130. package/.aether/docs/command-playbooks/continue-advance.md +115 -6
  131. package/.aether/docs/command-playbooks/continue-full.md +1 -0
  132. package/.aether/docs/command-playbooks/continue-verify.md +33 -0
  133. package/.aether/utils/clash-detect.sh +239 -0
  134. package/.aether/utils/council.sh +425 -0
  135. package/.aether/utils/error-handler.sh +3 -3
  136. package/.aether/utils/flag.sh +23 -12
  137. package/.aether/utils/hive.sh +2 -2
  138. package/.aether/utils/hooks/clash-pre-tool-use.js +99 -0
  139. package/.aether/utils/immune.sh +508 -0
  140. package/.aether/utils/learning.sh +2 -2
  141. package/.aether/utils/merge-driver-lockfile.sh +35 -0
  142. package/.aether/utils/midden.sh +712 -0
  143. package/.aether/utils/pheromone.sh +1376 -108
  144. package/.aether/utils/queen.sh +31 -21
  145. package/.aether/utils/session.sh +264 -0
  146. package/.aether/utils/spawn-tree.sh +7 -7
  147. package/.aether/utils/spawn.sh +2 -2
  148. package/.aether/utils/state-api.sh +216 -5
  149. package/.aether/utils/swarm.sh +1 -1
  150. package/.aether/utils/worktree.sh +189 -0
  151. package/.claude/commands/ant/colonize.md +2 -0
  152. package/.claude/commands/ant/council.md +205 -0
  153. package/.claude/commands/ant/init.md +53 -14
  154. package/.claude/commands/ant/insert-phase.md +4 -0
  155. package/.claude/commands/ant/plan.md +27 -1
  156. package/.claude/commands/ant/quick.md +100 -0
  157. package/.claude/commands/ant/resume-colony.md +3 -2
  158. package/.claude/commands/ant/resume.md +9 -0
  159. package/.claude/commands/ant/run.md +37 -1
  160. package/.claude/commands/ant/seal.md +9 -0
  161. package/.claude/commands/ant/status.md +45 -1
  162. package/.opencode/commands/ant/colonize.md +2 -0
  163. package/.opencode/commands/ant/council.md +143 -0
  164. package/.opencode/commands/ant/init.md +53 -13
  165. package/.opencode/commands/ant/insert-phase.md +4 -0
  166. package/.opencode/commands/ant/plan.md +26 -1
  167. package/.opencode/commands/ant/quick.md +91 -0
  168. package/.opencode/commands/ant/resume-colony.md +3 -2
  169. package/.opencode/commands/ant/resume.md +9 -0
  170. package/.opencode/commands/ant/run.md +37 -1
  171. package/.opencode/commands/ant/status.md +2 -0
  172. package/CHANGELOG.md +116 -0
  173. package/README.md +34 -8
  174. package/bin/cli.js +103 -61
  175. package/bin/lib/banner.js +14 -0
  176. package/bin/lib/init.js +8 -7
  177. package/bin/lib/interactive-setup.js +251 -0
  178. package/bin/npx-entry.js +21 -0
  179. package/bin/npx-install.js +9 -167
  180. package/bin/validate-package.sh +23 -0
  181. package/package.json +11 -3
  182. package/.aether/docs/plans/pheromone-display-plan.md +0 -257
  183. package/.aether/schemas/example-prompt-builder.xml +0 -234
  184. package/.aether/scripts/incident-test-add.sh +0 -47
  185. package/.aether/scripts/weekly-audit.sh +0 -79
@@ -0,0 +1,425 @@
1
+ #!/bin/bash
2
+ # Council deliberation module — Advocate/Challenger/Sage model with spawn budget guards
3
+ # Provides: _council_deliberate, _council_advocate, _council_challenger, _council_sage,
4
+ # _council_history, _council_budget_check
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, COLONY_DATA_DIR, error constants) is available.
9
+ # _spawn_can_spawn is available from spawn.sh (sourced before this module).
10
+
11
+ # ---------------------------------------------------------------------------
12
+ # Internal helpers
13
+ # ---------------------------------------------------------------------------
14
+
15
+ _council_data_dir() {
16
+ echo "$COLONY_DATA_DIR/council"
17
+ }
18
+
19
+ _council_deliberations_file() {
20
+ echo "$COLONY_DATA_DIR/council/deliberations.json"
21
+ }
22
+
23
+ _council_ensure_file() {
24
+ local cf
25
+ cf="$(_council_deliberations_file)"
26
+ mkdir -p "$(_council_data_dir)"
27
+ if [[ ! -f "$cf" ]]; then
28
+ printf '%s\n' '{"version":"1.0","deliberations":[]}' > "$cf"
29
+ fi
30
+ }
31
+
32
+ # ---------------------------------------------------------------------------
33
+ # _council_deliberate
34
+ # Usage: council-deliberate --proposal <text> [--budget N] [--depth light|standard|deep]
35
+ # ---------------------------------------------------------------------------
36
+ _council_deliberate() {
37
+ local cd_proposal=""
38
+ local cd_budget="3"
39
+ local cd_depth="standard"
40
+
41
+ while [[ $# -gt 0 ]]; do
42
+ case "$1" in
43
+ --proposal) cd_proposal="${2:-}"; shift 2 ;;
44
+ --budget) cd_budget="${2:-3}"; shift 2 ;;
45
+ --depth) cd_depth="${2:-standard}"; shift 2 ;;
46
+ *) shift ;;
47
+ esac
48
+ done
49
+
50
+ if [[ -z "$cd_proposal" ]]; then
51
+ json_err "$E_VALIDATION_FAILED" "council-deliberate requires --proposal"
52
+ return
53
+ fi
54
+
55
+ if ! [[ "$cd_budget" =~ ^[0-9]+$ ]]; then
56
+ json_err "$E_VALIDATION_FAILED" "council-deliberate --budget must be a positive integer"
57
+ return
58
+ fi
59
+
60
+ local cd_ts
61
+ cd_ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
62
+ local cd_unix
63
+ cd_unix=$(date -u +%s)
64
+ local cd_id="delib_${cd_unix}"
65
+
66
+ _council_ensure_file
67
+
68
+ local cd_file
69
+ cd_file="$(_council_deliberations_file)"
70
+
71
+ acquire_lock "$cd_file" 2>/dev/null || true
72
+ # shellcheck disable=SC2064
73
+ trap "release_lock '$cd_file' 2>/dev/null || true" EXIT
74
+
75
+ local cd_updated
76
+ cd_updated=$(jq \
77
+ --arg id "$cd_id" \
78
+ --arg proposal "$cd_proposal" \
79
+ --arg ts "$cd_ts" \
80
+ --argjson budget "$cd_budget" \
81
+ --arg depth "$cd_depth" \
82
+ '.deliberations += [{
83
+ "id": $id,
84
+ "proposal": $proposal,
85
+ "advocate": null,
86
+ "challenger": null,
87
+ "sage": null,
88
+ "budget": $budget,
89
+ "depth": $depth,
90
+ "created_at": $ts,
91
+ "status": "pending"
92
+ }]' "$cd_file") || {
93
+ release_lock "$cd_file" 2>/dev/null || true
94
+ trap - EXIT
95
+ json_err "$E_UNKNOWN" "Failed to write deliberation"
96
+ return
97
+ }
98
+
99
+ atomic_write "$cd_file" "$cd_updated" || {
100
+ release_lock "$cd_file" 2>/dev/null || true
101
+ trap - EXIT
102
+ json_err "$E_UNKNOWN" "Failed to persist deliberation"
103
+ return
104
+ }
105
+
106
+ release_lock "$cd_file" 2>/dev/null || true
107
+ trap - EXIT
108
+
109
+ json_ok "$(jq -n \
110
+ --arg id "$cd_id" \
111
+ --arg proposal "$cd_proposal" \
112
+ --argjson budget "$cd_budget" \
113
+ '{"id":$id,"proposal":$proposal,"status":"pending","budget":$budget}')"
114
+ }
115
+
116
+ # ---------------------------------------------------------------------------
117
+ # _council_advocate
118
+ # Usage: council-advocate --deliberation-id <id> --argument <text>
119
+ # ---------------------------------------------------------------------------
120
+ _council_advocate() {
121
+ local ca_id=""
122
+ local ca_argument=""
123
+
124
+ while [[ $# -gt 0 ]]; do
125
+ case "$1" in
126
+ --deliberation-id) ca_id="${2:-}"; shift 2 ;;
127
+ --argument) ca_argument="${2:-}"; shift 2 ;;
128
+ *) shift ;;
129
+ esac
130
+ done
131
+
132
+ if [[ -z "$ca_id" ]]; then
133
+ json_err "$E_VALIDATION_FAILED" "council-advocate requires --deliberation-id"
134
+ return
135
+ fi
136
+
137
+ if [[ -z "$ca_argument" ]]; then
138
+ json_err "$E_VALIDATION_FAILED" "council-advocate requires --argument"
139
+ return
140
+ fi
141
+
142
+ local ca_file
143
+ ca_file="$(_council_deliberations_file)"
144
+
145
+ if [[ ! -f "$ca_file" ]]; then
146
+ json_err "$E_VALIDATION_FAILED" "No deliberations found; run council-deliberate first"
147
+ return
148
+ fi
149
+
150
+ local ca_exists
151
+ ca_exists=$(jq -r --arg id "$ca_id" '.deliberations[] | select(.id == $id) | .id' "$ca_file" 2>/dev/null || echo "")
152
+ if [[ -z "$ca_exists" ]]; then
153
+ json_err "$E_VALIDATION_FAILED" "Deliberation not found: $ca_id"
154
+ return
155
+ fi
156
+
157
+ acquire_lock "$ca_file" 2>/dev/null || true
158
+ # shellcheck disable=SC2064
159
+ trap "release_lock '$ca_file' 2>/dev/null || true" EXIT
160
+
161
+ local ca_updated
162
+ ca_updated=$(jq \
163
+ --arg id "$ca_id" \
164
+ --arg arg "$ca_argument" \
165
+ '(.deliberations[] | select(.id == $id)).advocate = $arg
166
+ | (.deliberations[] | select(.id == $id)).status = "in_progress"' \
167
+ "$ca_file") || {
168
+ release_lock "$ca_file" 2>/dev/null || true
169
+ trap - EXIT
170
+ json_err "$E_UNKNOWN" "Failed to record advocate argument"
171
+ return
172
+ }
173
+
174
+ atomic_write "$ca_file" "$ca_updated" || {
175
+ release_lock "$ca_file" 2>/dev/null || true
176
+ trap - EXIT
177
+ json_err "$E_UNKNOWN" "Failed to persist advocate argument"
178
+ return
179
+ }
180
+
181
+ release_lock "$ca_file" 2>/dev/null || true
182
+ trap - EXIT
183
+
184
+ json_ok '{"role":"advocate","recorded":true}'
185
+ }
186
+
187
+ # ---------------------------------------------------------------------------
188
+ # _council_challenger
189
+ # Usage: council-challenger --deliberation-id <id> --argument <text>
190
+ # ---------------------------------------------------------------------------
191
+ _council_challenger() {
192
+ local cc_id=""
193
+ local cc_argument=""
194
+
195
+ while [[ $# -gt 0 ]]; do
196
+ case "$1" in
197
+ --deliberation-id) cc_id="${2:-}"; shift 2 ;;
198
+ --argument) cc_argument="${2:-}"; shift 2 ;;
199
+ *) shift ;;
200
+ esac
201
+ done
202
+
203
+ if [[ -z "$cc_id" ]]; then
204
+ json_err "$E_VALIDATION_FAILED" "council-challenger requires --deliberation-id"
205
+ return
206
+ fi
207
+
208
+ if [[ -z "$cc_argument" ]]; then
209
+ json_err "$E_VALIDATION_FAILED" "council-challenger requires --argument"
210
+ return
211
+ fi
212
+
213
+ local cc_file
214
+ cc_file="$(_council_deliberations_file)"
215
+
216
+ if [[ ! -f "$cc_file" ]]; then
217
+ json_err "$E_VALIDATION_FAILED" "No deliberations found; run council-deliberate first"
218
+ return
219
+ fi
220
+
221
+ local cc_exists
222
+ cc_exists=$(jq -r --arg id "$cc_id" '.deliberations[] | select(.id == $id) | .id' "$cc_file" 2>/dev/null || echo "")
223
+ if [[ -z "$cc_exists" ]]; then
224
+ json_err "$E_VALIDATION_FAILED" "Deliberation not found: $cc_id"
225
+ return
226
+ fi
227
+
228
+ acquire_lock "$cc_file" 2>/dev/null || true
229
+ # shellcheck disable=SC2064
230
+ trap "release_lock '$cc_file' 2>/dev/null || true" EXIT
231
+
232
+ local cc_updated
233
+ cc_updated=$(jq \
234
+ --arg id "$cc_id" \
235
+ --arg arg "$cc_argument" \
236
+ '(.deliberations[] | select(.id == $id)).challenger = $arg
237
+ | (.deliberations[] | select(.id == $id)).status = "in_progress"' \
238
+ "$cc_file") || {
239
+ release_lock "$cc_file" 2>/dev/null || true
240
+ trap - EXIT
241
+ json_err "$E_UNKNOWN" "Failed to record challenger argument"
242
+ return
243
+ }
244
+
245
+ atomic_write "$cc_file" "$cc_updated" || {
246
+ release_lock "$cc_file" 2>/dev/null || true
247
+ trap - EXIT
248
+ json_err "$E_UNKNOWN" "Failed to persist challenger argument"
249
+ return
250
+ }
251
+
252
+ release_lock "$cc_file" 2>/dev/null || true
253
+ trap - EXIT
254
+
255
+ json_ok '{"role":"challenger","recorded":true}'
256
+ }
257
+
258
+ # ---------------------------------------------------------------------------
259
+ # _council_sage
260
+ # Usage: council-sage --deliberation-id <id> --synthesis <text> --recommendation <text>
261
+ # ---------------------------------------------------------------------------
262
+ _council_sage() {
263
+ local cs_id=""
264
+ local cs_synthesis=""
265
+ local cs_recommendation=""
266
+
267
+ while [[ $# -gt 0 ]]; do
268
+ case "$1" in
269
+ --deliberation-id) cs_id="${2:-}"; shift 2 ;;
270
+ --synthesis) cs_synthesis="${2:-}"; shift 2 ;;
271
+ --recommendation) cs_recommendation="${2:-}"; shift 2 ;;
272
+ *) shift ;;
273
+ esac
274
+ done
275
+
276
+ if [[ -z "$cs_id" ]]; then
277
+ json_err "$E_VALIDATION_FAILED" "council-sage requires --deliberation-id"
278
+ return
279
+ fi
280
+
281
+ if [[ -z "$cs_synthesis" ]]; then
282
+ json_err "$E_VALIDATION_FAILED" "council-sage requires --synthesis"
283
+ return
284
+ fi
285
+
286
+ if [[ -z "$cs_recommendation" ]]; then
287
+ json_err "$E_VALIDATION_FAILED" "council-sage requires --recommendation"
288
+ return
289
+ fi
290
+
291
+ local cs_file
292
+ cs_file="$(_council_deliberations_file)"
293
+
294
+ if [[ ! -f "$cs_file" ]]; then
295
+ json_err "$E_VALIDATION_FAILED" "No deliberations found; run council-deliberate first"
296
+ return
297
+ fi
298
+
299
+ local cs_exists
300
+ cs_exists=$(jq -r --arg id "$cs_id" '.deliberations[] | select(.id == $id) | .id' "$cs_file" 2>/dev/null || echo "")
301
+ if [[ -z "$cs_exists" ]]; then
302
+ json_err "$E_VALIDATION_FAILED" "Deliberation not found: $cs_id"
303
+ return
304
+ fi
305
+
306
+ acquire_lock "$cs_file" 2>/dev/null || true
307
+ # shellcheck disable=SC2064
308
+ trap "release_lock '$cs_file' 2>/dev/null || true" EXIT
309
+
310
+ local cs_updated
311
+ cs_updated=$(jq \
312
+ --arg id "$cs_id" \
313
+ --arg synthesis "$cs_synthesis" \
314
+ --arg rec "$cs_recommendation" \
315
+ '(.deliberations[] | select(.id == $id)).sage = {"synthesis": $synthesis, "recommendation": $rec}
316
+ | (.deliberations[] | select(.id == $id)).status = "complete"' \
317
+ "$cs_file") || {
318
+ release_lock "$cs_file" 2>/dev/null || true
319
+ trap - EXIT
320
+ json_err "$E_UNKNOWN" "Failed to record sage synthesis"
321
+ return
322
+ }
323
+
324
+ atomic_write "$cs_file" "$cs_updated" || {
325
+ release_lock "$cs_file" 2>/dev/null || true
326
+ trap - EXIT
327
+ json_err "$E_UNKNOWN" "Failed to persist sage synthesis"
328
+ return
329
+ }
330
+
331
+ release_lock "$cs_file" 2>/dev/null || true
332
+ trap - EXIT
333
+
334
+ json_ok "$(jq -n \
335
+ --arg rec "$cs_recommendation" \
336
+ '{"role":"sage","recommendation":$rec,"deliberation_complete":true}')"
337
+ }
338
+
339
+ # ---------------------------------------------------------------------------
340
+ # _council_history
341
+ # Usage: council-history [--limit N]
342
+ # ---------------------------------------------------------------------------
343
+ _council_history() {
344
+ local ch_limit=""
345
+
346
+ while [[ $# -gt 0 ]]; do
347
+ case "$1" in
348
+ --limit) ch_limit="${2:-}"; shift 2 ;;
349
+ *) shift ;;
350
+ esac
351
+ done
352
+
353
+ local ch_file
354
+ ch_file="$(_council_deliberations_file)"
355
+
356
+ if [[ ! -f "$ch_file" ]]; then
357
+ json_ok '{"total":0,"deliberations":[]}'
358
+ return
359
+ fi
360
+
361
+ local ch_total
362
+ ch_total=$(jq '.deliberations | length' "$ch_file" 2>/dev/null || echo 0)
363
+
364
+ if [[ -n "$ch_limit" ]] && [[ "$ch_limit" =~ ^[0-9]+$ ]]; then
365
+ local ch_result
366
+ ch_result=$(jq \
367
+ --argjson limit "$ch_limit" \
368
+ --argjson total "$ch_total" \
369
+ '{"total":$total,"deliberations":(.deliberations | .[-($limit):])}' \
370
+ "$ch_file" 2>/dev/null) || ch_result='{"total":0,"deliberations":[]}'
371
+ json_ok "$ch_result"
372
+ else
373
+ local ch_result
374
+ ch_result=$(jq \
375
+ --argjson total "$ch_total" \
376
+ '{"total":$total,"deliberations":.deliberations}' \
377
+ "$ch_file" 2>/dev/null) || ch_result='{"total":0,"deliberations":[]}'
378
+ json_ok "$ch_result"
379
+ fi
380
+ }
381
+
382
+ # ---------------------------------------------------------------------------
383
+ # _council_budget_check
384
+ # Usage: council-budget-check [--budget N]
385
+ # ---------------------------------------------------------------------------
386
+ _council_budget_check() {
387
+ local cb_budget="3"
388
+
389
+ while [[ $# -gt 0 ]]; do
390
+ case "$1" in
391
+ --budget) cb_budget="${2:-3}"; shift 2 ;;
392
+ *) shift ;;
393
+ esac
394
+ done
395
+
396
+ if ! [[ "$cb_budget" =~ ^[0-9]+$ ]]; then
397
+ json_err "$E_VALIDATION_FAILED" "council-budget-check --budget must be a positive integer"
398
+ return
399
+ fi
400
+
401
+ # Delegate to spawn-can-spawn at depth 1
402
+ local cb_spawn_result
403
+ cb_spawn_result=$(_spawn_can_spawn 1 2>/dev/null || echo '{"can_spawn":false,"current_total":0,"global_cap":10}')
404
+
405
+ local cb_can
406
+ cb_can=$(echo "$cb_spawn_result" | jq -r '.result.can_spawn // .can_spawn // false' 2>/dev/null || echo "false")
407
+ local cb_current
408
+ cb_current=$(echo "$cb_spawn_result" | jq -r '.result.current_total // .current_total // 0' 2>/dev/null || echo 0)
409
+ local cb_cap
410
+ cb_cap=$(echo "$cb_spawn_result" | jq -r '.result.global_cap // .global_cap // 10' 2>/dev/null || echo 10)
411
+ local cb_remaining=$(( cb_cap - cb_current ))
412
+ [[ $cb_remaining -lt 0 ]] && cb_remaining=0
413
+
414
+ # allowed is true only if spawn is allowed AND remaining >= requested budget
415
+ local cb_allowed="false"
416
+ if [[ "$cb_can" == "true" ]] && [[ $cb_remaining -ge $cb_budget ]]; then
417
+ cb_allowed="true"
418
+ fi
419
+
420
+ json_ok "$(jq -n \
421
+ --argjson allowed "$cb_allowed" \
422
+ --argjson remaining "$cb_remaining" \
423
+ --argjson budget "$cb_budget" \
424
+ '{"allowed":$allowed,"remaining":$remaining,"budget":$budget}')"
425
+ }
@@ -87,7 +87,7 @@ json_err() {
87
87
  "$code" "$escaped_message" "$details_json" "$recovery" "$timestamp" >&2
88
88
 
89
89
  # Log to activity.log (best effort)
90
- if [[ -n "${COLONY_DATA_DIR:-}" ]]; then
90
+ if [[ -n "${COLONY_DATA_DIR:-}" ]] && [[ "${AETHER_TESTING:-}" != "1" ]]; then
91
91
  echo "[$timestamp] ERROR $code: $escaped_message" >> "$COLONY_DATA_DIR/activity.log" 2>/dev/null || true
92
92
  fi
93
93
 
@@ -111,7 +111,7 @@ json_warn() {
111
111
  "$code" "$escaped_message" "$timestamp"
112
112
 
113
113
  # Log to activity.log (best effort)
114
- if [[ -n "${COLONY_DATA_DIR:-}" ]]; then
114
+ if [[ -n "${COLONY_DATA_DIR:-}" ]] && [[ "${AETHER_TESTING:-}" != "1" ]]; then
115
115
  echo "[$timestamp] WARN $code: $escaped_message" >> "$COLONY_DATA_DIR/activity.log" 2>/dev/null || true
116
116
  fi
117
117
  }
@@ -153,7 +153,7 @@ error_handler() {
153
153
  "$E_BASH_ERROR" "$details" "$(_recovery_default)" "$timestamp" >&2
154
154
 
155
155
  # Log to activity.log (best effort)
156
- if [[ -n "${COLONY_DATA_DIR:-}" ]]; then
156
+ if [[ -n "${COLONY_DATA_DIR:-}" ]] && [[ "${AETHER_TESTING:-}" != "1" ]]; then
157
157
  echo "[$timestamp] ERROR $E_BASH_ERROR: Command failed at line $line_num (exit $exit_code)" >> "$COLONY_DATA_DIR/activity.log" 2>/dev/null || true
158
158
  fi
159
159
 
@@ -204,22 +204,33 @@ _flag_list() {
204
204
  exit 0
205
205
  fi
206
206
 
207
- # Build jq filter
208
- jq_filter='.flags'
209
-
210
- if [[ "$show_all" != "true" ]]; then
211
- jq_filter+=' | [.[] | select(.resolved_at == null)]'
212
- fi
213
-
214
- if [[ -n "$filter_type" ]]; then
215
- jq_filter+=" | [.[] | select(.type == \"$filter_type\")]"
207
+ # Validate filter_phase as numeric (safe for --argjson)
208
+ if [[ -n "$filter_phase" && "$filter_phase" =~ ^[0-9]+$ ]]; then
209
+ phase_num="$filter_phase"
210
+ else
211
+ phase_num=""
216
212
  fi
217
213
 
218
- if [[ -n "$filter_phase" && "$filter_phase" =~ ^[0-9]+$ ]]; then
219
- jq_filter+=" | [.[] | select(.phase == $filter_phase or .phase == null)]"
214
+ # Build jq command with --arg to prevent filter injection
215
+ # filter_type is passed via --arg (safe string interpolation in jq)
216
+ # phase_num is passed via --argjson (numeric-only, pre-validated)
217
+ if [[ -n "$phase_num" ]]; then
218
+ result=$(jq --arg ft "$filter_type" --argjson ph "$phase_num" --argjson all "$show_all" '
219
+ .flags
220
+ | (if $all | not then [.[] | select(.resolved_at == null)] else . end)
221
+ | (if $ft != "" then [.[] | select(.type == $ft)] else . end)
222
+ | (if $ph != null then [.[] | select(.phase == $ph or .phase == null)] else . end)
223
+ | {flags: ., count: length}
224
+ ' "$flags_file")
225
+ else
226
+ result=$(jq --arg ft "$filter_type" --argjson all "$show_all" '
227
+ .flags
228
+ | (if $all | not then [.[] | select(.resolved_at == null)] else . end)
229
+ | (if $ft != "" then [.[] | select(.type == $ft)] else . end)
230
+ | {flags: ., count: length}
231
+ ' "$flags_file")
220
232
  fi
221
233
 
222
- result=$(jq "{flags: ($jq_filter), count: ($jq_filter | length)}" "$flags_file")
223
234
  json_ok "$result"
224
235
  }
225
236
 
@@ -306,14 +306,14 @@ _hive_read() {
306
306
  --argjson limit "$hr_limit" '
307
307
  .entries
308
308
  | map(
309
- select((.confidence | tonumber) >= $min_conf)
309
+ select(((.confidence // 0) | tonumber) >= $min_conf)
310
310
  | if ($domain_filter | length) > 0 then
311
311
  select(
312
312
  [.domain_tags[] as $dt | $domain_filter[] | select(. == $dt)] | length > 0
313
313
  )
314
314
  else . end
315
315
  )
316
- | sort_by(-(.confidence | tonumber), -.validated_count)
316
+ | sort_by(-((.confidence // 0) | tonumber), -.validated_count)
317
317
  | { total_matched: length, entries: .[:$limit], returned_ids: [.[:$limit][].id] }
318
318
  ' "$hr_wisdom_file" 2>/dev/null) || {
319
319
  json_ok '{"entries":[],"total_matched":0,"fallback":"filter_error"}'
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env node
2
+ // Clash Detection PreToolUse Hook
3
+ //
4
+ // Runs before Edit/Write tool calls in Claude Code.
5
+ // Checks if any other active git worktree has uncommitted changes to the
6
+ // same file, preventing agents from silently overwriting each other's work.
7
+ //
8
+ // Exit codes:
9
+ // 0 - Allow the operation
10
+ // 2 - Block the operation (conflict detected)
11
+ //
12
+ // Design principle: fail-open. If the hook errors for any reason,
13
+ // the operation is allowed. The goal is safety guidance, not blocking work.
14
+
15
+ const { execSync } = require('child_process');
16
+ const path = require('path');
17
+
18
+ // Files that are branch-local state and never clash across worktrees
19
+ const ALLOWLIST = [
20
+ '.aether/data/',
21
+ ];
22
+
23
+ // Timeout for clash-detect subprocess (ms)
24
+ const DETECT_TIMEOUT = 5000;
25
+
26
+ let input = '';
27
+ const stdinTimeout = setTimeout(() => process.exit(0), 3000);
28
+ process.stdin.setEncoding('utf8');
29
+ process.stdin.on('data', chunk => input += chunk);
30
+ process.stdin.on('end', () => {
31
+ clearTimeout(stdinTimeout);
32
+ try {
33
+ const data = JSON.parse(input);
34
+ const toolName = data.tool_name;
35
+
36
+ // Only check Edit and Write operations
37
+ if (toolName !== 'Edit' && toolName !== 'Write') {
38
+ process.exit(0);
39
+ }
40
+
41
+ // Extract file path
42
+ const filePath = data.tool_input?.file_path || '';
43
+ if (!filePath) {
44
+ process.exit(0);
45
+ }
46
+
47
+ // Check allowlist (branch-local state files never clash)
48
+ if (ALLOWLIST.some(pattern => filePath.includes(pattern))) {
49
+ process.exit(0);
50
+ }
51
+
52
+ // Find the clash-detect script
53
+ // It lives at .aether/utils/clash-detect.sh relative to the repo root
54
+ const cwd = data.cwd || process.cwd();
55
+ const repoClashDetect = path.join(cwd, '.aether', 'utils', 'clash-detect.sh');
56
+
57
+ // Extract just the relative file path for clash-detect
58
+ const relPath = path.relative(cwd, filePath);
59
+
60
+ // Determine which clash-detect to use: repo-local or PATH
61
+ const fs = require('fs');
62
+ const clashDetect = fs.existsSync(repoClashDetect) ? repoClashDetect : 'clash-detect';
63
+
64
+ // Run clash-detect
65
+ try {
66
+ const cmd = clashDetect === 'clash-detect'
67
+ ? `clash-detect --file "${relPath}"`
68
+ : `bash "${clashDetect}" --file "${relPath}"`;
69
+ const result = execSync(cmd, {
70
+ timeout: DETECT_TIMEOUT,
71
+ encoding: 'utf8',
72
+ cwd: cwd,
73
+ stdio: ['pipe', 'pipe', 'pipe'],
74
+ });
75
+
76
+ const parsed = JSON.parse(result.trim());
77
+ if (parsed.ok && parsed.result?.conflict) {
78
+ const worktrees = (parsed.result.conflicting_worktrees || []).join(', ');
79
+ const output = {
80
+ decision: 'block',
81
+ reason: `File "${relPath}" has uncommitted changes in worktree(s): ${worktrees}. ` +
82
+ 'Coordinate with the other agent or wait for their changes to be committed.',
83
+ };
84
+ process.stderr.write(JSON.stringify(output) + '\n');
85
+ process.exit(2);
86
+ }
87
+
88
+ // No conflict -- allow
89
+ process.exit(0);
90
+ } catch (err) {
91
+ // Fail-open: if clash-detect fails, allow the operation
92
+ // This prevents the hook from blocking all work when something goes wrong
93
+ process.exit(0);
94
+ }
95
+ } catch (e) {
96
+ // Fail-open on any parsing or unexpected error
97
+ process.exit(0);
98
+ }
99
+ });