@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
@@ -2,55 +2,126 @@
2
2
 
3
3
  set -uo pipefail
4
4
 
5
- # Kullanım: ./keychain-save.sh <service-adı>
6
- # Örnek: ./keychain-save.sh github-pat
7
- # ./keychain-save.sh firebase-sa
5
+ # Kullanım (interaktif): ./keychain-save.sh <service-adı>
6
+ # Örnek: ./keychain-save.sh github-pat
7
+ # ./keychain-save.sh firebase-sa
8
+ #
9
+ # HEADLESS: terminal yokken script SORMAZ. Üç `read` koruma olmadan duruyordu
10
+ # ve bir launchd ajanının altında ilki sonsuza kadar bekler; kurulum "asılı
11
+ # kaldı" diye görünür.
12
+ #
13
+ # MULTI_AGENT_UNATTENDED=1 terminal olsa bile sorulmaz
14
+ # ./keychain-save.sh <ad> --stdin token stdin'den okunur (satır değil,
15
+ # tüm gövde - JSON çok satırlıdır)
16
+ # ./keychain-save.sh <ad> --json <yol> JSON dosyadan okunur
17
+ #
18
+ # Secret İÇİN env değişkeni bilerek YOK. Bir env değeri çocuk süreçlere
19
+ # miras kalır ve bazı sistemlerde `ps e` ile görünür; borudan okumak onu
20
+ # yalnız bu sürecin belleğinde tutar. Aynı sebeple argv'ye de hiç değmez.
8
21
 
9
22
  SERVICE_NAME="${1:-}"
23
+ MODE="${2:-}"
24
+ MODE_ARG="${3:-}"
25
+
26
+ usage() {
27
+ echo "Kullanım: $0 <service-adı> (interaktif)"
28
+ echo " $0 <service-adı> --stdin (token borudan)"
29
+ echo " $0 <service-adı> --json <yol> (JSON dosyadan)"
30
+ }
10
31
 
11
32
  if [ -z "$SERVICE_NAME" ]; then
12
- echo "Kullanım: $0 <service-adı>"
13
- echo "Örnek: $0 github-pat"
33
+ usage
14
34
  exit 1
15
35
  fi
16
36
 
17
- echo "Ne tür bir secret kaydedeceksin?"
18
- echo " 1) Personal Access Token / API Key"
19
- echo " 2) JSON dosyası (service account vb.)"
20
- read -rp "Seçim (1/2): " CHOICE
37
+ # Başsızken mod verilmediyse, soruyu sormak yerine reddet. Sessizce beklemek
38
+ # en kötü seçenek: dışarı hiçbir şey yazmaz ve kimse neyi beklediğini bilmez.
39
+ if [ -z "$MODE" ] && { [ ! -t 0 ] || [ "${MULTI_AGENT_UNATTENDED:-}" = "1" ]; }; then
40
+ # Sebebi doğru söyle. "terminal yok" derken terminalin VAR olması,
41
+ # okuyanı kendi kurulumunu yanlış yerde aramaya gönderir.
42
+ if [ "${MULTI_AGENT_UNATTENDED:-}" = "1" ]; then
43
+ echo "Hata: MULTI_AGENT_UNATTENDED=1 ve mod verilmedi." >&2
44
+ else
45
+ echo "Hata: terminal yok ve mod verilmedi." >&2
46
+ fi
47
+ usage >&2
48
+ exit 1
49
+ fi
50
+
51
+ read_json_file() {
52
+ local path="$1"
53
+ # Kullanıcının girdiği tırnak işaretlerini temizle
54
+ path="${path//\'/}"
55
+ path="${path//\"/}"
56
+ # Tilde (~) expand
57
+ path="${path/#\~/$HOME}"
58
+ if [ ! -f "$path" ]; then
59
+ echo "Hata: Dosya bulunamadı: $path" >&2
60
+ return 1
61
+ fi
62
+ # JSON olduğu gibi saklanır. Kimlik deposu çok satırlı değeri
63
+ # bayt bayt geri verir; base64 sarmalı okuma tarafında çözülmeyen
64
+ # bir katman ekliyordu.
65
+ cat "$path"
66
+ }
21
67
 
