@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
@@ -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,18 +42,40 @@ 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) {
66
+ if (row.type === "custom_message") {
67
+ if (row.display === false)
68
+ return;
69
+ const text = typeof row.content === "string" ? row.content.trim() : "";
70
+ return text === "" ? undefined : text;
71
+ }
43
72
  if (row.type === "message") {
44
73
  if (row.message?.role !== "user")
45
74
  return;
46
- if (isInjectedPrompt(row))
75
+ const text = firstTextContent(row);
76
+ if (text === undefined)
47
77
  return;
48
- const content2 = row.message.content;
49
- if (typeof content2 === "string")
50
- return content2;
51
- if (!Array.isArray(content2))
52
- return;
53
- return content2.find((b) => b.type === "text")?.text;
78
+ return skillInvocationPrompt(text) ?? text;
54
79
  }
55
80
  if (row.type !== "user" || row.isMeta || row.isCompactSummary)
56
81
  return;
@@ -61,13 +86,6 @@ function promptText(row) {
61
86
  return;
62
87
  return content.find((b) => b.type === "text")?.text;
63
88
  }
64
- function isInjectedPrompt(row) {
65
- if (row.type !== "message" || row.message?.role !== "user")
66
- return false;
67
- const content = row.message.content;
68
- const text = typeof content === "string" ? content : Array.isArray(content) ? content.find((b) => b.type === "text")?.text : undefined;
69
- return typeof text === "string" && text.replace(/^\s+/, "").startsWith('<skill name="');
70
- }
71
89
  function sumTokens(all) {
72
90
  const total = zeroTokens();
73
91
  for (const t of all)
@@ -139,77 +157,14 @@ function accumulate(acc, row, ts) {
139
157
  });
140
158
  }
141
159
  }
142
- function isCompaction(row) {
143
- return row.type === "compaction" || row.type === "system" && row.subtype === "compact_boundary";
144
- }
145
- function sniffFormat(rows) {
146
- for (const row of rows) {
147
- if (row.type === "message" && PI_ROLES.has(row.message?.role ?? ""))
148
- return "pi";
149
- if (row.type === "user" || row.type === "assistant")
150
- return "claude";
151
- }
152
- return "unknown";
153
- }
154
- function parseRows(lines) {
155
- const rows = [];
156
- let skippedLines = 0;
157
- for (const line of lines) {
158
- if (!line.trim())
159
- continue;
160
- let row;
161
- try {
162
- row = JSON.parse(line);
163
- } catch {
164
- skippedLines++;
165
- continue;
166
- }
167
- const ts = row.timestamp ? Date.parse(row.timestamp) : Number.NaN;
168
- if (!Number.isNaN(ts))
169
- rows.push([row, ts]);
170
- }
171
- return { rows, skippedLines, format: sniffFormat(rows.map(([r]) => r)) };
172
- }
173
- function resolveTranscript(env, projectsRoot = join(homedir(), ".claude", "projects"), override) {
174
- if (override)
175
- return existsSync(override) ? { ok: true, path: override } : { ok: false, reason: "no transcript" };
176
- const id = env.CLAUDE_CODE_SESSION_ID;
177
- if (!id) {
178
- const piFile = env.PI_SESSION_FILE;
179
- if (piFile !== undefined && piFile !== "") {
180
- return existsSync(piFile) ? { ok: true, path: piFile } : { ok: false, reason: `PI_SESSION_FILE ${piFile} does not exist` };
181
- }
182
- return {
183
- ok: false,
184
- reason: "no host session id; pass --transcript <path> (pi: ~/.pi/agent/sessions/<cwd-slug>/<file>.jsonl, " + "or set PI_SESSION_FILE)"
185
- };
186
- }
187
- if (!SESSION_ID.test(id))
188
- return { ok: false, reason: "refusing a session id with path characters" };
189
- if (!existsSync(projectsRoot))
190
- return { ok: false, reason: `no transcript root ${projectsRoot}` };
191
- for (const dir of readdirSync(projectsRoot)) {
192
- const path = join(projectsRoot, dir, `${id}.jsonl`);
193
- if (existsSync(path))
194
- return { ok: true, path };
195
- }
196
- return { ok: false, reason: `no transcript for session ${id}` };
197
- }
198
-
199
- // plugins/sp/lib/env.ts
200
- function getEnvVars2() {
201
- return process.env;
202
- }
203
-
204
- // plugins/sp/lib/run-summary-core.ts
205
160
  var IDLE_GAP_MS = 300000;
