aether-colony 5.1.0 → 5.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/.aether/aether-utils.sh +122 -42
  2. package/.aether/commands/colonize.yaml +4 -0
  3. package/.aether/commands/council.yaml +205 -0
  4. package/.aether/commands/init.yaml +46 -13
  5. package/.aether/commands/insert-phase.yaml +4 -0
  6. package/.aether/commands/plan.yaml +53 -2
  7. package/.aether/commands/quick.yaml +104 -0
  8. package/.aether/commands/resume-colony.yaml +6 -4
  9. package/.aether/commands/resume.yaml +9 -0
  10. package/.aether/commands/run.yaml +37 -1
  11. package/.aether/commands/seal.yaml +9 -0
  12. package/.aether/commands/status.yaml +45 -1
  13. package/.aether/docs/command-playbooks/build-full.md +2 -1
  14. package/.aether/docs/command-playbooks/build-prep.md +2 -1
  15. package/.aether/docs/command-playbooks/continue-full.md +1 -0
  16. package/.aether/docs/command-playbooks/continue-verify.md +1 -0
  17. package/.aether/utils/council.sh +425 -0
  18. package/.aether/utils/error-handler.sh +3 -3
  19. package/.aether/utils/flag.sh +23 -12
  20. package/.aether/utils/hive.sh +2 -2
  21. package/.aether/utils/immune.sh +508 -0
  22. package/.aether/utils/learning.sh +2 -2
  23. package/.aether/utils/midden.sh +178 -0
  24. package/.aether/utils/queen.sh +29 -17
  25. package/.aether/utils/session.sh +264 -0
  26. package/.aether/utils/spawn-tree.sh +7 -7
  27. package/.aether/utils/spawn.sh +2 -2
  28. package/.aether/utils/state-api.sh +191 -1
  29. package/.claude/commands/ant/colonize.md +2 -0
  30. package/.claude/commands/ant/council.md +205 -0
  31. package/.claude/commands/ant/init.md +46 -13
  32. package/.claude/commands/ant/insert-phase.md +4 -0
  33. package/.claude/commands/ant/plan.md +27 -1
  34. package/.claude/commands/ant/quick.md +100 -0
  35. package/.claude/commands/ant/resume-colony.md +3 -2
  36. package/.claude/commands/ant/resume.md +9 -0
  37. package/.claude/commands/ant/run.md +37 -1
  38. package/.claude/commands/ant/seal.md +9 -0
  39. package/.claude/commands/ant/status.md +45 -1
  40. package/.opencode/commands/ant/colonize.md +2 -0
  41. package/.opencode/commands/ant/council.md +143 -0
  42. package/.opencode/commands/ant/init.md +46 -13
  43. package/.opencode/commands/ant/insert-phase.md +4 -0
  44. package/.opencode/commands/ant/plan.md +26 -1
  45. package/.opencode/commands/ant/quick.md +91 -0
  46. package/.opencode/commands/ant/resume-colony.md +3 -2
  47. package/.opencode/commands/ant/resume.md +9 -0
  48. package/.opencode/commands/ant/run.md +37 -1
  49. package/.opencode/commands/ant/status.md +2 -0
  50. package/CHANGELOG.md +90 -0
  51. package/README.md +23 -0
  52. package/package.json +10 -2
@@ -341,6 +341,15 @@ body_claude: |
341
341
  bash .aether/aether-utils.sh activity-log "MODIFIED" "Queen" "Colony sealed - wisdom review completed"
342
342
  ```
343
343
 
344
+ ### Step 4.4: Checkpoint State
345
+
346
+ Before modifying colony state, create a rolling backup:
347
+
348
+ Run using the Bash tool with description "Checkpointing colony state before seal...":
349
+ ```bash
350
+ bash .aether/aether-utils.sh state-checkpoint "pre-seal" 2>/dev/null || echo "Warning: State checkpoint failed -- continuing without backup" >&2
351
+ ```
352
+
344
353
  ### Step 4.5: Increment Colony Version
345
354
 
346
355
  Before writing the Crowned Anthill milestone, increment `colony_version` in COLONY_STATE.json.
@@ -300,9 +300,53 @@ body: |
300
300
  ```
301
301
 
302
302
  {{#claude}}
303
- **Data Safety:**
303
+ **Colony Vital Signs:**
304
304
  After the Memory Health table, run:
305
305
  ```bash
306
+ bash .aether/aether-utils.sh colony-vital-signs
307
+ ```
308
+
309
+ Extract from JSON result:
310
+ - build_velocity.phases_per_day and build_velocity.trend
311
+ - error_rate.errors_per_day and error_rate.status
312
+ - signal_health.active_count and signal_health.status
313
+ - memory_pressure.instinct_count and memory_pressure.status
314
+ - colony_age_hours
315
+ - overall_health (0-100 score)
316
+
317
+ Map overall_health score to a label:
318
+ - 80-100: "Thriving"
319
+ - 60-79: "Healthy"
320
+ - 40-59: "Stable"
321
+ - 20-39: "Struggling"
322
+ - 0-19: "Critical"
323
+
324
+ Display:
325
+ ```
326
+ 💓 Colony Vital Signs
327
+ ┌─────────────────┬────────────┬─────────────────────────────┐
328
+ │ Vital Sign │ Value │ Status │
329
+ ├─────────────────┼────────────┼─────────────────────────────┤
330
+ │ Build Velocity │ {phases_per_day}/d │ {trend} │
331
+ │ Error Rate │ {errors_per_day}/d │ {error_status} │
332
+ │ Signal Health │ {active_count} │ {signal_status} │
333
+ │ Memory Pressure │ {instinct_count} │ {memory_status} │
334
+ │ Colony Age │ {colony_age_hours}h │ │
335
+ ├─────────────────┼────────────┼─────────────────────────────┤
336
+ │ Overall Health │ {overall_health}% │ {health_label} │
337
+ └─────────────────┴────────────┴─────────────────────────────┘
338
+ ```
339
+
340
+ If the command fails or returns no data, display:
341
+ ```
342
+ 💓 Colony Vital Signs: No data available
343
+ ```
344
+ {{/claude}}
345
+
346
+ {{#claude}}
347
+ **Data Safety:**
348
+ After the Colony Vital Signs panel, run:
349
+ ```bash
306
350
  bash .aether/aether-utils.sh data-safety-stats
307
351
  ```
308
352
 
@@ -98,7 +98,8 @@ If the command fails (non-zero exit or JSON has ok: false):
98
98
  If successful:
99
99
  1. Parse the state JSON from result field
100
100
  2. Check if goal is null - if so: "No colony initialized. Run /ant:init first." and stop
101
- 3. Extract current_phase and phase name from plan.phases[current_phase - 1].name
101
+ 3. Check if `milestone` == `"Crowned Anthill"` - if so: "This colony has been sealed. Start a new colony with `/ant:init \"new goal\"`." and stop
102
+ 4. Extract current_phase and phase name from plan.phases[current_phase - 1].name
102
103
  4. Display brief resumption context:
103
104
  ```
104
105
  🔄 Resuming: Phase X - Name
@@ -98,7 +98,8 @@ If the command fails (non-zero exit or JSON has ok: false):
98
98
  If successful:
99
99
  1. Parse the state JSON from result field
100
100
  2. Check if goal is null - if so: "No colony initialized. Run /ant:init first." and stop
101
- 3. Extract current_phase and phase name from plan.phases[current_phase - 1].name
101
+ 3. Check if `milestone` == `"Crowned Anthill"` - if so: "This colony has been sealed. Start a new colony with `/ant:init \"new goal\"`." and stop
102
+ 4. Extract current_phase and phase name from plan.phases[current_phase - 1].name
102
103
  4. Display brief resumption context:
103
104
  ```
104
105
  🔄 Resuming: Phase X - Name
@@ -26,6 +26,7 @@ Extract: `goal`, `state`, `current_phase`, `plan.phases`, `errors`, `memory`, `e
26
26
 
27
27
  **Validation:**
28
28
  - If `goal: null` -> output "No colony initialized. Run /ant:init first." and stop.
29
+ - If `milestone` == `"Crowned Anthill"` -> output "This colony has been sealed. Start a new colony with `/ant:init \"new goal\"`." and stop.
29
30
  - If `plan.phases` is empty -> output "No project plan. Run /ant:plan first." and stop.
30
31
 
31
32
  ### Step 1.5: Load State and Show Resumption Context
@@ -26,6 +26,7 @@ Extract: `goal`, `state`, `current_phase`, `plan.phases`, `errors`, `memory`, `e
26
26
 
27
27
  **Validation:**
28
28
  - If `goal: null` -> output "No colony initialized. Run /ant:init first." and stop.
29
+ - If `milestone` == `"Crowned Anthill"` -> output "This colony has been sealed. Start a new colony with `/ant:init \"new goal\"`." and stop.
29
30
  - If `plan.phases` is empty -> output "No project plan. Run /ant:plan first." and stop.
30
31
 
31
32
  ### Step 1.5: Load State and Show Resumption Context
@@ -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"}'