@gobing-ai/spur 0.4.1 → 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 (159) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/config.global.yaml +4 -2
  3. package/config/decisions/maker-liveness.yaml +39 -0
  4. package/config/plugin-scripts.json +0 -4
  5. package/config/rules/README.md +1 -0
  6. package/config/rules/docs/sourced-api-claims.yaml +56 -12
  7. package/config/rules/strict/runtime-boundaries.yaml +7 -0
  8. package/config/script-placement-baseline.json +1 -19
  9. package/config/templates/AGENTS.md +1 -1
  10. package/config/workflows/feature-lifecycle.yaml +46 -1
  11. package/config/workflows/feature-verification.yaml +11 -0
  12. package/config/workflows/wrapup-pipeline.yaml +1 -1
  13. package/package.json +10 -10
  14. package/plugins/sp/README.md +23 -18
  15. package/plugins/sp/agents/super-planner.md +5 -6
  16. package/plugins/sp/agents/super-reviewer.md +43 -0
  17. package/plugins/sp/bunfig.toml +8 -5
  18. package/plugins/sp/commands/dev-review-session.md +2 -1
  19. package/plugins/sp/commands/dev-runall.md +18 -9
  20. package/plugins/sp/hooks/careful-guard.ts +1 -1
  21. package/plugins/sp/hooks/context-post-tool.ts +7 -404
  22. package/plugins/sp/hooks/context-session-start.ts +6 -192
  23. package/plugins/sp/hooks/context-session-stop.ts +6 -91
  24. package/plugins/sp/hooks/pi/guard-extension.ts +26 -12
  25. package/plugins/sp/hooks/task-write-guard.ts +1 -1
  26. package/plugins/sp/{hooks → lib}/agent-hint.ts +1 -1
  27. package/plugins/sp/lib/context-owner.ts +56 -0
  28. package/plugins/sp/lib/context-post-tool.ts +432 -0
  29. package/plugins/sp/lib/context-session-start.ts +214 -0
  30. package/plugins/sp/lib/context-session-stop.ts +196 -0
  31. package/plugins/sp/lib/dogfood-pipeline.ts +265 -0
  32. package/plugins/sp/lib/dogfood-report.ts +278 -0
  33. package/plugins/sp/lib/idea-handoff.generated.mjs +6 -4
  34. package/plugins/sp/lib/inline-run-app.ts +40 -0
  35. package/plugins/sp/lib/inline-run.generated.d.mts +3 -3
  36. package/plugins/sp/lib/inline-run.generated.mjs +1361 -87
  37. package/plugins/sp/lib/plan-projection.generated.d.mts +169 -0
  38. package/plugins/sp/lib/plan-projection.generated.mjs +704 -1
  39. package/plugins/sp/lib/quality-gate.generated.d.mts +0 -12
  40. package/plugins/sp/lib/quality-gate.generated.mjs +62 -231
  41. package/plugins/sp/lib/residual-scan.generated.d.mts +7 -1
  42. package/plugins/sp/lib/residual-scan.generated.mjs +71 -6
  43. package/plugins/sp/lib/run-summary-core.ts +34 -370
  44. package/plugins/sp/lib/transcript.ts +37 -200
  45. package/plugins/sp/plugin.json +1 -1
  46. package/plugins/sp/references/roles.md +1 -1
  47. package/plugins/sp/scripts/batch-plan.mjs +740 -1
  48. package/plugins/sp/scripts/batch-plan.ts +48 -3
  49. package/plugins/sp/scripts/batch-preflight.mjs +23 -1
  50. package/plugins/sp/scripts/batch-preflight.ts +35 -295
  51. package/plugins/sp/scripts/daily-summary/daily-summary.mjs +108 -87
  52. package/plugins/sp/scripts/daily-summary/daily-summary.ts +111 -104
  53. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.mjs +2 -1
  54. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts +7 -265
  55. package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +3 -0
  56. package/plugins/sp/scripts/dogfood-testing/validate-report.ts +6 -272
  57. package/plugins/sp/scripts/feature-verification-steps.mjs +7 -123
  58. package/plugins/sp/scripts/feature-verification-steps.ts +29 -181
  59. package/plugins/sp/scripts/inline-run-dispatch.mjs +27 -26
  60. package/plugins/sp/scripts/inline-run-dispatch.ts +1 -21
  61. package/plugins/sp/scripts/inline-run-setup.mjs +158 -120
  62. package/plugins/sp/scripts/inline-run-setup.ts +1 -21
  63. package/plugins/sp/scripts/pr-reviewing.mjs +19 -5
  64. package/plugins/sp/scripts/pr-reviewing.ts +34 -9
  65. package/plugins/sp/scripts/quality-gate.mjs +38 -208
  66. package/plugins/sp/scripts/residual-scan.mjs +73 -6
  67. package/plugins/sp/scripts/residual-scan.ts +8 -5
  68. package/plugins/sp/scripts/run-summary.mjs +160 -95
  69. package/plugins/sp/scripts/run-summary.ts +7 -5
  70. package/plugins/sp/scripts/session-timeline.mjs +57 -29
  71. package/plugins/sp/scripts/session-timeline.ts +4 -18
  72. package/plugins/sp/scripts/task-diffstat.mjs +43 -30
  73. package/plugins/sp/scripts/task-diffstat.ts +40 -71
  74. package/plugins/sp/skills/code-implementation/SKILL.md +7 -1
  75. package/plugins/sp/skills/code-verification/references/secu-review.md +2 -1
  76. package/plugins/sp/skills/code-verification/references/verdict-schema.md +4 -0
  77. package/plugins/sp/skills/dogfood-testing/SKILL.md +2 -2
  78. package/plugins/sp/skills/indexed-context/SKILL.md +14 -7
  79. package/plugins/sp/skills/next-feature/SKILL.md +1 -1
  80. package/plugins/sp/skills/parallel-execution/SKILL.md +2 -2
  81. package/plugins/sp/skills/parallel-execution/references/dispatch-surface.md +2 -2
  82. package/plugins/sp/skills/parallel-execution/references/fan-out-patterns.md +2 -2
  83. package/plugins/sp/skills/pr-reviewing/SKILL.md +7 -3
  84. package/plugins/sp/skills/session-review/SKILL.md +5 -3
  85. package/plugins/sp/skills/source-driven-development/SKILL.md +7 -4
  86. package/plugins/sp/skills/spur-cli/references/builder.md +3 -2
  87. package/plugins/sp/skills/spur-cli/references/decision.md +40 -8
  88. package/plugins/sp/skills/spur-cli/references/features.md +46 -5
  89. package/plugins/sp/skills/spur-cli/references/projects.md +7 -5
  90. package/plugins/sp/skills/spur-dev/SKILL.md +2 -0
  91. package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +27 -0
  92. package/plugins/sp/skills/spur-dev/references/dev-operations.md +2 -3
  93. package/plugins/sp/skills/spur-dev/references/execution-batch-report.md +6 -1
  94. package/plugins/sp/skills/spur-dev/references/execution-batch.md +56 -7
  95. package/plugins/sp/skills/spur-dev/references/execution-parallel-isolation.md +4 -1
  96. package/plugins/sp/skills/spur-dev/references/execution-worktree-landing.md +22 -0
  97. package/plugins/sp/skills/spur-dev/references/execution-worktree-setup.md +14 -0
  98. package/plugins/sp/skills/spur-dev/references/flag-glossary.md +13 -11
  99. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +37 -25
  100. package/spur.js +89792 -76992
  101. package/web/_astro/{BoardApp.D66k_s_T.js → BoardApp.5IIl2O-z.js} +1 -1
  102. package/web/_astro/BoardApp.Drnx-KeM.js +324 -0
  103. package/web/_astro/TaskDetail.Ci4LNn9S.js +1 -0
  104. package/web/_astro/{arc._BUadrdH.js → arc.j2EVkYVd.js} +1 -1
  105. package/web/_astro/{architectureDiagram-3BPJPVTR.Dh7xS1Pp.js → architectureDiagram-3BPJPVTR.DZWSFqEG.js} +1 -1
  106. package/web/_astro/{blockDiagram-GPEHLZMM.DFDVpfXm.js → blockDiagram-GPEHLZMM.DE6GpXCN.js} +1 -1
  107. package/web/_astro/{c4Diagram-AAUBKEIU.V7_3_glh.js → c4Diagram-AAUBKEIU.DoVR1G6B.js} +1 -1
  108. package/web/_astro/channel.DmdDxGsq.js +1 -0
  109. package/web/_astro/{chunk-2J33WTMH.De1yx7Jz.js → chunk-2J33WTMH.Do6-Gbyf.js} +1 -1
  110. package/web/_astro/{chunk-4BX2VUAB.BDhHFuxx.js → chunk-4BX2VUAB.BYV1HGHx.js} +1 -1
  111. package/web/_astro/{chunk-55IACEB6.DDLkWnmC.js → chunk-55IACEB6.CvI1zkrc.js} +1 -1
  112. package/web/_astro/{chunk-727SXJPM.CwfiLnA7.js → chunk-727SXJPM.DNGSCRlX.js} +1 -1
  113. package/web/_astro/{chunk-AQP2D5EJ.CtGDmUnM.js → chunk-AQP2D5EJ.DXhhgT5z.js} +1 -1
  114. package/web/_astro/{chunk-FMBD7UC4.BOLjpw73.js → chunk-FMBD7UC4.dzkRKP2u.js} +1 -1
  115. package/web/_astro/{chunk-ND2GUHAM.BDhQls6O.js → chunk-ND2GUHAM.DujGLeEf.js} +1 -1
  116. package/web/_astro/{chunk-QZHKN3VN.76qBynDF.js → chunk-QZHKN3VN.BWtIT3ZU.js} +1 -1
  117. package/web/_astro/{classDiagram-4FO5ZUOK.N0mpPHT0.js → classDiagram-4FO5ZUOK.DyApg0Nn.js} +1 -1
  118. package/web/_astro/{classDiagram-v2-Q7XG4LA2.N0mpPHT0.js → classDiagram-v2-Q7XG4LA2.DyApg0Nn.js} +1 -1
  119. package/web/_astro/{cose-bilkent-S5V4N54A.CKknFfjB.js → cose-bilkent-S5V4N54A.DJQB-u_l.js} +1 -1
  120. package/web/_astro/{cynefin-OW5HDTMX.Cw3ZWBAh.js → cynefin-OW5HDTMX.bi3D_siV.js} +1 -1
  121. package/web/_astro/{dagre-BM42HDAG.D3yIKCRD.js → dagre-BM42HDAG.C-f7TrRg.js} +1 -1
  122. package/web/_astro/{diagram-2AECGRRQ.D51dO83Z.js → diagram-2AECGRRQ.DrPfjHQB.js} +1 -1
  123. package/web/_astro/{diagram-5GNKFQAL.Bb-FViDj.js → diagram-5GNKFQAL.C9S4WUHO.js} +1 -1
  124. package/web/_astro/{diagram-KO2AKTUF.D3mN8S6x.js → diagram-KO2AKTUF.Dzzye8QH.js} +1 -1
  125. package/web/_astro/{diagram-LMA3HP47.dMMyPcbJ.js → diagram-LMA3HP47.DX-LRiof.js} +1 -1
  126. package/web/_astro/{diagram-OG6HWLK6.AzbvzY4C.js → diagram-OG6HWLK6.Z-kAKwIz.js} +1 -1
  127. package/web/_astro/{erDiagram-TEJ5UH35.BR5I4OZb.js → erDiagram-TEJ5UH35.D6dyClSv.js} +1 -1
  128. package/web/_astro/{flowDiagram-I6XJVG4X.DFqvhGkn.js → flowDiagram-I6XJVG4X.Cx3q2cxD.js} +1 -1
  129. package/web/_astro/{ganttDiagram-6RSMTGT7.ByJbhH6J.js → ganttDiagram-6RSMTGT7.DU3gH6CT.js} +1 -1
  130. package/web/_astro/{gitGraphDiagram-PVQCEYII.CInHjQR3.js → gitGraphDiagram-PVQCEYII.ISmCnJ87.js} +1 -1
  131. package/web/_astro/index.Do2lLdgo.css +1 -0
  132. package/web/_astro/{infoDiagram-5YYISTIA.Cm0dGVjJ.js → infoDiagram-5YYISTIA.DR6_iNOK.js} +1 -1
  133. package/web/_astro/{ishikawaDiagram-YF4QCWOH.C3nOP6Sj.js → ishikawaDiagram-YF4QCWOH.D7wY-ZGA.js} +1 -1
  134. package/web/_astro/{journeyDiagram-JHISSGLW.4IvLa2MW.js → journeyDiagram-JHISSGLW.DkP9-WK8.js} +1 -1
  135. package/web/_astro/{kanban-definition-UN3LZRKU.DS87Cl-k.js → kanban-definition-UN3LZRKU.wtgY98M2.js} +1 -1
  136. package/web/_astro/{linear.MzmRrNER.js → linear.CI6djNi7.js} +1 -1
  137. package/web/_astro/{mermaid.core.fK-WNswS.js → mermaid.core.CuQPxS8b.js} +4 -4
  138. package/web/_astro/{mindmap-definition-RKZ34NQL.BqipOHhY.js → mindmap-definition-RKZ34NQL.5HNIEG7R.js} +1 -1
  139. package/web/_astro/{pieDiagram-4H26LBE5.0nBiPqFG.js → pieDiagram-4H26LBE5.CchhDFjE.js} +1 -1
  140. package/web/_astro/{quadrantDiagram-W4KKPZXB.Szj7Zpx5.js → quadrantDiagram-W4KKPZXB.PYXXJPSJ.js} +1 -1
  141. package/web/_astro/{requirementDiagram-4Y6WPE33.D4UR6TWo.js → requirementDiagram-4Y6WPE33.cUQuNAKU.js} +1 -1
  142. package/web/_astro/{sankeyDiagram-5OEKKPKP.DAE7BdOs.js → sankeyDiagram-5OEKKPKP.BO7MnRIA.js} +1 -1
  143. package/web/_astro/{sequenceDiagram-3UESZ5HK.BP9ZVq25.js → sequenceDiagram-3UESZ5HK.D9bElyD6.js} +1 -1
  144. package/web/_astro/{stateDiagram-AJRCARHV.BQW7mIxk.js → stateDiagram-AJRCARHV.DhGdRz7I.js} +1 -1
  145. package/web/_astro/{stateDiagram-v2-BHNVJYJU.PbK4nDih.js → stateDiagram-v2-BHNVJYJU.5GvLeAoL.js} +1 -1
  146. package/web/_astro/{timeline-definition-PNZ67QCA.Ci_GvUpo.js → timeline-definition-PNZ67QCA.LtAT9haM.js} +1 -1
  147. package/web/_astro/{vennDiagram-CIIHVFJN.DNw1Y5qx.js → vennDiagram-CIIHVFJN.DRth1yLA.js} +1 -1
  148. package/web/_astro/{wardleyDiagram-YWT4CUSO.CwlkaaNe.js → wardleyDiagram-YWT4CUSO.h9MlZfcr.js} +1 -1
  149. package/web/_astro/{xychartDiagram-2RQKCTM6.MI6ZuiH_.js → xychartDiagram-2RQKCTM6.DHqy9DYA.js} +1 -1
  150. package/web/index.html +2 -2
  151. package/config/proportional-route-table.ts +0 -155
  152. package/plugins/sp/commands/dev-parallel.md +0 -37
  153. package/plugins/sp/scripts/daily-summary/logger.ts +0 -28
  154. package/web/_astro/BoardApp.D6sjAJr_.js +0 -324
  155. package/web/_astro/TaskDetail.BynLnMNc.js +0 -1
  156. package/web/_astro/channel.DZVfto-r.js +0 -1
  157. package/web/_astro/index.COh_kBxN.css +0 -1
  158. /package/plugins/sp/{hooks → lib}/destructive-policy.ts +0 -0
  159. /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,18 +77,40 @@ 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) {
101
+ if (row.type === "custom_message") {
102
+ if (row.display === false)
103
+ return;
104
+ const text = typeof row.content === "string" ? row.content.trim() : "";
105
+ return text === "" ? undefined : text;
106
+ }
63
107
  if (row.type === "message") {
64
108
  if (row.message?.role !== "user")
65
109
  return;
66
- if (isInjectedPrompt(row))
67
- return;
68
- const content2 = row.message.content;
69
- if (typeof content2 === "string")
70
- return content2;
71
- if (!Array.isArray(content2))
110
+ const text = firstTextContent(row);
111
+ if (text === undefined)
72
112
  return;
73
- return content2.find((b) => b.type === "text")?.text;
113
+ return skillInvocationPrompt(text) ?? text;
74
114
  }
75
115
  if (row.type !== "user" || row.isMeta || row.isCompactSummary)
76
116
  return;
@@ -81,13 +121,6 @@ function promptText(row) {
81
121
  return;
82
122
  return content.find((b) => b.type === "text")?.text;
83
123
  }
84
- function isInjectedPrompt(row) {
85
- if (row.type !== "message" || row.message?.role !== "user")
86
- return false;
87
- const content = row.message.content;
88
- const text = typeof content === "string" ? content : Array.isArray(content) ? content.find((b) => b.type === "text")?.text : undefined;
89
- return typeof text === "string" && text.replace(/^\s+/, "").startsWith('<skill name="');
90
- }
91
124
  function sumTokens(all) {
92
125
  const total = zeroTokens();
93
126
  for (const t of all)
@@ -159,72 +192,14 @@ function accumulate(acc, row, ts) {
159
192
  });
160
193
  }
