@gobing-ai/spur 0.3.99 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/config.global.yaml +17 -0
  3. package/config/decisions/task-pipeline.yaml +55 -0
  4. package/config/plugin-scripts.json +10 -0
  5. package/config/rules/boundary/sp-runtime-path.yaml +2 -0
  6. package/config/rules/quality/coverage-gate.yaml +5 -3
  7. package/config/rules/strict/runtime-boundaries.yaml +11 -0
  8. package/config/rules/structure/protected-files.yaml +3 -0
  9. package/config/workflows/task-pipeline.yaml +178 -18
  10. package/package.json +9 -9
  11. package/plugins/sp/README.md +14 -1
  12. package/plugins/sp/agents/super-reviewer.md +25 -11
  13. package/plugins/sp/commands/dev-changelog.md +5 -4
  14. package/plugins/sp/commands/dev-review-session.md +12 -7
  15. package/plugins/sp/commands/dev-run.md +5 -2
  16. package/plugins/sp/commands/dev-runall.md +5 -2
  17. package/plugins/sp/hooks/agent-lifecycle.ts +155 -0
  18. package/plugins/sp/hooks/fleet-guest-stop.ts +180 -0
  19. package/plugins/sp/hooks/hooks.json +45 -1
  20. package/plugins/sp/hooks/pi/guard-extension.ts +21 -0
  21. package/plugins/sp/lib/history-anatomy.generated.mjs +5 -0
  22. package/plugins/sp/lib/idea-handoff.generated.mjs +34 -9
  23. package/plugins/sp/lib/inline-run.generated.d.mts +1 -0
  24. package/plugins/sp/lib/inline-run.generated.mjs +49 -19
  25. package/plugins/sp/lib/transcript.ts +160 -0
  26. package/plugins/sp/plugin.json +1 -1
  27. package/plugins/sp/scripts/history-anatomy-cache.mjs +5 -0
  28. package/plugins/sp/scripts/inline-run-setup.mjs +21 -20
  29. package/plugins/sp/scripts/inline-run-setup.ts +25 -31
  30. package/plugins/sp/scripts/residual-scan.mjs +2 -7
  31. package/plugins/sp/scripts/residual-scan.ts +7 -8
  32. package/plugins/sp/scripts/run-summary.mjs +286 -0
  33. package/plugins/sp/scripts/run-summary.ts +241 -0
  34. package/plugins/sp/scripts/session-timeline.mjs +234 -0
  35. package/plugins/sp/scripts/session-timeline.ts +152 -0
  36. package/plugins/sp/skills/code-verification/SKILL.md +5 -0
  37. package/plugins/sp/skills/fleet-join/SKILL.md +63 -0
  38. package/plugins/sp/skills/session-review/SKILL.md +46 -24
  39. package/plugins/sp/skills/spur-cli/SKILL.md +1 -0
  40. package/plugins/sp/skills/spur-cli/references/agent.md +108 -6
  41. package/plugins/sp/skills/spur-cli/references/decision.md +97 -0
  42. package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +22 -0
  43. package/plugins/sp/skills/spur-composer/SKILL.md +27 -1
  44. package/plugins/sp/skills/spur-dev/references/dev-operations.md +28 -16
  45. package/plugins/sp/skills/spur-dev/references/execution-batch.md +64 -7
  46. package/plugins/sp/skills/spur-dev/references/execution-workflow.md +14 -0
  47. package/plugins/sp/skills/spur-dev/references/flag-glossary.md +16 -4
  48. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +32 -7
  49. package/plugins/sp/skills/spur-dev/references/planning-workflow.md +6 -0
  50. package/schemas/decision-catalog.schema.json +226 -0
  51. package/schemas/spur-config.schema.json +25 -0
  52. package/spur.js +7440 -2424
  53. package/web/_astro/BoardApp.Bzr4OXrW.js +324 -0
  54. package/web/_astro/{BoardApp.i4eh_fHA.js → BoardApp.VeoxxLBy.js} +1 -1
  55. package/web/_astro/{TaskDetail.C5Cu8LdB.js → TaskDetail.LAyR_Pvn.js} +1 -1
  56. package/web/_astro/{arc.Bu5XpBEz.js → arc.f4e0OYLg.js} +1 -1
  57. package/web/_astro/{architectureDiagram-3BPJPVTR.9vHQTvhF.js → architectureDiagram-3BPJPVTR.SSIvxi-Z.js} +1 -1
  58. package/web/_astro/{blockDiagram-GPEHLZMM.Cn2Xl7D6.js → blockDiagram-GPEHLZMM.DSXIHOFf.js} +1 -1
  59. package/web/_astro/{c4Diagram-AAUBKEIU.7o0P_QDo.js → c4Diagram-AAUBKEIU.DG9Ubu85.js} +1 -1
  60. package/web/_astro/channel.CUFdtWhm.js +1 -0
  61. package/web/_astro/{chunk-2J33WTMH.C3zZFsNt.js → chunk-2J33WTMH.C7QU88Qh.js} +1 -1
  62. package/web/_astro/{chunk-4BX2VUAB.CNy_M7fO.js → chunk-4BX2VUAB.26Z94xBj.js} +1 -1
  63. package/web/_astro/{chunk-55IACEB6.2TiPnLBL.js → chunk-55IACEB6.DYRiBkqw.js} +1 -1
  64. package/web/_astro/{chunk-727SXJPM.5CGSyKAp.js → chunk-727SXJPM.D5Vfv5pe.js} +1 -1
  65. package/web/_astro/{chunk-AQP2D5EJ.w-ZIgfsK.js → chunk-AQP2D5EJ.DXHV47bN.js} +1 -1
  66. package/web/_astro/{chunk-FMBD7UC4.BkcEyhKa.js → chunk-FMBD7UC4.D1suAfOs.js} +1 -1
  67. package/web/_astro/{chunk-ND2GUHAM.mxKKbBG4.js → chunk-ND2GUHAM.D6BmdHEY.js} +1 -1
  68. package/web/_astro/{chunk-QZHKN3VN.cyA0z9aT.js → chunk-QZHKN3VN.rnPU-Ak-.js} +1 -1
  69. package/web/_astro/{classDiagram-4FO5ZUOK.CqtUzwhX.js → classDiagram-4FO5ZUOK.CvEfXIo1.js} +1 -1
  70. package/web/_astro/{classDiagram-v2-Q7XG4LA2.CqtUzwhX.js → classDiagram-v2-Q7XG4LA2.CvEfXIo1.js} +1 -1
  71. package/web/_astro/{cose-bilkent-S5V4N54A.BExPTopI.js → cose-bilkent-S5V4N54A.DkMisLSm.js} +1 -1
  72. package/web/_astro/{cynefin-OW5HDTMX.Bq2i10Cn.js → cynefin-OW5HDTMX.C77Q-pF6.js} +1 -1
  73. package/web/_astro/{dagre-BM42HDAG.By_HsOuv.js → dagre-BM42HDAG.YaV8ce27.js} +1 -1
  74. package/web/_astro/{diagram-2AECGRRQ.BtU0b7hz.js → diagram-2AECGRRQ.CUyGEKge.js} +1 -1
  75. package/web/_astro/{diagram-5GNKFQAL.D00ztMdT.js → diagram-5GNKFQAL.CRgd8dgj.js} +1 -1
  76. package/web/_astro/{diagram-KO2AKTUF.DMVJWaUN.js → diagram-KO2AKTUF.Bmzys9hq.js} +1 -1
  77. package/web/_astro/{diagram-LMA3HP47.Cxqrjg5-.js → diagram-LMA3HP47.BvmJY4Lw.js} +1 -1
  78. package/web/_astro/{diagram-OG6HWLK6.2lz_1B5h.js → diagram-OG6HWLK6.0hEhD30d.js} +1 -1
  79. package/web/_astro/{erDiagram-TEJ5UH35.BVQEuJ3T.js → erDiagram-TEJ5UH35.Bjt8lrnm.js} +1 -1
  80. package/web/_astro/{flowDiagram-I6XJVG4X.Qlyo90ky.js → flowDiagram-I6XJVG4X.CJZTjRDV.js} +1 -1
  81. package/web/_astro/{ganttDiagram-6RSMTGT7.DRwb8HHZ.js → ganttDiagram-6RSMTGT7._VqoVafc.js} +1 -1
  82. package/web/_astro/{gitGraphDiagram-PVQCEYII.YfoPthfS.js → gitGraphDiagram-PVQCEYII.CEDb7tVo.js} +1 -1
  83. package/web/_astro/index.Dg_3bOaJ.css +1 -0
  84. package/web/_astro/{infoDiagram-5YYISTIA.DvFREck1.js → infoDiagram-5YYISTIA.CgwifPpK.js} +1 -1
  85. package/web/_astro/{ishikawaDiagram-YF4QCWOH.BzCj9Q0-.js → ishikawaDiagram-YF4QCWOH.B99wx2J1.js} +1 -1
  86. package/web/_astro/{journeyDiagram-JHISSGLW.BDMT_AFb.js → journeyDiagram-JHISSGLW.2hRwJbEo.js} +1 -1
  87. package/web/_astro/{kanban-definition-UN3LZRKU.D6Fh3gj9.js → kanban-definition-UN3LZRKU.BahJkKNC.js} +1 -1
  88. package/web/_astro/{linear.Dxw00fGQ.js → linear.DguUUe6V.js} +1 -1
  89. package/web/_astro/{mermaid.core.CsoxUWa0.js → mermaid.core.CvgfmqVr.js} +4 -4
  90. package/web/_astro/{mindmap-definition-RKZ34NQL.CN8oeHLu.js → mindmap-definition-RKZ34NQL.B3qEA3x9.js} +1 -1
  91. package/web/_astro/{pieDiagram-4H26LBE5.C7ADqQ_7.js → pieDiagram-4H26LBE5.FlWA-_ru.js} +1 -1
  92. package/web/_astro/{quadrantDiagram-W4KKPZXB.Cw9lKyHP.js → quadrantDiagram-W4KKPZXB.9bVn35X7.js} +1 -1
  93. package/web/_astro/{requirementDiagram-4Y6WPE33.B1ni-z4q.js → requirementDiagram-4Y6WPE33.ChykIhct.js} +1 -1
  94. package/web/_astro/{sankeyDiagram-5OEKKPKP.BhexdjVr.js → sankeyDiagram-5OEKKPKP.Z2Y3hkYb.js} +1 -1
  95. package/web/_astro/{sequenceDiagram-3UESZ5HK.C3Rm7dpQ.js → sequenceDiagram-3UESZ5HK.J7JsHWDt.js} +1 -1
  96. package/web/_astro/{stateDiagram-AJRCARHV.CkuMt27u.js → stateDiagram-AJRCARHV.DAPbiCSS.js} +1 -1
  97. package/web/_astro/{stateDiagram-v2-BHNVJYJU.Dc_aazUA.js → stateDiagram-v2-BHNVJYJU.D2jvSi0q.js} +1 -1
  98. package/web/_astro/{timeline-definition-PNZ67QCA.ja7sUFhH.js → timeline-definition-PNZ67QCA.psW2ci29.js} +1 -1
  99. package/web/_astro/{vennDiagram-CIIHVFJN.BxIshX_K.js → vennDiagram-CIIHVFJN.DlILYVmg.js} +1 -1
  100. package/web/_astro/{wardleyDiagram-YWT4CUSO.DfHlC0sh.js → wardleyDiagram-YWT4CUSO.xNWwLJnn.js} +1 -1
  101. package/web/_astro/{xychartDiagram-2RQKCTM6.DSnhHwCh.js → xychartDiagram-2RQKCTM6.EU_BuH7V.js} +1 -1
  102. package/web/board-runtime.json +1 -1
  103. package/web/index.html +2 -2
  104. package/web/_astro/BoardApp.BVp-LYi6.js +0 -322
  105. package/web/_astro/channel.6IFIKBhu.js +0 -1
  106. package/web/_astro/index.CbNz17Sx.css +0 -1
