@gobing-ai/spur 0.4.1 → 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/config.global.yaml +4 -2
- 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/templates/AGENTS.md +1 -1
- package/config/workflows/feature-lifecycle.yaml +46 -1
- package/config/workflows/feature-verification.yaml +11 -0
- package/config/workflows/wrapup-pipeline.yaml +1 -1
- package/package.json +10 -10
- package/plugins/sp/README.md +23 -18
- package/plugins/sp/agents/super-planner.md +5 -6
- package/plugins/sp/agents/super-reviewer.md +43 -0
- package/plugins/sp/bunfig.toml +8 -5
- package/plugins/sp/commands/dev-review-session.md +2 -1
- package/plugins/sp/commands/dev-runall.md +18 -9
- package/plugins/sp/hooks/careful-guard.ts +1 -1
- package/plugins/sp/hooks/context-post-tool.ts +7 -404
- package/plugins/sp/hooks/context-session-start.ts +6 -192
- package/plugins/sp/hooks/context-session-stop.ts +6 -91
- package/plugins/sp/hooks/pi/guard-extension.ts +26 -12
- 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-owner.ts +56 -0
- 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 +6 -4
- 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 +1361 -87
- package/plugins/sp/lib/plan-projection.generated.d.mts +169 -0
- package/plugins/sp/lib/plan-projection.generated.mjs +704 -1
- package/plugins/sp/lib/quality-gate.generated.d.mts +0 -12
- package/plugins/sp/lib/quality-gate.generated.mjs +62 -231
- 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 -200
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/references/roles.md +1 -1
- package/plugins/sp/scripts/batch-plan.mjs +740 -1
- package/plugins/sp/scripts/batch-plan.ts +48 -3
- 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 +158 -120
- package/plugins/sp/scripts/inline-run-setup.ts +1 -21
- package/plugins/sp/scripts/pr-reviewing.mjs +19 -5
- package/plugins/sp/scripts/pr-reviewing.ts +34 -9
- package/plugins/sp/scripts/quality-gate.mjs +38 -208
- 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 +160 -95
- package/plugins/sp/scripts/run-summary.ts +7 -5
- package/plugins/sp/scripts/session-timeline.mjs +57 -29
- package/plugins/sp/scripts/session-timeline.ts +4 -18
- package/plugins/sp/scripts/task-diffstat.mjs +43 -30
- package/plugins/sp/scripts/task-diffstat.ts +40 -71
- 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/indexed-context/SKILL.md +14 -7
- package/plugins/sp/skills/next-feature/SKILL.md +1 -1
- package/plugins/sp/skills/parallel-execution/SKILL.md +2 -2
- package/plugins/sp/skills/parallel-execution/references/dispatch-surface.md +2 -2
- package/plugins/sp/skills/parallel-execution/references/fan-out-patterns.md +2 -2
- package/plugins/sp/skills/pr-reviewing/SKILL.md +7 -3
- package/plugins/sp/skills/session-review/SKILL.md +5 -3
- 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 +40 -8
- package/plugins/sp/skills/spur-cli/references/features.md +46 -5
- package/plugins/sp/skills/spur-cli/references/projects.md +7 -5
- package/plugins/sp/skills/spur-dev/SKILL.md +2 -0
- package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +27 -0
- package/plugins/sp/skills/spur-dev/references/dev-operations.md +2 -3
- package/plugins/sp/skills/spur-dev/references/execution-batch-report.md +6 -1
- package/plugins/sp/skills/spur-dev/references/execution-batch.md +56 -7
- package/plugins/sp/skills/spur-dev/references/execution-parallel-isolation.md +4 -1
- 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 +13 -11
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +37 -25
- package/spur.js +89792 -76992
- package/web/_astro/{BoardApp.D66k_s_T.js → BoardApp.5IIl2O-z.js} +1 -1
- package/web/_astro/BoardApp.Drnx-KeM.js +324 -0
- package/web/_astro/TaskDetail.Ci4LNn9S.js +1 -0
- package/web/_astro/{arc._BUadrdH.js → arc.j2EVkYVd.js} +1 -1
- package/web/_astro/{architectureDiagram-3BPJPVTR.Dh7xS1Pp.js → architectureDiagram-3BPJPVTR.DZWSFqEG.js} +1 -1
- package/web/_astro/{blockDiagram-GPEHLZMM.DFDVpfXm.js → blockDiagram-GPEHLZMM.DE6GpXCN.js} +1 -1
- package/web/_astro/{c4Diagram-AAUBKEIU.V7_3_glh.js → c4Diagram-AAUBKEIU.DoVR1G6B.js} +1 -1
- package/web/_astro/channel.DmdDxGsq.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.De1yx7Jz.js → chunk-2J33WTMH.Do6-Gbyf.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.BDhHFuxx.js → chunk-4BX2VUAB.BYV1HGHx.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.DDLkWnmC.js → chunk-55IACEB6.CvI1zkrc.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.CwfiLnA7.js → chunk-727SXJPM.DNGSCRlX.js} +1 -1
- package/web/_astro/{chunk-AQP2D5EJ.CtGDmUnM.js → chunk-AQP2D5EJ.DXhhgT5z.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.BOLjpw73.js → chunk-FMBD7UC4.dzkRKP2u.js} +1 -1
- package/web/_astro/{chunk-ND2GUHAM.BDhQls6O.js → chunk-ND2GUHAM.DujGLeEf.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.76qBynDF.js → chunk-QZHKN3VN.BWtIT3ZU.js} +1 -1
- package/web/_astro/{classDiagram-4FO5ZUOK.N0mpPHT0.js → classDiagram-4FO5ZUOK.DyApg0Nn.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.N0mpPHT0.js → classDiagram-v2-Q7XG4LA2.DyApg0Nn.js} +1 -1
- package/web/_astro/{cose-bilkent-S5V4N54A.CKknFfjB.js → cose-bilkent-S5V4N54A.DJQB-u_l.js} +1 -1
- package/web/_astro/{cynefin-OW5HDTMX.Cw3ZWBAh.js → cynefin-OW5HDTMX.bi3D_siV.js} +1 -1
- package/web/_astro/{dagre-BM42HDAG.D3yIKCRD.js → dagre-BM42HDAG.C-f7TrRg.js} +1 -1
- package/web/_astro/{diagram-2AECGRRQ.D51dO83Z.js → diagram-2AECGRRQ.DrPfjHQB.js} +1 -1
- package/web/_astro/{diagram-5GNKFQAL.Bb-FViDj.js → diagram-5GNKFQAL.C9S4WUHO.js} +1 -1
- package/web/_astro/{diagram-KO2AKTUF.D3mN8S6x.js → diagram-KO2AKTUF.Dzzye8QH.js} +1 -1
- package/web/_astro/{diagram-LMA3HP47.dMMyPcbJ.js → diagram-LMA3HP47.DX-LRiof.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.AzbvzY4C.js → diagram-OG6HWLK6.Z-kAKwIz.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35.BR5I4OZb.js → erDiagram-TEJ5UH35.D6dyClSv.js} +1 -1
- package/web/_astro/{flowDiagram-I6XJVG4X.DFqvhGkn.js → flowDiagram-I6XJVG4X.Cx3q2cxD.js} +1 -1
- package/web/_astro/{ganttDiagram-6RSMTGT7.ByJbhH6J.js → ganttDiagram-6RSMTGT7.DU3gH6CT.js} +1 -1
- package/web/_astro/{gitGraphDiagram-PVQCEYII.CInHjQR3.js → gitGraphDiagram-PVQCEYII.ISmCnJ87.js} +1 -1
- package/web/_astro/index.Do2lLdgo.css +1 -0
- package/web/_astro/{infoDiagram-5YYISTIA.Cm0dGVjJ.js → infoDiagram-5YYISTIA.DR6_iNOK.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.C3nOP6Sj.js → ishikawaDiagram-YF4QCWOH.D7wY-ZGA.js} +1 -1
- package/web/_astro/{journeyDiagram-JHISSGLW.4IvLa2MW.js → journeyDiagram-JHISSGLW.DkP9-WK8.js} +1 -1
- package/web/_astro/{kanban-definition-UN3LZRKU.DS87Cl-k.js → kanban-definition-UN3LZRKU.wtgY98M2.js} +1 -1
- package/web/_astro/{linear.MzmRrNER.js → linear.CI6djNi7.js} +1 -1
- package/web/_astro/{mermaid.core.fK-WNswS.js → mermaid.core.CuQPxS8b.js} +4 -4
- package/web/_astro/{mindmap-definition-RKZ34NQL.BqipOHhY.js → mindmap-definition-RKZ34NQL.5HNIEG7R.js} +1 -1
- package/web/_astro/{pieDiagram-4H26LBE5.0nBiPqFG.js → pieDiagram-4H26LBE5.CchhDFjE.js} +1 -1
- package/web/_astro/{quadrantDiagram-W4KKPZXB.Szj7Zpx5.js → quadrantDiagram-W4KKPZXB.PYXXJPSJ.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.D4UR6TWo.js → requirementDiagram-4Y6WPE33.cUQuNAKU.js} +1 -1
- package/web/_astro/{sankeyDiagram-5OEKKPKP.DAE7BdOs.js → sankeyDiagram-5OEKKPKP.BO7MnRIA.js} +1 -1
- package/web/_astro/{sequenceDiagram-3UESZ5HK.BP9ZVq25.js → sequenceDiagram-3UESZ5HK.D9bElyD6.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.BQW7mIxk.js → stateDiagram-AJRCARHV.DhGdRz7I.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.PbK4nDih.js → stateDiagram-v2-BHNVJYJU.5GvLeAoL.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.Ci_GvUpo.js → timeline-definition-PNZ67QCA.LtAT9haM.js} +1 -1
- package/web/_astro/{vennDiagram-CIIHVFJN.DNw1Y5qx.js → vennDiagram-CIIHVFJN.DRth1yLA.js} +1 -1
- package/web/_astro/{wardleyDiagram-YWT4CUSO.CwlkaaNe.js → wardleyDiagram-YWT4CUSO.h9MlZfcr.js} +1 -1
- package/web/_astro/{xychartDiagram-2RQKCTM6.MI6ZuiH_.js → xychartDiagram-2RQKCTM6.DHqy9DYA.js} +1 -1
- package/web/index.html +2 -2
- package/config/proportional-route-table.ts +0 -155
- package/plugins/sp/commands/dev-parallel.md +0 -37
- package/plugins/sp/scripts/daily-summary/logger.ts +0 -28
- package/web/_astro/BoardApp.D6sjAJr_.js +0 -324
- package/web/_astro/TaskDetail.BynLnMNc.js +0 -1
- package/web/_astro/channel.DZVfto-r.js +0 -1
- package/web/_astro/index.COh_kBxN.css +0 -1
- /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,18 +77,40 @@ 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) {
|
|
101
|
+
if (row.type === "custom_message") {
|
|
102
|
+
if (row.display === false)
|
|
103
|
+
return;
|
|
104
|
+
const text = typeof row.content === "string" ? row.content.trim() : "";
|
|
105
|
+
return text === "" ? undefined : text;
|
|
106
|
+
}
|
|
63
107
|
if (row.type === "message") {
|
|
64
108
|
if (row.message?.role !== "user")
|
|
65
109
|
return;
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const content2 = row.message.content;
|
|
69
|
-
if (typeof content2 === "string")
|
|
70
|
-
return content2;
|
|
71
|
-
if (!Array.isArray(content2))
|
|
110
|
+
const text = firstTextContent(row);
|
|
111
|
+
if (text === undefined)
|
|
72
112
|
return;
|
|
73
|
-
return
|
|
113
|
+
return skillInvocationPrompt(text) ?? text;
|
|
74
114
|
}
|
|
75
115
|
if (row.type !== "user" || row.isMeta || row.isCompactSummary)
|
|
76
116
|
return;
|
|
@@ -81,13 +121,6 @@ function promptText(row) {
|
|
|
81
121
|
return;
|
|
82
122
|
return content.find((b) => b.type === "text")?.text;
|
|
83
123
|
}
|
|
84
|
-
function isInjectedPrompt(row) {
|
|
85
|
-
if (row.type !== "message" || row.message?.role !== "user")
|
|
86
|
-
return false;
|
|
87
|
-
const content = row.message.content;
|
|
88
|
-
const text = typeof content === "string" ? content : Array.isArray(content) ? content.find((b) => b.type === "text")?.text : undefined;
|
|
89
|
-
return typeof text === "string" && text.replace(/^\s+/, "").startsWith('<skill name="');
|
|
90
|
-
}
|
|
91
124
|
function sumTokens(all) {
|
|
92
125
|
const total = zeroTokens();
|
|
93
126
|
for (const t of all)
|
|
@@ -159,72 +192,14 @@ function accumulate(acc, row, ts) {
|
|
|
159
192
|
});
|
|
160
193
|
}
|
|
161
194
|
}
|
|
162
|
-
function isCompaction(row) {
|
|
163
|
-
return row.type === "compaction" || row.type === "system" && row.subtype === "compact_boundary";
|
|
164
|
-
}
|
|
165
|
-
function sniffFormat(rows) {
|
|
166
|
-
for (const row of rows) {
|
|
167
|
-
if (row.type === "message" && PI_ROLES.has(row.message?.role ?? ""))
|
|
168
|
-
return "pi";
|
|
169
|
-
if (row.type === "user" || row.type === "assistant")
|
|
170
|
-
return "claude";
|
|
171
|
-
}
|
|
172
|
-
return "unknown";
|
|
173
|
-
}
|
|
174
|
-
function parseRows(lines) {
|
|
175
|
-
const rows = [];
|
|
176
|
-
let skippedLines = 0;
|
|
177
|
-
for (const line of lines) {
|
|
178
|
-
if (!line.trim())
|
|
179
|
-
continue;
|
|
180
|
-
let row;
|
|
181
|
-
try {
|
|
182
|
-
row = JSON.parse(line);
|
|
183
|
-
} catch {
|
|
184
|
-
skippedLines++;
|
|
185
|
-
continue;
|
|
186
|
-
}
|
|
187
|
-
const ts = row.timestamp ? Date.parse(row.timestamp) : Number.NaN;
|
|
188
|
-
if (!Number.isNaN(ts))
|
|
189
|
-
rows.push([row, ts]);
|
|
190
|
-
}
|
|
191
|
-
return { rows, skippedLines, format: sniffFormat(rows.map(([r]) => r)) };
|
|
192
|
-
}
|
|
193
|
-
function resolveTranscript(env, projectsRoot = join(homedir(), ".claude", "projects"), override) {
|
|
194
|
-
if (override)
|
|
195
|
-
return existsSync(override) ? { ok: true, path: override } : { ok: false, reason: "no transcript" };
|
|
196
|
-
const id = env.CLAUDE_CODE_SESSION_ID;
|
|
197
|
-
if (!id) {
|
|
198
|
-
const piFile = env.PI_SESSION_FILE;
|
|
199
|
-
if (piFile !== undefined && piFile !== "") {
|
|
200
|
-
return existsSync(piFile) ? { ok: true, path: piFile } : { ok: false, reason: `PI_SESSION_FILE ${piFile} does not exist` };
|
|
201
|
-
}
|
|
202
|
-
return {
|
|
203
|
-
ok: false,
|
|
204
|
-
reason: "no host session id; pass --transcript <path> (pi: ~/.pi/agent/sessions/<cwd-slug>/<file>.jsonl, " + "or set PI_SESSION_FILE)"
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
if (!SESSION_ID.test(id))
|
|
208
|
-
return { ok: false, reason: "refusing a session id with path characters" };
|
|
209
|
-
if (!existsSync(projectsRoot))
|
|
210
|
-
return { ok: false, reason: `no transcript root ${projectsRoot}` };
|
|
211
|
-
for (const dir of readdirSync(projectsRoot)) {
|
|
212
|
-
const path = join(projectsRoot, dir, `${id}.jsonl`);
|
|
213
|
-
if (existsSync(path))
|
|
214
|
-
return { ok: true, path };
|
|
215
|
-
}
|
|
216
|
-
return { ok: false, reason: `no transcript for session ${id}` };
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// plugins/sp/lib/run-summary-core.ts
|
|
220
195
|
var IDLE_GAP_MS = 300000;
|
|
221
196
|
var SHELL_TOOLS = new Set(["bash", "shell", "exec", "exec_command", "run_command", "command"]);
|
|
222
197
|
var SUBAGENT_TOOLS = new Set(["agent", "task", "invoke_subagent", "subagent", "dispatch_agent"]);
|
|
223
|
-
var
|
|
198
|
+
var LOWERCASE_OPERATOR_TOOLS = new Set(["askuserquestion", "ask_user_question"]);
|
|
224
199
|
var ACTOR_PRECEDENCE = ["operator", "subagent", "shell", "other"];
|
|
225
200
|
function actorOf(tool) {
|
|
226
201
|
const t = tool.toLowerCase().replace(/[^a-z_]/g, "");
|
|
227
|
-
if (
|
|
202
|
+
if (LOWERCASE_OPERATOR_TOOLS.has(t))
|
|
228
203
|
return "operator";
|
|
229
204
|
if (SUBAGENT_TOOLS.has(t))
|
|
230
205
|
return "subagent";
|
|
@@ -374,7 +349,8 @@ function summaryRow(stage, status, span, measured) {
|
|
|
374
349
|
token: formatTokenSplit(measured ? span.tokens : null)
|
|
375
350
|
};
|
|
376
351
|
}
|
|
377
|
-
function buildRunSummary(
|
|
352
|
+
function buildRunSummary(rows, inputs, opts = {}) {
|
|
353
|
+
const { since, until } = opts;
|
|
378
354
|
const windows = [];
|
|
379
355
|
for (const { label, progress } of inputs) {
|
|
380
356
|
const states = (progress.states ?? []).flatMap((s) => {
|
|
@@ -400,7 +376,6 @@ function buildRunSummary(lines, inputs, since, until) {
|
|
|
400
376
|
}
|
|
401
377
|
}
|
|
402
378
|
windows.sort((a, b) => a.start - b.start);
|
|
403
|
-
const { rows, skippedLines } = parseRows(lines);
|
|
404
379
|
const measure = (start, end) => {
|
|
405
380
|
const acc = newAcc(start);
|
|
406
381
|
for (const [row, ts] of rows)
|
|
@@ -448,9 +423,87 @@ function buildRunSummary(lines, inputs, since, until) {
|
|
|
448
423
|
total,
|
|
449
424
|
actors: actorSplit(rows, totalStart, totalEnd),
|
|
450
425
|
gate: { ms: null, reason: "no matching check receipt" },
|
|
451
|
-
skippedLines
|
|
426
|
+
skippedLines: opts.skippedLines ?? 0
|
|
452
427
|
};
|
|
453
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
|
|
454
507
|
function renderSummaryMarkdown(summary) {
|
|
455
508
|
const line = (r, bold = false) => {
|
|
456
509
|
const stage = bold ? `**${r.stage}**` : r.stage;
|
|
@@ -482,9 +535,9 @@ function renderSummaryMarkdown(summary) {
|
|
|
482
535
|
}
|
|
483
536
|
function writeCloseSummary(run, opts = {}) {
|
|
484
537
|
const cwd = opts.cwd ?? process.cwd();
|
|
485
|
-
const runDir =
|
|
486
|
-
const rel =
|
|
487
|
-
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);
|
|
488
541
|
const na = (reason) => {
|
|
489
542
|
mkdirSync(runDir, { recursive: true });
|
|
490
543
|
writeFileSync(abs, `Execution summary: n/a (${reason})
|
|
@@ -497,15 +550,19 @@ function writeCloseSummary(run, opts = {}) {
|
|
|
497
550
|
const resolved = resolveTranscript(opts.env ?? getEnvVars(), opts.projectsRoot);
|
|
498
551
|
if (!resolved.ok)
|
|
499
552
|
return na(resolved.reason);
|
|
500
|
-
const
|
|
501
|
-
`);
|
|
502
|
-
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
|
+
});
|
|
503
560
|
if (summary.rows.length === 0)
|
|
504
561
|
return na("no timed attempts");
|
|
505
562
|
mkdirSync(runDir, { recursive: true });
|
|
506
563
|
writeFileSync(abs, `${renderSummaryMarkdown(summary)}
|
|
507
564
|
`);
|
|
508
|
-
writeFileSync(
|
|
565
|
+
writeFileSync(join3(runDir, `${run.runId}-summary.json`), JSON.stringify({ ...summary, runId: run.runId, startedAt: run.startedAt, completedAt: run.completedAt }, null, 2));
|
|
509
566
|
return rel;
|
|
510
567
|
} catch (error) {
|
|
511
568
|
return na(error instanceof Error ? error.message : String(error));
|
|
@@ -541,29 +598,10 @@ function refuseUnsafeRunId(runId) {
|
|
|
541
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.");
|
|
542
599
|
process.exit(1);
|
|
543
600
|
}
|
|
544
|
-
function resolveAppEntry(spurBin) {
|
|
545
|
-
const fallback = fileURLToPath(new URL("../../../apps/cli/src/index.ts", import.meta.url));
|
|
546
|
-
const candidates = spurBin !== "" ? [spurBin] : [fallback];
|
|
547
|
-
for (const candidate of candidates) {
|
|
548
|
-
const tokens = candidate.split(/\s+/).filter(Boolean);
|
|
549
|
-
const mainModule = [...tokens].reverse().find((t) => t.endsWith(".ts"));
|
|
550
|
-
if (mainModule === undefined || !existsSync3(mainModule))
|
|
551
|
-
continue;
|
|
552
|
-
const repoRoot = resolve(dirname(mainModule), "..", "..", "..");
|
|
553
|
-
const appEntry = join3(repoRoot, "packages", "app", "src", "index.ts");
|
|
554
|
-
if (existsSync3(appEntry))
|
|
555
|
-
return { entry: appEntry, portable: false };
|
|
556
|
-
}
|
|
557
|
-
const entry = fileURLToPath(new URL("../lib/inline-run.generated.mjs", import.meta.url));
|
|
558
|
-
if (!existsSync3(entry)) {
|
|
559
|
-
throw new Error("inline application bundle is missing \u2014 rebuild/install the sp plugin before running inline");
|
|
560
|
-
}
|
|
561
|
-
return { entry, portable: true };
|
|
562
|
-
}
|
|
563
601
|
var SETUP_EXPORTS = ["readInstalledInventory", "runInlineRunSetup", "writeInlineRunOutcome"];
|
|
564
602
|
async function main() {
|
|
565
603
|
if (!process.versions.bun) {
|
|
566
|
-
const child = spawnSync("bun", [
|
|
604
|
+
const child = spawnSync("bun", [fileURLToPath2(import.meta.url), ...process.argv.slice(2)], {
|
|
567
605
|
stdio: "inherit"
|
|
568
606
|
});
|
|
569
607
|
if (child.error)
|
|
@@ -632,7 +670,7 @@ async function main() {
|
|
|
632
670
|
refuseUnsafeRunId(runId);
|
|
633
671
|
try {
|
|
634
672
|
const { app: app2, portable: portable2 } = await loadInlineApp(spurBin, resolveAppEntry, ["runInlineRunDecide"]);
|
|
635
|
-
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"]);
|
|
636
674
|
const loadOpts = portable2 ? { embeddedSchemas: lib.EMBEDDED_SPUR_SCHEMAS } : undefined;
|
|
637
675
|
const [enabled, spurConfig] = await Promise.all([
|
|
638
676
|
lib.resolveDecideDecisionMakerEnabled(process.cwd(), loadOpts),
|
|
@@ -688,7 +726,7 @@ async function main() {
|
|
|
688
726
|
inventory = await app.readInstalledInventory({
|
|
689
727
|
file,
|
|
690
728
|
spurBin,
|
|
691
|
-
localCli:
|
|
729
|
+
localCli: fileURLToPath2(new URL("../../../apps/cli/src/index.ts", import.meta.url))
|
|
692
730
|
});
|
|
693
731
|
} catch (error) {
|
|
694
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
|
|
|
@@ -480,6 +480,15 @@ ${requestMarker(pr.headRefOid)}`;
|
|
|
480
480
|
writeStatus(args, "REQUESTED");
|
|
481
481
|
emit(args, { ok: true, requested: true, pr: pr.number, url: pr.url, head: pr.headRefOid, requestedAt, body }, `Requested GitHub Codex review on PR #${pr.number} (${pr.url}) at HEAD ${pr.headRefOid.slice(0, 7)}.`);
|
|
482
482
|
}
|
|
483
|
+
function revalidateWaitTarget(args, number) {
|
|
484
|
+
const fresh = viewPr();
|
|
485
|
+
if (!fresh || fresh.number !== number) {
|
|
486
|
+
writeStatus(args, "FAIL");
|
|
487
|
+
fail(args, `PR #${number} is no longer the branch's PR (view reports ${fresh ? `#${fresh.number}` : "none"}) \u2014 request a fresh review`, 2);
|
|
488
|
+
}
|
|
489
|
+
requireExpectedHead(args, fresh);
|
|
490
|
+
return fresh;
|
|
491
|
+
}
|
|
483
492
|
function cmdWait(args) {
|
|
484
493
|
const since = args.flags.get("--since") ?? new Date().toISOString();
|
|
485
494
|
const timeoutSec = Number(args.flags.get("--timeout") ?? "600");
|
|
@@ -494,24 +503,29 @@ function cmdWait(args) {
|
|
|
494
503
|
writeStatus(args, "FAIL");
|
|
495
504
|
fail(args, "no PR for the current branch", 2);
|
|
496
505
|
}
|
|
506
|
+
const number = pr.number;
|
|
497
507
|
requireExpectedHead(args, pr);
|
|
498
508
|
const deadline = Date.now() + timeoutSec * 1000;
|
|
499
509
|
for (;; ) {
|
|
500
|
-
const
|
|
501
|
-
const
|
|
510
|
+
const observed = revalidateWaitTarget(args, number);
|
|
511
|
+
const reviews = fetchReviews(ctx, number);
|
|
512
|
+
const findings = normalizeFindings(reviews, fetchInlineComments(ctx, number), fetchIssueComments(ctx, number), since, observed.headRefOid);
|
|
502
513
|
if (findings.length > 0) {
|
|
514
|
+
revalidateWaitTarget(args, number);
|
|
503
515
|
writeStatus(args, "FOUND");
|
|
504
516
|
emit(args, { ok: true, verdict: "FOUND", findings }, renderFindings(findings));
|
|
505
517
|
return;
|
|
506
518
|
}
|
|
507
|
-
if (hasCurrentCleanReview(reviews, since,
|
|
519
|
+
if (hasCurrentCleanReview(reviews, since, observed.headRefOid)) {
|
|
520
|
+
const fresh = revalidateWaitTarget(args, number);
|
|
508
521
|
writeStatus(args, "CLEAN");
|
|
509
|
-
emit(args, { ok: true, verdict: "CLEAN", pr:
|
|
522
|
+
emit(args, { ok: true, verdict: "CLEAN", pr: fresh.number, url: fresh.url, head: fresh.headRefOid, findings: [] }, `Codex review completed cleanly for HEAD ${fresh.headRefOid.slice(0, 7)}.`);
|
|
510
523
|
return;
|
|
511
524
|
}
|
|
512
525
|
if (Date.now() >= deadline) {
|
|
526
|
+
revalidateWaitTarget(args, number);
|
|
513
527
|
writeStatus(args, "TIMEOUT");
|
|
514
|
-
emit(args, { ok: true, verdict: "TIMEOUT", pr:
|
|
528
|
+
emit(args, { ok: true, verdict: "TIMEOUT", pr: number, url: pr.url }, `No Codex review within ${timeoutSec}s \u2014 still pending. Collect later with /sp:dev-pr-review collect.
|
|
515
529
|
PR: ${pr.url}`);
|
|
516
530
|
throw new ScriptExit("", 3, args.booleans.has("--json"));
|
|
517
531
|
}
|
|
@@ -703,6 +703,26 @@ function cmdRequest(args: ParsedArgs): void {
|
|
|
703
703
|
);
|
|
704
704
|
}
|
|
705
705
|
|
|
706
|
+
/**
|
|
707
|
+
* 1161 R1: re-read the PR and re-assert the pinned identity + expected head. Called at the start of
|
|
708
|
+
* every poll and again immediately before any FOUND/CLEAN/TIMEOUT status or output, because a head
|
|
709
|
+
* captured once at wait start lets a later push return CLEAN for an old commit. A vanished PR, a
|
|
710
|
+
* different PR on the branch, or a moved head is FAIL/exit2 through the existing error channel.
|
|
711
|
+
*/
|
|
712
|
+
function revalidateWaitTarget(args: ParsedArgs, number: number): GhPr {
|
|
713
|
+
const fresh = viewPr();
|
|
714
|
+
if (!fresh || fresh.number !== number) {
|
|
715
|
+
writeStatus(args, 'FAIL');
|
|
716
|
+
fail(
|
|
717
|
+
args,
|
|
718
|
+
`PR #${number} is no longer the branch's PR (view reports ${fresh ? `#${fresh.number}` : 'none'}) — request a fresh review`,
|
|
719
|
+
2,
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
requireExpectedHead(args, fresh);
|
|
723
|
+
return fresh;
|
|
724
|
+
}
|
|
725
|
+
|
|
706
726
|
function cmdWait(args: ParsedArgs): void {
|
|
707
727
|
const since = args.flags.get('--since') ?? new Date().toISOString();
|
|
708
728
|
const timeoutSec = Number(args.flags.get('--timeout') ?? '600');
|
|
@@ -715,36 +735,41 @@ function cmdWait(args: ParsedArgs): void {
|
|
|
715
735
|
writeStatus(args, 'FAIL');
|
|
716
736
|
fail(args, 'no PR for the current branch', 2);
|
|
717
737
|
}
|
|
738
|
+
const number = pr.number; // 1161 R1: pinned — a switched PR number is a failure, not a retarget.
|
|
718
739
|
requireExpectedHead(args, pr);
|
|
719
|
-
const deadline = Date.now() + timeoutSec * 1000;
|
|
740
|
+
const deadline = Date.now() + timeoutSec * 1000; // fixed once; validation never resets it
|
|
720
741
|
for (;;) {
|
|
721
|
-
const
|
|
742
|
+
const observed = revalidateWaitTarget(args, number);
|
|
743
|
+
const reviews = fetchReviews(ctx, number);
|
|
722
744
|
const findings = normalizeFindings(
|
|
723
745
|
reviews,
|
|
724
|
-
fetchInlineComments(ctx,
|
|
725
|
-
fetchIssueComments(ctx,
|
|
746
|
+
fetchInlineComments(ctx, number),
|
|
747
|
+
fetchIssueComments(ctx, number),
|
|
726
748
|
since,
|
|
727
|
-
|
|
749
|
+
observed.headRefOid,
|
|
728
750
|
);
|
|
729
751
|
if (findings.length > 0) {
|
|
752
|
+
revalidateWaitTarget(args, number); // pre-return recheck: never FOUND for a moved head
|
|
730
753
|
writeStatus(args, 'FOUND');
|
|
731
754
|
emit(args, { ok: true, verdict: 'FOUND', findings }, renderFindings(findings));
|
|
732
755
|
return;
|
|
733
756
|
}
|
|
734
|
-
if (hasCurrentCleanReview(reviews, since,
|
|
757
|
+
if (hasCurrentCleanReview(reviews, since, observed.headRefOid)) {
|
|
758
|
+
const fresh = revalidateWaitTarget(args, number); // pre-return recheck: never CLEAN for a moved head
|
|
735
759
|
writeStatus(args, 'CLEAN');
|
|
736
760
|
emit(
|
|
737
761
|
args,
|
|
738
|
-
{ ok: true, verdict: 'CLEAN', pr:
|
|
739
|
-
`Codex review completed cleanly for HEAD ${
|
|
762
|
+
{ ok: true, verdict: 'CLEAN', pr: fresh.number, url: fresh.url, head: fresh.headRefOid, findings: [] },
|
|
763
|
+
`Codex review completed cleanly for HEAD ${fresh.headRefOid.slice(0, 7)}.`,
|
|
740
764
|
);
|
|
741
765
|
return;
|
|
742
766
|
}
|
|
743
767
|
if (Date.now() >= deadline) {
|
|
768
|
+
revalidateWaitTarget(args, number); // a head moved while pending is a failure, not a timeout
|
|
744
769
|
writeStatus(args, 'TIMEOUT');
|
|
745
770
|
emit(
|
|
746
771
|
args,
|
|
747
|
-
{ ok: true, verdict: 'TIMEOUT', pr:
|
|
772
|
+
{ ok: true, verdict: 'TIMEOUT', pr: number, url: pr.url },
|
|
748
773
|
`No Codex review within ${timeoutSec}s — still pending. Collect later with /sp:dev-pr-review collect.\nPR: ${pr.url}`,
|
|
749
774
|
);
|
|
750
775
|
throw new ScriptExit('', 3, args.booleans.has('--json'));
|