@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
@@ -85,12 +85,24 @@ add_finding() {
85
85
  FINDINGS+=("$sev|$file|$line|$pattern|$message")
86
86
  }
87
87
 
88
- # --- Pattern scanning ---------------------------------------------------
88
+ # --- Pattern scanning (single-pass tree-wide) ----------------------------
89
+ #
90
+ # Each pattern family runs as ONE grep over the whole file list instead of
91
+ # one grep per file (the per-file design spawned thousands of processes and
92
+ # dominated install time). Findings are tagged with (fileIdx, familyIdx,
93
+ # seq) sort keys and re-ordered afterwards so the output is byte-identical
94
+ # to the historical per-file scan order:
95
+ # per file: critical(1 shell-pipe, 2 base64-pipe, 3 eval-net, 4 bidi)
96
+ # high(5 js-eval / 6 py-exec, 7 credential, 8 pastebin, 9 chmod)
97
+ # medium(10 long-base64) low(11 unknown-endpoint, 12 SKILL.md)
89
98
 
90
99
  # Files to consider: *.md, *.sh, *.py, *.mjs, *.js, *.ts (skill content)
91
100
  # Exclude: binary, images, large fixtures, hidden
92
- SCAN_LIST=$(mktemp)
93
- trap 'rm -f "$SCAN_LIST"' EXIT
101
+ WORK="$(mktemp -d)"
102
+ trap 'rm -rf "$WORK"' EXIT
103
+ SCAN_LIST="$WORK/files"
104
+ RAW="$WORK/raw"
105
+ : > "$RAW"
94
106
 
95
107
  find "$ROOT" -type f \
96
108
  \( -name "*.md" -o -name "*.sh" -o -name "*.py" -o -name "*.mjs" -o -name "*.js" -o -name "*.ts" \) \
@@ -98,156 +110,230 @@ find "$ROOT" -type f \
98
110
  -not -path "*/.git/*" \
99
111
  2>/dev/null > "$SCAN_LIST"
100
112
 