@@ -0,0 +1,234 @@
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
+ var SESSION_ID = /^[A-Za-z0-9_-]+$/;
17
+ var OPERATOR_TOOLS = new Set(["AskUserQuestion"]);
18
+ var zeroTokens = () => ({ input: 0, cacheCreate: 0, cacheRead: 0, output: 0 });
19
+ function formatDuration(ms) {
20
+ const s = Math.round(ms / 1000);
21
+ const h = Math.floor(s / 3600);
22
+ const m = Math.floor(s % 3600 / 60);
23
+ const ss = String(s % 60).padStart(2, "0");
24
+ return h > 0 ? `${h}:${String(m).padStart(2, "0")}:${ss}` : `${m}:${ss}`;
25
+ }
26
+ function formatTokens(n) {
27
+ if (n >= 999500)
28
+ return `${(n / 1e6).toFixed(1)}M`;
29
+ return n >= 1000 ? `${Math.round(n / 1000)}k` : String(n);
30
+ }
31
+ function formatTokenSplit(t) {
32
+ if (!t)
33
+ return "n/a";
34
+ const nonCached = t.input + t.cacheCreate + t.output;
35
+ return `${formatTokens(nonCached + t.cacheRead)} / ${formatTokens(nonCached)}`;
36
+ }
37
+ function promptText(row) {
38
+ if (row.type !== "user" || row.isMeta || row.isCompactSummary)
39
+ return;
40
+ const content = row.message?.content;
41
+ if (typeof content === "string")
42
+ return content;
43
+ if (!Array.isArray(content) || content.some((b) => b.type === "tool_result"))
44
+ return;
45
+ return content.find((b) => b.type === "text")?.text;
46
+ }
47
+ function sumTokens(all) {
48
+ const total = zeroTokens();
49
+ for (const t of all)
50
+ for (const k of Object.keys(total))
51
+ total[k] += t[k];
52
+ return total;
53
+ }
54
+ function sumSpans(spans) {
55
+ return {
56
+ workMs: spans.reduce((n, s) => n + s.workMs, 0),
57
+ waitMs: spans.reduce((n, s) => n + s.waitMs, 0),
58
+ toolCalls: spans.reduce((n, s) => n + s.toolCalls, 0),
59
+ tokens: sumTokens(spans.map((s) => s.tokens))
60
+ };
61
+ }
62
+ var newAcc = (ts) => ({
63
+ last: ts,
64
+ askMs: 0,
65
+ asks: new Map,
66
+ toolIds: new Set,
67
+ messages: new Map
68
+ });
69
+ function accumulate(acc, row, ts) {
70
+ acc.last = Math.max(acc.last, ts);
71
+ const blocks = Array.isArray(row.message?.content) ? row.message.content : [];
72
+ for (const b of blocks) {
73
+ if (b.type === "tool_use" && b.id) {
74
+ acc.toolIds.add(b.id);
75
+ if (b.name && OPERATOR_TOOLS.has(b.name))
76
+ acc.asks.set(b.id, ts);
77
+ } else if (b.type === "tool_result" && b.tool_use_id && acc.asks.has(b.tool_use_id)) {
78
+ acc.askMs += ts - (acc.asks.get(b.tool_use_id) ?? ts);
79
+ }
80
+ }
81
+ const u = row.message?.usage;
82
+ if (row.type === "assistant" && u && row.message?.id) {
83
+ acc.messages.set(row.message.id, {
84
+ input: u.input_tokens ?? 0,
85
+ cacheCreate: u.cache_creation_input_tokens ?? 0,
86
+ cacheRead: u.cache_read_input_tokens ?? 0,
87
+ output: u.output_tokens ?? 0
88
+ });
89
+ }
90
+ }
91
+ function parseRows(lines) {
92
+ const rows = [];
93
+ let skippedLines = 0;
94
+ for (const line of lines) {
95
+ if (!line.trim())
96
+ continue;
97
+ let row;
98
+ try {
99
+ row = JSON.parse(line);
100
+ } catch {
101
+ skippedLines++;
102
+ continue;
103
+ }
104
+ const ts = row.timestamp ? Date.parse(row.timestamp) : Number.NaN;
105
+ if (!Number.isNaN(ts))
106
+ rows.push([row, ts]);
107
+ }
108
+ return { rows, skippedLines };
109
+ }
110
+ function resolveTranscript(env, projectsRoot = join(homedir(), ".claude", "projects"), override) {
111
+ if (override)
112
+ return existsSync(override) ? { ok: true, path: override } : { ok: false, reason: "no transcript" };
113
+ const id = env.CLAUDE_CODE_SESSION_ID;
114
+ if (!id)
115
+ return { ok: false, reason: "no host session id (CLAUDE_CODE_SESSION_ID); pass --transcript <path>" };
116
+ if (!SESSION_ID.test(id))
117
+ return { ok: false, reason: "refusing a session id with path characters" };
118
+ if (!existsSync(projectsRoot))
119
+ return { ok: false, reason: `no transcript root ${projectsRoot}` };
120
+ for (const dir of readdirSync(projectsRoot)) {
121
+ const path = join(projectsRoot, dir, `${id}.jsonl`);
122
+ if (existsSync(path))
123
+ return { ok: true, path };
124
+ }
125
+ return { ok: false, reason: `no transcript for session ${id}` };
126
+ }
127
+
128
+ // plugins/sp/scripts/session-timeline.ts
129
+ var PROMPT_EXCERPT = 80;
130
+ var render = (span) => ({
131
+ ...span,
132
+ work: formatDuration(span.workMs),
133
+ wait: formatDuration(span.waitMs),
134
+ token: formatTokenSplit(span.tokens)
135
+ });
136
+ function parseGroups(spec, count) {
137
+ let next = 1;
138
+ return spec.split(",").map((part) => {
139
+ const [a = Number.NaN, b = a] = part.trim().split("-").map(Number);
140
+ if (!Number.isInteger(a) || !Number.isInteger(b) || a < next || b < a || b > count) {
141
+ throw new Error(`invalid --group "${part}" (segments 1-${count}, ascending, no overlap)`);
142
+ }
143
+ next = b + 1;
144
+ return [a, b];
145
+ });
146
+ }
147
+ function buildTimeline(lines, group) {
148
+ const open = [];
149
+ const { rows, skippedLines } = parseRows(lines);
150
+ for (const [row, ts] of rows) {
151
+ const prompt = promptText(row);
152
+ if (prompt !== undefined) {
153
+ open.push({ start: ts, prompt, ...newAcc(ts) });
154
+ continue;
155
+ }
156
+ const seg = open.at(-1);
157
+ if (seg)
158
+ accumulate(seg, row, ts);
159
+ }
160
+ const segments = open.map((seg, i) => {
161
+ const next = open[i + 1]?.start;
162
+ const idle = next === undefined ? 0 : Math.max(0, next - seg.last);
163
+ const oneLine = seg.prompt.replace(/\s+/g, " ").trim();
164
+ return render({
165
+ index: i + 1,
166
+ start: new Date(seg.start).toISOString(),
167
+ prompt: oneLine.length > PROMPT_EXCERPT ? `${oneLine.slice(0, PROMPT_EXCERPT)}\u2026` : oneLine,
168
+ workMs: seg.last - seg.start - seg.askMs,
169
+ waitMs: idle + seg.askMs,
170
+ toolCalls: seg.toolIds.size,
171
+ tokens: sumTokens([...seg.messages.values()])
172
+ });
173
+ });
174
+ const first = open[0];
175
+ const last = open.at(-1);
176
+ const elapsedMs = first && last ? last.last - first.start : 0;
177
+ const timeline = {
178
+ available: true,
179
+ segments,
180
+ totals: render({ ...sumSpans(segments), elapsedMs, elapsed: formatDuration(elapsedMs) }),
181
+ skippedLines
182
+ };
183
+ if (group) {
184
+ timeline.stages = parseGroups(group, segments.length).map(([a, b]) => render({ ...sumSpans(segments.slice(a - 1, b)), segments: a === b ? `${a}` : `${a}-${b}` }));
185
+ }
186
+ return timeline;
187
+ }
188
+ var SESSION_TIMELINE_USAGE = 'usage: session-timeline [--transcript <path>] [--group "1-3,4,..."]';
189
+ function main(argv, env = getEnvVars(), write = (s) => process.stdout.write(s), projectsRoot, writeErr = (s) => process.stderr.write(s)) {
190
+ let transcript;
191
+ let group;
192
+ for (let i = 0;i < argv.length; i++) {
193
+ const arg = argv[i];
194
+ if (arg === "--transcript" && argv[i + 1])
195
+ transcript = argv[++i];
196
+ else if (arg === "--group" && argv[i + 1])
197
+ group = argv[++i];
198
+ else if (arg === "--spur-bin" && argv[i + 1])
199
+ i++;
200
+ else {
201
+ writeErr(`${SESSION_TIMELINE_USAGE}
202
+ `);
203
+ return 2;
204
+ }
205
+ }
206
+ const resolved = resolveTranscript(env, projectsRoot, transcript);
207
+ if (!resolved.ok) {
208
+ write(`${JSON.stringify({ available: false, reason: resolved.reason })}
209
+ `);
210
+ return 0;
211
+ }
212
+ try {
213
+ const timeline = buildTimeline(readFileSync(resolved.path, "utf8").split(`
214
+ `), group);
215
+ write(`${JSON.stringify({ ...timeline, transcript: resolved.path })}
216
+ `);
217
+ return 0;
218
+ } catch (error) {
219
+ writeErr(`session-timeline: ${error.message}
220
+ `);
221
+ return 2;
222
+ }
223
+ }
224
+ process.exit(main(process.argv.slice(2)));
225
+ export {
226
+ resolveTranscript,
227
+ parseGroups,
228
+ main,
229
+ formatTokens,
230
+ formatTokenSplit,
231
+ formatDuration,
232
+ buildTimeline,
233
+ SESSION_TIMELINE_USAGE
234
+ };
@@ -0,0 +1,152 @@
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
+ resolveTranscript,
23
+ type Span,
24
+ sumSpans,
25
+ sumTokens,
26
+ } from '../lib/transcript';
27
+
28
+ export { formatDuration, formatTokenSplit, formatTokens, resolveTranscript } from '../lib/transcript';
29
+
30
+ /** A span plus its `M:SS` / `H:MM:SS` and token renderings, so the review table needs no arithmetic. */
31
+ export type Rendered<T> = T & { work: string; wait: string; token: string };
32
+
33
+ export interface Timeline {
34
+ available: true;
35
+ transcript?: string;
36
+ segments: Rendered<Span & { index: number; start: string; prompt: string }>[];
37
+ stages?: Rendered<Span & { segments: string }>[];
38
+ totals: Rendered<Span & { elapsedMs: number; elapsed: string }>;
39
+ skippedLines: number;
40
+ }
41
+
42
+ const PROMPT_EXCERPT = 80;
43
+
44
+ const render = <T extends Span>(span: T): Rendered<T> => ({
45
+ ...span,
46
+ work: formatDuration(span.workMs),
47
+ wait: formatDuration(span.waitMs),
48
+ token: formatTokenSplit(span.tokens),
49
+ });
50
+
51
+ /** "1-3,4" → [[1,3],[4,4]]; 1-based, ascending, in range, non-overlapping. */
52
+ export function parseGroups(spec: string, count: number): [number, number][] {
53
+ let next = 1;
54
+ return spec.split(',').map((part) => {
55
+ const [a = Number.NaN, b = a] = part.trim().split('-').map(Number);
56
+ if (!Number.isInteger(a) || !Number.isInteger(b) || a < next || b < a || b > count) {
57
+ throw new Error(`invalid --group "${part}" (segments 1-${count}, ascending, no overlap)`);
58
+ }
59
+ next = b + 1;
60
+ return [a, b] as [number, number];
61
+ });
62
+ }
63
+
64
+ interface Open extends Acc {
65
+ start: number;
66
+ prompt: string;
67
+ }
68
+
69
+ export function buildTimeline(lines: string[], group?: string): Timeline {
70
+ const open: Open[] = [];
71
+ const { rows, skippedLines } = parseRows(lines);
72
+
73
+ for (const [row, ts] of rows) {
74
+ const prompt = promptText(row);
75
+ if (prompt !== undefined) {
76
+ open.push({ start: ts, prompt, ...newAcc(ts) });
77
+ continue;
78
+ }
79
+ const seg = open.at(-1);
80
+ if (seg) accumulate(seg, row, ts); // rows before the first prompt are session preamble
81
+ }
82
+
83
+ const segments = open.map((seg, i) => {
84
+ const next = open[i + 1]?.start;
85
+ const idle = next === undefined ? 0 : Math.max(0, next - seg.last);
86
+ const oneLine = seg.prompt.replace(/\s+/g, ' ').trim();
87
+ return render({
88
+ index: i + 1,
89
+ start: new Date(seg.start).toISOString(),
90
+ prompt: oneLine.length > PROMPT_EXCERPT ? `${oneLine.slice(0, PROMPT_EXCERPT)}…` : oneLine,
91
+ workMs: seg.last - seg.start - seg.askMs,
92
+ waitMs: idle + seg.askMs,
93
+ toolCalls: seg.toolIds.size,
94
+ tokens: sumTokens([...seg.messages.values()]),
95
+ });
96
+ });
97
+
98
+ const first = open[0];
99
+ const last = open.at(-1);
100
+ const elapsedMs = first && last ? last.last - first.start : 0;
101
+ const timeline: Timeline = {
102
+ available: true,
103
+ segments,
104
+ totals: render({ ...sumSpans(segments), elapsedMs, elapsed: formatDuration(elapsedMs) }),
105
+ skippedLines,
106
+ };
107
+ if (group) {
108
+ timeline.stages = parseGroups(group, segments.length).map(([a, b]) =>
109
+ render({ ...sumSpans(segments.slice(a - 1, b)), segments: a === b ? `${a}` : `${a}-${b}` }),
110
+ );
111
+ }
112
+ return timeline;
113
+ }
114
+
115
+ export const SESSION_TIMELINE_USAGE = 'usage: session-timeline [--transcript <path>] [--group "1-3,4,..."]';
116
+
117
+ export function main(
118
+ argv: string[],
119
+ env: Record<string, string | undefined> = getEnvVars(),
120
+ write: (s: string) => void = (s) => process.stdout.write(s),
121
+ projectsRoot?: string,
122
+ writeErr: (s: string) => void = (s) => process.stderr.write(s),
123
+ ): number {
124
+ let transcript: string | undefined;
125
+ let group: string | undefined;
126
+ for (let i = 0; i < argv.length; i++) {
127
+ const arg = argv[i];
128
+ if (arg === '--transcript' && argv[i + 1]) transcript = argv[++i];
129
+ else if (arg === '--group' && argv[i + 1]) group = argv[++i];
130
+ else if (arg === '--spur-bin' && argv[i + 1])
131
+ i++; // 0482 R2: accepted, unused (no spur calls)
132
+ else {
133
+ writeErr(`${SESSION_TIMELINE_USAGE}\n`);
134
+ return 2;
135
+ }
136
+ }
137
+ const resolved = resolveTranscript(env, projectsRoot, transcript);
138
+ if (!resolved.ok) {
139
+ write(`${JSON.stringify({ available: false, reason: resolved.reason })}\n`);
140
+ return 0;
141
+ }
142
+ try {
143
+ const timeline = buildTimeline(readFileSync(resolved.path, 'utf8').split('\n'), group);
144
+ write(`${JSON.stringify({ ...timeline, transcript: resolved.path })}\n`);
145
+ return 0;
146
+ } catch (error) {
147
+ writeErr(`session-timeline: ${(error as Error).message}\n`);
148
+ return 2;
149
+ }
150
+ }
151
+
152
+ if (import.meta.main) process.exit(main(process.argv.slice(2)));
@@ -315,6 +315,11 @@ node "$RESIDUAL" fold <wbs>
315
315
  > for each dropped MET-matched scenario key and when the new rows match no feature scenario —