206
161
  var SHELL_TOOLS = new Set(["bash", "shell", "exec", "exec_command", "run_command", "command"]);
207
162
  var SUBAGENT_TOOLS = new Set(["agent", "task", "invoke_subagent", "subagent", "dispatch_agent"]);
208
- var OPERATOR_TOOLS2 = new Set(["askuserquestion", "ask_user_question"]);
163
+ var LOWERCASE_OPERATOR_TOOLS = new Set(["askuserquestion", "ask_user_question"]);
209
164
  var ACTOR_PRECEDENCE = ["operator", "subagent", "shell", "other"];
210
165
  function actorOf(tool) {
211
166
  const t = tool.toLowerCase().replace(/[^a-z_]/g, "");
212
- if (OPERATOR_TOOLS2.has(t))
167
+ if (LOWERCASE_OPERATOR_TOOLS.has(t))
213
168
  return "operator";
214
169
  if (SUBAGENT_TOOLS.has(t))
215
170
  return "subagent";
@@ -359,7 +314,8 @@ function summaryRow(stage, status, span, measured) {
359
314
  token: formatTokenSplit(measured ? span.tokens : null)
360
315
  };
361
316
  }
362
- function buildRunSummary(lines, inputs, since, until) {
317
+ function buildRunSummary(rows, inputs, opts = {}) {
318
+ const { since, until } = opts;
363
319
  const windows = [];
364
320
  for (const { label, progress } of inputs) {
365
321
  const states = (progress.states ?? []).flatMap((s) => {
@@ -385,7 +341,6 @@ function buildRunSummary(lines, inputs, since, until) {
385
341
  }
386
342
  }
387
343
  windows.sort((a, b) => a.start - b.start);
388
- const { rows, skippedLines } = parseRows(lines);
389
344
  const measure = (start, end) => {
390
345
  const acc = newAcc(start);
391
346
  for (const [row, ts] of rows)
@@ -433,9 +388,87 @@ function buildRunSummary(lines, inputs, since, until) {
433
388
  total,
434
389
  actors: actorSplit(rows, totalStart, totalEnd),
435
390
  gate: { ms: null, reason: "no matching check receipt" },
436
- skippedLines
391
+ skippedLines: opts.skippedLines ?? 0
437
392
  };
438
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
439
472
  function renderSummaryMarkdown(summary) {
440
473
  const line = (r, bold = false) => {
441
474
  const stage = bold ? `**${r.stage}**` : r.stage;
@@ -536,9 +569,13 @@ function writeCloseSummary(run, opts = {}) {
536
569
  const resolved = resolveTranscript(opts.env ?? getEnvVars2(), opts.projectsRoot);
537
570
  if (!resolved.ok)
538
571
  return na(resolved.reason);
539
- const lines = readFileSync(resolved.path, "utf8").split(`
540
- `);
541
- 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
+ });
542
579
  if (summary.rows.length === 0)
543
580
  return na("no timed attempts");
544
581
  mkdirSync(runDir, { recursive: true });
@@ -572,8 +609,35 @@ import { existsSync as existsSync3, readdirSync as readdirSync2 } from "node:fs"
572
609
  import { homedir as homedir2 } from "node:os";
573
610
  import { join as join3 } from "node:path";
574
611
  var SESSION_ID2 = /^[A-Za-z0-9_-]+$/;
575
- var OPERATOR_TOOLS3 = new Set(["AskUserQuestion", "ask_user_question"]);
576
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
+ }
577
641
  function resolveTranscript2(env, projectsRoot = join3(homedir2(), ".claude", "projects"), override) {
578
642
  if (override)
579
643
  return existsSync3(override) ? { ok: true, path: override } : { ok: false, reason: "no transcript" };
@@ -668,8 +732,9 @@ function main(argv, env = getEnvVars(), write = (s) => process.stdout.write(s),
668
732
  const [label, file] = eq > 0 ? [arg.slice(0, eq), arg.slice(eq + 1)] : [undefined, arg];
669
733
  return { label, progress: JSON.parse(readFileSync2(file, "utf8")) };
670
734
  });
671
- const summary = buildRunSummary(readFileSync2(resolved.path, "utf8").split(`
672
- `), inputs, since);
735
+ const { rows, skippedLines } = parseRows2(readFileSync2(resolved.path, "utf8").split(`
736
+ `));
737
+ const summary = buildRunSummary(rows, inputs, { since, skippedLines });
673
738
  summary.gate = gateFromReceipt(process.cwd(), inputs[0]?.progress.runId ?? "");
674
739
  write(`${markdown ? renderSummaryMarkdown(summary) : JSON.stringify({ ...summary, transcript: resolved.path })}
675
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,18 +38,37 @@ 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) {
59
+ if (row.type === "custom_message") {
60
+ if (row.display === false)
61
+ return;
62
+ const text = typeof row.content === "string" ? row.content.trim() : "";
63
+ return text === "" ? undefined : text;
64
+ }
39
65
  if (row.type === "message") {
40
66
  if (row.message?.role !== "user")
41
67
  return;
42
- if (isInjectedPrompt(row))
68
+ const text = firstTextContent(row);
69
+ if (text === undefined)
43
70
  return;
44
- const content2 = row.message.content;
45
- if (typeof content2 === "string")
46
- return content2;
47
- if (!Array.isArray(content2))
48
- return;
49
- return content2.find((b) => b.type === "text")?.text;
71
+ return skillInvocationPrompt(text) ?? text;
50
72
  }
51
73
  if (row.type !== "user" || row.isMeta || row.isCompactSummary)
52
74
  return;
@@ -57,13 +79,6 @@ function promptText(row) {
57
79
  return;
58
80
  return content.find((b) => b.type === "text")?.text;
59
81
  }
60
- function isInjectedPrompt(row) {
61
- if (row.type !== "message" || row.message?.role !== "user")
62
- return false;
63
- const content = row.message.content;
64
- const text = typeof content === "string" ? content : Array.isArray(content) ? content.find((b) => b.type === "text")?.text : undefined;
65
- return typeof text === "string" && text.replace(/^\s+/, "").startsWith('<skill name="');
66
- }
67
82
  function sumTokens(all) {
68
83
  const total = zeroTokens();
69
84
  for (const t of all)
@@ -135,6 +150,29 @@ function accumulate(acc, row, ts) {
135
150
  });
136
151
  }
137
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"]);
138
176
  function sniffFormat(rows) {
139
177
  for (const row of rows) {
140
178
  if (row.type === "message" && PI_ROLES.has(row.message?.role ?? ""))
@@ -212,15 +250,7 @@ function buildTimeline(lines, group) {
212
250
  const open = [];
213
251
  const { rows, skippedLines, format } = parseRows(lines);
214
252
  const isPi = format === "pi";
215
- let injectedPrompts = 0;
216
253
  for (const [row, ts] of rows) {
217
- if (isPi && isInjectedPrompt(row)) {
218
- injectedPrompts++;
219
- const injectedSeg = open.at(-1);
220
- if (injectedSeg)
221
- accumulate(injectedSeg, row, ts);
222
- continue;
223
- }
224
254
  const prompt = promptText(row);
225
255
  if (prompt !== undefined) {
226
256
  open.push({ start: ts, prompt, ...newAcc(ts) });
@@ -253,8 +283,7 @@ function buildTimeline(lines, group) {
253
283
  available: true,
254
284
  segments,
255
285
  totals: render(isPi ? { ...totals, compactions: segments.reduce((n, s) => n + (s.compactions ?? 0), 0) } : totals),
256
- skippedLines,
257
- ...isPi ? { injectedPrompts } : {}
286
+ skippedLines
258
287
  };
259
288
  if (group) {
260
289
  timeline.stages = parseGroups(group, segments.length).map(([a, b]) => render({ ...sumSpans(segments.slice(a - 1, b)), segments: a === b ? `${a}` : `${a}-${b}` }));
@@ -273,8 +302,7 @@ function zeroSegmentReason(rows) {
273
302
  const census = [...counts.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, 5).map(([kind, n]) => `${kind} ${n}`).join(", ");
274
303
  return `unrecognized transcript format (row types: ${census})`;
275
304
  }
276
- const injected = rows.filter(([r]) => isInjectedPrompt(r)).length;
277
- return `${format} transcript with no operator prompts (${rows.length} rows, ${injected} injected)`;
305
+ return `${format} transcript with no operator prompts (${rows.length} rows)`;
278
306
  }
279
307
  function main(argv, env = getEnvVars(), write = (s) => process.stdout.write(s), projectsRoot, writeErr = (s) => process.stderr.write(s)) {
280
308
  let transcript;
@@ -16,7 +16,6 @@ import {
16
16
  accumulate,
17
17
  formatDuration,
18
18
  formatTokenSplit,
19
- isInjectedPrompt,
20
19
  newAcc,
21
20
  parseRows,
22
21
  promptText,
@@ -40,8 +39,6 @@ export interface Timeline {
40
39
  stages?: Rendered<Span & { segments: string }>[];
41
40
  totals: Rendered<Span & { elapsedMs: number; elapsed: string; compactions?: number }>;
42
41
  skippedLines: number;
43
- /** pi only (1138 R2): skill-injected user bodies counted as activity, never as segments. */
44
- injectedPrompts?: number;
45
42
  }
46
43
 
47
44
  const PROMPT_EXCERPT = 80;
@@ -75,17 +72,8 @@ export function buildTimeline(lines: string[], group?: string): Timeline {
75
72
  const open: Open[] = [];
76
73
  const { rows, skippedLines, format } = parseRows(lines);
77
74
  const isPi = format === 'pi';
78
- let injectedPrompts = 0;
79
75
 
80
76
  for (const [row, ts] of rows) {
81
- // 1138 R2: an injected skill body is activity, not a prompt — accumulate it into the open
82
- // segment (its tools and usage are real work) and count it, never open a segment.
83
- if (isPi && isInjectedPrompt(row)) {
84
- injectedPrompts++;
85
- const injectedSeg = open.at(-1);
86
- if (injectedSeg) accumulate(injectedSeg, row, ts);
87
- continue;
88
- }
89
77
  const prompt = promptText(row);
90
78
  if (prompt !== undefined) {
91
79
  open.push({ start: ts, prompt, ...newAcc(ts) });
@@ -124,8 +112,6 @@ export function buildTimeline(lines: string[], group?: string): Timeline {
124
112
  isPi ? { ...totals, compactions: segments.reduce((n, s) => n + (s.compactions ?? 0), 0) } : totals,
125
113
  ),
126
114
  skippedLines,
127
- // Emitted only for pi, as `compactions` is, so Claude output is unchanged (1138 R2).
128
- ...(isPi ? { injectedPrompts } : {}),
129
115
  };
130
116
  if (group) {
131
117
  timeline.stages = parseGroups(group, segments.length).map(([a, b]) =>
@@ -139,8 +125,9 @@ export const SESSION_TIMELINE_USAGE = 'usage: session-timeline [--transcript <pa
139
125
 
140
126
  /**
141
127
  * 1138 R3: a zero-segment result must say WHAT was detected, so an unparseable-but-recognised
142
- * transcript can never read as an authoritative all-zero timeline. A known format names the
143
- * row count and the injected count; an unknown one lists the top-5 row-type census.
128
+ * transcript can never read as an authoritative all-zero timeline. A known format names the row
129
+ * count; an unknown one lists the top-5 row-type census. (1151 R2 removed the pi-only injected
130
+ * count: pi injects no skill bodies, so a wrapper row is an operator prompt and the class is gone.)
144
131
  */
145
132
  export function zeroSegmentReason(rows: [Row, number][]): string {
146
133
  const format = sniffFormat(rows.map(([r]) => r));
@@ -157,8 +144,7 @@ export function zeroSegmentReason(rows: [Row, number][]): string {
157
144
  .join(', ');
158
145
  return `unrecognized transcript format (row types: ${census})`;
159
146
  }
160
- const injected = rows.filter(([r]) => isInjectedPrompt(r)).length;
161
- return `${format} transcript with no operator prompts (${rows.length} rows, ${injected} injected)`;
147
+ return `${format} transcript with no operator prompts (${rows.length} rows)`;
162
148
  }
163
149
 
164
150
  export function main(