101
- scan_critical() {
102
- local f="$1"
103
- # curl|wget piping to shell
104
- while IFS= read -r hit; do
105
- [ -z "$hit" ] && continue
106
- local ln="${hit%%:*}"
107
- add_finding critical "$f" "$ln" "shell-pipe-exec" "curl/wget piped to shell interpreter"
108
- done < <(grep -nE '(curl|wget)[^|]*\|[[:space:]]*(sh|bash|zsh|ksh|ash|dash)([[:space:]]|$)' "$f" 2>/dev/null)
113
+ FILE_COUNT=$(wc -l < "$SCAN_LIST" | tr -d '[:space:]')
109
114
 
110
- # base64 -d | shell
111
- while IFS= read -r hit; do
112
- [ -z "$hit" ] && continue
113
- local ln="${hit%%:*}"
114
- add_finding critical "$f" "$ln" "base64-pipe-exec" "base64 decoded output piped to shell"
115
- done < <(grep -nE '(base64|openssl[[:space:]]+base64)[^|]*-d[^|]*\|[[:space:]]*(sh|bash|zsh|eval|sudo)' "$f" 2>/dev/null)
115
+ # One grep invocation for a whole pattern family. xargs preserves argument
116
+ # order, so hits come out grouped per file in SCAN_LIST order.
117
+ tree_grep() {
118
+ tr '\n' '\0' < "$SCAN_LIST" | xargs -0 grep -nHE -- "$1" 2>/dev/null
119
+ return 0
120
+ }
116
121
 
117
- # eval $(curl|wget ...) or eval `curl ...`
118
- while IFS= read -r hit; do
119
- [ -z "$hit" ] && continue
120
- local ln="${hit%%:*}"
121
- add_finding critical "$f" "$ln" "eval-of-network" "eval of network-fetched content"
122
- done < <(grep -nE 'eval[[:space:]]+(\$\([[:space:]]*(curl|wget|fetch)|`[[:space:]]*(curl|wget|fetch))' "$f" 2>/dev/null)
122
+ # stdin: "path:line:content" grep hits -> stdout: "fileIdx|path|line|content"
123
+ index_hits() {
124
+ awk -v listfile="$SCAN_LIST" '
125
+ BEGIN {
126
+ i = 0
127
+ while ((getline l < listfile) > 0) { i++; idx[l] = i }
128
+ close(listfile)
129
+ }
130
+ {
131
+ p = index($0, ":"); f = substr($0, 1, p - 1)
132
+ rest = substr($0, p + 1)
133
+ q = index(rest, ":"); ln = substr(rest, 1, q - 1)
134
+ printf "%d|%s|%s|%s\n", (f in idx ? idx[f] : 999999), f, ln, substr(rest, q + 1)
135
+ }'
136
+ }
123
137
 
124
- # Unicode bidi override characters - invisible injection attack
125
- if LC_ALL=C grep -l $'\xe2\x80\xad\|\xe2\x80\xae\|\xe2\x81\xa6\|\xe2\x81\xa7\|\xe2\x81\xa8\|\xe2\x81\xa9' "$f" 2>/dev/null >/dev/null; then
126
- add_finding critical "$f" "0" "unicode-bidi" "bidirectional control chars (U+202D-202E, U+2066-2069) - trojan source risk"
127
- fi
138
+ # stdin: bare file paths -> stdout: "fileIdx|path"
139
+ index_files() {
140
+ awk -v listfile="$SCAN_LIST" '
141
+ BEGIN {
142
+ i = 0
143
+ while ((getline l < listfile) > 0) { i++; idx[l] = i }
144
+ close(listfile)
145
+ }
146
+ { printf "%d|%s\n", ($0 in idx ? idx[$0] : 999999), $0 }'
128
147
  }
129
148
 
130
- scan_high() {
131
- local f="$1"
132
- local ext="${f##*.}"
149
+ emit_raw() {
150
+ # $1 fileIdx $2 familyIdx $3 seq $4 sev $5 file $6 line $7 pattern $8 message
151
+ printf '%s|%s|%s|%s|%s|%s|%s|%s\n' "$1" "$2" "$3" "$4" "$5" "$6" "$7" "$8" >> "$RAW"
152
+ }
153
+
154
+ # Split an indexed hit "fileIdx|path|line|content" into HIT_IDX/HIT_FILE/
155
+ # HIT_LINE/HIT_CONTENT (content may itself contain pipes - it is the rest).
156
+ parse_hit() {
157
+ HIT_IDX="${1%%|*}"; local rest="${1#*|}"
158
+ HIT_FILE="${rest%%|*}"; rest="${rest#*|}"
159
+ HIT_LINE="${rest%%|*}"
160
+ HIT_CONTENT="${rest#*|}"
161
+ }
133
162
 
163
+ # --- critical families (rank 0 - always at/above threshold) ------------
164
+
165
+ seq=0
166
+ while IFS= read -r hit; do
167
+ [ -z "$hit" ] && continue
168
+ parse_hit "$hit"
169
+ seq=$((seq+1))
170
+ emit_raw "$HIT_IDX" 1 "$seq" critical "$HIT_FILE" "$HIT_LINE" "shell-pipe-exec" "curl/wget piped to shell interpreter"
171
+ done < <(tree_grep '(curl|wget)[^|]*\|[[:space:]]*(sh|bash|zsh|ksh|ash|dash)([[:space:]]|$)' | index_hits)
172
+
173
+ seq=0
174
+ while IFS= read -r hit; do
175
+ [ -z "$hit" ] && continue
176
+ parse_hit "$hit"
177
+ seq=$((seq+1))
178
+ emit_raw "$HIT_IDX" 2 "$seq" critical "$HIT_FILE" "$HIT_LINE" "base64-pipe-exec" "base64 decoded output piped to shell"
179
+ done < <(tree_grep '(base64|openssl[[:space:]]+base64)[^|]*-d[^|]*\|[[:space:]]*(sh|bash|zsh|eval|sudo)' | index_hits)
180
+
181
+ seq=0
182
+ while IFS= read -r hit; do
183
+ [ -z "$hit" ] && continue
184
+ parse_hit "$hit"
185
+ seq=$((seq+1))
186
+ emit_raw "$HIT_IDX" 3 "$seq" critical "$HIT_FILE" "$HIT_LINE" "eval-of-network" "eval of network-fetched content"
187
+ done < <(tree_grep 'eval[[:space:]]+(\$\([[:space:]]*(curl|wget|fetch)|`[[:space:]]*(curl|wget|fetch))' | index_hits)
188
+
189
+ # Unicode bidi override characters - invisible injection attack
190
+ seq=0
191
+ while IFS= read -r row; do
192
+ [ -z "$row" ] && continue
193
+ seq=$((seq+1))
194
+ emit_raw "${row%%|*}" 4 "$seq" critical "${row#*|}" "0" "unicode-bidi" "bidirectional control chars (U+202D-202E, U+2066-2069) - trojan source risk"
195
+ done < <(tr '\n' '\0' < "$SCAN_LIST" \
196
+ | LC_ALL=C xargs -0 grep -l $'\xe2\x80\xad\|\xe2\x80\xae\|\xe2\x81\xa6\|\xe2\x81\xa7\|\xe2\x81\xa8\|\xe2\x81\xa9' 2>/dev/null \
197
+ | index_files; true)
198
+
199
+ # --- high families (rank 1) ----------------------------------------------
200
+
201
+ if [ "$THRESHOLD_RANK" -ge 1 ]; then
134
202
  # JavaScript/TypeScript: eval(, new Function(, Function(
135
- if [ "$ext" = "js" ] || [ "$ext" = "mjs" ] || [ "$ext" = "ts" ]; then
136
- while IFS= read -r hit; do
137
- [ -z "$hit" ] && continue
138
- local ln="${hit%%:*}"
139
- add_finding high "$f" "$ln" "js-dynamic-eval" "JavaScript dynamic code execution"
140
- done < <(grep -nE '\b(eval|new[[:space:]]+Function|Function)\s*\(' "$f" 2>/dev/null)
141
- fi
203
+ seq=0
204
+ while IFS= read -r hit; do
205
+ [ -z "$hit" ] && continue
206
+ parse_hit "$hit"
207
+ case "$HIT_FILE" in *.js|*.mjs|*.ts) ;; *) continue ;; esac
208
+ seq=$((seq+1))
209
+ emit_raw "$HIT_IDX" 5 "$seq" high "$HIT_FILE" "$HIT_LINE" "js-dynamic-eval" "JavaScript dynamic code execution"
210
+ done < <(tree_grep '\b(eval|new[[:space:]]+Function|Function)\s*\(' | index_hits)
142
211
 
