@wildorder/nightshift 0.17.0 → 0.18.0

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 (99) hide show
  1. package/README.md +269 -1
  2. package/dist/agent-runner.d.ts +41 -3
  3. package/dist/agent-runner.d.ts.map +1 -1
  4. package/dist/agent-runner.js +197 -12
  5. package/dist/agent-runner.js.map +1 -1
  6. package/dist/agent-session.d.ts +123 -0
  7. package/dist/agent-session.d.ts.map +1 -0
  8. package/dist/agent-session.js +114 -0
  9. package/dist/agent-session.js.map +1 -0
  10. package/dist/atomic-write.d.ts +18 -0
  11. package/dist/atomic-write.d.ts.map +1 -0
  12. package/dist/atomic-write.js +74 -0
  13. package/dist/atomic-write.js.map +1 -0
  14. package/dist/author.d.ts +112 -6
  15. package/dist/author.d.ts.map +1 -1
  16. package/dist/author.js +597 -137
  17. package/dist/author.js.map +1 -1
  18. package/dist/causal-analysis.d.ts +6 -2
  19. package/dist/causal-analysis.d.ts.map +1 -1
  20. package/dist/causal-analysis.js +55 -2
  21. package/dist/causal-analysis.js.map +1 -1
  22. package/dist/cli.js +22 -2
  23. package/dist/cli.js.map +1 -1
  24. package/dist/concurrency.d.ts +24 -0
  25. package/dist/concurrency.d.ts.map +1 -0
  26. package/dist/concurrency.js +28 -0
  27. package/dist/concurrency.js.map +1 -0
  28. package/dist/decider-review.d.ts +10 -3
  29. package/dist/decider-review.d.ts.map +1 -1
  30. package/dist/decider-review.js +6 -4
  31. package/dist/decider-review.js.map +1 -1
  32. package/dist/decision-ledger.d.ts +7 -0
  33. package/dist/decision-ledger.d.ts.map +1 -1
  34. package/dist/decision-ledger.js +25 -10
  35. package/dist/decision-ledger.js.map +1 -1
  36. package/dist/detect-package-manager.d.ts +10 -0
  37. package/dist/detect-package-manager.d.ts.map +1 -1
  38. package/dist/detect-package-manager.js +12 -0
  39. package/dist/detect-package-manager.js.map +1 -1
  40. package/dist/index.d.ts +3 -1
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +3 -1
  43. package/dist/index.js.map +1 -1
  44. package/dist/manifest.d.ts +24 -5
  45. package/dist/manifest.d.ts.map +1 -1
  46. package/dist/manifest.js +254 -108
  47. package/dist/manifest.js.map +1 -1
  48. package/dist/permits.d.ts +18 -0
  49. package/dist/permits.d.ts.map +1 -1
  50. package/dist/permits.js +155 -0
  51. package/dist/permits.js.map +1 -1
  52. package/dist/preflight.d.ts +2 -2
  53. package/dist/preflight.d.ts.map +1 -1
  54. package/dist/preflight.js.map +1 -1
  55. package/dist/review-pass.d.ts +52 -10
  56. package/dist/review-pass.d.ts.map +1 -1
  57. package/dist/review-pass.js +114 -30
  58. package/dist/review-pass.js.map +1 -1
  59. package/dist/run-analytics-report.d.ts +114 -1
  60. package/dist/run-analytics-report.d.ts.map +1 -1
  61. package/dist/run-analytics-report.js +606 -14
  62. package/dist/run-analytics-report.js.map +1 -1
  63. package/dist/run-analytics.d.ts +107 -1
  64. package/dist/run-analytics.d.ts.map +1 -1
  65. package/dist/run-analytics.js +257 -25
  66. package/dist/run-analytics.js.map +1 -1
  67. package/dist/run-log.d.ts +16 -0
  68. package/dist/run-log.d.ts.map +1 -0
  69. package/dist/run-log.js +32 -0
  70. package/dist/run-log.js.map +1 -0
  71. package/dist/run-program.d.ts +203 -7
  72. package/dist/run-program.d.ts.map +1 -1
  73. package/dist/run-program.js +1526 -320
  74. package/dist/run-program.js.map +1 -1
  75. package/dist/scheduler.d.ts +75 -0
  76. package/dist/scheduler.d.ts.map +1 -0
  77. package/dist/scheduler.js +126 -0
  78. package/dist/scheduler.js.map +1 -0
  79. package/dist/verify-cache.d.ts +64 -0
  80. package/dist/verify-cache.d.ts.map +1 -0
  81. package/dist/verify-cache.js +56 -0
  82. package/dist/verify-cache.js.map +1 -0
  83. package/dist/whole-program-review.d.ts +2 -2
  84. package/dist/whole-program-review.d.ts.map +1 -1
  85. package/dist/whole-program-review.js.map +1 -1
  86. package/dist/worktree.d.ts +124 -0
  87. package/dist/worktree.d.ts.map +1 -0
  88. package/dist/worktree.js +397 -0
  89. package/dist/worktree.js.map +1 -0
  90. package/dist/write-queue.d.ts +25 -0
  91. package/dist/write-queue.d.ts.map +1 -0
  92. package/dist/write-queue.js +52 -0
  93. package/dist/write-queue.js.map +1 -0
  94. package/package.json +2 -2
  95. package/skills/plan-program/SKILL.md +37 -5
  96. package/dist/worktree-guard.d.ts +0 -40
  97. package/dist/worktree-guard.d.ts.map +0 -1
  98. package/dist/worktree-guard.js +0 -91
  99. package/dist/worktree-guard.js.map +0 -1
