@ikunin/sprintpilot 1.0.5 → 2.0.5

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 (35) hide show
  1. package/README.md +48 -1
  2. package/_Sprintpilot/Sprintpilot.md +14 -1
  3. package/_Sprintpilot/manifest.yaml +1 -1
  4. package/_Sprintpilot/modules/autopilot/config.yaml +22 -0
  5. package/_Sprintpilot/modules/autopilot/profiles/_base.yaml +45 -0
  6. package/_Sprintpilot/modules/autopilot/profiles/large.yaml +22 -0
  7. package/_Sprintpilot/modules/autopilot/profiles/legacy.yaml +35 -0
  8. package/_Sprintpilot/modules/autopilot/profiles/medium.yaml +5 -0
  9. package/_Sprintpilot/modules/autopilot/profiles/nano.yaml +35 -0
  10. package/_Sprintpilot/modules/autopilot/profiles/small.yaml +5 -0
  11. package/_Sprintpilot/modules/git/config.yaml +8 -0
  12. package/_Sprintpilot/modules/ma/config.yaml +42 -0
  13. package/_Sprintpilot/scripts/agent-adapter.js +247 -0
  14. package/_Sprintpilot/scripts/cached-read.js +238 -0
  15. package/_Sprintpilot/scripts/check-prereqs.js +139 -0
  16. package/_Sprintpilot/scripts/dispatch-layer.js +192 -0
  17. package/_Sprintpilot/scripts/git-portable.js +219 -0
  18. package/_Sprintpilot/scripts/infer-dependencies.js +594 -0
  19. package/_Sprintpilot/scripts/inject-tasks-section.js +279 -0
  20. package/_Sprintpilot/scripts/list-remaining-stories.js +295 -0
  21. package/_Sprintpilot/scripts/log-timing.js +425 -0
  22. package/_Sprintpilot/scripts/mark-done-stories-tasks.js +254 -0
  23. package/_Sprintpilot/scripts/merge-shards.js +339 -0
  24. package/_Sprintpilot/scripts/preflight-merge.js +235 -0
  25. package/_Sprintpilot/scripts/resolve-dag.js +559 -0
  26. package/_Sprintpilot/scripts/resolve-profile.js +355 -0
  27. package/_Sprintpilot/scripts/state-shard.js +602 -0
  28. package/_Sprintpilot/scripts/submodule-lock.js +130 -0
  29. package/_Sprintpilot/scripts/summarize-timings.js +362 -0
  30. package/_Sprintpilot/scripts/sync-status.js +13 -0
  31. package/_Sprintpilot/scripts/with-retry.js +145 -0
  32. package/_Sprintpilot/skills/sprint-autopilot-on/workflow.md +572 -42
  33. package/bin/sprintpilot.js +4 -0
  34. package/lib/commands/install.js +157 -1
  35. package/package.json +1 -1
package/README.md CHANGED
@@ -197,6 +197,32 @@ Output files:
197
197
 
198
198
  ---
199
199
 