143
212
  # Python: exec(, eval( on non-literal - exclude re.compile (regex) and subprocess
144
- if [ "$ext" = "py" ]; then
145
- while IFS= read -r hit; do
146
- [ -z "$hit" ] && continue
147
- local ln="${hit%%:*}"
148
- local content="${hit#*:}"
149
- # Skip re.compile (standard regex) and subprocess.* (legitimate process invocation)
150
- echo "$content" | grep -qE '(\bre\.compile|\bre2\.compile|subprocess\.|typing\.)' && continue
151
- add_finding high "$f" "$ln" "py-dynamic-exec" "Python dynamic code execution"
152
- done < <(grep -nE '(^|[^a-zA-Z0-9_.])(exec|eval)[[:space:]]*\(' "$f" 2>/dev/null)
153
- fi
213
+ seq=0
214
+ while IFS= read -r hit; do
215
+ [ -z "$hit" ] && continue
216
+ parse_hit "$hit"
217
+ case "$HIT_FILE" in *.py) ;; *) continue ;; esac
218
+ # Skip re.compile (standard regex) and subprocess.* (legitimate process invocation)
219
+ echo "$HIT_CONTENT" | grep -qE '(\bre\.compile|\bre2\.compile|subprocess\.|typing\.)' && continue
220
+ seq=$((seq+1))
221
+ emit_raw "$HIT_IDX" 6 "$seq" high "$HIT_FILE" "$HIT_LINE" "py-dynamic-exec" "Python dynamic code execution"
222
+ done < <(tree_grep '(^|[^a-zA-Z0-9_.])(exec|eval)[[:space:]]*\(' | index_hits)
154
223
 
155
224
  # Hardcoded API keys - AWS, OpenAI, GitHub, generic sk-* with length
156
225
  # Skip lines in FORBIDDEN/NEVER/example blocks (false positives from docs)
226
+ seq=0
157
227
  while IFS= read -r hit; do
158
228
  [ -z "$hit" ] && continue
159
- local ln="${hit%%:*}"
160
- # Check ±3 lines around match for documentation context markers
161
- local ctx_start=$((ln - 3))
229
+ parse_hit "$hit"
230
+ # Check +/-3 lines around match for documentation context markers
231
+ ctx_start=$((HIT_LINE - 3))
162
232
  [ "$ctx_start" -lt 1 ] && ctx_start=1
