@ikunin/sprintpilot 2.0.4 → 2.0.6
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
|
|
|
@@ -222,6 +222,11 @@ function main() {
|
|
|
222
222
|
const acSectionName = opts['ac-section'] || 'Acceptance Criteria';
|
|
223
223
|
const projectRoot = opts['project-root'] || process.cwd();
|
|
224
224
|
const storyKey = storyKeyFromFile(storyFile);
|
|
225
|
+
if (storyKey === null && timing.isEnabled(projectRoot)) {
|
|
226
|
+
log.error(
|
|
227
|
+
`inject-tasks-section: cannot derive a STORY_RE-compatible key from '${path.basename(storyFile)}' (must lower-case to /^[a-z0-9][a-z0-9-]*$/ after stripping leading 'story-' and trailing '.md'); skipping timing emit`,
|
|
228
|
+
);
|
|
229
|
+
}
|
|
225
230
|
|
|
226
231
|
const body = fs.readFileSync(storyFile, 'utf8');
|
|
227
232
|
const info = inspectTasksSection(body);
|
|
@@ -9,13 +9,16 @@
|
|
|
9
9
|
// start Emit {event:"start", story, phase, ts:<iso8601>}
|
|
10
10
|
// end Emit {event:"end", story, phase, ts:<iso8601>}
|
|
11
11
|
// once Emit a single-event marker (for things like health-check-run)
|
|
12
|
-
// mark Single-call replacement for start/end pairs. Reads a
|
|
13
|
-
// marker file (.timings/.mark
|
|
14
|
-
// since the previous mark
|
|
15
|
-
// PREVIOUS phase, and writes a new
|
|
16
|
-
// phase. Designed for LLM-driven
|
|
17
|
-
// may forget to call `end` after a
|
|
18
|
-
// needs to be called ONCE per phase
|
|
12
|
+
// mark Single-call replacement for start/end pairs. Reads a per-story
|
|
13
|
+
// marker file (.timings/.mark.<story>.json), computes the duration
|
|
14
|
+
// since the previous mark for the same story key, emits one
|
|
15
|
+
// duration record for the PREVIOUS phase, and writes a new
|
|
16
|
+
// marker for the current phase. Designed for LLM-driven
|
|
17
|
+
// workflows where the agent may forget to call `end` after a
|
|
18
|
+
// long skill — `mark` only needs to be called ONCE per phase
|
|
19
|
+
// transition. Per-story markers (added in 2.0.5) make
|
|
20
|
+
// concurrent sub-agents marking different stories race-free
|
|
21
|
+
// against the same project root.
|
|
19
22
|
//
|
|
20
23
|
// Output path:
|
|
21
24
|
// <project-root>/_bmad-output/implementation-artifacts/.timings/<story>.jsonl
|
|
@@ -43,7 +46,18 @@ const PHASE_RE = /^[a-z][a-z0-9-.]*$/;
|
|
|
43
46
|
const META_MAX_BYTES = 2048;
|
|
44
47
|
const LINE_MAX_BYTES = 4096; // POSIX PIPE_BUF floor — single write() is atomic
|
|
45
48
|
const VALID_ACTIONS = ['start', 'end', 'once', 'mark'];
|
|
46
|
-
|
|
49
|
+
// Marker filenames are `.mark.<story>.json` — built by `markerPath()`.
|
|
50
|
+
// Pre-2.0.5 used a single global `.mark.json`, which corrupted timing
|
|
51
|
+
// data under parallel dispatch (concurrent sub-agents racing on one
|
|
52
|
+
// rename target). The constant is gone; runtime always uses per-story
|
|
53
|
+
// paths.
|
|
54
|
+
//
|
|
55
|
+
// Sanity ceiling for a single duration record. A wall-clock skip
|
|
56
|
+
// forward of more than this many ms is treated as clock skew rather
|
|
57
|
+
// than a real duration — clamped to 0 with `clock_skew: true` stamped.
|
|
58
|
+
// 24h chosen because no realistic skill phase is longer than that, and
|
|
59
|
+
// it's well above any plausible CI timeout.
|
|
60
|
+
const MAX_PLAUSIBLE_DURATION_MS = 24 * 60 * 60 * 1000;
|
|
47
61
|
|
|
48
62
|
function help() {
|
|
49
63
|
log.out(
|
|
@@ -191,43 +205,81 @@ function buildEntry(action, story, phase, meta) {
|
|
|
191
205
|
// `mark` — single-call timing
|
|
192
206
|
// ---------------------------------------------------------------
|
|
193
207
|
|
|
194
|
-
function markerPath(projectRoot) {
|
|
195
|
-
|
|
208
|
+
function markerPath(projectRoot, story) {
|
|
209
|
+
if (!story) throw new Error('markerPath requires a story key');
|
|
210
|
+
return path.join(timingsDir(projectRoot), `.mark.${story}.json`);
|
|
196
211
|
}
|
|
197
212
|
|
|
198
|
-
function readMarker(projectRoot) {
|
|
199
|
-
const file = markerPath(projectRoot);
|
|
200
|
-
|
|
213
|
+
function readMarker(projectRoot, story) {
|
|
214
|
+
const file = markerPath(projectRoot, story);
|
|
215
|
+
let raw;
|
|
201
216
|
try {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
if (
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
} catch {
|
|
214
|
-
|
|
217
|
+
raw = fs.readFileSync(file, 'utf8');
|
|
218
|
+
} catch (e) {
|
|
219
|
+
if (e.code === 'ENOENT') return null;
|
|
220
|
+
// EACCES / EISDIR / other I/O — surface to stderr so silent corruption
|
|
221
|
+
// doesn't masquerade as "first mark of session".
|
|
222
|
+
log.error(`timing marker read failed (${file}): ${e.message}`);
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
let parsed;
|
|
226
|
+
try {
|
|
227
|
+
parsed = JSON.parse(raw);
|
|
228
|
+
} catch (e) {
|
|
229
|
+
log.error(`timing marker corrupt (${file}): ${e.message} — treating as absent`);
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
if (
|
|
233
|
+
!parsed ||
|
|
234
|
+
typeof parsed !== 'object' ||
|
|
235
|
+
typeof parsed.story !== 'string' ||
|
|
236
|
+
typeof parsed.phase !== 'string' ||
|
|
237
|
+
typeof parsed.ts !== 'string'
|
|
238
|
+
) {
|
|
239
|
+
return null;
|
|
215
240
|
}
|
|
216
|
-
|
|
241
|
+
// Re-validate `story` and `phase` against their regexes. CLI input is
|
|
242
|
+
// already validated, but a corrupted/hand-edited marker could carry a
|
|
243
|
+
// path-traversing story (e.g. "../../etc") — `parsed.story` flows into
|
|
244
|
+
// `appendLine(projectRoot, prev.story, ...)` which path.joins to
|
|
245
|
+
// `<timingsDir>/<story>.jsonl`. Defense-in-depth: refuse any value that
|
|
246
|
+
// doesn't match STORY_RE / PHASE_RE.
|
|
247
|
+
if (!STORY_RE.test(parsed.story)) {
|
|
248
|
+
log.error(`timing marker (${file}) has invalid story '${parsed.story}'; treating as absent`);
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
if (parsed.phase !== '_end' && !PHASE_RE.test(parsed.phase)) {
|
|
252
|
+
log.error(`timing marker (${file}) has invalid phase '${parsed.phase}'; treating as absent`);
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
return parsed;
|
|
217
256
|
}
|
|
218
257
|
|
|
219
|
-
function writeMarker(projectRoot, marker) {
|
|
258
|
+
function writeMarker(projectRoot, story, marker) {
|
|
220
259
|
const dir = timingsDir(projectRoot);
|
|
221
260
|
fs.mkdirSync(dir, { recursive: true });
|
|
222
|
-
const file = markerPath(projectRoot);
|
|
261
|
+
const file = markerPath(projectRoot, story);
|
|
223
262
|
// Atomic-ish: write tmp + rename. Marker is small, single-line JSON.
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
263
|
+
// Tmp filename includes story + pid + random suffix to avoid collisions
|
|
264
|
+
// between concurrent same-process writers (rare in normal use, common in
|
|
265
|
+
// parallel test runs) and PID-reuse.
|
|
266
|
+
const tmp = `${file}.tmp.${process.pid}.${Math.random().toString(36).slice(2, 10)}`;
|
|
267
|
+
try {
|
|
268
|
+
fs.writeFileSync(tmp, JSON.stringify(marker));
|
|
269
|
+
fs.renameSync(tmp, file);
|
|
270
|
+
} catch (e) {
|
|
271
|
+
// Clean up tmp on rename failure so we don't leak orphan files.
|
|
272
|
+
try {
|
|
273
|
+
fs.unlinkSync(tmp);
|
|
274
|
+
} catch {
|
|
275
|
+
/* ignore — tmp may not exist */
|
|
276
|
+
}
|
|
277
|
+
throw e;
|
|
278
|
+
}
|
|
227
279
|
}
|
|
228
280
|
|
|
229
|
-
function clearMarker(projectRoot) {
|
|
230
|
-
const file = markerPath(projectRoot);
|
|
281
|
+
function clearMarker(projectRoot, story) {
|
|
282
|
+
const file = markerPath(projectRoot, story);
|
|
231
283
|
try {
|
|
232
284
|
fs.unlinkSync(file);
|
|
233
285
|
} catch {
|
|
@@ -238,27 +290,56 @@ function clearMarker(projectRoot) {
|
|
|
238
290
|
/**
|
|
239
291
|
* mark: single-call timing API.
|
|
240
292
|
*
|
|
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
|
|
293
|
+
* Emits a duration record for THIS story's PREVIOUS phase (if any),
|
|
294
|
+
* covering the interval since the previous mark for the same story key,
|
|
295
|
+
* then writes a new marker for the current phase. The very first mark
|
|
296
|
+
* for a given story emits no duration record — there's no "previous
|
|
297
|
+
* phase" yet for that story.
|
|
298
|
+
*
|
|
299
|
+
* Pre-2.0.5 used a single global marker file shared across stories,
|
|
300
|
+
* which under parallel dispatch (sub-agents marking different stories
|
|
301
|
+
* concurrently against the same project root) raced on a single file —
|
|
302
|
+
* one rename clobbered the other and durations were attributed to the
|
|
303
|
+
* wrong (story, phase). Per-story markers eliminate the race entirely:
|
|
304
|
+
* each story has its own marker file `.mark.<story>.json`.
|
|
305
|
+
*
|
|
306
|
+
* Use phase = "_end" to close THIS story's last open phase without
|
|
307
|
+
* starting a new one (e.g. at sprint-complete time, or per-story
|
|
308
|
+
* cleanup). `_end` only touches the marker for the named story; other
|
|
309
|
+
* stories' markers are untouched.
|
|
310
|
+
*
|
|
311
|
+
* Order of operations is interrupt-safe: the new marker is written
|
|
312
|
+
* BEFORE the duration record is appended. If the process is killed
|
|
313
|
+
* between the marker rename and the duration append, we lose one
|
|
314
|
+
* duration record but the next mark will read the new marker (not the
|
|
315
|
+
* stale prev) and won't double-count.
|
|
245
316
|
*
|
|
246
|
-
*
|
|
247
|
-
*
|
|
317
|
+
* Wall-clock skew: durations are clamped to [0, MAX_PLAUSIBLE_DURATION_MS]
|
|
318
|
+
* with a `clock_skew: true` flag in the entry so aggregators don't get
|
|
319
|
+
* poisoned by NTP backsteps, DST transitions, or container clock skips
|
|
320
|
+
* forward of unrealistic magnitudes (e.g. "this skill ran for 7 hours").
|
|
248
321
|
*
|
|
249
322
|
* Returns { duration_ms, prev_phase } so callers can log/inspect.
|
|
250
323
|
*/
|
|
251
324
|
function markPhase(projectRoot, story, phase, meta) {
|
|
252
325
|
const now = new Date();
|
|
253
|
-
const prev = readMarker(projectRoot);
|
|
326
|
+
const prev = readMarker(projectRoot, story);
|
|
327
|
+
|
|
328
|
+
// Build the duration entry from prev (if any) before mutating marker
|
|
329
|
+
// state. We append AFTER writing the new marker, so an interrupt
|
|
330
|
+
// between the two yields one missed record (acceptable) rather than a
|
|
331
|
+
// stale marker that would double-count on the next call.
|
|
332
|
+
let durationEntry = null;
|
|
254
333
|
let durationMs = null;
|
|
255
334
|
let prevPhase = null;
|
|
256
335
|
if (prev) {
|
|
257
336
|
const prevTs = Date.parse(prev.ts);
|
|
258
337
|
if (!Number.isNaN(prevTs)) {
|
|
259
|
-
|
|
338
|
+
const rawDelta = now.getTime() - prevTs;
|
|
339
|
+
const clamped = rawDelta < 0 || rawDelta > MAX_PLAUSIBLE_DURATION_MS;
|
|
340
|
+
durationMs = clamped ? 0 : rawDelta;
|
|
260
341
|
prevPhase = prev.phase;
|
|
261
|
-
|
|
342
|
+
durationEntry = {
|
|
262
343
|
event: 'duration',
|
|
263
344
|
story: prev.story,
|
|
264
345
|
phase: prev.phase,
|
|
@@ -266,17 +347,26 @@ function markPhase(projectRoot, story, phase, meta) {
|
|
|
266
347
|
ended: now.toISOString(),
|
|
267
348
|
duration_ms: durationMs,
|
|
268
349
|
};
|
|
350
|
+
if (clamped) durationEntry.clock_skew = true;
|
|
269
351
|
if (prev.meta !== undefined) durationEntry.meta = prev.meta;
|
|
270
|
-
appendLine(projectRoot, prev.story, durationEntry);
|
|
271
352
|
}
|
|
272
353
|
}
|
|
354
|
+
|
|
355
|
+
// 1. Commit the marker state transition first.
|
|
273
356
|
if (phase === '_end') {
|
|
274
|
-
clearMarker(projectRoot);
|
|
357
|
+
clearMarker(projectRoot, story);
|
|
275
358
|
} else {
|
|
276
359
|
const marker = { story, phase, ts: now.toISOString() };
|
|
277
360
|
if (meta !== undefined) marker.meta = meta;
|
|
278
|
-
writeMarker(projectRoot, marker);
|
|
361
|
+
writeMarker(projectRoot, story, marker);
|
|
279
362
|
}
|
|
363
|
+
|
|
364
|
+
// 2. Append the duration record after the marker is committed. If
|
|
365
|
+
// this throws, the marker is already correct for the next mark.
|
|
366
|
+
if (durationEntry !== null) {
|
|
367
|
+
appendLine(projectRoot, prev.story, durationEntry);
|
|
368
|
+
}
|
|
369
|
+
|
|
280
370
|
return { duration_ms: durationMs, prev_phase: prevPhase };
|
|
281
371
|
}
|
|
282
372
|
|
|
@@ -337,7 +427,7 @@ module.exports = {
|
|
|
337
427
|
PHASE_RE,
|
|
338
428
|
META_MAX_BYTES,
|
|
339
429
|
LINE_MAX_BYTES,
|
|
340
|
-
|
|
430
|
+
MAX_PLAUSIBLE_DURATION_MS,
|
|
341
431
|
VALID_ACTIONS,
|
|
342
432
|
validateStory,
|
|
343
433
|
validatePhase,
|
|
@@ -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