@mmerterden/multi-agent-pipeline 11.4.0 → 12.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/CHANGELOG.md +172 -0
  2. package/README.md +1 -0
  3. package/docs/adr/0007-multi-tool-adapter-framework.md +2 -2
  4. package/index.js +9 -2
  5. package/install/_common.mjs +107 -5
  6. package/install/_telemetry.mjs +5 -23
  7. package/install/claude.mjs +95 -11
  8. package/install/copilot.mjs +75 -6
  9. package/install/index.mjs +34 -1
  10. package/package.json +4 -10
  11. package/pipeline/commands/multi-agent/SKILL.md +1 -0
  12. package/pipeline/commands/multi-agent/analysis/SKILL.md +1 -0
  13. package/pipeline/commands/multi-agent/analysis-resolve/SKILL.md +1 -0
  14. package/pipeline/commands/multi-agent/autopilot/SKILL.md +1 -0
  15. package/pipeline/commands/multi-agent/build-optimize/SKILL.md +1 -0
  16. package/pipeline/commands/multi-agent/channels/SKILL.md +1 -0
  17. package/pipeline/commands/multi-agent/create-jira/SKILL.md +1 -0
  18. package/pipeline/commands/multi-agent/dev/SKILL.md +1 -0
  19. package/pipeline/commands/multi-agent/dev-autopilot/SKILL.md +1 -0
  20. package/pipeline/commands/multi-agent/dev-local/SKILL.md +2 -1
  21. package/pipeline/commands/multi-agent/dev-local-autopilot/SKILL.md +1 -0
  22. package/pipeline/commands/multi-agent/diff-explain/SKILL.md +1 -0
  23. package/pipeline/commands/multi-agent/finish/SKILL.md +1 -0
  24. package/pipeline/commands/multi-agent/garbage-collect/SKILL.md +29 -3
  25. package/pipeline/commands/multi-agent/help/SKILL.md +3 -2
  26. package/pipeline/commands/multi-agent/issue/SKILL.md +1 -0
  27. package/pipeline/commands/multi-agent/jira/SKILL.md +1 -0
  28. package/pipeline/commands/multi-agent/kill/SKILL.md +1 -0
  29. package/pipeline/commands/multi-agent/language/SKILL.md +20 -1
  30. package/pipeline/commands/multi-agent/local/SKILL.md +1 -0
  31. package/pipeline/commands/multi-agent/local-autopilot/SKILL.md +1 -0
  32. package/pipeline/commands/multi-agent/log/SKILL.md +1 -0
  33. package/pipeline/commands/multi-agent/manual-test/SKILL.md +1 -0
  34. package/pipeline/commands/multi-agent/prune-logs/SKILL.md +1 -0
  35. package/pipeline/commands/multi-agent/purge/SKILL.md +27 -24
  36. package/pipeline/commands/multi-agent/refactor/SKILL.md +1 -0
  37. package/pipeline/commands/multi-agent/resume/SKILL.md +1 -0
  38. package/pipeline/commands/multi-agent/review/SKILL.md +1 -0
  39. package/pipeline/commands/multi-agent/review-issue/SKILL.md +1 -0
  40. package/pipeline/commands/multi-agent/review-jira/SKILL.md +1 -0
  41. package/pipeline/commands/multi-agent/scan/SKILL.md +1 -0
  42. package/pipeline/commands/multi-agent/search/SKILL.md +1 -0
  43. package/pipeline/commands/multi-agent/setup/SKILL.md +1 -0
  44. package/pipeline/commands/multi-agent/stack/SKILL.md +1 -0
  45. package/pipeline/commands/multi-agent/status/SKILL.md +1 -0
  46. package/pipeline/commands/multi-agent/sync/SKILL.md +20 -2
  47. package/pipeline/commands/multi-agent/test/SKILL.md +1 -0
  48. package/pipeline/commands/multi-agent/uninstall/SKILL.md +87 -0
  49. package/pipeline/commands/multi-agent/update/SKILL.md +1 -0
  50. package/pipeline/lib/account-resolver.sh +61 -23
  51. package/pipeline/lib/channels-multi-repo.sh +7 -2
  52. package/pipeline/lib/count-lib.sh +27 -0
  53. package/pipeline/lib/credential-store.sh +23 -6
  54. package/pipeline/lib/extract-conventions.sh +27 -21
  55. package/pipeline/lib/fetch-confluence.sh +25 -13
  56. package/pipeline/lib/fetch-crashlytics.sh +30 -8
  57. package/pipeline/lib/fetch-fortify.sh +11 -2
  58. package/pipeline/lib/fetch-swagger.sh +18 -7
  59. package/pipeline/lib/figma-mcp-refresh.sh +26 -11
  60. package/pipeline/lib/figma-screenshot.sh +11 -2
  61. package/pipeline/lib/issue-fetcher.sh +31 -6
  62. package/pipeline/lib/multi-repo-pipeline.sh +84 -20
  63. package/pipeline/lib/plan-todos.sh +12 -3
  64. package/pipeline/lib/post-pr-review.sh +5 -3
  65. package/pipeline/lib/repo-cache.sh +53 -9
  66. package/pipeline/lib/review-watch.sh +26 -6
  67. package/pipeline/lib/shadow-git.sh +45 -4
  68. package/pipeline/lib/vercel-deploy.sh +10 -1
  69. package/pipeline/multi-agent-refs/cross-cli-contract.md +5 -5
  70. package/pipeline/multi-agent-refs/phases/phase-0-init.md +12 -1
  71. package/pipeline/scripts/audit-log-rotate.sh +38 -10
  72. package/pipeline/scripts/check-md-links.mjs +34 -9
  73. package/pipeline/scripts/diff-risk-score.mjs +4 -1
  74. package/pipeline/scripts/evidence-gate.mjs +10 -1
  75. package/pipeline/scripts/fixtures/install-layout.tsv +6 -4
  76. package/pipeline/scripts/fixtures/pack-expected-count.txt +1 -0
  77. package/pipeline/scripts/gc-tmp.sh +67 -8
  78. package/pipeline/scripts/gc-worktrees.sh +140 -0
  79. package/pipeline/scripts/keychain-save.sh +13 -9
  80. package/pipeline/scripts/lint-skills.mjs +40 -2
  81. package/pipeline/scripts/localize-commands.mjs +130 -0
  82. package/pipeline/scripts/migrate-prefs.mjs +26 -7
  83. package/pipeline/scripts/phase-tracker.sh +71 -0
  84. package/pipeline/scripts/pre-commit-check.sh +39 -0
  85. package/pipeline/scripts/prune-logs.sh +100 -15
  86. package/pipeline/scripts/purge.sh +235 -0
  87. package/pipeline/scripts/scan-skills.sh +217 -127
  88. package/pipeline/scripts/smoke-bitbucket-contract.sh +21 -5
  89. package/pipeline/scripts/smoke-description-tr.sh +82 -0
  90. package/pipeline/scripts/smoke-fetchers-offline.sh +382 -0
  91. package/pipeline/scripts/smoke-gc-tmp.sh +50 -4
  92. package/pipeline/scripts/smoke-gc-worktrees.sh +125 -0
  93. package/pipeline/scripts/smoke-install-layout.sh +11 -3
  94. package/pipeline/scripts/smoke-lib-scripts.sh +54 -1
  95. package/pipeline/scripts/smoke-pack-contents.sh +140 -0
  96. package/pipeline/scripts/smoke-plugin-validate.sh +64 -0
  97. package/pipeline/scripts/smoke-prune-logs.sh +57 -7
  98. package/pipeline/scripts/smoke-purge.sh +138 -0
  99. package/pipeline/scripts/smoke-shadow-git.sh +48 -1
  100. package/pipeline/scripts/smoke-skill-authoring.sh +1 -1
  101. package/pipeline/scripts/smoke-update-check.sh +13 -0
  102. package/pipeline/scripts/smoke-workflow-audit.sh +69 -0
  103. package/pipeline/scripts/test-gap-scan.mjs +12 -1
  104. package/pipeline/scripts/triage-memory.mjs +10 -1
  105. package/pipeline/scripts/uninstall.mjs +146 -42
  106. package/pipeline/scripts/update-check.sh +9 -0
  107. package/pipeline/scripts/update-issue-progress.sh +60 -41
  108. package/pipeline/scripts/write-state.mjs +14 -4
  109. package/pipeline/skills/.skill-manifest.json +7 -7
  110. package/pipeline/skills/.skills-index.json +9 -9
  111. package/pipeline/skills/shared/README.md +19 -10
  112. package/pipeline/skills/shared/core/multi-agent/SKILL.md +1 -0
  113. package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +2 -2
  114. package/pipeline/skills/shared/core/multi-agent-uninstall/SKILL.md +74 -0
  115. package/pipeline/skills/skills-index.md +1 -1
  116. package/pipeline/commands/multi-agent/delete/SKILL.md +0 -67
  117. package/pipeline/skills/shared/core/multi-agent-delete/SKILL.md +0 -66