316
316
  > treat those warnings as a re-key instruction, not noise.
317
317
 
318
+ > **Keying rule for feature-linked tasks (1086/1089 re-audits).** Key AC rows by the exact feature
319
+ > scenario title (`R1 — …`): feature `Scenario:` titles are declared identities and satisfy L4
320
+ > scenario matching directly. Bare `AC1` fails `ac-identity` on Gherkin AC sections, and checklist
321
+ > labels truncate at the first colon (a `(req: …)` suffix), so the task's own AC title may not resolve.
322
+
318
323
  > **Do not write `## Review` directly, ever.** The `## Review` section is owned by the
319
324
  > `review` coordinator (`/sp:dev-review` → `sp:super-reviewer`), which merges
320
325
  > `functional-review` + `code-verification` review mode + `code-improvement` fragments. The
@@ -0,0 +1,63 @@
1
+ ---
2
+ name: fleet-join
3
+ description: >-
4
+ Join a project fleet as a guest occupant and pull its work. Use when the operator asks to
5
+ "join the fleet", "become a fleet guest", "pull fleet inbox work in this session", or when a
6
+ session should receive fleet messages without being supervised. Runs `spur agent join`, then
7
+ loops `spur agent wait --inbox`, and releases the lease with `spur agent leave`.
8
+ ---
9
+ # fleet-join — a live session as a fleet guest (G73, task 1081)
10
+
11
+ A guest is a session that **pulls** work: it is neither supervised nor restarted, it is addressed
12
+ by concrete id only, and it never receives a stage (`requiresCapabilities` stages can never route
13
+ to it — a guest carries no attestation). Joining registers an occupant row plus a heartbeat lease;
14
+ leaving (or an expired lease) releases it and returns any claimed messages to `pending`.
15
+
16
+ ## Join
17
+
18
+ ```bash
19
+ spur agent join --role reviewer --json # id defaults to <role>-g<n>
20
+ spur agent join --role coder --id my-session --json
21
+ ```
22
+
23
+ Pick the role whose work this session can actually do. `--session-id` defaults to
24
+ `CLAUDE_CODE_SESSION_ID`, which is what the Stop hook matches against — without it the hook cannot
25
+ find this session, though the `wait --inbox` loop still works.
26
+
27
+ ## Pull loop
28
+
29
+ `spur agent wait --inbox <id> --timeout 540000` returns **0** the moment queued work exists and
30
+ **1** on timeout. Each call heartbeats the lease, so a looping session stays joined. The Bash tool's
31
+ own ~10-minute limit is why the timeout stays under it: one bounded call per turn, re-armed after
32
+ the work is done.
33
+
34
+ 1. `spur agent wait --inbox <id> --timeout 540000 --json` → exit 0 means work is waiting.
35
+ 2. `spur message inbox --agent <id> --json` → read the messages.
36
+ 3. Do the work the message asks for.
37
+ 4. `spur message reply <msgId> "<result>"` → reply to each one.
38
+ 5. Re-arm: go back to step 1 while the operator wants this session in the fleet.
39
+
40
+ ## Leave
41
+
42
+ ```bash
43
+ spur agent leave --json # the guest joined by this session
44
+ spur agent leave my-session --json # an explicit id
45
+ ```
46
+
47
+ Leaving releases the lease immediately. If the session dies without leaving, the lease expires and
48
+ the reconciler pass retires the guest — pending work is never lost, it returns to `queued`.
49
+
50
+ ## Stop-hook delivery (opportunistic)
51
+
52
+ When the host fires a `Stop` hook and the host supports the block contract, the
53
+ `fleet-guest-stop` plugin hook delivers pending messages at turn end by replying
54
+ `{"decision":"block","reason":"Pending fleet messages: …"}` — only for a session whose id matches a
55
+ joined guest, never twice in one continued turn, and always fail-open. Claude Code supports this;
56
+ Codex `exec` on 0.160.0 does not deliver `Stop` at all (`docs/plans/2026-10-05-codex-guest-spike.md`),
57
+ so the pull loop above — not the hook — is what makes a guest work anywhere.
58
+
59
+ ## Boundaries
60
+
61
+ - Guests are **not** supervised: do not expect restart, pid ownership, or `--drain` injection.
62
+ - Never dispatch a stage to a guest id: `spur agent run --spec <guest-id>` is refused on purpose.
63
+ - Never hold the write slot through a guest: the guest lease is the only heartbeat (R5).
@@ -1,8 +1,8 @@
1
1
  ---
