@ikunin/sprintpilot 1.0.3 → 1.0.4
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/_Sprintpilot/Sprintpilot.md +17 -1
- package/_Sprintpilot/manifest.yaml +1 -1
- package/_Sprintpilot/modules/autopilot/config.yaml +18 -0
- package/_Sprintpilot/skills/sprint-autopilot-on/workflow.md +144 -7
- package/lib/commands/install.js +247 -3
- package/lib/prompts.js +22 -0
- package/lib/substitute.js +1 -1
- package/package.json +4 -1
|
@@ -25,6 +25,22 @@ When Sprintpilot or the git addon is active:
|
|
|
25
25
|
- Each story gets its own isolated worktree and branch (`story/<key>`)
|
|
26
26
|
- Commits use conventional format: `feat(<epic>): <title> (<key>)`
|
|
27
27
|
|
|
28
|
+
### Autopilot configuration
|
|
29
|
+
|
|
30
|
+
Edit `_Sprintpilot/modules/autopilot/config.yaml`:
|
|
31
|
+
|
|
32
|
+
| Setting | Default | Values | Purpose |
|
|
33
|
+
|---------|---------|--------|---------|
|
|
34
|
+
| `autopilot.session_story_limit` | `3` | integer ≥ 0 | Stories fully implemented per autopilot run before checkpoint. `0` = unlimited. |
|
|
35
|
+
| `autopilot.retrospective_mode` | `auto` | `auto` / `stop` / `skip` | How epic-end retrospectives are handled (see below). |
|
|
36
|
+
|
|
37
|
+
`retrospective_mode` options:
|
|
38
|
+
- **`auto`** *(default)* — autopilot writes a deterministic retrospective artifact from `sprint-status.yaml` + `decision-log.yaml`, then continues. Single pass, no external skill call, safe under every CLI.
|
|
39
|
+
- **`stop`** — autopilot pauses at epic completion. Run `/bmad-retrospective` interactively, then re-run `/sprint-autopilot-on` to resume. Use this when you want the full multi-persona discussion as part of your process.
|
|
40
|
+
- **`skip`** — no retrospective artifact is written. **Not recommended** — you lose the epic-level learning record.
|
|
41
|
+
|
|
42
|
+
Both settings are prompted during `sprintpilot install` (interactive mode) with existing values as defaults, so reinstalls preserve your choices.
|
|
43
|
+
|
|
28
44
|
---
|
|
29
45
|
|
|
30
46
|
## Full skill reference by lifecycle phase
|
|
@@ -59,7 +75,7 @@ See **Mandatory sequence per story** section below.
|
|
|
59
75
|
|
|
60
76
|
| Skill | When to use |
|
|
61
77
|
|-------|-------------|
|
|
62
|
-
| `bmad-retrospective` | Run after all stories in an epic are `done`; saves lessons, marks epic `done` |
|
|
78
|
+
| `bmad-retrospective` | Run after all stories in an epic are `done`; saves lessons, marks epic `done`. Under autopilot this is driven by `autopilot.retrospective_mode` (`auto` inline, `stop` to pause for interactive use, or `skip`). |
|
|
63
79
|
|
|
64
80
|
---
|
|
65
81
|
|
|
@@ -18,3 +18,21 @@ autopilot:
|
|
|
18
18
|
#
|
|
19
19
|
# Set 0 to disable the limit and run until the sprint is complete.
|
|
20
20
|
session_story_limit: 3
|
|
21
|
+
|
|
22
|
+
# Retrospective handling at epic completion.
|
|
23
|
+
#
|
|
24
|
+
# - "auto" (default): autopilot generates a deterministic retrospective
|
|
25
|
+
# from sprint-status.yaml + decision-log.yaml, then continues with the
|
|
26
|
+
# next epic. Safe under every CLI including Gemini CLI — no external
|
|
27
|
+
# skill call, no persona simulation, single pass.
|
|
28
|
+
# - "stop": autopilot pauses at epic completion so you can run
|
|
29
|
+
# /bmad-retrospective interactively. Re-run /sprint-autopilot-on to
|
|
30
|
+
# resume with the next epic.
|
|
31
|
+
# - "skip": autopilot skips the retrospective entirely and continues.
|
|
32
|
+
# NOT RECOMMENDED — you lose the epic-level learning record.
|
|
33
|
+
#
|
|
34
|
+
# Background: the external bmad-retrospective skill can enter a
|
|
35
|
+
# multi-persona discussion loop when driven by autopilot on some CLIs,
|
|
36
|
+
# which exhausts the token/memory budget. That's why "auto" generates
|
|
37
|
+
# the artifact inline instead of invoking the external skill.
|
|
38
|
+
retrospective_mode: auto
|
|
@@ -138,6 +138,7 @@ All BMAD skills are fully automatable (auto-continue past menus, derive decision
|
|
|
138
138
|
| `bmad-create-ux-design` | Automatable if PRD exists; BLOCKER if no PRD |
|
|
139
139
|
| `bmad-party-mode` | Skip — inherently interactive |
|
|
140
140
|
| `bmad-brainstorming` | Skip — inherently interactive |
|
|
141
|
+
| `bmad-retrospective` | Under autopilot, handled per `autopilot.retrospective_mode`: `auto` (default — inline artifact, no external skill call), `stop` (pause so user runs `/bmad-retrospective` interactively, then resumes autopilot), or `skip` (not recommended). The external skill is NOT invoked from autopilot because it enters a multi-persona discussion loop under some CLIs. |
|
|
141
142
|
|
|
142
143
|
---
|
|
143
144
|
|
|
@@ -173,13 +174,15 @@ Resolve:
|
|
|
173
174
|
</action>
|
|
174
175
|
<action>Read `{project-root}/_Sprintpilot/modules/autopilot/config.yaml` (if present) and set:
|
|
175
176
|
- `{{session_story_limit}}` from `autopilot.session_story_limit` (default: 3). A value of 0 disables the limit (run until sprint complete).
|
|
176
|
-
|
|
177
|
+
- `{{retrospective_mode}}` from `autopilot.retrospective_mode` (default: `auto`). Valid values: `auto` | `stop` | `skip`. Any unknown value falls back to `auto`.
|
|
178
|
+
If the file or either key is missing, fall back to the defaults above.
|
|
177
179
|
</action>
|
|
178
180
|
</check>
|
|
179
181
|
|
|
180
182
|
<check if="manifest does NOT exist">
|
|
181
183
|
<action>Set `{{git_enabled}}` = false</action>
|
|
182
184
|
<action>Set `{{session_story_limit}}` = 3</action>
|
|
185
|
+
<action>Set `{{retrospective_mode}}` = `auto`</action>
|
|
183
186
|
<action>Log: "No _Sprintpilot/manifest.yaml found — running stock autopilot (no git)"</action>
|
|
184
187
|
</check>
|
|
185
188
|
|
|
@@ -315,6 +318,34 @@ Resolve:
|
|
|
315
318
|
- Advance to next non-done story
|
|
316
319
|
- Update `{state_file}` with reconciled values
|
|
317
320
|
</action>
|
|
321
|
+
|
|
322
|
+
<!-- Resume from a `retrospective_mode: stop` pause.
|
|
323
|
+
The user was told to run /bmad-retrospective interactively. If they
|
|
324
|
+
did, the epic is now `done` in {status_file} (or a retrospective
|
|
325
|
+
artifact exists). Otherwise, re-issue the instructions and halt. -->
|
|
326
|
+
<check if="{state_file}.paused_at is epic-complete-awaiting-retrospective">
|
|
327
|
+
<action>Set `{{paused_epic_id}}` from `{state_file}.paused_epic_id`</action>
|
|
328
|
+
<action>Check whether epic `{{paused_epic_id}}` is now `done` in `{status_file}` OR an artifact exists at `{implementation_artifacts}/retrospectives/epic-{{paused_epic_id}}-*.md`</action>
|
|
329
|
+
<check if="epic is done OR retrospective artifact exists">
|
|
330
|
+
<action>Clear `paused_at`, `paused_epic_id`, and `next_action` from `{state_file}`</action>
|
|
331
|
+
<action>Log: "Epic {{paused_epic_id}} retrospective detected — resuming autopilot"</action>
|
|
332
|
+
</check>
|
|
333
|
+
<check if="epic is NOT done AND no retrospective artifact">
|
|
334
|
+
<action>Report:
|
|
335
|
+
```
|
|
336
|
+
Autopilot still paused — epic {{paused_epic_id}} retrospective not yet done.
|
|
337
|
+
|
|
338
|
+
Run `/bmad-retrospective` interactively for epic {{paused_epic_id}},
|
|
339
|
+
then re-run `/sprint-autopilot-on` to continue.
|
|
340
|
+
|
|
341
|
+
(To bypass, edit _Sprintpilot/modules/autopilot/config.yaml and set
|
|
342
|
+
retrospective_mode to `auto` or `skip`.)
|
|
343
|
+
```
|
|
344
|
+
</action>
|
|
345
|
+
<action>STOP</action>
|
|
346
|
+
</check>
|
|
347
|
+
</check>
|
|
348
|
+
|
|
318
349
|
<goto step="2">Jump to execution loop with reconciled state</goto>
|
|
319
350
|
</check>
|
|
320
351
|
|
|
@@ -369,6 +400,7 @@ Resolve:
|
|
|
369
400
|
Git integration: {{git_enabled}}
|
|
370
401
|
Platform: {{platform}}
|
|
371
402
|
Session limit: {{session_story_limit}} stories, then checkpoint + new session
|
|
403
|
+
Retrospective mode: {{retrospective_mode}}
|
|
372
404
|
|
|
373
405
|
Beginning autonomous execution. I will only stop for true blockers or session checkpoints.
|
|
374
406
|
```
|
|
@@ -586,8 +618,12 @@ pr_base: {{pr_base}}
|
|
|
586
618
|
<goto step="7">Mark story done</goto>
|
|
587
619
|
</check>
|
|
588
620
|
|
|
589
|
-
<check if="{{completed_skill}}
|
|
590
|
-
<action>Log: "Epic retrospective
|
|
621
|
+
<check if="{{completed_skill}} is retrospective-auto">
|
|
622
|
+
<action>Log: "Epic retrospective generated inline by autopilot — sprint-status.yaml updated"</action>
|
|
623
|
+
</check>
|
|
624
|
+
|
|
625
|
+
<check if="{{completed_skill}} is retrospective-skip">
|
|
626
|
+
<action>Log: "Epic retrospective skipped per config — sprint-status.yaml updated inline"</action>
|
|
591
627
|
</check>
|
|
592
628
|
|
|
593
629
|
<check if="{{completed_skill}} was bmad-create-epics-and-stories">
|
|
@@ -858,10 +894,111 @@ Instruct: "Re-verify code review for story {{current_story}} — all patch findi
|
|
|
858
894
|
|
|
859
895
|
<action>Check if ALL stories in this epic are `done`</action>
|
|
860
896
|
<check if="epic complete">
|
|
861
|
-
<action>
|
|
862
|
-
<action>
|
|
863
|
-
|
|
864
|
-
|
|
897
|
+
<action>Resolve `{{epic_id}}` (e.g. "1") and `{{epic_title}}` from `{status_file}` for the current epic</action>
|
|
898
|
+
<action>Create task "[epic {{epic_id}}] retrospective" → `in_progress`</action>
|
|
899
|
+
|
|
900
|
+
<!-- Retrospective handling is driven by `autopilot.retrospective_mode`
|
|
901
|
+
in modules/autopilot/config.yaml. See the SKILL AUTOMATABLE REFERENCE
|
|
902
|
+
table for rationale. The external `bmad-retrospective` skill is
|
|
903
|
+
NEVER invoked from autopilot — it enters a multi-persona discussion
|
|
904
|
+
loop under some CLIs. -->
|
|
905
|
+
|
|
906
|
+
<check if="{{retrospective_mode}} is auto">
|
|
907
|
+
<!-- Deterministic single-pass retrospective. No persona simulation,
|
|
908
|
+
no rounds, no external skill call. All inputs are on-disk. -->
|
|
909
|
+
<action>Collect from `{status_file}` for epic `{{epic_id}}`:
|
|
910
|
+
- list of done stories with { story-key, title, test_pass_count, patch_count }
|
|
911
|
+
- epic title, start/end dates if present
|
|
912
|
+
</action>
|
|
913
|
+
<action>Collect decision-log entries for epic `{{epic_id}}` from `{decision_log_file}` (match on `story` prefix `{{epic_id}}-` or `phase: autopilot:*` entries tagged to this epic)</action>
|
|
914
|
+
<action>Identify open risks / carry-over notes from sprint-status (any story with `notes` or `risks` fields, any `workaround` decisions in the log for this epic)</action>
|
|
915
|
+
<action>Ensure directory `{implementation_artifacts}/retrospectives/` exists</action>
|
|
916
|
+
<action>Write `{implementation_artifacts}/retrospectives/epic-{{epic_id}}-retrospective.md` using this template:
|
|
917
|
+
```markdown
|
|
918
|
+
# Epic {{epic_id}} — {{epic_title}} — Retrospective
|
|
919
|
+
|
|
920
|
+
**Completed:** {current_date}
|
|
921
|
+
**Stories done:** {{n_done}}/{{n_total}}
|
|
922
|
+
|
|
923
|
+
## Stories
|
|
924
|
+
{{#each stories}}
|
|
925
|
+
- **{{story-key}}** — {{title}}
|
|
926
|
+
- Tests: {{test_pass_count}}
|
|
927
|
+
- Patches applied: {{patch_count}}
|
|
928
|
+
{{/each}}
|
|
929
|
+
|
|
930
|
+
## Key decisions
|
|
931
|
+
{{#each decisions}}
|
|
932
|
+
- [{{impact}}] {{category}}: {{decision}} — {{rationale}}
|
|
933
|
+
{{/each}}
|
|
934
|
+
|
|
935
|
+
## Risks carried forward
|
|
936
|
+
{{#each open_risks}}
|
|
937
|
+
- {{risk}}
|
|
938
|
+
{{/each}}
|
|
939
|
+
|
|
940
|
+
## Notes
|
|
941
|
+
Generated inline by Sprintpilot autopilot per `autopilot.retrospective_mode: auto`.
|
|
942
|
+
```
|
|
943
|
+
</action>
|
|
944
|
+
<action>Update `{status_file}`:
|
|
945
|
+
- `epics.{{epic_id}}.status` = `done`
|
|
946
|
+
- `epics.{{epic_id}}.retrospective_path` = the retrospective file path (relative to project root)
|
|
947
|
+
- `epics.{{epic_id}}.completed_at` = {current_date}
|
|
948
|
+
</action>
|
|
949
|
+
<action>Append decision-log entry:
|
|
950
|
+
`{ category: workaround, decision: "retrospective generated inline", rationale: "autopilot.retrospective_mode=auto — avoids external skill's multi-persona loop", impact: low, phase: autopilot:retrospective, story: "epic-{{epic_id}}" }`
|
|
951
|
+
</action>
|
|
952
|
+
<action>Mark retrospective task → `completed`</action>
|
|
953
|
+
<action>Set `{{completed_skill}}` = `retrospective-auto`</action>
|
|
954
|
+
</check>
|
|
955
|
+
|
|
956
|
+
<check if="{{retrospective_mode}} is stop">
|
|
957
|
+
<!-- Pause autopilot so user can run /bmad-retrospective interactively.
|
|
958
|
+
On the next /sprint-autopilot-on the resume logic in step 1 will
|
|
959
|
+
detect the cleared state and move to the next epic. -->
|
|
960
|
+
<action>Update `{state_file}`:
|
|
961
|
+
- `paused_at` = `epic-complete-awaiting-retrospective`
|
|
962
|
+
- `paused_epic_id` = `{{epic_id}}`
|
|
963
|
+
- `next_action` = "run /bmad-retrospective interactively for epic {{epic_id}}, then re-run /sprint-autopilot-on"
|
|
964
|
+
</action>
|
|
965
|
+
<action>Append decision-log entry:
|
|
966
|
+
`{ category: workaround, decision: "paused for interactive retrospective", rationale: "autopilot.retrospective_mode=stop", impact: low, phase: autopilot:retrospective, story: "epic-{{epic_id}}" }`
|
|
967
|
+
</action>
|
|
968
|
+
<action>Mark retrospective task → `completed` (handed off to user)</action>
|
|
969
|
+
<action>Report:
|
|
970
|
+
```
|
|
971
|
+
Autopilot paused — epic {{epic_id}} complete, retrospective handed off.
|
|
972
|
+
|
|
973
|
+
Per `autopilot.retrospective_mode: stop` in
|
|
974
|
+
_Sprintpilot/modules/autopilot/config.yaml, autopilot does not run the
|
|
975
|
+
retrospective automatically.
|
|
976
|
+
|
|
977
|
+
To continue:
|
|
978
|
+
1. Run `/bmad-retrospective` interactively for epic {{epic_id}}
|
|
979
|
+
2. When done, run `/sprint-autopilot-on` to resume with the next epic
|
|
980
|
+
|
|
981
|
+
State saved to: {state_file}
|
|
982
|
+
```
|
|
983
|
+
</action>
|
|
984
|
+
<action>STOP</action>
|
|
985
|
+
</check>
|
|
986
|
+
|
|
987
|
+
<check if="{{retrospective_mode}} is skip">
|
|
988
|
+
<!-- User opted out of retrospective. Record and move on. -->
|
|
989
|
+
<action>Update `{status_file}`:
|
|
990
|
+
- `epics.{{epic_id}}.status` = `done`
|
|
991
|
+
- `epics.{{epic_id}}.retrospective_path` = null
|
|
992
|
+
- `epics.{{epic_id}}.retrospective_skipped` = true
|
|
993
|
+
- `epics.{{epic_id}}.completed_at` = {current_date}
|
|
994
|
+
</action>
|
|
995
|
+
<action>Append decision-log entry:
|
|
996
|
+
`{ category: workaround, decision: "retrospective skipped", rationale: "autopilot.retrospective_mode=skip (NOT RECOMMENDED)", impact: medium, phase: autopilot:retrospective, story: "epic-{{epic_id}}" }`
|
|
997
|
+
</action>
|
|
998
|
+
<action>Mark retrospective task → `completed` (skipped)</action>
|
|
999
|
+
<action>Set `{{completed_skill}}` = `retrospective-skip`</action>
|
|
1000
|
+
<action>Log: "Epic {{epic_id}} retrospective skipped per config — continuing with next epic"</action>
|
|
1001
|
+
</check>
|
|
865
1002
|
|
|
866
1003
|
<!-- GIT: Epic completion — suggest merge, cleanup worktrees -->
|
|
867
1004
|
<check if="{{git_enabled}}">
|
package/lib/commands/install.js
CHANGED
|
@@ -64,6 +64,9 @@ const { V1_ADDON_DIR_NAME, V1_SKILL_NAMES, detectV1Installation } = require('../
|
|
|
64
64
|
|
|
65
65
|
const ADDON_DIR = path.resolve(__dirname, '..', '..', '_Sprintpilot');
|
|
66
66
|
const PROJECT_ADDON_DIR_NAME = '_Sprintpilot';
|
|
67
|
+
const DEFAULT_SESSION_STORY_LIMIT = 3;
|
|
68
|
+
const DEFAULT_RETROSPECTIVE_MODE = 'auto';
|
|
69
|
+
const RETROSPECTIVE_MODES = ['auto', 'stop', 'skip'];
|
|
67
70
|
const RUNTIME_RESOURCES = [
|
|
68
71
|
'Sprintpilot.md',
|
|
69
72
|
'manifest.yaml',
|
|
@@ -603,6 +606,211 @@ async function evictV1Installation(projectRoot, { dryRun, migrateV1, yes }) {
|
|
|
603
606
|
return { migrated: true, moduleConfigSnapshot: snapshot };
|
|
604
607
|
}
|
|
605
608
|
|
|
609
|
+
// Parse the user's existing autopilot config (if any) so interactive prompts
|
|
610
|
+
// can default to the current values AND so a v1 migration's patcher run
|
|
611
|
+
// preserves user-edited values rather than overwriting them with bundled
|
|
612
|
+
// defaults.
|
|
613
|
+
//
|
|
614
|
+
// Checks both locations, in order of precedence:
|
|
615
|
+
// 1. `_Sprintpilot/modules/autopilot/config.yaml` — normal upgrade
|
|
616
|
+
// 2. `_bmad-addons/modules/autopilot/config.yaml` — v1 legacy (bmad-
|
|
617
|
+
// autopilot-addon), picked up BEFORE evictV1Installation moves it
|
|
618
|
+
//
|
|
619
|
+
// Regex-based so we don't add a YAML parser dep for two scalar fields.
|
|
620
|
+
// Unrecognized / unreadable files fall back to bundled defaults.
|
|
621
|
+
async function readExistingAutopilotConfig(projectRoot, v1Snapshot) {
|
|
622
|
+
const out = { sessionStoryLimit: null, retrospectiveMode: null };
|
|
623
|
+
let raw = null;
|
|
624
|
+
|
|
625
|
+
// Precedence order:
|
|
626
|
+
// 1. `_Sprintpilot/modules/autopilot/config.yaml` — normal upgrade
|
|
627
|
+
// 2. `_bmad-addons/modules/autopilot/config.yaml` — v1 legacy still
|
|
628
|
+
// on disk (install was invoked before evictV1Installation ran)
|
|
629
|
+
// 3. v1 in-memory snapshot — v1 legacy already
|
|
630
|
+
// evicted; evictV1Installation captured the buffer before removing
|
|
631
|
+
// the directory, so we extract the autopilot/config.yaml bytes
|
|
632
|
+
// from there. Without this the patcher would overwrite the user's
|
|
633
|
+
// v1-preserved values with bundled defaults.
|
|
634
|
+
const candidates = [
|
|
635
|
+
path.join(projectRoot, PROJECT_ADDON_DIR_NAME, 'modules', 'autopilot', 'config.yaml'),
|
|
636
|
+
path.join(projectRoot, V1_ADDON_DIR_NAME, 'modules', 'autopilot', 'config.yaml'),
|
|
637
|
+
];
|
|
638
|
+
for (const file of candidates) {
|
|
639
|
+
if (!(await fs.pathExists(file))) continue;
|
|
640
|
+
try {
|
|
641
|
+
raw = await fs.readFile(file, 'utf8');
|
|
642
|
+
break;
|
|
643
|
+
} catch {
|
|
644
|
+
/* try next candidate */
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
if (raw == null && v1Snapshot && Array.isArray(v1Snapshot.autopilot)) {
|
|
649
|
+
const entry = v1Snapshot.autopilot.find((f) => f.relPath === 'config.yaml');
|
|
650
|
+
if (entry && Buffer.isBuffer(entry.buffer)) {
|
|
651
|
+
try {
|
|
652
|
+
raw = entry.buffer.toString('utf8');
|
|
653
|
+
} catch {
|
|
654
|
+
/* unreadable — fall back to defaults */
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (raw == null) return out;
|
|
660
|
+
|
|
661
|
+
// Both patterns tolerate a trailing `# comment` tail so users can annotate
|
|
662
|
+
// their config without breaking upgrade detection (e.g.
|
|
663
|
+
// `retrospective_mode: stop # we want manual retros`). `[ \t]` rather
|
|
664
|
+
// than `\s` inside each line so matches never cross a newline.
|
|
665
|
+
const commentTail = /[ \t]*(?:#.*)?$/.source;
|
|
666
|
+
|
|
667
|
+
// `session_story_limit: 3` — unquoted integer, optional trailing comment
|
|
668
|
+
const limitMatch = raw.match(
|
|
669
|
+
new RegExp(`^[ \\t]*session_story_limit:[ \\t]*(\\d+)${commentTail}`, 'm'),
|
|
670
|
+
);
|
|
671
|
+
if (limitMatch) {
|
|
672
|
+
const n = Number.parseInt(limitMatch[1], 10);
|
|
673
|
+
if (Number.isFinite(n) && n >= 0) out.sessionStoryLimit = n;
|
|
674
|
+
}
|
|
675
|
+
// `retrospective_mode: auto` — unquoted or single/double-quoted string,
|
|
676
|
+
// optional trailing comment
|
|
677
|
+
const modeMatch = raw.match(
|
|
678
|
+
new RegExp(`^[ \\t]*retrospective_mode:[ \\t]*["']?([a-zA-Z_-]+)["']?${commentTail}`, 'm'),
|
|
679
|
+
);
|
|
680
|
+
if (modeMatch && RETROSPECTIVE_MODES.includes(modeMatch[1])) {
|
|
681
|
+
out.retrospectiveMode = modeMatch[1];
|
|
682
|
+
}
|
|
683
|
+
return out;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// Rewrite the `session_story_limit:` and `retrospective_mode:` scalar lines
|
|
687
|
+
// in the freshly-copied autopilot/config.yaml with the user's chosen values.
|
|
688
|
+
// Done as a line-level string replacement instead of a token-substitution
|
|
689
|
+
// because workflow.md uses the `{{session_story_limit}}` / `{{retrospective_mode}}`
|
|
690
|
+
// variable-reference syntax — threading these keys through `renderString`
|
|
691
|
+
// would match the inner `{key}` inside `{{key}}` and corrupt the workflow
|
|
692
|
+
// variables to literal `{value}` strings.
|
|
693
|
+
//
|
|
694
|
+
// Handles three shapes for each key:
|
|
695
|
+
// 1. line present with a value → in-place replace (preserves trailing comment)
|
|
696
|
+
// 2. line present with no value → in-place fill
|
|
697
|
+
// 3. line missing → append to the `autopilot:` block
|
|
698
|
+
// (3) is the path that catches v1 (bmad-autopilot-addon) migrations whose
|
|
699
|
+
// legacy config predates `retrospective_mode` — without it, the user's
|
|
700
|
+
// prompted choice would be silently dropped.
|
|
701
|
+
function applyScalar(source, key, value) {
|
|
702
|
+
// Structure of a YAML scalar line we support:
|
|
703
|
+
// [indent][key]:[space(s)][value][space(s) + # comment]?
|
|
704
|
+
//
|
|
705
|
+
// Groups, non-greedy on value so the trailing-comment capture wins:
|
|
706
|
+
// indent — leading whitespace (preserved verbatim)
|
|
707
|
+
// value — `\S[^#\n]*?` — starts non-ws, stops as early as possible so
|
|
708
|
+
// the comment tail can match. Optional, so this also matches
|
|
709
|
+
// "key:\n" (no value at all) — P1-C.
|
|
710
|
+
// tail — `\s*#.*` — the whitespace-before-# is INSIDE the tail capture
|
|
711
|
+
// so the original spacing ("value # note") round-trips
|
|
712
|
+
// instead of collapsing to a single space.
|
|
713
|
+
// Using [ \t] instead of \s everywhere INSIDE the line so the match
|
|
714
|
+
// cannot cross a newline — `\s*` would greedily consume the trailing
|
|
715
|
+
// `\n` after "key:" on an empty-value line, breaking the rewrite.
|
|
716
|
+
const replaceRe = new RegExp(`^([ \\t]*)${key}:[ \\t]*(\\S[^#\\n]*?)?([ \\t]*#.*)?$`, 'm');
|
|
717
|
+
if (replaceRe.test(source)) {
|
|
718
|
+
return source.replace(replaceRe, (_m, indent, _oldValue, tail) => {
|
|
719
|
+
return `${indent}${key}: ${value}${tail || ''}`;
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// Key is absent. Append under the `autopilot:` block at the file's end.
|
|
724
|
+
// The bundled shipping config always starts with `autopilot:` at column
|
|
725
|
+
// 0 and indents children two spaces — match that style. If the file has
|
|
726
|
+
// no `autopilot:` header at all (hand-edited to a different shape), bail
|
|
727
|
+
// rather than guess.
|
|
728
|
+
if (!/^autopilot:\s*$/m.test(source)) return source;
|
|
729
|
+
const trimmed = source.endsWith('\n') ? source : `${source}\n`;
|
|
730
|
+
return `${trimmed} ${key}: ${value}\n`;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
async function patchAutopilotConfig(projectRoot, { sessionStoryLimit, retrospectiveMode }) {
|
|
734
|
+
const file = path.join(
|
|
735
|
+
projectRoot,
|
|
736
|
+
PROJECT_ADDON_DIR_NAME,
|
|
737
|
+
'modules',
|
|
738
|
+
'autopilot',
|
|
739
|
+
'config.yaml',
|
|
740
|
+
);
|
|
741
|
+
if (!(await fs.pathExists(file))) return;
|
|
742
|
+
const original = await fs.readFile(file, 'utf8');
|
|
743
|
+
let updated = applyScalar(original, 'session_story_limit', sessionStoryLimit);
|
|
744
|
+
updated = applyScalar(updated, 'retrospective_mode', retrospectiveMode);
|
|
745
|
+
if (updated !== original) {
|
|
746
|
+
await writeAtomic(file, updated);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
async function resolveAutopilotSettings({ projectRoot, yes, dryRun, v1Snapshot }) {
|
|
751
|
+
const existing = await readExistingAutopilotConfig(projectRoot, v1Snapshot);
|
|
752
|
+
const defaultLimit = existing.sessionStoryLimit ?? DEFAULT_SESSION_STORY_LIMIT;
|
|
753
|
+
const defaultMode = existing.retrospectiveMode ?? DEFAULT_RETROSPECTIVE_MODE;
|
|
754
|
+
|
|
755
|
+
if (yes) {
|
|
756
|
+
if (existing.sessionStoryLimit != null || existing.retrospectiveMode != null) {
|
|
757
|
+
console.log(
|
|
758
|
+
pc.dim(
|
|
759
|
+
`Preserving autopilot config: session_story_limit=${defaultLimit}, retrospective_mode=${defaultMode}`,
|
|
760
|
+
),
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
return { sessionStoryLimit: defaultLimit, retrospectiveMode: defaultMode };
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
if (dryRun) {
|
|
767
|
+
console.log(
|
|
768
|
+
pc.dim(
|
|
769
|
+
`[DRY RUN] Would prompt for autopilot config (current: session_story_limit=${defaultLimit}, retrospective_mode=${defaultMode})`,
|
|
770
|
+
),
|
|
771
|
+
);
|
|
772
|
+
return { sessionStoryLimit: defaultLimit, retrospectiveMode: defaultMode };
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
const limitRaw = await prompts.text({
|
|
776
|
+
message: 'Autopilot: stories to fully implement per session (0 = unlimited)',
|
|
777
|
+
initialValue: String(defaultLimit),
|
|
778
|
+
validate(value) {
|
|
779
|
+
if (value == null || value === '') return undefined;
|
|
780
|
+
const n = Number.parseInt(value, 10);
|
|
781
|
+
if (!Number.isFinite(n) || String(n) !== String(value).trim() || n < 0) {
|
|
782
|
+
return 'Enter a non-negative integer (0 = unlimited)';
|
|
783
|
+
}
|
|
784
|
+
return undefined;
|
|
785
|
+
},
|
|
786
|
+
});
|
|
787
|
+
const sessionStoryLimit =
|
|
788
|
+
limitRaw == null || String(limitRaw).trim() === ''
|
|
789
|
+
? defaultLimit
|
|
790
|
+
: Number.parseInt(String(limitRaw).trim(), 10);
|
|
791
|
+
|
|
792
|
+
const retrospectiveMode = await prompts.select({
|
|
793
|
+
message: 'Autopilot: retrospective handling at epic completion',
|
|
794
|
+
options: [
|
|
795
|
+
{
|
|
796
|
+
value: 'auto',
|
|
797
|
+
label: 'Auto — autopilot generates retrospective inline and continues (recommended)',
|
|
798
|
+
},
|
|
799
|
+
{
|
|
800
|
+
value: 'stop',
|
|
801
|
+
label: 'Stop — pause autopilot so you can run /bmad-retrospective manually',
|
|
802
|
+
},
|
|
803
|
+
{
|
|
804
|
+
value: 'skip',
|
|
805
|
+
label: 'Skip — do not generate a retrospective (NOT RECOMMENDED)',
|
|
806
|
+
},
|
|
807
|
+
],
|
|
808
|
+
initialValue: defaultMode,
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
return { sessionStoryLimit, retrospectiveMode };
|
|
812
|
+
}
|
|
813
|
+
|
|
606
814
|
async function runInteractiveToolPicker(detected) {
|
|
607
815
|
const options = ALL_TOOLS.map((tool) => ({
|
|
608
816
|
value: tool,
|
|
@@ -665,12 +873,24 @@ async function runInstall(options = {}) {
|
|
|
665
873
|
|
|
666
874
|
// 2. Resolve output_folder
|
|
667
875
|
const outputFolder = await readOutputFolder(projectRoot);
|
|
668
|
-
const ctx = buildContext({ outputFolder });
|
|
669
876
|
if (outputFolder !== '_bmad-output') {
|
|
670
877
|
console.log(pc.dim(`Using output_folder: ${outputFolder}`));
|
|
671
878
|
console.log('');
|
|
672
879
|
}
|
|
673
880
|
|
|
881
|
+
// 2a. Autopilot configuration (prompt or preserve existing values).
|
|
882
|
+
// These values are patched into modules/autopilot/config.yaml AFTER the
|
|
883
|
+
// runtime copy — they're NOT threaded through `renderString`, because
|
|
884
|
+
// workflow.md's `{{session_story_limit}}` / `{{retrospective_mode}}`
|
|
885
|
+
// variable references would collide with single-brace token matching.
|
|
886
|
+
const { sessionStoryLimit, retrospectiveMode } = await resolveAutopilotSettings({
|
|
887
|
+
projectRoot,
|
|
888
|
+
yes,
|
|
889
|
+
dryRun,
|
|
890
|
+
v1Snapshot: v1ConfigSnapshot,
|
|
891
|
+
});
|
|
892
|
+
const ctx = buildContext({ outputFolder });
|
|
893
|
+
|
|
674
894
|
// 3. Detect + select tools
|
|
675
895
|
const detected = await detectInstalledTools(projectRoot);
|
|
676
896
|
|
|
@@ -888,6 +1108,12 @@ async function runInstall(options = {}) {
|
|
|
888
1108
|
// .sprintpilot-v1-snapshot*.json were added up-front in
|
|
889
1109
|
// evictV1Installation (step 0) so they're in place even if the
|
|
890
1110
|
// module-snapshot branch never runs.
|
|
1111
|
+
|
|
1112
|
+
// 6b. Apply the resolved autopilot settings. Runs AFTER step 6 (which
|
|
1113
|
+
// wrote the bundled default config) AND after the v1 snapshot
|
|
1114
|
+
// reapply (which might have restored an older config.yaml without
|
|
1115
|
+
// `retrospective_mode`). The user's prompted values always win.
|
|
1116
|
+
await patchAutopilotConfig(projectRoot, { sessionStoryLimit, retrospectiveMode });
|
|
891
1117
|
}
|
|
892
1118
|
|
|
893
1119
|
// 7. Verify git check-ignore
|
|
@@ -936,8 +1162,15 @@ async function runInstall(options = {}) {
|
|
|
936
1162
|
console.log(' multi_agent.max_parallel_analysis 5 Codebase analysis agents');
|
|
937
1163
|
console.log('');
|
|
938
1164
|
console.log(' _Sprintpilot/modules/autopilot/config.yaml');
|
|
1165
|
+
// padEnd so the description column stays aligned across both rows,
|
|
1166
|
+
// independent of the chosen values' widths (e.g. 2-digit limit, 4-char mode).
|
|
1167
|
+
const apKey = (k) => k.padEnd(31, ' ');
|
|
1168
|
+
const apVal = (v) => String(v).padEnd(6, ' ');
|
|
1169
|
+
console.log(
|
|
1170
|
+
` ${apKey('autopilot.session_story_limit')}${apVal(sessionStoryLimit)} Stories to fully implement per run (0 = unlimited)`,
|
|
1171
|
+
);
|
|
939
1172
|
console.log(
|
|
940
|
-
'
|
|
1173
|
+
` ${apKey('autopilot.retrospective_mode')}${apVal(retrospectiveMode)} Epic-end retrospective: auto (inline) | stop (pause) | skip (not recommended)`,
|
|
941
1174
|
);
|
|
942
1175
|
console.log('');
|
|
943
1176
|
console.log('Multi-agent skills — run parallel subagents for faster analysis:');
|
|
@@ -963,4 +1196,15 @@ async function runInstall(options = {}) {
|
|
|
963
1196
|
console.log('Apache 2.0 License — Igor Kunin — https://github.com/ikunin/sprintpilot');
|
|
964
1197
|
}
|
|
965
1198
|
|
|
966
|
-
module.exports = {
|
|
1199
|
+
module.exports = {
|
|
1200
|
+
runInstall,
|
|
1201
|
+
// Exported for unit tests only — do not depend on this surface elsewhere.
|
|
1202
|
+
_internals: {
|
|
1203
|
+
readExistingAutopilotConfig,
|
|
1204
|
+
patchAutopilotConfig,
|
|
1205
|
+
applyScalar,
|
|
1206
|
+
RETROSPECTIVE_MODES,
|
|
1207
|
+
DEFAULT_SESSION_STORY_LIMIT,
|
|
1208
|
+
DEFAULT_RETROSPECTIVE_MODE,
|
|
1209
|
+
},
|
|
1210
|
+
};
|
package/lib/prompts.js
CHANGED
|
@@ -27,6 +27,26 @@ async function confirm(opts) {
|
|
|
27
27
|
return result;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
async function text(opts) {
|
|
31
|
+
const clack = await loadClack();
|
|
32
|
+
const result = await clack.text(opts);
|
|
33
|
+
if (clack.isCancel(result)) {
|
|
34
|
+
clack.cancel('Cancelled');
|
|
35
|
+
process.exit(0);
|
|
36
|
+
}
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function select(opts) {
|
|
41
|
+
const clack = await loadClack();
|
|
42
|
+
const result = await clack.select(opts);
|
|
43
|
+
if (clack.isCancel(result)) {
|
|
44
|
+
clack.cancel('Cancelled');
|
|
45
|
+
process.exit(0);
|
|
46
|
+
}
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
|
|
30
50
|
async function intro(message) {
|
|
31
51
|
const clack = await loadClack();
|
|
32
52
|
clack.intro(message);
|
|
@@ -76,5 +96,7 @@ module.exports = {
|
|
|
76
96
|
note,
|
|
77
97
|
multiselect,
|
|
78
98
|
confirm,
|
|
99
|
+
text,
|
|
100
|
+
select,
|
|
79
101
|
log,
|
|
80
102
|
};
|
package/lib/substitute.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ikunin/sprintpilot",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
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": {
|
|
@@ -33,6 +33,9 @@
|
|
|
33
33
|
"engines": {
|
|
34
34
|
"node": ">=18.0.0"
|
|
35
35
|
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"prepare": "node scripts/setup-git-hooks.mjs"
|
|
38
|
+
},
|
|
36
39
|
"dependencies": {
|
|
37
40
|
"@clack/core": "^1.0.0",
|
|
38
41
|
"@clack/prompts": "^1.0.0",
|