@mmerterden/multi-agent-pipeline 17.6.0 → 18.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 (103) hide show
  1. package/CHANGELOG.md +127 -0
  2. package/README.md +43 -1
  3. package/README.tr.md +41 -0
  4. package/docs/adr/0011-dormant-ci.md +25 -1
  5. package/docs/server-readiness.md +188 -0
  6. package/index.js +16 -1
  7. package/install/_common.mjs +42 -17
  8. package/install/_dev-only-files.mjs +8 -0
  9. package/install/_unattended-profile.mjs +113 -0
  10. package/install/index.mjs +48 -0
  11. package/manifest.json +1049 -0
  12. package/package.json +5 -2
  13. package/pipeline/commands/multi-agent/status/SKILL.md +52 -21
  14. package/pipeline/lib/_jira-auth.sh +8 -0
  15. package/pipeline/lib/analysis-jira-write.sh +32 -0
  16. package/pipeline/lib/ask-choice.sh +13 -2
  17. package/pipeline/lib/autopilot-state.sh +8 -0
  18. package/pipeline/lib/fatal.mjs +129 -0
  19. package/pipeline/lib/figma-mcp-refresh.sh +18 -0
  20. package/pipeline/lib/figma-screenshot.sh +18 -0
  21. package/pipeline/lib/invoked-directly.mjs +43 -0
  22. package/pipeline/lib/jira-publish.sh +42 -0
  23. package/pipeline/lib/md2confluence-v3.py +47 -0
  24. package/pipeline/lib/outbound-gate.mjs +175 -0
  25. package/pipeline/lib/plan-todos.sh +27 -6
  26. package/pipeline/lib/post-pr-review.sh +77 -8
  27. package/pipeline/lib/repo-hygiene.sh +8 -3
  28. package/pipeline/lib/require-jq.sh +40 -0
  29. package/pipeline/lib/run-paths.sh +335 -0
  30. package/pipeline/multi-agent-refs/features/autopilot-circuit-breaker.md +70 -0
  31. package/pipeline/multi-agent-refs/features/cost-analysis.md +93 -0
  32. package/pipeline/multi-agent-refs/features/doctor.md +45 -0
  33. package/pipeline/multi-agent-refs/features/verify.md +83 -0
  34. package/pipeline/multi-agent-refs/phases/operations.md +13 -2
  35. package/pipeline/multi-agent-refs/phases/phase-0-init.md +1 -1
  36. package/pipeline/multi-agent-refs/unattended-contract.md +129 -0
  37. package/pipeline/scripts/_run-paths.mjs +372 -0
  38. package/pipeline/scripts/aggregate-metrics.mjs +64 -64
  39. package/pipeline/scripts/autopilot-arming.mjs +2 -1
  40. package/pipeline/scripts/autopilot-intake.mjs +2 -1
  41. package/pipeline/scripts/autopilot-runner.mjs +206 -2
  42. package/pipeline/scripts/build-references.mjs +2 -1
  43. package/pipeline/scripts/build-stack-plugins.mjs +10 -2
  44. package/pipeline/scripts/capture-evidence.sh +7 -2
  45. package/pipeline/scripts/classify-plan-safety.mjs +2 -1
  46. package/pipeline/scripts/cost-analyze.mjs +600 -0
  47. package/pipeline/scripts/cost-budget-check.mjs +4 -12
  48. package/pipeline/scripts/council-view.mjs +2 -1
  49. package/pipeline/scripts/crush-json.mjs +2 -1
  50. package/pipeline/scripts/diff-explain.mjs +6 -9
  51. package/pipeline/scripts/diff-risk-score.mjs +2 -1
  52. package/pipeline/scripts/doctor.mjs +138 -4
  53. package/pipeline/scripts/evidence-gate.mjs +9 -3
  54. package/pipeline/scripts/feedback-send.mjs +12 -2
  55. package/pipeline/scripts/gc-abandoned.sh +29 -13
  56. package/pipeline/scripts/gc-worktrees.sh +11 -4
  57. package/pipeline/scripts/github-ssh-setup.sh +64 -7
  58. package/pipeline/scripts/graph-mermaid.mjs +4 -2
  59. package/pipeline/scripts/keychain-save.sh +101 -30
  60. package/pipeline/scripts/learn-from-transcripts.mjs +2 -1
  61. package/pipeline/scripts/learning-curve.mjs +34 -29
  62. package/pipeline/scripts/make-manifest.mjs +199 -0
  63. package/pipeline/scripts/migrate-prefs.mjs +2 -1
  64. package/pipeline/scripts/migrate-state.mjs +94 -4
  65. package/pipeline/scripts/phase-banner.sh +6 -2
  66. package/pipeline/scripts/phase-tracker.sh +41 -3
  67. package/pipeline/scripts/plan-coverage-gate.mjs +6 -2
  68. package/pipeline/scripts/pre-commit-check.sh +7 -0
  69. package/pipeline/scripts/pre-push-check.sh +7 -0
  70. package/pipeline/scripts/purge.sh +23 -6
  71. package/pipeline/scripts/render-agent-log-cost.sh +9 -2
  72. package/pipeline/scripts/render-cost-summary.sh +9 -2
  73. package/pipeline/scripts/render-work-summary.sh +11 -4
  74. package/pipeline/scripts/review-file-filter.mjs +4 -2
  75. package/pipeline/scripts/review-scope.mjs +2 -1
  76. package/pipeline/scripts/routine-registry.mjs +2 -1
  77. package/pipeline/scripts/run-aggregator.mjs +13 -14
  78. package/pipeline/scripts/run-metrics.mjs +3 -1
  79. package/pipeline/scripts/runs-index.mjs +343 -0
  80. package/pipeline/scripts/scorecard-snapshot.mjs +178 -0
  81. package/pipeline/scripts/search-logs.sh +18 -0
  82. package/pipeline/scripts/test-gap-scan.mjs +2 -1
  83. package/pipeline/scripts/test-integrity-gate.mjs +2 -1
  84. package/pipeline/scripts/update-issue-progress.sh +56 -7
  85. package/pipeline/scripts/usage-report.mjs +12 -1
  86. package/pipeline/scripts/validate-analysis-doc.mjs +2 -1
  87. package/pipeline/scripts/validate-code-graph.mjs +6 -3
  88. package/pipeline/scripts/validate-complaint-doc.mjs +2 -1
  89. package/pipeline/scripts/validate-diff-risk.mjs +6 -3
  90. package/pipeline/scripts/validate-test-gap.mjs +6 -3
  91. package/pipeline/scripts/validate-triage.mjs +3 -1
  92. package/pipeline/scripts/verify-citations.mjs +4 -2
  93. package/pipeline/scripts/verify.mjs +327 -0
  94. package/pipeline/scripts/worktree-finalize.sh +13 -4
  95. package/pipeline/scripts/write-state.mjs +154 -15
  96. package/pipeline/skills/.skill-manifest.json +2 -2
  97. package/pipeline/skills/.skills-index.json +56 -1
  98. package/pipeline/skills/shared/README.md +8 -3
  99. package/pipeline/skills/shared/core/multi-agent-status/SKILL.md +33 -9
  100. package/pipeline/skills/shared/external/macos-spm-app-packaging/assets/templates/package_app.sh +4 -1
  101. package/pipeline/skills/shared/external/macos-spm-app-packaging/assets/templates/setup_dev_signing.sh +4 -1
  102. package/pipeline/skills/shared/external/macos-spm-app-packaging/assets/templates/sign-and-notarize.sh +2 -1
  103. package/pipeline/skills/skills-index.md +6 -1
