@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
@@ -1,377 +1,43 @@
1
1
  /**
2
- * run-summary core — the measurement and rendering behind the `run-summary` CLI (task 1146).
2
+ * run-summary core — the rendering and host IO behind the `run-summary` CLI (task 1146; task 1178 R2).
3
3
  *
4
4
  * Split out of `plugins/sp/scripts/run-summary.ts` when the actor split and the batch roll-up pushed
5
- * that script past the ADR-130 glue budget: the script keeps argv parsing and dispatch, this module
6
- * keeps the logic. `plugins/sp/lib/transcript.ts` is the same shape of shared plugin-side module.
7
- * Node builtins plus relative imports only; `import type` for the app projection it is handed.
5
+ * that script past the ADR-130 glue budget. The PURE aggregation and its types now live in
6
+ * `packages/app/src/workflow/run-summary.ts`, reached through the generated pure plan-projection
7
+ * entry; this module keeps only what belongs to the host: the close-step callback, the receipt read,
8
+ * Markdown rendering and durable/scratch report writing. Node builtins plus relative imports only;
9
+ * `import type` for the app projection it is handed.
8
10
  */
9
11
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
10
12
  import { join } from 'node:path';
11
13
  import type { WorkflowProgressProjection } from '@gobing-ai/spur-app';
