@ikunin/sprintpilot 1.0.2 → 1.0.3

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.
@@ -1,6 +1,6 @@
1
1
  addon:
2
2
  name: sprintpilot
3
- version: 1.0.2
3
+ version: 1.0.3
4
4
  description: Sprintpilot — autopilot and multi-agent addon for BMad Method (git workflow, parallel agents, autonomous story execution)
5
5
  bmad_compatibility: ">=6.2.0"
6
6
  modules:
@@ -0,0 +1,457 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Cross-platform codebase scanner.
5
+ *
6
+ * Replaces bash pipelines like `find ... -exec wc -l {} + | sort -rn | head -N`
7
+ * so sprintpilot skills work on Windows PowerShell / cmd / Gemini CLI, not just bash.
8
+ *
9
+ * Subcommands:
10
+ * files List files matching include globs, excluding ignore globs.
11
+ * Flags: --include, --exclude, --root, --limit, --count
12
+ * largest Top N files by line count.
13
+ * Flags: --include, --exclude, --root, --limit (default 10)
14
+ * loc Total line count across matched files.
15
+ * Flags: --include, --exclude, --root
16
+ * extensions Extension frequency histogram, descending.
17
+ * Flags: --exclude, --root, --limit (default 20)
18
+ */
19
+
20
+ const fs = require('fs');
21
+ const path = require('path');
22
+ const { parseArgs } = require('../lib/runtime/args');
23
+ const log = require('../lib/runtime/log');
24
+
25
+ const DEFAULT_EXCLUDES = [
26
+ 'node_modules',
27
+ '.git',
28
+ 'vendor',
29
+ 'target',
30
+ 'dist',
31
+ 'build',
32
+ '.next',
33
+ '.nuxt',
34
+ '.svelte-kit',
35
+ 'coverage',
36
+ '.turbo',
37
+ '.cache',
38
+ '__pycache__',
39
+ '.venv',
40
+ 'venv',
41
+ '.worktrees',
42
+ ];
43
+
44
+ function help() {
45
+ log.out(
46
+ 'Usage: scan.js <files|largest|loc|extensions> [--include <globs>] [--exclude <globs>] [--root <path>] [--limit <N>] [--count]',
47
+ );
48
+ }
49
+
50
+ // Split a comma-delimited list, but keep commas inside {a,b} brace groups intact.
51
+ function splitList(value) {
52
+ if (value === undefined || value === null || value === true) return [];
53
+ const s = String(value);
54
+ const out = [];
55
+ let buf = '';
56
+ let depth = 0;
57
+ for (let i = 0; i < s.length; i++) {
58
+ const c = s[i];
59
+ if (c === '{') depth++;
60
+ else if (c === '}') depth = Math.max(0, depth - 1);
61
+ if (c === ',' && depth === 0) {
62
+ if (buf.trim()) out.push(buf.trim());
63
+ buf = '';
64
+ } else {
65
+ buf += c;
66
+ }
67
+ }
68
+ if (buf.trim()) out.push(buf.trim());
69
+ return out;
70
+ }
71
+
72
+ // Find matching closing brace, respecting nesting. Returns -1 if unterminated.
73
+ function findBraceClose(glob, start) {
74
+ let depth = 1;
75
+ for (let i = start + 1; i < glob.length; i++) {
76
+ const c = glob[i];
77
+ if (c === '\\' && i + 1 < glob.length) {
78
+ i++;
79
+ continue;
80
+ }
81
+ if (c === '{') depth++;
82
+ else if (c === '}') {
83
+ depth--;
84
+ if (depth === 0) return i;
85
+ }
86
+ }
87
+ return -1;
88
+ }
89
+
90
+ // Split a brace group's body on commas at depth 0 (so nested braces stay intact).
91
+ function splitBraceAlts(body) {
92
+ const parts = [];
93
+ let buf = '';
94
+ let depth = 0;
95
+ for (let i = 0; i < body.length; i++) {
96
+ const c = body[i];
97
+ if (c === '\\' && i + 1 < body.length) {
98
+ buf += c + body[i + 1];
99
+ i++;
100
+ continue;
101
+ }
102
+ if (c === '{') depth++;
103
+ else if (c === '}') depth = Math.max(0, depth - 1);
104
+ if (c === ',' && depth === 0) {
105
+ parts.push(buf);
106
+ buf = '';
107
+ } else {
108
+ buf += c;
109
+ }
110
+ }
111
+ parts.push(buf);
112
+ return parts;
113
+ }
114
+
115
+ // Convert a glob pattern into a RegExp.
116
+ // Supports: * (any chars except /), ** (any chars incl. /), ? (single non-/),
117
+ // {a,b} alternation (nestable), and literal path segments.
118
+ // Matching is against forward-slash paths.
119
+ function globToRegex(glob) {
120
+ let re = '';
121
+ let i = 0;
122
+ while (i < glob.length) {
123
+ const c = glob[i];
124
+ if (c === '\\') {
125
+ if (i + 1 >= glob.length) {
126
+ // Trailing lone backslash — emit as a literal backslash.
127
+ re += '\\\\';
128
+ i++;
129
+ continue;
130
+ }
131
+ // Literal escape: pass the next char through verbatim.
132
+ const next = glob[i + 1];
133
+ re += '.+^$()|[]{}?*\\'.includes(next) ? '\\' + next : next;
134
+ i += 2;
135
+ continue;
136
+ }
137
+ if (c === '*') {
138
+ if (glob[i + 1] === '*') {
139
+ re += '.*';
140
+ i += 2;
141
+ if (glob[i] === '/') i++; // consume trailing slash of ** segment
142
+ } else {
143
+ re += '[^/]*';
144
+ i++;
145
+ }
146
+ continue;
147
+ }
148
+ if (c === '?') {
149
+ re += '[^/]';
150
+ i++;
151
+ continue;
152
+ }
153
+ if (c === '{') {
154
+ const end = findBraceClose(glob, i);
155
+ if (end === -1) {
156
+ // Unterminated brace — treat as literal.
157
+ re += '\\{';
158
+ i++;
159
+ continue;
160
+ }
161
+ const alts = splitBraceAlts(glob.slice(i + 1, end));
162
+ const altRegexes = alts.map((p) => globToRegex(p).source.slice(1, -1));
163
+ re += `(?:${altRegexes.join('|')})`;
164
+ i = end + 1;
165
+ continue;
166
+ }
167
+ if ('.+^$()|[]'.includes(c)) {
168
+ re += '\\' + c;
169
+ i++;
170
+ continue;
171
+ }
172
+ re += c;
173
+ i++;
174
+ }
175
+ return new RegExp('^' + re + '$');
176
+ }
177
+
178
+ function toPosix(p) {
179
+ return p.split(path.sep).join('/');
180
+ }
181
+
182
+ // Compile a pattern into { raw, re, pathAnchored }.
183
+ // pathAnchored = true if the pattern contains a path separator; such patterns
184
+ // only match the full relative path. Basename-only patterns (no '/') match
185
+ // both the full path and the basename, so "*.ts" works at any depth.
186
+ function compilePatterns(patterns) {
187
+ return patterns.map((p) => ({
188
+ raw: p,
189
+ re: globToRegex(p),
190
+ pathAnchored: p.includes('/'),
191
+ }));
192
+ }
193
+
194
+ function matchesAny(relPath, compiled) {
195
+ if (compiled.length === 0) return false;
196
+ const basename = relPath.slice(relPath.lastIndexOf('/') + 1);
197
+ for (const { re, pathAnchored } of compiled) {
198
+ if (re.test(relPath)) return true;
199
+ if (!pathAnchored && re.test(basename)) return true;
200
+ }
201
+ return false;
202
+ }
203
+
204
+ function isExcludedDir(name, excludeBasenames) {
205
+ return excludeBasenames.has(name);
206
+ }
207
+
208
+ function matchesExcludePath(relPath, compiled) {
209
+ return matchesAny(relPath, compiled);
210
+ }
211
+
212
+ // Resolve a dirent to { kind: 'file' | 'dir' | 'other' }, following symlinks
213
+ // through stat(). Returns 'other' on broken links or errors.
214
+ function classifyEntry(fullPath, entry) {
215
+ if (entry.isFile()) return 'file';
216
+ if (entry.isDirectory()) return 'dir';
217
+ if (entry.isSymbolicLink()) {
218
+ try {
219
+ const st = fs.statSync(fullPath);
220
+ if (st.isFile()) return 'file';
221
+ if (st.isDirectory()) return 'dir';
222
+ } catch {
223
+ return 'other';
224
+ }
225
+ }
226
+ return 'other';
227
+ }
228
+
229
+ function isWithinRoot(real, rootReal) {
230
+ if (real === rootReal) return true;
231
+ const prefix = rootReal.endsWith(path.sep) ? rootReal : rootReal + path.sep;
232
+ return real.startsWith(prefix);
233
+ }
234
+
235
+ // Walk directory tree, yielding files that match includes and not excludes.
236
+ // Follows symlinks (like GNU find's default) but:
237
+ // - breaks cycles by tracking the realpath of every directory visited
238
+ // - refuses to traverse symlinks that escape the --root boundary
239
+ function* walk(root, includes, excludes, excludeBasenames) {
240
+ const visited = new Set();
241
+ let rootReal;
242
+ try {
243
+ rootReal = fs.realpathSync(root);
244
+ } catch {
245
+ rootReal = path.resolve(root);
246
+ }
247
+ visited.add(rootReal);
248
+
249
+ const stack = [root];
250
+ while (stack.length) {
251
+ const dir = stack.pop();
252
+ let entries;
253
+ try {
254
+ entries = fs.readdirSync(dir, { withFileTypes: true });
255
+ } catch {
256
+ continue;
257
+ }
258
+ for (const entry of entries) {
259
+ const full = path.join(dir, entry.name);
260
+ const rel = toPosix(path.relative(root, full));
261
+ const kind = classifyEntry(full, entry);
262
+ if (kind === 'dir') {
263
+ if (isExcludedDir(entry.name, excludeBasenames)) continue;
264
+ if (matchesExcludePath(rel, excludes)) continue;
265
+ let real;
266
+ try {
267
+ real = fs.realpathSync(full);
268
+ } catch {
269
+ continue;
270
+ }
271
+ if (visited.has(real)) continue;
272
+ if (!isWithinRoot(real, rootReal)) continue; // refuse symlinks that escape root
273
+ visited.add(real);
274
+ stack.push(full);
275
+ continue;
276
+ }
277
+ if (kind !== 'file') continue;
278
+ if (matchesExcludePath(rel, excludes)) continue;
279
+ if (includes.length > 0 && !matchesAny(rel, includes)) continue;
280
+ // For symlinked files, verify the target is within root.
281
+ if (entry.isSymbolicLink()) {
282
+ try {
283
+ const fileReal = fs.realpathSync(full);
284
+ if (!isWithinRoot(fileReal, rootReal)) continue;
285
+ } catch {
286
+ continue;
287
+ }
288
+ }
289
+ yield rel;
290
+ }
291
+ }
292
+ }
293
+
294
+ function readLineCount(fullPath) {
295
+ let fd;
296
+ try {
297
+ fd = fs.openSync(fullPath, 'r');
298
+ const buf = Buffer.alloc(64 * 1024);
299
+ let count = 0;
300
+ let bytesRead;
301
+ let lastByte = null;
302
+ let total = 0;
303
+ while ((bytesRead = fs.readSync(fd, buf, 0, buf.length, null)) > 0) {
304
+ total += bytesRead;
305
+ for (let i = 0; i < bytesRead; i++) {
306
+ if (buf[i] === 0x0a) count++;
307
+ }
308
+ lastByte = buf[bytesRead - 1];
309
+ }
310
+ // Count the final line if the file is non-empty and doesn't end with \n
311
+ if (total > 0 && lastByte !== 0x0a) count++;
312
+ return count;
313
+ } catch {
314
+ return 0;
315
+ } finally {
316
+ if (fd !== undefined) {
317
+ try {
318
+ fs.closeSync(fd);
319
+ } catch {
320
+ /* ignore */
321
+ }
322
+ }
323
+ }
324
+ }
325
+
326
+ function resolveRoot(opts) {
327
+ const root = opts.root ? path.resolve(opts.root) : process.cwd();
328
+ try {
329
+ if (!fs.statSync(root).isDirectory()) {
330
+ log.fail(`root is not a directory: ${root}`);
331
+ }
332
+ } catch {
333
+ log.fail(`root does not exist: ${root}`);
334
+ }
335
+ return root;
336
+ }
337
+
338
+ function buildExcludes(extra) {
339
+ const list = [...DEFAULT_EXCLUDES, ...extra];
340
+ // Patterns: match the basename of a directory OR any path containing it.
341
+ const patterns = [];
342
+ const basenames = new Set();
343
+ for (const item of list) {
344
+ if (item.includes('/') || item.includes('*')) {
345
+ patterns.push(item);
346
+ } else {
347
+ basenames.add(item);
348
+ patterns.push(`**/${item}/**`);
349
+ patterns.push(item);
350
+ }
351
+ }
352
+ return { compiled: compilePatterns(patterns), basenames };
353
+ }
354
+
355
+ function cmdFiles(opts) {
356
+ const root = resolveRoot(opts);
357
+ const includes = compilePatterns(splitList(opts.include));
358
+ const { compiled: excludes, basenames } = buildExcludes(splitList(opts.exclude));
359
+ const limit = opts.limit ? Number(opts.limit) : 0;
360
+ const count = opts.count === true || opts.count === 'true';
361
+
362
+ let n = 0;
363
+ const out = [];
364
+ for (const rel of walk(root, includes, excludes, basenames)) {
365
+ n++;
366
+ if (!count) {
367
+ out.push(rel);
368
+ if (limit > 0 && out.length >= limit) break;
369
+ }
370
+ }
371
+ if (count) {
372
+ log.out(String(n));
373
+ } else {
374
+ for (const p of out) log.out(p);
375
+ }
376
+ }
377
+
378
+ function cmdLargest(opts) {
379
+ const root = resolveRoot(opts);
380
+ const includes = compilePatterns(splitList(opts.include));
381
+ const { compiled: excludes, basenames } = buildExcludes(splitList(opts.exclude));
382
+ const limit = opts.limit ? Number(opts.limit) : 10;
383
+
384
+ const heap = []; // simple array; N is small so O(files * log N) is fine
385
+ for (const rel of walk(root, includes, excludes, basenames)) {
386
+ const full = path.join(root, rel);
387
+ const lines = readLineCount(full);
388
+ heap.push({ lines, path: rel });
389
+ }
390
+ heap.sort((a, b) => b.lines - a.lines);
391
+ for (const item of heap.slice(0, limit)) {
392
+ log.out(`${item.lines}\t${item.path}`);
393
+ }
394
+ }
395
+
396
+ function cmdLoc(opts) {
397
+ const root = resolveRoot(opts);
398
+ const includes = compilePatterns(splitList(opts.include));
399
+ const { compiled: excludes, basenames } = buildExcludes(splitList(opts.exclude));
400
+
401
+ let total = 0;
402
+ let fileCount = 0;
403
+ for (const rel of walk(root, includes, excludes, basenames)) {
404
+ total += readLineCount(path.join(root, rel));
405
+ fileCount++;
406
+ }
407
+ log.out(`${total}\t${fileCount}`);
408
+ }
409
+
410
+ function cmdExtensions(opts) {
411
+ const root = resolveRoot(opts);
412
+ const { compiled: excludes, basenames } = buildExcludes(splitList(opts.exclude));
413
+ const limit = opts.limit ? Number(opts.limit) : 20;
414
+
415
+ const counts = new Map();
416
+ for (const rel of walk(root, [], excludes, basenames)) {
417
+ const base = rel.split('/').pop();
418
+ const dot = base.lastIndexOf('.');
419
+ const ext = dot > 0 ? base.slice(dot + 1) : '(no-ext)';
420
+ counts.set(ext, (counts.get(ext) || 0) + 1);
421
+ }
422
+ const rows = [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, limit);
423
+ for (const [ext, n] of rows) {
424
+ log.out(`${n}\t${ext}`);
425
+ }
426
+ }
427
+
428
+ function main() {
429
+ const { opts, positional } = parseArgs(process.argv.slice(2), {
430
+ booleanFlags: ['count'],
431
+ });
432
+ if (opts.help || positional.length === 0) {
433
+ help();
434
+ process.exit(opts.help ? 0 : 1);
435
+ }
436
+ const cmd = positional[0];
437
+ switch (cmd) {
438
+ case 'files':
439
+ cmdFiles(opts);
440
+ break;
441
+ case 'largest':
442
+ cmdLargest(opts);
443
+ break;
444
+ case 'loc':
445
+ cmdLoc(opts);
446
+ break;
447
+ case 'extensions':
448
+ cmdExtensions(opts);
449
+ break;
450
+ default:
451
+ log.error(`Unknown subcommand: ${cmd}`);
452
+ help();
453
+ process.exit(1);
454
+ }
455
+ }
456
+
457
+ main();
@@ -8,6 +8,24 @@ You do NOT hardcode the workflow sequence. After each completed skill, read its
8
8
 
