@gobing-ai/spur 0.3.86 → 0.3.87

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.
Files changed (46) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/plugin-scripts.json +4 -0
  3. package/config/templates/feature/default.md +2 -0
  4. package/config/templates/task/brainstorm.md +2 -2
  5. package/config/templates/task/feature-impl.md +2 -2
  6. package/config/templates/task/issue.md +2 -2
  7. package/config/templates/task/meta.md +2 -2
  8. package/config/templates/task/review.md +2 -2
  9. package/config/templates/task/standard.md +2 -2
  10. package/config/workflow-candidates.json +13 -35
  11. package/config/workflows/feature-lifecycle.yaml +6 -0
  12. package/config/workflows/feature-verification.yaml +6 -5
  13. package/config/workflows/idea-pipeline.yaml +72 -36
  14. package/package.json +1 -1
  15. package/plugins/sp/README.md +7 -4
  16. package/plugins/sp/commands/dev-idea.md +9 -2
  17. package/plugins/sp/commands/dev-refactor.md +33 -0
  18. package/plugins/sp/plugin.json +1 -1
  19. package/plugins/sp/references/roles.md +1 -1
  20. package/plugins/sp/scripts/idea-coverage-check.ts +168 -0
  21. package/plugins/sp/scripts/inline-pipeline-parity-check.ts +114 -3
  22. package/plugins/sp/scripts/inline-run-setup.ts +28 -18
  23. package/plugins/sp/skills/brainstorm/SKILL.md +4 -0
  24. package/plugins/sp/skills/code-refactoring/SKILL.md +155 -0
  25. package/plugins/sp/skills/code-refactoring/references/finding-schema.md +74 -0
  26. package/plugins/sp/skills/code-refactoring/references/fix-ladder.md +52 -0
  27. package/plugins/sp/skills/code-refactoring/references/focus-detection.md +44 -0
  28. package/plugins/sp/skills/code-refactoring/references/refactor-finding.schema.json +95 -0
  29. package/plugins/sp/skills/spec-decomposition/references/decomposition.md +23 -16
  30. package/plugins/sp/skills/spur-cli/references/features/acceptance-criteria.md +4 -2
  31. package/plugins/sp/skills/spur-cli/references/features.md +6 -1
  32. package/plugins/sp/skills/spur-cli/references/workflows/authoring-workflows.md +2 -2
  33. package/plugins/sp/skills/spur-cli/references/workflows/operations.md +3 -3
  34. package/plugins/sp/skills/spur-cli/references/workflows/workflow-fit-and-tuning.md +1 -1
  35. package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +24 -0
  36. package/plugins/sp/skills/spur-dev/references/dev-operations.md +18 -2
  37. package/plugins/sp/skills/spur-dev/references/flag-glossary.md +22 -3
  38. package/plugins/sp/skills/spur-dev/references/idea-evaluation.md +11 -1
  39. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +33 -1
  40. package/plugins/sp/skills/taste-refactoring-api/SKILL.md +44 -1
  41. package/plugins/sp/skills/taste-refactoring-api/references/protocol-modes.md +31 -0
  42. package/plugins/sp/skills/taste-refactoring-architect/SKILL.md +43 -0
  43. package/plugins/sp/skills/taste-refactoring-tests/SKILL.md +42 -0
  44. package/plugins/sp/skills/taste-refactoring-ui/SKILL.md +43 -0
  45. package/schemas/task-batch.schema.json +2 -2
  46. package/spur.js +124 -23
@@ -1,6 +1,18 @@
1
1
  ---
2
2
  name: taste-refactoring-api
3
3
  description: Design, review, and refactor REST/HTTP, RPC/gRPC, GraphQL, and event API contracts safely.
4
+ license: Apache-2.0
5
+ metadata:
6
+ author: spur
7
+ version: "1.0"
8
+ platforms: "claude-code,codex,openclaw,opencode,antigravity"
9
+ category: execution
10
+ interactions:
11
+ - technique
12
+ operations:
13
+ - refactor-apis
14
+ openclaw:
15
+ emoji: "🔌"
4
16
  ---
5
17
 
6
18
  # taste-refactoring-api
@@ -174,7 +186,7 @@ The goal is to eliminate repeated low-level API decisions, not to create bureauc
174
186
 
175
187
  ## Protocol-specific review
176
188
 
177
- After classifying the API, read the matching REST/HTTP, RPC/gRPC, GraphQL, or event/webhook section in [references/protocol-modes.md](references/protocol-modes.md). Apply that checklist before continuing with the refactoring strategy.
189
+ After classifying the API, read the matching REST/HTTP, RPC/gRPC, GraphQL, or event/webhook section in [references/protocol-modes.md](references/protocol-modes.md). Apply that checklist before continuing with the refactoring strategy. For command-line surface work (designing or reviewing a CLI), read the [CLI mode](references/protocol-modes.md) section instead — a CLI is a contract surface with the same compatibility duties as any wire protocol.
178
190
 
179
191
  ## Refactoring strategy
180
192
 
@@ -332,3 +344,34 @@ Specify the tests/checks needed: contract tests, schema validation, compatibilit
332
344
  ## Decision rule
333
345
 
334
346
  A “better” API is not the one with the prettiest route names. It is the one that makes common client code obvious, predictable, safe under failure, compatible over time, secure by default, and operable in production.