200
+ ## Adaptive Process Scaling (v2)
201
+
202
+ Sprintpilot v2 introduced **complexity profiles** as a first-class config dimension. The right amount of process for a 2-story bug-fix sprint is different from a 30-story green-field rebuild — and the cost of running the heavy flow on a small change is real (more LLM turns, more context rot, more time). One knob picks the right balance:
203
+
204
+ | Profile | Per-story flow | Branching | Worktrees | Parallel stories | Use it for |
205
+ |---------|---------------|-----------|-----------|------------------|-----------|
206
+ | `nano` | `bmad-quick-dev` (one-shot) | `epic` (one PR per epic) | off | n/a | Tiny patch sprints, hot-fix runs |
207
+ | `small` | Full 7-step BMad cycle | `story` (one PR per story) | on | off | Single-developer projects, ≤10 stories |
208
+ | `medium` *(default)* | Full 7-step BMad cycle | `story` | on | off | Default — balanced for most sprints |
209
+ | `large` | Full 7-step BMad cycle | `story` | on | **on** (Claude Code) | Multi-epic sprints, 20+ stories |
210
+ | `legacy` | Pinned to v1.0.5 behavior byte-for-byte | `story` | on | off | Existing installs that want zero behavior change |
211
+
212
+ Pick the profile at install time — interactive installer asks, non-interactive flag is `--profile <nano|small|medium|large|legacy>`. Missing profile defaults to `medium` with no behavior change vs. v1.0.5.
213
+
214
+ **One knob per feature** — every v2 optimization layer can be disabled in isolation without uninstalling. See [Configuration Reference](docs/CONFIGURATION.md#autopilot-configuration-modulesautopilotconfigyaml).
215
+
216
+ ### What v2 ships on top of the core flow
217
+
218
+ - **Phase timing instrumentation** — `mark` action emits `duration` records per skill phase; auto-emitted on critical paths (no LLM bracket calls to skip). `summarize-timings.js` reports hotspots > 5% of total time.
219
+ - **State sharding** — non-critical writes accumulate in `.pending/` shards, flushed atomically at story boundaries / session checkpoints / sprint complete. Crash-recovery keys still write straight through.
220
+ - **Conditional boot work** — clean-repo sessions skip the slow health-check / branch-reconciliation block (saves 8–30s per session).
221
+ - **Cached reads** — TTL + source-mtime aware file cache; any writer's mtime advance forces a miss without explicit invalidate.
222
+ - **Auto-inferred story DAG** — autopilot infers inter-story dependencies once after `bmad-sprint-planning` and writes `_Sprintpilot/sprints/dependencies.yaml` with an `# AUTO-INFERRED` marker. Hand-authored files are detected and respected silently.
223
+ - **Parallel story dispatch** — when `parallel_stories: true` and the host supports it, layer-aware dispatch runs N stories concurrently in their own worktrees, then merges their state shards. Claude Code today; Gemini CLI experimentally.
224
+ - **Cross-platform** — every workflow.md call site runs under bash, zsh, Git Bash, PowerShell, and cmd. Portable Node.js helpers replace POSIX-shell idioms.
225
+
200
226
  ## Quick Start
201
227
 
202
228
  ```bash
@@ -215,9 +241,12 @@ npx bmad-method install
215
241
  ```
216
242
 
217
243
  ```bash
218
- # 2. Install Sprintpilot (interactive — select your tool when prompted)
244
+ # 2. Install Sprintpilot (interactive — select your tool and complexity profile when prompted)
219
245
  npx @ikunin/sprintpilot@latest
220
246
 
247
+ # 2b. Or pick the profile non-interactively
248
+ npx @ikunin/sprintpilot@latest install --tools claude-code --profile medium --yes
249
+
221
250
  # 3. Start the autopilot in your IDE
222
251
  /sprint-autopilot-on
223
252
  ```
@@ -298,6 +327,19 @@ All settings live in two YAML files — edit after install to customize behavior
298
327
  | `git.lock.stale_timeout_minutes` | `30` | Auto-remove orphaned lock files |
299
328
  | `git.worktree.cleanup_on_merge` | `true` | Delete worktrees after merge |
300
329
 
330
+ ### Autopilot (`_Sprintpilot/modules/autopilot/config.yaml`)
331
+
332
+ | Setting | Default (medium) | Description |
333
+ |---------|------------------|-------------|
334
+ | `complexity_profile` | `medium` | One of `nano`, `small`, `medium`, `large`, `legacy`. Selects the per-story flow + which v2 layers are enabled. |
335
+ | `autopilot.session_story_limit` | `3` (nano: `5`) | Stories per session before checkpoint. `0` = unlimited. |
336
+ | `autopilot.retrospective_mode` | `auto` | `auto` (deterministic artifact) / `stop` (pause for `/bmad-retrospective`) / `skip`. |
337
+ | `autopilot.auto_infer_dependencies` | `true` (nano + legacy: `false`) | Infer story DAG once after `bmad-sprint-planning`. Hand-authored sidecars (no `# AUTO-INFERRED` marker) are respected silently. |
338
+ | `autopilot.phase_timings` | `true` (legacy: `false`) | Emit phase duration records via `log-timing.js mark`. |
339
+ | `autopilot.coalesce_state_writes` | `true` (legacy: `false`) | Buffer non-critical state in `.pending/` shards. |
340
+ | `autopilot.conditional_boot_work` | `true` (large + legacy: `false`) | Skip health-check / branch-reconciliation on clean repos. |
341
+ | `autopilot.cache_shared_reads` | `true` (legacy: `false`) | TTL + mtime-aware file cache for hot reads. |
342
+
301
343
  ### Multi-Agent (`_Sprintpilot/modules/ma/config.yaml`)
302
344
 
303
345
  | Setting | Default | Description |
@@ -305,6 +347,11 @@ All settings live in two YAML files — edit after install to customize behavior
305
347
  | `multi_agent.enabled` | `true` | Enable parallel agent skills |
306
348
  | `multi_agent.max_parallel_research` | `3` | Concurrent research agents per batch |
307
349
  | `multi_agent.max_parallel_analysis` | `5` | Concurrent codebase analysis agents |
350
+ | `ma.state_sharding` | `auto` (large: `always`) | `auto`, `always`, `never` — shards per-story state instead of contending on root YAMLs. |
351
+ | `ma.parallel_stories` | `false` (large: `true`) | Dispatch independent stories from a DAG layer concurrently. Requires Claude Code (or Gemini CLI w/ experimental flag). |
352
+ | `ma.max_parallel_stories` | `2` (large: `3`) | Cap on concurrent stories per layer. |
353
+ | `ma.experimental_parallel_on_gemini` | `false` | Opt-in parallel dispatch under Gemini CLI (worktree-scoped subagents are still upstream). |
354
+ | `ma.parallel_epics` | `false` | EXPERIMENTAL — cross-epic parallelism with merge-conflict preflight. Off on every profile by default. |
308
355
 
309
356
  See the [Configuration Reference](docs/CONFIGURATION.md) for the full list.
310
357
 
@@ -31,8 +31,9 @@ Edit `_Sprintpilot/modules/autopilot/config.yaml`:
31
31
 
32
32
  | Setting | Default | Values | Purpose |
33
33
  |---------|---------|--------|---------|
34
- | `autopilot.session_story_limit` | `3` | integer ≥ 0 | Stories fully implemented per autopilot run before checkpoint. `0` = unlimited. |
34
+ | `autopilot.session_story_limit` | `3` (nano: `5`) | integer ≥ 0 | Stories fully implemented per autopilot run before checkpoint. `0` = unlimited. Retuned in 2.0.1 after context-rot exposure on longer sessions; nano is cheaper per story so fits a higher cap. |
35
35
  | `autopilot.retrospective_mode` | `auto` | `auto` / `stop` / `skip` | How epic-end retrospectives are handled (see below). |
36
+ | `autopilot.auto_infer_dependencies` | `true` (nano + legacy: `false`) | bool | 2.0.2 — autopilot session infers inter-story DAG once after `bmad-sprint-planning` and writes `_Sprintpilot/sprints/dependencies.yaml`. Hand-authored sidecars (no `# AUTO-INFERRED` marker) are detected and respected. See "Dependency Inference" below. |
36
37
 
37
38
  `retrospective_mode` options:
38
39
  - **`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.
@@ -41,6 +42,18 @@ Edit `_Sprintpilot/modules/autopilot/config.yaml`:
41
42
 
42
43
  Both settings are prompted during `sprintpilot install` (interactive mode) with existing values as defaults, so reinstalls preserve your choices.
43
44
 
45
+ ### Dependency Inference
46
+
47
+ After `bmad-sprint-planning` completes, the autopilot session reads `epics.md`, `architecture.md`, and `sprint-status.yaml` and emits a JSON dependency envelope. `_Sprintpilot/scripts/infer-dependencies.js` validates it (schema, unknown keys, self-deps, cross-epic edges, missing rationales, cycles) and writes `_Sprintpilot/sprints/dependencies.yaml` with an `# AUTO-INFERRED` marker header. The script never calls an LLM — the autopilot session is the inference caller.
48
+
49
+ This unblocks parallel story dispatch (`parallel_stories: true` + `dispatch-layer.js`) without requiring users to discover and hand-author the sidecar. Hand-authored files (no marker) are respected silently. Failure modes (invalid JSON, validation errors) log and continue — `resolve-dag.js` falls back to its safe linear `ordering` strategy on dispatch.
50
+
51
+ ### Mandatory fresh-context finalize
52
+
53
+ Independent of `session_story_limit`, the autopilot forces an extra session at end-of-sprint. When step 2 detects all stories are done, it writes `current_bmad_step = sprint-finalize-pending` to the state file and halts — it does **not** run step 10 (cleanup) in that session. The next `/sprint-autopilot-on` invocation reads the marker in step 1 and jumps directly to step 10 with a clean context window, where seven CRITICAL deterministic script calls run the cleanup (checkbox marking, worktree removal, lock release, artifact commit, sprint-complete state, verification, state-file delete).
54
+
55
+ This behavior is not configurable: it's a mitigation for late-session instruction decay that was reliably dropping cleanup actions in long single-session runs. The extra session is short (typically ~60-100 turns, under $2). See `_Sprintpilot/skills/sprint-autopilot-on/workflow.md` step 2 and step 10 for the exact protocol.
56
+
44
57
  ---
45
58
 
46
59
  ## Full skill reference by lifecycle phase
@@ -1,6 +1,6 @@
1
1
  addon:
2
2
  name: sprintpilot
3
- version: 1.0.5
3
+ version: 2.0.5
4
4
  description: Sprintpilot — autopilot and multi-agent addon for BMad Method (git workflow, parallel agents, autonomous story execution)
5
5
  bmad_compatibility: ">=6.2.0"
6
6
  modules:
@@ -2,6 +2,28 @@
2
2
  # Controls /sprint-autopilot-on session behavior.
3
3
 
4
4
  autopilot:
5
+ # Adaptive Process Scaling profile.
6
+ #
7
+ # nano — toy / tutorial / learning project, solo, small codebase.
8
+ # Routes through bmad-quick-dev one-shot (PR 4+). Fastest.
9
+ # small — MVP / internal tool / prototype, solo or 1–2 devs.
10
+ # medium — team product with real users. Default, recommended.
11
+ # large — production system, compliance / uptime stakes. Full
12
+ # reconciliation, interactive retrospectives, tighter
13
+ # checkpoints, parallelism on by default.
14
+ # legacy — rollback to v1.0.5 behavior. No v2 optimizations active.
15
+ # Pinned by version_pinned in profiles/legacy.yaml so future
16
+ # changes cannot silently affect legacy users.
17
+ #
18
+ # Profile defaults live in _Sprintpilot/modules/autopilot/profiles/*.yaml.
19
+ # Override specific knobs here or in git/ma config.yaml — user overrides
20
+ # always win over profile defaults. See docs/adaptive-process-scaling.md.
21
+ #
22
+ # Missing key defaults to 'medium' (matches v1.0.5 behavior byte-for-byte).
23
+ # Upgrading v1 installs don't need to set this; new installs get it from
24
+ # the installer prompt or `sprintpilot install --profile <name>`.
25
+ complexity_profile: medium
26
+
5
27
  # Maximum stories to FULLY IMPLEMENT in a single autopilot session.
6
28
  #
7
29
  # "Fully implement" means the complete per-story cycle:
@@ -0,0 +1,45 @@
1
+ # Adaptive Process Scaling — base defaults for all non-legacy profiles.
2
+ # Per-profile YAML files override only the keys they want to change.
3
+ # See docs/adaptive-process-scaling.md and docs/implementation-plan.md.
4
+
5
+ name: _base
6
+ version_pinned: null
7
+
8
+ autopilot:
9
+ implementation_flow: full # full | quick
10
+ # Proactive handoff threshold. Lowered from 5 → 3 because context rot
11
+ # (late-session instruction decay) caused the LLM to skip late-step
12
+ # actions in longer single-session runs. Keeping sessions short enough
13
+ # that every step 10 CRITICAL action fits comfortably inside the prompt
14
+ # budget is cheaper than paying for skipped cleanup actions.
15
+ session_story_limit: 3
16
+ retrospective_mode: auto
17
+ phase_timings: true # M0, PR 2 — enabled by default (legacy overrides to false)
18
+ coalesce_state_writes: true # M3, PR 6 — batch non-critical writes, flush at story boundary
19
+ conditional_boot_work: true # M4, PR 7 — skip health-check + branch reconciliation on a clean repo
20
+ cache_shared_reads: true # M5, PR 8 — memoize sprint-status / git-status / decision-log reads per loop iteration
21
+ # 2.0.2 — autopilot session infers inter-story DAG once after bmad-sprint-planning
22
+ # and writes _Sprintpilot/sprints/dependencies.yaml. Hand-authored sidecars
23
+ # (no AUTO-INFERRED marker) are detected and respected.
24
+ auto_infer_dependencies: true
25
+
26
+ git:
27
+ granularity: story # story | epic
28
+ worktree:
29
+ enabled: true
30
+ squash_on_merge: false
31
+
32
+ ma:
33
+ parallel_stories: false
34
+ max_parallel_stories: 2
35
+ # PR 11 intra-epic parallelism safety rails. Honored by dispatch-layer.js
36
+ # when a host declares parallel support (see agent-adapter.js).
37
+ min_epic_duration_for_parallel_sec: 300
38
+ baseline_story_duration_sec: 180
39
+ max_consecutive_conflicts: 2
40
+ effective_parallel_floor: 1
41
+ parallel_epics: false
42
+ # PR 3: state_sharding semantics — auto = shard when parallelism is active,
43
+ # always = shard unconditionally (for forcing the code path in tests),
44
+ # never = write authoritative files directly (pre-PR behavior, rollback).
45
+ state_sharding: auto
@@ -0,0 +1,22 @@
1
+ # Large profile — production system, compliance / uptime stakes.
2
+ # Parallelism on by default; full reconciliation every boot; tighter
3
+ # session checkpointing; interactive retrospectives.
4
+
5
+ name: large
6
+
7
+ autopilot:
8
+ session_story_limit: 3
9
+ retrospective_mode: stop
10
+ conditional_boot_work: false # always run full reconciliation
11
+
12
+ ma:
13
+ parallel_stories: true
14
+ max_parallel_stories: 3
15
+ # PR 11 safety rails (inherited defaults via _base, overridden here).
16
+ max_consecutive_conflicts: 2
17
+ effective_parallel_floor: 1
18
+ # PR 12 cross-epic parallelism stays OFF even on large — experimental.
19
+ # Explicitly pinned (not inherited) so future _base changes can't silently
20
+ # flip it on for compliance-conscious users.
21
+ parallel_epics: false
22
+ state_sharding: always
@@ -0,0 +1,35 @@
1
+ # Legacy profile — rollback to pre-v2 (v1.0.5) behavior.
2
+ #
3
+ # Explicitly duplicates every setting that was default in v1.0.5 so future
4
+ # changes to _base.yaml cannot silently affect legacy behavior. This is
5
+ # the forward-compatibility guarantee: legacy produces a file-and-schema
6
+ # superset match against the v1.0.5 snapshot.
7
+ #
8
+ # Unlike the other profile YAMLs, legacy does NOT extend _base. The
9
+ # resolver detects version_pinned and skips the base overlay.
10
+
11
+ name: legacy
12
+ version_pinned: "v1.0.5"
13
+
14
+ autopilot:
15
+ implementation_flow: full
16
+ session_story_limit: 3
17
+ retrospective_mode: auto
18
+ phase_timings: false
19
+ coalesce_state_writes: false
20
+ conditional_boot_work: false
21
+ cache_shared_reads: false
22
+ # Legacy is byte-for-byte v1.0.5; no v2 LLM calls.
23
+ auto_infer_dependencies: false
24
+
25
+ git:
26
+ granularity: story
27
+ worktree:
28
+ enabled: true
29
+ squash_on_merge: false
30
+
31
+ ma:
32
+ parallel_stories: false
33
+ max_parallel_stories: 2
34
+ parallel_epics: false
35
+ state_sharding: never
@@ -0,0 +1,5 @@
1
+ # Medium profile — team product with real users (default).
2
+ # Matches _base defaults. Parallelism is opt-in: edit ma/config.yaml
3
+ # or run sprintpilot install --profile medium --parallel 2.
4
+
5
+ name: medium
@@ -0,0 +1,35 @@
1
+ # Nano profile — toy / tutorial / learning projects, solo.
2
+ # Routes each story through bmad-quick-dev (one-shot Implement → Review
3
+ # → Classify → Commit). Drops worktrees, per-story PRs, retrospectives,
4
+ # and session checkpoints. Quality gates preserved via quick-dev's
5
+ # internal review step.
6
+ #
7
+ # Behavior is gated on PR 4 (nano routing) landing. Until then this
8
+ # profile is configurable but has no runtime effect.
9
+
10
+ name: nano
11
+
12
+ autopilot:
13
+ implementation_flow: quick
14
+ # Nano's quick-dev one-shot is cheaper per story, so nano can run longer
15
+ # sessions — but unlimited (0) runs into the same context-rot failure
16
+ # mode as the full flow. 5 stories per session keeps step 10's CRITICAL
17
+ # actions reliably in-context without paying for excessive session boots.
18
+ session_story_limit: 5
19
+ # Nano runs at epic granularity with parallelism off — auto-inferring a
20
+ # story DAG would just burn one LLM call per sprint and never get used.
21
+ auto_infer_dependencies: false
22
+ retrospective_mode: skip
23
+ # Nano safety net — if a story fails under quick-dev, escalate to the
24
+ # small profile for the remainder of the session (scope: session only,
25
+ # never written back to config.yaml).
26
+ nano:
27
+ fallback_on_tests_fail: true
28
+ fallback_on_quick_dev_high_severity: true
29
+ fallback_target: small
30
+
31
+ git:
32
+ granularity: epic
33
+ worktree:
34
+ enabled: false
35
+ squash_on_merge: true
@@ -0,0 +1,5 @@
1
+ # Small profile — MVP / internal tool / prototype, solo or 1–2 devs.
2
+ # Matches _base defaults; kept as a distinct file so --profile small
3
+ # is always resolvable even if _base evolves.
4
+
5
+ name: small
@@ -6,6 +6,14 @@ git:
6
6
  base_branch: main
7
7
  # Git status is tracked in git-status.yaml (Sprintpilot-owned), NOT sprint-status.yaml (BMad Method-owned)
8
8
 
9
+ # Branch + PR granularity.
10
+ # story — one branch + one PR per story (default).
11
+ # epic — one branch + one PR per epic; stories committed on it.
12
+ # Epic branch name: {branch_prefix}epic-{epic_id}.
13
+ # Epic PR merges with --squash; the PR body lists each
14
+ # story commit. Set on nano profile by default.
15
+ granularity: story
16
+
9
17
  # Branch naming
10
18
  branch_prefix: "story/" # prefix for story branches (e.g., story/1-2-user-auth)
11
19
  max_branch_length: 60 # truncate + 6-char hash if longer
@@ -7,3 +7,45 @@ multi_agent:
7
7
  max_parallel_research: 3 # Max concurrent research agents per batch
8
8
  max_parallel_analysis: 5 # Max concurrent codebase analysis agents
9
9
  # session_story_limit NOT duplicated here — uses autopilot's single authoritative limit
10
+
11
+ # PR 11: intra-epic parallel story execution.
12
+ # parallel_stories: true enables dispatch-layer.js on Claude Code;
13
+ # other hosts silently fall back to sequential (per agent-adapter.js
14
+ # detection — confidence high + supports_parallel true).
15
+ # max_parallel_stories: cap on concurrent sub-agents per layer.
16
+ # min_epic_duration_for_parallel_sec: skip parallelism for epics
17
+ # whose estimated wall-clock is below this threshold (saves
18
+ # dispatch overhead).
19
+ # max_consecutive_conflicts: disable parallelism for the rest of the
20
+ # session once N consecutive merge conflicts occur.
21
+ # effective_parallel_floor: never drop below this mid-session even
22
+ # after failure-driven concurrency reduction.
23
+ parallel_stories: false
24
+ max_parallel_stories: 2
25
+ min_epic_duration_for_parallel_sec: 300
26
+ baseline_story_duration_sec: 180
27
+ max_consecutive_conflicts: 2
28
+ effective_parallel_floor: 1
29
+
30
+ # EXPERIMENTAL: enable parallel_stories dispatch on Gemini CLI.
31
+ #
32
+ # Gemini CLI has a subagent primitive (invoke_subagent) but its
33
+ # worktree-scoped variant is not yet shipped upstream (tracker:
34
+ # github.com/google-gemini/gemini-cli#22967) and real-world parallelism
35
+ # reports serialization + quota throttling. Sprintpilot detects Gemini
36
+ # CLI via GEMINI_CLI=1 (env, HIGH confidence) or parent process `gemini`
37
+ # (MEDIUM), but supports_parallel stays false by default.
38
+ #
39
+ # Flip to true PER PROJECT once upstream worktree support lands (or to
40
+ # experiment at your own risk). Workflow logs an "experimental parallel"
41
+ # warning once per session when this is true AND host=gemini-cli.
42
+ experimental_parallel_on_gemini: false
43
+
44
+ # PR 12 — cross-epic parallelism (EXPERIMENTAL).
45
+ # Off by default on ALL profiles including large. Enabling requires:
46
+ # 1. Both epics carry `independent: true` in dependencies.yaml.
47
+ # 2. preflight-merge.js reports no conflicts between them.
48
+ # 3. max_parallel_epics is hardcoded at 2 — no tuning knob.
49
+ # A single cross-epic merge conflict in a session disables parallel_epics
50
+ # for the rest of the session.
51
+ parallel_epics: false
@@ -0,0 +1,247 @@
1
+ #!/usr/bin/env node
2
+
3
+ // agent-adapter.js — detect the host coding agent currently running the
4
+ // Sprintpilot workflow. Output informs whether parallel sub-agent
5
+ // dispatch (PR 11) is safe.
6
+ //
7
+ // Usage:
8
+ // agent-adapter.js detect [--project-root <path>]
9
+ //
10
+ // Output (stdout, JSON):
11
+ // {
12
+ // "host": "claude-code" | "cursor" | "windsurf" | "aider" | "cline"
13
+ // | "roo" | "trae" | "kiro" | "copilot" | "unknown",
14
+ // "supports_parallel": boolean,
15
+ // "detection_reason": string,
16
+ // "confidence": "high" | "medium" | "low"
17
+ // }
18
+ //
19
+ // Detection priority (first match wins):
20
+ // 1. Env vars set by the running host → HIGH confidence
21
+ // 2. Parent process name → MEDIUM confidence
22
+ // 3. Filesystem install markers → LOW confidence
23
+ //
24
+ // Tautology guard (concept §M13): filesystem markers prove the INSTALL
25
+ // target, not the CURRENT host. confidence=low forces supports_parallel
26
+ // = false regardless of which host the markers suggest.
27
+
28
+ const fs = require('node:fs');
29
+ const path = require('node:path');
30
+ const { spawnSync } = require('node:child_process');
31
+
32
+ const { parseArgs } = require('../lib/runtime/args');
33
+ const log = require('../lib/runtime/log');
34
+
35
+ // Host capability table. supports_parallel is true only for hosts with
36
+ // a first-class multi-agent API that Sprintpilot's dispatch-layer.js
37
+ // can reliably drive — which today means worktree-scoped sub-agents
38
+ // with parallel fan-out. Claude Code is the only host that meets both
39
+ // bars at the time of writing. Gemini CLI has a subagent primitive
40
+ // (invoke_subagent) but its worktree-scoped variant is still open
41
+ // upstream (github.com/google-gemini/gemini-cli#22967) and real-world
42
+ // parallelism reports serialization + quota throttling (#25534); hence
43
+ // supports_parallel=false by default, with an experimental opt-in via
44
+ // `ma.experimental_parallel_on_gemini: true` handled at workflow level.
45
+ const HOSTS = {
46
+ 'claude-code': { supports_parallel: true },
47
+ 'gemini-cli': { supports_parallel: false, subagents: 'experimental' },
48
+ cursor: { supports_parallel: false },
49
+ windsurf: { supports_parallel: false },
50
+ aider: { supports_parallel: false },
51
+ cline: { supports_parallel: false },
52
+ roo: { supports_parallel: false },
53
+ trae: { supports_parallel: false },
54
+ kiro: { supports_parallel: false },
55
+ copilot: { supports_parallel: false },
56
+ unknown: { supports_parallel: false },
57
+ };
58
+
59
+ const ENV_DETECTORS = [
60
+ { host: 'claude-code', match: (env) => env.CLAUDECODE === '1' || !!env.CLAUDE_CODE_SESSION_ID },
61
+ // Gemini CLI sets GEMINI_CLI=1 for every subprocess it spawns
62
+ // (docs/tools/shell.md + docs/reference/commands.md as of v0.33.x).
63
+ { host: 'gemini-cli', match: (env) => env.GEMINI_CLI === '1' || !!env.GEMINI_CLI_SURFACE },
64
+ { host: 'cursor', match: (env) => !!env.CURSOR_SESSION_ID || !!env.CURSOR_TRACE_ID },
65
+ { host: 'windsurf', match: (env) => !!env.WINDSURF_SESSION },
66
+ { host: 'aider', match: (env) => !!env.AIDER_SESSION || !!env.AIDER_HISTORY_FILE },
67
+ { host: 'cline', match: (env) => !!env.CLINE_SESSION || !!env.CLINE_CONFIG },
68
+ ];
69
+
70
+ const PARENT_DETECTORS = [
71
+ { host: 'claude-code', parent: 'claude' },
72
+ { host: 'gemini-cli', parent: 'gemini' },
73
+ { host: 'cursor', parent: 'cursor-agent' },
74
+ { host: 'aider', parent: 'aider' },
75
+ ];
76
+
77
+ function help() {
78
+ log.out('Usage: agent-adapter.js detect [--project-root <path>]');
79
+ }
80
+
81
+ function detectFromEnv(env) {
82
+ for (const d of ENV_DETECTORS) {
83
+ if (d.match(env)) {
84
+ return { host: d.host, confidence: 'high', detection_reason: `env var set (${d.host})` };
85
+ }
86
+ }
87
+ return null;
88
+ }
89
+
90
+ // Parsers extracted as pure functions so the platform branches stay
91
+ // unit-testable on any OS. Each takes the raw stdout from the platform
92
+ // command and returns the basename or null. Negative test cases
93
+ // (empty input, "INFO: No tasks…", malformed lines) covered by tests.
94
+
95
+ /** Parse `ps -p <pid> -o comm=` output (POSIX). */
96
+ function parsePsOutput(raw) {
97
+ if (!raw) return null;
98
+ const trimmed = raw.trim();
99
+ if (!trimmed) return null;
100
+ return path.basename(trimmed.split(/\s+/)[0]);
101
+ }
102
+
103
+ /**
104
+ * Parse `tasklist /FO CSV /NH` output (Windows). Strips `.exe` so the
105
+ * basename matches the POSIX path (so PARENT_DETECTORS only needs the
106
+ * non-extension name once).
107
+ * Sample row: "claude.exe","12345","Console","1","123,456 K"
108
+ */
109
+ function parseTasklistOutput(raw) {
110
+ if (!raw) return null;
111
+ const trimmed = raw.trim();
112
+ if (!trimmed || /^INFO:/i.test(trimmed)) return null; // "INFO: No tasks…"
113
+ const m = trimmed.match(/^"([^"]+)"/);
114
+ if (!m) return null;
115
+ return path.basename(m[1]).replace(/\.exe$/i, '');
116
+ }
117
+
118
+ function parentProcessName() {
119
+ try {
120
+ const pid = process.ppid;
121
+ if (process.platform === 'win32') {
122
+ const res = spawnSync(
123
+ 'tasklist',
124
+ ['/FI', `PID eq ${pid}`, '/FO', 'CSV', '/NH'],
125
+ { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] },
126
+ );
127
+ if (res.status !== 0) return null;
128
+ return parseTasklistOutput(res.stdout || '');
129
+ }
130
+ // POSIX: macOS and Linux both support `ps -p <pid> -o comm=`.
131
+ const res = spawnSync('ps', ['-p', String(pid), '-o', 'comm='], {
132
+ encoding: 'utf8',
133
+ stdio: ['ignore', 'pipe', 'ignore'],
134
+ });
135
+ if (res.status !== 0) return null;
136
+ return parsePsOutput(res.stdout || '');
137
+ } catch {
138
+ return null;
139
+ }
140
+ }
141
+
142
+ function detectFromParent() {
143
+ const parent = parentProcessName();
144
+ if (!parent) return null;
145
+ for (const d of PARENT_DETECTORS) {
146
+ if (parent === d.parent) {
147
+ return {
148
+ host: d.host,
149
+ confidence: 'medium',
150
+ detection_reason: `parent process '${parent}'`,
151
+ };
152
+ }
153
+ }
154
+ return null;
155
+ }
156
+
157
+ function detectFromFilesystem(projectRoot) {
158
+ const markers = [
159
+ { path: path.join(projectRoot, '.claude', 'skills'), host: 'claude-code' },
160
+ { path: path.join(projectRoot, '.claude-code'), host: 'claude-code' },
161
+ { path: path.join(projectRoot, '.cursor'), host: 'cursor' },
162
+ { path: path.join(projectRoot, '.windsurf'), host: 'windsurf' },
163
+ { path: path.join(projectRoot, '.aider.conf.yml'), host: 'aider' },
164
+ { path: path.join(projectRoot, '.cline'), host: 'cline' },
165
+ ];
166
+ for (const m of markers) {
167
+ if (fs.existsSync(m.path)) {
168
+ return {
169
+ host: m.host,
170
+ confidence: 'low',
171
+ detection_reason: `filesystem marker '${path.relative(projectRoot, m.path)}' — NOT proof of current host`,
172
+ };
173
+ }
174
+ }
175
+ return null;
176
+ }
177
+
178
+ function detect({ env = process.env, projectRoot = process.cwd() } = {}) {
179
+ const fromEnv = detectFromEnv(env);
180
+ if (fromEnv) {
181
+ const caps = HOSTS[fromEnv.host] || HOSTS.unknown;
182
+ return {
183
+ host: fromEnv.host,
184
+ supports_parallel: caps.supports_parallel,
185
+ detection_reason: fromEnv.detection_reason,
186
+ confidence: 'high',
187
+ };
188
+ }
189
+ const fromParent = detectFromParent();
190
+ if (fromParent) {
191
+ const caps = HOSTS[fromParent.host] || HOSTS.unknown;
192
+ return {
193
+ host: fromParent.host,
194
+ supports_parallel: caps.supports_parallel,
195
+ detection_reason: fromParent.detection_reason,
196
+ confidence: 'medium',
197
+ };
198
+ }
199
+ const fromFs = detectFromFilesystem(projectRoot);
200
+ if (fromFs) {
201
+ // Tautology guard: filesystem markers never enable parallel.
202
+ return {
203
+ host: fromFs.host,
204
+ supports_parallel: false,
205
+ detection_reason: fromFs.detection_reason,
206
+ confidence: 'low',
207
+ };
208
+ }
209
+ return {
210
+ host: 'unknown',
211
+ supports_parallel: false,
212
+ detection_reason: 'no env/parent/filesystem signal',
213
+ confidence: 'low',
214
+ };
215
+ }
216
+
217
+ function main() {
218
+ const { opts, positional } = parseArgs(process.argv.slice(2));
219
+ if (opts.help || positional.length === 0) {
220
+ help();
221
+ process.exit(opts.help ? 0 : 1);
222
+ }
223
+ const action = positional[0];
224
+ if (action !== 'detect') {
225
+ log.error(`unknown action '${action}'. Valid: detect`);
226
+ process.exit(1);
227
+ }
228
+ const projectRoot = opts['project-root'] || process.cwd();
229
+ const result = detect({ env: process.env, projectRoot });
230
+ process.stdout.write(`${JSON.stringify(result)}\n`);
231
+ }
232
+
233
+ module.exports = {
234
+ HOSTS,
235
+ ENV_DETECTORS,
236
+ PARENT_DETECTORS,
237
+ detectFromEnv,
238
+ detectFromParent,
239
+ detectFromFilesystem,
240
+ detect,
241
+ parsePsOutput,
242
+ parseTasklistOutput,
243
+ };
244
+
245
+ if (require.main === module) {
246
+ main();
247
+ }