14
+ import { getEnvVars } from './env';
12
15
  import {
13
- accumulate,
14
- type Block,
16
+ type ActorSplit,
17
+ buildRunSummary,
15
18
  formatDuration,
16
19
  formatTokenSplit,
17
- isCompaction,
18
- newAcc,
19
- parseRows,
20
- promptText,
21
- resolveTranscript,
22
- type Row,
23
- type Span,
24
- sumSpans,
20
+ type GateTime,
21
+ type Progress,
22
+ type RunSummary,
23
+ type SummaryRow,
25
24
  sumTokens,
26
- type Tokens,
27
25
  zeroTokens,
28
- } from './transcript';
29
- import { getEnvVars } from './env';
30
-
31
- interface ProgressAttempt {
32
- startedAt?: string;
33
- completedAt?: string;
34
- }
35
-
36
- /**
37
- * The subset of the progress projection the summary windows over. The close path hands over the
38
- * full `WorkflowProgressProjection`; the extra fields are ignored here, so the reader stays a
39
- * narrow structural view of it rather than a second copy of the contract.
40
- */
41
- export interface Progress {
42
- runId: string;
43
- workflow?: string;
44
- status?: string;
45
- states?: { state: string; visit?: number; status?: string; actions?: { attempts?: ProgressAttempt[] }[] }[];
46
- }
47
-
48
- /** A stage row; `toolCalls`/`tokens` are null when the window holds no host transcript record. */
49
- export interface SummaryRow {
50
- stage: string;
51
- status: string;
52
- workMs: number;
53
- waitMs: number;
54
- toolCalls: number | null;
55
- tokens: Tokens | null;
56
- time: string;
57
- wait: string;
58
- token: string;
59
- }
60
-
61
- /** 1146 R4: how the total window is spent, by actor. Rows sum exactly to the window. */
62
- export interface ActorSplit {
63
- operatorMs: number;
64
- /** The longest single operator wait, so a total is not read as one stall. */
65
- longestOperatorWaitMs: number;
66
- modelMs: number;
67
- shellMs: number;
68
- subagentMs: number;
69
- otherToolMs: number;
70
- idleMs: number;
71
- compactions: number;
72
- subagentErrors: number;
73
- }
74
-
75
- /** A gate-receipt measurement, or the reason there is none (1146 R4: never a fabricated 0). */
76
- export type GateTime = { ms: number } | { ms: null; reason: string };
77
-
78
- export interface RunSummary {
79
- available: true;
80
- transcript?: string;
81
- rows: SummaryRow[];
82
- total: SummaryRow;
83
- actors: ActorSplit;
84
- gate: GateTime;
85
- skippedLines: number;
86
- }
87
-
88
- /** A gap at or above this is idle, not work (1146 R4, closed decision: fixed, no flag). */
89
- const IDLE_GAP_MS = 300_000;
90
- const SHELL_TOOLS = new Set(['bash', 'shell', 'exec', 'exec_command', 'run_command', 'command']);
91
- const SUBAGENT_TOOLS = new Set(['agent', 'task', 'invoke_subagent', 'subagent', 'dispatch_agent']);
92
- const OPERATOR_TOOLS = new Set(['askuserquestion', 'ask_user_question']);
93
-
94
- type Actor = 'operator' | 'subagent' | 'shell' | 'other';
95
- /** Overlap is counted once, and subagent outranks shell outranks other tools (1146 AC4). */
96
- const ACTOR_PRECEDENCE: Actor[] = ['operator', 'subagent', 'shell', 'other'];
97
-
98
- function actorOf(tool: string): Actor {
99
- const t = tool.toLowerCase().replace(/[^a-z_]/g, '');
100
- if (OPERATOR_TOOLS.has(t)) return 'operator';
101
- if (SUBAGENT_TOOLS.has(t)) return 'subagent';
102
- if (SHELL_TOOLS.has(t)) return 'shell';
103
- return 'other';
104
- }
105
-
106
- interface Call {
107
- id: string;
108
- actor: Actor;
109
- ts: number;
110
- }
111
-
112
- /** One resolved tool span: [start, end] attributed to one actor. */
113
- interface Resolved extends Call {
114
- end: number;
115
- }
116
-
117
- /** Union length of a set of intervals, clipped to [start, end]. */
118
- function unionMs(spans: Array<[number, number]>, start: number, end: number): number {
119
- const clipped = spans
120
- .map(([a, b]) => [Math.max(a, start), Math.min(b, end)] as [number, number])
121
- .filter(([a, b]) => b > a)
122
- .sort((x, y) => x[0] - y[0]);
123
- let total = 0;
124
- let cursor = Number.NaN;
125
- let openUntil = Number.NaN;
126
- for (const [a, b] of clipped) {
127
- if (!Number.isFinite(cursor)) {
128
- cursor = a;
129
- openUntil = b;
130
- continue;
131
- }
132
- if (a > openUntil) {
133
- total += openUntil - cursor;
134
- cursor = a;
135
- openUntil = b;
136
- } else {
137
- openUntil = Math.max(openUntil, b);
138
- }
139
- }
140
- if (Number.isFinite(cursor)) total += openUntil - cursor;
141
- return total;
142
- }
143
-
144
- /** Subtract already-claimed intervals from a candidate set, returning the still-free part. */
145
- function subtract(spans: Array<[number, number]>, claimed: Array<[number, number]>): Array<[number, number]> {
146
- let free = spans;
147
- for (const [ca, cb] of claimed) {
148
- const next: Array<[number, number]> = [];
149
- for (const [a, b] of free) {
150
- if (cb <= a || ca >= b) {
151
- next.push([a, b]);
152
- continue;
153
- }
154
- if (ca > a) next.push([a, ca]);
155
- if (cb < b) next.push([cb, b]);
156
- }
157
- free = next;
158
- }
159
- return free;
160
- }
161
-
162
- /**
163
- * 1146 R4: partition the window [start, end] by actor. Tool calls are paired with their results
164
- * (either host shape); overlaps resolve by `ACTOR_PRECEDENCE`, counted once; unclaimed gaps at or
165
- * above IDLE_GAP_MS are idle; the remainder is model time, so the rows always sum to the window.
166
- */
167
- export function actorSplit(rows: [Row, number][], start: number, end: number): ActorSplit {
168
- const empty: ActorSplit = {
169
- operatorMs: 0,
170
- longestOperatorWaitMs: 0,
171
- modelMs: 0,
172
- shellMs: 0,
173
- subagentMs: 0,
174
- otherToolMs: 0,
175
- idleMs: 0,
176
- compactions: 0,
177
- subagentErrors: 0,
178
- };
179
- const window = end - start;
180
- if (window <= 0) return empty;
181
-
182
- const calls = new Map<string, Call>();
183
- const results = new Map<string, { ts: number; isError: boolean }>();
184
- const eventTs: number[] = [];
185
- let compactions = 0;
186
-
187
- for (const [row, ts] of rows) {
188
- if (ts < start || ts > end) continue;
189
- eventTs.push(ts);
190
- if (isCompaction(row)) compactions++;
191
- const blocks = Array.isArray(row.message?.content) ? (row.message?.content as Block[]) : [];
192
- if (row.message?.role === 'toolResult' && row.message.toolCallId !== undefined) {
193
- results.set(row.message.toolCallId, { ts, isError: row.message.isError === true });
194
- }
195
- for (const b of blocks) {
196
- if ((b.type === 'tool_use' || b.type === 'toolCall') && b.id !== undefined && b.name !== undefined) {
197
- calls.set(b.id, { id: b.id, actor: actorOf(b.name), ts });
198
- } else if (b.type === 'tool_result' && b.tool_use_id !== undefined) {
199
- results.set(b.tool_use_id, { ts, isError: b.is_error === true });
200
- }
201
- }
202
- }
203
-
204
- let subagentErrors = 0;
205
- const resolved: Resolved[] = [];
206
- for (const call of calls.values()) {
207
- const result = results.get(call.id);
208
- if (result?.isError === true && call.actor === 'subagent') subagentErrors++;
209
- resolved.push({ ...call, end: Math.min(result?.ts ?? end, end) });
210
- }
211
-
212
- const claimed: Array<[number, number]> = [];
213
- const byActor = new Map<Actor, number>();
214
- for (const actor of ACTOR_PRECEDENCE) {
215
- const mine = resolved.filter((r) => r.actor === actor).map((r) => [r.ts, r.end] as [number, number]);
216
- const free = subtract(mine, claimed);
217
- for (const span of free) claimed.push(span);
218
- const ms = unionMs(free, start, end);
219
- byActor.set(actor, ms);
220
- if (actor === 'operator') {
221
- empty.longestOperatorWaitMs = Math.max(0, ...mine.map(([a, b]) => b - a));
222
- }
223
- }
224
-
225
- // Idle: unclaimed gaps between consecutive event boundaries, at or above the threshold.
226
- const boundaries = [...new Set([start, end, ...eventTs, ...claimed.flat()])].sort((a, b) => a - b);
227
- const claimedSorted = [...claimed].sort((a, b) => a[0] - b[0]);
228
- let idleMs = 0;
229
- for (let i = 0; i < boundaries.length - 1; i++) {
230
- const a = boundaries[i] as number;
231
- const b = boundaries[i + 1] as number;
232
- if (b - a < IDLE_GAP_MS) continue;
233
- if (claimedSorted.some(([ca, cb]) => ca < b && cb > a)) continue;
234
- idleMs += b - a;
235
- }
236
-
237
- const accounted =
238
- (byActor.get('operator') ?? 0) +
239
- (byActor.get('subagent') ?? 0) +
240
- (byActor.get('shell') ?? 0) +
241
- (byActor.get('other') ?? 0) +
242
- idleMs;
243
- return {
244
- ...empty,
245
- operatorMs: byActor.get('operator') ?? 0,
246
- subagentMs: byActor.get('subagent') ?? 0,
247
- shellMs: byActor.get('shell') ?? 0,
248
- otherToolMs: byActor.get('other') ?? 0,
249
- idleMs,
250
- modelMs: Math.max(0, window - accounted),
251
- compactions,
252
- subagentErrors,
253
- };
254
- }
255
-
256
- interface Window {
257
- stage: string;
258
- status: string;
259
- start: number;
260
- end: number;
261
- }
262
-
263
- function attemptWindow(attempts: ProgressAttempt[]): [number, number] | undefined {
264
- const starts = attempts.map((a) => Date.parse(a.startedAt ?? '')).filter((n) => !Number.isNaN(n));
265
- const ends = attempts.map((a) => Date.parse(a.completedAt ?? '')).filter((n) => !Number.isNaN(n));
266
- return starts.length && ends.length ? [Math.min(...starts), Math.max(...ends)] : undefined;
267
- }
268
-
269
- function summaryRow(stage: string, status: string, span: Span, measured: boolean): SummaryRow {
270
- return {
271
- stage,
272
- status,
273
- workMs: span.workMs,
274
- waitMs: span.waitMs,
275
- toolCalls: measured ? span.toolCalls : null,
276
- tokens: measured ? span.tokens : null,
277
- time: formatDuration(span.workMs),
278
- wait: formatDuration(span.waitMs),
279
- token: formatTokenSplit(measured ? span.tokens : null),
280
- };
281
- }
282
-
283
- /**
284
- * dev-run / dev-runall execution summary. One progress file → one row per state visit; several →
285
- * one row per run. Each window spans its first attempt start → last completion (inclusive) and
286
- * slices the host transcript; work = window − AskUserQuestion answer time.
287
- */
288
- export function buildRunSummary(
289
- lines: string[],
290
- inputs: { label?: string; progress: Progress }[],
291
- since?: string,
292
- until?: string,
293
- ): RunSummary {
294
- const windows: Window[] = [];
295
- for (const { label, progress } of inputs) {
296
- const states = (progress.states ?? []).flatMap((s) => {
297
- const w = attemptWindow((s.actions ?? []).flatMap((a) => a.attempts ?? []));
298
- return w ? [{ s, w }] : [];
299
- });
300
- if (!states.length) throw new Error(`progress for run ${progress.runId} has no timed attempts`);
301
- if (inputs.length === 1) {
302
- for (const { s, w } of states) {
303
- const stage = (s.visit ?? 1) > 1 ? `${s.state} (visit ${s.visit})` : s.state;
304
- windows.push({ stage, status: s.status ?? '', start: w[0], end: w[1] });
305
- }
306
- } else {
307
- const stage = label ?? `${progress.workflow ?? 'run'} ${progress.runId.slice(0, 8)}`;
308
- const start = Math.min(...states.map(({ w }) => w[0]));
309
- windows.push({
310
- stage,
311
- status: progress.status ?? '',
312
- start,
313
- end: Math.max(...states.map(({ w }) => w[1])),
314
- });
315
- }
316
- }
317
- windows.sort((a, b) => a.start - b.start);
318
-
319
- const { rows, skippedLines } = parseRows(lines);
320
- const measure = (start: number, end: number): { span: Span; measured: boolean } => {
321
- const acc = newAcc(start);
322
- for (const [row, ts] of rows)
323
- if (ts >= start && ts <= end && promptText(row) === undefined) accumulate(acc, row, ts);
324
- const span = {
325
- workMs: end - start - acc.askMs,
326
- waitMs: acc.askMs,
327
- toolCalls: acc.toolIds.size,
328
- tokens: sumTokens([...acc.messages.values()]),
329
- };
330
- return { span, measured: acc.toolIds.size > 0 || acc.messages.size > 0 };
331
- };
332
-
333
- const sinceMs = since ? Date.parse(since) : Number.NaN;
334
- const totalStart = Number.isNaN(sinceMs) ? Math.min(...windows.map((w) => w.start)) : sinceMs;
335
- let totalEnd = Math.max(...windows.map((w) => w.end));
336
- // With --since the total runs to the latest host activity, so setup/close work lands in overhead.
337
- if (!Number.isNaN(sinceMs)) for (const [, ts] of rows) if (ts >= sinceMs) totalEnd = Math.max(totalEnd, ts);
338
- // 1146 R1: the close passes its own completion instant, which caps the window exactly.
339
- const untilMs = until ? Date.parse(until) : Number.NaN;
340
- if (!Number.isNaN(untilMs) && untilMs > totalStart) totalEnd = untilMs;
341
-
342
- const stageRows = windows.map((w) => {
343
- const { span, measured } = measure(w.start, w.end);
344
- return summaryRow(w.stage, w.status, span, measured);
345
- });
346
- const totalMeasure = measure(totalStart, totalEnd);
347
- const statuses = [...new Set(inputs.map((i) => i.progress.status ?? ''))].join('/');
348
- const total = summaryRow('Total', statuses, totalMeasure.span, totalMeasure.measured);
349
-
350
- // Parallel runs overlap, so their rows cannot partition the total: no overhead row then.
351
- const overlaps = windows.some((w, i) => i > 0 && w.start < (windows[i - 1]?.end ?? 0));
352
- const sum = sumSpans(
353
- stageRows.map((r) => ({ ...r, toolCalls: r.toolCalls ?? 0, tokens: r.tokens ?? zeroTokens() })),
354
- );
355
- const overhead: Span = {
356
- workMs: total.workMs - sum.workMs,
357
- waitMs: total.waitMs - sum.waitMs,
358
- toolCalls: (total.toolCalls ?? 0) - sum.toolCalls,
359
- tokens: Object.fromEntries(
360
- (Object.keys(sum.tokens) as (keyof Tokens)[]).map((k) => [k, (total.tokens?.[k] ?? 0) - sum.tokens[k]]),
361
- ) as Tokens,
362
- };
363
- if (!overlaps && overhead.workMs > 0)
364
- stageRows.push(summaryRow('driver overhead', '', overhead, totalMeasure.measured));
365
-
366
- return {
367
- available: true,
368
- rows: stageRows,
369
- total,
370
- actors: actorSplit(rows, totalStart, totalEnd),
371
- gate: { ms: null, reason: 'no matching check receipt' },
372
- skippedLines,
373
- };
374
- }
26
+ } from './plan-projection.generated.mjs';
27
+ import { parseRows, resolveTranscript } from './transcript';
28
+
29
+ // Task 1178 R2: the pure aggregation moved to `packages/app/src/workflow/run-summary.ts`. Re-export
30
+ // it from here so the adapter's existing surface (the `run-summary` script and its tests) keeps one
31
+ // import path; the implementation is delegated, never duplicated.
32
+ export {
33
+ type ActorSplit,
34
+ actorSplit,
35
+ buildRunSummary,
36
+ type GateTime,
37
+ type Progress,
38
+ type RunSummary,
39
+ type SummaryRow,
40
+ } from './plan-projection.generated.mjs';
375
41
 