22
- case "$CHOICE" in
23
- 1)
24
- read -rsp "Token'ı yapıştır (gizli yazılır): " SECRET
25
- echo ""
68
+ case "$MODE" in
69
+ --stdin)
70
+ # `read` DEĞİL `cat`: `read` ilk satırda durur ve bir service-account
71
+ # JSON'unun geri kalanını sessizce atar.
72
+ SECRET=$(cat)
26
73
  if [ -z "$SECRET" ]; then
27
- echo "Hata: Token boş olamaz."
74
+ echo "Hata: stdin boş." >&2
28
75
  exit 1
29
76
  fi
30
77
  ;;
31
- 2)
32
- read -rp "JSON dosya yolunu gir: " JSON_PATH
33
- # Kullanıcının girdiği tırnak işaretlerini temizle
34
- JSON_PATH="${JSON_PATH//\'/}"
35
- JSON_PATH="${JSON_PATH//\"/}"
36
- # Tilde (~) expand
37
- JSON_PATH="${JSON_PATH/#\~/$HOME}"
38
- if [ ! -f "$JSON_PATH" ]; then
39
- echo "Hata: Dosya bulunamadı: $JSON_PATH"
78
+ --json)
79
+ if [ -z "$MODE_ARG" ]; then
80
+ echo "Hata: --json bir dosya yolu ister." >&2
81
+ usage >&2
40
82
  exit 1
41
83
  fi
42
- # JSON olduğu gibi saklanır. Kimlik deposu çok satırlı değeri
43
- # bayt bayt geri verir; base64 sarmalı okuma tarafında çözülmeyen
44
- # bir katman ekliyordu.
45
- SECRET=$(cat "$JSON_PATH")
84
+ SECRET=$(read_json_file "$MODE_ARG") || exit 1
46
85
  if [ -z "$SECRET" ]; then
47
- echo "Hata: Dosya okunamadı."
86
+ echo "Hata: Dosya okunamadı." >&2
48
87
  exit 1
49
88
  fi
50
- echo "JSON olduğu gibi saklanacak."
89
+ ;;
90
+ "")
91
+ echo "Ne tür bir secret kaydedeceksin?"
92
+ echo " 1) Personal Access Token / API Key"
93
+ echo " 2) JSON dosyası (service account vb.)"
94
+ # EOF (Ctrl-D) bir cevaptır: korumasız `read` onu boş bir seçime
95
+ # çevirip aşağıdaki "Geçersiz seçim"e düşürür, ki doğru sonuç.
96
+ read -rp "Seçim (1/2): " CHOICE || CHOICE=""
97
+
98
+ case "$CHOICE" in
99
+ 1)
100
+ read -rsp "Token'ı yapıştır (gizli yazılır): " SECRET || SECRET=""
101
+ echo ""
102
+ if [ -z "$SECRET" ]; then
103
+ echo "Hata: Token boş olamaz."
104
+ exit 1
105
+ fi
106
+ ;;
107
+ 2)
108
+ read -rp "JSON dosya yolunu gir: " JSON_PATH || JSON_PATH=""
109
+ SECRET=$(read_json_file "$JSON_PATH") || exit 1
110
+ if [ -z "$SECRET" ]; then
111
+ echo "Hata: Dosya okunamadı."
112
+ exit 1
113
+ fi
114
+ echo "JSON olduğu gibi saklanacak."
115
+ ;;
116
+ *)
117
+ echo "Geçersiz seçim."
118
+ exit 1
119
+ ;;
120
+ esac
51
121
  ;;
52
122
  *)
53
- echo "Geçersiz seçim."
123
+ echo "Hata: bilinmeyen mod: $MODE" >&2
124
+ usage >&2
54
125
  exit 1