@@ -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
- ' > "$CACHE_FILE"
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
- BB_USER="${BB_USER:-${BB_USER_KEY:+$("$HOME/.claude/lib/credential-store.sh" get "$BB_USER_KEY" 2>/dev/null || true)}}"
110
- BB_TOKEN=$("$HOME/.claude/lib/credential-store.sh" get "$BB_TOKEN_KEY" 2>/dev/null || true)
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 -sf -u "${BB_USER:-bot}:${BB_TOKEN}" \
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
- ' > "$CACHE_FILE"
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
- : > "$CACHE_FILE.tmp"
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" >> "$CACHE_FILE.tmp"
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
- ' "$CACHE_FILE.tmp" > "$CACHE_FILE"
222
- rm -f "$CACHE_FILE.tmp"
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" --arg t "$updated_at" \
200
- '.reviewed_prs[$n] = $s | .last_seen_iso = $t' <<<"$state")
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
- for r in "${REPOS[@]}"; do review_one_repo "$r"; done
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
- while IFS= read -r r; do [ -n "$r" ] && review_one_repo "$r"; done < <(read_repos_from_prefs)
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
- # Mirror the project's .gitignore so we don't snapshot node_modules / Pods.
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
- cp "$project_root/.gitignore" "$sd/.gitignore"
111
+ cat "$project_root/.gitignore" >> "$sd/.git/info/exclude"
89
112
  fi