9
9
  **Git integration** is additive. If `_Sprintpilot/manifest.yaml` doesn't exist or `git.enabled: false`, all git operations are silently skipped and this workflow behaves identically to the stock autopilot.
10
10
 
11
+ ### Shell portability (IMPORTANT)
12
+
13
+ Sprintpilot runs under any LLM CLI (Claude Code, Gemini CLI, Cursor, etc.) on any OS. The shell that executes commands may be **bash, zsh, PowerShell, or cmd** depending on platform and CLI. Shell-specific idioms will fail silently when the wrong shell is used.
14
+
15
+ **When you encounter bash-style idioms below, translate them to your shell.** The table applies to **external commands** (like `git`); cmdlets have slightly different conventions.
16
+
17
+ | Bash idiom | PowerShell equivalent | Meaning |
18
+ |---|---|---|
19
+ | `A && B` | `A; if ($LASTEXITCODE -eq 0) { B }` (or separate commands, guarding B manually) | Run B only if A succeeded |
20
+ | `A \|\| true` | `A; $LASTEXITCODE = 0` (or `try { A } catch {}` for cmdlets) | Run A, ignore failures |
21
+ | `2>/dev/null` | `2>$null` | Suppress stderr |
22
+ | `rm -rf <dir>` | `Remove-Item -Recurse -Force <dir>` | Recursive delete |
23
+ | `if [ -f X ]; then ... fi` | `if (Test-Path -PathType Leaf X) { ... }` | File-exists check (regular file, not dir) |
24
+
25
+ **Safer:** when in doubt, use the cross-platform Node helpers under `_Sprintpilot/scripts/`. For ad-hoc file ops, invoke Node inline: `node -e "require('fs').rmSync('<path>', {recursive: true, force: true})"`.
26
+
27
+ If a step below uses `&&` to chain "run B only on A's success", and you cannot express that in one line, **run the commands separately and STOP if any step fails** — do not proceed past a failed step.
28
+
11
29
  ---