2
2
  name: session-review
3
- description: "Review the active coding-agent session: separate resolved from open issues with evidence, propose bounded improvements. With --triage, apply pure-doc / 1–2-line fixes inline and file the rest as one or more tasks. Triggers: review this session, wrap-up, triage findings."
3
+ description: "Review the active coding-agent session: separate resolved from open issues with evidence, propose bounded improvements. With --fix auto (alias --triage), apply pure-doc / 1–2-line fixes inline and file the rest as tasks; with --fix all, fix every actionable finding inline and file none. Triggers: review this session, wrap-up, triage findings."
4
4
  license: Apache-2.0
5
- version: 1.1.0
5
+ version: 1.2.0
6
6
  metadata:
7
7
  author: spur
8
8
  platforms: "claude-code,codex,openclaw,opencode,antigravity,pi"
@@ -31,7 +31,8 @@ cross-agent windows, recurrence, trends, or quantitative performance forensics.
31
31
  | Argument | Description | Default |
32
32
  | --- | --- | --- |
33
33
  | `[focus]` | Question or operation to emphasize. It changes ordering, not evidence collection. | full active session |
34
- | `--triage` | Opt into bounded remediation after the report: apply direct fixes (pure docs / one-to-two-line fixes) inline, then file all remaining actionable findings as one or more implement-ready tasks via the CLI-gated corpus surface. | off (report-only) |
34
+ | `--fix <none\|auto\|all>` | Remediation after the report. `none`: report-only. `auto`: apply direct fixes (pure docs / one-to-two-line fixes) inline, then file all remaining actionable findings as one or more implement-ready tasks via the CLI-gated corpus surface. `all`: fix every actionable finding inline and file no task. | `none` |
35
+ | `--triage` | `--triage` is an alias of `--fix auto`; an explicit `--fix` wins when both are passed. | off |
35
36
 