90
- # Also exclude these from snapshots even if .gitignore omits them.
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
- echo "ERROR: VERCEL_TOKEN not set. Resolve via env or keychain ('~/.claude/lib/credential-store.sh get <vercel-key>')." >&2
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
@@ -9,11 +9,11 @@
9
9
  ## 1. Command Inventory (38 commands)
10
10
 
11
11
  ```
12
- analysis, analysis-resolve, autopilot, build-optimize, channels, create-jira, delete, dev,
12
+ analysis, analysis-resolve, autopilot, build-optimize, channels, create-jira, dev,
13
13
  dev-autopilot, dev-local, dev-local-autopilot, diff-explain, finish, garbage-collect,
14
14
  help, issue, jira, kill, language, local,
15
15
  local-autopilot, log, manual-test, prune-logs, purge, refactor, resume, review, review-issue, review-jira,
16
- scan, search, setup, stack, status, sync, test, update
16
+ scan, search, setup, stack, status, sync, test, uninstall, update
17
17
  ```
18
18
 
19
19
  Categories:
@@ -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`, `review-jira`, `review-issue`, `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`, `uninstall`, `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.
@@ -163,10 +163,10 @@ argument-hint: "<input hint>"
163
163
 
164
164
  | Direction | Action |
165
165
  |---|---|