163
- local ctx_end=$((ln + 3))
164
- local ctx
165
- ctx=$(sed -n "${ctx_start},${ctx_end}p" "$f" 2>/dev/null)
233
+ ctx_end=$((HIT_LINE + 3))
234
+ ctx=$(sed -n "${ctx_start},${ctx_end}p" "$HIT_FILE" 2>/dev/null)
166
235
  if echo "$ctx" | grep -qiE '(FORBIDDEN|NEVER do|don.t do|example:|placeholder|sample credential|DO NOT|✗|XXX|YYY|dummy|fake.?key)'; then
167
236
  continue
168
237
  fi
169
- add_finding high "$f" "$ln" "hardcoded-credential" "possible hardcoded API credential"
170
- done < <(grep -nE '(AKIA[0-9A-Z]{16}|sk-(live|test|proj|ant|or)-[A-Za-z0-9_-]{20,}|ghp_[A-Za-z0-9]{36}|github_pat_[A-Za-z0-9_]{82}|gho_[A-Za-z0-9]{36}|xox[bp]-[A-Za-z0-9-]{10,})' "$f" 2>/dev/null)
238
+ seq=$((seq+1))
239
+ emit_raw "$HIT_IDX" 7 "$seq" high "$HIT_FILE" "$HIT_LINE" "hardcoded-credential" "possible hardcoded API credential"
240
+ done < <(tree_grep '(AKIA[0-9A-Z]{16}|sk-(live|test|proj|ant|or)-[A-Za-z0-9_-]{20,}|ghp_[A-Za-z0-9]{36}|github_pat_[A-Za-z0-9_]{82}|gho_[A-Za-z0-9]{36}|xox[bp]-[A-Za-z0-9-]{10,})' | index_hits)
171
241
 
172
242
  # Pastebin / URL shortener raw content fetching
243
+ seq=0
173
244
  while IFS= read -r hit; do
174
245
  [ -z "$hit" ] && continue
175
- local ln="${hit%%:*}"
176
- add_finding high "$f" "$ln" "pastebin-fetch" "fetch from ephemeral/obscured content host"
177
- done < <(grep -nE 'https?://(pastebin\.com/raw|paste\.ee|ghostbin|hastebin|bit\.ly|tinyurl\.com|goo\.gl|t\.co|is\.gd|ow\.ly|rebrand\.ly|gist\.github\.com/[^/]+/[a-f0-9]+/raw)' "$f" 2>/dev/null)
246
+ parse_hit "$hit"
247
+ seq=$((seq+1))
248
+ emit_raw "$HIT_IDX" 8 "$seq" high "$HIT_FILE" "$HIT_LINE" "pastebin-fetch" "fetch from ephemeral/obscured content host"
249
+ done < <(tree_grep 'https?://(pastebin\.com/raw|paste\.ee|ghostbin|hastebin|bit\.ly|tinyurl\.com|goo\.gl|t\.co|is\.gd|ow\.ly|rebrand\.ly|gist\.github\.com/[^/]+/[a-f0-9]+/raw)' | index_hits)
178
250
 
179
251
  # chmod +x followed by execution on same file in same block
252
+ seq=0
180
253
  while IFS= read -r hit; do
181
254
  [ -z "$hit" ] && continue
182
- local ln="${hit%%:*}"
183
- add_finding high "$f" "$ln" "chmod-then-exec" "script made executable and immediately invoked"
184
- done < <(grep -nE 'chmod[[:space:]]+\+x[[:space:]]+[^&;]+[[:space:]]*(&&|;)[[:space:]]*\./' "$f" 2>/dev/null)
185
- }
255
+ parse_hit "$hit"
256
+ seq=$((seq+1))
257
+ emit_raw "$HIT_IDX" 9 "$seq" high "$HIT_FILE" "$HIT_LINE" "chmod-then-exec" "script made executable and immediately invoked"
258
+ done < <(tree_grep 'chmod[[:space:]]+\+x[[:space:]]+[^&;]+[[:space:]]*(&&|;)[[:space:]]*\./' | index_hits)
259
+ fi
186
260
 
