@mmerterden/multi-agent-pipeline 17.6.0 → 19.0.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/CHANGELOG.md +310 -0
- package/README.md +76 -18
- package/README.tr.md +55 -16
- package/docs/adr/0002-instruction-driven-flag.md +1 -0
- package/docs/adr/0005-lazy-phase-docs.md +11 -1
- package/docs/adr/0008-installer-modularization-and-secret-leak-defense.md +1 -0
- package/docs/adr/0010-own-code-graph.md +1 -0
- package/docs/adr/0011-dormant-ci.md +25 -1
- package/docs/adr/0014-six-phase-consolidation.md +134 -0
- package/docs/adr/README.md +2 -1
- package/docs/architecture.md +37 -38
- package/docs/best-practices.md +1 -1
- package/docs/ecosystem.md +37 -26
- package/docs/engineering.md +1 -1
- package/docs/facts.json +45 -0
- package/docs/features.md +54 -53
- package/docs/performance.md +5 -5
- package/docs/recovery-guide.md +9 -9
- package/docs/server-readiness.md +188 -0
- package/docs/token-budget-history.md +3 -1
- package/index.js +18 -3
- package/install/_codex-agents.mjs +1 -1
- package/install/_common.mjs +42 -17
- package/install/_dev-only-files.mjs +8 -0
- package/install/_unattended-profile.mjs +113 -0
- package/install/index.mjs +48 -0
- package/install/templates/claude-hooks.json +1 -1
- package/install/templates/codex-instructions.md +1 -1
- package/install/templates/copilot-instructions.md +28 -28
- package/manifest.json +1065 -0
- package/package.json +6 -3
- package/pipeline/agents/dev-critic.md +3 -3
- package/pipeline/commands/figma-to-swiftui.md +1 -1
- package/pipeline/commands/multi-agent/SKILL.md +8 -8
- package/pipeline/commands/multi-agent/analysis/SKILL.md +9 -9
- package/pipeline/commands/multi-agent/autopilot/SKILL.md +7 -7
- package/pipeline/commands/multi-agent/channels/SKILL.md +15 -15
- package/pipeline/commands/multi-agent/diff-explain/SKILL.md +6 -6
- package/pipeline/commands/multi-agent/garbage-collect/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/graph/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/help/SKILL.md +62 -62
- package/pipeline/commands/multi-agent/language/SKILL.md +2 -2
- package/pipeline/commands/multi-agent/local/SKILL.md +11 -11
- package/pipeline/commands/multi-agent/local-autopilot/SKILL.md +13 -13
- package/pipeline/commands/multi-agent/log/SKILL.md +2 -2
- package/pipeline/commands/multi-agent/manual-test/SKILL.md +9 -9
- package/pipeline/commands/multi-agent/model/SKILL.md +69 -0
- package/pipeline/commands/multi-agent/refactor/SKILL.md +3 -3
- package/pipeline/commands/multi-agent/resume/SKILL.md +4 -4
- package/pipeline/commands/multi-agent/resume-local/SKILL.md +19 -17
- package/pipeline/commands/multi-agent/review/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/route-off/SKILL.md +36 -0
- package/pipeline/commands/multi-agent/route-on/SKILL.md +74 -0
- package/pipeline/commands/multi-agent/route-status/SKILL.md +56 -0
- package/pipeline/commands/multi-agent/setup/SKILL.md +2 -2
- package/pipeline/commands/multi-agent/status/SKILL.md +54 -23
- package/pipeline/commands/multi-agent/steer/SKILL.md +2 -2
- package/pipeline/commands/multi-agent/sync/SKILL.md +12 -13
- package/pipeline/commands/multi-agent/test/SKILL.md +1 -1
- package/pipeline/lib/_jira-auth.sh +8 -0
- package/pipeline/lib/analysis-jira-write.sh +32 -0
- package/pipeline/lib/ask-choice.sh +13 -2
- package/pipeline/lib/autopilot-state.sh +8 -0
- package/pipeline/lib/credential-inventory.sh +1 -1
- package/pipeline/lib/fatal.mjs +129 -0
- package/pipeline/lib/fetch-fortify.sh +1 -1
- package/pipeline/lib/figma-mcp-refresh.sh +18 -0
- package/pipeline/lib/figma-screenshot.sh +18 -0
- package/pipeline/lib/invoked-directly.mjs +43 -0
- package/pipeline/lib/jira-publish.sh +42 -0
- package/pipeline/lib/md2confluence-v3.py +47 -0
- package/pipeline/lib/model-rung.sh +142 -0
- package/pipeline/lib/outbound-gate.mjs +175 -0
- package/pipeline/lib/phase-schema.mjs +88 -0
- package/pipeline/lib/plan-todos.sh +32 -11
- package/pipeline/lib/post-pr-review.sh +77 -8
- package/pipeline/lib/repo-hygiene.sh +8 -3
- package/pipeline/lib/require-jq.sh +40 -0
- package/pipeline/lib/route-state.sh +161 -0
- package/pipeline/lib/run-paths.sh +335 -0
- package/pipeline/multi-agent-refs/_account-picker.md +1 -1
- package/pipeline/multi-agent-refs/_dev-context.md +1 -1
- package/pipeline/multi-agent-refs/_input-parser.md +1 -1
- package/pipeline/multi-agent-refs/analysis/evidence.md +0 -9
- package/pipeline/multi-agent-refs/analysis/intake.md +1 -1
- package/pipeline/multi-agent-refs/analysis/locked.md +21 -22
- package/pipeline/multi-agent-refs/analysis/render.md +1 -1
- package/pipeline/multi-agent-refs/analysis/synthesis.md +12 -6
- package/pipeline/multi-agent-refs/android-guide.md +1 -1
- package/pipeline/multi-agent-refs/audit-guide.md +13 -13
- package/pipeline/multi-agent-refs/channels/issue-comment.md +2 -2
- package/pipeline/multi-agent-refs/channels/jira.md +3 -3
- package/pipeline/multi-agent-refs/channels/pr.md +4 -4
- package/pipeline/multi-agent-refs/channels/wiki.md +1 -1
- package/pipeline/multi-agent-refs/component-dispatch.md +3 -3
- package/pipeline/multi-agent-refs/cross-cli-contract.md +31 -6
- package/pipeline/multi-agent-refs/features/autopilot-circuit-breaker.md +74 -4
- package/pipeline/multi-agent-refs/features/code-graph.md +5 -5
- package/pipeline/multi-agent-refs/features/cost-analysis.md +93 -0
- package/pipeline/multi-agent-refs/features/design-conformance.md +1 -1
- package/pipeline/multi-agent-refs/features/dev-critic.md +3 -3
- package/pipeline/multi-agent-refs/features/doctor.md +47 -2
- package/pipeline/multi-agent-refs/features/external-context-injection.md +3 -3
- package/pipeline/multi-agent-refs/features/maturity-followup.md +3 -3
- package/pipeline/multi-agent-refs/features/model-fallback.md +5 -5
- package/pipeline/multi-agent-refs/features/plan-todos.md +1 -1
- package/pipeline/multi-agent-refs/features/repo-map.md +1 -1
- package/pipeline/multi-agent-refs/features/review-delta.md +3 -3
- package/pipeline/multi-agent-refs/features/review-multi-repo.md +1 -1
- package/pipeline/multi-agent-refs/features/scope-check.md +4 -4
- package/pipeline/multi-agent-refs/features/skill-conformance.md +2 -2
- package/pipeline/multi-agent-refs/features/stack-skill-routing.md +1 -1
- package/pipeline/multi-agent-refs/features/verify-by-test.md +4 -4
- package/pipeline/multi-agent-refs/features/verify.md +83 -0
- package/pipeline/multi-agent-refs/features/visual-evidence.md +19 -19
- package/pipeline/multi-agent-refs/features/worktree-finalize.md +6 -6
- package/pipeline/multi-agent-refs/issue-jira-triad.md +10 -10
- package/pipeline/multi-agent-refs/knowledge.md +11 -11
- package/pipeline/multi-agent-refs/multi-repo-integration-build.md +13 -13
- package/pipeline/multi-agent-refs/payload-contracts.md +8 -8
- package/pipeline/multi-agent-refs/phases/log-format.md +10 -10
- package/pipeline/multi-agent-refs/phases/modes.md +30 -30
- package/pipeline/multi-agent-refs/phases/operations.md +21 -10
- package/pipeline/multi-agent-refs/phases/phase-0-init.md +25 -25
- package/pipeline/multi-agent-refs/phases/phase-1-plan.md +599 -0
- package/pipeline/multi-agent-refs/phases/{phase-3-dev.md → phase-2-dev.md} +129 -49
- package/pipeline/multi-agent-refs/phases/{phase-4-review.md → phase-3-review.md} +225 -107
- package/pipeline/multi-agent-refs/phases/{phase-6-commit.md → phase-4-commit.md} +23 -23
- package/pipeline/multi-agent-refs/phases/{phase-7-report.md → phase-5-report.md} +29 -29
- package/pipeline/multi-agent-refs/phases.md +44 -48
- package/pipeline/multi-agent-refs/picker-contract.md +1 -1
- package/pipeline/multi-agent-refs/progress-contract.md +6 -6
- package/pipeline/multi-agent-refs/readiness-review.md +1 -1
- package/pipeline/multi-agent-refs/rules.md +7 -7
- package/pipeline/multi-agent-refs/swiftui-guide.md +2 -2
- package/pipeline/multi-agent-refs/tracker-contract.md +31 -32
- package/pipeline/multi-agent-refs/unattended-contract.md +129 -0
- package/pipeline/multi-agent-refs/wiki-capture.md +14 -14
- package/pipeline/preferences-template.json +9 -1
- package/pipeline/rules/outside-the-pipeline.md +1 -1
- package/pipeline/schemas/agent-state.schema.json +50 -50
- package/pipeline/schemas/analysis-output.schema.json +2 -2
- package/pipeline/schemas/autopilot-config.schema.json +1 -1
- package/pipeline/schemas/code-graph.schema.json +1 -1
- package/pipeline/schemas/criteria-manifest.schema.json +1 -1
- package/pipeline/schemas/dev-critic-output.schema.json +1 -1
- package/pipeline/schemas/diff-risk.schema.json +1 -1
- package/pipeline/schemas/migrations/prefs-2.4.0-to-2.5.0.mjs +2 -2
- package/pipeline/schemas/migrations/prefs-2.6.0-to-2.7.0.mjs +31 -0
- package/pipeline/schemas/migrations/state-2.1.0-to-2.2.0.mjs +129 -0
- package/pipeline/schemas/phases.json +105 -0
- package/pipeline/schemas/plan-todos.schema.json +5 -5
- package/pipeline/schemas/planning-output.schema.json +1 -1
- package/pipeline/schemas/prefs.schema.json +100 -56
- package/pipeline/schemas/reviewer-output.schema.json +3 -3
- package/pipeline/schemas/route-config.schema.json +74 -0
- package/pipeline/schemas/scope-check.schema.json +1 -1
- package/pipeline/schemas/test-gap.schema.json +1 -1
- package/pipeline/schemas/token-budget.json +12 -18
- package/pipeline/schemas/triage-output.schema.json +6 -6
- package/pipeline/scripts/README.md +3 -3
- package/pipeline/scripts/_code-graph.mjs +2 -2
- package/pipeline/scripts/_run-paths.mjs +372 -0
- package/pipeline/scripts/_smoke-root.sh +1 -1
- package/pipeline/scripts/aggregate-metrics.mjs +65 -65
- package/pipeline/scripts/autopilot-arming.mjs +2 -1
- package/pipeline/scripts/autopilot-intake.mjs +2 -1
- package/pipeline/scripts/autopilot-runner.mjs +206 -2
- package/pipeline/scripts/build-references.mjs +2 -1
- package/pipeline/scripts/build-stack-plugins.mjs +10 -2
- package/pipeline/scripts/capture-evidence.sh +7 -2
- package/pipeline/scripts/capture-flush.sh +8 -8
- package/pipeline/scripts/capture-resume.sh +3 -3
- package/pipeline/scripts/classify-plan-safety.mjs +3 -2
- package/pipeline/scripts/cost-analyze.mjs +600 -0
- package/pipeline/scripts/cost-budget-check.mjs +4 -12
- package/pipeline/scripts/council-view.mjs +2 -1
- package/pipeline/scripts/crush-json.mjs +2 -1
- package/pipeline/scripts/diff-explain.mjs +7 -10
- package/pipeline/scripts/diff-risk-score.mjs +2 -1
- package/pipeline/scripts/doctor.mjs +140 -6
- package/pipeline/scripts/evidence-gate.mjs +9 -3
- package/pipeline/scripts/feedback-send.mjs +12 -2
- package/pipeline/scripts/gc-abandoned.sh +32 -16
- package/pipeline/scripts/gc-tmp.sh +1 -1
- package/pipeline/scripts/gc-worktrees.sh +12 -5
- package/pipeline/scripts/gen-facts.mjs +175 -0
- package/pipeline/scripts/gen-mode-dispatch.mjs +32 -37
- package/pipeline/scripts/gen-ref-toc.mjs +1 -1
- package/pipeline/scripts/github-ssh-setup.sh +64 -7
- package/pipeline/scripts/graph-mermaid.mjs +4 -2
- package/pipeline/scripts/graph-report.mjs +1 -1
- package/pipeline/scripts/jira-attach.sh +1 -1
- package/pipeline/scripts/keychain-save.sh +101 -30
- package/pipeline/scripts/learn-from-transcripts.mjs +3 -2
- package/pipeline/scripts/learning-curve.mjs +36 -31
- package/pipeline/scripts/log-metric.sh +17 -4
- package/pipeline/scripts/make-manifest.mjs +199 -0
- package/pipeline/scripts/memory-save.sh +1 -1
- package/pipeline/scripts/migrate-prefs.mjs +24 -6
- package/pipeline/scripts/migrate-state.mjs +94 -4
- package/pipeline/scripts/phase-banner.sh +26 -22
- package/pipeline/scripts/phase-tracker.sh +48 -10
- package/pipeline/scripts/plan-coverage-gate.mjs +8 -4
- package/pipeline/scripts/pre-commit-check.sh +7 -0
- package/pipeline/scripts/pre-push-check.sh +7 -0
- package/pipeline/scripts/purge.sh +23 -6
- package/pipeline/scripts/render-agent-log-cost.sh +10 -3
- package/pipeline/scripts/render-cost-summary.sh +9 -2
- package/pipeline/scripts/render-work-summary.sh +14 -7
- package/pipeline/scripts/review-file-filter.mjs +5 -3
- package/pipeline/scripts/review-scope.mjs +2 -1
- package/pipeline/scripts/routine-registry.mjs +2 -1
- package/pipeline/scripts/run-aggregator.mjs +26 -20
- package/pipeline/scripts/run-metrics.mjs +4 -2
- package/pipeline/scripts/runs-index.mjs +353 -0
- package/pipeline/scripts/scorecard-snapshot.mjs +178 -0
- package/pipeline/scripts/search-logs.sh +18 -0
- package/pipeline/scripts/smoke-cross-cli-behavior.sh +6 -6
- package/pipeline/scripts/smoke-schema-validation.sh +26 -7
- package/pipeline/scripts/test-gap-scan.mjs +2 -1
- package/pipeline/scripts/test-integrity-gate.mjs +2 -1
- package/pipeline/scripts/token-budget-report.mjs +13 -2
- package/pipeline/scripts/triage-memory.mjs +2 -2
- package/pipeline/scripts/update-issue-progress.sh +56 -7
- package/pipeline/scripts/usage-report.mjs +12 -1
- package/pipeline/scripts/validate-analysis-doc.mjs +75 -18
- package/pipeline/scripts/validate-code-graph.mjs +6 -3
- package/pipeline/scripts/validate-complaint-doc.mjs +2 -1
- package/pipeline/scripts/validate-diff-risk.mjs +6 -3
- package/pipeline/scripts/validate-planning.mjs +1 -1
- package/pipeline/scripts/validate-reviewer.mjs +1 -1
- package/pipeline/scripts/validate-state.mjs +45 -5
- package/pipeline/scripts/validate-test-gap.mjs +6 -3
- package/pipeline/scripts/validate-triage.mjs +6 -4
- package/pipeline/scripts/verify-citations.mjs +4 -2
- package/pipeline/scripts/verify.mjs +327 -0
- package/pipeline/scripts/worktree-finalize.sh +18 -9
- package/pipeline/scripts/write-state.mjs +154 -15
- package/pipeline/skills/.skill-manifest.json +37 -21
- package/pipeline/skills/.skills-index.json +104 -5
- package/pipeline/skills/shared/README.md +15 -6
- package/pipeline/skills/shared/core/apple-archive-compliance/SKILL.md +2 -2
- package/pipeline/skills/shared/core/google-play-compliance/SKILL.md +2 -2
- package/pipeline/skills/shared/core/multi-agent/SKILL.md +69 -71
- package/pipeline/skills/shared/core/multi-agent-autopilot/SKILL.md +3 -3
- package/pipeline/skills/shared/core/multi-agent-channels/SKILL.md +14 -14
- package/pipeline/skills/shared/core/multi-agent-diff-explain/SKILL.md +5 -5
- package/pipeline/skills/shared/core/multi-agent-graph/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-help/SKILL.md +25 -23
- package/pipeline/skills/shared/core/multi-agent-language/SKILL.md +2 -2
- package/pipeline/skills/shared/core/multi-agent-local/SKILL.md +2 -2
- package/pipeline/skills/shared/core/multi-agent-local-autopilot/SKILL.md +8 -8
- package/pipeline/skills/shared/core/multi-agent-manual-test/SKILL.md +6 -6
- package/pipeline/skills/shared/core/multi-agent-model/SKILL.md +71 -0
- package/pipeline/skills/shared/core/multi-agent-refactor/SKILL.md +3 -3
- package/pipeline/skills/shared/core/multi-agent-resume/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-resume-local/SKILL.md +7 -7
- package/pipeline/skills/shared/core/multi-agent-route-off/SKILL.md +39 -0
- package/pipeline/skills/shared/core/multi-agent-route-on/SKILL.md +76 -0
- package/pipeline/skills/shared/core/multi-agent-route-status/SKILL.md +59 -0
- package/pipeline/skills/shared/core/multi-agent-setup/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-status/SKILL.md +35 -11
- package/pipeline/skills/shared/core/multi-agent-steer/SKILL.md +2 -2
- package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +6 -5
- package/pipeline/skills/shared/external/macos-spm-app-packaging/assets/templates/package_app.sh +4 -1
- package/pipeline/skills/shared/external/macos-spm-app-packaging/assets/templates/setup_dev_signing.sh +4 -1
- package/pipeline/skills/shared/external/macos-spm-app-packaging/assets/templates/sign-and-notarize.sh +2 -1
- package/pipeline/skills/skills-index.md +13 -4
- package/pipeline/multi-agent-refs/phases/phase-1-analysis.md +0 -263
- package/pipeline/multi-agent-refs/phases/phase-2-planning.md +0 -344
- package/pipeline/multi-agent-refs/phases/phase-5-test.md +0 -182
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* @file diff-explain.mjs - v7.8.0 Paket B
|
|
5
5
|
*
|
|
6
|
-
* Bridges Phase
|
|
6
|
+
* Bridges Phase 3 triage output to the actual git diff. For each accepted /
|
|
7
7
|
* deferred / rejected finding, locates the corresponding hunk in the branch's
|
|
8
8
|
* diff against base and renders an annotated markdown report.
|
|
9
9
|
*
|
|
@@ -31,9 +31,8 @@
|
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
33
|
import { readFileSync, existsSync } from "fs";
|
|
34
|
-
import { join } from "path";
|
|
35
|
-
import { homedir } from "os";
|
|
36
34
|
import { execFileSync } from "child_process";
|
|
35
|
+
import { resolveRunFile, taskIdVariants, logsRoot } from "./_run-paths.mjs";
|
|
37
36
|
|
|
38
37
|
const argv = process.argv.slice(2);
|
|
39
38
|
const flags = {};
|
|
@@ -71,14 +70,12 @@ Output:
|
|
|
71
70
|
}
|
|
72
71
|
|
|
73
72
|
function resolveStateByTaskId(id) {
|
|
74
|
-
const
|
|
75
|
-
const logRoot = join(homedir(), ".claude", "logs", "multi-agent");
|
|
76
|
-
const candidates = [...new Set([String(id), bare, `task-${bare}`])].map((d) =>
|
|
77
|
-
join(logRoot, d, "agent-state.json"),
|
|
78
|
-
);
|
|
79
|
-
const found = candidates.find((p) => existsSync(p));
|
|
73
|
+
const found = resolveRunFile(id, "agent-state.json", flags.project);
|
|
80
74
|
if (!found)
|
|
81
|
-
die(
|
|
75
|
+
die(
|
|
76
|
+
`no agent-state.json for task ${id} under ${logsRoot()} ` +
|
|
77
|
+
`(tried ${taskIdVariants(id).join(", ")} in both layouts)`,
|
|
78
|
+
);
|
|
82
79
|
return found;
|
|
83
80
|
}
|
|
84
81
|
|
|
@@ -369,8 +369,9 @@ function main() {
|
|
|
369
369
|
files: TOP ? rows.slice(0, Number(TOP)) : rows,
|
|
370
370
|
};
|
|
371
371
|
|
|
372
|
+
// Returning, not exiting: this payload carries a row per changed file and
|
|
373
|
+
// process.exit() would cut it at the pipe buffer.
|
|
372
374
|
process.stdout.write(PRETTY ? JSON.stringify(out, null, 2) + "\n" : JSON.stringify(out) + "\n");
|
|
373
|
-
process.exit(0);
|
|
374
375
|
}
|
|
375
376
|
|
|
376
377
|
try {
|
|
@@ -53,6 +53,7 @@ import { join, dirname } from "node:path";
|
|
|
53
53
|
import { homedir } from "node:os";
|
|
54
54
|
import { execFileSync } from "node:child_process";
|
|
55
55
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
56
|
+
import { runMain } from "../lib/fatal.mjs";
|
|
56
57
|
|
|
57
58
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
58
59
|
const HOME = homedir();
|
|
@@ -63,6 +64,12 @@ const JSON_OUT = argv.includes("--json");
|
|
|
63
64
|
const PROBE = argv.includes("--probe");
|
|
64
65
|
const EXPLAIN = argv.includes("--explain");
|
|
65
66
|
const LIST = argv.includes("--list-checks");
|
|
67
|
+
// `--profile server` adds checks that only matter when nobody is at the
|
|
68
|
+
// keyboard. They are ADDITIVE: the default run is byte-for-byte what it was,
|
|
69
|
+
// because a laptop being told it fails a server check would teach people to
|
|
70
|
+
// ignore doctor.
|
|
71
|
+
const profileFlag = argv.find((a) => a.startsWith("--profile="));
|
|
72
|
+
const PROFILE = profileFlag ? profileFlag.slice("--profile=".length) : "";
|
|
66
73
|
const taskFlag = argv.find((a) => a.startsWith("--task-tools="));
|
|
67
74
|
const TASK_TOOLS = taskFlag ? taskFlag.split("=")[1] : null;
|
|
68
75
|
|
|
@@ -89,6 +96,15 @@ const CHECK_IDS = [
|
|
|
89
96
|
"worktree-residue",
|
|
90
97
|
];
|
|
91
98
|
|
|
99
|
+
// Only under `--profile server`. Kept in their own list so `--list-checks`
|
|
100
|
+
// can say which ones a default run does not include.
|
|
101
|
+
const SERVER_CHECK_IDS = [
|
|
102
|
+
"unattended-contract",
|
|
103
|
+
"unattended-permissions",
|
|
104
|
+
"scheduler",
|
|
105
|
+
"keychain-unlock",
|
|
106
|
+
];
|
|
107
|
+
|
|
92
108
|
// Only these six may return BLOCK. Enforced below, not merely documented: a
|
|
93
109
|
// check that returns BLOCK without being listed here is downgraded and the
|
|
94
110
|
// downgrade is reported, because an unenforced rule is a comment.
|
|
@@ -728,7 +744,7 @@ function checkDiskSpace() {
|
|
|
728
744
|
}
|
|
729
745
|
|
|
730
746
|
// A finished task removes its own worktree at PR time, but a run that dies
|
|
731
|
-
// before Phase
|
|
747
|
+
// before Phase 4 never reaches that step and nothing else collects it: the
|
|
732
748
|
// finalizer only runs on success and gc-worktrees only sweeps entries git no
|
|
733
749
|
// longer knows about. So worktrees accumulate silently, and each one is a full
|
|
734
750
|
// second checkout. Measured on one real iOS repo: 15 left behind, 11 GB.
|
|
@@ -772,7 +788,7 @@ function checkWorktreeResidue() {
|
|
|
772
788
|
report(
|
|
773
789
|
"worktree-residue",
|
|
774
790
|
"WARN",
|
|
775
|
-
`${entries.length} worktree(s) left under ${wt}${size}; runs that stopped before Phase
|
|
791
|
+
`${entries.length} worktree(s) left under ${wt}${size}; runs that stopped before Phase 4 are never collected`,
|
|
776
792
|
"run /multi-agent:garbage-collect, or /multi-agent:kill for a task you know is dead",
|
|
777
793
|
);
|
|
778
794
|
return;
|
|
@@ -782,21 +798,138 @@ function checkWorktreeResidue() {
|
|
|
782
798
|
|
|
783
799
|
/* ------------------------------------------------------------------ main -- */
|
|
784
800
|
|
|
801
|
+
/**
|
|
802
|
+
* Checks that only matter when nobody is at the keyboard.
|
|
803
|
+
*
|
|
804
|
+
* Every one of them is a way a server run stops WITHOUT SAYING SO - the
|
|
805
|
+
* failure mode this whole profile exists for. A laptop hits none of them
|
|
806
|
+
* because a human is there to answer, unlock and restart, which is exactly why
|
|
807
|
+
* they are not in the default run.
|
|
808
|
+
*
|
|
809
|
+
* None may BLOCK: `MAY_BLOCK` is a closed set and these are readiness, not
|
|
810
|
+
* correctness. A machine that fails all four still runs the pipeline fine with
|
|
811
|
+
* someone watching it.
|
|
812
|
+
*
|
|
813
|
+
* @param {{root: string}|any} resolved the install layout from checkInstallPresent
|
|
814
|
+
*/
|
|
815
|
+
function checkServerProfile(resolved) {
|
|
816
|
+
const root = resolved?.root || join(homedir(), ".claude");
|
|
817
|
+
|
|
818
|
+
// 1. The contract exists on this machine. Without it nothing tells the
|
|
819
|
+
// operator which entry points honour MULTI_AGENT_UNATTENDED.
|
|
820
|
+
const contract = join(root, "multi-agent-refs", "unattended-contract.md");
|
|
821
|
+
if (existsSync(contract)) {
|
|
822
|
+
ok("unattended-contract");
|
|
823
|
+
} else {
|
|
824
|
+
report(
|
|
825
|
+
"unattended-contract",
|
|
826
|
+
"WARN",
|
|
827
|
+
"unattended-contract.md is not installed, so nothing here documents what an unattended run resolves",
|
|
828
|
+
"run /multi-agent:update to refresh the reference tree",
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// 2. The permission posture. autopilot spawns `claude --permission-prompts
|
|
833
|
+
// none`, but the tools it then calls still have to be allowed somewhere,
|
|
834
|
+
// and on a fresh machine they are not - the run stops at the first
|
|
835
|
+
// prompt with nobody to answer it.
|
|
836
|
+
const settings = join(root, "settings.json");
|
|
837
|
+
let allow = [];
|
|
838
|
+
try {
|
|
839
|
+
allow = JSON.parse(readFileSync(settings, "utf-8"))?.permissions?.allow ?? [];
|
|
840
|
+
} catch {
|
|
841
|
+
allow = [];
|
|
842
|
+
}
|
|
843
|
+
const needed = ["Bash", "Edit", "Write"];
|
|
844
|
+
const missing = needed.filter((t) => !allow.some((a) => String(a).startsWith(t)));
|
|
845
|
+
if (!missing.length) {
|
|
846
|
+
ok("unattended-permissions");
|
|
847
|
+
} else {
|
|
848
|
+
report(
|
|
849
|
+
"unattended-permissions",
|
|
850
|
+
"WARN",
|
|
851
|
+
`permissions.allow does not cover ${missing.join(", ")}; an unattended run stops at the first prompt`,
|
|
852
|
+
"run /multi-agent:setup, or add them to permissions.allow in settings.json",
|
|
853
|
+
{ settings, allow },
|
|
854
|
+
);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// 3. Something has to start the work. launchd is the only scheduler this
|
|
858
|
+
// ships a template for; a server with no agent loaded runs nothing and
|
|
859
|
+
// looks idle rather than broken.
|
|
860
|
+
let loaded = false;
|
|
861
|
+
try {
|
|
862
|
+
loaded = execFileSync("launchctl", ["list"], { encoding: "utf-8", timeout: 5000 }).includes(
|
|
863
|
+
"multi-agent",
|
|
864
|
+
);
|
|
865
|
+
} catch {
|
|
866
|
+
// launchctl absent or refusing: treat as "nothing is loaded", which is the
|
|
867
|
+
// conservative read - claiming a scheduler we could not see would be worse.
|
|
868
|
+
}
|
|
869
|
+
if (loaded) {
|
|
870
|
+
ok("scheduler");
|
|
871
|
+
} else {
|
|
872
|
+
report(
|
|
873
|
+
"scheduler",
|
|
874
|
+
"WARN",
|
|
875
|
+
"no multi-agent launchd agent is loaded, so nothing starts a queued run on its own",
|
|
876
|
+
"run /multi-agent:autopilot to install the agent, or schedule the runner yourself",
|
|
877
|
+
);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// 4. The keychain. Every credential this pipeline reads lives there, and a
|
|
881
|
+
// LaunchDaemon started before login gets a LOCKED keychain: each fetch
|
|
882
|
+
// fails, and the failures surface far away as 401s that blame the token.
|
|
883
|
+
// Reading a real credential here would be a network call and a side
|
|
884
|
+
// effect; the presence of a login-keychain path is the cheap proxy.
|
|
885
|
+
let keychain = "";
|
|
886
|
+
try {
|
|
887
|
+
keychain = execFileSync("security", ["default-keychain"], {
|
|
888
|
+
encoding: "utf-8",
|
|
889
|
+
timeout: 5000,
|
|
890
|
+
}).trim();
|
|
891
|
+
} catch {
|
|
892
|
+
// Same shape: unreachable reads as absent.
|
|
893
|
+
}
|
|
894
|
+
if (keychain) {
|
|
895
|
+
ok("keychain-unlock");
|
|
896
|
+
} else {
|
|
897
|
+
report(
|
|
898
|
+
"keychain-unlock",
|
|
899
|
+
"WARN",
|
|
900
|
+
"no default keychain is reachable from this session; a boot-time daemon sees a locked one and every credential read fails as a 401",
|
|
901
|
+
"run the LaunchAgent at login rather than a LaunchDaemon at boot (docs/server-readiness.md)",
|
|
902
|
+
);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
|
|
785
906
|
function main() {
|
|
786
907
|
if (LIST) {
|
|
908
|
+
// Lists what THIS invocation would run, which is what the name promises
|
|
909
|
+
// and what the registry gate compares against the output of a real run.
|
|
910
|
+
// Listing the server checks unconditionally made `--list-checks` disagree
|
|
911
|
+
// with a default run by four, and the gate that counts printed lines
|
|
912
|
+
// against listed checks caught it immediately.
|
|
787
913
|
for (const id of CHECK_IDS) process.stdout.write(`${id}\n`);
|
|
914
|
+
if (PROFILE === "server") for (const id of SERVER_CHECK_IDS) process.stdout.write(`${id}\n`);
|
|
788
915
|
return;
|
|
789
916
|
}
|
|
790
917
|
const unknown = argv.filter(
|
|
791
|
-
(a) =>
|
|
918
|
+
(a) =>
|
|
919
|
+
a.startsWith("--") && !/^--(probe|json|explain|list-checks|task-tools=|profile=)/.test(a),
|
|
792
920
|
);
|
|
793
921
|
if (unknown.length) {
|
|
794
922
|
process.stderr.write(
|
|
795
|
-
`usage: doctor.mjs [--probe] [--json] [--explain] [--task-tools=yes|no] | --list-checks\n`,
|
|
923
|
+
`usage: doctor.mjs [--probe] [--json] [--explain] [--task-tools=yes|no] [--profile=server] | --list-checks\n`,
|
|
796
924
|
);
|
|
797
925
|
process.exitCode = 3;
|
|
798
926
|
return;
|
|
799
927
|
}
|
|
928
|
+
if (PROFILE && PROFILE !== "server") {
|
|
929
|
+
process.stderr.write(`usage: --profile takes "server" (got "${PROFILE}")\n`);
|
|
930
|
+
process.exitCode = 3;
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
800
933
|
if (taskFlag && TASK_TOOLS !== "yes" && TASK_TOOLS !== "no") {
|
|
801
934
|
process.stderr.write("usage: --task-tools takes yes or no\n");
|
|
802
935
|
process.exitCode = 3;
|
|
@@ -837,6 +970,7 @@ function main() {
|
|
|
837
970
|
checkMcpSurface();
|
|
838
971
|
checkDiskSpace();
|
|
839
972
|
checkWorktreeResidue();
|
|
973
|
+
if (PROFILE === "server") checkServerProfile(resolved);
|
|
840
974
|
|
|
841
975
|
const blocked = results.filter((r) => r.severity === "BLOCK");
|
|
842
976
|
const warned = results.filter((r) => r.severity === "WARN");
|
|
@@ -852,7 +986,7 @@ function main() {
|
|
|
852
986
|
// Every check prints, in registry order, including the ones that found
|
|
853
987
|
// nothing and the ones that were not run. A list that shows only problems
|
|
854
988
|
// cannot be told apart from a list that was never produced.
|
|
855
|
-
for (const id of CHECK_IDS) {
|
|
989
|
+
for (const id of [...CHECK_IDS, ...SERVER_CHECK_IDS]) {
|
|
856
990
|
const r = results.find((x) => x.id === id);
|
|
857
991
|
if (!r) continue;
|
|
858
992
|
if (r.severity === "OK") {
|
|
@@ -897,5 +1031,5 @@ function invokedDirectly() {
|
|
|
897
1031
|
}
|
|
898
1032
|
|
|
899
1033
|
if (invokedDirectly()) {
|
|
900
|
-
main
|
|
1034
|
+
runMain("doctor", main);
|
|
901
1035
|
}
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
// 1 - pass claim is UNVERIFIED (no evidence / empty / shows failure / no success marker) -> caller must NOT record passed
|
|
36
36
|
// 2 - usage error
|
|
37
37
|
|
|
38
|
-
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
38
|
+
import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
39
39
|
|
|
40
40
|
// Failure markers are kept DEFINITIVE (not broad) because failure now overrides
|
|
41
41
|
// success (see the decision logic): a bare "error:" or stray "FAILED" in an
|
|
@@ -103,12 +103,18 @@ function parseArgs(argv) {
|
|
|
103
103
|
return out;
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
// Both of these have to stop the run where they are called - the gate's whole
|
|
107
|
+
// contract is "verdict, then nothing else happens" - so process.exit() stays.
|
|
108
|
+
// What changes is the write: console.log to a pipe is asynchronous and exit
|
|
109
|
+
// discards what has not drained, while writeFileSync to fd 1 returns only once
|
|
110
|
+
// the bytes are gone. The verdict is short today; the verdict is also the only
|
|
111
|
+
// thing a caller reads, so losing half of it is the one failure that matters.
|
|
106
112
|
function fail(msg) {
|
|
107
|
-
|
|
113
|
+
writeFileSync(1, `${JSON.stringify({ ok: false, code: 1, reason: msg })}\n`);
|
|
108
114
|
process.exit(1);
|
|
109
115
|
}
|
|
110
116
|
function ok(msg) {
|
|
111
|
-
|
|
117
|
+
writeFileSync(1, `${JSON.stringify({ ok: true, code: 0, reason: msg })}\n`);
|
|
112
118
|
process.exit(0);
|
|
113
119
|
}
|
|
114
120
|
function usage(msg) {
|
|
@@ -138,8 +138,10 @@ async function main() {
|
|
|
138
138
|
};
|
|
139
139
|
|
|
140
140
|
if (process.argv.includes("--dry-run")) {
|
|
141
|
+
// return, not process.exit(0): the whole point of --dry-run is to read this
|
|
142
|
+
// payload, and exiting truncates it whenever stdout is a pipe.
|
|
141
143
|
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
142
|
-
|
|
144
|
+
return;
|
|
143
145
|
}
|
|
144
146
|
|
|
145
147
|
if (!endpointAllowed(endpoint)) {
|
|
@@ -180,4 +182,12 @@ async function main() {
|
|
|
180
182
|
}
|
|
181
183
|
}
|
|
182
184
|
|
|
183
|
-
main()
|
|
185
|
+
// `main()` is async, and calling it bare leaves any rejection it does not
|
|
186
|
+
// handle itself as an unhandled rejection: Node prints its own stack trace and
|
|
187
|
+
// exits 1, so the user of a feedback command sees a crash dump instead of the
|
|
188
|
+
// one line this file writes for every other failure. The catch keeps the
|
|
189
|
+
// failure in this file's own voice.
|
|
190
|
+
main().catch((err) => {
|
|
191
|
+
process.stderr.write(`ERROR: feedback-send failed - ${err?.message ?? err}\n`);
|
|
192
|
+
process.exit(1);
|
|
193
|
+
});
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
# Three things keep this from being a foot-gun, and each is a rule rather than a
|
|
12
12
|
# heuristic:
|
|
13
13
|
#
|
|
14
|
-
# 1. A run WAITING FOR YOU is never reaped. An open PR, Phase
|
|
14
|
+
# 1. A run WAITING FOR YOU is never reaped. An open PR, Phase 4 or 7, or
|
|
15
15
|
# `status: awaiting_input` means the work landed and the pipeline is
|
|
16
16
|
# holding for an answer by design. That is finished work, not residue.
|
|
17
17
|
# 2. A path that is not strictly inside `<repo>/.worktrees/` is never removed.
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
#
|
|
50
50
|
# State is moved to `<run-dir>/artifacts/` before the worktree goes, so the log,
|
|
51
51
|
# the findings and the phase history survive the sweep. The same recovery
|
|
52
|
-
# contract Phase
|
|
52
|
+
# contract Phase 4 uses when it removes a finished worktree.
|
|
53
53
|
#
|
|
54
54
|
# SAFE BY DEFAULT: dry-run. Removes nothing until you pass --yes.
|
|
55
55
|
#
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
#
|
|
65
65
|
# Exit: 0 on success (including "nothing to do"), 2 on usage error.
|
|
66
66
|
|
|
67
|
-
set -
|
|
67
|
+
set -euo pipefail
|
|
68
68
|
|
|
69
69
|
LOGS="$HOME/.claude/logs/multi-agent"
|
|
70
70
|
REPOS="$HOME"
|
|
@@ -121,20 +121,25 @@ HANDLED=""
|
|
|
121
121
|
# sweep that walks one layout leaves the other copy claiming the run is live.
|
|
122
122
|
while IFS= read -r state; do
|
|
123
123
|
[ -f "$state" ] || continue
|
|
124
|
-
|
|
124
|
+
# Every jq read in this loop is forgiven. A malformed state file is exactly
|
|
125
|
+
# what a sweep for abandoned runs expects to meet, and under `set -e` the
|
|
126
|
+
# first one would end the sweep - leaving every run AFTER it untouched and
|
|
127
|
+
# reporting a clean tree. An empty value already means "unknown", and the
|
|
128
|
+
# rules below refuse to delete on unknown.
|
|
129
|
+
status=$(jq -r '.status // ""' "$state" 2>/dev/null || true)
|
|
125
130
|
# Only runs that CLAIM to be running. A file with no status is unknown, and
|
|
126
131
|
# unknown is not a licence to delete.
|
|
127
132
|
case "$status" in in_progress | paused | awaiting_input) ;; *) continue ;; esac
|
|
128
133
|
|
|
129
|
-
task=$(jq -r '.taskId // .jiraId // ""' "$state" 2>/dev/null)
|
|
134
|
+
task=$(jq -r '.taskId // .jiraId // ""' "$state" 2>/dev/null || true)
|
|
130
135
|
[ -n "$task" ] || continue
|
|
131
|
-
phase=$(jq -r '.currentPhase // "?"' "$state" 2>/dev/null)
|
|
136
|
+
phase=$(jq -r '.currentPhase // "?"' "$state" 2>/dev/null || true)
|
|
132
137
|
# `.pr` is an object in the schema and a bare URL string in three state files
|
|
133
138
|
# on disk. `jq '.pr.url'` on a string errors, and with stderr suppressed that
|
|
134
139
|
# reads as "no PR" - so a run whose PR is already open would be classed as dead
|
|
135
140
|
# and reaped. Read the shape, do not assume it.
|
|
136
|
-
pr=$(jq -r 'if (.pr|type)=="string" then .pr elif (.pr|type)=="object" then (.pr.url // .pr.number // "") else "" end | tostring' "$state" 2>/dev/null)
|
|
137
|
-
wt=$(jq -r '.worktreePath // ""' "$state" 2>/dev/null)
|
|
141
|
+
pr=$(jq -r 'if (.pr|type)=="string" then .pr elif (.pr|type)=="object" then (.pr.url // .pr.number // "") else "" end | tostring' "$state" 2>/dev/null || true)
|
|
142
|
+
wt=$(jq -r '.worktreePath // ""' "$state" 2>/dev/null || true)
|
|
138
143
|
|
|
139
144
|
# Rule 1: waiting for you is not residue.
|
|
140
145
|
if [ "$status" = "awaiting_input" ] || [ -n "$pr" ] || [ "$phase" = "6" ] || [ "$phase" = "7" ]; then
|
|
@@ -142,7 +147,8 @@ while IFS= read -r state; do
|
|
|
142
147
|
continue
|
|
143
148
|
fi
|
|
144
149
|
|
|
145
|
-
mtime=$(_mtime "$state")
|
|
150
|
+
mtime=$(_mtime "$state" || true)
|
|
151
|
+
[ -n "$mtime" ] || continue
|
|
146
152
|
age_days=$(((NOW - mtime) / 86400))
|
|
147
153
|
if [ "$phase" = "0" ]; then
|
|
148
154
|
limit="$PHASE0_DAYS"; kind="left at a Phase 0 question"
|
|
@@ -155,7 +161,8 @@ while IFS= read -r state; do
|
|
|
155
161
|
# root recorded in worktreePath is the shape that would delete a checkout.
|
|
156
162
|
safe_wt=""
|
|
157
163
|
if [ -n "$wt" ] && [ -d "$wt" ] && [ ! -L "$wt" ]; then
|
|
158
|
-
|
|
164
|
+
# A worktree path that no longer resolves is the ordinary case here.
|
|
165
|
+
real=$(cd "$wt" 2>/dev/null && pwd -P || true)
|
|
159
166
|
case "$real" in
|
|
160
167
|
*/.worktrees/*) safe_wt="$real" ;;
|
|
161
168
|
*) skipped_unsafe=$((skipped_unsafe + 1)) ;;
|
|
@@ -187,8 +194,11 @@ $safe_wt"
|
|
|
187
194
|
fi
|
|
188
195
|
|
|
189
196
|
run_dir=$(dirname "$state")
|
|
190
|
-
|
|
191
|
-
|
|
197
|
+
# Forgiven, both of them: this is the salvage copy, and a run whose artefacts
|
|
198
|
+
# cannot be copied is still a run that has to be marked abandoned. Under
|
|
199
|
+
# `set -e` an unreadable state file here would end the entire sweep.
|
|
200
|
+
mkdir -p "$run_dir/artifacts" 2>/dev/null || true
|
|
201
|
+
cp "$state" "$run_dir/artifacts/agent-state.json" 2>/dev/null || true
|
|
192
202
|
tmp="$state.tmp.$$"
|
|
193
203
|
if jq --arg r "abandoned by gc-abandoned after ${age_days}d idle" \
|
|
194
204
|
'.status = "failed" | .haltReason = $r' "$state" > "$tmp" 2>/dev/null; then
|
|
@@ -209,7 +219,8 @@ $safe_wt"
|
|
|
209
219
|
if [ -n "$safe_wt" ]; then
|
|
210
220
|
repo=${safe_wt%%/.worktrees/*}
|
|
211
221
|
git -C "$repo" worktree remove --force "$safe_wt" >/dev/null 2>&1 || rm -rf "$safe_wt"
|
|
212
|
-
|
|
222
|
+
# Forgiven: one flaky prune must not end the sweep before its summary line.
|
|
223
|
+
git -C "$repo" worktree prune >/dev/null 2>&1 || true
|
|
213
224
|
printf '→ removed: %s (%s, idle %sd)\n' "$task" "$human" "$age_days"
|
|
214
225
|
freed_kb=$((freed_kb + ${size_kb:-0}))
|
|
215
226
|
else
|
|
@@ -233,7 +244,11 @@ if [ "$STATE_ONLY" -eq 0 ] && [ -d "$REPOS" ]; then
|
|
|
233
244
|
# re-walk of the log tree for each of 28 directories.
|
|
234
245
|
index=$(find "$LOGS" -name agent-state.json -type f -maxdepth 4 -not -path '*/artifacts/*' 2>/dev/null |
|
|
235
246
|
while IFS= read -r f; do
|
|
236
|
-
|
|
247
|
+
# One malformed state file must not truncate the index: under `set -e`
|
|
248
|
+
# a jq failure inside this subshell ends the loop, and the entries after
|
|
249
|
+
# it would silently not exist - which reads as "no worktree matches" and
|
|
250
|
+
# protects nothing.
|
|
251
|
+
jq -r '[(.worktreePath // ""), (.taskId // .jiraId // ""), (.status // ""), (if (.pr|type)=="string" then .pr elif (.pr|type)=="object" then (.pr.url // .pr.number // "") else "" end | tostring), ((.currentPhase // "") | tostring)] | @tsv' "$f" 2>/dev/null || true
|
|
237
252
|
done)
|
|
238
253
|
|
|
239
254
|
for wtroot in "$REPOS"/*/.worktrees; do
|
|
@@ -256,7 +271,7 @@ if [ "$STATE_ONLY" -eq 0 ] && [ -d "$REPOS" ]; then
|
|
|
256
271
|
ph=$(printf '%s' "$row" | cut -f5)
|
|
257
272
|
|
|
258
273
|
# Rule 1 again: landed work is not residue, whichever pass finds it. Order
|
|
259
|
-
# matters here - the phase-
|
|
274
|
+
# matters here - the phase-4/5 test is a proxy for "still waiting", and a
|
|
260
275
|
# run whose status already says it FINISHED is not waiting for anyone. A
|
|
261
276
|
# complete run sitting at phase 7 was read as waiting and never reaped,
|
|
262
277
|
# which is how three finished worktrees held 5.1 GB indefinitely.
|
|
@@ -322,7 +337,8 @@ if [ "$STATE_ONLY" -eq 0 ] && [ -d "$REPOS" ]; then
|
|
|
322
337
|
continue
|
|
323
338
|
fi
|
|
324
339
|
git -C "$repo" worktree remove --force "$real" >/dev/null 2>&1 || rm -rf "$real"
|
|
325
|
-
|
|
340
|
+
# Same reason as the first pass: a failed prune is not a reason to stop.
|
|
341
|
+
git -C "$repo" worktree prune >/dev/null 2>&1 || true
|
|
326
342
|
printf '→ removed worktree: %s (%s, %s, idle %sd)\n' "$id" "$why" "$human" "$age_days"
|
|
327
343
|
freed_kb=$((freed_kb + ${size_kb:-0}))
|
|
328
344
|
removed=$((removed + 1))
|
|
@@ -107,7 +107,7 @@ NAME_ARGS=(
|
|
|
107
107
|
# `complaint-analysis-*` needs its own entry: find matches the basename against
|
|
108
108
|
# the whole glob, so `analysis-*` never matched it. Same for the other three,
|
|
109
109
|
# which were writing into $ROOT with nothing sweeping them (phase-0-init.md,
|
|
110
|
-
# phase-
|
|
110
|
+
# phase-4-commit.md, generate-issue.md).
|
|
111
111
|
#
|
|
112
112
|
# `*-wiki` is the one loose pattern here, and a suffix glob over $ROOT could
|
|
113
113
|
# name something a person created. It is narrowed twice: `-type d` above, and
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
# `.pipeline/` left behind by a finished run stops reading as residue.
|
|
22
22
|
#
|
|
23
23
|
# Registered, healthy worktrees are NEVER touched. Finishing a task removes its
|
|
24
|
-
# own worktree in Phase
|
|
24
|
+
# own worktree in Phase 4 (worktree-finalize, v14.1.0+); killing one is
|
|
25
25
|
# /multi-agent:kill. This sweep only reaps orphans neither of those left behind.
|
|
26
26
|
#
|
|
27
27
|
# SAFE BY DEFAULT: dry-run. Deletes nothing until you pass --yes.
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
#
|
|
35
35
|
# Exit: 0 on success (including "nothing to do"), 2 on usage error.
|
|
36
36
|
|
|
37
|
-
set -
|
|
37
|
+
set -euo pipefail
|
|
38
38
|
|
|
39
39
|
REPO="$PWD"
|
|
40
40
|
DELETE=0
|
|
@@ -69,7 +69,9 @@ if ! git -C "$REPO" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
|
69
69
|
echo "gc-worktrees: $REPO is not a git work tree - nothing to do"
|
|
70
70
|
exit 0
|
|
71
71
|
fi
|
|
72
|
-
|
|
72
|
+
# A repo path that cannot be entered is a usage error with a message, not a
|
|
73
|
+
# silent exit 1 three lines before the message exists.
|
|
74
|
+
REPO="$(cd "$REPO" && pwd -P)" || { echo "gc-worktrees: cannot enter $REPO" >&2; exit 2; }
|
|
73
75
|
# Anchor on the MAIN worktree (first porcelain entry) so a run started from a
|
|
74
76
|
# subdirectory OR from inside a linked worktree still targets
|
|
75
77
|
# <main-repo>/.worktrees/ rather than a bogus <subdir>/.worktrees/ (which
|
|
@@ -107,7 +109,9 @@ EOF
|
|
|
107
109
|
for d in "$WT_ROOT"/*/; do
|
|
108
110
|
[ -d "$d" ] || continue
|
|
109
111
|
[ -L "${d%/}" ] && continue # never follow a symlinked entry out of the tree
|
|
110
|
-
|
|
112
|
+
# A directory that vanished between the glob and this line is ordinary on a
|
|
113
|
+
# machine where runs are finishing; skipping it is the answer, not aborting.
|
|
114
|
+
dir="$(cd "$d" && pwd -P)" || continue
|
|
111
115
|
# Path guard: only ever consider dirs strictly inside <repo>/.worktrees/.
|
|
112
116
|
case "$dir" in "$WT_ROOT"/*) ;; *) continue ;; esac
|
|
113
117
|
if printf '%s\n' "$registered" | grep -qxF "$dir"; then
|
|
@@ -120,7 +124,10 @@ EOF
|
|
|
120
124
|
done
|
|
121
125
|
fi
|
|
122
126
|
for dir in ${orphans+"${orphans[@]}"}; do
|
|
123
|
-
size
|
|
127
|
+
# Cosmetic: the size goes in a report line and prints as "?" when du cannot
|
|
128
|
+
# read it. Under `set -e` with pipefail an unreadable directory would end the
|
|
129
|
+
# sweep instead.
|
|
130
|
+
size=$(du -sh "$dir" 2>/dev/null | cut -f1 || true)
|
|
124
131
|
if [ "$DELETE" -eq 1 ]; then
|
|
125
132
|
rm -rf "$dir" && { echo "→ removed orphan worktree dir: ${dir#"$REPO"/} (${size:-?})"; changed=$((changed + 1)); }
|
|
126
133
|
else
|