@@ -12,6 +12,10 @@
12
12
  # $HOME/.claude/logs/multi-agent/<task_id>/tracker-state.json
13
13
  # (override with $TRACKER_FILE env var)
14
14
  #
15
+ # `init` writes that flat path. READS go through pipeline/lib/run-paths.sh,
16
+ # which also finds a tracker nested under <project>/<task_id>/ - the multi-repo
17
+ # path writes there, and a hard-coded flat lookup used to miss those runs.
18
+ #
15
19
  # Commands:
16
20
  # init <task_id> Reset tracker for this task
17
21
  # add <phase_id> "<name>" Register a phase (status=pending); idempotent - existing id is a no-op
@@ -52,6 +56,12 @@
52
56
 
53
57
  set -uo pipefail
54
58
 
59
+ # Run-state path resolution: pipeline/lib/run-paths.sh owns both layouts.
60
+ _MA_RP_HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
61
+ # shellcheck source=/dev/null
62
+ . "$_MA_RP_HERE/../lib/run-paths.sh" 2>/dev/null || . "$HOME/.claude/lib/run-paths.sh"
63
+
64
+
55
65
  if [ "$#" -lt 1 ]; then
56
66
  cat >&2 <<USAGE
57
67
  usage:
@@ -110,7 +120,13 @@ if [ -z "$TRACKER_FILE" ]; then
110
120
  exit 64
