@gobing-ai/spur 0.4.7 → 0.4.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/config/pipeline-budgets.json +2 -2
- package/config/rules/boundary/config-loading-ownership.yaml +1 -0
- package/config/rules/boundary/sp-plugin-standalone.yaml +1 -1
- package/config/rules/boundary/sp-script-placement.yaml +1 -1
- package/config/rules/docs/sourced-api-claims.yaml +45 -7
- package/config/rules/strict/runtime-boundaries.yaml +40 -1
- package/config/rules/structure/protected-files.yaml +1 -1
- package/config/rules/surface/check-cli-surface.yaml +33 -0
- package/config/rules/typescript/no-debugger.yaml +3 -0
- package/config/workflow-candidates.json +2 -2
- package/config/workflows/feature-verification.yaml +6 -11
- package/config/workflows/history-anatomy.yaml +14 -28
- package/config/workflows/idea-pipeline.yaml +6 -19
- package/config/workflows/pr-review.yaml +9 -16
- package/config/workflows/task-pipeline.yaml +16 -23
- package/config/workflows/wrapup-pipeline.yaml +21 -63
- package/package.json +3 -3
- package/plugins/sp/README.md +70 -37
- package/plugins/sp/agents/super-planner.md +5 -5
- package/plugins/sp/agents/super-reviewer.md +2 -2
- package/plugins/sp/commands/dev-changelog.md +1 -1
- package/plugins/sp/commands/dev-daily.md +1 -1
- package/plugins/sp/commands/dev-pr-review.md +1 -1
- package/plugins/sp/commands/dev-review-session.md +1 -1
- package/plugins/sp/commands/dev-runall.md +4 -4
- package/plugins/sp/commands/dev-verify.md +4 -6
- package/plugins/sp/commands/dev-verifyall.md +4 -5
- package/plugins/sp/hooks/hooks.json +11 -1
- package/plugins/sp/lib/context-post-tool.ts +1 -1
- package/plugins/sp/lib/context-session-start.ts +1 -1
- package/plugins/sp/lib/context-session-stop.ts +1 -1
- package/plugins/sp/lib/destructive-policy.ts +65 -1
- package/plugins/sp/plugin.json +2 -5
- package/plugins/sp/{hooks → scripts/hooks}/agent-lifecycle.ts +1 -1
- package/plugins/sp/scripts/hooks/careful-guard.ts +94 -0
- package/plugins/sp/{hooks → scripts/hooks}/context-post-tool.ts +2 -2
- package/plugins/sp/{hooks → scripts/hooks}/context-session-start.ts +1 -1
- package/plugins/sp/{hooks → scripts/hooks}/context-session-stop.ts +1 -1
- package/plugins/sp/{hooks → scripts/hooks}/fleet-guest-stop.ts +6 -4
- package/plugins/sp/{hooks → scripts/hooks}/task-write-guard.ts +38 -21
- package/plugins/sp/skills/code-implementation/SKILL.md +1 -1
- package/plugins/sp/skills/code-verification/SKILL.md +8 -8
- package/plugins/sp/skills/code-verification/references/secu-review.md +4 -3
- package/plugins/sp/skills/code-verification/references/verdict-schema.md +9 -1
- package/plugins/sp/skills/daily-summary/SKILL.md +4 -5
- package/plugins/sp/skills/dogfood-testing/SKILL.md +9 -9
- package/plugins/sp/skills/dogfood-testing/references/report-template.md +1 -1
- package/plugins/sp/skills/history-anatomy/references/modes.md +2 -2
- package/plugins/sp/skills/next-router/references/routing-table.md +1 -1
- package/plugins/sp/skills/parallel-execution/SKILL.md +1 -1
- package/plugins/sp/skills/pr-reviewing/SKILL.md +14 -15
- package/plugins/sp/skills/session-review/SKILL.md +3 -3
- package/plugins/sp/skills/source-driven-development/SKILL.md +7 -4
- package/plugins/sp/skills/spur-check/SKILL.md +14 -13
- package/plugins/sp/skills/spur-cli/SKILL.md +1 -1
- package/plugins/sp/skills/spur-cli/references/agent.md +12 -0
- package/plugins/sp/skills/spur-cli/references/builder.md +27 -6
- package/plugins/sp/skills/spur-cli/references/features.md +6 -1
- package/plugins/sp/skills/spur-cli/references/history.md +3 -0
- package/plugins/sp/skills/spur-cli/references/self.md +5 -3
- package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +3 -0
- package/plugins/sp/skills/spur-cli/references/tasks.md +13 -3
- package/plugins/sp/skills/spur-cli/references/workflows.md +36 -1
- package/plugins/sp/skills/spur-dev/SKILL.md +1 -1
- package/plugins/sp/skills/spur-dev/references/cross-cutting.md +1 -1
- package/plugins/sp/skills/spur-dev/references/dev-operations.md +12 -12
- package/plugins/sp/skills/spur-dev/references/execution-batch-report.md +10 -11
- package/plugins/sp/skills/spur-dev/references/execution-batch.md +16 -17
- package/plugins/sp/skills/spur-dev/references/execution-parallel-isolation.md +1 -1
- package/plugins/sp/skills/spur-dev/references/execution-worktree-landing.md +8 -11
- package/plugins/sp/skills/spur-dev/references/execution-worktree-setup.md +6 -8
- package/plugins/sp/skills/spur-dev/references/gate-checklists.md +1 -1
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +33 -40
- package/plugins/sp/skills/spur-dev/references/structured-trace-emission.md +24 -24
- package/plugins/sp/skills/spur-doctor/SKILL.md +5 -4
- package/spur.js +41901 -30598
- package/config/plugin-scripts.json +0 -116
- package/config/script-placement-baseline.json +0 -33
- package/plugins/sp/hooks/careful-guard.ts +0 -74
- package/plugins/sp/hooks/pi/guard-extension.ts +0 -304
- package/plugins/sp/lib/dogfood-pipeline.ts +0 -265
- package/plugins/sp/lib/dogfood-report.ts +0 -278
- package/plugins/sp/lib/history-anatomy.generated.d.mts +0 -112
- package/plugins/sp/lib/history-anatomy.generated.mjs +0 -691
- package/plugins/sp/lib/idea-handoff.generated.d.mts +0 -17
- package/plugins/sp/lib/idea-handoff.generated.mjs +0 -1446
- package/plugins/sp/lib/inline-run-app.ts +0 -68
- package/plugins/sp/lib/inline-run.generated.d.mts +0 -37
- package/plugins/sp/lib/inline-run.generated.mjs +0 -3103
- package/plugins/sp/lib/plan-projection.generated.d.mts +0 -197
- package/plugins/sp/lib/plan-projection.generated.mjs +0 -982
- package/plugins/sp/lib/quality-gate.generated.d.mts +0 -139
- package/plugins/sp/lib/quality-gate.generated.mjs +0 -30510
- package/plugins/sp/lib/residual-scan.generated.d.mts +0 -93
- package/plugins/sp/lib/residual-scan.generated.mjs +0 -383
- package/plugins/sp/lib/run-summary-core.ts +0 -225
- package/plugins/sp/lib/spur-bin.ts +0 -36
- package/plugins/sp/lib/step-profile.generated.d.mts +0 -71
- package/plugins/sp/lib/step-profile.generated.mjs +0 -174
- package/plugins/sp/lib/transcript.ts +0 -112
- package/plugins/sp/scripts/batch-plan.mjs +0 -1096
- package/plugins/sp/scripts/batch-plan.ts +0 -145
- package/plugins/sp/scripts/batch-preflight.mjs +0 -423
- package/plugins/sp/scripts/batch-preflight.ts +0 -270
- package/plugins/sp/scripts/commit-guard.mjs +0 -226
- package/plugins/sp/scripts/commit-guard.ts +0 -248
- package/plugins/sp/scripts/daily-summary/daily-summary.mjs +0 -626
- package/plugins/sp/scripts/daily-summary/daily-summary.ts +0 -855
- package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.mjs +0 -211
- package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts +0 -110
- package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +0 -282
- package/plugins/sp/scripts/dogfood-testing/validate-report.ts +0 -98
- package/plugins/sp/scripts/feature-verification-steps.mjs +0 -82
- package/plugins/sp/scripts/feature-verification-steps.ts +0 -164
- package/plugins/sp/scripts/history-anatomy-cache.mjs +0 -931
- package/plugins/sp/scripts/history-anatomy-cache.ts +0 -247
- package/plugins/sp/scripts/idea-handoff.mjs +0 -32
- package/plugins/sp/scripts/idea-handoff.ts +0 -47
- package/plugins/sp/scripts/inline-run-dispatch.mjs +0 -131
- package/plugins/sp/scripts/inline-run-dispatch.ts +0 -115
- package/plugins/sp/scripts/inline-run-setup.mjs +0 -746
- package/plugins/sp/scripts/inline-run-setup.ts +0 -198
- package/plugins/sp/scripts/persist-out-check.mjs +0 -217
- package/plugins/sp/scripts/persist-out-check.ts +0 -249
- package/plugins/sp/scripts/pr-reviewing.mjs +0 -712
- package/plugins/sp/scripts/pr-reviewing.ts +0 -952
- package/plugins/sp/scripts/quality-gate.mjs +0 -30491
- package/plugins/sp/scripts/quality-gate.ts +0 -80
- package/plugins/sp/scripts/residual-scan.mjs +0 -634
- package/plugins/sp/scripts/residual-scan.ts +0 -248
- package/plugins/sp/scripts/run-summary.mjs +0 -758
- package/plugins/sp/scripts/run-summary.ts +0 -107
- package/plugins/sp/scripts/script-root.mjs +0 -129
- package/plugins/sp/scripts/script-root.ts +0 -199
- package/plugins/sp/scripts/session-timeline.mjs +0 -359
- package/plugins/sp/scripts/session-timeline.ts +0 -190
- package/plugins/sp/scripts/task-diffstat.mjs +0 -181
- package/plugins/sp/scripts/task-diffstat.ts +0 -219
- package/plugins/sp/scripts/workflow-step-profile.mjs +0 -338
- package/plugins/sp/scripts/workflow-step-profile.ts +0 -165
- package/plugins/sp/scripts/wrapup-drift-probe.mjs +0 -190
- package/plugins/sp/scripts/wrapup-drift-probe.ts +0 -261
- package/plugins/sp/scripts/wrapup-steps.mjs +0 -627
- package/plugins/sp/scripts/wrapup-steps.ts +0 -891
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// @bun
|
|
3
|
-
|
|
4
|
-
// plugins/sp/scripts/task-diffstat.ts
|
|
5
|
-
import { spawnSync } from "child_process";
|
|
6
|
-
import { fstatSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "fs";
|
|
7
|
-
import { join } from "path";
|
|
8
|
-
|
|
9
|
-
// plugins/sp/lib/env.ts
|
|
10
|
-
function getEnvVars() {
|
|
11
|
-
return process.env;
|
|
12
|
-
}
|
|
13
|
-
|
|
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"]);
|
|
23
|
-
var SENSITIVE_PREFIXES = ["drizzle/", "packages/config/", ".github/", "plugins/sp/hooks/"];
|
|
24
|
-
var SENSITIVE_GLOBS = ["apps/server/src/**/auth*", "**/*secret*", "**/*.sql"];
|
|
25
|
-
function globToRegex(glob) {
|
|
26
|
-
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replaceAll("**/", "\x00").replaceAll("**", "\x01").replaceAll("*", "[^/]*").replaceAll("\x00", "(?:.*/)?").replaceAll("\x01", ".*");
|
|
27
|
-
return new RegExp(`^${escaped}$`, "s");
|
|
28
|
-
}
|
|
29
|
-
var SENSITIVE_MATCHERS = [
|
|
30
|
-
...SENSITIVE_PREFIXES.map((prefix) => ({
|
|
31
|
-
label: prefix,
|
|
32
|
-
test: (path) => path.startsWith(prefix)
|
|
33
|
-
})),
|
|
34
|
-
...SENSITIVE_GLOBS.map((glob) => {
|
|
35
|
-
const regex = globToRegex(glob);
|
|
36
|
-
return { label: glob, test: (path) => regex.test(path) };
|
|
37
|
-
})
|
|
38
|
-
];
|
|
39
|
-
function sensitiveReasonForPath(path) {
|
|
40
|
-
const match = SENSITIVE_MATCHERS.find((entry) => entry.test(path));
|
|
41
|
-
return match ? `matches sensitive pattern ${match.label}` : null;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// plugins/sp/scripts/task-diffstat.ts
|
|
45
|
-
function defaultGitRunner(cwd) {
|
|
46
|
-
return (args) => {
|
|
47
|
-
const run = spawnSync("git", args, { cwd, encoding: "utf8" });
|
|
48
|
-
return { status: run.status ?? 1, stdout: run.stdout ?? "" };
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
function countNewlines(path, cwd) {
|
|
52
|
-
let content;
|
|
53
|
-
try {
|
|
54
|
-
content = readFileSync(cwd ? join(cwd, path) : path, "utf8");
|
|
55
|
-
} catch {
|
|
56
|
-
return 0;
|
|
57
|
-
}
|
|
58
|
-
let lines = 0;
|
|
59
|
-
for (const ch of content)
|
|
60
|
-
if (ch === `
|
|
61
|
-
`)
|
|
62
|
-
lines += 1;
|
|
63
|
-
return lines;
|
|
64
|
-
}
|
|
65
|
-
function addCounts(add, del) {
|
|
66
|
-
for (const count of [add, del])
|
|
67
|
-
if (count !== "-" && !/^\d+$/.test(count))
|
|
68
|
-
return null;
|
|
69
|
-
const [a, d] = [add === "-" ? 0 : Number(add), del === "-" ? 0 : Number(del)];
|
|
70
|
-
return Number.isSafeInteger(a) && Number.isSafeInteger(d) ? [a, d] : null;
|
|
71
|
-
}
|
|
72
|
-
function runDiffstat(env, options = {}, git = defaultGitRunner(options.cwd)) {
|
|
73
|
-
const cwd = options.cwd;
|
|
74
|
-
const wbs = env.wbs ?? "";
|
|
75
|
-
if (wbs.length === 0) {
|
|
76
|
-
process.stderr.write(`task-diffstat: wbs is empty \u2014 refusing to guess the run artifact path
|
|
77
|
-
`);
|
|
78
|
-
return { files: 0, insertions: 0, deletions: 0, paths: [], sensitive: true, resultFile: "", exitCode: 1 };
|
|
79
|
-
}
|
|
80
|
-
const relRun = join(".spur", "run");
|
|
81
|
-
const relResult = join(relRun, `${wbs}-diffstat.json`);
|
|
82
|
-
const abs = (p) => cwd ? join(cwd, p) : p;
|
|
83
|
-
mkdirSync(abs(relRun), { recursive: true });
|
|
84
|
-
const failSafe = (reason) => {
|
|
85
|
-
process.stderr.write(`task-diffstat: ${reason} \u2014 failing safe (sensitive)
|
|
86
|
-
`);
|
|
87
|
-
const row = { files: 0, insertions: 0, deletions: 0, paths: [], sensitive: true };
|
|
88
|
-
writeArtifact(abs(relResult), row);
|
|
89
|
-
return { ...row, resultFile: relResult, exitCode: 0 };
|
|
90
|
-
};
|
|
91
|
-
let stdoutIsArtifact = false;
|
|
92
|
-
try {
|
|
93
|
-
const [out, art] = [fstatSync(1), statSync(abs(relResult))];
|
|
94
|
-
stdoutIsArtifact = out.dev === art.dev && out.ino === art.ino;
|
|
95
|
-
} catch {}
|
|
96
|
-
let base = "";
|
|
97
|
-
try {
|
|
98
|
-
base = readFileSync(abs(join(relRun, `${wbs}-base.sha`)), "utf8").trim();
|
|
99
|
-
} catch {}
|
|
100
|
-
if (!/^[0-9a-f]{7,40}$/i.test(base))
|
|
101
|
-
return failSafe("run base .spur/run/<wbs>-base.sha missing or malformed");
|
|
102
|
-
const numstat = git(["diff", "--numstat", "-z", base]);
|
|
103
|
-
if (numstat.status !== 0)
|
|
104
|
-
return failSafe(`git diff --numstat failed (status=${numstat.status})`);
|
|
105
|
-
if (numstat.stdout.length > 0 && !numstat.stdout.endsWith("\x00"))
|
|
106
|
-
return failSafe("git diff --numstat -z output is not NUL-terminated (malformed framing)");
|
|
107
|
-
const paths = new Set;
|
|
108
|
-
let insertions = 0;
|
|
109
|
-
let deletions = 0;
|
|
110
|
-
const records = numstat.stdout.split("\x00").slice(0, -1);
|
|
111
|
-
for (let i = 0;i < records.length; i++) {
|
|
112
|
-
const header = records[i] ?? "";
|
|
113
|
-
const first = header.indexOf("\t");
|
|
114
|
-
const second = header.indexOf("\t", first + 1);
|
|
115
|
-
if (first < 1 || second < first + 2)
|
|
116
|
-
return failSafe("git diff --numstat -z record is missing counts or path");
|
|
117
|
-
const counts = addCounts(header.slice(0, first), header.slice(first + 1, second));
|
|
118
|
-
if (counts === null)
|
|
119
|
-
return failSafe("git diff --numstat -z record has a malformed count");
|
|
120
|
-
insertions += counts[0];
|
|
121
|
-
deletions += counts[1];
|
|
122
|
-
const path = header.slice(second + 1);
|
|
123
|
-
if (path !== "") {
|
|
124
|
-
paths.add(path);
|
|
125
|
-
continue;
|
|
126
|
-
}
|
|
127
|
-
const oldPath = records[++i];
|
|
128
|
-
const newPath = records[++i];
|
|
129
|
-
if (!oldPath || !newPath)
|
|
130
|
-
return failSafe("git diff --numstat -z rename record has incomplete paths");
|
|
131
|
-
paths.add(oldPath);
|
|
132
|
-
paths.add(newPath);
|
|
133
|
-
}
|
|
134
|
-
const others = git(["ls-files", "--others", "--exclude-standard", "-z"]);
|
|
135
|
-
if (others.status !== 0)
|
|
136
|
-
return failSafe(`git ls-files failed (status=${others.status})`);
|
|
137
|
-
for (const path of others.stdout.split("\x00")) {
|
|
138
|
-
if (path.length === 0 || path === ".spur" || path.startsWith(".spur/"))
|
|
139
|
-
continue;
|
|
140
|
-
paths.add(path);
|
|
141
|
-
insertions += countNewlines(path, cwd);
|
|
142
|
-
}
|
|
143
|
-
const all = [...paths].filter((p) => p.length > 0).sort();
|
|
144
|
-
const sensitive = all.some((p) => sensitiveReasonForPath(p) !== null);
|
|
145
|
-
const row = { files: all.length, insertions, deletions, paths: all, sensitive };
|
|
146
|
-
writeArtifact(abs(relResult), row);
|
|
147
|
-
if (stdoutIsArtifact)
|
|
148
|
-
process.stderr.write(`task-diffstat: stdout redirected onto the artifact \u2014 kept; redirect stdout elsewhere
|
|
149
|
-
`);
|
|
150
|
-
return { ...row, resultFile: relResult, exitCode: stdoutIsArtifact ? 1 : 0 };
|
|
151
|
-
}
|
|
152
|
-
function writeArtifact(absPath, row) {
|
|
153
|
-
const tmp = `${absPath}.tmp`;
|
|
154
|
-
writeFileSync(tmp, `${JSON.stringify(row)}
|
|
155
|
-
`);
|
|
156
|
-
renameSync(tmp, absPath);
|
|
157
|
-
}
|
|
158
|
-
var TASK_DIFFSTAT_USAGE = "usage: task-diffstat (env: wbs)";
|
|
159
|
-
function main(argv, env = getEnvVars(), options = {}) {
|
|
160
|
-
for (let i = 0;i < argv.length; i++) {
|
|
161
|
-
if (argv[i] === "--spur-bin") {
|
|
162
|
-
i++;
|
|
163
|
-
continue;
|
|
164
|
-
}
|
|
165
|
-
process.stderr.write(`${TASK_DIFFSTAT_USAGE}
|
|
166
|
-
`);
|
|
167
|
-
return 2;
|
|
168
|
-
}
|
|
169
|
-
return runDiffstat(env, options).exitCode;
|
|
170
|
-
}
|
|
171
|
-
{
|
|
172
|
-
process.exit(main(process.argv.slice(2)));
|
|
173
|
-
}
|
|
174
|
-
export {
|
|
175
|
-
SENSITIVE_GLOBS,
|
|
176
|
-
SENSITIVE_PREFIXES,
|
|
177
|
-
TASK_DIFFSTAT_USAGE,
|
|
178
|
-
main,
|
|
179
|
-
runDiffstat,
|
|
180
|
-
sensitiveReasonForPath
|
|
181
|
-
};
|
|
@@ -1,219 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
/**
|
|
3
|
-
* task-diffstat — deterministic diff-evidence producer behind the task-pipeline `triage`
|
|
4
|
-
* onEnter (task 0943, feature D64, ADR-125 lane routing).
|
|
5
|
-
*
|
|
6
|
-
* Reads the run's anchored base commit `.spur/run/<wbs>-base.sha` (written once by
|
|
7
|
-
* `precheck`, F96 R1) and the current working tree, then writes:
|
|
8
|
-
* `.spur/run/<wbs>-diffstat.json` `{ files, insertions, deletions, paths[], sensitive }`
|
|
9
|
-
*
|
|
10
|
-
* Counts come from `git diff --numstat -z <base>` (tracked changes; binary rows count the path
|
|
11
|
-
* but no lines; NUL records carry RAW paths, so a non-ASCII or newline-bearing path reaches the
|
|
12
|
-
* sensitive matchers intact) plus untracked files (`git ls-files --others --exclude-standard -z`,
|
|
13
|
-
* newline-counted — # ponytail: untracked line counts are a newline scan, numstat-grade binary
|
|
14
|
-
* detection is unnecessary here; switch to `--numstat --no-index` if a miscount flips a lane).
|
|
15
|
-
*
|
|
16
|
-
* `sensitive` is true when ANY changed path matches the exported SENSITIVE list (0943 R2c);
|
|
17
|
-
* the workflow's pre-decide guard maps sensitive (or >400 changed lines) to the safety lane
|
|
18
|
-
* without a model call. Fail safe: a missing/malformed base, a failing git call, or any
|
|
19
|
-
* other probe problem writes `sensitive: true` (and exits 0) so the lane degrades to
|
|
20
|
-
* safety, never to fast.
|
|
21
|
-
*
|
|
22
|
-
* The artifact is written atomically (tmp + rename), so redirecting stdout onto the artifact path
|
|
23
|
-
* cannot corrupt it — the rename swaps in the real JSON after the shell's redirect truncates the
|
|
24
|
-
* old inode (1067 R3); that redirect is still refused with a nonzero exit. Normal output is silent.
|
|
25
|
-
*
|
|
26
|
-
* Node-builtin imports only; the git lookup is a spawnable `GitRunner` so tests can fake
|
|
27
|
-
* it; `main(argv, env, options)` is the same injection point the workflow wrapper and its
|
|
28
|
-
* node twin exercise. Only an empty `wbs` (mis-invocation) exits nonzero.
|
|
29
|
-
*/
|
|
30
|
-
|
|
31
|
-
import { spawnSync } from 'node:child_process';
|
|
32
|
-
import { fstatSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs';
|
|
33
|
-
import { join } from 'node:path';
|
|
34
|
-
import { getEnvVars } from '../lib/env';
|
|
35
|
-
|
|
36
|
-
export interface DiffstatEnv {
|
|
37
|
-
wbs?: string;
|
|
38
|
-
[key: string]: string | undefined;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export interface DiffstatOptions {
|
|
42
|
-
/** Base directory for `.spur/run` and the git probes; defaults to the process cwd. */
|
|
43
|
-
cwd?: string;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** Minimal spawnable git surface — tests fake this to return canned probe output. */
|
|
47
|
-
export interface GitRunResult {
|
|
48
|
-
status: number;
|
|
49
|
-
stdout: string;
|
|
50
|
-
}
|
|
51
|
-
export type GitRunner = (args: string[]) => GitRunResult;
|
|
52
|
-
|
|
53
|
-
/** Diffstat row written to `.spur/run/<wbs>-diffstat.json` (0943 R2a, frozen shape). */
|
|
54
|
-
export interface Diffstat {
|
|
55
|
-
files: number;
|
|
56
|
-
insertions: number;
|
|
57
|
-
deletions: number;
|
|
58
|
-
paths: string[];
|
|
59
|
-
sensitive: boolean;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export interface DiffstatResult extends Diffstat {
|
|
63
|
-
resultFile: string;
|
|
64
|
-
/** Exit code the workflow wrapper observes (only an empty wbs is a hard failure). */
|
|
65
|
-
exitCode: number;
|
|
66
|
-
}
|
|
67
|
-
|
|
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 };
|
|
76
|
-
|
|
77
|
-
function defaultGitRunner(cwd?: string): GitRunner {
|
|
78
|
-
return (args: string[]): GitRunResult => {
|
|
79
|
-
const run = spawnSync('git', args, { cwd, encoding: 'utf8' });
|
|
80
|
-
return { status: run.status ?? 1, stdout: run.stdout ?? '' };
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** Newline count (wc -l semantics) — untracked files have no numstat row. */
|
|
85
|
-
function countNewlines(path: string, cwd?: string): number {
|
|
86
|
-
let content: string;
|
|
87
|
-
try {
|
|
88
|
-
content = readFileSync(cwd ? join(cwd, path) : path, 'utf8');
|
|
89
|
-
} catch {
|
|
90
|
-
return 0;
|
|
91
|
-
}
|
|
92
|
-
let lines = 0;
|
|
93
|
-
for (const ch of content) if (ch === '\n') lines += 1;
|
|
94
|
-
return lines;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function addCounts(add: string, del: string): [number, number] | null {
|
|
98
|
-
for (const count of [add, del]) if (count !== '-' && !/^\d+$/.test(count)) return null;
|
|
99
|
-
const [a, d] = [add === '-' ? 0 : Number(add), del === '-' ? 0 : Number(del)];
|
|
100
|
-
return Number.isSafeInteger(a) && Number.isSafeInteger(d) ? [a, d] : null;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* `probe` — write `.spur/run/<wbs>-diffstat.json`. Always exits 0 after probing; every
|
|
105
|
-
* probe problem fails safe (empty diff, sensitive true), never to a fast lane.
|
|
106
|
-
*/
|
|
107
|
-
export function runDiffstat(
|
|
108
|
-
env: DiffstatEnv,
|
|
109
|
-
options: DiffstatOptions = {},
|
|
110
|
-
git: GitRunner = defaultGitRunner(options.cwd),
|
|
111
|
-
): DiffstatResult {
|
|
112
|
-
const cwd = options.cwd;
|
|
113
|
-
const wbs = env.wbs ?? '';
|
|
114
|
-
if (wbs.length === 0) {
|
|
115
|
-
process.stderr.write('task-diffstat: wbs is empty — refusing to guess the run artifact path\n');
|
|
116
|
-
return { files: 0, insertions: 0, deletions: 0, paths: [], sensitive: true, resultFile: '', exitCode: 1 };
|
|
117
|
-
}
|
|
118
|
-
const relRun = join('.spur', 'run');
|
|
119
|
-
const relResult = join(relRun, `${wbs}-diffstat.json`);
|
|
120
|
-
const abs = (p: string): string => (cwd ? join(cwd, p) : p);
|
|
121
|
-
mkdirSync(abs(relRun), { recursive: true });
|
|
122
|
-
|
|
123
|
-
const failSafe = (reason: string): DiffstatResult => {
|
|
124
|
-
process.stderr.write(`task-diffstat: ${reason} — failing safe (sensitive)\n`);
|
|
125
|
-
const row: Diffstat = { files: 0, insertions: 0, deletions: 0, paths: [], sensitive: true };
|
|
126
|
-
writeArtifact(abs(relResult), row);
|
|
127
|
-
return { ...row, resultFile: relResult, exitCode: 0 };
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
let stdoutIsArtifact = false; // 1067 R3: stdout redirected onto the artifact itself clobbered it once
|
|
131
|
-
try {
|
|
132
|
-
const [out, art] = [fstatSync(1), statSync(abs(relResult))];
|
|
133
|
-
stdoutIsArtifact = out.dev === art.dev && out.ino === art.ino;
|
|
134
|
-
} catch {
|
|
135
|
-
/* artifact absent or stdout not a regular file — nothing to compare */
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
let base = '';
|
|
139
|
-
try {
|
|
140
|
-
base = readFileSync(abs(join(relRun, `${wbs}-base.sha`)), 'utf8').trim();
|
|
141
|
-
} catch {
|
|
142
|
-
// fall through to the shape check below
|
|
143
|
-
}
|
|
144
|
-
if (!/^[0-9a-f]{7,40}$/i.test(base)) return failSafe('run base .spur/run/<wbs>-base.sha missing or malformed');
|
|
145
|
-
|
|
146
|
-
const numstat = git(['diff', '--numstat', '-z', base]);
|
|
147
|
-
if (numstat.status !== 0) return failSafe(`git diff --numstat failed (status=${numstat.status})`);
|
|
148
|
-
if (numstat.stdout.length > 0 && !numstat.stdout.endsWith('\u0000'))
|
|
149
|
-
return failSafe('git diff --numstat -z output is not NUL-terminated (malformed framing)');
|
|
150
|
-
const paths = new Set<string>();
|
|
151
|
-
let insertions = 0;
|
|
152
|
-
let deletions = 0;
|
|
153
|
-
const records = numstat.stdout.split('\u0000').slice(0, -1); // drop the terminating NUL tail
|
|
154
|
-
for (let i = 0; i < records.length; i++) {
|
|
155
|
-
const header = records[i] ?? '';
|
|
156
|
-
const first = header.indexOf('\t'); // first two tabs only
|
|
157
|
-
const second = header.indexOf('\t', first + 1); // a raw path may itself hold more tabs
|
|
158
|
-
if (first < 1 || second < first + 2) return failSafe('git diff --numstat -z record is missing counts or path');
|
|
159
|
-
const counts = addCounts(header.slice(0, first), header.slice(first + 1, second));
|
|
160
|
-
if (counts === null) return failSafe('git diff --numstat -z record has a malformed count');
|
|
161
|
-
insertions += counts[0];
|
|
162
|
-
deletions += counts[1]; // once per count header, never once per rename half
|
|
163
|
-
const path = header.slice(second + 1);
|
|
164
|
-
if (path !== '') {
|
|
165
|
-
paths.add(path);
|
|
166
|
-
continue;
|
|
167
|
-
}
|
|
168
|
-
// An empty path field is Git's rename/copy form: the next two records are old and new.
|
|
169
|
-
const oldPath = records[++i];
|
|
170
|
-
const newPath = records[++i];
|
|
171
|
-
if (!oldPath || !newPath) return failSafe('git diff --numstat -z rename record has incomplete paths');
|
|
172
|
-
paths.add(oldPath);
|
|
173
|
-
paths.add(newPath);
|
|
174
|
-
}
|
|
175
|
-
const others = git(['ls-files', '--others', '--exclude-standard', '-z']);
|
|
176
|
-
if (others.status !== 0) return failSafe(`git ls-files failed (status=${others.status})`);
|
|
177
|
-
for (const path of others.stdout.split('\u0000')) {
|
|
178
|
-
// Pipeline bookkeeping (the run's own .spur state) is not product work — skip it.
|
|
179
|
-
if (path.length === 0 || path === '.spur' || path.startsWith('.spur/')) continue;
|
|
180
|
-
paths.add(path);
|
|
181
|
-
insertions += countNewlines(path, cwd);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const all = [...paths].filter((p) => p.length > 0).sort();
|
|
185
|
-
const sensitive = all.some((p) => sensitiveReasonForPath(p) !== null);
|
|
186
|
-
const row: Diffstat = { files: all.length, insertions, deletions, paths: all, sensitive };
|
|
187
|
-
writeArtifact(abs(relResult), row);
|
|
188
|
-
if (stdoutIsArtifact)
|
|
189
|
-
process.stderr.write('task-diffstat: stdout redirected onto the artifact — kept; redirect stdout elsewhere\n');
|
|
190
|
-
return { ...row, resultFile: relResult, exitCode: stdoutIsArtifact ? 1 : 0 };
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/** Atomic artifact write (tmp + rename): a stdout redirect onto the path cannot interleave (1067 R3). */
|
|
194
|
-
function writeArtifact(absPath: string, row: Diffstat): void {
|
|
195
|
-
const tmp = `${absPath}.tmp`;
|
|
196
|
-
writeFileSync(tmp, `${JSON.stringify(row)}\n`);
|
|
197
|
-
renameSync(tmp, absPath);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
export const TASK_DIFFSTAT_USAGE = 'usage: task-diffstat (env: wbs)';
|
|
201
|
-
|
|
202
|
-
export function main(argv: string[], env: DiffstatEnv = getEnvVars(), options: DiffstatOptions = {}): number {
|
|
203
|
-
// 0482 R2: the workflow hands every plugin script `--spur-bin` for PATH-independence.
|
|
204
|
-
// task-diffstat shells only git (never spur), so the flag is accepted and unused;
|
|
205
|
-
// any other argv stays a usage error.
|
|
206
|
-
for (let i = 0; i < argv.length; i++) {
|
|
207
|
-
if (argv[i] === '--spur-bin') {
|
|
208
|
-
i++;
|
|
209
|
-
continue;
|
|
210
|
-
}
|
|
211
|
-
process.stderr.write(`${TASK_DIFFSTAT_USAGE}\n`);
|
|
212
|
-
return 2;
|
|
213
|
-
}
|
|
214
|
-
return runDiffstat(env, options).exitCode;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
if (import.meta.main) {
|
|
218
|
-
process.exit(main(process.argv.slice(2)));
|
|
219
|
-
}
|