@gobing-ai/spur 0.4.2 → 0.4.4

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 (168) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/decisions/maker-liveness.yaml +39 -0
  3. package/config/plugin-scripts.json +0 -4
  4. package/config/rules/README.md +1 -0
  5. package/config/rules/docs/sourced-api-claims.yaml +56 -12
  6. package/config/rules/docs/verdict-confidence.yaml +68 -0
  7. package/config/rules/quality/coverage-gate.yaml +13 -0
  8. package/config/rules/recommended-pre-check.yaml +6 -2
  9. package/config/rules/strict/runtime-boundaries.yaml +10 -0
  10. package/config/rules/surface/check-cli-surface.yaml +1 -3
  11. package/config/script-placement-baseline.json +1 -19
  12. package/config/templates/AGENTS.md +7 -0
  13. package/config/workflows/feature-lifecycle.yaml +46 -1
  14. package/config/workflows/feature-verification.yaml +20 -1
  15. package/config/workflows/task-lifecycle.yaml +50 -0
  16. package/config/workflows/task-pipeline.yaml +15 -9
  17. package/package.json +10 -10
  18. package/plugins/sp/README.md +22 -16
  19. package/plugins/sp/agents/super-reviewer.md +46 -0
  20. package/plugins/sp/bunfig.toml +8 -5
  21. package/plugins/sp/commands/dev-changelog.md +1 -1
  22. package/plugins/sp/commands/dev-gitmsg.md +6 -2
  23. package/plugins/sp/commands/dev-handover.md +1 -0
  24. package/plugins/sp/commands/dev-job-dump.md +2 -1
  25. package/plugins/sp/commands/dev-refine.md +14 -2
  26. package/plugins/sp/commands/dev-refineall.md +7 -1
  27. package/plugins/sp/commands/dev-review.md +1 -0
  28. package/plugins/sp/hooks/careful-guard.ts +1 -1
  29. package/plugins/sp/hooks/context-post-tool.ts +7 -427
  30. package/plugins/sp/hooks/context-session-start.ts +5 -213
  31. package/plugins/sp/hooks/context-session-stop.ts +5 -191
  32. package/plugins/sp/hooks/pi/guard-extension.ts +6 -5
  33. package/plugins/sp/hooks/task-write-guard.ts +1 -1
  34. package/plugins/sp/{hooks → lib}/agent-hint.ts +1 -1
  35. package/plugins/sp/lib/context-post-tool.ts +432 -0
  36. package/plugins/sp/lib/context-session-start.ts +214 -0
  37. package/plugins/sp/lib/context-session-stop.ts +196 -0
  38. package/plugins/sp/lib/dogfood-pipeline.ts +265 -0
  39. package/plugins/sp/lib/dogfood-report.ts +278 -0
  40. package/plugins/sp/lib/history-anatomy.generated.mjs +18 -18
  41. package/plugins/sp/lib/idea-handoff.generated.mjs +91 -91
  42. package/plugins/sp/lib/inline-run-app.ts +40 -0
  43. package/plugins/sp/lib/inline-run.generated.d.mts +3 -3
  44. package/plugins/sp/lib/inline-run.generated.mjs +1425 -212
  45. package/plugins/sp/lib/plan-projection.generated.d.mts +150 -0
  46. package/plugins/sp/lib/plan-projection.generated.mjs +623 -9
  47. package/plugins/sp/lib/quality-gate.generated.d.mts +1 -0
  48. package/plugins/sp/lib/quality-gate.generated.mjs +13809 -13969
  49. package/plugins/sp/lib/residual-scan.generated.d.mts +20 -1
  50. package/plugins/sp/lib/residual-scan.generated.mjs +120 -25
  51. package/plugins/sp/lib/run-summary-core.ts +34 -370
  52. package/plugins/sp/lib/step-profile.generated.mjs +8 -8
  53. package/plugins/sp/lib/transcript.ts +37 -228
  54. package/plugins/sp/plugin.json +1 -1
  55. package/plugins/sp/scripts/batch-plan.mjs +619 -11
  56. package/plugins/sp/scripts/batch-preflight.mjs +57 -13
  57. package/plugins/sp/scripts/batch-preflight.ts +35 -295
  58. package/plugins/sp/scripts/commit-guard.mjs +6 -6
  59. package/plugins/sp/scripts/daily-summary/daily-summary.mjs +127 -108
  60. package/plugins/sp/scripts/daily-summary/daily-summary.ts +111 -104
  61. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.mjs +13 -12
  62. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts +7 -265
  63. package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +7 -4
  64. package/plugins/sp/scripts/dogfood-testing/validate-report.ts +6 -272
  65. package/plugins/sp/scripts/feature-verification-steps.mjs +20 -125
  66. package/plugins/sp/scripts/feature-verification-steps.ts +57 -181
  67. package/plugins/sp/scripts/history-anatomy-cache.mjs +19 -19
  68. package/plugins/sp/scripts/idea-handoff.mjs +2 -2
  69. package/plugins/sp/scripts/inline-run-dispatch.mjs +25 -24
  70. package/plugins/sp/scripts/inline-run-dispatch.ts +1 -21
  71. package/plugins/sp/scripts/inline-run-setup.mjs +164 -137
  72. package/plugins/sp/scripts/inline-run-setup.ts +1 -21
  73. package/plugins/sp/scripts/persist-out-check.mjs +5 -5
  74. package/plugins/sp/scripts/pr-reviewing.mjs +13 -13
  75. package/plugins/sp/scripts/quality-gate.mjs +13730 -13887
  76. package/plugins/sp/scripts/residual-scan.mjs +129 -31
  77. package/plugins/sp/scripts/residual-scan.ts +16 -13
  78. package/plugins/sp/scripts/run-summary.mjs +166 -112
  79. package/plugins/sp/scripts/run-summary.ts +7 -5
  80. package/plugins/sp/scripts/script-root.mjs +6 -6
  81. package/plugins/sp/scripts/session-timeline.mjs +62 -35
  82. package/plugins/sp/scripts/task-diffstat.mjs +19 -9
  83. package/plugins/sp/scripts/task-diffstat.ts +8 -39
  84. package/plugins/sp/scripts/workflow-step-profile.mjs +12 -12
  85. package/plugins/sp/scripts/wrapup-drift-probe.mjs +9 -9
  86. package/plugins/sp/scripts/wrapup-steps.mjs +25 -25
  87. package/plugins/sp/skills/code-implementation/SKILL.md +7 -1
  88. package/plugins/sp/skills/code-verification/references/secu-review.md +2 -1
  89. package/plugins/sp/skills/code-verification/references/verdict-schema.md +4 -0
  90. package/plugins/sp/skills/dogfood-testing/SKILL.md +2 -2
  91. package/plugins/sp/skills/next-feature/SKILL.md +1 -1
  92. package/plugins/sp/skills/next-router/SKILL.md +3 -1
  93. package/plugins/sp/skills/next-router/references/routing-table.md +9 -3
  94. package/plugins/sp/skills/parallel-execution/references/dispatch-surface.md +1 -1
  95. package/plugins/sp/skills/source-driven-development/SKILL.md +7 -4
  96. package/plugins/sp/skills/spur-cli/references/builder.md +3 -2
  97. package/plugins/sp/skills/spur-cli/references/decision.md +32 -7
  98. package/plugins/sp/skills/spur-cli/references/features.md +46 -5
  99. package/plugins/sp/skills/spur-cli/references/tasks.md +24 -4
  100. package/plugins/sp/skills/spur-dev/references/cross-cutting.md +8 -1
  101. package/plugins/sp/skills/spur-dev/references/dev-operations.md +37 -11
  102. package/plugins/sp/skills/spur-dev/references/execution-batch-report.md +1 -1
  103. package/plugins/sp/skills/spur-dev/references/execution-batch.md +19 -6
  104. package/plugins/sp/skills/spur-dev/references/execution-worktree-landing.md +22 -0
  105. package/plugins/sp/skills/spur-dev/references/execution-worktree-setup.md +14 -0
  106. package/plugins/sp/skills/spur-dev/references/flag-glossary.md +4 -2
  107. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +49 -12
  108. package/schemas/section-matrix.schema.json +3 -2
  109. package/spur.js +66591 -73262
  110. package/web/_astro/{BoardApp.B3GQZvj8.js → BoardApp.Niubpe6z.js} +1 -1
  111. package/web/_astro/{BoardApp.BT7L2kSb.js → BoardApp.hF0RAIrg.js} +150 -150
  112. package/web/_astro/TaskDetail.DJpapt38.js +1 -0
  113. package/web/_astro/{arc.vaT7tPOB.js → arc.BgKcK0i8.js} +1 -1
  114. package/web/_astro/{architectureDiagram-3BPJPVTR.DRMmTjkN.js → architectureDiagram-3BPJPVTR.ChP-Ig0V.js} +1 -1
  115. package/web/_astro/{blockDiagram-GPEHLZMM.cla0LO_q.js → blockDiagram-GPEHLZMM.BVaCBoeN.js} +1 -1
  116. package/web/_astro/{c4Diagram-AAUBKEIU.D4YAIBKn.js → c4Diagram-AAUBKEIU.CHSDpEO5.js} +1 -1
  117. package/web/_astro/channel.DkpvN2tD.js +1 -0
  118. package/web/_astro/{chunk-2J33WTMH.BrSJbMRZ.js → chunk-2J33WTMH.MYRcYVRa.js} +1 -1
  119. package/web/_astro/{chunk-4BX2VUAB.CuLMgKF5.js → chunk-4BX2VUAB.S5j22Zqa.js} +1 -1
  120. package/web/_astro/{chunk-55IACEB6.zkvgu54F.js → chunk-55IACEB6.gciRPvmZ.js} +1 -1
  121. package/web/_astro/{chunk-727SXJPM.DxWkACh8.js → chunk-727SXJPM.CdqrSQkX.js} +1 -1
  122. package/web/_astro/{chunk-AQP2D5EJ.42XrmIlH.js → chunk-AQP2D5EJ.CyokdHT3.js} +1 -1
  123. package/web/_astro/{chunk-FMBD7UC4.Bao2wMK9.js → chunk-FMBD7UC4.Bg9KJhjW.js} +1 -1
  124. package/web/_astro/{chunk-ND2GUHAM.BhydojD1.js → chunk-ND2GUHAM.1gxxI-vD.js} +1 -1
  125. package/web/_astro/{chunk-QZHKN3VN.CmZ82g-H.js → chunk-QZHKN3VN.DDEYtCKU.js} +1 -1
  126. package/web/_astro/{classDiagram-4FO5ZUOK.Blr83Gt_.js → classDiagram-4FO5ZUOK.Ch3zi37T.js} +1 -1
  127. package/web/_astro/{classDiagram-v2-Q7XG4LA2.Blr83Gt_.js → classDiagram-v2-Q7XG4LA2.Ch3zi37T.js} +1 -1
  128. package/web/_astro/{cose-bilkent-S5V4N54A.qmfv0SPn.js → cose-bilkent-S5V4N54A.D2q5AINI.js} +1 -1
  129. package/web/_astro/{cynefin-OW5HDTMX.BtELhXPR.js → cynefin-OW5HDTMX.BA9kC2YT.js} +1 -1
  130. package/web/_astro/{dagre-BM42HDAG.AWmaAvXo.js → dagre-BM42HDAG.CHO08nRp.js} +1 -1
  131. package/web/_astro/{diagram-2AECGRRQ.DdH34KDi.js → diagram-2AECGRRQ.BfFQt0LW.js} +1 -1
  132. package/web/_astro/{diagram-5GNKFQAL.BmNPix1k.js → diagram-5GNKFQAL.CHMwMTe4.js} +1 -1
  133. package/web/_astro/{diagram-KO2AKTUF.BNQd_Ppo.js → diagram-KO2AKTUF.BJ-7gK-w.js} +1 -1
  134. package/web/_astro/{diagram-LMA3HP47.BAPgDi0e.js → diagram-LMA3HP47.C5xqcc0t.js} +1 -1
  135. package/web/_astro/{diagram-OG6HWLK6.DZ4zyLdC.js → diagram-OG6HWLK6.C2pQjfrC.js} +1 -1
  136. package/web/_astro/{erDiagram-TEJ5UH35.Q1TySMmw.js → erDiagram-TEJ5UH35._FoNyxgC.js} +1 -1
  137. package/web/_astro/{flowDiagram-I6XJVG4X.BXhFBicG.js → flowDiagram-I6XJVG4X.B_ENkT2I.js} +1 -1
  138. package/web/_astro/{ganttDiagram-6RSMTGT7.vHSgcuo2.js → ganttDiagram-6RSMTGT7.B3q8h77C.js} +1 -1
  139. package/web/_astro/{gitGraphDiagram-PVQCEYII.Bi1f73C7.js → gitGraphDiagram-PVQCEYII.QWZFDboZ.js} +1 -1
  140. package/web/_astro/index.BFyW13JJ.css +1 -0
  141. package/web/_astro/{infoDiagram-5YYISTIA.DPvXZEWm.js → infoDiagram-5YYISTIA.Cs6eEc92.js} +1 -1
  142. package/web/_astro/{ishikawaDiagram-YF4QCWOH.Cz829fsV.js → ishikawaDiagram-YF4QCWOH.BU9DJMHA.js} +1 -1
  143. package/web/_astro/{journeyDiagram-JHISSGLW.enRnB9Me.js → journeyDiagram-JHISSGLW.D35k6Jpe.js} +1 -1
  144. package/web/_astro/{kanban-definition-UN3LZRKU.DnTNk6d2.js → kanban-definition-UN3LZRKU.DBqcAT4m.js} +1 -1
  145. package/web/_astro/{linear.BzGnBwNL.js → linear.D-LSNK8W.js} +1 -1
  146. package/web/_astro/{mermaid.core.CSbTpIVz.js → mermaid.core.CSruWVOa.js} +4 -4
  147. package/web/_astro/{mindmap-definition-RKZ34NQL.DGJDy8zT.js → mindmap-definition-RKZ34NQL.BiFaUsfj.js} +1 -1
  148. package/web/_astro/{pieDiagram-4H26LBE5.HSPdi5_8.js → pieDiagram-4H26LBE5.D1KTKqb9.js} +1 -1
  149. package/web/_astro/{quadrantDiagram-W4KKPZXB.6Gv8mz2Y.js → quadrantDiagram-W4KKPZXB.DBHflsMG.js} +1 -1
  150. package/web/_astro/{requirementDiagram-4Y6WPE33.CnBCtFwa.js → requirementDiagram-4Y6WPE33.hwX9CNdk.js} +1 -1
  151. package/web/_astro/{sankeyDiagram-5OEKKPKP.5M8VWAu8.js → sankeyDiagram-5OEKKPKP.DkI8mJiE.js} +1 -1
  152. package/web/_astro/{sequenceDiagram-3UESZ5HK.DN41XN-U.js → sequenceDiagram-3UESZ5HK.DMz7jBDx.js} +1 -1
  153. package/web/_astro/{stateDiagram-AJRCARHV.ErOO3W8j.js → stateDiagram-AJRCARHV.B6Ef2xJ1.js} +1 -1
  154. package/web/_astro/{stateDiagram-v2-BHNVJYJU.B_ZHICM-.js → stateDiagram-v2-BHNVJYJU.CgoaufB9.js} +1 -1
  155. package/web/_astro/{timeline-definition-PNZ67QCA.CWdosr07.js → timeline-definition-PNZ67QCA.B3c8h1t-.js} +1 -1
  156. package/web/_astro/{vennDiagram-CIIHVFJN.BHzYjNV5.js → vennDiagram-CIIHVFJN.BdHPAxAb.js} +1 -1
  157. package/web/_astro/{wardleyDiagram-YWT4CUSO.h8x4IwPX.js → wardleyDiagram-YWT4CUSO.BrJXPPAj.js} +1 -1
  158. package/web/_astro/{xychartDiagram-2RQKCTM6.RTapW2X8.js → xychartDiagram-2RQKCTM6.CMH5yFsV.js} +1 -1
  159. package/web/index.html +2 -2
  160. package/web/spur_logo.svg +1 -1
  161. package/config/proportional-route-table.ts +0 -155
  162. package/plugins/sp/scripts/daily-summary/logger.ts +0 -28
  163. package/web/_astro/TaskDetail.Cxov1_N_.js +0 -1
  164. package/web/_astro/channel.D4nG9YLf.js +0 -1
  165. package/web/_astro/index.BhSwN8yY.css +0 -1
  166. /package/plugins/sp/{hooks → lib}/context-owner.ts +0 -0
  167. /package/plugins/sp/{hooks → lib}/destructive-policy.ts +0 -0
  168. /package/plugins/sp/{hooks → lib}/task-file-policy.ts +0 -0
@@ -15,6 +15,32 @@ function getEnvVars() {
15
15
  // plugins/sp/lib/residual-scan.generated.mjs
16
16
  import { join } from "node:path";
17
17
  import { createHash } from "node:crypto";
18
+ var CLAIM_MARKERS = [
19
+ /@[a-z0-9._-]+\/[a-z0-9._-]+/i,
20
+ /\bnode_modules\//,
21
+ /\b\d+\.\d+\.\d+\b/,
22
+ /`[A-Za-z_$][\w.$]*\s*\([^`]*\)`/
23
+ ];
24
+ var CITATION_FORMS = [
25
+ /`[^`\s]+:\d+(?:-\d+)?`/,
26
+ /`[^`]+`\s+lines?\s+\d+/i,
27
+ /\b[\w./-]+\.[A-Za-z0-9]+:\d+(?:-\d+)?\b/,
28
+ /https?:\/\/\S+/i
29
+ ];
30
+ function hasClaimMarker(evidence) {
31
+ return CLAIM_MARKERS.some((re) => re.test(evidence));
32
+ }
33
+ function hasCitationForm(evidence) {
34
+ return CITATION_FORMS.some((re) => re.test(evidence));
35
+ }
36
+ var CONFIDENCE_LEVELS = ["HIGH", "MEDIUM", "LOW"];
37
+ function extractAnswerConfidence(text) {
38
+ const matches = [...text.matchAll(/^\s*Confidence:\s*(\S+)\s*$/gim)];
39
+ if (matches.length !== 1)
40
+ return;
41
+ const value = (matches[0]?.[1] ?? "").toUpperCase();
42
+ return CONFIDENCE_LEVELS.includes(value) ? value : undefined;
43
+ }
18
44
  var MARKER_PATTERN = /TODO|FIXME|XXX|HACK/;
19
45
  var PRIORITY_PATTERN = /^P[1-4]/;
20
46
  var RANGE_PRIORITY = /^P[1-4]\s*[\u2013\u2014-]\s*P?[1-4]/;
@@ -41,11 +67,15 @@ function locationOf(locationCell, finding) {
41
67
  const backtick = finding.match(/`([^`]+)`/)?.[1];
42
68
  return backtick === undefined ? "" : normalizeAnchor(backtick);
43
69
  }
