@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.
- package/CHANGELOG.md +118 -6
- package/README.md +20 -9
- package/README.tr.md +150 -0
- package/docs/FIGMA_PIPELINE.md +3 -3
- package/docs/adr/0006-skills-core-external-split.md +1 -1
- package/docs/adr/0009-claude-stack-skills-plugin-only.md +31 -0
- package/docs/adr/README.md +1 -0
- package/docs/architecture.md +24 -9
- package/docs/ecosystem.md +237 -0
- package/docs/features.md +5 -5
- package/index.js +2 -0
- package/install/_codex-agents.mjs +11 -2
- package/install/_common.mjs +65 -1
- package/install/_dev-only-files.mjs +0 -1
- package/install/_platform-filter.mjs +73 -7
- package/install/_plugin-skills.mjs +33 -8
- package/install/claude.mjs +144 -59
- package/install/codex.mjs +37 -7
- package/install/copilot.mjs +36 -11
- package/install/index.mjs +6 -2
- package/install/templates/codex-instructions.md +1 -1
- package/install/templates/copilot-instructions.md +15 -12
- package/package.json +1 -2
- package/pipeline/commands/multi-agent/SKILL.md +4 -2
- package/pipeline/commands/multi-agent/analysis/SKILL.md +5 -5
- package/pipeline/commands/multi-agent/analysis-resolve/SKILL.md +2 -2
- package/pipeline/commands/multi-agent/autopilot/SKILL.md +6 -2
- package/pipeline/commands/multi-agent/build-optimize/SKILL.md +9 -9
- package/pipeline/commands/multi-agent/channels/SKILL.md +16 -5
- package/pipeline/commands/multi-agent/complaint-analysis/SKILL.md +186 -0
- package/pipeline/commands/multi-agent/create-jira/SKILL.md +4 -4
- package/pipeline/commands/multi-agent/dev/SKILL.md +10 -23
- package/pipeline/commands/multi-agent/dev-autopilot/SKILL.md +10 -2
- package/pipeline/commands/multi-agent/dev-local/SKILL.md +10 -24
- package/pipeline/commands/multi-agent/dev-local-autopilot/SKILL.md +10 -3
- package/pipeline/commands/multi-agent/garbage-collect/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/help/SKILL.md +19 -4
- package/pipeline/commands/multi-agent/ios-coding-standard/SKILL.md +2 -2
- package/pipeline/commands/multi-agent/jira/SKILL.md +13 -2
- package/pipeline/commands/multi-agent/language/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/local/SKILL.md +6 -2
- package/pipeline/commands/multi-agent/local-autopilot/SKILL.md +6 -2
- package/pipeline/commands/multi-agent/log/SKILL.md +7 -1
- package/pipeline/commands/multi-agent/prune-prompts/SKILL.md +81 -0
- package/pipeline/commands/multi-agent/resume/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/{ship → resume-local}/SKILL.md +13 -9
- package/pipeline/commands/multi-agent/setup/SKILL.md +5 -5
- package/pipeline/commands/multi-agent/stack/SKILL.md +55 -43
- package/pipeline/commands/multi-agent/store-ready/SKILL.md +3 -3
- package/pipeline/commands/multi-agent/sync/SKILL.md +18 -11
- package/pipeline/commands/multi-agent/testflight-validation/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/uninstall/SKILL.md +2 -0
- package/pipeline/commands/multi-agent/update/SKILL.md +1 -1
- package/pipeline/lib/extract-conventions.sh +44 -15
- package/pipeline/lib/fetch-figma-annotations.sh +8 -1
- package/pipeline/lib/fetch-fortify.sh +23 -8
- package/pipeline/lib/figma-screenshot.sh +11 -1
- package/pipeline/lib/issue-fetcher.sh +77 -10
- package/pipeline/lib/md2confluence-v3.py +16 -2
- package/pipeline/lib/parse-complaints.sh +306 -0
- package/pipeline/lib/plan-todos.sh +5 -2
- package/pipeline/lib/post-pr-review.sh +8 -6
- package/pipeline/lib/shadow-git.sh +50 -9
- package/pipeline/lib/submodule-detector.sh +8 -1
- package/pipeline/multi-agent-refs/_input-parser.md +1 -1
- package/pipeline/multi-agent-refs/channels/confluence.md +3 -0
- package/pipeline/multi-agent-refs/channels/issue-comment.md +2 -2
- package/pipeline/multi-agent-refs/channels/jira.md +13 -2
- package/pipeline/multi-agent-refs/channels/pr-review-actions.md +1 -1
- package/pipeline/multi-agent-refs/channels/pr.md +20 -0
- package/pipeline/multi-agent-refs/channels/wiki.md +4 -4
- package/pipeline/multi-agent-refs/complaint-analysis-template.md +99 -0
- package/pipeline/multi-agent-refs/component-dispatch.md +6 -6
- package/pipeline/multi-agent-refs/cross-cli-contract.md +16 -16
- package/pipeline/multi-agent-refs/features/external-context-injection.md +1 -1
- package/pipeline/multi-agent-refs/features/stack-skill-routing.md +5 -5
- package/pipeline/multi-agent-refs/features/worktree-finalize.md +1 -1
- package/pipeline/multi-agent-refs/generate-issue.md +3 -3
- package/pipeline/multi-agent-refs/issue-jira-triad.md +3 -3
- package/pipeline/multi-agent-refs/payload-contracts.md +67 -0
- package/pipeline/multi-agent-refs/phases/modes.md +20 -0
- package/pipeline/multi-agent-refs/phases/phase-0-init.md +2 -2
- package/pipeline/multi-agent-refs/phases/phase-1-analysis.md +7 -7
- package/pipeline/multi-agent-refs/phases/phase-2-planning.md +5 -5
- package/pipeline/multi-agent-refs/phases/phase-3-dev.md +3 -3
- package/pipeline/multi-agent-refs/phases/phase-4-review.md +12 -12
- package/pipeline/multi-agent-refs/phases/phase-5-test.md +1 -1
- package/pipeline/multi-agent-refs/phases/phase-6-commit.md +8 -40
- package/pipeline/multi-agent-refs/phases/phase-7-report.md +5 -3
- package/pipeline/multi-agent-refs/phases.md +6 -0
- package/pipeline/multi-agent-refs/rules.md +2 -0
- package/pipeline/multi-agent-refs/tracker-contract.md +1 -1
- package/pipeline/multi-agent-refs/wiki-capture.md +2 -2
- package/pipeline/preferences-template.json +13 -5
- package/pipeline/rules/figma-pipeline.md +2 -2
- package/pipeline/schemas/agent-state.schema.json +1 -1
- package/pipeline/schemas/complaint-analysis-spec.schema.json +216 -0
- package/pipeline/schemas/migrations/prefs-2.5.0-to-2.6.0.mjs +46 -0
- package/pipeline/schemas/prefs.schema.json +277 -67
- package/pipeline/schemas/token-budget.json +2 -2
- package/pipeline/scripts/_stack-routing.mjs +79 -0
- package/pipeline/scripts/audit-log-rotate.sh +14 -1
- package/pipeline/scripts/build-skills-index.mjs +11 -0
- package/pipeline/scripts/build-stack-plugins.mjs +35 -60
- package/pipeline/scripts/check-derived-drift.mjs +65 -29
- package/pipeline/scripts/diff-explain.mjs +41 -3
- package/pipeline/scripts/diff-risk-score.mjs +72 -8
- package/pipeline/scripts/gc-worktrees.sh +4 -1
- package/pipeline/scripts/gen-mode-dispatch.mjs +1 -1
- package/pipeline/scripts/gen-skills-index.mjs +1 -1
- package/pipeline/scripts/learning-curve.mjs +8 -2
- package/pipeline/scripts/match-skills.mjs +8 -2
- package/pipeline/scripts/migrate-prefs.mjs +28 -20
- package/pipeline/scripts/output-quality-check.sh +15 -4
- package/pipeline/scripts/phase-tracker.sh +33 -12
- package/pipeline/scripts/phase0-exit-gate.mjs +3 -2
- package/pipeline/scripts/pre-commit-check.sh +69 -22
- package/pipeline/scripts/render-agent-log-cost.sh +8 -3
- package/pipeline/scripts/render-cost-summary.sh +42 -22
- package/pipeline/scripts/render-work-summary.sh +47 -13
- package/pipeline/scripts/review-scope.mjs +1 -1
- package/pipeline/scripts/run-aggregator.mjs +43 -14
- package/pipeline/scripts/scan-agent-config.sh +1 -1
- package/pipeline/scripts/skill-conformance.mjs +165 -30
- package/pipeline/scripts/smoke-cross-cli-behavior.sh +1 -1
- package/pipeline/scripts/smoke-schema-validation.sh +5 -1
- package/pipeline/scripts/test-gap-rules/android.json +25 -0
- package/pipeline/scripts/test-gap-rules/ios.json +34 -0
- package/pipeline/scripts/test-gap-rules/node.json +29 -0
- package/pipeline/scripts/test-gap-rules/python.json +25 -0
- package/pipeline/scripts/test-gap-scan.mjs +45 -6
- package/pipeline/scripts/uninstall.mjs +196 -14
- package/pipeline/scripts/update-issue-progress.sh +12 -16
- package/pipeline/scripts/validate-complaint-doc.mjs +229 -0
- package/pipeline/scripts/validate-reviewer.mjs +9 -3
- package/pipeline/scripts/worktree-finalize.sh +23 -2
- package/pipeline/skills/.skill-manifest.json +156 -108
- package/pipeline/skills/.skills-index.json +459 -13
- package/pipeline/skills/shared/README.md +15 -11
- package/pipeline/skills/shared/core/multi-agent-analysis-resolve/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-autopilot/SKILL.md +4 -0
- package/pipeline/skills/shared/core/multi-agent-build-optimize/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-complaint-analysis/SKILL.md +49 -0
- package/pipeline/skills/shared/core/multi-agent-create-jira/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-dev/SKILL.md +4 -17
- package/pipeline/skills/shared/core/multi-agent-dev-autopilot/SKILL.md +8 -0
- package/pipeline/skills/shared/core/multi-agent-dev-local/SKILL.md +5 -18
- package/pipeline/skills/shared/core/multi-agent-dev-local-autopilot/SKILL.md +8 -0
- package/pipeline/skills/shared/core/multi-agent-ios-coding-standard/SKILL.md +2 -2
- package/pipeline/skills/shared/core/multi-agent-language/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-local/SKILL.md +4 -0
- package/pipeline/skills/shared/core/multi-agent-local-autopilot/SKILL.md +4 -0
- package/pipeline/skills/shared/core/multi-agent-prune-prompts/SKILL.md +83 -0
- package/pipeline/skills/shared/core/{multi-agent-ship → multi-agent-resume-local}/SKILL.md +10 -6
- package/pipeline/skills/shared/core/multi-agent-stack/SKILL.md +79 -22
- package/pipeline/skills/shared/core/multi-agent-store-ready/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +8 -8
- package/pipeline/skills/shared/core/multi-agent-testflight-validation/SKILL.md +1 -1
- package/pipeline/skills/shared/external/ios-coding-standard/modules/_TEMPLATE.yml +2 -2
- package/pipeline/skills/shared/external/ios-coding-standard/references/rules.yml +368 -33
- package/pipeline/skills/shared/external/ios-coding-standard/references/swiftlint.draft.yml +1 -2
- package/pipeline/skills/shared/external/ios-coding-standard/scripts/check_structure.py +765 -0
- package/pipeline/skills/shared/external/ios-module-structure/SKILL.md +75 -0
- package/pipeline/skills/shared/external/ios-module-structure/modules/_TEMPLATE.yml +131 -0
- package/pipeline/skills/shared/external/ios-module-structure/references/rules.yml +559 -0
- package/pipeline/skills/shared/external/ios-module-structure/scripts/check_structure.py +765 -0
- package/pipeline/skills/shared/external/localization-reuse-map/SKILL.md +302 -0
- package/pipeline/skills/shared/external/localization-reuse-map/example-mapping.json +187 -0
- package/pipeline/skills/shared/external/localization-reuse-map/reference/format-and-output.md +156 -0
- package/pipeline/skills/shared/external/localization-reuse-map/reference/publish-and-snapshot.md +108 -0
- package/pipeline/skills/shared/external/localization-reuse-map/reference/sources-and-recipes.md +176 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-artifact.py +865 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-spreadsheet.py +335 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-annotations.py +344 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-legacy-labels.py +130 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/publish-confluence.py +264 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-key-shots.py +298 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-overlay.py +529 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-legacy-values.py +187 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-new-values.py +171 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/scan-screen-keys.py +184 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/snapshot-resources.sh +26 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/verify-map.py +173 -0
- 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
|
|
108
|
-
if (
|
|
109
|
-
cur =
|
|
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
|
|
193
|
-
if (
|
|
194
|
-
cur =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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(
|
|
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(
|
|
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
|
-
//
|
|
336
|
-
//
|
|
337
|
-
//
|
|
338
|
-
if (!out.global.
|
|
339
|
-
out.global.
|
|
340
|
-
changes.push("added
|
|
341
|
-
}
|
|
342
|
-
if (typeof out.global.
|
|
343
|
-
const legacyAutoFix =
|
|
344
|
-
|
|
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
|
|
348
|
-
: "added
|
|
353
|
+
? "moved ship/finish autoFix -> resumeLocal.autoFix (value preserved)"
|
|
354
|
+
: "added resumeLocal.autoFix=false",
|
|
349
355
|
);
|
|
350
356
|
}
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
"
|
|
263
|
-
"
|
|
264
|
-
"
|
|
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:
|
|
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" "{\"
|
|
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" "{\"
|
|
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
|
-
|
|
770
|
-
|
|
771
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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" "" "{\"
|
|
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))
|
|
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(
|
|
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
|
-
#
|
|
54
|
-
#
|
|
55
|
-
#
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
123
|
-
#
|
|
124
|
-
#
|
|
125
|
-
#
|
|
126
|
-
#
|
|
127
|
-
#
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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; }
|