12
30
 
13
31
  ## AUTOPILOT RULES — READ BEFORE PROCEEDING
@@ -170,7 +188,9 @@ Resolve:
170
188
  <check if="not a git repo">
171
189
  <action>HALT: "No git repository found. Initialize one first:
172
190
  ```
173
- git init && git add -A && git commit -m 'initial commit'
191
+ git init
192
+ git add -A
193
+ git commit -m 'initial commit'
174
194
  git remote add origin <your-repo-url>
175
195
  ```
176
196
  Then run /sprint-autopilot-on again."</action>
@@ -201,15 +221,20 @@ Resolve:
201
221
  - COMMITTED: log "Recoverable work found for <name> — will push via git -C"
202
222
  Push the branch: `git -C .worktrees/<name> push -u origin <branch> 2>&1`
203
223
  If `{{create_pr}}` is true AND platform != git_only: create PR via `node {{project_root}}/_Sprintpilot/scripts/create-pr.js ...`
204
- If `{{create_pr}}` is false OR platform is git_only: merge directly `git checkout -B {{base_branch}} origin/{{base_branch}} && git merge <branch> --no-edit && git push origin {{base_branch}}`
224
+ If `{{create_pr}}` is false OR platform is git_only: merge directly. Run each as a separate command; **STOP and log the failure if any step fails — do not proceed past a failed step**:
225
+ 1. `git checkout -B {{base_branch}} origin/{{base_branch}}`
226
+ 2. `git merge <branch> --no-edit`
227
+ 3. `git push origin {{base_branch}}`
205
228
  Then remove worktree.
206
229
  - STALE: `git worktree remove .worktrees/<name> --force` + prune
207
230
  - DIRTY: warn user, ask how to proceed (stash/commit/discard)
208
- - ORPHAN: `rm -rf .worktrees/<name>` + `git worktree prune`
231
+ - ORPHAN: remove the directory cross-platform with `node -e "require('fs').rmSync('.worktrees/<name>', {recursive: true, force: true})"`, then `git worktree prune`
209
232
  </action>
210
233
 
211
234
  <action>**Branch reconciliation** — detect pushed-but-unmerged story branches.
212
- Run: `git fetch origin && git branch -r --list "origin/{{branch_prefix}}*"`
235
+ Run as separate commands — **if `git fetch origin` fails (network/auth), STOP branch reconciliation and log a warning; do not operate on stale local refs**:
236
+ 1. `git fetch origin`
237
+ 2. `git branch -r --list "origin/{{branch_prefix}}*"`
213
238
  For each remote branch:
214
239
  - Extract story-key from branch name (strip "origin/{{branch_prefix}}" prefix)
215
240
  - Look up story status in `{status_file}`
@@ -226,16 +251,16 @@ Resolve:
226
251
  - If merge fails: log warning, continue (branch is preserved on remote)
227
252
  - If merge succeeds:
228
253
  - Re-read `{status_file}` from HEAD (may now include story artifacts after merge)
229
- - Update `{git_status_file}` via sync-status.sh: set `--merge-status "recovered"` for this story.
230
- **IMPORTANT:** sync-status.sh does full block replacement. If the story already has an entry in `{git_status_file}`, re-read its existing fields and pass ALL of them alongside `--merge-status`. If no entry exists yet, pass at minimum `--branch` and `--push-status "pushed"`.
254
+ - Update `{git_status_file}` via sync-status.js: set `--merge-status "recovered"` for this story.
255
+ **IMPORTANT:** sync-status.js does full block replacement. If the story already has an entry in `{git_status_file}`, re-read its existing fields and pass ALL of them alongside `--merge-status`. If no entry exists yet, pass at minimum `--branch` and `--push-status "pushed"`.
231
256
  - If `{{platform}}` is NOT git_only (github, gitlab, bitbucket, gitea) AND `{{create_pr}}` is true:
232
257
  - Check if PR/MR already exists for this branch (platform-specific check via create-pr.sh or CLI)
233
258
  - If no PR: create one via `node {{project_root}}/_Sprintpilot/scripts/create-pr.js --platform {{platform}} ...`
234
259
  - Log: "PR created/found for <story-key>"
235
- - Update `{git_status_file}` via sync-status.sh: set `--merge-status "pr_pending"` for this story (same full-field requirement as above)
260
+ - Update `{git_status_file}` via sync-status.js: set `--merge-status "pr_pending"` for this story (same full-field requirement as above)
236
261
  - If status IS "done" AND branch still exists AND `{{cleanup_on_merge}}` is true:
237
262
  - Log: "Stale remote branch: <branch> — story already done, cleaning up"
238
- - Delete remote branch: `git push origin --delete <branch> 2>/dev/null || true`
263
+ - Delete remote branch (ignore failure — the branch may already be gone): `git push origin --delete <branch>`
239
264
  </action>
240
265
 
241
266
  <action>Set `{{git_enabled}}` = true, `{{platform}}` = detected value</action>
@@ -486,8 +511,8 @@ Resolve:
486
511
  </action>
487
512
 
488
513
  <action>**Init submodules** if needed.
489
- Run: `if [ -f .gitmodules ]; then timeout 30 git submodule update --init --recursive 2>&1 || echo "SUBMODULE_TIMEOUT"; fi`
490
- If SUBMODULE_TIMEOUT: warn "Submodule init timed out (may need auth). Continuing without."
514
+ First check for `.gitmodules` (use your file-exists tool, or `node -e "process.exit(require('fs').existsSync('.gitmodules')?0:1)"`). If not present, skip this step.
515
+ If present, run `git submodule update --init --recursive` (give it ~30 seconds). If the command fails or hangs, warn "Submodule init failed (may need auth). Continuing without." and proceed.
491
516
  </action>
492
517
 
493
518
  <action>Set `{{in_worktree}}` = true</action>
@@ -595,17 +620,18 @@ pr_base: {{pr_base}}
595
620
  <!-- GIT: Commit planning artifacts to main after planning skills -->
596
621
  <check if="{{git_enabled}} AND {{completed_skill}} is a planning skill (bmad-create-prd, bmad-create-architecture, bmad-create-ux-design, bmad-create-epics-and-stories, bmad-sprint-planning, bmad-check-implementation-readiness, bmad-create-story)">
597
622
  <action>**Commit planning artifacts to main** — keep track of all planning decisions in git.
598
- Stage all changed artifacts:
623
+ Stage all changed artifacts (ignore errors — any of these paths may not yet exist):
599
624
  ```
600
- git add _bmad-output/planning-artifacts/ _bmad-output/implementation-artifacts/ _bmad-output/stories/ 2>/dev/null || true
625
+ git add _bmad-output/planning-artifacts/ _bmad-output/implementation-artifacts/ _bmad-output/stories/
601
626
  ```
