aether-colony 5.3.1 → 5.3.3

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 (165) hide show
  1. package/.aether/aether-utils.sh +181 -5
  2. package/.aether/commands/build.yaml +35 -0
  3. package/.aether/commands/entomb.yaml +1 -1
  4. package/.aether/commands/init.yaml +29 -12
  5. package/.aether/commands/oracle.yaml +70 -0
  6. package/.aether/commands/patrol.yaml +2 -2
  7. package/.aether/commands/run.yaml +3 -3
  8. package/.aether/commands/swarm.yaml +1 -1
  9. package/.aether/docs/command-playbooks/build-complete.md +41 -8
  10. package/.aether/docs/command-playbooks/build-full.md +7 -7
  11. package/.aether/docs/command-playbooks/build-prep.md +1 -1
  12. package/.aether/docs/command-playbooks/continue-advance.md +33 -0
  13. package/.aether/docs/command-playbooks/continue-finalize.md +15 -1
  14. package/.aether/docs/command-playbooks/continue-full.md +15 -1
  15. package/.aether/docs/source-of-truth-map.md +10 -10
  16. package/.aether/docs/structural-learning-stack.md +283 -0
  17. package/.aether/utils/consolidation-seal.sh +196 -0
  18. package/.aether/utils/consolidation.sh +127 -0
  19. package/.aether/utils/curation-ants/archivist.sh +97 -0
  20. package/.aether/utils/curation-ants/critic.sh +214 -0
  21. package/.aether/utils/curation-ants/herald.sh +102 -0
  22. package/.aether/utils/curation-ants/janitor.sh +121 -0
  23. package/.aether/utils/curation-ants/librarian.sh +99 -0
  24. package/.aether/utils/curation-ants/nurse.sh +153 -0
  25. package/.aether/utils/curation-ants/orchestrator.sh +181 -0
  26. package/.aether/utils/curation-ants/scribe.sh +164 -0
  27. package/.aether/utils/curation-ants/sentinel.sh +119 -0
  28. package/.aether/utils/event-bus.sh +301 -0
  29. package/.aether/utils/graph.sh +559 -0
  30. package/.aether/utils/instinct-store.sh +401 -0
  31. package/.aether/utils/learning.sh +79 -7
  32. package/.aether/utils/session.sh +13 -0
  33. package/.aether/utils/state-api.sh +1 -1
  34. package/.aether/utils/trust-scoring.sh +347 -0
  35. package/.aether/utils/worktree.sh +97 -0
  36. package/.claude/commands/ant/entomb.md +1 -1
  37. package/.claude/commands/ant/init.md +29 -12
  38. package/.claude/commands/ant/oracle.md +35 -0
  39. package/.claude/commands/ant/patrol.md +2 -2
  40. package/.claude/commands/ant/run.md +3 -3
  41. package/.claude/commands/ant/swarm.md +1 -1
  42. package/.opencode/commands/ant/build.md +35 -0
  43. package/.opencode/commands/ant/init.md +29 -12
  44. package/.opencode/commands/ant/oracle.md +35 -0
  45. package/.opencode/commands/ant/patrol.md +2 -2
  46. package/.opencode/commands/ant/run.md +3 -3
  47. package/CHANGELOG.md +83 -0
  48. package/README.md +34 -37
  49. package/bin/lib/update-transaction.js +8 -3
  50. package/bin/npx-entry.js +0 -0
  51. package/package.json +1 -1
  52. package/.aether/agents/aether-ambassador.md +0 -140
  53. package/.aether/agents/aether-archaeologist.md +0 -108
  54. package/.aether/agents/aether-architect.md +0 -133
  55. package/.aether/agents/aether-auditor.md +0 -144
  56. package/.aether/agents/aether-builder.md +0 -184
  57. package/.aether/agents/aether-chaos.md +0 -115
  58. package/.aether/agents/aether-chronicler.md +0 -122
  59. package/.aether/agents/aether-gatekeeper.md +0 -116
  60. package/.aether/agents/aether-includer.md +0 -117
  61. package/.aether/agents/aether-keeper.md +0 -177
  62. package/.aether/agents/aether-measurer.md +0 -128
  63. package/.aether/agents/aether-oracle.md +0 -137
  64. package/.aether/agents/aether-probe.md +0 -133
  65. package/.aether/agents/aether-queen.md +0 -286
  66. package/.aether/agents/aether-route-setter.md +0 -130
  67. package/.aether/agents/aether-sage.md +0 -106
  68. package/.aether/agents/aether-scout.md +0 -101
  69. package/.aether/agents/aether-surveyor-disciplines.md +0 -391
  70. package/.aether/agents/aether-surveyor-nest.md +0 -329
  71. package/.aether/agents/aether-surveyor-pathogens.md +0 -264
  72. package/.aether/agents/aether-surveyor-provisions.md +0 -334
  73. package/.aether/agents/aether-tracker.md +0 -137
  74. package/.aether/agents/aether-watcher.md +0 -174
  75. package/.aether/agents/aether-weaver.md +0 -130
  76. package/.aether/commands/claude/archaeology.md +0 -334
  77. package/.aether/commands/claude/build.md +0 -65
  78. package/.aether/commands/claude/chaos.md +0 -336
  79. package/.aether/commands/claude/colonize.md +0 -259
  80. package/.aether/commands/claude/continue.md +0 -60
  81. package/.aether/commands/claude/council.md +0 -507
  82. package/.aether/commands/claude/data-clean.md +0 -81
  83. package/.aether/commands/claude/dream.md +0 -268
  84. package/.aether/commands/claude/entomb.md +0 -498
  85. package/.aether/commands/claude/export-signals.md +0 -57
  86. package/.aether/commands/claude/feedback.md +0 -96
  87. package/.aether/commands/claude/flag.md +0 -151
  88. package/.aether/commands/claude/flags.md +0 -169
  89. package/.aether/commands/claude/focus.md +0 -76
  90. package/.aether/commands/claude/help.md +0 -154
  91. package/.aether/commands/claude/history.md +0 -140
  92. package/.aether/commands/claude/import-signals.md +0 -71
  93. package/.aether/commands/claude/init.md +0 -505
  94. package/.aether/commands/claude/insert-phase.md +0 -105
  95. package/.aether/commands/claude/interpret.md +0 -278
  96. package/.aether/commands/claude/lay-eggs.md +0 -210
  97. package/.aether/commands/claude/maturity.md +0 -113
  98. package/.aether/commands/claude/memory-details.md +0 -77
  99. package/.aether/commands/claude/migrate-state.md +0 -171
  100. package/.aether/commands/claude/oracle.md +0 -642
  101. package/.aether/commands/claude/organize.md +0 -232
  102. package/.aether/commands/claude/patrol.md +0 -620
  103. package/.aether/commands/claude/pause-colony.md +0 -233
  104. package/.aether/commands/claude/phase.md +0 -115
  105. package/.aether/commands/claude/pheromones.md +0 -156
  106. package/.aether/commands/claude/plan.md +0 -693
  107. package/.aether/commands/claude/preferences.md +0 -65
  108. package/.aether/commands/claude/quick.md +0 -100
  109. package/.aether/commands/claude/redirect.md +0 -76
  110. package/.aether/commands/claude/resume-colony.md +0 -197
  111. package/.aether/commands/claude/resume.md +0 -388
  112. package/.aether/commands/claude/run.md +0 -231
  113. package/.aether/commands/claude/seal.md +0 -774
  114. package/.aether/commands/claude/skill-create.md +0 -286
  115. package/.aether/commands/claude/status.md +0 -410
  116. package/.aether/commands/claude/swarm.md +0 -349
  117. package/.aether/commands/claude/tunnels.md +0 -426
  118. package/.aether/commands/claude/update.md +0 -132
  119. package/.aether/commands/claude/verify-castes.md +0 -143
  120. package/.aether/commands/claude/watch.md +0 -239
  121. package/.aether/commands/opencode/archaeology.md +0 -331
  122. package/.aether/commands/opencode/build.md +0 -1168
  123. package/.aether/commands/opencode/chaos.md +0 -329
  124. package/.aether/commands/opencode/colonize.md +0 -195
  125. package/.aether/commands/opencode/continue.md +0 -1436
  126. package/.aether/commands/opencode/council.md +0 -437
  127. package/.aether/commands/opencode/data-clean.md +0 -77
  128. package/.aether/commands/opencode/dream.md +0 -260
  129. package/.aether/commands/opencode/entomb.md +0 -377
  130. package/.aether/commands/opencode/export-signals.md +0 -54
  131. package/.aether/commands/opencode/feedback.md +0 -99
  132. package/.aether/commands/opencode/flag.md +0 -149
  133. package/.aether/commands/opencode/flags.md +0 -167
  134. package/.aether/commands/opencode/focus.md +0 -73
  135. package/.aether/commands/opencode/help.md +0 -157
  136. package/.aether/commands/opencode/history.md +0 -136
  137. package/.aether/commands/opencode/import-signals.md +0 -68
  138. package/.aether/commands/opencode/init.md +0 -518
  139. package/.aether/commands/opencode/insert-phase.md +0 -111
  140. package/.aether/commands/opencode/interpret.md +0 -272
  141. package/.aether/commands/opencode/lay-eggs.md +0 -213
  142. package/.aether/commands/opencode/maturity.md +0 -108
  143. package/.aether/commands/opencode/memory-details.md +0 -83
  144. package/.aether/commands/opencode/migrate-state.md +0 -165
  145. package/.aether/commands/opencode/oracle.md +0 -593
  146. package/.aether/commands/opencode/organize.md +0 -226
  147. package/.aether/commands/opencode/patrol.md +0 -626
  148. package/.aether/commands/opencode/pause-colony.md +0 -203
  149. package/.aether/commands/opencode/phase.md +0 -113
  150. package/.aether/commands/opencode/pheromones.md +0 -162
  151. package/.aether/commands/opencode/plan.md +0 -684
  152. package/.aether/commands/opencode/preferences.md +0 -71
  153. package/.aether/commands/opencode/quick.md +0 -91
  154. package/.aether/commands/opencode/redirect.md +0 -84
  155. package/.aether/commands/opencode/resume-colony.md +0 -190
  156. package/.aether/commands/opencode/resume.md +0 -394
  157. package/.aether/commands/opencode/run.md +0 -237
  158. package/.aether/commands/opencode/seal.md +0 -452
  159. package/.aether/commands/opencode/skill-create.md +0 -63
  160. package/.aether/commands/opencode/status.md +0 -307
  161. package/.aether/commands/opencode/swarm.md +0 -15
  162. package/.aether/commands/opencode/tunnels.md +0 -400
  163. package/.aether/commands/opencode/update.md +0 -127
  164. package/.aether/commands/opencode/verify-castes.md +0 -139
  165. package/.aether/commands/opencode/watch.md +0 -227
