@ikunin/sprintpilot 1.0.2 → 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.
@@ -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
  ---
@@ -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
- ' autopilot.session_story_limit 3 Stories to fully implement per run (0 = unlimited)',
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 = { runInstall };
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
@@ -19,7 +19,7 @@ function renderString(text, ctx) {
19
19
  return out;
20
20
  }
21
21
 
22
- function buildContext({ outputFolder }) {
22
+ function buildContext({ outputFolder } = {}) {
23
23
  const out = outputFolder || '_bmad-output';
24
24
  return {
25
25
  output_folder: out,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ikunin/sprintpilot",
3
- "version": "1.0.2",
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",