111
121
  fi
112
122
  else
113
- TRACKER_FILE="$HOME/.claude/logs/multi-agent/${TASK_ID_FROM_ENV}/tracker-state.json"
123
+ # Resolve through run-paths.sh: this script writes flat, but 13 of the
124
+ # trackers on a real machine sit nested under a project directory (written
125
+ # by the multi-repo path), and the hard-coded flat path could not see them.
126
+ # A run that does not exist yet resolves to the flat path, which is exactly
127
+ # where `init` will create it - so new-run behaviour is unchanged.
128
+ TRACKER_FILE="$(ma_resolve_run_file "$TASK_ID_FROM_ENV" tracker-state.json 2>/dev/null \
129
+ || echo "$HOME/.claude/logs/multi-agent/${TASK_ID_FROM_ENV}/tracker-state.json")"
114
130
  fi
115
131
  fi
116
132
  TRACKER_DIR="$(dirname "$TRACKER_FILE")"
@@ -430,7 +446,16 @@ save_state() {
430
446
  # corrupt document (render exits 65). $$ keeps each writer's temp private; the
431
447
  # rename is still atomic, so the last full write wins instead of a torn one.
432
448
  local tmp="${TRACKER_FILE}.tmp.$$"
433
- printf '%s\n' "$1" > "$tmp"
449
+ local doc="$1"
450
+ # Stamp the unlocked-write count, if there were any. jq is guarded because a
451
+ # machine without it must still get its state written - losing the annotation
452
+ # is acceptable, losing the write is not.
453
+ if [ "${TRACKER_LOCK_LOST:-0}" -gt 0 ] && command -v jq >/dev/null 2>&1; then
454
+ doc=$(printf '%s' "$doc" | jq --argjson n "$TRACKER_LOCK_LOST" \
455
+ '.unlockedWrites = ((.unlockedWrites // 0) + $n)' 2>/dev/null || printf '%s' "$doc")
456
+ TRACKER_LOCK_LOST=0
457
+ fi
458
+ printf '%s\n' "$doc" > "$tmp"
434
459
  mv "$tmp" "$TRACKER_FILE"
435
460
  }
436
461
 
