@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.
Files changed (272) hide show
  1. package/CHANGELOG.md +310 -0
  2. package/README.md +76 -18
  3. package/README.tr.md +55 -16
  4. package/docs/adr/0002-instruction-driven-flag.md +1 -0
  5. package/docs/adr/0005-lazy-phase-docs.md +11 -1
  6. package/docs/adr/0008-installer-modularization-and-secret-leak-defense.md +1 -0
  7. package/docs/adr/0010-own-code-graph.md +1 -0
  8. package/docs/adr/0011-dormant-ci.md +25 -1
  9. package/docs/adr/0014-six-phase-consolidation.md +134 -0
  10. package/docs/adr/README.md +2 -1
  11. package/docs/architecture.md +37 -38
  12. package/docs/best-practices.md +1 -1
  13. package/docs/ecosystem.md +37 -26
  14. package/docs/engineering.md +1 -1
  15. package/docs/facts.json +45 -0
  16. package/docs/features.md +54 -53
  17. package/docs/performance.md +5 -5
  18. package/docs/recovery-guide.md +9 -9
  19. package/docs/server-readiness.md +188 -0
  20. package/docs/token-budget-history.md +3 -1
  21. package/index.js +18 -3
  22. package/install/_codex-agents.mjs +1 -1
  23. package/install/_common.mjs +42 -17
  24. package/install/_dev-only-files.mjs +8 -0
  25. package/install/_unattended-profile.mjs +113 -0
  26. package/install/index.mjs +48 -0
  27. package/install/templates/claude-hooks.json +1 -1
  28. package/install/templates/codex-instructions.md +1 -1
  29. package/install/templates/copilot-instructions.md +28 -28
  30. package/manifest.json +1065 -0
  31. package/package.json +6 -3
  32. package/pipeline/agents/dev-critic.md +3 -3
  33. package/pipeline/commands/figma-to-swiftui.md +1 -1
  34. package/pipeline/commands/multi-agent/SKILL.md +8 -8
  35. package/pipeline/commands/multi-agent/analysis/SKILL.md +9 -9
  36. package/pipeline/commands/multi-agent/autopilot/SKILL.md +7 -7
  37. package/pipeline/commands/multi-agent/channels/SKILL.md +15 -15
  38. package/pipeline/commands/multi-agent/diff-explain/SKILL.md +6 -6
  39. package/pipeline/commands/multi-agent/garbage-collect/SKILL.md +1 -1
  40. package/pipeline/commands/multi-agent/graph/SKILL.md +1 -1
  41. package/pipeline/commands/multi-agent/help/SKILL.md +62 -62
  42. package/pipeline/commands/multi-agent/language/SKILL.md +2 -2
  43. package/pipeline/commands/multi-agent/local/SKILL.md +11 -11
  44. package/pipeline/commands/multi-agent/local-autopilot/SKILL.md +13 -13
  45. package/pipeline/commands/multi-agent/log/SKILL.md +2 -2
  46. package/pipeline/commands/multi-agent/manual-test/SKILL.md +9 -9
  47. package/pipeline/commands/multi-agent/model/SKILL.md +69 -0
  48. package/pipeline/commands/multi-agent/refactor/SKILL.md +3 -3
  49. package/pipeline/commands/multi-agent/resume/SKILL.md +4 -4
  50. package/pipeline/commands/multi-agent/resume-local/SKILL.md +19 -17
  51. package/pipeline/commands/multi-agent/review/SKILL.md +1 -1
  52. package/pipeline/commands/multi-agent/route-off/SKILL.md +36 -0
  53. package/pipeline/commands/multi-agent/route-on/SKILL.md +74 -0
  54. package/pipeline/commands/multi-agent/route-status/SKILL.md +56 -0
  55. package/pipeline/commands/multi-agent/setup/SKILL.md +2 -2
  56. package/pipeline/commands/multi-agent/status/SKILL.md +54 -23
  57. package/pipeline/commands/multi-agent/steer/SKILL.md +2 -2
  58. package/pipeline/commands/multi-agent/sync/SKILL.md +12 -13
  59. package/pipeline/commands/multi-agent/test/SKILL.md +1 -1
  60. package/pipeline/lib/_jira-auth.sh +8 -0
  61. package/pipeline/lib/analysis-jira-write.sh +32 -0
  62. package/pipeline/lib/ask-choice.sh +13 -2
  63. package/pipeline/lib/autopilot-state.sh +8 -0
  64. package/pipeline/lib/credential-inventory.sh +1 -1
  65. package/pipeline/lib/fatal.mjs +129 -0
  66. package/pipeline/lib/fetch-fortify.sh +1 -1
  67. package/pipeline/lib/figma-mcp-refresh.sh +18 -0
  68. package/pipeline/lib/figma-screenshot.sh +18 -0
  69. package/pipeline/lib/invoked-directly.mjs +43 -0
  70. package/pipeline/lib/jira-publish.sh +42 -0
  71. package/pipeline/lib/md2confluence-v3.py +47 -0
  72. package/pipeline/lib/model-rung.sh +142 -0
  73. package/pipeline/lib/outbound-gate.mjs +175 -0
  74. package/pipeline/lib/phase-schema.mjs +88 -0
  75. package/pipeline/lib/plan-todos.sh +32 -11
  76. package/pipeline/lib/post-pr-review.sh +77 -8
  77. package/pipeline/lib/repo-hygiene.sh +8 -3
  78. package/pipeline/lib/require-jq.sh +40 -0
  79. package/pipeline/lib/route-state.sh +161 -0
  80. package/pipeline/lib/run-paths.sh +335 -0
  81. package/pipeline/multi-agent-refs/_account-picker.md +1 -1
  82. package/pipeline/multi-agent-refs/_dev-context.md +1 -1
  83. package/pipeline/multi-agent-refs/_input-parser.md +1 -1
  84. package/pipeline/multi-agent-refs/analysis/evidence.md +0 -9
  85. package/pipeline/multi-agent-refs/analysis/intake.md +1 -1
  86. package/pipeline/multi-agent-refs/analysis/locked.md +21 -22
  87. package/pipeline/multi-agent-refs/analysis/render.md +1 -1
  88. package/pipeline/multi-agent-refs/analysis/synthesis.md +12 -6
  89. package/pipeline/multi-agent-refs/android-guide.md +1 -1
  90. package/pipeline/multi-agent-refs/audit-guide.md +13 -13
  91. package/pipeline/multi-agent-refs/channels/issue-comment.md +2 -2
  92. package/pipeline/multi-agent-refs/channels/jira.md +3 -3
  93. package/pipeline/multi-agent-refs/channels/pr.md +4 -4
  94. package/pipeline/multi-agent-refs/channels/wiki.md +1 -1
  95. package/pipeline/multi-agent-refs/component-dispatch.md +3 -3
  96. package/pipeline/multi-agent-refs/cross-cli-contract.md +31 -6
  97. package/pipeline/multi-agent-refs/features/autopilot-circuit-breaker.md +74 -4
  98. package/pipeline/multi-agent-refs/features/code-graph.md +5 -5
  99. package/pipeline/multi-agent-refs/features/cost-analysis.md +93 -0
  100. package/pipeline/multi-agent-refs/features/design-conformance.md +1 -1
  101. package/pipeline/multi-agent-refs/features/dev-critic.md +3 -3
  102. package/pipeline/multi-agent-refs/features/doctor.md +47 -2
  103. package/pipeline/multi-agent-refs/features/external-context-injection.md +3 -3
  104. package/pipeline/multi-agent-refs/features/maturity-followup.md +3 -3
  105. package/pipeline/multi-agent-refs/features/model-fallback.md +5 -5
  106. package/pipeline/multi-agent-refs/features/plan-todos.md +1 -1
  107. package/pipeline/multi-agent-refs/features/repo-map.md +1 -1
  108. package/pipeline/multi-agent-refs/features/review-delta.md +3 -3
  109. package/pipeline/multi-agent-refs/features/review-multi-repo.md +1 -1
  110. package/pipeline/multi-agent-refs/features/scope-check.md +4 -4
  111. package/pipeline/multi-agent-refs/features/skill-conformance.md +2 -2
  112. package/pipeline/multi-agent-refs/features/stack-skill-routing.md +1 -1
  113. package/pipeline/multi-agent-refs/features/verify-by-test.md +4 -4
  114. package/pipeline/multi-agent-refs/features/verify.md +83 -0
  115. package/pipeline/multi-agent-refs/features/visual-evidence.md +19 -19
  116. package/pipeline/multi-agent-refs/features/worktree-finalize.md +6 -6
  117. package/pipeline/multi-agent-refs/issue-jira-triad.md +10 -10
  118. package/pipeline/multi-agent-refs/knowledge.md +11 -11
  119. package/pipeline/multi-agent-refs/multi-repo-integration-build.md +13 -13
  120. package/pipeline/multi-agent-refs/payload-contracts.md +8 -8
  121. package/pipeline/multi-agent-refs/phases/log-format.md +10 -10
  122. package/pipeline/multi-agent-refs/phases/modes.md +30 -30
  123. package/pipeline/multi-agent-refs/phases/operations.md +21 -10
  124. package/pipeline/multi-agent-refs/phases/phase-0-init.md +25 -25
  125. package/pipeline/multi-agent-refs/phases/phase-1-plan.md +599 -0
  126. package/pipeline/multi-agent-refs/phases/{phase-3-dev.md → phase-2-dev.md} +129 -49
  127. package/pipeline/multi-agent-refs/phases/{phase-4-review.md → phase-3-review.md} +225 -107
  128. package/pipeline/multi-agent-refs/phases/{phase-6-commit.md → phase-4-commit.md} +23 -23
  129. package/pipeline/multi-agent-refs/phases/{phase-7-report.md → phase-5-report.md} +29 -29
  130. package/pipeline/multi-agent-refs/phases.md +44 -48
  131. package/pipeline/multi-agent-refs/picker-contract.md +1 -1
  132. package/pipeline/multi-agent-refs/progress-contract.md +6 -6
  133. package/pipeline/multi-agent-refs/readiness-review.md +1 -1
  134. package/pipeline/multi-agent-refs/rules.md +7 -7
  135. package/pipeline/multi-agent-refs/swiftui-guide.md +2 -2
  136. package/pipeline/multi-agent-refs/tracker-contract.md +31 -32
  137. package/pipeline/multi-agent-refs/unattended-contract.md +129 -0
  138. package/pipeline/multi-agent-refs/wiki-capture.md +14 -14
  139. package/pipeline/preferences-template.json +9 -1
  140. package/pipeline/rules/outside-the-pipeline.md +1 -1
  141. package/pipeline/schemas/agent-state.schema.json +50 -50
  142. package/pipeline/schemas/analysis-output.schema.json +2 -2
  143. package/pipeline/schemas/autopilot-config.schema.json +1 -1
  144. package/pipeline/schemas/code-graph.schema.json +1 -1
  145. package/pipeline/schemas/criteria-manifest.schema.json +1 -1
  146. package/pipeline/schemas/dev-critic-output.schema.json +1 -1
  147. package/pipeline/schemas/diff-risk.schema.json +1 -1
  148. package/pipeline/schemas/migrations/prefs-2.4.0-to-2.5.0.mjs +2 -2
  149. package/pipeline/schemas/migrations/prefs-2.6.0-to-2.7.0.mjs +31 -0
  150. package/pipeline/schemas/migrations/state-2.1.0-to-2.2.0.mjs +129 -0
  151. package/pipeline/schemas/phases.json +105 -0
  152. package/pipeline/schemas/plan-todos.schema.json +5 -5
  153. package/pipeline/schemas/planning-output.schema.json +1 -1
  154. package/pipeline/schemas/prefs.schema.json +100 -56
  155. package/pipeline/schemas/reviewer-output.schema.json +3 -3
  156. package/pipeline/schemas/route-config.schema.json +74 -0
  157. package/pipeline/schemas/scope-check.schema.json +1 -1
  158. package/pipeline/schemas/test-gap.schema.json +1 -1
  159. package/pipeline/schemas/token-budget.json +12 -18
  160. package/pipeline/schemas/triage-output.schema.json +6 -6
  161. package/pipeline/scripts/README.md +3 -3
  162. package/pipeline/scripts/_code-graph.mjs +2 -2
  163. package/pipeline/scripts/_run-paths.mjs +372 -0
  164. package/pipeline/scripts/_smoke-root.sh +1 -1
  165. package/pipeline/scripts/aggregate-metrics.mjs +65 -65
  166. package/pipeline/scripts/autopilot-arming.mjs +2 -1
  167. package/pipeline/scripts/autopilot-intake.mjs +2 -1
  168. package/pipeline/scripts/autopilot-runner.mjs +206 -2
  169. package/pipeline/scripts/build-references.mjs +2 -1
  170. package/pipeline/scripts/build-stack-plugins.mjs +10 -2
  171. package/pipeline/scripts/capture-evidence.sh +7 -2
  172. package/pipeline/scripts/capture-flush.sh +8 -8
  173. package/pipeline/scripts/capture-resume.sh +3 -3
  174. package/pipeline/scripts/classify-plan-safety.mjs +3 -2
  175. package/pipeline/scripts/cost-analyze.mjs +600 -0
  176. package/pipeline/scripts/cost-budget-check.mjs +4 -12
  177. package/pipeline/scripts/council-view.mjs +2 -1
  178. package/pipeline/scripts/crush-json.mjs +2 -1
  179. package/pipeline/scripts/diff-explain.mjs +7 -10
  180. package/pipeline/scripts/diff-risk-score.mjs +2 -1
  181. package/pipeline/scripts/doctor.mjs +140 -6
  182. package/pipeline/scripts/evidence-gate.mjs +9 -3
  183. package/pipeline/scripts/feedback-send.mjs +12 -2
  184. package/pipeline/scripts/gc-abandoned.sh +32 -16
  185. package/pipeline/scripts/gc-tmp.sh +1 -1
  186. package/pipeline/scripts/gc-worktrees.sh +12 -5
  187. package/pipeline/scripts/gen-facts.mjs +175 -0
  188. package/pipeline/scripts/gen-mode-dispatch.mjs +32 -37
  189. package/pipeline/scripts/gen-ref-toc.mjs +1 -1
  190. package/pipeline/scripts/github-ssh-setup.sh +64 -7
  191. package/pipeline/scripts/graph-mermaid.mjs +4 -2
  192. package/pipeline/scripts/graph-report.mjs +1 -1
  193. package/pipeline/scripts/jira-attach.sh +1 -1
  194. package/pipeline/scripts/keychain-save.sh +101 -30
  195. package/pipeline/scripts/learn-from-transcripts.mjs +3 -2
  196. package/pipeline/scripts/learning-curve.mjs +36 -31
  197. package/pipeline/scripts/log-metric.sh +17 -4
  198. package/pipeline/scripts/make-manifest.mjs +199 -0
  199. package/pipeline/scripts/memory-save.sh +1 -1
  200. package/pipeline/scripts/migrate-prefs.mjs +24 -6
  201. package/pipeline/scripts/migrate-state.mjs +94 -4
  202. package/pipeline/scripts/phase-banner.sh +26 -22
  203. package/pipeline/scripts/phase-tracker.sh +48 -10
  204. package/pipeline/scripts/plan-coverage-gate.mjs +8 -4
  205. package/pipeline/scripts/pre-commit-check.sh +7 -0
  206. package/pipeline/scripts/pre-push-check.sh +7 -0
  207. package/pipeline/scripts/purge.sh +23 -6
  208. package/pipeline/scripts/render-agent-log-cost.sh +10 -3
  209. package/pipeline/scripts/render-cost-summary.sh +9 -2
  210. package/pipeline/scripts/render-work-summary.sh +14 -7
  211. package/pipeline/scripts/review-file-filter.mjs +5 -3
  212. package/pipeline/scripts/review-scope.mjs +2 -1
  213. package/pipeline/scripts/routine-registry.mjs +2 -1
  214. package/pipeline/scripts/run-aggregator.mjs +26 -20
  215. package/pipeline/scripts/run-metrics.mjs +4 -2
  216. package/pipeline/scripts/runs-index.mjs +353 -0
  217. package/pipeline/scripts/scorecard-snapshot.mjs +178 -0
  218. package/pipeline/scripts/search-logs.sh +18 -0
  219. package/pipeline/scripts/smoke-cross-cli-behavior.sh +6 -6
  220. package/pipeline/scripts/smoke-schema-validation.sh +26 -7
  221. package/pipeline/scripts/test-gap-scan.mjs +2 -1
  222. package/pipeline/scripts/test-integrity-gate.mjs +2 -1
  223. package/pipeline/scripts/token-budget-report.mjs +13 -2
  224. package/pipeline/scripts/triage-memory.mjs +2 -2
  225. package/pipeline/scripts/update-issue-progress.sh +56 -7
  226. package/pipeline/scripts/usage-report.mjs +12 -1
  227. package/pipeline/scripts/validate-analysis-doc.mjs +75 -18
  228. package/pipeline/scripts/validate-code-graph.mjs +6 -3
  229. package/pipeline/scripts/validate-complaint-doc.mjs +2 -1
  230. package/pipeline/scripts/validate-diff-risk.mjs +6 -3
  231. package/pipeline/scripts/validate-planning.mjs +1 -1
  232. package/pipeline/scripts/validate-reviewer.mjs +1 -1
  233. package/pipeline/scripts/validate-state.mjs +45 -5
  234. package/pipeline/scripts/validate-test-gap.mjs +6 -3
  235. package/pipeline/scripts/validate-triage.mjs +6 -4
  236. package/pipeline/scripts/verify-citations.mjs +4 -2
  237. package/pipeline/scripts/verify.mjs +327 -0
  238. package/pipeline/scripts/worktree-finalize.sh +18 -9
  239. package/pipeline/scripts/write-state.mjs +154 -15
  240. package/pipeline/skills/.skill-manifest.json +37 -21
  241. package/pipeline/skills/.skills-index.json +104 -5
  242. package/pipeline/skills/shared/README.md +15 -6
  243. package/pipeline/skills/shared/core/apple-archive-compliance/SKILL.md +2 -2
  244. package/pipeline/skills/shared/core/google-play-compliance/SKILL.md +2 -2
  245. package/pipeline/skills/shared/core/multi-agent/SKILL.md +69 -71
  246. package/pipeline/skills/shared/core/multi-agent-autopilot/SKILL.md +3 -3
  247. package/pipeline/skills/shared/core/multi-agent-channels/SKILL.md +14 -14
  248. package/pipeline/skills/shared/core/multi-agent-diff-explain/SKILL.md +5 -5
  249. package/pipeline/skills/shared/core/multi-agent-graph/SKILL.md +1 -1
  250. package/pipeline/skills/shared/core/multi-agent-help/SKILL.md +25 -23
  251. package/pipeline/skills/shared/core/multi-agent-language/SKILL.md +2 -2
  252. package/pipeline/skills/shared/core/multi-agent-local/SKILL.md +2 -2
  253. package/pipeline/skills/shared/core/multi-agent-local-autopilot/SKILL.md +8 -8
  254. package/pipeline/skills/shared/core/multi-agent-manual-test/SKILL.md +6 -6
  255. package/pipeline/skills/shared/core/multi-agent-model/SKILL.md +71 -0
  256. package/pipeline/skills/shared/core/multi-agent-refactor/SKILL.md +3 -3
  257. package/pipeline/skills/shared/core/multi-agent-resume/SKILL.md +1 -1
  258. package/pipeline/skills/shared/core/multi-agent-resume-local/SKILL.md +7 -7
  259. package/pipeline/skills/shared/core/multi-agent-route-off/SKILL.md +39 -0
  260. package/pipeline/skills/shared/core/multi-agent-route-on/SKILL.md +76 -0
  261. package/pipeline/skills/shared/core/multi-agent-route-status/SKILL.md +59 -0
  262. package/pipeline/skills/shared/core/multi-agent-setup/SKILL.md +1 -1
  263. package/pipeline/skills/shared/core/multi-agent-status/SKILL.md +35 -11
  264. package/pipeline/skills/shared/core/multi-agent-steer/SKILL.md +2 -2
  265. package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +6 -5
  266. package/pipeline/skills/shared/external/macos-spm-app-packaging/assets/templates/package_app.sh +4 -1
  267. package/pipeline/skills/shared/external/macos-spm-app-packaging/assets/templates/setup_dev_signing.sh +4 -1
  268. package/pipeline/skills/shared/external/macos-spm-app-packaging/assets/templates/sign-and-notarize.sh +2 -1
  269. package/pipeline/skills/skills-index.md +13 -4
  270. package/pipeline/multi-agent-refs/phases/phase-1-analysis.md +0 -263
  271. package/pipeline/multi-agent-refs/phases/phase-2-planning.md +0 -344
  272. 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 4 triage output to the actual git diff. For each accepted /
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 bare = String(id).replace(/^#/, "");
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(`no agent-state.json for task ${id} under ${logRoot} (tried ${candidates.join(", ")})`);
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 6 never reaches that step and nothing else collects it: the
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 6 are never collected`,
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) => a.startsWith("--") && !/^--(probe|json|explain|list-checks|task-tools=)/.test(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
- console.log(JSON.stringify({ ok: false, code: 1, reason: msg }));
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
- console.log(JSON.stringify({ ok: true, code: 0, reason: msg }));
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
- process.exit(0);
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 6 or 7, or
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 6 uses when it removes a finished worktree.
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 -uo pipefail
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
- status=$(jq -r '.status // ""' "$state" 2>/dev/null)
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
- real=$(cd "$wt" 2>/dev/null && pwd -P)
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
- mkdir -p "$run_dir/artifacts" 2>/dev/null
191
- cp "$state" "$run_dir/artifacts/agent-state.json" 2>/dev/null
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
- git -C "$repo" worktree prune >/dev/null 2>&1
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
- 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
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-6/7 test is a proxy for "still waiting", and a
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
- git -C "$repo" worktree prune >/dev/null 2>&1
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-6-commit.md, generate-issue.md).
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 6 (worktree-finalize, v14.1.0+); killing one is
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 -uo pipefail
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
- REPO="$(cd "$REPO" && pwd -P)"
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
- dir="$(cd "$d" && pwd -P)"
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=$(du -sh "$dir" 2>/dev/null | cut -f1)
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