36
37
  ## Evidence boundary
37
38
 
@@ -46,14 +47,16 @@ cross-agent windows, recurrence, trends, or quantitative performance forensics.
46
47
  name the confirmation needed.
47
48
  - State `not available` when compaction or missing output removed evidence. Never reconstruct it from
48
49
  memory or claim a verification that did not run.
49
- - Derive timing only from timestamps and tool-call records visible in the active session. Use
50
- non-overlapping stages whose durations sum to elapsed time; render unavailable duration or call
51
- counts as `n/a` instead of estimating them.
50
+ - Derive timing and tokens only from the host transcript measurement (Protocol step 1): the visible
51
+ conversation carries no timestamps or usage, so never estimate them from it. Use non-overlapping
52
+ stages whose durations sum to elapsed time; render a value as `n/a` only when the measurement
53
+ reports `available: false` or omits it.
52
54
 
53
- ## Triage mode (`--triage`)
55
+ ## Fix modes (`--fix auto|all`)
54
56
 
55
- Report-only stays the default. With `--triage`, run the same evidence pass, then remediate in
56
- three buckets — never skip triage and start fixing from the raw findings list.
57
+ Report-only stays the default. With `--fix auto` or `--fix all`, run the same
58
+ evidence pass, then remediate in three buckets — never skip triage and start fixing from the raw
59
+ findings list. Steps 1–4 below are `--fix auto`; `--fix all` differs only as stated after them.
57
60
 
