@gobing-ai/spur 0.4.8 → 0.4.10
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/pipeline-budgets.json +2 -2
- package/config/rules/README.md +1 -1
- package/config/rules/boundary/config-loading-ownership.yaml +1 -0
- package/config/rules/boundary/env-var-hygiene.yaml +0 -1
- package/config/rules/boundary/sp-plugin-standalone.yaml +1 -1
- package/config/rules/boundary/sp-script-placement.yaml +3 -3
- package/config/rules/docs/sourced-api-claims.yaml +29 -64
- package/config/rules/strict/runtime-boundaries.yaml +40 -1
- package/config/rules/structure/protected-files.yaml +1 -1
- package/config/rules/surface/check-cli-surface.yaml +33 -0
- package/config/rules/typescript/no-debugger.yaml +3 -0
- package/config/workflow-candidates.json +2 -2
- package/config/workflows/feature-verification.yaml +6 -19
- package/config/workflows/history-anatomy.yaml +14 -28
- package/config/workflows/idea-pipeline.yaml +6 -19
- package/config/workflows/pr-review.yaml +9 -16
- package/config/workflows/task-pipeline.yaml +18 -25
- package/config/workflows/wrapup-pipeline.yaml +21 -63
- package/package.json +3 -3
- package/plugins/sp/README.md +70 -37
- package/plugins/sp/agents/expert-spur.md +0 -1
- package/plugins/sp/agents/super-planner.md +5 -5
- package/plugins/sp/agents/super-reviewer.md +2 -2
- package/plugins/sp/commands/dev-changelog.md +1 -1
- package/plugins/sp/commands/dev-daily.md +1 -1
- package/plugins/sp/commands/dev-pr-review.md +1 -1
- package/plugins/sp/commands/dev-review-session.md +1 -1
- package/plugins/sp/commands/dev-runall.md +4 -4
- package/plugins/sp/commands/dev-verify.md +4 -6
- package/plugins/sp/commands/dev-verifyall.md +4 -5
- package/plugins/sp/hooks/hooks.json +11 -1
- package/plugins/sp/lib/context-post-tool.ts +1 -1
- package/plugins/sp/lib/context-session-start.ts +1 -1
- package/plugins/sp/lib/context-session-stop.ts +1 -1
- package/plugins/sp/lib/destructive-policy.ts +65 -1
- package/plugins/sp/plugin.json +2 -5
- package/plugins/sp/{hooks → scripts/hooks}/agent-lifecycle.ts +1 -1
- package/plugins/sp/scripts/hooks/careful-guard.ts +94 -0
- package/plugins/sp/{hooks → scripts/hooks}/context-post-tool.ts +2 -2
- package/plugins/sp/{hooks → scripts/hooks}/context-session-start.ts +1 -1
- package/plugins/sp/{hooks → scripts/hooks}/context-session-stop.ts +1 -1
- package/plugins/sp/{hooks → scripts/hooks}/fleet-guest-stop.ts +6 -4
- package/plugins/sp/{hooks → scripts/hooks}/task-write-guard.ts +38 -21
- package/plugins/sp/skills/brainstorm/SKILL.md +12 -11
- package/plugins/sp/skills/brainstorm/agents/openai.yaml +1 -1
- package/plugins/sp/skills/brainstorm/examples/ideation-example.md +0 -4
- package/plugins/sp/skills/brainstorm/metadata.openclaw +1 -1
- package/plugins/sp/skills/brainstorm/references/workflows.md +4 -14
- package/plugins/sp/skills/code-implementation/SKILL.md +1 -1
- package/plugins/sp/skills/code-verification/SKILL.md +8 -8
- package/plugins/sp/skills/code-verification/references/secu-review.md +4 -4
- package/plugins/sp/skills/code-verification/references/verdict-schema.md +9 -1
- package/plugins/sp/skills/daily-summary/SKILL.md +4 -5
- package/plugins/sp/skills/dogfood-testing/SKILL.md +9 -9
- package/plugins/sp/skills/dogfood-testing/references/report-template.md +1 -1
- package/plugins/sp/skills/history-anatomy/references/modes.md +2 -2
- package/plugins/sp/skills/history-anatomy/references/operations.md +3 -4
- package/plugins/sp/skills/history-anatomy/references/report-contract.md +2 -3
- package/plugins/sp/skills/issue-finding/SKILL.md +1 -1
- package/plugins/sp/skills/next-router/references/routing-table.md +1 -1
- package/plugins/sp/skills/parallel-execution/SKILL.md +2 -2
- package/plugins/sp/skills/parallel-execution/references/result-synthesis.md +9 -10
- package/plugins/sp/skills/pr-reviewing/SKILL.md +14 -15
- package/plugins/sp/skills/reverse-engineering/SKILL.md +4 -6
- package/plugins/sp/skills/session-review/SKILL.md +13 -29
- package/plugins/sp/skills/source-driven-development/SKILL.md +26 -27
- package/plugins/sp/skills/spur-check/SKILL.md +14 -13
- package/plugins/sp/skills/spur-cli/SKILL.md +1 -1
- package/plugins/sp/skills/spur-cli/references/agent.md +17 -2
- package/plugins/sp/skills/spur-cli/references/builder.md +27 -6
- package/plugins/sp/skills/spur-cli/references/features.md +6 -1
- package/plugins/sp/skills/spur-cli/references/history.md +3 -0
- package/plugins/sp/skills/spur-cli/references/self.md +5 -3
- package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +3 -0
- package/plugins/sp/skills/spur-cli/references/tasks.md +13 -3
- package/plugins/sp/skills/spur-cli/references/workflows/authoring-workflows.md +1 -1
- package/plugins/sp/skills/spur-cli/references/workflows.md +36 -1
- package/plugins/sp/skills/spur-dev/SKILL.md +1 -1
- package/plugins/sp/skills/spur-dev/references/cross-cutting.md +1 -1
- package/plugins/sp/skills/spur-dev/references/dev-operations.md +12 -13
- package/plugins/sp/skills/spur-dev/references/execution-batch-report.md +10 -11
- package/plugins/sp/skills/spur-dev/references/execution-batch.md +16 -17
- package/plugins/sp/skills/spur-dev/references/execution-parallel-isolation.md +1 -1
- package/plugins/sp/skills/spur-dev/references/execution-worktree-landing.md +9 -12
- package/plugins/sp/skills/spur-dev/references/execution-worktree-setup.md +6 -8
- package/plugins/sp/skills/spur-dev/references/gate-checklists.md +1 -1
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +33 -40
- package/plugins/sp/skills/spur-dev/references/structured-trace-emission.md +24 -24
- package/plugins/sp/skills/spur-doctor/SKILL.md +5 -4
- package/spur.js +12726 -1456
- package/web/_astro/{BoardApp.CaNvQBD_.js → BoardApp.B5rTVNd_.js} +1 -1
- package/web/_astro/{BoardApp.C5jnkkKP.js → BoardApp.BMprmPRc.js} +141 -141
- package/web/_astro/{TaskDetail.DGKndhI8.js → TaskDetail.Ddsur8qd.js} +1 -1
- package/web/_astro/{arc.DHrwdg3G.js → arc.DLqVVBxP.js} +1 -1
- package/web/_astro/{architectureDiagram-3BPJPVTR.pvKpK1s5.js → architectureDiagram-3BPJPVTR.C-Pk3uAi.js} +1 -1
- package/web/_astro/{blockDiagram-GPEHLZMM.BQmRcZsS.js → blockDiagram-GPEHLZMM.BRO1i_Nm.js} +1 -1
- package/web/_astro/{c4Diagram-AAUBKEIU.CCLjHeH1.js → c4Diagram-AAUBKEIU.BU_cWu6e.js} +1 -1
- package/web/_astro/channel.Bs3Illhy.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.DL6Yuoma.js → chunk-2J33WTMH.D2VXiQ2t.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.CzlyEI7l.js → chunk-4BX2VUAB.BcHT772J.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.B60EWxp1.js → chunk-55IACEB6.DhDtICmo.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.c7v_fRLo.js → chunk-727SXJPM.BoKHp7gV.js} +1 -1
- package/web/_astro/{chunk-AQP2D5EJ.8BxLWdDc.js → chunk-AQP2D5EJ.Chu4JRpE.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.DmKuxGFT.js → chunk-FMBD7UC4.wHdwKPlq.js} +1 -1
- package/web/_astro/{chunk-ND2GUHAM.1Zwd4mqV.js → chunk-ND2GUHAM.ffN-M1JB.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.BvYLu-A1.js → chunk-QZHKN3VN.FRubwg4S.js} +1 -1
- package/web/_astro/{classDiagram-4FO5ZUOK.CEqRt0WU.js → classDiagram-4FO5ZUOK.c22tQsc_.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.CEqRt0WU.js → classDiagram-v2-Q7XG4LA2.c22tQsc_.js} +1 -1
- package/web/_astro/{cose-bilkent-S5V4N54A.C7JiUdj2.js → cose-bilkent-S5V4N54A.B9c6fiSj.js} +1 -1
- package/web/_astro/{cynefin-OW5HDTMX.B6mZ-1be.js → cynefin-OW5HDTMX.DrZsrUHl.js} +1 -1
- package/web/_astro/{dagre-BM42HDAG.BuryfDy6.js → dagre-BM42HDAG.DG1U4b3y.js} +1 -1
- package/web/_astro/{diagram-2AECGRRQ.dao39YQM.js → diagram-2AECGRRQ.CH15b-K3.js} +1 -1
- package/web/_astro/{diagram-5GNKFQAL.lnGZe4At.js → diagram-5GNKFQAL.BH1j7twR.js} +1 -1
- package/web/_astro/{diagram-KO2AKTUF.uWfhzEfT.js → diagram-KO2AKTUF.q2zCkbAj.js} +1 -1
- package/web/_astro/{diagram-LMA3HP47.ppq8-198.js → diagram-LMA3HP47.BLaaZnHV.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.CKSCqDt5.js → diagram-OG6HWLK6.BcpRdQwI.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35.sFSf2yBN.js → erDiagram-TEJ5UH35.D7KBlqfM.js} +1 -1
- package/web/_astro/{flowDiagram-I6XJVG4X.BJQjKLbz.js → flowDiagram-I6XJVG4X.D_gmuRqg.js} +1 -1
- package/web/_astro/{ganttDiagram-6RSMTGT7.DAHKNj2N.js → ganttDiagram-6RSMTGT7.DzgWYniD.js} +1 -1
- package/web/_astro/{gitGraphDiagram-PVQCEYII.BlmzcW_O.js → gitGraphDiagram-PVQCEYII.576i3hm3.js} +1 -1
- package/web/_astro/index.DiC1a6lh.css +1 -0
- package/web/_astro/{infoDiagram-5YYISTIA.DGhjaqsi.js → infoDiagram-5YYISTIA.DMCclBiz.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.gKIpgt6F.js → ishikawaDiagram-YF4QCWOH.9Kyca4SH.js} +1 -1
- package/web/_astro/{journeyDiagram-JHISSGLW.DG3tmP0R.js → journeyDiagram-JHISSGLW.chbdG-8q.js} +1 -1
- package/web/_astro/{kanban-definition-UN3LZRKU.CgHDSxkR.js → kanban-definition-UN3LZRKU.Bds54-e0.js} +1 -1
- package/web/_astro/{linear.BuGEpcuM.js → linear.D5ND9GN_.js} +1 -1
- package/web/_astro/{mermaid.core.DBVqkWBp.js → mermaid.core.D9N_PGbj.js} +4 -4
- package/web/_astro/{mindmap-definition-RKZ34NQL.D-kOcji9.js → mindmap-definition-RKZ34NQL.t0xop5vp.js} +1 -1
- package/web/_astro/{pieDiagram-4H26LBE5.BvGPwtZt.js → pieDiagram-4H26LBE5.DqgKh6hM.js} +1 -1
- package/web/_astro/{quadrantDiagram-W4KKPZXB.BKb_9FV6.js → quadrantDiagram-W4KKPZXB.DeuHVi2t.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.Cx0fvD8v.js → requirementDiagram-4Y6WPE33.BdYFSG4W.js} +1 -1
- package/web/_astro/{sankeyDiagram-5OEKKPKP.fatULi0W.js → sankeyDiagram-5OEKKPKP.k1LPPETJ.js} +1 -1
- package/web/_astro/{sequenceDiagram-3UESZ5HK.ChHyKqco.js → sequenceDiagram-3UESZ5HK.DdyfpzoG.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.BdUpKWyj.js → stateDiagram-AJRCARHV.DSEI8il0.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.fmJxqIm5.js → stateDiagram-v2-BHNVJYJU.CoMnQRKo.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.BayDn5-V.js → timeline-definition-PNZ67QCA.DM1AHNm-.js} +1 -1
- package/web/_astro/{vennDiagram-CIIHVFJN.Bm2Aggwa.js → vennDiagram-CIIHVFJN.C-cJmVtl.js} +1 -1
- package/web/_astro/{wardleyDiagram-YWT4CUSO.DI75O-uG.js → wardleyDiagram-YWT4CUSO.D0xMzKPb.js} +1 -1
- package/web/_astro/{xychartDiagram-2RQKCTM6.DL90E7Q7.js → xychartDiagram-2RQKCTM6.BH5xJcEq.js} +1 -1
- package/web/index.html +2 -2
- package/config/plugin-scripts.json +0 -116
- package/config/script-placement-baseline.json +0 -33
- package/plugins/sp/hooks/careful-guard.ts +0 -74
- package/plugins/sp/hooks/pi/guard-extension.ts +0 -304
- package/plugins/sp/lib/dogfood-pipeline.ts +0 -265
- package/plugins/sp/lib/dogfood-report.ts +0 -278
- package/plugins/sp/lib/history-anatomy.generated.d.mts +0 -112
- package/plugins/sp/lib/history-anatomy.generated.mjs +0 -691
- package/plugins/sp/lib/idea-handoff.generated.d.mts +0 -17
- package/plugins/sp/lib/idea-handoff.generated.mjs +0 -1446
- package/plugins/sp/lib/inline-run-app.ts +0 -68
- package/plugins/sp/lib/inline-run.generated.d.mts +0 -37
- package/plugins/sp/lib/inline-run.generated.mjs +0 -3103
- package/plugins/sp/lib/plan-projection.generated.d.mts +0 -197
- package/plugins/sp/lib/plan-projection.generated.mjs +0 -982
- package/plugins/sp/lib/quality-gate.generated.d.mts +0 -139
- package/plugins/sp/lib/quality-gate.generated.mjs +0 -30510
- package/plugins/sp/lib/residual-scan.generated.d.mts +0 -93
- package/plugins/sp/lib/residual-scan.generated.mjs +0 -383
- package/plugins/sp/lib/run-summary-core.ts +0 -225
- package/plugins/sp/lib/spur-bin.ts +0 -36
- package/plugins/sp/lib/step-profile.generated.d.mts +0 -71
- package/plugins/sp/lib/step-profile.generated.mjs +0 -174
- package/plugins/sp/lib/transcript.ts +0 -112
- package/plugins/sp/scripts/batch-plan.mjs +0 -1096
- package/plugins/sp/scripts/batch-plan.ts +0 -145
- package/plugins/sp/scripts/batch-preflight.mjs +0 -423
- package/plugins/sp/scripts/batch-preflight.ts +0 -270
- package/plugins/sp/scripts/commit-guard.mjs +0 -226
- package/plugins/sp/scripts/commit-guard.ts +0 -248
- package/plugins/sp/scripts/daily-summary/daily-summary.mjs +0 -626
- package/plugins/sp/scripts/daily-summary/daily-summary.ts +0 -855
- package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.mjs +0 -211
- package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts +0 -110
- package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +0 -282
- package/plugins/sp/scripts/dogfood-testing/validate-report.ts +0 -98
- package/plugins/sp/scripts/feature-verification-steps.mjs +0 -82
- package/plugins/sp/scripts/feature-verification-steps.ts +0 -164
- package/plugins/sp/scripts/history-anatomy-cache.mjs +0 -931
- package/plugins/sp/scripts/history-anatomy-cache.ts +0 -247
- package/plugins/sp/scripts/idea-handoff.mjs +0 -32
- package/plugins/sp/scripts/idea-handoff.ts +0 -47
- package/plugins/sp/scripts/inline-run-dispatch.mjs +0 -131
- package/plugins/sp/scripts/inline-run-dispatch.ts +0 -115
- package/plugins/sp/scripts/inline-run-setup.mjs +0 -746
- package/plugins/sp/scripts/inline-run-setup.ts +0 -198
- package/plugins/sp/scripts/persist-out-check.mjs +0 -217
- package/plugins/sp/scripts/persist-out-check.ts +0 -249
- package/plugins/sp/scripts/pr-reviewing.mjs +0 -712
- package/plugins/sp/scripts/pr-reviewing.ts +0 -952
- package/plugins/sp/scripts/quality-gate.mjs +0 -30491
- package/plugins/sp/scripts/quality-gate.ts +0 -80
- package/plugins/sp/scripts/residual-scan.mjs +0 -634
- package/plugins/sp/scripts/residual-scan.ts +0 -248
- package/plugins/sp/scripts/run-summary.mjs +0 -758
- package/plugins/sp/scripts/run-summary.ts +0 -107
- package/plugins/sp/scripts/script-root.mjs +0 -129
- package/plugins/sp/scripts/script-root.ts +0 -199
- package/plugins/sp/scripts/session-timeline.mjs +0 -359
- package/plugins/sp/scripts/session-timeline.ts +0 -190
- package/plugins/sp/scripts/task-diffstat.mjs +0 -181
- package/plugins/sp/scripts/task-diffstat.ts +0 -219
- package/plugins/sp/scripts/workflow-step-profile.mjs +0 -338
- package/plugins/sp/scripts/workflow-step-profile.ts +0 -165
- package/plugins/sp/scripts/wrapup-drift-probe.mjs +0 -190
- package/plugins/sp/scripts/wrapup-drift-probe.ts +0 -261
- package/plugins/sp/scripts/wrapup-steps.mjs +0 -627
- package/plugins/sp/scripts/wrapup-steps.ts +0 -891
- package/web/_astro/channel.C7wG9RxA.js +0 -1
- package/web/_astro/index.BCY1JJHd.css +0 -1
|
@@ -1,359 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// @bun
|
|
3
|
-
|
|
4
|
-
// plugins/sp/scripts/session-timeline.ts
|
|
5
|
-
import { readFileSync } from "fs";
|
|
6
|
-
|
|
7
|
-
// plugins/sp/lib/env.ts
|
|
8
|
-
function getEnvVars() {
|
|
9
|
-
return process.env;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
// plugins/sp/lib/transcript.ts
|
|
13
|
-
import { existsSync, readdirSync } from "node:fs";
|
|
14
|
-
import { homedir } from "node:os";
|
|
15
|
-
import { join } from "node:path";
|
|
16
|
-
|
|
17
|
-
// plugins/sp/lib/plan-projection.generated.mjs
|
|
18
|
-
var NON_ABORTING_PREFLIGHT_CODES = new Set([
|
|
19
|
-
"L4.scenario-unverified",
|
|
20
|
-
"L4.verifying-incomplete-tasks"
|
|
21
|
-
]);
|
|
22
|
-
var zeroTokens = () => ({ input: 0, cacheCreate: 0, cacheRead: 0, output: 0 });
|
|
23
|
-
function formatDuration(ms) {
|
|
24
|
-
const s = Math.round(ms / 1000);
|
|
25
|
-
const h = Math.floor(s / 3600);
|
|
26
|
-
const m = Math.floor(s % 3600 / 60);
|
|
27
|
-
const ss = String(s % 60).padStart(2, "0");
|
|
28
|
-
return h > 0 ? `${h}:${String(m).padStart(2, "0")}:${ss}` : `${m}:${ss}`;
|
|
29
|
-
}
|
|
30
|
-
function formatTokens(n) {
|
|
31
|
-
if (n >= 999500)
|
|
32
|
-
return `${(n / 1e6).toFixed(1)}M`;
|
|
33
|
-
return n >= 1000 ? `${Math.round(n / 1000)}k` : String(n);
|
|
34
|
-
}
|
|
35
|
-
function formatTokenSplit(t) {
|
|
36
|
-
if (!t)
|
|
37
|
-
return "n/a";
|
|
38
|
-
const nonCached = t.input + t.cacheCreate + t.output;
|
|
39
|
-
return `${formatTokens(nonCached + t.cacheRead)} / ${formatTokens(nonCached)}`;
|
|
40
|
-
}
|
|
41
|
-
var OPERATOR_TOOLS = new Set(["AskUserQuestion", "ask_user_question"]);
|
|
42
|
-
function firstTextContent(row) {
|
|
43
|
-
const content = row.message?.content;
|
|
44
|
-
if (typeof content === "string")
|
|
45
|
-
return content;
|
|
46
|
-
if (!Array.isArray(content))
|
|
47
|
-
return;
|
|
48
|
-
return content.find((b) => b.type === "text")?.text;
|
|
49
|
-
}
|
|
50
|
-
function skillInvocationPrompt(text) {
|
|
51
|
-
const name = /^\s*<skill name="([^"]+)"/.exec(text)?.[1];
|
|
52
|
-
if (name === undefined)
|
|
53
|
-
return;
|
|
54
|
-
const close = text.lastIndexOf("</skill>");
|
|
55
|
-
const args = close < 0 ? "" : text.slice(close + "</skill>".length).trim();
|
|
56
|
-
return args === "" ? `/${name}` : `/${name} ${args}`;
|
|
57
|
-
}
|
|
58
|
-
function promptText(row) {
|
|
59
|
-
if (row.type === "custom_message") {
|
|
60
|
-
if (row.display === false)
|
|
61
|
-
return;
|
|
62
|
-
const text = typeof row.content === "string" ? row.content.trim() : "";
|
|
63
|
-
return text === "" ? undefined : text;
|
|
64
|
-
}
|
|
65
|
-
if (row.type === "message") {
|
|
66
|
-
if (row.message?.role !== "user")
|
|
67
|
-
return;
|
|
68
|
-
const text = firstTextContent(row);
|
|
69
|
-
if (text === undefined)
|
|
70
|
-
return;
|
|
71
|
-
return skillInvocationPrompt(text) ?? text;
|
|
72
|
-
}
|
|
73
|
-
if (row.type !== "user" || row.isMeta || row.isCompactSummary)
|
|
74
|
-
return;
|
|
75
|
-
const content = row.message?.content;
|
|
76
|
-
if (typeof content === "string")
|
|
77
|
-
return content;
|
|
78
|
-
if (!Array.isArray(content) || content.some((b) => b.type === "tool_result"))
|
|
79
|
-
return;
|
|
80
|
-
return content.find((b) => b.type === "text")?.text;
|
|
81
|
-
}
|
|
82
|
-
function sumTokens(all) {
|
|
83
|
-
const total = zeroTokens();
|
|
84
|
-
for (const t of all)
|
|
85
|
-
for (const k of Object.keys(total))
|
|
86
|
-
total[k] += t[k];
|
|
87
|
-
return total;
|
|
88
|
-
}
|
|
89
|
-
function sumSpans(spans) {
|
|
90
|
-
return {
|
|
91
|
-
workMs: spans.reduce((n, s) => n + s.workMs, 0),
|
|
92
|
-
waitMs: spans.reduce((n, s) => n + s.waitMs, 0),
|
|
93
|
-
toolCalls: spans.reduce((n, s) => n + s.toolCalls, 0),
|
|
94
|
-
tokens: sumTokens(spans.map((s) => s.tokens))
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
var newAcc = (ts) => ({
|
|
98
|
-
last: ts,
|
|
99
|
-
askMs: 0,
|
|
100
|
-
asks: new Map,
|
|
101
|
-
toolIds: new Set,
|
|
102
|
-
messages: new Map,
|
|
103
|
-
compactions: 0
|
|
104
|
-
});
|
|
105
|
-
function accumulate(acc, row, ts) {
|
|
106
|
-
acc.last = Math.max(acc.last, ts);
|
|
107
|
-
if (row.type === "message") {
|
|
108
|
-
const m = row.message;
|
|
109
|
-
if (m?.role === "assistant") {
|
|
110
|
-
const blocks = Array.isArray(m.content) ? m.content : [];
|
|
111
|
-
for (const b of blocks) {
|
|
112
|
-
if (b.type === "toolCall" && b.id) {
|
|
113
|
-
acc.toolIds.add(b.id);
|
|
114
|
-
if (b.name && OPERATOR_TOOLS.has(b.name))
|
|
115
|
-
acc.asks.set(b.id, ts);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
const u = m.usage;
|
|
119
|
-
if (u && row.id)
|
|
120
|
-
acc.messages.set(row.id, {
|
|
121
|
-
input: u.input ?? 0,
|
|
122
|
-
cacheCreate: u.cacheWrite ?? 0,
|
|
123
|
-
cacheRead: u.cacheRead ?? 0,
|
|
124
|
-
output: u.output ?? 0
|
|
125
|
-
});
|
|
126
|
-
} else if (m?.role === "toolResult" && m.toolCallId && acc.asks.has(m.toolCallId)) {
|
|
127
|
-
acc.askMs += ts - (acc.asks.get(m.toolCallId) ?? ts);
|
|
128
|
-
}
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
if (row.type === "compaction")
|
|
132
|
-
acc.compactions++;
|
|
133
|
-
const blocks = Array.isArray(row.message?.content) ? row.message.content : [];
|
|
134
|
-
for (const b of blocks) {
|
|
135
|
-
if (b.type === "tool_use" && b.id) {
|
|
136
|
-
acc.toolIds.add(b.id);
|
|
137
|
-
if (b.name && OPERATOR_TOOLS.has(b.name))
|
|
138
|
-
acc.asks.set(b.id, ts);
|
|
139
|
-
} else if (b.type === "tool_result" && b.tool_use_id && acc.asks.has(b.tool_use_id)) {
|
|
140
|
-
acc.askMs += ts - (acc.asks.get(b.tool_use_id) ?? ts);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
const u = row.message?.usage;
|
|
144
|
-
if (row.type === "assistant" && u && row.message?.id) {
|
|
145
|
-
acc.messages.set(row.message.id, {
|
|
146
|
-
input: u.input_tokens ?? 0,
|
|
147
|
-
cacheCreate: u.cache_creation_input_tokens ?? 0,
|
|
148
|
-
cacheRead: u.cache_read_input_tokens ?? 0,
|
|
149
|
-
output: u.output_tokens ?? 0
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
var SHELL_TOOLS = new Set(["bash", "shell", "exec", "exec_command", "run_command", "command"]);
|
|
154
|
-
var SUBAGENT_TOOLS = new Set(["agent", "task", "invoke_subagent", "subagent", "dispatch_agent"]);
|
|
155
|
-
var LOWERCASE_OPERATOR_TOOLS = new Set(["askuserquestion", "ask_user_question"]);
|
|
156
|
-
var SENSITIVE_PREFIXES = ["drizzle/", "packages/config/", ".github/", "plugins/sp/hooks/"];
|
|
157
|
-
var SENSITIVE_GLOBS = ["apps/server/src/**/auth*", "**/*secret*", "**/*.sql"];
|
|
158
|
-
function globToRegex(glob) {
|
|
159
|
-
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replaceAll("**/", "\x00").replaceAll("**", "\x01").replaceAll("*", "[^/]*").replaceAll("\x00", "(?:.*/)?").replaceAll("\x01", ".*");
|
|
160
|
-
return new RegExp(`^${escaped}$`, "s");
|
|
161
|
-
}
|
|
162
|
-
var SENSITIVE_MATCHERS = [
|
|
163
|
-
...SENSITIVE_PREFIXES.map((prefix) => ({
|
|
164
|
-
label: prefix,
|
|
165
|
-
test: (path) => path.startsWith(prefix)
|
|
166
|
-
})),
|
|
167
|
-
...SENSITIVE_GLOBS.map((glob) => {
|
|
168
|
-
const regex = globToRegex(glob);
|
|
169
|
-
return { label: glob, test: (path) => regex.test(path) };
|
|
170
|
-
})
|
|
171
|
-
];
|
|
172
|
-
|
|
173
|
-
// plugins/sp/lib/transcript.ts
|
|
174
|
-
var SESSION_ID = /^[A-Za-z0-9_-]+$/;
|
|
175
|
-
var PI_ROLES = new Set(["user", "assistant", "toolResult"]);
|
|
176
|
-
function sniffFormat(rows) {
|
|
177
|
-
for (const row of rows) {
|
|
178
|
-
if (row.type === "message" && PI_ROLES.has(row.message?.role ?? ""))
|
|
179
|
-
return "pi";
|
|
180
|
-
if (row.type === "user" || row.type === "assistant")
|
|
181
|
-
return "claude";
|
|
182
|
-
}
|
|
183
|
-
return "unknown";
|
|
184
|
-
}
|
|
185
|
-
function parseRows(lines) {
|
|
186
|
-
const rows = [];
|
|
187
|
-
let skippedLines = 0;
|
|
188
|
-
for (const line of lines) {
|
|
189
|
-
if (!line.trim())
|
|
190
|
-
continue;
|
|
191
|
-
let row;
|
|
192
|
-
try {
|
|
193
|
-
row = JSON.parse(line);
|
|
194
|
-
} catch {
|
|
195
|
-
skippedLines++;
|
|
196
|
-
continue;
|
|
197
|
-
}
|
|
198
|
-
const ts = row.timestamp ? Date.parse(row.timestamp) : Number.NaN;
|
|
199
|
-
if (!Number.isNaN(ts))
|
|
200
|
-
rows.push([row, ts]);
|
|
201
|
-
}
|
|
202
|
-
return { rows, skippedLines, format: sniffFormat(rows.map(([r]) => r)) };
|
|
203
|
-
}
|
|
204
|
-
function resolveTranscript(env, projectsRoot = join(homedir(), ".claude", "projects"), override) {
|
|
205
|
-
if (override)
|
|
206
|
-
return existsSync(override) ? { ok: true, path: override } : { ok: false, reason: "no transcript" };
|
|
207
|
-
const id = env.CLAUDE_CODE_SESSION_ID;
|
|
208
|
-
if (!id) {
|
|
209
|
-
const piFile = env.PI_SESSION_FILE;
|
|
210
|
-
if (piFile !== undefined && piFile !== "") {
|
|
211
|
-
return existsSync(piFile) ? { ok: true, path: piFile } : { ok: false, reason: `PI_SESSION_FILE ${piFile} does not exist` };
|
|
212
|
-
}
|
|
213
|
-
return {
|
|
214
|
-
ok: false,
|
|
215
|
-
reason: "no host session id; pass --transcript <path> (pi: ~/.pi/agent/sessions/<cwd-slug>/<file>.jsonl, " + "or set PI_SESSION_FILE)"
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
if (!SESSION_ID.test(id))
|
|
219
|
-
return { ok: false, reason: "refusing a session id with path characters" };
|
|
220
|
-
if (!existsSync(projectsRoot))
|
|
221
|
-
return { ok: false, reason: `no transcript root ${projectsRoot}` };
|
|
222
|
-
for (const dir of readdirSync(projectsRoot)) {
|
|
223
|
-
const path = join(projectsRoot, dir, `${id}.jsonl`);
|
|
224
|
-
if (existsSync(path))
|
|
225
|
-
return { ok: true, path };
|
|
226
|
-
}
|
|
227
|
-
return { ok: false, reason: `no transcript for session ${id}` };
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// plugins/sp/scripts/session-timeline.ts
|
|
231
|
-
var PROMPT_EXCERPT = 80;
|
|
232
|
-
var render = (span) => ({
|
|
233
|
-
...span,
|
|
234
|
-
work: formatDuration(span.workMs),
|
|
235
|
-
wait: formatDuration(span.waitMs),
|
|
236
|
-
token: formatTokenSplit(span.tokens)
|
|
237
|
-
});
|
|
238
|
-
function parseGroups(spec, count) {
|
|
239
|
-
let next = 1;
|
|
240
|
-
return spec.split(",").map((part) => {
|
|
241
|
-
const [a = Number.NaN, b = a] = part.trim().split("-").map(Number);
|
|
242
|
-
if (!Number.isInteger(a) || !Number.isInteger(b) || a < next || b < a || b > count) {
|
|
243
|
-
throw new Error(`invalid --group "${part}" (segments 1-${count}, ascending, no overlap)`);
|
|
244
|
-
}
|
|
245
|
-
next = b + 1;
|
|
246
|
-
return [a, b];
|
|
247
|
-
});
|
|
248
|
-
}
|
|
249
|
-
function buildTimeline(lines, group) {
|
|
250
|
-
const open = [];
|
|
251
|
-
const { rows, skippedLines, format } = parseRows(lines);
|
|
252
|
-
const isPi = format === "pi";
|
|
253
|
-
for (const [row, ts] of rows) {
|
|
254
|
-
const prompt = promptText(row);
|
|
255
|
-
if (prompt !== undefined) {
|
|
256
|
-
open.push({ start: ts, prompt, ...newAcc(ts) });
|
|
257
|
-
continue;
|
|
258
|
-
}
|
|
259
|
-
const seg = open.at(-1);
|
|
260
|
-
if (seg)
|
|
261
|
-
accumulate(seg, row, ts);
|
|
262
|
-
}
|
|
263
|
-
const segments = open.map((seg, i) => {
|
|
264
|
-
const next = open[i + 1]?.start;
|
|
265
|
-
const idle = next === undefined ? 0 : Math.max(0, next - seg.last);
|
|
266
|
-
const oneLine = seg.prompt.replace(/\s+/g, " ").trim();
|
|
267
|
-
const span = {
|
|
268
|
-
index: i + 1,
|
|
269
|
-
start: new Date(seg.start).toISOString(),
|
|
270
|
-
prompt: oneLine.length > PROMPT_EXCERPT ? `${oneLine.slice(0, PROMPT_EXCERPT)}\u2026` : oneLine,
|
|
271
|
-
workMs: seg.last - seg.start - seg.askMs,
|
|
272
|
-
waitMs: idle + seg.askMs,
|
|
273
|
-
toolCalls: seg.toolIds.size,
|
|
274
|
-
tokens: sumTokens([...seg.messages.values()])
|
|
275
|
-
};
|
|
276
|
-
return render(isPi ? { ...span, compactions: seg.compactions } : span);
|
|
277
|
-
});
|
|
278
|
-
const first = open[0];
|
|
279
|
-
const last = open.at(-1);
|
|
280
|
-
const elapsedMs = first && last ? last.last - first.start : 0;
|
|
281
|
-
const totals = { ...sumSpans(segments), elapsedMs, elapsed: formatDuration(elapsedMs) };
|
|
282
|
-
const timeline = {
|
|
283
|
-
available: true,
|
|
284
|
-
segments,
|
|
285
|
-
totals: render(isPi ? { ...totals, compactions: segments.reduce((n, s) => n + (s.compactions ?? 0), 0) } : totals),
|
|
286
|
-
skippedLines
|
|
287
|
-
};
|
|
288
|
-
if (group) {
|
|
289
|
-
timeline.stages = parseGroups(group, segments.length).map(([a, b]) => render({ ...sumSpans(segments.slice(a - 1, b)), segments: a === b ? `${a}` : `${a}-${b}` }));
|
|
290
|
-
}
|
|
291
|
-
return timeline;
|
|
292
|
-
}
|
|
293
|
-
var SESSION_TIMELINE_USAGE = 'usage: session-timeline [--transcript <path>] [--group "1-3,4,..."]';
|
|
294
|
-
function zeroSegmentReason(rows) {
|
|
295
|
-
const format = sniffFormat(rows.map(([r]) => r));
|
|
296
|
-
if (format === "unknown") {
|
|
297
|
-
const counts = new Map;
|
|
298
|
-
for (const [r] of rows) {
|
|
299
|
-
const kind = typeof r.type === "string" && r.type !== "" ? r.type : "(untyped)";
|
|
300
|
-
counts.set(kind, (counts.get(kind) ?? 0) + 1);
|
|
301
|
-
}
|
|
302
|
-
const census = [...counts.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, 5).map(([kind, n]) => `${kind} ${n}`).join(", ");
|
|
303
|
-
return `unrecognized transcript format (row types: ${census})`;
|
|
304
|
-
}
|
|
305
|
-
return `${format} transcript with no operator prompts (${rows.length} rows)`;
|
|
306
|
-
}
|
|
307
|
-
function main(argv, env = getEnvVars(), write = (s) => process.stdout.write(s), projectsRoot, writeErr = (s) => process.stderr.write(s)) {
|
|
308
|
-
let transcript;
|
|
309
|
-
let group;
|
|
310
|
-
for (let i = 0;i < argv.length; i++) {
|
|
311
|
-
const arg = argv[i];
|
|
312
|
-
if (arg === "--transcript" && argv[i + 1])
|
|
313
|
-
transcript = argv[++i];
|
|
314
|
-
else if (arg === "--group" && argv[i + 1])
|
|
315
|
-
group = argv[++i];
|
|
316
|
-
else if (arg === "--spur-bin" && argv[i + 1])
|
|
317
|
-
i++;
|
|
318
|
-
else {
|
|
319
|
-
writeErr(`${SESSION_TIMELINE_USAGE}
|
|
320
|
-
`);
|
|
321
|
-
return 2;
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
const resolved = resolveTranscript(env, projectsRoot, transcript);
|
|
325
|
-
if (!resolved.ok) {
|
|
326
|
-
write(`${JSON.stringify({ available: false, reason: resolved.reason })}
|
|
327
|
-
`);
|
|
328
|
-
return 0;
|
|
329
|
-
}
|
|
330
|
-
try {
|
|
331
|
-
const lines = readFileSync(resolved.path, "utf8").split(`
|
|
332
|
-
`);
|
|
333
|
-
const timeline = buildTimeline(lines, group);
|
|
334
|
-
if (timeline.segments.length === 0) {
|
|
335
|
-
write(`${JSON.stringify({ available: false, reason: zeroSegmentReason(parseRows(lines).rows) })}
|
|
336
|
-
`);
|
|
337
|
-
return 0;
|
|
338
|
-
}
|
|
339
|
-
write(`${JSON.stringify({ ...timeline, transcript: resolved.path })}
|
|
340
|
-
`);
|
|
341
|
-
return 0;
|
|
342
|
-
} catch (error) {
|
|
343
|
-
writeErr(`session-timeline: ${error.message}
|
|
344
|
-
`);
|
|
345
|
-
return 2;
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
process.exit(main(process.argv.slice(2)));
|
|
349
|
-
export {
|
|
350
|
-
SESSION_TIMELINE_USAGE,
|
|
351
|
-
buildTimeline,
|
|
352
|
-
formatDuration,
|
|
353
|
-
formatTokenSplit,
|
|
354
|
-
formatTokens,
|
|
355
|
-
main,
|
|
356
|
-
parseGroups,
|
|
357
|
-
resolveTranscript,
|
|
358
|
-
zeroSegmentReason
|
|
359
|
-
};
|
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
/**
|
|
3
|
-
* session-timeline — measured time and token evidence for the sp:session-review Time breakdown.
|
|
4
|
-
* The visible conversation carries no timestamps or usage (so the table read `n/a`); the host
|
|
5
|
-
* transcript carries both. Reads the active Claude Code JSONL read-only
|
|
6
|
-
* (`~/.claude/projects/<slug>/<CLAUDE_CODE_SESSION_ID>.jsonl`, or `--transcript`), one segment per
|
|
7
|
-
* operator prompt: work = prompt → last activity; wait = idle until the next prompt plus time spent
|
|
8
|
-
* answering an AskUserQuestion; tokens counted once per message id and rendered `<total> /
|
|
9
|
-
* <non-cached>`. `--group "1-3,4"` sums segments into stages. No transcript → `{available:false}`,
|
|
10
|
-
* exit 0, and the review renders `n/a` instead of guessing. Node builtins only (standalone plugin).
|
|
11
|
-
*/
|
|
12
|
-
import { readFileSync } from 'node:fs';
|
|
13
|
-
import { getEnvVars } from '../lib/env';
|
|
14
|
-
import {
|
|
15
|
-
type Acc,
|
|
16
|
-
accumulate,
|
|
17
|
-
formatDuration,
|
|
18
|
-
formatTokenSplit,
|
|
19
|
-
newAcc,
|
|
20
|
-
parseRows,
|
|
21
|
-
promptText,
|
|
22
|
-
type Row,
|
|
23
|
-
resolveTranscript,
|
|
24
|
-
type Span,
|
|
25
|
-
sniffFormat,
|
|
26
|
-
sumSpans,
|
|
27
|
-
sumTokens,
|
|
28
|
-
} from '../lib/transcript';
|
|
29
|
-
|
|
30
|
-
export { formatDuration, formatTokenSplit, formatTokens, resolveTranscript } from '../lib/transcript';
|
|
31
|
-
|
|
32
|
-
/** A span plus its `M:SS` / `H:MM:SS` and token renderings, so the review table needs no arithmetic. */
|
|
33
|
-
export type Rendered<T> = T & { work: string; wait: string; token: string };
|
|
34
|
-
|
|
35
|
-
export interface Timeline {
|
|
36
|
-
available: true;
|
|
37
|
-
transcript?: string;
|
|
38
|
-
segments: Rendered<Span & { index: number; start: string; prompt: string; compactions?: number }>[];
|
|
39
|
-
stages?: Rendered<Span & { segments: string }>[];
|
|
40
|
-
totals: Rendered<Span & { elapsedMs: number; elapsed: string; compactions?: number }>;
|
|
41
|
-
skippedLines: number;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const PROMPT_EXCERPT = 80;
|
|
45
|
-
|
|
46
|
-
const render = <T extends Span>(span: T): Rendered<T> => ({
|
|
47
|
-
...span,
|
|
48
|
-
work: formatDuration(span.workMs),
|
|
49
|
-
wait: formatDuration(span.waitMs),
|
|
50
|
-
token: formatTokenSplit(span.tokens),
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
/** "1-3,4" → [[1,3],[4,4]]; 1-based, ascending, in range, non-overlapping. */
|
|
54
|
-
export function parseGroups(spec: string, count: number): [number, number][] {
|
|
55
|
-
let next = 1;
|
|
56
|
-
return spec.split(',').map((part) => {
|
|
57
|
-
const [a = Number.NaN, b = a] = part.trim().split('-').map(Number);
|
|
58
|
-
if (!Number.isInteger(a) || !Number.isInteger(b) || a < next || b < a || b > count) {
|
|
59
|
-
throw new Error(`invalid --group "${part}" (segments 1-${count}, ascending, no overlap)`);
|
|
60
|
-
}
|
|
61
|
-
next = b + 1;
|
|
62
|
-
return [a, b] as [number, number];
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
interface Open extends Acc {
|
|
67
|
-
start: number;
|
|
68
|
-
prompt: string;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function buildTimeline(lines: string[], group?: string): Timeline {
|
|
72
|
-
const open: Open[] = [];
|
|
73
|
-
const { rows, skippedLines, format } = parseRows(lines);
|
|
74
|
-
const isPi = format === 'pi';
|
|
75
|
-
|
|
76
|
-
for (const [row, ts] of rows) {
|
|
77
|
-
const prompt = promptText(row);
|
|
78
|
-
if (prompt !== undefined) {
|
|
79
|
-
open.push({ start: ts, prompt, ...newAcc(ts) });
|
|
80
|
-
continue;
|
|
81
|
-
}
|
|
82
|
-
const seg = open.at(-1);
|
|
83
|
-
if (seg) accumulate(seg, row, ts); // rows before the first prompt are session preamble
|
|
84
|
-
}
|
|
85
|
-
|
|
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
|
-
);
|
|
103
|
-
|
|
104
|
-
const first = open[0];
|
|
105
|
-
const last = open.at(-1);
|
|
106
|
-
const elapsedMs = first && last ? last.last - first.start : 0;
|
|
107
|
-
const totals = { ...sumSpans(segments), elapsedMs, elapsed: formatDuration(elapsedMs) };
|
|
108
|
-
const timeline: Timeline = {
|
|
109
|
-
available: true,
|
|
110
|
-
segments,
|
|
111
|
-
totals: render(
|
|
112
|
-
isPi ? { ...totals, compactions: segments.reduce((n, s) => n + (s.compactions ?? 0), 0) } : totals,
|
|
113
|
-
),
|
|
114
|
-
skippedLines,
|
|
115
|
-
};
|
|
116
|
-
if (group) {
|
|
117
|
-
timeline.stages = parseGroups(group, segments.length).map(([a, b]) =>
|
|
118
|
-
render({ ...sumSpans(segments.slice(a - 1, b)), segments: a === b ? `${a}` : `${a}-${b}` }),
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
|
-
return timeline;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
export const SESSION_TIMELINE_USAGE = 'usage: session-timeline [--transcript <path>] [--group "1-3,4,..."]';
|
|
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
|
-
|
|
150
|
-
export function main(
|
|
151
|
-
argv: string[],
|
|
152
|
-
env: Record<string, string | undefined> = getEnvVars(),
|
|
153
|
-
write: (s: string) => void = (s) => process.stdout.write(s),
|
|
154
|
-
projectsRoot?: string,
|
|
155
|
-
writeErr: (s: string) => void = (s) => process.stderr.write(s),
|
|
156
|
-
): number {
|
|
157
|
-
let transcript: string | undefined;
|
|
158
|
-
let group: string | undefined;
|
|
159
|
-
for (let i = 0; i < argv.length; i++) {
|
|
160
|
-
const arg = argv[i];
|
|
161
|
-
if (arg === '--transcript' && argv[i + 1]) transcript = argv[++i];
|
|
162
|
-
else if (arg === '--group' && argv[i + 1]) group = argv[++i];
|
|
163
|
-
else if (arg === '--spur-bin' && argv[i + 1])
|
|
164
|
-
i++; // 0482 R2: accepted, unused (no spur calls)
|
|
165
|
-
else {
|
|
166
|
-
writeErr(`${SESSION_TIMELINE_USAGE}\n`);
|
|
167
|
-
return 2;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
const resolved = resolveTranscript(env, projectsRoot, transcript);
|
|
171
|
-
if (!resolved.ok) {
|
|
172
|
-
write(`${JSON.stringify({ available: false, reason: resolved.reason })}\n`);
|
|
173
|
-
return 0;
|
|
174
|
-
}
|
|
175
|
-
try {
|
|
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
|
-
}
|
|
182
|
-
write(`${JSON.stringify({ ...timeline, transcript: resolved.path })}\n`);
|
|
183
|
-
return 0;
|
|
184
|
-
} catch (error) {
|
|
185
|
-
writeErr(`session-timeline: ${(error as Error).message}\n`);
|
|
186
|
-
return 2;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
if (import.meta.main) process.exit(main(process.argv.slice(2)));
|