@gobing-ai/spur 0.4.8 → 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,248 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
/**
|
|
3
|
-
* commit-guard — refuse a driver commit that would sweep a *foreign* writer's working-tree changes
|
|
4
|
-
* (task 1129, feature H1; incidents `a94f9f431`, `36f274590` — commits swept other sessions' files).
|
|
5
|
-
* Read/write `.spur/run/<id>-tree-start.json`; stage only the listed paths this run wrote; report
|
|
6
|
-
* the rest. "Written by this run" = "changed since base" (task-diffstat semantics) minus start-dirty
|
|
7
|
-
* paths whose content still matches the snapshot — a path dirty at start and untouched by the run
|
|
8
|
-
* belongs to another writer. Exit codes: 0 = staged all requested · 2 = some refused as foreign ·
|
|
9
|
-
* 3 = conflict or unmerged target (nothing staged) · 1 = mis-invocation, a missing start snapshot
|
|
10
|
-
* (`start` first), or a failed Git safety probe (nothing staged/reported).
|
|
11
|
-
* `node:*` builtins and relative imports only (sp-plugin-standalone); `options.git` is the test
|
|
12
|
-
* seam; `.spur/` is excluded from both sets (run bookkeeping is not product work — task-diffstat).
|
|
13
|
-
* shortcut: a foreign edit to a path CLEAN at `start` is indistinguishable from this run's own
|
|
14
|
-
* write — git records content, not authorship — caught only once dirty at start. Upgrade: a
|
|
15
|
-
* per-run write journal if that case recurs.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { spawnSync } from 'node:child_process';
|
|
19
|
-
import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
20
|
-
import { join } from 'node:path';
|
|
21
|
-
|
|
22
|
-
/** Base directory for the probes; `git` is the test seam (a fake runner instead of spawning). */
|
|
23
|
-
export interface GuardOptions {
|
|
24
|
-
cwd?: string;
|
|
25
|
-
base?: string;
|
|
26
|
-
git?: GitRunner;
|
|
27
|
-
}
|
|
28
|
-
/** Minimal spawnable git surface — tests fake this to return canned probe output. */
|
|
29
|
-
export interface GitRunResult {
|
|
30
|
-
status: number;
|
|
31
|
-
stdout: string;
|
|
32
|
-
}
|
|
33
|
-
export type GitRunner = (args: string[]) => GitRunResult;
|
|
34
|
-
export interface TreeStart {
|
|
35
|
-
head: string;
|
|
36
|
-
/** Working-file blob hashes at start; `sha` is null when the path did not exist. */
|
|
37
|
-
dirty: Array<{ path: string; sha: string | null }>;
|
|
38
|
-
}
|
|
39
|
-
/** `exitCode` is what `main` returns: 0 all staged · 2 some refused · 3 conflict · 1 usage or a failed Git probe. */
|
|
40
|
-
export type StageResult = { staged: string[]; foreign: string[]; exitCode: number };
|
|
41
|
-
|
|
42
|
-
export const COMMIT_GUARD_USAGE =
|
|
43
|
-
'usage: commit-guard start --run <id> | stage --run <id> [--base <sha>] -- <paths…> | check --run <id>';
|
|
44
|
-
|
|
45
|
-
/** Run bookkeeping is not product work — neither set may contain it (task-diffstat's rule). */
|
|
46
|
-
const isBookkeeping = (path: string): boolean => path === '.spur' || path.startsWith('.spur/');
|
|
47
|
-
/** Probe/artifact paths are repo-relative; `cwd` only anchors them for tests and callers. */
|
|
48
|
-
const at = (options: GuardOptions, rel: string): string => (options.cwd ? join(options.cwd, rel) : rel);
|
|
49
|
-
const artifactPath = (run: string): string => join('.spur', 'run', `${run}-tree-start.json`);
|
|
50
|
-
function gitOf(options: GuardOptions): GitRunner {
|
|
51
|
-
if (options.git) return options.git;
|
|
52
|
-
return (args: string[]): GitRunResult => {
|
|
53
|
-
const run = spawnSync('git', args, { cwd: options.cwd, encoding: 'utf8' });
|
|
54
|
-
return { status: run.status ?? 1, stdout: run.stdout ?? '' };
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
/** Working-file blob hash, or null when the path is absent (`git hash-object` failed). */
|
|
58
|
-
function hashPath(path: string, git: GitRunner): string | null {
|
|
59
|
-
const run = git(['hash-object', '--', path]);
|
|
60
|
-
return run.status === 0 ? run.stdout.trim() : null;
|
|
61
|
-
}
|
|
62
|
-
/** Paths from `git status --porcelain=v1 -z` (`XY <path>` entries; a rename's source token is consumed, not registered). */
|
|
63
|
-
export function parsePorcelainZ(stdout: string): string[] {
|
|
64
|
-
const tokens = stdout.split('\u0000');
|
|
65
|
-
const paths: string[] = [];
|
|
66
|
-
for (let i = 0; i < tokens.length; i++) {
|
|
67
|
-
const entry = tokens[i] ?? '';
|
|
68
|
-
if (entry.length < 4) continue;
|
|
69
|
-
const path = entry.slice(3);
|
|
70
|
-
if (!isBookkeeping(path)) paths.push(path);
|
|
71
|
-
if (/[RC]/.test(entry.slice(0, 2))) i++;
|
|
72
|
-
}
|
|
73
|
-
return paths;
|
|
74
|
-
}
|
|
75
|
-
/** An unresolved conflict marker: an opener or closer line, or a `=======` between them — a `===`
|
|
76
|
-
* setext underline in prose is not a marker without an opener. */
|
|
77
|
-
export function conflictMarkerLine(content: string): boolean {
|
|
78
|
-
let opener = false;
|
|
79
|
-
for (const line of content.split('\n')) {
|
|
80
|
-
if (/^<{7}( |$)/.test(line)) opener = true;
|
|
81
|
-
else if (/^={7}(\s|$)/.test(line) && opener) return true;
|
|
82
|
-
else if (/^>{7}( |$)/.test(line)) return true;
|
|
83
|
-
}
|
|
84
|
-
return opener;
|
|
85
|
-
}
|
|
86
|
-
/** "Changed since base" (task-diffstat semantics): tracked changes + untracked, minus bookkeeping.
|
|
87
|
-
* A failed probe throws (1209): an empty set would certify a tree the guard never actually saw. */
|
|
88
|
-
function changedSinceBase(base: string, git: GitRunner): Set<string> {
|
|
89
|
-
const tracked = git(['diff', '--name-only', base]);
|
|
90
|
-
if (tracked.status !== 0) throw new Error(`git diff --name-only failed (status ${tracked.status})`);
|
|
91
|
-
const others = git(['ls-files', '--others', '--exclude-standard', '-z']);
|
|
92
|
-
if (others.status !== 0) throw new Error(`git ls-files --others failed (status ${others.status})`);
|
|
93
|
-
const clean = (out: string, sep: string): string[] =>
|
|
94
|
-
out
|
|
95
|
-
.split(sep)
|
|
96
|
-
.map((p) => p.trim())
|
|
97
|
-
.filter((p) => p.length > 0 && !isBookkeeping(p));
|
|
98
|
-
return new Set([...clean(tracked.stdout, '\n'), ...clean(others.stdout, '\u0000')]);
|
|
99
|
-
}
|
|
100
|
-
function readTreeStart(run: string, options: GuardOptions): TreeStart | null {
|
|
101
|
-
try {
|
|
102
|
-
const parsed = JSON.parse(readFileSync(at(options, artifactPath(run)), 'utf8')) as TreeStart;
|
|
103
|
-
if (typeof parsed.head === 'string' && Array.isArray(parsed.dirty)) return parsed;
|
|
104
|
-
} catch {
|
|
105
|
-
/* absent or malformed — the caller fails closed */
|
|
106
|
-
}
|
|
107
|
-
return null;
|
|
108
|
-
}
|
|
109
|
-
/** Start snapshot + resolved base + the paths this run wrote: "changed since base" minus start-dirty paths unchanged since. */
|
|
110
|
-
function runContext(run: string, options: GuardOptions) {
|
|
111
|
-
const git = gitOf(options);
|
|
112
|
-
const start = readTreeStart(run, options);
|
|
113
|
-
if (!start) return null;
|
|
114
|
-
let base = options.base ?? '';
|
|
115
|
-
if (!base) {
|
|
116
|
-
try {
|
|
117
|
-
const anchored = readFileSync(at(options, join('.spur', 'run', `${run}-base.sha`)), 'utf8').trim();
|
|
118
|
-
if (/^[0-9a-f]{7,40}$/i.test(anchored)) base = anchored;
|
|
119
|
-
} catch {
|
|
120
|
-
/* no precheck anchor — the start head is the base */
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
if (!base) base = start.head.length > 0 ? start.head : 'HEAD';
|
|
124
|
-
const written = changedSinceBase(base, git);
|
|
125
|
-
for (const entry of start.dirty) if (hashPath(entry.path, git) === entry.sha) written.delete(entry.path);
|
|
126
|
-
return { git, base, start, written };
|
|
127
|
-
}
|
|
128
|
-
function missingStart(run: string): number {
|
|
129
|
-
process.stderr.write(`commit-guard: no ${artifactPath(run)} — run \`commit-guard start --run ${run}\` first\n`);
|
|
130
|
-
return 1;
|
|
131
|
-
}
|
|
132
|
-
/** Fingerprint the tree once, before the run writes anything (R1). */
|
|
133
|
-
export function runStart(run: string, options: GuardOptions = {}): number {
|
|
134
|
-
const git = gitOf(options);
|
|
135
|
-
const head = git(['rev-parse', 'HEAD']);
|
|
136
|
-
const status = git(['status', '--porcelain=v1', '-z']);
|
|
137
|
-
if (head.status !== 0 || status.status !== 0) {
|
|
138
|
-
process.stderr.write('commit-guard: git probe failed (rev-parse HEAD / status) — is this a work tree?\n');
|
|
139
|
-
return 1;
|
|
140
|
-
}
|
|
141
|
-
const snapshot: TreeStart = {
|
|
142
|
-
head: head.stdout.trim(),
|
|
143
|
-
dirty: parsePorcelainZ(status.stdout).map((path) => ({ path, sha: hashPath(path, git) })),
|
|
144
|
-
};
|
|
145
|
-
const abs = at(options, artifactPath(run));
|
|
146
|
-
mkdirSync(join(abs, '..'), { recursive: true });
|
|
147
|
-
writeFileSync(`${abs}.tmp`, `${JSON.stringify(snapshot, null, 2)}\n`);
|
|
148
|
-
renameSync(`${abs}.tmp`, abs); // atomic: a concurrent reader never sees a half-written snapshot
|
|
149
|
-
return 0;
|
|
150
|
-
}
|
|
151
|
-
/** The first path that is unmerged or carries conflict markers, or null (R4, fail closed).
|
|
152
|
-
* The unmerged probe is a safety gate too: a failed one throws instead of staging blind (1209 R2). */
|
|
153
|
-
function conflictingTarget(paths: string[], options: GuardOptions, git: GitRunner): string | null {
|
|
154
|
-
const probe = git(['diff', '--name-only', '--diff-filter=U']);
|
|
155
|
-
if (probe.status !== 0) throw new Error(`git diff --name-only --diff-filter=U failed (status ${probe.status})`);
|
|
156
|
-
const unmerged = new Set(probe.stdout.split('\n').map((p) => p.trim()));
|
|
157
|
-
for (const path of paths) {
|
|
158
|
-
if (unmerged.has(path)) return `${path} (unmerged)`;
|
|
159
|
-
try {
|
|
160
|
-
if (conflictMarkerLine(readFileSync(at(options, path), 'utf8'))) return `${path} (conflict markers)`;
|
|
161
|
-
} catch {
|
|
162
|
-
/* deleted target — nothing to scan */
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
return null;
|
|
166
|
-
}
|
|
167
|
-
/** Print the usage line and report exit 1 — shared by runStage's empty-path guard and `main`. */
|
|
168
|
-
function usageError(): number {
|
|
169
|
-
process.stderr.write(`${COMMIT_GUARD_USAGE}\n`);
|
|
170
|
-
return 1;
|
|
171
|
-
}
|
|
172
|
-
/** One diagnostic for any failed safety probe: the tree state is unproven, so nothing else happens. */
|
|
173
|
-
function probeDiagnostic(err: unknown): string {
|
|
174
|
-
const detail = err instanceof Error ? err.message : String(err);
|
|
175
|
-
return `commit-guard: git probe failed (${detail}) — tree state unproven, nothing staged or reported\n`;
|
|
176
|
-
}
|
|
177
|
-
/** Stage only the listed paths this run wrote (R2) and print `{staged, foreign}` (AC1). */
|
|
178
|
-
export function runStage(run: string, paths: string[], options: GuardOptions = {}): StageResult {
|
|
179
|
-
if (paths.length === 0) return { staged: [], foreign: [], exitCode: usageError() };
|
|
180
|
-
let context: ReturnType<typeof runContext>, conflict: string | null;
|
|
181
|
-
try {
|
|
182
|
-
context = runContext(run, options);
|
|
183
|
-
if (!context) return { staged: [], foreign: [], exitCode: missingStart(run) };
|
|
184
|
-
conflict = conflictingTarget(paths, options, context.git);
|
|
185
|
-
} catch (err) {
|
|
186
|
-
process.stderr.write(probeDiagnostic(err)); // 1209 R2: git add is never reached after a failed probe
|
|
187
|
-
return { staged: [], foreign: [], exitCode: 1 };
|
|
188
|
-
}
|
|
189
|
-
if (conflict) {
|
|
190
|
-
process.stderr.write(`commit-guard: refusing to stage ${conflict}; nothing staged\n`);
|
|
191
|
-
return { staged: [], foreign: [], exitCode: 3 };
|
|
192
|
-
}
|
|
193
|
-
const dirtyAtStart = new Set(context.start.dirty.map((entry) => entry.path));
|
|
194
|
-
const staged = paths.filter((path) => context.written.has(path));
|
|
195
|
-
const refused = paths.filter((path) => !context.written.has(path));
|
|
196
|
-
for (const path of refused) {
|
|
197
|
-
const why = dirtyAtStart.has(path) ? 'foreign — dirty at start, untouched by this run' : 'unchanged since base';
|
|
198
|
-
process.stderr.write(`commit-guard: refused ${path} (${why})\n`);
|
|
199
|
-
}
|
|
200
|
-
if (staged.length > 0 && context.git(['add', '--', ...staged]).status !== 0) {
|
|
201
|
-
process.stderr.write('commit-guard: git add failed — nothing staged\n');
|
|
202
|
-
return { staged: [], foreign: [], exitCode: 1 };
|
|
203
|
-
}
|
|
204
|
-
const foreign = refused.filter((path) => dirtyAtStart.has(path));
|
|
205
|
-
process.stdout.write(`${JSON.stringify({ staged, foreign })}\n`);
|
|
206
|
-
return { staged, foreign, exitCode: refused.length > 0 ? 2 : 0 };
|
|
207
|
-
}
|
|
208
|
-
/** Report the working-tree changes this run did not write, staging nothing (R3). */
|
|
209
|
-
export function runCheck(run: string, options: GuardOptions = {}): number {
|
|
210
|
-
try {
|
|
211
|
-
const context = runContext(run, options); // first changed-path scan
|
|
212
|
-
if (!context) return missingStart(run);
|
|
213
|
-
const { base, git, written } = context; // second changed-path scan — guarded too (1209 R1)
|
|
214
|
-
const foreign = [...changedSinceBase(base, git)].filter((path) => !written.has(path)).sort();
|
|
215
|
-
process.stdout.write(`${JSON.stringify({ foreign })}\n`);
|
|
216
|
-
return 0;
|
|
217
|
-
} catch (err) {
|
|
218
|
-
process.stderr.write(probeDiagnostic(err)); // a failed probe emits no success JSON (R1)
|
|
219
|
-
return 1;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
/** Pull one `--flag value` pair out of argv, or undefined when absent (a missing value is usage). */
|
|
223
|
-
function takeFlag(args: string[], name: string): string | undefined {
|
|
224
|
-
const index = args.indexOf(name);
|
|
225
|
-
const value = index === -1 ? undefined : args[index + 1];
|
|
226
|
-
const missing = value === undefined || value.startsWith('--');
|
|
227
|
-
if (index !== -1) args.splice(index, missing ? 1 : 2);
|
|
228
|
-
return missing ? undefined : value;
|
|
229
|
-
}
|
|
230
|
-
export function main(argv: string[], options: GuardOptions = {}): number {
|
|
231
|
-
const args = [...argv];
|
|
232
|
-
takeFlag(args, '--spur-bin'); // 0482 R2: pipelines hand every plugin script this flag
|
|
233
|
-
const sub = args.shift();
|
|
234
|
-
const run = takeFlag(args, '--run'),
|
|
235
|
-
base = takeFlag(args, '--base');
|
|
236
|
-
if (!run) return usageError();
|
|
237
|
-
if (sub === 'start') return args.length === 0 ? runStart(run, options) : usageError();
|
|
238
|
-
if (sub === 'check') return args.length === 0 ? runCheck(run, { ...options, base }) : usageError();
|
|
239
|
-
if (sub === 'stage')
|
|
240
|
-
return runStage(
|
|
241
|
-
run,
|
|
242
|
-
args.filter((arg) => arg !== '--'),
|
|
243
|
-
{ ...options, base },
|
|
244
|
-
).exitCode;
|
|
245
|
-
return usageError();
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
if (import.meta.main) process.exit(main(process.argv.slice(2)));
|