58
61
  1. **Triage every finding into exactly one bucket:**
59
62
  - **Direct fix** — pure documentation work, or a one-to-two-line fix with obvious, local,
@@ -74,10 +77,22 @@ three buckets — never skip triage and start fixing from the raw findings list.
74
77
  4. **Report** — add a Triage section: applied fixes (path + one-line what + verification) and
75
78
  each created task WBS. The Resolved/Open tables keep their evidence rules unchanged.
76
79
 
80
+ **`--fix all` (defer nothing).** Triage still runs, but the Task bucket is fixed inline instead of
81
+ filed, so the mode creates no task. Fix in dependency order with the same surgical-diff and
82
+ per-fix re-verification rule as direct fixes, then run the project gate once at the end. A finding
83
+ that needs a design or operator decision is asked through the host question tool, never deferred;
84
+ one that still cannot be fixed (failed re-verification, blocked by the environment) stays in Open
85
+ issues with its evidence. Notes stay report-only.
86
+
77
87
  ## Protocol
78
88
 
79
89
  1. **Resolve scope.** Review the active session from the operator's initiating request through the
80
- latest result. Use `[focus]` only to rank relevant material.
90
+ latest result. Use `[focus]` only to rank relevant material. Measure it with
91
+ `node "$(superskill script path sp session-timeline.mjs)" --group "<ranges>"`: one segment per
92
+ operator prompt, read-only from the host transcript. Run it once without `--group` to list the
93
+ segments, then map them to stages (`"1-3,4,5-6"`). It splits each segment into `work` and
94
+ operator `wait` (idle before the next prompt plus AskUserQuestion answer time) and counts tokens
95
+ once per message id.
81
96
  2. **Inventory outcomes.** List requested outcomes and classify each as completed, partial, blocked,
