@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
@@ -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)
@@ -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
  `);
@@ -4,9 +4,10 @@
4
4
  * (`--no-summary` skips it). Inputs: one or more `spur workflow progress <run-id> --json` files
5
5
  * (`--progress [<label>=]<file>`) and the host transcript (resolved like session-timeline, or
6
6
  * `--transcript`), OR the close-written per-run summary files (`--rollup [<label>=]<file>`, 1146 R3).
7
- * Measurement and rendering live in `../lib/run-summary-core` (ADR-130 glue budget); this script is
8
- * argv parsing and dispatch. Reporting only: no transcript or no timed attempts → an `n/a` result,
9
- * exit 0. Node builtins only.
7
+ * Measurement lives in `packages/app/src/workflow/run-summary.ts` (reached through the generated
8
+ * pure bundle entry); presentation and host IO live in `../lib/run-summary-core` (ADR-130 glue
9
+ * budget). This script is argv parsing and dispatch. Reporting only: no transcript or no timed
10
+ * attempts → an `n/a` result, exit 0. Node builtins only.
10
11
  */
11
12
  import { readFileSync } from 'node:fs';
12
13
  import { getEnvVars } from '../lib/env';
@@ -18,7 +19,7 @@ import {
18
19
  renderRollupMarkdown,
19
20
  renderSummaryMarkdown,
20
21
  } from '../lib/run-summary-core';
21
- import { resolveTranscript } from '../lib/transcript';
22
+ import { parseRows, resolveTranscript } from '../lib/transcript';
22
23
 
23
24
  // Re-exported so the suite and any consumer keep one import path for the whole surface.
24
25
  export * from '../lib/run-summary-core';
@@ -90,7 +91,8 @@ export function main(
90
91
  const [label, file] = eq > 0 ? [arg.slice(0, eq), arg.slice(eq + 1)] : [undefined, arg];
91
92
  return { label, progress: JSON.parse(readFileSync(file, 'utf8')) as Progress };
92
93
  });
93
- const summary = buildRunSummary(readFileSync(resolved.path, 'utf8').split('\n'), inputs, since);
94
+ const { rows, skippedLines } = parseRows(readFileSync(resolved.path, 'utf8').split('\n'));
95
+ const summary = buildRunSummary(rows, inputs, { since, skippedLines });
94
96
  summary.gate = gateFromReceipt(process.cwd(), inputs[0]?.progress.runId ?? '');
95
97
  write(
96
98
  `${markdown ? renderSummaryMarkdown(summary) : JSON.stringify({ ...summary, transcript: resolved.path })}\n`,
@@ -13,9 +13,12 @@ function getEnvVars() {
13
13
  import { existsSync, readdirSync } from "node:fs";
14
14
  import { homedir } from "node:os";
15
15
  import { join } from "node:path";
16
- var SESSION_ID = /^[A-Za-z0-9_-]+$/;
17
- var OPERATOR_TOOLS = new Set(["AskUserQuestion", "ask_user_question"]);
18
- var PI_ROLES = new Set(["user", "assistant", "toolResult"]);
16
+
17
+ // plugins/sp/lib/plan-projection.generated.mjs
18
+ var NON_ABORTING_PREFLIGHT_CODES = new Set([
19
+ "L4.scenario-unverified",
20
+ "L4.verifying-incomplete-tasks"
21
+ ]);
19
22
  var zeroTokens = () => ({ input: 0, cacheCreate: 0, cacheRead: 0, output: 0 });
20
23
  function formatDuration(ms) {
21
24
  const s = Math.round(ms / 1000);
@@ -35,6 +38,23 @@ function formatTokenSplit(t) {
35
38
  const nonCached = t.input + t.cacheCreate + t.output;
36
39
  return `${formatTokens(nonCached + t.cacheRead)} / ${formatTokens(nonCached)}`;
37
40
  }
41
+ var OPERATOR_TOOLS = new Set(["AskUserQuestion", "ask_user_question"]);
42
+ function firstTextContent(row) {
43
+ const content = row.message?.content;
44
+ if (typeof content === "string")
45
+ return content;
46
+ if (!Array.isArray(content))
47
+ return;
48
+ return content.find((b) => b.type === "text")?.text;
49
+ }
50
+ function skillInvocationPrompt(text) {
51
+ const name = /^\s*<skill name="([^"]+)"/.exec(text)?.[1];
52
+ if (name === undefined)
53
+ return;
54
+ const close = text.lastIndexOf("</skill>");
55
+ const args = close < 0 ? "" : text.slice(close + "</skill>".length).trim();
56
+ return args === "" ? `/${name}` : `/${name} ${args}`;
57
+ }
38
58
  function promptText(row) {
39
59
  if (row.type === "custom_message") {
40
60
  if (row.display === false)
@@ -59,22 +79,6 @@ function promptText(row) {
59
79
  return;
60
80
  return content.find((b) => b.type === "text")?.text;
61
81
  }
62
- function firstTextContent(row) {
63
- const content = row.message?.content;
64
- if (typeof content === "string")
65
- return content;
66
- if (!Array.isArray(content))
67
- return;
68
- return content.find((b) => b.type === "text")?.text;
69
- }
70
- function skillInvocationPrompt(text) {
71
- const name = /^\s*<skill name="([^"]+)"/.exec(text)?.[1];
72
- if (name === undefined)
73
- return;
74
- const close = text.lastIndexOf("</skill>");
75
- const args = close < 0 ? "" : text.slice(close + "</skill>".length).trim();
76
- return args === "" ? `/${name}` : `/${name} ${args}`;
77
- }
78
82
  function sumTokens(all) {
79
83
  const total = zeroTokens();
80
84
  for (const t of all)
@@ -146,6 +150,29 @@ function accumulate(acc, row, ts) {
146
150
  });
147
151
  }
148
152
  }
153
+ var SHELL_TOOLS = new Set(["bash", "shell", "exec", "exec_command", "run_command", "command"]);
154
+ var SUBAGENT_TOOLS = new Set(["agent", "task", "invoke_subagent", "subagent", "dispatch_agent"]);
155
+ var LOWERCASE_OPERATOR_TOOLS = new Set(["askuserquestion", "ask_user_question"]);
156
+ var SENSITIVE_PREFIXES = ["drizzle/", "packages/config/", ".github/", "plugins/sp/hooks/"];
157
+ var SENSITIVE_GLOBS = ["apps/server/src/**/auth*", "**/*secret*", "**/*.sql"];
158
+ function globToRegex(glob) {
159
+ const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replaceAll("**/", "\x00").replaceAll("**", "\x01").replaceAll("*", "[^/]*").replaceAll("\x00", "(?:.*/)?").replaceAll("\x01", ".*");
160
+ return new RegExp(`^${escaped}$`, "s");
161
+ }
162
+ var SENSITIVE_MATCHERS = [
163
+ ...SENSITIVE_PREFIXES.map((prefix) => ({
164
+ label: prefix,
165
+ test: (path) => path.startsWith(prefix)
166
+ })),
167
+ ...SENSITIVE_GLOBS.map((glob) => {
168
+ const regex = globToRegex(glob);
169
+ return { label: glob, test: (path) => regex.test(path) };
170
+ })
171
+ ];
172
+
173
+ // plugins/sp/lib/transcript.ts
174
+ var SESSION_ID = /^[A-Za-z0-9_-]+$/;
175
+ var PI_ROLES = new Set(["user", "assistant", "toolResult"]);
149
176
  function sniffFormat(rows) {
150
177
  for (const row of rows) {
151
178
  if (row.type === "message" && PI_ROLES.has(row.message?.role ?? ""))
@@ -11,7 +11,15 @@ function getEnvVars() {
11
11
  return process.env;
12
12
  }
13
13
 
14
- // plugins/sp/scripts/task-diffstat.ts
14
+ // plugins/sp/lib/plan-projection.generated.mjs
15
+ var NON_ABORTING_PREFLIGHT_CODES = new Set([
16
+ "L4.scenario-unverified",
17
+ "L4.verifying-incomplete-tasks"
18
+ ]);
19
+ var OPERATOR_TOOLS = new Set(["AskUserQuestion", "ask_user_question"]);
20
+ var SHELL_TOOLS = new Set(["bash", "shell", "exec", "exec_command", "run_command", "command"]);
21
+ var SUBAGENT_TOOLS = new Set(["agent", "task", "invoke_subagent", "subagent", "dispatch_agent"]);
22
+ var LOWERCASE_OPERATOR_TOOLS = new Set(["askuserquestion", "ask_user_question"]);
15
23
  var SENSITIVE_PREFIXES = ["drizzle/", "packages/config/", ".github/", "plugins/sp/hooks/"];
16
24
  var SENSITIVE_GLOBS = ["apps/server/src/**/auth*", "**/*secret*", "**/*.sql"];
17
25
  function globToRegex(glob) {
@@ -32,6 +40,8 @@ function sensitiveReasonForPath(path) {
32
40
  const match = SENSITIVE_MATCHERS.find((entry) => entry.test(path));
33
41
  return match ? `matches sensitive pattern ${match.label}` : null;
34
42
  }
43
+
44
+ // plugins/sp/scripts/task-diffstat.ts
35
45
  function defaultGitRunner(cwd) {
36
46
  return (args) => {
37
47
  const run = spawnSync("git", args, { cwd, encoding: "utf8" });
@@ -65,45 +65,14 @@ export interface DiffstatResult extends Diffstat {
65
65
  exitCode: number;
66
66
  }
67
67
 
68
- /**
69
- * Sensitive paths (0943 R2c): any changed path under one of these forces the safety lane
70
- * before the `task-triage` decide runs — sensitive work is never lane-negotiated with a
71
- * model. Frozen list: `drizzle/`, `packages/config/`, auth-prefixed basenames anywhere
72
- * under the server src tree, `*secret*` basenames, `.github/`, `plugins/sp/hooks/`, and
73
- * any `*.sql` — spelled out in SENSITIVE_PREFIXES / SENSITIVE_GLOBS below (kept as data,
74
- * not prose, so the `*` / `/` sequences survive the doc comment).
75
- */
76
- export const SENSITIVE_PREFIXES = ['drizzle/', 'packages/config/', '.github/', 'plugins/sp/hooks/'] as const;
77
- export const SENSITIVE_GLOBS = ['apps/server/src/**/auth*', '**/*secret*', '**/*.sql'] as const;
78
-
79
- // Glob → RegExp: `**` + `/` matches any-or-no directories, bare `**` any characters, `*` within one segment.
80
- function globToRegex(glob: string): RegExp {
81
- const escaped = glob
82
- .replace(/[.+^${}()|[\]\\]/g, '\\$&')
83
- .replaceAll('**/', '\u0000')
84
- .replaceAll('**', '\u0001')
85
- .replaceAll('*', '[^/]*')
86
- .replaceAll('\u0000', '(?:.*/)?')
87
- .replaceAll('\u0001', '.*');
88
- return new RegExp(`^${escaped}$`, 's');
89
- }
90
-
91
- const SENSITIVE_MATCHERS: Array<{ label: string; test: (path: string) => boolean }> = [
92
- ...SENSITIVE_PREFIXES.map((prefix) => ({
93
- label: prefix,
94
- test: (path: string): boolean => path.startsWith(prefix),
95
- })),
96
- ...SENSITIVE_GLOBS.map((glob) => {
97
- const regex = globToRegex(glob);
98
- return { label: glob, test: (path: string): boolean => regex.test(path) };
99
- }),
100
- ];
101
-
102
- /** Sensitive-pattern match for one path, or null — mirrors wrapup-drift-probe's driftReasonForPath. */
103
- export function sensitiveReasonForPath(path: string): string | null {
104
- const match = SENSITIVE_MATCHERS.find((entry) => entry.test(path));
105
- return match ? `matches sensitive pattern ${match.label}` : null;
106
- }
68
+ // Task 1176: the sensitive-path policy (0943 R2c) moved to the workflow/task owner —
69
+ // `packages/app/src/workflow/task-triage.ts`, reached through the generated pure bundle. That policy
70
+ // still forces the safety lane for any changed path under the frozen sensitive list before the
71
+ // `task-triage` decide runs, so sensitive work is never lane-negotiated with a model; this entry
72
+ // keeps git capture and the output artifact, and re-exports the policy for existing callers.
73
+ import { SENSITIVE_GLOBS, SENSITIVE_PREFIXES, sensitiveReasonForPath } from '../lib/plan-projection.generated.mjs';
74
+
75
+ export { SENSITIVE_GLOBS, SENSITIVE_PREFIXES, sensitiveReasonForPath };
107
76
 
108
77
  function defaultGitRunner(cwd?: string): GitRunner {
109
78
  return (args: string[]): GitRunResult => {
@@ -82,6 +82,12 @@ not have and work you were not asked to do.
82
82
  - **Never implement a requirement belonging to another WBS**, even when it looks like a
83
83
  prerequisite. If the target genuinely cannot proceed without it, stop and say so — a blocked task
84
84
  is a cheap finding; a two-task diff costs the reverts.
85
+ - **Flip the target's `## Plan` boxes as each step completes.** They are this pass's progress
86
+ ledger, and the record-time residual sweep treats an *unchecked* Plan box as blocking: it
87
+ downgrades the verdict to PARTIAL and closes the `done` gate
88
+ (`docs/design/task-residual-sweep.md`), which costs a remediation cycle and a manual re-record.
89
+ Leave every finished step ticked through `spur task update <wbs> --section Plan --from-file`, and
90
+ if you deliberately skipped one, say so in the Solution change map.
85
91
 