602
- If there are staged changes, commit:
627
+ Check if there's anything staged; if yes, commit:
603
628
  ```
604
- git diff --cached --quiet || git commit -m "docs: {{completed_skill}} artifacts"
629
+ git diff --cached --quiet
605
630
  ```
606
- Push to remote if possible:
631
+ If that exits non-zero (there are staged changes), run: `git commit -m "docs: {{completed_skill}} artifacts"`
632
+ Then push (log a warning if push fails; do not halt autopilot):
607
633
  ```
608
- git push origin {{base_branch}} 2>/dev/null || true
634
+ git push origin {{base_branch}}
609
635
  ```
610
636
  </action>
611
637
  </check>
@@ -775,12 +801,12 @@ Instruct: "Re-verify code review for story {{current_story}} — all patch findi
775
801
  Log warning but do NOT halt. The branch is pushed and preserved.
776
802
  Boot reconciliation (INITIALIZATION branch reconciliation) will retry on next session.
777
803
 
778
- Note: `{{merge_status}}` is persisted by the full sync-status.sh call later in this step (via `--merge-status`). Do NOT call sync-status.sh separately here — it does full block replacement and would destroy other fields.
804
+ Note: `{{merge_status}}` is persisted by the full sync-status.js call later in this step (via `--merge-status`). Do NOT call sync-status.js separately here — it does full block replacement and would destroy other fields.
779
805
  </action>
780
806
  <check if="{{cleanup_on_merge}} is true">
781
- <action>**Cleanup worktree** for merged story — branch was merged locally, worktree is no longer needed:
807
+ <action>**Cleanup worktree** for merged story — branch was merged locally, worktree is no longer needed. Ignore failures from the remove (the worktree may already be gone):
782
808
  ```
783
- git worktree remove .worktrees/{{current_story}} --force 2>/dev/null || true
809
+ git worktree remove .worktrees/{{current_story}} --force
784
810
  git worktree prune
785
811
  ```
786
812
  </action>
@@ -804,17 +830,21 @@ Instruct: "Re-verify code review for story {{current_story}} — all patch findi
804
830
  This writes to `git-status.yaml` (addon-owned). Sprint-status.yaml is BMAD-owned — updated by BMAD skills only.
805
831
  </action>
806
832
 
807
- <action>**Stage and commit artifacts** — explicitly include git-status.yaml and decision-log.yaml:
833
+ <action>**Stage and commit artifacts** — explicitly include git-status.yaml and decision-log.yaml. Ignore errors from the `git add` (any listed path may not yet exist):
834
+ ```
835
+ git add _bmad-output/implementation-artifacts/sprint-status.yaml _bmad-output/implementation-artifacts/git-status.yaml _bmad-output/implementation-artifacts/autopilot-state.yaml _bmad-output/implementation-artifacts/decision-log.yaml _bmad-output/stories/ _bmad-output/planning-artifacts/
836
+ ```
837
+ Check if anything is staged: `git diff --cached --quiet`. If that exits non-zero, commit:
838
+ `git commit -m "docs: story {{current_story}} done — {{test_count}} tests{{#if pr_url}}, PR: {{pr_url}}{{/if}}"`
839
+ Then push (log a warning if push fails; do not halt autopilot):
808
840
  ```
809
- git add _bmad-output/implementation-artifacts/sprint-status.yaml _bmad-output/implementation-artifacts/git-status.yaml _bmad-output/implementation-artifacts/autopilot-state.yaml _bmad-output/implementation-artifacts/decision-log.yaml _bmad-output/stories/ _bmad-output/planning-artifacts/ 2>/dev/null || true
810
- git diff --cached --quiet || git commit -m "docs: story {{current_story}} done — {{test_count}} tests{{#if pr_url}}, PR: {{pr_url}}{{/if}}"
811
- git push origin {{base_branch}} 2>/dev/null || true
841
+ git push origin {{base_branch}}
812
842
  ```
813
843
  This ensures sprint-status.yaml, git-status.yaml, story files, and any updated artifacts are on main even when story code is on a PR branch.
814
844
  </action>
815
845
  </check>
816
846
 
817
- <!-- Story git status was already written by sync-status.sh above (when git_enabled AND in_worktree).
847
+ <!-- Story git status was already written by sync-status.js above (when git_enabled AND in_worktree).
818
848
  sprint-status.yaml is BMAD-owned — updated by bmad-dev-story / bmad-code-review directly. -->
819
849
  <check if="NOT {{git_enabled}}">
820
850
  <action>Log: "Story {{current_story}} complete — BMAD dev-story updates sprint-status.yaml directly"</action>
@@ -917,7 +947,7 @@ pr_base: {{pr_base}}
917
947
  `git merge <branch-ref> --no-edit`
918
948
  `git push origin {{base_branch}}`
919
949
  - If merge succeeds: update merge_status in `{git_status_file}`.
920
- **IMPORTANT:** sync-status.sh does full block replacement — you MUST re-read the story's existing fields from `{git_status_file}` (branch, commit, patch_commits, push_status, pr_url, lint_result, worktree, platform, base_branch, worktree_cleaned) and pass ALL of them along with `--merge-status "merged"`. Omitting fields destroys them.
950
+ **IMPORTANT:** sync-status.js does full block replacement — you MUST re-read the story's existing fields from `{git_status_file}` (branch, commit, patch_commits, push_status, pr_url, lint_result, worktree, platform, base_branch, worktree_cleaned) and pass ALL of them along with `--merge-status "merged"`. Omitting fields destroys them.
921
951
  - If merge fails: `git merge --abort`, update merge_status to "failed" in `{git_status_file}` (same full-field requirement), log warning, continue
922
952
  Log: "Pre-checkpoint merge: N stories verified on {{base_branch}}"
923
953
  </action>
@@ -999,13 +1029,14 @@ If the skill is not available or fails, generate a minimal README.md:
999
1029
 
1000
1030
  <!-- GIT: Commit documentation and final artifacts to main -->
1001
1031
  <check if="{{git_enabled}}">
1002
- <action>**Commit final artifacts and documentation to main**:
1032
+ <action>**Commit final artifacts and documentation to main**. Run each step; if an early step fails, STOP and log — don't proceed past a failed step. `git add` may fail for missing optional paths (`docs/`, `README.md`); ignore those path-specific errors. Failure of the final push should log a warning but not halt autopilot:
1003
1033
  ```
1004
1034
  git checkout -B {{base_branch}} origin/{{base_branch}}
1005
- git add _bmad-output/ README.md docs/ 2>/dev/null || true
1006
- git diff --cached --quiet || git commit -m "docs: project documentation and final artifacts"
1007
- git push origin {{base_branch}} 2>/dev/null || true
1035
+ git add _bmad-output/ README.md docs/
1008
1036
  ```
1037
+ Check if anything is staged: `git diff --cached --quiet`. If that exits non-zero, commit:
1038
+ `git commit -m "docs: project documentation and final artifacts"`
1039
+ Then: `git push origin {{base_branch}}`
1009
1040
  </action>
1010
1041
  </check>
1011
1042
 
@@ -1035,8 +1066,8 @@ If the skill is not available or fails, generate a minimal README.md:
1035
1066
  <check if="{{git_enabled}}">
1036
1067
  <action>**Cleanup all remaining worktrees**:
1037
1068
  Run: `git worktree list --porcelain`
1038
- For each worktree that is NOT the main worktree:
1039
- `git worktree remove <path> --force 2>/dev/null || true`
1069
+ For each worktree that is NOT the main worktree, run the following — log and continue on failure; some worktrees may already be gone:
1070
+ `git worktree remove <path> --force`
1040
1071
  Then: `git worktree prune`
1041
1072
  </action>
1042
1073
  </check>
@@ -19,28 +19,35 @@ Scan the project at `{{project_root}}` and write your findings to `{{output_file
19
19
  - `*.key`, `*.pem`, `*.p12` (private keys)
20
20
  - `credentials.json`, `service-account.json`
21
21
 
22
- ## Exploration Commands
22
+ ## Exploration
23
23
 
24
- ```bash
25
- # Top-level structure
26
- ls -la
27
- find . -maxdepth 2 -type d -not -path '*/node_modules/*' -not -path '*/.git/*' -not -path '*/vendor/*' | head -50
24
+ Use your native file tools (Read, Glob, Grep). The lists below describe what data to collect; pick the appropriate tool for your CLI.
28
25
 
29
- # Entry points
30
- cat index.ts index.js main.py main.go cmd/main.go src/main.rs lib/main.rb app.py manage.py main.c main.cpp src/main.c src/main.cpp 2>/dev/null | head -30
26
+ ### Top-level structure
27
+ Glob the root for directories and files (exclude `node_modules`, `.git`, `vendor`, `target`, `dist`, `build`). Look 1-2 levels deep to understand the layout.
31
28
 