161
194
  }
162
- function isCompaction(row) {
163
- return row.type === "compaction" || row.type === "system" && row.subtype === "compact_boundary";
164
- }
165
- function sniffFormat(rows) {
166
- for (const row of rows) {
167
- if (row.type === "message" && PI_ROLES.has(row.message?.role ?? ""))
168
- return "pi";
169
- if (row.type === "user" || row.type === "assistant")
170
- return "claude";
171
- }
172
- return "unknown";
173
- }
174
- function parseRows(lines) {
175
- const rows = [];
176
- let skippedLines = 0;
177
- for (const line of lines) {
178
- if (!line.trim())
179
- continue;
180
- let row;
181
- try {
182
- row = JSON.parse(line);
183
- } catch {
184
- skippedLines++;
185
- continue;
186
- }
187
- const ts = row.timestamp ? Date.parse(row.timestamp) : Number.NaN;
188
- if (!Number.isNaN(ts))
189
- rows.push([row, ts]);
190
- }
191
- return { rows, skippedLines, format: sniffFormat(rows.map(([r]) => r)) };
192
- }
193
- function resolveTranscript(env, projectsRoot = join(homedir(), ".claude", "projects"), override) {
194
- if (override)
195
- return existsSync(override) ? { ok: true, path: override } : { ok: false, reason: "no transcript" };
196
- const id = env.CLAUDE_CODE_SESSION_ID;
197
- if (!id) {
198
- const piFile = env.PI_SESSION_FILE;
199
- if (piFile !== undefined && piFile !== "") {
200
- return existsSync(piFile) ? { ok: true, path: piFile } : { ok: false, reason: `PI_SESSION_FILE ${piFile} does not exist` };
201
- }
202
- return {
203
- ok: false,
204
- reason: "no host session id; pass --transcript <path> (pi: ~/.pi/agent/sessions/<cwd-slug>/<file>.jsonl, " + "or set PI_SESSION_FILE)"
205
- };
206
- }
207
- if (!SESSION_ID.test(id))
208
- return { ok: false, reason: "refusing a session id with path characters" };
209
- if (!existsSync(projectsRoot))
210
- return { ok: false, reason: `no transcript root ${projectsRoot}` };
211
- for (const dir of readdirSync(projectsRoot)) {
212
- const path = join(projectsRoot, dir, `${id}.jsonl`);
213
- if (existsSync(path))
214
- return { ok: true, path };
215
- }
216
- return { ok: false, reason: `no transcript for session ${id}` };
217
- }
218
-
219
- // plugins/sp/lib/run-summary-core.ts
220
195
  var IDLE_GAP_MS = 300000;
