@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
@@ -2,7 +2,12 @@
2
2
  * GENERATED by scripts/commands/bundle-plugin-lib.ts — do not edit.
3
3
  * Source of truth: packages/app/src/services/residual-scan.ts (task 1003 R5).
4
4
  */
5
- export declare type ResidualCategory = 'review-finding' | 'diff-marker' | 'unchecked-box' | 'staging-residue';
5
+ export declare type ResidualCategory =
6
+ | 'review-finding'
7
+ | 'diff-marker'
8
+ | 'unchecked-box'
9
+ | 'staging-residue'
10
+ | 'review-contract';
6
11
  export declare type ResidualClass = 'blocking' | 'deferrable' | 'advisory' | 'housekeeping';
7
12
  export declare interface ResidualItem {
8
13
  id: string;
@@ -42,6 +47,8 @@ export declare interface ParsedScanArgs {
42
47
  spurBin?: string;
43
48
  root: string;
44
49
  tmpDir: string;
50
+ /** The --review-answer <path> flag: the review answer the review-gate mode grades (task 1204). */
51
+ reviewAnswer?: string;
45
52
  }
46
53
  export declare const ALLOW_PRAGMA: 'residual-scan:allow';
47
54
  export declare function recordedVerdictPath(runDir: string, wbs: string, fs: Pick<typeof import('node:fs'), 'lstatSync' | 'existsSync'>): string;
@@ -62,10 +69,22 @@ export declare function classify(
62
69
  export declare function scanResiduals(inputs: ResidualScanInputs): ResidualArtifact;
63
70
  export declare function parseDeferralEntries(raw: unknown): Deferral[];
64
71
  export declare function blockingReviewFindings(taskContent: string, deferrals: Deferral[]): ResidualItem[];
72
+ export declare function reviewContractViolations(taskContent: string): Array<{ location: string; text: string }>;
73
+ export declare function reviewConfidenceViolation(
74
+ reviewAnswer: string,
75
+ ackLowConfidence: string | undefined,
76
+ ): { location: string; text: string } | null;
77
+ export declare function reviewGradingInput(
78
+ root: string,
79
+ reviewAnswer: string | undefined,
80
+ ackLowConfidence: string | undefined,
81
+ fs: Pick<typeof import('node:fs'), 'readFileSync' | 'statSync'>,
82
+ ): { answer: string; ackLowConfidence: string | undefined } | undefined;
65
83
  export declare function buildReviewGateArtifact(
66
84
  wbs: string,
67
85
  taskContent: string,
68
86
  deferrals: Deferral[],
87
+ review?: { answer: string; ackLowConfidence: string | undefined },
69
88
  ): { artifact: ResidualArtifact; note: string };
70
89
  export declare function parseScanArgs(
71
90
  argv: readonly (string | undefined)[],
@@ -1,6 +1,36 @@
1
1
  // packages/app/src/services/residual-scan.ts
2
2
  import { join } from "node:path";
3
3
  import { createHash } from "node:crypto";
4
+
5
+ // packages/app/src/services/verify-answer-lint.ts
6
+ var CLAIM_MARKERS = [
7
+ /@[a-z0-9._-]+\/[a-z0-9._-]+/i,
8
+ /\bnode_modules\//,
9
+ /\b\d+\.\d+\.\d+\b/,
10
+ /`[A-Za-z_$][\w.$]*\s*\([^`]*\)`/
11
+ ];
12
+ var CITATION_FORMS = [
13
+ /`[^`\s]+:\d+(?:-\d+)?`/,
14
+ /`[^`]+`\s+lines?\s+\d+/i,
15
+ /\b[\w./-]+\.[A-Za-z0-9]+:\d+(?:-\d+)?\b/,
16
+ /https?:\/\/\S+/i
17
+ ];
18
+ function hasClaimMarker(evidence) {
19
+ return CLAIM_MARKERS.some((re) => re.test(evidence));
20
+ }
21
+ function hasCitationForm(evidence) {
22
+ return CITATION_FORMS.some((re) => re.test(evidence));
23
+ }
24
+ var CONFIDENCE_LEVELS = ["HIGH", "MEDIUM", "LOW"];
25
+ function extractAnswerConfidence(text) {
26
+ const matches = [...text.matchAll(/^\s*Confidence:\s*(\S+)\s*$/gim)];
27
+ if (matches.length !== 1)
28
+ return;
29
+ const value = (matches[0]?.[1] ?? "").toUpperCase();
30
+ return CONFIDENCE_LEVELS.includes(value) ? value : undefined;
31
+ }
32
+
33
+ // packages/app/src/services/residual-scan.ts
4
34
  var MARKER_PATTERN = /TODO|FIXME|XXX|HACK/;
5
35
  var PRIORITY_PATTERN = /^P[1-4]/;
6
36
  var RANGE_PRIORITY = /^P[1-4]\s*[\u2013\u2014-]\s*P?[1-4]/;
@@ -27,11 +57,15 @@ function locationOf(locationCell, finding) {
27
57
  const backtick = finding.match(/`([^`]+)`/)?.[1];
28
58
  return backtick === undefined ? "" : normalizeAnchor(backtick);
29
59
  }
30
- function parseReviewFindings(taskContent) {
60
+ function reviewBody(taskContent) {
31
61
  const section = taskContent.split(/^### Review\b/m)[1];
32
- if (section === undefined)
62
+ return section === undefined ? null : section.split(/^### /m)[0] ?? "";
63
+ }
64
+ function parseReviewFindings(taskContent) {
65
+ const section = reviewBody(taskContent);
66
+ if (section === null)
33
67
  return [];
34
- const body = section.split(/^### /m)[0] ?? "";
68
+ const body = section;
35
69
  const out = [];
36
70
  const lines = body.split(`
37
71
  `);
@@ -144,7 +178,8 @@ function scanResiduals(inputs) {
144
178
  "review-finding": true,
145
179
  "diff-marker": base !== null,
146
180
  "unchecked-box": true,
147
- "staging-residue": true
181
+ "staging-residue": true,
182
+ "review-contract": false
148
183
  },
149
184
  items,
150
185
  counts
@@ -165,8 +200,61 @@ function blockingReviewFindings(taskContent, deferrals) {
165
200
  }));
166
201
  return classify(items, [...tableDeferrals, ...deferrals]);
167
202
  }
168
- function buildReviewGateArtifact(wbs, taskContent, deferrals) {
169
- const items = blockingReviewFindings(taskContent, deferrals);
203
+ function reviewContractViolations(taskContent) {
204
+ const violations = [];
205
+ const body = reviewBody(taskContent);
206
+ if (body !== null && extractAnswerConfidence(body.replace(/\*\*/g, "")) === undefined) {
207
+ violations.push({
208
+ location: "task-file:Review",
209
+ 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)."
210
+ });
211
+ }
212
+ for (const row of parseReviewFindings(taskContent)) {
213
+ if (hasClaimMarker(row.text) && !hasCitationForm(row.text)) {
214
+ violations.push({
215
+ location: row.location.length > 0 ? row.location : "task-file:Review",
216
+ 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)}`
217
+ });
218
+ }
219
+ }
220
+ return violations;
221
+ }
222
+ function reviewConfidenceViolation(reviewAnswer, ackLowConfidence) {
223
+ if (ackLowConfidence === "true")
224
+ return null;
225
+ const level = extractAnswerConfidence(reviewAnswer.replace(/\*\*/g, ""));
226
+ if (level === "HIGH" || level === "MEDIUM")
227
+ return null;
228
+ return {
229
+ location: "review-answer:Confidence",
230
+ 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"
231
+ };
232
+ }
233
+ function reviewGradingInput(root, reviewAnswer, ackLowConfidence, fs) {
234
+ if (reviewAnswer === undefined)
235
+ return;
236
+ const path = join(root, reviewAnswer);
237
+ let isFile = false;
238
+ try {
239
+ isFile = fs.statSync(path).isFile();
240
+ } catch {
241
+ isFile = false;
242
+ }
243
+ return { answer: isFile ? fs.readFileSync(path, "utf8") : "", ackLowConfidence };
244
+ }
245
+ function buildReviewGateArtifact(wbs, taskContent, deferrals, review) {
246
+ const confidence = review === undefined ? null : reviewConfidenceViolation(review.answer, review.ackLowConfidence);
247
+ const contractViolations = [...reviewContractViolations(taskContent), ...confidence === null ? [] : [confidence]];
248
+ const items = [
249
+ ...blockingReviewFindings(taskContent, deferrals),
250
+ ...contractViolations.map((v) => ({
251
+ id: makeItemId("review-contract", v.location, v.text),
252
+ category: "review-contract",
253
+ class: "blocking",
254
+ location: v.location,
255
+ text: v.text
256
+ }))
257
+ ];
170
258
  const blocking = items.filter((i) => i.class === "blocking");
171
259
  const counts = { blocking: 0, deferrable: 0, advisory: 0, housekeeping: 0 };
172
260
  for (const item of items)
@@ -178,7 +266,8 @@ function buildReviewGateArtifact(wbs, taskContent, deferrals) {
178
266
  "review-finding": true,
179
267
  "diff-marker": false,
180
268
  "unchecked-box": false,
181
- "staging-residue": false
269
+ "staging-residue": false,
270
+ "review-contract": true
182
271
  },
183
272
  items,
184
273
  counts
@@ -291,6 +380,7 @@ function parseScanArgs(argv, cwd, defaultTmpDir) {
291
380
  let spurBin;
292
381
  let root = cwd;
293
382
  let tmpDir = defaultTmpDir;
383
+ let reviewAnswer;
294
384
  for (let i = 0;i < argv.length; i++) {
295
385
  const a = argv[i];
296
386
  if (a === undefined)
@@ -301,6 +391,8 @@ function parseScanArgs(argv, cwd, defaultTmpDir) {
301
391
  root = argv[++i] ?? root;
302
392
  else if (a === "--tmp-dir")
303
393
  tmpDir = argv[++i] ?? tmpDir;
394
+ else if (a === "--review-answer")
395
+ reviewAnswer = argv[++i];
304
396
  else if (a === "--help" || a === "-h")
305
397
  return null;
306
398
  else if (mode === "")
@@ -308,25 +400,28 @@ function parseScanArgs(argv, cwd, defaultTmpDir) {
308
400
  else if (wbs === "")
309
401
  wbs = a;
310
402
  }
311
- return mode === "" || wbs === "" ? null : { mode, wbs, spurBin, root, tmpDir };
403
+ return mode === "" || wbs === "" ? null : { mode, wbs, spurBin, root, tmpDir, reviewAnswer };
312
404
  }
313
405
  export {
314
- verdictDisagreementNote,
315
- scanResiduals,
316
- renderReport,
317
- recordedVerdictPath,
318
- parseScanArgs,
319
- parseReviewFindings,
320
- parseDiffMarkers,
321
- parseDeferralEntries,
322
- normalizeAnchor,
323
- makeItemId,
324
- locationOf,
325
- foldVerdict,
326
- findUncheckedBoxes,
327
- classify,
328
- buildReviewGateArtifact,
329
- blockingReviewFindings,
406
+ ALLOW_PRAGMA,
330
407
  blockingAnchors,
331
- ALLOW_PRAGMA
408
+ blockingReviewFindings,
409
+ buildReviewGateArtifact,
410
+ classify,
411
+ findUncheckedBoxes,
412
+ foldVerdict,
413
+ locationOf,
414
+ makeItemId,
415
+ normalizeAnchor,
416
+ parseDeferralEntries,
417
+ parseDiffMarkers,
418
+ parseReviewFindings,
419
+ parseScanArgs,
420
+ recordedVerdictPath,
421
+ renderReport,
422
+ reviewConfidenceViolation,
423
+ reviewContractViolations,
424
+ reviewGradingInput,
425
+ scanResiduals,
426
+ verdictDisagreementNote
332
427
  };
@@ -1,377 +1,43 @@
1
1
  /**
2
- * run-summary core — the measurement and rendering behind the `run-summary` CLI (task 1146).
2
+ * run-summary core — the rendering and host IO behind the `run-summary` CLI (task 1146; task 1178 R2).
3
3
  *
4
4
  * Split out of `plugins/sp/scripts/run-summary.ts` when the actor split and the batch roll-up pushed
5
- * that script past the ADR-130 glue budget: the script keeps argv parsing and dispatch, this module
6
- * keeps the logic. `plugins/sp/lib/transcript.ts` is the same shape of shared plugin-side module.
7
- * Node builtins plus relative imports only; `import type` for the app projection it is handed.
5
+ * that script past the ADR-130 glue budget. The PURE aggregation and its types now live in
6
+ * `packages/app/src/workflow/run-summary.ts`, reached through the generated pure plan-projection
7
+ * entry; this module keeps only what belongs to the host: the close-step callback, the receipt read,
8
+ * Markdown rendering and durable/scratch report writing. Node builtins plus relative imports only;
9
+ * `import type` for the app projection it is handed.
8
10
  */
9
11
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
10
12
  import { join } from 'node:path';
11
13
  import type { WorkflowProgressProjection } from '@gobing-ai/spur-app';
14
+ import { getEnvVars } from './env';
12
15
  import {
13
- accumulate,
14
- type Block,
16
+ type ActorSplit,
17
+ buildRunSummary,
15
18
  formatDuration,
16
19
  formatTokenSplit,
17
- isCompaction,
18
- newAcc,
19
- parseRows,
20
- promptText,
21
- resolveTranscript,
22
- type Row,
23
- type Span,
24
- sumSpans,
20
+ type GateTime,
21
+ type Progress,
22
+ type RunSummary,
23
+ type SummaryRow,
25
24
  sumTokens,
26
- type Tokens,
27
25
  zeroTokens,
28
- } from './transcript';
29
- import { getEnvVars } from './env';
30
-
31
- interface ProgressAttempt {
32
- startedAt?: string;
33
- completedAt?: string;
34
- }
35
-
36
- /**
37
- * The subset of the progress projection the summary windows over. The close path hands over the
38
- * full `WorkflowProgressProjection`; the extra fields are ignored here, so the reader stays a
39
- * narrow structural view of it rather than a second copy of the contract.
40
- */
41
- export interface Progress {
42
- runId: string;
43
- workflow?: string;
44
- status?: string;
45
- states?: { state: string; visit?: number; status?: string; actions?: { attempts?: ProgressAttempt[] }[] }[];
46
- }
47
-
48
- /** A stage row; `toolCalls`/`tokens` are null when the window holds no host transcript record. */
49
- export interface SummaryRow {
50
- stage: string;
51
- status: string;
52
- workMs: number;
53
- waitMs: number;
54
- toolCalls: number | null;
55
- tokens: Tokens | null;
56
- time: string;
57
- wait: string;
58
- token: string;
59
- }
60
-
61
- /** 1146 R4: how the total window is spent, by actor. Rows sum exactly to the window. */
62
- export interface ActorSplit {
63
- operatorMs: number;
64
- /** The longest single operator wait, so a total is not read as one stall. */
65
- longestOperatorWaitMs: number;
66
- modelMs: number;
67
- shellMs: number;
68
- subagentMs: number;
69
- otherToolMs: number;
70
- idleMs: number;
71
- compactions: number;
72
- subagentErrors: number;
73
- }
74
-
75
- /** A gate-receipt measurement, or the reason there is none (1146 R4: never a fabricated 0). */
76
- export type GateTime = { ms: number } | { ms: null; reason: string };
77
-
78
- export interface RunSummary {
79
- available: true;
80
- transcript?: string;
81
- rows: SummaryRow[];
82
- total: SummaryRow;
83
- actors: ActorSplit;
84
- gate: GateTime;
85
- skippedLines: number;
86
- }
87
-
88
- /** A gap at or above this is idle, not work (1146 R4, closed decision: fixed, no flag). */
89
- const IDLE_GAP_MS = 300_000;
90
- const SHELL_TOOLS = new Set(['bash', 'shell', 'exec', 'exec_command', 'run_command', 'command']);
91
- const SUBAGENT_TOOLS = new Set(['agent', 'task', 'invoke_subagent', 'subagent', 'dispatch_agent']);
92
- const OPERATOR_TOOLS = new Set(['askuserquestion', 'ask_user_question']);
93
-
94
- type Actor = 'operator' | 'subagent' | 'shell' | 'other';
95
- /** Overlap is counted once, and subagent outranks shell outranks other tools (1146 AC4). */
96
- const ACTOR_PRECEDENCE: Actor[] = ['operator', 'subagent', 'shell', 'other'];
97
-
98
- function actorOf(tool: string): Actor {
99
- const t = tool.toLowerCase().replace(/[^a-z_]/g, '');
100
- if (OPERATOR_TOOLS.has(t)) return 'operator';
101
- if (SUBAGENT_TOOLS.has(t)) return 'subagent';
102
- if (SHELL_TOOLS.has(t)) return 'shell';
103
- return 'other';
104
- }
105
-
106
- interface Call {
107
- id: string;
108
- actor: Actor;
109
- ts: number;
110
- }
111
-
112
- /** One resolved tool span: [start, end] attributed to one actor. */
113
- interface Resolved extends Call {
114
- end: number;
115
- }
116
-
117
- /** Union length of a set of intervals, clipped to [start, end]. */
118
- function unionMs(spans: Array<[number, number]>, start: number, end: number): number {
119
- const clipped = spans
120
- .map(([a, b]) => [Math.max(a, start), Math.min(b, end)] as [number, number])
121
- .filter(([a, b]) => b > a)
122
- .sort((x, y) => x[0] - y[0]);
123
- let total = 0;
124
- let cursor = Number.NaN;
125
- let openUntil = Number.NaN;
126
- for (const [a, b] of clipped) {
127
- if (!Number.isFinite(cursor)) {
128
- cursor = a;
129
- openUntil = b;
130
- continue;
131
- }
132
- if (a > openUntil) {
133
- total += openUntil - cursor;
134
- cursor = a;
135
- openUntil = b;
136
- } else {
137
- openUntil = Math.max(openUntil, b);
138
- }
139
- }
140
- if (Number.isFinite(cursor)) total += openUntil - cursor;
141
- return total;
142
- }
143
-
144
- /** Subtract already-claimed intervals from a candidate set, returning the still-free part. */
145
- function subtract(spans: Array<[number, number]>, claimed: Array<[number, number]>): Array<[number, number]> {
146
- let free = spans;
147
- for (const [ca, cb] of claimed) {
148
- const next: Array<[number, number]> = [];
149
- for (const [a, b] of free) {
150
- if (cb <= a || ca >= b) {
151
- next.push([a, b]);
152
- continue;
153
- }
154
- if (ca > a) next.push([a, ca]);
155
- if (cb < b) next.push([cb, b]);
156
- }
157
- free = next;
158
- }
159
- return free;
160
- }
161
-
162
- /**
163
- * 1146 R4: partition the window [start, end] by actor. Tool calls are paired with their results
164
- * (either host shape); overlaps resolve by `ACTOR_PRECEDENCE`, counted once; unclaimed gaps at or
165
- * above IDLE_GAP_MS are idle; the remainder is model time, so the rows always sum to the window.
166
- */
167
- export function actorSplit(rows: [Row, number][], start: number, end: number): ActorSplit {
168
- const empty: ActorSplit = {
169
- operatorMs: 0,
170
- longestOperatorWaitMs: 0,
171
- modelMs: 0,
172
- shellMs: 0,
173
- subagentMs: 0,
174
- otherToolMs: 0,
175
- idleMs: 0,
176
- compactions: 0,
177
- subagentErrors: 0,
178
- };
179
- const window = end - start;
180
- if (window <= 0) return empty;
181
-
182
- const calls = new Map<string, Call>();
183
- const results = new Map<string, { ts: number; isError: boolean }>();
184
- const eventTs: number[] = [];
185
- let compactions = 0;
186
-
187
- for (const [row, ts] of rows) {
188
- if (ts < start || ts > end) continue;
189
- eventTs.push(ts);
190
- if (isCompaction(row)) compactions++;
191
- const blocks = Array.isArray(row.message?.content) ? (row.message?.content as Block[]) : [];
192
- if (row.message?.role === 'toolResult' && row.message.toolCallId !== undefined) {
193
- results.set(row.message.toolCallId, { ts, isError: row.message.isError === true });
194
- }
195
- for (const b of blocks) {
196
- if ((b.type === 'tool_use' || b.type === 'toolCall') && b.id !== undefined && b.name !== undefined) {
197
- calls.set(b.id, { id: b.id, actor: actorOf(b.name), ts });
198
- } else if (b.type === 'tool_result' && b.tool_use_id !== undefined) {
199
- results.set(b.tool_use_id, { ts, isError: b.is_error === true });
200
- }
201
- }
202
- }
203
-
204
- let subagentErrors = 0;
205
- const resolved: Resolved[] = [];
206
- for (const call of calls.values()) {
207
- const result = results.get(call.id);
208
- if (result?.isError === true && call.actor === 'subagent') subagentErrors++;
209
- resolved.push({ ...call, end: Math.min(result?.ts ?? end, end) });
210
- }
211
-
212
- const claimed: Array<[number, number]> = [];
213
- const byActor = new Map<Actor, number>();
214
- for (const actor of ACTOR_PRECEDENCE) {
215
- const mine = resolved.filter((r) => r.actor === actor).map((r) => [r.ts, r.end] as [number, number]);
216
- const free = subtract(mine, claimed);
217
- for (const span of free) claimed.push(span);
218
- const ms = unionMs(free, start, end);
219
- byActor.set(actor, ms);
220
- if (actor === 'operator') {
221
- empty.longestOperatorWaitMs = Math.max(0, ...mine.map(([a, b]) => b - a));
222
- }
223
- }
224
-
225
- // Idle: unclaimed gaps between consecutive event boundaries, at or above the threshold.
226
- const boundaries = [...new Set([start, end, ...eventTs, ...claimed.flat()])].sort((a, b) => a - b);
227
- const claimedSorted = [...claimed].sort((a, b) => a[0] - b[0]);
228
- let idleMs = 0;
229
- for (let i = 0; i < boundaries.length - 1; i++) {
230
- const a = boundaries[i] as number;
231
- const b = boundaries[i + 1] as number;
232
- if (b - a < IDLE_GAP_MS) continue;
233
- if (claimedSorted.some(([ca, cb]) => ca < b && cb > a)) continue;
234
- idleMs += b - a;
235
- }
236
-
237
- const accounted =
238
- (byActor.get('operator') ?? 0) +
239
- (byActor.get('subagent') ?? 0) +
240
- (byActor.get('shell') ?? 0) +
241
- (byActor.get('other') ?? 0) +
242
- idleMs;
243
- return {
244
- ...empty,
245
- operatorMs: byActor.get('operator') ?? 0,
246
- subagentMs: byActor.get('subagent') ?? 0,
247
- shellMs: byActor.get('shell') ?? 0,
248
- otherToolMs: byActor.get('other') ?? 0,
249
- idleMs,
250
- modelMs: Math.max(0, window - accounted),
251
- compactions,
252
- subagentErrors,
253
- };
254
- }
255
-
256
- interface Window {
257
- stage: string;
258
- status: string;
259
- start: number;
260
- end: number;
261
- }
262
-
263
- function attemptWindow(attempts: ProgressAttempt[]): [number, number] | undefined {
264
- const starts = attempts.map((a) => Date.parse(a.startedAt ?? '')).filter((n) => !Number.isNaN(n));
265
- const ends = attempts.map((a) => Date.parse(a.completedAt ?? '')).filter((n) => !Number.isNaN(n));
266
- return starts.length && ends.length ? [Math.min(...starts), Math.max(...ends)] : undefined;
267
- }
268
-
269
- function summaryRow(stage: string, status: string, span: Span, measured: boolean): SummaryRow {
270
- return {
271
- stage,
272
- status,
273
- workMs: span.workMs,
274
- waitMs: span.waitMs,
275
- toolCalls: measured ? span.toolCalls : null,
276
- tokens: measured ? span.tokens : null,
277
- time: formatDuration(span.workMs),
278
- wait: formatDuration(span.waitMs),
279
- token: formatTokenSplit(measured ? span.tokens : null),
280
- };
281
- }
282
-
283
- /**
284
- * dev-run / dev-runall execution summary. One progress file → one row per state visit; several →
285
- * one row per run. Each window spans its first attempt start → last completion (inclusive) and
286
- * slices the host transcript; work = window − AskUserQuestion answer time.
287
- */
288
- export function buildRunSummary(
289
- lines: string[],
290
- inputs: { label?: string; progress: Progress }[],
291
- since?: string,
292
- until?: string,
293
- ): RunSummary {
294
- const windows: Window[] = [];
295
- for (const { label, progress } of inputs) {
296
- const states = (progress.states ?? []).flatMap((s) => {
297
- const w = attemptWindow((s.actions ?? []).flatMap((a) => a.attempts ?? []));
298
- return w ? [{ s, w }] : [];
299
- });
300
- if (!states.length) throw new Error(`progress for run ${progress.runId} has no timed attempts`);
301
- if (inputs.length === 1) {
302
- for (const { s, w } of states) {
303
- const stage = (s.visit ?? 1) > 1 ? `${s.state} (visit ${s.visit})` : s.state;
304
- windows.push({ stage, status: s.status ?? '', start: w[0], end: w[1] });
305
- }
306
- } else {
307
- const stage = label ?? `${progress.workflow ?? 'run'} ${progress.runId.slice(0, 8)}`;
308
- const start = Math.min(...states.map(({ w }) => w[0]));
309
- windows.push({
310
- stage,
311
- status: progress.status ?? '',
312
- start,
313
- end: Math.max(...states.map(({ w }) => w[1])),
314
- });
315
- }
316
- }
317
- windows.sort((a, b) => a.start - b.start);
318
-
319
- const { rows, skippedLines } = parseRows(lines);
320
- const measure = (start: number, end: number): { span: Span; measured: boolean } => {
321
- const acc = newAcc(start);
322
- for (const [row, ts] of rows)
323
- if (ts >= start && ts <= end && promptText(row) === undefined) accumulate(acc, row, ts);
324
- const span = {
325
- workMs: end - start - acc.askMs,
326
- waitMs: acc.askMs,
327
- toolCalls: acc.toolIds.size,
328
- tokens: sumTokens([...acc.messages.values()]),
329
- };
330
- return { span, measured: acc.toolIds.size > 0 || acc.messages.size > 0 };
331
- };
332
-
333
- const sinceMs = since ? Date.parse(since) : Number.NaN;
334
- const totalStart = Number.isNaN(sinceMs) ? Math.min(...windows.map((w) => w.start)) : sinceMs;
335
- let totalEnd = Math.max(...windows.map((w) => w.end));
336
- // With --since the total runs to the latest host activity, so setup/close work lands in overhead.
337
- if (!Number.isNaN(sinceMs)) for (const [, ts] of rows) if (ts >= sinceMs) totalEnd = Math.max(totalEnd, ts);
338
- // 1146 R1: the close passes its own completion instant, which caps the window exactly.
339
- const untilMs = until ? Date.parse(until) : Number.NaN;
340
- if (!Number.isNaN(untilMs) && untilMs > totalStart) totalEnd = untilMs;
341
-
342
- const stageRows = windows.map((w) => {
343
- const { span, measured } = measure(w.start, w.end);
344
- return summaryRow(w.stage, w.status, span, measured);
345
- });
346
- const totalMeasure = measure(totalStart, totalEnd);
347
- const statuses = [...new Set(inputs.map((i) => i.progress.status ?? ''))].join('/');
348
- const total = summaryRow('Total', statuses, totalMeasure.span, totalMeasure.measured);
349
-
350
- // Parallel runs overlap, so their rows cannot partition the total: no overhead row then.
351
- const overlaps = windows.some((w, i) => i > 0 && w.start < (windows[i - 1]?.end ?? 0));
352
- const sum = sumSpans(
353
- stageRows.map((r) => ({ ...r, toolCalls: r.toolCalls ?? 0, tokens: r.tokens ?? zeroTokens() })),
354
- );
355
- const overhead: Span = {
356
- workMs: total.workMs - sum.workMs,
357
- waitMs: total.waitMs - sum.waitMs,
358
- toolCalls: (total.toolCalls ?? 0) - sum.toolCalls,
359
- tokens: Object.fromEntries(
360
- (Object.keys(sum.tokens) as (keyof Tokens)[]).map((k) => [k, (total.tokens?.[k] ?? 0) - sum.tokens[k]]),
361
- ) as Tokens,
362
- };
363
- if (!overlaps && overhead.workMs > 0)
364
- stageRows.push(summaryRow('driver overhead', '', overhead, totalMeasure.measured));
365
-
366
- return {
367
- available: true,
368
- rows: stageRows,
369
- total,
370
- actors: actorSplit(rows, totalStart, totalEnd),
371
- gate: { ms: null, reason: 'no matching check receipt' },
372
- skippedLines,
373
- };
374
- }
26
+ } from './plan-projection.generated.mjs';
27
+ import { parseRows, resolveTranscript } from './transcript';
28
+
29
+ // Task 1178 R2: the pure aggregation moved to `packages/app/src/workflow/run-summary.ts`. Re-export
30
+ // it from here so the adapter's existing surface (the `run-summary` script and its tests) keeps one
31
+ // import path; the implementation is delegated, never duplicated.
32
+ export {
33
+ type ActorSplit,
34
+ actorSplit,
35
+ buildRunSummary,
36
+ type GateTime,
37
+ type Progress,
38
+ type RunSummary,
39
+ type SummaryRow,
40
+ } from './plan-projection.generated.mjs';
375
41
 
376
42
  export function renderSummaryMarkdown(summary: RunSummary): string {
377
43
  const line = (r: SummaryRow, bold = false) => {
@@ -516,13 +182,12 @@ export function writeCloseSummary(
516
182
  if (run.startedAt === undefined) return na('run row has no started_at');
517
183
  const resolved = resolveTranscript(opts.env ?? getEnvVars(), opts.projectsRoot);
518
184
  if (!resolved.ok) return na(resolved.reason);
519
- const lines = readFileSync(resolved.path, 'utf8').split('\n');
520
- const summary = buildRunSummary(
521
- lines,
522
- [{ progress: run.progress as unknown as Progress }],
523
- run.startedAt,
524
- run.completedAt,
525
- );
185
+ const { rows, skippedLines } = parseRows(readFileSync(resolved.path, 'utf8').split('\n'));
186
+ const summary = buildRunSummary(rows, [{ progress: run.progress as unknown as Progress }], {
187
+ since: run.startedAt,
188
+ until: run.completedAt,
189
+ skippedLines,
190
+ });
526
191
  if (summary.rows.length === 0) return na('no timed attempts');
527
192
  mkdirSync(runDir, { recursive: true });
528
193
  writeFileSync(abs, `${renderSummaryMarkdown(summary)}\n`);
@@ -558,4 +223,3 @@ export function gateFromReceipt(cwd: string, runId: string): GateTime {
558
223
  return { ms: null, reason: 'check receipt is unreadable' };
559
224
  }
560
225
  }
561
-