32
- # Route definitions
33
- grep -rn "router\.\|app\.\(get\|post\|put\|delete\|patch\)\|@app\.route\|@Controller\|@RequestMapping\|CROW_ROUTE\|CPPREST_\|Pistache::" --include='*.ts' --include='*.js' --include='*.py' --include='*.java' --include='*.go' --include='*.xml' --include='*.c' --include='*.h' --include='*.cpp' --include='*.hpp' --include='*.cc' --include='*.cxx' --include='*.hxx' | head -30
29
+ ### Entry points
30
+ Read whichever of these exist: `index.ts`, `index.js`, `main.py`, `main.go`, `cmd/main.go`, `src/main.rs`, `lib/main.rb`, `app.py`, `manage.py`, `main.c`, `main.cpp`, `src/main.c`, `src/main.cpp`. 30 lines is usually enough to identify the entry path.
34
31
 
35
- # Module exports / barrel files
36
- find . -name 'index.ts' -o -name 'index.js' -o -name '__init__.py' -o -name 'mod.rs' | head -20
32
+ ### Route definitions
33
+ Use Grep to find route declarations across `*.ts`, `*.js`, `*.py`, `*.java`, `*.go`, `*.xml`, C/C++ headers. Pattern set:
34
+ ```
35
+ router\.|app\.(get|post|put|delete|patch)|@app\.route|@Controller|@RequestMapping|CROW_ROUTE|CPPREST_|Pistache::
36
+ ```
37
+ Limit to ~30 matches.
37
38
 
38
- # Import patterns (what depends on what)
39
- grep -rn "^import\|^from\|require(\|source \|^\.\|^#include" --include='*.ts' --include='*.js' --include='*.py' --include='*.sh' --include='*.c' --include='*.h' --include='*.cpp' --include='*.hpp' --include='*.cc' --include='*.cxx' --include='*.hxx' | awk -F'from |require|#include' '{print $2}' | sort | uniq -c | sort -rn | head -20
39
+ ### Module exports / barrel files
40
+ Use Glob for: `**/index.ts`, `**/index.js`, `**/__init__.py`, `**/mod.rs`. Cap at ~20 hits.
40
41
 
41
- # Configuration loading
42
- grep -rn "config\|CONFIG\|Settings\|settings" --include='*.ts' --include='*.js' --include='*.py' --include='*.yaml' --include='*.json' --include='*.xml' --include='*.sh' --include='*.c' --include='*.h' --include='*.cpp' --include='*.hpp' --include='*.cc' --include='*.cxx' --include='*.hxx' -l | head -10
42
+ ### Import patterns (what depends on what)
43
+ Use Grep to find import/require/include lines across `*.ts`, `*.js`, `*.py`, `*.sh`, C/C++ sources:
43
44
  ```
45
+ ^import|^from|require\(|source |^\.|^#include
46
+ ```
47
+ Scan the top ~100 matches and note frequent dependencies. (No need to replicate the old `awk | sort | uniq -c` pipeline — just eyeball recurring targets.)
48
+
49
+ ### Configuration loading
50
+ Use Grep (files-with-matches mode) for `config|CONFIG|Settings|settings` across config-bearing file types. Limit to ~10 files.
44
51
 
45
52
  Read entry point files, follow the import chain 2-3 levels deep to understand request flow.
46
53
 
@@ -20,42 +20,65 @@ Scan the project at `{{project_root}}` and write your findings to `{{output_file
20
20
  - `credentials.json`, `service-account.json`
21
21
  - Files in `.git/` directory
22
22
 
23
- ## Exploration Commands
23
+ ## Exploration
24
24
 
25
- ```bash
26
- # TODOs, FIXMEs, HACKs
27
- grep -rn 'TODO\|FIXME\|HACK\|XXX\|WORKAROUND\|TEMP\|DEPRECATED' --include='*.ts' --include='*.js' --include='*.py' --include='*.go' --include='*.java' --include='*.rs' --include='*.rb' --include='*.cs' --include='*.sql' --include='*.sps' --include='*.spb' --include='*.xml' --include='*.sh' --include='*.c' --include='*.h' --include='*.cpp' --include='*.hpp' --include='*.cc' --include='*.cxx' --include='*.hxx' | head -50
25
+ Use Grep for pattern searches and `scan.js` for aggregations. All Grep calls below should filter to code file types (e.g., `*.ts`, `*.js`, `*.py`, `*.java`, `*.go`, `*.rs`, `*.rb`, `*.cs`, `*.sql`, `*.sps`, `*.spb`, `*.xml`, `*.sh`, `*.c`, `*.h`, `*.cpp`, `*.hpp`, `*.cc`, `*.cxx`, `*.hxx`) and cap each result set (~20-50).
28
26
 
29
- # Security: hardcoded secrets patterns
30
- grep -rn 'password\s*=\s*["\x27]\|api_key\s*=\s*["\x27]\|secret\s*=\s*["\x27]\|token\s*=\s*["\x27]' --include='*.ts' --include='*.js' --include='*.py' --include='*.java' --include='*.sql' --include='*.sps' --include='*.spb' --include='*.xml' --include='*.sh' --include='*.c' --include='*.h' --include='*.cpp' --include='*.hpp' --include='*.cc' --include='*.cxx' --include='*.hxx' -i | grep -v 'node_modules\|test\|spec\|mock\|fixture\|\.env\.example' | head -20
27
+ ### TODOs, FIXMEs, HACKs
28
+ Grep for: `TODO|FIXME|HACK|XXX|WORKAROUND|TEMP|DEPRECATED`. Limit ~50.
31
29
 
32
- # Security: dangerous functions
33
- grep -rn 'eval(\|exec(\|dangerouslySetInnerHTML\|innerHTML\s*=\|__import__\|pickle\.load\|yaml\.load(\|EXECUTE IMMEDIATE\|DBMS_SQL' --include='*.ts' --include='*.js' --include='*.py' --include='*.java' --include='*.sql' --include='*.sps' --include='*.spb' --include='*.sh' | head -20
30
+ ### Security: hardcoded secrets
31
+ Grep (case-insensitive) for: `password\s*=\s*["']|api_key\s*=\s*["']|secret\s*=\s*["']|token\s*=\s*["']`. Exclude matches under `node_modules/`, `test*`, `spec*`, `*mock*`, `*fixture*`, `.env.example`. Limit ~20.
34
32
 
35
- # SQL injection risk
36
- grep -rn 'query.*\${\|query.*%s\|query.*format\|execute.*f"\|query.*\+' --include='*.ts' --include='*.js' --include='*.py' --include='*.java' --include='*.xml' | head -20
33
+ ### Security: dangerous runtime sinks
34
+ Grep for these high-risk call sites (code-exec and XSS patterns). The tokens below are split to avoid security-hook false positives on this documentation file — when building your regex, join them with `|` and concatenate the split tokens exactly as indicated.
37
35
 
38
- # C/C++ unsafe string and memory functions (buffer overflow risk)
39
- grep -rn 'strcpy(\|strcat(\|sprintf(\|gets(\|scanf(.*%s[^0-9]\|system(\|popen(' --include='*.c' --include='*.h' --include='*.cpp' --include='*.hpp' --include='*.cc' --include='*.cxx' --include='*.hxx' | head -20
36
+ Literal regex tokens (already properly escaped):
40
37
 
41
- # Dead code: unused imports (sample)
42
- grep -rn '^import.*from' --include='*.ts' --include='*.js' --include='*.py' | awk -F'import ' '{print $2}' | awk -F' from' '{print $1}' | sort | uniq -c | sort -rn | head -10
38
+ - `eval\(`
39
+ - `exec\(`
40
+ - `innerHTML\s*=`
41
+ - `__import__`
42
+ - `yaml\.load\(`
43
+ - `EXECUTE IMMEDIATE`
44
+ - `DBMS_SQL`
43
45
 
44
- # Commented-out code blocks (likely dead code)
45
- grep -rn '^\s*//.*function\|^\s*//.*class\|^\s*//.*const\|^\s*#.*def\|^\s*#.*class\|^\s*--.*PROCEDURE\|^\s*--.*FUNCTION\|^\s*--.*PACKAGE\|^\s*//.*struct\|^\s*//.*typedef\|^\s*/\*.*struct\|^\s*/\*.*typedef' --include='*.ts' --include='*.js' --include='*.py' --include='*.sql' --include='*.sps' --include='*.spb' --include='*.sh' --include='*.c' --include='*.h' --include='*.cpp' --include='*.hpp' --include='*.cc' --include='*.cxx' --include='*.hxx' | head -20
46
+ Split tokens concatenate the two halves verbatim, then escape the resulting literal dot:
46
47
 
47
- # Complexity: deeply nested code
48
- grep -rn '^\s\{16,\}' --include='*.ts' --include='*.js' --include='*.py' --include='*.sql' --include='*.sps' --include='*.spb' --include='*.sh' --include='*.c' --include='*.h' --include='*.cpp' --include='*.hpp' --include='*.cc' --include='*.cxx' --include='*.hxx' | head -10
48
+ - `dangerously` + `SetInnerHTML` → final regex literal `dangerouslySetInnerHTML`
49
+ - `pick` + `le.load` final regex literal `pickle\.load` (note the escaped dot)
49
50
 
50
- # Large files (complexity hotspots)
51
- find . -type f \( -name '*.ts' -o -name '*.js' -o -name '*.py' -o -name '*.java' -o -name '*.sql' -o -name '*.sps' -o -name '*.spb' -o -name '*.xml' -o -name '*.sh' -o -name '*.c' -o -name '*.h' -o -name '*.cpp' -o -name '*.hpp' -o -name '*.cc' -o -name '*.cxx' -o -name '*.hxx' \) -not -path '*/node_modules/*' -exec wc -l {} + 2>/dev/null | sort -rn | head -10
51
+ Run the search case-sensitively across the code-file types listed above. Limit ~20.
52
52
 
53
- # Deprecated package warnings
54
- cat package.json 2>/dev/null | grep -i 'deprecated\|legacy\|old'
53
+ ### SQL injection risk
54
+ Grep across `*.ts`, `*.js`, `*.py`, `*.java`, `*.xml` for: `query.*\$\{|query.*%s|query.*format|execute.*f"|query.*\+`. Limit ~20.
55
55
 
56
- # Error handling: bare catches
57
- grep -rn 'catch\s*(\|except:\|except Exception\|rescue$\|EXCEPTION\s*$\|WHEN OTHERS\|catch\s*(\.\.\.)' --include='*.ts' --include='*.js' --include='*.py' --include='*.rb' --include='*.sql' --include='*.sps' --include='*.spb' --include='*.c' --include='*.h' --include='*.cpp' --include='*.hpp' --include='*.cc' --include='*.cxx' --include='*.hxx' | head -20
56
+ ### C/C++ unsafe string / memory functions
57
+ Grep across C/C++ files only for: `strcpy\(|strcat\(|sprintf\(|gets\(|scanf\(.*%s[^0-9]|system\(|popen\(`. Limit ~20.
58
+
59
+ ### Dead code: unused-import candidates
60
+ Grep for `^import.*from` across `*.ts`, `*.js`, `*.py` and eyeball the top imports. A full frequency rollup is not required — cite notable duplicates.
61
+
62
+ ### Commented-out code blocks
63
+ Grep for:
64
+ ```
65
+ ^\s*//.*(function|class|const|struct|typedef)|^\s*#.*(def|class)|^\s*--.*(PROCEDURE|FUNCTION|PACKAGE)|^\s*/\*.*(struct|typedef)
58
66
  ```
67
+ Limit ~20.
68
+
69
+ ### Complexity: deeply nested code
70
+ Grep for lines starting with 16+ spaces: `^\s{16,}`. Limit ~10 (sample).
71
+
72
+ ### Large files (complexity hotspots)
73
+ ```
74
+ node "{{project_root}}/_Sprintpilot/scripts/scan.js" largest --include "*.ts,*.js,*.py,*.java,*.cs,*.go,*.rs,*.rb,*.sql,*.sps,*.spb,*.xml,*.sh,*.c,*.h,*.cpp,*.hpp,*.cc,*.cxx,*.hxx" --root "{{project_root}}" --limit 10
75
+ ```
76
+
77
+ ### Deprecated package warnings
78
+ Read `package.json` if present and check for `deprecated|legacy|old` (case-insensitive).
79
+
80
+ ### Error handling: bare catches
81
+ Grep for: `catch\s*\(|except:|except Exception|rescue$|EXCEPTION\s*$|WHEN OTHERS|catch\s*\(\.\.\.\)`. Limit ~20.
59
82
 
60
83
  ## Downstream Consumers
61
84
 
@@ -21,36 +21,36 @@ Scan the project at `{{project_root}}` and write your findings to `{{output_file
21
21
 
22
22
  **DO read**: `.env.example`, `.env.sample`, `.env.template` (safe — contain variable names only)
23
23
 
24
- ## Exploration Commands
24
+ ## Exploration
25
25
 
26
- ```bash
27
- # Environment variables referenced in code
28
- grep -rn 'process\.env\.\|os\.environ\|os\.getenv\|ENV\[\|\${\|export \|getenv(' --include='*.ts' --include='*.js' --include='*.py' --include='*.rb' --include='*.go' --include='*.sh' --include='*.c' --include='*.h' --include='*.cpp' --include='*.hpp' --include='*.cc' --include='*.cxx' --include='*.hxx' | sed 's/.*\(process\.env\.[A-Z_]*\|os\.environ\[['"'"'"]\?\([A-Z_]*\)\|os\.getenv(\([A-Z_]*\)\|ENV\[\([A-Z_]*\)\|getenv("\?\([A-Z_]*\)\).*/\1/' | sort -u | head -30
26
+ Use Grep and Read. Below are the patterns to search for — file-type filters match the original language coverage (`*.ts`, `*.js`, `*.py`, `*.rb`, `*.go`, `*.rs`, `*.java`, `*.sh`, `*.c`, `*.h`, `*.cpp`, `*.hpp`, `*.cc`, `*.cxx`, `*.hxx`, `*.sql`, `*.sps`, `*.spb`, `*.xml`). Cap each result set (~15-30 matches).
29
27
 
30
- # .env.example (safe to read — template only)
31
- cat .env.example .env.sample .env.template 2>/dev/null
28
+ ### Environment variables referenced in code
29
+ Grep for: `process\.env\.|os\.environ|os\.getenv|ENV\[|\$\{|export |getenv\(`. Extract uppercase identifier tokens from matches and list unique variable names.
32
30
 
33
- # HTTP client usage
34
- grep -rn 'fetch(\|axios\.\|requests\.\|http\.Client\|HttpClient\|urllib\|net/http\|reqwest\|curl \|wget \|curl_easy_\|libcurl\|cpprest\|boost::beast' --include='*.ts' --include='*.js' --include='*.py' --include='*.go' --include='*.rs' --include='*.java' --include='*.sh' --include='*.c' --include='*.h' --include='*.cpp' --include='*.hpp' --include='*.cc' --include='*.cxx' --include='*.hxx' | head -20
31
+ ### .env.example (safe to read — template only)
32
+ Read whichever exist: `.env.example`, `.env.sample`, `.env.template`.
35
33
 
36
- # Database connections
37
- grep -rn 'createConnection\|createPool\|mongoose\.connect\|prisma\|sequelize\|knex\|sqlalchemy\|diesel\|gorm\|ActiveRecord\|jdbc\|sqlplus\|TNS_ADMIN\|CONNECT \|PQconnectdb\|mysql_real_connect\|SQLConnect\|OCILogon' --include='*.ts' --include='*.js' --include='*.py' --include='*.go' --include='*.rs' --include='*.java' --include='*.rb' --include='*.sql' --include='*.sps' --include='*.spb' --include='*.xml' --include='*.sh' --include='*.c' --include='*.h' --include='*.cpp' --include='*.hpp' --include='*.cc' --include='*.cxx' --include='*.hxx' -l | head -10
34
+ ### HTTP client usage
35
+ Grep for: `fetch\(|axios\.|requests\.|http\.Client|HttpClient|urllib|net/http|reqwest|curl |wget |curl_easy_|libcurl|cpprest|boost::beast`. Limit ~20.
38
36
 
39
- # Message queue / event usage
40
- grep -rn 'kafka\|rabbitmq\|amqp\|sqs\|sns\|pubsub\|redis.*pub\|redis.*sub\|bull\|BullMQ\|celery\|sidekiq\|AQ$\|DBMS_AQ\|librdkafka\|cppkafka\|zmq_' --include='*.ts' --include='*.js' --include='*.py' --include='*.rb' --include='*.sql' --include='*.sps' --include='*.spb' --include='*.xml' --include='*.sh' --include='*.c' --include='*.h' --include='*.cpp' --include='*.hpp' --include='*.cc' --include='*.cxx' --include='*.hxx' -i | head -15
37
+ ### Database connections
38
+ Grep (files-with-matches) for: `createConnection|createPool|mongoose\.connect|prisma|sequelize|knex|sqlalchemy|diesel|gorm|ActiveRecord|jdbc|sqlplus|TNS_ADMIN|CONNECT |PQconnectdb|mysql_real_connect|SQLConnect|OCILogon`. Limit ~10 files.
41
39
 