376
42
  export function renderSummaryMarkdown(summary: RunSummary): string {
377
43
  const line = (r: SummaryRow, bold = false) => {
@@ -516,13 +182,12 @@ export function writeCloseSummary(
516
182
  if (run.startedAt === undefined) return na('run row has no started_at');
517
183
  const resolved = resolveTranscript(opts.env ?? getEnvVars(), opts.projectsRoot);
518
184
  if (!resolved.ok) return na(resolved.reason);
519
- const lines = readFileSync(resolved.path, 'utf8').split('\n');
520
- const summary = buildRunSummary(
521
- lines,
522
- [{ progress: run.progress as unknown as Progress }],
523
- run.startedAt,
524
- run.completedAt,
525
- );
185
+ const { rows, skippedLines } = parseRows(readFileSync(resolved.path, 'utf8').split('\n'));
186
+ const summary = buildRunSummary(rows, [{ progress: run.progress as unknown as Progress }], {
187
+ since: run.startedAt,
188
+ until: run.completedAt,
189
+ skippedLines,
190
+ });
526
191
  if (summary.rows.length === 0) return na('no timed attempts');
527
192
  mkdirSync(runDir, { recursive: true });
528
193
  writeFileSync(abs, `${renderSummaryMarkdown(summary)}\n`);
@@ -558,4 +223,3 @@ export function gateFromReceipt(cwd: string, runId: string): GateTime {
558
223
  return { ms: null, reason: 'check receipt is unreadable' };
559
224
  }
560
225
  }
561
-
@@ -1,212 +1,49 @@
1
1
  /**
2
- * Host-transcript measurement primitives shared by session-timeline (session review) and
3
- * run-summary (dev-run / dev-runall execution summary). Reads Claude Code JSONL rows read-only;
4
- * tokens are counted once per message id (a response spans several records). Node builtins only.
2
+ * Host-transcript adapter shared by session-timeline (session review) and run-summary (dev-run /
3
+ * dev-runall execution summary). Owns the host side: resolving the active transcript path from the
4
+ * environment, detecting the host row format, and parsing raw JSONL lines into timestamped rows.
5
+ * Reads Claude Code / pi JSONL rows read-only. Node builtins only.
6
+ *
7
+ * Task 1178 R2: the pure measurement (row/token/actor aggregation and its normalized types) lives
8
+ * in `packages/app/src/workflow/run-summary.ts` and is reached through the generated pure
9
+ * plan-projection entry. Normalize once here (`parseRows`) and hand the rows to that owner; the
10
+ * re-exports below keep the plugin's one import path for the whole surface.
5
11
  */
6
12
  import { existsSync, readdirSync } from 'node:fs';
7
13
  import { homedir } from 'node:os';
8
14
  import { join } from 'node:path';
9
-
10
- export type Tokens = Record<'input' | 'cacheCreate' | 'cacheRead' | 'output', number>;
11
-
12
- export interface Span {
13
- workMs: number;
14
- waitMs: number;
15
- toolCalls: number;
16
- tokens: Tokens;
17
- }
18
-
19
- export interface Block {
20
- type?: string;
21
- id?: string;
22
- name?: string;
23
- text?: string;
24
- tool_use_id?: string;
25
- /** Claude tool_result error flag (1146 R4): a failed subagent call is counted. */
26
- is_error?: boolean;
27
- }
28
-
29
- export interface Row {
30
- type?: string;
31
- /** Claude `system` rows carry the compaction subtype (1146 R4). */
32
- subtype?: string;
33
- timestamp?: string;
34
- isMeta?: boolean;
35
- isCompactSummary?: boolean;
36
- id?: string; // pi: the row id doubles as the once-per-message usage key
37
- message?: {
38
- id?: string;
39
- role?: string;
40
- content?: unknown;
41
- usage?: Record<string, number | undefined>;
42
- toolCallId?: string;
43
- /** pi toolResult rows name their tool here (1146 R4). */
44
- toolName?: string;
45
- /** pi toolResult error flag (1146 R4). */
46
- isError?: boolean;
47
- };
48
- }
49
-
50
- /** Running measurement of one segment or window. */
51
- export interface Acc {
52
- last: number;
53
- askMs: number;
54
- asks: Map<string, number>;
55
- toolIds: Set<string>;
56
- messages: Map<string, Tokens>;
57
- compactions: number;
58
- }
15
+ import type { Row } from './plan-projection.generated.mjs';
16
+
17
+ export {
18
+ type Acc,
19
+ type ActorSplit,
20
+ accumulate,
21
+ actorSplit,
22
+ type Block,
23
+ buildRunSummary,
24
+ formatDuration,
25
+ formatTokenSplit,
26
+ formatTokens,
27
+ type GateTime,
28
+ isCompaction,
29
+ newAcc,
30
+ type Progress,
31
+ promptText,
32
+ type Row,
33
+ type RunSummary,
34
+ type Span,
35
+ type SummaryRow,
36
+ skillInvocationPrompt,
37
+ sumSpans,
38
+ sumTokens,
39
+ type Tokens,
40
+ zeroTokens,
41
+ } from './plan-projection.generated.mjs';
59
42
 
60
43
  const SESSION_ID = /^[A-Za-z0-9_-]+$/;
61
- /** Host tools whose call→result gap is the operator answering, not the agent working. */
62
- const OPERATOR_TOOLS = new Set(['AskUserQuestion', 'ask_user_question']);
44
+ /** pi roles that prove a `type:"message"` row is a pi transcript, not Claude's `user`/`assistant`. */
63
45
  const PI_ROLES = new Set(['user', 'assistant', 'toolResult']);
64
46
 
65
- export const zeroTokens = (): Tokens => ({ input: 0, cacheCreate: 0, cacheRead: 0, output: 0 });
66
-
67
- export function formatDuration(ms: number): string {
68
- const s = Math.round(ms / 1000);
69
- const h = Math.floor(s / 3600);
70
- const m = Math.floor((s % 3600) / 60);
71
- const ss = String(s % 60).padStart(2, '0');
72
- return h > 0 ? `${h}:${String(m).padStart(2, '0')}:${ss}` : `${m}:${ss}`;
73
- }
74
-
75
- /** Compact token units: `950`, `41k`, `28.4M`. */
76
- export function formatTokens(n: number): string {
77
- if (n >= 999_500) return `${(n / 1e6).toFixed(1)}M`;
78
- return n >= 1000 ? `${Math.round(n / 1000)}k` : String(n);
79
- }
80
-
81
- /**
82
- * `<total> / <non-cached>`: total = every input class + output; non-cached drops cache reads, the
83
- * share re-served from the prompt cache, leaving the fresh input, cache writes and output.
84
- */
85
- export function formatTokenSplit(t: Tokens | null): string {
86
- if (!t) return 'n/a';
87
- const nonCached = t.input + t.cacheCreate + t.output;
88
- return `${formatTokens(nonCached + t.cacheRead)} / ${formatTokens(nonCached)}`;
89
- }
90
-
91
- /** Operator prompt text, or undefined for tool results, meta rows and compaction summaries. */
92
- export function promptText(row: Row): string | undefined {
93
- if (row.type === 'message') {
94
- // pi (1130): only a user-role message with text content is a prompt; toolResult rows are not.
95
- if (row.message?.role !== 'user') return undefined;
96
- // pi (1138 R2): a skill-injected body is activity, not an operator prompt.
97
- if (isInjectedPrompt(row)) return undefined;
98
- const content = row.message.content;
99
- if (typeof content === 'string') return content;
100
- if (!Array.isArray(content)) return undefined;
101
- return (content as Block[]).find((b) => b.type === 'text')?.text;
102
- }
103
- if (row.type !== 'user' || row.isMeta || row.isCompactSummary) return undefined;
104
- const content = row.message?.content;
105
- if (typeof content === 'string') return content;
106
- if (!Array.isArray(content) || (content as Block[]).some((b) => b.type === 'tool_result')) return undefined;
107
- return (content as Block[]).find((b) => b.type === 'text')?.text;
108
- }
109
-
110
- /**
111
- * 1138 R2: a pi user row whose first text block, after leading whitespace, opens with
112
- * `<skill name="`, is an injected body (pi has no `isMeta` to key on). It is accumulated as
113
- * activity and must not open a segment. Operator text that merely CONTAINS `<skill` later in the
114
- * body is still a prompt. Claude rows are unaffected: their injected bodies already arrive as `isMeta`.
115
- */
116
- export function isInjectedPrompt(row: Row): boolean {
117
- if (row.type !== 'message' || row.message?.role !== 'user') return false;
118
- const content = row.message.content;
119
- const text =
120
- typeof content === 'string'
121
- ? content
122
- : Array.isArray(content)
123
- ? (content as Block[]).find((b) => b.type === 'text')?.text
124
- : undefined;
125
- return typeof text === 'string' && text.replace(/^\s+/, '').startsWith('<skill name="');
126
- }
127
-
128
- export function sumTokens(all: Tokens[]): Tokens {
129
- const total = zeroTokens();
130
- for (const t of all) for (const k of Object.keys(total) as (keyof Tokens)[]) total[k] += t[k];
131
- return total;
132
- }
133
-
134
- export function sumSpans(spans: Span[]): Span {
135
- return {
136
- workMs: spans.reduce((n, s) => n + s.workMs, 0),
137
- waitMs: spans.reduce((n, s) => n + s.waitMs, 0),
138
- toolCalls: spans.reduce((n, s) => n + s.toolCalls, 0),
139
- tokens: sumTokens(spans.map((s) => s.tokens)),
140
- };
141
- }
142
-
143
- export const newAcc = (ts: number): Acc => ({
144
- last: ts,
145
- askMs: 0,
146
- asks: new Map(),
147
- toolIds: new Set(),
148
- messages: new Map(),
149
- compactions: 0,
150
- });
151
-
152
- /** Fold one timestamped non-prompt record into a segment or window. */
153
- export function accumulate(acc: Acc, row: Row, ts: number): void {
154
- acc.last = Math.max(acc.last, ts);
155
- if (row.type === 'message') {
156
- // pi (1130): mirrors the importer's field semantics (piRole / normalizeOmpToolCall /
157
- // ompToolResultTiming) — toolCall blocks on assistant rows, results paired by toolCallId.
158
- const m = row.message;
159
- if (m?.role === 'assistant') {
160
- const blocks = Array.isArray(m.content) ? (m.content as Block[]) : [];
161
- for (const b of blocks) {
162
- if (b.type === 'toolCall' && b.id) {
163
- acc.toolIds.add(b.id);
164
- if (b.name && OPERATOR_TOOLS.has(b.name)) acc.asks.set(b.id, ts);
165
- }
166
- }
167
- const u = m.usage;
168
- if (u && row.id)
169
- acc.messages.set(row.id, {
170
- input: u.input ?? 0,
171
- cacheCreate: u.cacheWrite ?? 0,
172
- cacheRead: u.cacheRead ?? 0,
173
- output: u.output ?? 0,
174
- });
175
- } else if (m?.role === 'toolResult' && m.toolCallId && acc.asks.has(m.toolCallId)) {
176
- acc.askMs += ts - (acc.asks.get(m.toolCallId) ?? ts);
177
- }
178
- return;
179
- }
180
- if (row.type === 'compaction') acc.compactions++; // pi-only row type (1130 R4)
181
- const blocks = Array.isArray(row.message?.content) ? (row.message.content as Block[]) : [];
182
- for (const b of blocks) {
183
- if (b.type === 'tool_use' && b.id) {
184
- acc.toolIds.add(b.id);
185
- if (b.name && OPERATOR_TOOLS.has(b.name)) acc.asks.set(b.id, ts);
186
- } else if (b.type === 'tool_result' && b.tool_use_id && acc.asks.has(b.tool_use_id)) {
187
- acc.askMs += ts - (acc.asks.get(b.tool_use_id) ?? ts);
188
- }
189
- }
190
- const u = row.message?.usage;
191
- // Split records of one response repeat its usage: keyed by message id, overwrite, never add.
192
- if (row.type === 'assistant' && u && row.message?.id) {
193
- acc.messages.set(row.message.id, {
194
- input: u.input_tokens ?? 0,
195
- cacheCreate: u.cache_creation_input_tokens ?? 0,
196
- cacheRead: u.cache_read_input_tokens ?? 0,
197
- output: u.output_tokens ?? 0,
198
- });
199
- }
200
- }
201
-
202
- /**
203
- * 1146 R4: a compaction boundary in either host shape — pi writes a `compaction` row, Claude a
204
- * `system` row with `subtype:"compact_boundary"`.
205
- */
206
- export function isCompaction(row: Row): boolean {
207
- return row.type === 'compaction' || (row.type === 'system' && row.subtype === 'compact_boundary');
208
- }
209
-
210
47
  /** First row matching a known shape decides: `type:"message"` with a pi role → pi, else Claude. */
211
48
  export function sniffFormat(rows: Row[]): 'claude' | 'pi' | 'unknown' {
212
49
  for (const row of rows) {