55
126
  ;;
56
127
  esac
@@ -66,6 +66,7 @@ import { execFileSync } from "node:child_process";
66
66
  import { basename, dirname, join } from "node:path";
67
67
  import { homedir, tmpdir } from "node:os";
68
68
  import { fileURLToPath } from "node:url";
69
+ import { runMain } from "../lib/fatal.mjs";
69
70
 
70
71
  const HERE = dirname(fileURLToPath(import.meta.url));
71
72
  const argv = process.argv.slice(2);
@@ -622,4 +623,4 @@ function main() {
622
623
  }
623
624
  }
624
625
 
625
- main();
626
+ runMain("learn-from-transcripts", main);
@@ -19,7 +19,7 @@
19
19
  //
20
20
  // Exit: 0 always (advisory; empty/missing metrics -> friendly note).
21
21
 
22
- import { existsSync, readFileSync } from "node:fs";
22
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
23
23
  import { homedir } from "node:os";
24
24
  import { join } from "node:path";
25
25
 
@@ -39,8 +39,11 @@ for (const a of args) {
39
39
  const METRICS_FILE =
40
40
  process.env.METRICS_FILE || join(homedir(), ".claude", "logs", "multi-agent", "metrics.jsonl");
41
41
 
42
+ // Called from module scope and has to stop the script, so the write goes to
43
+ // fd 1 synchronously: console.log would still be in the pipe when process.exit
44
+ // tore the process down.
42
45
  function emitEmpty(reason) {
43
- if (opts.json) console.log(JSON.stringify({ ok: false, reason, buckets: [] }));
46
+ if (opts.json) writeFileSync(1, `${JSON.stringify({ ok: false, reason, buckets: [] })}\n`);
44
47
  else console.error(`learning-curve: ${reason}`);
45
48
  process.exit(0);
46
49
  }
@@ -183,35 +186,37 @@ if (buckets.length >= 2) {
183
186
 
184
187
  const out = { ok: true, bucketDays: opts.bucketDays, buckets, trend };
185
188
 
189
+ // else, not process.exit(0) after the write: stdout to a pipe is asynchronous
190
+ // and exiting drops whatever has not drained, so the JSON arrived truncated to
191
+ // anything reading it through a pipe.
186
192
  if (opts.json) {
187
193
  console.log(JSON.stringify(out, null, 2));
188
- process.exit(0);
189
- }
190
-
191
- // Markdown / text (default)
192
- const arrow = (v, goodIsUp) => {
193
- if (v == null || v === 0) return "-";
194
- const up = v > 0;
195
- const good = up === goodIsUp;
196
- return `${up ? "+" : ""}${v} ${good ? "(better)" : "(worse)"}`;
197
- };
198
- console.log(`# Learning curve (${opts.bucketDays}-day buckets)\n`);
199
- console.log(
200
- "| Bucket | Tasks | First-pass clean | Avg review cycles | Rework/task | Tokens/task | Cache ratio | Memory precision | Mined lessons/task | Offload bytes saved/task |",
201
- );
202
- console.log("|---|---|---|---|---|---|---|---|---|---|");
203
- for (const b of buckets) {
194
+ } else {
195
+ // Markdown / text (default)
196
+ const arrow = (v, goodIsUp) => {
197
+ if (v == null || v === 0) return "-";
198
+ const up = v > 0;
199
+ const good = up === goodIsUp;
200
+ return `${up ? "+" : ""}${v} ${good ? "(better)" : "(worse)"}`;
201
+ };
202
+ console.log(`# Learning curve (${opts.bucketDays}-day buckets)\n`);
204
203
  console.log(
205
- `| ${b.bucketStart} | ${b.tasks} | ${b.firstPassCleanRate ?? "-"} | ${b.avgReviewCycles ?? "-"} | ${b.reworkPerTask} | ${b.tokensPerTask} | ${b.cacheRatio ?? "-"} | ${b.memoryPrecision ?? "-"} | ${b.minedLessonsPerTask ?? "-"} | ${b.offloadBytesSavedPerTask ?? "-"} |`,
204
+ "| Bucket | Tasks | First-pass clean | Avg review cycles | Rework/task | Tokens/task | Cache ratio | Memory precision | Mined lessons/task | Offload bytes saved/task |",
206
205
  );
207
- }
208
- if (trend) {
209
- console.log(`\n**Trend ${trend.from} -> ${trend.to}:**`);
210
- console.log(`- First-pass clean: ${arrow(trend.firstPassCleanRate, true)}`);
211
- console.log(`- Avg review cycles: ${arrow(trend.avgReviewCycles, false)}`);
212
- console.log(`- Tokens/task: ${arrow(trend.tokensPerTask, false)}`);
213
- console.log(`- Cache ratio: ${arrow(trend.cacheRatio, true)}`);
214
- console.log(`- Memory precision: ${arrow(trend.memoryPrecision, true)}`);
215
- } else {
216
- console.log("\n(need >= 2 buckets for a trend; keep running the pipeline)");
206
+ console.log("|---|---|---|---|---|---|---|---|---|---|");
207
+ for (const b of buckets) {
208
+ console.log(
209
+ `| ${b.bucketStart} | ${b.tasks} | ${b.firstPassCleanRate ?? "-"} | ${b.avgReviewCycles ?? "-"} | ${b.reworkPerTask} | ${b.tokensPerTask} | ${b.cacheRatio ?? "-"} | ${b.memoryPrecision ?? "-"} | ${b.minedLessonsPerTask ?? "-"} | ${b.offloadBytesSavedPerTask ?? "-"} |`,
210
+ );
211
+ }
212
+ if (trend) {
213
+ console.log(`\n**Trend ${trend.from} -> ${trend.to}:**`);
214
+ console.log(`- First-pass clean: ${arrow(trend.firstPassCleanRate, true)}`);
215
+ console.log(`- Avg review cycles: ${arrow(trend.avgReviewCycles, false)}`);
216
+ console.log(`- Tokens/task: ${arrow(trend.tokensPerTask, false)}`);
217
+ console.log(`- Cache ratio: ${arrow(trend.cacheRatio, true)}`);
218
+ console.log(`- Memory precision: ${arrow(trend.memoryPrecision, true)}`);
219
+ } else {
220
+ console.log("\n(need >= 2 buckets for a trend; keep running the pipeline)");
221
+ }
217
222
  }
@@ -0,0 +1,199 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * make-manifest.mjs - a digest of everything the package ships.
4
+ *
5
+ * "Is my install the one that was published, or has something edited it" is a
6
+ * question this tree could not answer. The install is a COPY: `install.js`
7
+ * writes into ~/.claude, ~/.copilot and ~/.codex, and from that moment the two
8
+ * halves drift independently. An edit made in the installed copy looks like a
9
+ * feature nobody can find the source of; a file the installer failed to write
10
+ * looks like a bug in a script that was never there.
11
+ *
12
+ * The manifest turns both into one command. It records a SHA-256 for every
13
+ * file the package ships, the version, and the commit it was built from, and
14
+ * `multi-agent-pipeline verify` compares an installed tree against it, file by
15
+ * file.
16
+ *
17
+ * The file set is not guessed: it comes from `npm pack --dry-run --json`, so
18
+ * it is by construction the same set npm will publish, `files` globs and all.
19
+ * `--ignore-scripts` is on the inner call because this script runs FROM
20
+ * prepack, and without it npm would run prepack again.
21
+ *
22
+ * Two things the manifest cannot cover, said here rather than discovered later:
23
+ * it cannot hash itself (it does not exist when the list is taken), and a
24
+ * signature over it does not authenticate the tarball - the verifier and the
25
+ * public key travel inside the same tarball, so anyone able to rewrite one can
26
+ * rewrite the others. What a signature DOES catch is a manifest damaged in
27
+ * transit or edited after install, which is the failure this file exists for.
28
+ * Provenance belongs to npm's own integrity field, not to us.
29
+ *
30
+ * Usage:
31
+ * make-manifest.mjs write manifest.json at the repo root
32
+ * make-manifest.mjs --sign also write manifest.sig (needs a signing key)
33
+ * make-manifest.mjs --check recompute and report drift, write nothing
34
+ * make-manifest.mjs --json
35
+ *
36
+ * Exit codes:
37
+ * 0 written, or --check found no drift
38
+ * 1 --check found drift
39
+ * 2 usage or environment error
40
+ */
41
+
42
+ import { execFileSync } from "node:child_process";
43
+ import { createHash, createPrivateKey, sign as cryptoSign } from "node:crypto";
44
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
45
+ import { dirname, join } from "node:path";
46
+ import { fileURLToPath } from "node:url";
47
+ import { runMain } from "../lib/fatal.mjs";
48
+
49
+ const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
50
+ const MANIFEST = join(ROOT, "manifest.json");
51
+
52
+ const argv = process.argv.slice(2);
53
+ const has = (f) => argv.includes(f);
54
+
55
+ /** The exact set npm would publish, in npm's own words. */
56
+ function shippedFiles() {
57
+ const out = execFileSync("npm", ["pack", "--dry-run", "--json", "--ignore-scripts"], {
58
+ cwd: ROOT,
59
+ encoding: "utf-8",
60
+ maxBuffer: 64 * 1024 * 1024,
61
+ stdio: ["ignore", "pipe", "ignore"],
62
+ });
63
+ const parsed = JSON.parse(out);
64
+ const files = parsed?.[0]?.files;
65
+ if (!Array.isArray(files) || !files.length) {
66
+ throw new Error("npm pack --dry-run returned no file list");
67
+ }
68
+ return files
69
+ .map((f) => f.path)
70
+ .filter((p) => p !== "manifest.json" && p !== "manifest.sig")
71
+ .sort();
72
+ }
73
+
74
+ function sha256(path) {
75
+ return createHash("sha256").update(readFileSync(path)).digest("hex");
76
+ }
77
+
78
+ function gitCommit() {
79
+ try {
80
+ return execFileSync("git", ["rev-parse", "HEAD"], {
81
+ cwd: ROOT,
82
+ encoding: "utf-8",
83
+ stdio: ["ignore", "pipe", "ignore"],
84
+ }).trim();
85
+ } catch {
86
+ // A tarball unpacked outside a checkout has no git. The manifest is still
87
+ // valid; only the provenance line is unknown, and saying so beats writing
88
+ // a placeholder that reads like a real commit.
89
+ return null;
90
+ }
91
+ }
92
+
93
+ function build() {
94
+ const pkg = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf-8"));
95
+ const files = {};
96
+ for (const rel of shippedFiles()) {
97
+ const abs = join(ROOT, rel);
98
+ if (!existsSync(abs)) continue;
99
+ files[rel] = sha256(abs);
100
+ }
101
+ return {
102
+ name: pkg.name,
103
+ version: pkg.version,
104
+ commit: gitCommit(),
105
+ generatedAt: new Date().toISOString(),
106
+ algorithm: "sha256",
107
+ fileCount: Object.keys(files).length,
108
+ files,
109
+ };
110
+ }
111
+
112
+ /**
113
+ * Sign the manifest with an ed25519 key. Optional, and silent about its absence
114
+ * on the common path: a package with no signature still verifies on hashes
115
+ * alone, which is the property that actually gets used.
116
+ *
117
+ * The key is read through the credential store, never taken from argv and never
118
+ * printed. MULTI_AGENT_SIGNING_KEY is the fallback for a build host that has no
119
+ * store (CI), and it holds the PEM itself.
120
+ *
121
+ * @param {string} manifestJson exact bytes that were written to disk
122
+ * @returns {string|null} base64 signature, or null when no key is configured
123
+ */
124
+ function sign(manifestJson) {
125
+ let pem = process.env.MULTI_AGENT_SIGNING_KEY || "";
126
+ if (!pem) {
127
+ try {
128
+ pem = execFileSync(
129
+ join(ROOT, "pipeline", "lib", "credential-store.sh"),
130
+ ["get", "pipeline_signing"],
131
+ {
132
+ encoding: "utf-8",
133
+ stdio: ["ignore", "pipe", "ignore"],
134
+ },
135
+ ).trim();
136
+ } catch {
137
+ pem = "";
138
+ }
139
+ }
140
+ if (!pem) {
141
+ process.stderr.write(
142
+ "make-manifest: no signing key configured - manifest written unsigned (hashes still verify)\n",
143
+ );
144
+ return null;
145
+ }
146
+ const privateKey = createPrivateKey(pem);
147
+ return cryptoSign(null, Buffer.from(manifestJson), privateKey).toString("base64");
148
+ }
149
+
150
+ function main() {
151
+ if (has("--check")) {
152
+ if (!existsSync(MANIFEST)) {
153
+ process.stderr.write("make-manifest: no manifest.json to check\n");
154
+ process.exit(2);
155
+ }
156
+ const stored = JSON.parse(readFileSync(MANIFEST, "utf-8"));
157
+ const fresh = build();
158
+ const changed = [];
159
+ for (const [rel, hash] of Object.entries(fresh.files)) {
160
+ if (stored.files[rel] !== hash) changed.push(rel);
161
+ }
162
+ const gone = Object.keys(stored.files).filter((r) => !(r in fresh.files));
163
+ const report = { changed, gone, stale: changed.length + gone.length > 0 };
164
+ if (has("--json")) process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
165
+ else if (!report.stale)
166
+ process.stdout.write(`manifest.json is current (${fresh.fileCount} files)\n`);
167
+ else {
168
+ process.stdout.write(
169
+ `manifest.json is stale: ${changed.length} changed, ${gone.length} gone\n`,
170
+ );
171
+ for (const r of [...changed, ...gone].slice(0, 20)) process.stdout.write(` ${r}\n`);
172
+ }
173
+ process.exitCode = report.stale ? 1 : 0;
174
+ return;
175
+ }
176
+
177
+ const manifest = build();
178
+ const json = `${JSON.stringify(manifest, null, 2)}\n`;
179
+ writeFileSync(MANIFEST, json);
180
+ if (has("--sign")) {
181
+ const sig = sign(json);
182
+ if (sig) writeFileSync(join(ROOT, "manifest.sig"), `${sig}\n`);
183
+ }
184
+ if (has("--json"))
185
+ process.stdout.write(
186
+ `${JSON.stringify({ written: MANIFEST, ...manifest, files: undefined }, null, 2)}\n`,
187
+ );
188
+ else
189
+ // STDERR, not stdout. This runs as `prepack`, and npm passes a prepack
190
+ // script's stdout straight through - so one friendly line here lands INSIDE
191
+ // `npm pack --dry-run --json` and every consumer that parses that JSON
192
+ // fails with "Unexpected token 'm'". Two gates caught it within minutes;
193
+ // a publish would not have.
194
+ process.stderr.write(
195
+ `manifest.json: ${manifest.fileCount} files, version ${manifest.version}, commit ${manifest.commit?.slice(0, 12) ?? "unknown"}\n`,
196
+ );
197
+ }
198
+
199
+ runMain("make-manifest", main);
@@ -14,6 +14,7 @@ import fs from "node:fs";
14
14
  import path from "node:path";
15
15
  import os from "node:os";
16
16
  import { fileURLToPath } from "node:url";
17
+ import { runMain } from "../lib/fatal.mjs";
17
18
 
18
19
  const DEFAULT_FILE = path.join(os.homedir(), ".claude", "multi-agent-preferences.json");
19
20
 
@@ -544,4 +545,4 @@ function main() {
544
545
  console.log(`✓ wrote migrated prefs to ${args.file}`);
545
546
  }
546
547
 
547
- main();
548
+ runMain("migrate-prefs", main);
@@ -13,6 +13,8 @@
13
13
  * node migrate-state.mjs --type=state <path> # force state type
14
14
  * node migrate-state.mjs --type=prefs <path> # force prefs type
15
15
  * node migrate-state.mjs --dry-run <path> # preview, no write
16
+ * node migrate-state.mjs --all # sweep every run on disk
17
+ * node migrate-state.mjs --all --dry-run # sweep, report only
16
18
  *
17
19
  * Exit codes:
18
20
  * 0 - migration applied (or file already current)
@@ -56,20 +58,101 @@ function parseArgs() {
56
58
  const args = process.argv.slice(2);
57
59
  let type = null;
58
60
  let dryRun = false;
61
+ let all = false;
59
62
  let path = null;
60
63
  for (const a of args) {
61
64
  if (a.startsWith("--type=")) type = a.slice(7);
62
65
  else if (a === "--dry-run") dryRun = true;
66
+ else if (a === "--all") all = true;
63
67
  else if (a.startsWith("--")) {
64
68
  console.error(`unknown flag: ${a}`);
65
69
  process.exit(1);
66
70
  } else path = a;
67
71
  }
68
- if (!path) {
69
- console.error("usage: migrate-state.mjs [--type=state|prefs] [--dry-run] <path>");
72
+ if (!path && !all) {
73
+ console.error("usage: migrate-state.mjs [--type=state|prefs] [--dry-run] <path> | --all");
70
74
  process.exit(1);
71
75
  }
72
- return { type, dryRun, path };
76
+ return { type, dryRun, all, path };
77
+ }
78
+
79
+ /**
80
+ * Sweep every run on disk and report what the migration chain can and cannot
81
+ * do with it.
82
+ *
83
+ * This exists because a per-file runner answers "is THIS file current" and
84
+ * nobody was asking it. Measured on a real install: 43 of 43 agent-state.json
85
+ * files carried no schemaVersion, so the whole schemas/migrations/ chain had
86
+ * never run on real data and nothing said so.
87
+ *
88
+ * A file with no schemaVersion is REPORTED, never stamped. Stamping it would
89
+ * assert that it conforms to a version nothing checked; validate-state.mjs
90
+ * already treats such files as resume-safe, which is the property that matters.
91
+ *
92
+ * @param {boolean} dryRun
93
+ * @returns {Promise<number>} process exit code
94
+ */
95
+ async function sweepAll(dryRun) {
96
+ const { listRuns } = await import("./_run-paths.mjs");
97
+ const runs = listRuns();
98
+ let current = 0;
99
+ let migrated = 0;
100
+ let legacy = 0;
101
+ let unreadable = 0;
102
+ const legacyIds = [];
103
+
104
+ for (const run of runs) {
105
+ const file = join(run.dir, "agent-state.json");
106
+ if (!existsSync(file)) continue;
107
+ let doc;
108
+ try {
109
+ doc = JSON.parse(readFileSync(file, "utf-8"));
110
+ } catch {
111
+ unreadable++;
112
+ console.log(` ✗ ${run.taskId} unreadable JSON`);
113
+ continue;
114
+ }
115
+ const from = doc?.schemaVersion;
116
+ if (typeof from !== "string") {
117
+ legacy++;
118
+ legacyIds.push(run.taskId);
119
+ continue;
120
+ }
121
+ if (from === TARGET_VERSIONS.state) {
122
+ current++;
123
+ continue;
124
+ }
125
+ const chain = resolveChain("state", from, TARGET_VERSIONS.state);
126
+ if (!chain) {
127
+ console.log(` ✗ ${run.taskId} no path ${from} → ${TARGET_VERSIONS.state}`);
128
+ unreadable++;
129
+ continue;
130
+ }
131
+ let next = doc;
132
+ for (const step of chain) {
133
+ const mod = await import(pathToFileURL(step.path).href);
134
+ next = mod.default(next);
135
+ }
136
+ if (!dryRun) writeFileSync(file, JSON.stringify(next, null, 2) + "\n");
137
+ migrated++;
138
+ console.log(
139
+ ` ✓ ${run.taskId} ${from} → ${TARGET_VERSIONS.state}${dryRun ? " (dry run)" : ""}`,
140
+ );
141
+ }
142
+
143
+ console.log(
144
+ `\nmigrate-state --all: ${runs.length} runs, ${current} current, ` +
145
+ `${migrated} migrated${dryRun ? " (dry run)" : ""}, ${legacy} legacy-unstamped, ${unreadable} unreadable`,
146
+ );
147
+ if (legacy) {
148
+ console.log(
149
+ `\n${legacy} state file(s) predate schemaVersion stamping and are reported, not stamped -\n` +
150
+ `stamping would claim a conformance nothing verified. They stay resume-safe\n` +
151
+ `(validate-state.mjs tolerates them by design). New runs are stamped by write-state.mjs.\n` +
152
+ ` ${legacyIds.slice(0, 8).join(", ")}${legacyIds.length > 8 ? ", ..." : ""}`,
153
+ );
154
+ }
155
+ return unreadable ? 1 : 0;
73
156
  }
74
157
 
75
158
  /**
@@ -139,6 +222,11 @@ function resolveChain(type, from, to) {
139
222
  }
140
223
 
141
224
  async function main() {
225
+ const parsed = parseArgs();
226
+ if (parsed.all) {
227
+ process.exit(await sweepAll(parsed.dryRun));
228
+ }
229
+
142
230
  const { type: forced, dryRun, path } = parseArgs();
143
231
 
144
232
  if (!existsSync(path)) {
@@ -199,9 +287,11 @@ async function main() {
199
287
  }
200
288
 
201
289
  if (dryRun) {
290
+ // return, not process.exit(0): a state file is the payload here, and exiting
291
+ // truncates it when stdout is a pipe - which is how a dry run gets reviewed.
202
292
  console.log("--- dry run, not writing ---");
203
293
  console.log(JSON.stringify(current, null, 2));
204
- process.exit(0);
294
+ return;
205
295
  }
206
296
 
207
297
  writeFileSync(path, JSON.stringify(current, null, 2) + "\n");
@@ -23,7 +23,8 @@
23
23
  # Turkish. The `auto` sentinel in place of [name] triggers a table lookup.
24
24
  # Canonical label set mirrors pipeline/multi-agent-refs/phases.md.
25
25
  #
26
- # Output is colored when stdout is a TTY (TERM != dumb). Plain otherwise so
26
+ # Output is colored when stdout is a TTY (TERM != dumb) and the run is not
27
+ # marked unattended. Plain otherwise so
27
28
  # logs and CI artifacts stay readable. Always written to stdout - log
28
29
  # capture is the caller's responsibility.
29
30
 
@@ -88,7 +89,10 @@ fi
88
89
  PHASE_WORD="$(phase_word "$PHASE_LANG")"
89
90
 
90
91
  # Color helpers (skip if not a TTY or TERM=dumb).
91
- if [ -t 1 ] && [ "${TERM:-}" != "dumb" ]; then
92
+ # Sunucuda terminal olabilir ve renk kimseye gitmez; log dosyasına ANSI
93
+ # kaçışları yazmak onu greplenemez yapar. MULTI_AGENT_UNATTENDED=1 bunu da
94
+ # kapatır; tanımsızken karar bugünküyle aynı.
95
+ if [ -t 1 ] && [ "${TERM:-}" != "dumb" ] && [ "${MULTI_AGENT_UNATTENDED:-}" != "1" ]; then
92
96
  C_RESET=$'\033[0m'
93
97
  C_DIM=$'\033[2m'
94
98
  C_BOLD=$'\033[1m'