@gobing-ai/spur 0.3.76 → 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.76",
10
+ "version": "0.3.78",
11
11
  "source": "./plugins/sp"
12
12
  }
13
13
  ]
@@ -36,6 +36,10 @@
36
36
  "contract": "standard",
37
37
  "twin": "history-anatomy-cache.mjs"
38
38
  },
39
+ {
40
+ "rel": "inline-run-setup.ts",
41
+ "contract": "repo-only"
42
+ },
39
43
  {
40
44
  "rel": "inline-pipeline-parity-check.ts",
41
45
  "contract": "repo-only"
@@ -71,6 +71,7 @@ rules:
71
71
  - "packages/config/src/loader.ts" # sync resolveConfigFile() / schema specifier lookup
72
72
  - "packages/config/src/executor-update.ts" # atomic single-entry config update (task 0797): O_EXCL lock, fsync'd tmp+rename commit, lstat symlink refusal, mode preservation — ts-runtime FileSystem seam has no lstat/fsync/chmod/O_EXCL
73
73
  - "packages/app/src/services/project-start.ts" # sync CLI binary path checks before daemon spawn
74
+ - "apps/cli/src/commands/serve.ts" # sync resolveServeCwd() directory validation before server startup (task 0805 R2); mirrors task.ts sync-template exemption
74
75
 
75
76
  # Synchronous template & workflow resolvers:
76
77
  - "apps/cli/src/commands/task.ts" # sync loadTemplateContent/Bodies callbacks
@@ -270,12 +270,20 @@ states:
270
270
  # WorkflowAppService.run(); the wbs fallback keeps a driver-less invocation from writing to
271
271
  # a bare "-route-reason.txt". The log line carries the run id for the same reason — an
272
272
  # unattributed append is log scraping, which R5 explicitly rejects as evidence.
273
+ # 0804 R8: the run id must be a single safe filename component before it
274
+ # becomes REASON_FILE — unresolved interpolation ($ { } vars.), path
275
+ # separators and dot traversal are a failed action (nonzero, no artifact).
276
+ # NOTE: no `#` comments inside the folded scalar — `>-` joins all same-indent
277
+ # lines into ONE shell line, so an in-scalar `#` comments out the rest of the
278
+ # command (the original R8 draft broke exactly this way; the case statement
279
+ # must stay one line).
273
280
  - kind: shell
274
281
  options:
275
282
  command: >-
276
283
  mkdir -p .spur/run .spur/memory &&
277
284
  RUN_ID="$__runId" &&
278
285
  if [ -z "$RUN_ID" ]; then RUN_ID="pipeline-$wbs"; fi &&
286
+ case "$RUN_ID" in *'$'*|*'{'*|*'}'*|*vars.*|*/*|*'\'*|*..*) echo "route-reason: refusing unsafe run id: $RUN_ID" >&2; exit 1 ;; *) : ;; esac &&
279
287
  REASON_FILE=".spur/run/$RUN_ID-route-reason.txt" &&
280
288
  if [ "$mode" = "fast" ]; then
281
289
  echo "fast:evidence complete+consistent" > "$REASON_FILE";
@@ -335,7 +343,7 @@ states:
335
343
  RETRY_OUTPUT=$("$@" 2>&1); RETRY_RC=$?;
336
344
  printf '%s\n' "$RETRY_OUTPUT";
337
345
  if [ "$RETRY_RC" -eq 0 ]; then return 0; fi;
338
- if ! printf '%s' "$RETRY_OUTPUT" | grep -Eq 'ENOENT|EBUSY|ENOTEMPTY|database is locked'; then return "$RETRY_RC"; fi;
346
+ if ! printf '%s' "$RETRY_OUTPUT" | grep -Eq 'ENOENT|EBUSY|ENOTEMPTY|database is locked|SQLite database .*is busy|SQLITE_BUSY'; then return "$RETRY_RC"; fi;
339
347
  sleep 2;
340
348
  RETRY_SECOND_OUTPUT=$("$@" 2>&1); RETRY_RC=$?;
341
349
  printf '%s\n' "$RETRY_SECOND_OUTPUT";
@@ -431,7 +439,7 @@ states:
431
439
  ATTEMPT_LOG="$LOG_FILE.attempt-$gate_attempt";
432
440
  sh -c "$qualityGateCmd" > "$ATTEMPT_LOG" 2>&1; gate_rc=$?;
433
441
  gate_locked=0;
434
- grep -q 'SQLiteError: database is locked' "$ATTEMPT_LOG" && gate_locked=1;
442
+ grep -Eq 'SQLiteError: database is locked|SQLite database .*is busy|SQLITE_BUSY' "$ATTEMPT_LOG" && gate_locked=1;
435
443
  cat "$ATTEMPT_LOG" >> "$LOG_FILE";
436
444
  rm -f "$ATTEMPT_LOG";
437
445
  if [ "$gate_rc" -eq 0 ] || [ "$gate_locked" -ne 1 ] || [ "$gate_attempt" -ge 5 ]; then break; fi;
@@ -547,7 +555,7 @@ states:
547
555
  ATTEMPT_LOG="$LOG_FILE.attempt-$gate_attempt";
548
556
  sh -c "$qualityGateCmd" > "$ATTEMPT_LOG" 2>&1; gate_rc=$?;
549
557
  gate_locked=0;
550
- grep -q 'SQLiteError: database is locked' "$ATTEMPT_LOG" && gate_locked=1;
558
+ grep -Eq 'SQLiteError: database is locked|SQLite database .*is busy|SQLITE_BUSY' "$ATTEMPT_LOG" && gate_locked=1;
551
559
  cat "$ATTEMPT_LOG" >> "$LOG_FILE";
552
560
  rm -f "$ATTEMPT_LOG";
553
561
  if [ "$gate_rc" -eq 0 ] || [ "$gate_locked" -ne 1 ] || [ "$gate_attempt" -ge 5 ]; then break; fi;
@@ -747,7 +755,7 @@ states:
747
755
  RETRY_OUTPUT=$("$@" 2>&1); RETRY_RC=$?;
748
756
  printf '%s\n' "$RETRY_OUTPUT";
749
757
  if [ "$RETRY_RC" -eq 0 ]; then return 0; fi;
750
- if ! printf '%s' "$RETRY_OUTPUT" | grep -Eq 'ENOENT|EBUSY|ENOTEMPTY|database is locked'; then return "$RETRY_RC"; fi;
758
+ if ! printf '%s' "$RETRY_OUTPUT" | grep -Eq 'ENOENT|EBUSY|ENOTEMPTY|database is locked|SQLite database .*is busy|SQLITE_BUSY'; then return "$RETRY_RC"; fi;
751
759
  sleep 2;
752
760
  RETRY_SECOND_OUTPUT=$("$@" 2>&1); RETRY_RC=$?;
753
761
  printf '%s\n' "$RETRY_SECOND_OUTPUT";
@@ -794,7 +802,7 @@ states:
794
802
  RETRY_OUTPUT=$("$@" 2>&1); RETRY_RC=$?;
795
803
  printf '%s\n' "$RETRY_OUTPUT";
796
804
  if [ "$RETRY_RC" -eq 0 ]; then return 0; fi;
797
- if ! printf '%s' "$RETRY_OUTPUT" | grep -Eq 'ENOENT|EBUSY|ENOTEMPTY|database is locked'; then return "$RETRY_RC"; fi;
805
+ if ! printf '%s' "$RETRY_OUTPUT" | grep -Eq 'ENOENT|EBUSY|ENOTEMPTY|database is locked|SQLite database .*is busy|SQLITE_BUSY'; then return "$RETRY_RC"; fi;
798
806
  sleep 2;
799
807
  RETRY_SECOND_OUTPUT=$("$@" 2>&1); RETRY_RC=$?;
800
808
  printf '%s\n' "$RETRY_SECOND_OUTPUT";
@@ -1016,8 +1024,13 @@ transitions:
1016
1024
  guard:
1017
1025
  kind: shell
1018
1026
  options:
1027
+ # 0804 R6: `--as done` projects the check onto the done target so open
1028
+ # Plan/AC checkboxes fail HERE — before the done action runs — mirroring
1029
+ # the CLI's own target-aware `--no-lifecycle` backstop (F92 R3). No `#`
1030
+ # comments inside the folded scalar: `>-` joins same-indent lines into
1031
+ # one shell line.
1019
1032
  command: >-
1020
- $spurBin task check $wbs &&
1033
+ $spurBin task check $wbs --as done &&
1021
1034
  test "$(jq -r .verdict .spur/run/$wbs-verdict.json 2>/dev/null)" = PASS &&
1022
1035
  test "$(jq -r '.proof.digest // ""' .spur/run/$wbs-verdict.json 2>/dev/null)" = "$proofDigest"
1023
1036
  - from: record
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gobing-ai/spur",
3
- "version": "0.3.76",
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.76",
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