@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,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-transcript measurement primitives shared by session-timeline (session review) and
|
|
3
|
+
* run-summary (dev-run / dev-runall execution summary). Reads Claude Code JSONL rows read-only;
|
|
4
|
+
* tokens are counted once per message id (a response spans several records). Node builtins only.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
7
|
+
import { homedir } from 'node:os';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
|
|
10
|
+
export type Tokens = Record<'input' | 'cacheCreate' | 'cacheRead' | 'output', number>;
|
|
11
|
+
|
|
12
|
+
export interface Span {
|
|
13
|
+
workMs: number;
|
|
14
|
+
waitMs: number;
|
|
15
|
+
toolCalls: number;
|
|
16
|
+
tokens: Tokens;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type Block = Partial<Record<'type' | 'id' | 'name' | 'text' | 'tool_use_id', string>>;
|
|
20
|
+
|
|
21
|
+
export interface Row {
|
|
22
|
+
type?: string;
|
|
23
|
+
timestamp?: string;
|
|
24
|
+
isMeta?: boolean;
|
|
25
|
+
isCompactSummary?: boolean;
|
|
26
|
+
message?: { id?: string; content?: unknown; usage?: Record<string, number | undefined> };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Running measurement of one segment or window. */
|
|
30
|
+
export interface Acc {
|
|
31
|
+
last: number;
|
|
32
|
+
askMs: number;
|
|
33
|
+
asks: Map<string, number>;
|
|
34
|
+
toolIds: Set<string>;
|
|
35
|
+
messages: Map<string, Tokens>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const SESSION_ID = /^[A-Za-z0-9_-]+$/;
|
|
39
|
+
/** Host tools whose call→result gap is the operator answering, not the agent working. */
|
|
40
|
+
const OPERATOR_TOOLS = new Set(['AskUserQuestion']);
|
|
41
|
+
|
|
42
|
+
export const zeroTokens = (): Tokens => ({ input: 0, cacheCreate: 0, cacheRead: 0, output: 0 });
|
|
43
|
+
|
|
44
|
+
export function formatDuration(ms: number): string {
|
|
45
|
+
const s = Math.round(ms / 1000);
|
|
46
|
+
const h = Math.floor(s / 3600);
|
|
47
|
+
const m = Math.floor((s % 3600) / 60);
|
|
48
|
+
const ss = String(s % 60).padStart(2, '0');
|
|
49
|
+
return h > 0 ? `${h}:${String(m).padStart(2, '0')}:${ss}` : `${m}:${ss}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Compact token units: `950`, `41k`, `28.4M`. */
|
|
53
|
+
export function formatTokens(n: number): string {
|
|
54
|
+
if (n >= 999_500) return `${(n / 1e6).toFixed(1)}M`;
|
|
55
|
+
return n >= 1000 ? `${Math.round(n / 1000)}k` : String(n);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* `<total> / <non-cached>`: total = every input class + output; non-cached drops cache reads, the
|
|
60
|
+
* share re-served from the prompt cache, leaving the fresh input, cache writes and output.
|
|
61
|
+
*/
|
|
62
|
+
export function formatTokenSplit(t: Tokens | null): string {
|
|
63
|
+
if (!t) return 'n/a';
|
|
64
|
+
const nonCached = t.input + t.cacheCreate + t.output;
|
|
65
|
+
return `${formatTokens(nonCached + t.cacheRead)} / ${formatTokens(nonCached)}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Operator prompt text, or undefined for tool results, meta rows and compaction summaries. */
|
|
69
|
+
export function promptText(row: Row): string | undefined {
|
|
70
|
+
if (row.type !== 'user' || row.isMeta || row.isCompactSummary) return undefined;
|
|
71
|
+
const content = row.message?.content;
|
|
72
|
+
if (typeof content === 'string') return content;
|
|
73
|
+
if (!Array.isArray(content) || (content as Block[]).some((b) => b.type === 'tool_result')) return undefined;
|
|
74
|
+
return (content as Block[]).find((b) => b.type === 'text')?.text;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function sumTokens(all: Tokens[]): Tokens {
|
|
78
|
+
const total = zeroTokens();
|
|
79
|
+
for (const t of all) for (const k of Object.keys(total) as (keyof Tokens)[]) total[k] += t[k];
|
|
80
|
+
return total;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function sumSpans(spans: Span[]): Span {
|
|
84
|
+
return {
|
|
85
|
+
workMs: spans.reduce((n, s) => n + s.workMs, 0),
|
|
86
|
+
waitMs: spans.reduce((n, s) => n + s.waitMs, 0),
|
|
87
|
+
toolCalls: spans.reduce((n, s) => n + s.toolCalls, 0),
|
|
88
|
+
tokens: sumTokens(spans.map((s) => s.tokens)),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export const newAcc = (ts: number): Acc => ({
|
|
93
|
+
last: ts,
|
|
94
|
+
askMs: 0,
|
|
95
|
+
asks: new Map(),
|
|
96
|
+
toolIds: new Set(),
|
|
97
|
+
messages: new Map(),
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
/** Fold one timestamped non-prompt record into a segment or window. */
|
|
101
|
+
export function accumulate(acc: Acc, row: Row, ts: number): void {
|
|
102
|
+
acc.last = Math.max(acc.last, ts);
|
|
103
|
+
const blocks = Array.isArray(row.message?.content) ? (row.message.content as Block[]) : [];
|
|
104
|
+
for (const b of blocks) {
|
|
105
|
+
if (b.type === 'tool_use' && b.id) {
|
|
106
|
+
acc.toolIds.add(b.id);
|
|
107
|
+
if (b.name && OPERATOR_TOOLS.has(b.name)) acc.asks.set(b.id, ts);
|
|
108
|
+
} else if (b.type === 'tool_result' && b.tool_use_id && acc.asks.has(b.tool_use_id)) {
|
|
109
|
+
acc.askMs += ts - (acc.asks.get(b.tool_use_id) ?? ts);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const u = row.message?.usage;
|
|
113
|
+
// Split records of one response repeat its usage: keyed by message id, overwrite, never add.
|
|
114
|
+
if (row.type === 'assistant' && u && row.message?.id) {
|
|
115
|
+
acc.messages.set(row.message.id, {
|
|
116
|
+
input: u.input_tokens ?? 0,
|
|
117
|
+
cacheCreate: u.cache_creation_input_tokens ?? 0,
|
|
118
|
+
cacheRead: u.cache_read_input_tokens ?? 0,
|
|
119
|
+
output: u.output_tokens ?? 0,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Timestamped rows in file order; malformed lines counted, timestamp-less rows dropped. */
|
|
125
|
+
export function parseRows(lines: string[]): { rows: [Row, number][]; skippedLines: number } {
|
|
126
|
+
const rows: [Row, number][] = [];
|
|
127
|
+
let skippedLines = 0;
|
|
128
|
+
for (const line of lines) {
|
|
129
|
+
if (!line.trim()) continue;
|
|
130
|
+
let row: Row;
|
|
131
|
+
try {
|
|
132
|
+
row = JSON.parse(line) as Row;
|
|
133
|
+
} catch {
|
|
134
|
+
skippedLines++;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const ts = row.timestamp ? Date.parse(row.timestamp) : Number.NaN;
|
|
138
|
+
if (!Number.isNaN(ts)) rows.push([row, ts]);
|
|
139
|
+
}
|
|
140
|
+
return { rows, skippedLines };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export type Resolved = { ok: true; path: string } | { ok: false; reason: string };
|
|
144
|
+
|
|
145
|
+
export function resolveTranscript(
|
|
146
|
+
env: Record<string, string | undefined>,
|
|
147
|
+
projectsRoot = join(homedir(), '.claude', 'projects'),
|
|
148
|
+
override?: string,
|
|
149
|
+
): Resolved {
|
|
150
|
+
if (override) return existsSync(override) ? { ok: true, path: override } : { ok: false, reason: 'no transcript' };
|
|
151
|
+
const id = env.CLAUDE_CODE_SESSION_ID;
|
|
152
|
+
if (!id) return { ok: false, reason: 'no host session id (CLAUDE_CODE_SESSION_ID); pass --transcript <path>' };
|
|
153
|
+
if (!SESSION_ID.test(id)) return { ok: false, reason: 'refusing a session id with path characters' };
|
|
154
|
+
if (!existsSync(projectsRoot)) return { ok: false, reason: `no transcript root ${projectsRoot}` };
|
|
155
|
+
for (const dir of readdirSync(projectsRoot)) {
|
|
156
|
+
const path = join(projectsRoot, dir, `${id}.jsonl`);
|
|
157
|
+
if (existsSync(path)) return { ok: true, path };
|
|
158
|
+
}
|
|
159
|
+
return { ok: false, reason: `no transcript for session ${id}` };
|
|
160
|
+
}
|
package/plugins/sp/plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Spur — a local-first harness engineering toolkit that wraps mainstream coding agents with constraint checking, workflow orchestration, and history analytics.",
|
|
5
5
|
"extensions": {
|
|
6
6
|
"pi": ["./hooks/pi/guard-extension.ts"]
|
|
@@ -264,6 +264,7 @@ var FINDING_CATEGORIES = [
|
|
|
264
264
|
"telemetry",
|
|
265
265
|
"positive"
|
|
266
266
|
];
|
|
267
|
+
var FINDING_CONFIDENCES = ["high", "medium", "low"];
|
|
267
268
|
function checkReportStructure(reportMarkdown) {
|
|
268
269
|
const problems = [];
|
|
269
270
|
if (/TODO|PLACEHOLDER|FIXME|^\|\s*\|/im.test(reportMarkdown))
|
|
@@ -308,6 +309,10 @@ function checkReportStructure(reportMarkdown) {
|
|
|
308
309
|
if (!/(^|[\s`])P[123]([\s`.]|$)/.test(block) && !block.includes("symbolic-severity")) {
|
|
309
310
|
problems.push("finding-invalid-severity");
|
|
310
311
|
}
|
|
312
|
+
const confValue = block.match(/`confidence`\s*[:=]\s*`?([^`\n]*?)`?\s*(?:\n|$)/)?.[1]?.trim();
|
|
313
|
+
if (confValue !== undefined && confValue !== "" && !FINDING_CONFIDENCES.includes(confValue.toLowerCase())) {
|
|
314
|
+
problems.push(`finding-invalid-confidence:${confValue}`);
|
|
315
|
+
}
|
|
311
316
|
const catValue = block.match(/`category`\s*[:=]\s*`?([^`\n]+?)`?\s*(?:\n|$)/)?.[1];
|
|
312
317
|
if (catValue && !FINDING_CATEGORIES.includes(catValue.trim())) {
|
|
313
318
|
problems.push(`finding-invalid-category:${catValue.trim()}`);
|
|
@@ -18,7 +18,7 @@ function usage() {
|
|
|
18
18
|
const text = [
|
|
19
19
|
"Usage: bun plugins/sp/scripts/inline-run-setup.ts --run-id <id> --file <definition> [--spur-bin <path>]",
|
|
20
20
|
" bun plugins/sp/scripts/inline-run-setup.ts --fingerprint --task-file <path> [--feature-file <path>] [--spur-bin <path>]",
|
|
21
|
-
" bun plugins/sp/scripts/inline-run-setup.ts --action --run-id <id> --node <state> --kind <kind> --status <done|failed> --ok <true|false> --duration-ms <n> [--spur-bin <path>]",
|
|
21
|
+
" bun plugins/sp/scripts/inline-run-setup.ts --action --run-id <id> --node <state> --kind <kind> --status <done|failed> --ok <true|false> --duration-ms <n> [--estimated] [--spur-bin <path>]",
|
|
22
22
|
" bun plugins/sp/scripts/inline-run-setup.ts --actions-file <json-file> --run-id <id> [--spur-bin <path>] (1007 R5 batch trace emission)",
|
|
23
23
|
" bun plugins/sp/scripts/inline-run-setup.ts --close --run-id <id> --status <done|failed|paused> [--reason <terminal-reason>] [--spur-bin <path>]",
|
|
24
24
|
" bun plugins/sp/scripts/inline-run-setup.ts --persist-out --from <worktree-path> [--task-file <path>]... [--spur-bin <path>]",
|
|
@@ -55,22 +55,6 @@ function resolveAppEntry(spurBin) {
|
|
|
55
55
|
}
|
|
56
56
|
return { entry, portable: true };
|
|
57
57
|
}
|
|
58
|
-
async function readInstalledInventory(file, spurBin) {
|
|
59
|
-
const localCli = fileURLToPath(new URL("../../../apps/cli/src/index.ts", import.meta.url));
|
|
60
|
-
const bundle = fileURLToPath(new URL("../lib/inline-run.generated.mjs", import.meta.url));
|
|
61
|
-
const { splitLaunchCommand } = await import(bundle);
|
|
62
|
-
const launch = spurBin ? splitLaunchCommand(spurBin, 'inline-run-setup "spurBin"') : existsSync(localCli) ? { command: "bun", leadingArgs: [localCli] } : { command: "spur", leadingArgs: [] };
|
|
63
|
-
if ("error" in launch)
|
|
64
|
-
throw new Error(launch.error);
|
|
65
|
-
const result = spawnSync(launch.command, [...launch.leadingArgs, "workflow", "show", file, "--format", "todo", "--json"], { cwd: process.cwd(), encoding: "utf8", timeout: 30000, maxBuffer: 4194304 });
|
|
66
|
-
if (result.status !== 0) {
|
|
67
|
-
throw new Error(`could not resolve the workflow definition with the installed CLI: ${result.error?.message ?? result.stderr}`);
|
|
68
|
-
}
|
|
69
|
-
const value = JSON.parse(result.stdout);
|
|
70
|
-
if (value && typeof value === "object" && "ok" in value && "data" in value && value.ok === true)
|
|
71
|
-
return value.data;
|
|
72
|
-
return value;
|
|
73
|
-
}
|
|
74
58
|
var TERMINAL_REASONS = new Set([
|
|
75
59
|
"done",
|
|
76
60
|
"paused-operator",
|
|
@@ -92,7 +76,7 @@ async function main() {
|
|
|
92
76
|
process.exit(child.status ?? 1);
|
|
93
77
|
}
|
|
94
78
|
const flags = new Map;
|
|
95
|
-
let fingerprint = false, action = false, close = false, decide = false, persistOut = false;
|
|
79
|
+
let fingerprint = false, action = false, close = false, decide = false, persistOut = false, estimated = false;
|
|
96
80
|
const taskFiles = [];
|
|
97
81
|
let spurBin = getEnvVar("SPUR_BIN") ?? "";
|
|
98
82
|
const argv = process.argv.slice(2);
|
|
@@ -108,6 +92,8 @@ async function main() {
|
|
|
108
92
|
decide = true;
|
|
109
93
|
else if (flag === "--persist-out")
|
|
110
94
|
persistOut = true;
|
|
95
|
+
else if (flag === "--estimated")
|
|
96
|
+
estimated = true;
|
|
111
97
|
else if (flag === "--task-file")
|
|
112
98
|
taskFiles.push(argv[++i] ?? "");
|
|
113
99
|
else if (flag === "--spur-bin")
|
|
@@ -127,6 +113,8 @@ async function main() {
|
|
|
127
113
|
const okRaw = flags.get("--ok") ?? "";
|
|
128
114
|
const durationRaw = flags.get("--duration-ms") ?? "";
|
|
129
115
|
const actionsFile = flags.get("--actions-file") ?? "";
|
|
116
|
+
if (estimated && !action)
|
|
117
|
+
usage();
|
|
130
118
|
if (fingerprint) {
|
|
131
119
|
if (runId !== "" || file !== "" || taskFiles.length !== 1 || (taskFiles[0] ?? "").trim() === "")
|
|
132
120
|
usage();
|
|
@@ -207,7 +195,16 @@ async function main() {
|
|
|
207
195
|
const durationMs = Number(durationRaw);
|
|
208
196
|
if (durationRaw.trim() === "" || !Number.isFinite(durationMs) || durationMs < 0)
|
|
209
197
|
usage();
|
|
210
|
-
process.exit(await app2.runInlineRunTrace({
|
|
198
|
+
process.exit(await app2.runInlineRunTrace({
|
|
199
|
+
runId,
|
|
200
|
+
close: false,
|
|
201
|
+
node,
|
|
202
|
+
kind,
|
|
203
|
+
status,
|
|
204
|
+
ok: okRaw === "true",
|
|
205
|
+
durationMs,
|
|
206
|
+
...estimated ? { estimated: true } : {}
|
|
207
|
+
}));
|
|
211
208
|
}
|
|
212
209
|
if (runId.trim() === "" || file.trim() === "")
|
|
213
210
|
usage();
|
|
@@ -217,7 +214,11 @@ async function main() {
|
|
|
217
214
|
const app = await import(entry);
|
|
218
215
|
let inventory;
|
|
219
216
|
try {
|
|
220
|
-
inventory = await readInstalledInventory(
|
|
217
|
+
inventory = await app.readInstalledInventory({
|
|
218
|
+
file,
|
|
219
|
+
spurBin,
|
|
220
|
+
localCli: fileURLToPath(new URL("../../../apps/cli/src/index.ts", import.meta.url))
|
|
221
|
+
});
|
|
221
222
|
} catch (error) {
|
|
222
223
|
const message = `could not resolve the workflow definition: ${error instanceof Error ? error.message : String(error)}`;
|
|
223
224
|
app.writeInlineRunOutcome(runId, { ok: false, runId, error: message });
|
|
@@ -18,7 +18,7 @@ function usage(): never {
|
|
|
18
18
|
const text = [
|
|
19
19
|
'Usage: bun plugins/sp/scripts/inline-run-setup.ts --run-id <id> --file <definition> [--spur-bin <path>]',
|
|
20
20
|
' bun plugins/sp/scripts/inline-run-setup.ts --fingerprint --task-file <path> [--feature-file <path>] [--spur-bin <path>]',
|
|
21
|
-
' bun plugins/sp/scripts/inline-run-setup.ts --action --run-id <id> --node <state> --kind <kind> --status <done|failed> --ok <true|false> --duration-ms <n> [--spur-bin <path>]',
|
|
21
|
+
' bun plugins/sp/scripts/inline-run-setup.ts --action --run-id <id> --node <state> --kind <kind> --status <done|failed> --ok <true|false> --duration-ms <n> [--estimated] [--spur-bin <path>]',
|
|
22
22
|
' bun plugins/sp/scripts/inline-run-setup.ts --actions-file <json-file> --run-id <id> [--spur-bin <path>] (1007 R5 batch trace emission)',
|
|
23
23
|
' bun plugins/sp/scripts/inline-run-setup.ts --close --run-id <id> --status <done|failed|paused> [--reason <terminal-reason>] [--spur-bin <path>]',
|
|
24
24
|
' bun plugins/sp/scripts/inline-run-setup.ts --persist-out --from <worktree-path> [--task-file <path>]... [--spur-bin <path>]',
|
|
@@ -61,33 +61,6 @@ function resolveAppEntry(spurBin: string): { entry: string; portable: boolean }
|
|
|
61
61
|
return { entry, portable: true };
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
/** Let the selected CLI own config and layer resolution, then revalidate its snapshot in the app. */
|
|
65
|
-
async function readInstalledInventory(file: string, spurBin: string): Promise<unknown> {
|
|
66
|
-
const localCli = fileURLToPath(new URL('../../../apps/cli/src/index.ts', import.meta.url));
|
|
67
|
-
const bundle = fileURLToPath(new URL('../lib/inline-run.generated.mjs', import.meta.url));
|
|
68
|
-
const { splitLaunchCommand } = (await import(bundle)) as typeof import('../lib/inline-run.generated.mjs');
|
|
69
|
-
const launch = spurBin
|
|
70
|
-
? splitLaunchCommand(spurBin, 'inline-run-setup "spurBin"')
|
|
71
|
-
: existsSync(localCli)
|
|
72
|
-
? { command: 'bun', leadingArgs: [localCli] }
|
|
73
|
-
: { command: 'spur', leadingArgs: [] };
|
|
74
|
-
if ('error' in launch) throw new Error(launch.error);
|
|
75
|
-
const result = spawnSync(
|
|
76
|
-
launch.command,
|
|
77
|
-
[...launch.leadingArgs, 'workflow', 'show', file, '--format', 'todo', '--json'],
|
|
78
|
-
{ cwd: process.cwd(), encoding: 'utf8', timeout: 30_000, maxBuffer: 4 * 1024 * 1024 },
|
|
79
|
-
);
|
|
80
|
-
if (result.status !== 0) {
|
|
81
|
-
throw new Error(
|
|
82
|
-
`could not resolve the workflow definition with the installed CLI: ${result.error?.message ?? result.stderr}`,
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
|
-
const value: unknown = JSON.parse(result.stdout);
|
|
86
|
-
// Honor the existing optional JSON envelope without inventing another projection format.
|
|
87
|
-
if (value && typeof value === 'object' && 'ok' in value && 'data' in value && value.ok === true) return value.data;
|
|
88
|
-
return value;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
64
|
// 0937 R2 closed terminal-reason vocabulary for `--close`. COPIED from
|
|
92
65
|
// packages/app/src/workflow/terminal-reason.ts (no value import); parity test asserts equality.
|
|
93
66
|
const TERMINAL_REASONS = new Set([
|
|
@@ -116,7 +89,8 @@ async function main(): Promise<void> {
|
|
|
116
89
|
action = false,
|
|
117
90
|
close = false,
|
|
118
91
|
decide = false,
|
|
119
|
-
persistOut = false
|
|
92
|
+
persistOut = false,
|
|
93
|
+
estimated = false;
|
|
120
94
|
const taskFiles: string[] = [];
|
|
121
95
|
let spurBin = getEnvVar('SPUR_BIN') ?? '';
|
|
122
96
|
const argv = process.argv.slice(2);
|
|
@@ -127,6 +101,7 @@ async function main(): Promise<void> {
|
|
|
127
101
|
else if (flag === '--close') close = true;
|
|
128
102
|
else if (flag === '--decide') decide = true;
|
|
129
103
|
else if (flag === '--persist-out') persistOut = true;
|
|
104
|
+
else if (flag === '--estimated') estimated = true;
|
|
130
105
|
else if (flag === '--task-file') taskFiles.push(argv[++i] ?? '');
|
|
131
106
|
else if (flag === '--spur-bin') spurBin = argv[++i] ?? spurBin;
|
|
132
107
|
else if (flag !== undefined) flags.set(flag, argv[++i] ?? '');
|
|
@@ -145,6 +120,10 @@ async function main(): Promise<void> {
|
|
|
145
120
|
const durationRaw = flags.get('--duration-ms') ?? '';
|
|
146
121
|
const actionsFile = flags.get('--actions-file') ?? '';
|
|
147
122
|
|
|
123
|
+
// `--estimated` labels a host-reported duration the driver did NOT time around the action
|
|
124
|
+
// (1070 R2). Only `--action` carries one: every other mode is refused here, before any write.
|
|
125
|
+
if (estimated && !action) usage();
|
|
126
|
+
|
|
148
127
|
if (fingerprint) {
|
|
149
128
|
if (runId !== '' || file !== '' || taskFiles.length !== 1 || (taskFiles[0] ?? '').trim() === '') usage();
|
|
150
129
|
const app = (await import(resolveAppEntry(spurBin).entry)) as InlineApp;
|
|
@@ -218,7 +197,16 @@ async function main(): Promise<void> {
|
|
|
218
197
|
const durationMs = Number(durationRaw);
|
|
219
198
|
if (durationRaw.trim() === '' || !Number.isFinite(durationMs) || durationMs < 0) usage();
|
|
220
199
|
process.exit(
|
|
221
|
-
await app.runInlineRunTrace({
|
|
200
|
+
await app.runInlineRunTrace({
|
|
201
|
+
runId,
|
|
202
|
+
close: false,
|
|
203
|
+
node,
|
|
204
|
+
kind,
|
|
205
|
+
status,
|
|
206
|
+
ok: okRaw === 'true',
|
|
207
|
+
durationMs,
|
|
208
|
+
...(estimated ? { estimated: true } : {}),
|
|
209
|
+
}),
|
|
222
210
|
);
|
|
223
211
|
}
|
|
224
212
|
|
|
@@ -228,7 +216,13 @@ async function main(): Promise<void> {
|
|
|
228
216
|
const app = (await import(entry)) as InlineApp;
|
|
229
217
|
let inventory: unknown;
|
|
230
218
|
try {
|
|
231
|
-
|
|
219
|
+
// The CLI walk lives in the app service (ADR-130 glue budget): the script passes only the
|
|
220
|
+
// paths it derives from its own module URL.
|
|
221
|
+
inventory = await app.readInstalledInventory({
|
|
222
|
+
file,
|
|
223
|
+
spurBin,
|
|
224
|
+
localCli: fileURLToPath(new URL('../../../apps/cli/src/index.ts', import.meta.url)),
|
|
225
|
+
});
|
|
232
226
|
} catch (error) {
|
|
233
227
|
const message = `could not resolve the workflow definition: ${error instanceof Error ? error.message : String(error)}`;
|
|
234
228
|
app.writeInlineRunOutcome(runId, { ok: false, runId, error: message });
|
|
@@ -427,12 +427,7 @@ function settleMode(opts, env, io) {
|
|
|
427
427
|
prior = JSON.parse(fs.readFileSync(residualsPath, "utf8"));
|
|
428
428
|
const deferred = scan.items.filter((i) => i.class === "deferrable");
|
|
429
429
|
if (deferred.length > 0 && prior.followUp === undefined) {
|
|
430
|
-
|
|
431
|
-
io.err(`residual-settle: ${wbs} deferrals pending but feature_id unknown; re-run: residual-scan settle ${wbs}
|
|
432
|
-
`);
|
|
433
|
-
return 0;
|
|
434
|
-
}
|
|
435
|
-
const args = ["task", "create", `Residuals from ${wbs}`, "--feature", task.featureId, "--skip-ready", "--json"];
|
|
430
|
+
const args = ["task", "create", `Residuals from ${wbs}`, "--skip-ready", "--json"];
|
|
436
431
|
const created = spur(env, opts.spurBin, args, opts.root);
|
|
437
432
|
let wbsNew = "";
|
|
438
433
|
try {
|
|
@@ -448,7 +443,7 @@ function settleMode(opts, env, io) {
|
|
|
448
443
|
return 0;
|
|
449
444
|
}
|
|
450
445
|
const rows = deferred.map((i) => `- ${i.id} \u2014 ${i.location}: ${i.text}`);
|
|
451
|
-
const head = `Source task: ${wbs} (feature ${task.featureId}) \u2014 deferred residuals filed by residual-scan settle.`;
|
|
446
|
+
const head = `Source task: ${wbs}${task.featureId === "" ? "" : ` (feature ${task.featureId})`} \u2014 deferred residuals filed by residual-scan settle (unlinked: a deferral must not hold the completing feature open).`;
|
|
452
447
|
const bgFile = join2(runDir, `${wbs}-residual-background.md`);
|
|
453
448
|
fs.writeFileSync(bgFile, `${[head, "", ...rows].join(`
|
|
454
449
|
`)}
|
|
@@ -170,13 +170,12 @@ function settleMode(opts: ParsedArgs, env: ScanEnv, io: ScanIo): number {
|
|
|
170
170
|
if (fs.existsSync(residualsPath)) prior = JSON.parse(fs.readFileSync(residualsPath, 'utf8'));
|
|
171
171
|
const deferred = scan.items.filter((i) => i.class === 'deferrable');
|
|
172
172
|
if (deferred.length > 0 && prior.followUp === undefined) {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
}
|
|
179
|
-
const args = ['task', 'create', `Residuals from ${wbs}`, '--feature', task.featureId, '--skip-ready', '--json'];
|
|
173
|
+
// The follow-up is filed UNLINKED: carrying a feature edge to the feature whose run produced
|
|
174
|
+
// it made that feature's done-gate fail with `L4.verifying-incomplete-tasks` the moment a
|
|
175
|
+
// residual was deferred (2026-10-04 session, E72/1086→1087), so a deferral must not be able
|
|
176
|
+
// to block the completing feature. The source task and its feature stay named in the
|
|
177
|
+
// Background, which keeps the traceability without the lifecycle coupling.
|
|
178
|
+
const args = ['task', 'create', `Residuals from ${wbs}`, '--skip-ready', '--json'];
|
|
180
179
|
const created = spur(env, opts.spurBin, args, opts.root);
|
|
181
180
|
let wbsNew = '';
|
|
182
181
|
try {
|
|
@@ -191,7 +190,7 @@ function settleMode(opts: ParsedArgs, env: ScanEnv, io: ScanIo): number {
|
|
|
191
190
|
return 0;
|
|
192
191
|
}
|
|
193
192
|
const rows = deferred.map((i) => `- ${i.id} — ${i.location}: ${i.text}`);
|
|
194
|
-
const head = `Source task: ${wbs} (feature ${task.featureId}) — deferred residuals filed by residual-scan settle.`;
|
|
193
|
+
const head = `Source task: ${wbs}${task.featureId === '' ? '' : ` (feature ${task.featureId})`} — deferred residuals filed by residual-scan settle (unlinked: a deferral must not hold the completing feature open).`;
|
|
195
194
|
const bgFile = join(runDir, `${wbs}-residual-background.md`);
|
|
196
195
|
fs.writeFileSync(bgFile, `${[head, '', ...rows].join('\n')}\n`);
|
|
197
196
|
const updArgs = ['task', 'update', wbsNew, '--section', 'Background', '--from-file', bgFile];
|