@gobing-ai/spur 0.4.0 → 0.4.1
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.
- package/.claude-plugin/marketplace.json +1 -1
- package/config/decisions/gates.yaml +36 -0
- package/config/decisions/history-anatomy.yaml +35 -0
- package/config/decisions/idea-pipeline.yaml +38 -0
- package/config/plugin-scripts.json +15 -0
- package/config/rules/docs/sourced-api-claims.yaml +60 -0
- package/config/rules/recommended-pre-check.yaml +8 -3
- package/config/rules/structure/scratch-evidence-pointer.yaml +35 -0
- package/config/script-placement-baseline.json +2 -2
- package/config/workflows/feature-lifecycle.yaml +9 -2
- package/config/workflows/history-anatomy.yaml +22 -0
- package/config/workflows/idea-pipeline.yaml +75 -0
- package/config/workflows/task-pipeline.yaml +216 -57
- package/config/workflows/wrapup-pipeline.yaml +106 -13
- package/package.json +9 -9
- package/plugins/sp/README.md +11 -1
- package/plugins/sp/agents/super-planner.md +11 -0
- package/plugins/sp/agents/super-reviewer.md +14 -0
- package/plugins/sp/bunfig.toml +8 -0
- package/plugins/sp/commands/dev-brainstorm.md +29 -16
- package/plugins/sp/commands/dev-find-way.md +80 -0
- package/plugins/sp/commands/dev-idea.md +6 -0
- package/plugins/sp/commands/dev-parallel.md +8 -2
- package/plugins/sp/commands/dev-plan.md +6 -0
- package/plugins/sp/commands/dev-review-session.md +3 -1
- package/plugins/sp/commands/dev-run.md +12 -2
- package/plugins/sp/commands/dev-runall.md +18 -10
- package/plugins/sp/hooks/context-session-start.ts +1 -1
- package/plugins/sp/lib/idea-handoff.generated.mjs +41 -5
- package/plugins/sp/lib/inline-run-app.ts +28 -0
- package/plugins/sp/lib/inline-run.generated.d.mts +5 -0
- package/plugins/sp/lib/inline-run.generated.mjs +333 -45
- package/plugins/sp/lib/plan-projection.generated.d.mts +28 -0
- package/plugins/sp/lib/plan-projection.generated.mjs +255 -0
- package/plugins/sp/lib/quality-gate.generated.d.mts +47 -1
- package/plugins/sp/lib/quality-gate.generated.mjs +30489 -82
- package/plugins/sp/lib/residual-scan.generated.d.mts +19 -0
- package/plugins/sp/lib/residual-scan.generated.mjs +66 -0
- package/plugins/sp/lib/run-summary-core.ts +561 -0
- package/plugins/sp/lib/transcript.ts +121 -6
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/references/roles.md +1 -1
- package/plugins/sp/scripts/batch-plan.mjs +335 -0
- package/plugins/sp/scripts/batch-plan.ts +100 -0
- package/plugins/sp/scripts/batch-preflight.mjs +51 -3
- package/plugins/sp/scripts/batch-preflight.ts +72 -1
- package/plugins/sp/scripts/commit-guard.mjs +216 -0
- package/plugins/sp/scripts/commit-guard.ts +249 -0
- package/plugins/sp/scripts/inline-run-dispatch.mjs +130 -0
- package/plugins/sp/scripts/inline-run-dispatch.ts +135 -0
- package/plugins/sp/scripts/inline-run-setup.mjs +551 -80
- package/plugins/sp/scripts/inline-run-setup.ts +52 -78
- package/plugins/sp/scripts/persist-out-check.mjs +90 -41
- package/plugins/sp/scripts/persist-out-check.ts +112 -57
- package/plugins/sp/scripts/quality-gate.mjs +30407 -73
- package/plugins/sp/scripts/quality-gate.ts +21 -5
- package/plugins/sp/scripts/residual-scan.mjs +86 -31
- package/plugins/sp/scripts/residual-scan.ts +28 -30
- package/plugins/sp/scripts/run-summary.mjs +421 -14
- package/plugins/sp/scripts/run-summary.ts +41 -177
- package/plugins/sp/scripts/session-timeline.mjs +109 -12
- package/plugins/sp/scripts/session-timeline.ts +71 -19
- package/plugins/sp/scripts/wrapup-steps.mjs +150 -2
- package/plugins/sp/scripts/wrapup-steps.ts +203 -1
- package/plugins/sp/skills/brainstorm/SKILL.md +9 -47
- package/plugins/sp/skills/branch-workflow/SKILL.md +1 -0
- package/plugins/sp/skills/branch-workflow/references/worktree-patterns.md +1 -0
- package/plugins/sp/skills/code-implementation/SKILL.md +48 -1
- package/plugins/sp/skills/code-verification/SKILL.md +3 -4
- package/plugins/sp/skills/code-verification/references/secu-review.md +3 -0
- package/plugins/sp/skills/code-verification/references/verdict-schema.md +68 -0
- package/plugins/sp/skills/fleet-join/SKILL.md +1 -1
- package/plugins/sp/skills/session-review/SKILL.md +38 -7
- package/plugins/sp/skills/source-driven-development/SKILL.md +5 -0
- package/plugins/sp/skills/spec-decomposition/references/decomposition.md +5 -3
- package/plugins/sp/skills/spur-cli/SKILL.md +5 -2
- package/plugins/sp/skills/spur-cli/references/agent.md +20 -15
- package/plugins/sp/skills/spur-cli/references/decision.md +7 -6
- package/plugins/sp/skills/spur-cli/references/message.md +7 -7
- package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +19 -3
- package/plugins/sp/skills/spur-cli/references/tasks.md +10 -7
- package/plugins/sp/skills/spur-cli/references/workflows/authoring-workflows.md +7 -0
- package/plugins/sp/skills/spur-cli/references/workflows.md +7 -2
- package/plugins/sp/skills/spur-composer/SKILL.md +1 -1
- package/plugins/sp/skills/spur-dev/SKILL.md +18 -14
- package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +6 -3
- package/plugins/sp/skills/spur-dev/references/cross-cutting.md +49 -14
- package/plugins/sp/skills/spur-dev/references/dev-operations.md +37 -16
- package/plugins/sp/skills/spur-dev/references/execution-batch-continuation.md +68 -0
- package/plugins/sp/skills/spur-dev/references/execution-batch-report.md +194 -0
- package/plugins/sp/skills/spur-dev/references/execution-batch.md +113 -1020
- package/plugins/sp/skills/spur-dev/references/execution-parallel-isolation.md +130 -0
- package/plugins/sp/skills/spur-dev/references/execution-workflow.md +6 -0
- package/plugins/sp/skills/spur-dev/references/execution-worktree-landing.md +471 -0
- package/plugins/sp/skills/spur-dev/references/execution-worktree-setup.md +379 -0
- package/plugins/sp/skills/spur-dev/references/flag-glossary.md +35 -5
- package/plugins/sp/skills/spur-dev/references/glossary.md +2 -2
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +343 -163
- package/plugins/sp/skills/spur-dev/references/structured-trace-emission.md +178 -0
- package/plugins/sp/skills/wayfinder/SKILL.md +10 -14
- package/schemas/state-machine-workflow.schema.json +23 -0
- package/spur.js +20728 -16630
- package/web/_astro/{BoardApp.VeoxxLBy.js → BoardApp.D66k_s_T.js} +1 -1
- package/web/_astro/{BoardApp.Bzr4OXrW.js → BoardApp.D6sjAJr_.js} +173 -173
- package/web/_astro/{TaskDetail.LAyR_Pvn.js → TaskDetail.BynLnMNc.js} +1 -1
- package/web/_astro/{arc.f4e0OYLg.js → arc._BUadrdH.js} +1 -1
- package/web/_astro/{architectureDiagram-3BPJPVTR.SSIvxi-Z.js → architectureDiagram-3BPJPVTR.Dh7xS1Pp.js} +1 -1
- package/web/_astro/{blockDiagram-GPEHLZMM.DSXIHOFf.js → blockDiagram-GPEHLZMM.DFDVpfXm.js} +1 -1
- package/web/_astro/{c4Diagram-AAUBKEIU.DG9Ubu85.js → c4Diagram-AAUBKEIU.V7_3_glh.js} +1 -1
- package/web/_astro/channel.DZVfto-r.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.C7QU88Qh.js → chunk-2J33WTMH.De1yx7Jz.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.26Z94xBj.js → chunk-4BX2VUAB.BDhHFuxx.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.DYRiBkqw.js → chunk-55IACEB6.DDLkWnmC.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.D5Vfv5pe.js → chunk-727SXJPM.CwfiLnA7.js} +1 -1
- package/web/_astro/{chunk-AQP2D5EJ.DXHV47bN.js → chunk-AQP2D5EJ.CtGDmUnM.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.D1suAfOs.js → chunk-FMBD7UC4.BOLjpw73.js} +1 -1
- package/web/_astro/{chunk-ND2GUHAM.D6BmdHEY.js → chunk-ND2GUHAM.BDhQls6O.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.rnPU-Ak-.js → chunk-QZHKN3VN.76qBynDF.js} +1 -1
- package/web/_astro/{classDiagram-4FO5ZUOK.CvEfXIo1.js → classDiagram-4FO5ZUOK.N0mpPHT0.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.CvEfXIo1.js → classDiagram-v2-Q7XG4LA2.N0mpPHT0.js} +1 -1
- package/web/_astro/{cose-bilkent-S5V4N54A.DkMisLSm.js → cose-bilkent-S5V4N54A.CKknFfjB.js} +1 -1
- package/web/_astro/{cynefin-OW5HDTMX.C77Q-pF6.js → cynefin-OW5HDTMX.Cw3ZWBAh.js} +1 -1
- package/web/_astro/{dagre-BM42HDAG.YaV8ce27.js → dagre-BM42HDAG.D3yIKCRD.js} +1 -1
- package/web/_astro/{diagram-2AECGRRQ.CUyGEKge.js → diagram-2AECGRRQ.D51dO83Z.js} +1 -1
- package/web/_astro/{diagram-5GNKFQAL.CRgd8dgj.js → diagram-5GNKFQAL.Bb-FViDj.js} +1 -1
- package/web/_astro/{diagram-KO2AKTUF.Bmzys9hq.js → diagram-KO2AKTUF.D3mN8S6x.js} +1 -1
- package/web/_astro/{diagram-LMA3HP47.BvmJY4Lw.js → diagram-LMA3HP47.dMMyPcbJ.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.0hEhD30d.js → diagram-OG6HWLK6.AzbvzY4C.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35.Bjt8lrnm.js → erDiagram-TEJ5UH35.BR5I4OZb.js} +1 -1
- package/web/_astro/{flowDiagram-I6XJVG4X.CJZTjRDV.js → flowDiagram-I6XJVG4X.DFqvhGkn.js} +1 -1
- package/web/_astro/{ganttDiagram-6RSMTGT7._VqoVafc.js → ganttDiagram-6RSMTGT7.ByJbhH6J.js} +1 -1
- package/web/_astro/{gitGraphDiagram-PVQCEYII.CEDb7tVo.js → gitGraphDiagram-PVQCEYII.CInHjQR3.js} +1 -1
- package/web/_astro/index.COh_kBxN.css +1 -0
- package/web/_astro/{infoDiagram-5YYISTIA.CgwifPpK.js → infoDiagram-5YYISTIA.Cm0dGVjJ.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.B99wx2J1.js → ishikawaDiagram-YF4QCWOH.C3nOP6Sj.js} +1 -1
- package/web/_astro/{journeyDiagram-JHISSGLW.2hRwJbEo.js → journeyDiagram-JHISSGLW.4IvLa2MW.js} +1 -1
- package/web/_astro/{kanban-definition-UN3LZRKU.BahJkKNC.js → kanban-definition-UN3LZRKU.DS87Cl-k.js} +1 -1
- package/web/_astro/{linear.DguUUe6V.js → linear.MzmRrNER.js} +1 -1
- package/web/_astro/{mermaid.core.CvgfmqVr.js → mermaid.core.fK-WNswS.js} +4 -4
- package/web/_astro/{mindmap-definition-RKZ34NQL.B3qEA3x9.js → mindmap-definition-RKZ34NQL.BqipOHhY.js} +1 -1
- package/web/_astro/{pieDiagram-4H26LBE5.FlWA-_ru.js → pieDiagram-4H26LBE5.0nBiPqFG.js} +1 -1
- package/web/_astro/{quadrantDiagram-W4KKPZXB.9bVn35X7.js → quadrantDiagram-W4KKPZXB.Szj7Zpx5.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.ChykIhct.js → requirementDiagram-4Y6WPE33.D4UR6TWo.js} +1 -1
- package/web/_astro/{sankeyDiagram-5OEKKPKP.Z2Y3hkYb.js → sankeyDiagram-5OEKKPKP.DAE7BdOs.js} +1 -1
- package/web/_astro/{sequenceDiagram-3UESZ5HK.J7JsHWDt.js → sequenceDiagram-3UESZ5HK.BP9ZVq25.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.DAPbiCSS.js → stateDiagram-AJRCARHV.BQW7mIxk.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.D2jvSi0q.js → stateDiagram-v2-BHNVJYJU.PbK4nDih.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.psW2ci29.js → timeline-definition-PNZ67QCA.Ci_GvUpo.js} +1 -1
- package/web/_astro/{vennDiagram-CIIHVFJN.DlILYVmg.js → vennDiagram-CIIHVFJN.DNw1Y5qx.js} +1 -1
- package/web/_astro/{wardleyDiagram-YWT4CUSO.xNWwLJnn.js → wardleyDiagram-YWT4CUSO.CwlkaaNe.js} +1 -1
- package/web/_astro/{xychartDiagram-2RQKCTM6.EU_BuH7V.js → xychartDiagram-2RQKCTM6.MI6ZuiH_.js} +1 -1
- package/web/index.html +2 -2
- package/web/_astro/channel.CUFdtWhm.js +0 -1
- package/web/_astro/index.Dg_3bOaJ.css +0 -1
|
@@ -2,189 +2,29 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* run-summary — the measured execution summary `/sp:dev-run` and `/sp:dev-runall` print by default
|
|
4
4
|
* (`--no-summary` skips it). Inputs: one or more `spur workflow progress <run-id> --json` files
|
|
5
|
-
* (`--progress [<label>=]<file>`) and the host
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* record (a subprocess stage) reads `n/a`, never 0. `--markdown` prints the table. Reporting only:
|
|
11
|
-
* no transcript → `{available:false}` (or one `n/a` line), exit 0. Node builtins only.
|
|
5
|
+
* (`--progress [<label>=]<file>`) and the host transcript (resolved like session-timeline, or
|
|
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.
|
|
12
10
|
*/
|
|
13
11
|
import { readFileSync } from 'node:fs';
|
|
14
12
|
import { getEnvVars } from '../lib/env';
|
|
15
13
|
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
zeroTokens,
|
|
28
|
-
} from '../lib/transcript';
|
|
29
|
-
|
|
30
|
-
interface ProgressAttempt {
|
|
31
|
-
startedAt?: string;
|
|
32
|
-
completedAt?: string;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** The subset of `spur workflow progress <run-id> --json` the summary reads. */
|
|
36
|
-
export interface Progress {
|
|
37
|
-
runId: string;
|
|
38
|
-
workflow?: string;
|
|
39
|
-
status?: string;
|
|
40
|
-
states?: { state: string; visit?: number; status?: string; actions?: { attempts?: ProgressAttempt[] }[] }[];
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/** A stage row; `toolCalls`/`tokens` are null when the window holds no host transcript record. */
|
|
44
|
-
export interface SummaryRow {
|
|
45
|
-
stage: string;
|
|
46
|
-
status: string;
|
|
47
|
-
workMs: number;
|
|
48
|
-
waitMs: number;
|
|
49
|
-
toolCalls: number | null;
|
|
50
|
-
tokens: Tokens | null;
|
|
51
|
-
time: string;
|
|
52
|
-
wait: string;
|
|
53
|
-
token: string;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export interface RunSummary {
|
|
57
|
-
available: true;
|
|
58
|
-
transcript?: string;
|
|
59
|
-
rows: SummaryRow[];
|
|
60
|
-
total: SummaryRow;
|
|
61
|
-
skippedLines: number;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
interface Window {
|
|
65
|
-
stage: string;
|
|
66
|
-
status: string;
|
|
67
|
-
start: number;
|
|
68
|
-
end: number;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function attemptWindow(attempts: ProgressAttempt[]): [number, number] | undefined {
|
|
72
|
-
const starts = attempts.map((a) => Date.parse(a.startedAt ?? '')).filter((n) => !Number.isNaN(n));
|
|
73
|
-
const ends = attempts.map((a) => Date.parse(a.completedAt ?? '')).filter((n) => !Number.isNaN(n));
|
|
74
|
-
return starts.length && ends.length ? [Math.min(...starts), Math.max(...ends)] : undefined;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function summaryRow(stage: string, status: string, span: Span, measured: boolean): SummaryRow {
|
|
78
|
-
return {
|
|
79
|
-
stage,
|
|
80
|
-
status,
|
|
81
|
-
workMs: span.workMs,
|
|
82
|
-
waitMs: span.waitMs,
|
|
83
|
-
toolCalls: measured ? span.toolCalls : null,
|
|
84
|
-
tokens: measured ? span.tokens : null,
|
|
85
|
-
time: formatDuration(span.workMs),
|
|
86
|
-
wait: formatDuration(span.waitMs),
|
|
87
|
-
token: formatTokenSplit(measured ? span.tokens : null),
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* dev-run / dev-runall execution summary. One progress file → one row per state visit; several →
|
|
93
|
-
* one row per run. Each window spans its first attempt start → last completion (inclusive) and
|
|
94
|
-
* slices the host transcript; work = window − AskUserQuestion answer time.
|
|
95
|
-
*/
|
|
96
|
-
export function buildRunSummary(
|
|
97
|
-
lines: string[],
|
|
98
|
-
inputs: { label?: string; progress: Progress }[],
|
|
99
|
-
since?: string,
|
|
100
|
-
): RunSummary {
|
|
101
|
-
const windows: Window[] = [];
|
|
102
|
-
for (const { label, progress } of inputs) {
|
|
103
|
-
const states = (progress.states ?? []).flatMap((s) => {
|
|
104
|
-
const w = attemptWindow((s.actions ?? []).flatMap((a) => a.attempts ?? []));
|
|
105
|
-
return w ? [{ s, w }] : [];
|
|
106
|
-
});
|
|
107
|
-
if (!states.length) throw new Error(`progress for run ${progress.runId} has no timed attempts`);
|
|
108
|
-
if (inputs.length === 1) {
|
|
109
|
-
for (const { s, w } of states) {
|
|
110
|
-
const stage = (s.visit ?? 1) > 1 ? `${s.state} (visit ${s.visit})` : s.state;
|
|
111
|
-
windows.push({ stage, status: s.status ?? '', start: w[0], end: w[1] });
|
|
112
|
-
}
|
|
113
|
-
} else {
|
|
114
|
-
const stage = label ?? `${progress.workflow ?? 'run'} ${progress.runId.slice(0, 8)}`;
|
|
115
|
-
const start = Math.min(...states.map(({ w }) => w[0]));
|
|
116
|
-
windows.push({
|
|
117
|
-
stage,
|
|
118
|
-
status: progress.status ?? '',
|
|
119
|
-
start,
|
|
120
|
-
end: Math.max(...states.map(({ w }) => w[1])),
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
windows.sort((a, b) => a.start - b.start);
|
|
125
|
-
|
|
126
|
-
const { rows, skippedLines } = parseRows(lines);
|
|
127
|
-
const measure = (start: number, end: number): { span: Span; measured: boolean } => {
|
|
128
|
-
const acc = newAcc(start);
|
|
129
|
-
for (const [row, ts] of rows)
|
|
130
|
-
if (ts >= start && ts <= end && promptText(row) === undefined) accumulate(acc, row, ts);
|
|
131
|
-
const span = {
|
|
132
|
-
workMs: end - start - acc.askMs,
|
|
133
|
-
waitMs: acc.askMs,
|
|
134
|
-
toolCalls: acc.toolIds.size,
|
|
135
|
-
tokens: sumTokens([...acc.messages.values()]),
|
|
136
|
-
};
|
|
137
|
-
return { span, measured: acc.toolIds.size > 0 || acc.messages.size > 0 };
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
const sinceMs = since ? Date.parse(since) : Number.NaN;
|
|
141
|
-
const totalStart = Number.isNaN(sinceMs) ? Math.min(...windows.map((w) => w.start)) : sinceMs;
|
|
142
|
-
let totalEnd = Math.max(...windows.map((w) => w.end));
|
|
143
|
-
// With --since the total runs to the latest host activity, so setup/close work lands in overhead.
|
|
144
|
-
if (!Number.isNaN(sinceMs)) for (const [, ts] of rows) if (ts >= sinceMs) totalEnd = Math.max(totalEnd, ts);
|
|
145
|
-
|
|
146
|
-
const stageRows = windows.map((w) => {
|
|
147
|
-
const { span, measured } = measure(w.start, w.end);
|
|
148
|
-
return summaryRow(w.stage, w.status, span, measured);
|
|
149
|
-
});
|
|
150
|
-
const totalMeasure = measure(totalStart, totalEnd);
|
|
151
|
-
const statuses = [...new Set(inputs.map((i) => i.progress.status ?? ''))].join('/');
|
|
152
|
-
const total = summaryRow('Total', statuses, totalMeasure.span, totalMeasure.measured);
|
|
153
|
-
|
|
154
|
-
// Parallel runs overlap, so their rows cannot partition the total: no overhead row then.
|
|
155
|
-
const overlaps = windows.some((w, i) => i > 0 && w.start < (windows[i - 1]?.end ?? 0));
|
|
156
|
-
const sum = sumSpans(
|
|
157
|
-
stageRows.map((r) => ({ ...r, toolCalls: r.toolCalls ?? 0, tokens: r.tokens ?? zeroTokens() })),
|
|
158
|
-
);
|
|
159
|
-
const overhead: Span = {
|
|
160
|
-
workMs: total.workMs - sum.workMs,
|
|
161
|
-
waitMs: total.waitMs - sum.waitMs,
|
|
162
|
-
toolCalls: (total.toolCalls ?? 0) - sum.toolCalls,
|
|
163
|
-
tokens: Object.fromEntries(
|
|
164
|
-
(Object.keys(sum.tokens) as (keyof Tokens)[]).map((k) => [k, (total.tokens?.[k] ?? 0) - sum.tokens[k]]),
|
|
165
|
-
) as Tokens,
|
|
166
|
-
};
|
|
167
|
-
if (!overlaps && overhead.workMs > 0)
|
|
168
|
-
stageRows.push(summaryRow('driver overhead', '', overhead, totalMeasure.measured));
|
|
169
|
-
|
|
170
|
-
return { available: true, rows: stageRows, total, skippedLines };
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
export function renderSummaryMarkdown(summary: RunSummary): string {
|
|
174
|
-
const line = (r: SummaryRow, bold = false) => {
|
|
175
|
-
const stage = bold ? `**${r.stage}**` : r.stage;
|
|
176
|
-
return `| ${stage} | ${r.status} | ${r.time} | ${r.wait} | ${r.toolCalls ?? 'n/a'} | ${r.token} |`;
|
|
177
|
-
};
|
|
178
|
-
return [
|
|
179
|
-
'| Stage | Status | Time | Wait | Tool calls | Token (total / non-cached) |',
|
|
180
|
-
'| --- | --- | ---: | ---: | ---: | ---: |',
|
|
181
|
-
...summary.rows.map((r) => line(r)),
|
|
182
|
-
line(summary.total, true),
|
|
183
|
-
].join('\n');
|
|
184
|
-
}
|
|
14
|
+
buildRunSummary,
|
|
15
|
+
gateFromReceipt,
|
|
16
|
+
type Progress,
|
|
17
|
+
type RollupInput,
|
|
18
|
+
renderRollupMarkdown,
|
|
19
|
+
renderSummaryMarkdown,
|
|
20
|
+
} from '../lib/run-summary-core';
|
|
21
|
+
import { resolveTranscript } from '../lib/transcript';
|
|
22
|
+
|
|
23
|
+
// Re-exported so the suite and any consumer keep one import path for the whole surface.
|
|
24
|
+
export * from '../lib/run-summary-core';
|
|
185
25
|
|
|
186
26
|
export const RUN_SUMMARY_USAGE =
|
|
187
|
-
'usage: run-summary --progress [<label>=]<file> [--progress ...] [--transcript <path>] [--since <iso>] [--markdown]';
|
|
27
|
+
'usage: run-summary (--progress [<label>=]<file> [--progress ...] | --rollup [<label>=]<summary.json> [--rollup ...]) [--transcript <path>] [--since <iso>] [--markdown]';
|
|
188
28
|
|
|
189
29
|
export function main(
|
|
190
30
|
argv: string[],
|
|
@@ -196,10 +36,12 @@ export function main(
|
|
|
196
36
|
let since: string | undefined;
|
|
197
37
|
let markdown = false;
|
|
198
38
|
const progressArgs: string[] = [];
|
|
39
|
+
const rollupArgs: string[] = [];
|
|
199
40
|
for (let i = 0; i < argv.length; i++) {
|
|
200
41
|
const arg = argv[i];
|
|
201
42
|
if (arg === '--transcript' && argv[i + 1]) transcript = argv[++i];
|
|
202
43
|
else if (arg === '--progress' && argv[i + 1]) progressArgs.push(argv[++i] as string);
|
|
44
|
+
else if (arg === '--rollup' && argv[i + 1]) rollupArgs.push(argv[++i] as string);
|
|
203
45
|
else if (arg === '--since' && argv[i + 1]) since = argv[++i];
|
|
204
46
|
else if (arg === '--markdown') markdown = true;
|
|
205
47
|
else if (arg === '--spur-bin' && argv[i + 1])
|
|
@@ -209,6 +51,27 @@ export function main(
|
|
|
209
51
|
return 2;
|
|
210
52
|
}
|
|
211
53
|
}
|
|
54
|
+
// 1146 AC5: --rollup is a different mode — it reads close-written summary files and never
|
|
55
|
+
// re-measures, so combining it with a measurement source is a usage error.
|
|
56
|
+
if (rollupArgs.length > 0 && (progressArgs.length > 0 || since !== undefined || transcript !== undefined)) {
|
|
57
|
+
process.stderr.write(`${RUN_SUMMARY_USAGE}\n`);
|
|
58
|
+
return 2;
|
|
59
|
+
}
|
|
60
|
+
if (rollupArgs.length > 0) {
|
|
61
|
+
try {
|
|
62
|
+
const inputs: RollupInput[] = rollupArgs.map((arg) => {
|
|
63
|
+
const eq = arg.indexOf('=');
|
|
64
|
+
const [label, file] = eq > 0 ? [arg.slice(0, eq), arg.slice(eq + 1)] : [undefined, arg];
|
|
65
|
+
const summary = JSON.parse(readFileSync(file as string, 'utf8')) as RollupInput['summary'];
|
|
66
|
+
return { label: label ?? summary.runId ?? (file as string), summary };
|
|
67
|
+
});
|
|
68
|
+
write(`${renderRollupMarkdown(inputs)}\n`);
|
|
69
|
+
return 0;
|
|
70
|
+
} catch (error) {
|
|
71
|
+
process.stderr.write(`run-summary: ${(error as Error).message}\n`);
|
|
72
|
+
return 2;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
212
75
|
if (!progressArgs.length) {
|
|
213
76
|
process.stderr.write(`${RUN_SUMMARY_USAGE}\n`);
|
|
214
77
|
return 2;
|
|
@@ -228,6 +91,7 @@ export function main(
|
|
|
228
91
|
return { label, progress: JSON.parse(readFileSync(file, 'utf8')) as Progress };
|
|
229
92
|
});
|
|
230
93
|
const summary = buildRunSummary(readFileSync(resolved.path, 'utf8').split('\n'), inputs, since);
|
|
94
|
+
summary.gate = gateFromReceipt(process.cwd(), inputs[0]?.progress.runId ?? '');
|
|
231
95
|
write(
|
|
232
96
|
`${markdown ? renderSummaryMarkdown(summary) : JSON.stringify({ ...summary, transcript: resolved.path })}\n`,
|
|
233
97
|
);
|
|
@@ -14,7 +14,8 @@ import { existsSync, readdirSync } from "node:fs";
|
|
|
14
14
|
import { homedir } from "node:os";
|
|
15
15
|
import { join } from "node:path";
|
|
16
16
|
var SESSION_ID = /^[A-Za-z0-9_-]+$/;
|
|
17
|
-
var OPERATOR_TOOLS = new Set(["AskUserQuestion"]);
|
|
17
|
+
var OPERATOR_TOOLS = new Set(["AskUserQuestion", "ask_user_question"]);
|
|
18
|
+
var PI_ROLES = new Set(["user", "assistant", "toolResult"]);
|
|
18
19
|
var zeroTokens = () => ({ input: 0, cacheCreate: 0, cacheRead: 0, output: 0 });
|
|
19
20
|
function formatDuration(ms) {
|
|
20
21
|
const s = Math.round(ms / 1000);
|
|
@@ -35,6 +36,18 @@ function formatTokenSplit(t) {
|
|
|
35
36
|
return `${formatTokens(nonCached + t.cacheRead)} / ${formatTokens(nonCached)}`;
|
|
36
37
|
}
|
|
37
38
|
function promptText(row) {
|
|
39
|
+
if (row.type === "message") {
|
|
40
|
+
if (row.message?.role !== "user")
|
|
41
|
+
return;
|
|
42
|
+
if (isInjectedPrompt(row))
|
|
43
|
+
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;
|
|
50
|
+
}
|
|
38
51
|
if (row.type !== "user" || row.isMeta || row.isCompactSummary)
|
|
39
52
|
return;
|
|
40
53
|
const content = row.message?.content;
|
|
@@ -44,6 +57,13 @@ function promptText(row) {
|
|
|
44
57
|
return;
|
|
45
58
|
return content.find((b) => b.type === "text")?.text;
|
|
46
59
|
}
|
|
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
|
+
}
|
|
47
67
|
function sumTokens(all) {
|
|
48
68
|
const total = zeroTokens();
|
|
49
69
|
for (const t of all)
|
|
@@ -64,10 +84,37 @@ var newAcc = (ts) => ({
|
|
|
64
84
|
askMs: 0,
|
|
65
85
|
asks: new Map,
|
|
66
86
|
toolIds: new Set,
|
|
67
|
-
messages: new Map
|
|
87
|
+
messages: new Map,
|
|
88
|
+
compactions: 0
|
|
68
89
|
});
|
|
69
90
|
function accumulate(acc, row, ts) {
|
|
70
91
|
acc.last = Math.max(acc.last, ts);
|
|
92
|
+
if (row.type === "message") {
|
|
93
|
+
const m = row.message;
|
|
94
|
+
if (m?.role === "assistant") {
|
|
95
|
+
const blocks2 = Array.isArray(m.content) ? m.content : [];
|
|
96
|
+
for (const b of blocks2) {
|
|
97
|
+
if (b.type === "toolCall" && b.id) {
|
|
98
|
+
acc.toolIds.add(b.id);
|
|
99
|
+
if (b.name && OPERATOR_TOOLS.has(b.name))
|
|
100
|
+
acc.asks.set(b.id, ts);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const u2 = m.usage;
|
|
104
|
+
if (u2 && row.id)
|
|
105
|
+
acc.messages.set(row.id, {
|
|
106
|
+
input: u2.input ?? 0,
|
|
107
|
+
cacheCreate: u2.cacheWrite ?? 0,
|
|
108
|
+
cacheRead: u2.cacheRead ?? 0,
|
|
109
|
+
output: u2.output ?? 0
|
|
110
|
+
});
|
|
111
|
+
} else if (m?.role === "toolResult" && m.toolCallId && acc.asks.has(m.toolCallId)) {
|
|
112
|
+
acc.askMs += ts - (acc.asks.get(m.toolCallId) ?? ts);
|
|
113
|
+
}
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (row.type === "compaction")
|
|
117
|
+
acc.compactions++;
|
|
71
118
|
const blocks = Array.isArray(row.message?.content) ? row.message.content : [];
|
|
72
119
|
for (const b of blocks) {
|
|
73
120
|
if (b.type === "tool_use" && b.id) {
|
|
@@ -88,6 +135,15 @@ function accumulate(acc, row, ts) {
|
|
|
88
135
|
});
|
|
89
136
|
}
|
|
90
137
|
}
|
|
138
|
+
function sniffFormat(rows) {
|
|
139
|
+
for (const row of rows) {
|
|
140
|
+
if (row.type === "message" && PI_ROLES.has(row.message?.role ?? ""))
|
|
141
|
+
return "pi";
|
|
142
|
+
if (row.type === "user" || row.type === "assistant")
|
|
143
|
+
return "claude";
|
|
144
|
+
}
|
|
145
|
+
return "unknown";
|
|
146
|
+
}
|
|
91
147
|
function parseRows(lines) {
|
|
92
148
|
const rows = [];
|
|
93
149
|
let skippedLines = 0;
|
|
@@ -105,14 +161,22 @@ function parseRows(lines) {
|
|
|
105
161
|
if (!Number.isNaN(ts))
|
|
106
162
|
rows.push([row, ts]);
|
|
107
163
|
}
|
|
108
|
-
return { rows, skippedLines };
|
|
164
|
+
return { rows, skippedLines, format: sniffFormat(rows.map(([r]) => r)) };
|
|
109
165
|
}
|
|
110
166
|
function resolveTranscript(env, projectsRoot = join(homedir(), ".claude", "projects"), override) {
|
|
111
167
|
if (override)
|
|
112
168
|
return existsSync(override) ? { ok: true, path: override } : { ok: false, reason: "no transcript" };
|
|
113
169
|
const id = env.CLAUDE_CODE_SESSION_ID;
|
|
114
|
-
if (!id)
|
|
115
|
-
|
|
170
|
+
if (!id) {
|
|
171
|
+
const piFile = env.PI_SESSION_FILE;
|
|
172
|
+
if (piFile !== undefined && piFile !== "") {
|
|
173
|
+
return existsSync(piFile) ? { ok: true, path: piFile } : { ok: false, reason: `PI_SESSION_FILE ${piFile} does not exist` };
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
ok: false,
|
|
177
|
+
reason: "no host session id; pass --transcript <path> (pi: ~/.pi/agent/sessions/<cwd-slug>/<file>.jsonl, " + "or set PI_SESSION_FILE)"
|
|
178
|
+
};
|
|
179
|
+
}
|
|
116
180
|
if (!SESSION_ID.test(id))
|
|
117
181
|
return { ok: false, reason: "refusing a session id with path characters" };
|
|
118
182
|
if (!existsSync(projectsRoot))
|
|
@@ -146,8 +210,17 @@ function parseGroups(spec, count) {
|
|
|
146
210
|
}
|
|
147
211
|
function buildTimeline(lines, group) {
|
|
148
212
|
const open = [];
|
|
149
|
-
const { rows, skippedLines } = parseRows(lines);
|
|
213
|
+
const { rows, skippedLines, format } = parseRows(lines);
|
|
214
|
+
const isPi = format === "pi";
|
|
215
|
+
let injectedPrompts = 0;
|
|
150
216
|
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
|
+
}
|
|
151
224
|
const prompt = promptText(row);
|
|
152
225
|
if (prompt !== undefined) {
|
|
153
226
|
open.push({ start: ts, prompt, ...newAcc(ts) });
|
|
@@ -161,7 +234,7 @@ function buildTimeline(lines, group) {
|
|
|
161
234
|
const next = open[i + 1]?.start;
|
|
162
235
|
const idle = next === undefined ? 0 : Math.max(0, next - seg.last);
|
|
163
236
|
const oneLine = seg.prompt.replace(/\s+/g, " ").trim();
|
|
164
|
-
|
|
237
|
+
const span = {
|
|
165
238
|
index: i + 1,
|
|
166
239
|
start: new Date(seg.start).toISOString(),
|
|
167
240
|
prompt: oneLine.length > PROMPT_EXCERPT ? `${oneLine.slice(0, PROMPT_EXCERPT)}\u2026` : oneLine,
|
|
@@ -169,16 +242,19 @@ function buildTimeline(lines, group) {
|
|
|
169
242
|
waitMs: idle + seg.askMs,
|
|
170
243
|
toolCalls: seg.toolIds.size,
|
|
171
244
|
tokens: sumTokens([...seg.messages.values()])
|
|
172
|
-
}
|
|
245
|
+
};
|
|
246
|
+
return render(isPi ? { ...span, compactions: seg.compactions } : span);
|
|
173
247
|
});
|
|
174
248
|
const first = open[0];
|
|
175
249
|
const last = open.at(-1);
|
|
176
250
|
const elapsedMs = first && last ? last.last - first.start : 0;
|
|
251
|
+
const totals = { ...sumSpans(segments), elapsedMs, elapsed: formatDuration(elapsedMs) };
|
|
177
252
|
const timeline = {
|
|
178
253
|
available: true,
|
|
179
254
|
segments,
|
|
180
|
-
totals: render({ ...
|
|
181
|
-
skippedLines
|
|
255
|
+
totals: render(isPi ? { ...totals, compactions: segments.reduce((n, s) => n + (s.compactions ?? 0), 0) } : totals),
|
|
256
|
+
skippedLines,
|
|
257
|
+
...isPi ? { injectedPrompts } : {}
|
|
182
258
|
};
|
|
183
259
|
if (group) {
|
|
184
260
|
timeline.stages = parseGroups(group, segments.length).map(([a, b]) => render({ ...sumSpans(segments.slice(a - 1, b)), segments: a === b ? `${a}` : `${a}-${b}` }));
|
|
@@ -186,6 +262,20 @@ function buildTimeline(lines, group) {
|
|
|
186
262
|
return timeline;
|
|
187
263
|
}
|
|
188
264
|
var SESSION_TIMELINE_USAGE = 'usage: session-timeline [--transcript <path>] [--group "1-3,4,..."]';
|
|
265
|
+
function zeroSegmentReason(rows) {
|
|
266
|
+
const format = sniffFormat(rows.map(([r]) => r));
|
|
267
|
+
if (format === "unknown") {
|
|
268
|
+
const counts = new Map;
|
|
269
|
+
for (const [r] of rows) {
|
|
270
|
+
const kind = typeof r.type === "string" && r.type !== "" ? r.type : "(untyped)";
|
|
271
|
+
counts.set(kind, (counts.get(kind) ?? 0) + 1);
|
|
272
|
+
}
|
|
273
|
+
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
|
+
return `unrecognized transcript format (row types: ${census})`;
|
|
275
|
+
}
|
|
276
|
+
const injected = rows.filter(([r]) => isInjectedPrompt(r)).length;
|
|
277
|
+
return `${format} transcript with no operator prompts (${rows.length} rows, ${injected} injected)`;
|
|
278
|
+
}
|
|
189
279
|
function main(argv, env = getEnvVars(), write = (s) => process.stdout.write(s), projectsRoot, writeErr = (s) => process.stderr.write(s)) {
|
|
190
280
|
let transcript;
|
|
191
281
|
let group;
|
|
@@ -210,8 +300,14 @@ function main(argv, env = getEnvVars(), write = (s) => process.stdout.write(s),
|
|
|
210
300
|
return 0;
|
|
211
301
|
}
|
|
212
302
|
try {
|
|
213
|
-
const
|
|
214
|
-
`)
|
|
303
|
+
const lines = readFileSync(resolved.path, "utf8").split(`
|
|
304
|
+
`);
|
|
305
|
+
const timeline = buildTimeline(lines, group);
|
|
306
|
+
if (timeline.segments.length === 0) {
|
|
307
|
+
write(`${JSON.stringify({ available: false, reason: zeroSegmentReason(parseRows(lines).rows) })}
|
|
308
|
+
`);
|
|
309
|
+
return 0;
|
|
310
|
+
}
|
|
215
311
|
write(`${JSON.stringify({ ...timeline, transcript: resolved.path })}
|
|
216
312
|
`);
|
|
217
313
|
return 0;
|
|
@@ -223,6 +319,7 @@ function main(argv, env = getEnvVars(), write = (s) => process.stdout.write(s),
|
|
|
223
319
|
}
|
|
224
320
|
process.exit(main(process.argv.slice(2)));
|
|
225
321
|
export {
|
|
322
|
+
zeroSegmentReason,
|
|
226
323
|
resolveTranscript,
|
|
227
324
|
parseGroups,
|
|
228
325
|
main,
|
|
@@ -16,11 +16,14 @@ import {
|
|
|
16
16
|
accumulate,
|
|
17
17
|
formatDuration,
|
|
18
18
|
formatTokenSplit,
|
|
19
|
+
isInjectedPrompt,
|
|
19
20
|
newAcc,
|
|
20
21
|
parseRows,
|
|
21
22
|
promptText,
|
|
23
|
+
type Row,
|
|
22
24
|
resolveTranscript,
|
|
23
25
|
type Span,
|
|
26
|
+
sniffFormat,
|
|
24
27
|
sumSpans,
|
|
25
28
|
sumTokens,
|
|
26
29
|
} from '../lib/transcript';
|
|
@@ -33,10 +36,12 @@ export type Rendered<T> = T & { work: string; wait: string; token: string };
|
|
|
33
36
|
export interface Timeline {
|
|
34
37
|
available: true;
|
|
35
38
|
transcript?: string;
|
|
36
|
-
segments: Rendered<Span & { index: number; start: string; prompt: string }>[];
|
|
39
|
+
segments: Rendered<Span & { index: number; start: string; prompt: string; compactions?: number }>[];
|
|
37
40
|
stages?: Rendered<Span & { segments: string }>[];
|
|
38
|
-
totals: Rendered<Span & { elapsedMs: number; elapsed: string }>;
|
|
41
|
+
totals: Rendered<Span & { elapsedMs: number; elapsed: string; compactions?: number }>;
|
|
39
42
|
skippedLines: number;
|
|
43
|
+
/** pi only (1138 R2): skill-injected user bodies counted as activity, never as segments. */
|
|
44
|
+
injectedPrompts?: number;
|
|
40
45
|
}
|
|
41
46
|
|
|
42
47
|
const PROMPT_EXCERPT = 80;
|
|
@@ -68,9 +73,19 @@ interface Open extends Acc {
|
|
|
68
73
|
|
|
69
74
|
export function buildTimeline(lines: string[], group?: string): Timeline {
|
|
70
75
|
const open: Open[] = [];
|
|
71
|
-
const { rows, skippedLines } = parseRows(lines);
|
|
76
|
+
const { rows, skippedLines, format } = parseRows(lines);
|
|
77
|
+
const isPi = format === 'pi';
|
|
78
|
+
let injectedPrompts = 0;
|
|
72
79
|
|
|
73
80
|
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
|
+
}
|
|
74
89
|
const prompt = promptText(row);
|
|
75
90
|
if (prompt !== undefined) {
|
|
76
91
|
open.push({ start: ts, prompt, ...newAcc(ts) });
|
|
@@ -80,29 +95,37 @@ export function buildTimeline(lines: string[], group?: string): Timeline {
|
|
|
80
95
|
if (seg) accumulate(seg, row, ts); // rows before the first prompt are session preamble
|
|
81
96
|
}
|
|
82
97
|
|
|
83
|
-
const segments = open.map(
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
98
|
+
const segments = open.map(
|
|
99
|
+
(seg, i): Rendered<Span & { index: number; start: string; prompt: string; compactions?: number }> => {
|
|
100
|
+
const next = open[i + 1]?.start;
|
|
101
|
+
const idle = next === undefined ? 0 : Math.max(0, next - seg.last);
|
|
102
|
+
const oneLine = seg.prompt.replace(/\s+/g, ' ').trim();
|
|
103
|
+
const span = {
|
|
104
|
+
index: i + 1,
|
|
105
|
+
start: new Date(seg.start).toISOString(),
|
|
106
|
+
prompt: oneLine.length > PROMPT_EXCERPT ? `${oneLine.slice(0, PROMPT_EXCERPT)}…` : oneLine,
|
|
107
|
+
workMs: seg.last - seg.start - seg.askMs,
|
|
108
|
+
waitMs: idle + seg.askMs,
|
|
109
|
+
toolCalls: seg.toolIds.size,
|
|
110
|
+
tokens: sumTokens([...seg.messages.values()]),
|
|
111
|
+
};
|
|
112
|
+
return render(isPi ? { ...span, compactions: seg.compactions } : span);
|
|
113
|
+
},
|
|
114
|
+
);
|
|
97
115
|
|
|
98
116
|
const first = open[0];
|
|
99
117
|
const last = open.at(-1);
|
|
100
118
|
const elapsedMs = first && last ? last.last - first.start : 0;
|
|
119
|
+
const totals = { ...sumSpans(segments), elapsedMs, elapsed: formatDuration(elapsedMs) };
|
|
101
120
|
const timeline: Timeline = {
|
|
102
121
|
available: true,
|
|
103
122
|
segments,
|
|
104
|
-
totals: render(
|
|
123
|
+
totals: render(
|
|
124
|
+
isPi ? { ...totals, compactions: segments.reduce((n, s) => n + (s.compactions ?? 0), 0) } : totals,
|
|
125
|
+
),
|
|
105
126
|
skippedLines,
|
|
127
|
+
// Emitted only for pi, as `compactions` is, so Claude output is unchanged (1138 R2).
|
|
128
|
+
...(isPi ? { injectedPrompts } : {}),
|
|
106
129
|
};
|
|
107
130
|
if (group) {
|
|
108
131
|
timeline.stages = parseGroups(group, segments.length).map(([a, b]) =>
|
|
@@ -114,6 +137,30 @@ export function buildTimeline(lines: string[], group?: string): Timeline {
|
|
|
114
137
|
|
|
115
138
|
export const SESSION_TIMELINE_USAGE = 'usage: session-timeline [--transcript <path>] [--group "1-3,4,..."]';
|
|
116
139
|
|
|
140
|
+
/**
|
|
141
|
+
* 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.
|
|
144
|
+
*/
|
|
145
|
+
export function zeroSegmentReason(rows: [Row, number][]): string {
|
|
146
|
+
const format = sniffFormat(rows.map(([r]) => r));
|
|
147
|
+
if (format === 'unknown') {
|
|
148
|
+
const counts = new Map<string, number>();
|
|
149
|
+
for (const [r] of rows) {
|
|
150
|
+
const kind = typeof r.type === 'string' && r.type !== '' ? r.type : '(untyped)';
|
|
151
|
+
counts.set(kind, (counts.get(kind) ?? 0) + 1);
|
|
152
|
+
}
|
|
153
|
+
const census = [...counts.entries()]
|
|
154
|
+
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
|
155
|
+
.slice(0, 5)
|
|
156
|
+
.map(([kind, n]) => `${kind} ${n}`)
|
|
157
|
+
.join(', ');
|
|
158
|
+
return `unrecognized transcript format (row types: ${census})`;
|
|
159
|
+
}
|
|
160
|
+
const injected = rows.filter(([r]) => isInjectedPrompt(r)).length;
|
|
161
|
+
return `${format} transcript with no operator prompts (${rows.length} rows, ${injected} injected)`;
|
|
162
|
+
}
|
|
163
|
+
|
|
117
164
|
export function main(
|
|
118
165
|
argv: string[],
|
|
119
166
|
env: Record<string, string | undefined> = getEnvVars(),
|
|
@@ -140,7 +187,12 @@ export function main(
|
|
|
140
187
|
return 0;
|
|
141
188
|
}
|
|
142
189
|
try {
|
|
143
|
-
const
|
|
190
|
+
const lines = readFileSync(resolved.path, 'utf8').split('\n');
|
|
191
|
+
const timeline = buildTimeline(lines, group);
|
|
192
|
+
if (timeline.segments.length === 0) {
|
|
193
|
+
write(`${JSON.stringify({ available: false, reason: zeroSegmentReason(parseRows(lines).rows) })}\n`);
|
|
194
|
+
return 0;
|
|
195
|
+
}
|
|
144
196
|
write(`${JSON.stringify({ ...timeline, transcript: resolved.path })}\n`);
|
|
145
197
|
return 0;
|
|
146
198
|
} catch (error) {
|