@ikunin/sprintpilot 1.0.0 → 1.0.2

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 (34) hide show
  1. package/README.md +3 -3
  2. package/_Sprintpilot/lib/runtime/args.js +0 -2
  3. package/_Sprintpilot/lib/runtime/git.js +0 -2
  4. package/_Sprintpilot/lib/runtime/http.js +12 -5
  5. package/_Sprintpilot/lib/runtime/log.js +0 -2
  6. package/_Sprintpilot/lib/runtime/secrets.js +14 -16
  7. package/_Sprintpilot/lib/runtime/spawn.js +21 -8
  8. package/_Sprintpilot/lib/runtime/text.js +0 -2
  9. package/_Sprintpilot/lib/runtime/yaml-lite.js +9 -5
  10. package/_Sprintpilot/manifest.yaml +1 -1
  11. package/_Sprintpilot/modules/git/branching-and-pr-strategy.md +2 -2
  12. package/_Sprintpilot/scripts/create-pr.js +76 -38
  13. package/_Sprintpilot/scripts/detect-platform.js +35 -10
  14. package/_Sprintpilot/scripts/health-check.js +17 -8
  15. package/_Sprintpilot/scripts/lint-changed.js +35 -16
  16. package/_Sprintpilot/scripts/lock.js +22 -6
  17. package/_Sprintpilot/scripts/sanitize-branch.js +4 -2
  18. package/_Sprintpilot/scripts/stage-and-commit.js +15 -7
  19. package/_Sprintpilot/scripts/sync-status.js +16 -6
  20. package/_Sprintpilot/skills/sprint-autopilot-on/workflow.md +12 -12
  21. package/bin/sprintpilot.js +11 -4
  22. package/lib/commands/check-update.js +0 -2
  23. package/lib/commands/install.js +139 -49
  24. package/lib/commands/uninstall.js +21 -11
  25. package/lib/core/bmad-config.js +0 -2
  26. package/lib/core/file-ops.js +6 -6
  27. package/lib/core/gitignore.js +0 -2
  28. package/lib/core/markers.js +5 -3
  29. package/lib/core/tool-registry.js +19 -21
  30. package/lib/core/update-check.js +0 -2
  31. package/lib/core/v1-detect.js +0 -2
  32. package/lib/prompts.js +0 -2
  33. package/lib/substitute.js +1 -5
  34. package/package.json +1 -1
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- 'use strict';
3
2
 
4
3
  const fs = require('node:fs');
5
4
  const path = require('node:path');
@@ -54,7 +53,9 @@ function localBin(name) {
54
53
  try {
55
54
  const stat = fs.statSync(p);
56
55
  if (stat.isFile()) return p;
57
- } catch { /* ignore */ }
56
+ } catch {
57
+ /* ignore */
58
+ }
58
59
  return null;
59
60
  }
60
61
 
