@ikunin/sprintpilot 2.0.4 → 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.
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
|
|
|
@@ -43,6 +43,13 @@ const PHASE_RE = /^[a-z][a-z0-9-.]*$/;
|
|
|
43
43
|
const META_MAX_BYTES = 2048;
|
|
44
44
|
const LINE_MAX_BYTES = 4096; // POSIX PIPE_BUF floor — single write() is atomic
|
|
45
45
|
const VALID_ACTIONS = ['start', 'end', 'once', 'mark'];
|
|
46
|
+
// MARKER_FILE: per-story marker file template. The actual file is
|
|
47
|
+
// `.mark.<story>.json` so concurrent writers for different stories never
|
|
48
|
+
// race on the same path. Pre-2.0.5 used a single global `.mark.json`
|
|
49
|
+
// which corrupted timing data when sub-agents in the same project root
|
|
50
|
+
// marked phases concurrently (e.g. parallel story dispatch). Kept as
|
|
51
|
+
// `.mark.json` only as a back-compat constant; runtime always uses the
|
|
52
|
+
// per-story path via `markerPath(root, story)`.
|
|
46
53
|
const MARKER_FILE = '.mark.json';
|
|
47
54
|
|
|
48
55
|
function help() {
|
|
@@ -191,15 +198,24 @@ function buildEntry(action, story, phase, meta) {
|
|
|
191
198
|
// `mark` — single-call timing
|
|
192
199
|
// ---------------------------------------------------------------
|
|
193
200
|
|
|
194
|
-
function markerPath(projectRoot) {
|
|
195
|
-
|
|
201
|
+
function markerPath(projectRoot, story) {
|
|
202
|
+
if (!story) throw new Error('markerPath requires a story key');
|
|
203
|
+
return path.join(timingsDir(projectRoot), `.mark.${story}.json`);
|
|
196
204
|
}
|
|
197
205
|
|
|
198
|
-
function readMarker(projectRoot) {
|
|
199
|
-
const file = markerPath(projectRoot);
|
|
200
|
-
|
|
206
|
+
function readMarker(projectRoot, story) {
|
|
207
|
+
const file = markerPath(projectRoot, story);
|
|
208
|
+
let raw;
|
|
209
|
+
try {
|
|
210
|
+
raw = fs.readFileSync(file, 'utf8');
|
|
211
|
+
} catch (e) {
|
|
212
|
+
if (e.code === 'ENOENT') return null;
|
|
213
|
+
// EACCES / EISDIR / other I/O — surface to stderr so silent corruption
|
|
214
|
+
// doesn't masquerade as "first mark of session".
|
|
215
|
+
log.error(`timing marker read failed (${file}): ${e.message}`);
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
201
218
|
try {
|
|
202
|
-
const raw = fs.readFileSync(file, 'utf8');
|
|
203
219
|
const parsed = JSON.parse(raw);
|
|
204
220
|
if (
|
|
205
221
|
parsed &&
|
|
@@ -210,24 +226,37 @@ function readMarker(projectRoot) {
|
|
|
210
226
|
) {
|
|
211
227
|
return parsed;
|
|
212
228
|
}
|
|
213
|
-
} catch {
|
|
214
|
-
|
|
229
|
+
} catch (e) {
|
|
230
|
+
log.error(`timing marker corrupt (${file}): ${e.message} — treating as absent`);
|
|
215
231
|
}
|
|
216
232
|
return null;
|
|
217
233
|
}
|
|
218
234
|
|
|
219
|
-
function writeMarker(projectRoot, marker) {
|
|
235
|
+
function writeMarker(projectRoot, story, marker) {
|
|
220
236
|
const dir = timingsDir(projectRoot);
|
|
221
237
|
fs.mkdirSync(dir, { recursive: true });
|
|
222
|
-
const file = markerPath(projectRoot);
|
|
238
|
+
const file = markerPath(projectRoot, story);
|
|
223
239
|
// Atomic-ish: write tmp + rename. Marker is small, single-line JSON.
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
240
|
+
// Tmp filename includes story + pid + random suffix to avoid collisions
|
|
241
|
+
// between concurrent same-process writers (rare in normal use, common in
|
|
242
|
+
// parallel test runs) and PID-reuse.
|
|
243
|
+
const tmp = `${file}.tmp.${process.pid}.${Math.random().toString(36).slice(2, 10)}`;
|
|
244
|
+
try {
|
|
245
|
+
fs.writeFileSync(tmp, JSON.stringify(marker));
|
|
246
|
+
fs.renameSync(tmp, file);
|
|
247
|
+
} catch (e) {
|
|
248
|
+
// Clean up tmp on rename failure so we don't leak orphan files.
|
|
249
|
+
try {
|
|
250
|
+
fs.unlinkSync(tmp);
|
|
251
|
+
} catch {
|
|
252
|
+
/* ignore — tmp may not exist */
|
|
253
|
+
}
|
|
254
|
+
throw e;
|
|
255
|
+
}
|
|
227
256
|
}
|
|
228
257
|
|
|
229
|
-
function clearMarker(projectRoot) {
|
|
230
|
-
const file = markerPath(projectRoot);
|
|
258
|
+
function clearMarker(projectRoot, story) {
|
|
259
|
+
const file = markerPath(projectRoot, story);
|
|
231
260
|
try {
|
|
232
261
|
fs.unlinkSync(file);
|
|
233
262
|
} catch {
|
|
@@ -238,27 +267,54 @@ function clearMarker(projectRoot) {
|
|
|
238
267
|
/**
|
|
239
268
|
* mark: single-call timing API.
|
|
240
269
|
*
|
|
241
|
-
* Emits a duration record for
|
|
242
|
-
* interval since the previous mark
|
|
243
|
-
* current phase. The very first mark
|
|
244
|
-
* record — there's no "previous
|
|
270
|
+
* Emits a duration record for THIS story's PREVIOUS phase (if any),
|
|
271
|
+
* covering the interval since the previous mark for the same story key,
|
|
272
|
+
* then writes a new marker for the current phase. The very first mark
|
|
273
|
+
* for a given story emits no duration record — there's no "previous
|
|
274
|
+
* phase" yet for that story.
|
|
245
275
|
*
|
|
246
|
-
*
|
|
247
|
-
*
|
|
276
|
+
* Pre-2.0.5 used a single global marker file shared across stories,
|
|
277
|
+
* which under parallel dispatch (sub-agents marking different stories
|
|
278
|
+
* concurrently against the same project root) raced on a single file —
|
|
279
|
+
* one rename clobbered the other and durations were attributed to the
|
|
280
|
+
* wrong (story, phase). Per-story markers eliminate the race entirely:
|
|
281
|
+
* each story has its own marker file `.mark.<story>.json`.
|
|
282
|
+
*
|
|
283
|
+
* Use phase = "_end" to close THIS story's last open phase without
|
|
284
|
+
* starting a new one (e.g. at sprint-complete time, or per-story
|
|
285
|
+
* cleanup). `_end` only touches the marker for the named story; other
|
|
286
|
+
* stories' markers are untouched.
|
|
287
|
+
*
|
|
288
|
+
* Order of operations is interrupt-safe: the new marker is written
|
|
289
|
+
* BEFORE the duration record is appended. If the process is killed
|
|
290
|
+
* between the marker rename and the duration append, we lose one
|
|
291
|
+
* duration record but the next mark will read the new marker (not the
|
|
292
|
+
* stale prev) and won't double-count.
|
|
293
|
+
*
|
|
294
|
+
* Wall-clock skew: durations are clamped at 0 with a `clock_skew: true`
|
|
295
|
+
* flag in the entry so aggregators don't get poisoned by NTP backsteps
|
|
296
|
+
* or DST transitions.
|
|
248
297
|
*
|
|
249
298
|
* Returns { duration_ms, prev_phase } so callers can log/inspect.
|
|
250
299
|
*/
|
|
251
300
|
function markPhase(projectRoot, story, phase, meta) {
|
|
252
301
|
const now = new Date();
|
|
253
|
-
const prev = readMarker(projectRoot);
|
|
302
|
+
const prev = readMarker(projectRoot, story);
|
|
303
|
+
|
|
304
|
+
// Build the duration entry from prev (if any) before mutating marker
|
|
305
|
+
// state. We append AFTER writing the new marker, so an interrupt
|
|
306
|
+
// between the two yields one missed record (acceptable) rather than a
|
|
307
|
+
// stale marker that would double-count on the next call.
|
|
308
|
+
let durationEntry = null;
|
|
254
309
|
let durationMs = null;
|
|
255
310
|
let prevPhase = null;
|
|
256
311
|
if (prev) {
|
|
257
312
|
const prevTs = Date.parse(prev.ts);
|
|
258
313
|
if (!Number.isNaN(prevTs)) {
|
|
259
|
-
|
|
314
|
+
const rawDelta = now.getTime() - prevTs;
|
|
315
|
+
durationMs = Math.max(0, rawDelta);
|
|
260
316
|
prevPhase = prev.phase;
|
|
261
|
-
|
|
317
|
+
durationEntry = {
|
|
262
318
|
event: 'duration',
|
|
263
319
|
story: prev.story,
|
|
264
320
|
phase: prev.phase,
|
|
@@ -266,17 +322,26 @@ function markPhase(projectRoot, story, phase, meta) {
|
|
|
266
322
|
ended: now.toISOString(),
|
|
267
323
|
duration_ms: durationMs,
|
|
268
324
|
};
|
|
325
|
+
if (rawDelta < 0) durationEntry.clock_skew = true;
|
|
269
326
|
if (prev.meta !== undefined) durationEntry.meta = prev.meta;
|
|
270
|
-
appendLine(projectRoot, prev.story, durationEntry);
|
|
271
327
|
}
|
|
272
328
|
}
|
|
329
|
+
|
|
330
|
+
// 1. Commit the marker state transition first.
|
|
273
331
|
if (phase === '_end') {
|
|
274
|
-
clearMarker(projectRoot);
|
|
332
|
+
clearMarker(projectRoot, story);
|
|
275
333
|
} else {
|
|
276
334
|
const marker = { story, phase, ts: now.toISOString() };
|
|
277
335
|
if (meta !== undefined) marker.meta = meta;
|
|
278
|
-
writeMarker(projectRoot, marker);
|
|
336
|
+
writeMarker(projectRoot, story, marker);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// 2. Append the duration record after the marker is committed. If
|
|
340
|
+
// this throws, the marker is already correct for the next mark.
|
|
341
|
+
if (durationEntry !== null) {
|
|
342
|
+
appendLine(projectRoot, prev.story, durationEntry);
|
|
279
343
|
}
|
|
344
|
+
|
|
280
345
|
return { duration_ms: durationMs, prev_phase: prevPhase };
|
|
281
346
|
}
|
|
282
347
|
|
|
@@ -639,7 +639,7 @@ Parse stdout as a single JSON object: `{"remaining":[...],"state":"..."}`.
|
|
|
639
639
|
- Skill flow (full): bmad-create-story → bmad-check-implementation-readiness → bmad-dev-story → bmad-code-review → apply patch findings → re-run tests → set status=done in {status_file}
|
|
640
640
|
- Skill flow (quick): bmad-quick-dev (single skill; nano profile)
|
|
641
641
|
Use {{implementation_flow}} = `{{implementation_flow}}` to pick which flow.
|
|
642
|
-
Track timing via `node {{project_root}}/_Sprintpilot/scripts/log-timing.js mark --story K --phase <phase
|
|
642
|
+
Track timing via `node {{project_root}}/_Sprintpilot/scripts/log-timing.js mark --story K --phase <phase> --project-root {{project_root}}` after each skill returns. The explicit `--project-root` is REQUIRED — without it the script falls back to cwd (the worktree), which orphans timing data. With per-story markers (2.0.5+) concurrent sub-agents writing to the same project root no longer race.
|
|
643
643
|
Return a one-line JSON summary on completion: {"story":"K", "status":"done"|"failed", "tests":"<N/M>", "notes":"<short>"}
|
|
644
644
|
```
|
|
645
645
|
|
package/package.json
CHANGED