@gobing-ai/spur 0.4.2 → 0.4.4
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/docs/verdict-confidence.yaml +68 -0
- package/config/rules/quality/coverage-gate.yaml +13 -0
- package/config/rules/recommended-pre-check.yaml +6 -2
- package/config/rules/strict/runtime-boundaries.yaml +10 -0
- package/config/rules/surface/check-cli-surface.yaml +1 -3
- package/config/script-placement-baseline.json +1 -19
- package/config/templates/AGENTS.md +7 -0
- package/config/workflows/feature-lifecycle.yaml +46 -1
- package/config/workflows/feature-verification.yaml +20 -1
- package/config/workflows/task-lifecycle.yaml +50 -0
- package/config/workflows/task-pipeline.yaml +15 -9
- package/package.json +10 -10
- package/plugins/sp/README.md +22 -16
- package/plugins/sp/agents/super-reviewer.md +46 -0
- package/plugins/sp/bunfig.toml +8 -5
- package/plugins/sp/commands/dev-changelog.md +1 -1
- package/plugins/sp/commands/dev-gitmsg.md +6 -2
- package/plugins/sp/commands/dev-handover.md +1 -0
- package/plugins/sp/commands/dev-job-dump.md +2 -1
- package/plugins/sp/commands/dev-refine.md +14 -2
- package/plugins/sp/commands/dev-refineall.md +7 -1
- package/plugins/sp/commands/dev-review.md +1 -0
- 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/history-anatomy.generated.mjs +18 -18
- package/plugins/sp/lib/idea-handoff.generated.mjs +91 -91
- 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 +1425 -212
- package/plugins/sp/lib/plan-projection.generated.d.mts +150 -0
- package/plugins/sp/lib/plan-projection.generated.mjs +623 -9
- package/plugins/sp/lib/quality-gate.generated.d.mts +1 -0
- package/plugins/sp/lib/quality-gate.generated.mjs +13809 -13969
- package/plugins/sp/lib/residual-scan.generated.d.mts +20 -1
- package/plugins/sp/lib/residual-scan.generated.mjs +120 -25
- package/plugins/sp/lib/run-summary-core.ts +34 -370
- package/plugins/sp/lib/step-profile.generated.mjs +8 -8
- package/plugins/sp/lib/transcript.ts +37 -228
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/scripts/batch-plan.mjs +619 -11
- package/plugins/sp/scripts/batch-preflight.mjs +57 -13
- package/plugins/sp/scripts/batch-preflight.ts +35 -295
- package/plugins/sp/scripts/commit-guard.mjs +6 -6
- package/plugins/sp/scripts/daily-summary/daily-summary.mjs +127 -108
- package/plugins/sp/scripts/daily-summary/daily-summary.ts +111 -104
- package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.mjs +13 -12
- package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts +7 -265
- package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +7 -4
- package/plugins/sp/scripts/dogfood-testing/validate-report.ts +6 -272
- package/plugins/sp/scripts/feature-verification-steps.mjs +20 -125
- package/plugins/sp/scripts/feature-verification-steps.ts +57 -181
- package/plugins/sp/scripts/history-anatomy-cache.mjs +19 -19
- package/plugins/sp/scripts/idea-handoff.mjs +2 -2
- package/plugins/sp/scripts/inline-run-dispatch.mjs +25 -24
- package/plugins/sp/scripts/inline-run-dispatch.ts +1 -21
- package/plugins/sp/scripts/inline-run-setup.mjs +164 -137
- package/plugins/sp/scripts/inline-run-setup.ts +1 -21
- package/plugins/sp/scripts/persist-out-check.mjs +5 -5
- package/plugins/sp/scripts/pr-reviewing.mjs +13 -13
- package/plugins/sp/scripts/quality-gate.mjs +13730 -13887
- package/plugins/sp/scripts/residual-scan.mjs +129 -31
- package/plugins/sp/scripts/residual-scan.ts +16 -13
- package/plugins/sp/scripts/run-summary.mjs +166 -112
- package/plugins/sp/scripts/run-summary.ts +7 -5
- package/plugins/sp/scripts/script-root.mjs +6 -6
- package/plugins/sp/scripts/session-timeline.mjs +62 -35
- package/plugins/sp/scripts/task-diffstat.mjs +19 -9
- package/plugins/sp/scripts/task-diffstat.ts +8 -39
- package/plugins/sp/scripts/workflow-step-profile.mjs +12 -12
- package/plugins/sp/scripts/wrapup-drift-probe.mjs +9 -9
- package/plugins/sp/scripts/wrapup-steps.mjs +25 -25
- 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/next-router/SKILL.md +3 -1
- package/plugins/sp/skills/next-router/references/routing-table.md +9 -3
- 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-cli/references/tasks.md +24 -4
- package/plugins/sp/skills/spur-dev/references/cross-cutting.md +8 -1
- package/plugins/sp/skills/spur-dev/references/dev-operations.md +37 -11
- package/plugins/sp/skills/spur-dev/references/execution-batch-report.md +1 -1
- package/plugins/sp/skills/spur-dev/references/execution-batch.md +19 -6
- 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/flag-glossary.md +4 -2
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +49 -12
- package/schemas/section-matrix.schema.json +3 -2
- package/spur.js +66591 -73262
- package/web/_astro/{BoardApp.B3GQZvj8.js → BoardApp.Niubpe6z.js} +1 -1
- package/web/_astro/{BoardApp.BT7L2kSb.js → BoardApp.hF0RAIrg.js} +150 -150
- package/web/_astro/TaskDetail.DJpapt38.js +1 -0
- package/web/_astro/{arc.vaT7tPOB.js → arc.BgKcK0i8.js} +1 -1
- package/web/_astro/{architectureDiagram-3BPJPVTR.DRMmTjkN.js → architectureDiagram-3BPJPVTR.ChP-Ig0V.js} +1 -1
- package/web/_astro/{blockDiagram-GPEHLZMM.cla0LO_q.js → blockDiagram-GPEHLZMM.BVaCBoeN.js} +1 -1
- package/web/_astro/{c4Diagram-AAUBKEIU.D4YAIBKn.js → c4Diagram-AAUBKEIU.CHSDpEO5.js} +1 -1
- package/web/_astro/channel.DkpvN2tD.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.BrSJbMRZ.js → chunk-2J33WTMH.MYRcYVRa.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.CuLMgKF5.js → chunk-4BX2VUAB.S5j22Zqa.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.zkvgu54F.js → chunk-55IACEB6.gciRPvmZ.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.DxWkACh8.js → chunk-727SXJPM.CdqrSQkX.js} +1 -1
- package/web/_astro/{chunk-AQP2D5EJ.42XrmIlH.js → chunk-AQP2D5EJ.CyokdHT3.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.Bao2wMK9.js → chunk-FMBD7UC4.Bg9KJhjW.js} +1 -1
- package/web/_astro/{chunk-ND2GUHAM.BhydojD1.js → chunk-ND2GUHAM.1gxxI-vD.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.CmZ82g-H.js → chunk-QZHKN3VN.DDEYtCKU.js} +1 -1
- package/web/_astro/{classDiagram-4FO5ZUOK.Blr83Gt_.js → classDiagram-4FO5ZUOK.Ch3zi37T.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.Blr83Gt_.js → classDiagram-v2-Q7XG4LA2.Ch3zi37T.js} +1 -1
- package/web/_astro/{cose-bilkent-S5V4N54A.qmfv0SPn.js → cose-bilkent-S5V4N54A.D2q5AINI.js} +1 -1
- package/web/_astro/{cynefin-OW5HDTMX.BtELhXPR.js → cynefin-OW5HDTMX.BA9kC2YT.js} +1 -1
- package/web/_astro/{dagre-BM42HDAG.AWmaAvXo.js → dagre-BM42HDAG.CHO08nRp.js} +1 -1
- package/web/_astro/{diagram-2AECGRRQ.DdH34KDi.js → diagram-2AECGRRQ.BfFQt0LW.js} +1 -1
- package/web/_astro/{diagram-5GNKFQAL.BmNPix1k.js → diagram-5GNKFQAL.CHMwMTe4.js} +1 -1
- package/web/_astro/{diagram-KO2AKTUF.BNQd_Ppo.js → diagram-KO2AKTUF.BJ-7gK-w.js} +1 -1
- package/web/_astro/{diagram-LMA3HP47.BAPgDi0e.js → diagram-LMA3HP47.C5xqcc0t.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.DZ4zyLdC.js → diagram-OG6HWLK6.C2pQjfrC.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35.Q1TySMmw.js → erDiagram-TEJ5UH35._FoNyxgC.js} +1 -1
- package/web/_astro/{flowDiagram-I6XJVG4X.BXhFBicG.js → flowDiagram-I6XJVG4X.B_ENkT2I.js} +1 -1
- package/web/_astro/{ganttDiagram-6RSMTGT7.vHSgcuo2.js → ganttDiagram-6RSMTGT7.B3q8h77C.js} +1 -1
- package/web/_astro/{gitGraphDiagram-PVQCEYII.Bi1f73C7.js → gitGraphDiagram-PVQCEYII.QWZFDboZ.js} +1 -1
- package/web/_astro/index.BFyW13JJ.css +1 -0
- package/web/_astro/{infoDiagram-5YYISTIA.DPvXZEWm.js → infoDiagram-5YYISTIA.Cs6eEc92.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.Cz829fsV.js → ishikawaDiagram-YF4QCWOH.BU9DJMHA.js} +1 -1
- package/web/_astro/{journeyDiagram-JHISSGLW.enRnB9Me.js → journeyDiagram-JHISSGLW.D35k6Jpe.js} +1 -1
- package/web/_astro/{kanban-definition-UN3LZRKU.DnTNk6d2.js → kanban-definition-UN3LZRKU.DBqcAT4m.js} +1 -1
- package/web/_astro/{linear.BzGnBwNL.js → linear.D-LSNK8W.js} +1 -1
- package/web/_astro/{mermaid.core.CSbTpIVz.js → mermaid.core.CSruWVOa.js} +4 -4
- package/web/_astro/{mindmap-definition-RKZ34NQL.DGJDy8zT.js → mindmap-definition-RKZ34NQL.BiFaUsfj.js} +1 -1
- package/web/_astro/{pieDiagram-4H26LBE5.HSPdi5_8.js → pieDiagram-4H26LBE5.D1KTKqb9.js} +1 -1
- package/web/_astro/{quadrantDiagram-W4KKPZXB.6Gv8mz2Y.js → quadrantDiagram-W4KKPZXB.DBHflsMG.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.CnBCtFwa.js → requirementDiagram-4Y6WPE33.hwX9CNdk.js} +1 -1
- package/web/_astro/{sankeyDiagram-5OEKKPKP.5M8VWAu8.js → sankeyDiagram-5OEKKPKP.DkI8mJiE.js} +1 -1
- package/web/_astro/{sequenceDiagram-3UESZ5HK.DN41XN-U.js → sequenceDiagram-3UESZ5HK.DMz7jBDx.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.ErOO3W8j.js → stateDiagram-AJRCARHV.B6Ef2xJ1.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.B_ZHICM-.js → stateDiagram-v2-BHNVJYJU.CgoaufB9.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.CWdosr07.js → timeline-definition-PNZ67QCA.B3c8h1t-.js} +1 -1
- package/web/_astro/{vennDiagram-CIIHVFJN.BHzYjNV5.js → vennDiagram-CIIHVFJN.BdHPAxAb.js} +1 -1
- package/web/_astro/{wardleyDiagram-YWT4CUSO.h8x4IwPX.js → wardleyDiagram-YWT4CUSO.BrJXPPAj.js} +1 -1
- package/web/_astro/{xychartDiagram-2RQKCTM6.RTapW2X8.js → xychartDiagram-2RQKCTM6.CMH5yFsV.js} +1 -1
- package/web/index.html +2 -2
- package/web/spur_logo.svg +1 -1
- package/config/proportional-route-table.ts +0 -155
- package/plugins/sp/scripts/daily-summary/logger.ts +0 -28
- package/web/_astro/TaskDetail.Cxov1_N_.js +0 -1
- 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
|
@@ -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,61 @@ function blockingReviewFindings(taskContent, deferrals) {
|
|
|
179
210
|
}));
|
|
180
211
|
return classify(items, [...tableDeferrals, ...deferrals]);
|
|
181
212
|
}
|
|
182
|
-
function
|
|
183
|
-
const
|
|
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
|
+
}
|
|
232
|
+
function reviewConfidenceViolation(reviewAnswer, ackLowConfidence) {
|
|
233
|
+
if (ackLowConfidence === "true")
|
|
234
|
+
return null;
|
|
235
|
+
const level = extractAnswerConfidence(reviewAnswer.replace(/\*\*/g, ""));
|
|
236
|
+
if (level === "HIGH" || level === "MEDIUM")
|
|
237
|
+
return null;
|
|
238
|
+
return {
|
|
239
|
+
location: "review-answer:Confidence",
|
|
240
|
+
text: level === undefined ? "The review answer carries no valid `**Confidence:** HIGH | MEDIUM | LOW` line — a " + "PASS must be graded, or the operator must set ackLowConfidence" : `The review answer graded its PASS ${level} — HIGH/MEDIUM cross unadorned, ` + "LOW/absent needs the operator’s ackLowConfidence"
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
function reviewGradingInput(root, reviewAnswer, ackLowConfidence, fs) {
|
|
244
|
+
if (reviewAnswer === undefined)
|
|
245
|
+
return;
|
|
246
|
+
const path = join(root, reviewAnswer);
|
|
247
|
+
let isFile = false;
|
|
248
|
+
try {
|
|
249
|
+
isFile = fs.statSync(path).isFile();
|
|
250
|
+
} catch {
|
|
251
|
+
isFile = false;
|
|
252
|
+
}
|
|
253
|
+
return { answer: isFile ? fs.readFileSync(path, "utf8") : "", ackLowConfidence };
|
|
254
|
+
}
|
|
255
|
+
function buildReviewGateArtifact(wbs, taskContent, deferrals, review) {
|
|
256
|
+
const confidence = review === undefined ? null : reviewConfidenceViolation(review.answer, review.ackLowConfidence);
|
|
257
|
+
const contractViolations = [...reviewContractViolations(taskContent), ...confidence === null ? [] : [confidence]];
|
|
258
|
+
const items = [
|
|
259
|
+
...blockingReviewFindings(taskContent, deferrals),
|
|
260
|
+
...contractViolations.map((v) => ({
|
|
261
|
+
id: makeItemId("review-contract", v.location, v.text),
|
|
262
|
+
category: "review-contract",
|
|
263
|
+
class: "blocking",
|
|
264
|
+
location: v.location,
|
|
265
|
+
text: v.text
|
|
266
|
+
}))
|
|
267
|
+
];
|
|
184
268
|
const blocking = items.filter((i) => i.class === "blocking");
|
|
185
269
|
const counts = { blocking: 0, deferrable: 0, advisory: 0, housekeeping: 0 };
|
|
186
270
|
for (const item of items)
|
|
@@ -192,7 +276,8 @@ function buildReviewGateArtifact(wbs, taskContent, deferrals) {
|
|
|
192
276
|
"review-finding": true,
|
|
193
277
|
"diff-marker": false,
|
|
194
278
|
"unchecked-box": false,
|
|
195
|
-
"staging-residue": false
|
|
279
|
+
"staging-residue": false,
|
|
280
|
+
"review-contract": true
|
|
196
281
|
},
|
|
197
282
|
items,
|
|
198
283
|
counts
|
|
@@ -305,6 +390,7 @@ function parseScanArgs(argv, cwd, defaultTmpDir) {
|
|
|
305
390
|
let spurBin;
|
|
306
391
|
let root = cwd;
|
|
307
392
|
let tmpDir = defaultTmpDir;
|
|
393
|
+
let reviewAnswer;
|
|
308
394
|
for (let i = 0;i < argv.length; i++) {
|
|
309
395
|
const a = argv[i];
|
|
310
396
|
if (a === undefined)
|
|
@@ -315,6 +401,8 @@ function parseScanArgs(argv, cwd, defaultTmpDir) {
|
|
|
315
401
|
root = argv[++i] ?? root;
|
|
316
402
|
else if (a === "--tmp-dir")
|
|
317
403
|
tmpDir = argv[++i] ?? tmpDir;
|
|
404
|
+
else if (a === "--review-answer")
|
|
405
|
+
reviewAnswer = argv[++i];
|
|
318
406
|
else if (a === "--help" || a === "-h")
|
|
319
407
|
return null;
|
|
320
408
|
else if (mode === "")
|
|
@@ -322,7 +410,7 @@ function parseScanArgs(argv, cwd, defaultTmpDir) {
|
|
|
322
410
|
else if (wbs === "")
|
|
323
411
|
wbs = a;
|
|
324
412
|
}
|
|
325
|
-
return mode === "" || wbs === "" ? null : { mode, wbs, spurBin, root, tmpDir };
|
|
413
|
+
return mode === "" || wbs === "" ? null : { mode, wbs, spurBin, root, tmpDir, reviewAnswer };
|
|
326
414
|
}
|
|
327
415
|
|
|
328
416
|
// plugins/sp/lib/spur-bin.ts
|
|
@@ -332,7 +420,7 @@ function spurCommand(spurBin) {
|
|
|
332
420
|
}
|
|
333
421
|
|
|
334
422
|
// plugins/sp/scripts/residual-scan.ts
|
|
335
|
-
var RESIDUAL_SCAN_USAGE = "usage: residual-scan.ts <scan|fold|settle|report|review-gate> <wbs> [--spur-bin <bin>] [--root <dir>] [--tmp-dir <dir>]";
|
|
423
|
+
var RESIDUAL_SCAN_USAGE = "usage: residual-scan.ts <scan|fold|settle|report|review-gate> <wbs> [--spur-bin <bin>] [--root <dir>] [--tmp-dir <dir>] [--review-answer <path>]";
|
|
336
424
|
function run(cmd, args, cwd) {
|
|
337
425
|
const result = spawnSync(cmd, args, { cwd, encoding: "utf8" });
|
|
338
426
|
if (result.error !== undefined)
|
|
@@ -427,7 +515,8 @@ function reviewGateMode(opts, env, io) {
|
|
|
427
515
|
const runDir = join2(opts.root, ".spur", "run");
|
|
428
516
|
fs.mkdirSync(runDir, { recursive: true });
|
|
429
517
|
const { content } = loadTask(env, opts.spurBin, opts.wbs, opts.root);
|
|
430
|
-
const
|
|
518
|
+
const grading = reviewGradingInput(opts.root, opts.reviewAnswer, env.ackLowConfidence, fs);
|
|
519
|
+
const gate = buildReviewGateArtifact(opts.wbs, content, readDeferrals(runDir, opts.wbs), grading);
|
|
431
520
|
fs.writeFileSync(join2(runDir, `${opts.wbs}-residuals.json`), `${JSON.stringify(gate.artifact, null, 2)}
|
|
432
521
|
`);
|
|
433
522
|
io.out(gate.note);
|
|
@@ -518,6 +607,12 @@ function settleMode(opts, env, io) {
|
|
|
518
607
|
}
|
|
519
608
|
function reportMode(opts, env, io) {
|
|
520
609
|
const runDir = join2(opts.root, ".spur", "run");
|
|
610
|
+
const verdictPath = recordedVerdictPath(runDir, opts.wbs, fs);
|
|
611
|
+
if (!fs.existsSync(verdictPath)) {
|
|
612
|
+
io.err(`residual-report: no verdict at ${verdictPath}; nothing to report for ${opts.wbs}
|
|
613
|
+
`);
|
|
614
|
+
return 0;
|
|
615
|
+
}
|
|
521
616
|
const sweep = loadVerdict(runDir, opts.wbs).checks.find((c) => c.name === "residual-sweep");
|
|
522
617
|
if (sweep === undefined || sweep.status !== "fail")
|
|
523
618
|
return 0;
|
|
@@ -556,26 +651,29 @@ function main(argv, env = getEnvVars(), options = {}) {
|
|
|
556
651
|
}
|
|
557
652
|
process.exit(main(process.argv.slice(2)));
|
|
558
653
|
export {
|
|
559
|
-
|
|
560
|
-
scanResiduals2 as scanResiduals,
|
|
561
|
-
renderReport,
|
|
562
|
-
recordedVerdictPath,
|
|
563
|
-
parseScanArgs,
|
|
564
|
-
parseReviewFindings,
|
|
565
|
-
parseDiffMarkers,
|
|
566
|
-
parseDeferralEntries,
|
|
567
|
-
normalizeAnchor,
|
|
568
|
-
makeItemId,
|
|
569
|
-
main,
|
|
570
|
-
locationOf,
|
|
571
|
-
listStagingResidue,
|
|
572
|
-
foldVerdict,
|
|
573
|
-
findUncheckedBoxes,
|
|
574
|
-
collectAddedLines,
|
|
575
|
-
classify,
|
|
576
|
-
buildReviewGateArtifact,
|
|
577
|
-
blockingReviewFindings,
|
|
578
|
-
blockingAnchors,
|
|
654
|
+
ALLOW_PRAGMA,
|
|
579
655
|
RESIDUAL_SCAN_USAGE,
|
|
580
|
-
|
|
656
|
+
blockingAnchors,
|
|
657
|
+
blockingReviewFindings,
|
|
658
|
+
buildReviewGateArtifact,
|
|
659
|
+
classify,
|
|
660
|
+
collectAddedLines,
|
|
661
|
+
findUncheckedBoxes,
|
|
662
|
+
foldVerdict,
|
|
663
|
+
listStagingResidue,
|
|
664
|
+
locationOf,
|
|
665
|
+
main,
|
|
666
|
+
makeItemId,
|
|
667
|
+
normalizeAnchor,
|
|
668
|
+
parseDeferralEntries,
|
|
669
|
+
parseDiffMarkers,
|
|
670
|
+
parseReviewFindings,
|
|
671
|
+
parseScanArgs,
|
|
672
|
+
recordedVerdictPath,
|
|
673
|
+
renderReport,
|
|
674
|
+
reviewConfidenceViolation,
|
|
675
|
+
reviewContractViolations,
|
|
676
|
+
reviewGradingInput,
|
|
677
|
+
scanResiduals2 as scanResiduals,
|
|
678
|
+
verdictDisagreementNote
|
|
581
679
|
};
|
|
@@ -10,7 +10,7 @@ import { spurCommand } from '../lib/spur-bin';
|
|
|
10
10
|
|
|
11
11
|
export * from '../lib/residual-scan.generated.mjs';
|
|
12
12
|
export const RESIDUAL_SCAN_USAGE =
|
|
13
|
-
'usage: residual-scan.ts <scan|fold|settle|report|review-gate> <wbs> [--spur-bin <bin>] [--root <dir>] [--tmp-dir <dir>]';
|
|
13
|
+
'usage: residual-scan.ts <scan|fold|settle|report|review-gate> <wbs> [--spur-bin <bin>] [--root <dir>] [--tmp-dir <dir>] [--review-answer <path>]';
|
|
14
14
|
export type ScanEnv = { spurBin?: string } & Record<string, string | undefined>;
|
|
15
15
|
export type ScanIo = { out: (line: string) => void; err: (line: string) => void };
|
|
16
16
|
export type ScanOptions = { cwd?: string; io?: ScanIo };
|
|
@@ -104,18 +104,18 @@ function scanMode(opts: ParsedArgs, env: ScanEnv, io: ScanIo): number {
|
|
|
104
104
|
return 0;
|
|
105
105
|
}
|
|
106
106
|
/**
|
|
107
|
-
* Task 1122 R2: the review-time gate — the record sweep's review-finding slice
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
* into review-fail-triage.
|
|
107
|
+
* Task 1122 R2: the review-time gate — the record sweep's review-finding slice
|
|
108
|
+
* (core.buildReviewGateArtifact) at the review PASS edges; writes the same `<wbs>-residuals.json`
|
|
109
|
+
* the test-fix hop reads. Exit 1 on an open P1-P3 finding, or on a review answer graded LOW or left
|
|
110
|
+
* ungraded without `env.ackLowConfidence` (task 1204, graded via `core.reviewGradingInput` — it
|
|
111
|
+
* cannot sit inline in the edge because ADR-115 caps a guard at 5 logical commands).
|
|
113
112
|
*/
|
|
114
113
|
function reviewGateMode(opts: ParsedArgs, env: ScanEnv, io: ScanIo): number {
|
|
115
114
|
const runDir = join(opts.root, '.spur', 'run');
|
|
116
115
|
fs.mkdirSync(runDir, { recursive: true });
|
|
117
116
|
const { content } = loadTask(env, opts.spurBin, opts.wbs, opts.root);
|
|
118
|
-
const
|
|
117
|
+
const grading = core.reviewGradingInput(opts.root, opts.reviewAnswer, env.ackLowConfidence, fs);
|
|
118
|
+
const gate = core.buildReviewGateArtifact(opts.wbs, content, readDeferrals(runDir, opts.wbs), grading);
|
|
119
119
|
fs.writeFileSync(join(runDir, `${opts.wbs}-residuals.json`), `${JSON.stringify(gate.artifact, null, 2)}\n`);
|
|
120
120
|
io.out(gate.note);
|
|
121
121
|
return gate.artifact.counts.blocking > 0 ? 1 : 0;
|
|
@@ -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);
|
|
@@ -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)
|
|
@@ -107,21 +114,21 @@ function accumulate(acc, row, ts) {
|
|
|
107
114
|
if (row.type === "message") {
|
|
108
115
|
const m = row.message;
|
|
109
116
|
if (m?.role === "assistant") {
|
|
110
|
-
const
|
|
111
|
-
for (const b of
|
|
117
|
+
const blocks = Array.isArray(m.content) ? m.content : [];
|
|
118
|
+
for (const b of blocks) {
|
|
112
119
|
if (b.type === "toolCall" && b.id) {
|
|
113
120
|
acc.toolIds.add(b.id);
|
|
114
121
|
if (b.name && OPERATOR_TOOLS.has(b.name))
|
|
115
122
|
acc.asks.set(b.id, ts);
|
|
116
123
|
}
|
|
117
124
|
}
|
|
118
|
-
const
|
|
119
|
-
if (
|
|
125
|
+
const u = m.usage;
|
|
126
|
+
if (u && row.id)
|
|
120
127
|
acc.messages.set(row.id, {
|
|
121
|
-
input:
|
|
122
|
-
cacheCreate:
|
|
123
|
-
cacheRead:
|
|
124
|
-
output:
|
|
128
|
+
input: u.input ?? 0,
|
|
129
|
+
cacheCreate: u.cacheWrite ?? 0,
|
|
130
|
+
cacheRead: u.cacheRead ?? 0,
|
|
131
|
+
output: u.output ?? 0
|
|
125
132
|
});
|
|
126
133
|
} else if (m?.role === "toolResult" && m.toolCallId && acc.asks.has(m.toolCallId)) {
|
|
127
134
|
acc.askMs += ts - (acc.asks.get(m.toolCallId) ?? ts);
|
|
@@ -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
|
`);
|
|
@@ -693,12 +747,12 @@ function main(argv, env = getEnvVars(), write = (s) => process.stdout.write(s),
|
|
|
693
747
|
}
|
|
694
748
|
process.exit(main(process.argv.slice(2)));
|
|
695
749
|
export {
|
|
696
|
-
|
|
697
|
-
renderSummaryMarkdown,
|
|
698
|
-
renderRollupMarkdown,
|
|
699
|
-
main,
|
|
700
|
-
gateFromReceipt,
|
|
701
|
-
buildRunSummary,
|
|
750
|
+
RUN_SUMMARY_USAGE,
|
|
702
751
|
actorSplit,
|
|
703
|
-
|
|
752
|
+
buildRunSummary,
|
|
753
|
+
gateFromReceipt,
|
|
754
|
+
main,
|
|
755
|
+
renderRollupMarkdown,
|
|
756
|
+
renderSummaryMarkdown,
|
|
757
|
+
writeCloseSummary
|
|
704
758
|
};
|