@mmerterden/multi-agent-pipeline 14.2.1 → 15.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 (184) hide show
  1. package/CHANGELOG.md +118 -6
  2. package/README.md +20 -9
  3. package/README.tr.md +150 -0
  4. package/docs/FIGMA_PIPELINE.md +3 -3
  5. package/docs/adr/0006-skills-core-external-split.md +1 -1
  6. package/docs/adr/0009-claude-stack-skills-plugin-only.md +31 -0
  7. package/docs/adr/README.md +1 -0
  8. package/docs/architecture.md +24 -9
  9. package/docs/ecosystem.md +237 -0
  10. package/docs/features.md +5 -5
  11. package/index.js +2 -0
  12. package/install/_codex-agents.mjs +11 -2
  13. package/install/_common.mjs +65 -1
  14. package/install/_dev-only-files.mjs +0 -1
  15. package/install/_platform-filter.mjs +73 -7
  16. package/install/_plugin-skills.mjs +33 -8
  17. package/install/claude.mjs +144 -59
  18. package/install/codex.mjs +37 -7
  19. package/install/copilot.mjs +36 -11
  20. package/install/index.mjs +6 -2
  21. package/install/templates/codex-instructions.md +1 -1
  22. package/install/templates/copilot-instructions.md +15 -12
  23. package/package.json +1 -2
  24. package/pipeline/commands/multi-agent/SKILL.md +4 -2
  25. package/pipeline/commands/multi-agent/analysis/SKILL.md +5 -5
  26. package/pipeline/commands/multi-agent/analysis-resolve/SKILL.md +2 -2
  27. package/pipeline/commands/multi-agent/autopilot/SKILL.md +6 -2
  28. package/pipeline/commands/multi-agent/build-optimize/SKILL.md +9 -9
  29. package/pipeline/commands/multi-agent/channels/SKILL.md +16 -5
  30. package/pipeline/commands/multi-agent/complaint-analysis/SKILL.md +186 -0
  31. package/pipeline/commands/multi-agent/create-jira/SKILL.md +4 -4
  32. package/pipeline/commands/multi-agent/dev/SKILL.md +10 -23
  33. package/pipeline/commands/multi-agent/dev-autopilot/SKILL.md +10 -2
  34. package/pipeline/commands/multi-agent/dev-local/SKILL.md +10 -24
  35. package/pipeline/commands/multi-agent/dev-local-autopilot/SKILL.md +10 -3
  36. package/pipeline/commands/multi-agent/garbage-collect/SKILL.md +1 -1
  37. package/pipeline/commands/multi-agent/help/SKILL.md +19 -4
  38. package/pipeline/commands/multi-agent/ios-coding-standard/SKILL.md +2 -2
  39. package/pipeline/commands/multi-agent/jira/SKILL.md +13 -2
  40. package/pipeline/commands/multi-agent/language/SKILL.md +1 -1
  41. package/pipeline/commands/multi-agent/local/SKILL.md +6 -2
  42. package/pipeline/commands/multi-agent/local-autopilot/SKILL.md +6 -2
  43. package/pipeline/commands/multi-agent/log/SKILL.md +7 -1
  44. package/pipeline/commands/multi-agent/prune-prompts/SKILL.md +81 -0
  45. package/pipeline/commands/multi-agent/resume/SKILL.md +1 -1
  46. package/pipeline/commands/multi-agent/{ship → resume-local}/SKILL.md +13 -9
  47. package/pipeline/commands/multi-agent/setup/SKILL.md +5 -5
  48. package/pipeline/commands/multi-agent/stack/SKILL.md +55 -43
  49. package/pipeline/commands/multi-agent/store-ready/SKILL.md +3 -3
  50. package/pipeline/commands/multi-agent/sync/SKILL.md +18 -11
  51. package/pipeline/commands/multi-agent/testflight-validation/SKILL.md +1 -1
  52. package/pipeline/commands/multi-agent/uninstall/SKILL.md +2 -0
  53. package/pipeline/commands/multi-agent/update/SKILL.md +1 -1
  54. package/pipeline/lib/extract-conventions.sh +44 -15
  55. package/pipeline/lib/fetch-figma-annotations.sh +8 -1
  56. package/pipeline/lib/fetch-fortify.sh +23 -8
  57. package/pipeline/lib/figma-screenshot.sh +11 -1
  58. package/pipeline/lib/issue-fetcher.sh +77 -10
  59. package/pipeline/lib/md2confluence-v3.py +16 -2
  60. package/pipeline/lib/parse-complaints.sh +306 -0
  61. package/pipeline/lib/plan-todos.sh +5 -2
  62. package/pipeline/lib/post-pr-review.sh +8 -6
  63. package/pipeline/lib/shadow-git.sh +50 -9
  64. package/pipeline/lib/submodule-detector.sh +8 -1
  65. package/pipeline/multi-agent-refs/_input-parser.md +1 -1
  66. package/pipeline/multi-agent-refs/channels/confluence.md +3 -0
  67. package/pipeline/multi-agent-refs/channels/issue-comment.md +2 -2
  68. package/pipeline/multi-agent-refs/channels/jira.md +13 -2
  69. package/pipeline/multi-agent-refs/channels/pr-review-actions.md +1 -1
  70. package/pipeline/multi-agent-refs/channels/pr.md +20 -0
  71. package/pipeline/multi-agent-refs/channels/wiki.md +4 -4
  72. package/pipeline/multi-agent-refs/complaint-analysis-template.md +99 -0
  73. package/pipeline/multi-agent-refs/component-dispatch.md +6 -6
  74. package/pipeline/multi-agent-refs/cross-cli-contract.md +16 -16
  75. package/pipeline/multi-agent-refs/features/external-context-injection.md +1 -1
  76. package/pipeline/multi-agent-refs/features/stack-skill-routing.md +5 -5
  77. package/pipeline/multi-agent-refs/features/worktree-finalize.md +1 -1
  78. package/pipeline/multi-agent-refs/generate-issue.md +3 -3
  79. package/pipeline/multi-agent-refs/issue-jira-triad.md +3 -3
  80. package/pipeline/multi-agent-refs/payload-contracts.md +67 -0
  81. package/pipeline/multi-agent-refs/phases/modes.md +20 -0
  82. package/pipeline/multi-agent-refs/phases/phase-0-init.md +2 -2
  83. package/pipeline/multi-agent-refs/phases/phase-1-analysis.md +7 -7
  84. package/pipeline/multi-agent-refs/phases/phase-2-planning.md +5 -5
  85. package/pipeline/multi-agent-refs/phases/phase-3-dev.md +3 -3
  86. package/pipeline/multi-agent-refs/phases/phase-4-review.md +12 -12
  87. package/pipeline/multi-agent-refs/phases/phase-5-test.md +1 -1
  88. package/pipeline/multi-agent-refs/phases/phase-6-commit.md +8 -40
  89. package/pipeline/multi-agent-refs/phases/phase-7-report.md +5 -3
  90. package/pipeline/multi-agent-refs/phases.md +6 -0
  91. package/pipeline/multi-agent-refs/rules.md +2 -0
  92. package/pipeline/multi-agent-refs/tracker-contract.md +1 -1
  93. package/pipeline/multi-agent-refs/wiki-capture.md +2 -2
  94. package/pipeline/preferences-template.json +13 -5
  95. package/pipeline/rules/figma-pipeline.md +2 -2
  96. package/pipeline/schemas/agent-state.schema.json +1 -1
  97. package/pipeline/schemas/complaint-analysis-spec.schema.json +216 -0
  98. package/pipeline/schemas/migrations/prefs-2.5.0-to-2.6.0.mjs +46 -0
  99. package/pipeline/schemas/prefs.schema.json +277 -67
  100. package/pipeline/schemas/token-budget.json +2 -2
  101. package/pipeline/scripts/_stack-routing.mjs +79 -0
  102. package/pipeline/scripts/audit-log-rotate.sh +14 -1
  103. package/pipeline/scripts/build-skills-index.mjs +11 -0
  104. package/pipeline/scripts/build-stack-plugins.mjs +35 -60
  105. package/pipeline/scripts/check-derived-drift.mjs +65 -29
  106. package/pipeline/scripts/diff-explain.mjs +41 -3
  107. package/pipeline/scripts/diff-risk-score.mjs +72 -8
  108. package/pipeline/scripts/gc-worktrees.sh +4 -1
  109. package/pipeline/scripts/gen-mode-dispatch.mjs +1 -1
  110. package/pipeline/scripts/gen-skills-index.mjs +1 -1
  111. package/pipeline/scripts/learning-curve.mjs +8 -2
  112. package/pipeline/scripts/match-skills.mjs +8 -2
  113. package/pipeline/scripts/migrate-prefs.mjs +28 -20
  114. package/pipeline/scripts/output-quality-check.sh +15 -4
  115. package/pipeline/scripts/phase-tracker.sh +33 -12
  116. package/pipeline/scripts/phase0-exit-gate.mjs +3 -2
  117. package/pipeline/scripts/pre-commit-check.sh +69 -22
  118. package/pipeline/scripts/render-agent-log-cost.sh +8 -3
  119. package/pipeline/scripts/render-cost-summary.sh +42 -22
  120. package/pipeline/scripts/render-work-summary.sh +47 -13
  121. package/pipeline/scripts/review-scope.mjs +1 -1
  122. package/pipeline/scripts/run-aggregator.mjs +43 -14
  123. package/pipeline/scripts/scan-agent-config.sh +1 -1
  124. package/pipeline/scripts/skill-conformance.mjs +165 -30
  125. package/pipeline/scripts/smoke-cross-cli-behavior.sh +1 -1
  126. package/pipeline/scripts/smoke-schema-validation.sh +5 -1
  127. package/pipeline/scripts/test-gap-rules/android.json +25 -0
  128. package/pipeline/scripts/test-gap-rules/ios.json +34 -0
  129. package/pipeline/scripts/test-gap-rules/node.json +29 -0
  130. package/pipeline/scripts/test-gap-rules/python.json +25 -0
  131. package/pipeline/scripts/test-gap-scan.mjs +45 -6
  132. package/pipeline/scripts/uninstall.mjs +196 -14
  133. package/pipeline/scripts/update-issue-progress.sh +12 -16
  134. package/pipeline/scripts/validate-complaint-doc.mjs +229 -0
  135. package/pipeline/scripts/validate-reviewer.mjs +9 -3
  136. package/pipeline/scripts/worktree-finalize.sh +23 -2
  137. package/pipeline/skills/.skill-manifest.json +156 -108
  138. package/pipeline/skills/.skills-index.json +459 -13
  139. package/pipeline/skills/shared/README.md +15 -11
  140. package/pipeline/skills/shared/core/multi-agent-analysis-resolve/SKILL.md +1 -1
  141. package/pipeline/skills/shared/core/multi-agent-autopilot/SKILL.md +4 -0
  142. package/pipeline/skills/shared/core/multi-agent-build-optimize/SKILL.md +1 -1
  143. package/pipeline/skills/shared/core/multi-agent-complaint-analysis/SKILL.md +49 -0
  144. package/pipeline/skills/shared/core/multi-agent-create-jira/SKILL.md +1 -1
  145. package/pipeline/skills/shared/core/multi-agent-dev/SKILL.md +4 -17
  146. package/pipeline/skills/shared/core/multi-agent-dev-autopilot/SKILL.md +8 -0
  147. package/pipeline/skills/shared/core/multi-agent-dev-local/SKILL.md +5 -18
  148. package/pipeline/skills/shared/core/multi-agent-dev-local-autopilot/SKILL.md +8 -0
  149. package/pipeline/skills/shared/core/multi-agent-ios-coding-standard/SKILL.md +2 -2
  150. package/pipeline/skills/shared/core/multi-agent-language/SKILL.md +1 -1
  151. package/pipeline/skills/shared/core/multi-agent-local/SKILL.md +4 -0
  152. package/pipeline/skills/shared/core/multi-agent-local-autopilot/SKILL.md +4 -0
  153. package/pipeline/skills/shared/core/multi-agent-prune-prompts/SKILL.md +83 -0
  154. package/pipeline/skills/shared/core/{multi-agent-ship → multi-agent-resume-local}/SKILL.md +10 -6
  155. package/pipeline/skills/shared/core/multi-agent-stack/SKILL.md +79 -22
  156. package/pipeline/skills/shared/core/multi-agent-store-ready/SKILL.md +1 -1
  157. package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +8 -8
  158. package/pipeline/skills/shared/core/multi-agent-testflight-validation/SKILL.md +1 -1
  159. package/pipeline/skills/shared/external/ios-coding-standard/modules/_TEMPLATE.yml +2 -2
  160. package/pipeline/skills/shared/external/ios-coding-standard/references/rules.yml +368 -33
  161. package/pipeline/skills/shared/external/ios-coding-standard/references/swiftlint.draft.yml +1 -2
  162. package/pipeline/skills/shared/external/ios-coding-standard/scripts/check_structure.py +765 -0
  163. package/pipeline/skills/shared/external/ios-module-structure/SKILL.md +75 -0
  164. package/pipeline/skills/shared/external/ios-module-structure/modules/_TEMPLATE.yml +131 -0
  165. package/pipeline/skills/shared/external/ios-module-structure/references/rules.yml +559 -0
  166. package/pipeline/skills/shared/external/ios-module-structure/scripts/check_structure.py +765 -0
  167. package/pipeline/skills/shared/external/localization-reuse-map/SKILL.md +302 -0
  168. package/pipeline/skills/shared/external/localization-reuse-map/example-mapping.json +187 -0
  169. package/pipeline/skills/shared/external/localization-reuse-map/reference/format-and-output.md +156 -0
  170. package/pipeline/skills/shared/external/localization-reuse-map/reference/publish-and-snapshot.md +108 -0
  171. package/pipeline/skills/shared/external/localization-reuse-map/reference/sources-and-recipes.md +176 -0
  172. package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-artifact.py +865 -0
  173. package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-spreadsheet.py +335 -0
  174. package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-annotations.py +344 -0
  175. package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-legacy-labels.py +130 -0
  176. package/pipeline/skills/shared/external/localization-reuse-map/scripts/publish-confluence.py +264 -0
  177. package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-key-shots.py +298 -0
  178. package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-overlay.py +529 -0
  179. package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-legacy-values.py +187 -0
  180. package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-new-values.py +171 -0
  181. package/pipeline/skills/shared/external/localization-reuse-map/scripts/scan-screen-keys.py +184 -0
  182. package/pipeline/skills/shared/external/localization-reuse-map/scripts/snapshot-resources.sh +26 -0
  183. package/pipeline/skills/shared/external/localization-reuse-map/scripts/verify-map.py +173 -0
  184. package/pipeline/skills/skills-index.md +9 -5
