@gobing-ai/spur 0.3.38 → 0.3.40
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/config/corpus-baseline.json +0 -14
- package/config/templates/AGENTS.md +6 -0
- package/config/workflows/task-pipeline.yaml +57 -5
- package/package.json +1 -1
- package/spur.js +184 -22
|
@@ -1,20 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"note": "Accepted `spur task check` / `spur feature check` errors. This is a POLICY file, not a status dump: it is an INPUT to `bun run corpus-check`, so it must be committed — an untracked baseline means the gate behaves differently on every clone and CI, and the first person to meet 8 unexplained failures disables it. Do not move it under `.spur/` (gitignored, ephemeral run artifacts). The gate fails on any error NOT listed here, and ALSO on any entry that no longer reproduces, so this cannot rot into a silent suppression list. Every entry needs a real diagnosis, a reason, and a date — 'pre-existing, not investigated' is not a reason.",
|
|
3
3
|
"entries": [
|
|
4
|
-
{
|
|
5
|
-
"kind": "task",
|
|
6
|
-
"id": "0368",
|
|
7
|
-
"code": "L3.required-section-placeholder",
|
|
8
|
-
"reason": "RATCHET DRIFT, not a bypass. Closed 2026-07-28; the rule that flags it landed 2026-08-01 (f373e90b), four days later. It was legally closed under the rules of its day and became non-compliant retroactively. Backfill Testing/Solution or exempt permanently when someone next touches it.",
|
|
9
|
-
"since": "2026-08-07"
|
|
10
|
-
},
|
|
11
|
-
{
|
|
12
|
-
"kind": "task",
|
|
13
|
-
"id": "0454",
|
|
14
|
-
"code": "L3.solution-file-line",
|
|
15
|
-
"reason": "GENUINE PRE-FIX BYPASS. Closed 2026-08-06, well after the rule existed (2026-07-24), via the `--no-lifecycle` hole closed on 2026-08-07. Its Solution needs one file:line citation. The only entry here representing real unpaid debt.",
|
|
16
|
-
"since": "2026-08-07"
|
|
17
|
-
},
|
|
18
4
|
{
|
|
19
5
|
"kind": "feature",
|
|
20
6
|
"id": "F821",
|
|
@@ -173,6 +173,12 @@ catalog here or maintain generated per-platform capability copies in the project
|
|
|
173
173
|
- Surgical changes only — no drive-by refactors or speculative abstractions.
|
|
174
174
|
- Surface changes keep `docs/04_DESIGN.md` in the **same commit** (T3); run `sp:doc-evolve`
|
|
175
175
|
sync-check when unsure.
|
|
176
|
+
- **One writer per working tree.** Two agent sessions in one checkout overwrite each other silently
|
|
177
|
+
— the symptom reads as a model regression. Parallel agent work uses git worktree isolation (one
|
|
178
|
+
branch + one tree per agent).
|
|
179
|
+
- **Commit per task.** Start a task on a tree clean of other tasks' implementations; a dirty tree
|
|
180
|
+
mixes two tasks' evidence into one diff. The pipeline precheck warns (never blocks) with the file
|
|
181
|
+
list.
|
|
176
182
|
<!-- PROJECT-SPECIFIC: import aliases, forbidden paths, CI rules. -->
|
|
177
183
|
|
|
178
184
|
---
|
|
@@ -104,6 +104,12 @@ vars:
|
|
|
104
104
|
# Max checklist items under ## Plan before size precheck fails (R2, task 0454).
|
|
105
105
|
# Override with `--vars '{"maxImplementPlanItems":"15"}'`.
|
|
106
106
|
maxImplementPlanItems: "8"
|
|
107
|
+
# Diff-scope guard on the implement hop (R1, task 0487). When the target task
|
|
108
|
+
# body backticks at least one path, non-corpus changes outside those paths
|
|
109
|
+
# fail the step by name. New files beside a declared file are allowed. Empty
|
|
110
|
+
# (default) = on; set to "off" to bypass:
|
|
111
|
+
# `--vars '{"implementScopeGuard":"off"}'`.
|
|
112
|
+
implementScopeGuard: ""
|
|
107
113
|
|
|
108
114
|
states:
|
|
109
115
|
- id: precheck
|
|
@@ -114,16 +120,58 @@ states:
|
|
|
114
120
|
onEnter:
|
|
115
121
|
# Soft doctor — write status and always exit 0 so transitions can branch to
|
|
116
122
|
# `failed` cleanly (same pattern as the quality-gate soft probe).
|
|
123
|
+
#
|
|
124
|
+
# R2 (0487): probe BOTH resolved executors ($agent and $implementAgent) and
|
|
125
|
+
# FAIL on `authenticated: unauthenticated`. Previously only $agent was probed
|
|
126
|
+
# and auth was informational, so a run whose implement executor had no
|
|
127
|
+
# provider key sailed through precheck into a guaranteed implement failure
|
|
128
|
+
# (runs e8cb00e7 / b16bfbf4: "auth: no … API key not found for provider
|
|
129
|
+
# 'volc'" → precheck ✓). `unknown` auth keeps the old soft behavior — some
|
|
130
|
+
# agents expose no auth-status verb. `spur agent doctor` CLI exit-code
|
|
131
|
+
# semantics are deliberately untouched; the gate lives here.
|
|
132
|
+
# R4 (0487): one divergence line when the two executors differ (legitimate
|
|
133
|
+
# when only implementAgent is pinned, but it must be visible in the log).
|
|
117
134
|
- kind: shell
|
|
118
135
|
options:
|
|
119
136
|
command: >-
|
|
120
137
|
mkdir -p .spur/run &&
|
|
121
138
|
DOCTOR_FILE=".spur/run/$wbs-precheck-doctor.status" &&
|
|
122
|
-
|
|
123
|
-
|
|
139
|
+
STATUS=PASS;
|
|
140
|
+
if [ -n "$implementAgent" ] && [ "$implementAgent" != "$agent" ]; then
|
|
141
|
+
echo "precheck: agent=$agent implementAgent=$implementAgent (executors diverge)";
|
|
142
|
+
EXECS="$agent $implementAgent";
|
|
124
143
|
else
|
|
125
|
-
|
|
126
|
-
fi
|
|
144
|
+
EXECS="$agent";
|
|
145
|
+
fi;
|
|
146
|
+
for EXE in $EXECS; do
|
|
147
|
+
OUT=$($spurBin agent doctor "$EXE" --json 2>&1) || {
|
|
148
|
+
echo "precheck: FAIL - doctor exited non-zero for $EXE";
|
|
149
|
+
echo "$OUT";
|
|
150
|
+
STATUS=FAIL;
|
|
151
|
+
continue;
|
|
152
|
+
};
|
|
153
|
+
AUTH=$(printf '%s' "$OUT" | jq -r '.agents[0].authenticated // "unknown"' 2>/dev/null || echo unknown);
|
|
154
|
+
DETAIL=$(printf '%s' "$OUT" | jq -r '.agents[0].modelStatus.detail // ""' 2>/dev/null || echo "");
|
|
155
|
+
echo "precheck: $EXE auth=$AUTH $DETAIL";
|
|
156
|
+
if [ "$AUTH" = unauthenticated ]; then
|
|
157
|
+
echo "precheck: FAIL - executor $EXE is unauthenticated; $DETAIL";
|
|
158
|
+
STATUS=FAIL;
|
|
159
|
+
fi;
|
|
160
|
+
done;
|
|
161
|
+
echo "$STATUS" > "$DOCTOR_FILE";
|
|
162
|
+
exit 0
|
|
163
|
+
# R6 (0487): pre-launch hygiene WARNING (never a block) — starting a task on
|
|
164
|
+
# a tree already dirty with another task's implementation is how 0485's diff
|
|
165
|
+
# got swept into 0486's run. Corpus dirs are excluded: the pipeline writes
|
|
166
|
+
# those itself.
|
|
167
|
+
- kind: shell
|
|
168
|
+
options:
|
|
169
|
+
command: >-
|
|
170
|
+
DIRTY=$(git status --porcelain -- . ':(exclude)docs/tasks*' ':(exclude)docs/features' 2>/dev/null);
|
|
171
|
+
if [ -n "$DIRTY" ]; then
|
|
172
|
+
echo "precheck: WARNING - working tree has uncommitted non-corpus changes; commit or stash before starting a new task:";
|
|
173
|
+
echo "$DIRTY";
|
|
174
|
+
fi;
|
|
127
175
|
exit 0
|
|
128
176
|
- kind: note
|
|
129
177
|
options:
|
|
@@ -149,13 +197,17 @@ states:
|
|
|
149
197
|
# R2 (0454): task size precheck — evaluate R-item and Plan-item counts.
|
|
150
198
|
# Writes PASS/FAIL to .spur/run/<wbs>-precheck-size.status. Always exit 0
|
|
151
199
|
# (soft check, like doctor). The precheck→implement guard reads the file.
|
|
200
|
+
# R3 (0487): `--executor` adds the size-vs-capability gate — a task past the
|
|
201
|
+
# DEFAULT caps routed to a sub-`capable-1` executor blocks here instead of
|
|
202
|
+
# burning the full implementTimeoutMs and exiting 3 (run ca130182).
|
|
152
203
|
- kind: shell
|
|
153
204
|
options:
|
|
154
205
|
command: >-
|
|
155
206
|
SIZE_FILE=".spur/run/$wbs-precheck-size.status" &&
|
|
156
207
|
mkdir -p .spur/run &&
|
|
157
208
|
bun plugins/sp/scripts/task-size-precheck.ts "$wbs"
|
|
158
|
-
--spur-bin "$spurBin" --max-reqs "$maxImplementReqs" --max-plan-items "$maxImplementPlanItems"
|
|
209
|
+
--spur-bin "$spurBin" --max-reqs "$maxImplementReqs" --max-plan-items "$maxImplementPlanItems"
|
|
210
|
+
--executor "$implementAgent" &&
|
|
159
211
|
exit 0
|
|
160
212
|
|
|
161
213
|
- id: implement
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gobing-ai/spur",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.40",
|
|
4
4
|
"description": "Spur CLI — local-first harness for mainstream coding agents: constraint checking, workflow orchestration, agent health, and history analytics. Bun-native; exposes the `spur` command.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"spur",
|
package/spur.js
CHANGED
|
@@ -62497,7 +62497,12 @@ class AgentService {
|
|
|
62497
62497
|
}
|
|
62498
62498
|
}
|
|
62499
62499
|
if (args.json) {
|
|
62500
|
-
|
|
62500
|
+
const executorByName = new Map((executors ?? []).map((e) => [e.name, e]));
|
|
62501
|
+
const rows = results.map((result) => {
|
|
62502
|
+
const executor = executorByName.get(result.agent) ?? { name: result.agent, agent: result.agent };
|
|
62503
|
+
return { ...result, capabilityTier: getExecutorTier(executor) };
|
|
62504
|
+
});
|
|
62505
|
+
this.ctx.output.write(toJson({ agents: rows }));
|
|
62501
62506
|
} else if (args.agent !== undefined) {
|
|
62502
62507
|
this.ctx.output.write(renderDoctorDetail(results[0] ?? null));
|
|
62503
62508
|
} else {
|
|
@@ -67989,7 +67994,7 @@ function hasPopulatedPriorityTable(body) {
|
|
|
67989
67994
|
const cells = line.split("|");
|
|
67990
67995
|
if (cells.length < 3)
|
|
67991
67996
|
continue;
|
|
67992
|
-
const severityIdx = cells.findIndex((c3) => /^\s*P[1-4]\
|
|
67997
|
+
const severityIdx = cells.findIndex((c3) => /^\s*P[1-4]\b/.test(c3));
|
|
67993
67998
|
if (severityIdx === -1)
|
|
67994
67999
|
continue;
|
|
67995
68000
|
const hasContent = cells.some((c3, i2) => i2 !== severityIdx && !isPlaceholderCell(c3));
|
|
@@ -67999,8 +68004,12 @@ function hasPopulatedPriorityTable(body) {
|
|
|
67999
68004
|
return false;
|
|
68000
68005
|
}
|
|
68001
68006
|
function extractReviewSectionBody(markdown) {
|
|
68002
|
-
const
|
|
68003
|
-
|
|
68007
|
+
const heading = markdown.match(/^### Review[ \t]*\n/m);
|
|
68008
|
+
if (!heading)
|
|
68009
|
+
return null;
|
|
68010
|
+
const rest = markdown.slice((heading.index ?? 0) + heading[0].length);
|
|
68011
|
+
const next = rest.match(/^### /m);
|
|
68012
|
+
return next ? rest.slice(0, next.index ?? 0) : rest;
|
|
68004
68013
|
}
|
|
68005
68014
|
function isReviewScaffold(body) {
|
|
68006
68015
|
if (isPlaceholderBody(body))
|
|
@@ -71241,6 +71250,7 @@ var init_steering = __esm(() => {
|
|
|
71241
71250
|
});
|
|
71242
71251
|
|
|
71243
71252
|
// ../../packages/app/src/workflow/actions/agent-run.ts
|
|
71253
|
+
import { tmpdir } from "os";
|
|
71244
71254
|
import { dirname as dirname14, isAbsolute as isAbsolute3, join as join17 } from "path";
|
|
71245
71255
|
|
|
71246
71256
|
class AgentRunActionRunner {
|
|
@@ -71330,6 +71340,7 @@ class AgentRunActionRunner {
|
|
|
71330
71340
|
let resumeRetried = false;
|
|
71331
71341
|
let steeringNote;
|
|
71332
71342
|
let traced;
|
|
71343
|
+
const diffBaseline = requireDiff === true ? await createGitWorkingTreeSnapshot(cwd, this.agentConfig.excludeGlobs) : undefined;
|
|
71333
71344
|
try {
|
|
71334
71345
|
for (;; ) {
|
|
71335
71346
|
steeringNote = undefined;
|
|
@@ -71393,12 +71404,25 @@ class AgentRunActionRunner {
|
|
|
71393
71404
|
}
|
|
71394
71405
|
}
|
|
71395
71406
|
const stepLabel = context4.stateOrNodeId;
|
|
71396
|
-
if (ok && requireDiff === true
|
|
71397
|
-
|
|
71398
|
-
|
|
71399
|
-
|
|
71400
|
-
|
|
71401
|
-
|
|
71407
|
+
if (ok && requireDiff === true) {
|
|
71408
|
+
const changed = diffBaseline === undefined ? await gitNonCorpusChangedFiles(cwd, this.agentConfig.excludeGlobs) : await gitChangesSinceSnapshot(cwd, diffBaseline, this.agentConfig.excludeGlobs);
|
|
71409
|
+
if (changed.length === 0) {
|
|
71410
|
+
return {
|
|
71411
|
+
ok: false,
|
|
71412
|
+
data: buildResultData(exitCode, agentLabel, capture, answer, invocation),
|
|
71413
|
+
error: `agent.run '${stepLabel}' (${agentLabel}) exited 0 but produced zero non-corpus file changes \u2014 empty implement (no-op). The implement agent must change at least one file outside the configured task/feature folders; fix the implement input and re-run the pipeline.`
|
|
71414
|
+
};
|
|
71415
|
+
}
|
|
71416
|
+
const wbs = String(context4.vars.wbs ?? "");
|
|
71417
|
+
const guardOff = String(context4.vars.implementScopeGuard ?? "") === "off";
|
|
71418
|
+
const rogue = guardOff ? [] : await findOutOfScopeChanges(cwd, wbs, changed);
|
|
71419
|
+
if (rogue.length > 0) {
|
|
71420
|
+
return {
|
|
71421
|
+
ok: false,
|
|
71422
|
+
data: buildResultData(exitCode, agentLabel, capture, answer, invocation),
|
|
71423
|
+
error: `agent.run '${stepLabel}' (${agentLabel}) changed files outside task ${wbs}'s declared surfaces: ${rogue.join(", ")}. Implement only the target WBS; revert the out-of-scope changes (or name those paths in the task body). Set the run var implementScopeGuard: "off" to bypass.`
|
|
71424
|
+
};
|
|
71425
|
+
}
|
|
71402
71426
|
}
|
|
71403
71427
|
if (!ok) {
|
|
71404
71428
|
await writePartialWorkArtifact(context4, agentLabel, model, traced, cwd, sessionDir);
|
|
@@ -71439,7 +71463,10 @@ class AgentRunActionRunner {
|
|
|
71439
71463
|
...steeringNote !== undefined ? { __steeringNote: steeringNote } : {}
|
|
71440
71464
|
} : undefined
|
|
71441
71465
|
};
|
|
71442
|
-
} finally {
|
|
71466
|
+
} finally {
|
|
71467
|
+
if (diffBaseline !== undefined)
|
|
71468
|
+
await deleteSnapshotIndex(diffBaseline);
|
|
71469
|
+
}
|
|
71443
71470
|
}
|
|
71444
71471
|
}
|
|
71445
71472
|
async function discoverSessionId(sessionDir) {
|
|
@@ -71647,20 +71674,151 @@ async function gitDiffStat(cwd) {
|
|
|
71647
71674
|
return "";
|
|
71648
71675
|
}
|
|
71649
71676
|
}
|
|
71650
|
-
async function
|
|
71677
|
+
async function createGitWorkingTreeSnapshot(cwd, excludeGlobs = ["docs/tasks3/*", "docs/features/*"]) {
|
|
71678
|
+
const indexFile = join17(tmpdir(), `spur-implement-scope-${crypto.randomUUID()}.index`);
|
|
71679
|
+
const fs3 = createNodeFileSystem3(cwd);
|
|
71680
|
+
let keepIndex = false;
|
|
71681
|
+
try {
|
|
71682
|
+
await fs3.ensureDir(dirname14(indexFile));
|
|
71683
|
+
const env = Object.fromEntries(Object.entries(process.env).filter((entry) => entry[1] !== undefined));
|
|
71684
|
+
env.GIT_INDEX_FILE = indexFile;
|
|
71685
|
+
const executor = new NodeProcessExecutor3;
|
|
71686
|
+
const common4 = { cwd, env, forceBuffered: true, rejectOnError: false };
|
|
71687
|
+
const read = await executor.run({ command: "git", args: ["read-tree", "HEAD"], ...common4 });
|
|
71688
|
+
if (read.exitCode !== 0)
|
|
71689
|
+
return;
|
|
71690
|
+
const excludes = excludeGlobs.map((glob) => `:(exclude)${glob}`);
|
|
71691
|
+
const add = await executor.run({ command: "git", args: ["add", "-A", "--", ".", ...excludes], ...common4 });
|
|
71692
|
+
if (add.exitCode !== 0)
|
|
71693
|
+
return;
|
|
71694
|
+
const tree = await executor.run({ command: "git", args: ["write-tree"], ...common4 });
|
|
71695
|
+
if (tree.exitCode !== 0 || tree.stdout.trim() === "")
|
|
71696
|
+
return;
|
|
71697
|
+
keepIndex = true;
|
|
71698
|
+
return { indexFile, tree: tree.stdout.trim() };
|
|
71699
|
+
} catch {
|
|
71700
|
+
return;
|
|
71701
|
+
} finally {
|
|
71702
|
+
if (!keepIndex && await fs3.exists(indexFile))
|
|
71703
|
+
await fs3.deleteFile(indexFile);
|
|
71704
|
+
}
|
|
71705
|
+
}
|
|
71706
|
+
async function deleteSnapshotIndex(snapshot) {
|
|
71707
|
+
try {
|
|
71708
|
+
await createNodeFileSystem3().deleteFile(snapshot.indexFile);
|
|
71709
|
+
} catch {}
|
|
71710
|
+
}
|
|
71711
|
+
async function gitChangesSinceSnapshot(cwd, before, excludeGlobs = ["docs/tasks3/*", "docs/features/*"]) {
|
|
71712
|
+
const after = await createGitWorkingTreeSnapshot(cwd, excludeGlobs);
|
|
71713
|
+
if (after === undefined)
|
|
71714
|
+
return gitNonCorpusChangedFiles(cwd, excludeGlobs);
|
|
71715
|
+
try {
|
|
71716
|
+
const result = await new NodeProcessExecutor3().run({
|
|
71717
|
+
command: "git",
|
|
71718
|
+
args: ["diff", "--name-status", "-z", before.tree, after.tree],
|
|
71719
|
+
cwd,
|
|
71720
|
+
maxOutput: 1024 * 1024,
|
|
71721
|
+
forceBuffered: true,
|
|
71722
|
+
rejectOnError: false
|
|
71723
|
+
});
|
|
71724
|
+
return result.exitCode === 0 ? parseNameStatusPaths(result.stdout) : gitNonCorpusChangedFiles(cwd, excludeGlobs);
|
|
71725
|
+
} finally {
|
|
71726
|
+
await deleteSnapshotIndex(after);
|
|
71727
|
+
}
|
|
71728
|
+
}
|
|
71729
|
+
function parseNameStatusPaths(stdout) {
|
|
71730
|
+
const fields = stdout.split("\x00").filter(Boolean);
|
|
71731
|
+
const changes = [];
|
|
71732
|
+
for (let i2 = 0;i2 < fields.length; ) {
|
|
71733
|
+
const status = fields[i2++] ?? "";
|
|
71734
|
+
if (/^[RC]/.test(status))
|
|
71735
|
+
i2++;
|
|
71736
|
+
const path9 = fields[i2++] ?? "";
|
|
71737
|
+
if (path9)
|
|
71738
|
+
changes.push({ path: path9, untracked: status === "A" });
|
|
71739
|
+
}
|
|
71740
|
+
return changes;
|
|
71741
|
+
}
|
|
71742
|
+
async function gitNonCorpusChangedFiles(cwd, excludeGlobs = ["docs/tasks3/*", "docs/features/*"]) {
|
|
71651
71743
|
try {
|
|
71652
71744
|
const excludes = excludeGlobs.map((g) => `:(exclude)${g}`);
|
|
71653
71745
|
const result = await new NodeProcessExecutor3().run({
|
|
71654
71746
|
command: "git",
|
|
71655
|
-
args: ["status", "--porcelain", "--", ".", ...excludes],
|
|
71747
|
+
args: ["status", "--porcelain", "-uall", "--", ".", ...excludes],
|
|
71656
71748
|
cwd,
|
|
71657
71749
|
maxOutput: 1024 * 1024,
|
|
71658
71750
|
forceBuffered: true,
|
|
71659
71751
|
rejectOnError: false
|
|
71660
71752
|
});
|
|
71661
|
-
|
|
71753
|
+
if (result.exitCode !== 0)
|
|
71754
|
+
return [];
|
|
71755
|
+
return parsePorcelainPaths(result.stdout);
|
|
71662
71756
|
} catch {
|
|
71663
|
-
return
|
|
71757
|
+
return [];
|
|
71758
|
+
}
|
|
71759
|
+
}
|
|
71760
|
+
function parsePorcelainPaths(stdout) {
|
|
71761
|
+
const paths = [];
|
|
71762
|
+
for (const line of stdout.split(`
|
|
71763
|
+
`)) {
|
|
71764
|
+
if (line.length < 4)
|
|
71765
|
+
continue;
|
|
71766
|
+
let path9 = line.slice(3);
|
|
71767
|
+
const arrow = path9.indexOf(" -> ");
|
|
71768
|
+
if (arrow !== -1)
|
|
71769
|
+
path9 = path9.slice(arrow + 4);
|
|
71770
|
+
path9 = path9.trim().replace(/^"|"$/g, "");
|
|
71771
|
+
if (path9)
|
|
71772
|
+
paths.push({ path: path9, untracked: line.startsWith("??") });
|
|
71773
|
+
}
|
|
71774
|
+
return paths;
|
|
71775
|
+
}
|
|
71776
|
+
function extractTaskScopeAllowlist(taskMarkdown) {
|
|
71777
|
+
const rules = new Map;
|
|
71778
|
+
for (const match of taskMarkdown.matchAll(/`([^`\n]+)`/g)) {
|
|
71779
|
+
let token = (match[1] ?? "").trim();
|
|
71780
|
+
const prefix = /\/\*+$/.test(token);
|
|
71781
|
+
token = token.replace(/:\d+(-\d+)?$/, "").replace(/\/\*+$/, "").replace(/\/$/, "").replace(/^\.\//, "");
|
|
71782
|
+
if (/\s/.test(token) || token.startsWith("-") || token.startsWith("/") || token.startsWith("../")) {
|
|
71783
|
+
continue;
|
|
71784
|
+
}
|
|
71785
|
+
const isPath = token.includes("/") || /^[\w.-]+\.\w{1,10}$/.test(token);
|
|
71786
|
+
if (!isPath)
|
|
71787
|
+
continue;
|
|
71788
|
+
const isFile = /\.[\w-]{1,10}$/.test(token);
|
|
71789
|
+
const rule = { path: token, prefix: prefix || !isFile };
|
|
71790
|
+
rules.set(`${rule.prefix ? "prefix" : "file"}:${rule.path}`, rule);
|
|
71791
|
+
}
|
|
71792
|
+
return [...rules.values()];
|
|
71793
|
+
}
|
|
71794
|
+
function isInScope(change, allowlist) {
|
|
71795
|
+
return allowlist.some((rule) => {
|
|
71796
|
+
if (rule.prefix)
|
|
71797
|
+
return change.path === rule.path || change.path.startsWith(`${rule.path}/`);
|
|
71798
|
+
if (change.path === rule.path)
|
|
71799
|
+
return true;
|
|
71800
|
+
return change.untracked && dirname14(change.path) === dirname14(rule.path);
|
|
71801
|
+
});
|
|
71802
|
+
}
|
|
71803
|
+
async function findOutOfScopeChanges(cwd, wbs, changed) {
|
|
71804
|
+
if (!wbs)
|
|
71805
|
+
return [];
|
|
71806
|
+
try {
|
|
71807
|
+
const fs3 = createNodeFileSystem3(cwd);
|
|
71808
|
+
const locator = TaskLocator.forDirs(fs3, [
|
|
71809
|
+
join17(cwd, "docs", "tasks3"),
|
|
71810
|
+
join17(cwd, "docs", "tasks2"),
|
|
71811
|
+
join17(cwd, "docs", "tasks")
|
|
71812
|
+
]);
|
|
71813
|
+
const hit = await locator.findByWbs(wbs);
|
|
71814
|
+
if (!hit)
|
|
71815
|
+
return [];
|
|
71816
|
+
const allowlist = extractTaskScopeAllowlist(await fs3.readFile(hit.filePath));
|
|
71817
|
+
if (allowlist.length === 0)
|
|
71818
|
+
return [];
|
|
71819
|
+
return changed.filter((change) => !isInScope(change, allowlist)).map((change) => change.path);
|
|
71820
|
+
} catch {
|
|
71821
|
+
return [];
|
|
71664
71822
|
}
|
|
71665
71823
|
}
|
|
71666
71824
|
function tail(text4, maxChars) {
|
|
@@ -72912,26 +73070,30 @@ async function fileExists(path9) {
|
|
|
72912
73070
|
return await fs3.exists(path9);
|
|
72913
73071
|
}
|
|
72914
73072
|
async function resolveDefaultAgentVar(cwd, callerVars, warn) {
|
|
73073
|
+
const result = {};
|
|
73074
|
+
if (callerVars?.implementAgent === undefined && callerVars?.agent !== undefined) {
|
|
73075
|
+
result.implementAgent = callerVars.agent;
|
|
73076
|
+
}
|
|
72915
73077
|
let config4;
|
|
72916
73078
|
try {
|
|
72917
73079
|
config4 = await loadSpurConfig(cwd);
|
|
72918
73080
|
} catch {
|
|
72919
|
-
return
|
|
73081
|
+
return result;
|
|
72920
73082
|
}
|
|
72921
73083
|
const configured = config4.agent?.default;
|
|
72922
73084
|
if (typeof configured !== "string" || configured.length === 0) {
|
|
72923
|
-
return
|
|
73085
|
+
return result;
|
|
72924
73086
|
}
|
|
72925
73087
|
const valid = config4.agent?.executors?.some((e) => e.name === configured) === true || resolveAgentName(configured) !== undefined;
|
|
72926
73088
|
if (!valid) {
|
|
72927
73089
|
warn(`agent.default "${configured}" does not name a configured executor or agent binary; leaving the pipeline's literal agent in force`);
|
|
72928
|
-
return
|
|
73090
|
+
return result;
|
|
72929
73091
|
}
|
|
72930
|
-
const result = {};
|
|
72931
73092
|
if (callerVars?.agent === undefined)
|
|
72932
73093
|
result.agent = configured;
|
|
72933
|
-
if (callerVars?.implementAgent === undefined)
|
|
73094
|
+
if (result.implementAgent === undefined && callerVars?.implementAgent === undefined) {
|
|
72934
73095
|
result.implementAgent = configured;
|
|
73096
|
+
}
|
|
72935
73097
|
return result;
|
|
72936
73098
|
}
|
|
72937
73099
|
async function resolveWorkflowLogRetentionDays(cwd) {
|
|
@@ -81463,7 +81625,7 @@ init_dist6();
|
|
|
81463
81625
|
var CLI_CONFIG = {
|
|
81464
81626
|
binaryName: "spur",
|
|
81465
81627
|
binaryLabel: "spur",
|
|
81466
|
-
binaryVersion: "0.3.
|
|
81628
|
+
binaryVersion: "0.3.40",
|
|
81467
81629
|
configDir: ".spur",
|
|
81468
81630
|
configFile: ".spur/config.yaml",
|
|
81469
81631
|
databaseFile: ".spur/spur.db"
|
|
@@ -92879,7 +93041,7 @@ ${result.content}`);
|
|
|
92879
93041
|
'`SPUR_PROVENANCE_OVERRIDE=1 spur task update <wbs> done --force-done --reason "\u2026"`.',
|
|
92880
93042
|
"See the gate checklist (spur-dev/references/gate-checklists.md)."
|
|
92881
93043
|
].join(`
|
|
92882
|
-
`)).option("--section <name>", "Section name to replace").option("--from-file <path>", "File to read section body from (requires --section)").option("--feature <id>", "Set the feature_id frontmatter field (traceability edge)").option("--priority <p>", "Set the priority frontmatter field (P0\u2013P3)").option("--ac-numbering <mode>", "Set the ac_numbering frontmatter field (task-local) \u2014 opts the task into the Requirements\u2194AC coverage check").option("--no-lifecycle", "Suppress lifecycle workflow run creation (use during pipeline runs to avoid orphaned lifecycle runs)").option("--force-done", "Allow transitioning to `done` even when the verify verdict is not PASS; records an override (task 0292)").option("--reason <text>", "Rationale for a forced-done override (paired with --force-done; persisted as done_reason)").option("--verdict-dir <path>", "Directory holding <wbs>-verdict.json artifacts (default: .spur/run)").option("--folder <path>", "Custom tasks folder").option("--json", "Output machine-readable JSON").action(async (wbs, status, options) => {
|
|
93044
|
+
`)).option("--section <name>", "Section name to replace").option("--from-file <path>", "File to read section body from (requires --section)").option("--feature <id>", "Set the feature_id frontmatter field (traceability edge)").option("--priority <p>", "Set the priority frontmatter field (P0\u2013P3)").option("--ac-numbering <mode>", "Set the ac_numbering frontmatter field (task-local) \u2014 opts the task into the Requirements\u2194AC coverage check").option("--no-lifecycle", "Suppress lifecycle workflow run creation (use during pipeline runs to avoid orphaned lifecycle runs)").option("--force-done", "Allow transitioning to `done` even when the verify verdict is not PASS; records an override (task 0292). Waives the verdict only \u2014 the FSM path still applies, so from an earlier status walk the hops first: `todo` \u2192 `wip` \u2192 `testing` \u2192 `done` (each hop runs the structural `spur task check`)").option("--reason <text>", "Rationale for a forced-done override (paired with --force-done; persisted as done_reason)").option("--verdict-dir <path>", "Directory holding <wbs>-verdict.json artifacts (default: .spur/run)").option("--folder <path>", "Custom tasks folder").option("--json", "Output machine-readable JSON").action(async (wbs, status, options) => {
|
|
92883
93045
|
const svc = await makeService2(context4, options.folder, options.lifecycle === false);
|
|
92884
93046
|
try {
|
|
92885
93047
|
if (options.section !== undefined) {
|