@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.
- package/README.md +3 -3
- package/_Sprintpilot/lib/runtime/args.js +0 -2
- package/_Sprintpilot/lib/runtime/git.js +0 -2
- package/_Sprintpilot/lib/runtime/http.js +12 -5
- package/_Sprintpilot/lib/runtime/log.js +0 -2
- package/_Sprintpilot/lib/runtime/secrets.js +14 -16
- package/_Sprintpilot/lib/runtime/spawn.js +21 -8
- package/_Sprintpilot/lib/runtime/text.js +0 -2
- package/_Sprintpilot/lib/runtime/yaml-lite.js +9 -5
- package/_Sprintpilot/manifest.yaml +1 -1
- package/_Sprintpilot/modules/git/branching-and-pr-strategy.md +2 -2
- package/_Sprintpilot/scripts/create-pr.js +76 -38
- package/_Sprintpilot/scripts/detect-platform.js +35 -10
- package/_Sprintpilot/scripts/health-check.js +17 -8
- package/_Sprintpilot/scripts/lint-changed.js +35 -16
- package/_Sprintpilot/scripts/lock.js +22 -6
- package/_Sprintpilot/scripts/sanitize-branch.js +4 -2
- package/_Sprintpilot/scripts/stage-and-commit.js +15 -7
- package/_Sprintpilot/scripts/sync-status.js +16 -6
- package/_Sprintpilot/skills/sprint-autopilot-on/workflow.md +12 -12
- package/bin/sprintpilot.js +11 -4
- package/lib/commands/check-update.js +0 -2
- package/lib/commands/install.js +139 -49
- package/lib/commands/uninstall.js +21 -11
- package/lib/core/bmad-config.js +0 -2
- package/lib/core/file-ops.js +6 -6
- package/lib/core/gitignore.js +0 -2
- package/lib/core/markers.js +5 -3
- package/lib/core/tool-registry.js +19 -21
- package/lib/core/update-check.js +0 -2
- package/lib/core/v1-detect.js +0 -2
- package/lib/prompts.js +0 -2
- package/lib/substitute.js +1 -5
- 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 {
|
|
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'))
|
|
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'))
|
|
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'))
|
|
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'))
|
|
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'))
|
|
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'))
|
|
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'))
|
|
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'))
|
|
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'))
|
|
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'))
|
|
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) {
|
|
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
|
-
|
|
185
|
-
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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')
|
|
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) {
|
|
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(
|
|
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
|
-
|
|
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) {
|
|
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(
|
|
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
|
|
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 {
|
|
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 {
|
|
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) {
|
|
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.
|
|
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 =
|
|
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: `
|
|
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
|
-
`
|
|
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
|
-
`
|
|
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 `
|
|
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 `
|
|
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
|
-
`
|
|
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
|
-
`
|
|
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
|
-
`
|
|
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
|
-
`
|
|
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: `
|
|
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
|
-
`
|
|
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: `
|
|
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>
|
package/bin/sprintpilot.js
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
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 {
|