@gobing-ai/spur 0.4.8 → 0.4.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/config/pipeline-budgets.json +2 -2
- package/config/rules/boundary/config-loading-ownership.yaml +1 -0
- package/config/rules/boundary/sp-plugin-standalone.yaml +1 -1
- package/config/rules/boundary/sp-script-placement.yaml +1 -1
- package/config/rules/docs/sourced-api-claims.yaml +45 -7
- package/config/rules/strict/runtime-boundaries.yaml +40 -1
- package/config/rules/structure/protected-files.yaml +1 -1
- package/config/rules/surface/check-cli-surface.yaml +33 -0
- package/config/rules/typescript/no-debugger.yaml +3 -0
- package/config/workflow-candidates.json +2 -2
- package/config/workflows/feature-verification.yaml +6 -11
- package/config/workflows/history-anatomy.yaml +14 -28
- package/config/workflows/idea-pipeline.yaml +6 -19
- package/config/workflows/pr-review.yaml +9 -16
- package/config/workflows/task-pipeline.yaml +16 -23
- package/config/workflows/wrapup-pipeline.yaml +21 -63
- package/package.json +3 -3
- package/plugins/sp/README.md +70 -37
- package/plugins/sp/agents/super-planner.md +5 -5
- package/plugins/sp/agents/super-reviewer.md +2 -2
- package/plugins/sp/commands/dev-changelog.md +1 -1
- package/plugins/sp/commands/dev-daily.md +1 -1
- package/plugins/sp/commands/dev-pr-review.md +1 -1
- package/plugins/sp/commands/dev-review-session.md +1 -1
- package/plugins/sp/commands/dev-runall.md +4 -4
- package/plugins/sp/commands/dev-verify.md +4 -6
- package/plugins/sp/commands/dev-verifyall.md +4 -5
- package/plugins/sp/hooks/hooks.json +11 -1
- package/plugins/sp/lib/context-post-tool.ts +1 -1
- package/plugins/sp/lib/context-session-start.ts +1 -1
- package/plugins/sp/lib/context-session-stop.ts +1 -1
- package/plugins/sp/lib/destructive-policy.ts +65 -1
- package/plugins/sp/plugin.json +2 -5
- package/plugins/sp/{hooks → scripts/hooks}/agent-lifecycle.ts +1 -1
- package/plugins/sp/scripts/hooks/careful-guard.ts +94 -0
- package/plugins/sp/{hooks → scripts/hooks}/context-post-tool.ts +2 -2
- package/plugins/sp/{hooks → scripts/hooks}/context-session-start.ts +1 -1
- package/plugins/sp/{hooks → scripts/hooks}/context-session-stop.ts +1 -1
- package/plugins/sp/{hooks → scripts/hooks}/fleet-guest-stop.ts +6 -4
- package/plugins/sp/{hooks → scripts/hooks}/task-write-guard.ts +38 -21
- package/plugins/sp/skills/code-implementation/SKILL.md +1 -1
- package/plugins/sp/skills/code-verification/SKILL.md +8 -8
- package/plugins/sp/skills/code-verification/references/secu-review.md +4 -3
- package/plugins/sp/skills/code-verification/references/verdict-schema.md +9 -1
- package/plugins/sp/skills/daily-summary/SKILL.md +4 -5
- package/plugins/sp/skills/dogfood-testing/SKILL.md +9 -9
- package/plugins/sp/skills/dogfood-testing/references/report-template.md +1 -1
- package/plugins/sp/skills/history-anatomy/references/modes.md +2 -2
- package/plugins/sp/skills/next-router/references/routing-table.md +1 -1
- package/plugins/sp/skills/parallel-execution/SKILL.md +1 -1
- package/plugins/sp/skills/pr-reviewing/SKILL.md +14 -15
- package/plugins/sp/skills/session-review/SKILL.md +3 -3
- package/plugins/sp/skills/source-driven-development/SKILL.md +7 -4
- package/plugins/sp/skills/spur-check/SKILL.md +14 -13
- package/plugins/sp/skills/spur-cli/SKILL.md +1 -1
- package/plugins/sp/skills/spur-cli/references/agent.md +12 -0
- package/plugins/sp/skills/spur-cli/references/builder.md +27 -6
- package/plugins/sp/skills/spur-cli/references/features.md +6 -1
- package/plugins/sp/skills/spur-cli/references/history.md +3 -0
- package/plugins/sp/skills/spur-cli/references/self.md +5 -3
- package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +3 -0
- package/plugins/sp/skills/spur-cli/references/tasks.md +13 -3
- package/plugins/sp/skills/spur-cli/references/workflows.md +36 -1
- package/plugins/sp/skills/spur-dev/SKILL.md +1 -1
- package/plugins/sp/skills/spur-dev/references/cross-cutting.md +1 -1
- package/plugins/sp/skills/spur-dev/references/dev-operations.md +12 -12
- package/plugins/sp/skills/spur-dev/references/execution-batch-report.md +10 -11
- package/plugins/sp/skills/spur-dev/references/execution-batch.md +16 -17
- package/plugins/sp/skills/spur-dev/references/execution-parallel-isolation.md +1 -1
- package/plugins/sp/skills/spur-dev/references/execution-worktree-landing.md +8 -11
- package/plugins/sp/skills/spur-dev/references/execution-worktree-setup.md +6 -8
- package/plugins/sp/skills/spur-dev/references/gate-checklists.md +1 -1
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +33 -40
- package/plugins/sp/skills/spur-dev/references/structured-trace-emission.md +24 -24
- package/plugins/sp/skills/spur-doctor/SKILL.md +5 -4
- package/spur.js +41901 -30598
- package/config/plugin-scripts.json +0 -116
- package/config/script-placement-baseline.json +0 -33
- package/plugins/sp/hooks/careful-guard.ts +0 -74
- package/plugins/sp/hooks/pi/guard-extension.ts +0 -304
- package/plugins/sp/lib/dogfood-pipeline.ts +0 -265
- package/plugins/sp/lib/dogfood-report.ts +0 -278
- package/plugins/sp/lib/history-anatomy.generated.d.mts +0 -112
- package/plugins/sp/lib/history-anatomy.generated.mjs +0 -691
- package/plugins/sp/lib/idea-handoff.generated.d.mts +0 -17
- package/plugins/sp/lib/idea-handoff.generated.mjs +0 -1446
- package/plugins/sp/lib/inline-run-app.ts +0 -68
- package/plugins/sp/lib/inline-run.generated.d.mts +0 -37
- package/plugins/sp/lib/inline-run.generated.mjs +0 -3103
- package/plugins/sp/lib/plan-projection.generated.d.mts +0 -197
- package/plugins/sp/lib/plan-projection.generated.mjs +0 -982
- package/plugins/sp/lib/quality-gate.generated.d.mts +0 -139
- package/plugins/sp/lib/quality-gate.generated.mjs +0 -30510
- package/plugins/sp/lib/residual-scan.generated.d.mts +0 -93
- package/plugins/sp/lib/residual-scan.generated.mjs +0 -383
- package/plugins/sp/lib/run-summary-core.ts +0 -225
- package/plugins/sp/lib/spur-bin.ts +0 -36
- package/plugins/sp/lib/step-profile.generated.d.mts +0 -71
- package/plugins/sp/lib/step-profile.generated.mjs +0 -174
- package/plugins/sp/lib/transcript.ts +0 -112
- package/plugins/sp/scripts/batch-plan.mjs +0 -1096
- package/plugins/sp/scripts/batch-plan.ts +0 -145
- package/plugins/sp/scripts/batch-preflight.mjs +0 -423
- package/plugins/sp/scripts/batch-preflight.ts +0 -270
- package/plugins/sp/scripts/commit-guard.mjs +0 -226
- package/plugins/sp/scripts/commit-guard.ts +0 -248
- package/plugins/sp/scripts/daily-summary/daily-summary.mjs +0 -626
- package/plugins/sp/scripts/daily-summary/daily-summary.ts +0 -855
- package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.mjs +0 -211
- package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts +0 -110
- package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +0 -282
- package/plugins/sp/scripts/dogfood-testing/validate-report.ts +0 -98
- package/plugins/sp/scripts/feature-verification-steps.mjs +0 -82
- package/plugins/sp/scripts/feature-verification-steps.ts +0 -164
- package/plugins/sp/scripts/history-anatomy-cache.mjs +0 -931
- package/plugins/sp/scripts/history-anatomy-cache.ts +0 -247
- package/plugins/sp/scripts/idea-handoff.mjs +0 -32
- package/plugins/sp/scripts/idea-handoff.ts +0 -47
- package/plugins/sp/scripts/inline-run-dispatch.mjs +0 -131
- package/plugins/sp/scripts/inline-run-dispatch.ts +0 -115
- package/plugins/sp/scripts/inline-run-setup.mjs +0 -746
- package/plugins/sp/scripts/inline-run-setup.ts +0 -198
- package/plugins/sp/scripts/persist-out-check.mjs +0 -217
- package/plugins/sp/scripts/persist-out-check.ts +0 -249
- package/plugins/sp/scripts/pr-reviewing.mjs +0 -712
- package/plugins/sp/scripts/pr-reviewing.ts +0 -952
- package/plugins/sp/scripts/quality-gate.mjs +0 -30491
- package/plugins/sp/scripts/quality-gate.ts +0 -80
- package/plugins/sp/scripts/residual-scan.mjs +0 -634
- package/plugins/sp/scripts/residual-scan.ts +0 -248
- package/plugins/sp/scripts/run-summary.mjs +0 -758
- package/plugins/sp/scripts/run-summary.ts +0 -107
- package/plugins/sp/scripts/script-root.mjs +0 -129
- package/plugins/sp/scripts/script-root.ts +0 -199
- package/plugins/sp/scripts/session-timeline.mjs +0 -359
- package/plugins/sp/scripts/session-timeline.ts +0 -190
- package/plugins/sp/scripts/task-diffstat.mjs +0 -181
- package/plugins/sp/scripts/task-diffstat.ts +0 -219
- package/plugins/sp/scripts/workflow-step-profile.mjs +0 -338
- package/plugins/sp/scripts/workflow-step-profile.ts +0 -165
- package/plugins/sp/scripts/wrapup-drift-probe.mjs +0 -190
- package/plugins/sp/scripts/wrapup-drift-probe.ts +0 -261
- package/plugins/sp/scripts/wrapup-steps.mjs +0 -627
- package/plugins/sp/scripts/wrapup-steps.ts +0 -891
|
@@ -1,225 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* run-summary core — the rendering and host IO behind the `run-summary` CLI (task 1146; task 1178 R2).
|
|
3
|
-
*
|
|
4
|
-
* Split out of `plugins/sp/scripts/run-summary.ts` when the actor split and the batch roll-up pushed
|
|
5
|
-
* that script past the ADR-130 glue budget. The PURE aggregation and its types now live in
|
|
6
|
-
* `packages/app/src/workflow/run-summary.ts`, reached through the generated pure plan-projection
|
|
7
|
-
* entry; this module keeps only what belongs to the host: the close-step callback, the receipt read,
|
|
8
|
-
* Markdown rendering and durable/scratch report writing. Node builtins plus relative imports only;
|
|
9
|
-
* `import type` for the app projection it is handed.
|
|
10
|
-
*/
|
|
11
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
12
|
-
import { join } from 'node:path';
|
|
13
|
-
import type { WorkflowProgressProjection } from '@gobing-ai/spur-app';
|
|
14
|
-
import { getEnvVars } from './env';
|
|
15
|
-
import {
|
|
16
|
-
type ActorSplit,
|
|
17
|
-
buildRunSummary,
|
|
18
|
-
formatDuration,
|
|
19
|
-
formatTokenSplit,
|
|
20
|
-
type GateTime,
|
|
21
|
-
type Progress,
|
|
22
|
-
type RunSummary,
|
|
23
|
-
type SummaryRow,
|
|
24
|
-
sumTokens,
|
|
25
|
-
zeroTokens,
|
|
26
|
-
} from './plan-projection.generated.mjs';
|
|
27
|
-
import { parseRows, resolveTranscript } from './transcript';
|
|
28
|
-
|
|
29
|
-
// Task 1178 R2: the pure aggregation moved to `packages/app/src/workflow/run-summary.ts`. Re-export
|
|
30
|
-
// it from here so the adapter's existing surface (the `run-summary` script and its tests) keeps one
|
|
31
|
-
// import path; the implementation is delegated, never duplicated.
|
|
32
|
-
export {
|
|
33
|
-
type ActorSplit,
|
|
34
|
-
actorSplit,
|
|
35
|
-
buildRunSummary,
|
|
36
|
-
type GateTime,
|
|
37
|
-
type Progress,
|
|
38
|
-
type RunSummary,
|
|
39
|
-
type SummaryRow,
|
|
40
|
-
} from './plan-projection.generated.mjs';
|
|
41
|
-
|
|
42
|
-
export function renderSummaryMarkdown(summary: RunSummary): string {
|
|
43
|
-
const line = (r: SummaryRow, bold = false) => {
|
|
44
|
-
const stage = bold ? `**${r.stage}**` : r.stage;
|
|
45
|
-
return `| ${stage} | ${r.status} | ${r.time} | ${r.wait} | ${r.toolCalls ?? 'n/a'} | ${r.token} |`;
|
|
46
|
-
};
|
|
47
|
-
const a = summary.actors;
|
|
48
|
-
const window = summary.total.workMs + summary.total.waitMs;
|
|
49
|
-
const share = (ms: number): string => (window > 0 ? `${Math.round((ms / window) * 100)}%` : 'n/a');
|
|
50
|
-
const gate = summary.gate.ms === null ? `n/a (${summary.gate.reason})` : formatDuration(summary.gate.ms);
|
|
51
|
-
return [
|
|
52
|
-
'| Stage | Status | Time | Wait | Tool calls | Token (total / non-cached) |',
|
|
53
|
-
'| --- | --- | ---: | ---: | ---: | ---: |',
|
|
54
|
-
...summary.rows.map((r) => line(r)),
|
|
55
|
-
line(summary.total, true),
|
|
56
|
-
'',
|
|
57
|
-
'| Actor | Time | Share |',
|
|
58
|
-
'| --- | ---: | ---: |',
|
|
59
|
-
`| operator wait (longest ${formatDuration(a.longestOperatorWaitMs)}) | ${formatDuration(a.operatorMs)} | ${share(a.operatorMs)} |`,
|
|
60
|
-
`| model | ${formatDuration(a.modelMs)} | ${share(a.modelMs)} |`,
|
|
61
|
-
`| shell | ${formatDuration(a.shellMs)} | ${share(a.shellMs)} |`,
|
|
62
|
-
`| of which gate (informational) | ${gate} | — |`,
|
|
63
|
-
`| subagent | ${formatDuration(a.subagentMs)} | ${share(a.subagentMs)} |`,
|
|
64
|
-
`| other tools | ${formatDuration(a.otherToolMs)} | ${share(a.otherToolMs)} |`,
|
|
65
|
-
`| idle / unattributed | ${formatDuration(a.idleMs)} | ${share(a.idleMs)} |`,
|
|
66
|
-
'',
|
|
67
|
-
`Compactions: ${a.compactions} · Subagent errors: ${a.subagentErrors}`,
|
|
68
|
-
].join('\n');
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** One `--rollup` input: a close-written `<runId>-summary.json` plus its label. */
|
|
72
|
-
export interface RollupInput {
|
|
73
|
-
label: string;
|
|
74
|
-
summary: {
|
|
75
|
-
runId?: string;
|
|
76
|
-
startedAt?: string;
|
|
77
|
-
completedAt?: string;
|
|
78
|
-
total: SummaryRow;
|
|
79
|
-
actors: ActorSplit;
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/** Batch roll-up (1146 R3/AC5): one row per run's OWN window, then the sum, then the wall span. */
|
|
84
|
-
export function renderRollupMarkdown(rows: RollupInput[]): string {
|
|
85
|
-
const line = (label: string, r: SummaryRow, actors: ActorSplit, bold = false): string => {
|
|
86
|
-
const name = bold ? `**${label}**` : label;
|
|
87
|
-
return `| ${name} | ${r.status} | ${r.time} | ${r.wait} | ${r.toolCalls ?? 'n/a'} | ${r.token} | ${formatDuration(actors.operatorMs)} | ${formatDuration(actors.subagentMs)} |`;
|
|
88
|
-
};
|
|
89
|
-
const total = rows.reduce(
|
|
90
|
-
(acc, { summary }) => ({
|
|
91
|
-
workMs: acc.workMs + summary.total.workMs,
|
|
92
|
-
waitMs: acc.waitMs + summary.total.waitMs,
|
|
93
|
-
toolCalls: (acc.toolCalls ?? 0) + (summary.total.toolCalls ?? 0),
|
|
94
|
-
tokens: sumTokens([acc.tokens, summary.total.tokens ?? zeroTokens()]),
|
|
95
|
-
}),
|
|
96
|
-
{ workMs: 0, waitMs: 0, toolCalls: 0 as number | null, tokens: zeroTokens() },
|
|
97
|
-
);
|
|
98
|
-
const totalRow: SummaryRow = {
|
|
99
|
-
stage: 'Batch total',
|
|
100
|
-
status: '',
|
|
101
|
-
workMs: total.workMs,
|
|
102
|
-
waitMs: total.waitMs,
|
|
103
|
-
toolCalls: total.toolCalls,
|
|
104
|
-
tokens: total.tokens,
|
|
105
|
-
time: formatDuration(total.workMs),
|
|
106
|
-
wait: formatDuration(total.waitMs),
|
|
107
|
-
token: formatTokenSplit(total.tokens),
|
|
108
|
-
};
|
|
109
|
-
const totalActors: ActorSplit = {
|
|
110
|
-
operatorMs: 0,
|
|
111
|
-
longestOperatorWaitMs: 0,
|
|
112
|
-
modelMs: 0,
|
|
113
|
-
shellMs: 0,
|
|
114
|
-
subagentMs: 0,
|
|
115
|
-
otherToolMs: 0,
|
|
116
|
-
idleMs: 0,
|
|
117
|
-
compactions: 0,
|
|
118
|
-
subagentErrors: 0,
|
|
119
|
-
};
|
|
120
|
-
for (const { summary } of rows) {
|
|
121
|
-
totalActors.operatorMs += summary.actors.operatorMs;
|
|
122
|
-
totalActors.subagentMs += summary.actors.subagentMs;
|
|
123
|
-
totalActors.compactions += summary.actors.compactions;
|
|
124
|
-
totalActors.subagentErrors += summary.actors.subagentErrors;
|
|
125
|
-
}
|
|
126
|
-
const out = [
|
|
127
|
-
'| Run | Status | Time | Wait | Tool calls | Token (total / non-cached) | Operator wait | Subagent |',
|
|
128
|
-
'| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: |',
|
|
129
|
-
...rows.map(({ label, summary }) => line(label, summary.total, summary.actors)),
|
|
130
|
-
line('Batch total', totalRow, totalActors, true),
|
|
131
|
-
];
|
|
132
|
-
const starts = rows.map((r) => Date.parse(r.summary.startedAt ?? '')).filter((n) => !Number.isNaN(n));
|
|
133
|
-
const ends = rows.map((r) => Date.parse(r.summary.completedAt ?? '')).filter((n) => !Number.isNaN(n));
|
|
134
|
-
const overlaps =
|
|
135
|
-
starts.length === rows.length &&
|
|
136
|
-
rows.some((r, i) =>
|
|
137
|
-
rows.some(
|
|
138
|
-
(o, j) =>
|
|
139
|
-
i !== j &&
|
|
140
|
-
Date.parse(r.summary.startedAt ?? '') < Date.parse(o.summary.completedAt ?? '') &&
|
|
141
|
-
Date.parse(o.summary.startedAt ?? '') < Date.parse(r.summary.completedAt ?? ''),
|
|
142
|
-
),
|
|
143
|
-
);
|
|
144
|
-
if (overlaps && starts.length > 0 && ends.length > 0) {
|
|
145
|
-
out.push(
|
|
146
|
-
'',
|
|
147
|
-
`Wall span: ${new Date(Math.min(...starts)).toISOString()} → ${new Date(Math.max(...ends)).toISOString()}`,
|
|
148
|
-
);
|
|
149
|
-
}
|
|
150
|
-
return out.join('\n');
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/** 1146 R1: the close-step summarizer's input — a structural copy of the app's interface. */
|
|
154
|
-
export interface CloseSummaryRun {
|
|
155
|
-
readonly runId: string;
|
|
156
|
-
readonly startedAt: string | undefined;
|
|
157
|
-
readonly completedAt: string;
|
|
158
|
-
/** The app's full projection; the summary reads only the subset above. */
|
|
159
|
-
readonly progress: WorkflowProgressProjection;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* 1146 R1: write `.spur/run/<runId>-summary.{md,json}` and return the `.md` path (relative to cwd).
|
|
164
|
-
* Never throws: any failure (unresolved transcript, no timed attempts, anything else) leaves the
|
|
165
|
-
* `.md` holding exactly `Execution summary: n/a (<reason>)` and returns its path, so the close's
|
|
166
|
-
* exit code and JSON verdict are untouched.
|
|
167
|
-
*/
|
|
168
|
-
export function writeCloseSummary(
|
|
169
|
-
run: CloseSummaryRun,
|
|
170
|
-
opts: { env?: Record<string, string | undefined>; projectsRoot?: string; cwd?: string } = {},
|
|
171
|
-
): string {
|
|
172
|
-
const cwd = opts.cwd ?? process.cwd();
|
|
173
|
-
const runDir = join(cwd, '.spur', 'run');
|
|
174
|
-
const rel = join('.spur', 'run', `${run.runId}-summary.md`);
|
|
175
|
-
const abs = join(cwd, rel);
|
|
176
|
-
const na = (reason: string): string => {
|
|
177
|
-
mkdirSync(runDir, { recursive: true });
|
|
178
|
-
writeFileSync(abs, `Execution summary: n/a (${reason})\n`);
|
|
179
|
-
return rel;
|
|
180
|
-
};
|
|
181
|
-
try {
|
|
182
|
-
if (run.startedAt === undefined) return na('run row has no started_at');
|
|
183
|
-
const resolved = resolveTranscript(opts.env ?? getEnvVars(), opts.projectsRoot);
|
|
184
|
-
if (!resolved.ok) return na(resolved.reason);
|
|
185
|
-
const { rows, skippedLines } = parseRows(readFileSync(resolved.path, 'utf8').split('\n'));
|
|
186
|
-
const summary = buildRunSummary(rows, [{ progress: run.progress as unknown as Progress }], {
|
|
187
|
-
since: run.startedAt,
|
|
188
|
-
until: run.completedAt,
|
|
189
|
-
skippedLines,
|
|
190
|
-
});
|
|
191
|
-
if (summary.rows.length === 0) return na('no timed attempts');
|
|
192
|
-
mkdirSync(runDir, { recursive: true });
|
|
193
|
-
writeFileSync(abs, `${renderSummaryMarkdown(summary)}\n`);
|
|
194
|
-
writeFileSync(
|
|
195
|
-
join(runDir, `${run.runId}-summary.json`),
|
|
196
|
-
JSON.stringify(
|
|
197
|
-
{ ...summary, runId: run.runId, startedAt: run.startedAt, completedAt: run.completedAt },
|
|
198
|
-
null,
|
|
199
|
-
2,
|
|
200
|
-
),
|
|
201
|
-
);
|
|
202
|
-
return rel;
|
|
203
|
-
} catch (error) {
|
|
204
|
-
return na(error instanceof Error ? error.message : String(error));
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* 1146 R4: gate time comes only from THIS run's check receipt (`receipt.runId === runId`); a
|
|
210
|
-
* receipt belonging to another run is a cross-run scratch read (ADR-131) and renders n/a.
|
|
211
|
-
*/
|
|
212
|
-
export function gateFromReceipt(cwd: string, runId: string): GateTime {
|
|
213
|
-
const path = join(cwd, '.spur', 'run', `${runId}-check-receipt.json`);
|
|
214
|
-
if (!existsSync(path)) return { ms: null, reason: 'no matching check receipt' };
|
|
215
|
-
try {
|
|
216
|
-
const parsed = JSON.parse(readFileSync(path, 'utf8')) as { runId?: unknown; gateRuntimeMs?: unknown };
|
|
217
|
-
if (parsed.runId !== runId) return { ms: null, reason: 'check receipt belongs to another run' };
|
|
218
|
-
const ms = parsed.gateRuntimeMs;
|
|
219
|
-
if (typeof ms !== 'number' || !Number.isFinite(ms))
|
|
220
|
-
return { ms: null, reason: 'check receipt has no gate time' };
|
|
221
|
-
return { ms };
|
|
222
|
-
} catch {
|
|
223
|
-
return { ms: null, reason: 'check receipt is unreadable' };
|
|
224
|
-
}
|
|
225
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared spur-invocation helper for the sp plugin scripts (task 1007 R9, RF-architect-001).
|
|
3
|
-
*
|
|
4
|
-
* `spurCommand` and `defaultSpurBin` moved verbatim from `wrapup-steps.ts` and
|
|
5
|
-
* `workflow-step-profile.ts` so every surviving plugin script imports one copy (imported by
|
|
6
|
-
* relative path like `lib/env.ts`, so the standalone contract holds; not an app bundle
|
|
7
|
-
* because it carries no Spur-domain logic). Node/bun builtins and the vendored `./env`
|
|
8
|
-
* gateway only — bundled standalone by `superskill install`, so no `@gobing-ai/*` value
|
|
9
|
-
* imports (task 0669).
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { existsSync } from 'node:fs';
|
|
13
|
-
import { fileURLToPath } from 'node:url';
|
|
14
|
-
import { getEnvVar } from './env';
|
|
15
|
-
|
|
16
|
-
/** `spurBin` splits on whitespace into a command plus prefix args (so `bun x.ts` works). */
|
|
17
|
-
export function spurCommand(spurBin: string | undefined): { cmd: string; prefix: string[] } {
|
|
18
|
-
const parts = (spurBin ?? 'spur')
|
|
19
|
-
.trim()
|
|
20
|
-
.split(/\s+/)
|
|
21
|
-
.filter((p) => p.length > 0);
|
|
22
|
-
return { cmd: parts[0] ?? 'spur', prefix: parts.slice(1) };
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Resolve the spur CLI monorepo-safely (twin-safe resolution:
|
|
27
|
-
* self-contained and no plugin script imports another):
|
|
28
|
-
* --spur-bin > SPUR_BIN > monorepo-local CLI entry > PATH `spur`.
|
|
29
|
-
*/
|
|
30
|
-
export function defaultSpurBin(): string {
|
|
31
|
-
const fromEnv = getEnvVar('SPUR_BIN');
|
|
32
|
-
if (fromEnv) return fromEnv;
|
|
33
|
-
const local = fileURLToPath(new URL('../../../apps/cli/src/index.ts', import.meta.url));
|
|
34
|
-
if (existsSync(local)) return `bun ${local}`;
|
|
35
|
-
return 'spur';
|
|
36
|
-
}
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* GENERATED by scripts/commands/bundle-plugin-lib.ts — do not edit.
|
|
3
|
-
* Source of truth: packages/app/src/workflow/step-profile.ts (task 1005 R1).
|
|
4
|
-
*/
|
|
5
|
-
export declare interface ActionCostLike {
|
|
6
|
-
cacheHit: number | null;
|
|
7
|
-
}
|
|
8
|
-
export declare interface ActionCostAttributionLike {
|
|
9
|
-
exact: ActionCostLike | null;
|
|
10
|
-
estimated: ActionCostLike | null;
|
|
11
|
-
}
|
|
12
|
-
export declare interface TraceActionEvent {
|
|
13
|
-
kind: string;
|
|
14
|
-
node?: string;
|
|
15
|
-
actionKind?: string;
|
|
16
|
-
durationMs?: number | null;
|
|
17
|
-
startedAt?: string | null;
|
|
18
|
-
completedAt?: string | null;
|
|
19
|
-
invocation?: Record<string, string | number | boolean> | null;
|
|
20
|
-
cost?: ActionCostAttributionLike | null;
|
|
21
|
-
}
|
|
22
|
-
export declare interface StepProfileRun {
|
|
23
|
-
runId: string;
|
|
24
|
-
events: readonly TraceActionEvent[];
|
|
25
|
-
}
|
|
26
|
-
export declare interface TraceRunEntry {
|
|
27
|
-
runId: string;
|
|
28
|
-
isDryRun?: boolean;
|
|
29
|
-
status?: string;
|
|
30
|
-
workflowName?: string;
|
|
31
|
-
}
|
|
32
|
-
export declare type SessionMode = 'fresh' | 'resumed' | 'mixed';
|
|
33
|
-
export declare interface StepProfileExecution {
|
|
34
|
-
runId: string;
|
|
35
|
-
node: string;
|
|
36
|
-
actionKind: string;
|
|
37
|
-
durationMs: number | null;
|
|
38
|
-
idleGapMs: number | null;
|
|
39
|
-
session: 'fresh' | 'resumed' | null;
|
|
40
|
-
cacheHit: number | null;
|
|
41
|
-
}
|
|
42
|
-
export declare interface StepProfileRow {
|
|
43
|
-
node: string;
|
|
44
|
-
actionKind: string;
|
|
45
|
-
runs: number;
|
|
46
|
-
executions: number;
|
|
47
|
-
durationMs: { p50: number | null; max: number | null };
|
|
48
|
-
idleGapMs: { p50: number | null };
|
|
49
|
-
session: SessionMode | null;
|
|
50
|
-
cacheHit: { p50: number | null; known: number; of: number };
|
|
51
|
-
flags: string[];
|
|
52
|
-
}
|
|
53
|
-
export declare interface StepProfile {
|
|
54
|
-
workflow: string;
|
|
55
|
-
windowSec: number;
|
|
56
|
-
sampledRuns: number;
|
|
57
|
-
rows: StepProfileRow[];
|
|
58
|
-
}
|
|
59
|
-
export declare const DEFAULT_LAST: number;
|
|
60
|
-
export declare const DEFAULT_WINDOW_SEC: number;
|
|
61
|
-
export declare function nearestRankP50(values: readonly number[]): number | null;
|
|
62
|
-
export declare function extractExecutions(runId: string, events: readonly TraceActionEvent[]): StepProfileExecution[];
|
|
63
|
-
export declare function rowFlags(row: StepProfileRow, windowSec: number): string[];
|
|
64
|
-
export declare function buildRows(executions: readonly StepProfileExecution[], windowSec: number): StepProfileRow[];
|
|
65
|
-
export declare function buildStepProfile(input: {
|
|
66
|
-
workflow: string;
|
|
67
|
-
windowSec: number;
|
|
68
|
-
runs: readonly StepProfileRun[];
|
|
69
|
-
}): StepProfile;
|
|
70
|
-
export declare function nonDryRuns(entries: readonly TraceRunEntry[]): TraceRunEntry[];
|
|
71
|
-
export declare function formatStepProfileHuman(profile: StepProfile): string;
|
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
// packages/app/src/workflow/step-profile.ts
|
|
2
|
-
var DEFAULT_LAST = 20;
|
|
3
|
-
var DEFAULT_WINDOW_SEC = 300;
|
|
4
|
-
function nearestRankP50(values) {
|
|
5
|
-
if (values.length === 0)
|
|
6
|
-
return null;
|
|
7
|
-
const sorted = [...values].sort((a, b) => a - b);
|
|
8
|
-
return sorted[Math.ceil(sorted.length / 2) - 1] ?? null;
|
|
9
|
-
}
|
|
10
|
-
function numeric(value) {
|
|
11
|
-
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
12
|
-
}
|
|
13
|
-
function msBetween(from, to) {
|
|
14
|
-
if (typeof from !== "string" || typeof to !== "string")
|
|
15
|
-
return null;
|
|
16
|
-
const start = Date.parse(from);
|
|
17
|
-
const end = Date.parse(to);
|
|
18
|
-
if (!Number.isFinite(start) || !Number.isFinite(end))
|
|
19
|
-
return null;
|
|
20
|
-
return end - start;
|
|
21
|
-
}
|
|
22
|
-
function sessionOf(event) {
|
|
23
|
-
const value = event.invocation?.continue;
|
|
24
|
-
if (value === true)
|
|
25
|
-
return "resumed";
|
|
26
|
-
if (value === false)
|
|
27
|
-
return "fresh";
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
30
|
-
function cacheHitOf(event) {
|
|
31
|
-
return numeric(event.cost?.exact?.cacheHit);
|
|
32
|
-
}
|
|
33
|
-
function startKey(event) {
|
|
34
|
-
const parsed = typeof event.startedAt === "string" ? Date.parse(event.startedAt) : Number.NaN;
|
|
35
|
-
return Number.isFinite(parsed) ? parsed : Number.POSITIVE_INFINITY;
|
|
36
|
-
}
|
|
37
|
-
function extractExecutions(runId, events) {
|
|
38
|
-
const actions = events.filter((e) => e.kind === "action").sort((a, b) => {
|
|
39
|
-
const left = startKey(a);
|
|
40
|
-
const right = startKey(b);
|
|
41
|
-
return left === right ? 0 : left - right;
|
|
42
|
-
});
|
|
43
|
-
const executions = [];
|
|
44
|
-
for (const [index, event] of actions.entries()) {
|
|
45
|
-
const previous = index === 0 ? undefined : actions[index - 1];
|
|
46
|
-
executions.push({
|
|
47
|
-
runId,
|
|
48
|
-
node: event.node ?? "",
|
|
49
|
-
actionKind: event.actionKind ?? "",
|
|
50
|
-
durationMs: numeric(event.durationMs),
|
|
51
|
-
idleGapMs: msBetween(previous?.completedAt, event.startedAt),
|
|
52
|
-
session: sessionOf(event),
|
|
53
|
-
cacheHit: cacheHitOf(event)
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
return executions;
|
|
57
|
-
}
|
|
58
|
-
function foldSession(executions) {
|
|
59
|
-
const known = new Set(executions.map((e) => e.session).filter((s) => s !== null));
|
|
60
|
-
if (known.size === 0)
|
|
61
|
-
return null;
|
|
62
|
-
if (known.size === 1)
|
|
63
|
-
return [...known][0] ?? null;
|
|
64
|
-
return "mixed";
|
|
65
|
-
}
|
|
66
|
-
function rowFlags(row, windowSec) {
|
|
67
|
-
const wMs = windowSec * 1000;
|
|
68
|
-
const flags = [];
|
|
69
|
-
const agentRun = row.actionKind === "agent.run";
|
|
70
|
-
const resumed = row.session === "resumed" || row.session === "mixed";
|
|
71
|
-
const p50 = row.durationMs.p50;
|
|
72
|
-
if (!agentRun && p50 !== null && p50 > wMs)
|
|
73
|
-
flags.push("step-over-window");
|
|
74
|
-
const idle = row.idleGapMs.p50;
|
|
75
|
-
if (agentRun && resumed && idle !== null && idle > wMs)
|
|
76
|
-
flags.push("resume-after-idle");
|
|
77
|
-
const hit = row.cacheHit.p50;
|
|
78
|
-
if (agentRun && resumed && row.cacheHit.known > 0 && hit !== null && hit < 0.5)
|
|
79
|
-
flags.push("resume-cold-cache");
|
|
80
|
-
if (agentRun && p50 !== null && p50 > 2 * wMs)
|
|
81
|
-
flags.push("agent-run-over-2w");
|
|
82
|
-
return flags;
|
|
83
|
-
}
|
|
84
|
-
function buildRows(executions, windowSec) {
|
|
85
|
-
const grouped = new Map;
|
|
86
|
-
for (const execution of executions) {
|
|
87
|
-
const key = `${execution.node}\x00${execution.actionKind}`;
|
|
88
|
-
const bucket = grouped.get(key);
|
|
89
|
-
if (bucket === undefined)
|
|
90
|
-
grouped.set(key, [execution]);
|
|
91
|
-
else
|
|
92
|
-
bucket.push(execution);
|
|
93
|
-
}
|
|
94
|
-
const rows = [];
|
|
95
|
-
for (const bucket of grouped.values()) {
|
|
96
|
-
const first = bucket[0];
|
|
97
|
-
if (first === undefined)
|
|
98
|
-
continue;
|
|
99
|
-
const durations = bucket.map((e) => e.durationMs).filter((d) => d !== null);
|
|
100
|
-
const gaps = bucket.map((e) => e.idleGapMs).filter((g) => g !== null);
|
|
101
|
-
const hits = bucket.map((e) => e.cacheHit).filter((h) => h !== null);
|
|
102
|
-
const row = {
|
|
103
|
-
node: first.node,
|
|
104
|
-
actionKind: first.actionKind,
|
|
105
|
-
runs: new Set(bucket.map((e) => e.runId)).size,
|
|
106
|
-
executions: bucket.length,
|
|
107
|
-
durationMs: {
|
|
108
|
-
p50: nearestRankP50(durations),
|
|
109
|
-
max: durations.length === 0 ? null : Math.max(...durations)
|
|
110
|
-
},
|
|
111
|
-
idleGapMs: { p50: nearestRankP50(gaps) },
|
|
112
|
-
session: first.actionKind === "agent.run" ? foldSession(bucket) : null,
|
|
113
|
-
cacheHit: {
|
|
114
|
-
p50: nearestRankP50(hits),
|
|
115
|
-
known: hits.length,
|
|
116
|
-
of: bucket.length
|
|
117
|
-
},
|
|
118
|
-
flags: []
|
|
119
|
-
};
|
|
120
|
-
row.flags = rowFlags(row, windowSec);
|
|
121
|
-
rows.push(row);
|
|
122
|
-
}
|
|
123
|
-
return rows.sort((a, b) => a.node === b.node ? a.actionKind.localeCompare(b.actionKind) : a.node.localeCompare(b.node));
|
|
124
|
-
}
|
|
125
|
-
function buildStepProfile(input) {
|
|
126
|
-
const executions = input.runs.flatMap((run) => extractExecutions(run.runId, run.events));
|
|
127
|
-
return {
|
|
128
|
-
workflow: input.workflow,
|
|
129
|
-
windowSec: input.windowSec,
|
|
130
|
-
sampledRuns: input.runs.length,
|
|
131
|
-
rows: buildRows(executions, input.windowSec)
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
function nonDryRuns(entries) {
|
|
135
|
-
return entries.filter((e) => e.isDryRun !== true && typeof e.runId === "string" && e.runId.length > 0);
|
|
136
|
-
}
|
|
137
|
-
function seconds(value) {
|
|
138
|
-
return value === null ? "?" : `${(value / 1000).toFixed(1)}s`;
|
|
139
|
-
}
|
|
140
|
-
function ratio(value) {
|
|
141
|
-
return value === null ? "?" : value.toFixed(2);
|
|
142
|
-
}
|
|
143
|
-
function formatStepProfileHuman(profile) {
|
|
144
|
-
const lines = [
|
|
145
|
-
`step profile — ${profile.workflow} (window ${profile.windowSec}s, ${profile.sampledRuns} sampled runs)`
|
|
146
|
-
];
|
|
147
|
-
for (const row of profile.rows) {
|
|
148
|
-
lines.push([
|
|
149
|
-
row.node.padEnd(20),
|
|
150
|
-
row.actionKind.padEnd(14),
|
|
151
|
-
`${row.runs}/${row.executions}`.padEnd(8),
|
|
152
|
-
`p50=${seconds(row.durationMs.p50)}`.padEnd(12),
|
|
153
|
-
`max=${seconds(row.durationMs.max)}`.padEnd(12),
|
|
154
|
-
`idle=${seconds(row.idleGapMs.p50)}`.padEnd(12),
|
|
155
|
-
`session=${row.session ?? "?"}`.padEnd(15),
|
|
156
|
-
`cacheHit=${ratio(row.cacheHit.p50)} (${row.cacheHit.known}/${row.cacheHit.of})`.padEnd(20),
|
|
157
|
-
`flags=${row.flags.length === 0 ? "-" : row.flags.join(",")}`
|
|
158
|
-
].join(" "));
|
|
159
|
-
}
|
|
160
|
-
return `${lines.join(`
|
|
161
|
-
`)}
|
|
162
|
-
`;
|
|
163
|
-
}
|
|
164
|
-
export {
|
|
165
|
-
DEFAULT_LAST,
|
|
166
|
-
DEFAULT_WINDOW_SEC,
|
|
167
|
-
buildRows,
|
|
168
|
-
buildStepProfile,
|
|
169
|
-
extractExecutions,
|
|
170
|
-
formatStepProfileHuman,
|
|
171
|
-
nearestRankP50,
|
|
172
|
-
nonDryRuns,
|
|
173
|
-
rowFlags
|
|
174
|
-
};
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Host-transcript adapter shared by session-timeline (session review) and run-summary (dev-run /
|
|
3
|
-
* dev-runall execution summary). Owns the host side: resolving the active transcript path from the
|
|
4
|
-
* environment, detecting the host row format, and parsing raw JSONL lines into timestamped rows.
|
|
5
|
-
* Reads Claude Code / pi JSONL rows read-only. Node builtins only.
|
|
6
|
-
*
|
|
7
|
-
* Task 1178 R2: the pure measurement (row/token/actor aggregation and its normalized types) lives
|
|
8
|
-
* in `packages/app/src/workflow/run-summary.ts` and is reached through the generated pure
|
|
9
|
-
* plan-projection entry. Normalize once here (`parseRows`) and hand the rows to that owner; the
|
|
10
|
-
* re-exports below keep the plugin's one import path for the whole surface.
|
|
11
|
-
*/
|
|
12
|
-
import { existsSync, readdirSync } from 'node:fs';
|
|
13
|
-
import { homedir } from 'node:os';
|
|
14
|
-
import { join } from 'node:path';
|
|
15
|
-
import type { Row } from './plan-projection.generated.mjs';
|
|
16
|
-
|
|
17
|
-
export {
|
|
18
|
-
type Acc,
|
|
19
|
-
type ActorSplit,
|
|
20
|
-
accumulate,
|
|
21
|
-
actorSplit,
|
|
22
|
-
type Block,
|
|
23
|
-
buildRunSummary,
|
|
24
|
-
formatDuration,
|
|
25
|
-
formatTokenSplit,
|
|
26
|
-
formatTokens,
|
|
27
|
-
type GateTime,
|
|
28
|
-
isCompaction,
|
|
29
|
-
newAcc,
|
|
30
|
-
type Progress,
|
|
31
|
-
promptText,
|
|
32
|
-
type Row,
|
|
33
|
-
type RunSummary,
|
|
34
|
-
type Span,
|
|
35
|
-
type SummaryRow,
|
|
36
|
-
skillInvocationPrompt,
|
|
37
|
-
sumSpans,
|
|
38
|
-
sumTokens,
|
|
39
|
-
type Tokens,
|
|
40
|
-
zeroTokens,
|
|
41
|
-
} from './plan-projection.generated.mjs';
|
|
42
|
-
|
|
43
|
-
const SESSION_ID = /^[A-Za-z0-9_-]+$/;
|
|
44
|
-
/** pi roles that prove a `type:"message"` row is a pi transcript, not Claude's `user`/`assistant`. */
|
|
45
|
-
const PI_ROLES = new Set(['user', 'assistant', 'toolResult']);
|
|
46
|
-
|
|
47
|
-
/** First row matching a known shape decides: `type:"message"` with a pi role → pi, else Claude. */
|
|
48
|
-
export function sniffFormat(rows: Row[]): 'claude' | 'pi' | 'unknown' {
|
|
49
|
-
for (const row of rows) {
|
|
50
|
-
if (row.type === 'message' && PI_ROLES.has(row.message?.role ?? '')) return 'pi';
|
|
51
|
-
if (row.type === 'user' || row.type === 'assistant') return 'claude';
|
|
52
|
-
}
|
|
53
|
-
return 'unknown';
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** Timestamped rows in file order; malformed lines counted, timestamp-less rows dropped. */
|
|
57
|
-
export function parseRows(lines: string[]): {
|
|
58
|
-
rows: [Row, number][];
|
|
59
|
-
skippedLines: number;
|
|
60
|
-
format: 'claude' | 'pi' | 'unknown';
|
|
61
|
-
} {
|
|
62
|
-
const rows: [Row, number][] = [];
|
|
63
|
-
let skippedLines = 0;
|
|
64
|
-
for (const line of lines) {
|
|
65
|
-
if (!line.trim()) continue;
|
|
66
|
-
let row: Row;
|
|
67
|
-
try {
|
|
68
|
-
row = JSON.parse(line) as Row;
|
|
69
|
-
} catch {
|
|
70
|
-
skippedLines++;
|
|
71
|
-
continue;
|
|
72
|
-
}
|
|
73
|
-
const ts = row.timestamp ? Date.parse(row.timestamp) : Number.NaN;
|
|
74
|
-
if (!Number.isNaN(ts)) rows.push([row, ts]);
|
|
75
|
-
}
|
|
76
|
-
return { rows, skippedLines, format: sniffFormat(rows.map(([r]) => r)) };
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export type Resolved = { ok: true; path: string } | { ok: false; reason: string };
|
|
80
|
-
|
|
81
|
-
export function resolveTranscript(
|
|
82
|
-
env: Record<string, string | undefined>,
|
|
83
|
-
projectsRoot = join(homedir(), '.claude', 'projects'),
|
|
84
|
-
override?: string,
|
|
85
|
-
): Resolved {
|
|
86
|
-
if (override) return existsSync(override) ? { ok: true, path: override } : { ok: false, reason: 'no transcript' };
|
|
87
|
-
const id = env.CLAUDE_CODE_SESSION_ID;
|
|
88
|
-
if (!id) {
|
|
89
|
-
// 1138 R1: pi exports PI_SESSION_FILE (an exact path) and no Claude session id. Order is
|
|
90
|
-
// explicit override → Claude id → PI_SESSION_FILE; PI_SESSION_ID is never used for lookup
|
|
91
|
-
// because pi's filename embeds a timestamp, so the path is not derivable from the id.
|
|
92
|
-
const piFile = env.PI_SESSION_FILE;
|
|
93
|
-
if (piFile !== undefined && piFile !== '') {
|
|
94
|
-
return existsSync(piFile)
|
|
95
|
-
? { ok: true, path: piFile }
|
|
96
|
-
: { ok: false, reason: `PI_SESSION_FILE ${piFile} does not exist` };
|
|
97
|
-
}
|
|
98
|
-
return {
|
|
99
|
-
ok: false,
|
|
100
|
-
reason:
|
|
101
|
-
'no host session id; pass --transcript <path> (pi: ~/.pi/agent/sessions/<cwd-slug>/<file>.jsonl, ' +
|
|
102
|
-
'or set PI_SESSION_FILE)',
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
if (!SESSION_ID.test(id)) return { ok: false, reason: 'refusing a session id with path characters' };
|
|
106
|
-
if (!existsSync(projectsRoot)) return { ok: false, reason: `no transcript root ${projectsRoot}` };
|
|
107
|
-
for (const dir of readdirSync(projectsRoot)) {
|
|
108
|
-
const path = join(projectsRoot, dir, `${id}.jsonl`);
|
|
109
|
-
if (existsSync(path)) return { ok: true, path };
|
|
110
|
-
}
|
|
111
|
-
return { ok: false, reason: `no transcript for session ${id}` };
|
|
112
|
-
}
|