@ikunin/sprintpilot 2.0.5 → 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.
@@ -1,6 +1,6 @@
1
1
  addon:
2
2
  name: sprintpilot
3
- version: 2.0.5
3
+ version: 2.0.6
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:
@@ -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 tiny
13
- // marker file (.timings/.mark.json), computes the duration
14
- // since the previous mark, emits one duration record for the
15
- // PREVIOUS phase, and writes a new marker for the current
16
- // phase. Designed for LLM-driven workflows where the agent
17
- // may forget to call `end` after a long skill — `mark` only
18
- // needs to be called ONCE per phase transition.
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,14 +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
- // 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)`.
53
- const MARKER_FILE = '.mark.json';
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;
54
61
 
55
62
  function help() {
56
63
  log.out(
@@ -215,21 +222,37 @@ function readMarker(projectRoot, story) {
215
222
  log.error(`timing marker read failed (${file}): ${e.message}`);
216
223
  return null;
217
224
  }
225
+ let parsed;
218
226
  try {
219
- const parsed = JSON.parse(raw);
220
- if (
221
- parsed &&
222
- typeof parsed === 'object' &&
223
- typeof parsed.story === 'string' &&
224
- typeof parsed.phase === 'string' &&
225
- typeof parsed.ts === 'string'
226
- ) {
227
- return parsed;
228
- }
227
+ parsed = JSON.parse(raw);
229
228
  } catch (e) {
230
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;
240
+ }
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;
231
254
  }
232
- return null;
255
+ return parsed;
233
256
  }
234
257
 
235
258
  function writeMarker(projectRoot, story, marker) {
@@ -291,9 +314,10 @@ function clearMarker(projectRoot, story) {
291
314
  * duration record but the next mark will read the new marker (not the
292
315
  * stale prev) and won't double-count.
293
316
  *
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.
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").
297
321
  *
298
322
  * Returns { duration_ms, prev_phase } so callers can log/inspect.
299
323
  */
@@ -312,7 +336,8 @@ function markPhase(projectRoot, story, phase, meta) {
312
336
  const prevTs = Date.parse(prev.ts);
313
337
  if (!Number.isNaN(prevTs)) {
314
338
  const rawDelta = now.getTime() - prevTs;
315
- durationMs = Math.max(0, rawDelta);
339
+ const clamped = rawDelta < 0 || rawDelta > MAX_PLAUSIBLE_DURATION_MS;
340
+ durationMs = clamped ? 0 : rawDelta;
316
341
  prevPhase = prev.phase;
317
342
  durationEntry = {
318
343
  event: 'duration',
@@ -322,7 +347,7 @@ function markPhase(projectRoot, story, phase, meta) {
322
347
  ended: now.toISOString(),
323
348
  duration_ms: durationMs,
324
349
  };
325
- if (rawDelta < 0) durationEntry.clock_skew = true;
350
+ if (clamped) durationEntry.clock_skew = true;
326
351
  if (prev.meta !== undefined) durationEntry.meta = prev.meta;
327
352
  }
328
353
  }
@@ -402,7 +427,7 @@ module.exports = {
402
427
  PHASE_RE,
403
428
  META_MAX_BYTES,
404
429
  LINE_MAX_BYTES,
405
- MARKER_FILE,
430
+ MAX_PLAUSIBLE_DURATION_MS,
406
431
  VALID_ACTIONS,
407
432
  validateStory,
408
433
  validatePhase,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ikunin/sprintpilot",
3
- "version": "2.0.5",
3
+ "version": "2.0.6",
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": {