166
- | Claude → Copilot | Add `name`, `user-invocable: true`, `argument-hint`; remove `allowed-tools`; may optionally translate `description` into Turkish (existing pattern) |
166
+ | Claude → Copilot | Add `name`, `user-invocable: true`, `argument-hint`; remove `allowed-tools`; `description` stays English (localization happens only in the installed Claude tree via `description-tr` + `localize-commands.mjs`, never in repo or Copilot files) |
167
167
  | Copilot → Claude | Remove `name`, `user-invocable`, `argument-hint`; add `allowed-tools` (inferred from command's actual tool calls) |
168
168
 
169
- **Invariant**: the `description` field, after potential translation, must have the same meaning on both sides. Translation is allowed; semantic drift is not.
169
+ **Invariant**: the English `description` must have the same meaning on both sides, and a `description-tr` line (when present) must be a faithful translation of it. Semantic drift is not allowed. `description-en` sidecars are an installed-tree artifact and must never appear in repo or Copilot files.
170
170
 
171
171
  ---
172
172
 
@@ -425,7 +425,7 @@ git -C $PROJECT_ROOT config user.email "{identity.email}"
425
425
 
426
426
  `worktreePath` = `$PROJECT_ROOT`, `localMode` = `true`.
427
427
 
428
- **If normal mode** (worktree - default): 2. Worktree path: Jira → `.worktrees/{jiraId}/`, GitHub → `.worktrees/GH{issueNo}/`, free-text → `.worktrees/task-{shortId}/` 3. **Heal stale admin state first** (see "Worktree stale-lock heal" below), then `git -C $PROJECT_ROOT worktree add {path} -b {branch} origin/{baseBranch}` (if exists: enter, pull) 4. Set identity: `git -C {worktree-path} config user.name/email` 5. Create log dir + `agent-log.md` + `agent-state.json`:
428
+ **If normal mode** (worktree - default): 2. Worktree path: Jira → `.worktrees/{jiraId}/`, GitHub → `.worktrees/GH{issueNo}/`, free-text → `.worktrees/task-{shortId}/` 3. **Heal stale admin state first** (see "Worktree stale-lock heal" below) and **apply the residue guard** (see "Worktree residue guard" below), then `git -C $PROJECT_ROOT worktree add {path} -b {branch} origin/{baseBranch}` (if exists: enter, pull) 4. Set identity: `git -C {worktree-path} config user.name/email` 5. Create log dir + `agent-log.md` + `agent-state.json`:
429
429
 
430
430
  **Worktree stale-lock heal (required before every `worktree add`):** a run killed mid-`worktree add` (OOM, SIGTERM, disk full) leaves a locked or broken admin entry under `.git/worktrees/{id}/`, so the retry fails with `fatal: '<path>' already exists`. Always run the heal first - it is a no-op on a clean repo:
431
431
 
@@ -438,6 +438,14 @@ fi
438
438
 
439
439
  If the path is still registered and healthy after the heal, enter + pull instead of re-adding (existing behavior). Only when `worktree add` still fails after the heal do the rollback / collision flow in the multi-repo block below.
440
440
 
441
+ **Worktree residue guard (required once per repo, idempotent):** every worktree dir contains a `.git` file, so a blanket `git add -A` / `git add .` in the parent tree records `.worktrees/{id}` as a gitlink ("Subproject commit ...") that pollutes the branch and later needs a manual removal commit. Keep `.worktrees/` out of the index via the clone-local exclude file (never committed, so no PR noise in shared repos):
442
+
443
+ ```bash
444
+ ex="$(git -C "$PROJECT_ROOT" rev-parse --path-format=absolute --git-common-dir)/info/exclude"
445
+ mkdir -p "$(dirname "$ex")"
446
+ grep -qxF '.worktrees/' "$ex" 2>/dev/null || printf '.worktrees/\n' >> "$ex"
447
+ ```
448
+
441
449
  **v2.1.0+ Multi-Repo Worktree Setup**:
442
450
 
443
451
  When `state.projects[].length > 1`, repeat steps 2-4 **serially per repo** (worktrees are cheap; serial keeps git index sane and surfaces collisions one at a time):
@@ -448,6 +456,9 @@ for proj in "${PROJECTS[@]}"; do
448
456
  git -C "$proj" worktree prune 2>/dev/null || true # heal stale admin state
449
457
  git -C "$proj" worktree list --porcelain | grep -qF "$WT_PATH" \
450
458
  && git -C "$proj" worktree unlock "$WT_PATH" 2>/dev/null || true
459
+ ex="$(git -C "$proj" rev-parse --path-format=absolute --git-common-dir)/info/exclude"
460
+ mkdir -p "$(dirname "$ex")"
461
+ grep -qxF '.worktrees/' "$ex" 2>/dev/null || printf '.worktrees/\n' >> "$ex" # residue guard
451
462
  git -C "$proj" worktree add "$WT_PATH" -b "$BRANCH" "origin/$BASE_BRANCH"
452
463
  git -C "$WT_PATH" config user.name "${proj_identity_name}"
453
464
  git -C "$WT_PATH" config user.email "${proj_identity_email}"
@@ -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
- STAMP="$(date -u -r "$AUDIT_FILE" +"%Y-%m-%d" 2>/dev/null || date -u +"%Y-%m-%d")"
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
- # Don't clobber a rotated file from earlier today (multiple invocations)
41
- if [ -f "$ROTATED" ]; then
42
- cat "$AUDIT_FILE" >> "$ROTATED"
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 "$AUDIT_FILE" "$ROTATED"
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. Verifies that every
5
- // relative link target ([text](./path) or [text](path#anchor)) resolves to
6
- // an existing file or directory. External (http/https/mailto) links and
7
- // pure #anchors are skipped - this is a drift gate for file moves/renames,
8
- // not a network crawler.
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
- ].filter((f) => !f.endsWith("CHANGELOG-archive.md"));
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
- const resolved = resolve(dirname(file), decodeURIComponent(target));
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
- const showsFailure = failureRe.test(body);
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,19 @@
1
+ .claude/.pipeline-version 1
1
2
  .claude/CLAUDE.md 1
2
3
  .claude/agents 8
3
4
  .claude/commands 44
4
- .claude/lib 24
5
+ .claude/lib 25
5
6
  .claude/multi-agent-preferences.json 1
6
7
  .claude/multi-agent-refs 51
7
8
  .claude/rules 12
8
9
  .claude/schemas 23
9
- .claude/scripts 180
10
+ .claude/scripts 190
10
11
  .claude/settings.json 1
11
12
  .claude/skills 428
13
+ .copilot/.pipeline-version 1
12
14
  .copilot/agents 8
13
15
  .copilot/copilot-instructions.md 1
14
- .copilot/lib 24
16
+ .copilot/lib 25
15
17
  .copilot/schemas 23
16
- .copilot/scripts 180
18
+ .copilot/scripts 190
17
19
  .copilot/skills 467