187
- scan_medium() {
188
- local f="$1"
261
+ # --- medium families (rank 2) ---------------------------------------------
189
262
 
263
+ if [ "$THRESHOLD_RANK" -ge 2 ]; then
190
264
  # Long base64 blobs - obfuscation indicator.
191
265
  # Skip .md files (documentation often shows example b64) - only scan executable content types.
192
- case "$f" in
193
- *.md) ;;
194
- *)
195
- while IFS= read -r hit; do
196
- [ -z "$hit" ] && continue
197
- local ln="${hit%%:*}"
198
- add_finding medium "$f" "$ln" "long-base64" "base64-looking blob >200 chars (possible obfuscation)"
199
- done < <(grep -nE '[A-Za-z0-9+/]{200,}={0,2}' "$f" 2>/dev/null)
200
- ;;
201
- esac
202
- }
266
+ seq=0
267
+ while IFS= read -r hit; do
268
+ [ -z "$hit" ] && continue
269
+ parse_hit "$hit"
270
+ case "$HIT_FILE" in *.md) continue ;; esac
271
+ seq=$((seq+1))
272
+ emit_raw "$HIT_IDX" 10 "$seq" medium "$HIT_FILE" "$HIT_LINE" "long-base64" "base64-looking blob >200 chars (possible obfuscation)"
273
+ done < <(tree_grep '[A-Za-z0-9+/]{200,}={0,2}' | index_hits)
274
+ fi
203
275
 
204
- scan_info() {
205
- local f="$1"
276
+ # --- low families (rank 3) -------------------------------------------------
206
277
 
278
+ if [ "$THRESHOLD_RANK" -ge 3 ]; then
207
279
  # Unknown network endpoints - LOW severity (informational). URLs in skills
208
280
  # are usually doc references, not exfil channels. Only the critical
209
281
  # shell-pipe-exec patterns detect active network abuse.
282
+ # Historical behavior: at most the first 20 URL-bearing lines per file.
283
+ url_re='https?://[a-zA-Z0-9._-]+'
284
+ allow_re="^${ALLOW_DOMAINS}\$"
285
+ seq=0
286
+ prev_file=""
287
+ url_lines=0
210
288
  while IFS= read -r hit; do
211
289
  [ -z "$hit" ] && continue
212
- local ln="${hit%%:*}"
213
- local url_line="${hit#*:}"
214
- local url
215
- url=$(echo "$url_line" | grep -oE 'https?://[a-zA-Z0-9._-]+' | head -1)
216
- [ -z "$url" ] && continue
217
- local host="${url#*://}"
290
+ parse_hit "$hit"
291
+ if [ "$HIT_FILE" != "$prev_file" ]; then
292
+ prev_file="$HIT_FILE"
293
+ url_lines=0
294
+ fi
295
+ url_lines=$((url_lines+1))
296
+ [ "$url_lines" -gt 20 ] && continue
297
+ [[ "$HIT_CONTENT" =~ $url_re ]] || continue
298
+ url="${BASH_REMATCH[0]}"
299
+ host="${url#*://}"
218
300
  host="${host%%/*}"
219
- if ! echo "$host" | grep -qE "^${ALLOW_DOMAINS}$" 2>/dev/null; then
220
- add_finding low "$f" "$ln" "unknown-endpoint" "network endpoint not in allow-list: $host"
301
+ if ! [[ "$host" =~ $allow_re ]]; then
302
+ seq=$((seq+1))
303
+ emit_raw "$HIT_IDX" 11 "$seq" low "$HIT_FILE" "$HIT_LINE" "unknown-endpoint" "network endpoint not in allow-list: $host"
221
304
  fi
222
- done < <(grep -nE 'https?://[a-zA-Z0-9._-]+' "$f" 2>/dev/null | head -20)
223
- }
305
+ done < <(tree_grep 'https?://[a-zA-Z0-9._-]+' | index_hits)
224
306
 