347
+
348
+ ## Spur contract
349
+
350
+ Machine-facing adapter for `sp:code-refactoring` dispatch (feature H13). Everything above is the
351
+ lens's native output and is unchanged; this section only maps it to the shared finding schema.
352
+
353
+ **Inputs received:** a scope path, an optional change description, and the `api` focus. Read
354
+ first: the classification and protocol section above (plus [references/protocol-modes.md](references/protocol-modes.md)),
355
+ then `../code-refactoring/references/finding-schema.md` for shared field semantics.
356
+
357
+ **Native finding → schema mapping:** each “Highest-impact refactor” item → `id` as
358
+ `RF-api-<nnn>`; the item's compatibility label → `rung` verbatim (`additive` | `risky` |
359
+ `breaking`); consumer impact → `title` and `proposal` (imperative); cited routes/lines →
360
+ `evidence` as `{file, line}` entries inside scope; `Verification` items → `verify`; the
361
+ “Proposed contract” snippets stay in `proposal`; new findings start at `status: open`.
362
+
363
+ **Severity mapping (design §5):** a `breaking` change already shipped, or contract ambiguity that
364
+ corrupts data → `P1`; a `risky` inconsistency across ≥2 endpoints → `P2`; `additive` cleanups →
365
+ `P3`; naming/docs → `P4`.
366
+
367
+ **Preserved-behavior inventory (required before proposals):** emit the consumer contract —
368
+ audience (public/partner/internal/service-to-service), endpoints/operations in scope, existing
369
+ clients that must remain compatible, and the compatibility promise — before the first finding.
370
+
371
+ **Preservation class (reuses the native classification):** `additive` → `preserving`; `risky`
372
+ and `breaking` → `breaking`; removal of an endpoint, field, or code path with callers →
373
+ `cutting` even when the change reads additive to remaining consumers.
374
+
375
+ **Stop rules:** an established style guide or public compatibility promise is a constraint, not
376
+ suggestion; `cutting`/`breaking` is never below `P2` and never `fix_eligibility: auto`; risky and
377
+ breaking changes carry a migration strategy; anything outside the scope path is not a finding.
@@ -77,3 +77,34 @@ For mutation races, prefer explicit optimistic concurrency such as ETags / `If-M
77
77
  - Make consumers tolerant of additive fields.
78
78
  - Do not use events as disguised synchronous RPC responses when the caller needs an immediate result.
79
79
 
80
+ ## CLI mode
81
+
82
+ A command-line interface is a contract surface with consumers (scripts, other agents, CI), not a
83
+ collection of convenience shortcuts. Review it with the same compatibility discipline as REST or
84
+ gRPC. Spur's own `apps/cli` is the first target.
85
+
86
+ **Noun/verb grammar:** keep one noun per domain and verbs per action (`spur task show`, not
87
+ `spur showTaskForTask`). Do not introduce a second grammar for the same concept — one spelling per
88
+ noun, one verb per operation, consistent object order.
89
+
90
+ **Flag vocabulary consistency:** shared flags (`--json`, `--scope`, `--agent`, `--fix`) keep the
91
+ same name, arity, and semantics everywhere they appear. A flag that means something new per
92
+ command is a defect; declare a new flag instead.
93
+
94
+ **Exit codes:** `0` = success, nonzero = failure, deterministic and machine-checkable. Never
95
+ swallow failures into `0`; never return nonzero for advisory output. Validation errors and
96
+ runtime failures should be distinguishable from output where practical.
97
+
98
+ **`--json` envelope stability:** `--json` output is a public schema. Add fields additively; never
99
+ remove or rename existing fields, never change a field's type, and emit no human decoration
100
+ (banners, progress text) on the JSON stream. Machine consumers parse the documented envelope only.
101
+
102
+ **Help-text parity:** every accepted flag appears in `--help` with its real arity and default;
103
+ every documented example runs as printed. A flag that works but is not documented, or documented
104
+ but rejected, is a parity break.
105
+
106
+ **Additive vs breaking:** adding a noun, verb, flag, or output field is additive. Renaming or
107
+ removing any of them, re-purposing a flag, changing a default, or altering existing output shape
108
+ is breaking — it requires a migration path and a deprecation window, and it never rides in a
109
+ patch release.
110
+
@@ -1,6 +1,18 @@
1
1
  ---
2
2
  name: taste-refactoring-architect
3
3
  description: Review, simplify, and refactor system architecture toward minimum sufficient architecture while preserving required capabilities, quality attributes, delivery safety, and data invariants. Backs architectural refactoring and boundary reviews.
4
+ license: Apache-2.0
5
+ metadata:
6
+ author: spur
7
+ version: "1.0"
8
+ platforms: "claude-code,codex,openclaw,opencode,antigravity"
9
+ category: execution
10
+ interactions:
11
+ - technique
12
+ operations:
13
+ - refactor-architecture
14
+ openclaw:
15
+ emoji: "🏛️"
4
16
  ---
5
17
 
6
18
  # taste-refactoring-architect
@@ -469,3 +481,34 @@ Do not block on perfect documentation. Produce:
469
481
  - decisions that can safely proceed now.
470
482
 
471
483
  Use A7 DEFER/OBSERVE when evidence is too weak for a structural change.
484
+
485
+ ## Spur contract
486
+
487
+ Machine-facing adapter for `sp:code-refactoring` dispatch (feature H13). Everything above is the
488
+ lens's native output and is unchanged; this section only maps it to the shared finding schema.
489
+
490
+ **Inputs received:** a scope path, an optional change description, and the `architect` focus.
491
+ Read first: the intervention ladder and workflow above, then
492
+ `../code-refactoring/references/finding-schema.md` for shared field semantics.
493
+
494
+ **Native finding → schema mapping:** `ID` → `id` as `RF-architect-<nnn>`; `Action` (A0–A7) →
495
+ `rung` verbatim; `Observation`/`Why it matters` → `title` plus `proposal` (imperative); `Evidence`
496
+ → `evidence` as `{file, line}` entries inside scope; `Fitness functions` → `verify`; new findings
497
+ start at `status: open`. ADR candidates and migration plans stay prose findings with
498
+ `fix_eligibility: suggest`.
499
+
500
+ **Severity mapping (design §5):** any axis ≤1 with a correctness/safety consequence → `P1`; axis
501
+ ≤2 or A5–A7 seam problems → `P2`; A3–A4 → `P3`; A0–A2, ADR candidates, deferred questions → `P4`.
502
+
503
+ **Preserved-behavior inventory (required before proposals):** emit the Preservation Contract
504
+ table — capabilities, quality attributes, data invariants, and their thresholds — for everything
505
+ in scope before the first finding.
506
+
507
+ **Preservation class:** A0/A3/A4 mechanical consolidations with identical behavior →
508
+ `preserving`; A1/A2 removal of a live service, endpoint, queue, or code path with callers →
509
+ `cutting`; A5–A7 seam or behavior changes → `breaking`; migration plans and ADR candidates →
510
+ `preserving` prose with `fix_eligibility: suggest`.
511
+
512
+ **Stop rules:** no removal without dependency/traffic/contract evidence; `cutting`/`breaking` is
513
+ never below `P2` and never `fix_eligibility: auto`; multi-task migrations are `suggest`, applied
514
+ only after an explicit operator answer; anything outside the scope path is not a finding.
@@ -1,6 +1,18 @@
1
1
  ---
2
2
  name: taste-refactoring-tests
3
3
  description: Refactor unit tests for failure sensitivity, deterministic confidence, and regression protection without mock-heavy brittle tests or vanity coverage. Backs test suite quality audits.
4
+ license: Apache-2.0
5
+ metadata:
6
+ author: spur
7
+ version: "1.0"
8
+ platforms: "claude-code,codex,openclaw,opencode,antigravity"
9
+ category: execution
10
+ interactions:
11
+ - technique
12
+ operations:
13
+ - refactor-tests
14
+ openclaw:
15
+ emoji: "🧪"
4
16
  ---
5
17
 
6
18
  # taste-refactoring-tests
@@ -480,3 +492,33 @@ A test refactor is complete only when:
480
492
  - delivery speed is not degraded without a justified risk trade-off.
481
493
 
482
494
  The goal is **earned confidence**: green means something because the suite has demonstrated that it can turn red when the product is wrong.
495
+
496
+ ## Spur contract
497
+
498
+ Machine-facing adapter for `sp:code-refactoring` dispatch (feature H13). Everything above is the
499
+ lens's native output and is unchanged; this section only maps it to the shared finding schema.
500
+
501
+ **Inputs received:** a scope path, an optional change description, and the `tests` focus. Read
502
+ first: the intervention ladder, Confidence Contract, and review severity above, then
503
+ `../code-refactoring/references/finding-schema.md` for shared field semantics.
504
+
505
+ **Native finding → schema mapping:** `ID` → `id` as `RF-tests-<nnn>`; `Action` (T0–T7) → `rung`
506
+ verbatim; `Severity` → `severity` via the map below; `Evidence` → `evidence` as `{file, line}`
507
+ entries inside scope; `Why it matters` → `title`; `Proposed change` → `proposal` (imperative);
508
+ `Expected regression-detection improvement` → folded into `proposal`; new findings start at
509
+ `status: open`.
510
+
511
+ **Severity mapping (design §5):** native P0 → `P1`; P1 → `P2`; P2 → `P3`; P3 → `P4`.
512
+
513
+ **Preserved-behavior inventory (required before proposals):** emit the Confidence Contract —
514
+ critical business rules, compatibility contracts, authorization/security decisions, data
515
+ integrity invariants, error semantics, and incident regressions the suite must keep protecting —
516
+ before the first finding.
517
+
518
+ **Preservation class:** T0 and rungs that keep or add protection (fixture cleanup, adding a
519
+ missing test) → `preserving`; removing a test or test file (T1) → `cutting`; weakening or
520
+ semantically changing existing assertions → `breaking`.
521
+
522
+ **Stop rules:** unknown protection is a risk, never permission to delete; a `cutting`/`breaking`
523
+ finding is never `fix_eligibility: auto` and never below `P2`; tests are never removed or
524
+ weakened by an `auto` fix; anything outside the scope path is not a finding.
@@ -1,6 +1,18 @@
1
1
  ---
2
2
  name: taste-refactoring-ui
3
3
  description: Design, review, and refactor UI hierarchy, layout, typography, spacing, color, and interactions.
4
+ license: Apache-2.0
5
+ metadata:
6
+ author: spur
7
+ version: "1.0"
8
+ platforms: "claude-code,codex,openclaw,opencode,antigravity"
9
+ category: execution
10
+ interactions:
11
+ - technique
12
+ operations:
13
+ - refactor-ui
14
+ openclaw:
15
+ emoji: "🎨"
4
16
  ---
5
17
 
6
18
  # taste-refactoring-ui
@@ -288,3 +300,34 @@ For deeper reasoning and daily use, read:
288
300
  - `references/refactoring-ui-playbook.md` — chapter-by-chapter distilled guidance.
289
301
  - `checklists/daily-ui-review.md` — fast audit and ship checklist.
290
302
  - `examples/review-template.md` — reusable UI critique response structure.
303
+
304
+ ## Spur contract
305
+
306
+ Machine-facing adapter for `sp:code-refactoring` dispatch (feature H13). Everything above is the
307
+ lens's native output and is unchanged; this section only maps it to the shared finding schema.
308
+
309
+ **Inputs received:** a scope path, an optional change description, and the `ui` focus. Read
310
+ first: the refactoring protocol and output contract above, then
311
+ `../code-refactoring/references/finding-schema.md` for shared field semantics.
312
+
313
+ **Native finding → schema mapping:** each issue → `id` as `RF-ui-<nnn>`; the pass that owns it
314
+ (hierarchy, spacing, typography, color, states) → `rung`; `Observation` + `Refactor` → `title`
315
+ and `proposal` (imperative); the cited component/file and line → `evidence` as `{file, line}`
316
+ entries inside scope; the `System rule` → folded into `proposal`; new findings start at
317
+ `status: open`.
318
+
319
+ **Severity mapping (design §5):** native P0 (task failure) → `P1`; P1 (hierarchy/structure) →
320
+ `P2`; P2 (system inconsistency) → `P3`; P3 (polish) → `P4`.
321
+
322
+ **Preserved-behavior inventory (required before proposals):** emit the controls and interactions
323
+ in scope — primary/secondary actions, navigation, form semantics, accessible names, states —
324
+ before the first finding.
325
+
326
+ **Preservation class:** token normalization and layout/typography consolidation with identical
327
+ function → `preserving`; adding a missing state or a11y attribute → `preserving`; removing or
328
+ merging a control or interaction → `cutting`; changes to user-visible flows or behavior →
329
+ `breaking`.
330
+
331
+ **Stop rules:** meaning is never carried by color alone; preserve component semantics and
332
+ accessibility; a `cutting`/`breaking` finding is never `fix_eligibility: auto` and never below
333
+ `P2`; anything outside the scope path is not a finding.
@@ -20,7 +20,7 @@
20
20
  },
21
21
  "requirements": {
22
22
  "type": "string",
23
- "description": "R-numbered requirement items (R1. R2. …) — a referenceable list."
23
+ "description": "Pre-filled ### Requirements body: one R-item per line, exactly `- [ ] R1. <text>` (checkbox + `R<n>.`); any other form fails `spur task check`."
24
24
  },
25
25
  "design": {
26
26
  "type": "string",
@@ -32,7 +32,7 @@
32
32
  },
33
33
  "acceptance_criteria": {
34
34
  "type": "string",
35
- "description": "Pre-filled ### Acceptance Criteria body (Gherkin or checklist)."
35
+ "description": "Pre-filled ### Acceptance Criteria body: items numbered AC1, AC2, … (never R<n>, the Requirements namespace): `- [ ] AC1 — <feature scenario title without its R-number>` bullets or `Scenario: AC1 — <title>` blocks (L4.uncovered-task-scenario matches the title after the prefix); `(req: R<n>)` binds a task requirement; task-only checks go in prose after the bullets."
36
36
  },
37
37
  "feature_id": {
38
38
  "type": "string",
package/spur.js CHANGED
@@ -60176,12 +60176,12 @@ function stripScenarioPrefixes(title) {
60176
60176
  let previous;
60177
60177
  do {
60178
60178
  previous = out;
60179
- out = out.replace(/^\[[^\]]*\]\s*/, "").replace(/\s*\[[^\]]*\]\s*$/, "").replace(/^Scenario:\s*/i, "").replace(/^R\d+\s*[:\-\u2014]?\s*/, "").trim();
60179
+ out = out.replace(/^\[[^\]]*\]\s*/, "").replace(/\s*\[[^\]]*\]\s*$/, "").replace(/^Scenario:\s*/i, "").replace(/^(?:AC|R)\d+\.?\s*[:\-\u2014]?\s*/, "").replace(/\s*\(req:[^)]*\)/i, " ").trim();
60180
60180
  } while (out !== previous);
60181
60181
  return out;
60182
60182
  }
60183
60183
  function normalizeTitle(title) {
60184
- return stripScenarioPrefixes(title).replace(/^(R\d+)\s*[:\-\u2014]?\s*/, "").trim().toLowerCase().replace(/[\u0027\u2018\u2019\u201c\u201d]/g, "").replace(/\s+/g, " ").trim();
60184
+ return stripScenarioPrefixes(title).replace(/^(?:AC|R)\d+\.?\s*[:\-\u2014]?\s*/, "").trim().toLowerCase().replace(/[\u0027\u2018\u2019\u201c\u201d]/g, "").replace(/\s+/g, " ").trim();
60185
60185
  }
60186
60186
  function stripCoversClause(text3) {
60187
60187
  return text3.replace(new RegExp(COVERS_RE_SOURCE, "gi"), " ").replace(/\s+/g, " ").trim();
@@ -60680,6 +60680,13 @@ class ActionRunDao {
60680
60680
  constructor(db2) {
60681
60681
  this.db = db2;
60682
60682
  }
60683
+ async completedAtById(id) {
60684
+ const row = await this.db.queryFirst("SELECT completed_at FROM action_runs WHERE id = ?", id);
60685
+ return row?.completed_at ?? null;
60686
+ }
60687
+ async setStartedAt(id, startedAt) {
60688
+ await this.db.run("UPDATE action_runs SET started_at = ? WHERE id = ?", startedAt, id);
60689
+ }
60683
60690
  async actionRowsByRunId(runId) {
60684
60691
  try {
60685
60692
  return await this.db.queryAll("SELECT id, node, kind, status, duration_ms, ok, result_json, started_at, completed_at, created_at FROM action_runs WHERE run_id = ? ORDER BY created_at", runId);
@@ -79285,11 +79292,14 @@ function hasAdjacentFileLineColumns(body) {
79285
79292
  }
79286
79293
  return false;
79287
79294
  }
79295
+ function hasGateLanguage(body) {
79296
+ return /(?<![\w-])(HITL|human[- ]in[- ]the-loop|approval|approved|merge event|merged|content-gate|GATED|capstone)(?![\w-])/i.test(body);
79297
+ }
79288
79298
  function hasSolutionFileLineCitation(body) {
79289
79299
  const hasFileLine = /`[^`]+?:\d+(-\d+)?`/.test(body) || /[^\s`]\.\w+:\d+/.test(body);
79290
79300
  return hasFileLine || hasAdjacentFileLineColumns(body);
79291
79301
  }
79292
- var TESTING_VERDICT_STUB_RE, EXTERNAL_EVIDENCE_RE, TaskCheckService;
79302
+ var TESTING_VERDICT_STUB_RE, EXTERNAL_EVIDENCE_RE, GATE_LANGUAGE_SECTIONS, TaskCheckService;
79293
79303
  var init_task_check = __esm(() => {
79294
79304
  init_src2();
79295
79305
  init_planning_check_base();
@@ -79298,6 +79308,7 @@ var init_task_check = __esm(() => {
79298
79308
  init_verify_verdict();
79299
79309
  TESTING_VERDICT_STUB_RE = /^\|\s*[\u2014\u2013-]+\s*\|\s*[\u2014\u2013-]+\s*\|\s*No requirements recorded; verify verdict\s+\S+\s*\|/m;
79300
79310
  EXTERNAL_EVIDENCE_RE = /`([^`\n]+?)`\s+(?:line|lines?)\s+(\d+)(?:-(\d+))?/g;
79311
+ GATE_LANGUAGE_SECTIONS = ["Background", "Requirements", "Design", "Acceptance Criteria", "Plan"];
79301
79312
  TaskCheckService = class TaskCheckService extends PlanningCheckService {
79302
79313
  locator;
79303
79314
  constructor(fs3, matrix, locator) {
@@ -79449,9 +79460,17 @@ var init_task_check = __esm(() => {
79449
79460
  if (s2 === null)
79450
79461
  continue;
79451
79462
  scenarioCount++;
79452
- const id = /^R(\d+)\b/.exec((s2[1] ?? "").trim());
79453
- if (id?.[1] !== undefined)
79454
- acIds.add(id[1]);
79463
+ const title = (s2[1] ?? "").trim();
79464
+ const req = /\(req:\s*([^)]*)\)/i.exec(title);
79465
+ if (req?.[1] !== undefined) {
79466
+ for (const m of req[1].matchAll(/\bR(\d+)\b/g))
79467
+ if (m[1] !== undefined)
79468
+ acIds.add(m[1]);
79469
+ } else {
79470
+ const id = /^R(\d+)\b/.exec(title);
79471
+ if (id?.[1] !== undefined)
79472
+ acIds.add(id[1]);
79473
+ }
79455
79474
  }
79456
79475
  const byNumber = (a2, b) => Number(a2) - Number(b);
79457
79476
  if (reqIds.size > 0 && scenarioCount > 0 && acIds.size === 0) {
@@ -79460,7 +79479,7 @@ var init_task_check = __esm(() => {
79460
79479
  code: FINDING_CODES.L3_AC_REQUIREMENT_COVERAGE,
79461
79480
  severity: "warning",
79462
79481
  section: "Acceptance Criteria",
79463
- message: "ac_numbering is task-local but no scenario is R-numbered \u2014 prefix each scenario with the requirement it covers (Scenario: R1 \u2014 \u2026)"
79482
+ message: "ac_numbering is task-local but no scenario binds a requirement \u2014 add `(req: R1)` to each `Scenario: AC<n> \u2014 \u2026` title (legacy: `Scenario: R1 \u2014 \u2026`)"
79464
79483
  });
79465
79484
  } else if (reqIds.size > 0 && acIds.size > 0) {
79466
79485
  const uncovered = [...reqIds].filter((id) => !acIds.has(id)).sort(byNumber);
@@ -79856,11 +79875,11 @@ var init_task_check = __esm(() => {
79856
79875
  const declaredDeps = doc2.frontmatterData?.dependencies;
79857
79876
  if (Array.isArray(declaredDeps) && declaredDeps.length > 0)
79858
79877
  return;
79859
- for (const section of ["Background", "Requirements", "Design", "Acceptance Criteria", "Plan"]) {
79878
+ for (const section of GATE_LANGUAGE_SECTIONS) {
79860
79879
  const body = doc2.getSection(section);
79861
79880
  if (body === null)
79862
79881
  continue;
79863
- if (/(?<![\w-])(HITL|human[- ]in[- ]the-loop|approval|approved|merge event|merged|content-gate|GATED|capstone)(?![\w-])/i.test(body)) {
79882
+ if (hasGateLanguage(body)) {
79864
79883
  findings.push({
79865
79884
  layer: "L4",
79866
79885
  code: FINDING_CODES.L4_GATE_LANGUAGE,
@@ -83097,7 +83116,17 @@ class FeatureService {
83097
83116
  }
83098
83117
  const raw = await this.ctx.fs.readFile(sourceFile);
83099
83118
  const body = stripLeadingSectionHeader(raw, sectionName);
83100
- return this.ctx.writeService.updateSection(ref, sectionName, body);
83119
+ let result = await this.ctx.writeService.updateSection(ref, sectionName, body);
83120
+ if (sectionName === "Acceptance Criteria" && body.trim().length > 0) {
83121
+ const acBody = stripAcFence(body);
83122
+ const looksGherkin = /^\s*(Feature:|Scenario:|Scenario Outline:)/m.test(acBody);
83123
+ if (looksGherkin || parseChecklist(acBody).length === 0) {
83124
+ const errors6 = validateAcceptanceCriteria(acBody).errors.map((e) => `L3.ac-bdd-error: BDD: ${e.message}`);
83125
+ if (errors6.length > 0)
83126
+ result = { ...result, warnings: [...result.warnings ?? [], ...errors6] };
83127
+ }
83128
+ }
83129
+ return result;
83101
83130
  }
83102
83131
  async updateBody(id, body) {
83103
83132
  const ref = await this.refFor(id);
@@ -83649,6 +83678,8 @@ updated_at: "${now2}"
83649
83678
  ## Acceptance Criteria
83650
83679
 
83651
83680
  \`\`\`gherkin
83681
+ # Keep the Feature: line (feature check L3.ac-bdd-error without it). Each Scenario: title is the
83682
+ # identity key tasks reference verbatim ("- [ ] AC1 \u2014 <title>"); number R1, R2, \u2026; never rename after tasks link.
83652
83683
  Feature: ${name}
83653
83684
 
83654
83685
  Scenario: Basic acceptance
@@ -88049,24 +88080,30 @@ import { join as join22 } from "path";
88049
88080
  class WorkflowActionTraceWriter {
88050
88081
  inner;
88051
88082
  recordFailure;
88083
+ db;
88052
88084
  boundaries = new Map;
88053
- constructor(inner, recordFailure) {
88085
+ constructor(inner, recordFailure, db2) {
88054
88086
  this.inner = inner;
88055
88087
  this.recordFailure = recordFailure;
88088
+ this.db = db2;
88056
88089
  }
88057
88090
  async saveActionStart(runId, node, kind, options) {
88058
88091
  const outcome = await this.guard("action.start", { runId, node, kind }, () => this.inner.saveActionStart(runId, node, kind, options));
88059
88092
  if (!outcome.ok) {
88060
88093
  const syntheticId = `${UNPERSISTED_PREFIX}${crypto.randomUUID()}`;
88061
88094
  this.boundaries.set(syntheticId, { runId, node, kind });
88095
+ this.lastStart = { runId, node, kind };
88062
88096
  return syntheticId;
88063
88097
  }
88064
88098
  this.boundaries.set(outcome.value, { runId, node, kind });
88099
+ this.lastStart = { runId, node, kind };
88065
88100
  return outcome.value;
88066
88101
  }
88102
+ lastStart;
88067
88103
  async saveActionFinalize(actionId, status, durationMs, ok, kind, result, redactor) {
88068
88104
  const boundary2 = this.boundaries.get(actionId);
88069
- await this.guard("action.finish", { runId: boundary2?.runId ?? "", node: boundary2?.node, kind }, () => this.inner.saveActionFinalize(actionId, status, durationMs, ok, kind, result, redactor));
88105
+ const origin2 = boundary2 ?? this.lastStart;
88106
+ await this.guard("action.finish", { runId: origin2?.runId ?? "", node: origin2?.node, kind }, () => this.inner.saveActionFinalize(actionId, status, durationMs, ok, kind, result, redactor));
88070
88107
  this.boundaries.delete(actionId);
88071
88108
  }
88072
88109
  async finalizeRun(runId, status, completedAt) {
@@ -88077,7 +88114,23 @@ class WorkflowActionTraceWriter {
88077
88114
  if (!start.ok)
88078
88115
  return start;
88079
88116
  const finish = await this.guard("action.finish", { runId: boundary2.runId, node: boundary2.node, kind: boundary2.kind }, () => this.inner.saveActionFinalize(start.value, boundary2.status, boundary2.durationMs, boundary2.ok, boundary2.kind, boundary2.result, boundary2.redactor));
88080
- return finish.ok ? { ok: true, actionId: start.value } : finish;
88117
+ if (!finish.ok)
88118
+ return finish;
88119
+ await this.backdateStart(start.value, boundary2);
88120
+ return { ok: true, actionId: start.value };
88121
+ }
88122
+ async backdateStart(actionId, boundary2) {
88123
+ const db2 = this.db;
88124
+ if (db2 === undefined || boundary2.durationMs <= 0)
88125
+ return;
88126
+ await this.guard("action.backdate", { runId: boundary2.runId, node: boundary2.node, kind: boundary2.kind }, async () => {
88127
+ const dao2 = new ActionRunDao(db2);
88128
+ const completedAt = await dao2.completedAtById(actionId);
88129
+ if (completedAt === null)
88130
+ return;
88131
+ const startedAt = new Date(new Date(completedAt).getTime() - boundary2.durationMs).toISOString();
88132
+ await dao2.setStartedAt(actionId, startedAt);
88133
+ });
88081
88134
  }
88082
88135
  async closeRun(runId, status, completedAt) {
88083
88136
  const existing = await this.inner.loadRun(runId);
@@ -88149,7 +88202,7 @@ function withActionTrace(persistence2, recordFailure) {
88149
88202
  return new WorkflowActionTraceWriter(persistence2, recordFailure);
88150
88203
  }
88151
88204
  function createWorkflowActionTraceWriter(db2, recordFailure) {
88152
- return new WorkflowActionTraceWriter(new DbWorkflowPersistenceAdapter(db2), recordFailure);
88205
+ return new WorkflowActionTraceWriter(new DbWorkflowPersistenceAdapter(db2), recordFailure, db2);
88153
88206
  }
88154
88207
  function createRunLogTraceFailureRecorder(cwd) {
88155
88208
  const fileSystem = createNodeFileSystem3();
@@ -88160,7 +88213,7 @@ function createRunLogTraceFailureRecorder(cwd) {
88160
88213
  failure.node !== undefined ? `node=${failure.node}` : "",
88161
88214
  failure.kind !== undefined ? `kind=${failure.kind}` : ""
88162
88215
  ].filter((part) => part !== "").join(" ");
88163
- const line = `[${failure.at}] trace-emission-failed operation=${failure.operation} run=${failure.runId}${location === "" ? "" : ` ${location}`}: ${failure.error}
88216
+ const line = `[${failure.at.replace(/\.\d{3}Z$/, "Z")}] trace-emission-failed operation=${failure.operation} run=${failure.runId}${location === "" ? "" : ` ${location}`}: ${failure.error}
88164
88217
  `;
88165
88218
  (async () => {
88166
88219
  await fileSystem.ensureDir(dir);
@@ -88172,6 +88225,7 @@ function createRunLogTraceFailureRecorder(cwd) {
88172
88225
  }
88173
88226
  var UNPERSISTED_PREFIX = "trace-unpersisted:", RunRowNotFoundError;
88174
88227
  var init_action_trace = __esm(() => {
88228
+ init_src2();
88175
88229
  init_dist8();
88176
88230
  init_dist5();
88177
88231
  RunRowNotFoundError = class RunRowNotFoundError extends Error {
@@ -96514,7 +96568,16 @@ ${templateBackground}`;
96514
96568
  }
96515
96569
  const filePath = await this.resolveTaskFile(wbs);
96516
96570
  const ref = { kind: "task", id: wbs, filePath, folder: this.ctx.tasksDir };
96517
- return this.writeService.updateFrontmatter(ref, key2, value);
96571
+ let result = await this.writeService.updateFrontmatter(ref, key2, value);
96572
+ if (key2 === "feature_id") {
96573
+ const ac = MarkdownDocument.parse(await this.ctx.fs.readFile(filePath), "task").getSection("Acceptance Criteria");
96574
+ if (ac !== null && ac.trim().length > 0) {
96575
+ const warnings = await this.checkAcSubsetWarning(filePath, ac);
96576
+ if (warnings.length > 0)
96577
+ result = { ...result, warnings: [...result.warnings ?? [], ...warnings] };
96578
+ }
96579
+ }
96580
+ return result;
96518
96581
  }
96519
96582
  async mutateDependencies(wbs, op, values = []) {
96520
96583
  if (op === "clear") {
@@ -96728,22 +96791,41 @@ ${templateBackground}`;
96728
96791
  throw new SectionMutationError("invalid-solution", "Solution must contain at least one `file:line` citation");
96729
96792
  }
96730
96793
  const ref = { kind: "task", id: wbs, filePath, folder: this.ctx.tasksDir };
96731
- const result = await this.writeService.updateSection(ref, sectionName, body);
96794
+ let result = await this.writeService.updateSection(ref, sectionName, body);
96732
96795
  if (sectionName === "Acceptance Criteria") {
96733
96796
  const warnings = await this.checkAcSubsetWarning(filePath, body);
96734
96797
  if (warnings.length > 0) {
96735
- return {
96798
+ result = { ...result, warnings: [...result.warnings ?? [], ...warnings] };
96799
+ }
96800
+ }
96801
+ if (GATE_LANGUAGE_SECTIONS.includes(sectionName) && hasGateLanguage(body)) {
96802
+ const deps = MarkdownDocument.parse(await this.ctx.fs.readFile(filePath), "task").frontmatterData?.dependencies;
96803
+ if (!Array.isArray(deps) || deps.length === 0) {
96804
+ result = {
96736
96805
  ...result,
96737
- warnings: [...result.warnings ?? [], ...warnings]
96806
+ warnings: [
96807
+ ...result.warnings ?? [],
96808
+ `L4.gate-language: ${sectionName} contains gate language (HITL/approval/approved/merged/\u2026) \u2014 say "operator answer"/"accepted", or model the gate as a frontmatter dependency`
96809
+ ]
96738
96810
  };
96739
96811
  }
96740
96812
  }
96741
96813
  if (sectionName === "Requirements" || sectionName === "Plan") {
96742
- const report = evaluateTaskSize(await this.ctx.fs.readFile(filePath));
96743
- if (!report.ok) {
96814
+ const written = await this.ctx.fs.readFile(filePath);
96815
+ const warnings = [];
96816
+ if (sectionName === "Requirements") {
96817
+ for (const f of structuralFindings(written, "task")) {
96818
+ if (f.code === FINDING_CODES.L3_REQUIREMENTS_CHECKBOX)
96819
+ warnings.push(`${f.code}: ${f.message}`);
96820
+ }
96821
+ }
96822
+ const report = evaluateTaskSize(written);
96823
+ if (!report.ok)
96824
+ warnings.push(...report.reasons);
96825
+ if (warnings.length > 0) {
96744
96826
  return {
96745
96827
  ...result,
96746
- warnings: [...result.warnings ?? [], ...report.reasons]
96828
+ warnings: [...result.warnings ?? [], ...warnings]
96747
96829
  };
96748
96830
  }
96749
96831
  }
@@ -97187,6 +97269,8 @@ var init_task_service = __esm(() => {
97187
97269
  init_dist();
97188
97270
  init_errors7();
97189
97271
  init_pipeline_run_link();
97272
+ init_planning_check_base();
97273
+ init_structural_repair();
97190
97274
  init_task_check();
97191
97275
  init_task_locator();
97192
97276
  init_task_record();
@@ -98439,6 +98523,7 @@ async function projectWorkflowProgress(runId, options) {
98439
98523
  group.push(row);
98440
98524
  actionsByNode[row.node] = group;
98441
98525
  }
98526
+ const matchedActionRowIds = new Set;
98442
98527
  for (const { state: stateId, visit: visit2 } of stateVisits) {
98443
98528
  const defState = defStates.find((s3) => s3.id === stateId);
98444
98529
  const onEnterActions = Array.isArray(defState?.onEnter) ? defState.onEnter : [];
@@ -98474,6 +98559,8 @@ async function projectWorkflowProgress(runId, options) {
98474
98559
  const stateEffect = "may-write";
98475
98560
  const evidenceEffect = "none";
98476
98561
  const candidateRows = stateActionRows.filter((r) => r.kind === kind && !usedActionRowIds.has(r.id));
98562
+ for (const r of candidateRows)
98563
+ matchedActionRowIds.add(r.id);
98477
98564
  let actionStatus = "pending";
98478
98565
  const attempts = [];
98479
98566
  if (!isVisited) {
@@ -98550,6 +98637,14 @@ async function projectWorkflowProgress(runId, options) {
98550
98637
  });
98551
98638
  }
98552
98639
  }
98640
+ for (const row of actionRows) {
98641
+ if (!matchedActionRowIds.has(row.id)) {
98642
+ diagnostics.push({
98643
+ code: "orphan-action-row",
98644
+ message: `Action row ${row.id} (node ${row.node}, kind ${row.kind}) matches no declared state action`
98645
+ });
98646
+ }
98647
+ }
98553
98648
  return {
98554
98649
  schemaVersion: 1,
98555
98650
  runId,
@@ -108124,6 +108219,9 @@ ${result.content}`);
108124
108219
  '`spur task update <wbs> done --force-done --provenance-bypass --reason "\u2026"`.',
108125
108220
  "See the gate checklist (spur-dev/references/gate-checklists.md).",
108126
108221
  "Valid section names (no failed write): `spur task sections <wbs> list`.",
108222
+ "Requirements body: one `- [ ] R1. <text>` per line. Acceptance Criteria body: `- [ ] AC1 \u2014 <feature",
108223
+ "scenario title>` bullets or `Scenario: AC1 \u2014 <title>` blocks (AC<n> is task-local, never R<n>);",
108224
+ "`(req: R<n>)` binds a requirement; `ac_altitude: task-local` exempts task-only AC.",
108127
108225
  'Sections replace, with one exception: `--section "Q&A"` APPENDS a timestamped',
108128
108226
  "`#### Q&A entry \u2014 <ISO>` block. Start the body with `<!-- qa:replace -->` to replace it wholesale."
108129
108227
  ].join(`
@@ -108172,6 +108270,9 @@ ${result.content}`);
108172
108270
  if (options.json) {
108173
108271
  context4.output.write(toEnvelopeJson(result, { enveloped: options.jsonEnvelope }));
108174
108272
  } else {
108273
+ for (const warning of result.warnings ?? []) {
108274
+ context4.output.error(warning);
108275
+ }
108175
108276
  context4.output.write(`Set ${key2}=${value} on task ${result.ref.id}`);
108176
108277
  }
108177
108278
  } else if (status !== undefined) {
@@ -110776,7 +110877,7 @@ import { createRequire } from "module";
110776
110877
  var CLI_CONFIG = {
110777
110878
  binaryName: "spur",
110778
110879
  binaryLabel: "spur",
110779
- binaryVersion: "0.3.86",
110880
+ binaryVersion: "0.3.87",
110780
110881
  configDir: ".spur",
110781
110882
  configFile: ".spur/config.yaml",
110782
110883
  databaseFile: ".spur/spur.db"