@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.
@@ -7,7 +7,7 @@
7
7
  "plugins": [
8
8
  {
9
9
  "name": "sp",
10
- "version": "0.3.77",
10
+ "version": "0.3.78",
11
11
  "source": "./plugins/sp"
12
12
  }
13
13
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gobing-ai/spur",
3
- "version": "0.3.77",
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",
57
- "@gobing-ai/ts-ai-runner": "^0.4.57",
58
- "@gobing-ai/ts-dual-workflow-engine": "^0.4.57",
59
- "@gobing-ai/ts-infra": "^0.4.57",
60
- "@gobing-ai/ts-llm-jsonl-importer": "^0.4.57",
61
- "@gobing-ai/ts-rule-engine": "^0.4.57",
62
- "@gobing-ai/ts-runtime": "^0.4.57",
63
- "@gobing-ai/ts-utils": "^0.4.57",
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)
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sp",
3
- "version": "0.3.77",
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 === "--wbs")
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 { status, deps, depStatuses, wbs, recovery, help, json };
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 === '--wbs') wbs = argv[++i] ?? wbs;
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 { status, deps, depStatuses, wbs, recovery, help, json };
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 stripAcWrappers(title)
292
- .replace(/^R\d+\s*[:\-—]?\s*/, '')
293
- .toLowerCase()
294
- .replace(/[ʼ‘’“”]/g, '')
295
- .replace(/\s+/g, ' ')
296
- .trim();
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: