@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
|
@@ -13,13 +13,16 @@ function getEnvVars() {
|
|
|
13
13
|
import { existsSync as existsSync2, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
14
14
|
import { join as join2 } from "node:path";
|
|
15
15
|
|
|
16
|
-
// plugins/sp/lib/
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
var
|
|
16
|
+
// plugins/sp/lib/env.ts
|
|
17
|
+
function getEnvVars2() {
|
|
18
|
+
return process.env;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// plugins/sp/lib/plan-projection.generated.mjs
|
|
22
|
+
var NON_ABORTING_PREFLIGHT_CODES = new Set([
|
|
23
|
+
"L4.scenario-unverified",
|
|
24
|
+
"L4.verifying-incomplete-tasks"
|
|
25
|
+
]);
|
|
23
26
|
var zeroTokens = () => ({ input: 0, cacheCreate: 0, cacheRead: 0, output: 0 });
|
|
24
27
|
function formatDuration(ms) {
|
|
25
28
|
const s = Math.round(ms / 1000);
|
|
@@ -39,6 +42,26 @@ function formatTokenSplit(t) {
|
|
|
39
42
|
const nonCached = t.input + t.cacheCreate + t.output;
|
|
40
43
|
return `${formatTokens(nonCached + t.cacheRead)} / ${formatTokens(nonCached)}`;
|
|
41
44
|
}
|
|
45
|
+
var OPERATOR_TOOLS = new Set(["AskUserQuestion", "ask_user_question"]);
|
|
46
|
+
function isCompaction(row) {
|
|
47
|
+
return row.type === "compaction" || row.type === "system" && row.subtype === "compact_boundary";
|
|
48
|
+
}
|
|
49
|
+
function firstTextContent(row) {
|
|
50
|
+
const content = row.message?.content;
|
|
51
|
+
if (typeof content === "string")
|
|
52
|
+
return content;
|
|
53
|
+
if (!Array.isArray(content))
|
|
54
|
+
return;
|
|
55
|
+
return content.find((b) => b.type === "text")?.text;
|
|
56
|
+
}
|
|
57
|
+
function skillInvocationPrompt(text) {
|
|
58
|
+
const name = /^\s*<skill name="([^"]+)"/.exec(text)?.[1];
|
|
59
|
+
if (name === undefined)
|
|
60
|
+
return;
|
|
61
|
+
const close = text.lastIndexOf("</skill>");
|
|
62
|
+
const args = close < 0 ? "" : text.slice(close + "</skill>".length).trim();
|
|
63
|
+
return args === "" ? `/${name}` : `/${name} ${args}`;
|
|
64
|
+
}
|
|
42
65
|
function promptText(row) {
|
|
43
66
|
if (row.type === "custom_message") {
|
|
44
67
|
if (row.display === false)
|
|
@@ -63,22 +86,6 @@ function promptText(row) {
|
|
|
63
86
|
return;
|
|
64
87
|
return content.find((b) => b.type === "text")?.text;
|
|
65
88
|
}
|
|
66
|
-
function firstTextContent(row) {
|
|
67
|
-
const content = row.message?.content;
|
|
68
|
-
if (typeof content === "string")
|
|
69
|
-
return content;
|
|
70
|
-
if (!Array.isArray(content))
|
|
71
|
-
return;
|
|
72
|
-
return content.find((b) => b.type === "text")?.text;
|
|
73
|
-
}
|
|
74
|
-
function skillInvocationPrompt(text) {
|
|
75
|
-
const name = /^\s*<skill name="([^"]+)"/.exec(text)?.[1];
|
|
76
|
-
if (name === undefined)
|
|
77
|
-
return;
|
|
78
|
-
const close = text.lastIndexOf("</skill>");
|
|
79
|
-
const args = close < 0 ? "" : text.slice(close + "</skill>".length).trim();
|
|
80
|
-
return args === "" ? `/${name}` : `/${name} ${args}`;
|
|
81
|
-
}
|
|
82
89
|
function sumTokens(all) {
|
|
83
90
|
const total = zeroTokens();
|
|
84
91
|
for (const t of all)
|
|
@@ -150,77 +157,14 @@ function accumulate(acc, row, ts) {
|
|
|
150
157
|
});
|
|
151
158
|
}
|
|
152
159
|
}
|
|
153
|
-
function isCompaction(row) {
|
|
154
|
-
return row.type === "compaction" || row.type === "system" && row.subtype === "compact_boundary";
|
|
155
|
-
}
|
|
156
|
-
function sniffFormat(rows) {
|
|
157
|
-
for (const row of rows) {
|
|
158
|
-
if (row.type === "message" && PI_ROLES.has(row.message?.role ?? ""))
|
|
159
|
-
return "pi";
|
|
160
|
-
if (row.type === "user" || row.type === "assistant")
|
|
161
|
-
return "claude";
|
|
162
|
-
}
|
|
163
|
-
return "unknown";
|
|
164
|
-
}
|
|
165
|
-
function parseRows(lines) {
|
|
166
|
-
const rows = [];
|
|
167
|
-
let skippedLines = 0;
|
|
168
|
-
for (const line of lines) {
|
|
169
|
-
if (!line.trim())
|
|
170
|
-
continue;
|
|
171
|
-
let row;
|
|
172
|
-
try {
|
|
173
|
-
row = JSON.parse(line);
|
|
174
|
-
} catch {
|
|
175
|
-
skippedLines++;
|
|
176
|
-
continue;
|
|
177
|
-
}
|
|
178
|
-
const ts = row.timestamp ? Date.parse(row.timestamp) : Number.NaN;
|
|
179
|
-
if (!Number.isNaN(ts))
|
|
180
|
-
rows.push([row, ts]);
|
|
181
|
-
}
|
|
182
|
-
return { rows, skippedLines, format: sniffFormat(rows.map(([r]) => r)) };
|
|
183
|
-
}
|
|
184
|
-
function resolveTranscript(env, projectsRoot = join(homedir(), ".claude", "projects"), override) {
|
|
185
|
-
if (override)
|
|
186
|
-
return existsSync(override) ? { ok: true, path: override } : { ok: false, reason: "no transcript" };
|
|
187
|
-
const id = env.CLAUDE_CODE_SESSION_ID;
|
|
188
|
-
if (!id) {
|
|
189
|
-
const piFile = env.PI_SESSION_FILE;
|
|
190
|
-
if (piFile !== undefined && piFile !== "") {
|
|
191
|
-
return existsSync(piFile) ? { ok: true, path: piFile } : { ok: false, reason: `PI_SESSION_FILE ${piFile} does not exist` };
|
|
192
|
-
}
|
|
193
|
-
return {
|
|
194
|
-
ok: false,
|
|
195
|
-
reason: "no host session id; pass --transcript <path> (pi: ~/.pi/agent/sessions/<cwd-slug>/<file>.jsonl, " + "or set PI_SESSION_FILE)"
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
if (!SESSION_ID.test(id))
|
|
199
|
-
return { ok: false, reason: "refusing a session id with path characters" };
|
|
200
|
-
if (!existsSync(projectsRoot))
|
|
201
|
-
return { ok: false, reason: `no transcript root ${projectsRoot}` };
|
|
202
|
-
for (const dir of readdirSync(projectsRoot)) {
|
|
203
|
-
const path = join(projectsRoot, dir, `${id}.jsonl`);
|
|
204
|
-
if (existsSync(path))
|
|
205
|
-
return { ok: true, path };
|
|
206
|
-
}
|
|
207
|
-
return { ok: false, reason: `no transcript for session ${id}` };
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// plugins/sp/lib/env.ts
|
|
211
|
-
function getEnvVars2() {
|
|
212
|
-
return process.env;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// plugins/sp/lib/run-summary-core.ts
|
|
216
160
|
var IDLE_GAP_MS = 300000;
|
|
217
161
|
var SHELL_TOOLS = new Set(["bash", "shell", "exec", "exec_command", "run_command", "command"]);
|
|
218
162
|
var SUBAGENT_TOOLS = new Set(["agent", "task", "invoke_subagent", "subagent", "dispatch_agent"]);
|
|
219
|
-
var
|
|
163
|
+
var LOWERCASE_OPERATOR_TOOLS = new Set(["askuserquestion", "ask_user_question"]);
|
|
220
164
|
var ACTOR_PRECEDENCE = ["operator", "subagent", "shell", "other"];
|
|
221
165
|
function actorOf(tool) {
|
|
222
166
|
const t = tool.toLowerCase().replace(/[^a-z_]/g, "");
|
|
223
|
-
if (
|
|
167
|
+
if (LOWERCASE_OPERATOR_TOOLS.has(t))
|
|
224
168
|
return "operator";
|
|
225
169
|
if (SUBAGENT_TOOLS.has(t))
|
|
226
170
|
return "subagent";
|
|
@@ -370,7 +314,8 @@ function summaryRow(stage, status, span, measured) {
|
|
|
370
314
|
token: formatTokenSplit(measured ? span.tokens : null)
|
|
371
315
|
};
|
|
372
316
|
}
|
|
373
|
-
function buildRunSummary(
|
|
317
|
+
function buildRunSummary(rows, inputs, opts = {}) {
|
|
318
|
+
const { since, until } = opts;
|
|
374
319
|
const windows = [];
|
|
375
320
|
for (const { label, progress } of inputs) {
|
|
376
321
|
const states = (progress.states ?? []).flatMap((s) => {
|
|
@@ -396,7 +341,6 @@ function buildRunSummary(lines, inputs, since, until) {
|
|
|
396
341
|
}
|
|
397
342
|
}
|
|
398
343
|
windows.sort((a, b) => a.start - b.start);
|
|
399
|
-
const { rows, skippedLines } = parseRows(lines);
|
|
400
344
|
const measure = (start, end) => {
|
|
401
345
|
const acc = newAcc(start);
|
|
402
346
|
for (const [row, ts] of rows)
|
|
@@ -444,9 +388,87 @@ function buildRunSummary(lines, inputs, since, until) {
|
|
|
444
388
|
total,
|
|
445
389
|
actors: actorSplit(rows, totalStart, totalEnd),
|
|
446
390
|
gate: { ms: null, reason: "no matching check receipt" },
|
|
447
|
-
skippedLines
|
|
391
|
+
skippedLines: opts.skippedLines ?? 0
|
|
448
392
|
};
|
|
449
393
|
}
|
|
394
|
+
var SENSITIVE_PREFIXES = ["drizzle/", "packages/config/", ".github/", "plugins/sp/hooks/"];
|
|
395
|
+
var SENSITIVE_GLOBS = ["apps/server/src/**/auth*", "**/*secret*", "**/*.sql"];
|
|
396
|
+
function globToRegex(glob) {
|
|
397
|
+
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replaceAll("**/", "\x00").replaceAll("**", "\x01").replaceAll("*", "[^/]*").replaceAll("\x00", "(?:.*/)?").replaceAll("\x01", ".*");
|
|
398
|
+
return new RegExp(`^${escaped}$`, "s");
|
|
399
|
+
}
|
|
400
|
+
var SENSITIVE_MATCHERS = [
|
|
401
|
+
...SENSITIVE_PREFIXES.map((prefix) => ({
|
|
402
|
+
label: prefix,
|
|
403
|
+
test: (path) => path.startsWith(prefix)
|
|
404
|
+
})),
|
|
405
|
+
...SENSITIVE_GLOBS.map((glob) => {
|
|
406
|
+
const regex = globToRegex(glob);
|
|
407
|
+
return { label: glob, test: (path) => regex.test(path) };
|
|
408
|
+
})
|
|
409
|
+
];
|
|
410
|
+
|
|
411
|
+
// plugins/sp/lib/transcript.ts
|
|
412
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
413
|
+
import { homedir } from "node:os";
|
|
414
|
+
import { join } from "node:path";
|
|
415
|
+
var SESSION_ID = /^[A-Za-z0-9_-]+$/;
|
|
416
|
+
var PI_ROLES = new Set(["user", "assistant", "toolResult"]);
|
|
417
|
+
function sniffFormat(rows) {
|
|
418
|
+
for (const row of rows) {
|
|
419
|
+
if (row.type === "message" && PI_ROLES.has(row.message?.role ?? ""))
|
|
420
|
+
return "pi";
|
|
421
|
+
if (row.type === "user" || row.type === "assistant")
|
|
422
|
+
return "claude";
|
|
423
|
+
}
|
|
424
|
+
return "unknown";
|
|
425
|
+
}
|
|
426
|
+
function parseRows(lines) {
|
|
427
|
+
const rows = [];
|
|
428
|
+
let skippedLines = 0;
|
|
429
|
+
for (const line of lines) {
|
|
430
|
+
if (!line.trim())
|
|
431
|
+
continue;
|
|
432
|
+
let row;
|
|
433
|
+
try {
|
|
434
|
+
row = JSON.parse(line);
|
|
435
|
+
} catch {
|
|
436
|
+
skippedLines++;
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
const ts = row.timestamp ? Date.parse(row.timestamp) : Number.NaN;
|
|
440
|
+
if (!Number.isNaN(ts))
|
|
441
|
+
rows.push([row, ts]);
|
|
442
|
+
}
|
|
443
|
+
return { rows, skippedLines, format: sniffFormat(rows.map(([r]) => r)) };
|
|
444
|
+
}
|
|
445
|
+
function resolveTranscript(env, projectsRoot = join(homedir(), ".claude", "projects"), override) {
|
|
446
|
+
if (override)
|
|
447
|
+
return existsSync(override) ? { ok: true, path: override } : { ok: false, reason: "no transcript" };
|
|
448
|
+
const id = env.CLAUDE_CODE_SESSION_ID;
|
|
449
|
+
if (!id) {
|
|
450
|
+
const piFile = env.PI_SESSION_FILE;
|
|
451
|
+
if (piFile !== undefined && piFile !== "") {
|
|
452
|
+
return existsSync(piFile) ? { ok: true, path: piFile } : { ok: false, reason: `PI_SESSION_FILE ${piFile} does not exist` };
|
|
453
|
+
}
|
|
454
|
+
return {
|
|
455
|
+
ok: false,
|
|
456
|
+
reason: "no host session id; pass --transcript <path> (pi: ~/.pi/agent/sessions/<cwd-slug>/<file>.jsonl, " + "or set PI_SESSION_FILE)"
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
if (!SESSION_ID.test(id))
|
|
460
|
+
return { ok: false, reason: "refusing a session id with path characters" };
|
|
461
|
+
if (!existsSync(projectsRoot))
|
|
462
|
+
return { ok: false, reason: `no transcript root ${projectsRoot}` };
|
|
463
|
+
for (const dir of readdirSync(projectsRoot)) {
|
|
464
|
+
const path = join(projectsRoot, dir, `${id}.jsonl`);
|
|
465
|
+
if (existsSync(path))
|
|
466
|
+
return { ok: true, path };
|
|
467
|
+
}
|
|
468
|
+
return { ok: false, reason: `no transcript for session ${id}` };
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// plugins/sp/lib/run-summary-core.ts
|
|
450
472
|
function renderSummaryMarkdown(summary) {
|
|
451
473
|
const line = (r, bold = false) => {
|
|
452
474
|
const stage = bold ? `**${r.stage}**` : r.stage;
|
|
@@ -547,9 +569,13 @@ function writeCloseSummary(run, opts = {}) {
|
|
|
547
569
|
const resolved = resolveTranscript(opts.env ?? getEnvVars2(), opts.projectsRoot);
|
|
548
570
|
if (!resolved.ok)
|
|
549
571
|
return na(resolved.reason);
|
|
550
|
-
const
|
|
551
|
-
`);
|
|
552
|
-
const summary = buildRunSummary(
|
|
572
|
+
const { rows, skippedLines } = parseRows(readFileSync(resolved.path, "utf8").split(`
|
|
573
|
+
`));
|
|
574
|
+
const summary = buildRunSummary(rows, [{ progress: run.progress }], {
|
|
575
|
+
since: run.startedAt,
|
|
576
|
+
until: run.completedAt,
|
|
577
|
+
skippedLines
|
|
578
|
+
});
|
|
553
579
|
if (summary.rows.length === 0)
|
|
554
580
|
return na("no timed attempts");
|
|
555
581
|
mkdirSync(runDir, { recursive: true });
|
|
@@ -583,8 +609,35 @@ import { existsSync as existsSync3, readdirSync as readdirSync2 } from "node:fs"
|
|
|
583
609
|
import { homedir as homedir2 } from "node:os";
|
|
584
610
|
import { join as join3 } from "node:path";
|
|
585
611
|
var SESSION_ID2 = /^[A-Za-z0-9_-]+$/;
|
|
586
|
-
var OPERATOR_TOOLS3 = new Set(["AskUserQuestion", "ask_user_question"]);
|
|
587
612
|
var PI_ROLES2 = new Set(["user", "assistant", "toolResult"]);
|
|
613
|
+
function sniffFormat2(rows) {
|
|
614
|
+
for (const row of rows) {
|
|
615
|
+
if (row.type === "message" && PI_ROLES2.has(row.message?.role ?? ""))
|
|
616
|
+
return "pi";
|
|
617
|
+
if (row.type === "user" || row.type === "assistant")
|
|
618
|
+
return "claude";
|
|
619
|
+
}
|
|
620
|
+
return "unknown";
|
|
621
|
+
}
|
|
622
|
+
function parseRows2(lines) {
|
|
623
|
+
const rows = [];
|
|
624
|
+
let skippedLines = 0;
|
|
625
|
+
for (const line of lines) {
|
|
626
|
+
if (!line.trim())
|
|
627
|
+
continue;
|
|
628
|
+
let row;
|
|
629
|
+
try {
|
|
630
|
+
row = JSON.parse(line);
|
|
631
|
+
} catch {
|
|
632
|
+
skippedLines++;
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
635
|
+
const ts = row.timestamp ? Date.parse(row.timestamp) : Number.NaN;
|
|
636
|
+
if (!Number.isNaN(ts))
|
|
637
|
+
rows.push([row, ts]);
|
|
638
|
+
}
|
|
639
|
+
return { rows, skippedLines, format: sniffFormat2(rows.map(([r]) => r)) };
|
|
640
|
+
}
|
|
588
641
|
function resolveTranscript2(env, projectsRoot = join3(homedir2(), ".claude", "projects"), override) {
|
|
589
642
|
if (override)
|
|
590
643
|
return existsSync3(override) ? { ok: true, path: override } : { ok: false, reason: "no transcript" };
|
|
@@ -679,8 +732,9 @@ function main(argv, env = getEnvVars(), write = (s) => process.stdout.write(s),
|
|
|
679
732
|
const [label, file] = eq > 0 ? [arg.slice(0, eq), arg.slice(eq + 1)] : [undefined, arg];
|
|
680
733
|
return { label, progress: JSON.parse(readFileSync2(file, "utf8")) };
|
|
681
734
|
});
|
|
682
|
-
const
|
|
683
|
-
`)
|
|
735
|
+
const { rows, skippedLines } = parseRows2(readFileSync2(resolved.path, "utf8").split(`
|
|
736
|
+
`));
|
|
737
|
+
const summary = buildRunSummary(rows, inputs, { since, skippedLines });
|
|
684
738
|
summary.gate = gateFromReceipt(process.cwd(), inputs[0]?.progress.runId ?? "");
|
|
685
739
|
write(`${markdown ? renderSummaryMarkdown(summary) : JSON.stringify({ ...summary, transcript: resolved.path })}
|
|
686
740
|
`);
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
* (`--no-summary` skips it). Inputs: one or more `spur workflow progress <run-id> --json` files
|
|
5
5
|
* (`--progress [<label>=]<file>`) and the host transcript (resolved like session-timeline, or
|
|
6
6
|
* `--transcript`), OR the close-written per-run summary files (`--rollup [<label>=]<file>`, 1146 R3).
|
|
7
|
-
* Measurement
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* Measurement lives in `packages/app/src/workflow/run-summary.ts` (reached through the generated
|
|
8
|
+
* pure bundle entry); presentation and host IO live in `../lib/run-summary-core` (ADR-130 glue
|
|
9
|
+
* budget). This script is argv parsing and dispatch. Reporting only: no transcript or no timed
|
|
10
|
+
* attempts → an `n/a` result, exit 0. Node builtins only.
|
|
10
11
|
*/
|
|
11
12
|
import { readFileSync } from 'node:fs';
|
|
12
13
|
import { getEnvVars } from '../lib/env';
|
|
@@ -18,7 +19,7 @@ import {
|
|
|
18
19
|
renderRollupMarkdown,
|
|
19
20
|
renderSummaryMarkdown,
|
|
20
21
|
} from '../lib/run-summary-core';
|
|
21
|
-
import { resolveTranscript } from '../lib/transcript';
|
|
22
|
+
import { parseRows, resolveTranscript } from '../lib/transcript';
|
|
22
23
|
|
|
23
24
|
// Re-exported so the suite and any consumer keep one import path for the whole surface.
|
|
24
25
|
export * from '../lib/run-summary-core';
|
|
@@ -90,7 +91,8 @@ export function main(
|
|
|
90
91
|
const [label, file] = eq > 0 ? [arg.slice(0, eq), arg.slice(eq + 1)] : [undefined, arg];
|
|
91
92
|
return { label, progress: JSON.parse(readFileSync(file, 'utf8')) as Progress };
|
|
92
93
|
});
|
|
93
|
-
const
|
|
94
|
+
const { rows, skippedLines } = parseRows(readFileSync(resolved.path, 'utf8').split('\n'));
|
|
95
|
+
const summary = buildRunSummary(rows, inputs, { since, skippedLines });
|
|
94
96
|
summary.gate = gateFromReceipt(process.cwd(), inputs[0]?.progress.runId ?? '');
|
|
95
97
|
write(
|
|
96
98
|
`${markdown ? renderSummaryMarkdown(summary) : JSON.stringify({ ...summary, transcript: resolved.path })}\n`,
|
|
@@ -13,9 +13,12 @@ function getEnvVars() {
|
|
|
13
13
|
import { existsSync, readdirSync } from "node:fs";
|
|
14
14
|
import { homedir } from "node:os";
|
|
15
15
|
import { join } from "node:path";
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
var
|
|
16
|
+
|
|
17
|
+
// plugins/sp/lib/plan-projection.generated.mjs
|
|
18
|
+
var NON_ABORTING_PREFLIGHT_CODES = new Set([
|
|
19
|
+
"L4.scenario-unverified",
|
|
20
|
+
"L4.verifying-incomplete-tasks"
|
|
21
|
+
]);
|
|
19
22
|
var zeroTokens = () => ({ input: 0, cacheCreate: 0, cacheRead: 0, output: 0 });
|
|
20
23
|
function formatDuration(ms) {
|
|
21
24
|
const s = Math.round(ms / 1000);
|
|
@@ -35,6 +38,23 @@ function formatTokenSplit(t) {
|
|
|
35
38
|
const nonCached = t.input + t.cacheCreate + t.output;
|
|
36
39
|
return `${formatTokens(nonCached + t.cacheRead)} / ${formatTokens(nonCached)}`;
|
|
37
40
|
}
|
|
41
|
+
var OPERATOR_TOOLS = new Set(["AskUserQuestion", "ask_user_question"]);
|
|
42
|
+
function firstTextContent(row) {
|
|
43
|
+
const content = row.message?.content;
|
|
44
|
+
if (typeof content === "string")
|
|
45
|
+
return content;
|
|
46
|
+
if (!Array.isArray(content))
|
|
47
|
+
return;
|
|
48
|
+
return content.find((b) => b.type === "text")?.text;
|
|
49
|
+
}
|
|
50
|
+
function skillInvocationPrompt(text) {
|
|
51
|
+
const name = /^\s*<skill name="([^"]+)"/.exec(text)?.[1];
|
|
52
|
+
if (name === undefined)
|
|
53
|
+
return;
|
|
54
|
+
const close = text.lastIndexOf("</skill>");
|
|
55
|
+
const args = close < 0 ? "" : text.slice(close + "</skill>".length).trim();
|
|
56
|
+
return args === "" ? `/${name}` : `/${name} ${args}`;
|
|
57
|
+
}
|
|
38
58
|
function promptText(row) {
|
|
39
59
|
if (row.type === "custom_message") {
|
|
40
60
|
if (row.display === false)
|
|
@@ -59,22 +79,6 @@ function promptText(row) {
|
|
|
59
79
|
return;
|
|
60
80
|
return content.find((b) => b.type === "text")?.text;
|
|
61
81
|
}
|
|
62
|
-
function firstTextContent(row) {
|
|
63
|
-
const content = row.message?.content;
|
|
64
|
-
if (typeof content === "string")
|
|
65
|
-
return content;
|
|
66
|
-
if (!Array.isArray(content))
|
|
67
|
-
return;
|
|
68
|
-
return content.find((b) => b.type === "text")?.text;
|
|
69
|
-
}
|
|
70
|
-
function skillInvocationPrompt(text) {
|
|
71
|
-
const name = /^\s*<skill name="([^"]+)"/.exec(text)?.[1];
|
|
72
|
-
if (name === undefined)
|
|
73
|
-
return;
|
|
74
|
-
const close = text.lastIndexOf("</skill>");
|
|
75
|
-
const args = close < 0 ? "" : text.slice(close + "</skill>".length).trim();
|
|
76
|
-
return args === "" ? `/${name}` : `/${name} ${args}`;
|
|
77
|
-
}
|
|
78
82
|
function sumTokens(all) {
|
|
79
83
|
const total = zeroTokens();
|
|
80
84
|
for (const t of all)
|
|
@@ -146,6 +150,29 @@ function accumulate(acc, row, ts) {
|
|
|
146
150
|
});
|
|
147
151
|
}
|
|
148
152
|
}
|
|
153
|
+
var SHELL_TOOLS = new Set(["bash", "shell", "exec", "exec_command", "run_command", "command"]);
|
|
154
|
+
var SUBAGENT_TOOLS = new Set(["agent", "task", "invoke_subagent", "subagent", "dispatch_agent"]);
|
|
155
|
+
var LOWERCASE_OPERATOR_TOOLS = new Set(["askuserquestion", "ask_user_question"]);
|
|
156
|
+
var SENSITIVE_PREFIXES = ["drizzle/", "packages/config/", ".github/", "plugins/sp/hooks/"];
|
|
157
|
+
var SENSITIVE_GLOBS = ["apps/server/src/**/auth*", "**/*secret*", "**/*.sql"];
|
|
158
|
+
function globToRegex(glob) {
|
|
159
|
+
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replaceAll("**/", "\x00").replaceAll("**", "\x01").replaceAll("*", "[^/]*").replaceAll("\x00", "(?:.*/)?").replaceAll("\x01", ".*");
|
|
160
|
+
return new RegExp(`^${escaped}$`, "s");
|
|
161
|
+
}
|
|
162
|
+
var SENSITIVE_MATCHERS = [
|
|
163
|
+
...SENSITIVE_PREFIXES.map((prefix) => ({
|
|
164
|
+
label: prefix,
|
|
165
|
+
test: (path) => path.startsWith(prefix)
|
|
166
|
+
})),
|
|
167
|
+
...SENSITIVE_GLOBS.map((glob) => {
|
|
168
|
+
const regex = globToRegex(glob);
|
|
169
|
+
return { label: glob, test: (path) => regex.test(path) };
|
|
170
|
+
})
|
|
171
|
+
];
|
|
172
|
+
|
|
173
|
+
// plugins/sp/lib/transcript.ts
|
|
174
|
+
var SESSION_ID = /^[A-Za-z0-9_-]+$/;
|
|
175
|
+
var PI_ROLES = new Set(["user", "assistant", "toolResult"]);
|
|
149
176
|
function sniffFormat(rows) {
|
|
150
177
|
for (const row of rows) {
|
|
151
178
|
if (row.type === "message" && PI_ROLES.has(row.message?.role ?? ""))
|
|
@@ -11,7 +11,15 @@ function getEnvVars() {
|
|
|
11
11
|
return process.env;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
// plugins/sp/
|
|
14
|
+
// plugins/sp/lib/plan-projection.generated.mjs
|
|
15
|
+
var NON_ABORTING_PREFLIGHT_CODES = new Set([
|
|
16
|
+
"L4.scenario-unverified",
|
|
17
|
+
"L4.verifying-incomplete-tasks"
|
|
18
|
+
]);
|
|
19
|
+
var OPERATOR_TOOLS = new Set(["AskUserQuestion", "ask_user_question"]);
|
|
20
|
+
var SHELL_TOOLS = new Set(["bash", "shell", "exec", "exec_command", "run_command", "command"]);
|
|
21
|
+
var SUBAGENT_TOOLS = new Set(["agent", "task", "invoke_subagent", "subagent", "dispatch_agent"]);
|
|
22
|
+
var LOWERCASE_OPERATOR_TOOLS = new Set(["askuserquestion", "ask_user_question"]);
|
|
15
23
|
var SENSITIVE_PREFIXES = ["drizzle/", "packages/config/", ".github/", "plugins/sp/hooks/"];
|
|
16
24
|
var SENSITIVE_GLOBS = ["apps/server/src/**/auth*", "**/*secret*", "**/*.sql"];
|
|
17
25
|
function globToRegex(glob) {
|
|
@@ -32,6 +40,8 @@ function sensitiveReasonForPath(path) {
|
|
|
32
40
|
const match = SENSITIVE_MATCHERS.find((entry) => entry.test(path));
|
|
33
41
|
return match ? `matches sensitive pattern ${match.label}` : null;
|
|
34
42
|
}
|
|
43
|
+
|
|
44
|
+
// plugins/sp/scripts/task-diffstat.ts
|
|
35
45
|
function defaultGitRunner(cwd) {
|
|
36
46
|
return (args) => {
|
|
37
47
|
const run = spawnSync("git", args, { cwd, encoding: "utf8" });
|
|
@@ -65,45 +65,14 @@ export interface DiffstatResult extends Diffstat {
|
|
|
65
65
|
exitCode: number;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
export const SENSITIVE_PREFIXES = ['drizzle/', 'packages/config/', '.github/', 'plugins/sp/hooks/'] as const;
|
|
77
|
-
export const SENSITIVE_GLOBS = ['apps/server/src/**/auth*', '**/*secret*', '**/*.sql'] as const;
|
|
78
|
-
|
|
79
|
-
// Glob → RegExp: `**` + `/` matches any-or-no directories, bare `**` any characters, `*` within one segment.
|
|
80
|
-
function globToRegex(glob: string): RegExp {
|
|
81
|
-
const escaped = glob
|
|
82
|
-
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
83
|
-
.replaceAll('**/', '\u0000')
|
|
84
|
-
.replaceAll('**', '\u0001')
|
|
85
|
-
.replaceAll('*', '[^/]*')
|
|
86
|
-
.replaceAll('\u0000', '(?:.*/)?')
|
|
87
|
-
.replaceAll('\u0001', '.*');
|
|
88
|
-
return new RegExp(`^${escaped}$`, 's');
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const SENSITIVE_MATCHERS: Array<{ label: string; test: (path: string) => boolean }> = [
|
|
92
|
-
...SENSITIVE_PREFIXES.map((prefix) => ({
|
|
93
|
-
label: prefix,
|
|
94
|
-
test: (path: string): boolean => path.startsWith(prefix),
|
|
95
|
-
})),
|
|
96
|
-
...SENSITIVE_GLOBS.map((glob) => {
|
|
97
|
-
const regex = globToRegex(glob);
|
|
98
|
-
return { label: glob, test: (path: string): boolean => regex.test(path) };
|
|
99
|
-
}),
|
|
100
|
-
];
|
|
101
|
-
|
|
102
|
-
/** Sensitive-pattern match for one path, or null — mirrors wrapup-drift-probe's driftReasonForPath. */
|
|
103
|
-
export function sensitiveReasonForPath(path: string): string | null {
|
|
104
|
-
const match = SENSITIVE_MATCHERS.find((entry) => entry.test(path));
|
|
105
|
-
return match ? `matches sensitive pattern ${match.label}` : null;
|
|
106
|
-
}
|
|
68
|
+
// Task 1176: the sensitive-path policy (0943 R2c) moved to the workflow/task owner —
|
|
69
|
+
// `packages/app/src/workflow/task-triage.ts`, reached through the generated pure bundle. That policy
|
|
70
|
+
// still forces the safety lane for any changed path under the frozen sensitive list before the
|
|
71
|
+
// `task-triage` decide runs, so sensitive work is never lane-negotiated with a model; this entry
|
|
72
|
+
// keeps git capture and the output artifact, and re-exports the policy for existing callers.
|
|
73
|
+
import { SENSITIVE_GLOBS, SENSITIVE_PREFIXES, sensitiveReasonForPath } from '../lib/plan-projection.generated.mjs';
|
|
74
|
+
|
|
75
|
+
export { SENSITIVE_GLOBS, SENSITIVE_PREFIXES, sensitiveReasonForPath };
|
|
107
76
|
|
|
108
77
|
function defaultGitRunner(cwd?: string): GitRunner {
|
|
109
78
|
return (args: string[]): GitRunResult => {
|
|
@@ -82,6 +82,12 @@ not have and work you were not asked to do.
|
|
|
82
82
|
- **Never implement a requirement belonging to another WBS**, even when it looks like a
|
|
83
83
|
prerequisite. If the target genuinely cannot proceed without it, stop and say so — a blocked task
|
|
84
84
|
is a cheap finding; a two-task diff costs the reverts.
|
|
85
|
+
- **Flip the target's `## Plan` boxes as each step completes.** They are this pass's progress
|
|
86
|
+
ledger, and the record-time residual sweep treats an *unchecked* Plan box as blocking: it
|
|
87
|
+
downgrades the verdict to PARTIAL and closes the `done` gate
|
|
88
|
+
(`docs/design/task-residual-sweep.md`), which costs a remediation cycle and a manual re-record.
|
|
89
|
+
Leave every finished step ticked through `spur task update <wbs> --section Plan --from-file`, and
|
|
90
|
+
if you deliberately skipped one, say so in the Solution change map.
|
|
85
91
|
|
|
86
92
|
Why this is a rule and not a nicety: driving task 0486 lost several hours to exactly this. Task 0485
|
|
87
93
|
was committed but still `todo` in the tree, and two different executors (omp run `ca130182`, claude
|
|
@@ -165,7 +171,7 @@ that single full gate.
|
|
|
165
171
|
1. The **changed-path matrix** below (narrow behavior tests + affected consumers);
|
|
166
172
|
2. the resolved `quality-gate` script in `light` mode (source-repo probe, else the installed twin) (biome on changed files, per-workspace typecheck, rule presets `rules:pre`/`rules:post`, related tests).
|
|
167
173
|
|
|
168
|
-
Neither step takes the
|
|
174
|
+
Neither step takes the full project gate. The full gate is named only for the pipeline's `test` and `test-recheck` hops.
|
|
169
175
|
|
|
170
176
|
| Changed surface | Required targeted tests | Required typechecks |
|
|
171
177
|
| --- | --- | --- |
|
|
@@ -92,7 +92,8 @@ Before declaring a task `done`, run this lightweight checklist. It catches the m
|
|
|
92
92
|
- [ ] Solution section contains `file:line` citations for every changed file.
|
|
93
93
|
- [ ] API/library claims in changed docs carry a source citation (URL or `path:line`) and an honest
|
|
94
94
|
confidence level (HIGH/MEDIUM/LOW) per source-driven-development; the deterministic advisory backstop is
|
|
95
|
-
the `cited-version-strings` / `cited-api-claims`
|
|
95
|
+
the `cited-version-strings` / `cited-api-claims` / `cited-api-claims-confidence` rules
|
|
96
|
+
(`.spur/rules/docs/sourced-api-claims.yaml`, run ad hoc — not wired into a preset).
|
|
96
97
|
- [ ] Review section has P1–P4 findings table (even if all rows are empty — the table itself proves review happened).
|
|
97
98
|
|
|
98
99
|
This checklist runs as part of the verify step when the task reaches `testing` status. It does not replace the formal gates — it augments them with between-pipeline hygiene checks.
|
|
@@ -202,6 +202,10 @@ citation in the SAME cell, or lint rejects the row with `evidence-citation` (blo
|
|
|
202
202
|
- The rule is cell-local: a citation in another row or a "receipt above" cross-reference does not
|
|
203
203
|
satisfy it. Marker-free receipts (test counts, reviewed-log notes) need no citation. The check is
|
|
204
204
|
presence-only — which citation proves which claim is not deterministically decidable.
|
|
205
|
+
- The same two predicates (`hasClaimMarker` / `hasCitationForm` in
|
|
206
|
+
`packages/app/src/services/verify-answer-lint.ts`) gate the review report's findings table through
|
|
207
|
+
`residual-scan review-gate` as the blocking `review-contract` category — one definition of
|
|
208
|
+
"external claim" and one set of accepted citation forms for both paths.
|
|
205
209
|
|
|
206
210
|
## Hedged evidence is rejected on MET rows
|
|
207
211
|
|
|
@@ -82,11 +82,11 @@ The command forwards these via `$ARGUMENTS`:
|
|
|
82
82
|
> <!-- pipeline-tokens:end -->
|
|
83
83
|
> matched as a **distinct hyphen-word**
|
|
84
84
|
> (machine-checked by
|
|
85
|
-
> [`detectPipelineDriving`](../../
|
|
85
|
+
> [`detectPipelineDriving`](../../lib/dogfood-pipeline.ts);
|
|
86
86
|
> see [§Pipeline-driving word-boundary contract](#pipeline-driving-word-boundary-contract))
|
|
87
87
|
> → `⚠ pipeline-driving testee detected; pass --max-retry 0 (observe-only) or --max-retry N (fix mode, tree mutation acknowledged)`.
|
|
88
88
|
> - **Mutating `--fix` modes** — `--fix all` / `--fix blockers-first`, boundary-guarded via
|
|
89
|
-
> [`hasMutatingFixMode`](../../
|
|
89
|
+
> [`hasMutatingFixMode`](../../lib/dogfood-pipeline.ts) (never matches
|
|
90
90
|
> `--fix none` / `--focus all` / `--prefix all`). No pipeline token required: a verify/review leg
|
|
91
91
|
> with `--fix all` mutates the tree on its own (0280 dogfood P2, task 0293). Honesty note:
|
|
92
92
|
> `--max-retry 0` here bounds **the driver only** — the testee still mutates the tree — so the
|
|
@@ -64,7 +64,7 @@ Run the steps in order. Each step's depth lives in its reference; this file is t
|
|
|
64
64
|
change a frontier feature's status, the report **leads with the drift summary** ("sync first")
|
|
65
65
|
and the ranking is computed over the *post-sync* status view. Details:
|
|
66
66
|
[references/signal-derivation.md](references/signal-derivation.md) §0.
|
|
67
|
-
1. **Assemble the candidate set.** `spur feature list --json`; drop `done`/`cancelled`, drop
|
|
67
|
+
1. **Assemble the candidate set.** `spur feature list --json`; drop `done`/`cancelled`/`deprecated`, drop
|
|
68
68
|
structural containers (has children, no own open tasks — defect D1), drop the map/feature that
|
|
69
69
|
owns this investigation if present. The `group` tag is **not** a reliable container marker (D3).
|
|
70
70
|
2. **Gate on actionability.** Read the frontier predicate **at runtime** from
|
|
@@ -167,7 +167,7 @@ rule at the source, so no per-path shim is needed:
|
|
|
167
167
|
| `spur agent loop` | `AgentService.run` per drained iteration | Same resolution path as `spur agent run`; inherits its own `SPUR_ROLE` |
|
|
168
168
|
| `spur serve` team supervisor → member | spawns `spur agent loop` | Member inherits the supervisor's `SPUR_ROLE` (recursive by construction) |
|
|
169
169
|
| Native subagent fan-out (this skill's default) | in-session `Task()`/`Skill()` | In-session subagents share the host session; when they themselves dispatch, the host's role is already in the session env — the rule holds at the next `spur agent run` boundary |
|
|
170
|
-
| `
|
|
170
|
+
| `scripts/commands/skill-eval.ts` | `spawnSync('spur agent run', …)` per scenario | Out of scope: a top-level eval harness, not a fan-out — no dispatcher role exists to inherit; each scenario is an independent top-level run (documented, no shim) |
|
|
171
171
|
|
|
172
172
|
The inventory is recorded in `docs/04_DESIGN.md` § `spur agent run` envelope (`roleOrigin`).
|
|
173
173
|
Paths that shell out to `spur agent run` without the rule would silently drop or double-attribute
|
|
@@ -101,10 +101,13 @@ line: `Verification: HIGH|MEDIUM|LOW — <first failing check and deviation, or
|
|
|
101
101
|
- **Honest confidence** — the stated HIGH/MEDIUM/LOW matches the evidence: HIGH requires a check against the pinned source this session; MEDIUM/LOW items are labeled, and LOW is never presented as fact.
|
|
102
102
|
- **HIGH** — all three checks pass. **MEDIUM** — coverage intact but some citations are off-version (latest docs for a pinned dependency) or secondary-source only. **LOW** — an API/library claim lacks any citation, or a LOW-confidence claim was asserted as fact.
|
|
103
103
|
|
|
104
|
-
Deterministic backstop: the `cited-version-strings
|
|
105
|
-
(`.spur/rules/docs/sourced-api-claims.yaml
|
|
106
|
-
assert version
|
|
107
|
-
|
|
104
|
+
Deterministic backstop: the `cited-version-strings`, `cited-api-claims` and
|
|
105
|
+
`cited-api-claims-confidence` rules (`.spur/rules/docs/sourced-api-claims.yaml`) flag markdown lines
|
|
106
|
+
that assert a version string or a time-anchored API claim with no citation anchor on the same line,
|
|
107
|
+
and claim lines that carry no HIGH/MEDIUM/LOW level. They are an info-severity advisory backstop for
|
|
108
|
+
this contract and are deliberately NOT wired into `recommended-pre-check` — run them ad hoc with
|
|
109
|
+
`spur rule run --file .spur/rules/docs/sourced-api-claims.yaml`. The checks above and the SECUA
|
|
110
|
+
checklist remain the enforcement points.
|
|
108
111
|
|
|
109
112
|
## Common Rationalizations
|
|
110
113
|
|
|
@@ -8,8 +8,9 @@ see_also:
|
|
|
8
8
|
# spur builder - release plumbing
|
|
9
9
|
|
|
10
10
|
`spur builder` is the CLI for **version bumps and release tags**. It wraps the internal
|
|
11
|
-
`
|
|
12
|
-
`scripts/commands/release.ts`
|
|
11
|
+
`release-ops` implementation behind a public two-verb surface. The repo-local forwarder
|
|
12
|
+
(`scripts/commands/release.ts`) and its `spur-dev` dispatcher cases were retired in task 1173; the
|
|
13
|
+
root `bun run bump-ver` / `bun run drop-tags` names route to `spur builder` directly. Package ids are
|
|
13
14
|
the unscoped short names (`@gobing-ai/spur` → `spur`); the released set and the aggregate tag are
|
|
14
15
|
discovered from the repo's own workspace manifests, so the same code serves any git+semver
|
|
15
16
|
monorepo.
|