@@ -104,9 +104,9 @@ function loadNumstat() {
104
104
  const map = new Map();
105
105
  let cur = null;
106
106
  for (const line of text.split("\n")) {
107
- const m = line.match(/^diff --git a\/(.+?) b\/(.+)$/);
108
- if (m) {
109
- cur = m[2];
107
+ const p = matchDiffGitHeader(line);
108
+ if (p !== null) {
109
+ cur = p;
110
110
  if (!map.has(cur)) map.set(cur, { added: 0, removed: 0 });
111
111
  continue;
112
112
  }
@@ -122,7 +122,7 @@ function loadNumstat() {
122
122
  return out.split("\n").map((line) => {
123
123
  const [a, r, ...rest] = line.split("\t");
124
124
  return {
125
- path: rest.join("\t"),
125
+ path: unquotePathIfNeeded(rest.join("\t")),
126
126
  added: a === "-" ? 0 : Number(a) || 0,
127
127
  removed: r === "-" ? 0 : Number(r) || 0,
128
128
  };
@@ -162,6 +162,60 @@ const TEST_PATH_GLOBS = [
162
162
  /\.spec\.(ts|tsx|js|mjs|py)$/i,
163
163
  ];
164
164
 
165
+ // git quotes a path in a `diff --git` header (core.quotePath, on by default)
166
+ // whenever it contains a non-ASCII byte or other unusual character -
167
+ // "diff --git \"a/\\303\\226deme.swift\" \"b/\\303\\226deme.swift\"" for a
168
+ // Turkish filename, not the bare `a/`/`b/` form. `/^diff --git a\/(.+?)
169
+ // b\/(.+)$/` doesn't match a line starting with a quote at all, so every
170
+ // non-ASCII-named file was silently dropped from risk scoring.
171
+ const DIFF_GIT_QUOTED_RE = /^diff --git "a\/((?:[^"\\]|\\.)*)" "b\/((?:[^"\\]|\\.)*)"$/;
172
+ const DIFF_GIT_UNQUOTED_RE = /^diff --git a\/(.+?) b\/(.+)$/;
173
+
174
+ // Reverse git's C-style quoting: octal byte escapes (\NNN, one per raw byte -
175
+ // a multi-byte UTF-8 character is several consecutive \NNN triplets) plus
176
+ // the standard \\ \" \t \n \r escapes.
177
+ function unquoteGitPath(s) {
178
+ const bytes = [];
179
+ for (let i = 0; i < s.length; i++) {
180
+ if (s[i] === "\\") {
181
+ const octal = s.slice(i + 1, i + 4);
182
+ if (/^[0-7]{3}$/.test(octal)) {
183
+ bytes.push(parseInt(octal, 8));
184
+ i += 3;
185
+ continue;
186
+ }
187
+ const simple = { "\\": 92, '"': 34, t: 9, n: 10, r: 13 };
188
+ const next = s[i + 1];
189
+ if (next in simple) {
190
+ bytes.push(simple[next]);
191
+ i += 1;
192
+ continue;
193
+ }
194
+ }
195
+ bytes.push(s.charCodeAt(i));
196
+ }
197
+ return Buffer.from(bytes).toString("utf-8");
198
+ }
199
+
200
+ // `git diff --numstat` quotes a non-ASCII/unusual path the same C-style way
201
+ // as the `diff --git` header line (e.g. `"\303\226deme.swift"`), just
202
+ // without the surrounding "diff --git a/...b/..." context to detect it by.
203
+ function unquotePathIfNeeded(s) {
204
+ if (s.length >= 2 && s[0] === '"' && s[s.length - 1] === '"') {
205
+ return unquoteGitPath(s.slice(1, -1));
206
+ }
207
+ return s;
208
+ }
209
+
210
+ // Returns the b/-side path from a `diff --git` header line, or null if the
211
+ // line isn't one. Handles both the quoted and unquoted forms.
212
+ function matchDiffGitHeader(line) {
213
+ const q = line.match(DIFF_GIT_QUOTED_RE);
214
+ if (q) return unquoteGitPath(q[2]);
215
+ const u = line.match(DIFF_GIT_UNQUOTED_RE);
216
+ return u ? u[2] : null;
217
+ }
218
+
165
219
  function matchesAny(path, regexes) {
166
220
  return regexes.some((r) => r.test(path));
167
221
  }
@@ -170,6 +224,16 @@ function isTestPath(path) {
170
224
  return matchesAny(path, TEST_PATH_GLOBS);
171
225
  }
172
226
 
227
+ // pairedTestPaths only knows how to derive a test-file name for these
228
+ // extensions; for anything else (README.md, package.json, .yaml configs,
229
+ // lockfiles, ...) it falls back to the untouched filename as `base` with an
230
+ // empty `ext`, producing candidates like "README.mdTests." that can never
231
+ // match a real path. Gating no_test_change on this is what "source touched"
232
+ // in the comment above actually means - without it, the signal fired on
233
+ // every non-test file in a diff, README included, which made HIGH_STAKES in
234
+ // review-scope.mjs unavoidable for almost any real-world diff.
235
+ const PAIRABLE_SOURCE_EXT_RE = /\.(swift|kt|ts|tsx|js|mjs|py)$/;
236
+
173
237
  function pairedTestPaths(srcPath) {
174
238
  // Return likely test path candidates for a source file.
175
239
  const parts = srcPath.split("/");
@@ -189,9 +253,9 @@ function parseHunks(diffText) {
189
253
  const perFile = new Map();
190
254
  let cur = null;
191
255
  for (const line of diffText.split("\n")) {
192
- const m = line.match(/^diff --git a\/(.+?) b\/(.+)$/);
193
- if (m) {
194
- cur = m[2];
256
+ const p = matchDiffGitHeader(line);
257
+ if (p !== null) {
258
+ cur = p;
195
259
  if (!perFile.has(cur)) perFile.set(cur, []);
196
260
  continue;
197
261
  }
@@ -271,7 +335,7 @@ function buildRow(stat, addedLines, allChangedPaths) {
271
335
  }
272
336
 
273
337
  // No-test-change: source touched + no matching test file in the changed set.
274
- if (!isTestPath(path)) {
338
+ if (!isTestPath(path) && PAIRABLE_SOURCE_EXT_RE.test(path)) {
275
339
  const candidates = pairedTestPaths(path).map((c) => c.toLowerCase());
276
340
  const anyTest = allChangedPaths.some((p) => {
277
341
  if (!isTestPath(p)) return false;
@@ -125,7 +125,10 @@ done
125
125
 
126
126
  # 3. Gitlink residue in the index ("Subproject commit" entries under
127
127
  # .worktrees/ from a pre-guard `git add -A`).
128
- gitlinks=$(git -C "$REPO" ls-files -s -- .worktrees 2>/dev/null | awk '$1 == "160000" { print $4 }')
128
+ # Path is the tab-separated second field ("mode sha stage\tpath"); splitting on
129
+ # whitespace truncated any path containing a space and the follow-up
130
+ # `git rm --ignore-unmatch` exited 0 on the phantom, reporting a false success.
131
+ gitlinks=$(git -C "$REPO" ls-files -s -- .worktrees 2>/dev/null | awk -F'\t' '$1 ~ /^160000 / { print $2 }')
129
132
  if [ -n "$gitlinks" ]; then
130
133
  while IFS= read -r path; do
131
134
  [ -z "$path" ] && continue
@@ -145,7 +145,7 @@ bash $HOME/.claude/scripts/phase-tracker.sh update 0 in_progress
145
145
  bash $HOME/.claude/scripts/phase-tracker.sh update <N> in_progress|completed|failed|skipped
146
146
 
147
147
  # After every LLM call (every CLI):
148
- bash $HOME/.claude/scripts/phase-tracker.sh tokens <N> <in> <out>
148
+ bash $HOME/.claude/scripts/phase-tracker.sh tokens <N> <in> <out> [cached]
149
149
  \`\`\`
150
150
 
151
151
  ### Visual channel - Claude Code (native TaskList widget, required)
@@ -82,7 +82,7 @@ md += `**Total:** ${total} skills (${breakdown}). Auto-generated by \`scripts/ge
82
82
  md += `## Directory layout\n\n`;
83
83
  md += `- **\`core/\`** - ${counts.core} \`multi-agent*\` orchestration skills that are pipeline-critical. Edits here are core-code changes.\n`;
84
84
  md += `- **\`external/\`** - ${counts.external} iOS / Android / generic skills imported from the upstream skill library. Mirrors of third-party guidance.\n`;
85
- md += `- Install destination stays flat: both trees flatten into \`~/.claude/skills/\` and \`~/.copilot/skills/\`. See ADR-0006 for the rationale.\n\n`;
85
+ md += `- Install destinations (ADR-0009): Claude Code gets NO local copy of \`external/\` - it loads those skills from the \`multi-agent-plugins\` marketplace, namespaced (\`ai-<stack>-toolkit:<name>\`); only the two compliance catalogs from \`core/\` land in \`~/.claude/skills/\`. Copilot CLI and Codex CLI receive a flat copy filtered to the enabled stacks. \`external/\` remains the single authoring source that \`build-stack-plugins.mjs\` publishes from.\n\n`;
86
86
  md += `Source layout is logical grouping only - skill discovery at runtime is unchanged.\n\n`;
87
87
  md += `## Categories\n\n`;
88
88
  for (const k of order) {
@@ -78,8 +78,14 @@ for (const e of events) {
78
78
  }
79
79
  if (e.ts && (!t.firstTs || e.ts < t.firstTs)) t.firstTs = e.ts;
80
80
  const d = e.details || {};
81
- if (e.event === "review.completed" && d.review_cycles != null)
82
- t.reviewCycles = num(d.review_cycles);
81
+ // phase-4-review.md's actual review.completed emission (raw_count=/
82
+ // accepted=/deferred=/rejected=/approved=/duration_ms=) carries no
83
+ // review_cycles field - that name only appears on Phase 7's task.completed
84
+ // summary event, which this never matched. Phase 4 re-runs (and logs its
85
+ // own review.completed) once per rework cycle, so the cycle count for a
86
+ // task is how many review.completed events share its task_id - the same
87
+ // derivation aggregate-metrics.mjs already uses for this metric.
88
+ if (e.event === "review.completed") t.reviewCycles = (t.reviewCycles ?? 0) + 1;
83
89
  if (e.event === "rework.started") t.rework += 1;
84
90
  t.tokensIn += num(d.tokens_in);
85
91
  t.tokensOut += num(d.tokens_out);
@@ -202,13 +202,19 @@ for (const entry of index.entries) {
202
202
  results.sort((a, b) => b.score - a.score || a.name.localeCompare(b.name));
203
203
  const top = results.slice(0, opts.limit);
204
204
 
205
+ // On Claude Code an external skill resolves only through its marketplace plugin,
206
+ // so surface `invokeAs` (plugin-namespaced) as the primary name. Old indexes
207
+ // without the field fall back to the bare name - same behaviour as before.
205
208
  if (opts.json) {
206
- process.stdout.write(JSON.stringify({ task: taskText, matches: top }, null, 2) + "\n");
209
+ const matches = top.map((m) => ({ ...m, invokeAs: m.invokeAs || m.name }));
210
+ process.stdout.write(JSON.stringify({ task: taskText, matches }, null, 2) + "\n");
207
211
  } else {
208
212
  for (const m of top) {
209
213
  const tag = `[${m.group}${m.platform ? "/" + m.platform : ""}]`;
210
214
  const desc = (m.description || "").slice(0, 80);
211
- console.log(`${String(m.score).padStart(3)} ${m.name.padEnd(32)} ${tag.padEnd(18)} ${desc}`);
215
+ console.log(
216
+ `${String(m.score).padStart(3)} ${(m.invokeAs || m.name).padEnd(46)} ${tag.padEnd(18)} ${desc}`,
217
+ );
212
218
  }
213
219
  if (top.length === 0) console.log("(no skills matched)");
214
220
  }
@@ -13,12 +13,13 @@
13
13
  import fs from "node:fs";
14
14
  import path from "node:path";
15
15
  import os from "node:os";
16
+ import { fileURLToPath } from "node:url";
16
17
 
17
18
  const DEFAULT_FILE = path.join(os.homedir(), ".claude", "multi-agent-preferences.json");
18
19
 
19
20
  // Single source of truth for the migration target version. Referenced by the
20
21
  // migrate() bump chain AND every user-facing message, so they cannot drift.
21
- const TARGET_VERSION = "2.5.0";
22
+ const TARGET_VERSION = "2.6.0";
22
23
 
23
24
  const USAGE = "Usage: migrate-prefs.mjs [--dry-run] [--file <path>]";
24
25
 
@@ -75,7 +76,7 @@ function defaultSettings() {
75
76
  * @returns {Set<string>}
76
77
  */
77
78
  function readMigratableVersions(target) {
78
- const here = path.dirname(new URL(import.meta.url).pathname);
79
+ const here = path.dirname(fileURLToPath(import.meta.url));
79
80
  const candidates = [
80
81
  path.join(here, "..", "schemas", "prefs.schema.json"),
81
82
  path.join(here, "..", "..", "pipeline", "schemas", "prefs.schema.json"),
@@ -297,7 +298,9 @@ function migrate(prefs) {
297
298
  seeded += 1;
298
299
  }
299
300
  if (seeded > 0) {
300
- changes.push(`seeded ${seeded} recentBranches entr${seeded === 1 ? "y" : "ies"} for ${projectKey} from legacy projects[].branches`);
301
+ changes.push(
302
+ `seeded ${seeded} recentBranches entr${seeded === 1 ? "y" : "ies"} for ${projectKey} from legacy projects[].branches`,
303
+ );
301
304
  }
302
305
  }
303
306
  if (!Array.isArray(out.global.recentGroups)) {
@@ -332,27 +335,32 @@ function migrate(prefs) {
332
335
  out.global.skillConformance.blockOnCoverageGap = false;
333
336
  changes.push("added skillConformance.blockOnCoverageGap=false (report, do not halt)");
334
337
  }
335
- // v2.5.0: /multi-agent:finish became /multi-agent:ship, and its autoFix key -
336
- // referenced by the command spec since it shipped but never declared in the
337
- // schema - is declared here. Carry any value the user had rather than reset it.
338
- if (!out.global.ship || typeof out.global.ship !== "object") {
339
- out.global.ship = {};
340
- changes.push("added ship (v2.5.0 - /multi-agent:finish renamed to :ship)");
341
- }
342
- if (typeof out.global.ship.autoFix !== "boolean") {
343
- const legacyAutoFix = out.global.finish?.autoFix;
344
- out.global.ship.autoFix = typeof legacyAutoFix === "boolean" ? legacyAutoFix : false;
338
+ // Rename chain for the pipeline-tail command: /multi-agent:finish (pre-v2.5.0)
339
+ // -> :ship (v2.5.0) -> :resume-local (current). The autoFix value survives every
340
+ // hop - carry whichever legacy block still holds one rather than reset it.
341
+ if (!out.global.resumeLocal || typeof out.global.resumeLocal !== "object") {
342
+ out.global.resumeLocal = {};
343
+ changes.push("added resumeLocal (/multi-agent:ship renamed to :resume-local)");
344
+ }
345
+ if (typeof out.global.resumeLocal.autoFix !== "boolean") {
346
+ const legacyAutoFix =
347
+ typeof out.global.ship?.autoFix === "boolean"
348
+ ? out.global.ship.autoFix
349
+ : out.global.finish?.autoFix;
350
+ out.global.resumeLocal.autoFix = typeof legacyAutoFix === "boolean" ? legacyAutoFix : false;
345
351
  changes.push(
346
352
  typeof legacyAutoFix === "boolean"
347
- ? "moved finish.autoFix -> ship.autoFix (value preserved)"
348
- : "added ship.autoFix=false",
353
+ ? "moved ship/finish autoFix -> resumeLocal.autoFix (value preserved)"
354
+ : "added resumeLocal.autoFix=false",
349
355
  );
350
356
  }
351
- if (out.global.finish && typeof out.global.finish === "object") {
352
- delete out.global.finish.autoFix;
353
- if (Object.keys(out.global.finish).length === 0) {
354
- delete out.global.finish;
355
- changes.push("removed obsolete finish block (renamed to ship)");
357
+ for (const legacy of ["ship", "finish"]) {
358
+ if (out.global[legacy] && typeof out.global[legacy] === "object") {
359
+ delete out.global[legacy].autoFix;
360
+ if (Object.keys(out.global[legacy]).length === 0) {
361
+ delete out.global[legacy];
362
+ changes.push(`removed obsolete ${legacy} block (renamed to resumeLocal)`);
363
+ }
356
364
  }
357
365
  }
358
366
  if (!out.global.serviceStatus) {
@@ -80,11 +80,22 @@ if [ -f "$PR_BODY" ]; then
80
80
  else
81
81
  ok "PR body free of HTML entities"
82
82
  fi
83
- for sec in "## Summary" "## How to Test"; do
84
- grep -q "$sec" "$PR_BODY" \
85
- && ok "PR body has '$sec' section" \
86
- || warn "PR body missing '$sec' section"
83
+ # Canonical section set is channels/pr.md's: summary -> changes ->
84
+ # architecture (cond.) -> verification -> dependencies (cond.) -> related.
85
+ # Headings render in outputLanguage, so match the always-required three by
86
+ # either language rather than pinning English. "## How to Test" was the old
87
+ # phase-6 template's heading and is not part of the canonical set.
88
+ for sec in "Summary|Özet" "Changes|Değişiklikler" "Verification|Doğrulama"; do
89
+ label="${sec%%|*}"
90
+ grep -qE "^## ($sec)" "$PR_BODY" \
91
+ && ok "PR body has '$label' section" \
92
+ || warn "PR body missing '$label' section (canonical set: channels/pr.md)"
87
93
  done
94
+ if grep -qE '^h[1-6]\. |\{\{[A-Za-z_]' "$PR_BODY"; then
95
+ bad "PR body contains Jira wiki markup (h2./{{code}}) - PR bodies are Markdown"
96
+ else
97
+ ok "PR body free of Jira wiki markup"
98
+ fi
88
99
  else
89
100
  warn "no .pr-body.md in task dir (skipped PR-body check)"
90
101
  fi
@@ -259,9 +259,9 @@ emit_otel_span() {
259
259
  startTimeUnixNano: $ts,
260
260
  endTimeUnixNano: $ts,
261
261
  attributes: ({
262
- "multi_agent.task_id": $task,
263
- "multi_agent.phase_id": $phase,
264
- "multi_agent.subject": $subj,
262
+ "task_id": $task,
263
+ "phase_id": $phase,
264
+ "subject": $subj,
265
265
  "service.name": "multi-agent-pipeline"
266
266
  } + $extra)
267
267
  }' >> "$spans_file" 2>/dev/null || true
@@ -382,6 +382,9 @@ load_state() {
382
382
  fi
383
383
  }
384
384
  save_state() {
385
+ # Refuse to persist an empty document: every caller pipes jq output in, and
386
+ # a failed jq yields "" - writing that would destroy the whole tracker state.
387
+ [ -n "$1" ] || { echo "save_state: refusing to write empty state" >&2; return 65; }
385
388
  mkdir -p "$TRACKER_DIR" 2>/dev/null
386
389
  printf '%s\n' "$1" > "${TRACKER_FILE}.tmp"
387
390
  mv "${TRACKER_FILE}.tmp" "$TRACKER_FILE"
@@ -693,7 +696,7 @@ case "$ACTION" in
693
696
  state=$(load_state)
694
697
  # Idempotent: a phase id that already exists is a no-op (name, status, and
695
698
  # token history are preserved). This is what lets continuation commands
696
- # (/multi-agent:ship, resume) re-declare their phase set against a
699
+ # (/multi-agent:resume-local, resume) re-declare their phase set against a
697
700
  # pre-existing tracker without duplicating or resetting tiles.
698
701
  new=$(echo "$state" | jq --arg id "$PID" --arg name "$PNAME" '
699
702
  if any(.phases[]?; .id == $id) then .
@@ -728,7 +731,7 @@ case "$ACTION" in
728
731
  ')
729
732
  save_state "$new"
730
733
  release_state_lock
731
- emit_otel_span "phase.update" "$PID" "$STATUS" "{\"multi_agent.status\": \"$STATUS\"}"
734
+ emit_otel_span "phase.update" "$PID" "$STATUS" "{\"status\": \"$STATUS\"}"
732
735
  render
733
736
  ;;
734
737
 
@@ -757,7 +760,7 @@ case "$ACTION" in
757
760
  )')
758
761
  save_state "$new"
759
762
  release_state_lock
760
- emit_otel_span "phase.sub" "$PID" "$SNAME" "{\"multi_agent.sub_id\": \"$SID\", \"multi_agent.status\": \"$SSTATUS\"}"
763
+ emit_otel_span "phase.sub" "$PID" "$SNAME" "{\"sub_id\": \"$SID\", \"status\": \"$SSTATUS\"}"
761
764
  render
762
765
  ;;
763
766
 
@@ -766,9 +769,13 @@ case "$ACTION" in
766
769
  [ "$#" -ge 3 ] || { echo "tokens needs <phase_id> <in> <out> [cached]" >&2; exit 64; }
767
770
  PID="$1"; T_IN="$2"; T_OUT="$3"; T_CACHED="${4:-0}"
768
771
  # Validate integers (cached is optional, defaults to 0 for back-compat).
769
- case "$T_IN$T_OUT$T_CACHED" in
770
- *[!0-9]*) echo "tokens: in/out/cached must be non-negative integers" >&2; exit 64 ;;
771
- esac
772
+ # Each field checked separately: concatenation would let an empty field
773
+ # pass and feed jq --argjson an empty string, nuking the state file.
774
+ for v in "$T_IN" "$T_OUT" "$T_CACHED"; do
775
+ case "$v" in
776
+ ''|*[!0-9]*) echo "tokens: in/out/cached must be non-negative integers" >&2; exit 64 ;;
777
+ esac
778
+ done
772
779
  acquire_state_lock
773
780
  state=$(load_state)
774
781
  # Additive: add to existing totals so multiple LLM calls accumulate.
@@ -783,7 +790,20 @@ case "$ACTION" in
783
790
  ')
784
791
  save_state "$new"
785
792
  release_state_lock
786
- emit_otel_span "phase.tokens" "$PID" "" "{\"multi_agent.tokens_in_delta\": $T_IN, \"multi_agent.tokens_out_delta\": $T_OUT, \"multi_agent.tokens_cached_delta\": $T_CACHED}"
793
+ # Carry the phase's current model/name onto the span too: the cost
794
+ # renderers' OTel-fallback path groups phase.tokens spans by phase_id and
795
+ # reads model/phase_name off the first span in each group, so without
796
+ # these the fallback always renders the default model/"?" name even
797
+ # though the tracker JSON already knows both.
798
+ PHASE_MODEL_ATTR=$(printf '%s' "$new" | jq -r --arg id "$PID" '(.phases[] | select(.id == $id) | .model) // ""')
799
+ PHASE_NAME_ATTR=$(printf '%s' "$new" | jq -r --arg id "$PID" '(.phases[] | select(.id == $id) | .name) // ""')
800
+ TOKENS_EXTRA=$(jq -nc \
801
+ --argjson ti "$T_IN" --argjson to "$T_OUT" --argjson tc "$T_CACHED" \
802
+ --arg model "$PHASE_MODEL_ATTR" --arg name "$PHASE_NAME_ATTR" \
803
+ '{tokens_in_delta: $ti, tokens_out_delta: $to, tokens_cached_delta: $tc}
804
+ + (if $model != "" then {model: $model} else {} end)
805
+ + (if $name != "" then {phase_name: $name} else {} end)')
806
+ emit_otel_span "phase.tokens" "$PID" "" "$TOKENS_EXTRA"
787
807
  # Re-render so live token count is visible on the active phase. Callers
788
808
  # that want silent accumulation (e.g. tight loops) can suppress with
789
809
  # TRACKER_QUIET=1 to skip the re-render but still write state.
@@ -806,7 +826,8 @@ case "$ACTION" in
806
826
  )')
807
827
  save_state "$new"
808
828
  release_state_lock
809
- emit_otel_span "phase.meta" "$PID" "$KEY" "{\"multi_agent.meta_key\": \"$KEY\", \"multi_agent.meta_value\": \"$VALUE\"}"
829
+ META_EXTRA=$(jq -nc --arg k "$KEY" --arg v "$VALUE" '{meta_key: $k, meta_value: $v}')
830
+ emit_otel_span "phase.meta" "$PID" "$KEY" "$META_EXTRA"
810
831
  if [ "${TRACKER_QUIET:-0}" != "1" ]; then
811
832
  render
812
833
  fi
@@ -824,7 +845,7 @@ case "$ACTION" in
824
845
  )')
825
846
  save_state "$new"
826
847
  release_state_lock
827
- emit_otel_span "phase.model" "$PID" "" "{\"multi_agent.model\": \"$MODEL\"}"
848
+ emit_otel_span "phase.model" "$PID" "" "{\"model\": \"$MODEL\"}"
828
849
  if [ "${TRACKER_QUIET:-0}" != "1" ]; then
829
850
  render
830
851
  fi
@@ -76,7 +76,8 @@ export function inputTextOf(state) {
76
76
  // evidence[].url / figmaFrames[] are where the analysis phase parks references.
77
77
  for (const key of ["figmaFrames", "designRefs"]) {
78
78
  const v = state[key];
79
- if (Array.isArray(v)) parts.push(v.map((x) => (typeof x === "string" ? x : JSON.stringify(x))).join(" "));
79
+ if (Array.isArray(v))
80
+ parts.push(v.map((x) => (typeof x === "string" ? x : JSON.stringify(x))).join(" "));
80
81
  }
81
82
  return parts.join("\n");
82
83
  }
@@ -198,7 +199,7 @@ function main(argv) {
198
199
  const args = argv.slice(2);
199
200
  const taskId = args.find((a) => !a.startsWith("--"));
200
201
  if (!taskId) {
201
- console.error("usage: phase0-exit-gate.mjs <task_id> [--input \"<text>\"] [--json]");
202
+ console.error('usage: phase0-exit-gate.mjs <task_id> [--input "<text>"] [--json]');
202
203
  return 2;
203
204
  }
204
205
  const inputIdx = args.indexOf("--input");
@@ -12,6 +12,7 @@ set -uo pipefail
12
12
  # Direct invocations (empty/non-JSON stdin, e.g. smoke tests or manual runs)
13
13
  # fall through to the full scan. Extraction failure also falls through - the
14
14
  # only cost is an unnecessary scan, never a skipped one on a real commit.
15
+ SCAN_UNSTAGED=0
15
16
  if [ ! -t 0 ]; then
16
17
  HOOK_INPUT="$(cat 2>/dev/null || true)"
17
18
  if [ -n "$HOOK_INPUT" ]; then
@@ -40,42 +41,53 @@ process.stdin.on("end", () => {
40
41
  | grep -qE '(^|[^[:alnum:]_./-])git[[:space:]]+([^|&;]*[[:space:]])?commit([^[:alnum:]_-]|$)'; then
41
42
  exit 0
42
43
  fi
44
+ # PreToolUse fires BEFORE the tool runs, so a compound command like
45
+ # `git add -A && git commit -m ...` reaches this point with NOTHING
46
+ # staged yet from either half - `git diff --cached` alone sees an
47
+ # empty index and this scan silently no-ops right before the commit
48
+ # it exists to gate. When the same command also runs `git add`, widen
49
+ # the scan to the working tree (tracked-unstaged + untracked), not
50
+ # just the index, so that case is still caught.
51
+ if printf '%s\n' "$HOOK_COMMAND" \
52
+ | grep -qE '(^|[^[:alnum:]_./-])git[[:space:]]+add([^[:alnum:]_-]|$)'; then
53
+ SCAN_UNSTAGED=1
54
+ fi
43
55
  fi
44
56
  fi
45
57
  fi
46
58
 
47
- if [ -z "$(git diff --cached --name-only 2>/dev/null)" ]; then
48
- exit 0
49
- fi
50
-
51
59
  FOUND=0
52
60
 
53
- # NUL-delimited so filenames with spaces are scanned (a space-split loop would
54
- # silently skip them - a secret false-negative). Process substitution keeps the
55
- # loop in the current shell so FOUND survives.
56
- while IFS= read -r -d '' file; do
57
- [ -z "$file" ] && continue
58
- [ ! -f "$file" ] && continue
59
- file "$file" 2>/dev/null | grep -q "binary" && continue
61
+ # `file "$f" | grep -q "binary"` never matches real `file` output (it names a
62
+ # concrete type - "ELF 64-bit", "Mach-O", "PNG image data" - not the word
63
+ # "binary"), so every binary was scanned as text. --mime's charset=binary is
64
+ # the actual signal libmagic gives for non-text content.
65
+ is_binary() {
66
+ file --mime "$1" 2>/dev/null | grep -q 'charset=binary'
67
+ }
60
68
 
61
- # Only check added/modified lines
62
- content=$(git diff --cached -- "$file" 2>/dev/null | grep "^+" | grep -v "^+++" || true)
63
- [ -z "$content" ] && continue
69
+ # All BLOCKED checks for one file, given the content to scan (added lines for
70
+ # a diff, or the whole file for something not yet tracked at all - every line
71
+ # of a brand-new file is an addition).
72
+ scan_file() {
73
+ local file="$1" content="$2"
64
74
 
65
75
  # .env files
66
76
  if echo "$file" | grep -qE '\.env($|\.)'; then
67
77
  echo "BLOCKED: .env file staged: $file"
68
78
  FOUND=1
69
- continue
79
+ return 0
70
80
  fi
71
81
 
72
82
  # Credentials files
73
83
  if echo "$file" | grep -qiE '(credentials|secrets|tokens)\.(json|yaml|yml|plist)$'; then
74
84
  echo "BLOCKED: Credentials file staged: $file"
75
85
  FOUND=1
76
- continue
86
+ return 0
77
87
  fi
78
88
 
89
+ [ -z "$content" ] && return 0
90
+
79
91
  # API keys / tokens (key = "long_string" pattern)
80
92
  if echo "$content" | grep -qiE '(api[_-]?key|api[_-]?secret|access[_-]?token|auth[_-]?token|secret[_-]?key)\s*[:=]\s*["'"'"'][A-Za-z0-9+/=_-]{20,}'; then
81
93
  echo "BLOCKED: Possible API key/token in $file"
@@ -119,12 +131,12 @@ while IFS= read -r -d '' file; do
119
131
  # `smoke-pre-commit.sh` is this gate's OWN test suite: it must contain
120
132
  # secret-shaped fixtures (a fake `ghp_` token, a fake JWT, a random 44-char
121
133
  # string) in order to prove the detector fires on them. Only ADDED lines are
122
- # scanned (see the `git diff --cached | grep "^+"` above), so existing fixtures
123
- # are invisible - but ADDING a new detector test blocks the commit that adds
124
- # it, which makes the detector untestable. Entropy only: the provider-prefix,
125
- # AWS, private-key, JWT and service-account checks above still apply to this
126
- # file, so a real token pasted here is still caught. Scoped to this one path on
127
- # purpose - widen the list and the gate stops meaning anything.
134
+ # scanned, so existing fixtures are invisible - but ADDING a new detector
135
+ # test blocks the commit that adds it, which makes the detector untestable.
136
+ # Entropy only: the provider-prefix, AWS, private-key, JWT and
137
+ # service-account checks above still apply to this file, so a real token
138
+ # pasted here is still caught. Scoped to this one path on purpose - widen
139
+ # the list and the gate stops meaning anything.
128
140
  # The generic high-entropy exemptions stay case-insensitive (lockfile names
129
141
  # vary). The gate's own suite is matched separately and CASE-SENSITIVELY on its
130
142
  # exact repo-relative path: `(^|/)smoke-pre-commit\.sh$` under `grep -i` would
@@ -184,8 +196,43 @@ while IFS= read -r -d '' file; do
184
196
  fi
185
197
  fi
186
198
  fi
199
+ }
200
+
201
+ # NUL-delimited so filenames with spaces are scanned (a space-split loop would
202
+ # silently skip them - a secret false-negative).
203
+
204
+ # Already-staged content - the common case, and the only source when nothing
205
+ # in the triggering command itself runs `git add`.
206
+ while IFS= read -r -d '' file; do
207
+ [ -z "$file" ] && continue
208
+ [ ! -f "$file" ] && continue
209
+ is_binary "$file" && continue
210
+ content=$(git diff --cached -- "$file" 2>/dev/null | grep "^+" | grep -v "^+++" || true)
211
+ scan_file "$file" "$content"
187
212
  done < <(git diff --cached --name-only -z 2>/dev/null)
188
213
 
214
+ if [ "$SCAN_UNSTAGED" = "1" ]; then
215
+ # Tracked and modified, but not staged yet (the `git add` half of the
216
+ # compound command hasn't run at this point - see SCAN_UNSTAGED above).
217
+ while IFS= read -r -d '' file; do
218
+ [ -z "$file" ] && continue
219
+ [ ! -f "$file" ] && continue
220
+ is_binary "$file" && continue
221
+ content=$(git diff HEAD -- "$file" 2>/dev/null | grep "^+" | grep -v "^+++" || true)
222
+ scan_file "$file" "$content"
223
+ done < <(git diff HEAD --name-only -z 2>/dev/null)
224
+
225
+ # Untracked (brand-new) files - there is no diff to take yet, so every line
226
+ # of the file is "added" content for scanning purposes.
227
+ while IFS= read -r -d '' file; do
228
+ [ -z "$file" ] && continue
229
+ [ ! -f "$file" ] && continue
230
+ is_binary "$file" && continue
231
+ content=$(sed 's/^/+/' "$file" 2>/dev/null || true)
232
+ scan_file "$file" "$content"
233
+ done < <(git ls-files --others --exclude-standard -z 2>/dev/null)
234
+ fi
235
+
189
236
  if [ $FOUND -eq 1 ]; then
190
237
  echo ""
191
238
  echo "Secret check failed. Remove secrets from staged files before committing."
@@ -30,9 +30,14 @@ while [ $# -gt 0 ]; do
30
30
  esac
31
31
  done
32
32
 
33
- ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
34
- COST_TABLE="$ROOT/pipeline/scripts/cost-table.json"
35
- COST_LIB="$ROOT/pipeline/scripts/cost-lib.sh"
33
+ # cost-table.json/cost-lib.sh ship as siblings of this script both in the
34
+ # repo (pipeline/scripts/) and once installed (~/.claude/scripts/) - a
35
+ # repo-root-relative path (../../pipeline/scripts/...) only resolves in the
36
+ # repo checkout and silently misses in the installed layout. Match
37
+ # phase-tracker.sh's own resolution instead of re-deriving a repo root.
38
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
39
+ COST_TABLE="$SCRIPT_DIR/cost-table.json"
40
+ COST_LIB="$SCRIPT_DIR/cost-lib.sh"
36
41
  if [ -f "$COST_LIB" ]; then . "$COST_LIB"; else COST_JQ_DEFS=""; fi
37
42
 
38
43
  command -v jq >/dev/null 2>&1 || { echo "render-agent-log-cost: jq required" >&2; exit 2; }