225
- scan_low() {
226
- local f="$1"
227
307
  # Missing SKILL.md frontmatter (only check files named SKILL.md)
228
- case "$f" in
229
- */SKILL.md)
230
- if ! head -1 "$f" | grep -Fxq -- "---"; then
231
- add_finding low "$f" "1" "missing-frontmatter" "SKILL.md without YAML frontmatter"
232
- elif ! grep -qE '^description:' "$f"; then
233
- add_finding low "$f" "0" "missing-description" "SKILL.md frontmatter missing 'description'"
234
- fi
235
- ;;
236
- esac
237
- }
238
-
239
- # --- Run scan -----------------------------------------------------------
308
+ seq=0
309
+ file_idx=0
310
+ while IFS= read -r file; do
311
+ file_idx=$((file_idx+1))
312
+ [ -z "$file" ] && continue
313
+ case "$file" in
314
+ */SKILL.md)
315
+ if ! head -1 "$file" | grep -Fxq -- "---"; then
316
+ seq=$((seq+1))
317
+ emit_raw "$file_idx" 12 "$seq" low "$file" "1" "missing-frontmatter" "SKILL.md without YAML frontmatter"
318
+ elif ! grep -qE '^description:' "$file"; then
319
+ seq=$((seq+1))
320
+ emit_raw "$file_idx" 12 "$seq" low "$file" "0" "missing-description" "SKILL.md frontmatter missing 'description'"
321
+ fi
322
+ ;;
323
+ esac
324
+ done < "$SCAN_LIST"
325
+ fi
240
326
 
241
- FILE_COUNT=0
242
- while IFS= read -r file; do
243
- [ -z "$file" ] && continue
244
- FILE_COUNT=$((FILE_COUNT+1))
245
- scan_critical "$file"
246
- scan_high "$file"
247
- scan_medium "$file"
248
- scan_info "$file"
249
- scan_low "$file"
250
- done < "$SCAN_LIST"
327
+ # Re-order into the historical per-file scan order and load into FINDINGS.
328
+ while IFS= read -r row; do
329
+ [ -z "$row" ] && continue
330
+ rest="${row#*|}"; rest="${rest#*|}"; rest="${rest#*|}" # drop idx|fam|seq|
331
+ sev="${rest%%|*}"; rest="${rest#*|}"
332
+ file="${rest%%|*}"; rest="${rest#*|}"
333
+ line="${rest%%|*}"; rest="${rest#*|}"
334
+ pat="${rest%%|*}"; msg="${rest#*|}"
335
+ add_finding "$sev" "$file" "$line" "$pat" "$msg"
336
+ done < <(sort -t'|' -k1,1n -k2,2n -k3,3n "$RAW")
251
337
 
252
338
  # --- Report -------------------------------------------------------------
253
339
 
@@ -261,20 +347,24 @@ if [ "$JSON" -eq 1 ]; then
261
347
  printf ' "findings": [\n'
262
348
  local_i=0
263
349
  total=${#FINDINGS[@]}
264
- for f in "${FINDINGS[@]:-}"; do
265
- local_i=$((local_i+1))
266
- sev="${f%%|*}"; rest="${f#*|}"
267
- file="${rest%%|*}"; rest="${rest#*|}"
268
- line="${rest%%|*}"; rest="${rest#*|}"
269
- pat="${rest%%|*}"; msg="${rest#*|}"
270
- # json-escape file/msg minimally
271
- jf=$(printf '%s' "$file" | sed 's/"/\\"/g')
272
- jm=$(printf '%s' "$msg" | sed 's/"/\\"/g')
273
- printf ' { "severity": "%s", "file": "%s", "line": "%s", "pattern": "%s", "message": "%s" }' \
274
- "$sev" "$jf" "$line" "$pat" "$jm"
275
- [ "$local_i" -lt "$total" ] && printf ','
276
- printf '\n'
277
- done
350
+ # Guard the expansion: on an empty array "${FINDINGS[@]:-}" yields one empty
351
+ # word, which used to emit a spurious all-empty findings element.
352
+ if [ "$total" -gt 0 ]; then
353
+ for f in "${FINDINGS[@]}"; do
354
+ local_i=$((local_i+1))
355
+ sev="${f%%|*}"; rest="${f#*|}"
356
+ file="${rest%%|*}"; rest="${rest#*|}"
357
+ line="${rest%%|*}"; rest="${rest#*|}"
358
+ pat="${rest%%|*}"; msg="${rest#*|}"
359
+ # json-escape file/msg minimally
360
+ jf=$(printf '%s' "$file" | sed 's/"/\\"/g')
361
+ jm=$(printf '%s' "$msg" | sed 's/"/\\"/g')
362
+ printf ' { "severity": "%s", "file": "%s", "line": "%s", "pattern": "%s", "message": "%s" }' \
363
+ "$sev" "$jf" "$line" "$pat" "$jm"
364
+ [ "$local_i" -lt "$total" ] && printf ','
365
+ printf '\n'
366
+ done
367
+ fi
278
368
  printf ' ]\n}\n'
279
369
  else
280
370
  # Colored text report
@@ -295,7 +385,7 @@ else
295
385
  printf ' ✓ clean (0 findings)\n'
296
386
  else
297
387
  printf ' %sfound %d%s: critical=%d high=%d medium=%d low=%d\n\n' "$C_BLD" "$total" "$C_RST" "$CRIT" "$HIGH" "$MED" "$LOW"
298
- for f in "${FINDINGS[@]:-}"; do
388
+ for f in "${FINDINGS[@]}"; do
299
389
  sev="${f%%|*}"; rest="${f#*|}"
300
390
  file="${rest%%|*}"; rest="${rest#*|}"
301
391
  line="${rest%%|*}"; rest="${rest#*|}"
@@ -307,7 +397,7 @@ else
307
397
  low) ico="· "; col="$C_DIM" ;;
