@mmerterden/multi-agent-pipeline 14.1.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 +162 -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/deploy.md +4 -1
- package/pipeline/commands/multi-agent/SKILL.md +8 -5
- 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/help/SKILL.md +49 -11
- 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/setup/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/ship/SKILL.md +5 -1
- package/pipeline/commands/multi-agent/store-ready/SKILL.md +340 -0
- package/pipeline/commands/multi-agent/sync/SKILL.md +7 -6
- package/pipeline/commands/multi-agent/test/SKILL.md +18 -8
- package/pipeline/commands/multi-agent/test-accessibility/SKILL.md +33 -0
- package/pipeline/commands/multi-agent/test-dark-mode/SKILL.md +33 -0
- package/pipeline/commands/multi-agent/test-dynamic-type/SKILL.md +33 -0
- package/pipeline/commands/multi-agent/test-screenshots/SKILL.md +41 -0
- package/pipeline/commands/multi-agent/testflight-validation/SKILL.md +28 -201
- package/pipeline/commands/multi-agent/update/SKILL.md +1 -1
- package/pipeline/commands/sim-test.md +45 -36
- 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 +6 -5
- 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/knowledge.md +1 -1
- 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-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 +2 -2
- 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 +58 -4
- package/pipeline/skills/shared/README.md +12 -7
- package/pipeline/skills/shared/core/multi-agent/SKILL.md +13 -17
- 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-help/SKILL.md +50 -12
- 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-purge/SKILL.md +18 -3
- package/pipeline/skills/shared/core/multi-agent-ship/SKILL.md +4 -0
- package/pipeline/skills/shared/core/multi-agent-store-ready/SKILL.md +50 -0
- package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +6 -5
- package/pipeline/skills/shared/core/multi-agent-test/SKILL.md +18 -8
- package/pipeline/skills/shared/core/multi-agent-test-accessibility/SKILL.md +37 -0
- package/pipeline/skills/shared/core/multi-agent-test-dark-mode/SKILL.md +37 -0
- package/pipeline/skills/shared/core/multi-agent-test-dynamic-type/SKILL.md +37 -0
- package/pipeline/skills/shared/core/multi-agent-test-screenshots/SKILL.md +44 -0
- package/pipeline/skills/shared/core/multi-agent-testflight-validation/SKILL.md +29 -101
- package/pipeline/skills/shared/external/firebase/SKILL.md +1 -1
- 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 +10 -4
|
@@ -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
|
|
|
@@ -82,14 +82,27 @@ function die(msg) {
|
|
|
82
82
|
process.exit(1);
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
85
|
+
// phase-tracker.sh writes tracker-state.json (never phase-tracker.json) at
|
|
86
|
+
// $HOME/.claude/logs/multi-agent/<task-id>/ (no <project> segment) - the
|
|
87
|
+
// same drift render-cost-summary.sh/render-agent-log-cost.sh already guard
|
|
88
|
+
// against with a candidate list. Mirror that list here instead of assuming
|
|
89
|
+
// one fixed filename/path.
|
|
90
|
+
function resolveTrackerCandidates() {
|
|
91
|
+
const candidates = [];
|
|
92
|
+
if (flags["task-dir"]) {
|
|
93
|
+
candidates.push(join(flags["task-dir"], "tracker-state.json"));
|
|
94
|
+
}
|
|
95
|
+
if (flags["task-id"]) {
|
|
88
96
|
const HOME = process.env.HOME || process.env.USERPROFILE;
|
|
89
97
|
if (!HOME) die("neither HOME nor USERPROFILE is set - pass --task-dir");
|
|
90
|
-
|
|
98
|
+
const taskId = String(flags["task-id"]);
|
|
99
|
+
if (flags.project) {
|
|
100
|
+
candidates.push(join(HOME, ".claude", "logs", "multi-agent", flags.project, taskId, "tracker-state.json"));
|
|
101
|
+
}
|
|
102
|
+
candidates.push(join(HOME, ".claude", "logs", "multi-agent", taskId, "tracker-state.json"));
|
|
91
103
|
}
|
|
92
|
-
die("either --task-dir or --task-id
|
|
104
|
+
if (!candidates.length) die("either --task-dir or --task-id is required");
|
|
105
|
+
return candidates;
|
|
93
106
|
}
|
|
94
107
|
|
|
95
108
|
function loadJson(path) {
|
|
@@ -161,11 +174,14 @@ function tsCompare(a, b) {
|
|
|
161
174
|
}
|
|
162
175
|
|
|
163
176
|
// ─────────────────────────────────────────────────────────────────────────
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
177
|
+
const trackerCandidates = resolveTrackerCandidates();
|
|
178
|
+
const trackerPath = trackerCandidates.find((p) => existsSync(p));
|
|
179
|
+
if (!trackerPath) {
|
|
180
|
+
die(`tracker state not found, tried:\n${trackerCandidates.map((p) => ` ${p}`).join("\n")}`);
|
|
181
|
+
}
|
|
182
|
+
const taskDir = dirname(trackerPath);
|
|
183
|
+
const tracker = loadJson(trackerPath);
|
|
184
|
+
if (!tracker) die(`failed to parse tracker state at ${trackerPath}`);
|
|
169
185
|
|
|
170
186
|
const otelSpans = loadJsonl(join(taskDir, "otel-spans.jsonl"));
|
|
171
187
|
const costTable = loadCostTable();
|
|
@@ -187,7 +203,15 @@ let totalOut = 0;
|
|
|
187
203
|
let totalCost = 0;
|
|
188
204
|
let unknownModelHits = 0;
|
|
189
205
|
|
|
190
|
-
|
|
206
|
+
// tracker.phases ships in two shapes (same contract render-cost-summary.sh
|
|
207
|
+
// normalizes): an array (phase-tracker.sh's real output) or an object keyed
|
|
208
|
+
// by phase id (older fixtures / hand-built variants). Normalize once so the
|
|
209
|
+
// rest of this file can treat it as a plain array.
|
|
210
|
+
const phaseList = Array.isArray(tracker.phases)
|
|
211
|
+
? tracker.phases
|
|
212
|
+
: Object.entries(tracker.phases || {}).map(([key, value]) => ({ ...value, id: value?.id ?? key }));
|
|
213
|
+
|
|
214
|
+
for (const phase of phaseList) {
|
|
191
215
|
const tIn = phase.tokens_in || 0;
|
|
192
216
|
const tOut = phase.tokens_out || 0;
|
|
193
217
|
const model = phase.model || inferModelForPhase(phase.id);
|
|
@@ -279,9 +303,9 @@ const summary = {
|
|
|
279
303
|
kind: "summary",
|
|
280
304
|
ts: lastTs,
|
|
281
305
|
task_id: taskId,
|
|
282
|
-
phases_total:
|
|
283
|
-
phases_completed:
|
|
284
|
-
phases_failed:
|
|
306
|
+
phases_total: phaseList.length,
|
|
307
|
+
phases_completed: phaseList.filter((p) => p.status === "completed").length,
|
|
308
|
+
phases_failed: phaseList.filter((p) => p.status === "failed").length,
|
|
285
309
|
tokens_in: totalIn,
|
|
286
310
|
tokens_out: totalOut,
|
|
287
311
|
total_cost_usd: Number(totalCost.toFixed(6)),
|
|
@@ -91,10 +91,14 @@ echo "→ 4. prefs.schema.json - keychainMapping covers every resolved service
|
|
|
91
91
|
# A floor, not a pin: new credentials get added over time, and pinning the count
|
|
92
92
|
# made every addition a false failure. What matters is that the keys shipped
|
|
93
93
|
# flows resolve are all declared.
|
|
94
|
+
# process.stdout.write of a String, not console.log of a Number: console.log
|
|
95
|
+
# inspects non-string arguments, so with FORCE_COLOR set in the environment the
|
|
96
|
+
# count comes back wrapped in ANSI codes and the -ge comparison below silently
|
|
97
|
+
# fails on a value that is actually in range.
|
|
94
98
|
KM_COUNT=$(node -e "
|
|
95
99
|
const s = JSON.parse(require('fs').readFileSync('$PREFS_SCHEMA', 'utf8'));
|
|
96
100
|
const km = s.properties.global.properties.keychainMapping.properties;
|
|
97
|
-
|
|
101
|
+
process.stdout.write(String(Object.keys(km).length));
|
|
98
102
|
" 2>/dev/null)
|
|
99
103
|
if [ "${KM_COUNT:-0}" -ge 12 ] 2>/dev/null; then
|
|
100
104
|
pass "keychainMapping declares $KM_COUNT services (≥12 expected)"
|
|
@@ -141,6 +141,45 @@ function isExcluded(path) {
|
|
|
141
141
|
return false;
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
+
// git quotes a `diff --git` path (core.quotePath, on by default) whenever it
|
|
145
|
+
// has a non-ASCII byte or other unusual character - e.g. a Turkish filename
|
|
146
|
+
// renders as `diff --git "a/\303\226deme.swift" "b/\303\226deme.swift"`, not
|
|
147
|
+
// the bare a/...b/... form. The plain regex this replaced never matched that
|
|
148
|
+
// line at all, so every non-ASCII-named file's added lines were silently
|
|
149
|
+
// dropped from the test-gap scan.
|
|
150
|
+
const DIFF_GIT_QUOTED_RE = /^diff --git "a\/((?:[^"\\]|\\.)*)" "b\/((?:[^"\\]|\\.)*)"$/;
|
|
151
|
+
const DIFF_GIT_UNQUOTED_RE = /^diff --git a\/(.+?) b\/(.+)$/;
|
|
152
|
+
|
|
153
|
+
function unquoteGitPath(s) {
|
|
154
|
+
const bytes = [];
|
|
155
|
+
for (let i = 0; i < s.length; i++) {
|
|
156
|
+
if (s[i] === "\\") {
|
|
157
|
+
const octal = s.slice(i + 1, i + 4);
|
|
158
|
+
if (/^[0-7]{3}$/.test(octal)) {
|
|
159
|
+
bytes.push(parseInt(octal, 8));
|
|
160
|
+
i += 3;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const simple = { "\\": 92, '"': 34, t: 9, n: 10, r: 13 };
|
|
164
|
+
const next = s[i + 1];
|
|
165
|
+
if (next in simple) {
|
|
166
|
+
bytes.push(simple[next]);
|
|
167
|
+
i += 1;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
bytes.push(s.charCodeAt(i));
|
|
172
|
+
}
|
|
173
|
+
return Buffer.from(bytes).toString("utf-8");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function matchDiffGitHeader(line) {
|
|
177
|
+
const q = line.match(DIFF_GIT_QUOTED_RE);
|
|
178
|
+
if (q) return unquoteGitPath(q[2]);
|
|
179
|
+
const u = line.match(DIFF_GIT_UNQUOTED_RE);
|
|
180
|
+
return u ? u[2] : null;
|
|
181
|
+
}
|
|
182
|
+
|
|
144
183
|
function isTestPath(path) {
|
|
145
184
|
if (Array.isArray(rules.testPathSuffixes) && rules.testPathSuffixes.some((s) => path.endsWith(s)))
|
|
146
185
|
return true;
|
|
@@ -190,9 +229,9 @@ function parseDiffAddedLines(diffText) {
|
|
|
190
229
|
const perFile = new Map();
|
|
191
230
|
let cur = null;
|
|
192
231
|
for (const line of diffText.split("\n")) {
|
|
193
|
-
const
|
|
194
|
-
if (
|
|
195
|
-
cur =
|
|
232
|
+
const p = matchDiffGitHeader(line);
|
|
233
|
+
if (p !== null) {
|
|
234
|
+
cur = p;
|
|
196
235
|
if (!perFile.has(cur)) perFile.set(cur, { lines: [], isNew: false });
|
|
197
236
|
continue;
|
|
198
237
|
}
|
|
@@ -300,9 +339,9 @@ function symbolMentionedInTest(diffText, symbol) {
|
|
|
300
339
|
const lines = diffText.split("\n");
|
|
301
340
|
let inTestFile = false;
|
|
302
341
|
for (const line of lines) {
|
|
303
|
-
const fm = line
|
|
304
|
-
if (fm) {
|
|
305
|
-
inTestFile = isTestPath(fm
|
|
342
|
+
const fm = matchDiffGitHeader(line);
|
|
343
|
+
if (fm !== null) {
|
|
344
|
+
inTestFile = isTestPath(fm);
|
|
306
345
|
continue;
|
|
307
346
|
}
|
|
308
347
|
if (!inTestFile) continue;
|