@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/config/plugin-scripts.json +4 -0
- package/config/templates/feature/default.md +2 -0
- package/config/templates/task/brainstorm.md +2 -2
- package/config/templates/task/feature-impl.md +2 -2
- package/config/templates/task/issue.md +2 -2
- package/config/templates/task/meta.md +2 -2
- package/config/templates/task/review.md +2 -2
- package/config/templates/task/standard.md +2 -2
- package/config/workflow-candidates.json +13 -35
- package/config/workflows/feature-lifecycle.yaml +6 -0
- package/config/workflows/feature-verification.yaml +6 -5
- package/config/workflows/idea-pipeline.yaml +72 -36
- package/package.json +1 -1
- package/plugins/sp/README.md +7 -4
- package/plugins/sp/commands/dev-idea.md +9 -2
- package/plugins/sp/commands/dev-refactor.md +33 -0
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/references/roles.md +1 -1
- package/plugins/sp/scripts/idea-coverage-check.ts +168 -0
- package/plugins/sp/scripts/inline-pipeline-parity-check.ts +114 -3
- package/plugins/sp/scripts/inline-run-setup.ts +28 -18
- package/plugins/sp/skills/brainstorm/SKILL.md +4 -0
- package/plugins/sp/skills/code-refactoring/SKILL.md +155 -0
- package/plugins/sp/skills/code-refactoring/references/finding-schema.md +74 -0
- package/plugins/sp/skills/code-refactoring/references/fix-ladder.md +52 -0
- package/plugins/sp/skills/code-refactoring/references/focus-detection.md +44 -0
- package/plugins/sp/skills/code-refactoring/references/refactor-finding.schema.json +95 -0
- package/plugins/sp/skills/spec-decomposition/references/decomposition.md +23 -16
- package/plugins/sp/skills/spur-cli/references/features/acceptance-criteria.md +4 -2
- package/plugins/sp/skills/spur-cli/references/features.md +6 -1
- package/plugins/sp/skills/spur-cli/references/workflows/authoring-workflows.md +2 -2
- package/plugins/sp/skills/spur-cli/references/workflows/operations.md +3 -3
- package/plugins/sp/skills/spur-cli/references/workflows/workflow-fit-and-tuning.md +1 -1
- package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +24 -0
- package/plugins/sp/skills/spur-dev/references/dev-operations.md +18 -2
- package/plugins/sp/skills/spur-dev/references/flag-glossary.md +22 -3
- package/plugins/sp/skills/spur-dev/references/idea-evaluation.md +11 -1
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +33 -1
- package/plugins/sp/skills/taste-refactoring-api/SKILL.md +44 -1
- package/plugins/sp/skills/taste-refactoring-api/references/protocol-modes.md +31 -0
- package/plugins/sp/skills/taste-refactoring-architect/SKILL.md +43 -0
- package/plugins/sp/skills/taste-refactoring-tests/SKILL.md +42 -0
- package/plugins/sp/skills/taste-refactoring-ui/SKILL.md +43 -0
- package/schemas/task-batch.schema.json +2 -2
- 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-
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
79453
|
-
|
|
79454
|
-
|
|
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
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: [
|
|
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
|
|
96743
|
-
|
|
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 ?? [], ...
|
|
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.
|
|
110880
|
+
binaryVersion: "0.3.87",
|
|
110780
110881
|
configDir: ".spur",
|
|
110781
110882
|
configFile: ".spur/config.yaml",
|
|
110782
110883
|
databaseFile: ".spur/spur.db"
|