308
398
  *) ico="? "; col="" ;;
309
399
  esac
310
- rel="${file#$REPO_ROOT/}"
400
+ rel="${file#"$REPO_ROOT"/}"
311
401
  printf ' %s%s%s %-8s %s:%s [%s]\n' "$col" "$ico" "$C_RST" "$sev" "$rel" "$line" "$pat"
312
402
  printf ' %s%s%s\n' "$C_DIM" "$msg" "$C_RST"
313
403
  done
@@ -66,11 +66,20 @@ cat > "$TMP/fixture.json" <<'EOF'
66
66
  EOF
67
67
 
68
68
  # Stub curl that captures invocation and returns the fixture. PATH-shadows the
69
- # real curl for this smoke only.
69
+ # real curl for this smoke only. Auth travels via `-K <config-file>` (so the
70
+ # token never hits argv); the stub dumps the config file so the smoke can
71
+ # assert its contents.
70
72
  cat > "$TMP/curl" <<EOF
71
73
  #!/usr/bin/env bash
72
- # Capture every invocation so we can assert the URL + auth header shape.
74
+ # Capture every invocation so we can assert the URL + auth config shape.
73
75
  echo "INVOCATION:" "\$@" >> "$TMP/curl-calls.log"
76
+ prev=""
77
+ for arg in "\$@"; do
78
+ if [ "\$prev" = "-K" ]; then
79
+ echo "AUTH-CONFIG: \$(cat "\$arg" 2>/dev/null)" >> "$TMP/curl-calls.log"
80
+ fi
81
+ prev="\$arg"
82
+ done
74
83
  # Echo the fixture body (BB Server response) to stdout - same as real curl -sf.
75
84
  cat "$TMP/fixture.json"
76
85
  exit 0
@@ -153,10 +162,17 @@ else
153
162
  fail "limit param missing"
154
163
  fi
155
164
 
156
- if grep -q "test-bb-user:test-bb-token" "$TMP/curl-calls.log"; then
157
- ok "Basic auth uses {user}:{token} from credential store"
165
+ if grep -q 'AUTH-CONFIG: user = "test-bb-user:test-bb-token"' "$TMP/curl-calls.log"; then
166
+ ok "Basic auth uses {user}:{token} via curl -K config (not argv)"
158
167
  else
159
- fail "auth header wrong; expected user:token, got: $(grep -o '\-u [^ ]*' "$TMP/curl-calls.log" || echo none)"
168
+ fail "auth config wrong; captured: $(grep 'AUTH-CONFIG' "$TMP/curl-calls.log" || echo none)"
169
+ fi
170
+
171
+ # The raw token must never appear on curl argv (visible to ps).
172
+ if grep "^INVOCATION:" "$TMP/curl-calls.log" | grep -q "test-bb-token"; then
173
+ fail "token leaked onto curl argv"
174
+ else
175
+ ok "token absent from curl argv"
160
176
  fi
161
177
 