44
- function parseReviewFindings(taskContent) {
70
+ function reviewBody(taskContent) {
45
71
  const section = taskContent.split(/^### Review\b/m)[1];
46
- if (section === undefined)
72
+ return section === undefined ? null : section.split(/^### /m)[0] ?? "";
73
+ }
74
+ function parseReviewFindings(taskContent) {
75
+ const section = reviewBody(taskContent);
76
+ if (section === null)
47
77
  return [];
48
- const body = section.split(/^### /m)[0] ?? "";
78
+ const body = section;
49
79
  const out = [];
50
80
  const lines = body.split(`
51
81
  `);
@@ -158,7 +188,8 @@ function scanResiduals(inputs) {
158
188
  "review-finding": true,
159
189
  "diff-marker": base !== null,
160
190
  "unchecked-box": true,
161
- "staging-residue": true
191
+ "staging-residue": true,
192
+ "review-contract": false
162
193
  },
163
194
  items,
164
195
  counts
@@ -179,8 +210,61 @@ function blockingReviewFindings(taskContent, deferrals) {
179
210
  }));
180
211
  return classify(items, [...tableDeferrals, ...deferrals]);
181
212
  }
182
- function buildReviewGateArtifact(wbs, taskContent, deferrals) {
183
- const items = blockingReviewFindings(taskContent, deferrals);
213
+ function reviewContractViolations(taskContent) {
214
+ const violations = [];
215
+ const body = reviewBody(taskContent);
216
+ if (body !== null && extractAnswerConfidence(body.replace(/\*\*/g, "")) === undefined) {
217
+ violations.push({
218
+ location: "task-file:Review",
219
+ text: "Review report carries no single valid `**Confidence:** HIGH | MEDIUM | LOW` line — " + "state the level the report can defend (HIGH: every requirement row MET, every finding " + "dispositioned, evidence cited; MEDIUM/LOW: name the gap)."
220
+ });
221
+ }
222
+ for (const row of parseReviewFindings(taskContent)) {
223
+ if (hasClaimMarker(row.text) && !hasCitationForm(row.text)) {
224
+ violations.push({
225
+ location: row.location.length > 0 ? row.location : "task-file:Review",
226
+ text: `${row.priority} finding asserts an external API/library/version claim without a ` + "same-cell citation (add a repo `path:line`, an external named-origin form, or a URL " + `to the Finding cell): ${row.text.slice(0, 80)}`
227
+ });
228
+ }
229
+ }
230
+ return violations;
231
+ }
232
+ function reviewConfidenceViolation(reviewAnswer, ackLowConfidence) {
233
+ if (ackLowConfidence === "true")
234
+ return null;
235
+ const level = extractAnswerConfidence(reviewAnswer.replace(/\*\*/g, ""));
236
+ if (level === "HIGH" || level === "MEDIUM")
237
+ return null;
238
+ return {
239
+ location: "review-answer:Confidence",
240
+ text: level === undefined ? "The review answer carries no valid `**Confidence:** HIGH | MEDIUM | LOW` line — a " + "PASS must be graded, or the operator must set ackLowConfidence" : `The review answer graded its PASS ${level} — HIGH/MEDIUM cross unadorned, ` + "LOW/absent needs the operator’s ackLowConfidence"
241
+ };
242
+ }
243
+ function reviewGradingInput(root, reviewAnswer, ackLowConfidence, fs) {
244
+ if (reviewAnswer === undefined)
245
+ return;
246
+ const path = join(root, reviewAnswer);
247
+ let isFile = false;
248
+ try {
249
+ isFile = fs.statSync(path).isFile();
250
+ } catch {
251
+ isFile = false;
252
+ }
253
+ return { answer: isFile ? fs.readFileSync(path, "utf8") : "", ackLowConfidence };
254
+ }
255
+ function buildReviewGateArtifact(wbs, taskContent, deferrals, review) {
256
+ const confidence = review === undefined ? null : reviewConfidenceViolation(review.answer, review.ackLowConfidence);
257
+ const contractViolations = [...reviewContractViolations(taskContent), ...confidence === null ? [] : [confidence]];
258
+ const items = [
259
+ ...blockingReviewFindings(taskContent, deferrals),
260
+ ...contractViolations.map((v) => ({
261
+ id: makeItemId("review-contract", v.location, v.text),
262
+ category: "review-contract",
263
+ class: "blocking",
264
+ location: v.location,
265
+ text: v.text
266
+ }))
267
+ ];
184
268
  const blocking = items.filter((i) => i.class === "blocking");
185
269
  const counts = { blocking: 0, deferrable: 0, advisory: 0, housekeeping: 0 };
186
270
  for (const item of items)
@@ -192,7 +276,8 @@ function buildReviewGateArtifact(wbs, taskContent, deferrals) {
192
276
  "review-finding": true,
193
277
  "diff-marker": false,
194
278
  "unchecked-box": false,
195
- "staging-residue": false
279
+ "staging-residue": false,
280
+ "review-contract": true
196
281
  },
197
282
  items,
198
283
  counts
@@ -305,6 +390,7 @@ function parseScanArgs(argv, cwd, defaultTmpDir) {
305
390
  let spurBin;
306
391
  let root = cwd;
307
392
  let tmpDir = defaultTmpDir;
393
+ let reviewAnswer;
308
394
  for (let i = 0;i < argv.length; i++) {
309
395
  const a = argv[i];
310
396
  if (a === undefined)
@@ -315,6 +401,8 @@ function parseScanArgs(argv, cwd, defaultTmpDir) {
315
401
  root = argv[++i] ?? root;
316
402
  else if (a === "--tmp-dir")
317
403
  tmpDir = argv[++i] ?? tmpDir;
404
+ else if (a === "--review-answer")
405
+ reviewAnswer = argv[++i];
318
406
  else if (a === "--help" || a === "-h")
319
407
  return null;
320
408
  else if (mode === "")
@@ -322,7 +410,7 @@ function parseScanArgs(argv, cwd, defaultTmpDir) {
322
410
  else if (wbs === "")
323
411
  wbs = a;
324
412
  }
325
- return mode === "" || wbs === "" ? null : { mode, wbs, spurBin, root, tmpDir };
413
+ return mode === "" || wbs === "" ? null : { mode, wbs, spurBin, root, tmpDir, reviewAnswer };
326
414
  }
327
415
 
328
416
  // plugins/sp/lib/spur-bin.ts
@@ -332,7 +420,7 @@ function spurCommand(spurBin) {
332
420
  }
333
421
 
334
422
  // plugins/sp/scripts/residual-scan.ts
335
- var RESIDUAL_SCAN_USAGE = "usage: residual-scan.ts <scan|fold|settle|report|review-gate> <wbs> [--spur-bin <bin>] [--root <dir>] [--tmp-dir <dir>]";
423
+ var RESIDUAL_SCAN_USAGE = "usage: residual-scan.ts <scan|fold|settle|report|review-gate> <wbs> [--spur-bin <bin>] [--root <dir>] [--tmp-dir <dir>] [--review-answer <path>]";
336
424
  function run(cmd, args, cwd) {
337
425
  const result = spawnSync(cmd, args, { cwd, encoding: "utf8" });
338
426
  if (result.error !== undefined)
@@ -427,7 +515,8 @@ function reviewGateMode(opts, env, io) {
427
515
  const runDir = join2(opts.root, ".spur", "run");
428
516
  fs.mkdirSync(runDir, { recursive: true });
429
517
  const { content } = loadTask(env, opts.spurBin, opts.wbs, opts.root);
430
- const gate = buildReviewGateArtifact(opts.wbs, content, readDeferrals(runDir, opts.wbs));
518
+ const grading = reviewGradingInput(opts.root, opts.reviewAnswer, env.ackLowConfidence, fs);
519
+ const gate = buildReviewGateArtifact(opts.wbs, content, readDeferrals(runDir, opts.wbs), grading);
431
520
  fs.writeFileSync(join2(runDir, `${opts.wbs}-residuals.json`), `${JSON.stringify(gate.artifact, null, 2)}
432
521
  `);
433
522
  io.out(gate.note);
@@ -518,6 +607,12 @@ function settleMode(opts, env, io) {
518
607
  }
519
608
  function reportMode(opts, env, io) {
520
609
  const runDir = join2(opts.root, ".spur", "run");
610
+ const verdictPath = recordedVerdictPath(runDir, opts.wbs, fs);
611
+ if (!fs.existsSync(verdictPath)) {
612
+ io.err(`residual-report: no verdict at ${verdictPath}; nothing to report for ${opts.wbs}
613
+ `);
614
+ return 0;
615
+ }
521
616
  const sweep = loadVerdict(runDir, opts.wbs).checks.find((c) => c.name === "residual-sweep");
522
617
  if (sweep === undefined || sweep.status !== "fail")
523
618
  return 0;
@@ -556,26 +651,29 @@ function main(argv, env = getEnvVars(), options = {}) {
556
651
  }
557
652
  process.exit(main(process.argv.slice(2)));
558
653
  export {
559
- verdictDisagreementNote,
560
- scanResiduals2 as scanResiduals,
561
- renderReport,
562
- recordedVerdictPath,
563
- parseScanArgs,
564
- parseReviewFindings,
565
- parseDiffMarkers,
566
- parseDeferralEntries,
567
- normalizeAnchor,
568
- makeItemId,
569
- main,
570
- locationOf,
571
- listStagingResidue,
572
- foldVerdict,
573
- findUncheckedBoxes,
574
- collectAddedLines,
575
- classify,
576
- buildReviewGateArtifact,
577
- blockingReviewFindings,
578
- blockingAnchors,
654
+ ALLOW_PRAGMA,
579
655
  RESIDUAL_SCAN_USAGE,
580
- ALLOW_PRAGMA
656
+ blockingAnchors,
657
+ blockingReviewFindings,
658
+ buildReviewGateArtifact,
659
+ classify,
660
+ collectAddedLines,
661
+ findUncheckedBoxes,
662
+ foldVerdict,
663
+ listStagingResidue,
664
+ locationOf,
665
+ main,
666
+ makeItemId,
667
+ normalizeAnchor,
668
+ parseDeferralEntries,
669
+ parseDiffMarkers,
670
+ parseReviewFindings,
671
+ parseScanArgs,
672
+ recordedVerdictPath,
673
+ renderReport,
674
+ reviewConfidenceViolation,
675
+ reviewContractViolations,
676
+ reviewGradingInput,
677
+ scanResiduals2 as scanResiduals,
678
+ verdictDisagreementNote
581
679
  };
@@ -10,7 +10,7 @@ import { spurCommand } from '../lib/spur-bin';
10
10
 
11
11
  export * from '../lib/residual-scan.generated.mjs';
12
12
  export const RESIDUAL_SCAN_USAGE =
13
- 'usage: residual-scan.ts <scan|fold|settle|report|review-gate> <wbs> [--spur-bin <bin>] [--root <dir>] [--tmp-dir <dir>]';
13
+ 'usage: residual-scan.ts <scan|fold|settle|report|review-gate> <wbs> [--spur-bin <bin>] [--root <dir>] [--tmp-dir <dir>] [--review-answer <path>]';
14
14
  export type ScanEnv = { spurBin?: string } & Record<string, string | undefined>;
15
15
  export type ScanIo = { out: (line: string) => void; err: (line: string) => void };
16
16
  export type ScanOptions = { cwd?: string; io?: ScanIo };
@@ -104,18 +104,18 @@ function scanMode(opts: ParsedArgs, env: ScanEnv, io: ScanIo): number {
104
104
  return 0;
105
105
  }
106
106
  /**
107
- * Task 1122 R2: the review-time gate — the record sweep's review-finding slice with the same
108
- * parse/classify/deferrals (core.buildReviewGateArtifact), applied at the review PASS edges
109
- * before verify spends the cycle. Writes the same `<wbs>-residuals.json` artifact the test-fix
110
- * hop already appends to the gate log (its remediation input); the record sweep stays the final
111
- * authority and overwrites it. Exit 1 on an open P1-P3 finding fails the review PASS edge closed
112
- * into review-fail-triage.
107
+ * Task 1122 R2: the review-time gate — the record sweep's review-finding slice
108
+ * (core.buildReviewGateArtifact) at the review PASS edges; writes the same `<wbs>-residuals.json`
109
+ * the test-fix hop reads. Exit 1 on an open P1-P3 finding, or on a review answer graded LOW or left
110
+ * ungraded without `env.ackLowConfidence` (task 1204, graded via `core.reviewGradingInput` — it
111
+ * cannot sit inline in the edge because ADR-115 caps a guard at 5 logical commands).
113
112
  */
114
113
  function reviewGateMode(opts: ParsedArgs, env: ScanEnv, io: ScanIo): number {
115
114
  const runDir = join(opts.root, '.spur', 'run');
116
115
  fs.mkdirSync(runDir, { recursive: true });
117
116
  const { content } = loadTask(env, opts.spurBin, opts.wbs, opts.root);
118
- const gate = core.buildReviewGateArtifact(opts.wbs, content, readDeferrals(runDir, opts.wbs));
117
+ const grading = core.reviewGradingInput(opts.root, opts.reviewAnswer, env.ackLowConfidence, fs);
118
+ const gate = core.buildReviewGateArtifact(opts.wbs, content, readDeferrals(runDir, opts.wbs), grading);
119
119
  fs.writeFileSync(join(runDir, `${opts.wbs}-residuals.json`), `${JSON.stringify(gate.artifact, null, 2)}\n`);
120
120
  io.out(gate.note);
121
121
  return gate.artifact.counts.blocking > 0 ? 1 : 0;
@@ -162,11 +162,8 @@ function settleMode(opts: ParsedArgs, env: ScanEnv, io: ScanIo): number {
162
162
  if (fs.existsSync(residualsPath)) prior = JSON.parse(fs.readFileSync(residualsPath, 'utf8'));
163
163
  const deferred = scan.items.filter((i) => i.class === 'deferrable');
164
164
  if (deferred.length > 0 && prior.followUp === undefined) {
165
- // The follow-up is filed UNLINKED: carrying a feature edge to the feature whose run produced
166
- // it made that feature's done-gate fail with `L4.verifying-incomplete-tasks` the moment a
167
- // residual was deferred (2026-10-04 session, E72/1086→1087), so a deferral must not be able
168
- // to block the completing feature. The source task and its feature stay named in the
169
- // Background, which keeps the traceability without the lifecycle coupling.
165
+ // Filed UNLINKED: a feature edge failed that feature's done-gate (`L4.verifying-incomplete-tasks`,
166
+ // E72/1086→1087); the Background still names the source task and feature for traceability.
170
167
  const args = ['task', 'create', `Residuals from ${wbs}`, '--skip-ready', '--json'];
171
168
  const created = spur(env, opts.spurBin, args, opts.root);
172
169
  let wbsNew = '';
@@ -206,6 +203,12 @@ function settleMode(opts: ParsedArgs, env: ScanEnv, io: ScanIo): number {
206
203
  }
207
204
  function reportMode(opts: ParsedArgs, env: ScanEnv, io: ScanIo): number {
208
205
  const runDir = join(opts.root, '.spur', 'run');
206
+ // A fix-budget-exhausted run never reaches verify: no verdict is a no-op, not ENOENT (1178 run 1).
207
+ const verdictPath = core.recordedVerdictPath(runDir, opts.wbs, fs);
208
+ if (!fs.existsSync(verdictPath)) {
209
+ io.err(`residual-report: no verdict at ${verdictPath}; nothing to report for ${opts.wbs}\n`);
210
+ return 0;
211
+ }
209
212
  const sweep = loadVerdict(runDir, opts.wbs).checks.find((c) => c.name === 'residual-sweep');
210
213
  if (sweep === undefined || sweep.status !== 'fail') return 0;
211
214
  const { content } = loadTask(env, opts.spurBin, opts.wbs, opts.root);
@@ -13,13 +13,16 @@ function getEnvVars() {
13
13
  import { existsSync as existsSync2, mkdirSync, readFileSync, writeFileSync } from "node:fs";
14
14
  import { join as join2 } from "node:path";
15
15
 
16
- // plugins/sp/lib/transcript.ts
17
- import { existsSync, readdirSync } from "node:fs";
18
- import { homedir } from "node:os";
19
- import { join } from "node:path";
20
- var SESSION_ID = /^[A-Za-z0-9_-]+$/;
21
- var OPERATOR_TOOLS = new Set(["AskUserQuestion", "ask_user_question"]);
22
- var PI_ROLES = new Set(["user", "assistant", "toolResult"]);
16
+ // plugins/sp/lib/env.ts
17
+ function getEnvVars2() {
18
+ return process.env;
19
+ }
20
+
21
+ // plugins/sp/lib/plan-projection.generated.mjs
22
+ var NON_ABORTING_PREFLIGHT_CODES = new Set([
23
+ "L4.scenario-unverified",
24
+ "L4.verifying-incomplete-tasks"
25
+ ]);
23
26
  var zeroTokens = () => ({ input: 0, cacheCreate: 0, cacheRead: 0, output: 0 });
24
27
  function formatDuration(ms) {
25
28
  const s = Math.round(ms / 1000);
@@ -39,6 +42,26 @@ function formatTokenSplit(t) {
39
42
  const nonCached = t.input + t.cacheCreate + t.output;
40
43
  return `${formatTokens(nonCached + t.cacheRead)} / ${formatTokens(nonCached)}`;
41
44
  }
45
+ var OPERATOR_TOOLS = new Set(["AskUserQuestion", "ask_user_question"]);
46
+ function isCompaction(row) {
47
+ return row.type === "compaction" || row.type === "system" && row.subtype === "compact_boundary";
48
+ }
49
+ function firstTextContent(row) {
50
+ const content = row.message?.content;
51
+ if (typeof content === "string")
52
+ return content;
53
+ if (!Array.isArray(content))
54
+ return;
55
+ return content.find((b) => b.type === "text")?.text;
56
+ }
57
+ function skillInvocationPrompt(text) {
58
+ const name = /^\s*<skill name="([^"]+)"/.exec(text)?.[1];
59
+ if (name === undefined)
60
+ return;
61
+ const close = text.lastIndexOf("</skill>");
62
+ const args = close < 0 ? "" : text.slice(close + "</skill>".length).trim();
63
+ return args === "" ? `/${name}` : `/${name} ${args}`;
64
+ }
42
65
  function promptText(row) {
43
66
  if (row.type === "custom_message") {
44
67
  if (row.display === false)
@@ -63,22 +86,6 @@ function promptText(row) {
63
86
  return;
64
87
  return content.find((b) => b.type === "text")?.text;
65
88
  }
66
- function firstTextContent(row) {
67
- const content = row.message?.content;
68
- if (typeof content === "string")
69
- return content;
70
- if (!Array.isArray(content))
71
- return;
72
- return content.find((b) => b.type === "text")?.text;
73
- }
74
- function skillInvocationPrompt(text) {
75
- const name = /^\s*<skill name="([^"]+)"/.exec(text)?.[1];
76
- if (name === undefined)
77
- return;
78
- const close = text.lastIndexOf("</skill>");
79
- const args = close < 0 ? "" : text.slice(close + "</skill>".length).trim();
80
- return args === "" ? `/${name}` : `/${name} ${args}`;
81
- }
82
89
  function sumTokens(all) {
83
90
  const total = zeroTokens();
84
91
  for (const t of all)
@@ -107,21 +114,21 @@ function accumulate(acc, row, ts) {
107
114
  if (row.type === "message") {
108
115
  const m = row.message;
109
116
  if (m?.role === "assistant") {
110
- const blocks2 = Array.isArray(m.content) ? m.content : [];
111
- for (const b of blocks2) {
117
+ const blocks = Array.isArray(m.content) ? m.content : [];
118
+ for (const b of blocks) {
112
119
  if (b.type === "toolCall" && b.id) {
113
120
  acc.toolIds.add(b.id);
114
121
  if (b.name && OPERATOR_TOOLS.has(b.name))
115
122
  acc.asks.set(b.id, ts);
116
123
  }
117
124
  }
118
- const u2 = m.usage;
119
- if (u2 && row.id)
125
+ const u = m.usage;
126
+ if (u && row.id)
120
127
  acc.messages.set(row.id, {
121
- input: u2.input ?? 0,
122
- cacheCreate: u2.cacheWrite ?? 0,
123
- cacheRead: u2.cacheRead ?? 0,
124
- output: u2.output ?? 0
128
+ input: u.input ?? 0,
129
+ cacheCreate: u.cacheWrite ?? 0,
130
+ cacheRead: u.cacheRead ?? 0,
131
+ output: u.output ?? 0
125
132
  });
126
133
  } else if (m?.role === "toolResult" && m.toolCallId && acc.asks.has(m.toolCallId)) {
127
134
  acc.askMs += ts - (acc.asks.get(m.toolCallId) ?? ts);
@@ -150,77 +157,14 @@ function accumulate(acc, row, ts) {
150
157
  });
151
158
  }
152
159
  }
153
- function isCompaction(row) {
154
- return row.type === "compaction" || row.type === "system" && row.subtype === "compact_boundary";
155
- }
156
- function sniffFormat(rows) {
157
- for (const row of rows) {
158
- if (row.type === "message" && PI_ROLES.has(row.message?.role ?? ""))
159
- return "pi";
160
- if (row.type === "user" || row.type === "assistant")
161
- return "claude";
162
- }
163
- return "unknown";
164
- }
165
- function parseRows(lines) {
166
- const rows = [];
167
- let skippedLines = 0;
168
- for (const line of lines) {
169
- if (!line.trim())
170
- continue;
171
- let row;
172
- try {
173
- row = JSON.parse(line);
174
- } catch {
175
- skippedLines++;
176
- continue;
177
- }
178
- const ts = row.timestamp ? Date.parse(row.timestamp) : Number.NaN;
179
- if (!Number.isNaN(ts))
180
- rows.push([row, ts]);
181
- }
182
- return { rows, skippedLines, format: sniffFormat(rows.map(([r]) => r)) };
183
- }
184
- function resolveTranscript(env, projectsRoot = join(homedir(), ".claude", "projects"), override) {
185
- if (override)
186
- return existsSync(override) ? { ok: true, path: override } : { ok: false, reason: "no transcript" };
187
- const id = env.CLAUDE_CODE_SESSION_ID;
188
- if (!id) {
189
- const piFile = env.PI_SESSION_FILE;
190
- if (piFile !== undefined && piFile !== "") {
191
- return existsSync(piFile) ? { ok: true, path: piFile } : { ok: false, reason: `PI_SESSION_FILE ${piFile} does not exist` };
192
- }
193
- return {
194
- ok: false,
195
- reason: "no host session id; pass --transcript <path> (pi: ~/.pi/agent/sessions/<cwd-slug>/<file>.jsonl, " + "or set PI_SESSION_FILE)"
196
- };
197
- }
198
- if (!SESSION_ID.test(id))
199
- return { ok: false, reason: "refusing a session id with path characters" };
200
- if (!existsSync(projectsRoot))
201
- return { ok: false, reason: `no transcript root ${projectsRoot}` };
202
- for (const dir of readdirSync(projectsRoot)) {
203
- const path = join(projectsRoot, dir, `${id}.jsonl`);
204
- if (existsSync(path))
205
- return { ok: true, path };
206
- }
207
- return { ok: false, reason: `no transcript for session ${id}` };
208
- }
209
-
210
- // plugins/sp/lib/env.ts
211
- function getEnvVars2() {
212
- return process.env;
213
- }
214
-
215
- // plugins/sp/lib/run-summary-core.ts
216
160
  var IDLE_GAP_MS = 300000;
217
161
  var SHELL_TOOLS = new Set(["bash", "shell", "exec", "exec_command", "run_command", "command"]);
218
162
  var SUBAGENT_TOOLS = new Set(["agent", "task", "invoke_subagent", "subagent", "dispatch_agent"]);
219
- var OPERATOR_TOOLS2 = new Set(["askuserquestion", "ask_user_question"]);
163
+ var LOWERCASE_OPERATOR_TOOLS = new Set(["askuserquestion", "ask_user_question"]);
220
164
  var ACTOR_PRECEDENCE = ["operator", "subagent", "shell", "other"];
221
165
  function actorOf(tool) {
222
166
  const t = tool.toLowerCase().replace(/[^a-z_]/g, "");
223
- if (OPERATOR_TOOLS2.has(t))
167
+ if (LOWERCASE_OPERATOR_TOOLS.has(t))
224
168
  return "operator";
225
169
  if (SUBAGENT_TOOLS.has(t))
226
170
  return "subagent";
@@ -370,7 +314,8 @@ function summaryRow(stage, status, span, measured) {
370
314
  token: formatTokenSplit(measured ? span.tokens : null)
371
315
  };
372
316
  }
373
- function buildRunSummary(lines, inputs, since, until) {
317
+ function buildRunSummary(rows, inputs, opts = {}) {
318
+ const { since, until } = opts;
374
319
  const windows = [];
375
320
  for (const { label, progress } of inputs) {
376
321
  const states = (progress.states ?? []).flatMap((s) => {
@@ -396,7 +341,6 @@ function buildRunSummary(lines, inputs, since, until) {
396
341
  }
397
342
  }
398
343
  windows.sort((a, b) => a.start - b.start);
399
- const { rows, skippedLines } = parseRows(lines);
400
344
  const measure = (start, end) => {
401
345
  const acc = newAcc(start);
402
346
  for (const [row, ts] of rows)
@@ -444,9 +388,87 @@ function buildRunSummary(lines, inputs, since, until) {
444
388
  total,
445
389
  actors: actorSplit(rows, totalStart, totalEnd),
446
390
  gate: { ms: null, reason: "no matching check receipt" },
447
- skippedLines
391
+ skippedLines: opts.skippedLines ?? 0
448
392
  };
449
393
  }
394
+ var SENSITIVE_PREFIXES = ["drizzle/", "packages/config/", ".github/", "plugins/sp/hooks/"];
395
+ var SENSITIVE_GLOBS = ["apps/server/src/**/auth*", "**/*secret*", "**/*.sql"];
396
+ function globToRegex(glob) {
397
+ const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replaceAll("**/", "\x00").replaceAll("**", "\x01").replaceAll("*", "[^/]*").replaceAll("\x00", "(?:.*/)?").replaceAll("\x01", ".*");
398
+ return new RegExp(`^${escaped}$`, "s");
399
+ }
400
+ var SENSITIVE_MATCHERS = [
401
+ ...SENSITIVE_PREFIXES.map((prefix) => ({
402
+ label: prefix,
403
+ test: (path) => path.startsWith(prefix)
404
+ })),
405
+ ...SENSITIVE_GLOBS.map((glob) => {
406
+ const regex = globToRegex(glob);
407
+ return { label: glob, test: (path) => regex.test(path) };
408
+ })
409
+ ];
410
+
411
+ // plugins/sp/lib/transcript.ts
412
+ import { existsSync, readdirSync } from "node:fs";
413
+ import { homedir } from "node:os";
414
+ import { join } from "node:path";
415
+ var SESSION_ID = /^[A-Za-z0-9_-]+$/;
416
+ var PI_ROLES = new Set(["user", "assistant", "toolResult"]);
417
+ function sniffFormat(rows) {
418
+ for (const row of rows) {
419
+ if (row.type === "message" && PI_ROLES.has(row.message?.role ?? ""))
420
+ return "pi";
421
+ if (row.type === "user" || row.type === "assistant")
422
+ return "claude";
423
+ }
424
+ return "unknown";
425
+ }
426
+ function parseRows(lines) {
427
+ const rows = [];
428
+ let skippedLines = 0;
429
+ for (const line of lines) {
430
+ if (!line.trim())
431
+ continue;
432
+ let row;
433
+ try {
434
+ row = JSON.parse(line);
435
+ } catch {
436
+ skippedLines++;
437
+ continue;
438
+ }
439
+ const ts = row.timestamp ? Date.parse(row.timestamp) : Number.NaN;
440
+ if (!Number.isNaN(ts))
441
+ rows.push([row, ts]);
442
+ }
443
+ return { rows, skippedLines, format: sniffFormat(rows.map(([r]) => r)) };
444
+ }
445
+ function resolveTranscript(env, projectsRoot = join(homedir(), ".claude", "projects"), override) {
446
+ if (override)
447
+ return existsSync(override) ? { ok: true, path: override } : { ok: false, reason: "no transcript" };
448
+ const id = env.CLAUDE_CODE_SESSION_ID;
449
+ if (!id) {
450
+ const piFile = env.PI_SESSION_FILE;
451
+ if (piFile !== undefined && piFile !== "") {
452
+ return existsSync(piFile) ? { ok: true, path: piFile } : { ok: false, reason: `PI_SESSION_FILE ${piFile} does not exist` };
453
+ }
454
+ return {
455
+ ok: false,
456
+ reason: "no host session id; pass --transcript <path> (pi: ~/.pi/agent/sessions/<cwd-slug>/<file>.jsonl, " + "or set PI_SESSION_FILE)"
457
+ };
458
+ }
459
+ if (!SESSION_ID.test(id))
460
+ return { ok: false, reason: "refusing a session id with path characters" };
461
+ if (!existsSync(projectsRoot))
462
+ return { ok: false, reason: `no transcript root ${projectsRoot}` };
463
+ for (const dir of readdirSync(projectsRoot)) {
464
+ const path = join(projectsRoot, dir, `${id}.jsonl`);
465
+ if (existsSync(path))
466
+ return { ok: true, path };
467
+ }
468
+ return { ok: false, reason: `no transcript for session ${id}` };
469
+ }
470
+
471
+ // plugins/sp/lib/run-summary-core.ts
450
472
  function renderSummaryMarkdown(summary) {
451
473
  const line = (r, bold = false) => {
452
474
  const stage = bold ? `**${r.stage}**` : r.stage;
@@ -547,9 +569,13 @@ function writeCloseSummary(run, opts = {}) {
547
569
  const resolved = resolveTranscript(opts.env ?? getEnvVars2(), opts.projectsRoot);
548
570
  if (!resolved.ok)
549
571
  return na(resolved.reason);
550
- const lines = readFileSync(resolved.path, "utf8").split(`
551
- `);
552
- const summary = buildRunSummary(lines, [{ progress: run.progress }], run.startedAt, run.completedAt);
572
+ const { rows, skippedLines } = parseRows(readFileSync(resolved.path, "utf8").split(`
573
+ `));
574
+ const summary = buildRunSummary(rows, [{ progress: run.progress }], {
575
+ since: run.startedAt,
576
+ until: run.completedAt,
577
+ skippedLines
578
+ });
553
579
  if (summary.rows.length === 0)
554
580
  return na("no timed attempts");
555
581
  mkdirSync(runDir, { recursive: true });
@@ -583,8 +609,35 @@ import { existsSync as existsSync3, readdirSync as readdirSync2 } from "node:fs"
583
609
  import { homedir as homedir2 } from "node:os";
584
610
  import { join as join3 } from "node:path";
585
611
  var SESSION_ID2 = /^[A-Za-z0-9_-]+$/;
586
- var OPERATOR_TOOLS3 = new Set(["AskUserQuestion", "ask_user_question"]);
587
612
  var PI_ROLES2 = new Set(["user", "assistant", "toolResult"]);
613
+ function sniffFormat2(rows) {
614
+ for (const row of rows) {
615
+ if (row.type === "message" && PI_ROLES2.has(row.message?.role ?? ""))
616
+ return "pi";
617
+ if (row.type === "user" || row.type === "assistant")
618
+ return "claude";
619
+ }
620
+ return "unknown";
621
+ }
622
+ function parseRows2(lines) {
623
+ const rows = [];
624
+ let skippedLines = 0;
625
+ for (const line of lines) {
626
+ if (!line.trim())
627
+ continue;
628
+ let row;
629
+ try {
630
+ row = JSON.parse(line);
631
+ } catch {
632
+ skippedLines++;
633
+ continue;
634
+ }
635
+ const ts = row.timestamp ? Date.parse(row.timestamp) : Number.NaN;
636
+ if (!Number.isNaN(ts))
637
+ rows.push([row, ts]);
638
+ }
639
+ return { rows, skippedLines, format: sniffFormat2(rows.map(([r]) => r)) };
640
+ }
588
641
  function resolveTranscript2(env, projectsRoot = join3(homedir2(), ".claude", "projects"), override) {
589
642
  if (override)
590
643
  return existsSync3(override) ? { ok: true, path: override } : { ok: false, reason: "no transcript" };
@@ -679,8 +732,9 @@ function main(argv, env = getEnvVars(), write = (s) => process.stdout.write(s),
679
732
  const [label, file] = eq > 0 ? [arg.slice(0, eq), arg.slice(eq + 1)] : [undefined, arg];
680
733
  return { label, progress: JSON.parse(readFileSync2(file, "utf8")) };
681
734
  });
682
- const summary = buildRunSummary(readFileSync2(resolved.path, "utf8").split(`
683
- `), inputs, since);
735
+ const { rows, skippedLines } = parseRows2(readFileSync2(resolved.path, "utf8").split(`
736
+ `));
737
+ const summary = buildRunSummary(rows, inputs, { since, skippedLines });
684
738
  summary.gate = gateFromReceipt(process.cwd(), inputs[0]?.progress.runId ?? "");
685
739
  write(`${markdown ? renderSummaryMarkdown(summary) : JSON.stringify({ ...summary, transcript: resolved.path })}
686
740
  `);
@@ -693,12 +747,12 @@ function main(argv, env = getEnvVars(), write = (s) => process.stdout.write(s),
693
747
  }
694
748
  process.exit(main(process.argv.slice(2)));
695
749
  export {
696
- writeCloseSummary,
697
- renderSummaryMarkdown,
698
- renderRollupMarkdown,
699
- main,
700
- gateFromReceipt,
701
- buildRunSummary,
750
+ RUN_SUMMARY_USAGE,
702
751
  actorSplit,
703
- RUN_SUMMARY_USAGE
752
+ buildRunSummary,
753
+ gateFromReceipt,
754
+ main,
755
+ renderRollupMarkdown,
756
+ renderSummaryMarkdown,
757
+ writeCloseSummary
704
758
  };