@mmerterden/multi-agent-pipeline 11.4.0 → 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 +118 -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 +70 -10
- package/install/copilot.mjs +75 -6
- package/package.json +4 -10
- package/pipeline/commands/multi-agent/dev-local/SKILL.md +1 -1
- 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/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 +4 -4
- 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-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-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/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
|
@@ -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
|
|
@@ -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
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
.claude/CLAUDE.md 1
|
|
2
2
|
.claude/agents 8
|
|
3
3
|
.claude/commands 44
|
|
4
|
-
.claude/lib
|
|
4
|
+
.claude/lib 25
|
|
5
5
|
.claude/multi-agent-preferences.json 1
|
|
6
6
|
.claude/multi-agent-refs 51
|
|
7
7
|
.claude/rules 12
|
|
8
8
|
.claude/schemas 23
|
|
9
|
-
.claude/scripts
|
|
9
|
+
.claude/scripts 184
|
|
10
10
|
.claude/settings.json 1
|
|
11
11
|
.claude/skills 428
|
|
12
12
|
.copilot/agents 8
|
|
13
13
|
.copilot/copilot-instructions.md 1
|
|
14
|
-
.copilot/lib
|
|
14
|
+
.copilot/lib 25
|
|
15
15
|
.copilot/schemas 23
|
|
16
|
-
.copilot/scripts
|
|
16
|
+
.copilot/scripts 184
|
|
17
17
|
.copilot/skills 467
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
943
|
|
@@ -17,11 +17,11 @@ fi
|
|
|
17
17
|
echo "Ne tür bir secret kaydedeceksin?"
|
|
18
18
|
echo " 1) Personal Access Token / API Key"
|
|
19
19
|
echo " 2) JSON dosyası (service account vb.)"
|
|
20
|
-
read -
|
|
20
|
+
read -rp "Seçim (1/2): " CHOICE
|
|
21
21
|
|
|
22
22
|
case "$CHOICE" in
|
|
23
23
|
1)
|
|
24
|
-
read -
|
|
24
|
+
read -rsp "Token'ı yapıştır (gizli yazılır): " SECRET
|
|
25
25
|
echo ""
|
|
26
26
|
if [ -z "$SECRET" ]; then
|
|
27
27
|
echo "Hata: Token boş olamaz."
|
|
@@ -29,7 +29,7 @@ case "$CHOICE" in
|
|
|
29
29
|
fi
|
|
30
30
|
;;
|
|
31
31
|
2)
|
|
32
|
-
read -
|
|
32
|
+
read -rp "JSON dosya yolunu gir: " JSON_PATH
|
|
33
33
|
# Kullanıcının girdiği tırnak işaretlerini temizle
|
|
34
34
|
JSON_PATH="${JSON_PATH//\'/}"
|
|
35
35
|
JSON_PATH="${JSON_PATH//\"/}"
|
|
@@ -53,20 +53,24 @@ case "$CHOICE" in
|
|
|
53
53
|
;;
|
|
54
54
|
esac
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
# Locate the credential helper via the resolver so Copilot-only installs work.
|
|
57
|
+
# shellcheck disable=SC1090,SC1091
|
|
58
|
+
. "$HOME/.claude/lib/credential-store-resolver.sh" 2>/dev/null \
|
|
59
|
+
|| . "$HOME/.copilot/lib/credential-store-resolver.sh" 2>/dev/null \
|
|
60
|
+
|| { echo "Hata: credential-store bulunamadi. Kurulum: npx @mmerterden/multi-agent-pipeline install"; exit 1; }
|
|
61
|
+
CRED="$CRED_STORE"
|
|
57
62
|
|
|
58
63
|
"$CRED" delete "$SERVICE_NAME" >/dev/null 2>&1 || true
|
|
59
|
-
"
|
|
60
|
-
|
|
61
|
-
if [ $? -eq 0 ]; then
|
|
64
|
+
# Secret goes through stdin ("set <key> -") so it never appears on argv.
|
|
65
|
+
if printf '%s' "$SECRET" | "$CRED" set "$SERVICE_NAME" -; then
|
|
62
66
|
echo "Kaydedildi: $SERVICE_NAME (platform: $("$CRED" platform))"
|
|
63
67
|
echo ""
|
|
64
68
|
echo "Okumak icin:"
|
|
65
|
-
echo "
|
|
69
|
+
echo " $CRED get \"$SERVICE_NAME\""
|
|
66
70
|
if [ "$CHOICE" = "2" ]; then
|
|
67
71
|
echo ""
|
|
68
72
|
echo "JSON'a geri cevirmek icin:"
|
|
69
|
-
echo "
|
|
73
|
+
echo " $CRED get \"$SERVICE_NAME\" | base64 -d"
|
|
70
74
|
fi
|
|
71
75
|
else
|
|
72
76
|
echo "Hata: Kaydedilemedi."
|
|
@@ -1,19 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Frontmatter linter for every SKILL.md shipped by the pipeline.
|
|
3
3
|
//
|
|
4
|
-
// Until v10.0.0 the
|
|
4
|
+
// Until v10.0.0 the shipped SKILL.md files were counted by smokes but never
|
|
5
5
|
// content-checked, so a skill could ship without the frontmatter that
|
|
6
6
|
// Claude Code / Copilot CLI need to register it. This gate enforces the
|
|
7
7
|
// minimal contract; it is zero-dependency (line-based YAML subset) so it
|
|
8
8
|
// runs inside `npm test` and ci-lite without installs.
|
|
9
9
|
//
|
|
10
|
-
// Contract per SKILL.md:
|
|
10
|
+
// Contract per SKILL.md (errors - fail the gate):
|
|
11
11
|
// - frontmatter block present (--- ... ---) starting at line 1
|
|
12
12
|
// - name: required, lowercase kebab-case (underscores tolerated for
|
|
13
13
|
// legacy skills already published under a snake_case name)
|
|
14
14
|
// - description: required, non-empty, single line, <= 1024 chars
|
|
15
15
|
// - user-invocable: when present, must be true or false
|
|
16
16
|
// - platform: when present, must be ios | android | common | all
|
|
17
|
+
//
|
|
18
|
+
// Spec-conformance signals (warnings - never fail the gate):
|
|
19
|
+
// - name should match the agent-skills spec pattern
|
|
20
|
+
// (lowercase letters/digits/hyphens, <= 64 chars)
|
|
21
|
+
// - description should carry a routing/trigger clause ("use when ...")
|
|
22
|
+
// so the model can decide when to load the skill
|
|
23
|
+
// - a body over ~500 lines with no companion-file reference should be
|
|
24
|
+
// split into references/ files loaded on demand
|
|
17
25
|
|
|
18
26
|
import { readFileSync } from "node:fs";
|
|
19
27
|
import { execSync } from "node:child_process";
|
|
@@ -61,6 +69,16 @@ for (const file of files) {
|
|
|
61
69
|
fail(file, "missing required field: name");
|
|
62
70
|
} else if (!/^[a-z0-9]+([-_][a-z0-9]+)*$/.test(fields.name)) {
|
|
63
71
|
fail(file, `name is not lowercase kebab/snake-case: "${fields.name}"`);
|
|
72
|
+
} else {
|
|
73
|
+
// Spec conformance (warn-only): the agent-skills spec allows lowercase
|
|
74
|
+
// letters, digits, and hyphens, max 64 chars. Snake_case passes the
|
|
75
|
+
// legacy error check above but is out of spec.
|
|
76
|
+
if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(fields.name)) {
|
|
77
|
+
warn(file, `name is outside the agent-skills spec pattern (hyphens only): "${fields.name}"`);
|
|
78
|
+
}
|
|
79
|
+
if (fields.name.length > 64) {
|
|
80
|
+
warn(file, `name exceeds the spec's 64-char limit (${fields.name.length})`);
|
|
81
|
+
}
|
|
64
82
|
}
|
|
65
83
|
|
|
66
84
|
if (!("description" in fields)) {
|
|
@@ -81,6 +99,26 @@ for (const file of files) {
|
|
|
81
99
|
if (!isBlockScalar && words < 6) {
|
|
82
100
|
warn(file, `description is terse (${words} words) - say what it does AND when to use it`);
|
|
83
101
|
}
|
|
102
|
+
// Routing quality (warn-only): without a trigger clause the model has no
|
|
103
|
+
// signal for WHEN to load the skill. Heuristic phrase match; block
|
|
104
|
+
// scalars are skipped for the same parser reason as above.
|
|
105
|
+
const ROUTING_RE = /\b(use (this|it|when|for|whenever)|when (the|a|an|you|asked|working|building|implementing|writing|reviewing)|triggers?|invoked?( when| for)?|applies when|activated?)\b/i;
|
|
106
|
+
if (!isBlockScalar && !ROUTING_RE.test(desc)) {
|
|
107
|
+
warn(file, "description has no routing/trigger clause (e.g. \"use when ...\")");
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Body size (warn-only): the agent-skills guidance is to keep SKILL.md
|
|
112
|
+
// lean and push detail into companion files loaded on demand. A ~500+
|
|
113
|
+
// line body that references no companion file loads whole every time.
|
|
114
|
+
const body = raw.slice(end + 4);
|
|
115
|
+
const bodyLines = body.split("\n").length;
|
|
116
|
+
if (bodyLines > 500) {
|
|
117
|
+
const referencesCompanion =
|
|
118
|
+
/\]\((?!https?:|mailto:|#)[^)]+\)/.test(body) || /`[^`\n]*\/[^`\n]*\.(md|sh|mjs|json|py|txt)`/.test(body);
|
|
119
|
+
if (!referencesCompanion) {
|
|
120
|
+
warn(file, `body is ${bodyLines} lines with no companion-file reference - consider splitting into references/`);
|
|
121
|
+
}
|
|
84
122
|
}
|
|
85
123
|
|
|
86
124
|
if ("user-invocable" in fields && !["true", "false"].includes(fields["user-invocable"])) {
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// migrate-prefs.mjs - migrates multi-agent-preferences.json
|
|
2
|
+
// migrate-prefs.mjs - migrates multi-agent-preferences.json to the current
|
|
3
|
+
// target schema (see TARGET_VERSION below).
|
|
3
4
|
//
|
|
4
5
|
// Usage:
|
|
5
6
|
// node migrate-prefs.mjs [--dry-run] [--file <path>]
|
|
6
7
|
//
|
|
7
|
-
// - Idempotent: safe to run multiple times.
|
|
8
|
+
// - Idempotent: safe to run multiple times. Target-version files pass through unchanged.
|
|
8
9
|
// - Atomic: writes to a .tmp file and rename()s. No partial writes.
|
|
9
10
|
// - Non-destructive: renames gitIdentities → identities, preserves everything else.
|
|
10
11
|
// - Fills defaults for new v2.1.0 fields.
|
|
@@ -15,15 +16,33 @@ import os from "node:os";
|
|
|
15
16
|
|
|
16
17
|
const DEFAULT_FILE = path.join(os.homedir(), ".claude", "multi-agent-preferences.json");
|
|
17
18
|
|
|
19
|
+
// Single source of truth for the migration target version. Referenced by the
|
|
20
|
+
// migrate() bump chain AND every user-facing message, so they cannot drift.
|
|
21
|
+
const TARGET_VERSION = "2.3.0";
|
|
22
|
+
|
|
23
|
+
const USAGE = "Usage: migrate-prefs.mjs [--dry-run] [--file <path>]";
|
|
24
|
+
|
|
18
25
|
function parseArgs(argv) {
|
|
19
26
|
const args = { file: DEFAULT_FILE, dryRun: false };
|
|
20
27
|
for (let i = 2; i < argv.length; i++) {
|
|
21
28
|
const a = argv[i];
|
|
22
29
|
if (a === "--dry-run") args.dryRun = true;
|
|
23
|
-
else if (a === "--file")
|
|
24
|
-
|
|
25
|
-
|
|
30
|
+
else if (a === "--file") {
|
|
31
|
+
const v = argv[++i];
|
|
32
|
+
if (v === undefined) {
|
|
33
|
+
console.error("ERROR: --file requires a path argument");
|
|
34
|
+
console.error(USAGE);
|
|
35
|
+
process.exit(64);
|
|
36
|
+
}
|
|
37
|
+
args.file = v;
|
|
38
|
+
} else if (a === "--help" || a === "-h") {
|
|
39
|
+
console.log(USAGE);
|
|
26
40
|
process.exit(0);
|
|
41
|
+
} else {
|
|
42
|
+
// A typo like --dry-rnu must never fall through to a real write.
|
|
43
|
+
console.error(`ERROR: unknown argument: ${a}`);
|
|
44
|
+
console.error(USAGE);
|
|
45
|
+
process.exit(64);
|
|
27
46
|
}
|
|
28
47
|
}
|
|
29
48
|
return args;
|
|
@@ -142,7 +161,7 @@ function migrate(prefs) {
|
|
|
142
161
|
// v2.3.0 is the v7.4.0 target - adds optional `accounts[]` (per-account host
|
|
143
162
|
// overrides) and `recentAccounts[]` (LRU for autopilot account picker).
|
|
144
163
|
// Both default to empty arrays; old prefs continue to validate without them.
|
|
145
|
-
const TARGET =
|
|
164
|
+
const TARGET = TARGET_VERSION;
|
|
146
165
|
if (out.schemaVersion === TARGET) {
|
|
147
166
|
// already on target - skip version bump, but still run sub-migrations below
|
|
148
167
|
} else if (out.schemaVersion === "2.2.0") {
|
|
@@ -371,7 +390,7 @@ function main() {
|
|
|
371
390
|
const { out, changes, alreadyMigrated } = migrate(prefs);
|
|
372
391
|
|
|
373
392
|
if (alreadyMigrated) {
|
|
374
|
-
console.log(`✓ already
|
|
393
|
+
console.log(`✓ already v${TARGET_VERSION} - no changes: ${args.file}`);
|
|
375
394
|
process.exit(0);
|
|
376
395
|
}
|
|
377
396
|
|