@mmerterden/multi-agent-pipeline 14.2.1 → 14.2.2
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 +42 -0
- package/README.md +10 -6
- package/README.tr.md +143 -0
- package/docs/architecture.md +23 -8
- package/docs/ecosystem.md +237 -0
- package/install/_plugin-skills.mjs +16 -2
- package/install/codex.mjs +9 -4
- package/install/templates/copilot-instructions.md +12 -9
- package/package.json +1 -1
- package/pipeline/commands/multi-agent/SKILL.md +2 -2
- package/pipeline/commands/multi-agent/analysis/SKILL.md +2 -2
- package/pipeline/commands/multi-agent/autopilot/SKILL.md +6 -2
- package/pipeline/commands/multi-agent/channels/SKILL.md +15 -4
- 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/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/ship/SKILL.md +5 -1
- package/pipeline/commands/multi-agent/sync/SKILL.md +3 -3
- 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 +76 -9
- package/pipeline/lib/md2confluence-v3.py +16 -2
- 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 +1 -1
- package/pipeline/multi-agent-refs/cross-cli-contract.md +3 -3
- package/pipeline/multi-agent-refs/features/worktree-finalize.md +1 -1
- package/pipeline/multi-agent-refs/generate-issue.md +2 -2
- 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 +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/schemas/prefs.schema.json +1 -1
- package/pipeline/scripts/audit-log-rotate.sh +10 -0
- package/pipeline/scripts/build-stack-plugins.mjs +8 -1
- package/pipeline/scripts/check-derived-drift.mjs +13 -1
- package/pipeline/scripts/diff-explain.mjs +41 -3
- package/pipeline/scripts/diff-risk-score.mjs +72 -8
- package/pipeline/scripts/gen-mode-dispatch.mjs +1 -1
- package/pipeline/scripts/learning-curve.mjs +8 -2
- package/pipeline/scripts/output-quality-check.sh +15 -4
- package/pipeline/scripts/phase-tracker.sh +21 -8
- 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 +38 -14
- package/pipeline/scripts/smoke-schema-validation.sh +5 -1
- package/pipeline/scripts/test-gap-scan.mjs +45 -6
- package/pipeline/scripts/uninstall.mjs +39 -4
- package/pipeline/scripts/update-issue-progress.sh +12 -16
- package/pipeline/scripts/worktree-finalize.sh +23 -2
- package/pipeline/skills/.skills-index.json +11 -2
- package/pipeline/skills/shared/README.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-autopilot/SKILL.md +4 -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-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-ship/SKILL.md +4 -0
- package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +2 -2
- package/pipeline/skills/shared/external/localization-reuse-map/SKILL.md +302 -0
- package/pipeline/skills/shared/external/localization-reuse-map/example-mapping.json +144 -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 +175 -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 +3 -2
|
@@ -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;
|
|
@@ -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)
|
|
@@ -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);
|
|
@@ -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
|
|
@@ -728,7 +728,7 @@ case "$ACTION" in
|
|
|
728
728
|
')
|
|
729
729
|
save_state "$new"
|
|
730
730
|
release_state_lock
|
|
731
|
-
emit_otel_span "phase.update" "$PID" "$STATUS" "{\"
|
|
731
|
+
emit_otel_span "phase.update" "$PID" "$STATUS" "{\"status\": \"$STATUS\"}"
|
|
732
732
|
render
|
|
733
733
|
;;
|
|
734
734
|
|
|
@@ -757,7 +757,7 @@ case "$ACTION" in
|
|
|
757
757
|
)')
|
|
758
758
|
save_state "$new"
|
|
759
759
|
release_state_lock
|
|
760
|
-
emit_otel_span "phase.sub" "$PID" "$SNAME" "{\"
|
|
760
|
+
emit_otel_span "phase.sub" "$PID" "$SNAME" "{\"sub_id\": \"$SID\", \"status\": \"$SSTATUS\"}"
|
|
761
761
|
render
|
|
762
762
|
;;
|
|
763
763
|
|
|
@@ -783,7 +783,20 @@ case "$ACTION" in
|
|
|
783
783
|
')
|
|
784
784
|
save_state "$new"
|
|
785
785
|
release_state_lock
|
|
786
|
-
|
|
786
|
+
# Carry the phase's current model/name onto the span too: the cost
|
|
787
|
+
# renderers' OTel-fallback path groups phase.tokens spans by phase_id and
|
|
788
|
+
# reads model/phase_name off the first span in each group, so without
|
|
789
|
+
# these the fallback always renders the default model/"?" name even
|
|
790
|
+
# though the tracker JSON already knows both.
|
|
791
|
+
PHASE_MODEL_ATTR=$(printf '%s' "$new" | jq -r --arg id "$PID" '(.phases[] | select(.id == $id) | .model) // ""')
|
|
792
|
+
PHASE_NAME_ATTR=$(printf '%s' "$new" | jq -r --arg id "$PID" '(.phases[] | select(.id == $id) | .name) // ""')
|
|
793
|
+
TOKENS_EXTRA=$(jq -nc \
|
|
794
|
+
--argjson ti "$T_IN" --argjson to "$T_OUT" --argjson tc "$T_CACHED" \
|
|
795
|
+
--arg model "$PHASE_MODEL_ATTR" --arg name "$PHASE_NAME_ATTR" \
|
|
796
|
+
'{tokens_in_delta: $ti, tokens_out_delta: $to, tokens_cached_delta: $tc}
|
|
797
|
+
+ (if $model != "" then {model: $model} else {} end)
|
|
798
|
+
+ (if $name != "" then {phase_name: $name} else {} end)')
|
|
799
|
+
emit_otel_span "phase.tokens" "$PID" "" "$TOKENS_EXTRA"
|
|
787
800
|
# Re-render so live token count is visible on the active phase. Callers
|
|
788
801
|
# that want silent accumulation (e.g. tight loops) can suppress with
|
|
789
802
|
# TRACKER_QUIET=1 to skip the re-render but still write state.
|
|
@@ -806,7 +819,7 @@ case "$ACTION" in
|
|
|
806
819
|
)')
|
|
807
820
|
save_state "$new"
|
|
808
821
|
release_state_lock
|
|
809
|
-
emit_otel_span "phase.meta" "$PID" "$KEY" "{\"
|
|
822
|
+
emit_otel_span "phase.meta" "$PID" "$KEY" "{\"meta_key\": \"$KEY\", \"meta_value\": \"$VALUE\"}"
|
|
810
823
|
if [ "${TRACKER_QUIET:-0}" != "1" ]; then
|
|
811
824
|
render
|
|
812
825
|
fi
|
|
@@ -824,7 +837,7 @@ case "$ACTION" in
|
|
|
824
837
|
)')
|
|
825
838
|
save_state "$new"
|
|
826
839
|
release_state_lock
|
|
827
|
-
emit_otel_span "phase.model" "$PID" "" "{\"
|
|
840
|
+
emit_otel_span "phase.model" "$PID" "" "{\"model\": \"$MODEL\"}"
|
|
828
841
|
if [ "${TRACKER_QUIET:-0}" != "1" ]; then
|
|
829
842
|
render
|
|
830
843
|
fi
|
|
@@ -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; }
|
|
@@ -24,23 +24,32 @@ while [ $# -gt 0 ]; do
|
|
|
24
24
|
esac
|
|
25
25
|
done
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
# cost-table.json/cost-lib.sh ship as siblings of this script both in the
|
|
28
|
+
# repo (pipeline/scripts/) and once installed (~/.claude/scripts/) - a
|
|
29
|
+
# repo-root-relative path (../../pipeline/scripts/...) only resolves in the
|
|
30
|
+
# repo checkout and silently misses in the installed layout. Match
|
|
31
|
+
# phase-tracker.sh's own resolution instead of re-deriving a repo root.
|
|
32
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
33
|
+
COST_TABLE="$SCRIPT_DIR/cost-table.json"
|
|
34
|
+
COST_LIB="$SCRIPT_DIR/cost-lib.sh"
|
|
30
35
|
if [ -f "$COST_LIB" ]; then . "$COST_LIB"; else COST_JQ_DEFS=""; fi
|
|
31
36
|
|
|
32
37
|
command -v jq >/dev/null 2>&1 || { echo "render-cost-summary: jq required" >&2; exit 2; }
|
|
33
38
|
|
|
34
|
-
# Tracker JSON
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
# the
|
|
39
|
+
# Tracker JSON candidates, worktree convention first, then the location
|
|
40
|
+
# phase-tracker.sh actually writes ($HOME/.claude/logs/multi-agent/<task>/
|
|
41
|
+
# tracker-state.json). The list must stay in lockstep with
|
|
42
|
+
# render-agent-log-cost.sh - before the logs/ candidates were added here,
|
|
43
|
+
# every standard run exited 2 and the channels Cost Summary silently never
|
|
44
|
+
# rendered, because only the worktree paths were tried.
|
|
38
45
|
tracker_file=""
|
|
39
46
|
task_id_bare="${TASK_ID##*-}"
|
|
40
47
|
for candidate in \
|
|
41
48
|
"$PWD/.worktrees/$TASK_ID/phase-tracker.json" \
|
|
42
49
|
"$PWD/.worktrees/$task_id_bare/phase-tracker.json" \
|
|
43
|
-
"$PWD/.worktrees/task-$task_id_bare/phase-tracker.json"
|
|
50
|
+
"$PWD/.worktrees/task-$task_id_bare/phase-tracker.json" \
|
|
51
|
+
"$HOME/.claude/logs/multi-agent/$TASK_ID/tracker-state.json" \
|
|
52
|
+
"$HOME/.claude/logs/multi-agent/$task_id_bare/tracker-state.json"
|
|
44
53
|
do
|
|
45
54
|
[ -f "$candidate" ] && { tracker_file="$candidate"; break; }
|
|
46
55
|
done
|
|
@@ -52,17 +61,27 @@ fi
|
|
|
52
61
|
# Build rows - prefer tracker JSON, fall back to spans aggregation
|
|
53
62
|
rows_json="[]"
|
|
54
63
|
if [ -n "$tracker_file" ]; then
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
# Tracker JSON ships in two shapes (same contract as render-agent-log-cost.sh):
|
|
65
|
+
# array shape - phase-tracker.sh writes phases:[{id, name, model, tokens_in, ...}]
|
|
66
|
+
# object shape - older fixture / hand-built variant phases:{ "1": {...}, ... }
|
|
67
|
+
rows_json=$(jq '
|
|
68
|
+
(if (.phases | type) == "array" then
|
|
69
|
+
[.phases[] | {key: (.id | tostring), value: .}]
|
|
70
|
+
else
|
|
71
|
+
(.phases // {} | to_entries)
|
|
72
|
+
end)
|
|
73
|
+
| [ .[] |
|
|
74
|
+
select(.value.tokens_in // 0 > 0 or .value.tokens_out // 0 > 0) |
|
|
75
|
+
{
|
|
76
|
+
phase: .key,
|
|
77
|
+
phase_name: (.value.name // .key),
|
|
78
|
+
model: (.value.model // " - "),
|
|
79
|
+
tokens_in: (.value.tokens_in // 0),
|
|
80
|
+
tokens_out: (.value.tokens_out // 0),
|
|
81
|
+
tokens_cached: (.value.tokens_cached // 0)
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
| sort_by(.phase | tonumber? // 999)' "$tracker_file")
|
|
66
85
|
fi
|
|
67
86
|
|
|
68
87
|
if [ "$rows_json" = "[]" ] && [ -n "$SPANS_FILE" ] && [ -f "$SPANS_FILE" ]; then
|
|
@@ -72,9 +91,10 @@ if [ "$rows_json" = "[]" ] && [ -n "$SPANS_FILE" ] && [ -f "$SPANS_FILE" ]; then
|
|
|
72
91
|
map({
|
|
73
92
|
phase: (.[0].attributes.phase_id // .[0].attributes.phase // "?"),
|
|
74
93
|
phase_name: (.[0].attributes.phase_name // (.[0].attributes.phase_id // "?")),
|
|
75
|
-
model: (.[0].attributes.model // "
|
|
94
|
+
model: (.[0].attributes.model // " - "),
|
|
76
95
|
tokens_in: ([.[].attributes.tokens_in_delta // 0] | add),
|
|
77
|
-
tokens_out: ([.[].attributes.tokens_out_delta // 0] | add)
|
|
96
|
+
tokens_out: ([.[].attributes.tokens_out_delta // 0] | add),
|
|
97
|
+
tokens_cached: ([.[].attributes.tokens_cached_delta // 0] | add)
|
|
78
98
|
})
|
|
79
99
|
' "$SPANS_FILE")
|
|
80
100
|
fi
|
|
@@ -85,7 +105,7 @@ joined=$(jq -n --argjson rows "$rows_json" --slurpfile prices "$COST_TABLE" "$CO
|
|
|
85
105
|
. as $r |
|
|
86
106
|
($prices[0].prices[$r.model] // null) as $p |
|
|
87
107
|
. + {
|
|
88
|
-
usd: cost_usd_of($p; $r.tokens_in; $r.tokens_out; 0)
|
|
108
|
+
usd: cost_usd_of($p; $r.tokens_in; $r.tokens_out; ($r.tokens_cached // 0))
|
|
89
109
|
}
|
|
90
110
|
)
|
|
91
111
|
')
|
|
@@ -69,9 +69,11 @@ fi
|
|
|
69
69
|
|
|
70
70
|
STATE_FILE=""
|
|
71
71
|
TRACKER_FILE=""
|
|
72
|
+
TRIAGE_FILE=""
|
|
72
73
|
if [ -n "$WORKTREE" ]; then
|
|
73
|
-
[ -f "$WORKTREE/agent-state.json" ]
|
|
74
|
-
[ -f "$WORKTREE/phase-tracker.json" ]
|
|
74
|
+
[ -f "$WORKTREE/agent-state.json" ] && STATE_FILE="$WORKTREE/agent-state.json"
|
|
75
|
+
[ -f "$WORKTREE/phase-tracker.json" ] && TRACKER_FILE="$WORKTREE/phase-tracker.json"
|
|
76
|
+
[ -f "$WORKTREE/triage-output.json" ] && TRIAGE_FILE="$WORKTREE/triage-output.json"
|
|
75
77
|
fi
|
|
76
78
|
|
|
77
79
|
# Fall back to the salvaged artefacts when the worktree is gone. Phase 6 removes it
|
|
@@ -88,8 +90,22 @@ if [ -z "$STATE_FILE" ] || [ -z "$TRACKER_FILE" ]; then
|
|
|
88
90
|
"$HOME"/.claude/logs/multi-agent/*/"$task_bare"/artifacts
|
|
89
91
|
do
|
|
90
92
|
[ -d "$base" ] || continue
|
|
91
|
-
[ -z "$STATE_FILE" ] && [ -f "$base/agent-state.json" ]
|
|
92
|
-
[ -z "$TRACKER_FILE" ] && [ -f "$base/phase-tracker.json" ]
|
|
93
|
+
[ -z "$STATE_FILE" ] && [ -f "$base/agent-state.json" ] && STATE_FILE="$base/agent-state.json"
|
|
94
|
+
[ -z "$TRACKER_FILE" ] && [ -f "$base/phase-tracker.json" ] && TRACKER_FILE="$base/phase-tracker.json"
|
|
95
|
+
[ -z "$TRIAGE_FILE" ] && [ -f "$base/triage-output.json" ] && TRIAGE_FILE="$base/triage-output.json"
|
|
96
|
+
done
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
# Last resort for the tracker: the file phase-tracker.sh actually writes on
|
|
100
|
+
# every run. The worktree/artifacts copies above exist only when something
|
|
101
|
+
# mirrored them there; the logs/ state file always exists for a tracked task.
|
|
102
|
+
if [ -z "$TRACKER_FILE" ]; then
|
|
103
|
+
task_bare="${TASK_ID##*-}"
|
|
104
|
+
for f in \
|
|
105
|
+
"$HOME/.claude/logs/multi-agent/$TASK_ID/tracker-state.json" \
|
|
106
|
+
"$HOME/.claude/logs/multi-agent/$task_bare/tracker-state.json"
|
|
107
|
+
do
|
|
108
|
+
[ -f "$f" ] && { TRACKER_FILE="$f"; break; }
|
|
93
109
|
done
|
|
94
110
|
fi
|
|
95
111
|
|
|
@@ -126,9 +142,15 @@ if [ -n "$STATE_FILE" ]; then
|
|
|
126
142
|
state_jira=$(jq -r '.taskId // ""' "$STATE_FILE")
|
|
127
143
|
state_branch=$(jq -r '.branch // (.projects // [] | .[0].branch // "")' "$STATE_FILE")
|
|
128
144
|
state_base=$(jq -r '.baseBranch // (.projects // [] | .[0].baseBranch // "")' "$STATE_FILE")
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
145
|
+
# agent-state.schema.json has no .prNumber/.prUrl/.planTodos/.reviewConsensus/
|
|
146
|
+
# .phases["2"].tasks/.phases["4"].triage - PR data lives at .pr.{number,url},
|
|
147
|
+
# the plan at .plan.todos[], and review triage in the separate
|
|
148
|
+
# triage-output.json (read via TRIAGE_FILE below, not out of agent-state).
|
|
149
|
+
state_pr=$(jq -r '.pr.number // .pr.url // ""' "$STATE_FILE")
|
|
150
|
+
state_tasks=$(jq -c '.plan.todos // []' "$STATE_FILE" 2>/dev/null || echo "[]")
|
|
151
|
+
fi
|
|
152
|
+
if [ -n "$TRIAGE_FILE" ]; then
|
|
153
|
+
state_triage=$(jq -c '.' "$TRIAGE_FILE" 2>/dev/null || echo "{}")
|
|
132
154
|
fi
|
|
133
155
|
|
|
134
156
|
# Flag overrides
|
|
@@ -141,13 +163,19 @@ fi
|
|
|
141
163
|
phase_ticks=""
|
|
142
164
|
if [ -n "$TRACKER_FILE" ]; then
|
|
143
165
|
phase_ticks=$(jq -r '
|
|
144
|
-
. as
|
|
166
|
+
# Normalize both tracker shapes: phase-tracker.sh writes phases as an
|
|
167
|
+
# array of {id,...}; older fixtures keyed an object by phase id. The
|
|
168
|
+
# string-index below errors on an array and 2>/dev/null would swallow
|
|
169
|
+
# it, silently dropping the whole tick strip.
|
|
170
|
+
(if (.phases | type) == "array"
|
|
171
|
+
then (.phases | map({key: (.id | tostring), value: .}) | from_entries)
|
|
172
|
+
else (.phases // {}) end) as $ph |
|
|
145
173
|
["0 Init","1 Analysis","2 Planning","3 Dev","4 Review","5 Test","6 Commit","7 Report"] as $labels |
|
|
146
174
|
[range(0;8) | tostring] as $ids |
|
|
147
175
|
$ids
|
|
148
176
|
| map(. as $i |
|
|
149
177
|
($labels[$i | tonumber]) as $label |
|
|
150
|
-
(
|
|
178
|
+
($ph[$i] // {}) as $p |
|
|
151
179
|
(if $p.status == "completed" then "✅"
|
|
152
180
|
elif $p.status == "in_progress" then "▶"
|
|
153
181
|
elif $p.status == "failed" then "❌"
|
|
@@ -195,16 +223,19 @@ if [ -n "$DIFF_IN" ]; then
|
|
|
195
223
|
fi
|
|
196
224
|
fi
|
|
197
225
|
|
|
198
|
-
# Task list with ✅ / ⏳ marks
|
|
226
|
+
# Task list with ✅ / ⏳ marks. plan-todos.schema.json's todo items carry
|
|
227
|
+
# `.task` (not `.title`/`.subject`) and a status enum of
|
|
228
|
+
# pending|in_progress|completed|skipped|failed (not "done"/"deferred") -
|
|
229
|
+
# the old check here never matched real data, so every todo rendered ⏳.
|
|
199
230
|
tasks_block=""
|
|
200
231
|
if [ "$(jq 'length' <<< "$state_tasks")" != "0" ]; then
|
|
201
232
|
tasks_block=$(jq -r '
|
|
202
233
|
.[] |
|
|
203
234
|
"- " +
|
|
204
|
-
(if (.status // "
|
|
235
|
+
(if (.status // "pending") == "completed" then "✅ " else "⏳ " end) +
|
|
205
236
|
(.id // "") +
|
|
206
237
|
(if (.id // "") != "" then " " else "" end) +
|
|
207
|
-
(.
|
|
238
|
+
(.task // "(untitled)")
|
|
208
239
|
' <<< "$state_tasks")
|
|
209
240
|
fi
|
|
210
241
|
|
|
@@ -221,7 +252,10 @@ header="**Task**: \`$state_jira\`"
|
|
|
221
252
|
[ -n "$state_branch" ] && header="$header | **Branch**: \`$state_branch\`"
|
|
222
253
|
[ -n "$state_base" ] && header="$header | **Base**: \`$state_base\`"
|
|
223
254
|
if [ -n "$state_pr" ] && [ "$state_pr" != "null" ]; then
|
|
224
|
-
|
|
255
|
+
case "$state_pr" in
|
|
256
|
+
*[!0-9]*) header="$header | **PR**: $state_pr" ;;
|
|
257
|
+
*) header="$header | **PR**: #$state_pr" ;;
|
|
258
|
+
esac
|
|
225
259
|
fi
|
|
226
260
|
printf '%s\n\n' "$header"
|
|
227
261
|
|
|
@@ -81,7 +81,7 @@ function main() {
|
|
|
81
81
|
const blockers = [];
|
|
82
82
|
for (const f of files) {
|
|
83
83
|
for (const s of f.signals || []) {
|
|
84
|
-
if (HIGH_STAKES.has(s.name)) blockers.push({ file: f.
|
|
84
|
+
if (HIGH_STAKES.has(s.name)) blockers.push({ file: f.path, signal: s.name });
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
87
|
|