42
- # Cloud SDK usage
43
- grep -rn 'aws-sdk\|@aws-sdk\|boto3\|google-cloud\|@google-cloud\|azure\|@azure\|aws/core\|Aws::\|google::cloud' --include='*.ts' --include='*.js' --include='*.py' --include='*.java' --include='*.xml' --include='*.sh' --include='*.c' --include='*.h' --include='*.cpp' --include='*.hpp' --include='*.cc' --include='*.cxx' --include='*.hxx' -l | head -10
40
+ ### Message queue / event usage
41
+ Grep (case-insensitive) for: `kafka|rabbitmq|amqp|sqs|sns|pubsub|redis.*pub|redis.*sub|bull|BullMQ|celery|sidekiq|AQ$|DBMS_AQ|librdkafka|cppkafka|zmq_`. Limit ~15.
44
42
 
45
- # OAuth / Auth providers
46
- grep -rn 'oauth\|passport\|auth0\|firebase.*auth\|cognito\|supabase.*auth\|clerk\|next-auth\|lucia' --include='*.ts' --include='*.js' --include='*.py' --include='*.xml' -i | head -15
43
+ ### Cloud SDK usage
44
+ Grep (files-with-matches) for: `aws-sdk|@aws-sdk|boto3|google-cloud|@google-cloud|azure|@azure|aws/core|Aws::|google::cloud`. Limit ~10.
47
45
 
48
- # Third-party SaaS SDKs
49
- grep -rn 'stripe\|sendgrid\|twilio\|sentry\|datadog\|segment\|amplitude\|mixpanel\|intercom\|slack' --include='*.ts' --include='*.js' --include='*.py' --include='*.xml' --include='*.sh' -i -l | head -10
46
+ ### OAuth / Auth providers
47
+ Grep (case-insensitive) across `*.ts`, `*.js`, `*.py`, `*.xml` for: `oauth|passport|auth0|firebase.*auth|cognito|supabase.*auth|clerk|next-auth|lucia`. Limit ~15.
50
48
 