@@ -72,7 +73,8 @@ async function lintLanguage(lang, files) {
72
73
  if (lang === 'python') {
73
74
  if (await hasCli('ruff')) return runLinter('ruff', 'ruff', ['check'], files);
74
75
  if (await hasCli('flake8')) return runLinter('flake8', 'flake8', [], files);
75
- if (await hasCli('pylint')) return runLinter('pylint', 'pylint', ['--output-format=text'], files);
76
+ if (await hasCli('pylint'))
77
+ return runLinter('pylint', 'pylint', ['--output-format=text'], files);
76
78
  return null;
77
79
  }
78
80
  if (lang === 'js-ts') {
@@ -83,15 +85,18 @@ async function lintLanguage(lang, files) {
83
85
  return null;
84
86
  }
85
87
  if (lang === 'rust') {
86
- if (await hasCli('cargo')) return runLinter('cargo-clippy', 'cargo', ['clippy', '--message-format=short'], []);
88
+ if (await hasCli('cargo'))
89
+ return runLinter('cargo-clippy', 'cargo', ['clippy', '--message-format=short'], []);
87
90
  return null;
88
91
  }
89
92
  if (lang === 'go') {
90
- if (await hasCli('golangci-lint')) return runLinter('golangci-lint', 'golangci-lint', ['run'], []);
93
+ if (await hasCli('golangci-lint'))
94
+ return runLinter('golangci-lint', 'golangci-lint', ['run'], []);
91
95
  return null;
92
96
  }
93
97
  if (lang === 'ruby') {
94
- if (await hasCli('rubocop')) return runLinter('rubocop', 'rubocop', ['--format', 'simple'], files);
98
+ if (await hasCli('rubocop'))
99
+ return runLinter('rubocop', 'rubocop', ['--format', 'simple'], files);
95
100
  return null;
96
101
  }
97
102
  if (lang === 'java') {
@@ -108,25 +113,35 @@ async function lintLanguage(lang, files) {
108
113
  return null;
109
114
  }
110
115
  if (lang === 'c') {
111
- if (await hasCli('cppcheck')) return runLinter('cppcheck', 'cppcheck', ['--enable=warning,style'], files);
116
+ if (await hasCli('cppcheck'))
117
+ return runLinter('cppcheck', 'cppcheck', ['--enable=warning,style'], files);
112
118
  if (await hasCli('clang-tidy')) return runLinter('clang-tidy', 'clang-tidy', [], files);
113
119
  return null;
114
120
  }
115
121
  if (lang === 'cpp') {
116
- if (await hasCli('cppcheck')) return runLinter('cppcheck', 'cppcheck', ['--enable=warning,style', '--language=c++'], files);
122
+ if (await hasCli('cppcheck'))
123
+ return runLinter('cppcheck', 'cppcheck', ['--enable=warning,style', '--language=c++'], files);
117
124
  if (await hasCli('clang-tidy')) return runLinter('clang-tidy', 'clang-tidy', [], files);
118
125
  return null;
119
126
  }
120
127
  if (lang === 'csharp') {
121
- if (await hasCli('dotnet')) return runLinter('dotnet-format', 'dotnet', ['format', '--verify-no-changes', '--diagnostics'], []);
128
+ if (await hasCli('dotnet'))
129
+ return runLinter(
130
+ 'dotnet-format',
131
+ 'dotnet',
132
+ ['format', '--verify-no-changes', '--diagnostics'],
133
+ [],
134
+ );
122
135
  return null;
123
136
  }
124
137
  if (lang === 'swift') {
125
- if (await hasCli('swiftlint')) return runLinter('swiftlint', 'swiftlint', ['lint', '--quiet'], files);
138
+ if (await hasCli('swiftlint'))
139
+ return runLinter('swiftlint', 'swiftlint', ['lint', '--quiet'], files);
126
140
  return null;
127
141
  }
128
142
  if (lang === 'sql') {
129
- if (await hasCli('sqlfluff')) return runLinter('sqlfluff', 'sqlfluff', ['lint', '--dialect', 'oracle'], files);
143
+ if (await hasCli('sqlfluff'))
144
+ return runLinter('sqlfluff', 'sqlfluff', ['lint', '--dialect', 'oracle'], files);
130
145
  return null;
131
146
  }
132
147
  if (lang === 'kotlin') {
@@ -135,7 +150,8 @@ async function lintLanguage(lang, files) {
135
150
  return null;
136
151
  }
137
152
  if (lang === 'php') {
138
- if (await hasCli('phpstan')) return runLinter('phpstan', 'phpstan', ['analyse', '--no-progress'], files);
153
+ if (await hasCli('phpstan'))
154
+ return runLinter('phpstan', 'phpstan', ['analyse', '--no-progress'], files);
139
155
  if (await hasCli('phpcs')) return runLinter('phpcs', 'phpcs', [], files);
140
156
  return null;
141
157
  }
@@ -172,7 +188,10 @@ async function runOverride(name, files) {
172
188
 
173
189
  async function main() {
174
190
  const { opts } = parseArgs(process.argv.slice(2));
175
- if (opts.help) { help(); process.exit(0); }
191
+ if (opts.help) {
192
+ help();
193
+ process.exit(0);
194
+ }
176
195
 
177
196
  const limit = parseInt(opts.limit || '100', 10);
178
197
  const outputFile = opts['output-file'] || '';
@@ -180,9 +199,9 @@ async function main() {
180
199
 
181
200
  const modified = await tryGitStdout(['diff', '--name-only', 'HEAD']);
182
201
  const untracked = await tryGitStdout(['ls-files', '--others', '--exclude-standard']);
183
- const all = Array.from(
184
- new Set([...splitLines(modified || ''), ...splitLines(untracked || '')])
185
- ).filter(Boolean).sort();
202
+ const all = Array.from(new Set([...splitLines(modified || ''), ...splitLines(untracked || '')]))
203
+ .filter(Boolean)
204
+ .sort();
186
205
 
187
206
  if (all.length === 0) {
188
207
  log.out('No changed files to lint');
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- 'use strict';
3
2
 
4
3
  const fs = require('node:fs');
5
4
  const path = require('node:path');
@@ -88,12 +87,20 @@ function writeLockExclusive(lockFile, id) {
88
87
  fs.writeSync(fd, content, 0, 'utf8');
89
88
  wrote = true;
90
89
  } finally {
91
- try { fs.closeSync(fd); } catch { /* ignore */ }
90
+ try {
91
+ fs.closeSync(fd);
92
+ } catch {
93
+ /* ignore */
94
+ }
92
95
  if (!wrote) {
93
96
  // writeSync failed (ENOSPC, EIO): leaving an empty lockfile behind
94
97
  // would look "corrupt" to the next acquirer and permanently wedge
95
98
  // the autopilot. Unlink so the next try can re-create cleanly.
96
- try { fs.unlinkSync(lockFile); } catch { /* ignore */ }
99
+ try {
100
+ fs.unlinkSync(lockFile);
101
+ } catch {
102
+ /* ignore */
103
+ }
97
104
  }
98
105
  }
99
106
  }
@@ -143,7 +150,11 @@ function main() {
143
150
  // of them gets EEXIST.
144
151
  const info = readLockInfo(lockFile, staleSeconds);
145
152
  if (info.state === 'STALE') {
146
- try { fs.unlinkSync(lockFile); } catch { /* ignore */ }
153
+ try {
154
+ fs.unlinkSync(lockFile);
155
+ } catch {
156
+ /* ignore */
157
+ }
147
158
  try {
148
159
  writeLockExclusive(lockFile, id);
149
160
  log.out(`ACQUIRED_STALE:${id}`);
@@ -176,7 +187,11 @@ function main() {
176
187
 
177
188
  if (action === 'release') {
178
189
  if (fs.existsSync(lockFile)) {
179
- try { fs.unlinkSync(lockFile); } catch { /* ignore */ }
190
+ try {
191
+ fs.unlinkSync(lockFile);
192
+ } catch {
193
+ /* ignore */
194
+ }
180
195
  log.out('RELEASED');
181
196
  } else {
182
197
  log.out('NO_LOCK');
@@ -187,7 +202,8 @@ function main() {
187
202
  if (action === 'status') {
188
203
  const info = readLockInfo(lockFile, staleSeconds);
189
204
  if (info.state === 'FREE') log.out('Lock: free (no active session)');
190
- else if (info.state === 'LOCKED') log.out(`Lock: ACTIVE — session ${info.id}, age ${info.ageMin}m`);
205
+ else if (info.state === 'LOCKED')
206
+ log.out(`Lock: ACTIVE — session ${info.id}, age ${info.ageMin}m`);
191
207
  else log.out(`Lock: STALE — session ${info.id}, age ${info.ageMin}m (will auto-remove)`);
192
208
  }
193
209
  }
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- 'use strict';
3
2
 
4
3
  const crypto = require('node:crypto');
5
4
 
@@ -54,7 +53,10 @@ async function validateRefFormat(fullName) {
54
53
 
55
54
  async function main() {
56
55
  const { opts, positional } = parseArgs(process.argv.slice(2));
57
- if (opts.help) { help(); process.exit(0); }
56
+ if (opts.help) {
57
+ help();
58
+ process.exit(0);
59
+ }
58
60
  const storyKey = positional[0];
59
61
  const prefix = opts.prefix ?? 'story/';
60
62
  const maxLength = parseInt(opts['max-length'] || '60', 10);
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- 'use strict';
3
2
 
4
3
  const fs = require('node:fs');
5
4
  const path = require('node:path');
@@ -15,7 +14,9 @@ const {
15
14
  const log = require('../lib/runtime/log');
16
15
 
17
16
  function help() {
18
- log.out("Usage: stage-and-commit.js --message 'msg' [--allowlist path] [--max-size-mb 1] [--file-list path] [--dry-run]");
17
+ log.out(
18
+ "Usage: stage-and-commit.js --message 'msg' [--allowlist path] [--max-size-mb 1] [--file-list path] [--dry-run]",
19
+ );
19
20
  }
20
21
 
21
22
  function splitOut(out) {
@@ -34,8 +35,9 @@ async function collectChanges() {
34
35
  // add-side list so we don't `git add` a path that no longer exists and
35
36
  // emit a spurious warning; the dedicated `git rm` loop handles them.
36
37
  const deletedSet = new Set(deleted);
37
- const all = dedupeSorted([...splitOut(modified), ...splitOut(untracked)])
38
- .filter((f) => !deletedSet.has(f));
38
+ const all = dedupeSorted([...splitOut(modified), ...splitOut(untracked)]).filter(
39
+ (f) => !deletedSet.has(f),
40
+ );
39
41
  return { all, deleted };
40
42
  }
41
43
 
@@ -52,7 +54,10 @@ function parseFileListMarkdown(filePath) {
52
54
 
53
55
  async function main() {
54
56
  const { opts } = parseArgs(process.argv.slice(2), { booleanFlags: ['dry-run'] });
55
- if (opts.help) { help(); process.exit(0); }
57
+ if (opts.help) {
58
+ help();
59
+ process.exit(0);
60
+ }
56
61
 
57
62
  const message = opts.message ?? opts.m;
58
63
  const allowlist = opts.allowlist;
@@ -95,7 +100,9 @@ async function main() {
95
100
 
96
101
  if (!isAllowlisted(file, allowPatterns)) {
97
102
  if (lstat.size > MAX_SCAN_BYTES) {
98
- warnings.push(`secret scan skipped for ${file} (size ${Math.floor(lstat.size / 1024)} KB > ${MAX_SCAN_BYTES / 1024} KB limit)`);
103
+ warnings.push(
104
+ `secret scan skipped for ${file} (size ${Math.floor(lstat.size / 1024)} KB > ${MAX_SCAN_BYTES / 1024} KB limit)`,
105
+ );
99
106
  } else if (!isBinary) {
100
107
  try {
101
108
  const raw = fs.readFileSync(file, 'utf8');
@@ -126,7 +133,8 @@ async function main() {
126
133
  if (fs.existsSync('.gitignore')) {
127
134
  // Exact line match — substring tests were fooled by the entry appearing
128
135
  // inside a comment (e.g. "# .autopilot.lock is auto-created").
129
- const entries = fs.readFileSync('.gitignore', 'utf8')
136
+ const entries = fs
137
+ .readFileSync('.gitignore', 'utf8')
130
138
  .split(/\r?\n/)
131
139
  .map((l) => l.trim())
132
140
  .filter((l) => l && !l.startsWith('#'));
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- 'use strict';
3
2
 
4
3
  const fs = require('node:fs');
5
4
  const path = require('node:path');
@@ -40,12 +39,20 @@ function atomicWrite(targetPath, content) {
40
39
  try {
41
40
  fs.writeFileSync(targetPath, content, 'utf8');
42
41
  } finally {
43
- try { fs.unlinkSync(tmp); } catch { /* best effort */ }
42
+ try {
43
+ fs.unlinkSync(tmp);
44
+ } catch {
45
+ /* best effort */
46
+ }
44
47
  }
45
48
  return;
46
49
  }
47
50
  // Any other error: clean up tmp so we don't leak cruft.
48
- try { fs.unlinkSync(tmp); } catch { /* best effort */ }
51
+ try {
52
+ fs.unlinkSync(tmp);
53
+ } catch {
54
+ /* best effort */
55
+ }
49
56
  throw e;
50
57
  }
51
58
  }
@@ -78,7 +85,10 @@ function buildHeader(baseBranch, platform) {
78
85
 
79
86
  function main() {
80
87
  const { opts } = parseArgs(process.argv.slice(2));
81
- if (opts.help) { help(); process.exit(0); }
88
+ if (opts.help) {
89
+ help();
90
+ process.exit(0);
91
+ }
82
92
 
83
93
  const story = opts.story;
84
94
  const statusFile = opts['git-status-file'];
@@ -102,12 +112,12 @@ function main() {
102
112
  // value", so we must NOT emit the field when the flag is absent. The
103
113
  // previous logic defaulted to 'false' and overwrote a prior 'true' every
104
114
  // call.
105
- const hasWorktreeCleaned = Object.prototype.hasOwnProperty.call(opts, 'worktree-cleaned');
115
+ const hasWorktreeCleaned = Object.hasOwn(opts, 'worktree-cleaned');
106
116
  let worktreeCleaned;
107
117
  if (hasWorktreeCleaned) {
108
118
  const v = opts['worktree-cleaned'];
109
119
  // Accept 'true'/'false' strings (any case) and boolean true.
110
- worktreeCleaned = (v === true || String(v).toLowerCase() === 'true') ? 'true' : 'false';
120
+ worktreeCleaned = v === true || String(v).toLowerCase() === 'true' ? 'true' : 'false';
111
121
  }
112
122
 
113
123
  const fields = [
@@ -177,7 +177,7 @@ Resolve:
177
177
  <action>STOP</action>
178
178
  </check>
179
179
 
180
- <action>**Lock file** — run: `bash {{project_root}}/_Sprintpilot/scripts/lock.sh acquire`
180
+ <action>**Lock file** — run: `node {{project_root}}/_Sprintpilot/scripts/lock.js acquire`
181
181
  Output will be one of:
182
182
  - `ACQUIRED:<session-id>` → proceed
183
183
  - `ACQUIRED_STALE:<session-id>` → stale lock removed, proceed
@@ -189,18 +189,18 @@ Resolve:
189
189
  </check>
190
190
 
191
191
  <action>**Detect platform** — run:
192
- `bash {{project_root}}/_Sprintpilot/scripts/detect-platform.sh --provider {{git.platform.provider}}`
192
+ `node {{project_root}}/_Sprintpilot/scripts/detect-platform.js --provider {{git.platform.provider}}`
193
193
  Output: `github`, `gitlab`, or `git_only`. Set `{{platform}}` to the output.
194
194
  Log: "Platform detected: {{platform}}"
195
195
  </action>
196
196
 
197
197
  <action>**Worktree health check** — run:
198
- `bash {{project_root}}/_Sprintpilot/scripts/health-check.sh --base-branch {{base_branch}} --status-file {{status_file}}`
198
+ `node {{project_root}}/_Sprintpilot/scripts/health-check.js --base-branch {{base_branch}} --status-file {{status_file}}`
199
199
  Output classifies each worktree as CLEAN_DONE, COMMITTED, STALE, DIRTY, or ORPHAN.
200
200
  - CLEAN_DONE: `git worktree remove .worktrees/<name>` + `git worktree prune`
201
201
  - COMMITTED: log "Recoverable work found for <name> — will push via git -C"
202
202
  Push the branch: `git -C .worktrees/<name> push -u origin <branch> 2>&1`
203
- If `{{create_pr}}` is true AND platform != git_only: create PR via `bash {{project_root}}/_Sprintpilot/scripts/create-pr.sh ...`
203
+ If `{{create_pr}}` is true AND platform != git_only: create PR via `node {{project_root}}/_Sprintpilot/scripts/create-pr.js ...`
204
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}}`
205
205
  Then remove worktree.
206
206
  - STALE: `git worktree remove .worktrees/<name> --force` + prune
@@ -230,7 +230,7 @@ Resolve:
230
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"`.
231
231
  - If `{{platform}}` is NOT git_only (github, gitlab, bitbucket, gitea) AND `{{create_pr}}` is true:
232
232
  - Check if PR/MR already exists for this branch (platform-specific check via create-pr.sh or CLI)
233
- - If no PR: create one via `bash {{project_root}}/_Sprintpilot/scripts/create-pr.sh --platform {{platform}} ...`
233
+ - If no PR: create one via `node {{project_root}}/_Sprintpilot/scripts/create-pr.js --platform {{platform}} ...`
234
234
  - Log: "PR created/found for <story-key>"
235
235
  - Update `{git_status_file}` via sync-status.sh: set `--merge-status "pr_pending"` for this story (same full-field requirement as above)
236
236
  - If status IS "done" AND branch still exists AND `{{cleanup_on_merge}}` is true:
@@ -427,7 +427,7 @@ Resolve:
427
427
  <!-- GIT: Enter worktree before dev-story -->
428
428
  <check if="{{git_enabled}} AND {{next_skill}} is bmad-dev-story">
429
429
  <action>**Sanitize branch name** — run:
430
- `bash {{project_root}}/_Sprintpilot/scripts/sanitize-branch.sh "{{current_story}}" --prefix "{{branch_prefix}}" --max-length 60`
430
+ `node {{project_root}}/_Sprintpilot/scripts/sanitize-branch.js "{{current_story}}" --prefix "{{branch_prefix}}" --max-length 60`
431
431
  Output: sanitized name (without prefix). Set `{{branch_name}}` = output.
432
432
  Full branch ref will be `{{branch_prefix}}{{branch_name}}`.
433
433
  </action>
@@ -530,7 +530,7 @@ pr_base: {{pr_base}}
530
530
  <!-- GIT: Lint, stage, and commit after dev-story -->
531
531
  <check if="{{git_enabled}} AND {{in_worktree}}">
532
532
  <action>**Lint changed files** — run:
533
- `bash {{project_root}}/_Sprintpilot/scripts/lint-changed.sh --limit 100 --output-file lint-output.txt`
533
+ `node {{project_root}}/_Sprintpilot/scripts/lint-changed.js --limit 100 --output-file lint-output.txt`
534
534
  Log the output summary (non-blocking — lint never halts the autopilot).
535
535
  Set `{{lint_result}}` from the summary line.
536
536
  </action>
@@ -542,7 +542,7 @@ pr_base: {{pr_base}}
542
542
  - `{patch-title}` → from review finding title, fallback to "code review fix"
543
543
  Read the commit template from `git.commit_templates.story` in config (default: `feat({epic}): {story-title} ({story-key})`).
544
544
  Then run:
545
- `bash {{project_root}}/_Sprintpilot/scripts/stage-and-commit.sh --message "feat({{epic}}): {{story-title}} ({{current_story}})" --allowlist {{project_root}}/_Sprintpilot/.secrets-allowlist`
545
+ `node {{project_root}}/_Sprintpilot/scripts/stage-and-commit.js --message "feat({{epic}}): {{story-title}} ({{current_story}})" --allowlist {{project_root}}/_Sprintpilot/.secrets-allowlist`
546
546
  Output: commit SHA. Set `{{story_commit}}` = output.
547
547
  Warnings (secrets, large files) printed to stderr — review but don't halt unless user says to.
548
548
  </action>
@@ -588,7 +588,7 @@ pr_base: {{pr_base}}
588
588
  <action>Sanitize branch name for `{{current_story}}` (same logic as step 3)</action>
589
589
  <action>Check if branch already registered in `{git_status_file}` for this story → skip if so</action>
590
590
  <action>Register branch in `{git_status_file}`:
591
- `bash {{project_root}}/_Sprintpilot/scripts/sync-status.sh --story "{{current_story}}" --git-status-file "{{project_root}}/_bmad-output/implementation-artifacts/git-status.yaml" --branch "{{branch_prefix}}{{branch_name}}" --platform "{{platform}}" --base-branch "{{base_branch}}"`
591
+ `node {{project_root}}/_Sprintpilot/scripts/sync-status.js --story "{{current_story}}" --git-status-file "{{project_root}}/_bmad-output/implementation-artifacts/git-status.yaml" --branch "{{branch_prefix}}{{branch_name}}" --platform "{{platform}}" --base-branch "{{base_branch}}"`
592
592
  </action>
593
593
  </check>
594
594
 
@@ -741,7 +741,7 @@ Instruct: "Re-verify code review for story {{current_story}} — all patch findi
741
741
  - `{lint-result}` → `{{lint_result}}`
742
742
  - `{test-result}` → from last test run output
743
743
  - `{patch-count}` → number of patch commits
744
- 3. Run: `bash {{project_root}}/_Sprintpilot/scripts/create-pr.sh --platform {{platform}} --branch {{branch_prefix}}{{branch_name}} --base {{pr_base}} --title "{{story-title}} ({{current_story}})" --body "<filled template>"`
744
+ 3. Run: `node {{project_root}}/_Sprintpilot/scripts/create-pr.js --platform {{platform}} --branch {{branch_prefix}}{{branch_name}} --base {{pr_base}} --title "{{story-title}} ({{current_story}})" --body "<filled template>"`
745
745
  4. Output: PR URL or "SKIPPED". Set `{{pr_url}}` = output.
746
746
  If creation fails → log warning, set `{{pr_url}}` = null, continue.
747
747
  </action>
@@ -800,7 +800,7 @@ Instruct: "Re-verify code review for story {{current_story}} — all patch findi
800
800
  </action>
801
801
 
802
802
  <action>**Write git status** to addon's own file (NEVER modify sprint-status.yaml) — runs AFTER checkout to base branch so the file persists in the working tree for the commit below:
803
- `bash {{project_root}}/_Sprintpilot/scripts/sync-status.sh --story "{{current_story}}" --git-status-file "{{project_root}}/_bmad-output/implementation-artifacts/git-status.yaml" --branch "{{branch_prefix}}{{branch_name}}" --commit "{{story_commit}}" --patch-commits "{{patch_commits_csv}}" --push-status "{{push_status}}" --merge-status "{{merge_status}}" --pr-url "{{pr_url}}" --lint-result "{{lint_result}}" --worktree "{{project_root}}/.worktrees/{{current_story}}" --platform "{{platform}}" --base-branch "{{base_branch}}"`
803
+ `node {{project_root}}/_Sprintpilot/scripts/sync-status.js --story "{{current_story}}" --git-status-file "{{project_root}}/_bmad-output/implementation-artifacts/git-status.yaml" --branch "{{branch_prefix}}{{branch_name}}" --commit "{{story_commit}}" --patch-commits "{{patch_commits_csv}}" --push-status "{{push_status}}" --merge-status "{{merge_status}}" --pr-url "{{pr_url}}" --lint-result "{{lint_result}}" --worktree "{{project_root}}/.worktrees/{{current_story}}" --platform "{{platform}}" --base-branch "{{base_branch}}"`
804
804
  This writes to `git-status.yaml` (addon-owned). Sprint-status.yaml is BMAD-owned — updated by BMAD skills only.
805
805
  </action>
806
806
 
@@ -1043,7 +1043,7 @@ If the skill is not available or fails, generate a minimal README.md:
1043
1043
 
1044
1044
  <!-- GIT: Release lock -->
1045
1045
  <check if="{{git_enabled}}">
1046
- <action>Release lock: `bash {{project_root}}/_Sprintpilot/scripts/lock.sh release`</action>
1046
+ <action>Release lock: `node {{project_root}}/_Sprintpilot/scripts/lock.js release`</action>
1047
1047
  </check>
1048
1048
 
1049
1049
  <action>Delete `{state_file}` — sprint complete</action>
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- 'use strict';
3
2
 
4
3
  const path = require('node:path');
5
4
  const { Command } = require('commander');
@@ -42,16 +41,24 @@ async function main() {
42
41
 
43
42
  program
44
43
  .name('sprintpilot')
45
- .description('Sprintpilot — autopilot and multi-agent addon for BMad Method: autonomous story execution, parallel agents, git workflow')
44
+ .description(
45
+ 'Sprintpilot — autopilot and multi-agent addon for BMad Method: autonomous story execution, parallel agents, git workflow',
46
+ )
46
47
  .version(await resolveVersion(), '-v, --version', 'Show version');
47
48
 
48
49
  program
49
50
  .command('install', { isDefault: true })
50
51
  .description('Install Sprintpilot into the current BMad Method project')
51
- .option('--tools <list>', 'Comma-separated tools (claude-code,cursor,windsurf,cline,roo,trae,kiro,gemini-cli,github-copilot,all)')
52
+ .option(
53
+ '--tools <list>',
54
+ 'Comma-separated tools (claude-code,cursor,windsurf,cline,roo,trae,kiro,gemini-cli,github-copilot,all)',
55
+ )
52
56
  .option('--dry-run', 'Preview without making changes')
53
57
  .option('--force', 'Skip backup of existing skills')
54
- .option('--migrate-v1', 'Migrate from bmad-autopilot-addon v1 (auto-detected; this flag is for non-interactive CI)')
58
+ .option(
59
+ '--migrate-v1',
60
+ 'Migrate from bmad-autopilot-addon v1 (auto-detected; this flag is for non-interactive CI)',
61
+ )
55
62
  .option('-y, --yes', 'Non-interactive mode')
56
63
  .action(async (options) => {
57
64
  try {
@@ -1,5 +1,3 @@
1
- 'use strict';
2
-
3
1
  const path = require('node:path');
4
2
  const fs = require('fs-extra');
5
3