@mmerterden/multi-agent-pipeline 11.3.2 → 11.5.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 +141 -0
- package/README.md +1 -0
- package/index.js +9 -2
- package/install/_common.mjs +107 -5
- package/install/_telemetry.mjs +5 -23
- package/install/claude.mjs +113 -11
- package/install/copilot.mjs +75 -6
- package/package.json +4 -10
- package/pipeline/commands/multi-agent/SKILL.md +5 -1
- package/pipeline/commands/multi-agent/dev-local/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/help/SKILL.md +6 -2
- package/pipeline/commands/multi-agent/review/SKILL.md +36 -4
- package/pipeline/commands/multi-agent/review-issue/SKILL.md +22 -0
- package/pipeline/commands/multi-agent/review-jira/SKILL.md +22 -0
- package/pipeline/commands/multi-agent/sync/SKILL.md +4 -4
- package/pipeline/lib/account-resolver.sh +61 -23
- package/pipeline/lib/channels-multi-repo.sh +7 -2
- package/pipeline/lib/count-lib.sh +27 -0
- package/pipeline/lib/credential-store.sh +23 -6
- package/pipeline/lib/extract-conventions.sh +27 -21
- package/pipeline/lib/fetch-confluence.sh +25 -13
- package/pipeline/lib/fetch-crashlytics.sh +30 -8
- package/pipeline/lib/fetch-fortify.sh +11 -2
- package/pipeline/lib/fetch-swagger.sh +18 -7
- package/pipeline/lib/figma-mcp-refresh.sh +26 -11
- package/pipeline/lib/figma-screenshot.sh +11 -2
- package/pipeline/lib/issue-fetcher.sh +31 -6
- package/pipeline/lib/multi-repo-pipeline.sh +84 -20
- package/pipeline/lib/plan-todos.sh +12 -3
- package/pipeline/lib/post-pr-review.sh +5 -3
- package/pipeline/lib/repo-cache.sh +53 -9
- package/pipeline/lib/review-watch.sh +26 -6
- package/pipeline/lib/shadow-git.sh +45 -4
- package/pipeline/lib/vercel-deploy.sh +10 -1
- package/pipeline/multi-agent-refs/cross-cli-contract.md +4 -4
- package/pipeline/multi-agent-refs/readiness-review.md +65 -0
- package/pipeline/scripts/audit-log-rotate.sh +38 -10
- package/pipeline/scripts/check-md-links.mjs +34 -9
- package/pipeline/scripts/diff-risk-score.mjs +4 -1
- package/pipeline/scripts/evidence-gate.mjs +10 -1
- package/pipeline/scripts/fixtures/install-layout.tsv +7 -7
- package/pipeline/scripts/fixtures/pack-expected-count.txt +1 -0
- package/pipeline/scripts/keychain-save.sh +13 -9
- package/pipeline/scripts/lint-skills.mjs +40 -2
- package/pipeline/scripts/migrate-prefs.mjs +26 -7
- package/pipeline/scripts/phase-tracker.sh +71 -0
- package/pipeline/scripts/pre-commit-check.sh +39 -0
- package/pipeline/scripts/scan-skills.sh +217 -127
- package/pipeline/scripts/smoke-bitbucket-contract.sh +21 -5
- package/pipeline/scripts/smoke-fetchers-offline.sh +382 -0
- package/pipeline/scripts/smoke-generate-issue.sh +3 -3
- package/pipeline/scripts/smoke-install-layout.sh +11 -3
- package/pipeline/scripts/smoke-lib-scripts.sh +54 -1
- package/pipeline/scripts/smoke-pack-contents.sh +140 -0
- package/pipeline/scripts/smoke-plugin-validate.sh +64 -0
- package/pipeline/scripts/smoke-review-readiness.sh +91 -0
- package/pipeline/scripts/smoke-shadow-git.sh +48 -1
- package/pipeline/scripts/smoke-skill-authoring.sh +1 -1
- package/pipeline/scripts/smoke-workflow-audit.sh +69 -0
- package/pipeline/scripts/smoke-wrapper-preservation.sh +68 -0
- package/pipeline/scripts/test-gap-scan.mjs +12 -1
- package/pipeline/scripts/triage-memory.mjs +10 -1
- package/pipeline/scripts/uninstall.mjs +105 -36
- package/pipeline/scripts/update-issue-progress.sh +60 -41
- package/pipeline/scripts/write-state.mjs +14 -4
- package/pipeline/skills/.skill-manifest.json +13 -5
- package/pipeline/skills/.skills-index.json +20 -2
- package/pipeline/skills/shared/core/multi-agent-help/SKILL.md +6 -2
- package/pipeline/skills/shared/core/multi-agent-review/SKILL.md +19 -10
- package/pipeline/skills/shared/core/multi-agent-review-issue/SKILL.md +25 -0
- package/pipeline/skills/shared/core/multi-agent-review-jira/SKILL.md +25 -0
- package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +3 -3
- package/pipeline/skills/skills-index.md +4 -2
|
@@ -61,7 +61,10 @@ resolve_state() {
|
|
|
61
61
|
|
|
62
62
|
write_state() {
|
|
63
63
|
local path="$1" content="$2"
|
|
64
|
-
|
|
64
|
+
# mktemp NEXT TO the target: a $TMPDIR temp file can live on a different
|
|
65
|
+
# filesystem than $HOME, where mv degrades to copy+unlink and loses the
|
|
66
|
+
# atomic-rename guarantee.
|
|
67
|
+
local tmp; tmp=$(mktemp "${path}.XXXXXX")
|
|
65
68
|
printf '%s\n' "$content" > "$tmp"
|
|
66
69
|
mv "$tmp" "$path"
|
|
67
70
|
}
|
|
@@ -93,6 +96,12 @@ do_set() {
|
|
|
93
96
|
echo "plan-todos: input must be an object with .title (string) and .todos (array)" >&2
|
|
94
97
|
exit 2
|
|
95
98
|
fi
|
|
99
|
+
# Per-item validation: every todo needs a string .id and .task, otherwise
|
|
100
|
+
# downstream jq like `.id + "\t" + .task` crashes on (null + string).
|
|
101
|
+
if ! jq -e '.todos | all(type == "object" and (.id | type == "string") and (.task | type == "string"))' <<<"$plan_blob" >/dev/null 2>&1; then
|
|
102
|
+
echo "plan-todos: every todo must be an object with string .id and .task fields" >&2
|
|
103
|
+
exit 2
|
|
104
|
+
fi
|
|
96
105
|
local state_path; state_path=$(resolve_state "$task_id")
|
|
97
106
|
local now; now=$(iso_now)
|
|
98
107
|
local new
|
|
@@ -209,7 +218,7 @@ do_next() {
|
|
|
209
218
|
)
|
|
210
219
|
)
|
|
211
220
|
| (.[0] // empty)
|
|
212
|
-
| if . == null or . == "" then "" else .id + "\t" + .task end
|
|
221
|
+
| if . == null or . == "" then "" else (.id // "") + "\t" + (.task // "") end
|
|
213
222
|
' "$state_path"
|
|
214
223
|
}
|
|
215
224
|
|
|
@@ -229,7 +238,7 @@ do_list() {
|
|
|
229
238
|
skipped: "- [/]",
|
|
230
239
|
failed: "- [!]"
|
|
231
240
|
} as $marks
|
|
232
|
-
| ($marks[.status] // "- [ ]") + " **" + .id + "** " + .task
|
|
241
|
+
| ($marks[.status] // "- [ ]") + " **" + (.id // "") + "** " + (.task // "")
|
|
233
242
|
+ (if (.notes // "") != "" then " _(" + .notes + ")_" else "" end)
|
|
234
243
|
+ (if (.failureReason // "") != "" then " _**failed:** " + .failureReason + "_" else "" end)
|
|
235
244
|
+ (if (.skipReason // "") != "" then " _**skipped:** " + .skipReason + "_" else "" end))
|
|
@@ -75,9 +75,11 @@ ITERATION=$(jq -r '.review.iterationNumber // 1' "$AGENT_STATE")
|
|
|
75
75
|
# --- Decision ---------------------------------------------------------------
|
|
76
76
|
|
|
77
77
|
# Single jq pass - group accepted findings by severity into tab-separated counts.
|
|
78
|
+
# `.review.findings // []` so a null/absent findings array (clean review)
|
|
79
|
+
# cannot raise "Cannot iterate over null" under set -e.
|
|
78
80
|
read -r ACCEPTED_BLOCKING ACCEPTED_IMPORTANT < <(
|
|
79
81
|
jq -r '
|
|
80
|
-
[.review.findings[] | select(.status == "accepted")] as $a
|
|
82
|
+
[(.review.findings // [])[] | select(.status == "accepted")] as $a
|
|
81
83
|
| [($a | map(select(.severity == "blocking")) | length),
|
|
82
84
|
($a | map(select(.severity == "important")) | length)]
|
|
83
85
|
| @tsv
|
|
@@ -242,7 +244,7 @@ post_github() {
|
|
|
242
244
|
fi
|
|
243
245
|
|
|
244
246
|
if [ "$DECISION" = "needs_work" ]; then
|
|
245
|
-
jq -c '.review.findings[] | select(.status == "accepted" and (.severity == "blocking" or .severity == "important"))' "$AGENT_STATE" \
|
|
247
|
+
jq -c '(.review.findings // [])[] | select(.status == "accepted" and (.severity == "blocking" or .severity == "important"))' "$AGENT_STATE" \
|
|
246
248
|
| while read -r finding; do
|
|
247
249
|
local sev path line issue_b64 fix_b64 rule_id_b64 issue fix rule_id body fingerprint
|
|
248
250
|
IFS=$'\t' read -r sev path line issue_b64 fix_b64 rule_id_b64 < <(
|
|
@@ -333,7 +335,7 @@ post_bitbucket_server() {
|
|
|
333
335
|
fi
|
|
334
336
|
|
|
335
337
|
if [ "$DECISION" = "needs_work" ]; then
|
|
336
|
-
jq -c '.review.findings[] | select(.status == "accepted" and (.severity == "blocking" or .severity == "important"))' "$AGENT_STATE" \
|
|
338
|
+
jq -c '(.review.findings // [])[] | select(.status == "accepted" and (.severity == "blocking" or .severity == "important"))' "$AGENT_STATE" \
|
|
337
339
|
| while read -r finding; do
|
|
338
340
|
local sev path line issue_b64 fix_b64 rule_id_b64 issue fix rule_id body payload http fingerprint
|
|
339
341
|
IFS=$'\t' read -r sev path line issue_b64 fix_b64 rule_id_b64 < <(
|
|
@@ -59,6 +59,36 @@ if [ $REFRESH -eq 0 ] && cache_fresh; then
|
|
|
59
59
|
exit 0
|
|
60
60
|
fi
|
|
61
61
|
|
|
62
|
+
# Refresh staging: providers write to $CACHE_TMP, then commit_cache validates
|
|
63
|
+
# and mv's it into place. Writing straight to $CACHE_FILE truncated the cache
|
|
64
|
+
# BEFORE the fetch ran, so any failure left an empty file that cache_fresh
|
|
65
|
+
# then served for the whole TTL.
|
|
66
|
+
CACHE_TMP="${CACHE_FILE}.tmp.$$"
|
|
67
|
+
cleanup_tmp() { rm -f "$CACHE_TMP" "$CACHE_TMP.list" 2>/dev/null || true; }
|
|
68
|
+
trap cleanup_tmp EXIT
|
|
69
|
+
|
|
70
|
+
# Validate the staged file (non-empty valid JSON) and move it into place.
|
|
71
|
+
# On failure keep the previous cache and serve it with a warning; hard-fail
|
|
72
|
+
# only when there is no previous cache to fall back to.
|
|
73
|
+
commit_cache() {
|
|
74
|
+
if [ -s "$CACHE_TMP" ] \
|
|
75
|
+
&& python3 -c 'import json,sys; json.load(open(sys.argv[1]))' "$CACHE_TMP" 2>/dev/null; then
|
|
76
|
+
mv "$CACHE_TMP" "$CACHE_FILE"
|
|
77
|
+
return 0
|
|
78
|
+
fi
|
|
79
|
+
rm -f "$CACHE_TMP"
|
|
80
|
+
if [ -f "$CACHE_FILE" ]; then
|
|
81
|
+
echo "WARN: repo-cache refresh failed for $PROVIDER/$SCOPE; serving previous cache" >&2
|
|
82
|
+
return 0
|
|
83
|
+
fi
|
|
84
|
+
echo "ERR: repo-cache refresh failed for $PROVIDER/$SCOPE and no previous cache exists" >&2
|
|
85
|
+
exit 3
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
# Basic-auth via a curl config fed through process substitution so the
|
|
89
|
+
# credential never appears in argv (argv is visible to `ps`).
|
|
90
|
+
bb_auth_cfg() { printf 'user = "%s:%s"\n' "$1" "$2"; }
|
|
91
|
+
|
|
62
92
|
case "$PROVIDER" in
|
|
63
93
|
github)
|
|
64
94
|
if ! command -v gh >/dev/null 2>&1; then
|
|
@@ -100,20 +130,32 @@ for r in data:
|
|
|
100
130
|
"provider": "github"
|
|
101
131
|
})
|
|
102
132
|
print(json.dumps(out))
|
|
103
|
-
' > "$
|
|
133
|
+
' > "$CACHE_TMP" || true
|
|
134
|
+
commit_cache
|
|
104
135
|
;;
|
|
105
136
|
|
|
106
137
|
bitbucket)
|
|
107
138
|
: "${BB_TOKEN_KEY:?BB_TOKEN_KEY required for bitbucket}"
|
|
108
139
|
: "${BB_HOST:?BB_HOST required (e.g. bitbucket.example.com)}"
|
|
109
|
-
|
|
110
|
-
|
|
140
|
+
# Resolve the credential helper via the shared resolver (works from the
|
|
141
|
+
# repo checkout and from both install trees) - never hardcode the path.
|
|
142
|
+
# A pre-set CRED_STORE (tests, custom installs) is honored as-is.
|
|
143
|
+
if [ -z "${CRED_STORE:-}" ]; then
|
|
144
|
+
# shellcheck disable=SC1090,SC1091
|
|
145
|
+
. "$(cd "$(dirname "$0")" && pwd)/credential-store-resolver.sh" 2>/dev/null \
|
|
146
|
+
|| . "$HOME/.claude/lib/credential-store-resolver.sh" 2>/dev/null \
|
|
147
|
+
|| . "$HOME/.copilot/lib/credential-store-resolver.sh" 2>/dev/null \
|
|
148
|
+
|| { echo "ERR: credential helper not found" >&2; exit 3; }
|
|
149
|
+
fi
|
|
150
|
+
BB_USER="${BB_USER:-${BB_USER_KEY:+$("$CRED_STORE" get "$BB_USER_KEY" 2>/dev/null || true)}}"
|
|
151
|
+
BB_TOKEN=$("$CRED_STORE" get "$BB_TOKEN_KEY" 2>/dev/null || true)
|
|
111
152
|
if [ -z "$BB_TOKEN" ]; then
|
|
112
153
|
echo "ERR: bitbucket token not found in credential store ($BB_TOKEN_KEY)" >&2
|
|
113
154
|
exit 3
|
|
114
155
|
fi
|
|
115
156
|
# Bitbucket Server REST: /rest/api/1.0/projects/{key}/repos?limit=200
|
|
116
|
-
curl
|
|
157
|
+
# Auth via curl config file (-K) so the token never appears on argv.
|
|
158
|
+
curl -sf -K <(bb_auth_cfg "${BB_USER:-bot}" "$BB_TOKEN") \
|
|
117
159
|
"https://${BB_HOST}/rest/api/1.0/projects/${SCOPE}/repos?limit=200" \
|
|
118
160
|
| python3 -c '
|
|
119
161
|
import json,sys
|
|
@@ -147,7 +189,8 @@ for r in vals:
|
|
|
147
189
|
"provider": "bitbucket"
|
|
148
190
|
})
|
|
149
191
|
print(json.dumps(out))
|
|
150
|
-
' > "$
|
|
192
|
+
' > "$CACHE_TMP" || true
|
|
193
|
+
commit_cache
|
|
151
194
|
;;
|
|
152
195
|
|
|
153
196
|
local)
|
|
@@ -161,7 +204,7 @@ print(json.dumps(out))
|
|
|
161
204
|
if [ -d "$SCOPE" ]; then
|
|
162
205
|
ROOTS="$SCOPE"
|
|
163
206
|
fi
|
|
164
|
-
: > "$
|
|
207
|
+
: > "$CACHE_TMP.list"
|
|
165
208
|
for root in $ROOTS; do
|
|
166
209
|
[ -d "$root" ] || continue
|
|
167
210
|
# -prune avoids descending into already-found .git trees (nested submodules
|
|
@@ -178,7 +221,7 @@ print(json.dumps(out))
|
|
|
178
221
|
# truly offline-only or a clone with an upstream we just chose
|
|
179
222
|
# to treat as local.
|
|
180
223
|
remote=$(git -C "$repo_dir" remote get-url origin 2>/dev/null || true)
|
|
181
|
-
printf '%s\t%s\t%s\n' "$name" "$repo_dir" "$remote" >> "$
|
|
224
|
+
printf '%s\t%s\t%s\n' "$name" "$repo_dir" "$remote" >> "$CACHE_TMP.list"
|
|
182
225
|
done
|
|
183
226
|
done
|
|
184
227
|
python3 -c '
|
|
@@ -218,8 +261,9 @@ for raw in open(sys.argv[1]):
|
|
|
218
261
|
"provider": "local"
|
|
219
262
|
})
|
|
220
263
|
print(json.dumps(out))
|
|
221
|
-
' "$
|
|
222
|
-
rm -f "$
|
|
264
|
+
' "$CACHE_TMP.list" > "$CACHE_TMP" || true
|
|
265
|
+
rm -f "$CACHE_TMP.list"
|
|
266
|
+
commit_cache
|
|
223
267
|
;;
|
|
224
268
|
|
|
225
269
|
*)
|
|
@@ -180,6 +180,10 @@ review_one_repo() {
|
|
|
180
180
|
title=$(jq -r '.title' <<<"$pr")
|
|
181
181
|
url=$(jq -r '.url' <<<"$pr")
|
|
182
182
|
|
|
183
|
+
# Advance the cursor candidate for every PR seen (including skips), so a
|
|
184
|
+
# PR that keeps its reviewed head SHA is not re-fetched on every poll.
|
|
185
|
+
if [ "$updated_at" \> "$newest_iso" ]; then newest_iso="$updated_at"; fi
|
|
186
|
+
|
|
183
187
|
# Did we already review THIS head SHA?
|
|
184
188
|
local prev_sha
|
|
185
189
|
prev_sha=$(jq -r --arg n "$pr_num" '.reviewed_prs[$n] // empty' <<<"$state")
|
|
@@ -196,12 +200,18 @@ review_one_repo() {
|
|
|
196
200
|
# (Claude Code session or future webhook handler). review-watch only
|
|
197
201
|
# produces the dispatch list - it does not run the LLM itself, because
|
|
198
202
|
# token cost decisions stay with the user.
|
|
199
|
-
state=$(jq --arg n "$pr_num" --arg s "$head_sha"
|
|
200
|
-
'.reviewed_prs[$n] = $s
|
|
201
|
-
|
|
202
|
-
if [ "$updated_at" \> "$newest_iso" ]; then newest_iso="$updated_at"; fi
|
|
203
|
+
state=$(jq --arg n "$pr_num" --arg s "$head_sha" \
|
|
204
|
+
'.reviewed_prs[$n] = $s' <<<"$state")
|
|
203
205
|
done < <(jq -c '.[]' <<<"$prs_json")
|
|
204
206
|
|
|
207
|
+
# Persist the NEWEST updatedAt seen this scan - the previous per-PR
|
|
208
|
+
# assignment stamped whichever PR the search returned last, which moves
|
|
209
|
+
# the cursor backwards whenever results are not oldest-first and causes
|
|
210
|
+
# permanent re-scans. newest_iso only ever grows (seeded from last_seen).
|
|
211
|
+
if [ -n "$newest_iso" ] && [ "$newest_iso" != "$last_seen" ]; then
|
|
212
|
+
state=$(jq --arg t "$newest_iso" '.last_seen_iso = $t' <<<"$state")
|
|
213
|
+
fi
|
|
214
|
+
|
|
205
215
|
save_state "$repo" "$state"
|
|
206
216
|
}
|
|
207
217
|
|
|
@@ -218,7 +228,13 @@ case "$CMD" in
|
|
|
218
228
|
while IFS= read -r r; do [ -n "$r" ] && REPOS+=("$r"); done < <(read_repos_from_prefs)
|
|
219
229
|
fi
|
|
220
230
|
[ "${#REPOS[@]}" -eq 0 ] && { err "no repos configured (prefs.reviewWatch.repos)"; exit 2; }
|
|
221
|
-
|
|
231
|
+
# Per-repo failures (transient gh/network errors) must not kill the run
|
|
232
|
+
# under set -e; log, continue, and reflect them in the exit code.
|
|
233
|
+
RC=0
|
|
234
|
+
for r in "${REPOS[@]}"; do
|
|
235
|
+
review_one_repo "$r" || { err "scan failed for $r (continuing)"; RC=3; }
|
|
236
|
+
done
|
|
237
|
+
exit "$RC"
|
|
222
238
|
;;
|
|
223
239
|
--watch)
|
|
224
240
|
require_jq
|
|
@@ -227,7 +243,11 @@ case "$CMD" in
|
|
|
227
243
|
[ "$interval" -lt 30 ] && interval=30 # GitHub rate limit guard
|
|
228
244
|
printf 'review-watch: starting loop (interval=%ds)\n' "$interval"
|
|
229
245
|
while :; do
|
|
230
|
-
|
|
246
|
+
# A transient failure on one repo must not kill the watch loop.
|
|
247
|
+
while IFS= read -r r; do
|
|
248
|
+
[ -n "$r" ] || continue
|
|
249
|
+
review_one_repo "$r" || err "scan failed for $r (continuing)"
|
|
250
|
+
done < <(read_repos_from_prefs)
|
|
231
251
|
sleep "$interval"
|
|
232
252
|
done
|
|
233
253
|
;;
|
|
@@ -61,6 +61,23 @@ usage() {
|
|
|
61
61
|
|
|
62
62
|
shadow_dir() { printf '%s/%s' "$SHADOW_ROOT" "$1"; }
|
|
63
63
|
|
|
64
|
+
# Task ids become path components under $SHADOW_ROOT, so an unvalidated id
|
|
65
|
+
# like ".." would make prune's `rm -rf` walk out of the shadow tree and
|
|
66
|
+
# delete ~/.claude/state. Allow only [A-Za-z0-9._-]+ and explicitly reject
|
|
67
|
+
# ".", "..", empty, and anything containing a slash.
|
|
68
|
+
require_task_id() {
|
|
69
|
+
case "${1:-}" in
|
|
70
|
+
"" | . | ..)
|
|
71
|
+
err "invalid task id: '${1:-}' (empty, '.' and '..' are rejected)"
|
|
72
|
+
exit 2
|
|
73
|
+
;;
|
|
74
|
+
*[!A-Za-z0-9._-]*)
|
|
75
|
+
err "invalid task id: '$1' (allowed characters: A-Z a-z 0-9 . _ -)"
|
|
76
|
+
exit 2
|
|
77
|
+
;;
|
|
78
|
+
esac
|
|
79
|
+
}
|
|
80
|
+
|
|
64
81
|
# Wrap git so .git lives in $SHADOW_DIR but worktree is the project root.
|
|
65
82
|
sg() {
|
|
66
83
|
local sd="$1" wt="$2"; shift 2
|
|
@@ -74,6 +91,7 @@ iso_now() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
|
|
|
74
91
|
do_init() {
|
|
75
92
|
local task_id="${1:-}" project_root="${2:-}"
|
|
76
93
|
[ -z "$task_id" ] || [ -z "$project_root" ] && { err "usage: init <task-id> <project-root>"; exit 1; }
|
|
94
|
+
require_task_id "$task_id"
|
|
77
95
|
[ ! -d "$project_root" ] && { err "project root not a directory: $project_root"; exit 1; }
|
|
78
96
|
local sd; sd=$(shadow_dir "$task_id")
|
|
79
97
|
if [ -d "$sd/.git" ]; then
|
|
@@ -83,12 +101,16 @@ do_init() {
|
|
|
83
101
|
mkdir -p "$sd"
|
|
84
102
|
# Bare-ish: .git inside $sd, worktree is the project root.
|
|
85
103
|
git init --quiet "$sd" >/dev/null
|
|
86
|
-
#
|
|
104
|
+
# Ignore rules must live in $GIT_DIR/info/exclude: the work tree is the
|
|
105
|
+
# project root, so a .gitignore placed next to the shadow .git is never
|
|
106
|
+
# read by git. Seed the exclude file with the project's own .gitignore
|
|
107
|
+
# (protects snapshots even if the project file is deleted mid-run) plus
|
|
108
|
+
# forced excludes for build artifacts the project may not ignore.
|
|
109
|
+
mkdir -p "$sd/.git/info"
|
|
87
110
|
if [ -f "$project_root/.gitignore" ]; then
|
|
88
|
-
|
|
111
|
+
cat "$project_root/.gitignore" >> "$sd/.git/info/exclude"
|
|
89
112
|
fi
|
|
90
|
-
|
|
91
|
-
cat >> "$sd/.gitignore" <<EOF
|
|
113
|
+
cat >> "$sd/.git/info/exclude" <<EOF
|
|
92
114
|
node_modules/
|
|
93
115
|
Pods/
|
|
94
116
|
.build/
|
|
@@ -111,6 +133,7 @@ do_snapshot() {
|
|
|
111
133
|
local task_id="${1:-}" project_root="${2:-}" label="${3:-}"
|
|
112
134
|
[ -z "$task_id" ] || [ -z "$project_root" ] || [ -z "$label" ] && {
|
|
113
135
|
err "usage: snapshot <task-id> <project-root> <label>"; exit 1; }
|
|
136
|
+
require_task_id "$task_id"
|
|
114
137
|
local sd; sd=$(shadow_dir "$task_id")
|
|
115
138
|
[ ! -d "$sd/.git" ] && { err "shadow not initialized for $task_id - run 'init' first"; exit 1; }
|
|
116
139
|
sg "$sd" "$project_root" add -A >/dev/null 2>&1 || true
|
|
@@ -129,6 +152,7 @@ do_snapshot() {
|
|
|
129
152
|
do_list() {
|
|
130
153
|
local task_id="${1:-}"
|
|
131
154
|
[ -z "$task_id" ] && { err "usage: list <task-id>"; exit 1; }
|
|
155
|
+
require_task_id "$task_id"
|
|
132
156
|
local sd; sd=$(shadow_dir "$task_id")
|
|
133
157
|
[ ! -d "$sd/.git" ] && { err "shadow not initialized for $task_id"; exit 1; }
|
|
134
158
|
# Use the embedded `iso:` line from the commit message body so the listing
|
|
@@ -141,12 +165,28 @@ do_restore() {
|
|
|
141
165
|
local task_id="${1:-}" project_root="${2:-}" sha="${3:-}" mode="${4:---files}"
|
|
142
166
|
[ -z "$task_id" ] || [ -z "$project_root" ] || [ -z "$sha" ] && {
|
|
143
167
|
err "usage: restore <task-id> <project-root> <sha> [--files|--state|--both]"; exit 1; }
|
|
168
|
+
require_task_id "$task_id"
|
|
144
169
|
local sd; sd=$(shadow_dir "$task_id")
|
|
145
170
|
[ ! -d "$sd/.git" ] && { err "shadow not initialized for $task_id"; exit 1; }
|
|
146
171
|
case "$mode" in
|
|
147
172
|
--files|--both)
|
|
148
173
|
sg "$sd" "$project_root" checkout --quiet "$sha" -- . \
|
|
149
174
|
|| { err "git checkout failed"; exit 3; }
|
|
175
|
+
# `checkout <sha> -- .` restores tracked content but leaves files that
|
|
176
|
+
# did not exist in the snapshot. Remove both classes of leftovers so a
|
|
177
|
+
# restore is a full rollback, staying strictly inside the work tree:
|
|
178
|
+
# a) files committed in snapshots AFTER $sha (added between sha..HEAD)
|
|
179
|
+
# b) files created since the last snapshot (still untracked; `clean`
|
|
180
|
+
# respects the info/exclude rules, so ignored trees are untouched)
|
|
181
|
+
local extra
|
|
182
|
+
extra=$(sg "$sd" "$project_root" diff --name-only --diff-filter=A "$sha" HEAD 2>/dev/null || true)
|
|
183
|
+
if [ -n "$extra" ]; then
|
|
184
|
+
printf '%s\n' "$extra" | while IFS= read -r f; do
|
|
185
|
+
[ -n "$f" ] && rm -f "$project_root/$f"
|
|
186
|
+
done
|
|
187
|
+
fi
|
|
188
|
+
( cd "$project_root" && \
|
|
189
|
+
git --git-dir="$sd/.git" --work-tree="$project_root" clean -fdq ) || true
|
|
150
190
|
printf 'shadow-git: restored files to %s\n' "$sha" >&2
|
|
151
191
|
;;
|
|
152
192
|
--state)
|
|
@@ -161,6 +201,7 @@ do_restore() {
|
|
|
161
201
|
do_prune() {
|
|
162
202
|
local task_id="${1:-}"; shift || true
|
|
163
203
|
[ -z "$task_id" ] && { err "usage: prune <task-id> [--older-than-days N]"; exit 1; }
|
|
204
|
+
require_task_id "$task_id"
|
|
164
205
|
local older=""
|
|
165
206
|
while [ "$#" -gt 0 ]; do
|
|
166
207
|
case "$1" in
|
|
@@ -110,7 +110,16 @@ cmd_deploy() {
|
|
|
110
110
|
fi
|
|
111
111
|
|
|
112
112
|
if [ -z "${VERCEL_TOKEN:-}" ]; then
|
|
113
|
-
|
|
113
|
+
# Route the credential-helper hint through the resolver instead of a
|
|
114
|
+
# hardcoded ~/.claude path so Copilot-only installs get a usable command.
|
|
115
|
+
# The resolver ships alongside this wrapper in lib/; fall back to the
|
|
116
|
+
# per-CLI install locations when running from a different cwd layout.
|
|
117
|
+
# shellcheck source=/dev/null
|
|
118
|
+
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/credential-store-resolver.sh" 2>/dev/null \
|
|
119
|
+
|| . "$HOME/.claude/lib/credential-store-resolver.sh" 2>/dev/null \
|
|
120
|
+
|| . "$HOME/.copilot/lib/credential-store-resolver.sh" 2>/dev/null \
|
|
121
|
+
|| true
|
|
122
|
+
echo "ERROR: VERCEL_TOKEN not set. Resolve via env or keychain ('${CRED_STORE:-credential-store.sh} get <vercel-key>')." >&2
|
|
114
123
|
echo "Hint: vercel CLI accepts the token via env var; do NOT pass --token= on argv (leaks on retry)." >&2
|
|
115
124
|
exit 1
|
|
116
125
|
fi
|
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
-
## 1. Command Inventory (
|
|
9
|
+
## 1. Command Inventory (38 commands)
|
|
10
10
|
|
|
11
11
|
```
|
|
12
12
|
analysis, analysis-resolve, autopilot, build-optimize, channels, create-jira, delete, dev,
|
|
13
13
|
dev-autopilot, dev-local, dev-local-autopilot, diff-explain, finish, garbage-collect,
|
|
14
14
|
help, issue, jira, kill, language, local,
|
|
15
|
-
local-autopilot, log, manual-test, prune-logs, purge, refactor, resume, review,
|
|
15
|
+
local-autopilot, log, manual-test, prune-logs, purge, refactor, resume, review, review-issue, review-jira,
|
|
16
16
|
scan, search, setup, stack, status, sync, test, update
|
|
17
17
|
```
|
|
18
18
|
|
|
@@ -23,7 +23,7 @@ Categories:
|
|
|
23
23
|
- **Full 8-phase modes**: `autopilot`, `local`, `local-autopilot`
|
|
24
24
|
- **Fast modes** (Init -> Dev(Opus) -> Commit -> Report): `dev`, `dev-autopilot`, `dev-local`, `dev-local-autopilot`
|
|
25
25
|
- **Tail modes** (run the pipeline tail over already-done local work): `finish`
|
|
26
|
-
- **Ops commands** (one-shot, no worktree): `status`, `log`, `kill`, `purge`, `delete`, `resume`, `review`, `analysis`, `analysis-resolve`, `build-optimize`, `channels`, `scan`, `search`, `diff-explain`, `garbage-collect`, `prune-logs`
|
|
26
|
+
- **Ops commands** (one-shot, no worktree): `status`, `log`, `kill`, `purge`, `delete`, `resume`, `review`, `review-jira`, `review-issue`, `analysis`, `analysis-resolve`, `build-optimize`, `channels`, `scan`, `search`, `diff-explain`, `garbage-collect`, `prune-logs`
|
|
27
27
|
- **Meta-ops**: `setup`, `sync`, `update`, `help`, `refactor`, `test`, `stack`, `manual-test`, `language`
|
|
28
28
|
|
|
29
29
|
> **Inventory drift is a contract violation.** Adding a slash command under `pipeline/commands/multi-agent/` without updating this list + its counterpart Copilot dir (`pipeline/skills/shared/core/multi-agent-<cmd>/`) is a merge blocker. `smoke-commands-skills-parity.sh` enforces command ↔ skill directory parity; `smoke-cross-cli-behavior.sh` enforces behavior parity. This doc is the authoritative command list - bump the count + table together.
|
|
@@ -245,7 +245,7 @@ For clipboard ops, callers still gate with `if command -v pbpaste >/dev/null; th
|
|
|
245
245
|
|
|
246
246
|
This contract is validated by:
|
|
247
247
|
|
|
248
|
-
- `smoke-cross-cli-behavior.sh` - asserts all
|
|
248
|
+
- `smoke-cross-cli-behavior.sh` - asserts all 38 commands behave identically, pulls from Section 2 (placeholder vocab), Section 5 (argument parsing), Section 6 (output formats); also regression-locks the 6-persona agent deployment
|
|
249
249
|
- `smoke-commands-skills-parity.sh` (50 assertions) - enforces colon-form command ↔ dash-form skill directory parity
|
|
250
250
|
- `smoke-compliance-skills.sh` (45 assertions) - enforces store-compliance skill catalog + 4 consumer wiring
|
|
251
251
|
- `smoke-personal-data.sh` - extended in 0.5.5 to treat deprecated placeholders (`{github-username}`, `{your-website}`, `{website-repo}`) as leaks; adds `mmerterden` to public-handle blocklist for generic docs
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Readiness Review (shared flow for review-jira + review-issue)
|
|
2
|
+
|
|
3
|
+
Assess whether a tracker item (Jira issue or GitHub issue) is READY to hand to the multi-agent pipeline, list the concrete gaps, and - after confirmation - post them back as a comment on the item so the reporter can fix it. Read-only on code: no worktree, no branch, no commits, no dev chaining. This is the inverse of `/multi-agent:create-jira` (which authors a well-formed item); here we grade an existing one.
|
|
4
|
+
|
|
5
|
+
Both `/multi-agent:review-jira` and `/multi-agent:review-issue` execute this flow; only the provider (fetch + comment endpoint + picker) differs.
|
|
6
|
+
|
|
7
|
+
## Language
|
|
8
|
+
Instruction prose here is English. The verdict shown in chat and the comment body follow `prefs.global.outputLanguage` (the comment is authored for the user's team, same exception as create-jira). AskUserQuestion `label`/`header` stay English; `question`/`description` follow `outputLanguage`.
|
|
9
|
+
|
|
10
|
+
## Step 1 - resolve the item
|
|
11
|
+
- With an argument: `review-jira KEY-123` / `https://<jira>/browse/KEY-123`; `review-issue #N` / `repo#N` / GitHub issue URL.
|
|
12
|
+
- With no argument: offer the picker - review-jira reuses the `jira` command's issue list (`jira/SKILL.md` JQL, assignee=currentUser, open); review-issue reuses the `issue` command's list (`gh issue list`). Single-select (one item reviewed per run; re-run for more).
|
|
13
|
+
|
|
14
|
+
## Step 2 - fetch + base maturity
|
|
15
|
+
Run `$HOME/.claude/lib/issue-fetcher.sh` (the same fetcher Phase 0 uses). It returns a descriptor with `title`, `type`, `status`, `description`, and `maturity = { score (0..100), blockers[], warnings[], summary }`. Reuse that maturity verbatim as the baseline; never re-fetch through MCP.
|
|
16
|
+
|
|
17
|
+
## Step 3 - readiness rubric (extends maturity)
|
|
18
|
+
Maturity is generic; add these pipeline-readiness dimensions by reading the fetched `title` + `description` (no invention - only judge what is written). Each is `pass` / `gap`:
|
|
19
|
+
|
|
20
|
+
| Dimension | Ready when | Gap signal |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| Scope is testable | the change is bounded + verifiable (what "done" means is stated) | vague ("improve X"), no measurable outcome |
|
|
23
|
+
| Acceptance criteria / DoD | AC or DoD present (Task/Story/Feature) | none (aligns with maturity `no_acceptance_criteria`) |
|
|
24
|
+
| Reproduction | Bugs have steps + expected/actual | none (aligns with maturity `no_repro_steps`) |
|
|
25
|
+
| Design reference | UI work links a Figma frame or attaches a screenshot | UI implied but no design source |
|
|
26
|
+
| API contract | backend/integration work names endpoints or a Swagger/OpenAPI ref | data/contract implied, none given |
|
|
27
|
+
| Platform / stack signal | the target stack is inferable (iOS / Android / frontend / backend) | ambiguous - Phase 1 could not route |
|
|
28
|
+
| Dependencies | blocking deps/links are called out when they exist | hidden dependency likely |
|
|
29
|
+
|
|
30
|
+
Only emit a dimension as a gap when its trigger applies (do not demand an API contract on a copy-change task). Merge: `gaps = maturity.blockers + maturity.warnings + rubric gaps`.
|
|
31
|
+
|
|
32
|
+
## Step 4 - verdict (chat, always)
|
|
33
|
+
Print a compact verdict; reuse the maturity blocker-vs-warning severity contract (same as the Phase 0 maturity check):
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
Readiness: KEY-123 - score 60/100 - NEEDS WORK
|
|
37
|
+
BLOCK Description is empty
|
|
38
|
+
WARN No acceptance criteria detected
|
|
39
|
+
GAP UI work but no design reference
|
|
40
|
+
GAP Target stack ambiguous
|
|
41
|
+
```
|
|
42
|
+
Verdict rule: any blocker -> NOT READY (blocking); else any warning/gap -> NEEDS WORK; else READY. Mirrors `jira/SKILL.md` maturity handling: blockers would halt a real run, warnings would prompt.
|
|
43
|
+
|
|
44
|
+
## Step 5 - post the comment (confirmed)
|
|
45
|
+
Posting to a tracker is outward-facing - confirm first (autopilot auto-posts):
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
question (outputLanguage): "Eksiklikleri {KEY|#N} üzerine yorum olarak ekleyeyim mi?"
|
|
49
|
+
header (English): "Post readiness comment"
|
|
50
|
+
options:
|
|
51
|
+
- label "Post comment", description (outputLanguage): "Eksiklik listesini item'a yorum olarak yaz"
|
|
52
|
+
- label "Chat only", description (outputLanguage): "Sadece sohbet özeti, item'a dokunma"
|
|
53
|
+
default: option 1
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Comment body: a short "Multi-agent readiness review" heading, the score + verdict, then the gap list grouped Blockers / Warnings / Gaps, each with a one-line fix suggestion. Tone rules (same as `channels/issue-comment.md`): no AI/Claude/Copilot attribution, no "generated by", no em-dash/section-sign, plain ASCII; if referencing another item use `Ref: #N` never `Closes/Fixes`. READY items get a short "ready to pick up" confirmation instead of a gap list.
|
|
57
|
+
|
|
58
|
+
Provider dispatch:
|
|
59
|
+
- **Jira** (`review-jira`): post via `$HOME/.claude/multi-agent-refs/channels/jira.md` comment path - `POST /rest/api/2/issue/{id}/comment`, Bearer token from `keychainMapping.jira` via `credential-store.sh`, UTF-8 verbatim (`jq --rawfile` + `curl --data-binary @payload.json`), markdown->wiki conversion.
|
|
60
|
+
- **GitHub** (`review-issue`): `gh issue comment "$N" --repo "$org/$repo" --body-file <file>` per `channels/issue-comment.md` (auth via the Phase 0 `gh` account).
|
|
61
|
+
|
|
62
|
+
On post failure, surface the failing endpoint on stderr; the chat verdict remains the source of truth.
|
|
63
|
+
|
|
64
|
+
## Not in scope
|
|
65
|
+
No status transitions, no assignment changes, no issue creation, no code review (that is `/multi-agent:review`). This command only reads the item and comments its readiness.
|
|
@@ -34,24 +34,52 @@ mkdir -p "$AUDIT_DIR"
|
|
|
34
34
|
|
|
35
35
|
# Rotate today's file (only if it has content)
|
|
36
36
|
if [ -s "$AUDIT_FILE" ]; then
|
|
37
|
-
|
|
37
|
+
# Portable file-mtime read. BSD `date -r` takes epoch SECONDS (not a file
|
|
38
|
+
# path), so the previous `date -r "$AUDIT_FILE"` always errored on macOS and
|
|
39
|
+
# mislabeled every rotated file with today's date.
|
|
40
|
+
case "$(uname -s)" in
|
|
41
|
+
Darwin|*BSD*) MTIME="$(stat -f %m "$AUDIT_FILE" 2>/dev/null || true)" ;;
|
|
42
|
+
*) MTIME="$(stat -c %Y "$AUDIT_FILE" 2>/dev/null || true)" ;;
|
|
43
|
+
esac
|
|
44
|
+
if [ -n "$MTIME" ]; then
|
|
45
|
+
# BSD: date -r <epoch>; GNU: date -d @<epoch>
|
|
46
|
+
STAMP="$(date -u -r "$MTIME" +"%Y-%m-%d" 2>/dev/null \
|
|
47
|
+
|| date -u -d "@$MTIME" +"%Y-%m-%d" 2>/dev/null \
|
|
48
|
+
|| date -u +"%Y-%m-%d")"
|
|
49
|
+
else
|
|
50
|
+
STAMP="$(date -u +"%Y-%m-%d")"
|
|
51
|
+
fi
|
|
38
52
|
ROTATED="$AUDIT_DIR/audit.${STAMP}.jsonl"
|
|
39
53
|
|
|
40
|
-
#
|
|
41
|
-
|
|
42
|
-
|
|
54
|
+
# Atomic against concurrent appenders: mv the live inode FIRST, then create
|
|
55
|
+
# a fresh empty live file. Writers appending via >> to the moved inode land
|
|
56
|
+
# in the spool and are preserved; the old cat-then-truncate flow lost any
|
|
57
|
+
# event appended between the copy and the truncate.
|
|
58
|
+
SPOOL="$AUDIT_DIR/.audit.rotate.$$"
|
|
59
|
+
mv "$AUDIT_FILE" "$SPOOL"
|
|
60
|
+
: > "$AUDIT_FILE"
|
|
61
|
+
chmod 600 "$AUDIT_FILE" 2>/dev/null || true
|
|
62
|
+
|
|
63
|
+
# Don't clobber a rotated file from earlier today (multiple invocations).
|
|
64
|
+
# Earlier rotations may already be gzipped; appending a second gzip member
|
|
65
|
+
# is valid (gunzip concatenates members).
|
|
66
|
+
if [ -f "$ROTATED.gz" ] && command -v gzip >/dev/null 2>&1; then
|
|
67
|
+
if gzip -c "$SPOOL" >> "$ROTATED.gz" 2>/dev/null; then
|
|
68
|
+
rm -f "$SPOOL"
|
|
69
|
+
else
|
|
70
|
+
mv "$SPOOL" "$ROTATED"
|
|
71
|
+
fi
|
|
72
|
+
elif [ -f "$ROTATED" ]; then
|
|
73
|
+
cat "$SPOOL" >> "$ROTATED"
|
|
74
|
+
rm -f "$SPOOL"
|
|
43
75
|
else
|
|
44
|
-
mv "$
|
|
76
|
+
mv "$SPOOL" "$ROTATED"
|
|
45
77
|
fi
|
|
46
78
|
|
|
47
79
|
# gzip immediately to save space
|
|
48
|
-
if command -v gzip >/dev/null 2>&1; then
|
|
80
|
+
if [ -f "$ROTATED" ] && command -v gzip >/dev/null 2>&1; then
|
|
49
81
|
gzip -f "$ROTATED" 2>/dev/null || true
|
|
50
82
|
fi
|
|
51
|
-
|
|
52
|
-
# Reset live file (mode 0600 - sensitive)
|
|
53
|
-
: > "$AUDIT_FILE"
|
|
54
|
-
chmod 600 "$AUDIT_FILE" 2>/dev/null || true
|
|
55
83
|
fi
|
|
56
84
|
|
|
57
85
|
# Drop archives older than KEEP_DAYS
|
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Internal markdown link checker for the user-facing docs surface
|
|
2
|
+
// Internal markdown link checker for the user-facing docs surface and the
|
|
3
|
+
// pipeline instruction tree.
|
|
3
4
|
//
|
|
4
|
-
// Scope: root *.md + docs/**/*.md + examples/**/*.md
|
|
5
|
-
// relative link target ([text](./path) or
|
|
6
|
-
// an existing file or directory.
|
|
7
|
-
// pure #anchors
|
|
8
|
-
//
|
|
5
|
+
// Scope: root *.md + docs/**/*.md + examples/**/*.md + pipeline/**/*.md.
|
|
6
|
+
// Verifies that every relative link target ([text](./path) or
|
|
7
|
+
// [text](path#anchor)) resolves to an existing file or directory. Skipped:
|
|
8
|
+
// external (http/https/mailto) links, pure #anchors, template links whose
|
|
9
|
+
// path contains a {placeholder} variable, and links through shell
|
|
10
|
+
// variables ($HOME, ...) - except $HOME/.claude/multi-agent-refs/, which
|
|
11
|
+
// is checked against its repo source (pipeline/multi-agent-refs/) so a
|
|
12
|
+
// ref rename cannot leave stale runtime links behind. This is a drift
|
|
13
|
+
// gate for file moves/renames, not a network crawler.
|
|
14
|
+
//
|
|
15
|
+
// Excluded trees: pipeline/skills/shared/external/ is vendored community
|
|
16
|
+
// skill content - several packs (the Apple HIG set) index upstream
|
|
17
|
+
// references/ companion files that are deliberately not vendored here.
|
|
9
18
|
|
|
10
19
|
import { readFileSync, existsSync, readdirSync, statSync } from "node:fs";
|
|
11
20
|
import { dirname, join, resolve } from "node:path";
|
|
@@ -25,10 +34,13 @@ function mdFiles(dir, acc = []) {
|
|
|
25
34
|
|
|
26
35
|
const targets = [
|
|
27
36
|
...readdirSync(repoRoot).filter((f) => f.endsWith(".md")).map((f) => join(repoRoot, f)),
|
|
28
|
-
...["docs", "examples"].filter((d) => existsSync(join(repoRoot, d))).flatMap((d) => mdFiles(join(repoRoot, d))),
|
|
37
|
+
...["docs", "examples", "pipeline"].filter((d) => existsSync(join(repoRoot, d))).flatMap((d) => mdFiles(join(repoRoot, d))),
|
|
29
38
|
// CHANGELOG-archive.md is frozen history - it legitimately references docs
|
|
30
39
|
// that were deleted in later releases.
|
|
31
|
-
]
|
|
40
|
+
]
|
|
41
|
+
.filter((f) => !f.endsWith("CHANGELOG-archive.md"))
|
|
42
|
+
// Vendored community skill packs (see header note).
|
|
43
|
+
.filter((f) => !f.includes(join("pipeline", "skills", "shared", "external") + "/"));
|
|
32
44
|
|
|
33
45
|
const LINK_RE = /\[[^\]]*\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
|
|
34
46
|
const errors = [];
|
|
@@ -42,9 +54,22 @@ for (const file of targets) {
|
|
|
42
54
|
for (const match of text.matchAll(LINK_RE)) {
|
|
43
55
|
const raw = match[1];
|
|
44
56
|
if (/^(https?:|mailto:|#)/.test(raw)) continue;
|
|
57
|
+
// Template placeholders ({fileKey}, {slug}, ...) resolve at runtime,
|
|
58
|
+
// never on disk - skip them.
|
|
59
|
+
if (raw.includes("{") || raw.includes("}")) continue;
|
|
45
60
|
const target = raw.split("#")[0];
|
|
46
61
|
if (target === "") continue;
|
|
47
|
-
|
|
62
|
+
let resolved;
|
|
63
|
+
const refsPrefix = "$HOME/.claude/multi-agent-refs/";
|
|
64
|
+
if (target.startsWith(refsPrefix)) {
|
|
65
|
+
// Runtime install path with a repo source - check the source tree.
|
|
66
|
+
resolved = join(repoRoot, "pipeline", "multi-agent-refs", target.slice(refsPrefix.length));
|
|
67
|
+
} else if (target.includes("$") || target.startsWith("~")) {
|
|
68
|
+
// Other shell-variable / home paths resolve at runtime only.
|
|
69
|
+
continue;
|
|
70
|
+
} else {
|
|
71
|
+
resolved = resolve(dirname(file), decodeURIComponent(target));
|
|
72
|
+
}
|
|
48
73
|
if (!existsSync(resolved)) {
|
|
49
74
|
errors.push(`${file.slice(repoRoot.length + 1)}: broken link -> ${raw}`);
|
|
50
75
|
}
|
|
@@ -85,7 +85,6 @@ function loadDiff() {
|
|
|
85
85
|
if (!existsSync(DIFF_FILE)) throw new Error(`diff file not found: ${DIFF_FILE}`);
|
|
86
86
|
return readFileSync(DIFF_FILE, "utf8");
|
|
87
87
|
}
|
|
88
|
-
BASE = resolveBase();
|
|
89
88
|
return git(["diff", "--unified=0", `${BASE}...${HEAD}`]);
|
|
90
89
|
}
|
|
91
90
|
|
|
@@ -295,6 +294,10 @@ function buildRow(stat, addedLines, allChangedPaths) {
|
|
|
295
294
|
}
|
|
296
295
|
|
|
297
296
|
function main() {
|
|
297
|
+
// Resolve the base ref ONCE, before any git diff runs. loadNumstat() and
|
|
298
|
+
// loadDiff() both interpolate BASE; resolving it lazily inside loadDiff()
|
|
299
|
+
// used to leave BASE=null for the numstat call (git diff null...HEAD).
|
|
300
|
+
if (!DIFF_FILE) BASE = resolveBase();
|
|
298
301
|
const numstat = loadNumstat();
|
|
299
302
|
if (numstat.length === 0) {
|
|
300
303
|
process.stderr.write("diff-risk-score: empty diff\n");
|
|
@@ -138,7 +138,16 @@ if (body.trim().length === 0) {
|
|
|
138
138
|
const successRe = userRegExp(args["success-pattern"], DEFAULT_MARKERS[claim].success, "success");
|
|
139
139
|
const failureRe = userRegExp(args["failure-pattern"], DEFAULT_MARKERS[claim].failure, "failure");
|
|
140
140
|
|
|
141
|
-
|
|
141
|
+
// Zero-count summaries ("0 failed", "0 tests failed out of 10", "0 failures")
|
|
142
|
+
// are success statements, not failure markers. The default \bFAILED\b marker
|
|
143
|
+
// would otherwise flip a passing log to fail - and failure is decisive. Only
|
|
144
|
+
// applied when the DEFAULT failure marker is in play; a caller-supplied
|
|
145
|
+
// --failure-pattern is matched against the raw body untouched.
|
|
146
|
+
const ZERO_FAIL_SUMMARY = /\b0\s+(?:\w+\s+){0,2}(?:failed|failures?|failing)\b/gi;
|
|
147
|
+
const usingDefaultFailure = !args["failure-pattern"] || args["failure-pattern"] === true;
|
|
148
|
+
const failureBody = usingDefaultFailure ? body.replace(ZERO_FAIL_SUMMARY, "") : body;
|
|
149
|
+
|
|
150
|
+
const showsFailure = failureRe.test(failureBody);
|
|
142
151
|
const showsSuccess = successRe.test(body);
|
|
143
152
|
|
|
144
153
|
// Failure is DECISIVE. A log that shows a definitive failure marker cannot
|