@@ -1,4 +1,4 @@
1
- import { BUCKETS, KNOWN_OUTCOMES, } from "./run-analytics.js";
1
+ import { BUCKETS, KNOWN_OUTCOMES, KNOWN_WAIT_CAUSES, SCHEDULING_POINT_KINDS, } from "./run-analytics.js";
2
2
  function compareIntervals(a, b) {
3
3
  return a.start - b.start || a.end - b.end || a.span.id.localeCompare(b.span.id);
4
4
  }
@@ -45,8 +45,17 @@ function findTopLevelAncestor(span, byId, topLevelIds) {
45
45
  * covered elementary sub-interval to exactly one owner — the covering span
46
46
  * that sorts first by `(startOffsetMs, endOffsetMs, id)` — so
47
47
  * `Σ(bucket durations)` equals the interval union exactly, even when
48
- * top-level spans overlap (D2). Any sub-interval covered by more than one
49
- * span is recorded as an `overlap` diagnostic.
48
+ * top-level spans overlap (D2).
49
+ *
50
+ * An `overlap` diagnostic fires only when at least two of the covering spans
51
+ * share the same *defined* `dimensions.workstream` (§3.2): a workstream's own
52
+ * stages are sequential by construction, so overlap within one workstream is
53
+ * still a bug worth a note. Overlap between different workstreams — or
54
+ * involving a span with no workstream at all — is the expected shape of a
55
+ * concurrent run and produces no diagnostic. Top-level spans are direct
56
+ * children of the `run` span, which itself carries no workstream (WS-01), so
57
+ * a top-level span's own `dimensions.workstream` already *is* its effective
58
+ * workstream — no ancestor walk is needed here.
50
59
  */
51
60
  function sweepBucketOwnership(intervals, diagnostics) {
52
61
  const totals = new Map(BUCKETS.map((bucket) => [bucket, 0]));
@@ -71,20 +80,433 @@ function sweepBucketOwnership(intervals, diagnostics) {
71
80
  const owner = covering[0];
72
81
  totals.set(owner.span.bucket, (totals.get(owner.span.bucket) ?? 0) + (b - a));
73
82
  if (covering.length > 1) {
74
- const ids = covering.map((interval) => interval.span.id).sort();
75
- const key = ids.join(",");
76
- if (!seenOverlaps.has(key)) {
77
- seenOverlaps.add(key);
83
+ const workstreams = new Set(covering.map((interval) => interval.span.dimensions.workstream).filter((ws) => ws !== undefined));
84
+ const sameWorkstream = [...workstreams].find((ws) => covering.filter((interval) => interval.span.dimensions.workstream === ws).length > 1);
85
+ if (sameWorkstream !== undefined) {
86
+ const ids = covering.map((interval) => interval.span.id).sort();
87
+ const key = ids.join(",");
88
+ if (!seenOverlaps.has(key)) {
89
+ seenOverlaps.add(key);
90
+ diagnostics.push({
91
+ kind: "overlap",
92
+ message: `${sameWorkstream}'s top-level spans overlap in [${a}, ${b}]ms: ${ids.join(", ")} — ` +
93
+ `attributed once, to ${owner.span.id} (${owner.span.bucket})`,
94
+ spanIds: ids,
95
+ });
96
+ }
97
+ }
98
+ }
99
+ }
100
+ return totals;
101
+ }
102
+ // ---------------------------------------------------------------------------
103
+ // WS-06: scheduling-observation reconciliation, the derived dependency-landing
104
+ // wait, the concurrency sweep, and the critical-path walk.
105
+ // ---------------------------------------------------------------------------
106
+ /** Two adjacent-but-distinct `monotonic()` reads can differ by a scheduler
107
+ * tick; this is orders of magnitude below any wait worth reporting and
108
+ * orders of magnitude above that jitter (§3.5). */
109
+ const SCHEDULING_IDENTITY_TOLERANCE_MS = 50;
110
+ const SCHEDULING_POINT_KIND_SET = new Set(SCHEDULING_POINT_KINDS);
111
+ function isFiniteNonNegative(value) {
112
+ return value !== undefined && Number.isFinite(value) && value >= 0;
113
+ }
114
+ /**
115
+ * Reconciles the three `SCHEDULING_POINT_KINDS` points into a per-workstream
116
+ * schedule, fail-open (§3.5): every malformed or contradictory observation is
117
+ * diagnosed and either skipped (malformed) or summed/kept-earliest-or-latest
118
+ * (a genuine duplicate) — never silently dropped, never thrown.
119
+ */
120
+ function reconcileScheduling(artifact, diagnostics) {
121
+ const readyPoints = new Map();
122
+ const scheduledPoints = new Map();
123
+ const waitPoints = new Map(); // key: `${workstream} ${cause}`
124
+ for (const point of artifact.points) {
125
+ if (!SCHEDULING_POINT_KIND_SET.has(point.kind))
126
+ continue;
127
+ const workstream = point.dimensions?.workstream;
128
+ if (workstream === undefined) {
129
+ diagnostics.push({
130
+ kind: "scheduling-observation",
131
+ message: `${point.kind} point ${point.id} has no dimensions.workstream and cannot be attributed to any subject — skipped`,
132
+ spanIds: [],
133
+ pointIds: [point.id],
134
+ });
135
+ continue;
136
+ }
137
+ if (point.kind === "workstream-waiting") {
138
+ const cause = point.dimensions?.waitCause;
139
+ const value = point.value;
140
+ if (cause === undefined || !isFiniteNonNegative(value)) {
141
+ diagnostics.push({
142
+ kind: "scheduling-observation",
143
+ message: `workstream-waiting point ${point.id} for ${workstream} has no waitCause, or a missing/negative/non-finite ` +
144
+ "value — skipped",
145
+ spanIds: [],
146
+ pointIds: [point.id],
147
+ });
148
+ continue;
149
+ }
150
+ const key = `${workstream} ${cause}`;
151
+ const list = waitPoints.get(key) ?? [];
152
+ list.push({ pointId: point.id, atOffsetMs: point.atOffsetMs, waitedMs: value });
153
+ waitPoints.set(key, list);
154
+ }
155
+ else {
156
+ if (!isFiniteNonNegative(point.atOffsetMs)) {
78
157
  diagnostics.push({
79
- kind: "overlap",
80
- message: `top-level spans overlap in [${a}, ${b}]ms: ${ids.join(", ")}` +
81
- `attributed once, to ${owner.span.id} (${owner.span.bucket})`,
82
- spanIds: ids,
158
+ kind: "scheduling-observation",
159
+ message: `${point.kind} point ${point.id} for ${workstream} has a missing, negative, or non-finite atOffsetMs skipped`,
160
+ spanIds: [],
161
+ pointIds: [point.id],
83
162
  });
163
+ continue;
84
164
  }
165
+ const map = point.kind === "workstream-ready" ? readyPoints : scheduledPoints;
166
+ const list = map.get(workstream) ?? [];
167
+ list.push({ pointId: point.id, atOffsetMs: point.atOffsetMs });
168
+ map.set(workstream, list);
85
169
  }
86
170
  }
87
- return totals;
171
+ function resolveInstant(list, pick) {
172
+ const sorted = [...list].sort((a, b) => a.atOffsetMs - b.atOffsetMs || a.pointId.localeCompare(b.pointId));
173
+ return pick === "earliest" ? sorted[0] : sorted[sorted.length - 1];
174
+ }
175
+ const readyByWorkstream = new Map();
176
+ for (const [workstream, list] of readyPoints) {
177
+ if (list.length > 1) {
178
+ diagnostics.push({
179
+ kind: "duplicate-scheduling-point",
180
+ message: `workstream ${workstream} emitted ${list.length} workstream-ready points; the earliest is kept and the rest diagnosed`,
181
+ spanIds: [],
182
+ pointIds: [...list.map((p) => p.pointId)].sort(),
183
+ });
184
+ }
185
+ readyByWorkstream.set(workstream, resolveInstant(list, "earliest"));
186
+ }
187
+ const scheduledByWorkstream = new Map();
188
+ for (const [workstream, list] of scheduledPoints) {
189
+ if (list.length > 1) {
190
+ diagnostics.push({
191
+ kind: "duplicate-scheduling-point",
192
+ message: `workstream ${workstream} emitted ${list.length} workstream-scheduled points; the latest is kept and the rest diagnosed`,
193
+ spanIds: [],
194
+ pointIds: [...list.map((p) => p.pointId)].sort(),
195
+ });
196
+ }
197
+ scheduledByWorkstream.set(workstream, resolveInstant(list, "latest"));
198
+ }
199
+ const resolvedWaits = [];
200
+ for (const [key, list] of waitPoints) {
201
+ const sep = key.indexOf(" ");
202
+ const workstream = key.slice(0, sep);
203
+ const cause = key.slice(sep + 1);
204
+ if (list.length > 1) {
205
+ diagnostics.push({
206
+ kind: "duplicate-scheduling-point",
207
+ message: `workstream ${workstream} emitted ${list.length} workstream-waiting points for cause "${cause}"; ` +
208
+ "their durations were summed and the anomaly diagnosed",
209
+ spanIds: [],
210
+ pointIds: [...list.map((p) => p.pointId)].sort(),
211
+ });
212
+ }
213
+ const waitedMs = list.reduce((sum, p) => sum + p.waitedMs, 0);
214
+ const endMs = list.reduce((max, p) => Math.max(max, p.atOffsetMs), 0);
215
+ resolvedWaits.push({
216
+ workstream,
217
+ cause,
218
+ waitedMs,
219
+ endMs,
220
+ derived: false,
221
+ pointIds: [...list.map((p) => p.pointId)].sort(),
222
+ });
223
+ }
224
+ const allScheduleWorkstreams = new Set([
225
+ ...readyByWorkstream.keys(),
226
+ ...scheduledByWorkstream.keys(),
227
+ ...resolvedWaits.map((w) => w.workstream),
228
+ ]);
229
+ // The `scheduled − ready === concurrency-cap` identity (rule 4, §3.3).
230
+ for (const workstream of allScheduleWorkstreams) {
231
+ const ready = readyByWorkstream.get(workstream);
232
+ const scheduled = scheduledByWorkstream.get(workstream);
233
+ if (scheduled === undefined)
234
+ continue;
235
+ if (ready === undefined) {
236
+ diagnostics.push({
237
+ kind: "scheduling-identity",
238
+ message: `workstream ${workstream} has a workstream-scheduled point with no matching workstream-ready point`,
239
+ spanIds: [],
240
+ pointIds: [scheduled.pointId],
241
+ });
242
+ continue;
243
+ }
244
+ const capWaitMs = resolvedWaits.find((w) => w.workstream === workstream && w.cause === "concurrency-cap")?.waitedMs ?? 0;
245
+ const diff = scheduled.atOffsetMs - ready.atOffsetMs;
246
+ if (Math.abs(diff - capWaitMs) > SCHEDULING_IDENTITY_TOLERANCE_MS) {
247
+ diagnostics.push({
248
+ kind: "scheduling-identity",
249
+ message: `workstream ${workstream}: scheduled − ready is ${diff}ms but the concurrency-cap wait is ${capWaitMs}ms ` +
250
+ `(beyond the ${SCHEDULING_IDENTITY_TOLERANCE_MS}ms tolerance)`,
251
+ spanIds: [],
252
+ pointIds: [ready.pointId, scheduled.pointId],
253
+ });
254
+ }
255
+ }
256
+ // The derived `dependency-landing` wait (§3.4).
257
+ const depsReadyByWorkstream = new Map();
258
+ for (const [workstream, ready] of readyByWorkstream) {
259
+ const specPendingMs = resolvedWaits.find((w) => w.workstream === workstream && w.cause === "spec-pending")?.waitedMs ?? 0;
260
+ depsReadyByWorkstream.set(workstream, ready.atOffsetMs - specPendingMs);
261
+ }
262
+ const schedulerStartMs = depsReadyByWorkstream.size > 0 ? Math.min(...depsReadyByWorkstream.values()) : undefined;
263
+ const derivedWaits = [];
264
+ if (schedulerStartMs !== undefined) {
265
+ for (const [workstream, depsReadyMs] of [...depsReadyByWorkstream].sort(([a], [b]) => a.localeCompare(b))) {
266
+ const landingMs = Math.max(0, depsReadyMs - schedulerStartMs);
267
+ if (landingMs > 0) {
268
+ derivedWaits.push({ workstream, cause: "dependency-landing", waitedMs: landingMs, endMs: depsReadyMs, derived: true, pointIds: [] });
269
+ }
270
+ }
271
+ }
272
+ const allWaits = [...resolvedWaits, ...derivedWaits];
273
+ const scheduleWorkstreams = new Set([...allScheduleWorkstreams, ...derivedWaits.map((w) => w.workstream)]);
274
+ const schedules = [...scheduleWorkstreams].sort().map((workstream) => {
275
+ const ready = readyByWorkstream.get(workstream);
276
+ const scheduled = scheduledByWorkstream.get(workstream);
277
+ const depsReadyMs = depsReadyByWorkstream.get(workstream);
278
+ const waits = allWaits
279
+ .filter((w) => w.workstream === workstream)
280
+ .sort((a, b) => a.cause.localeCompare(b.cause))
281
+ .map((w) => ({
282
+ workstream: w.workstream,
283
+ cause: w.cause,
284
+ waitedMs: w.waitedMs,
285
+ endMs: w.endMs,
286
+ derived: w.derived,
287
+ ...(!w.derived && w.pointIds.length > 0 ? { pointId: w.pointIds[0] } : {}),
288
+ }));
289
+ return {
290
+ workstream,
291
+ ...(ready !== undefined ? { readyMs: ready.atOffsetMs } : {}),
292
+ ...(depsReadyMs !== undefined ? { depsReadyMs } : {}),
293
+ ...(scheduled !== undefined ? { scheduledMs: scheduled.atOffsetMs } : {}),
294
+ ...(ready !== undefined && scheduled !== undefined ? { queuedMs: scheduled.atOffsetMs - ready.atOffsetMs } : {}),
295
+ waits,
296
+ };
297
+ });
298
+ function waitCauseTier(cause) {
299
+ if (KNOWN_WAIT_CAUSES.includes(cause))
300
+ return 0;
301
+ if (cause === "dependency-landing")
302
+ return 1;
303
+ return 2;
304
+ }
305
+ const causeTotals = new Map();
306
+ for (const w of allWaits) {
307
+ const entry = causeTotals.get(w.cause) ?? { totalMs: 0, workstreams: new Set(), derived: w.derived };
308
+ entry.totalMs += w.waitedMs;
309
+ entry.workstreams.add(w.workstream);
310
+ causeTotals.set(w.cause, entry);
311
+ }
312
+ const waitsByCause = [...causeTotals.entries()]
313
+ .map(([cause, v]) => ({ cause, totalMs: v.totalMs, workstreams: v.workstreams.size, derived: v.derived }))
314
+ .sort((a, b) => {
315
+ const tierDiff = waitCauseTier(a.cause) - waitCauseTier(b.cause);
316
+ if (tierDiff !== 0)
317
+ return tierDiff;
318
+ if (waitCauseTier(a.cause) === 0) {
319
+ return KNOWN_WAIT_CAUSES.indexOf(a.cause) - KNOWN_WAIT_CAUSES.indexOf(b.cause);
320
+ }
321
+ return a.cause.localeCompare(b.cause);
322
+ });
323
+ return { schedules, waitsByCause, ...(schedulerStartMs !== undefined ? { schedulerStartMs } : {}), allWaits };
324
+ }
325
+ /**
326
+ * Time-weighted distribution of how many *distinct* workstreams were live at
327
+ * once (§3.7), over top-level intervals that carry a `workstream` — run-level
328
+ * spans are excluded entirely. Reuses the boundary-sweep technique from
329
+ * `sweepBucketOwnership`, counting distinct workstream ids rather than spans
330
+ * so a same-workstream overlap (already diagnosed in §3.2) is never counted
331
+ * as parallelism.
332
+ */
333
+ function sweepWorkstreamConcurrency(topLevelIntervals) {
334
+ const withWorkstream = topLevelIntervals.filter((interval) => interval.span.dimensions.workstream !== undefined);
335
+ const cumulativeWorkstreamMs = withWorkstream.reduce((sum, interval) => sum + (interval.end - interval.start), 0);
336
+ if (withWorkstream.length === 0) {
337
+ return { meanWorkstreamConcurrency: 0, peakWorkstreamConcurrency: 0, overlappedMs: 0, cumulativeWorkstreamMs: 0 };
338
+ }
339
+ const boundarySet = new Set();
340
+ for (const interval of withWorkstream) {
341
+ boundarySet.add(interval.start);
342
+ boundarySet.add(interval.end);
343
+ }
344
+ const boundaries = [...boundarySet].sort((a, b) => a - b);
345
+ let peak = 0;
346
+ let overlappedMs = 0;
347
+ let weightedSum = 0;
348
+ let coveredLength = 0;
349
+ for (let i = 0; i < boundaries.length - 1; i++) {
350
+ const a = boundaries[i];
351
+ const b = boundaries[i + 1];
352
+ if (b <= a)
353
+ continue;
354
+ const distinct = new Set(withWorkstream.filter((interval) => interval.start <= a && interval.end >= b).map((interval) => interval.span.dimensions.workstream));
355
+ const count = distinct.size;
356
+ if (count === 0)
357
+ continue;
358
+ const length = b - a;
359
+ peak = Math.max(peak, count);
360
+ if (count >= 2)
361
+ overlappedMs += length;
362
+ weightedSum += count * length;
363
+ coveredLength += length;
364
+ }
365
+ const meanWorkstreamConcurrency = coveredLength > 0 ? weightedSum / coveredLength : 0;
366
+ return { meanWorkstreamConcurrency, peakWorkstreamConcurrency: peak, overlappedMs, cumulativeWorkstreamMs };
367
+ }
368
+ function ownerKeyOf(workstream) {
369
+ return workstream ?? "\u0000run-level";
370
+ }
371
+ function covers(start, end, cursor) {
372
+ return start < cursor && end >= cursor;
373
+ }
374
+ /**
375
+ * Backward walk from the end of the run (§3.6): at each step, either the
376
+ * current owner's own work explains the instant before the cursor (step 1),
377
+ * or another flow was holding the clock instead — the *truthful* leg for a
378
+ * cap/mutex wait, since somebody else was running, not nobody (step 2) — or,
379
+ * only when nothing at all was running, a genuine idle wait (step 3) — or,
380
+ * failing all three, an unexplained gap (step 4). Every branch strictly
381
+ * decreases the cursor, and an explicit `budget` backstops pathological
382
+ * input against a hang or a silently truncated path.
383
+ */
384
+ function computeCriticalPath(topLevelIntervals, allWaits, totalElapsedMs) {
385
+ // `start` is clamped into [0, totalElapsedMs] defensively: a well-formed
386
+ // artifact never has a negative startOffsetMs, but a pathological one must
387
+ // still tile [0, totalElapsedMs) exactly rather than producing a leg whose
388
+ // startMs is negative.
389
+ const stages = topLevelIntervals.map((interval) => ({
390
+ kind: "stage",
391
+ workstream: interval.span.dimensions.workstream,
392
+ stage: interval.span.stage,
393
+ spanId: interval.span.id,
394
+ start: Math.min(Math.max(interval.start, 0), totalElapsedMs),
395
+ end: Math.min(Math.max(interval.end, 0), totalElapsedMs),
396
+ }));
397
+ const waits = allWaits
398
+ .map((w) => {
399
+ const rawStart = w.endMs - w.waitedMs;
400
+ const start = Math.min(Math.max(rawStart, 0), totalElapsedMs);
401
+ const end = Math.min(Math.max(w.endMs, 0), totalElapsedMs);
402
+ return {
403
+ workstream: w.workstream,
404
+ cause: w.cause,
405
+ pointId: w.derived ? undefined : w.pointIds[0],
406
+ derived: w.derived,
407
+ start,
408
+ end,
409
+ };
410
+ })
411
+ .filter((w) => w.end > w.start);
412
+ const allEnds = [...stages.map((s) => s.end), ...waits.map((w) => w.end)];
413
+ const legs = [];
414
+ let cursor = totalElapsedMs;
415
+ let owner;
416
+ let budget = stages.length + waits.length + 8;
417
+ while (cursor > 0 && budget-- > 0) {
418
+ const ownerKey = ownerKeyOf(owner);
419
+ // Step 1: the owner's own work explains it.
420
+ let step1;
421
+ for (const s of stages) {
422
+ if (ownerKeyOf(s.workstream) !== ownerKey || !covers(s.start, s.end, cursor))
423
+ continue;
424
+ if (step1 === undefined ||
425
+ s.start > step1.start ||
426
+ (s.start === step1.start && (s.end < step1.end || (s.end === step1.end && s.spanId < step1.spanId)))) {
427
+ step1 = s;
428
+ }
429
+ }
430
+ if (step1 !== undefined) {
431
+ legs.push({
432
+ startMs: step1.start,
433
+ endMs: cursor,
434
+ durationMs: cursor - step1.start,
435
+ kind: "stage",
436
+ ...(step1.workstream !== undefined ? { workstream: step1.workstream } : {}),
437
+ stage: step1.stage,
438
+ spanId: step1.spanId,
439
+ });
440
+ cursor = step1.start;
441
+ continue;
442
+ }
443
+ // Step 2: somebody else held the clock.
444
+ let step2;
445
+ for (const s of stages) {
446
+ if (ownerKeyOf(s.workstream) === ownerKey || !covers(s.start, s.end, cursor))
447
+ continue;
448
+ if (step2 === undefined ||
449
+ s.end < step2.end ||
450
+ (s.end === step2.end && (s.start > step2.start || (s.start === step2.start && s.spanId < step2.spanId)))) {
451
+ step2 = s;
452
+ }
453
+ }
454
+ if (step2 !== undefined) {
455
+ const ownerWait = owner === undefined
456
+ ? undefined
457
+ : waits
458
+ .filter((w) => w.workstream === owner && covers(w.start, w.end, cursor))
459
+ .sort((a, b) => b.start - a.start || a.end - b.end || a.cause.localeCompare(b.cause))[0];
460
+ const previousLeg = legs.length > 0 ? legs[legs.length - 1] : undefined;
461
+ if (previousLeg !== undefined && previousLeg.kind !== "gap") {
462
+ previousLeg.handoff = {
463
+ ...(step2.workstream !== undefined ? { fromWorkstream: step2.workstream } : {}),
464
+ cause: ownerWait?.cause ?? "concurrent-work",
465
+ causeObserved: ownerWait !== undefined,
466
+ ...(ownerWait?.pointId !== undefined ? { pointId: ownerWait.pointId } : {}),
467
+ };
468
+ }
469
+ legs.push({
470
+ startMs: step2.start,
471
+ endMs: cursor,
472
+ durationMs: cursor - step2.start,
473
+ kind: "stage",
474
+ ...(step2.workstream !== undefined ? { workstream: step2.workstream } : {}),
475
+ stage: step2.stage,
476
+ spanId: step2.spanId,
477
+ });
478
+ owner = step2.workstream;
479
+ cursor = step2.start;
480
+ continue;
481
+ }
482
+ // Step 3: nothing was running — a genuine idle wait.
483
+ const step3 = owner === undefined
484
+ ? undefined
485
+ : waits
486
+ .filter((w) => w.workstream === owner && covers(w.start, w.end, cursor))
487
+ .sort((a, b) => b.start - a.start || a.end - b.end || a.cause.localeCompare(b.cause))[0];
488
+ if (step3 !== undefined) {
489
+ legs.push({
490
+ startMs: step3.start,
491
+ endMs: cursor,
492
+ durationMs: cursor - step3.start,
493
+ kind: "wait",
494
+ workstream: step3.workstream,
495
+ waitCause: step3.cause,
496
+ ...(step3.pointId !== undefined ? { pointId: step3.pointId } : {}),
497
+ });
498
+ cursor = step3.start;
499
+ continue;
500
+ }
501
+ // Step 4: nothing explains it at all.
502
+ const prevEnd = allEnds.filter((end) => end < cursor).reduce((max, end) => Math.max(max, end), 0);
503
+ legs.push({ startMs: prevEnd, endMs: cursor, durationMs: cursor - prevEnd, kind: "gap" });
504
+ cursor = prevEnd;
505
+ }
506
+ if (cursor > 0) {
507
+ legs.push({ startMs: 0, endMs: cursor, durationMs: cursor, kind: "gap" });
508
+ }
509
+ return legs.reverse();
88
510
  }
89
511
  const AGENT_ATTEMPT_STAGES = new Set(["implementer", "recovery", "informed-retry", "fix-now", "test-critique-fix"]);
90
512
  const AGENT_REPEAT_REASONS = new Set([
@@ -169,12 +591,24 @@ export function aggregateRunAnalytics(artifact) {
169
591
  const unattributedMs = Math.max(0, totalElapsedMs - attributedMs);
170
592
  bucketTotals.set("unattributed", unattributedMs);
171
593
  const spanCountByBucket = new Map(BUCKETS.map((bucket) => [bucket, 0]));
594
+ // `cumulativeByBucket` (§3.1) is computed from the exact same clamped
595
+ // `topLevelIntervals` the conserved `durationMs` uses — never the raw
596
+ // `endOffsetMs − startOffsetMs` — so an open or overrunning span's
597
+ // cumulative figure matches its owned duration on a serial run, keeping
598
+ // `cumulativeMs === durationMs` there for every bucket (the identity the
599
+ // renderer's self-gating suffix relies on).
600
+ const cumulativeByBucket = new Map(BUCKETS.map((bucket) => [bucket, 0]));
172
601
  for (const interval of topLevelIntervals) {
173
602
  spanCountByBucket.set(interval.span.bucket, (spanCountByBucket.get(interval.span.bucket) ?? 0) + 1);
603
+ cumulativeByBucket.set(interval.span.bucket, (cumulativeByBucket.get(interval.span.bucket) ?? 0) + (interval.end - interval.start));
174
604
  }
605
+ // `unattributed` is a residual, not a set of spans (§3.1): its cumulative
606
+ // figure is its own duration, and its span count stays 0.
607
+ cumulativeByBucket.set("unattributed", unattributedMs);
175
608
  const buckets = BUCKETS.map((bucket) => ({
176
609
  bucket,
177
610
  durationMs: bucketTotals.get(bucket) ?? 0,
611
+ cumulativeMs: cumulativeByBucket.get(bucket) ?? 0,
178
612
  percent: totalElapsedMs > 0 ? ((bucketTotals.get(bucket) ?? 0) / totalElapsedMs) * 100 : 0,
179
613
  spanCount: spanCountByBucket.get(bucket) ?? 0,
180
614
  }));
@@ -428,6 +862,48 @@ export function aggregateRunAnalytics(artifact) {
428
862
  ref: ref.ref,
429
863
  ...(ref.note !== undefined ? { note: ref.note } : {}),
430
864
  }));
865
+ // WS-06: cumulative (non-conserved) agent/verification time, at any span
866
+ // depth. `role` is not the same as "agent" — a verification-command span
867
+ // stamped with `role` (verifyAttempt) must land in the verification total,
868
+ // never the agent one, or every verify pass would inflate agent time.
869
+ let cumulativeAgentMs = 0;
870
+ let agentSpanCount = 0;
871
+ let cumulativeVerificationMs = 0;
872
+ let verificationSpanCount = 0;
873
+ for (const span of artifact.spans) {
874
+ const durationMs = Math.max(0, (span.endOffsetMs ?? totalElapsedMs) - span.startOffsetMs);
875
+ if (VERIFY_COMMAND_STAGES.has(span.stage)) {
876
+ // Role is not required here: an ordinary roleless verification-stage
877
+ // span (a deterministic check the runner ran itself) still counts.
878
+ // Role matters only to keep verification spans out of the agent total.
879
+ cumulativeVerificationMs += durationMs;
880
+ verificationSpanCount += 1;
881
+ }
882
+ else if (span.dimensions.role !== undefined) {
883
+ cumulativeAgentMs += durationMs;
884
+ agentSpanCount += 1;
885
+ }
886
+ }
887
+ // WS-06: reconcile the scheduling observations, sweep parallelism, and walk
888
+ // the critical path — all pure functions of the artifact already parsed
889
+ // above; see each function's own doc comment for the contract.
890
+ const scheduling = reconcileScheduling(artifact, diagnostics);
891
+ const parallelism = sweepWorkstreamConcurrency(topLevelIntervals);
892
+ const criticalPath = computeCriticalPath(topLevelIntervals, scheduling.allWaits, totalElapsedMs);
893
+ const concurrency = {
894
+ meanWorkstreamConcurrency: parallelism.meanWorkstreamConcurrency,
895
+ peakWorkstreamConcurrency: parallelism.peakWorkstreamConcurrency,
896
+ overlappedMs: parallelism.overlappedMs,
897
+ cumulativeAgentMs,
898
+ agentSpanCount,
899
+ cumulativeVerificationMs,
900
+ verificationSpanCount,
901
+ cumulativeWorkstreamMs: parallelism.cumulativeWorkstreamMs,
902
+ ...(scheduling.schedulerStartMs !== undefined ? { schedulerStartMs: scheduling.schedulerStartMs } : {}),
903
+ schedules: scheduling.schedules,
904
+ waitsByCause: scheduling.waitsByCause,
905
+ criticalPath,
906
+ };
431
907
  // Diagnostics are pushed as each condition is discovered while walking
432
908
  // `artifact.spans` in array order (unresolved-parent, invalid-interval,
433
909
  // interval-exceeds-total); `sweepBucketOwnership`'s overlap diagnostics are
@@ -435,9 +911,14 @@ export function aggregateRunAnalytics(artifact) {
435
911
  // other three are not — sorted here so the full summary stays
436
912
  // order-independent (SC-09): a shuffled `spans` array must never change
437
913
  // which diagnostics were found or their relative order, only the pass that
438
- // happened to find them first.
914
+ // happened to find them first. `reconcileScheduling`'s diagnostics are
915
+ // already order-independent too (grouped by (workstream, kind[, cause])
916
+ // key, never by array position), but the shared sort still needs a
917
+ // `pointIds` tie-break so two point-derived diagnostics with empty
918
+ // `spanIds` sort deterministically against each other.
439
919
  diagnostics.sort((a, b) => a.kind.localeCompare(b.kind) ||
440
920
  a.spanIds.join(",").localeCompare(b.spanIds.join(",")) ||
921
+ (a.pointIds ?? []).join(",").localeCompare((b.pointIds ?? []).join(",")) ||
441
922
  a.message.localeCompare(b.message));
442
923
  return {
443
924
  totalElapsedMs,
@@ -470,6 +951,7 @@ export function aggregateRunAnalytics(artifact) {
470
951
  ...(publish !== undefined ? { publish } : {}),
471
952
  evidence,
472
953
  evidenceTotalCount: evidenceMap.size,
954
+ concurrency,
473
955
  };
474
956
  }
475
957
  // ---------------------------------------------------------------------------
@@ -490,6 +972,108 @@ function formatDuration(ms) {
490
972
  function plural(count, noun) {
491
973
  return `${count} ${noun}${count === 1 ? "" : "s"}`;
492
974
  }
975
+ /**
976
+ * One clause per known wait cause, for the "Where the time went" wait table.
977
+ * A cause with no entry here still renders — with no gloss — because the
978
+ * cause vocabulary is open (§3.3, §3.9): forgetting to add a gloss for a new
979
+ * cause degrades legibility, never correctness.
980
+ */
981
+ const WAIT_CAUSE_GLOSS = {
982
+ dependency: "an author deferred re-authoring on an in-flight sibling's spec",
983
+ "concurrency-cap": "the shared --max-parallel budget was already saturated",
984
+ "integration-mutex": "another workstream held the serialized integration step",
985
+ "spec-pending": "dependencies had integrated but this workstream's own spec was still being authored",
986
+ "author-concurrency-cap": "the authoring half of the shared --max-parallel budget was saturated",
987
+ "dependency-landing": "derived by this report from the scheduler's derived start — not measured by the runner",
988
+ };
989
+ const CRITICAL_PATH_LEG_LIMIT = 40;
990
+ function describeLeg(leg) {
991
+ switch (leg.kind) {
992
+ case "stage":
993
+ return leg.workstream !== undefined ? `${leg.workstream} ${leg.stage}` : `run-level ${leg.stage}`;
994
+ case "wait":
995
+ return `${leg.workstream} waiting (${leg.waitCause})`;
996
+ case "gap":
997
+ return "unexplained gap — nothing observed covers this interval";
998
+ }
999
+ }
1000
+ function describeHandoff(leg) {
1001
+ if (leg.handoff === undefined)
1002
+ return "";
1003
+ const from = leg.handoff.fromWorkstream ?? "run-level work";
1004
+ const reason = leg.handoff.causeObserved
1005
+ ? `that flow's own recorded wait was on \`${leg.handoff.cause}\``
1006
+ : "no wait was recorded across this boundary";
1007
+ return ` — took the wall clock over from ${from} (${reason})`;
1008
+ }
1009
+ /** Renders the gated "Concurrency and the critical path" subsection (§3.9) — only ever called when `peakWorkstreamConcurrency > 1`. */
1010
+ function renderConcurrencySection(summary) {
1011
+ const c = summary.concurrency;
1012
+ const lines = ["### Concurrency and the critical path", ""];
1013
+ lines.push("Two or more workstreams ran at once in this run. The bucket table above is a partition of wall-clock " +
1014
+ "time, so it says how the run's length was spent, not how much work was done — concurrent work shares " +
1015
+ "the same wall clock.", "");
1016
+ lines.push(`Parallelism: **${c.meanWorkstreamConcurrency.toFixed(1)}×** mean workstream concurrency (peak ` +
1017
+ `${c.peakWorkstreamConcurrency}). ${formatDuration(c.overlappedMs)} of the ${formatDuration(summary.totalElapsedMs)} ` +
1018
+ "elapsed had two or more workstreams live.", "");
1019
+ lines.push(`Cumulative agent time: **${formatDuration(c.cumulativeAgentMs)}** across ${plural(c.agentSpanCount, "spawn")}. ` +
1020
+ `Cumulative verification time: **${formatDuration(c.cumulativeVerificationMs)}** across ` +
1021
+ `${plural(c.verificationSpanCount, "invocation")}. Both are sums over concurrent work and are deliberately ` +
1022
+ "non-conserved — they can exceed the total elapsed above because the run did more work than it spent time.", "");
1023
+ lines.push("The chain below is reconstructed from observed time: the run records why a workstream was waiting, not " +
1024
+ "which workstream released it, so each step names the flow that held the wall clock before it rather than " +
1025
+ "a recorded cause.", "");
1026
+ const legs = c.criticalPath;
1027
+ const shown = legs.length > CRITICAL_PATH_LEG_LIMIT ? legs.slice(0, CRITICAL_PATH_LEG_LIMIT - 1) : legs;
1028
+ for (const leg of shown) {
1029
+ lines.push(`- ${formatDuration(leg.startMs)}–${formatDuration(leg.endMs)}: ${describeLeg(leg)}${describeHandoff(leg)}`);
1030
+ }
1031
+ if (legs.length > CRITICAL_PATH_LEG_LIMIT) {
1032
+ const rest = legs.slice(CRITICAL_PATH_LEG_LIMIT - 1);
1033
+ const restMs = rest.reduce((sum, leg) => sum + leg.durationMs, 0);
1034
+ lines.push(`- other: ${plural(rest.length, "further leg")}, chronologically after the above, spanning ` +
1035
+ `${formatDuration(restMs)} combined (not shown individually)`);
1036
+ }
1037
+ lines.push("");
1038
+ lines.push("Wait totals, by cause — per-cause totals, **not a partition of the wall clock**: two of these causes are " +
1039
+ "measured on the author item and lie inside the same workstream's `spec-pending` window, so these rows " +
1040
+ "must never be summed together or into a run total.", "");
1041
+ if (c.waitsByCause.length === 0) {
1042
+ lines.push("No scheduling waits were observed.");
1043
+ }
1044
+ else {
1045
+ for (const wc of c.waitsByCause) {
1046
+ const gloss = WAIT_CAUSE_GLOSS[wc.cause];
1047
+ lines.push(`- \`${wc.cause}\`${wc.derived ? " (derived)" : ""}: ${formatDuration(wc.totalMs)} across ` +
1048
+ `${plural(wc.workstreams, "workstream")}${gloss ? ` — ${gloss}` : ""}`);
1049
+ }
1050
+ }
1051
+ lines.push("");
1052
+ lines.push("Per-workstream schedule:", "");
1053
+ if (c.schedules.length === 0) {
1054
+ lines.push("No scheduling observations were recorded for any workstream.");
1055
+ }
1056
+ else {
1057
+ for (const s of c.schedules) {
1058
+ const parts = [];
1059
+ parts.push(s.readyMs !== undefined ? `ready at ${formatDuration(s.readyMs)}` : "no ready instant observed");
1060
+ if (s.scheduledMs !== undefined) {
1061
+ parts.push(`scheduled at ${formatDuration(s.scheduledMs)}`);
1062
+ if (s.queuedMs !== undefined)
1063
+ parts.push(`queued ${formatDuration(s.queuedMs)}`);
1064
+ }
1065
+ else if (s.readyMs !== undefined) {
1066
+ parts.push("became ready but was never scheduled in this run");
1067
+ }
1068
+ lines.push(`- **${s.workstream}**: ${parts.join(", ")}`);
1069
+ for (const w of s.waits) {
1070
+ lines.push(` - \`${w.cause}\`: ${formatDuration(w.waitedMs)}${w.derived ? " (derived by this report, not measured by the runner)" : ""}`);
1071
+ }
1072
+ }
1073
+ }
1074
+ lines.push("");
1075
+ return lines;
1076
+ }
493
1077
  /**
494
1078
  * Renders the aggregated summary into the "Where the time went" section
495
1079
  * (SC-10, SC-11) — human-first, concise, and never a score or a grade.
@@ -501,9 +1085,17 @@ export function renderWhereTheTimeWent(summary) {
501
1085
  "excluded from this total and, when observed, recorded separately (see Publish below).", "");
502
1086
  lines.push("### Time by bucket", "");
503
1087
  for (const bucket of summary.buckets) {
504
- lines.push(`- **${bucket.bucket}**: ${formatDuration(bucket.durationMs)} (${bucket.percent.toFixed(1)}%, ${plural(bucket.spanCount, "span")})`);
1088
+ const cumulativeSuffix = bucket.cumulativeMs !== bucket.durationMs ? ` (cumulative ${formatDuration(bucket.cumulativeMs)})` : "";
1089
+ lines.push(`- **${bucket.bucket}**: ${formatDuration(bucket.durationMs)} (${bucket.percent.toFixed(1)}%, ${plural(bucket.spanCount, "span")})${cumulativeSuffix}`);
505
1090
  }
506
1091
  lines.push("");
1092
+ // WS-06: gated on observed overlap — a serial run (including a
1093
+ // `--max-parallel 1` run) always has `peakWorkstreamConcurrency <= 1`, so
1094
+ // this section, and the bucket-line cumulative suffix above, are never
1095
+ // present there (SC-09, SC-11).
1096
+ if (summary.concurrency.peakWorkstreamConcurrency > 1) {
1097
+ lines.push(...renderConcurrencySection(summary));
1098
+ }
507
1099
  lines.push("### Largest contributors", "");
508
1100
  const topStage = summary.rankings.byStage[0];
509
1101
  lines.push(topStage