@@ -0,0 +1,119 @@
1
+ #!/bin/bash
2
+ # Curation Sentinel — Memory Health Monitoring
3
+ # Checks health of all memory stores and reports issues.
4
+ #
5
+ # Functions:
6
+ # _curation_sentinel
7
+ #
8
+ # These functions are sourced by aether-utils.sh at startup.
9
+ # All shared infrastructure (json_ok, json_err, COLONY_DATA_DIR, DATA_DIR,
10
+ # error constants) is available when sourced.
11
+
12
+ # ============================================================================
13
+ # _curation_sentinel
14
+ # Check health of all memory stores.
15
+ # Usage: curation-sentinel
16
+ #
17
+ # Output: json_ok with {checks:[{store,status,details}], healthy:N, issues:N}
18
+ # ============================================================================
19
+ _curation_sentinel() {
20
+ local cs_data_dir="${COLONY_DATA_DIR:-${DATA_DIR:-}}"
21
+ if [[ -z "$cs_data_dir" ]]; then
22
+ json_err "$E_VALIDATION_FAILED" "curation-sentinel: COLONY_DATA_DIR is not set"
23
+ fi
24
+
25
+ local cs_checks_json="[]"
26
+ local cs_healthy=0
27
+ local cs_issues=0
28
+
29
+ # Helper: append a check entry
30
+ # status "healthy" increments healthy; "optional_missing" is neutral;
31
+ # all other statuses (missing, corrupt, empty) increment issues.
32
+ _cs_add_check() {
33
+ local store="$1"
34
+ local status="$2"
35
+ local details="$3"
36
+
37
+ cs_checks_json=$(echo "$cs_checks_json" | jq \
38
+ --arg store "$store" \
39
+ --arg status "$status" \
40
+ --arg details "$details" \
41
+ '. += [{store:$store, status:$status, details:$details}]')
42
+
43
+ if [[ "$status" == "healthy" ]]; then
44
+ cs_healthy=$(( cs_healthy + 1 ))
45
+ elif [[ "$status" != "optional_missing" ]]; then
46
+ cs_issues=$(( cs_issues + 1 ))
47
+ fi
48
+ }
49
+
50
+ # Helper: check a JSON file
51
+ _cs_check_json_file() {
52
+ local store="$1"
53
+ local filepath="$2"
54
+ local required="${3:-false}"
55
+
56
+ if [[ ! -f "$filepath" ]]; then
57
+ if [[ "$required" == "true" ]]; then
58
+ _cs_add_check "$store" "missing" "Required file not found: $filepath"
59
+ else
60
+ _cs_add_check "$store" "optional_missing" "Optional file not found: $filepath"
61
+ fi
62
+ return
63
+ fi
64
+
65
+ if [[ ! -s "$filepath" ]]; then
66
+ _cs_add_check "$store" "empty" "File exists but is empty: $filepath"
67
+ return
68
+ fi
69
+
70
+ if ! jq empty "$filepath" 2>/dev/null; then
71
+ _cs_add_check "$store" "corrupt" "File contains invalid JSON: $filepath"
72
+ return
73
+ fi
74
+
75
+ _cs_add_check "$store" "healthy" "OK"
76
+ }
77
+
78
+ # 1. learning-observations.json
79
+ _cs_check_json_file "learning-observations" \
80
+ "$cs_data_dir/learning-observations.json" "false"
81
+
82
+ # 2. instincts.json (optional)
83
+ _cs_check_json_file "instincts" \
84
+ "$cs_data_dir/instincts.json" "false"
85
+
86
+ # 3. instinct-graph.json (optional)
87
+ _cs_check_json_file "instinct-graph" \
88
+ "$cs_data_dir/instinct-graph.json" "false"
89
+
90
+ # 4. event-bus.jsonl (optional — check last line if exists)
91
+ local eb_file="$cs_data_dir/event-bus.jsonl"
92
+ if [[ ! -f "$eb_file" ]]; then
93
+ _cs_add_check "event-bus" "optional_missing" "Optional file not found: $eb_file"
94
+ elif [[ ! -s "$eb_file" ]]; then
95
+ _cs_add_check "event-bus" "healthy" "File is empty (no events)"
96
+ else
97
+ local last_line
98
+ last_line=$(tail -1 "$eb_file")
99
+ if echo "$last_line" | jq empty 2>/dev/null; then
100
+ _cs_add_check "event-bus" "healthy" "OK"
101
+ else
102
+ _cs_add_check "event-bus" "corrupt" "Last line is not valid JSON"
103
+ fi
104
+ fi
105
+
106
+ # 5. pheromones.json (required)
107
+ _cs_check_json_file "pheromones" \
108
+ "$cs_data_dir/pheromones.json" "true"
109
+
110
+ # 6. COLONY_STATE.json (required)
111
+ _cs_check_json_file "COLONY_STATE" \
112
+ "$cs_data_dir/COLONY_STATE.json" "true"
113
+
114
+ json_ok "$(jq -nc \
115
+ --argjson checks "$cs_checks_json" \
116
+ --argjson healthy "$cs_healthy" \
117
+ --argjson issues "$cs_issues" \
118
+ '{checks:$checks, healthy:$healthy, issues:$issues}')"
119
+ }
@@ -0,0 +1,301 @@
1
+ #!/bin/bash
2
+ # Event bus utility functions for the Aether Structural Learning Stack
3
+ # Provides: _event_publish, _event_subscribe, _event_cleanup, _event_replay
4
+ #
5
+ # These functions are sourced by aether-utils.sh at startup.
6
+ # All shared infrastructure (json_ok, json_err, json_warn, atomic_write, acquire_lock,
7
+ # release_lock, feature_enabled, LOCK_DIR, DATA_DIR, SCRIPT_DIR, error constants) is available.
8
+
9
+ # Default TTL for events in days
10
+ _EVENT_BUS_DEFAULT_TTL=30
11
+ _EVENT_BUS_DEFAULT_LIMIT=50
12
+
13
+ # ============================================================================
14
+ # _event_publish
15
+ # Publish an event to the JSONL event bus.
16
+ # Usage: event-publish --topic <topic> --payload <json> [--source <src>] [--ttl <days>]
17
+ # ============================================================================
18
+ _event_publish() {
19
+ local ep_topic=""
20
+ local ep_payload=""
21
+ local ep_source="system"
22
+ local ep_ttl="$_EVENT_BUS_DEFAULT_TTL"
23
+
24
+ # Parse arguments
25
+ while [[ $# -gt 0 ]]; do
26
+ case "$1" in
27
+ --topic)
28
+ ep_topic="${2:-}"
29
+ shift 2
30
+ ;;
31
+ --payload)
32
+ ep_payload="${2:-}"
33
+ shift 2
34
+ ;;
35
+ --source)
36
+ ep_source="${2:-system}"
37
+ shift 2
38
+ ;;
39
+ --ttl)
40
+ ep_ttl="${2:-$_EVENT_BUS_DEFAULT_TTL}"
41
+ shift 2
42
+ ;;
43
+ *)
44
+ shift
45
+ ;;
46
+ esac
47
+ done
48
+
49
+ [[ -z "$ep_topic" ]] && json_err "$E_VALIDATION_FAILED" "event-publish requires --topic"
50
+ [[ -z "$ep_payload" ]] && json_err "$E_VALIDATION_FAILED" "event-publish requires --payload"
51
+
52
+ # Validate payload is valid JSON
53
+ echo "$ep_payload" | jq empty 2>/dev/null \
54
+ || json_err "$E_JSON_INVALID" "event-publish --payload must be valid JSON"
55
+
56
+ mkdir -p "$COLONY_DATA_DIR"
57
+ local bus_file="$COLONY_DATA_DIR/event-bus.jsonl"
58
+
59
+ # Generate unique ID and timestamps
60
+ local ep_id
61
+ ep_id="evt_$(date +%s)_$(head -c 2 /dev/urandom | od -An -tx1 | tr -d ' \n')"
62
+ local ep_ts
63
+ ep_ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)
64
+ local ep_expires
65
+ ep_expires=$(date -u -v+"${ep_ttl}"d +%Y-%m-%dT%H:%M:%SZ 2>/dev/null \
66
+ || date -u -d "+${ep_ttl} days" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null \
67
+ || echo "2099-01-01T00:00:00Z")
68
+
69
+ # Build event JSON line
70
+ local ep_line
71
+ ep_line=$(jq -nc \
72
+ --arg id "$ep_id" \
73
+ --arg topic "$ep_topic" \
74
+ --argjson payload "$ep_payload" \
75
+ --arg source "$ep_source" \
76
+ --arg ts "$ep_ts" \
77
+ --argjson ttl_days "$ep_ttl" \
78
+ --arg expires_at "$ep_expires" \
79
+ '{id:$id,topic:$topic,payload:$payload,source:$source,timestamp:$ts,ttl_days:$ttl_days,expires_at:$expires_at}')
80
+
81
+ # Acquire lock for safe concurrent append
82
+ acquire_lock "event-bus" 5 2>/dev/null \
83
+ || json_err "$E_LOCK_FAILED" "event-publish: failed to acquire lock"
84
+ trap 'release_lock "event-bus" 2>/dev/null || true' EXIT
85
+
86
+ echo "$ep_line" >> "$bus_file"
87
+
88
+ release_lock "event-bus" 2>/dev/null || true
89
+
90
+ json_ok "$(jq -nc \
91
+ --arg event_id "$ep_id" \
92
+ --arg topic "$ep_topic" \
93
+ --argjson ttl_days "$ep_ttl" \
94
+ '{event_id:$event_id,topic:$topic,ttl_days:$ttl_days}')"
95
+ }
96
+
97
+ # ============================================================================
98
+ # _event_subscribe
99
+ # Read events matching a topic pattern from the JSONL bus.
100
+ # Usage: event-subscribe --topic <pattern> [--since <ISO-8601>] [--limit <N>]
101
+ # Pattern supports exact match or prefix with trailing '*' (e.g., "learning.*")
102
+ # ============================================================================
103
+ _event_subscribe() {
104
+ local es_topic=""
105
+ local es_since=""
106
+ local es_limit="$_EVENT_BUS_DEFAULT_LIMIT"
107
+
108
+ while [[ $# -gt 0 ]]; do
109
+ case "$1" in
110
+ --topic)
111
+ es_topic="${2:-}"
112
+ shift 2
113
+ ;;
114
+ --since)
115
+ es_since="${2:-}"
116
+ shift 2
117
+ ;;
118
+ --limit)
119
+ es_limit="${2:-$_EVENT_BUS_DEFAULT_LIMIT}"
120
+ shift 2
121
+ ;;
122
+ *)
123
+ shift
124
+ ;;
125
+ esac
126
+ done
127
+
128
+ [[ -z "$es_topic" ]] && json_err "$E_VALIDATION_FAILED" "event-subscribe requires --topic"
129
+
130
+ local bus_file="$COLONY_DATA_DIR/event-bus.jsonl"
131
+ local now_ts
132
+ now_ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)
133
+
134
+ # If bus file does not exist, return empty result
135
+ if [[ ! -f "$bus_file" ]]; then
136
+ json_ok "$(jq -nc \
137
+ --arg pattern "$es_topic" \
138
+ '{events:[],count:0,topic_pattern:$pattern}')"
139
+ return 0
140
+ fi
141
+
142
+ # Determine if pattern is prefix match (ends with *) or exact match
143
+ local es_jq_filter
144
+ if [[ "$es_topic" == *"*" ]]; then
145
+ local es_prefix="${es_topic%\*}"
146
+ es_jq_filter="startswith(\"$es_prefix\")"
147
+ else
148
+ es_jq_filter=". == \"$es_topic\""
149
+ fi
150
+
151
+ # Build jq filter: topic match + not expired + since filter
152
+ local es_jq_expr
153
+ es_jq_expr=". | select(.topic | $es_jq_filter) | select(.expires_at > \"$now_ts\")"
154
+ if [[ -n "$es_since" ]]; then
155
+ es_jq_expr="$es_jq_expr | select(.timestamp >= \"$es_since\")"
156
+ fi
157
+
158
+ local es_events
159
+ es_events=$(jq -sc \
160
+ --argjson limit "$es_limit" \
161
+ "[.[] | $es_jq_expr] | .[:(\$limit)]" \
162
+ "$bus_file" 2>/dev/null || echo "[]")
163
+
164
+ local es_count
165
+ es_count=$(echo "$es_events" | jq 'length')
166
+
167
+ json_ok "$(jq -nc \
168
+ --argjson events "$es_events" \
169
+ --argjson count "$es_count" \
170
+ --arg topic_pattern "$es_topic" \
171
+ '{events:$events,count:$count,topic_pattern:$topic_pattern}')"
172
+ }
173
+
174
+ # ============================================================================
175
+ # _event_cleanup
176
+ # Remove expired events from the JSONL bus.
177
+ # Usage: event-cleanup [--dry-run]
178
+ # ============================================================================
179
+ _event_cleanup() {
180
+ local ec_dry_run="false"
181
+
182
+ while [[ $# -gt 0 ]]; do
183
+ case "$1" in
184
+ --dry-run)
185
+ ec_dry_run="true"
186
+ shift
187
+ ;;
188
+ *)
189
+ shift
190
+ ;;
191
+ esac
192
+ done
193
+
194
+ local bus_file="$COLONY_DATA_DIR/event-bus.jsonl"
195
+ local now_ts
196
+ now_ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)
197
+
198
+ # If bus file does not exist, nothing to clean
199
+ if [[ ! -f "$bus_file" ]]; then
200
+ json_ok "$(jq -nc \
201
+ --argjson dry_run "$ec_dry_run" \
202
+ '{removed:0,remaining:0,dry_run:$dry_run}')"
203
+ return 0
204
+ fi
205
+
206
+ local ec_total
207
+ ec_total=$(wc -l < "$bus_file" | tr -d ' ')
208
+
209
+ local ec_kept
210
+ ec_kept=$(jq -c "select(.expires_at > \"$now_ts\")" "$bus_file" 2>/dev/null || true)
211
+
212
+ local ec_kept_count=0
213
+ [[ -n "$ec_kept" ]] && ec_kept_count=$(echo "$ec_kept" | wc -l | tr -d ' ')
214
+
215
+ local ec_removed=$(( ec_total - ec_kept_count ))
216
+
217
+ if [[ "$ec_dry_run" == "false" ]]; then
218
+ # Acquire lock for safe atomic rewrite
219
+ acquire_lock "event-bus" 5 2>/dev/null \
220
+ || json_err "$E_LOCK_FAILED" "event-cleanup: failed to acquire lock"
221
+ trap 'release_lock "event-bus" 2>/dev/null || true' EXIT
222
+
223
+ if [[ -n "$ec_kept" ]]; then
224
+ atomic_write "$bus_file" "$ec_kept"
225
+ else
226
+ # All events expired — write empty file (touch creates it, or truncate)
227
+ : > "$bus_file"
228
+ fi
229
+
230
+ release_lock "event-bus" 2>/dev/null || true
231
+ fi
232
+
233
+ json_ok "$(jq -nc \
234
+ --argjson removed "$ec_removed" \
235
+ --argjson remaining "$ec_kept_count" \
236
+ --argjson dry_run "$ec_dry_run" \
237
+ '{removed:$removed,remaining:$remaining,dry_run:$dry_run}')"
238
+ }
239
+
240
+ # ============================================================================
241
+ # _event_replay
242
+ # Replay events for a topic from a given timestamp.
243
+ # Usage: event-replay --topic <topic> --since <ISO-8601> [--limit <N>]
244
+ # ============================================================================
245
+ _event_replay() {
246
+ local er_topic=""
247
+ local er_since=""
248
+ local er_limit="$_EVENT_BUS_DEFAULT_LIMIT"
249
+
250
+ while [[ $# -gt 0 ]]; do
251
+ case "$1" in
252
+ --topic)
253
+ er_topic="${2:-}"
254
+ shift 2
255
+ ;;
256
+ --since)
257
+ er_since="${2:-}"
258
+ shift 2
259
+ ;;
260
+ --limit)
261
+ er_limit="${2:-$_EVENT_BUS_DEFAULT_LIMIT}"
262
+ shift 2
263
+ ;;
264
+ *)
265
+ shift
266
+ ;;
267
+ esac
268
+ done
269
+
270
+ [[ -z "$er_topic" ]] && json_err "$E_VALIDATION_FAILED" "event-replay requires --topic"
271
+ [[ -z "$er_since" ]] && json_err "$E_VALIDATION_FAILED" "event-replay requires --since"
272
+
273
+ local bus_file="$COLONY_DATA_DIR/event-bus.jsonl"
274
+ local now_ts
275
+ now_ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)
276
+
277
+ if [[ ! -f "$bus_file" ]]; then
278
+ json_ok "$(jq -nc \
279
+ --arg replayed_from "$er_since" \
280
+ '{events:[],count:0,replayed_from:$replayed_from}')"
281
+ return 0
282
+ fi
283
+
284
+ local er_events
285
+ er_events=$(jq -sc \
286
+ --arg topic "$er_topic" \
287
+ --arg since "$er_since" \
288
+ --arg now "$now_ts" \
289
+ --argjson limit "$er_limit" \
290
+ '[.[] | select(.topic == $topic) | select(.expires_at > $now) | select(.timestamp >= $since)] | sort_by(.timestamp) | .[:$limit]' \
291
+ "$bus_file" 2>/dev/null || echo "[]")
292
+
293
+ local er_count
294
+ er_count=$(echo "$er_events" | jq 'length')
295
+
296
+ json_ok "$(jq -nc \
297
+ --argjson events "$er_events" \
298
+ --argjson count "$er_count" \
299
+ --arg replayed_from "$er_since" \
300
+ '{events:$events,count:$count,replayed_from:$replayed_from}')"
301
+ }