@gobing-ai/spur 0.3.77 → 0.3.78
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/package.json +9 -9
- package/plugins/sp/commands/dev-refineall.md +1 -0
- package/plugins/sp/commands/dev-run.md +1 -0
- package/plugins/sp/commands/dev-runall.md +1 -0
- package/plugins/sp/commands/dev-verifyall.md +1 -0
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/scripts/batch-preflight.mjs +173 -2
- package/plugins/sp/scripts/batch-preflight.ts +257 -2
- package/plugins/sp/scripts/verify-answer-lint.ts +11 -6
- package/plugins/sp/skills/spur-dev/references/cross-cutting.md +33 -0
- package/plugins/sp/skills/spur-dev/references/execution-batch.md +17 -0
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +102 -13
- package/spur.js +1207 -538
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gobing-ai/spur",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.78",
|
|
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",
|
|
@@ -53,14 +53,14 @@
|
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"@commander-js/extra-typings": "^14.0.0",
|
|
56
|
-
"@gobing-ai/ts-db": "^0.4.
|
|
57
|
-
"@gobing-ai/ts-ai-runner": "^0.4.
|
|
58
|
-
"@gobing-ai/ts-dual-workflow-engine": "^0.4.
|
|
59
|
-
"@gobing-ai/ts-infra": "^0.4.
|
|
60
|
-
"@gobing-ai/ts-llm-jsonl-importer": "^0.4.
|
|
61
|
-
"@gobing-ai/ts-rule-engine": "^0.4.
|
|
62
|
-
"@gobing-ai/ts-runtime": "^0.4.
|
|
63
|
-
"@gobing-ai/ts-utils": "^0.4.
|
|
56
|
+
"@gobing-ai/ts-db": "^0.4.60",
|
|
57
|
+
"@gobing-ai/ts-ai-runner": "^0.4.60",
|
|
58
|
+
"@gobing-ai/ts-dual-workflow-engine": "^0.4.60",
|
|
59
|
+
"@gobing-ai/ts-infra": "^0.4.60",
|
|
60
|
+
"@gobing-ai/ts-llm-jsonl-importer": "^0.4.60",
|
|
61
|
+
"@gobing-ai/ts-rule-engine": "^0.4.60",
|
|
62
|
+
"@gobing-ai/ts-runtime": "^0.4.60",
|
|
63
|
+
"@gobing-ai/ts-utils": "^0.4.60",
|
|
64
64
|
"@types/bun": "1.3.14",
|
|
65
65
|
"@types/figlet": "^1.7.0",
|
|
66
66
|
"@types/node-notifier": "8.0.5",
|
|
@@ -62,5 +62,6 @@ thread `--depth` into each per-task refine.
|
|
|
62
62
|
## Implementation
|
|
63
63
|
|
|
64
64
|
- Apply the [inline-default execution-surface contract](../skills/spur-dev/references/cross-cutting.md#inline-default-execution-surface).
|
|
65
|
+
- Apply the [shared startup contract](../skills/spur-dev/references/cross-cutting.md#shared-startup-contract-task-0814-r1r3r4r6r7r8) — bootstrap checklist, quick readiness before isolation, workflow inventory before YAML, comprehensive checks at owning boundaries (task 0814 R1/R3/R4/R7/R8).
|
|
65
66
|
- Batch orchestration: `Skill(skill="sp:spur-dev", args="refineall $ARGUMENTS")`
|
|
66
67
|
- Per-task refine (inner): `Skill(skill="sp:spur-dev", args="refine <wbs> $SHARED_FLAGS")`
|
|
@@ -31,6 +31,7 @@ For shared semantics, see the [flag glossary](../skills/spur-dev/references/flag
|
|
|
31
31
|
## Implementation
|
|
32
32
|
|
|
33
33
|
- Apply the [inline-default execution-surface contract](../skills/spur-dev/references/cross-cutting.md#inline-default-execution-surface).
|
|
34
|
+
- Apply the [shared startup contract](../skills/spur-dev/references/cross-cutting.md#shared-startup-contract-task-0814-r1r3r4r6r7r8) — bootstrap checklist, quick readiness before isolation, workflow inventory before YAML, comprehensive checks at their owning boundaries (task 0814 R1/R3/R4/R7/R8).
|
|
34
35
|
- Full pipeline (default `--mode full`): interactive omit/`--agent inline` uses the
|
|
35
36
|
[inline pipeline driver](../skills/spur-dev/references/inline-pipeline-driver.md) via
|
|
36
37
|
`Skill(skill="sp:spur-dev", args="run-inline $ARGUMENTS")`; `--agent auto` or a named executor
|
|
@@ -82,5 +82,6 @@ full distinction.
|
|
|
82
82
|
## Implementation
|
|
83
83
|
|
|
84
84
|
- Apply the [inline-default execution-surface contract](../skills/spur-dev/references/cross-cutting.md#inline-default-execution-surface). Interactive sequential omit/`inline` keeps the orchestrator in the host session through the [inline pipeline driver](../skills/spur-dev/references/inline-pipeline-driver.md); the resolved selector applies uniformly — eligible `agent.run` stages dispatch once to a native subagent with host fallback (0508). `--agent auto`, a name, or parallel mode retains the isolated per-task workflow boundary.
|
|
85
|
+
- Apply the [shared startup contract](../skills/spur-dev/references/cross-cutting.md#shared-startup-contract-task-0814-r1r3r4r6r7r8) — bootstrap checklist, quick readiness before isolation, workflow inventory before YAML, comprehensive checks at owning boundaries (task 0814 R1/R3/R4/R7/R8).
|
|
85
86
|
- Interactive sequential omit/inline: `Skill(skill="sp:spur-dev", args="runall-inline $ARGUMENTS")`.
|
|
86
87
|
- Explicit executor or parallel mode: `Skill(skill="sp:spur-dev", args="runall $ARGUMENTS")` → `sp:super-planner` agent.
|
|
@@ -68,6 +68,7 @@ Without `--fix all`, shippable is not evaluated (optional note: use `--fix all`
|
|
|
68
68
|
## Implementation
|
|
69
69
|
|
|
70
70
|
- Apply the [inline-default execution-surface contract](../skills/spur-dev/references/cross-cutting.md#inline-default-execution-surface).
|
|
71
|
+
- Apply the [shared startup contract](../skills/spur-dev/references/cross-cutting.md#shared-startup-contract-task-0814-r1r3r4r6r7r8) — bootstrap checklist, quick readiness before isolation, workflow inventory before YAML, comprehensive checks at owning boundaries (task 0814 R1/R3/R4/R7/R8).
|
|
71
72
|
- Batch orchestration: `Skill(skill="sp:spur-dev", args="verifyall $ARGUMENTS")`
|
|
72
73
|
- Per-task verification (inner): `Skill(skill="sp:code-verification", args="verify <wbs> $SHARED_FLAGS")`
|
|
73
74
|
- Shippable gate: once after the batch (same skill § Shippable readiness gate)
|
package/plugins/sp/plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.78",
|
|
4
4
|
"description": "Spur — a local-first harness engineering toolkit that wraps mainstream coding agents with constraint checking, workflow orchestration, and history analytics.",
|
|
5
5
|
"extensions": {
|
|
6
6
|
"pi": ["./hooks/pi/guard-extension.ts"]
|
|
@@ -3,6 +3,119 @@ import { createRequire } from "node:module";
|
|
|
3
3
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
4
4
|
|
|
5
5
|
// plugins/sp/scripts/batch-preflight.ts
|
|
6
|
+
function quickReadiness(input) {
|
|
7
|
+
const status = (input.status ?? "").toLowerCase();
|
|
8
|
+
const operation = input.operation;
|
|
9
|
+
if (operation !== "run" && operation !== "refine" && operation !== "verify") {
|
|
10
|
+
return {
|
|
11
|
+
action: "invalid",
|
|
12
|
+
code: "IV",
|
|
13
|
+
reason: `quick-readiness: unknown operation '${operation}' (${input.wbs})`
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
if (input.filteredCount !== undefined) {
|
|
17
|
+
if (input.filteredCount < 0) {
|
|
18
|
+
return {
|
|
19
|
+
action: "invalid",
|
|
20
|
+
code: "IV",
|
|
21
|
+
reason: `quick-readiness: invalid negative filtered count '${input.filteredCount}' (${input.wbs})`
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
if (input.filteredCount === 0) {
|
|
25
|
+
return {
|
|
26
|
+
action: "skipped",
|
|
27
|
+
code: "EMPTY",
|
|
28
|
+
reason: `quick-readiness: empty status-filtered set — nothing to ${operation} (${input.wbs})`
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (status === "cancelled" || status === "done") {
|
|
33
|
+
if (operation === "verify" && input.force === true) {
|
|
34
|
+
return {
|
|
35
|
+
action: "runnable",
|
|
36
|
+
code: "FORCE",
|
|
37
|
+
reason: `quick-readiness: verify --force re-verification of ${status} task ${input.wbs}`
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
action: "skipped",
|
|
42
|
+
code: status === "done" ? "DONE" : "CANCELLED",
|
|
43
|
+
reason: `quick-readiness: already ${status} — no ${operation} hop (${input.wbs})`
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
if (status === "blocked") {
|
|
47
|
+
return {
|
|
48
|
+
action: "blocked",
|
|
49
|
+
code: "BLK",
|
|
50
|
+
reason: `quick-readiness: blocked — human/handover first (${input.wbs})`
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
if (operation === "run" && input.dependencies && input.dependencies.length > 0) {
|
|
54
|
+
const unmet = input.dependencies.filter((d) => (input.depStatuses?.[d] ?? "missing").toLowerCase() !== "done");
|
|
55
|
+
if (unmet.length > 0) {
|
|
56
|
+
return {
|
|
57
|
+
action: "blocked",
|
|
58
|
+
code: "DEP",
|
|
59
|
+
reason: `quick-readiness: unmet deps — ${unmet.join(", ")} (${input.wbs})`,
|
|
60
|
+
unmetDeps: unmet
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const required = input.requiredSections ?? [];
|
|
65
|
+
const present = input.presentSections ?? [];
|
|
66
|
+
const gaps = required.filter((s) => {
|
|
67
|
+
const finding = input.sectionFindings?.[s];
|
|
68
|
+
return !present.includes(s) || finding !== undefined && finding !== "";
|
|
69
|
+
});
|
|
70
|
+
if (operation === "refine") {
|
|
71
|
+
if (status !== "backlog" && status !== "todo") {
|
|
72
|
+
return {
|
|
73
|
+
action: "skipped",
|
|
74
|
+
code: "NONPLAN",
|
|
75
|
+
reason: `quick-readiness: refine targets backlog/todo only, not '${status}' (${input.wbs})`
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
action: "runnable",
|
|
80
|
+
code: "OK",
|
|
81
|
+
reason: `quick-readiness: refine ready for ${input.wbs} (${gaps.length} planning gap(s) to fill)`
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
if (operation === "verify") {
|
|
85
|
+
if (status !== "testing" && status !== "wip") {
|
|
86
|
+
return {
|
|
87
|
+
action: "invalid",
|
|
88
|
+
code: "NOVERIFY",
|
|
89
|
+
reason: `quick-readiness: verify needs testing/wip, not '${status}' (${input.wbs})`
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
action: "runnable",
|
|
94
|
+
code: "OK",
|
|
95
|
+
reason: `quick-readiness: verify ready for ${input.wbs}`
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
if (status !== "todo" && status !== "wip" && status !== "testing") {
|
|
99
|
+
return {
|
|
100
|
+
action: "invalid",
|
|
101
|
+
code: "NORUN",
|
|
102
|
+
reason: `quick-readiness: run needs todo/wip/testing, not '${status}' (${input.wbs})`
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
if (gaps.length > 0) {
|
|
106
|
+
return {
|
|
107
|
+
action: "needs-refinement",
|
|
108
|
+
code: "REFINE",
|
|
109
|
+
reason: `quick-readiness: implementation sections incomplete (${input.wbs})`,
|
|
110
|
+
gaps
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
action: "runnable",
|
|
115
|
+
code: "OK",
|
|
116
|
+
reason: `quick-readiness: run ready for ${input.wbs}`
|
|
117
|
+
};
|
|
118
|
+
}
|
|
6
119
|
function preflightTask(input) {
|
|
7
120
|
const status = (input.status ?? "").toLowerCase();
|
|
8
121
|
const deps = input.dependencies ?? [];
|
|
@@ -72,6 +185,11 @@ function parsePreflightCliArgs(argv) {
|
|
|
72
185
|
let recovery = false;
|
|
73
186
|
let help = false;
|
|
74
187
|
let json = false;
|
|
188
|
+
let operation = null;
|
|
189
|
+
let force = false;
|
|
190
|
+
let filteredCount = null;
|
|
191
|
+
let requiredSections = [];
|
|
192
|
+
let presentSections = [];
|
|
75
193
|
for (let i = 0;i < argv.length; i++) {
|
|
76
194
|
const a = argv[i];
|
|
77
195
|
if (a === "--help" || a === "-h")
|
|
@@ -80,7 +198,21 @@ function parsePreflightCliArgs(argv) {
|
|
|
80
198
|
json = true;
|
|
81
199
|
else if (a === "--recovery")
|
|
82
200
|
recovery = true;
|
|
83
|
-
else if (a === "--
|
|
201
|
+
else if (a === "--force")
|
|
202
|
+
force = true;
|
|
203
|
+
else if (a === "--operation") {
|
|
204
|
+
const v = argv[++i] ?? "";
|
|
205
|
+
operation = v === "run" || v === "refine" || v === "verify" ? v : null;
|
|
206
|
+
} else if (a === "--filtered-count") {
|
|
207
|
+
const v = Number(argv[++i]);
|
|
208
|
+
filteredCount = Number.isFinite(v) ? v : null;
|
|
209
|
+
} else if (a === "--required-sections") {
|
|
210
|
+
const raw = argv[++i] ?? "";
|
|
211
|
+
requiredSections = raw.length === 0 ? [] : raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
212
|
+
} else if (a === "--present-sections") {
|
|
213
|
+
const raw = argv[++i] ?? "";
|
|
214
|
+
presentSections = raw.length === 0 ? [] : raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
215
|
+
} else if (a === "--wbs")
|
|
84
216
|
wbs = argv[++i] ?? wbs;
|
|
85
217
|
else if (a === "--status")
|
|
86
218
|
status = argv[++i] ?? null;
|
|
@@ -96,11 +228,25 @@ function parsePreflightCliArgs(argv) {
|
|
|
96
228
|
}
|
|
97
229
|
}
|
|
98
230
|
}
|
|
99
|
-
return {
|
|
231
|
+
return {
|
|
232
|
+
status,
|
|
233
|
+
deps,
|
|
234
|
+
depStatuses,
|
|
235
|
+
wbs,
|
|
236
|
+
recovery,
|
|
237
|
+
help,
|
|
238
|
+
json,
|
|
239
|
+
operation,
|
|
240
|
+
force,
|
|
241
|
+
filteredCount,
|
|
242
|
+
requiredSections,
|
|
243
|
+
presentSections
|
|
244
|
+
};
|
|
100
245
|
}
|
|
101
246
|
var PREFLIGHT_CLI_USAGE = `Usage:
|
|
102
247
|
bun plugins/sp/scripts/batch-preflight.ts --wbs <wbs> --status <status> \\
|
|
103
248
|
[--deps 0275,0276] [--dep-status 0275:done,0276:todo] [--recovery] [--json]
|
|
249
|
+
bun plugins/sp/scripts/batch-preflight.ts --operation <run|refine|verify> --wbs <wbs> --status <status> [--filtered-count <n>] [--required-sections A,B] [--present-sections A,B] [--force] [--json]
|
|
104
250
|
|
|
105
251
|
Exit: 0 = run (or recovery hint printed); 2 = skip; 1 = usage.`;
|
|
106
252
|
function runPreflightCli(argv) {
|
|
@@ -117,6 +263,30 @@ function runPreflightCli(argv) {
|
|
|
117
263
|
`;
|
|
118
264
|
return { exitCode: 0, stdout: body, stderr: "" };
|
|
119
265
|
}
|
|
266
|
+
if (args.operation !== null) {
|
|
267
|
+
const result2 = quickReadiness({
|
|
268
|
+
wbs: args.wbs,
|
|
269
|
+
status: args.status,
|
|
270
|
+
operation: args.operation,
|
|
271
|
+
dependencies: args.deps,
|
|
272
|
+
depStatuses: args.depStatuses,
|
|
273
|
+
...args.filteredCount !== null ? { filteredCount: args.filteredCount } : {},
|
|
274
|
+
...args.requiredSections.length > 0 ? { requiredSections: args.requiredSections } : {},
|
|
275
|
+
...args.presentSections.length > 0 ? { presentSections: args.presentSections } : {},
|
|
276
|
+
...args.force ? { force: true } : {}
|
|
277
|
+
});
|
|
278
|
+
const runnable = result2.action === "runnable" || result2.action === "needs-refinement";
|
|
279
|
+
if (args.json) {
|
|
280
|
+
return { exitCode: runnable ? 0 : 2, stdout: `${JSON.stringify(result2, null, 2)}
|
|
281
|
+
`, stderr: "" };
|
|
282
|
+
}
|
|
283
|
+
return {
|
|
284
|
+
exitCode: runnable ? 0 : 2,
|
|
285
|
+
stdout: `${result2.action}${result2.code ? ` ${result2.code}` : ""}: ${result2.reason}
|
|
286
|
+
`,
|
|
287
|
+
stderr: ""
|
|
288
|
+
};
|
|
289
|
+
}
|
|
120
290
|
const result = preflightTask({
|
|
121
291
|
wbs: args.wbs,
|
|
122
292
|
status: args.status,
|
|
@@ -154,6 +324,7 @@ function runPreflightCli(argv) {
|
|
|
154
324
|
export {
|
|
155
325
|
runPreflightCli,
|
|
156
326
|
recoveryHint,
|
|
327
|
+
quickReadiness,
|
|
157
328
|
preflightTask,
|
|
158
329
|
parsePreflightCliArgs,
|
|
159
330
|
PREFLIGHT_CLI_USAGE
|
|
@@ -25,6 +25,182 @@ export type PreflightResult =
|
|
|
25
25
|
| { action: 'run'; code?: string; reason?: string }
|
|
26
26
|
| { action: 'skip'; code: string; reason: string; unmetDeps?: string[] };
|
|
27
27
|
|
|
28
|
+
// ── Command-aware quick readiness (task 0814 R2) ──────────────────────────────
|
|
29
|
+
// Read-only admission decision for the requested dev operation. Distinguishes
|
|
30
|
+
// runnable / needs-refinement / blocked / skipped / invalid outcomes without
|
|
31
|
+
// LLM dispatch, full tests/lint, live-data probes, feature mutation, or
|
|
32
|
+
// corpus-wide relational checking. Refinement gaps are work to do, not errors.
|
|
33
|
+
|
|
34
|
+
export type ReadinessOperation = 'run' | 'refine' | 'verify';
|
|
35
|
+
|
|
36
|
+
export interface QuickReadinessInput {
|
|
37
|
+
wbs: string;
|
|
38
|
+
status: TaskStatus;
|
|
39
|
+
operation: ReadinessOperation;
|
|
40
|
+
/** Frontmatter dependencies[] WBS list (run only). */
|
|
41
|
+
dependencies?: string[];
|
|
42
|
+
/** Status of each dependency WBS; missing → treated as unmet (run only). */
|
|
43
|
+
depStatuses?: Record<string, string>;
|
|
44
|
+
/** Size of the status-filtered candidate set after the selector resolved; 0 = empty set. */
|
|
45
|
+
filteredCount?: number;
|
|
46
|
+
/** Required planning sections for this variant+status; empty = not applicable. */
|
|
47
|
+
requiredSections?: string[];
|
|
48
|
+
/** Sections that are actually present (non-placeholder) in the task. */
|
|
49
|
+
presentSections?: string[];
|
|
50
|
+
/** L1/L2/L3 content-policy findings keyed by section; empty = clean. */
|
|
51
|
+
sectionFindings?: Record<string, string>;
|
|
52
|
+
/** Verify only: re-verification semantics (--force) — never a dirty-tree bypass. */
|
|
53
|
+
force?: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type QuickReadinessResult =
|
|
57
|
+
| { action: 'runnable'; code: string; reason: string }
|
|
58
|
+
| { action: 'needs-refinement'; code: string; reason: string; gaps: string[] }
|
|
59
|
+
| { action: 'blocked'; code: string; reason: string; unmetDeps?: string[] }
|
|
60
|
+
| { action: 'skipped'; code: string; reason: string }
|
|
61
|
+
| { action: 'invalid'; code: string; reason: string };
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Evaluate quick readiness for a requested dev operation (0814 R2). Read-only:
|
|
65
|
+
* no model, no full tests/lint, no live-data probe, no feature mutation, no
|
|
66
|
+
* corpus-wide relational check. An empty status-filtered set is `skipped`
|
|
67
|
+
* (mirrors the zero-task rule), never an error. Refinement gaps under `refine`
|
|
68
|
+
* are work to do, so they do not block; under `run` they are `needs-refinement`.
|
|
69
|
+
*/
|
|
70
|
+
export function quickReadiness(input: QuickReadinessInput): QuickReadinessResult {
|
|
71
|
+
const status = (input.status ?? '').toLowerCase();
|
|
72
|
+
const operation = input.operation;
|
|
73
|
+
|
|
74
|
+
if (operation !== 'run' && operation !== 'refine' && operation !== 'verify') {
|
|
75
|
+
return {
|
|
76
|
+
action: 'invalid',
|
|
77
|
+
code: 'IV',
|
|
78
|
+
reason: `quick-readiness: unknown operation '${operation}' (${input.wbs})`,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (input.filteredCount !== undefined) {
|
|
83
|
+
if (input.filteredCount < 0) {
|
|
84
|
+
return {
|
|
85
|
+
action: 'invalid',
|
|
86
|
+
code: 'IV',
|
|
87
|
+
reason: `quick-readiness: invalid negative filtered count '${input.filteredCount}' (${input.wbs})`,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
if (input.filteredCount === 0) {
|
|
91
|
+
return {
|
|
92
|
+
action: 'skipped',
|
|
93
|
+
code: 'EMPTY',
|
|
94
|
+
reason: `quick-readiness: empty status-filtered set — nothing to ${operation} (${input.wbs})`,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (status === 'cancelled' || status === 'done') {
|
|
100
|
+
// verify --force re-verification (R2 AC): an already-verified terminal task
|
|
101
|
+
// is re-checked, not skipped — but force never bypasses a dirty tree or the
|
|
102
|
+
// owning gates; it only re-admits a terminal task for re-verification.
|
|
103
|
+
if (operation === 'verify' && input.force === true) {
|
|
104
|
+
return {
|
|
105
|
+
action: 'runnable',
|
|
106
|
+
code: 'FORCE',
|
|
107
|
+
reason: `quick-readiness: verify --force re-verification of ${status} task ${input.wbs}`,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
action: 'skipped',
|
|
112
|
+
code: status === 'done' ? 'DONE' : 'CANCELLED',
|
|
113
|
+
reason: `quick-readiness: already ${status} — no ${operation} hop (${input.wbs})`,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (status === 'blocked') {
|
|
118
|
+
return {
|
|
119
|
+
action: 'blocked',
|
|
120
|
+
code: 'BLK',
|
|
121
|
+
reason: `quick-readiness: blocked — human/handover first (${input.wbs})`,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Unmet out-of-set dependency is a block for the operation that needs it.
|
|
126
|
+
if (operation === 'run' && input.dependencies && input.dependencies.length > 0) {
|
|
127
|
+
const unmet = input.dependencies.filter((d) => (input.depStatuses?.[d] ?? 'missing').toLowerCase() !== 'done');
|
|
128
|
+
if (unmet.length > 0) {
|
|
129
|
+
return {
|
|
130
|
+
action: 'blocked',
|
|
131
|
+
code: 'DEP',
|
|
132
|
+
reason: `quick-readiness: unmet deps — ${unmet.join(', ')} (${input.wbs})`,
|
|
133
|
+
unmetDeps: unmet,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const required = input.requiredSections ?? [];
|
|
139
|
+
const present = input.presentSections ?? [];
|
|
140
|
+
// A required section is a gap when it is absent from the present-set OR carries a
|
|
141
|
+
// content-policy finding (the caller-supplied `sectionFindings` from the matrix /
|
|
142
|
+
// `TaskCheckService.checkContentPolicy`). This lets the function detect a gap itself
|
|
143
|
+
// rather than depending on the caller to pre-enumerate every missing section.
|
|
144
|
+
const gaps = required.filter((s) => {
|
|
145
|
+
const finding = input.sectionFindings?.[s];
|
|
146
|
+
return !present.includes(s) || (finding !== undefined && finding !== '');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// refine: missing/incomplete planning sections are the work, not a failure.
|
|
150
|
+
if (operation === 'refine') {
|
|
151
|
+
if (status !== 'backlog' && status !== 'todo') {
|
|
152
|
+
return {
|
|
153
|
+
action: 'skipped',
|
|
154
|
+
code: 'NONPLAN',
|
|
155
|
+
reason: `quick-readiness: refine targets backlog/todo only, not '${status}' (${input.wbs})`,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
action: 'runnable',
|
|
160
|
+
code: 'OK',
|
|
161
|
+
reason: `quick-readiness: refine ready for ${input.wbs} (${gaps.length} planning gap(s) to fill)`,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (operation === 'verify') {
|
|
166
|
+
if (status !== 'testing' && status !== 'wip') {
|
|
167
|
+
return {
|
|
168
|
+
action: 'invalid',
|
|
169
|
+
code: 'NOVERIFY',
|
|
170
|
+
reason: `quick-readiness: verify needs testing/wip, not '${status}' (${input.wbs})`,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
action: 'runnable',
|
|
175
|
+
code: 'OK',
|
|
176
|
+
reason: `quick-readiness: verify ready for ${input.wbs}`,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// run: implementation admission. Eligible statuses are todo/wip/testing;
|
|
181
|
+
// a backlog task needs the chain's auto-promotion first (step 0).
|
|
182
|
+
if (status !== 'todo' && status !== 'wip' && status !== 'testing') {
|
|
183
|
+
return {
|
|
184
|
+
action: 'invalid',
|
|
185
|
+
code: 'NORUN',
|
|
186
|
+
reason: `quick-readiness: run needs todo/wip/testing, not '${status}' (${input.wbs})`,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
if (gaps.length > 0) {
|
|
190
|
+
return {
|
|
191
|
+
action: 'needs-refinement',
|
|
192
|
+
code: 'REFINE',
|
|
193
|
+
reason: `quick-readiness: implementation sections incomplete (${input.wbs})`,
|
|
194
|
+
gaps,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
action: 'runnable',
|
|
199
|
+
code: 'OK',
|
|
200
|
+
reason: `quick-readiness: run ready for ${input.wbs}`,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
28
204
|
/**
|
|
29
205
|
* Evaluate whether the batch should launch task-pipeline.yaml for this WBS.
|
|
30
206
|
* STOP codes align with routing-table TABLE A row ids (A2, A7, A8, A9).
|
|
@@ -113,6 +289,16 @@ export interface PreflightCliArgs {
|
|
|
113
289
|
recovery: boolean;
|
|
114
290
|
help: boolean;
|
|
115
291
|
json: boolean;
|
|
292
|
+
/** Quick-readiness operation (run|refine|verify); when set, run quickReadiness. */
|
|
293
|
+
operation: ReadinessOperation | null;
|
|
294
|
+
/** Verify-only re-verification (R2 AC). */
|
|
295
|
+
force: boolean;
|
|
296
|
+
/** Status-filtered candidate set size; 0 = empty set. */
|
|
297
|
+
filteredCount: number | null;
|
|
298
|
+
/** Required planning sections (matrix-selected). */
|
|
299
|
+
requiredSections: string[];
|
|
300
|
+
/** Sections actually present in the task. */
|
|
301
|
+
presentSections: string[];
|
|
116
302
|
}
|
|
117
303
|
|
|
118
304
|
export function parsePreflightCliArgs(argv: string[]): PreflightCliArgs {
|
|
@@ -123,13 +309,43 @@ export function parsePreflightCliArgs(argv: string[]): PreflightCliArgs {
|
|
|
123
309
|
let recovery = false;
|
|
124
310
|
let help = false;
|
|
125
311
|
let json = false;
|
|
312
|
+
let operation: ReadinessOperation | null = null;
|
|
313
|
+
let force = false;
|
|
314
|
+
let filteredCount: number | null = null;
|
|
315
|
+
let requiredSections: string[] = [];
|
|
316
|
+
let presentSections: string[] = [];
|
|
126
317
|
|
|
127
318
|
for (let i = 0; i < argv.length; i++) {
|
|
128
319
|
const a = argv[i];
|
|
129
320
|
if (a === '--help' || a === '-h') help = true;
|
|
130
321
|
else if (a === '--json') json = true;
|
|
131
322
|
else if (a === '--recovery') recovery = true;
|
|
132
|
-
else if (a === '--
|
|
323
|
+
else if (a === '--force') force = true;
|
|
324
|
+
else if (a === '--operation') {
|
|
325
|
+
const v = argv[++i] ?? '';
|
|
326
|
+
operation = v === 'run' || v === 'refine' || v === 'verify' ? v : null;
|
|
327
|
+
} else if (a === '--filtered-count') {
|
|
328
|
+
const v = Number(argv[++i]);
|
|
329
|
+
filteredCount = Number.isFinite(v) ? v : null;
|
|
330
|
+
} else if (a === '--required-sections') {
|
|
331
|
+
const raw = argv[++i] ?? '';
|
|
332
|
+
requiredSections =
|
|
333
|
+
raw.length === 0
|
|
334
|
+
? []
|
|
335
|
+
: raw
|
|
336
|
+
.split(',')
|
|
337
|
+
.map((s) => s.trim())
|
|
338
|
+
.filter(Boolean);
|
|
339
|
+
} else if (a === '--present-sections') {
|
|
340
|
+
const raw = argv[++i] ?? '';
|
|
341
|
+
presentSections =
|
|
342
|
+
raw.length === 0
|
|
343
|
+
? []
|
|
344
|
+
: raw
|
|
345
|
+
.split(',')
|
|
346
|
+
.map((s) => s.trim())
|
|
347
|
+
.filter(Boolean);
|
|
348
|
+
} else if (a === '--wbs') wbs = argv[++i] ?? wbs;
|
|
133
349
|
else if (a === '--status') status = argv[++i] ?? null;
|
|
134
350
|
else if (a === '--deps') {
|
|
135
351
|
const raw = argv[++i] ?? '';
|
|
@@ -149,12 +365,27 @@ export function parsePreflightCliArgs(argv: string[]): PreflightCliArgs {
|
|
|
149
365
|
}
|
|
150
366
|
}
|
|
151
367
|
}
|
|
152
|
-
return {
|
|
368
|
+
return {
|
|
369
|
+
status,
|
|
370
|
+
deps,
|
|
371
|
+
depStatuses,
|
|
372
|
+
wbs,
|
|
373
|
+
recovery,
|
|
374
|
+
help,
|
|
375
|
+
json,
|
|
376
|
+
operation,
|
|
377
|
+
force,
|
|
378
|
+
filteredCount,
|
|
379
|
+
requiredSections,
|
|
380
|
+
presentSections,
|
|
381
|
+
};
|
|
153
382
|
}
|
|
154
383
|
|
|
155
384
|
export const PREFLIGHT_CLI_USAGE = `Usage:
|
|
156
385
|
bun plugins/sp/scripts/batch-preflight.ts --wbs <wbs> --status <status> \\
|
|
157
386
|
[--deps 0275,0276] [--dep-status 0275:done,0276:todo] [--recovery] [--json]
|
|
387
|
+
bun plugins/sp/scripts/batch-preflight.ts --operation <run|refine|verify> --wbs <wbs> --status <status> \
|
|
388
|
+
[--filtered-count <n>] [--required-sections A,B] [--present-sections A,B] [--force] [--json]
|
|
158
389
|
|
|
159
390
|
Exit: 0 = run (or recovery hint printed); 2 = skip; 1 = usage.`;
|
|
160
391
|
|
|
@@ -173,6 +404,30 @@ export function runPreflightCli(argv: string[]): { exitCode: number; stdout: str
|
|
|
173
404
|
return { exitCode: 0, stdout: body, stderr: '' };
|
|
174
405
|
}
|
|
175
406
|
|
|
407
|
+
// Quick command-aware readiness (0814 R2) — read-only admission decision.
|
|
408
|
+
if (args.operation !== null) {
|
|
409
|
+
const result = quickReadiness({
|
|
410
|
+
wbs: args.wbs,
|
|
411
|
+
status: args.status,
|
|
412
|
+
operation: args.operation,
|
|
413
|
+
dependencies: args.deps,
|
|
414
|
+
depStatuses: args.depStatuses,
|
|
415
|
+
...(args.filteredCount !== null ? { filteredCount: args.filteredCount } : {}),
|
|
416
|
+
...(args.requiredSections.length > 0 ? { requiredSections: args.requiredSections } : {}),
|
|
417
|
+
...(args.presentSections.length > 0 ? { presentSections: args.presentSections } : {}),
|
|
418
|
+
...(args.force ? { force: true } : {}),
|
|
419
|
+
});
|
|
420
|
+
const runnable = result.action === 'runnable' || result.action === 'needs-refinement';
|
|
421
|
+
if (args.json) {
|
|
422
|
+
return { exitCode: runnable ? 0 : 2, stdout: `${JSON.stringify(result, null, 2)}\n`, stderr: '' };
|
|
423
|
+
}
|
|
424
|
+
return {
|
|
425
|
+
exitCode: runnable ? 0 : 2,
|
|
426
|
+
stdout: `${result.action}${result.code ? ` ${result.code}` : ''}: ${result.reason}\n`,
|
|
427
|
+
stderr: '',
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
176
431
|
const result = preflightTask({
|
|
177
432
|
wbs: args.wbs,
|
|
178
433
|
status: args.status,
|
|
@@ -288,12 +288,17 @@ function stripAcWrappers(title: string): string {
|
|
|
288
288
|
* and still fails.
|
|
289
289
|
*/
|
|
290
290
|
function normalizeAcTitle(title: string): string {
|
|
291
|
-
return
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
291
|
+
return (
|
|
292
|
+
stripAcWrappers(title)
|
|
293
|
+
.replace(/^R\d+\s*[:\-—]?\s*/, '')
|
|
294
|
+
.toLowerCase()
|
|
295
|
+
// Exact pre-refactor removal set (0809 R5): ASCII apostrophe + the four curly
|
|
296
|
+
// quotes. Escaped form keeps U+0027 visible next to lookalike curly glyphs;
|
|
297
|
+
// U+02BC stays a meaningful character, never removable punctuation.
|
|
298
|
+
.replace(/[\u0027\u2018\u2019\u201c\u201d]/g, '')
|
|
299
|
+
.replace(/\s+/g, ' ')
|
|
300
|
+
.trim()
|
|
301
|
+
);
|
|
297
302
|
}
|
|
298
303
|
|
|
299
304
|
/**
|
|
@@ -209,6 +209,39 @@ executor is the current coding agent. Interactive pipelines retain a run log and
|
|
|
209
209
|
through the inline driver; task pipelines additionally record a task run-link. If process isolation or an independently killable
|
|
210
210
|
stage is required, select the subprocess path (`--agent auto` or `--agent <name>`).
|
|
211
211
|
|
|
212
|
+
## Shared startup contract (task 0814 R1/R3/R4/R6/R7/R8)
|
|
213
|
+
|
|
214
|
+
The workflow-backed dev commands (`dev-run`, `dev-runall`, `dev-refineall`, `dev-verifyall`) share one
|
|
215
|
+
startup order. The order is load-bearing and applies on both the inline driver and the subprocess
|
|
216
|
+
path; skill-only operations (refine/verify batches with no nested workflow) display their owned
|
|
217
|
+
procedure and do not fabricate a workflow YAML.
|
|
218
|
+
|
|
219
|
+
1. **Publish a compact bootstrap checklist immediately** (host-preparation rows, never copied
|
|
220
|
+
workflow states): `A, Quick readiness` · `B, Prepare Git` · `C, Publish workflow plan` ·
|
|
221
|
+
`D, Comprehensive checking`.
|
|
222
|
+
2. **Quick deterministic readiness (R2), before isolation.** Evaluate `quickReadiness`
|
|
223
|
+
(`plugins/sp/scripts/batch-preflight.ts`) with the operation, status, filtered-set size, and the
|
|
224
|
+
selected matrix required/present sections + content-policy findings. This is an admission decision
|
|
225
|
+
(runnable / needs-refinement / blocked / skipped / invalid), never an implementation certificate.
|
|
226
|
+
3. **Isolation (R3), only when `--worktree` is valid.** After quick readiness and the required Git
|
|
227
|
+
safety checks, create/adopt and switch to the execution tree; confirm absolute cwd, branch, base
|
|
228
|
+
SHA, and ownership. An invalid/empty target, unsupported mode, ambiguous ownership, or stale target
|
|
229
|
+
stops without creating a tree or discarding work. All subsequent tools, agents, corpus writes, and
|
|
230
|
+
run artifacts use the confirmed execution tree.
|
|
231
|
+
4. **Publish the workflow inventory (R4), before reading the YAML.** `spur workflow show
|
|
232
|
+
<resolved-file> --no-logo --format todo --json`; validate with `parseWorkflowInventory` and bind to
|
|
233
|
+
the run's `__definitionDigest` with `assertInventoryIdentity`. Drift or projection failure stops the
|
|
234
|
+
run before any comprehensive/model work — never execute with a misleading plan.
|
|
235
|
+
5. **Load execution detail and run comprehensive checks (R7).** Only after the plan is visible (and
|
|
236
|
+
after isolation when requested) load the full YAML for the active stage and run the owning
|
|
237
|
+
comprehensive gates at their boundaries. Prefer deterministic checks; invoke semantic model work
|
|
238
|
+
only for an identified unresolved requirement/design/evidence question and record its reason.
|
|
239
|
+
|
|
240
|
+
Quick readiness and plan projection dispatch zero models and execute zero workflow actions. Record a
|
|
241
|
+
timestamped event trace under `.spur/run/<run-id>-event-trace.md` (R8) — event ordering,
|
|
242
|
+
time-to-first-visible-checklist, time-to-workflow-inventory, confirmed cwd, invocation counts — and
|
|
243
|
+
record unavailable measurements as `unknown`, never as invented savings.
|
|
244
|
+
|
|
212
245
|
## Every write is CLI-gated
|
|
213
246
|
|
|
214
247
|
Never edit a task or feature file directly. Every mutation goes through:
|