221
196
  var SHELL_TOOLS = new Set(["bash", "shell", "exec", "exec_command", "run_command", "command"]);
222
197
  var SUBAGENT_TOOLS = new Set(["agent", "task", "invoke_subagent", "subagent", "dispatch_agent"]);
223
- var OPERATOR_TOOLS2 = new Set(["askuserquestion", "ask_user_question"]);
198
+ var LOWERCASE_OPERATOR_TOOLS = new Set(["askuserquestion", "ask_user_question"]);
224
199
  var ACTOR_PRECEDENCE = ["operator", "subagent", "shell", "other"];
225
200
  function actorOf(tool) {
226
201
  const t = tool.toLowerCase().replace(/[^a-z_]/g, "");
227
- if (OPERATOR_TOOLS2.has(t))
202
+ if (LOWERCASE_OPERATOR_TOOLS.has(t))
228
203
  return "operator";
229
204
  if (SUBAGENT_TOOLS.has(t))
230
205
  return "subagent";
@@ -374,7 +349,8 @@ function summaryRow(stage, status, span, measured) {
374
349
  token: formatTokenSplit(measured ? span.tokens : null)
375
350
  };
376
351
  }
377
- function buildRunSummary(lines, inputs, since, until) {
352
+ function buildRunSummary(rows, inputs, opts = {}) {
353
+ const { since, until } = opts;
378
354
  const windows = [];
379
355
  for (const { label, progress } of inputs) {
380
356
  const states = (progress.states ?? []).flatMap((s) => {
@@ -400,7 +376,6 @@ function buildRunSummary(lines, inputs, since, until) {
400
376
  }
401
377
  }
402
378
  windows.sort((a, b) => a.start - b.start);
403
- const { rows, skippedLines } = parseRows(lines);
404
379
  const measure = (start, end) => {
405
380
  const acc = newAcc(start);
406
381
  for (const [row, ts] of rows)
@@ -448,9 +423,87 @@ function buildRunSummary(lines, inputs, since, until) {
448
423
  total,
449
424
  actors: actorSplit(rows, totalStart, totalEnd),
450
425
  gate: { ms: null, reason: "no matching check receipt" },
451
- skippedLines
426
+ skippedLines: opts.skippedLines ?? 0
452
427
  };
453
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
454
507
  function renderSummaryMarkdown(summary) {
455
508
  const line = (r, bold = false) => {
456
509
  const stage = bold ? `**${r.stage}**` : r.stage;
@@ -482,9 +535,9 @@ function renderSummaryMarkdown(summary) {
482
535
  }
483
536
  function writeCloseSummary(run, opts = {}) {
484
537
  const cwd = opts.cwd ?? process.cwd();
485
- const runDir = join2(cwd, ".spur", "run");
486
- const rel = join2(".spur", "run", `${run.runId}-summary.md`);
487
- 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);
488
541
  const na = (reason) => {
489
542
  mkdirSync(runDir, { recursive: true });
490
543
  writeFileSync(abs, `Execution summary: n/a (${reason})
@@ -497,15 +550,19 @@ function writeCloseSummary(run, opts = {}) {
497
550
  const resolved = resolveTranscript(opts.env ?? getEnvVars(), opts.projectsRoot);
498
551
  if (!resolved.ok)
499
552
  return na(resolved.reason);
500
- const lines = readFileSync(resolved.path, "utf8").split(`
501
- `);
502
- 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
+ });
503
560
  if (summary.rows.length === 0)
504
561
  return na("no timed attempts");
505
562
  mkdirSync(runDir, { recursive: true });
506
563
  writeFileSync(abs, `${renderSummaryMarkdown(summary)}
507
564
  `);
508
- 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));
509
566
  return rel;
510
567
  } catch (error) {
511
568
  return na(error instanceof Error ? error.message : String(error));
@@ -541,29 +598,10 @@ function refuseUnsafeRunId(runId) {
541
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.");
542
599
  process.exit(1);
543
600
  }
544
- function resolveAppEntry(spurBin) {
545
- const fallback = fileURLToPath(new URL("../../../apps/cli/src/index.ts", import.meta.url));
546
- const candidates = spurBin !== "" ? [spurBin] : [fallback];
547
- for (const candidate of candidates) {
548
- const tokens = candidate.split(/\s+/).filter(Boolean);
549
- const mainModule = [...tokens].reverse().find((t) => t.endsWith(".ts"));
550
- if (mainModule === undefined || !existsSync3(mainModule))
551
- continue;
552
- const repoRoot = resolve(dirname(mainModule), "..", "..", "..");
553
- const appEntry = join3(repoRoot, "packages", "app", "src", "index.ts");
554
- if (existsSync3(appEntry))
555
- return { entry: appEntry, portable: false };
556
- }
557
- const entry = fileURLToPath(new URL("../lib/inline-run.generated.mjs", import.meta.url));
558
- if (!existsSync3(entry)) {
559
- throw new Error("inline application bundle is missing \u2014 rebuild/install the sp plugin before running inline");
560
- }
561
- return { entry, portable: true };
562
- }
563
601
  var SETUP_EXPORTS = ["readInstalledInventory", "runInlineRunSetup", "writeInlineRunOutcome"];
564
602
  async function main() {
565
603
  if (!process.versions.bun) {
566
- 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)], {
567
605
  stdio: "inherit"
568
606
  });
569
607
  if (child.error)
@@ -632,7 +670,7 @@ async function main() {
632
670
  refuseUnsafeRunId(runId);
633
671
  try {
634
672
  const { app: app2, portable: portable2 } = await loadInlineApp(spurBin, resolveAppEntry, ["runInlineRunDecide"]);
635
- 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"]);
636
674
  const loadOpts = portable2 ? { embeddedSchemas: lib.EMBEDDED_SPUR_SCHEMAS } : undefined;
637
675
  const [enabled, spurConfig] = await Promise.all([
638
676
  lib.resolveDecideDecisionMakerEnabled(process.cwd(), loadOpts),
@@ -688,7 +726,7 @@ async function main() {
688
726
  inventory = await app.readInstalledInventory({
689
727
  file,
690
728
  spurBin,
691
- 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))
692
730
  });
693
731
  } catch (error) {
694
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
 
@@ -480,6 +480,15 @@ ${requestMarker(pr.headRefOid)}`;
480
480
  writeStatus(args, "REQUESTED");
481
481
  emit(args, { ok: true, requested: true, pr: pr.number, url: pr.url, head: pr.headRefOid, requestedAt, body }, `Requested GitHub Codex review on PR #${pr.number} (${pr.url}) at HEAD ${pr.headRefOid.slice(0, 7)}.`);
482
482
  }
483
+ function revalidateWaitTarget(args, number) {
484
+ const fresh = viewPr();
485
+ if (!fresh || fresh.number !== number) {
486
+ writeStatus(args, "FAIL");
487
+ fail(args, `PR #${number} is no longer the branch's PR (view reports ${fresh ? `#${fresh.number}` : "none"}) \u2014 request a fresh review`, 2);
488
+ }
489
+ requireExpectedHead(args, fresh);
490
+ return fresh;
491
+ }
483
492
  function cmdWait(args) {
484
493
  const since = args.flags.get("--since") ?? new Date().toISOString();
485
494
  const timeoutSec = Number(args.flags.get("--timeout") ?? "600");
@@ -494,24 +503,29 @@ function cmdWait(args) {
494
503
  writeStatus(args, "FAIL");
495
504
  fail(args, "no PR for the current branch", 2);
496
505
  }
506
+ const number = pr.number;
497
507
  requireExpectedHead(args, pr);
498
508
  const deadline = Date.now() + timeoutSec * 1000;
499
509
  for (;; ) {
500
- const reviews = fetchReviews(ctx, pr.number);
501
- const findings = normalizeFindings(reviews, fetchInlineComments(ctx, pr.number), fetchIssueComments(ctx, pr.number), since, pr.headRefOid);
510
+ const observed = revalidateWaitTarget(args, number);
511
+ const reviews = fetchReviews(ctx, number);
512
+ const findings = normalizeFindings(reviews, fetchInlineComments(ctx, number), fetchIssueComments(ctx, number), since, observed.headRefOid);
502
513
  if (findings.length > 0) {
514
+ revalidateWaitTarget(args, number);
503
515
  writeStatus(args, "FOUND");
504
516
  emit(args, { ok: true, verdict: "FOUND", findings }, renderFindings(findings));
505
517
  return;
506
518
  }
507
- if (hasCurrentCleanReview(reviews, since, pr.headRefOid)) {
519
+ if (hasCurrentCleanReview(reviews, since, observed.headRefOid)) {
520
+ const fresh = revalidateWaitTarget(args, number);
508
521
  writeStatus(args, "CLEAN");
509
- emit(args, { ok: true, verdict: "CLEAN", pr: pr.number, url: pr.url, head: pr.headRefOid, findings: [] }, `Codex review completed cleanly for HEAD ${pr.headRefOid.slice(0, 7)}.`);
522
+ emit(args, { ok: true, verdict: "CLEAN", pr: fresh.number, url: fresh.url, head: fresh.headRefOid, findings: [] }, `Codex review completed cleanly for HEAD ${fresh.headRefOid.slice(0, 7)}.`);
510
523
  return;
511
524
  }
512
525
  if (Date.now() >= deadline) {
526
+ revalidateWaitTarget(args, number);
513
527
  writeStatus(args, "TIMEOUT");
514
- emit(args, { ok: true, verdict: "TIMEOUT", pr: pr.number, url: pr.url }, `No Codex review within ${timeoutSec}s \u2014 still pending. Collect later with /sp:dev-pr-review collect.
528
+ emit(args, { ok: true, verdict: "TIMEOUT", pr: number, url: pr.url }, `No Codex review within ${timeoutSec}s \u2014 still pending. Collect later with /sp:dev-pr-review collect.
515
529
  PR: ${pr.url}`);
516
530
  throw new ScriptExit("", 3, args.booleans.has("--json"));
517
531
  }
@@ -703,6 +703,26 @@ function cmdRequest(args: ParsedArgs): void {
703
703
  );
704
704
  }
705
705
 
706
+ /**
707
+ * 1161 R1: re-read the PR and re-assert the pinned identity + expected head. Called at the start of
708
+ * every poll and again immediately before any FOUND/CLEAN/TIMEOUT status or output, because a head
709
+ * captured once at wait start lets a later push return CLEAN for an old commit. A vanished PR, a
710
+ * different PR on the branch, or a moved head is FAIL/exit2 through the existing error channel.
711
+ */
712
+ function revalidateWaitTarget(args: ParsedArgs, number: number): GhPr {
713
+ const fresh = viewPr();
714
+ if (!fresh || fresh.number !== number) {
715
+ writeStatus(args, 'FAIL');
716
+ fail(
717
+ args,
718
+ `PR #${number} is no longer the branch's PR (view reports ${fresh ? `#${fresh.number}` : 'none'}) — request a fresh review`,
719
+ 2,
720
+ );
721
+ }
722
+ requireExpectedHead(args, fresh);
723
+ return fresh;
724
+ }
725
+
706
726
  function cmdWait(args: ParsedArgs): void {
707
727
  const since = args.flags.get('--since') ?? new Date().toISOString();
708
728
  const timeoutSec = Number(args.flags.get('--timeout') ?? '600');
@@ -715,36 +735,41 @@ function cmdWait(args: ParsedArgs): void {
715
735
  writeStatus(args, 'FAIL');
716
736
  fail(args, 'no PR for the current branch', 2);
717
737
  }
738
+ const number = pr.number; // 1161 R1: pinned — a switched PR number is a failure, not a retarget.
718
739
  requireExpectedHead(args, pr);
719
- const deadline = Date.now() + timeoutSec * 1000;
740
+ const deadline = Date.now() + timeoutSec * 1000; // fixed once; validation never resets it
720
741
  for (;;) {
721
- const reviews = fetchReviews(ctx, pr.number);
742
+ const observed = revalidateWaitTarget(args, number);
743
+ const reviews = fetchReviews(ctx, number);
722
744
  const findings = normalizeFindings(
723
745
  reviews,
724
- fetchInlineComments(ctx, pr.number),
725
- fetchIssueComments(ctx, pr.number),
746
+ fetchInlineComments(ctx, number),
747
+ fetchIssueComments(ctx, number),
726
748
  since,
727
- pr.headRefOid,
749
+ observed.headRefOid,
728
750
  );
729
751
  if (findings.length > 0) {
752
+ revalidateWaitTarget(args, number); // pre-return recheck: never FOUND for a moved head
730
753
  writeStatus(args, 'FOUND');
731
754
  emit(args, { ok: true, verdict: 'FOUND', findings }, renderFindings(findings));
732
755
  return;
733
756
  }
734
- if (hasCurrentCleanReview(reviews, since, pr.headRefOid)) {
757
+ if (hasCurrentCleanReview(reviews, since, observed.headRefOid)) {
758
+ const fresh = revalidateWaitTarget(args, number); // pre-return recheck: never CLEAN for a moved head
735
759
  writeStatus(args, 'CLEAN');
736
760
  emit(
737
761
  args,
738
- { ok: true, verdict: 'CLEAN', pr: pr.number, url: pr.url, head: pr.headRefOid, findings: [] },
739
- `Codex review completed cleanly for HEAD ${pr.headRefOid.slice(0, 7)}.`,
762
+ { ok: true, verdict: 'CLEAN', pr: fresh.number, url: fresh.url, head: fresh.headRefOid, findings: [] },
763
+ `Codex review completed cleanly for HEAD ${fresh.headRefOid.slice(0, 7)}.`,
740
764
  );
741
765
  return;
742
766
  }
743
767
  if (Date.now() >= deadline) {
768
+ revalidateWaitTarget(args, number); // a head moved while pending is a failure, not a timeout
744
769
  writeStatus(args, 'TIMEOUT');
745
770
  emit(
746
771
  args,
747
- { ok: true, verdict: 'TIMEOUT', pr: pr.number, url: pr.url },
772
+ { ok: true, verdict: 'TIMEOUT', pr: number, url: pr.url },
748
773
  `No Codex review within ${timeoutSec}s — still pending. Collect later with /sp:dev-pr-review collect.\nPR: ${pr.url}`,
749
774
  );
750
775
  throw new ScriptExit('', 3, args.booleans.has('--json'));