@gobing-ai/spur 0.4.2 → 0.4.3
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/decisions/maker-liveness.yaml +39 -0
- package/config/plugin-scripts.json +0 -4
- package/config/rules/README.md +1 -0
- package/config/rules/docs/sourced-api-claims.yaml +56 -12
- package/config/rules/strict/runtime-boundaries.yaml +7 -0
- package/config/script-placement-baseline.json +1 -19
- package/config/workflows/feature-lifecycle.yaml +46 -1
- package/config/workflows/feature-verification.yaml +11 -0
- package/package.json +9 -9
- package/plugins/sp/README.md +22 -16
- package/plugins/sp/agents/super-reviewer.md +43 -0
- package/plugins/sp/bunfig.toml +8 -5
- package/plugins/sp/hooks/careful-guard.ts +1 -1
- package/plugins/sp/hooks/context-post-tool.ts +7 -427
- package/plugins/sp/hooks/context-session-start.ts +5 -213
- package/plugins/sp/hooks/context-session-stop.ts +5 -191
- package/plugins/sp/hooks/pi/guard-extension.ts +6 -5
- package/plugins/sp/hooks/task-write-guard.ts +1 -1
- package/plugins/sp/{hooks → lib}/agent-hint.ts +1 -1
- package/plugins/sp/lib/context-post-tool.ts +432 -0
- package/plugins/sp/lib/context-session-start.ts +214 -0
- package/plugins/sp/lib/context-session-stop.ts +196 -0
- package/plugins/sp/lib/dogfood-pipeline.ts +265 -0
- package/plugins/sp/lib/dogfood-report.ts +278 -0
- package/plugins/sp/lib/idea-handoff.generated.mjs +3 -3
- package/plugins/sp/lib/inline-run-app.ts +40 -0
- package/plugins/sp/lib/inline-run.generated.d.mts +3 -3
- package/plugins/sp/lib/inline-run.generated.mjs +1347 -90
- package/plugins/sp/lib/plan-projection.generated.d.mts +150 -0
- package/plugins/sp/lib/plan-projection.generated.mjs +591 -1
- package/plugins/sp/lib/quality-gate.generated.mjs +38 -25
- package/plugins/sp/lib/residual-scan.generated.d.mts +7 -1
- package/plugins/sp/lib/residual-scan.generated.mjs +71 -6
- package/plugins/sp/lib/run-summary-core.ts +34 -370
- package/plugins/sp/lib/transcript.ts +37 -228
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/scripts/batch-plan.mjs +586 -0
- package/plugins/sp/scripts/batch-preflight.mjs +23 -1
- package/plugins/sp/scripts/batch-preflight.ts +35 -295
- package/plugins/sp/scripts/daily-summary/daily-summary.mjs +108 -87
- package/plugins/sp/scripts/daily-summary/daily-summary.ts +111 -104
- package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.mjs +2 -1
- package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts +7 -265
- package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +3 -0
- package/plugins/sp/scripts/dogfood-testing/validate-report.ts +6 -272
- package/plugins/sp/scripts/feature-verification-steps.mjs +7 -123
- package/plugins/sp/scripts/feature-verification-steps.ts +29 -181
- package/plugins/sp/scripts/inline-run-dispatch.mjs +27 -26
- package/plugins/sp/scripts/inline-run-dispatch.ts +1 -21
- package/plugins/sp/scripts/inline-run-setup.mjs +149 -122
- package/plugins/sp/scripts/inline-run-setup.ts +1 -21
- package/plugins/sp/scripts/quality-gate.mjs +14 -2
- package/plugins/sp/scripts/residual-scan.mjs +73 -6
- package/plugins/sp/scripts/residual-scan.ts +8 -5
- package/plugins/sp/scripts/run-summary.mjs +151 -97
- package/plugins/sp/scripts/run-summary.ts +7 -5
- package/plugins/sp/scripts/session-timeline.mjs +46 -19
- package/plugins/sp/scripts/task-diffstat.mjs +11 -1
- package/plugins/sp/scripts/task-diffstat.ts +8 -39
- package/plugins/sp/skills/code-implementation/SKILL.md +7 -1
- package/plugins/sp/skills/code-verification/references/secu-review.md +2 -1
- package/plugins/sp/skills/code-verification/references/verdict-schema.md +4 -0
- package/plugins/sp/skills/dogfood-testing/SKILL.md +2 -2
- package/plugins/sp/skills/next-feature/SKILL.md +1 -1
- package/plugins/sp/skills/parallel-execution/references/dispatch-surface.md +1 -1
- package/plugins/sp/skills/source-driven-development/SKILL.md +7 -4
- package/plugins/sp/skills/spur-cli/references/builder.md +3 -2
- package/plugins/sp/skills/spur-cli/references/decision.md +32 -7
- package/plugins/sp/skills/spur-cli/references/features.md +46 -5
- package/plugins/sp/skills/spur-dev/references/execution-batch-report.md +1 -1
- package/plugins/sp/skills/spur-dev/references/execution-batch.md +13 -3
- package/plugins/sp/skills/spur-dev/references/execution-worktree-landing.md +22 -0
- package/plugins/sp/skills/spur-dev/references/execution-worktree-setup.md +14 -0
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +29 -12
- package/spur.js +80681 -79296
- package/web/_astro/{BoardApp.B3GQZvj8.js → BoardApp.5IIl2O-z.js} +1 -1
- package/web/_astro/{BoardApp.BT7L2kSb.js → BoardApp.Drnx-KeM.js} +150 -150
- package/web/_astro/{TaskDetail.Cxov1_N_.js → TaskDetail.Ci4LNn9S.js} +1 -1
- package/web/_astro/{arc.vaT7tPOB.js → arc.j2EVkYVd.js} +1 -1
- package/web/_astro/{architectureDiagram-3BPJPVTR.DRMmTjkN.js → architectureDiagram-3BPJPVTR.DZWSFqEG.js} +1 -1
- package/web/_astro/{blockDiagram-GPEHLZMM.cla0LO_q.js → blockDiagram-GPEHLZMM.DE6GpXCN.js} +1 -1
- package/web/_astro/{c4Diagram-AAUBKEIU.D4YAIBKn.js → c4Diagram-AAUBKEIU.DoVR1G6B.js} +1 -1
- package/web/_astro/channel.DmdDxGsq.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.BrSJbMRZ.js → chunk-2J33WTMH.Do6-Gbyf.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.CuLMgKF5.js → chunk-4BX2VUAB.BYV1HGHx.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.zkvgu54F.js → chunk-55IACEB6.CvI1zkrc.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.DxWkACh8.js → chunk-727SXJPM.DNGSCRlX.js} +1 -1
- package/web/_astro/{chunk-AQP2D5EJ.42XrmIlH.js → chunk-AQP2D5EJ.DXhhgT5z.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.Bao2wMK9.js → chunk-FMBD7UC4.dzkRKP2u.js} +1 -1
- package/web/_astro/{chunk-ND2GUHAM.BhydojD1.js → chunk-ND2GUHAM.DujGLeEf.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.CmZ82g-H.js → chunk-QZHKN3VN.BWtIT3ZU.js} +1 -1
- package/web/_astro/{classDiagram-4FO5ZUOK.Blr83Gt_.js → classDiagram-4FO5ZUOK.DyApg0Nn.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.Blr83Gt_.js → classDiagram-v2-Q7XG4LA2.DyApg0Nn.js} +1 -1
- package/web/_astro/{cose-bilkent-S5V4N54A.qmfv0SPn.js → cose-bilkent-S5V4N54A.DJQB-u_l.js} +1 -1
- package/web/_astro/{cynefin-OW5HDTMX.BtELhXPR.js → cynefin-OW5HDTMX.bi3D_siV.js} +1 -1
- package/web/_astro/{dagre-BM42HDAG.AWmaAvXo.js → dagre-BM42HDAG.C-f7TrRg.js} +1 -1
- package/web/_astro/{diagram-2AECGRRQ.DdH34KDi.js → diagram-2AECGRRQ.DrPfjHQB.js} +1 -1
- package/web/_astro/{diagram-5GNKFQAL.BmNPix1k.js → diagram-5GNKFQAL.C9S4WUHO.js} +1 -1
- package/web/_astro/{diagram-KO2AKTUF.BNQd_Ppo.js → diagram-KO2AKTUF.Dzzye8QH.js} +1 -1
- package/web/_astro/{diagram-LMA3HP47.BAPgDi0e.js → diagram-LMA3HP47.DX-LRiof.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.DZ4zyLdC.js → diagram-OG6HWLK6.Z-kAKwIz.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35.Q1TySMmw.js → erDiagram-TEJ5UH35.D6dyClSv.js} +1 -1
- package/web/_astro/{flowDiagram-I6XJVG4X.BXhFBicG.js → flowDiagram-I6XJVG4X.Cx3q2cxD.js} +1 -1
- package/web/_astro/{ganttDiagram-6RSMTGT7.vHSgcuo2.js → ganttDiagram-6RSMTGT7.DU3gH6CT.js} +1 -1
- package/web/_astro/{gitGraphDiagram-PVQCEYII.Bi1f73C7.js → gitGraphDiagram-PVQCEYII.ISmCnJ87.js} +1 -1
- package/web/_astro/index.Do2lLdgo.css +1 -0
- package/web/_astro/{infoDiagram-5YYISTIA.DPvXZEWm.js → infoDiagram-5YYISTIA.DR6_iNOK.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.Cz829fsV.js → ishikawaDiagram-YF4QCWOH.D7wY-ZGA.js} +1 -1
- package/web/_astro/{journeyDiagram-JHISSGLW.enRnB9Me.js → journeyDiagram-JHISSGLW.DkP9-WK8.js} +1 -1
- package/web/_astro/{kanban-definition-UN3LZRKU.DnTNk6d2.js → kanban-definition-UN3LZRKU.wtgY98M2.js} +1 -1
- package/web/_astro/{linear.BzGnBwNL.js → linear.CI6djNi7.js} +1 -1
- package/web/_astro/{mermaid.core.CSbTpIVz.js → mermaid.core.CuQPxS8b.js} +4 -4
- package/web/_astro/{mindmap-definition-RKZ34NQL.DGJDy8zT.js → mindmap-definition-RKZ34NQL.5HNIEG7R.js} +1 -1
- package/web/_astro/{pieDiagram-4H26LBE5.HSPdi5_8.js → pieDiagram-4H26LBE5.CchhDFjE.js} +1 -1
- package/web/_astro/{quadrantDiagram-W4KKPZXB.6Gv8mz2Y.js → quadrantDiagram-W4KKPZXB.PYXXJPSJ.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.CnBCtFwa.js → requirementDiagram-4Y6WPE33.cUQuNAKU.js} +1 -1
- package/web/_astro/{sankeyDiagram-5OEKKPKP.5M8VWAu8.js → sankeyDiagram-5OEKKPKP.BO7MnRIA.js} +1 -1
- package/web/_astro/{sequenceDiagram-3UESZ5HK.DN41XN-U.js → sequenceDiagram-3UESZ5HK.D9bElyD6.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.ErOO3W8j.js → stateDiagram-AJRCARHV.DhGdRz7I.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.B_ZHICM-.js → stateDiagram-v2-BHNVJYJU.5GvLeAoL.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.CWdosr07.js → timeline-definition-PNZ67QCA.LtAT9haM.js} +1 -1
- package/web/_astro/{vennDiagram-CIIHVFJN.BHzYjNV5.js → vennDiagram-CIIHVFJN.DRth1yLA.js} +1 -1
- package/web/_astro/{wardleyDiagram-YWT4CUSO.h8x4IwPX.js → wardleyDiagram-YWT4CUSO.h9MlZfcr.js} +1 -1
- package/web/_astro/{xychartDiagram-2RQKCTM6.RTapW2X8.js → xychartDiagram-2RQKCTM6.DHqy9DYA.js} +1 -1
- package/web/index.html +2 -2
- package/config/proportional-route-table.ts +0 -155
- package/plugins/sp/scripts/daily-summary/logger.ts +0 -28
- package/web/_astro/channel.D4nG9YLf.js +0 -1
- package/web/_astro/index.BhSwN8yY.css +0 -1
- /package/plugins/sp/{hooks → lib}/context-owner.ts +0 -0
- /package/plugins/sp/{hooks → lib}/destructive-policy.ts +0 -0
- /package/plugins/sp/{hooks → lib}/task-file-policy.ts +0 -0
|
@@ -3,9 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
// plugins/sp/scripts/inline-run-setup.ts
|
|
5
5
|
import { spawnSync } from "child_process";
|
|
6
|
-
import {
|
|
7
|
-
import { dirname, join as join3, resolve } from "path";
|
|
8
|
-
import { fileURLToPath } from "url";
|
|
6
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
9
7
|
|
|
10
8
|
// plugins/sp/lib/env.ts
|
|
11
9
|
function getEnvVar(name, fallback) {
|
|
@@ -14,8 +12,30 @@ function getEnvVar(name, fallback) {
|
|
|
14
12
|
}
|
|
15
13
|
|
|
16
14
|
// plugins/sp/lib/inline-run-app.ts
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
import { existsSync } from "node:fs";
|
|
16
|
+
import { dirname, join, resolve } from "node:path";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
function resolveAppEntry(spurBin) {
|
|
19
|
+
const fallback = fileURLToPath(new URL("../../../apps/cli/src/index.ts", import.meta.url));
|
|
20
|
+
const candidates = spurBin !== "" ? [spurBin] : [fallback];
|
|
21
|
+
for (const candidate of candidates) {
|
|
22
|
+
const tokens = candidate.split(/\s+/).filter(Boolean);
|
|
23
|
+
const mainModule = [...tokens].reverse().find((t) => t.endsWith(".ts"));
|
|
24
|
+
if (mainModule === undefined || !existsSync(mainModule))
|
|
25
|
+
continue;
|
|
26
|
+
const repoRoot = resolve(dirname(mainModule), "..", "..", "..");
|
|
27
|
+
const appEntry = join(repoRoot, "packages", "app", "src", "index.ts");
|
|
28
|
+
if (existsSync(appEntry))
|
|
29
|
+
return { entry: appEntry, portable: false };
|
|
30
|
+
}
|
|
31
|
+
const entry = fileURLToPath(new URL("../lib/inline-run.generated.mjs", import.meta.url));
|
|
32
|
+
if (!existsSync(entry)) {
|
|
33
|
+
throw new Error("inline application bundle is missing — rebuild/install the sp plugin before running inline");
|
|
34
|
+
}
|
|
35
|
+
return { entry, portable: true };
|
|
36
|
+
}
|
|
37
|
+
async function loadInlineApp(spurBin, resolveAppEntry2, required) {
|
|
38
|
+
const { entry, portable } = resolveAppEntry2(spurBin);
|
|
19
39
|
const app = await import(entry);
|
|
20
40
|
const missing = portable ? required.filter((name) => typeof app[name] !== "function") : [];
|
|
21
41
|
if (missing.length > 0) {
|
|
@@ -30,16 +50,14 @@ function getEnvVars() {
|
|
|
30
50
|
}
|
|
31
51
|
|
|
32
52
|
// plugins/sp/lib/run-summary-core.ts
|
|
33
|
-
import { existsSync as
|
|
34
|
-
import { join as
|
|
53
|
+
import { existsSync as existsSync3, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
54
|
+
import { join as join3 } from "node:path";
|
|
35
55
|
|
|
36
|
-
// plugins/sp/lib/
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
var OPERATOR_TOOLS = new Set(["AskUserQuestion", "ask_user_question"]);
|
|
42
|
-
var PI_ROLES = new Set(["user", "assistant", "toolResult"]);
|
|
56
|
+
// plugins/sp/lib/plan-projection.generated.mjs
|
|
57
|
+
var NON_ABORTING_PREFLIGHT_CODES = new Set([
|
|
58
|
+
"L4.scenario-unverified",
|
|
59
|
+
"L4.verifying-incomplete-tasks"
|
|
60
|
+
]);
|
|
43
61
|
var zeroTokens = () => ({ input: 0, cacheCreate: 0, cacheRead: 0, output: 0 });
|
|
44
62
|
function formatDuration(ms) {
|
|
45
63
|
const s = Math.round(ms / 1000);
|
|
@@ -59,6 +77,26 @@ function formatTokenSplit(t) {
|
|
|
59
77
|
const nonCached = t.input + t.cacheCreate + t.output;
|
|
60
78
|
return `${formatTokens(nonCached + t.cacheRead)} / ${formatTokens(nonCached)}`;
|
|
61
79
|
}
|
|
80
|
+
var OPERATOR_TOOLS = new Set(["AskUserQuestion", "ask_user_question"]);
|
|
81
|
+
function isCompaction(row) {
|
|
82
|
+
return row.type === "compaction" || row.type === "system" && row.subtype === "compact_boundary";
|
|
83
|
+
}
|
|
84
|
+
function firstTextContent(row) {
|
|
85
|
+
const content = row.message?.content;
|
|
86
|
+
if (typeof content === "string")
|
|
87
|
+
return content;
|
|
88
|
+
if (!Array.isArray(content))
|
|
89
|
+
return;
|
|
90
|
+
return content.find((b) => b.type === "text")?.text;
|
|
91
|
+
}
|
|
92
|
+
function skillInvocationPrompt(text) {
|
|
93
|
+
const name = /^\s*<skill name="([^"]+)"/.exec(text)?.[1];
|
|
94
|
+
if (name === undefined)
|
|
95
|
+
return;
|
|
96
|
+
const close = text.lastIndexOf("</skill>");
|
|
97
|
+
const args = close < 0 ? "" : text.slice(close + "</skill>".length).trim();
|
|
98
|
+
return args === "" ? `/${name}` : `/${name} ${args}`;
|
|
99
|
+
}
|
|
62
100
|
function promptText(row) {
|
|
63
101
|
if (row.type === "custom_message") {
|
|
64
102
|
if (row.display === false)
|
|
@@ -83,22 +121,6 @@ function promptText(row) {
|
|
|
83
121
|
return;
|
|
84
122
|
return content.find((b) => b.type === "text")?.text;
|
|
85
123
|
}
|
|
86
|
-
function firstTextContent(row) {
|
|
87
|
-
const content = row.message?.content;
|
|
88
|
-
if (typeof content === "string")
|
|
89
|
-
return content;
|
|
90
|
-
if (!Array.isArray(content))
|
|
91
|
-
return;
|
|
92
|
-
return content.find((b) => b.type === "text")?.text;
|
|
93
|
-
}
|
|
94
|
-
function skillInvocationPrompt(text) {
|
|
95
|
-
const name = /^\s*<skill name="([^"]+)"/.exec(text)?.[1];
|
|
96
|
-
if (name === undefined)
|
|
97
|
-
return;
|
|
98
|
-
const close = text.lastIndexOf("</skill>");
|
|
99
|
-
const args = close < 0 ? "" : text.slice(close + "</skill>".length).trim();
|
|
100
|
-
return args === "" ? `/${name}` : `/${name} ${args}`;
|
|
101
|
-
}
|
|
102
124
|
function sumTokens(all) {
|
|
103
125
|
const total = zeroTokens();
|
|
104
126
|
for (const t of all)
|
|
@@ -170,72 +192,14 @@ function accumulate(acc, row, ts) {
|
|
|
170
192
|
});
|
|
171
193
|
}
|
|
172
194
|
}
|
|
173
|
-
function isCompaction(row) {
|
|
174
|
-
return row.type === "compaction" || row.type === "system" && row.subtype === "compact_boundary";
|
|
175
|
-
}
|
|
176
|
-
function sniffFormat(rows) {
|
|
177
|
-
for (const row of rows) {
|
|
178
|
-
if (row.type === "message" && PI_ROLES.has(row.message?.role ?? ""))
|
|
179
|
-
return "pi";
|
|
180
|
-
if (row.type === "user" || row.type === "assistant")
|
|
181
|
-
return "claude";
|
|
182
|
-
}
|
|
183
|
-
return "unknown";
|
|
184
|
-
}
|
|
185
|
-
function parseRows(lines) {
|
|
186
|
-
const rows = [];
|
|
187
|
-
let skippedLines = 0;
|
|
188
|
-
for (const line of lines) {
|
|
189
|
-
if (!line.trim())
|
|
190
|
-
continue;
|
|
191
|
-
let row;
|
|
192
|
-
try {
|
|
193
|
-
row = JSON.parse(line);
|
|
194
|
-
} catch {
|
|
195
|
-
skippedLines++;
|
|
196
|
-
continue;
|
|
197
|
-
}
|
|
198
|
-
const ts = row.timestamp ? Date.parse(row.timestamp) : Number.NaN;
|
|
199
|
-
if (!Number.isNaN(ts))
|
|
200
|
-
rows.push([row, ts]);
|
|
201
|
-
}
|
|
202
|
-
return { rows, skippedLines, format: sniffFormat(rows.map(([r]) => r)) };
|
|
203
|
-
}
|
|
204
|
-
function resolveTranscript(env, projectsRoot = join(homedir(), ".claude", "projects"), override) {
|
|
205
|
-
if (override)
|
|
206
|
-
return existsSync(override) ? { ok: true, path: override } : { ok: false, reason: "no transcript" };
|
|
207
|
-
const id = env.CLAUDE_CODE_SESSION_ID;
|
|
208
|
-
if (!id) {
|
|
209
|
-
const piFile = env.PI_SESSION_FILE;
|
|
210
|
-
if (piFile !== undefined && piFile !== "") {
|
|
211
|
-
return existsSync(piFile) ? { ok: true, path: piFile } : { ok: false, reason: `PI_SESSION_FILE ${piFile} does not exist` };
|
|
212
|
-
}
|
|
213
|
-
return {
|
|
214
|
-
ok: false,
|
|
215
|
-
reason: "no host session id; pass --transcript <path> (pi: ~/.pi/agent/sessions/<cwd-slug>/<file>.jsonl, " + "or set PI_SESSION_FILE)"
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
if (!SESSION_ID.test(id))
|
|
219
|
-
return { ok: false, reason: "refusing a session id with path characters" };
|
|
220
|
-
if (!existsSync(projectsRoot))
|
|
221
|
-
return { ok: false, reason: `no transcript root ${projectsRoot}` };
|
|
222
|
-
for (const dir of readdirSync(projectsRoot)) {
|
|
223
|
-
const path = join(projectsRoot, dir, `${id}.jsonl`);
|
|
224
|
-
if (existsSync(path))
|
|
225
|
-
return { ok: true, path };
|
|
226
|
-
}
|
|
227
|
-
return { ok: false, reason: `no transcript for session ${id}` };
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// plugins/sp/lib/run-summary-core.ts
|
|
231
195
|
var IDLE_GAP_MS = 300000;
|
|
232
196
|
var SHELL_TOOLS = new Set(["bash", "shell", "exec", "exec_command", "run_command", "command"]);
|
|
233
197
|
var SUBAGENT_TOOLS = new Set(["agent", "task", "invoke_subagent", "subagent", "dispatch_agent"]);
|
|
234
|
-
var
|
|
198
|
+
var LOWERCASE_OPERATOR_TOOLS = new Set(["askuserquestion", "ask_user_question"]);
|
|
235
199
|
var ACTOR_PRECEDENCE = ["operator", "subagent", "shell", "other"];
|
|
236
200
|
function actorOf(tool) {
|
|
237
201
|
const t = tool.toLowerCase().replace(/[^a-z_]/g, "");
|
|
238
|
-
if (
|
|
202
|
+
if (LOWERCASE_OPERATOR_TOOLS.has(t))
|
|
239
203
|
return "operator";
|
|
240
204
|
if (SUBAGENT_TOOLS.has(t))
|
|
241
205
|
return "subagent";
|
|
@@ -385,7 +349,8 @@ function summaryRow(stage, status, span, measured) {
|
|
|
385
349
|
token: formatTokenSplit(measured ? span.tokens : null)
|
|
386
350
|
};
|
|
387
351
|
}
|
|
388
|
-
function buildRunSummary(
|
|
352
|
+
function buildRunSummary(rows, inputs, opts = {}) {
|
|
353
|
+
const { since, until } = opts;
|
|
389
354
|
const windows = [];
|
|
390
355
|
for (const { label, progress } of inputs) {
|
|
391
356
|
const states = (progress.states ?? []).flatMap((s) => {
|
|
@@ -411,7 +376,6 @@ function buildRunSummary(lines, inputs, since, until) {
|
|
|
411
376
|
}
|
|
412
377
|
}
|
|
413
378
|
windows.sort((a, b) => a.start - b.start);
|
|
414
|
-
const { rows, skippedLines } = parseRows(lines);
|
|
415
379
|
const measure = (start, end) => {
|
|
416
380
|
const acc = newAcc(start);
|
|
417
381
|
for (const [row, ts] of rows)
|
|
@@ -459,9 +423,87 @@ function buildRunSummary(lines, inputs, since, until) {
|
|
|
459
423
|
total,
|
|
460
424
|
actors: actorSplit(rows, totalStart, totalEnd),
|
|
461
425
|
gate: { ms: null, reason: "no matching check receipt" },
|
|
462
|
-
skippedLines
|
|
426
|
+
skippedLines: opts.skippedLines ?? 0
|
|
463
427
|
};
|
|
464
428
|
}
|
|
429
|
+
var SENSITIVE_PREFIXES = ["drizzle/", "packages/config/", ".github/", "plugins/sp/hooks/"];
|
|
430
|
+
var SENSITIVE_GLOBS = ["apps/server/src/**/auth*", "**/*secret*", "**/*.sql"];
|
|
431
|
+
function globToRegex(glob) {
|
|
432
|
+
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replaceAll("**/", "\x00").replaceAll("**", "\x01").replaceAll("*", "[^/]*").replaceAll("\x00", "(?:.*/)?").replaceAll("\x01", ".*");
|
|
433
|
+
return new RegExp(`^${escaped}$`, "s");
|
|
434
|
+
}
|
|
435
|
+
var SENSITIVE_MATCHERS = [
|
|
436
|
+
...SENSITIVE_PREFIXES.map((prefix) => ({
|
|
437
|
+
label: prefix,
|
|
438
|
+
test: (path) => path.startsWith(prefix)
|
|
439
|
+
})),
|
|
440
|
+
...SENSITIVE_GLOBS.map((glob) => {
|
|
441
|
+
const regex = globToRegex(glob);
|
|
442
|
+
return { label: glob, test: (path) => regex.test(path) };
|
|
443
|
+
})
|
|
444
|
+
];
|
|
445
|
+
|
|
446
|
+
// plugins/sp/lib/transcript.ts
|
|
447
|
+
import { existsSync as existsSync2, readdirSync } from "node:fs";
|
|
448
|
+
import { homedir } from "node:os";
|
|
449
|
+
import { join as join2 } from "node:path";
|
|
450
|
+
var SESSION_ID = /^[A-Za-z0-9_-]+$/;
|
|
451
|
+
var PI_ROLES = new Set(["user", "assistant", "toolResult"]);
|
|
452
|
+
function sniffFormat(rows) {
|
|
453
|
+
for (const row of rows) {
|
|
454
|
+
if (row.type === "message" && PI_ROLES.has(row.message?.role ?? ""))
|
|
455
|
+
return "pi";
|
|
456
|
+
if (row.type === "user" || row.type === "assistant")
|
|
457
|
+
return "claude";
|
|
458
|
+
}
|
|
459
|
+
return "unknown";
|
|
460
|
+
}
|
|
461
|
+
function parseRows(lines) {
|
|
462
|
+
const rows = [];
|
|
463
|
+
let skippedLines = 0;
|
|
464
|
+
for (const line of lines) {
|
|
465
|
+
if (!line.trim())
|
|
466
|
+
continue;
|
|
467
|
+
let row;
|
|
468
|
+
try {
|
|
469
|
+
row = JSON.parse(line);
|
|
470
|
+
} catch {
|
|
471
|
+
skippedLines++;
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
const ts = row.timestamp ? Date.parse(row.timestamp) : Number.NaN;
|
|
475
|
+
if (!Number.isNaN(ts))
|
|
476
|
+
rows.push([row, ts]);
|
|
477
|
+
}
|
|
478
|
+
return { rows, skippedLines, format: sniffFormat(rows.map(([r]) => r)) };
|
|
479
|
+
}
|
|
480
|
+
function resolveTranscript(env, projectsRoot = join2(homedir(), ".claude", "projects"), override) {
|
|
481
|
+
if (override)
|
|
482
|
+
return existsSync2(override) ? { ok: true, path: override } : { ok: false, reason: "no transcript" };
|
|
483
|
+
const id = env.CLAUDE_CODE_SESSION_ID;
|
|
484
|
+
if (!id) {
|
|
485
|
+
const piFile = env.PI_SESSION_FILE;
|
|
486
|
+
if (piFile !== undefined && piFile !== "") {
|
|
487
|
+
return existsSync2(piFile) ? { ok: true, path: piFile } : { ok: false, reason: `PI_SESSION_FILE ${piFile} does not exist` };
|
|
488
|
+
}
|
|
489
|
+
return {
|
|
490
|
+
ok: false,
|
|
491
|
+
reason: "no host session id; pass --transcript <path> (pi: ~/.pi/agent/sessions/<cwd-slug>/<file>.jsonl, " + "or set PI_SESSION_FILE)"
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
if (!SESSION_ID.test(id))
|
|
495
|
+
return { ok: false, reason: "refusing a session id with path characters" };
|
|
496
|
+
if (!existsSync2(projectsRoot))
|
|
497
|
+
return { ok: false, reason: `no transcript root ${projectsRoot}` };
|
|
498
|
+
for (const dir of readdirSync(projectsRoot)) {
|
|
499
|
+
const path = join2(projectsRoot, dir, `${id}.jsonl`);
|
|
500
|
+
if (existsSync2(path))
|
|
501
|
+
return { ok: true, path };
|
|
502
|
+
}
|
|
503
|
+
return { ok: false, reason: `no transcript for session ${id}` };
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// plugins/sp/lib/run-summary-core.ts
|
|
465
507
|
function renderSummaryMarkdown(summary) {
|
|
466
508
|
const line = (r, bold = false) => {
|
|
467
509
|
const stage = bold ? `**${r.stage}**` : r.stage;
|
|
@@ -493,9 +535,9 @@ function renderSummaryMarkdown(summary) {
|
|
|
493
535
|
}
|
|
494
536
|
function writeCloseSummary(run, opts = {}) {
|
|
495
537
|
const cwd = opts.cwd ?? process.cwd();
|
|
496
|
-
const runDir =
|
|
497
|
-
const rel =
|
|
498
|
-
const abs =
|
|
538
|
+
const runDir = join3(cwd, ".spur", "run");
|
|
539
|
+
const rel = join3(".spur", "run", `${run.runId}-summary.md`);
|
|
540
|
+
const abs = join3(cwd, rel);
|
|
499
541
|
const na = (reason) => {
|
|
500
542
|
mkdirSync(runDir, { recursive: true });
|
|
501
543
|
writeFileSync(abs, `Execution summary: n/a (${reason})
|
|
@@ -508,15 +550,19 @@ function writeCloseSummary(run, opts = {}) {
|
|
|
508
550
|
const resolved = resolveTranscript(opts.env ?? getEnvVars(), opts.projectsRoot);
|
|
509
551
|
if (!resolved.ok)
|
|
510
552
|
return na(resolved.reason);
|
|
511
|
-
const
|
|
512
|
-
`);
|
|
513
|
-
const summary = buildRunSummary(
|
|
553
|
+
const { rows, skippedLines } = parseRows(readFileSync(resolved.path, "utf8").split(`
|
|
554
|
+
`));
|
|
555
|
+
const summary = buildRunSummary(rows, [{ progress: run.progress }], {
|
|
556
|
+
since: run.startedAt,
|
|
557
|
+
until: run.completedAt,
|
|
558
|
+
skippedLines
|
|
559
|
+
});
|
|
514
560
|
if (summary.rows.length === 0)
|
|
515
561
|
return na("no timed attempts");
|
|
516
562
|
mkdirSync(runDir, { recursive: true });
|
|
517
563
|
writeFileSync(abs, `${renderSummaryMarkdown(summary)}
|
|
518
564
|
`);
|
|
519
|
-
writeFileSync(
|
|
565
|
+
writeFileSync(join3(runDir, `${run.runId}-summary.json`), JSON.stringify({ ...summary, runId: run.runId, startedAt: run.startedAt, completedAt: run.completedAt }, null, 2));
|
|
520
566
|
return rel;
|
|
521
567
|
} catch (error) {
|
|
522
568
|
return na(error instanceof Error ? error.message : String(error));
|
|
@@ -552,29 +598,10 @@ function refuseUnsafeRunId(runId) {
|
|
|
552
598
|
console.error(" The run id must be a single safe filename component (alphanumeric/._-, no leading dot, no path separators, interpolation or traversal; same class as the task-pipeline route-reason guard, task 0804 R8). Allocate a fresh run id (uuid or timestamp slug) and retry.");
|
|
553
599
|
process.exit(1);
|
|
554
600
|
}
|
|
555
|
-
function resolveAppEntry(spurBin) {
|
|
556
|
-
const fallback = fileURLToPath(new URL("../../../apps/cli/src/index.ts", import.meta.url));
|
|
557
|
-
const candidates = spurBin !== "" ? [spurBin] : [fallback];
|
|
558
|
-
for (const candidate of candidates) {
|
|
559
|
-
const tokens = candidate.split(/\s+/).filter(Boolean);
|
|
560
|
-
const mainModule = [...tokens].reverse().find((t) => t.endsWith(".ts"));
|
|
561
|
-
if (mainModule === undefined || !existsSync3(mainModule))
|
|
562
|
-
continue;
|
|
563
|
-
const repoRoot = resolve(dirname(mainModule), "..", "..", "..");
|
|
564
|
-
const appEntry = join3(repoRoot, "packages", "app", "src", "index.ts");
|
|
565
|
-
if (existsSync3(appEntry))
|
|
566
|
-
return { entry: appEntry, portable: false };
|
|
567
|
-
}
|
|
568
|
-
const entry = fileURLToPath(new URL("../lib/inline-run.generated.mjs", import.meta.url));
|
|
569
|
-
if (!existsSync3(entry)) {
|
|
570
|
-
throw new Error("inline application bundle is missing \u2014 rebuild/install the sp plugin before running inline");
|
|
571
|
-
}
|
|
572
|
-
return { entry, portable: true };
|
|
573
|
-
}
|
|
574
601
|
var SETUP_EXPORTS = ["readInstalledInventory", "runInlineRunSetup", "writeInlineRunOutcome"];
|
|
575
602
|
async function main() {
|
|
576
603
|
if (!process.versions.bun) {
|
|
577
|
-
const child = spawnSync("bun", [
|
|
604
|
+
const child = spawnSync("bun", [fileURLToPath2(import.meta.url), ...process.argv.slice(2)], {
|
|
578
605
|
stdio: "inherit"
|
|
579
606
|
});
|
|
580
607
|
if (child.error)
|
|
@@ -643,7 +670,7 @@ async function main() {
|
|
|
643
670
|
refuseUnsafeRunId(runId);
|
|
644
671
|
try {
|
|
645
672
|
const { app: app2, portable: portable2 } = await loadInlineApp(spurBin, resolveAppEntry, ["runInlineRunDecide"]);
|
|
646
|
-
const { app: lib } = await loadInlineApp(spurBin, () => ({ entry:
|
|
673
|
+
const { app: lib } = await loadInlineApp(spurBin, () => ({ entry: fileURLToPath2(new URL("../lib/inline-run.generated.mjs", import.meta.url)), portable: portable2 }), ["resolveDecideDecisionMakerEnabled", "loadSpurConfig"]);
|
|
647
674
|
const loadOpts = portable2 ? { embeddedSchemas: lib.EMBEDDED_SPUR_SCHEMAS } : undefined;
|
|
648
675
|
const [enabled, spurConfig] = await Promise.all([
|
|
649
676
|
lib.resolveDecideDecisionMakerEnabled(process.cwd(), loadOpts),
|
|
@@ -699,7 +726,7 @@ async function main() {
|
|
|
699
726
|
inventory = await app.readInstalledInventory({
|
|
700
727
|
file,
|
|
701
728
|
spurBin,
|
|
702
|
-
localCli:
|
|
729
|
+
localCli: fileURLToPath2(new URL("../../../apps/cli/src/index.ts", import.meta.url))
|
|
703
730
|
});
|
|
704
731
|
} catch (error) {
|
|
705
732
|
const message = `could not resolve the workflow definition: ${error instanceof Error ? error.message : String(error)}`;
|
|
@@ -3,13 +3,11 @@
|
|
|
3
3
|
* handshake, mode dispatch. Mode bodies live in the bundled app service (`runInlineRun*` exports); the `.mjs`
|
|
4
4
|
* twin re-enters Bun in `main()` (SQLite) and imports the bundle dynamically, so bare `node` never loads app code. */
|
|
5
5
|
import { spawnSync } from 'node:child_process';
|
|
6
|
-
import { existsSync } from 'node:fs';
|
|
7
|
-
import { dirname, join, resolve } from 'node:path';
|
|
8
6
|
import { fileURLToPath } from 'node:url';
|
|
9
7
|
// Type-only namespace import (erased before bundling; the smoke scanner exempts `import type`).
|
|
10
8
|
import type * as spurApp from '@gobing-ai/spur-app';
|
|
11
9
|
import { getEnvVar } from '../lib/env';
|
|
12
|
-
import { loadInlineApp } from '../lib/inline-run-app';
|
|
10
|
+
import { loadInlineApp, resolveAppEntry } from '../lib/inline-run-app';
|
|
13
11
|
import { writeCloseSummary } from './run-summary';
|
|
14
12
|
|
|
15
13
|
// EMBEDDED_SPUR_SCHEMAS: declared by the bundle twin, never exported by the barrel — optional
|
|
@@ -47,24 +45,6 @@ function refuseUnsafeRunId(runId: string): never {
|
|
|
47
45
|
}
|
|
48
46
|
|
|
49
47
|
/** Resolve the app entry: --spur-bin/SPUR_BIN main module if it proves a repo checkout, else the adjacent bundle. */
|
|
50
|
-
function resolveAppEntry(spurBin: string): { entry: string; portable: boolean } {
|
|
51
|
-
const fallback = fileURLToPath(new URL('../../../apps/cli/src/index.ts', import.meta.url));
|
|
52
|
-
const candidates = spurBin !== '' ? [spurBin] : [fallback];
|
|
53
|
-
for (const candidate of candidates) {
|
|
54
|
-
const tokens = candidate.split(/\s+/).filter(Boolean);
|
|
55
|
-
// Main module = last path-like token; only a .ts source entry proves a repo checkout.
|
|
56
|
-
const mainModule = [...tokens].reverse().find((t) => t.endsWith('.ts'));
|
|
57
|
-
if (mainModule === undefined || !existsSync(mainModule)) continue;
|
|
58
|
-
const repoRoot = resolve(dirname(mainModule), '..', '..', '..');
|
|
59
|
-
const appEntry = join(repoRoot, 'packages', 'app', 'src', 'index.ts');
|
|
60
|
-
if (existsSync(appEntry)) return { entry: appEntry, portable: false };
|
|
61
|
-
}
|
|
62
|
-
const entry = fileURLToPath(new URL('../lib/inline-run.generated.mjs', import.meta.url));
|
|
63
|
-
if (!existsSync(entry)) {
|
|
64
|
-
throw new Error('inline application bundle is missing — rebuild/install the sp plugin before running inline');
|
|
65
|
-
}
|
|
66
|
-
return { entry, portable: true };
|
|
67
|
-
}
|
|
68
48
|
|
|
69
49
|
const SETUP_EXPORTS = ['readInstalledInventory', 'runInlineRunSetup', 'writeInlineRunOutcome'] as const;
|
|
70
50
|
|
|
@@ -24437,6 +24437,7 @@ function isRfc3339Timestamp(value) {
|
|
|
24437
24437
|
var ALL_FINDING_CODES = [
|
|
24438
24438
|
"L1.markdown-parse",
|
|
24439
24439
|
"L1.schema-validation",
|
|
24440
|
+
"L1.wbs-collision",
|
|
24440
24441
|
"L2.missing-required-section",
|
|
24441
24442
|
"L2.forbidden-section",
|
|
24442
24443
|
"L2.disallowed-section",
|
|
@@ -24464,6 +24465,7 @@ var ALL_FINDING_CODES = [
|
|
|
24464
24465
|
"L4.design-placeholder",
|
|
24465
24466
|
"L4.feature-not-found",
|
|
24466
24467
|
"L4.feature-terminal",
|
|
24468
|
+
"L4.feature-deprecated",
|
|
24467
24469
|
"L4.missing-feature-id",
|
|
24468
24470
|
"L4.parent-not-found",
|
|
24469
24471
|
"L4.dependency-not-found",
|
|
@@ -24901,7 +24903,7 @@ var POST_WATERMARK_TABLES = [
|
|
|
24901
24903
|
"history_daily_stats",
|
|
24902
24904
|
"history_board_source_daily"
|
|
24903
24905
|
];
|
|
24904
|
-
var HISTORY_IMPORT_SCHEMA_VERSION = "0.5.
|
|
24906
|
+
var HISTORY_IMPORT_SCHEMA_VERSION = "0.5.24";
|
|
24905
24907
|
var HISTORY_IMPORT_SCHEMA_SQL = `
|
|
24906
24908
|
CREATE TABLE IF NOT EXISTS history_import_checkpoint (
|
|
24907
24909
|
source TEXT NOT NULL,
|
|
@@ -27620,6 +27622,8 @@ var artifactsTable = defineTable("artifacts", {
|
|
|
27620
27622
|
...standardColumns
|
|
27621
27623
|
});
|
|
27622
27624
|
var artifacts = artifactsTable.table;
|
|
27625
|
+
var encoder = new TextEncoder;
|
|
27626
|
+
var decoder = new TextDecoder;
|
|
27623
27627
|
init_schema();
|
|
27624
27628
|
var phaseRunsTable = defineTable("phase_runs", {
|
|
27625
27629
|
id: text("id").primaryKey(),
|
|
@@ -29820,7 +29824,15 @@ function escapeYamlValue(value) {
|
|
|
29820
29824
|
}
|
|
29821
29825
|
init_zod();
|
|
29822
29826
|
var TASK_STATUSES = ["backlog", "todo", "wip", "testing", "blocked", "done", "cancelled"];
|
|
29823
|
-
var FEATURE_STATUSES = [
|
|
29827
|
+
var FEATURE_STATUSES = [
|
|
29828
|
+
"backlog",
|
|
29829
|
+
"active",
|
|
29830
|
+
"verifying",
|
|
29831
|
+
"blocked",
|
|
29832
|
+
"done",
|
|
29833
|
+
"cancelled",
|
|
29834
|
+
"deprecated"
|
|
29835
|
+
];
|
|
29824
29836
|
var PRIORITIES = ["P0", "P1", "P2", "P3"];
|
|
29825
29837
|
var PROFILES = [
|
|
29826
29838
|
"simple",
|
|
@@ -15,6 +15,32 @@ function getEnvVars() {
|
|
|
15
15
|
// plugins/sp/lib/residual-scan.generated.mjs
|
|
16
16
|
import { join } from "node:path";
|
|
17
17
|
import { createHash } from "node:crypto";
|
|
18
|
+
var CLAIM_MARKERS = [
|
|
19
|
+
/@[a-z0-9._-]+\/[a-z0-9._-]+/i,
|
|
20
|
+
/\bnode_modules\//,
|
|
21
|
+
/\b\d+\.\d+\.\d+\b/,
|
|
22
|
+
/`[A-Za-z_$][\w.$]*\s*\([^`]*\)`/
|
|
23
|
+
];
|
|
24
|
+
var CITATION_FORMS = [
|
|
25
|
+
/`[^`\s]+:\d+(?:-\d+)?`/,
|
|
26
|
+
/`[^`]+`\s+lines?\s+\d+/i,
|
|
27
|
+
/\b[\w./-]+\.[A-Za-z0-9]+:\d+(?:-\d+)?\b/,
|
|
28
|
+
/https?:\/\/\S+/i
|
|
29
|
+
];
|
|
30
|
+
function hasClaimMarker(evidence) {
|
|
31
|
+
return CLAIM_MARKERS.some((re) => re.test(evidence));
|
|
32
|
+
}
|
|
33
|
+
function hasCitationForm(evidence) {
|
|
34
|
+
return CITATION_FORMS.some((re) => re.test(evidence));
|
|
35
|
+
}
|
|
36
|
+
var CONFIDENCE_LEVELS = ["HIGH", "MEDIUM", "LOW"];
|
|
37
|
+
function extractAnswerConfidence(text) {
|
|
38
|
+
const matches = [...text.matchAll(/^\s*Confidence:\s*(\S+)\s*$/gim)];
|
|
39
|
+
if (matches.length !== 1)
|
|
40
|
+
return;
|
|
41
|
+
const value = (matches[0]?.[1] ?? "").toUpperCase();
|
|
42
|
+
return CONFIDENCE_LEVELS.includes(value) ? value : undefined;
|
|
43
|
+
}
|
|
18
44
|
var MARKER_PATTERN = /TODO|FIXME|XXX|HACK/;
|
|
19
45
|
var PRIORITY_PATTERN = /^P[1-4]/;
|
|
20
46
|
var RANGE_PRIORITY = /^P[1-4]\s*[\u2013\u2014-]\s*P?[1-4]/;
|
|
@@ -41,11 +67,15 @@ function locationOf(locationCell, finding) {
|
|
|
41
67
|
const backtick = finding.match(/`([^`]+)`/)?.[1];
|
|
42
68
|
return backtick === undefined ? "" : normalizeAnchor(backtick);
|
|
43
69
|
}
|
|
44
|
-
function
|
|
70
|
+
function reviewBody(taskContent) {
|
|
45
71
|
const section = taskContent.split(/^### Review\b/m)[1];
|
|
46
|
-
|
|
72
|
+
return section === undefined ? null : section.split(/^### /m)[0] ?? "";
|
|
73
|
+
}
|
|
74
|
+
function parseReviewFindings(taskContent) {
|
|
75
|
+
const section = reviewBody(taskContent);
|
|
76
|
+
if (section === null)
|
|
47
77
|
return [];
|
|
48
|
-
const body = section
|
|
78
|
+
const body = section;
|
|
49
79
|
const out = [];
|
|
50
80
|
const lines = body.split(`
|
|
51
81
|
`);
|
|
@@ -158,7 +188,8 @@ function scanResiduals(inputs) {
|
|
|
158
188
|
"review-finding": true,
|
|
159
189
|
"diff-marker": base !== null,
|
|
160
190
|
"unchecked-box": true,
|
|
161
|
-
"staging-residue": true
|
|
191
|
+
"staging-residue": true,
|
|
192
|
+
"review-contract": false
|
|
162
193
|
},
|
|
163
194
|
items,
|
|
164
195
|
counts
|
|
@@ -179,8 +210,36 @@ function blockingReviewFindings(taskContent, deferrals) {
|
|
|
179
210
|
}));
|
|
180
211
|
return classify(items, [...tableDeferrals, ...deferrals]);
|
|
181
212
|
}
|
|
213
|
+
function reviewContractViolations(taskContent) {
|
|
214
|
+
const violations = [];
|
|
215
|
+
const body = reviewBody(taskContent);
|
|
216
|
+
if (body !== null && extractAnswerConfidence(body.replace(/\*\*/g, "")) === undefined) {
|
|
217
|
+
violations.push({
|
|
218
|
+
location: "task-file:Review",
|
|
219
|
+
text: "Review report carries no single valid `**Confidence:** HIGH | MEDIUM | LOW` line — " + "state the level the report can defend (HIGH: every requirement row MET, every finding " + "dispositioned, evidence cited; MEDIUM/LOW: name the gap)."
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
for (const row of parseReviewFindings(taskContent)) {
|
|
223
|
+
if (hasClaimMarker(row.text) && !hasCitationForm(row.text)) {
|
|
224
|
+
violations.push({
|
|
225
|
+
location: row.location.length > 0 ? row.location : "task-file:Review",
|
|
226
|
+
text: `${row.priority} finding asserts an external API/library/version claim without a ` + "same-cell citation (add a repo `path:line`, an external named-origin form, or a URL " + `to the Finding cell): ${row.text.slice(0, 80)}`
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return violations;
|
|
231
|
+
}
|
|
182
232
|
function buildReviewGateArtifact(wbs, taskContent, deferrals) {
|
|
183
|
-
const items =
|
|
233
|
+
const items = [
|
|
234
|
+
...blockingReviewFindings(taskContent, deferrals),
|
|
235
|
+
...reviewContractViolations(taskContent).map((v) => ({
|
|
236
|
+
id: makeItemId("review-contract", v.location, v.text),
|
|
237
|
+
category: "review-contract",
|
|
238
|
+
class: "blocking",
|
|
239
|
+
location: v.location,
|
|
240
|
+
text: v.text
|
|
241
|
+
}))
|
|
242
|
+
];
|
|
184
243
|
const blocking = items.filter((i) => i.class === "blocking");
|
|
185
244
|
const counts = { blocking: 0, deferrable: 0, advisory: 0, housekeeping: 0 };
|
|
186
245
|
for (const item of items)
|
|
@@ -192,7 +251,8 @@ function buildReviewGateArtifact(wbs, taskContent, deferrals) {
|
|
|
192
251
|
"review-finding": true,
|
|
193
252
|
"diff-marker": false,
|
|
194
253
|
"unchecked-box": false,
|
|
195
|
-
"staging-residue": false
|
|
254
|
+
"staging-residue": false,
|
|
255
|
+
"review-contract": true
|
|
196
256
|
},
|
|
197
257
|
items,
|
|
198
258
|
counts
|
|
@@ -518,6 +578,12 @@ function settleMode(opts, env, io) {
|
|
|
518
578
|
}
|
|
519
579
|
function reportMode(opts, env, io) {
|
|
520
580
|
const runDir = join2(opts.root, ".spur", "run");
|
|
581
|
+
const verdictPath = recordedVerdictPath(runDir, opts.wbs, fs);
|
|
582
|
+
if (!fs.existsSync(verdictPath)) {
|
|
583
|
+
io.err(`residual-report: no verdict at ${verdictPath}; nothing to report for ${opts.wbs}
|
|
584
|
+
`);
|
|
585
|
+
return 0;
|
|
586
|
+
}
|
|
521
587
|
const sweep = loadVerdict(runDir, opts.wbs).checks.find((c) => c.name === "residual-sweep");
|
|
522
588
|
if (sweep === undefined || sweep.status !== "fail")
|
|
523
589
|
return 0;
|
|
@@ -558,6 +624,7 @@ process.exit(main(process.argv.slice(2)));
|
|
|
558
624
|
export {
|
|
559
625
|
verdictDisagreementNote,
|
|
560
626
|
scanResiduals2 as scanResiduals,
|
|
627
|
+
reviewContractViolations,
|
|
561
628
|
renderReport,
|
|
562
629
|
recordedVerdictPath,
|
|
563
630
|
parseScanArgs,
|
|
@@ -162,11 +162,8 @@ function settleMode(opts: ParsedArgs, env: ScanEnv, io: ScanIo): number {
|
|
|
162
162
|
if (fs.existsSync(residualsPath)) prior = JSON.parse(fs.readFileSync(residualsPath, 'utf8'));
|
|
163
163
|
const deferred = scan.items.filter((i) => i.class === 'deferrable');
|
|
164
164
|
if (deferred.length > 0 && prior.followUp === undefined) {
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
// residual was deferred (2026-10-04 session, E72/1086→1087), so a deferral must not be able
|
|
168
|
-
// to block the completing feature. The source task and its feature stay named in the
|
|
169
|
-
// Background, which keeps the traceability without the lifecycle coupling.
|
|
165
|
+
// Filed UNLINKED: a feature edge failed that feature's done-gate (`L4.verifying-incomplete-tasks`,
|
|
166
|
+
// E72/1086→1087); the Background still names the source task and feature for traceability.
|
|
170
167
|
const args = ['task', 'create', `Residuals from ${wbs}`, '--skip-ready', '--json'];
|
|
171
168
|
const created = spur(env, opts.spurBin, args, opts.root);
|
|
172
169
|
let wbsNew = '';
|
|
@@ -206,6 +203,12 @@ function settleMode(opts: ParsedArgs, env: ScanEnv, io: ScanIo): number {
|
|
|
206
203
|
}
|
|
207
204
|
function reportMode(opts: ParsedArgs, env: ScanEnv, io: ScanIo): number {
|
|
208
205
|
const runDir = join(opts.root, '.spur', 'run');
|
|
206
|
+
// A fix-budget-exhausted run never reaches verify: no verdict is a no-op, not ENOENT (1178 run 1).
|
|
207
|
+
const verdictPath = core.recordedVerdictPath(runDir, opts.wbs, fs);
|
|
208
|
+
if (!fs.existsSync(verdictPath)) {
|
|
209
|
+
io.err(`residual-report: no verdict at ${verdictPath}; nothing to report for ${opts.wbs}\n`);
|
|
210
|
+
return 0;
|
|
211
|
+
}
|
|
209
212
|
const sweep = loadVerdict(runDir, opts.wbs).checks.find((c) => c.name === 'residual-sweep');
|
|
210
213
|
if (sweep === undefined || sweep.status !== 'fail') return 0;
|
|
211
214
|
const { content } = loadTask(env, opts.spurBin, opts.wbs, opts.root);
|