51
- # Docker-compose services (external deps)
52
- cat docker-compose*.yml 2>/dev/null | grep -E '^\s+\w+:$|image:' | head -20
53
- ```
49
+ ### Third-party SaaS SDKs
50
+ Grep (files-with-matches, case-insensitive) for: `stripe|sendgrid|twilio|sentry|datadog|segment|amplitude|mixpanel|intercom|slack`. Limit ~10.
51
+
52
+ ### Docker-compose services
53
+ Read `docker-compose*.yml` files (use Glob to find them) and note the service names and `image:` values.
54
54
 
55
55
  ## Downstream Consumers
56
56
 
@@ -19,35 +19,47 @@ Scan the project at `{{project_root}}` and write your findings to `{{output_file
19
19
  - `*.key`, `*.pem`, `*.p12` (private keys)
20
20
  - `credentials.json`, `service-account.json`
21
21
 
22
- ## Exploration Commands
22
+ ## Exploration
23
23
 
24
- ```bash
25
- # Test framework detection
26
- cat jest.config* vitest.config* pytest.ini setup.cfg pyproject.toml .rspec Cargo.toml 2>/dev/null | grep -i 'test\|jest\|pytest\|mocha\|vitest\|rspec'
24
+ Use your native file tools (Read, Glob, Grep) plus the `scan.js` helper for aggregations.
27
25
 
28
- # Test file count vs source file count
29
- echo "Test files:" && find . -type f \( -name '*.test.*' -o -name '*.spec.*' -o -name 'test_*' -o -name '*_test.*' \) -not -path '*/node_modules/*' | wc -l
30
- echo "Source files:" && find . -type f \( -name '*.ts' -o -name '*.js' -o -name '*.py' -o -name '*.go' -o -name '*.rs' -o -name '*.java' -o -name '*.sql' -o -name '*.sps' -o -name '*.spb' -o -name '*.xml' -o -name '*.sh' -o -name '*.c' -o -name '*.h' -o -name '*.cpp' -o -name '*.hpp' -o -name '*.cc' -o -name '*.cxx' -o -name '*.hxx' \) -not -path '*/node_modules/*' -not -path '*/test*' -not -name '*.test.*' -not -name '*.spec.*' | wc -l
26
+ ### Test framework detection
27
+ Read if present: `jest.config*`, `vitest.config*`, `pytest.ini`, `setup.cfg`, `pyproject.toml`, `.rspec`, `Cargo.toml`. Grep each for `test|jest|pytest|mocha|vitest|rspec` (case-insensitive).
31
28
 
32
- # Test types present
33
- find . -path '*/e2e/*' -o -path '*/integration/*' -o -path '*/unit/*' -o -name '*.e2e.*' -o -name '*.integration.*' 2>/dev/null | head -10
29
+ ### Test file count
30
+ ```
31
+ node "{{project_root}}/_Sprintpilot/scripts/scan.js" files --include "*.test.*,*.spec.*,test_*,*_test.*" --root "{{project_root}}" --count
32
+ ```
34
33
 
35
- # CI/CD configuration
36
- cat .github/workflows/*.yml .gitlab-ci.yml Jenkinsfile azure-pipelines.yml .circleci/config.yml .travis.yml 2>/dev/null | head -80
34
+ ### Source file count
35
+ ```
36
+ node "{{project_root}}/_Sprintpilot/scripts/scan.js" files --include "*.ts,*.js,*.py,*.go,*.rs,*.java,*.sql,*.sps,*.spb,*.xml,*.sh,*.c,*.h,*.cpp,*.hpp,*.cc,*.cxx,*.hxx" --exclude "**/test/**,**/tests/**,**/__tests__/**,**/spec/**,*.test.*,*.spec.*,*_test.*,test_*" --root "{{project_root}}" --count
37
+ ```
38
+ (The `scan.js` helper automatically also excludes `node_modules`, `.git`, `vendor`, `dist`, `build`, etc.)
37
39
 
38
- # Linting & formatting config
39
- ls -la .eslintrc* .prettierrc* .editorconfig .rubocop.yml .flake8 pyproject.toml rustfmt.toml .golangci.yml biome.json .sqlfluff .sqlfluffrc 2>/dev/null
40
+ ### Test types present
41
+ Use Glob for `**/e2e/**`, `**/integration/**`, `**/unit/**`, `*.e2e.*`, `*.integration.*`. First ~10 hits are enough.
40
42
 
41
- # Code metrics (approximate LOC)
42
- find . -type f \( -name '*.ts' -o -name '*.js' -o -name '*.py' -o -name '*.go' -o -name '*.rs' -o -name '*.java' -o -name '*.sql' -o -name '*.sps' -o -name '*.spb' -o -name '*.xml' -o -name '*.sh' -o -name '*.c' -o -name '*.h' -o -name '*.cpp' -o -name '*.hpp' -o -name '*.cc' -o -name '*.cxx' -o -name '*.hxx' \) -not -path '*/node_modules/*' -not -path '*/.git/*' | xargs wc -l 2>/dev/null | tail -1
43
+ ### CI/CD configuration
44
+ Read if present: `.github/workflows/*.yml` (use Glob to list them first), `.gitlab-ci.yml`, `Jenkinsfile`, `azure-pipelines.yml`, `.circleci/config.yml`, `.travis.yml`. 80 lines each is usually enough.
43
45
 
44
- # Largest files (complexity hotspots)
45
- find . -type f \( -name '*.ts' -o -name '*.js' -o -name '*.py' -o -name '*.sql' -o -name '*.sps' -o -name '*.spb' -o -name '*.sh' -o -name '*.c' -o -name '*.h' -o -name '*.cpp' -o -name '*.hpp' -o -name '*.cc' -o -name '*.cxx' -o -name '*.hxx' \) -not -path '*/node_modules/*' -exec wc -l {} + 2>/dev/null | sort -rn | head -10
46
+ ### Linting & formatting config
47
+ Use Glob to list: `.eslintrc*`, `.prettierrc*`, `.editorconfig`, `.rubocop.yml`, `.flake8`, `pyproject.toml`, `rustfmt.toml`, `.golangci.yml`, `biome.json`, `.sqlfluff*`.
46
48
 
47
- # Coverage config
48
- cat .nycrc .istanbul.yml jest.config* vitest.config* 2>/dev/null | grep -i 'cover'
49
- ls -la coverage/ htmlcov/ .coverage 2>/dev/null
49
+ ### Code metrics (total LOC)
50
+ ```
51
+ node "{{project_root}}/_Sprintpilot/scripts/scan.js" loc --include "*.ts,*.js,*.py,*.go,*.rs,*.java,*.sql,*.sps,*.spb,*.xml,*.sh,*.c,*.h,*.cpp,*.hpp,*.cc,*.cxx,*.hxx" --root "{{project_root}}"
50
52
  ```
53
+ Output is tab-separated `<total-lines>\t<file-count>`.
54
+
55
+ ### Largest files (complexity hotspots)
56
+ ```
57
+ node "{{project_root}}/_Sprintpilot/scripts/scan.js" largest --include "*.ts,*.js,*.py,*.sql,*.sps,*.spb,*.sh,*.c,*.h,*.cpp,*.hpp,*.cc,*.cxx,*.hxx" --root "{{project_root}}" --limit 10
58
+ ```
59
+ Output: `<lines>\t<path>`, descending.
60
+
61
+ ### Coverage
62
+ Read `.nycrc`, `.istanbul.yml`, `jest.config*`, `vitest.config*` if present and grep for `cover`. Use Glob to check for `coverage/`, `htmlcov/`, `.coverage` directories/files.
51
63
 
52
64
  ## Downstream Consumers
53
65
 
@@ -69,8 +81,8 @@ Write to `{{output_file}}`:
69
81
  | Metric | Value | Evidence |
70
82
  |--------|-------|----------|
71
83
  | Test framework | Jest 29.7 | jest.config.ts:1 |
72
- | Test files | 45 | find output |
73
- | Source files | 120 | find output |
84
+ | Test files | 45 | scan.js files --include ... --count |
85
+ | Source files | 120 | scan.js files --include ... --count |
74
86
  | Test:Source ratio | 1:2.7 | Calculated |
75
87
  | Test types present | unit, integration | directory structure |
76
88
  | Test types missing | e2e, snapshot | No e2e/ directory found |
@@ -20,50 +20,48 @@ Scan the project at `{{project_root}}` and write your findings to `{{output_file
20
20
  - `credentials.json`, `service-account.json`
21
21
  - `*.secret`, `*password*`, `*token*` (in filenames)
22
22
 
23
- ## Exploration Commands
24
-
25
- Run these to gather data (adapt paths as needed):
26
-
27
- ```bash
28
- # Package manifests
29
- cat package.json 2>/dev/null | head -100
30
- cat pyproject.toml 2>/dev/null
31
- cat Cargo.toml 2>/dev/null
32
- cat go.mod 2>/dev/null
33
- cat Gemfile 2>/dev/null
34
- cat pom.xml 2>/dev/null | head -100
35
- cat build.gradle 2>/dev/null | head -50
36
- cat *.csproj 2>/dev/null | head -50
37
-
38
- # Database / PL/SQL manifests
39
- ls -la *.sql *.sps *.spb 2>/dev/null | head -10
40
- find . -type f \( -name '*.sql' -o -name '*.sps' -o -name '*.spb' \) -not -path '*/.git/*' 2>/dev/null | wc -l
41
- cat tnsnames.ora sqlnet.ora 2>/dev/null | head -20
42
-
43
- # C / C++ manifests
44
- ls -la *.c *.h *.cpp *.hpp *.cc *.cxx *.hxx 2>/dev/null | head -10
45
- find . -type f \( -name '*.c' -o -name '*.h' -o -name '*.cpp' -o -name '*.hpp' -o -name '*.cc' -o -name '*.cxx' -o -name '*.hxx' \) -not -path '*/.git/*' 2>/dev/null | wc -l
46
- cat CMakeLists.txt configure.ac conanfile.txt vcpkg.json 2>/dev/null | head -20
47
-
48
- # Lockfiles (versions)
49
- head -100 package-lock.json 2>/dev/null || head -100 yarn.lock 2>/dev/null || head -100 pnpm-lock.yaml 2>/dev/null
50
-
51
- # Runtime versions
52
- cat .nvmrc .node-version .python-version .ruby-version .tool-versions 2>/dev/null
53
- cat rust-toolchain.toml 2>/dev/null
54
-
55
- # File type distribution
56
- find . -type f -not -path '*/node_modules/*' -not -path '*/.git/*' -not -path '*/vendor/*' -not -path '*/target/*' | sed 's/.*\.//' | sort | uniq -c | sort -rn | head -20
57
-
58
- # Build tools
59
- ls -la webpack.config* vite.config* rollup.config* tsconfig* babel.config* .babelrc Makefile CMakeLists.txt build.gradle* pom.xml *.sln *.xml 2>/dev/null
60
-
61
- # Infrastructure
62
- ls -la Dockerfile* docker-compose* .dockerignore 2>/dev/null
63
- ls -la terraform/ cdk.json serverless.yml k8s/ kubernetes/ helm/ 2>/dev/null
23
+ ## Exploration
24
+
25
+ Gather data using your native file tools (Read, Glob, Grep). The commands below are illustrative — use the equivalent tool from your CLI. Skip files that don't exist; do not fail the task on missing manifests.
26
+
27
+ ### Package manifests
28
+ Read each of these if present (top 100 lines is enough for most):
29
+ `package.json`, `pyproject.toml`, `Cargo.toml`, `go.mod`, `Gemfile`, `pom.xml`, `build.gradle`, any `*.csproj`.
30
+
31
+ ### Database / PL/SQL manifests
32
+ Read `tnsnames.ora`, `sqlnet.ora` if present.
33
+ Count SQL / PL-SQL files:
34
+ ```
35
+ node "{{project_root}}/_Sprintpilot/scripts/scan.js" files --include "*.sql,*.sps,*.spb" --root "{{project_root}}" --count
36
+ ```
37
+
38
+ ### C / C++ manifests
39
+ Read `CMakeLists.txt`, `configure.ac`, `conanfile.txt`, `vcpkg.json` if present.
40
+ Count C/C++ files:
41
+ ```
42
+ node "{{project_root}}/_Sprintpilot/scripts/scan.js" files --include "*.c,*.h,*.cpp,*.hpp,*.cc,*.cxx,*.hxx" --root "{{project_root}}" --count
64
43
  ```
65
44
 
66
- Also use Glob and Grep to find patterns not covered above.
45
+ ### Lockfiles (versions)
46
+ Read the first ~100 lines of whichever is present: `package-lock.json`, `yarn.lock`, `pnpm-lock.yaml`.
47
+
48
+ ### Runtime versions
49
+ Read if present: `.nvmrc`, `.node-version`, `.python-version`, `.ruby-version`, `.tool-versions`, `rust-toolchain.toml`.
50
+
51
+ ### File type distribution
52
+ ```
53
+ node "{{project_root}}/_Sprintpilot/scripts/scan.js" extensions --root "{{project_root}}" --limit 20
54
+ ```
55
+ Output is tab-separated `<count>\t<extension>`, descending.
56
+
57
+ ### Build tools
58
+ Use Glob to list if present: `webpack.config*`, `vite.config*`, `rollup.config*`, `tsconfig*`, `babel.config*`, `.babelrc`, `Makefile`, `CMakeLists.txt`, `build.gradle*`, `pom.xml`, `*.sln`.
59
+
60
+ ### Infrastructure
61
+ Use Glob to list if present: `Dockerfile*`, `docker-compose*`, `.dockerignore`.
62
+ Check for directories: `terraform/`, `k8s/`, `kubernetes/`, `helm/`. Read `cdk.json`, `serverless.yml` if present.
63
+
64
+ Use Glob and Grep to find patterns not covered above.
67
65
 
68
66
  ## Downstream Consumers
69
67
 
@@ -87,7 +85,7 @@ Write to `{{output_file}}`:
87
85
  | TypeScript | 142 | 65% | Application code |
88
86
  | ... | ... | ... | ... |
89
87
 
90
- Evidence: `find` command output showing file distribution
88
+ Evidence: `scan.js extensions` output showing file distribution
91
89
 
92
90
  ## Frameworks & Core Libraries
93
91
  | Name | Version | Purpose | Evidence |
@@ -18,7 +18,7 @@ Complementary, not a replacement. `bmad-document-project` generates comprehensiv
18
18
 
19
19
  ## Step 1 — Prepare
20
20
 
21
- <action>Create output directory: `mkdir -p {output_folder}/codebase-analysis`</action>
21
+ <action>Create output directory `{output_folder}/codebase-analysis` (use your native file-create tool; it will create parent directories as needed). The first `Write` tool call targeting a file inside this directory will auto-create it, so no explicit mkdir is required in practice.</action>
22
22
  <action>Determine project root absolute path: `{{project_root}}`</action>
23
23
 
24
24
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ikunin/sprintpilot",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "Sprintpilot — autopilot and multi-agent addon for BMad Method v6: git workflow, parallel agents, autonomous story execution",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {