@gobing-ai/spur 0.4.2 → 0.4.3

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 (133) 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/strict/runtime-boundaries.yaml +7 -0
  7. package/config/script-placement-baseline.json +1 -19
  8. package/config/workflows/feature-lifecycle.yaml +46 -1
  9. package/config/workflows/feature-verification.yaml +11 -0
  10. package/package.json +9 -9
  11. package/plugins/sp/README.md +22 -16
  12. package/plugins/sp/agents/super-reviewer.md +43 -0
  13. package/plugins/sp/bunfig.toml +8 -5
  14. package/plugins/sp/hooks/careful-guard.ts +1 -1
  15. package/plugins/sp/hooks/context-post-tool.ts +7 -427
  16. package/plugins/sp/hooks/context-session-start.ts +5 -213
  17. package/plugins/sp/hooks/context-session-stop.ts +5 -191
  18. package/plugins/sp/hooks/pi/guard-extension.ts +6 -5
  19. package/plugins/sp/hooks/task-write-guard.ts +1 -1
  20. package/plugins/sp/{hooks → lib}/agent-hint.ts +1 -1
  21. package/plugins/sp/lib/context-post-tool.ts +432 -0
  22. package/plugins/sp/lib/context-session-start.ts +214 -0
  23. package/plugins/sp/lib/context-session-stop.ts +196 -0
  24. package/plugins/sp/lib/dogfood-pipeline.ts +265 -0
  25. package/plugins/sp/lib/dogfood-report.ts +278 -0
  26. package/plugins/sp/lib/idea-handoff.generated.mjs +3 -3
  27. package/plugins/sp/lib/inline-run-app.ts +40 -0
  28. package/plugins/sp/lib/inline-run.generated.d.mts +3 -3
  29. package/plugins/sp/lib/inline-run.generated.mjs +1347 -90
  30. package/plugins/sp/lib/plan-projection.generated.d.mts +150 -0
  31. package/plugins/sp/lib/plan-projection.generated.mjs +591 -1
  32. package/plugins/sp/lib/quality-gate.generated.mjs +38 -25
  33. package/plugins/sp/lib/residual-scan.generated.d.mts +7 -1
  34. package/plugins/sp/lib/residual-scan.generated.mjs +71 -6
  35. package/plugins/sp/lib/run-summary-core.ts +34 -370
  36. package/plugins/sp/lib/transcript.ts +37 -228
  37. package/plugins/sp/plugin.json +1 -1
  38. package/plugins/sp/scripts/batch-plan.mjs +586 -0
  39. package/plugins/sp/scripts/batch-preflight.mjs +23 -1
  40. package/plugins/sp/scripts/batch-preflight.ts +35 -295
  41. package/plugins/sp/scripts/daily-summary/daily-summary.mjs +108 -87
  42. package/plugins/sp/scripts/daily-summary/daily-summary.ts +111 -104
  43. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.mjs +2 -1
  44. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts +7 -265
  45. package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +3 -0
  46. package/plugins/sp/scripts/dogfood-testing/validate-report.ts +6 -272
  47. package/plugins/sp/scripts/feature-verification-steps.mjs +7 -123
  48. package/plugins/sp/scripts/feature-verification-steps.ts +29 -181
  49. package/plugins/sp/scripts/inline-run-dispatch.mjs +27 -26
  50. package/plugins/sp/scripts/inline-run-dispatch.ts +1 -21
  51. package/plugins/sp/scripts/inline-run-setup.mjs +149 -122
  52. package/plugins/sp/scripts/inline-run-setup.ts +1 -21
  53. package/plugins/sp/scripts/quality-gate.mjs +14 -2
  54. package/plugins/sp/scripts/residual-scan.mjs +73 -6
  55. package/plugins/sp/scripts/residual-scan.ts +8 -5
  56. package/plugins/sp/scripts/run-summary.mjs +151 -97
  57. package/plugins/sp/scripts/run-summary.ts +7 -5
  58. package/plugins/sp/scripts/session-timeline.mjs +46 -19
  59. package/plugins/sp/scripts/task-diffstat.mjs +11 -1
  60. package/plugins/sp/scripts/task-diffstat.ts +8 -39
  61. package/plugins/sp/skills/code-implementation/SKILL.md +7 -1
  62. package/plugins/sp/skills/code-verification/references/secu-review.md +2 -1
  63. package/plugins/sp/skills/code-verification/references/verdict-schema.md +4 -0
  64. package/plugins/sp/skills/dogfood-testing/SKILL.md +2 -2
  65. package/plugins/sp/skills/next-feature/SKILL.md +1 -1
  66. package/plugins/sp/skills/parallel-execution/references/dispatch-surface.md +1 -1
  67. package/plugins/sp/skills/source-driven-development/SKILL.md +7 -4
  68. package/plugins/sp/skills/spur-cli/references/builder.md +3 -2
  69. package/plugins/sp/skills/spur-cli/references/decision.md +32 -7
  70. package/plugins/sp/skills/spur-cli/references/features.md +46 -5
  71. package/plugins/sp/skills/spur-dev/references/execution-batch-report.md +1 -1
  72. package/plugins/sp/skills/spur-dev/references/execution-batch.md +13 -3
  73. package/plugins/sp/skills/spur-dev/references/execution-worktree-landing.md +22 -0
  74. package/plugins/sp/skills/spur-dev/references/execution-worktree-setup.md +14 -0
  75. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +29 -12
  76. package/spur.js +80681 -79296
  77. package/web/_astro/{BoardApp.B3GQZvj8.js → BoardApp.5IIl2O-z.js} +1 -1
  78. package/web/_astro/{BoardApp.BT7L2kSb.js → BoardApp.Drnx-KeM.js} +150 -150
  79. package/web/_astro/{TaskDetail.Cxov1_N_.js → TaskDetail.Ci4LNn9S.js} +1 -1
  80. package/web/_astro/{arc.vaT7tPOB.js → arc.j2EVkYVd.js} +1 -1
  81. package/web/_astro/{architectureDiagram-3BPJPVTR.DRMmTjkN.js → architectureDiagram-3BPJPVTR.DZWSFqEG.js} +1 -1
  82. package/web/_astro/{blockDiagram-GPEHLZMM.cla0LO_q.js → blockDiagram-GPEHLZMM.DE6GpXCN.js} +1 -1
  83. package/web/_astro/{c4Diagram-AAUBKEIU.D4YAIBKn.js → c4Diagram-AAUBKEIU.DoVR1G6B.js} +1 -1
  84. package/web/_astro/channel.DmdDxGsq.js +1 -0
  85. package/web/_astro/{chunk-2J33WTMH.BrSJbMRZ.js → chunk-2J33WTMH.Do6-Gbyf.js} +1 -1
  86. package/web/_astro/{chunk-4BX2VUAB.CuLMgKF5.js → chunk-4BX2VUAB.BYV1HGHx.js} +1 -1
  87. package/web/_astro/{chunk-55IACEB6.zkvgu54F.js → chunk-55IACEB6.CvI1zkrc.js} +1 -1
  88. package/web/_astro/{chunk-727SXJPM.DxWkACh8.js → chunk-727SXJPM.DNGSCRlX.js} +1 -1
  89. package/web/_astro/{chunk-AQP2D5EJ.42XrmIlH.js → chunk-AQP2D5EJ.DXhhgT5z.js} +1 -1
  90. package/web/_astro/{chunk-FMBD7UC4.Bao2wMK9.js → chunk-FMBD7UC4.dzkRKP2u.js} +1 -1
  91. package/web/_astro/{chunk-ND2GUHAM.BhydojD1.js → chunk-ND2GUHAM.DujGLeEf.js} +1 -1
  92. package/web/_astro/{chunk-QZHKN3VN.CmZ82g-H.js → chunk-QZHKN3VN.BWtIT3ZU.js} +1 -1
  93. package/web/_astro/{classDiagram-4FO5ZUOK.Blr83Gt_.js → classDiagram-4FO5ZUOK.DyApg0Nn.js} +1 -1
  94. package/web/_astro/{classDiagram-v2-Q7XG4LA2.Blr83Gt_.js → classDiagram-v2-Q7XG4LA2.DyApg0Nn.js} +1 -1
  95. package/web/_astro/{cose-bilkent-S5V4N54A.qmfv0SPn.js → cose-bilkent-S5V4N54A.DJQB-u_l.js} +1 -1
  96. package/web/_astro/{cynefin-OW5HDTMX.BtELhXPR.js → cynefin-OW5HDTMX.bi3D_siV.js} +1 -1
  97. package/web/_astro/{dagre-BM42HDAG.AWmaAvXo.js → dagre-BM42HDAG.C-f7TrRg.js} +1 -1
  98. package/web/_astro/{diagram-2AECGRRQ.DdH34KDi.js → diagram-2AECGRRQ.DrPfjHQB.js} +1 -1
  99. package/web/_astro/{diagram-5GNKFQAL.BmNPix1k.js → diagram-5GNKFQAL.C9S4WUHO.js} +1 -1
  100. package/web/_astro/{diagram-KO2AKTUF.BNQd_Ppo.js → diagram-KO2AKTUF.Dzzye8QH.js} +1 -1
  101. package/web/_astro/{diagram-LMA3HP47.BAPgDi0e.js → diagram-LMA3HP47.DX-LRiof.js} +1 -1
  102. package/web/_astro/{diagram-OG6HWLK6.DZ4zyLdC.js → diagram-OG6HWLK6.Z-kAKwIz.js} +1 -1
  103. package/web/_astro/{erDiagram-TEJ5UH35.Q1TySMmw.js → erDiagram-TEJ5UH35.D6dyClSv.js} +1 -1
  104. package/web/_astro/{flowDiagram-I6XJVG4X.BXhFBicG.js → flowDiagram-I6XJVG4X.Cx3q2cxD.js} +1 -1
  105. package/web/_astro/{ganttDiagram-6RSMTGT7.vHSgcuo2.js → ganttDiagram-6RSMTGT7.DU3gH6CT.js} +1 -1
  106. package/web/_astro/{gitGraphDiagram-PVQCEYII.Bi1f73C7.js → gitGraphDiagram-PVQCEYII.ISmCnJ87.js} +1 -1
  107. package/web/_astro/index.Do2lLdgo.css +1 -0
  108. package/web/_astro/{infoDiagram-5YYISTIA.DPvXZEWm.js → infoDiagram-5YYISTIA.DR6_iNOK.js} +1 -1
  109. package/web/_astro/{ishikawaDiagram-YF4QCWOH.Cz829fsV.js → ishikawaDiagram-YF4QCWOH.D7wY-ZGA.js} +1 -1
  110. package/web/_astro/{journeyDiagram-JHISSGLW.enRnB9Me.js → journeyDiagram-JHISSGLW.DkP9-WK8.js} +1 -1
  111. package/web/_astro/{kanban-definition-UN3LZRKU.DnTNk6d2.js → kanban-definition-UN3LZRKU.wtgY98M2.js} +1 -1
  112. package/web/_astro/{linear.BzGnBwNL.js → linear.CI6djNi7.js} +1 -1
  113. package/web/_astro/{mermaid.core.CSbTpIVz.js → mermaid.core.CuQPxS8b.js} +4 -4
  114. package/web/_astro/{mindmap-definition-RKZ34NQL.DGJDy8zT.js → mindmap-definition-RKZ34NQL.5HNIEG7R.js} +1 -1
  115. package/web/_astro/{pieDiagram-4H26LBE5.HSPdi5_8.js → pieDiagram-4H26LBE5.CchhDFjE.js} +1 -1
  116. package/web/_astro/{quadrantDiagram-W4KKPZXB.6Gv8mz2Y.js → quadrantDiagram-W4KKPZXB.PYXXJPSJ.js} +1 -1
  117. package/web/_astro/{requirementDiagram-4Y6WPE33.CnBCtFwa.js → requirementDiagram-4Y6WPE33.cUQuNAKU.js} +1 -1
  118. package/web/_astro/{sankeyDiagram-5OEKKPKP.5M8VWAu8.js → sankeyDiagram-5OEKKPKP.BO7MnRIA.js} +1 -1
  119. package/web/_astro/{sequenceDiagram-3UESZ5HK.DN41XN-U.js → sequenceDiagram-3UESZ5HK.D9bElyD6.js} +1 -1
  120. package/web/_astro/{stateDiagram-AJRCARHV.ErOO3W8j.js → stateDiagram-AJRCARHV.DhGdRz7I.js} +1 -1
  121. package/web/_astro/{stateDiagram-v2-BHNVJYJU.B_ZHICM-.js → stateDiagram-v2-BHNVJYJU.5GvLeAoL.js} +1 -1
  122. package/web/_astro/{timeline-definition-PNZ67QCA.CWdosr07.js → timeline-definition-PNZ67QCA.LtAT9haM.js} +1 -1
  123. package/web/_astro/{vennDiagram-CIIHVFJN.BHzYjNV5.js → vennDiagram-CIIHVFJN.DRth1yLA.js} +1 -1
  124. package/web/_astro/{wardleyDiagram-YWT4CUSO.h8x4IwPX.js → wardleyDiagram-YWT4CUSO.h9MlZfcr.js} +1 -1
  125. package/web/_astro/{xychartDiagram-2RQKCTM6.RTapW2X8.js → xychartDiagram-2RQKCTM6.DHqy9DYA.js} +1 -1
  126. package/web/index.html +2 -2
  127. package/config/proportional-route-table.ts +0 -155
  128. package/plugins/sp/scripts/daily-summary/logger.ts +0 -28
  129. package/web/_astro/channel.D4nG9YLf.js +0 -1
  130. package/web/_astro/index.BhSwN8yY.css +0 -1
  131. /package/plugins/sp/{hooks → lib}/context-owner.ts +0 -0
  132. /package/plugins/sp/{hooks → lib}/destructive-policy.ts +0 -0
  133. /package/plugins/sp/{hooks → lib}/task-file-policy.ts +0 -0
@@ -3,9 +3,7 @@
3
3
 
4
4
  // plugins/sp/scripts/inline-run-setup.ts
5
5
  import { spawnSync } from "child_process";
6
- import { existsSync as existsSync3 } from "fs";
7
- import { dirname, join as join3, resolve } from "path";
8
- import { fileURLToPath } from "url";
6
+ import { fileURLToPath as fileURLToPath2 } from "url";
9
7
 
10
8
  // plugins/sp/lib/env.ts
11
9
  function getEnvVar(name, fallback) {
@@ -14,8 +12,30 @@ function getEnvVar(name, fallback) {
14
12
  }
15
13
 
16
14
  // plugins/sp/lib/inline-run-app.ts
17
- async function loadInlineApp(spurBin, resolveAppEntry, required) {
18
- const { entry, portable } = resolveAppEntry(spurBin);
15
+ import { existsSync } from "node:fs";
16
+ import { dirname, join, resolve } from "node:path";
17
+ import { fileURLToPath } from "node:url";
18
+ function resolveAppEntry(spurBin) {
19
+ const fallback = fileURLToPath(new URL("../../../apps/cli/src/index.ts", import.meta.url));
20
+ const candidates = spurBin !== "" ? [spurBin] : [fallback];
21
+ for (const candidate of candidates) {
22
+ const tokens = candidate.split(/\s+/).filter(Boolean);
23
+ const mainModule = [...tokens].reverse().find((t) => t.endsWith(".ts"));
24
+ if (mainModule === undefined || !existsSync(mainModule))
25
+ continue;
26
+ const repoRoot = resolve(dirname(mainModule), "..", "..", "..");
27
+ const appEntry = join(repoRoot, "packages", "app", "src", "index.ts");
28
+ if (existsSync(appEntry))
29
+ return { entry: appEntry, portable: false };
30
+ }
31
+ const entry = fileURLToPath(new URL("../lib/inline-run.generated.mjs", import.meta.url));
32
+ if (!existsSync(entry)) {
33
+ throw new Error("inline application bundle is missing — rebuild/install the sp plugin before running inline");
34
+ }
35
+ return { entry, portable: true };
36
+ }
37
+ async function loadInlineApp(spurBin, resolveAppEntry2, required) {
38
+ const { entry, portable } = resolveAppEntry2(spurBin);
19
39
  const app = await import(entry);
20
40
  const missing = portable ? required.filter((name) => typeof app[name] !== "function") : [];
21
41
  if (missing.length > 0) {
@@ -30,16 +50,14 @@ function getEnvVars() {
30
50
  }
31
51
 
32
52
  // plugins/sp/lib/run-summary-core.ts
33
- import { existsSync as existsSync2, mkdirSync, readFileSync, writeFileSync } from "node:fs";
34
- import { join as join2 } from "node:path";
53
+ import { existsSync as existsSync3, mkdirSync, readFileSync, writeFileSync } from "node:fs";
54
+ import { join as join3 } from "node:path";
35
55
 
36
- // plugins/sp/lib/transcript.ts
37
- import { existsSync, readdirSync } from "node:fs";
38
- import { homedir } from "node:os";
39
- import { join } from "node:path";
40
- var SESSION_ID = /^[A-Za-z0-9_-]+$/;
41
- var OPERATOR_TOOLS = new Set(["AskUserQuestion", "ask_user_question"]);
42
- var PI_ROLES = new Set(["user", "assistant", "toolResult"]);
56
+ // plugins/sp/lib/plan-projection.generated.mjs
57
+ var NON_ABORTING_PREFLIGHT_CODES = new Set([
58
+ "L4.scenario-unverified",
59
+ "L4.verifying-incomplete-tasks"
60
+ ]);
43
61
  var zeroTokens = () => ({ input: 0, cacheCreate: 0, cacheRead: 0, output: 0 });
44
62
  function formatDuration(ms) {
45
63
  const s = Math.round(ms / 1000);
@@ -59,6 +77,26 @@ function formatTokenSplit(t) {
59
77
  const nonCached = t.input + t.cacheCreate + t.output;
60
78
  return `${formatTokens(nonCached + t.cacheRead)} / ${formatTokens(nonCached)}`;
61
79
  }
80
+ var OPERATOR_TOOLS = new Set(["AskUserQuestion", "ask_user_question"]);
81
+ function isCompaction(row) {
82
+ return row.type === "compaction" || row.type === "system" && row.subtype === "compact_boundary";
83
+ }
84
+ function firstTextContent(row) {
85
+ const content = row.message?.content;
86
+ if (typeof content === "string")
87
+ return content;
88
+ if (!Array.isArray(content))
89
+ return;
90
+ return content.find((b) => b.type === "text")?.text;
91
+ }
92
+ function skillInvocationPrompt(text) {
93
+ const name = /^\s*<skill name="([^"]+)"/.exec(text)?.[1];
94
+ if (name === undefined)
95
+ return;
96
+ const close = text.lastIndexOf("</skill>");
97
+ const args = close < 0 ? "" : text.slice(close + "</skill>".length).trim();
98
+ return args === "" ? `/${name}` : `/${name} ${args}`;
99
+ }
62
100
  function promptText(row) {
63
101
  if (row.type === "custom_message") {
64
102
  if (row.display === false)
@@ -83,22 +121,6 @@ function promptText(row) {
83
121
  return;
84
122
  return content.find((b) => b.type === "text")?.text;
85
123
  }
86
- function firstTextContent(row) {
87
- const content = row.message?.content;
88
- if (typeof content === "string")
89
- return content;
90
- if (!Array.isArray(content))
91
- return;
92
- return content.find((b) => b.type === "text")?.text;
93
- }
94
- function skillInvocationPrompt(text) {
95
- const name = /^\s*<skill name="([^"]+)"/.exec(text)?.[1];
96
- if (name === undefined)
97
- return;
98
- const close = text.lastIndexOf("</skill>");
99
- const args = close < 0 ? "" : text.slice(close + "</skill>".length).trim();
100
- return args === "" ? `/${name}` : `/${name} ${args}`;
101
- }
102
124
  function sumTokens(all) {
103
125
  const total = zeroTokens();
104
126
  for (const t of all)
@@ -170,72 +192,14 @@ function accumulate(acc, row, ts) {
170
192
  });
171
193
  }
172
194
  }
173
- function isCompaction(row) {
174
- return row.type === "compaction" || row.type === "system" && row.subtype === "compact_boundary";
175
- }
176
- function sniffFormat(rows) {
177
- for (const row of rows) {
178
- if (row.type === "message" && PI_ROLES.has(row.message?.role ?? ""))
179
- return "pi";
180
- if (row.type === "user" || row.type === "assistant")
181
- return "claude";
182
- }
183
- return "unknown";
184
- }
185
- function parseRows(lines) {
186
- const rows = [];
187
- let skippedLines = 0;
188
- for (const line of lines) {
189
- if (!line.trim())
190
- continue;
191
- let row;
192
- try {
193
- row = JSON.parse(line);
194
- } catch {
195
- skippedLines++;
196
- continue;
197
- }
198
- const ts = row.timestamp ? Date.parse(row.timestamp) : Number.NaN;
199
- if (!Number.isNaN(ts))
200
- rows.push([row, ts]);
201
- }
202
- return { rows, skippedLines, format: sniffFormat(rows.map(([r]) => r)) };
203
- }
204
- function resolveTranscript(env, projectsRoot = join(homedir(), ".claude", "projects"), override) {
205
- if (override)
206
- return existsSync(override) ? { ok: true, path: override } : { ok: false, reason: "no transcript" };
207
- const id = env.CLAUDE_CODE_SESSION_ID;
208
- if (!id) {
209
- const piFile = env.PI_SESSION_FILE;
210
- if (piFile !== undefined && piFile !== "") {
211
- return existsSync(piFile) ? { ok: true, path: piFile } : { ok: false, reason: `PI_SESSION_FILE ${piFile} does not exist` };
212
- }
213
- return {
214
- ok: false,
215
- reason: "no host session id; pass --transcript <path> (pi: ~/.pi/agent/sessions/<cwd-slug>/<file>.jsonl, " + "or set PI_SESSION_FILE)"
216
- };
217
- }
218
- if (!SESSION_ID.test(id))
219
- return { ok: false, reason: "refusing a session id with path characters" };
220
- if (!existsSync(projectsRoot))
221
- return { ok: false, reason: `no transcript root ${projectsRoot}` };
222
- for (const dir of readdirSync(projectsRoot)) {
223
- const path = join(projectsRoot, dir, `${id}.jsonl`);
224
- if (existsSync(path))
225
- return { ok: true, path };
226
- }
227
- return { ok: false, reason: `no transcript for session ${id}` };
228
- }
229
-
230
- // plugins/sp/lib/run-summary-core.ts
231
195
  var IDLE_GAP_MS = 300000;
232
196
  var SHELL_TOOLS = new Set(["bash", "shell", "exec", "exec_command", "run_command", "command"]);
233
197
  var SUBAGENT_TOOLS = new Set(["agent", "task", "invoke_subagent", "subagent", "dispatch_agent"]);
234
- var OPERATOR_TOOLS2 = new Set(["askuserquestion", "ask_user_question"]);
198
+ var LOWERCASE_OPERATOR_TOOLS = new Set(["askuserquestion", "ask_user_question"]);
235
199
  var ACTOR_PRECEDENCE = ["operator", "subagent", "shell", "other"];
236
200
  function actorOf(tool) {
237
201
  const t = tool.toLowerCase().replace(/[^a-z_]/g, "");
238
- if (OPERATOR_TOOLS2.has(t))
202
+ if (LOWERCASE_OPERATOR_TOOLS.has(t))
239
203
  return "operator";
240
204
  if (SUBAGENT_TOOLS.has(t))
241
205
  return "subagent";
@@ -385,7 +349,8 @@ function summaryRow(stage, status, span, measured) {
385
349
  token: formatTokenSplit(measured ? span.tokens : null)
386
350
  };
387
351
  }
388
- function buildRunSummary(lines, inputs, since, until) {
352
+ function buildRunSummary(rows, inputs, opts = {}) {
353
+ const { since, until } = opts;
389
354
  const windows = [];
390
355
  for (const { label, progress } of inputs) {
391
356
  const states = (progress.states ?? []).flatMap((s) => {
@@ -411,7 +376,6 @@ function buildRunSummary(lines, inputs, since, until) {
411
376
  }
412
377
  }
413
378
  windows.sort((a, b) => a.start - b.start);
414
- const { rows, skippedLines } = parseRows(lines);
415
379
  const measure = (start, end) => {
416
380
  const acc = newAcc(start);
417
381
  for (const [row, ts] of rows)
@@ -459,9 +423,87 @@ function buildRunSummary(lines, inputs, since, until) {
459
423
  total,
460
424
  actors: actorSplit(rows, totalStart, totalEnd),
461
425
  gate: { ms: null, reason: "no matching check receipt" },
462
- skippedLines
426
+ skippedLines: opts.skippedLines ?? 0
463
427
  };
464
428
  }
429
+ var SENSITIVE_PREFIXES = ["drizzle/", "packages/config/", ".github/", "plugins/sp/hooks/"];
430
+ var SENSITIVE_GLOBS = ["apps/server/src/**/auth*", "**/*secret*", "**/*.sql"];
431
+ function globToRegex(glob) {
432
+ const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replaceAll("**/", "\x00").replaceAll("**", "\x01").replaceAll("*", "[^/]*").replaceAll("\x00", "(?:.*/)?").replaceAll("\x01", ".*");
433
+ return new RegExp(`^${escaped}$`, "s");
434
+ }
435
+ var SENSITIVE_MATCHERS = [
436
+ ...SENSITIVE_PREFIXES.map((prefix) => ({
437
+ label: prefix,
438
+ test: (path) => path.startsWith(prefix)
439
+ })),
440
+ ...SENSITIVE_GLOBS.map((glob) => {
441
+ const regex = globToRegex(glob);
442
+ return { label: glob, test: (path) => regex.test(path) };
443
+ })
444
+ ];
445
+
446
+ // plugins/sp/lib/transcript.ts
447
+ import { existsSync as existsSync2, readdirSync } from "node:fs";
448
+ import { homedir } from "node:os";
449
+ import { join as join2 } from "node:path";
450
+ var SESSION_ID = /^[A-Za-z0-9_-]+$/;
451
+ var PI_ROLES = new Set(["user", "assistant", "toolResult"]);
452
+ function sniffFormat(rows) {
453
+ for (const row of rows) {
454
+ if (row.type === "message" && PI_ROLES.has(row.message?.role ?? ""))
455
+ return "pi";
456
+ if (row.type === "user" || row.type === "assistant")
457
+ return "claude";
458
+ }
459
+ return "unknown";
460
+ }
461
+ function parseRows(lines) {
462
+ const rows = [];
463
+ let skippedLines = 0;
464
+ for (const line of lines) {
465
+ if (!line.trim())
466
+ continue;
467
+ let row;
468
+ try {
469
+ row = JSON.parse(line);
470
+ } catch {
471
+ skippedLines++;
472
+ continue;
473
+ }
474
+ const ts = row.timestamp ? Date.parse(row.timestamp) : Number.NaN;
475
+ if (!Number.isNaN(ts))
476
+ rows.push([row, ts]);
477
+ }
478
+ return { rows, skippedLines, format: sniffFormat(rows.map(([r]) => r)) };
479
+ }
480
+ function resolveTranscript(env, projectsRoot = join2(homedir(), ".claude", "projects"), override) {
481
+ if (override)
482
+ return existsSync2(override) ? { ok: true, path: override } : { ok: false, reason: "no transcript" };
483
+ const id = env.CLAUDE_CODE_SESSION_ID;
484
+ if (!id) {
485
+ const piFile = env.PI_SESSION_FILE;
486
+ if (piFile !== undefined && piFile !== "") {
487
+ return existsSync2(piFile) ? { ok: true, path: piFile } : { ok: false, reason: `PI_SESSION_FILE ${piFile} does not exist` };
488
+ }
489
+ return {
490
+ ok: false,
491
+ reason: "no host session id; pass --transcript <path> (pi: ~/.pi/agent/sessions/<cwd-slug>/<file>.jsonl, " + "or set PI_SESSION_FILE)"
492
+ };
493
+ }
494
+ if (!SESSION_ID.test(id))
495
+ return { ok: false, reason: "refusing a session id with path characters" };
496
+ if (!existsSync2(projectsRoot))
497
+ return { ok: false, reason: `no transcript root ${projectsRoot}` };
498
+ for (const dir of readdirSync(projectsRoot)) {
499
+ const path = join2(projectsRoot, dir, `${id}.jsonl`);
500
+ if (existsSync2(path))
501
+ return { ok: true, path };
502
+ }
503
+ return { ok: false, reason: `no transcript for session ${id}` };
504
+ }
505
+
506
+ // plugins/sp/lib/run-summary-core.ts
465
507
  function renderSummaryMarkdown(summary) {
466
508
  const line = (r, bold = false) => {
467
509
  const stage = bold ? `**${r.stage}**` : r.stage;
@@ -493,9 +535,9 @@ function renderSummaryMarkdown(summary) {
493
535
  }
494
536
  function writeCloseSummary(run, opts = {}) {
495
537
  const cwd = opts.cwd ?? process.cwd();
496
- const runDir = join2(cwd, ".spur", "run");
497
- const rel = join2(".spur", "run", `${run.runId}-summary.md`);
498
- const abs = join2(cwd, rel);
538
+ const runDir = join3(cwd, ".spur", "run");
539
+ const rel = join3(".spur", "run", `${run.runId}-summary.md`);
540
+ const abs = join3(cwd, rel);
499
541
  const na = (reason) => {
500
542
  mkdirSync(runDir, { recursive: true });
501
543
  writeFileSync(abs, `Execution summary: n/a (${reason})
@@ -508,15 +550,19 @@ function writeCloseSummary(run, opts = {}) {
508
550
  const resolved = resolveTranscript(opts.env ?? getEnvVars(), opts.projectsRoot);
509
551
  if (!resolved.ok)
510
552
  return na(resolved.reason);
511
- const lines = readFileSync(resolved.path, "utf8").split(`
512
- `);
513
- const summary = buildRunSummary(lines, [{ progress: run.progress }], run.startedAt, run.completedAt);
553
+ const { rows, skippedLines } = parseRows(readFileSync(resolved.path, "utf8").split(`
554
+ `));
555
+ const summary = buildRunSummary(rows, [{ progress: run.progress }], {
556
+ since: run.startedAt,
557
+ until: run.completedAt,
558
+ skippedLines
559
+ });
514
560
  if (summary.rows.length === 0)
515
561
  return na("no timed attempts");
516
562
  mkdirSync(runDir, { recursive: true });
517
563
  writeFileSync(abs, `${renderSummaryMarkdown(summary)}
518
564
  `);
519
- writeFileSync(join2(runDir, `${run.runId}-summary.json`), JSON.stringify({ ...summary, runId: run.runId, startedAt: run.startedAt, completedAt: run.completedAt }, null, 2));
565
+ writeFileSync(join3(runDir, `${run.runId}-summary.json`), JSON.stringify({ ...summary, runId: run.runId, startedAt: run.startedAt, completedAt: run.completedAt }, null, 2));
520
566
  return rel;
521
567
  } catch (error) {
522
568
  return na(error instanceof Error ? error.message : String(error));
@@ -552,29 +598,10 @@ function refuseUnsafeRunId(runId) {
552
598
  console.error(" The run id must be a single safe filename component (alphanumeric/._-, no leading dot, no path separators, interpolation or traversal; same class as the task-pipeline route-reason guard, task 0804 R8). Allocate a fresh run id (uuid or timestamp slug) and retry.");
553
599
  process.exit(1);
554
600
  }
555
- function resolveAppEntry(spurBin) {
556
- const fallback = fileURLToPath(new URL("../../../apps/cli/src/index.ts", import.meta.url));
557
- const candidates = spurBin !== "" ? [spurBin] : [fallback];
558
- for (const candidate of candidates) {
559
- const tokens = candidate.split(/\s+/).filter(Boolean);
560
- const mainModule = [...tokens].reverse().find((t) => t.endsWith(".ts"));
561
- if (mainModule === undefined || !existsSync3(mainModule))
562
- continue;
563
- const repoRoot = resolve(dirname(mainModule), "..", "..", "..");
564
- const appEntry = join3(repoRoot, "packages", "app", "src", "index.ts");
565
- if (existsSync3(appEntry))
566
- return { entry: appEntry, portable: false };
567
- }
568
- const entry = fileURLToPath(new URL("../lib/inline-run.generated.mjs", import.meta.url));
569
- if (!existsSync3(entry)) {
570
- throw new Error("inline application bundle is missing \u2014 rebuild/install the sp plugin before running inline");
571
- }
572
- return { entry, portable: true };
573
- }
574
601
  var SETUP_EXPORTS = ["readInstalledInventory", "runInlineRunSetup", "writeInlineRunOutcome"];
575
602
  async function main() {
576
603
  if (!process.versions.bun) {
577
- const child = spawnSync("bun", [fileURLToPath(import.meta.url), ...process.argv.slice(2)], {
604
+ const child = spawnSync("bun", [fileURLToPath2(import.meta.url), ...process.argv.slice(2)], {
578
605
  stdio: "inherit"
579
606
  });
580
607
  if (child.error)
@@ -643,7 +670,7 @@ async function main() {
643
670
  refuseUnsafeRunId(runId);
644
671
  try {
645
672
  const { app: app2, portable: portable2 } = await loadInlineApp(spurBin, resolveAppEntry, ["runInlineRunDecide"]);
646
- const { app: lib } = await loadInlineApp(spurBin, () => ({ entry: fileURLToPath(new URL("../lib/inline-run.generated.mjs", import.meta.url)), portable: portable2 }), ["resolveDecideDecisionMakerEnabled", "loadSpurConfig"]);
673
+ const { app: lib } = await loadInlineApp(spurBin, () => ({ entry: fileURLToPath2(new URL("../lib/inline-run.generated.mjs", import.meta.url)), portable: portable2 }), ["resolveDecideDecisionMakerEnabled", "loadSpurConfig"]);
647
674
  const loadOpts = portable2 ? { embeddedSchemas: lib.EMBEDDED_SPUR_SCHEMAS } : undefined;
648
675
  const [enabled, spurConfig] = await Promise.all([
649
676
  lib.resolveDecideDecisionMakerEnabled(process.cwd(), loadOpts),
@@ -699,7 +726,7 @@ async function main() {
699
726
  inventory = await app.readInstalledInventory({
700
727
  file,
701
728
  spurBin,
702
- localCli: fileURLToPath(new URL("../../../apps/cli/src/index.ts", import.meta.url))
729
+ localCli: fileURLToPath2(new URL("../../../apps/cli/src/index.ts", import.meta.url))
703
730
  });
704
731
  } catch (error) {
705
732
  const message = `could not resolve the workflow definition: ${error instanceof Error ? error.message : String(error)}`;
@@ -3,13 +3,11 @@
3
3
  * handshake, mode dispatch. Mode bodies live in the bundled app service (`runInlineRun*` exports); the `.mjs`
4
4
  * twin re-enters Bun in `main()` (SQLite) and imports the bundle dynamically, so bare `node` never loads app code. */
5
5
  import { spawnSync } from 'node:child_process';
6
- import { existsSync } from 'node:fs';
7
- import { dirname, join, resolve } from 'node:path';
8
6
  import { fileURLToPath } from 'node:url';
9
7
  // Type-only namespace import (erased before bundling; the smoke scanner exempts `import type`).
10
8
  import type * as spurApp from '@gobing-ai/spur-app';
11
9
  import { getEnvVar } from '../lib/env';
12
- import { loadInlineApp } from '../lib/inline-run-app';
10
+ import { loadInlineApp, resolveAppEntry } from '../lib/inline-run-app';
13
11
  import { writeCloseSummary } from './run-summary';
14
12
 
15
13
  // EMBEDDED_SPUR_SCHEMAS: declared by the bundle twin, never exported by the barrel — optional
@@ -47,24 +45,6 @@ function refuseUnsafeRunId(runId: string): never {
47
45
  }
48
46
 
49
47
  /** Resolve the app entry: --spur-bin/SPUR_BIN main module if it proves a repo checkout, else the adjacent bundle. */
50
- function resolveAppEntry(spurBin: string): { entry: string; portable: boolean } {
51
- const fallback = fileURLToPath(new URL('../../../apps/cli/src/index.ts', import.meta.url));
52
- const candidates = spurBin !== '' ? [spurBin] : [fallback];
53
- for (const candidate of candidates) {
54
- const tokens = candidate.split(/\s+/).filter(Boolean);
55
- // Main module = last path-like token; only a .ts source entry proves a repo checkout.
56
- const mainModule = [...tokens].reverse().find((t) => t.endsWith('.ts'));
57
- if (mainModule === undefined || !existsSync(mainModule)) continue;
58
- const repoRoot = resolve(dirname(mainModule), '..', '..', '..');
59
- const appEntry = join(repoRoot, 'packages', 'app', 'src', 'index.ts');
60
- if (existsSync(appEntry)) return { entry: appEntry, portable: false };
61
- }
62
- const entry = fileURLToPath(new URL('../lib/inline-run.generated.mjs', import.meta.url));
63
- if (!existsSync(entry)) {
64
- throw new Error('inline application bundle is missing — rebuild/install the sp plugin before running inline');
65
- }
66
- return { entry, portable: true };
67
- }
68
48
 
69
49
  const SETUP_EXPORTS = ['readInstalledInventory', 'runInlineRunSetup', 'writeInlineRunOutcome'] as const;
70
50
 
@@ -24437,6 +24437,7 @@ function isRfc3339Timestamp(value) {
24437
24437
  var ALL_FINDING_CODES = [
24438
24438
  "L1.markdown-parse",
24439
24439
  "L1.schema-validation",
24440
+ "L1.wbs-collision",
24440
24441
  "L2.missing-required-section",
24441
24442
  "L2.forbidden-section",
24442
24443
  "L2.disallowed-section",
@@ -24464,6 +24465,7 @@ var ALL_FINDING_CODES = [
24464
24465
  "L4.design-placeholder",
24465
24466
  "L4.feature-not-found",
24466
24467
  "L4.feature-terminal",
24468
+ "L4.feature-deprecated",
24467
24469
  "L4.missing-feature-id",
24468
24470
  "L4.parent-not-found",
24469
24471
  "L4.dependency-not-found",
@@ -24901,7 +24903,7 @@ var POST_WATERMARK_TABLES = [
24901
24903
  "history_daily_stats",
24902
24904
  "history_board_source_daily"
24903
24905
  ];
24904
- var HISTORY_IMPORT_SCHEMA_VERSION = "0.5.22";
24906
+ var HISTORY_IMPORT_SCHEMA_VERSION = "0.5.24";
24905
24907
  var HISTORY_IMPORT_SCHEMA_SQL = `
24906
24908
  CREATE TABLE IF NOT EXISTS history_import_checkpoint (
24907
24909
  source TEXT NOT NULL,
@@ -27620,6 +27622,8 @@ var artifactsTable = defineTable("artifacts", {
27620
27622
  ...standardColumns
27621
27623
  });
27622
27624
  var artifacts = artifactsTable.table;
27625
+ var encoder = new TextEncoder;
27626
+ var decoder = new TextDecoder;
27623
27627
  init_schema();
27624
27628
  var phaseRunsTable = defineTable("phase_runs", {
27625
27629
  id: text("id").primaryKey(),
@@ -29820,7 +29824,15 @@ function escapeYamlValue(value) {
29820
29824
  }
29821
29825
  init_zod();
29822
29826
  var TASK_STATUSES = ["backlog", "todo", "wip", "testing", "blocked", "done", "cancelled"];
29823
- var FEATURE_STATUSES = ["backlog", "active", "verifying", "blocked", "done", "cancelled"];
29827
+ var FEATURE_STATUSES = [
29828
+ "backlog",
29829
+ "active",
29830
+ "verifying",
29831
+ "blocked",
29832
+ "done",
29833
+ "cancelled",
29834
+ "deprecated"
29835
+ ];
29824
29836
  var PRIORITIES = ["P0", "P1", "P2", "P3"];
29825
29837
  var PROFILES = [
29826
29838
  "simple",
@@ -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,36 @@ function blockingReviewFindings(taskContent, deferrals) {
179
210
  }));
180
211
  return classify(items, [...tableDeferrals, ...deferrals]);
181
212
  }
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
+ }
182
232
  function buildReviewGateArtifact(wbs, taskContent, deferrals) {
183
- const items = blockingReviewFindings(taskContent, deferrals);
233
+ const items = [
234
+ ...blockingReviewFindings(taskContent, deferrals),
235
+ ...reviewContractViolations(taskContent).map((v) => ({
236
+ id: makeItemId("review-contract", v.location, v.text),
237
+ category: "review-contract",
238
+ class: "blocking",
239
+ location: v.location,
240
+ text: v.text
241
+ }))
242
+ ];
184
243
  const blocking = items.filter((i) => i.class === "blocking");
185
244
  const counts = { blocking: 0, deferrable: 0, advisory: 0, housekeeping: 0 };
186
245
  for (const item of items)
@@ -192,7 +251,8 @@ function buildReviewGateArtifact(wbs, taskContent, deferrals) {
192
251
  "review-finding": true,
193
252
  "diff-marker": false,
194
253
  "unchecked-box": false,
195
- "staging-residue": false
254
+ "staging-residue": false,
255
+ "review-contract": true
196
256
  },
197
257
  items,
198
258
  counts
@@ -518,6 +578,12 @@ function settleMode(opts, env, io) {
518
578
  }
519
579
  function reportMode(opts, env, io) {
520
580
  const runDir = join2(opts.root, ".spur", "run");
581
+ const verdictPath = recordedVerdictPath(runDir, opts.wbs, fs);
582
+ if (!fs.existsSync(verdictPath)) {
583
+ io.err(`residual-report: no verdict at ${verdictPath}; nothing to report for ${opts.wbs}
584
+ `);
585
+ return 0;
586
+ }
521
587
  const sweep = loadVerdict(runDir, opts.wbs).checks.find((c) => c.name === "residual-sweep");
522
588
  if (sweep === undefined || sweep.status !== "fail")
523
589
  return 0;
@@ -558,6 +624,7 @@ process.exit(main(process.argv.slice(2)));
558
624
  export {
559
625
  verdictDisagreementNote,
560
626
  scanResiduals2 as scanResiduals,
627
+ reviewContractViolations,
561
628
  renderReport,
562
629
  recordedVerdictPath,
563
630
  parseScanArgs,
@@ -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);