86
92
  Why this is a rule and not a nicety: driving task 0486 lost several hours to exactly this. Task 0485
87
93
  was committed but still `todo` in the tree, and two different executors (omp run `ca130182`, claude
@@ -165,7 +171,7 @@ that single full gate.
165
171
  1. The **changed-path matrix** below (narrow behavior tests + affected consumers);
166
172
  2. the resolved `quality-gate` script in `light` mode (source-repo probe, else the installed twin) (biome on changed files, per-workspace typecheck, rule presets `rules:pre`/`rules:post`, related tests).
167
173
 
168
- Neither step takes the host-wide full-gate lock. The full gate is named only for the pipeline's `test` and `test-recheck` hops.
174
+ Neither step takes the full project gate. The full gate is named only for the pipeline's `test` and `test-recheck` hops.
169
175
 
170
176
  | Changed surface | Required targeted tests | Required typechecks |
171
177
  | --- | --- | --- |
@@ -92,7 +92,8 @@ Before declaring a task `done`, run this lightweight checklist. It catches the m
92
92
  - [ ] Solution section contains `file:line` citations for every changed file.
93
93
  - [ ] API/library claims in changed docs carry a source citation (URL or `path:line`) and an honest
94
94
  confidence level (HIGH/MEDIUM/LOW) per source-driven-development; the deterministic advisory backstop is
95
- the `cited-version-strings` / `cited-api-claims` rules (`.spur/rules/docs/sourced-api-claims.yaml`).
95
+ the `cited-version-strings` / `cited-api-claims` / `cited-api-claims-confidence` rules
96
+ (`.spur/rules/docs/sourced-api-claims.yaml`, run ad hoc — not wired into a preset).
96
97
  - [ ] Review section has P1–P4 findings table (even if all rows are empty — the table itself proves review happened).
97
98
 
98
99
  This checklist runs as part of the verify step when the task reaches `testing` status. It does not replace the formal gates — it augments them with between-pipeline hygiene checks.
@@ -202,6 +202,10 @@ citation in the SAME cell, or lint rejects the row with `evidence-citation` (blo
202
202
  - The rule is cell-local: a citation in another row or a "receipt above" cross-reference does not
203
203
  satisfy it. Marker-free receipts (test counts, reviewed-log notes) need no citation. The check is
204
204
  presence-only — which citation proves which claim is not deterministically decidable.
205
+ - The same two predicates (`hasClaimMarker` / `hasCitationForm` in
206
+ `packages/app/src/services/verify-answer-lint.ts`) gate the review report's findings table through
207
+ `residual-scan review-gate` as the blocking `review-contract` category — one definition of
208
+ "external claim" and one set of accepted citation forms for both paths.
205
209
 
206
210
  ## Hedged evidence is rejected on MET rows
207
211
 
@@ -82,11 +82,11 @@ The command forwards these via `$ARGUMENTS`:
82
82
  > <!-- pipeline-tokens:end -->
83
83
  > matched as a **distinct hyphen-word**
84
84
  > (machine-checked by
85
- > [`detectPipelineDriving`](../../scripts/dogfood-testing/detect-pipeline-driving.ts);
85
+ > [`detectPipelineDriving`](../../lib/dogfood-pipeline.ts);
86
86
  > see [§Pipeline-driving word-boundary contract](#pipeline-driving-word-boundary-contract))
87
87
  > → `⚠ pipeline-driving testee detected; pass --max-retry 0 (observe-only) or --max-retry N (fix mode, tree mutation acknowledged)`.
88
88
  > - **Mutating `--fix` modes** — `--fix all` / `--fix blockers-first`, boundary-guarded via
89
- > [`hasMutatingFixMode`](../../scripts/dogfood-testing/detect-pipeline-driving.ts) (never matches
89
+ > [`hasMutatingFixMode`](../../lib/dogfood-pipeline.ts) (never matches
90
90
  > `--fix none` / `--focus all` / `--prefix all`). No pipeline token required: a verify/review leg
91
91
  > with `--fix all` mutates the tree on its own (0280 dogfood P2, task 0293). Honesty note:
92
92
  > `--max-retry 0` here bounds **the driver only** — the testee still mutates the tree — so the
@@ -64,7 +64,7 @@ Run the steps in order. Each step's depth lives in its reference; this file is t
64
64
  change a frontier feature's status, the report **leads with the drift summary** ("sync first")
65
65
  and the ranking is computed over the *post-sync* status view. Details:
66
66
  [references/signal-derivation.md](references/signal-derivation.md) §0.
67
- 1. **Assemble the candidate set.** `spur feature list --json`; drop `done`/`cancelled`, drop
67
+ 1. **Assemble the candidate set.** `spur feature list --json`; drop `done`/`cancelled`/`deprecated`, drop
68
68
  structural containers (has children, no own open tasks — defect D1), drop the map/feature that
69
69
  owns this investigation if present. The `group` tag is **not** a reliable container marker (D3).
70
70
  2. **Gate on actionability.** Read the frontier predicate **at runtime** from
@@ -167,7 +167,7 @@ rule at the source, so no per-path shim is needed:
167
167
  | `spur agent loop` | `AgentService.run` per drained iteration | Same resolution path as `spur agent run`; inherits its own `SPUR_ROLE` |
168
168
  | `spur serve` team supervisor → member | spawns `spur agent loop` | Member inherits the supervisor's `SPUR_ROLE` (recursive by construction) |
169
169
  | Native subagent fan-out (this skill's default) | in-session `Task()`/`Skill()` | In-session subagents share the host session; when they themselves dispatch, the host's role is already in the session env — the rule holds at the next `spur agent run` boundary |
170
- | `plugins/sp/evals/run-eval.ts` | `spawnSync('spur agent run', …)` per scenario | Out of scope: a top-level eval harness, not a fan-out — no dispatcher role exists to inherit; each scenario is an independent top-level run (documented, no shim) |
170
+ | `scripts/commands/skill-eval.ts` | `spawnSync('spur agent run', …)` per scenario | Out of scope: a top-level eval harness, not a fan-out — no dispatcher role exists to inherit; each scenario is an independent top-level run (documented, no shim) |
171
171
 
172
172
  The inventory is recorded in `docs/04_DESIGN.md` § `spur agent run` envelope (`roleOrigin`).
173
173
  Paths that shell out to `spur agent run` without the rule would silently drop or double-attribute
@@ -101,10 +101,13 @@ line: `Verification: HIGH|MEDIUM|LOW — <first failing check and deviation, or
101
101
  - **Honest confidence** — the stated HIGH/MEDIUM/LOW matches the evidence: HIGH requires a check against the pinned source this session; MEDIUM/LOW items are labeled, and LOW is never presented as fact.
102
102
  - **HIGH** — all three checks pass. **MEDIUM** — coverage intact but some citations are off-version (latest docs for a pinned dependency) or secondary-source only. **LOW** — an API/library claim lacks any citation, or a LOW-confidence claim was asserted as fact.
103
103
 
104
- Deterministic backstop: the `cited-version-strings` / `cited-api-claims` rules
105
- (`.spur/rules/docs/sourced-api-claims.yaml`, run in `recommended-pre-check`) flag markdown lines that
106
- assert version strings or time-anchored API claims with no citation anchor on the same line. They
107
- are an info-severity advisory backstop for this contract (they do not gate; the checks above and the SECUA checklist are the enforcement points).
104
+ Deterministic backstop: the `cited-version-strings`, `cited-api-claims` and
105
+ `cited-api-claims-confidence` rules (`.spur/rules/docs/sourced-api-claims.yaml`) flag markdown lines
106
+ that assert a version string or a time-anchored API claim with no citation anchor on the same line,
107
+ and claim lines that carry no HIGH/MEDIUM/LOW level. They are an info-severity advisory backstop for
108
+ this contract and are deliberately NOT wired into `recommended-pre-check` — run them ad hoc with
109
+ `spur rule run --file .spur/rules/docs/sourced-api-claims.yaml`. The checks above and the SECUA
110
+ checklist remain the enforcement points.
108
111
 
109
112
  ## Common Rationalizations
110
113
 
@@ -8,8 +8,9 @@ see_also:
8
8
  # spur builder - release plumbing
9
9
 
10
10
  `spur builder` is the CLI for **version bumps and release tags**. It wraps the internal
11
- `spur-dev release` flow behind a public two-verb surface, promoted verbatim from
12
- `scripts/commands/release.ts` (now a thin forwarder to the same implementation). Package ids are
11
+ `release-ops` implementation behind a public two-verb surface. The repo-local forwarder
12
+ (`scripts/commands/release.ts`) and its `spur-dev` dispatcher cases were retired in task 1173; the
13
+ root `bun run bump-ver` / `bun run drop-tags` names route to `spur builder` directly. Package ids are
13
14
  the unscoped short names (`@gobing-ai/spur` → `spur`); the released set and the aggregate tag are
14
15
  discovered from the repo's own workspace manifests, so the same code serves any git+semver
15
16
  monorepo.