82
97
  or not attempted. Collapse repeated attempts into one outcome.
83
98
  3. **Classify issues.** For every material issue, distinguish resolved, open, or attempted. Record
@@ -108,26 +123,32 @@ State the overall result in one to three sentences, including partial or blocked
108
123
  Summarize elapsed time and, when supported by evidence, productive work, avoidable setup/recovery,
109
124
  and operator wait time. Then render non-overlapping stages:
110
125
 
111
- | Stage | Time | Tool calls | Assessment |
112
- | --- | ---: | ---: | --- |
126
+ | Stage | Time | Wait | Tool calls | Token | Assessment |
127
+ | --- | ---: | ---: | ---: | ---: | --- |
113
128
 
114
129
  Format durations as `M:SS` below one hour and `H:MM:SS` at one hour or above (`1:44`, not `1m44s`;
115
130
  `0:33`, not `33s`). Include a `Total` row when elapsed time is available. Keep operator approval
116
- waits separate from execution bottlenecks. Use `n/a` for any value not supported by the active
117
- session evidence.
131
+ waits separate from execution bottlenecks. Time is the stage `work` and Wait its operator `wait`.
132
+ Token renders `<total> / <non-cached>` in compact units (`28.7M / 717k`) — the measurement's
133
+ `token` field: total = input + cache creation + cache read + output; non-cached = total minus cache
134
+ read (fresh input, cache writes and output). These are measured session counts, not the 0912 baseline token/USD claims that
135
+ step 4 excludes. Use `n/a` for any value not supported by the measurement.
118
136
 
