@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/config/config.global.yaml +17 -0
- package/config/decisions/task-pipeline.yaml +55 -0
- package/config/plugin-scripts.json +10 -0
- package/config/rules/boundary/sp-runtime-path.yaml +2 -0
- package/config/rules/quality/coverage-gate.yaml +5 -3
- package/config/rules/strict/runtime-boundaries.yaml +11 -0
- package/config/rules/structure/protected-files.yaml +3 -0
- package/config/workflows/task-pipeline.yaml +178 -18
- package/package.json +9 -9
- package/plugins/sp/README.md +14 -1
- package/plugins/sp/agents/super-reviewer.md +25 -11
- package/plugins/sp/commands/dev-changelog.md +5 -4
- package/plugins/sp/commands/dev-review-session.md +12 -7
- package/plugins/sp/commands/dev-run.md +5 -2
- package/plugins/sp/commands/dev-runall.md +5 -2
- package/plugins/sp/hooks/agent-lifecycle.ts +155 -0
- package/plugins/sp/hooks/fleet-guest-stop.ts +180 -0
- package/plugins/sp/hooks/hooks.json +45 -1
- package/plugins/sp/hooks/pi/guard-extension.ts +21 -0
- package/plugins/sp/lib/history-anatomy.generated.mjs +5 -0
- package/plugins/sp/lib/idea-handoff.generated.mjs +34 -9
- package/plugins/sp/lib/inline-run.generated.d.mts +1 -0
- package/plugins/sp/lib/inline-run.generated.mjs +49 -19
- package/plugins/sp/lib/transcript.ts +160 -0
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/scripts/history-anatomy-cache.mjs +5 -0
- package/plugins/sp/scripts/inline-run-setup.mjs +21 -20
- package/plugins/sp/scripts/inline-run-setup.ts +25 -31
- package/plugins/sp/scripts/residual-scan.mjs +2 -7
- package/plugins/sp/scripts/residual-scan.ts +7 -8
- package/plugins/sp/scripts/run-summary.mjs +286 -0
- package/plugins/sp/scripts/run-summary.ts +241 -0
- package/plugins/sp/scripts/session-timeline.mjs +234 -0
- package/plugins/sp/scripts/session-timeline.ts +152 -0
- package/plugins/sp/skills/code-verification/SKILL.md +5 -0
- package/plugins/sp/skills/fleet-join/SKILL.md +63 -0
- package/plugins/sp/skills/session-review/SKILL.md +46 -24
- package/plugins/sp/skills/spur-cli/SKILL.md +1 -0
- package/plugins/sp/skills/spur-cli/references/agent.md +108 -6
- package/plugins/sp/skills/spur-cli/references/decision.md +97 -0
- package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +22 -0
- package/plugins/sp/skills/spur-composer/SKILL.md +27 -1
- package/plugins/sp/skills/spur-dev/references/dev-operations.md +28 -16
- package/plugins/sp/skills/spur-dev/references/execution-batch.md +64 -7
- package/plugins/sp/skills/spur-dev/references/execution-workflow.md +14 -0
- package/plugins/sp/skills/spur-dev/references/flag-glossary.md +16 -4
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +32 -7
- package/plugins/sp/skills/spur-dev/references/planning-workflow.md +6 -0
- package/schemas/decision-catalog.schema.json +226 -0
- package/schemas/spur-config.schema.json +25 -0
- package/spur.js +7440 -2424
- package/web/_astro/BoardApp.Bzr4OXrW.js +324 -0
- package/web/_astro/{BoardApp.i4eh_fHA.js → BoardApp.VeoxxLBy.js} +1 -1
- package/web/_astro/{TaskDetail.C5Cu8LdB.js → TaskDetail.LAyR_Pvn.js} +1 -1
- package/web/_astro/{arc.Bu5XpBEz.js → arc.f4e0OYLg.js} +1 -1
- package/web/_astro/{architectureDiagram-3BPJPVTR.9vHQTvhF.js → architectureDiagram-3BPJPVTR.SSIvxi-Z.js} +1 -1
- package/web/_astro/{blockDiagram-GPEHLZMM.Cn2Xl7D6.js → blockDiagram-GPEHLZMM.DSXIHOFf.js} +1 -1
- package/web/_astro/{c4Diagram-AAUBKEIU.7o0P_QDo.js → c4Diagram-AAUBKEIU.DG9Ubu85.js} +1 -1
- package/web/_astro/channel.CUFdtWhm.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.C3zZFsNt.js → chunk-2J33WTMH.C7QU88Qh.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.CNy_M7fO.js → chunk-4BX2VUAB.26Z94xBj.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.2TiPnLBL.js → chunk-55IACEB6.DYRiBkqw.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.5CGSyKAp.js → chunk-727SXJPM.D5Vfv5pe.js} +1 -1
- package/web/_astro/{chunk-AQP2D5EJ.w-ZIgfsK.js → chunk-AQP2D5EJ.DXHV47bN.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.BkcEyhKa.js → chunk-FMBD7UC4.D1suAfOs.js} +1 -1
- package/web/_astro/{chunk-ND2GUHAM.mxKKbBG4.js → chunk-ND2GUHAM.D6BmdHEY.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.cyA0z9aT.js → chunk-QZHKN3VN.rnPU-Ak-.js} +1 -1
- package/web/_astro/{classDiagram-4FO5ZUOK.CqtUzwhX.js → classDiagram-4FO5ZUOK.CvEfXIo1.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.CqtUzwhX.js → classDiagram-v2-Q7XG4LA2.CvEfXIo1.js} +1 -1
- package/web/_astro/{cose-bilkent-S5V4N54A.BExPTopI.js → cose-bilkent-S5V4N54A.DkMisLSm.js} +1 -1
- package/web/_astro/{cynefin-OW5HDTMX.Bq2i10Cn.js → cynefin-OW5HDTMX.C77Q-pF6.js} +1 -1
- package/web/_astro/{dagre-BM42HDAG.By_HsOuv.js → dagre-BM42HDAG.YaV8ce27.js} +1 -1
- package/web/_astro/{diagram-2AECGRRQ.BtU0b7hz.js → diagram-2AECGRRQ.CUyGEKge.js} +1 -1
- package/web/_astro/{diagram-5GNKFQAL.D00ztMdT.js → diagram-5GNKFQAL.CRgd8dgj.js} +1 -1
- package/web/_astro/{diagram-KO2AKTUF.DMVJWaUN.js → diagram-KO2AKTUF.Bmzys9hq.js} +1 -1
- package/web/_astro/{diagram-LMA3HP47.Cxqrjg5-.js → diagram-LMA3HP47.BvmJY4Lw.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.2lz_1B5h.js → diagram-OG6HWLK6.0hEhD30d.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35.BVQEuJ3T.js → erDiagram-TEJ5UH35.Bjt8lrnm.js} +1 -1
- package/web/_astro/{flowDiagram-I6XJVG4X.Qlyo90ky.js → flowDiagram-I6XJVG4X.CJZTjRDV.js} +1 -1
- package/web/_astro/{ganttDiagram-6RSMTGT7.DRwb8HHZ.js → ganttDiagram-6RSMTGT7._VqoVafc.js} +1 -1
- package/web/_astro/{gitGraphDiagram-PVQCEYII.YfoPthfS.js → gitGraphDiagram-PVQCEYII.CEDb7tVo.js} +1 -1
- package/web/_astro/index.Dg_3bOaJ.css +1 -0
- package/web/_astro/{infoDiagram-5YYISTIA.DvFREck1.js → infoDiagram-5YYISTIA.CgwifPpK.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.BzCj9Q0-.js → ishikawaDiagram-YF4QCWOH.B99wx2J1.js} +1 -1
- package/web/_astro/{journeyDiagram-JHISSGLW.BDMT_AFb.js → journeyDiagram-JHISSGLW.2hRwJbEo.js} +1 -1
- package/web/_astro/{kanban-definition-UN3LZRKU.D6Fh3gj9.js → kanban-definition-UN3LZRKU.BahJkKNC.js} +1 -1
- package/web/_astro/{linear.Dxw00fGQ.js → linear.DguUUe6V.js} +1 -1
- package/web/_astro/{mermaid.core.CsoxUWa0.js → mermaid.core.CvgfmqVr.js} +4 -4
- package/web/_astro/{mindmap-definition-RKZ34NQL.CN8oeHLu.js → mindmap-definition-RKZ34NQL.B3qEA3x9.js} +1 -1
- package/web/_astro/{pieDiagram-4H26LBE5.C7ADqQ_7.js → pieDiagram-4H26LBE5.FlWA-_ru.js} +1 -1
- package/web/_astro/{quadrantDiagram-W4KKPZXB.Cw9lKyHP.js → quadrantDiagram-W4KKPZXB.9bVn35X7.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.B1ni-z4q.js → requirementDiagram-4Y6WPE33.ChykIhct.js} +1 -1
- package/web/_astro/{sankeyDiagram-5OEKKPKP.BhexdjVr.js → sankeyDiagram-5OEKKPKP.Z2Y3hkYb.js} +1 -1
- package/web/_astro/{sequenceDiagram-3UESZ5HK.C3Rm7dpQ.js → sequenceDiagram-3UESZ5HK.J7JsHWDt.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.CkuMt27u.js → stateDiagram-AJRCARHV.DAPbiCSS.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.Dc_aazUA.js → stateDiagram-v2-BHNVJYJU.D2jvSi0q.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.ja7sUFhH.js → timeline-definition-PNZ67QCA.psW2ci29.js} +1 -1
- package/web/_astro/{vennDiagram-CIIHVFJN.BxIshX_K.js → vennDiagram-CIIHVFJN.DlILYVmg.js} +1 -1
- package/web/_astro/{wardleyDiagram-YWT4CUSO.DfHlC0sh.js → wardleyDiagram-YWT4CUSO.xNWwLJnn.js} +1 -1
- package/web/_astro/{xychartDiagram-2RQKCTM6.DSnhHwCh.js → xychartDiagram-2RQKCTM6.EU_BuH7V.js} +1 -1
- package/web/board-runtime.json +1 -1
- package/web/index.html +2 -2
- package/web/_astro/BoardApp.BVp-LYi6.js +0 -322
- package/web/_astro/channel.6IFIKBhu.js +0 -1
- 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
|
|
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.
|
|
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
|
-
| `--
|
|
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
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
##
|
|
55
|
+
## Fix modes (`--fix auto|all`)
|
|
54
56
|
|
|
55
|
-
Report-only stays the default. With `--
|
|
56
|
-
three buckets — never skip triage and start fixing from the raw
|
|
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.
|
|
117
|
-
|
|
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
|
|
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
|
|
146
|
-
|
|
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 `--
|
|
161
|
-
bucket, and the triage tasks
|
|
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
|