162
178
  # --- Assert: shape conversion produced canonical schema ---------------------
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env bash
2
+ # smoke-description-tr.sh - localized picker-description contract.
3
+ #
4
+ # Verifies: every shipped command SKILL.md carries a description-tr line
5
+ # (coverage), repo description: lines stay English with no description-en
6
+ # sidecars committed (hygiene), and localize-commands.mjs apply/restore is
7
+ # an idempotent, byte-identical round trip (mechanics).
8
+
9
+ set -uo pipefail
10
+
11
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
12
+ REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
13
+ CMD_DIR="$REPO_ROOT/pipeline/commands/multi-agent"
14
+ LOCALIZER="$REPO_ROOT/pipeline/scripts/localize-commands.mjs"
15
+
16
+ PASS=0
17
+ FAIL=0
18
+ record_pass() { PASS=$((PASS + 1)); printf ' \342\234\223 %s\n' "$1"; }
19
+ record_fail() { FAIL=$((FAIL + 1)); printf ' \342\234\227 %s\n' "$1"; }
20
+
21
+ printf '\342\206\222 smoke-description-tr: localized picker-description contract\n'
22
+
23
+ # 1. Coverage: every command SKILL.md (incl. the dispatcher) has description-tr
24
+ missing=""
25
+ count=0
26
+ for f in "$CMD_DIR/SKILL.md" "$CMD_DIR"/*/SKILL.md; do
27
+ [ -f "$f" ] || continue
28
+ count=$((count + 1))
29
+ grep -q '^description-tr:' "$f" || missing="$missing ${f#"$CMD_DIR/"}"
30
+ done
31
+ if [ -z "$missing" ]; then
32
+ record_pass "all $count command SKILL.md files carry description-tr"
33
+ else
34
+ record_fail "missing description-tr in:$missing"
35
+ fi
36
+
37
+ # 2. Hygiene: repo descriptions stay English, no sidecars committed
38
+ if grep -rn '^description:.*[çğıöşüÇĞİÖŞÜ]' "$CMD_DIR" >/dev/null 2>&1; then
39
+ record_fail "repo description: lines must stay English (Turkish characters found)"
40
+ else
41
+ record_pass "repo description: lines are English"
42
+ fi
43
+ if grep -rln '^description-en:' "$CMD_DIR" >/dev/null 2>&1; then
44
+ record_fail "description-en sidecar committed to the repo (installed-tree artifact)"
45
+ else
46
+ record_pass "no description-en sidecar in the repo"
47
+ fi
48
+
49
+ # 3. Translation length within the agent-skills description budget
50
+ long=$(awk '/^description-tr:/ && length($0) > 1044 { print FILENAME }' "$CMD_DIR"/*/SKILL.md "$CMD_DIR/SKILL.md" 2>/dev/null | head -1)
51
+ if [ -z "$long" ]; then
52
+ record_pass "every description-tr is within the 1024-char budget"
53
+ else
54
+ record_fail "description-tr exceeds budget in $long"
55
+ fi
56
+
57
+ # 4. Round trip: apply is idempotent, restore is byte-identical
58
+ tmpdir=$(mktemp -d)
59
+ trap 'rm -rf "$tmpdir"' EXIT
60
+ cp -R "$CMD_DIR" "$tmpdir/cmds"
61
+ node "$LOCALIZER" apply --dir "$tmpdir/cmds" >/dev/null 2>&1
62
+ if grep -q '^description:.*[çğıöşüÇĞİÖŞÜ]' "$tmpdir/cmds/dev/SKILL.md" \
63
+ && grep -q '^description-en:' "$tmpdir/cmds/dev/SKILL.md"; then
64
+ record_pass "apply swaps description and writes the description-en sidecar"
65
+ else
66
+ record_fail "apply did not localize the sample file"
67
+ fi
68
+ second=$(node "$LOCALIZER" apply --dir "$tmpdir/cmds" 2>/dev/null)
69
+ if printf '%s' "$second" | grep -q ' 0 file(s) changed'; then
70
+ record_pass "second apply is a no-op (idempotent)"
71
+ else
72
+ record_fail "second apply changed files again: $second"
73
+ fi
74
+ node "$LOCALIZER" restore --dir "$tmpdir/cmds" >/dev/null 2>&1
75
+ if diff -r "$CMD_DIR" "$tmpdir/cmds" >/dev/null 2>&1; then
76
+ record_pass "restore returns a byte-identical tree"
77
+ else
78
+ record_fail "restore left differences vs the original tree"
79
+ fi
80
+
81
+ printf '\n\342\225\220\342\225\220 smoke-description-tr: %d passed, %d failed \342\225\220\342\225\220\n' "$PASS" "$FAIL"
82
+ [ "$FAIL" -eq 0 ]