119
137
  ### Resolved issues
120
138
 
121
- | Issue | Root cause | Resolution | Evidence |
122
- | --- | --- | --- | --- |
139
+ | Issue | Root cause | Resolution | Evidence | Confidence |
140
+ | --- | --- | --- | --- | --- |
123
141
 
124
142
  Evidence names the tool result, verification command, or concrete repository state visible in the
125
- session. Do not list ordinary implementation steps as issues.
143
+ session. Do not list ordinary implementation steps as issues. **Confidence** is `HIGH`, `MEDIUM`, or
144
+ `LOW` for that row's claim, with its grounds in the cell (a level without grounds is not
145
+ verification); any API/library statement in a row cites the installed source as `path:line` or a URL,
146
+ and a claim the session could not ground is written `LOW` with the missing evidence named.
126
147
 
127
148
  ### Open issues and risks
128
149
 
129
- | Issue or risk | State | Evidence or confirmation needed |
130
- | --- | --- | --- |
150
+ | Issue or risk | State | Evidence or confirmation needed | Confidence |
151
+ | --- | --- | --- | --- |
131
152
 
132
153
  ### Process and environment improvements
133
154
 
@@ -137,13 +158,13 @@ observation adopted from the 0912 workflow baseline cites its anchor
137
158
  (`docs/reports/i31/0912-workflow-baseline.md`) and owner handoff, and carries no unsupported
138
159
  performance claim.
139
160
 
140
- ### Triage (only when `--triage` was passed)
161
+ ### Triage (only with `--fix auto|all`)
141
162
 
142
163
  | Applied fix / created task | Bucket | What + verification |
143
164
  | --- | --- | --- |
144
165
 
145
- One row per applied fix and one per created task (with its WBS). Omit the section entirely when
146
- `--triage` was not passed.
166
+ One row per applied fix and one per created task (with its WBS; `--fix all` has none). Omit the
167
+ section entirely under `--fix none`.
147
168
 
148
169
  ### Next actions
149
170
 
@@ -157,8 +178,9 @@ improvement. Use `None` when the session is complete and no follow-up is justifi
157
178
  comparison or recurrence classification, or emit a twelve-section forensic report; those belong
158
179
  to imported-history analysis.
159
180
  - Report-only by default: do not create or update corpus items or edit files. The single exception
160
- is `--triage` mode, which permits exactly two mutation classes — direct fixes from the triage
161
- bucket, and the triage tasks. Anything beyond that stays a proposal.
181
+ is `--fix auto|all`: `auto` permits exactly two mutation classes — direct fixes from the triage
182
+ bucket, and the triage tasks; `all` permits one — inline fixes of actionable findings. Anything
183
+ beyond that stays a proposal.
162
184
  - Do not turn a single low-impact observation into a new policy. Report it as a candidate until it
163
185
  recurs or demonstrates a high-impact contract violation.
164
186
 
@@ -49,6 +49,7 @@ Pick the noun, read its reference. Each Tier A and Tier B reference owns that no
49
49
  | **Tier B** | **self** | Self-management verbs: scaffold (`init`), database maintenance (`maintain`), schema migrations (`migrate`), local web server (`serve`), status overview (`status`); `self init` runs post-scaffold validation probes & layout classification | [references/self.md](references/self.md) |
50
50
  | **Tier B** | **history** | Import agent histories, aggregate forensic artifacts, render reports, and run the checkpoint-resumed daily pipeline | [references/history.md](references/history.md) |
51
51
  | **Tier B** | **projects** | Manage the local multi-project registry and start/stop project servers | [references/projects.md](references/projects.md) |
52
+ | **Tier B** | **decision** | Decision-catalog transport: list visible decisions, show a served contract, run a decision to a served outcome, audit catalog health | [references/decision.md](references/decision.md) |
52
53
  | **Tier C** | **help** | Commander-generated help command; not a Spur noun | Generated `--help` |
53
54
 
54
55
  **Execute-First Contract:** Load the noun reference first and execute Tier A or Tier B commands