aether-colony 5.2.1 → 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 +35 -0
- 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/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/docs/command-playbooks/build-full.md +1 -1
- package/.aether/docs/command-playbooks/build-prep.md +10 -3
- 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-verify.md +32 -0
- package/.aether/utils/clash-detect.sh +239 -0
- package/.aether/utils/hooks/clash-pre-tool-use.js +99 -0
- package/.aether/utils/merge-driver-lockfile.sh +35 -0
- package/.aether/utils/midden.sh +534 -0
- package/.aether/utils/pheromone.sh +1376 -108
- package/.aether/utils/queen.sh +2 -4
- package/.aether/utils/state-api.sh +25 -4
- package/.aether/utils/swarm.sh +1 -1
- package/.aether/utils/worktree.sh +189 -0
- package/.claude/commands/ant/init.md +9 -3
- package/.opencode/commands/ant/init.md +9 -2
- package/CHANGELOG.md +26 -0
- package/README.md +11 -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 +2 -2
- 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
|
@@ -518,3 +518,537 @@ _midden_acknowledge() {
|
|
|
518
518
|
'{acknowledged: true, count: $count, reason: $reason}')"
|
|
519
519
|
return 0
|
|
520
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
|
+
}
|