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.
- package/.aether/aether-utils.sh +157 -42
- package/.aether/agents/aether-ambassador.md +140 -0
- package/.aether/agents/aether-archaeologist.md +108 -0
- package/.aether/agents/aether-architect.md +133 -0
- package/.aether/agents/aether-auditor.md +144 -0
- package/.aether/agents/aether-builder.md +184 -0
- package/.aether/agents/aether-chaos.md +115 -0
- package/.aether/agents/aether-chronicler.md +122 -0
- package/.aether/agents/aether-gatekeeper.md +116 -0
- package/.aether/agents/aether-includer.md +117 -0
- package/.aether/agents/aether-keeper.md +177 -0
- package/.aether/agents/aether-measurer.md +128 -0
- package/.aether/agents/aether-oracle.md +137 -0
- package/.aether/agents/aether-probe.md +133 -0
- package/.aether/agents/aether-queen.md +286 -0
- package/.aether/agents/aether-route-setter.md +130 -0
- package/.aether/agents/aether-sage.md +106 -0
- package/.aether/agents/aether-scout.md +101 -0
- package/.aether/agents/aether-surveyor-disciplines.md +391 -0
- package/.aether/agents/aether-surveyor-nest.md +329 -0
- package/.aether/agents/aether-surveyor-pathogens.md +264 -0
- package/.aether/agents/aether-surveyor-provisions.md +334 -0
- package/.aether/agents/aether-tracker.md +137 -0
- package/.aether/agents/aether-watcher.md +174 -0
- package/.aether/agents/aether-weaver.md +130 -0
- package/.aether/commands/claude/archaeology.md +334 -0
- package/.aether/commands/claude/build.md +65 -0
- package/.aether/commands/claude/chaos.md +336 -0
- package/.aether/commands/claude/colonize.md +259 -0
- package/.aether/commands/claude/continue.md +60 -0
- package/.aether/commands/claude/council.md +507 -0
- package/.aether/commands/claude/data-clean.md +81 -0
- package/.aether/commands/claude/dream.md +268 -0
- package/.aether/commands/claude/entomb.md +498 -0
- package/.aether/commands/claude/export-signals.md +57 -0
- package/.aether/commands/claude/feedback.md +96 -0
- package/.aether/commands/claude/flag.md +151 -0
- package/.aether/commands/claude/flags.md +169 -0
- package/.aether/commands/claude/focus.md +76 -0
- package/.aether/commands/claude/help.md +154 -0
- package/.aether/commands/claude/history.md +140 -0
- package/.aether/commands/claude/import-signals.md +71 -0
- package/.aether/commands/claude/init.md +505 -0
- package/.aether/commands/claude/insert-phase.md +105 -0
- package/.aether/commands/claude/interpret.md +278 -0
- package/.aether/commands/claude/lay-eggs.md +210 -0
- package/.aether/commands/claude/maturity.md +113 -0
- package/.aether/commands/claude/memory-details.md +77 -0
- package/.aether/commands/claude/migrate-state.md +171 -0
- package/.aether/commands/claude/oracle.md +642 -0
- package/.aether/commands/claude/organize.md +232 -0
- package/.aether/commands/claude/patrol.md +620 -0
- package/.aether/commands/claude/pause-colony.md +233 -0
- package/.aether/commands/claude/phase.md +115 -0
- package/.aether/commands/claude/pheromones.md +156 -0
- package/.aether/commands/claude/plan.md +693 -0
- package/.aether/commands/claude/preferences.md +65 -0
- package/.aether/commands/claude/quick.md +100 -0
- package/.aether/commands/claude/redirect.md +76 -0
- package/.aether/commands/claude/resume-colony.md +197 -0
- package/.aether/commands/claude/resume.md +388 -0
- package/.aether/commands/claude/run.md +231 -0
- package/.aether/commands/claude/seal.md +774 -0
- package/.aether/commands/claude/skill-create.md +286 -0
- package/.aether/commands/claude/status.md +410 -0
- package/.aether/commands/claude/swarm.md +349 -0
- package/.aether/commands/claude/tunnels.md +426 -0
- package/.aether/commands/claude/update.md +132 -0
- package/.aether/commands/claude/verify-castes.md +143 -0
- package/.aether/commands/claude/watch.md +239 -0
- package/.aether/commands/colonize.yaml +4 -0
- package/.aether/commands/council.yaml +205 -0
- package/.aether/commands/init.yaml +46 -13
- package/.aether/commands/insert-phase.yaml +4 -0
- package/.aether/commands/opencode/archaeology.md +331 -0
- package/.aether/commands/opencode/build.md +1168 -0
- package/.aether/commands/opencode/chaos.md +329 -0
- package/.aether/commands/opencode/colonize.md +195 -0
- package/.aether/commands/opencode/continue.md +1436 -0
- package/.aether/commands/opencode/council.md +437 -0
- package/.aether/commands/opencode/data-clean.md +77 -0
- package/.aether/commands/opencode/dream.md +260 -0
- package/.aether/commands/opencode/entomb.md +377 -0
- package/.aether/commands/opencode/export-signals.md +54 -0
- package/.aether/commands/opencode/feedback.md +99 -0
- package/.aether/commands/opencode/flag.md +149 -0
- package/.aether/commands/opencode/flags.md +167 -0
- package/.aether/commands/opencode/focus.md +73 -0
- package/.aether/commands/opencode/help.md +157 -0
- package/.aether/commands/opencode/history.md +136 -0
- package/.aether/commands/opencode/import-signals.md +68 -0
- package/.aether/commands/opencode/init.md +518 -0
- package/.aether/commands/opencode/insert-phase.md +111 -0
- package/.aether/commands/opencode/interpret.md +272 -0
- package/.aether/commands/opencode/lay-eggs.md +213 -0
- package/.aether/commands/opencode/maturity.md +108 -0
- package/.aether/commands/opencode/memory-details.md +83 -0
- package/.aether/commands/opencode/migrate-state.md +165 -0
- package/.aether/commands/opencode/oracle.md +593 -0
- package/.aether/commands/opencode/organize.md +226 -0
- package/.aether/commands/opencode/patrol.md +626 -0
- package/.aether/commands/opencode/pause-colony.md +203 -0
- package/.aether/commands/opencode/phase.md +113 -0
- package/.aether/commands/opencode/pheromones.md +162 -0
- package/.aether/commands/opencode/plan.md +684 -0
- package/.aether/commands/opencode/preferences.md +71 -0
- package/.aether/commands/opencode/quick.md +91 -0
- package/.aether/commands/opencode/redirect.md +84 -0
- package/.aether/commands/opencode/resume-colony.md +190 -0
- package/.aether/commands/opencode/resume.md +394 -0
- package/.aether/commands/opencode/run.md +237 -0
- package/.aether/commands/opencode/seal.md +452 -0
- package/.aether/commands/opencode/skill-create.md +63 -0
- package/.aether/commands/opencode/status.md +307 -0
- package/.aether/commands/opencode/swarm.md +15 -0
- package/.aether/commands/opencode/tunnels.md +400 -0
- package/.aether/commands/opencode/update.md +127 -0
- package/.aether/commands/opencode/verify-castes.md +139 -0
- package/.aether/commands/opencode/watch.md +227 -0
- package/.aether/commands/plan.yaml +53 -2
- package/.aether/commands/quick.yaml +104 -0
- package/.aether/commands/resume-colony.yaml +6 -4
- package/.aether/commands/resume.yaml +9 -0
- package/.aether/commands/run.yaml +37 -1
- package/.aether/commands/seal.yaml +9 -0
- package/.aether/commands/status.yaml +45 -1
- package/.aether/docs/command-playbooks/build-full.md +3 -2
- package/.aether/docs/command-playbooks/build-prep.md +12 -4
- package/.aether/docs/command-playbooks/build-verify.md +51 -0
- package/.aether/docs/command-playbooks/continue-advance.md +115 -6
- package/.aether/docs/command-playbooks/continue-full.md +1 -0
- package/.aether/docs/command-playbooks/continue-verify.md +33 -0
- package/.aether/utils/clash-detect.sh +239 -0
- package/.aether/utils/council.sh +425 -0
- package/.aether/utils/error-handler.sh +3 -3
- package/.aether/utils/flag.sh +23 -12
- package/.aether/utils/hive.sh +2 -2
- package/.aether/utils/hooks/clash-pre-tool-use.js +99 -0
- package/.aether/utils/immune.sh +508 -0
- package/.aether/utils/learning.sh +2 -2
- package/.aether/utils/merge-driver-lockfile.sh +35 -0
- package/.aether/utils/midden.sh +712 -0
- package/.aether/utils/pheromone.sh +1376 -108
- package/.aether/utils/queen.sh +31 -21
- package/.aether/utils/session.sh +264 -0
- package/.aether/utils/spawn-tree.sh +7 -7
- package/.aether/utils/spawn.sh +2 -2
- package/.aether/utils/state-api.sh +216 -5
- package/.aether/utils/swarm.sh +1 -1
- package/.aether/utils/worktree.sh +189 -0
- package/.claude/commands/ant/colonize.md +2 -0
- package/.claude/commands/ant/council.md +205 -0
- package/.claude/commands/ant/init.md +53 -14
- package/.claude/commands/ant/insert-phase.md +4 -0
- package/.claude/commands/ant/plan.md +27 -1
- package/.claude/commands/ant/quick.md +100 -0
- package/.claude/commands/ant/resume-colony.md +3 -2
- package/.claude/commands/ant/resume.md +9 -0
- package/.claude/commands/ant/run.md +37 -1
- package/.claude/commands/ant/seal.md +9 -0
- package/.claude/commands/ant/status.md +45 -1
- package/.opencode/commands/ant/colonize.md +2 -0
- package/.opencode/commands/ant/council.md +143 -0
- package/.opencode/commands/ant/init.md +53 -13
- package/.opencode/commands/ant/insert-phase.md +4 -0
- package/.opencode/commands/ant/plan.md +26 -1
- package/.opencode/commands/ant/quick.md +91 -0
- package/.opencode/commands/ant/resume-colony.md +3 -2
- package/.opencode/commands/ant/resume.md +9 -0
- package/.opencode/commands/ant/run.md +37 -1
- package/.opencode/commands/ant/status.md +2 -0
- package/CHANGELOG.md +116 -0
- package/README.md +34 -8
- package/bin/cli.js +103 -61
- package/bin/lib/banner.js +14 -0
- package/bin/lib/init.js +8 -7
- package/bin/lib/interactive-setup.js +251 -0
- package/bin/npx-entry.js +21 -0
- package/bin/npx-install.js +9 -167
- package/bin/validate-package.sh +23 -0
- package/package.json +11 -3
- package/.aether/docs/plans/pheromone-display-plan.md +0 -257
- package/.aether/schemas/example-prompt-builder.xml +0 -234
- package/.aether/scripts/incident-test-add.sh +0 -47
- package/.aether/scripts/weekly-audit.sh +0 -79
package/.aether/utils/midden.sh
CHANGED
|
@@ -244,6 +244,184 @@ _midden_ingest_errors() {
|
|
|
244
244
|
return 0
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
+
_midden_search() {
|
|
248
|
+
# Search midden entries by keyword match in message field
|
|
249
|
+
# Usage: midden-search <query> [--category <cat>] [--source <src>] [--limit N] [--include-acknowledged]
|
|
250
|
+
# Returns: JSON with query, match_count, and entries array
|
|
251
|
+
|
|
252
|
+
ms_query=""
|
|
253
|
+
ms_category=""
|
|
254
|
+
ms_source=""
|
|
255
|
+
ms_limit=10
|
|
256
|
+
ms_include_ack=false
|
|
257
|
+
|
|
258
|
+
# First positional arg is the query
|
|
259
|
+
if [[ $# -gt 0 && "$1" != --* ]]; then
|
|
260
|
+
ms_query="$1"
|
|
261
|
+
shift
|
|
262
|
+
fi
|
|
263
|
+
|
|
264
|
+
while [[ $# -gt 0 ]]; do
|
|
265
|
+
case "$1" in
|
|
266
|
+
--category) ms_category="${2:-}"; shift 2 ;;
|
|
267
|
+
--source) ms_source="${2:-}"; shift 2 ;;
|
|
268
|
+
--limit) ms_limit="${2:-10}"; shift 2 ;;
|
|
269
|
+
--include-acknowledged) ms_include_ack=true; shift ;;
|
|
270
|
+
*) shift ;;
|
|
271
|
+
esac
|
|
272
|
+
done
|
|
273
|
+
|
|
274
|
+
ms_midden_file="$COLONY_DATA_DIR/midden/midden.json"
|
|
275
|
+
|
|
276
|
+
if [[ ! -f "$ms_midden_file" ]]; then
|
|
277
|
+
json_ok "{\"query\":$(printf '%s' "$ms_query" | jq -Rs .),\"match_count\":0,\"entries\":[]}"
|
|
278
|
+
return 0
|
|
279
|
+
fi
|
|
280
|
+
|
|
281
|
+
ms_result=$(jq \
|
|
282
|
+
--arg query "$ms_query" \
|
|
283
|
+
--arg category "$ms_category" \
|
|
284
|
+
--arg source "$ms_source" \
|
|
285
|
+
--argjson limit "$ms_limit" \
|
|
286
|
+
--argjson include_ack "$ms_include_ack" \
|
|
287
|
+
'
|
|
288
|
+
[.entries // [] | .[] |
|
|
289
|
+
# Filter acknowledged unless --include-acknowledged
|
|
290
|
+
if $include_ack then . else select(.acknowledged != true) end |
|
|
291
|
+
# Filter by category if specified
|
|
292
|
+
if ($category | length) > 0 then select(.category == $category) else . end |
|
|
293
|
+
# Filter by source if specified
|
|
294
|
+
if ($source | length) > 0 then select(.source == $source) else . end |
|
|
295
|
+
# Filter by keyword match in message (case-insensitive)
|
|
296
|
+
if ($query | length) > 0 then
|
|
297
|
+
select(.message | ascii_downcase | contains($query | ascii_downcase))
|
|
298
|
+
else
|
|
299
|
+
.
|
|
300
|
+
end
|
|
301
|
+
] |
|
|
302
|
+
sort_by(.timestamp) | reverse |
|
|
303
|
+
. as $all |
|
|
304
|
+
{
|
|
305
|
+
query: $query,
|
|
306
|
+
match_count: ($all | length),
|
|
307
|
+
entries: ($all | .[:$limit])
|
|
308
|
+
}
|
|
309
|
+
' "$ms_midden_file" 2>/dev/null)
|
|
310
|
+
|
|
311
|
+
if [[ -z "$ms_result" ]]; then
|
|
312
|
+
json_ok "{\"query\":$(printf '%s' "$ms_query" | jq -Rs .),\"match_count\":0,\"entries\":[]}"
|
|
313
|
+
else
|
|
314
|
+
json_ok "$ms_result"
|
|
315
|
+
fi
|
|
316
|
+
return 0
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
_midden_tag() {
|
|
320
|
+
# Add or remove a tag from a midden entry's tags array
|
|
321
|
+
# Usage: midden-tag --id <entry_id> --tag <tag_name>
|
|
322
|
+
# OR: midden-tag --id <entry_id> --untag <tag_name>
|
|
323
|
+
# Returns: JSON with entry_id, tags array, and action
|
|
324
|
+
|
|
325
|
+
mt_id=""
|
|
326
|
+
mt_tag=""
|
|
327
|
+
mt_untag=""
|
|
328
|
+
|
|
329
|
+
while [[ $# -gt 0 ]]; do
|
|
330
|
+
case "$1" in
|
|
331
|
+
--id) mt_id="${2:-}"; shift 2 ;;
|
|
332
|
+
--tag) mt_tag="${2:-}"; shift 2 ;;
|
|
333
|
+
--untag) mt_untag="${2:-}"; shift 2 ;;
|
|
334
|
+
*) shift ;;
|
|
335
|
+
esac
|
|
336
|
+
done
|
|
337
|
+
|
|
338
|
+
# Validate: need --id
|
|
339
|
+
if [[ -z "$mt_id" ]]; then
|
|
340
|
+
json_err "$E_VALIDATION_FAILED" "midden-tag requires --id"
|
|
341
|
+
fi
|
|
342
|
+
|
|
343
|
+
# Validate: need --tag or --untag (but not both)
|
|
344
|
+
if [[ -z "$mt_tag" && -z "$mt_untag" ]]; then
|
|
345
|
+
json_err "$E_VALIDATION_FAILED" "midden-tag requires --tag or --untag"
|
|
346
|
+
fi
|
|
347
|
+
|
|
348
|
+
if [[ -n "$mt_tag" && -n "$mt_untag" ]]; then
|
|
349
|
+
json_err "$E_VALIDATION_FAILED" "midden-tag requires --tag or --untag, not both"
|
|
350
|
+
fi
|
|
351
|
+
|
|
352
|
+
mt_midden_file="$COLONY_DATA_DIR/midden/midden.json"
|
|
353
|
+
|
|
354
|
+
if [[ ! -f "$mt_midden_file" ]]; then
|
|
355
|
+
json_err "$E_FILE_NOT_FOUND" "midden.json not found"
|
|
356
|
+
fi
|
|
357
|
+
|
|
358
|
+
# Check entry exists
|
|
359
|
+
mt_exists=$(jq --arg id "$mt_id" '[.entries[]? | select(.id == $id)] | length > 0' "$mt_midden_file" 2>/dev/null || echo "false")
|
|
360
|
+
if [[ "$mt_exists" != "true" ]]; then
|
|
361
|
+
json_err "$E_RESOURCE_NOT_FOUND" "Midden entry '$mt_id' not found"
|
|
362
|
+
fi
|
|
363
|
+
|
|
364
|
+
# Acquire lock with trap-based cleanup
|
|
365
|
+
acquire_lock "$mt_midden_file" || {
|
|
366
|
+
json_err "$E_LOCK_FAILED" "Failed to acquire lock on midden.json"
|
|
367
|
+
}
|
|
368
|
+
trap 'release_lock 2>/dev/null || true' EXIT
|
|
369
|
+
|
|
370
|
+
if [[ -n "$mt_tag" ]]; then
|
|
371
|
+
# Add tag — create tags array if absent, append if tag not already present
|
|
372
|
+
mt_updated=$(jq \
|
|
373
|
+
--arg id "$mt_id" \
|
|
374
|
+
--arg tag "$mt_tag" \
|
|
375
|
+
'
|
|
376
|
+
.entries = [.entries[] |
|
|
377
|
+
if .id == $id then
|
|
378
|
+
. + {tags: ((.tags // []) | if contains([$tag]) then . else . + [$tag] end)}
|
|
379
|
+
else
|
|
380
|
+
.
|
|
381
|
+
end
|
|
382
|
+
]
|
|
383
|
+
' "$mt_midden_file" 2>/dev/null)
|
|
384
|
+
mt_action="added"
|
|
385
|
+
else
|
|
386
|
+
# Remove tag — remove from tags array if present
|
|
387
|
+
mt_updated=$(jq \
|
|
388
|
+
--arg id "$mt_id" \
|
|
389
|
+
--arg tag "$mt_untag" \
|
|
390
|
+
'
|
|
391
|
+
.entries = [.entries[] |
|
|
392
|
+
if .id == $id then
|
|
393
|
+
. + {tags: ((.tags // []) | map(select(. != $tag)))}
|
|
394
|
+
else
|
|
395
|
+
.
|
|
396
|
+
end
|
|
397
|
+
]
|
|
398
|
+
' "$mt_midden_file" 2>/dev/null)
|
|
399
|
+
mt_action="removed"
|
|
400
|
+
mt_tag="$mt_untag"
|
|
401
|
+
fi
|
|
402
|
+
|
|
403
|
+
if [[ -z "$mt_updated" ]]; then
|
|
404
|
+
trap - EXIT
|
|
405
|
+
release_lock 2>/dev/null || true
|
|
406
|
+
json_err "$E_INTERNAL" "Failed to update midden.json"
|
|
407
|
+
fi
|
|
408
|
+
|
|
409
|
+
atomic_write "$mt_midden_file" "$mt_updated"
|
|
410
|
+
|
|
411
|
+
trap - EXIT
|
|
412
|
+
release_lock 2>/dev/null || true
|
|
413
|
+
|
|
414
|
+
# Read back the updated tags for the entry
|
|
415
|
+
mt_tags=$(jq --arg id "$mt_id" '[.entries[]? | select(.id == $id) | .tags // []] | .[0] // []' "$mt_midden_file" 2>/dev/null || echo "[]")
|
|
416
|
+
|
|
417
|
+
json_ok "$(jq -n \
|
|
418
|
+
--arg entry_id "$mt_id" \
|
|
419
|
+
--argjson tags "$mt_tags" \
|
|
420
|
+
--arg action "$mt_action" \
|
|
421
|
+
'{entry_id: $entry_id, tags: $tags, action: $action}')"
|
|
422
|
+
return 0
|
|
423
|
+
}
|
|
424
|
+
|
|
247
425
|
_midden_acknowledge() {
|
|
248
426
|
# Acknowledge midden entries by id or by category
|
|
249
427
|
# Usage: midden-acknowledge --id <entry_id> [--reason <reason>]
|
|
@@ -340,3 +518,537 @@ _midden_acknowledge() {
|
|
|
340
518
|
'{acknowledged: true, count: $count, reason: $reason}')"
|
|
341
519
|
return 0
|
|
342
520
|
}
|
|
521
|
+
|
|
522
|
+
# ============================================================================
|
|
523
|
+
# Cross-Branch Midden Collection (Phase 41)
|
|
524
|
+
# ============================================================================
|
|
525
|
+
|
|
526
|
+
_midden_collect() {
|
|
527
|
+
# Collect midden entries from a merged branch worktree into main's midden
|
|
528
|
+
# Usage: midden-collect --branch <name> --merge-sha <sha> [--dry-run]
|
|
529
|
+
# Returns: JSON with collection status and counts
|
|
530
|
+
#
|
|
531
|
+
# Dual-layer idempotency:
|
|
532
|
+
# Layer 1: Merge fingerprint in collected-merges.json (fast path)
|
|
533
|
+
# Layer 2: Per-entry ID dedup (safety net)
|
|
534
|
+
|
|
535
|
+
mc_branch=""
|
|
536
|
+
mc_merge_sha=""
|
|
537
|
+
mc_dry_run=false
|
|
538
|
+
|
|
539
|
+
while [[ $# -gt 0 ]]; do
|
|
540
|
+
case "$1" in
|
|
541
|
+
--branch) mc_branch="${2:-}"; shift 2 ;;
|
|
542
|
+
--merge-sha) mc_merge_sha="${2:-}"; shift 2 ;;
|
|
543
|
+
--dry-run) mc_dry_run=true; shift ;;
|
|
544
|
+
*) shift ;;
|
|
545
|
+
esac
|
|
546
|
+
done
|
|
547
|
+
|
|
548
|
+
# Validate required args
|
|
549
|
+
if [[ -z "$mc_branch" ]]; then
|
|
550
|
+
json_err "$E_VALIDATION_FAILED" "midden-collect requires --branch"
|
|
551
|
+
fi
|
|
552
|
+
if [[ -z "$mc_merge_sha" ]]; then
|
|
553
|
+
json_err "$E_VALIDATION_FAILED" "midden-collect requires --merge-sha"
|
|
554
|
+
fi
|
|
555
|
+
|
|
556
|
+
# Resolve worktree midden path
|
|
557
|
+
mc_worktree_midden=""
|
|
558
|
+
mc_candidate="$AETHER_ROOT/.aether/worktrees/$mc_branch/.aether/data/midden/midden.json"
|
|
559
|
+
|
|
560
|
+
if [[ -f "$mc_candidate" ]]; then
|
|
561
|
+
mc_worktree_midden="$mc_candidate"
|
|
562
|
+
else
|
|
563
|
+
# Fallback: check git worktree list
|
|
564
|
+
mc_wt_path=$(git -C "$AETHER_ROOT" worktree list --porcelain 2>/dev/null | grep -F "worktree" | head -1 | cut -d' ' -f2 || true)
|
|
565
|
+
if [[ -n "$mc_wt_path" && -f "$mc_wt_path/.aether/data/midden/midden.json" ]]; then
|
|
566
|
+
mc_worktree_midden="$mc_wt_path/.aether/data/midden/midden.json"
|
|
567
|
+
fi
|
|
568
|
+
fi
|
|
569
|
+
|
|
570
|
+
if [[ -z "$mc_worktree_midden" || ! -f "$mc_worktree_midden" ]]; then
|
|
571
|
+
json_ok "$(jq -n --arg branch "$mc_branch" \
|
|
572
|
+
'{status: "worktree_not_found", entries_collected: 0, branch: $branch}')"
|
|
573
|
+
return 0
|
|
574
|
+
fi
|
|
575
|
+
|
|
576
|
+
# Read branch midden.json
|
|
577
|
+
mc_branch_data=$(cat "$mc_worktree_midden" 2>/dev/null || echo "")
|
|
578
|
+
|
|
579
|
+
if [[ -z "$mc_branch_data" ]]; then
|
|
580
|
+
json_ok '{"status":"empty_branch_midden","entries_collected":0}'
|
|
581
|
+
return 0
|
|
582
|
+
fi
|
|
583
|
+
|
|
584
|
+
# Validate branch midden.json is valid JSON
|
|
585
|
+
if ! echo "$mc_branch_data" | jq empty 2>/dev/null; then
|
|
586
|
+
json_err "$E_INTERNAL" "Branch midden.json is corrupt"
|
|
587
|
+
fi
|
|
588
|
+
|
|
589
|
+
# Check for entries
|
|
590
|
+
mc_branch_count=$(echo "$mc_branch_data" | jq '[.entries[]?] | length' 2>/dev/null || echo "0")
|
|
591
|
+
if [[ "$mc_branch_count" -eq 0 ]]; then
|
|
592
|
+
json_ok '{"status":"empty_branch_midden","entries_collected":0}'
|
|
593
|
+
return 0
|
|
594
|
+
fi
|
|
595
|
+
|
|
596
|
+
mc_midden_dir="$COLONY_DATA_DIR/midden"
|
|
597
|
+
mc_midden_file="$mc_midden_dir/midden.json"
|
|
598
|
+
mc_merges_file="$mc_midden_dir/collected-merges.json"
|
|
599
|
+
mc_timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
600
|
+
|
|
601
|
+
mkdir -p "$mc_midden_dir"
|
|
602
|
+
|
|
603
|
+
# Initialize midden.json if missing
|
|
604
|
+
if [[ ! -f "$mc_midden_file" ]]; then
|
|
605
|
+
printf '%s\n' '{"version":"1.0.0","entries":[]}' > "$mc_midden_file"
|
|
606
|
+
fi
|
|
607
|
+
|
|
608
|
+
# Initialize collected-merges.json if missing
|
|
609
|
+
if [[ ! -f "$mc_merges_file" ]]; then
|
|
610
|
+
printf '%s\n' '{"version":"1.0.0","merges":[]}' > "$mc_merges_file"
|
|
611
|
+
fi
|
|
612
|
+
|
|
613
|
+
# LAYER 1: Check merge fingerprint
|
|
614
|
+
mc_already=$(jq --arg sha "$mc_merge_sha" --arg branch "$mc_branch" \
|
|
615
|
+
'[.merges[]? | select(.merge_commit == $sha and .branch_name == $branch)] | length > 0' \
|
|
616
|
+
"$mc_merges_file" 2>/dev/null || echo "false")
|
|
617
|
+
|
|
618
|
+
if [[ "$mc_already" == "true" ]]; then
|
|
619
|
+
json_ok "$(jq -n --arg sha "$mc_merge_sha" --arg branch "$mc_branch" \
|
|
620
|
+
'{status: "already_collected", merge_commit: $sha, branch: $branch, entries_collected: 0}')"
|
|
621
|
+
return 0
|
|
622
|
+
fi
|
|
623
|
+
|
|
624
|
+
if [[ "$mc_dry_run" == "true" ]]; then
|
|
625
|
+
json_ok "$(jq -n --arg branch "$mc_branch" --arg sha "$mc_merge_sha" --argjson count "$mc_branch_count" \
|
|
626
|
+
'{status: "dry_run", branch: $branch, merge_commit: $sha, entries_would_collect: $count}')"
|
|
627
|
+
return 0
|
|
628
|
+
fi
|
|
629
|
+
|
|
630
|
+
# LAYER 2: Per-entry ID dedup — get existing IDs from main's midden
|
|
631
|
+
mc_existing_ids=$(jq -r '[.entries[]?.id] | map(select(. != null))' "$mc_midden_file" 2>/dev/null || echo "[]")
|
|
632
|
+
|
|
633
|
+
# Filter branch entries: exclude those with IDs already in main, then enrich
|
|
634
|
+
mc_new_entries=$(jq --argjson existing_ids "$mc_existing_ids" \
|
|
635
|
+
--arg branch "$mc_branch" \
|
|
636
|
+
--arg ts "$mc_timestamp" \
|
|
637
|
+
--arg sha "$mc_merge_sha" \
|
|
638
|
+
'
|
|
639
|
+
[.entries[]?] |
|
|
640
|
+
map(select([.id] | inside($existing_ids) | not)) |
|
|
641
|
+
map(. + {
|
|
642
|
+
collected_from: $branch,
|
|
643
|
+
collected_at: $ts,
|
|
644
|
+
merge_commit: $sha,
|
|
645
|
+
original_entry_id: .id
|
|
646
|
+
})
|
|
647
|
+
' "$mc_worktree_midden" 2>/dev/null || echo "[]")
|
|
648
|
+
|
|
649
|
+
mc_new_count=$(echo "$mc_new_entries" | jq 'length' 2>/dev/null || echo "0")
|
|
650
|
+
mc_skipped=$((mc_branch_count - mc_new_count))
|
|
651
|
+
|
|
652
|
+
if [[ "$mc_new_count" -gt 0 ]]; then
|
|
653
|
+
# Append enriched entries to main's midden
|
|
654
|
+
acquire_lock "$mc_midden_file" || {
|
|
655
|
+
json_err "$E_LOCK_FAILED" "Failed to acquire lock on midden.json"
|
|
656
|
+
}
|
|
657
|
+
trap 'release_lock 2>/dev/null || true' EXIT
|
|
658
|
+
|
|
659
|
+
mc_updated=$(jq --argjson new_entries "$mc_new_entries" \
|
|
660
|
+
'.entries += $new_entries' "$mc_midden_file" 2>/dev/null)
|
|
661
|
+
|
|
662
|
+
if [[ -z "$mc_updated" ]]; then
|
|
663
|
+
trap - EXIT
|
|
664
|
+
release_lock 2>/dev/null || true
|
|
665
|
+
json_err "$E_INTERNAL" "Failed to update midden.json with collected entries"
|
|
666
|
+
fi
|
|
667
|
+
|
|
668
|
+
atomic_write "$mc_midden_file" "$mc_updated"
|
|
669
|
+
|
|
670
|
+
trap - EXIT
|
|
671
|
+
release_lock 2>/dev/null || true
|
|
672
|
+
fi
|
|
673
|
+
|
|
674
|
+
# Write fingerprint to collected-merges.json
|
|
675
|
+
mc_fingerprint=$(printf '%s|%s|%d' "$mc_branch" "$mc_merge_sha" "$mc_new_count" | shasum -a 256 | cut -d' ' -f1)
|
|
676
|
+
|
|
677
|
+
acquire_lock "$mc_merges_file" || {
|
|
678
|
+
# Non-fatal — entries were collected but fingerprint may be missing
|
|
679
|
+
json_ok "$(jq -n --arg branch "$mc_branch" --arg sha "$mc_merge_sha" \
|
|
680
|
+
--argjson collected "$mc_new_count" --argjson skipped "$mc_skipped" \
|
|
681
|
+
'{status: "collected", entries_collected: $collected, entries_skipped_dup: $skipped, branch: $branch, merge_commit: $sha, warning: "fingerprint_write_failed"}')"
|
|
682
|
+
return 0
|
|
683
|
+
}
|
|
684
|
+
trap 'release_lock 2>/dev/null || true' EXIT
|
|
685
|
+
|
|
686
|
+
mc_merges_updated=$(jq --arg sha "$mc_merge_sha" --arg branch "$mc_branch" \
|
|
687
|
+
--arg ts "$mc_timestamp" --argjson collected "$mc_new_count" \
|
|
688
|
+
--argjson skipped "$mc_skipped" --arg fp "$mc_fingerprint" \
|
|
689
|
+
'.merges += [{
|
|
690
|
+
merge_commit: $sha,
|
|
691
|
+
branch_name: $branch,
|
|
692
|
+
collected_at: $ts,
|
|
693
|
+
entries_collected: $collected,
|
|
694
|
+
entries_skipped_dup: $skipped,
|
|
695
|
+
fingerprint: $fp
|
|
696
|
+
}]' "$mc_merges_file" 2>/dev/null)
|
|
697
|
+
|
|
698
|
+
if [[ -n "$mc_merges_updated" ]]; then
|
|
699
|
+
atomic_write "$mc_merges_file" "$mc_merges_updated"
|
|
700
|
+
fi
|
|
701
|
+
|
|
702
|
+
trap - EXIT
|
|
703
|
+
release_lock 2>/dev/null || true
|
|
704
|
+
|
|
705
|
+
json_ok "$(jq -n --arg branch "$mc_branch" --arg sha "$mc_merge_sha" \
|
|
706
|
+
--argjson collected "$mc_new_count" --argjson skipped "$mc_skipped" \
|
|
707
|
+
'{status: "collected", entries_collected: $collected, entries_skipped_dup: $skipped, branch: $branch, merge_commit: $sha}')"
|
|
708
|
+
return 0
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
_midden_handle_revert() {
|
|
712
|
+
# Tag entries from a reverted merge commit (not delete)
|
|
713
|
+
# Usage: midden-handle-revert --sha <revert-sha>
|
|
714
|
+
# OR: midden-handle-revert --revert-commit <sha> --original-merge <sha>
|
|
715
|
+
# Returns: JSON with revert status and tagged count
|
|
716
|
+
|
|
717
|
+
mhr_revert_sha=""
|
|
718
|
+
mhr_original_merge=""
|
|
719
|
+
|
|
720
|
+
while [[ $# -gt 0 ]]; do
|
|
721
|
+
case "$1" in
|
|
722
|
+
--sha) mhr_revert_sha="${2:-}"; shift 2 ;;
|
|
723
|
+
--revert-commit) mhr_revert_sha="${2:-}"; shift 2 ;;
|
|
724
|
+
--original-merge) mhr_original_merge="${2:-}"; shift 2 ;;
|
|
725
|
+
*) shift ;;
|
|
726
|
+
esac
|
|
727
|
+
done
|
|
728
|
+
|
|
729
|
+
if [[ -z "$mhr_revert_sha" ]]; then
|
|
730
|
+
json_err "$E_VALIDATION_FAILED" "midden-handle-revert requires --sha or --revert-commit"
|
|
731
|
+
fi
|
|
732
|
+
|
|
733
|
+
mc_midden_dir="$COLONY_DATA_DIR/midden"
|
|
734
|
+
mc_merges_file="$mc_midden_dir/collected-merges.json"
|
|
735
|
+
|
|
736
|
+
# If no original-merge given, try to find it from collected-merges by parsing commit message
|
|
737
|
+
if [[ -z "$mhr_original_merge" ]]; then
|
|
738
|
+
# Try git log to find the reverted merge
|
|
739
|
+
mhr_original_merge=$(git -C "$AETHER_ROOT" log -1 --format="%b" "$mhr_revert_sha" 2>/dev/null \
|
|
740
|
+
| grep -oE '[0-9a-f]{7,40}' | head -1 || true)
|
|
741
|
+
fi
|
|
742
|
+
|
|
743
|
+
if [[ -z "$mhr_original_merge" ]]; then
|
|
744
|
+
json_ok "$(jq -n --arg sha "$mhr_revert_sha" \
|
|
745
|
+
'{status: "original_merge_not_resolved", revert_commit: $sha, entries_tagged: 0}')"
|
|
746
|
+
return 0
|
|
747
|
+
fi
|
|
748
|
+
|
|
749
|
+
# Check collected-merges.json exists
|
|
750
|
+
if [[ ! -f "$mc_merges_file" ]]; then
|
|
751
|
+
json_ok "$(jq -n --arg sha "$mhr_revert_sha" --arg merge "$mhr_original_merge" \
|
|
752
|
+
'{status: "merge_not_found", revert_commit: $sha, original_merge: $merge, entries_tagged: 0}')"
|
|
753
|
+
return 0
|
|
754
|
+
fi
|
|
755
|
+
|
|
756
|
+
# Check if the original merge exists in collected-merges
|
|
757
|
+
mhr_found=$(jq --arg merge "$mhr_original_merge" \
|
|
758
|
+
'[.merges[]? | select(.merge_commit == $merge)] | length > 0' \
|
|
759
|
+
"$mc_merges_file" 2>/dev/null || echo "false")
|
|
760
|
+
|
|
761
|
+
if [[ "$mhr_found" != "true" ]]; then
|
|
762
|
+
json_ok "$(jq -n --arg sha "$mhr_revert_sha" --arg merge "$mhr_original_merge" \
|
|
763
|
+
'{status: "merge_not_found", revert_commit: $sha, original_merge: $merge, entries_tagged: 0}')"
|
|
764
|
+
return 0
|
|
765
|
+
fi
|
|
766
|
+
|
|
767
|
+
mhr_midden_file="$mc_midden_dir/midden.json"
|
|
768
|
+
mhr_timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
769
|
+
mhr_tagged=0
|
|
770
|
+
|
|
771
|
+
# Tag entries in main's midden.json
|
|
772
|
+
if [[ -f "$mhr_midden_file" ]]; then
|
|
773
|
+
acquire_lock "$mhr_midden_file" || {
|
|
774
|
+
json_err "$E_LOCK_FAILED" "Failed to acquire lock on midden.json"
|
|
775
|
+
}
|
|
776
|
+
trap 'release_lock 2>/dev/null || true' EXIT
|
|
777
|
+
|
|
778
|
+
mhr_updated=$(jq --arg revert_sha "$mhr_revert_sha" --arg merge_sha "$mhr_original_merge" \
|
|
779
|
+
'
|
|
780
|
+
.entries = [.entries[] |
|
|
781
|
+
if .merge_commit == $merge_sha then
|
|
782
|
+
. + {
|
|
783
|
+
tags: ((.tags // []) + ["reverted:" + $revert_sha]) | unique,
|
|
784
|
+
reviewed: false
|
|
785
|
+
}
|
|
786
|
+
else
|
|
787
|
+
.
|
|
788
|
+
end
|
|
789
|
+
]
|
|
790
|
+
' "$mhr_midden_file" 2>/dev/null)
|
|
791
|
+
|
|
792
|
+
if [[ -n "$mhr_updated" ]]; then
|
|
793
|
+
atomic_write "$mhr_midden_file" "$mhr_updated"
|
|
794
|
+
mhr_tagged=$(echo "$mhr_updated" | jq --arg merge_sha "$mhr_original_merge" \
|
|
795
|
+
'[.entries[] | select(.merge_commit == $merge_sha)] | length' 2>/dev/null || echo "0")
|
|
796
|
+
fi
|
|
797
|
+
|
|
798
|
+
trap - EXIT
|
|
799
|
+
release_lock 2>/dev/null || true
|
|
800
|
+
fi
|
|
801
|
+
|
|
802
|
+
# Update collected-merges.json: mark merge as reverted
|
|
803
|
+
acquire_lock "$mc_merges_file" || true
|
|
804
|
+
trap 'release_lock 2>/dev/null || true' EXIT
|
|
805
|
+
|
|
806
|
+
mhr_merges_updated=$(jq --arg revert_sha "$mhr_revert_sha" \
|
|
807
|
+
--arg merge_sha "$mhr_original_merge" --arg ts "$mhr_timestamp" \
|
|
808
|
+
'
|
|
809
|
+
.merges = [.merges[] |
|
|
810
|
+
if .merge_commit == $merge_sha then
|
|
811
|
+
. + {reverted_by: $revert_sha, reverted_at: $ts, status: "reverted"}
|
|
812
|
+
else
|
|
813
|
+
.
|
|
814
|
+
end
|
|
815
|
+
]
|
|
816
|
+
' "$mc_merges_file" 2>/dev/null)
|
|
817
|
+
|
|
818
|
+
if [[ -n "$mhr_merges_updated" ]]; then
|
|
819
|
+
atomic_write "$mc_merges_file" "$mhr_merges_updated"
|
|
820
|
+
fi
|
|
821
|
+
|
|
822
|
+
trap - EXIT
|
|
823
|
+
release_lock 2>/dev/null || true
|
|
824
|
+
|
|
825
|
+
json_ok "$(jq -n --arg sha "$mhr_revert_sha" --arg merge "$mhr_original_merge" \
|
|
826
|
+
--argjson tagged "$mhr_tagged" \
|
|
827
|
+
'{revert_commit: $sha, original_merge: $merge, entries_tagged: $tagged, entries_deleted: 0}')"
|
|
828
|
+
return 0
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
_midden_cross_pr_analysis() {
|
|
832
|
+
# Detect cross-PR failure patterns and auto-emit REDIRECT for systemic issues
|
|
833
|
+
# Usage: midden-cross-pr-analysis [--category <cat>] [--window <days>]
|
|
834
|
+
# Returns: JSON with category analysis, scores, classifications
|
|
835
|
+
|
|
836
|
+
mca_category=""
|
|
837
|
+
mca_window=14
|
|
838
|
+
|
|
839
|
+
while [[ $# -gt 0 ]]; do
|
|
840
|
+
case "$1" in
|
|
841
|
+
--category) mca_category="${2:-}"; shift 2 ;;
|
|
842
|
+
--window) mca_window="${2:-14}"; shift 2 ;;
|
|
843
|
+
*) shift ;;
|
|
844
|
+
esac
|
|
845
|
+
done
|
|
846
|
+
|
|
847
|
+
mca_midden_file="$COLONY_DATA_DIR/midden/midden.json"
|
|
848
|
+
|
|
849
|
+
if [[ ! -f "$mca_midden_file" ]]; then
|
|
850
|
+
json_ok "$(jq -n --argjson window "$mca_window" \
|
|
851
|
+
'{analysis_timestamp: "now", window_days: $window, total_entries_scanned: 0, categories: {}, systemic_categories: []}')"
|
|
852
|
+
return 0
|
|
853
|
+
fi
|
|
854
|
+
|
|
855
|
+
mca_timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
856
|
+
|
|
857
|
+
# Calculate cutoff timestamp: NOW - window_days
|
|
858
|
+
mca_cutoff=$(date -u -v-"${mca_window}d" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || \
|
|
859
|
+
python3 -c "import datetime; print((datetime.datetime.utcnow() - datetime.timedelta(days=$mca_window)).strftime('%Y-%m-%dT%H:%M:%SZ'))" 2>/dev/null || \
|
|
860
|
+
date -u -d "$mca_window days ago" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || echo "")
|
|
861
|
+
|
|
862
|
+
mca_result=$(jq --arg cutoff "$mca_cutoff" \
|
|
863
|
+
--arg category "$mca_category" --argjson window "$mca_window" \
|
|
864
|
+
'
|
|
865
|
+
# Collect cross-branch entries within window, excluding reverted
|
|
866
|
+
[.entries // [] | .[] |
|
|
867
|
+
select(.collected_from != null) |
|
|
868
|
+
select(.reviewed != true) |
|
|
869
|
+
select(.tags // [] | map(startswith("reverted:")) | any | not) |
|
|
870
|
+
select(if ($cutoff | length) > 0 then .timestamp >= $cutoff else true end) |
|
|
871
|
+
if ($category | length) > 0 then select(.category == $category) else . end
|
|
872
|
+
] |
|
|
873
|
+
. as $entries |
|
|
874
|
+
|
|
875
|
+
# Group by category
|
|
876
|
+
($entries | group_by(.category) | map({key: .[0].category, value: .}) | from_entries) as $by_cat |
|
|
877
|
+
|
|
878
|
+
# Compute metrics per category
|
|
879
|
+
($by_cat | to_entries | map({
|
|
880
|
+
key: .key,
|
|
881
|
+
value: {
|
|
882
|
+
total_entries: (.value | length),
|
|
883
|
+
unique_prs: ([.value[].collected_from] | unique | length),
|
|
884
|
+
entries_per_pr: (.value | group_by(.collected_from) | map({key: .[0].collected_from, value: length}) | from_entries),
|
|
885
|
+
cross_pr_score: (
|
|
886
|
+
(([.value[].collected_from] | unique | length) / 5) * 0.6 +
|
|
887
|
+
((.value | length) / 10) * 0.4
|
|
888
|
+
),
|
|
889
|
+
classification: (
|
|
890
|
+
if (([.value[].collected_from] | unique | length) >= 3) and ((.value | length) >= 5) then
|
|
891
|
+
"cross-pr-critical"
|
|
892
|
+
elif (([.value[].collected_from] | unique | length) >= 2) and ((.value | length) >= 3) then
|
|
893
|
+
"cross-pr-systemic"
|
|
894
|
+
else
|
|
895
|
+
"single-pr"
|
|
896
|
+
end
|
|
897
|
+
),
|
|
898
|
+
auto_redirect_emitted: false
|
|
899
|
+
}
|
|
900
|
+
}) | from_entries) as $analysis |
|
|
901
|
+
|
|
902
|
+
# Collect systemic categories
|
|
903
|
+
[$analysis | to_entries[] | select(.value.classification == "cross-pr-systemic" or .value.classification == "cross-pr-critical") | .key] as $systemic |
|
|
904
|
+
|
|
905
|
+
{
|
|
906
|
+
total_entries_scanned: ($entries | length),
|
|
907
|
+
categories: $analysis,
|
|
908
|
+
systemic_categories: $systemic
|
|
909
|
+
}
|
|
910
|
+
' "$mca_midden_file" 2>/dev/null)
|
|
911
|
+
|
|
912
|
+
if [[ -z "$mca_result" ]]; then
|
|
913
|
+
json_ok "$(jq -n --argjson window "$mca_window" \
|
|
914
|
+
'{analysis_timestamp: "now", window_days: $window, total_entries_scanned: 0, categories: {}, systemic_categories: []}')"
|
|
915
|
+
return 0
|
|
916
|
+
fi
|
|
917
|
+
|
|
918
|
+
# Auto-emit REDIRECT for systemic/critical categories
|
|
919
|
+
mca_systemic=$(echo "$mca_result" | jq -r '.systemic_categories // [] | .[]' 2>/dev/null || true)
|
|
920
|
+
for mca_cat in $mca_systemic; do
|
|
921
|
+
mca_cat_data=$(echo "$mca_result" | jq --arg cat "$mca_cat" '.categories[$cat]' 2>/dev/null || echo "{}")
|
|
922
|
+
mca_unique_prs=$(echo "$mca_cat_data" | jq -r '.unique_prs // 0' 2>/dev/null || echo "0")
|
|
923
|
+
mca_total=$(echo "$mca_cat_data" | jq -r '.total_entries // 0' 2>/dev/null || echo "0")
|
|
924
|
+
mca_score=$(echo "$mca_cat_data" | jq -r '.cross_pr_score // 0' 2>/dev/null || echo "0")
|
|
925
|
+
|
|
926
|
+
# Compute strength: 0.5 + (score * 0.5), capped at 1.0
|
|
927
|
+
mca_strength=$(jq -n --argjson score "$mca_score" '0.5 + ($score * 0.5) | if . > 1.0 then 1.0 else . * 100 | round / 100 end' 2>/dev/null || echo "0.7")
|
|
928
|
+
|
|
929
|
+
# NON-BLOCKING: emit REDIRECT, swallow all output
|
|
930
|
+
bash "$AETHER_ROOT/.aether/aether-utils.sh" pheromone-write REDIRECT \
|
|
931
|
+
"[cross-pr-pattern] $mca_cat failures across $mca_unique_prs PRs in $mca_window days ($mca_total entries)" \
|
|
932
|
+
--strength "$mca_strength" \
|
|
933
|
+
--source "auto:cross-pr" \
|
|
934
|
+
--reason "Auto-emitted: cross-PR systemic failure pattern detected" \
|
|
935
|
+
--ttl "30d" >/dev/null 2>&1 || true
|
|
936
|
+
|
|
937
|
+
# Mark as emitted in the result
|
|
938
|
+
mca_result=$(echo "$mca_result" | jq --arg cat "$mca_cat" '.categories[$cat].auto_redirect_emitted = true' 2>/dev/null || echo "$mca_result")
|
|
939
|
+
done
|
|
940
|
+
|
|
941
|
+
json_ok "$(echo "$mca_result" | jq --arg ts "$mca_timestamp" --argjson window "$mca_window" \
|
|
942
|
+
'. + {analysis_timestamp: $ts, window_days: $window}' 2>/dev/null || echo "$mca_result")"
|
|
943
|
+
return 0
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
_midden_prune() {
|
|
947
|
+
# Retention cleanup for collected merges and reverted entries
|
|
948
|
+
# Usage: midden-prune --stale-merges
|
|
949
|
+
# OR: midden-prune --reverted --age <days>
|
|
950
|
+
# Returns: JSON with prune counts
|
|
951
|
+
|
|
952
|
+
mp_stale_merges=false
|
|
953
|
+
mp_reverted=false
|
|
954
|
+
mp_age=30
|
|
955
|
+
|
|
956
|
+
while [[ $# -gt 0 ]]; do
|
|
957
|
+
case "$1" in
|
|
958
|
+
--stale-merges) mp_stale_merges=true; shift ;;
|
|
959
|
+
--reverted) mp_reverted=true; shift ;;
|
|
960
|
+
--age) mp_age="${2:-30}"; shift 2 ;;
|
|
961
|
+
*) shift ;;
|
|
962
|
+
esac
|
|
963
|
+
done
|
|
964
|
+
|
|
965
|
+
mp_midden_dir="$COLONY_DATA_DIR/midden"
|
|
966
|
+
mp_merges_file="$mp_midden_dir/collected-merges.json"
|
|
967
|
+
mp_midden_file="$mp_midden_dir/midden.json"
|
|
968
|
+
mp_pruned_merges=0
|
|
969
|
+
mp_pruned_reverted=0
|
|
970
|
+
|
|
971
|
+
if [[ "$mp_stale_merges" == "true" ]]; then
|
|
972
|
+
if [[ -f "$mp_merges_file" ]]; then
|
|
973
|
+
mp_cutoff=$(date -u -v-"90d" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || \
|
|
974
|
+
python3 -c "import datetime; print((datetime.datetime.utcnow() - datetime.timedelta(days=90)).strftime('%Y-%m-%dT%H:%M:%SZ'))" 2>/dev/null || \
|
|
975
|
+
date -u -d "90 days ago" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || echo "")
|
|
976
|
+
|
|
977
|
+
if [[ -n "$mp_cutoff" ]]; then
|
|
978
|
+
acquire_lock "$mp_merges_file" || true
|
|
979
|
+
trap 'release_lock 2>/dev/null || true' EXIT
|
|
980
|
+
|
|
981
|
+
mp_before=$(jq '.merges | length' "$mp_merges_file" 2>/dev/null || echo "0")
|
|
982
|
+
mp_before=${mp_before:-0}
|
|
983
|
+
mp_merges_updated=$(jq --arg cutoff "$mp_cutoff" \
|
|
984
|
+
'.merges = [.merges // [] | .[] | select(.collected_at >= $cutoff)]' \
|
|
985
|
+
"$mp_merges_file" 2>/dev/null)
|
|
986
|
+
mp_after=$(echo "$mp_merges_updated" | jq '.merges | length' 2>/dev/null || echo "0")
|
|
987
|
+
mp_after=${mp_after:-0}
|
|
988
|
+
|
|
989
|
+
if [[ -n "$mp_merges_updated" ]]; then
|
|
990
|
+
atomic_write "$mp_merges_file" "$mp_merges_updated"
|
|
991
|
+
mp_pruned_merges=$((mp_before - mp_after))
|
|
992
|
+
fi
|
|
993
|
+
|
|
994
|
+
trap - EXIT
|
|
995
|
+
release_lock 2>/dev/null || true
|
|
996
|
+
fi
|
|
997
|
+
fi
|
|
998
|
+
fi
|
|
999
|
+
|
|
1000
|
+
if [[ "$mp_reverted" == "true" ]]; then
|
|
1001
|
+
if [[ -f "$mp_midden_file" && -f "$mp_merges_file" ]]; then
|
|
1002
|
+
mp_cutoff=$(date -u -v-"${mp_age}d" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || \
|
|
1003
|
+
python3 -c "import datetime; print((datetime.datetime.utcnow() - datetime.timedelta(days=$mp_age)).strftime('%Y-%m-%dT%H:%M:%SZ'))" 2>/dev/null || \
|
|
1004
|
+
date -u -d "$mp_age days ago" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || echo "")
|
|
1005
|
+
|
|
1006
|
+
if [[ -n "$mp_cutoff" ]]; then
|
|
1007
|
+
# Find revert timestamps from collected-merges.json
|
|
1008
|
+
mp_revert_map=$(jq -r '[.merges // [] | .[] | select(.status == "reverted")] | map({merge_commit: .merge_commit, reverted_at: .reverted_at})' "$mp_merges_file" 2>/dev/null || echo "[]")
|
|
1009
|
+
|
|
1010
|
+
acquire_lock "$mp_midden_file" || true
|
|
1011
|
+
trap 'release_lock 2>/dev/null || true' EXIT
|
|
1012
|
+
|
|
1013
|
+
mp_now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
1014
|
+
|
|
1015
|
+
# Acknowledge reverted entries older than threshold
|
|
1016
|
+
mp_updated=$(jq --argjson revert_map "$mp_revert_map" \
|
|
1017
|
+
--arg cutoff "$mp_cutoff" --arg now "$mp_now" --argjson age "$mp_age" \
|
|
1018
|
+
'
|
|
1019
|
+
.entries = [.entries // [] | .[] |
|
|
1020
|
+
if .tags // [] | map(startswith("reverted:")) | any then
|
|
1021
|
+
# Find the revert timestamp for this entry
|
|
1022
|
+
($revert_map | map(select(.merge_commit == .merge_commit)) | first // {}) as $merge_info |
|
|
1023
|
+
if ($merge_info.reverted_at // "") != "" and ($merge_info.reverted_at < $cutoff) and .acknowledged != true then
|
|
1024
|
+
. + {
|
|
1025
|
+
acknowledged: true,
|
|
1026
|
+
acknowledged_at: $now,
|
|
1027
|
+
acknowledge_reason: ("auto-pruned: reverted entry older than " + ($age | tostring) + " days")
|
|
1028
|
+
}
|
|
1029
|
+
else .
|
|
1030
|
+
end
|
|
1031
|
+
else .
|
|
1032
|
+
end
|
|
1033
|
+
]
|
|
1034
|
+
' "$mp_midden_file" 2>/dev/null)
|
|
1035
|
+
|
|
1036
|
+
if [[ -n "$mp_updated" ]]; then
|
|
1037
|
+
mp_before=$(jq '[.entries // [] | .[] | select(.tags // [] | map(startswith("reverted:")) | any and .acknowledged != true)] | length' "$mp_midden_file" 2>/dev/null || echo "0")
|
|
1038
|
+
mp_before=${mp_before:-0}
|
|
1039
|
+
mp_after=$(jq '[.entries // [] | .[] | select(.tags // [] | map(startswith("reverted:")) | any and .acknowledged != true)] | length' <<< "$mp_updated" 2>/dev/null || echo "0")
|
|
1040
|
+
mp_after=${mp_after:-0}
|
|
1041
|
+
atomic_write "$mp_midden_file" "$mp_updated"
|
|
1042
|
+
mp_pruned_reverted=$((mp_before - mp_after))
|
|
1043
|
+
fi
|
|
1044
|
+
|
|
1045
|
+
trap - EXIT
|
|
1046
|
+
release_lock 2>/dev/null || true
|
|
1047
|
+
fi
|
|
1048
|
+
fi
|
|
1049
|
+
fi
|
|
1050
|
+
|
|
1051
|
+
json_ok "$(jq -n --argjson pruned_merges "$mp_pruned_merges" --argjson pruned_reverted "$mp_pruned_reverted" \
|
|
1052
|
+
'{pruned_merges: $pruned_merges, pruned_reverted: $pruned_reverted}')"
|
|
1053
|
+
return 0
|
|
1054
|
+
}
|