@@ -444,6 +469,9 @@ save_state() {
444
469
  # reclaimed.
445
470
  TRACKER_LOCK_DIR=""
446
471
  TRACKER_LOCK_HELD=0
472
+ # How many writes went through WITHOUT the lock in this process. Stamped into
473
+ # the state so a run's own record says its counts may be low.
474
+ TRACKER_LOCK_LOST=0
447
475
 
448
476
  state_lock_stale() {
449
477
  local pid mtime now age
@@ -482,7 +510,17 @@ acquire_state_lock() {
482
510
  fi
483
511
  tries=$((tries + 1))
484
512
  if [ "$tries" -ge 50 ]; then
485
- echo "phase-tracker: WARN - state lock busy ($TRACKER_LOCK_DIR); proceeding without lock" >&2
513
+ # STILL fails open, and that is the right call: a lost tracker write is
514
+ # worse than a lost lock, and a hung pipeline is worse than both.
515
+ #
516
+ # What changes is that it stops being invisible. Two writers in the
517
+ # critical section means one of their token deltas is dropped, and the
518
+ # only trace was a warning on stderr inside a background run nobody
519
+ # reads. The count now lands in the state file, so the run CARRIES the
520
+ # evidence that its numbers are low - which is the difference between an
521
+ # undercount and an undercount you can see.
522
+ TRACKER_LOCK_LOST=$((TRACKER_LOCK_LOST + 1))
523
+ echo "phase-tracker: WARN - state lock busy ($TRACKER_LOCK_DIR); proceeding without it. Token counts for this write may be low; recorded as unlockedWrites in the state." >&2
486
524
  return 0
487
525
  fi
488
526
  sleep 0.1
@@ -29,7 +29,7 @@
29
29
  //
30
30
  // Exit codes: 0 clean, 1 coverage gap, 2 usage / parse error.
31
31
 
32
- import { readFileSync, existsSync } from "node:fs";
32
+ import { readFileSync, existsSync, writeFileSync } from "node:fs";
33
33
  import { join, isAbsolute } from "node:path";
34
34
  import { pathToFileURL } from "node:url";
35
35
 
@@ -157,8 +157,12 @@ if (isMain) {
157
157
  const msg = Array.isArray(todos)
158
158
  ? "the plan has zero steps - that is an unusable plan, not a clean one; skip this gate explicitly for modes with no Phase 2"
159
159
  : "no plan todos found - if this mode has no Phase 2, skip this gate explicitly";
160
+ // fd 1 synchronously, then exit. This sits in `if (isMain)` at module scope,
161
+ // not in a function, so there is nothing to return from - and a
162
+ // process.stdout.write followed by process.exit() loses the verdict when
163
+ // stdout is a pipe.
160
164
  if (asJson)
161
- process.stdout.write(`${JSON.stringify({ verdict: "unusable", reason: msg }, null, 2)}\n`);
165
+ writeFileSync(1, `${JSON.stringify({ verdict: "unusable", reason: msg }, null, 2)}\n`);
162
166
  else process.stderr.write(`plan-coverage-gate: ${msg}\n`);
163
167
  process.exit(2);
164
168
  }
@@ -4,6 +4,13 @@
4
4
  # Exit 0 = clean, Exit 2 = secrets found. The PreToolUse hook contract
5
5
  # (install/templates/claude-hooks.json, agent-guard.sh) blocks the tool call
6
6
  # only on exit 2, with the reason on stderr.
7
+ #
8
+ # No `-e` here either, and for a sharper reason than in pre-push-check.sh: this
9
+ # file is a PreToolUse hook that fires on every Bash call, and it is built out
10
+ # of greps that are SUPPOSED to find nothing. Under `-e` the first clean
11
+ # detector would end the scan, and a hook that exits early reports "no secrets"
12
+ # for a file it never finished reading - a scanner failing open, on the path
13
+ # that exists to keep secrets out of commits.
7
14
 
8
15
  set -uo pipefail
9
16
 
@@ -43,6 +43,13 @@
43
43
  # 0 - all gates passed, safe to push
44
44
  # 1 - a gate failed (test, lint, schema, or personal-data leak)
45
45
  # 2 - environment problem (node missing, npm missing, wrong cwd)
46
+ #
47
+ # No `-e`, and that is a decision rather than an omission. This file RUNS the
48
+ # gates and counts how many failed; `-e` would abort at the first one, so a
49
+ # tree with three problems would report one, get fixed, report the next, and
50
+ # take three full runs to learn what one run already knew. The four destructive
51
+ # scripts in this directory DO use `-e`, because there the next command deletes
52
+ # something. Here the next command is another question.
46
53
 
47
54
  set -uo pipefail
48
55
 
@@ -33,7 +33,7 @@
33
33
  # Exit: 0 on success (including "nothing to do"), 2 on usage error or refused
34
34
  # repo.
35
35
 
36
- set -uo pipefail
36
+ set -euo pipefail
37
37
 
38
38
  REPO="$PWD"
39
39
  DELETE=0
@@ -66,7 +66,11 @@ fi
66
66
  # Anchor everything on the MAIN worktree (first porcelain entry), so running
67
67
  # purge from inside a linked worktree still targets <repo>/.worktrees/.
68
68
  MAIN_WT="$(git -C "$REPO" worktree list --porcelain 2>/dev/null | sed -n 's/^worktree //p' | head -1)"
69
- MAIN_WT="$(cd "$MAIN_WT" 2>/dev/null && pwd -P)"
69
+ # `|| true` on the resolve, not on the cd: an assignment takes the exit status
70
+ # of its command substitution, so under `set -e` a MAIN_WT that no longer exists
71
+ # would kill the script HERE - three lines before the guard whose whole job is
72
+ # to report exactly that, with a message, as exit 2.
73
+ MAIN_WT="$(cd "$MAIN_WT" 2>/dev/null && pwd -P || true)"
70
74
  HOME_REAL="$(cd "$HOME" 2>/dev/null && pwd -P || printf '%s' "$HOME")"
71
75
  if [ -z "$MAIN_WT" ] || [ "$MAIN_WT" = "/" ] || [ "$MAIN_WT" = "$HOME_REAL" ]; then
72
76
  echo "purge: refusing to operate on repo root '$MAIN_WT'" >&2
@@ -96,7 +100,11 @@ cur_branch=""
96
100
  flush_block() {
97
101
  [ -z "$cur_path" ] && { cur_branch=""; return 0; }
98
102
  local resolved
99
- resolved="$(resolve_dir "$cur_path")"
103
+ # A registered worktree whose directory is GONE is the ordinary case this
104
+ # function exists to handle - `git worktree list` still lists it, and purge is
105
+ # what cleans it up. resolve_dir fails there, and an unforgiven assignment
106
+ # would end the teardown with exit 1 and no output at all.
107
+ resolved="$(resolve_dir "$cur_path" || true)"
100
108
  case "$resolved" in
101
109
  "$WT_ROOT"/*)
102
110
  wt_paths+=("$resolved")
@@ -184,7 +192,12 @@ while [ "$i" -lt "${#wt_paths[@]}" ]; do
184
192
  case "$p" in
185
193
  "$WT_ROOT"/*)
186
194
  rm -rf "$p" 2>/dev/null && wt_gone=1
187
- git -C "$MAIN_WT" worktree prune >/dev/null 2>&1
195
+ # `|| true` because this branch only runs when `worktree remove --force`
196
+ # ALREADY failed - a locked or damaged worktree - which is exactly when
197
+ # the follow-up prune is likeliest to fail too. Under `set -e` that would
198
+ # abort the teardown mid-loop, leaving the remaining worktrees, the
199
+ # branches, the counter reset and the exclude-block removal undone.
200
+ git -C "$MAIN_WT" worktree prune >/dev/null 2>&1 || true
188
201
  ;;
189
202
  *) echo "purge: skipping worktree outside $WT_ROOT: $p" >&2 ;;
190
203
  esac
@@ -228,8 +241,12 @@ if [ -n "$COUNTER" ]; then
228
241
  rm -f "$COUNTER" 2>/dev/null && echo "→ reset task counter $COUNTER"
229
242
  fi
230
243
 
231
- # Drop the now-empty .worktrees/ shell (only when truly empty).
232
- [ -d "$WT_ROOT" ] && rmdir "$WT_ROOT" 2>/dev/null
244
+ # Drop the now-empty .worktrees/ shell (only when truly empty). The `|| true`
245
+ # is the point of the line, not noise on it: a symlink pointing out of the tree
246
+ # is deliberately NOT followed and NOT deleted, so the directory is routinely
247
+ # non-empty here, rmdir routinely fails, and under `set -e` that ordinary
248
+ # outcome would abort the teardown before the exclude block came out.
249
+ [ -d "$WT_ROOT" ] && { rmdir "$WT_ROOT" 2>/dev/null || true; }
233
250
 
234
251
  # purge is the whole-repo teardown, so it is the one place the managed exclude
235
252
  # block should come back out - a per-task finalize must not, because other tasks
@@ -19,6 +19,13 @@
19
19
 
20
20
  set -euo pipefail
21
21
 
22
+ # Run-state path resolution: pipeline/lib/run-paths.sh owns the two layouts
23
+ # (nested <root>/<project>/<id>/ and flat <root>/<id>/) and every id spelling.
24
+ _MA_RP_HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
25
+ # shellcheck source=/dev/null
26
+ . "$_MA_RP_HERE/../lib/run-paths.sh" 2>/dev/null || . "$HOME/.claude/lib/run-paths.sh"
27
+
28
+
22
29
  TASK_ID="${1:?usage: render-agent-log-cost.sh <task-id> [--otel-spans <path>]}"
23
30
  shift || true
24
31
 
@@ -50,8 +57,8 @@ for candidate in \
50
57
  "$PWD/.worktrees/$TASK_ID/phase-tracker.json" \
51
58
  "$PWD/.worktrees/$task_id_bare/phase-tracker.json" \
52
59
  "$PWD/.worktrees/task-$task_id_bare/phase-tracker.json" \
53
- "$HOME/.claude/logs/multi-agent/$TASK_ID/tracker-state.json" \
54
- "$HOME/.claude/logs/multi-agent/$task_id_bare/tracker-state.json"
60
+ "$(ma_resolve_run_file "$TASK_ID" tracker-state.json 2>/dev/null || echo /nonexistent)" \
61
+ "$(ma_resolve_run_file "$task_id_bare" tracker-state.json 2>/dev/null || echo /nonexistent)"
55
62
  do
56
63
  [ -f "$candidate" ] && { tracker_file="$candidate"; break; }
57
64
  done
@@ -13,6 +13,13 @@
13
13
 
14
14
  set -euo pipefail
15
15
 
16
+ # Run-state path resolution: pipeline/lib/run-paths.sh owns the two layouts
17
+ # (nested <root>/<project>/<id>/ and flat <root>/<id>/) and every id spelling.
18
+ _MA_RP_HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
19
+ # shellcheck source=/dev/null
20
+ . "$_MA_RP_HERE/../lib/run-paths.sh" 2>/dev/null || . "$HOME/.claude/lib/run-paths.sh"
21
+
22
+
16
23
  TASK_ID="${1:?usage: render-cost-summary.sh <task-id> [--otel-spans <path>]}"
17
24
  shift || true
18
25
 
@@ -50,8 +57,8 @@ for candidate in \
50
57
  "$PWD/.worktrees/$TASK_ID/phase-tracker.json" \
51
58
  "$PWD/.worktrees/$task_id_bare/phase-tracker.json" \
52
59
  "$PWD/.worktrees/task-$task_id_bare/phase-tracker.json" \
53
- "$HOME/.claude/logs/multi-agent/$TASK_ID/tracker-state.json" \
54
- "$HOME/.claude/logs/multi-agent/$task_id_bare/tracker-state.json"
60
+ "$(ma_resolve_run_file "$TASK_ID" tracker-state.json 2>/dev/null || echo /nonexistent)" \
61
+ "$(ma_resolve_run_file "$task_id_bare" tracker-state.json 2>/dev/null || echo /nonexistent)"
55
62
  do
56
63
  [ -f "$candidate" ] && { tracker_file="$candidate"; break; }
57
64
  done
@@ -40,6 +40,13 @@
40
40
 
41
41
  set -euo pipefail
42
42
 
43
+ # Run-state path resolution: pipeline/lib/run-paths.sh owns the two layouts
44
+ # (nested <root>/<project>/<id>/ and flat <root>/<id>/) and every id spelling.
45
+ _MA_RP_HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
46
+ # shellcheck source=/dev/null
47
+ . "$_MA_RP_HERE/../lib/run-paths.sh" 2>/dev/null || . "$HOME/.claude/lib/run-paths.sh"
48
+
49
+
43
50
  TASK_ID="${1:?usage: render-work-summary.sh <task-id>}"
44
51
  shift || true
45
52
 
@@ -85,11 +92,11 @@ fi
85
92
  # had a log-dir fallback all along; this is the same list.
86
93
  if [ -z "$STATE_FILE" ] || [ -z "$TRACKER_FILE" ]; then
87
94
  task_bare="${TASK_ID##*-}"
95
+ # The `*/` glob here saw only the nested layout, and an unmatched glob is a
96
+ # literal path in bash - so a flat-layout run fell through silently.
88
97
  for base in \
89
- "$HOME/.claude/logs/multi-agent/$TASK_ID/artifacts" \
90
- "$HOME/.claude/logs/multi-agent/$task_bare/artifacts" \
91
- "$HOME"/.claude/logs/multi-agent/*/"$TASK_ID"/artifacts \
92
- "$HOME"/.claude/logs/multi-agent/*/"$task_bare"/artifacts
98
+ "$(ma_resolve_run_dir_any "$TASK_ID" 2>/dev/null || echo /nonexistent)/artifacts" \
99
+ "$(ma_resolve_run_dir_any "$task_bare" 2>/dev/null || echo /nonexistent)/artifacts"
93
100
  do
94
101
  [ -d "$base" ] || continue
95
102
  [ -z "$STATE_FILE" ] && [ -f "$base/agent-state.json" ] && STATE_FILE="$base/agent-state.json"
@@ -44,6 +44,8 @@ import { readFileSync } from "node:fs";
44
44
  import { join } from "node:path";
45
45
  import { globToRegExp } from "./glob-match.mjs";
46
46
  import { unquotePathIfNeeded } from "./git-path.mjs";
47
+ import { runMain } from "../lib/fatal.mjs";
48
+ import { invokedDirectly } from "../lib/invoked-directly.mjs";
47
49
 
48
50
  const DEFAULT_PATTERNS = join(import.meta.dirname, "..", "schemas", "review-file-exclusions.json");
49
51
 
@@ -175,6 +177,6 @@ function main() {
175
177
  }
176
178
  }
177
179
 
178
- if (import.meta.url === `file://${process.argv[1]}`) {
179
- main();
180
+ if (invokedDirectly(import.meta.url)) {
181
+ runMain("review-file-filter", main);
180
182
  }
@@ -26,6 +26,7 @@
26
26
  * Exit 0 always. Invalid/empty input → fail SAFE to "full" (never under-review).
27
27
  */
28
28
  import { readFileSync } from "node:fs";
29
+ import { runMain } from "../lib/fatal.mjs";
29
30
 
30
31
  const args = process.argv.slice(2);
31
32
  const num = (flag, dflt) => {
@@ -97,4 +98,4 @@ function main() {
97
98
  emit({ scope: trivial ? "single" : "full", reason, churn, maxScore, blockers });
98
99
  }
99
100
 
100
- main();
101
+ runMain("review-scope", main);
@@ -27,6 +27,7 @@
27
27
 
28
28
  import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync, renameSync } from "node:fs";
29
29
  import { join, dirname } from "node:path";
30
+ import { runMain } from "../lib/fatal.mjs";
30
31
 
31
32
  const NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
32
33
  const MAX_NAME = 40;
@@ -218,4 +219,4 @@ function main() {
218
219
  }
219
220
  }
220
221
 
221
- main();
222
+ runMain("routine-registry", main);
@@ -38,6 +38,7 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs";
38
38
  import { join, dirname } from "node:path";
39
39
  import { fileURLToPath } from "node:url";
40
40
  import { costUsd } from "./_cost.mjs";
41
+ import { resolveRunFile, canonicalRunDir } from "./_run-paths.mjs";
41
42
 
42
43
  const __dirname = dirname(fileURLToPath(import.meta.url));
43
44
 
@@ -82,26 +83,24 @@ function die(msg) {
82
83
  process.exit(1);
83
84
  }
84
85
 
85
- // phase-tracker.sh writes tracker-state.json (never phase-tracker.json) at
86
- // $HOME/.claude/logs/multi-agent/<task-id>/ (no <project> segment) - the
87
- // same drift render-cost-summary.sh/render-agent-log-cost.sh already guard
88
- // against with a candidate list. Mirror that list here instead of assuming
89
- // one fixed filename/path.
86
+ // Path resolution lives in _run-paths.mjs: tracker-state.json is written to
87
+ // the flat <root>/<task-id>/ while agent-state.json documents the nested
88
+ // <root>/<project>/<task-id>/, and both layouts are populated in practice.
89
+ // This used to be a hand-rolled candidate list here, a second one in
90
+ // render-cost-summary.sh and a third in render-agent-log-cost.sh.
90
91
  function resolveTrackerCandidates() {
91
92
  const candidates = [];
92
93
  if (flags["task-dir"]) {
93
94
  candidates.push(join(flags["task-dir"], "tracker-state.json"));
94
95
  }
95
96
  if (flags["task-id"]) {
96
- const HOME = process.env.HOME || process.env.USERPROFILE;
97
- if (!HOME) die("neither HOME nor USERPROFILE is set - pass --task-dir");
98
- const taskId = String(flags["task-id"]);
99
- if (flags.project) {
100
- candidates.push(
101
- join(HOME, ".claude", "logs", "multi-agent", flags.project, taskId, "tracker-state.json"),
102
- );
103
- }
104
- candidates.push(join(HOME, ".claude", "logs", "multi-agent", taskId, "tracker-state.json"));
97
+ const found = resolveRunFile(flags["task-id"], "tracker-state.json", flags.project);
98
+ if (found) candidates.push(found);
99
+ // Keep the canonical path in the list even when nothing exists yet, so the
100
+ // "not found" message names a path instead of an empty candidate set.
101
+ candidates.push(
102
+ join(canonicalRunDir(String(flags["task-id"]), flags.project), "tracker-state.json"),
103
+ );
105
104
  }
106
105
  if (!candidates.length) die("either --task-dir or --task-id is required");
107
106
  return candidates;
@@ -215,5 +215,7 @@ const metrics = {
215
215
  },
216
216
  };
217
217
 
218
+ // No process.exit(0) here. Exit code 0 is already the default, and calling it
219
+ // would terminate before this write drained - stdout to a pipe is asynchronous,
220
+ // so `run-metrics.mjs | jq` would read a payload cut at a buffer boundary.
218
221
  console.log(JSON.stringify(metrics, null, 2));
219
- process.exit(0);