@gobing-ai/spur 0.4.0 → 0.4.2
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/config.global.yaml +4 -2
- 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/templates/AGENTS.md +1 -1
- 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 +107 -14
- package/package.json +10 -10
- package/plugins/sp/README.md +12 -3
- package/plugins/sp/agents/super-planner.md +16 -6
- 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-plan.md +6 -0
- package/plugins/sp/commands/dev-review-session.md +4 -1
- package/plugins/sp/commands/dev-run.md +12 -2
- package/plugins/sp/commands/dev-runall.md +32 -15
- package/plugins/sp/hooks/context-owner.ts +56 -0
- package/plugins/sp/hooks/context-post-tool.ts +23 -0
- package/plugins/sp/hooks/context-session-start.ts +49 -27
- package/plugins/sp/hooks/context-session-stop.ts +131 -30
- package/plugins/sp/hooks/pi/guard-extension.ts +21 -8
- package/plugins/sp/lib/idea-handoff.generated.mjs +43 -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 +351 -46
- package/plugins/sp/lib/plan-projection.generated.d.mts +47 -0
- package/plugins/sp/lib/plan-projection.generated.mjs +368 -0
- package/plugins/sp/lib/quality-gate.generated.d.mts +35 -1
- package/plugins/sp/lib/quality-gate.generated.mjs +30296 -71
- 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 +150 -7
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/references/roles.md +1 -1
- package/plugins/sp/scripts/batch-plan.mjs +488 -0
- package/plugins/sp/scripts/batch-plan.ts +145 -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 +562 -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/pr-reviewing.mjs +19 -5
- package/plugins/sp/scripts/pr-reviewing.ts +34 -9
- package/plugins/sp/scripts/quality-gate.mjs +30213 -61
- 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 +432 -14
- package/plugins/sp/scripts/run-summary.ts +41 -177
- package/plugins/sp/scripts/session-timeline.mjs +109 -11
- package/plugins/sp/scripts/session-timeline.ts +57 -19
- package/plugins/sp/scripts/task-diffstat.mjs +32 -29
- package/plugins/sp/scripts/task-diffstat.ts +34 -34
- 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/indexed-context/SKILL.md +14 -7
- package/plugins/sp/skills/parallel-execution/SKILL.md +2 -2
- package/plugins/sp/skills/parallel-execution/references/dispatch-surface.md +1 -1
- package/plugins/sp/skills/parallel-execution/references/fan-out-patterns.md +2 -2
- package/plugins/sp/skills/pr-reviewing/SKILL.md +7 -3
- package/plugins/sp/skills/session-review/SKILL.md +40 -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 +16 -8
- package/plugins/sp/skills/spur-cli/references/message.md +7 -7
- package/plugins/sp/skills/spur-cli/references/projects.md +7 -5
- 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 +20 -14
- package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +33 -3
- package/plugins/sp/skills/spur-dev/references/cross-cutting.md +49 -14
- package/plugins/sp/skills/spur-dev/references/dev-operations.md +37 -17
- package/plugins/sp/skills/spur-dev/references/execution-batch-continuation.md +68 -0
- package/plugins/sp/skills/spur-dev/references/execution-batch-report.md +199 -0
- package/plugins/sp/skills/spur-dev/references/execution-batch.md +156 -1024
- package/plugins/sp/skills/spur-dev/references/execution-parallel-isolation.md +133 -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 +44 -12
- package/plugins/sp/skills/spur-dev/references/glossary.md +2 -2
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +340 -165
- 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 +42402 -26889
- package/web/_astro/{BoardApp.VeoxxLBy.js → BoardApp.B3GQZvj8.js} +1 -1
- package/web/_astro/BoardApp.BT7L2kSb.js +324 -0
- package/web/_astro/TaskDetail.Cxov1_N_.js +1 -0
- package/web/_astro/{arc.f4e0OYLg.js → arc.vaT7tPOB.js} +1 -1
- package/web/_astro/{architectureDiagram-3BPJPVTR.SSIvxi-Z.js → architectureDiagram-3BPJPVTR.DRMmTjkN.js} +1 -1
- package/web/_astro/{blockDiagram-GPEHLZMM.DSXIHOFf.js → blockDiagram-GPEHLZMM.cla0LO_q.js} +1 -1
- package/web/_astro/{c4Diagram-AAUBKEIU.DG9Ubu85.js → c4Diagram-AAUBKEIU.D4YAIBKn.js} +1 -1
- package/web/_astro/channel.D4nG9YLf.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.C7QU88Qh.js → chunk-2J33WTMH.BrSJbMRZ.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.26Z94xBj.js → chunk-4BX2VUAB.CuLMgKF5.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.DYRiBkqw.js → chunk-55IACEB6.zkvgu54F.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.D5Vfv5pe.js → chunk-727SXJPM.DxWkACh8.js} +1 -1
- package/web/_astro/{chunk-AQP2D5EJ.DXHV47bN.js → chunk-AQP2D5EJ.42XrmIlH.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.D1suAfOs.js → chunk-FMBD7UC4.Bao2wMK9.js} +1 -1
- package/web/_astro/{chunk-ND2GUHAM.D6BmdHEY.js → chunk-ND2GUHAM.BhydojD1.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.rnPU-Ak-.js → chunk-QZHKN3VN.CmZ82g-H.js} +1 -1
- package/web/_astro/{classDiagram-4FO5ZUOK.CvEfXIo1.js → classDiagram-4FO5ZUOK.Blr83Gt_.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.CvEfXIo1.js → classDiagram-v2-Q7XG4LA2.Blr83Gt_.js} +1 -1
- package/web/_astro/{cose-bilkent-S5V4N54A.DkMisLSm.js → cose-bilkent-S5V4N54A.qmfv0SPn.js} +1 -1
- package/web/_astro/{cynefin-OW5HDTMX.C77Q-pF6.js → cynefin-OW5HDTMX.BtELhXPR.js} +1 -1
- package/web/_astro/{dagre-BM42HDAG.YaV8ce27.js → dagre-BM42HDAG.AWmaAvXo.js} +1 -1
- package/web/_astro/{diagram-2AECGRRQ.CUyGEKge.js → diagram-2AECGRRQ.DdH34KDi.js} +1 -1
- package/web/_astro/{diagram-5GNKFQAL.CRgd8dgj.js → diagram-5GNKFQAL.BmNPix1k.js} +1 -1
- package/web/_astro/{diagram-KO2AKTUF.Bmzys9hq.js → diagram-KO2AKTUF.BNQd_Ppo.js} +1 -1
- package/web/_astro/{diagram-LMA3HP47.BvmJY4Lw.js → diagram-LMA3HP47.BAPgDi0e.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.0hEhD30d.js → diagram-OG6HWLK6.DZ4zyLdC.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35.Bjt8lrnm.js → erDiagram-TEJ5UH35.Q1TySMmw.js} +1 -1
- package/web/_astro/{flowDiagram-I6XJVG4X.CJZTjRDV.js → flowDiagram-I6XJVG4X.BXhFBicG.js} +1 -1
- package/web/_astro/{ganttDiagram-6RSMTGT7._VqoVafc.js → ganttDiagram-6RSMTGT7.vHSgcuo2.js} +1 -1
- package/web/_astro/{gitGraphDiagram-PVQCEYII.CEDb7tVo.js → gitGraphDiagram-PVQCEYII.Bi1f73C7.js} +1 -1
- package/web/_astro/index.BhSwN8yY.css +1 -0
- package/web/_astro/{infoDiagram-5YYISTIA.CgwifPpK.js → infoDiagram-5YYISTIA.DPvXZEWm.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.B99wx2J1.js → ishikawaDiagram-YF4QCWOH.Cz829fsV.js} +1 -1
- package/web/_astro/{journeyDiagram-JHISSGLW.2hRwJbEo.js → journeyDiagram-JHISSGLW.enRnB9Me.js} +1 -1
- package/web/_astro/{kanban-definition-UN3LZRKU.BahJkKNC.js → kanban-definition-UN3LZRKU.DnTNk6d2.js} +1 -1
- package/web/_astro/{linear.DguUUe6V.js → linear.BzGnBwNL.js} +1 -1
- package/web/_astro/{mermaid.core.CvgfmqVr.js → mermaid.core.CSbTpIVz.js} +4 -4
- package/web/_astro/{mindmap-definition-RKZ34NQL.B3qEA3x9.js → mindmap-definition-RKZ34NQL.DGJDy8zT.js} +1 -1
- package/web/_astro/{pieDiagram-4H26LBE5.FlWA-_ru.js → pieDiagram-4H26LBE5.HSPdi5_8.js} +1 -1
- package/web/_astro/{quadrantDiagram-W4KKPZXB.9bVn35X7.js → quadrantDiagram-W4KKPZXB.6Gv8mz2Y.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.ChykIhct.js → requirementDiagram-4Y6WPE33.CnBCtFwa.js} +1 -1
- package/web/_astro/{sankeyDiagram-5OEKKPKP.Z2Y3hkYb.js → sankeyDiagram-5OEKKPKP.5M8VWAu8.js} +1 -1
- package/web/_astro/{sequenceDiagram-3UESZ5HK.J7JsHWDt.js → sequenceDiagram-3UESZ5HK.DN41XN-U.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.DAPbiCSS.js → stateDiagram-AJRCARHV.ErOO3W8j.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.D2jvSi0q.js → stateDiagram-v2-BHNVJYJU.B_ZHICM-.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.psW2ci29.js → timeline-definition-PNZ67QCA.CWdosr07.js} +1 -1
- package/web/_astro/{vennDiagram-CIIHVFJN.DlILYVmg.js → vennDiagram-CIIHVFJN.BHzYjNV5.js} +1 -1
- package/web/_astro/{wardleyDiagram-YWT4CUSO.xNWwLJnn.js → wardleyDiagram-YWT4CUSO.h8x4IwPX.js} +1 -1
- package/web/_astro/{xychartDiagram-2RQKCTM6.EU_BuH7V.js → xychartDiagram-2RQKCTM6.RTapW2X8.js} +1 -1
- package/web/index.html +2 -2
- package/plugins/sp/commands/dev-parallel.md +0 -31
- package/web/_astro/BoardApp.Bzr4OXrW.js +0 -324
- package/web/_astro/TaskDetail.LAyR_Pvn.js +0 -1
- 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,20 @@ function formatTokenSplit(t) {
|
|
|
35
36
|
return `${formatTokens(nonCached + t.cacheRead)} / ${formatTokens(nonCached)}`;
|
|
36
37
|
}
|
|
37
38
|
function promptText(row) {
|
|
39
|
+
if (row.type === "custom_message") {
|
|
40
|
+
if (row.display === false)
|
|
41
|
+
return;
|
|
42
|
+
const text = typeof row.content === "string" ? row.content.trim() : "";
|
|
43
|
+
return text === "" ? undefined : text;
|
|
44
|
+
}
|
|
45
|
+
if (row.type === "message") {
|
|
46
|
+
if (row.message?.role !== "user")
|
|
47
|
+
return;
|
|
48
|
+
const text = firstTextContent(row);
|
|
49
|
+
if (text === undefined)
|
|
50
|
+
return;
|
|
51
|
+
return skillInvocationPrompt(text) ?? text;
|
|
52
|
+
}
|
|
38
53
|
if (row.type !== "user" || row.isMeta || row.isCompactSummary)
|
|
39
54
|
return;
|
|
40
55
|
const content = row.message?.content;
|
|
@@ -44,6 +59,22 @@ function promptText(row) {
|
|
|
44
59
|
return;
|
|
45
60
|
return content.find((b) => b.type === "text")?.text;
|
|
46
61
|
}
|
|
62
|
+
function firstTextContent(row) {
|
|
63
|
+
const content = row.message?.content;
|
|
64
|
+
if (typeof content === "string")
|
|
65
|
+
return content;
|
|
66
|
+
if (!Array.isArray(content))
|
|
67
|
+
return;
|
|
68
|
+
return content.find((b) => b.type === "text")?.text;
|
|
69
|
+
}
|
|
70
|
+
function skillInvocationPrompt(text) {
|
|
71
|
+
const name = /^\s*<skill name="([^"]+)"/.exec(text)?.[1];
|
|
72
|
+
if (name === undefined)
|
|
73
|
+
return;
|
|
74
|
+
const close = text.lastIndexOf("</skill>");
|
|
75
|
+
const args = close < 0 ? "" : text.slice(close + "</skill>".length).trim();
|
|
76
|
+
return args === "" ? `/${name}` : `/${name} ${args}`;
|
|
77
|
+
}
|
|
47
78
|
function sumTokens(all) {
|
|
48
79
|
const total = zeroTokens();
|
|
49
80
|
for (const t of all)
|
|
@@ -64,10 +95,37 @@ var newAcc = (ts) => ({
|
|
|
64
95
|
askMs: 0,
|
|
65
96
|
asks: new Map,
|
|
66
97
|
toolIds: new Set,
|
|
67
|
-
messages: new Map
|
|
98
|
+
messages: new Map,
|
|
99
|
+
compactions: 0
|
|
68
100
|
});
|
|
69
101
|
function accumulate(acc, row, ts) {
|
|
70
102
|
acc.last = Math.max(acc.last, ts);
|
|
103
|
+
if (row.type === "message") {
|
|
104
|
+
const m = row.message;
|
|
105
|
+
if (m?.role === "assistant") {
|
|
106
|
+
const blocks2 = Array.isArray(m.content) ? m.content : [];
|
|
107
|
+
for (const b of blocks2) {
|
|
108
|
+
if (b.type === "toolCall" && b.id) {
|
|
109
|
+
acc.toolIds.add(b.id);
|
|
110
|
+
if (b.name && OPERATOR_TOOLS.has(b.name))
|
|
111
|
+
acc.asks.set(b.id, ts);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const u2 = m.usage;
|
|
115
|
+
if (u2 && row.id)
|
|
116
|
+
acc.messages.set(row.id, {
|
|
117
|
+
input: u2.input ?? 0,
|
|
118
|
+
cacheCreate: u2.cacheWrite ?? 0,
|
|
119
|
+
cacheRead: u2.cacheRead ?? 0,
|
|
120
|
+
output: u2.output ?? 0
|
|
121
|
+
});
|
|
122
|
+
} else if (m?.role === "toolResult" && m.toolCallId && acc.asks.has(m.toolCallId)) {
|
|
123
|
+
acc.askMs += ts - (acc.asks.get(m.toolCallId) ?? ts);
|
|
124
|
+
}
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (row.type === "compaction")
|
|
128
|
+
acc.compactions++;
|
|
71
129
|
const blocks = Array.isArray(row.message?.content) ? row.message.content : [];
|
|
72
130
|
for (const b of blocks) {
|
|
73
131
|
if (b.type === "tool_use" && b.id) {
|
|
@@ -88,6 +146,15 @@ function accumulate(acc, row, ts) {
|
|
|
88
146
|
});
|
|
89
147
|
}
|
|
90
148
|
}
|
|
149
|
+
function sniffFormat(rows) {
|
|
150
|
+
for (const row of rows) {
|
|
151
|
+
if (row.type === "message" && PI_ROLES.has(row.message?.role ?? ""))
|
|
152
|
+
return "pi";
|
|
153
|
+
if (row.type === "user" || row.type === "assistant")
|
|
154
|
+
return "claude";
|
|
155
|
+
}
|
|
156
|
+
return "unknown";
|
|
157
|
+
}
|
|
91
158
|
function parseRows(lines) {
|
|
92
159
|
const rows = [];
|
|
93
160
|
let skippedLines = 0;
|
|
@@ -105,14 +172,22 @@ function parseRows(lines) {
|
|
|
105
172
|
if (!Number.isNaN(ts))
|
|
106
173
|
rows.push([row, ts]);
|
|
107
174
|
}
|
|
108
|
-
return { rows, skippedLines };
|
|
175
|
+
return { rows, skippedLines, format: sniffFormat(rows.map(([r]) => r)) };
|
|
109
176
|
}
|
|
110
177
|
function resolveTranscript(env, projectsRoot = join(homedir(), ".claude", "projects"), override) {
|
|
111
178
|
if (override)
|
|
112
179
|
return existsSync(override) ? { ok: true, path: override } : { ok: false, reason: "no transcript" };
|
|
113
180
|
const id = env.CLAUDE_CODE_SESSION_ID;
|
|
114
|
-
if (!id)
|
|
115
|
-
|
|
181
|
+
if (!id) {
|
|
182
|
+
const piFile = env.PI_SESSION_FILE;
|
|
183
|
+
if (piFile !== undefined && piFile !== "") {
|
|
184
|
+
return existsSync(piFile) ? { ok: true, path: piFile } : { ok: false, reason: `PI_SESSION_FILE ${piFile} does not exist` };
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
ok: false,
|
|
188
|
+
reason: "no host session id; pass --transcript <path> (pi: ~/.pi/agent/sessions/<cwd-slug>/<file>.jsonl, " + "or set PI_SESSION_FILE)"
|
|
189
|
+
};
|
|
190
|
+
}
|
|
116
191
|
if (!SESSION_ID.test(id))
|
|
117
192
|
return { ok: false, reason: "refusing a session id with path characters" };
|
|
118
193
|
if (!existsSync(projectsRoot))
|
|
@@ -146,7 +221,8 @@ function parseGroups(spec, count) {
|
|
|
146
221
|
}
|
|
147
222
|
function buildTimeline(lines, group) {
|
|
148
223
|
const open = [];
|
|
149
|
-
const { rows, skippedLines } = parseRows(lines);
|
|
224
|
+
const { rows, skippedLines, format } = parseRows(lines);
|
|
225
|
+
const isPi = format === "pi";
|
|
150
226
|
for (const [row, ts] of rows) {
|
|
151
227
|
const prompt = promptText(row);
|
|
152
228
|
if (prompt !== undefined) {
|
|
@@ -161,7 +237,7 @@ function buildTimeline(lines, group) {
|
|
|
161
237
|
const next = open[i + 1]?.start;
|
|
162
238
|
const idle = next === undefined ? 0 : Math.max(0, next - seg.last);
|
|
163
239
|
const oneLine = seg.prompt.replace(/\s+/g, " ").trim();
|
|
164
|
-
|
|
240
|
+
const span = {
|
|
165
241
|
index: i + 1,
|
|
166
242
|
start: new Date(seg.start).toISOString(),
|
|
167
243
|
prompt: oneLine.length > PROMPT_EXCERPT ? `${oneLine.slice(0, PROMPT_EXCERPT)}\u2026` : oneLine,
|
|
@@ -169,15 +245,17 @@ function buildTimeline(lines, group) {
|
|
|
169
245
|
waitMs: idle + seg.askMs,
|
|
170
246
|
toolCalls: seg.toolIds.size,
|
|
171
247
|
tokens: sumTokens([...seg.messages.values()])
|
|
172
|
-
}
|
|
248
|
+
};
|
|
249
|
+
return render(isPi ? { ...span, compactions: seg.compactions } : span);
|
|
173
250
|
});
|
|
174
251
|
const first = open[0];
|
|
175
252
|
const last = open.at(-1);
|
|
176
253
|
const elapsedMs = first && last ? last.last - first.start : 0;
|
|
254
|
+
const totals = { ...sumSpans(segments), elapsedMs, elapsed: formatDuration(elapsedMs) };
|
|
177
255
|
const timeline = {
|
|
178
256
|
available: true,
|
|
179
257
|
segments,
|
|
180
|
-
totals: render({ ...
|
|
258
|
+
totals: render(isPi ? { ...totals, compactions: segments.reduce((n, s) => n + (s.compactions ?? 0), 0) } : totals),
|
|
181
259
|
skippedLines
|
|
182
260
|
};
|
|
183
261
|
if (group) {
|
|
@@ -186,6 +264,19 @@ function buildTimeline(lines, group) {
|
|
|
186
264
|
return timeline;
|
|
187
265
|
}
|
|
188
266
|
var SESSION_TIMELINE_USAGE = 'usage: session-timeline [--transcript <path>] [--group "1-3,4,..."]';
|
|
267
|
+
function zeroSegmentReason(rows) {
|
|
268
|
+
const format = sniffFormat(rows.map(([r]) => r));
|
|
269
|
+
if (format === "unknown") {
|
|
270
|
+
const counts = new Map;
|
|
271
|
+
for (const [r] of rows) {
|
|
272
|
+
const kind = typeof r.type === "string" && r.type !== "" ? r.type : "(untyped)";
|
|
273
|
+
counts.set(kind, (counts.get(kind) ?? 0) + 1);
|
|
274
|
+
}
|
|
275
|
+
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(", ");
|
|
276
|
+
return `unrecognized transcript format (row types: ${census})`;
|
|
277
|
+
}
|
|
278
|
+
return `${format} transcript with no operator prompts (${rows.length} rows)`;
|
|
279
|
+
}
|
|
189
280
|
function main(argv, env = getEnvVars(), write = (s) => process.stdout.write(s), projectsRoot, writeErr = (s) => process.stderr.write(s)) {
|
|
190
281
|
let transcript;
|
|
191
282
|
let group;
|
|
@@ -210,8 +301,14 @@ function main(argv, env = getEnvVars(), write = (s) => process.stdout.write(s),
|
|
|
210
301
|
return 0;
|
|
211
302
|
}
|
|
212
303
|
try {
|
|
213
|
-
const
|
|
214
|
-
`)
|
|
304
|
+
const lines = readFileSync(resolved.path, "utf8").split(`
|
|
305
|
+
`);
|
|
306
|
+
const timeline = buildTimeline(lines, group);
|
|
307
|
+
if (timeline.segments.length === 0) {
|
|
308
|
+
write(`${JSON.stringify({ available: false, reason: zeroSegmentReason(parseRows(lines).rows) })}
|
|
309
|
+
`);
|
|
310
|
+
return 0;
|
|
311
|
+
}
|
|
215
312
|
write(`${JSON.stringify({ ...timeline, transcript: resolved.path })}
|
|
216
313
|
`);
|
|
217
314
|
return 0;
|
|
@@ -223,6 +320,7 @@ function main(argv, env = getEnvVars(), write = (s) => process.stdout.write(s),
|
|
|
223
320
|
}
|
|
224
321
|
process.exit(main(process.argv.slice(2)));
|
|
225
322
|
export {
|
|
323
|
+
zeroSegmentReason,
|
|
226
324
|
resolveTranscript,
|
|
227
325
|
parseGroups,
|
|
228
326
|
main,
|
|
@@ -19,8 +19,10 @@ import {
|
|
|
19
19
|
newAcc,
|
|
20
20
|
parseRows,
|
|
21
21
|
promptText,
|
|
22
|
+
type Row,
|
|
22
23
|
resolveTranscript,
|
|
23
24
|
type Span,
|
|
25
|
+
sniffFormat,
|
|
24
26
|
sumSpans,
|
|
25
27
|
sumTokens,
|
|
26
28
|
} from '../lib/transcript';
|
|
@@ -33,9 +35,9 @@ export type Rendered<T> = T & { work: string; wait: string; token: string };
|
|
|
33
35
|
export interface Timeline {
|
|
34
36
|
available: true;
|
|
35
37
|
transcript?: string;
|
|
36
|
-
segments: Rendered<Span & { index: number; start: string; prompt: string }>[];
|
|
38
|
+
segments: Rendered<Span & { index: number; start: string; prompt: string; compactions?: number }>[];
|
|
37
39
|
stages?: Rendered<Span & { segments: string }>[];
|
|
38
|
-
totals: Rendered<Span & { elapsedMs: number; elapsed: string }>;
|
|
40
|
+
totals: Rendered<Span & { elapsedMs: number; elapsed: string; compactions?: number }>;
|
|
39
41
|
skippedLines: number;
|
|
40
42
|
}
|
|
41
43
|
|
|
@@ -68,7 +70,8 @@ interface Open extends Acc {
|
|
|
68
70
|
|
|
69
71
|
export function buildTimeline(lines: string[], group?: string): Timeline {
|
|
70
72
|
const open: Open[] = [];
|
|
71
|
-
const { rows, skippedLines } = parseRows(lines);
|
|
73
|
+
const { rows, skippedLines, format } = parseRows(lines);
|
|
74
|
+
const isPi = format === 'pi';
|
|
72
75
|
|
|
73
76
|
for (const [row, ts] of rows) {
|
|
74
77
|
const prompt = promptText(row);
|
|
@@ -80,28 +83,34 @@ export function buildTimeline(lines: string[], group?: string): Timeline {
|
|
|
80
83
|
if (seg) accumulate(seg, row, ts); // rows before the first prompt are session preamble
|
|
81
84
|
}
|
|
82
85
|
|
|
83
|
-
const segments = open.map(
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
86
|
+
const segments = open.map(
|
|
87
|
+
(seg, i): Rendered<Span & { index: number; start: string; prompt: string; compactions?: number }> => {
|
|
88
|
+
const next = open[i + 1]?.start;
|
|
89
|
+
const idle = next === undefined ? 0 : Math.max(0, next - seg.last);
|
|
90
|
+
const oneLine = seg.prompt.replace(/\s+/g, ' ').trim();
|
|
91
|
+
const span = {
|
|
92
|
+
index: i + 1,
|
|
93
|
+
start: new Date(seg.start).toISOString(),
|
|
94
|
+
prompt: oneLine.length > PROMPT_EXCERPT ? `${oneLine.slice(0, PROMPT_EXCERPT)}…` : oneLine,
|
|
95
|
+
workMs: seg.last - seg.start - seg.askMs,
|
|
96
|
+
waitMs: idle + seg.askMs,
|
|
97
|
+
toolCalls: seg.toolIds.size,
|
|
98
|
+
tokens: sumTokens([...seg.messages.values()]),
|
|
99
|
+
};
|
|
100
|
+
return render(isPi ? { ...span, compactions: seg.compactions } : span);
|
|
101
|
+
},
|
|
102
|
+
);
|
|
97
103
|
|
|
98
104
|
const first = open[0];
|
|
99
105
|
const last = open.at(-1);
|
|
100
106
|
const elapsedMs = first && last ? last.last - first.start : 0;
|
|
107
|
+
const totals = { ...sumSpans(segments), elapsedMs, elapsed: formatDuration(elapsedMs) };
|
|
101
108
|
const timeline: Timeline = {
|
|
102
109
|
available: true,
|
|
103
110
|
segments,
|
|
104
|
-
totals: render(
|
|
111
|
+
totals: render(
|
|
112
|
+
isPi ? { ...totals, compactions: segments.reduce((n, s) => n + (s.compactions ?? 0), 0) } : totals,
|
|
113
|
+
),
|
|
105
114
|
skippedLines,
|
|
106
115
|
};
|
|
107
116
|
if (group) {
|
|
@@ -114,6 +123,30 @@ export function buildTimeline(lines: string[], group?: string): Timeline {
|
|
|
114
123
|
|
|
115
124
|
export const SESSION_TIMELINE_USAGE = 'usage: session-timeline [--transcript <path>] [--group "1-3,4,..."]';
|
|
116
125
|
|
|
126
|
+
/**
|
|
127
|
+
* 1138 R3: a zero-segment result must say WHAT was detected, so an unparseable-but-recognised
|
|
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.)
|
|
131
|
+
*/
|
|
132
|
+
export function zeroSegmentReason(rows: [Row, number][]): string {
|
|
133
|
+
const format = sniffFormat(rows.map(([r]) => r));
|
|
134
|
+
if (format === 'unknown') {
|
|
135
|
+
const counts = new Map<string, number>();
|
|
136
|
+
for (const [r] of rows) {
|
|
137
|
+
const kind = typeof r.type === 'string' && r.type !== '' ? r.type : '(untyped)';
|
|
138
|
+
counts.set(kind, (counts.get(kind) ?? 0) + 1);
|
|
139
|
+
}
|
|
140
|
+
const census = [...counts.entries()]
|
|
141
|
+
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
|
142
|
+
.slice(0, 5)
|
|
143
|
+
.map(([kind, n]) => `${kind} ${n}`)
|
|
144
|
+
.join(', ');
|
|
145
|
+
return `unrecognized transcript format (row types: ${census})`;
|
|
146
|
+
}
|
|
147
|
+
return `${format} transcript with no operator prompts (${rows.length} rows)`;
|
|
148
|
+
}
|
|
149
|
+
|
|
117
150
|
export function main(
|
|
118
151
|
argv: string[],
|
|
119
152
|
env: Record<string, string | undefined> = getEnvVars(),
|
|
@@ -140,7 +173,12 @@ export function main(
|
|
|
140
173
|
return 0;
|
|
141
174
|
}
|
|
142
175
|
try {
|
|
143
|
-
const
|
|
176
|
+
const lines = readFileSync(resolved.path, 'utf8').split('\n');
|
|
177
|
+
const timeline = buildTimeline(lines, group);
|
|
178
|
+
if (timeline.segments.length === 0) {
|
|
179
|
+
write(`${JSON.stringify({ available: false, reason: zeroSegmentReason(parseRows(lines).rows) })}\n`);
|
|
180
|
+
return 0;
|
|
181
|
+
}
|
|
144
182
|
write(`${JSON.stringify({ ...timeline, transcript: resolved.path })}\n`);
|
|
145
183
|
return 0;
|
|
146
184
|
} catch (error) {
|