@gobing-ai/spur 0.2.11 → 0.2.12
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/package.json +1 -1
- package/spur-cli/config/workflows/feature-dev.yaml +5 -0
- package/spur-cli/config/workflows/idea-pipeline.yaml +285 -0
- package/spur-cli/config/workflows/planning-pipeline.yaml +19 -8
- package/spur-cli/config/workflows/task-pipeline.yaml +20 -0
- package/spur-cli/config/workflows/wrapup-pipeline.yaml +225 -0
- package/spur.js +40 -20
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gobing-ai/spur",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.12",
|
|
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",
|
|
@@ -86,6 +86,11 @@ states:
|
|
|
86
86
|
|
|
87
87
|
- id: done
|
|
88
88
|
description: Terminal — feature planned, all tasks executed, and the feature verified.
|
|
89
|
+
# Checkpoint write: record session state for resume (Phase 4, task 0171 R3)
|
|
90
|
+
onEnter:
|
|
91
|
+
- kind: shell
|
|
92
|
+
options:
|
|
93
|
+
command: 'mkdir -p .spur/memory/sessions && echo "checkpoint: feature-dev done featureId=${vars.featureId} ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)" > .spur/memory/sessions/${vars.featureId}-checkpoint.md'
|
|
89
94
|
|
|
90
95
|
- id: failed
|
|
91
96
|
description: Terminal — plan rejected, a task failed verification, or the feature check did not pass.
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
# Idea-to-feature pipeline — unified entry from a vague idea to a feature, AC, and task batch
|
|
2
|
+
# (design §idea-pipeline).
|
|
3
|
+
#
|
|
4
|
+
# Orchestration is configuration (ADR-022 / §3.2): this is YAML over the existing
|
|
5
|
+
# dual-workflow engine — zero new engine code. The pipeline STOPS at handoff — tasks are
|
|
6
|
+
# created but NOT executed. Use task-pipeline.yaml or feature-dev.yaml for execution.
|
|
7
|
+
# No state in this pipeline inlines another pipeline's state graph (no-nesting principle,
|
|
8
|
+
# design §System Principles 4).
|
|
9
|
+
#
|
|
10
|
+
# Shape: start -> discovery -> feature-create -> ac-generate -> feature-check
|
|
11
|
+
# -> system-design (conditional: needs_design signal or --design)
|
|
12
|
+
# -> design-approval (taste HITL gate; not auto-clicked by --auto)
|
|
13
|
+
# -> decompose -> batch-create -> handoff
|
|
14
|
+
# (feature-check failure routes back to ac-generate; batch-create failure to decompose).
|
|
15
|
+
#
|
|
16
|
+
# Vars (passed as a JSON object via `--vars`):
|
|
17
|
+
# idea — the idea text (required), e.g. --vars '{"idea":"add a --dry-run flag to dev-wrap"}'
|
|
18
|
+
# profile — set --vars '{"profile":"auto"}' to skip objective HITL gates (feature-check, batch-create)
|
|
19
|
+
# design — "auto" (default, signal-driven), "force" (--design), "skip" (--skip-design)
|
|
20
|
+
# spurBin — PATH-independent spur invocation (overridden by CLI at run start)
|
|
21
|
+
# agent — agent for agent.run steps (default: omp)
|
|
22
|
+
#
|
|
23
|
+
# Seeded by `spur init`; adapt the agent.run inputs to your project's command set.
|
|
24
|
+
|
|
25
|
+
"$schema": "@gobing-ai/spur/schemas/state-machine-workflow.schema.json"
|
|
26
|
+
kind: state-machine
|
|
27
|
+
name: idea-pipeline
|
|
28
|
+
description: "Idea to feature + AC + task batch: discovery, feature-create, ac-generate, feature-check, system-design, decompose, batch-create, handoff"
|
|
29
|
+
iterationBound: 15
|
|
30
|
+
initialState: start
|
|
31
|
+
terminalStates:
|
|
32
|
+
- handoff
|
|
33
|
+
- cancelled
|
|
34
|
+
- failed
|
|
35
|
+
vars:
|
|
36
|
+
idea: ""
|
|
37
|
+
profile: "standard"
|
|
38
|
+
design: "auto"
|
|
39
|
+
spurBin: "spur"
|
|
40
|
+
agent: "omp"
|
|
41
|
+
stepTimeoutMs: "600000"
|
|
42
|
+
|
|
43
|
+
states:
|
|
44
|
+
- id: start
|
|
45
|
+
description: >
|
|
46
|
+
Pipeline start. The idea text arrives as vars.idea. The pipeline stops at
|
|
47
|
+
handoff — no task execution.
|
|
48
|
+
onEnter:
|
|
49
|
+
- kind: note
|
|
50
|
+
options:
|
|
51
|
+
message: "Idea pipeline start for idea: ${vars.idea}"
|
|
52
|
+
- kind: shell
|
|
53
|
+
options:
|
|
54
|
+
command: 'rm -f .spur/run/idea-ac-retry-count .spur/run/idea-decompose-retry-count'
|
|
55
|
+
|
|
56
|
+
- id: discovery
|
|
57
|
+
description: >
|
|
58
|
+
Dispatch sp:brainstorm to explore the idea, generate approaches with trade-offs,
|
|
59
|
+
and record a design summary. The brainstorm ALWAYS records a design summary
|
|
60
|
+
("nothing is too simple" pattern). The brainstorm also emits a needs_design
|
|
61
|
+
boolean signal written to .spur/run/idea-needs-design.json — this determines
|
|
62
|
+
whether the system-design state runs.
|
|
63
|
+
onEnter:
|
|
64
|
+
- kind: agent.run
|
|
65
|
+
options:
|
|
66
|
+
agent: ${vars.agent}
|
|
67
|
+
input: "Run sp:brainstorm for the idea: ${vars.idea}. Generate 2-3 approaches with trade-offs and a design summary. Perform the scope decomposition check and write the needs_design boolean to .spur/run/idea-needs-design.json as {\"needs_design\": true|false}. Criteria: true if multiple subsystems/schema/transport/dependency changes; false if single-module/bug-fix/pattern-following. Ties lean true. ALWAYS record a design summary — no idea is too simple."
|
|
68
|
+
timeoutMs: ${vars.stepTimeoutMs}
|
|
69
|
+
|
|
70
|
+
- id: feature-create
|
|
71
|
+
description: >
|
|
72
|
+
Create a feature via spur feature create, or select an existing feature id.
|
|
73
|
+
The agent writes the feature id to .spur/run/idea-feature-id.txt for subsequent
|
|
74
|
+
states to read.
|
|
75
|
+
onEnter:
|
|
76
|
+
- kind: agent.run
|
|
77
|
+
options:
|
|
78
|
+
agent: ${vars.agent}
|
|
79
|
+
input: "Create a feature for the idea: ${vars.idea}. Use 'spur feature create \"<name>\" --json' to create it. Write the feature id to .spur/run/idea-feature-id.txt. If an existing feature is appropriate, use its id instead."
|
|
80
|
+
timeoutMs: ${vars.stepTimeoutMs}
|
|
81
|
+
|
|
82
|
+
- id: ac-generate
|
|
83
|
+
description: >
|
|
84
|
+
Generate acceptance criteria per ac-style-guide.md. Write AC scenarios to the
|
|
85
|
+
feature file via spur feature update (direct body edit, not --section). The
|
|
86
|
+
agent authors R-numbered Gherkin scenarios tied to the design summary.
|
|
87
|
+
onEnter:
|
|
88
|
+
- kind: agent.run
|
|
89
|
+
options:
|
|
90
|
+
agent: ${vars.agent}
|
|
91
|
+
input: "Generate acceptance criteria for the feature whose id is in .spur/run/idea-feature-id.txt. Read the feature file, author R-numbered BDD Gherkin scenarios per ac-style-guide.md, and write them to the feature body via direct file edit (spur feature update does not support --section for features). Run 'spur feature check <id>' to validate."
|
|
92
|
+
timeoutMs: ${vars.stepTimeoutMs}
|
|
93
|
+
|
|
94
|
+
- id: feature-check
|
|
95
|
+
description: >
|
|
96
|
+
Objective gate: run spur feature check <id> --strict. Under --auto, routes
|
|
97
|
+
around the HITL gate when the check passes. On failure, routes back to
|
|
98
|
+
ac-generate for AC revision. The feature id is read from
|
|
99
|
+
.spur/run/idea-feature-id.txt.
|
|
100
|
+
onEnter:
|
|
101
|
+
- kind: note
|
|
102
|
+
options:
|
|
103
|
+
message: "Running feature check for feature $(cat .spur/run/idea-feature-id.txt)"
|
|
104
|
+
|
|
105
|
+
- id: system-design
|
|
106
|
+
description: >
|
|
107
|
+
Dispatch sp:sys-architecture to produce the system design. This state runs
|
|
108
|
+
only when needs_design=true (or --design forces it). The agent creates ADR
|
|
109
|
+
entries, architecture updates, and design satellites through constitution rules.
|
|
110
|
+
The design-approval state follows (taste gate).
|
|
111
|
+
onEnter:
|
|
112
|
+
- kind: agent.run
|
|
113
|
+
options:
|
|
114
|
+
agent: ${vars.agent}
|
|
115
|
+
input: "Run sp:sys-architecture for the feature whose id is in .spur/run/idea-feature-id.txt. Read the brainstorm artifact and feature AC. Produce ADR entries, architecture updates, and design satellites (docs/design/<slug>.md) following the constitution edit rules. Do not write task or feature corpus files directly."
|
|
116
|
+
timeoutMs: ${vars.stepTimeoutMs}
|
|
117
|
+
|
|
118
|
+
- id: design-approval
|
|
119
|
+
description: >
|
|
120
|
+
HITL taste gate. The operator reviews the system design and approves or
|
|
121
|
+
rejects it. This gate is NOT auto-clicked by --auto (Auto-Decision Principle
|
|
122
|
+
#5: taste-decision -> surface to human). Only routes around when
|
|
123
|
+
vars.design_approved=true (explicit prior approval).
|
|
124
|
+
pause: true
|
|
125
|
+
onEnter:
|
|
126
|
+
- kind: hitl.confirm
|
|
127
|
+
options:
|
|
128
|
+
prompt: "Review the system design for feature $(cat .spur/run/idea-feature-id.txt). Approve to proceed to decomposition?"
|
|
129
|
+
|
|
130
|
+
- id: decompose
|
|
131
|
+
description: >
|
|
132
|
+
Dispatch sp:spec-decomposition with the brainstorm artifact, feature AC, and
|
|
133
|
+
design doc as input. The agent produces a task-batch JSON file at
|
|
134
|
+
.spur/run/idea-task-batch.json, validated against task-batch.schema.json.
|
|
135
|
+
onEnter:
|
|
136
|
+
- kind: agent.run
|
|
137
|
+
options:
|
|
138
|
+
agent: ${vars.agent}
|
|
139
|
+
input: "Run sp:spec-decomposition for the feature whose id is in .spur/run/idea-feature-id.txt. Read the brainstorm artifact, feature AC, and design doc. Produce a task-batch JSON array at .spur/run/idea-task-batch.json, validated against apps/cli/schemas/task-batch.schema.json. Each entry must have filled Requirements, AC, Design, and Plan sections."
|
|
140
|
+
timeoutMs: ${vars.stepTimeoutMs}
|
|
141
|
+
|
|
142
|
+
- id: batch-create
|
|
143
|
+
description: >
|
|
144
|
+
Objective gate: run spur task batch-create with the batch JSON from the
|
|
145
|
+
decompose state. Under --auto, routes around the HITL gate when the batch
|
|
146
|
+
validates. On failure, routes back to decompose.
|
|
147
|
+
onEnter:
|
|
148
|
+
- kind: note
|
|
149
|
+
options:
|
|
150
|
+
message: "Running batch-create for feature $(cat .spur/run/idea-feature-id.txt)"
|
|
151
|
+
|
|
152
|
+
- id: handoff
|
|
153
|
+
description: >
|
|
154
|
+
Terminal — idea pipeline complete. Output: feature id, task WBS list, and
|
|
155
|
+
the next command to run (/sp:dev-run <first-wbs> or /sp:dev-runall --tasks feature:<id>).
|
|
156
|
+
No task execution — the pipeline stops here.
|
|
157
|
+
onEnter:
|
|
158
|
+
- kind: note
|
|
159
|
+
options:
|
|
160
|
+
message: "Idea pipeline handoff. Feature: $(cat .spur/run/idea-feature-id.txt). Tasks created. Next: /sp:dev-runall --tasks feature:$(cat .spur/run/idea-feature-id.txt)"
|
|
161
|
+
# Checkpoint write: record session state for resume (Phase 4, task 0171 R3)
|
|
162
|
+
- kind: shell
|
|
163
|
+
options:
|
|
164
|
+
command: 'mkdir -p .spur/memory/sessions && echo "checkpoint: idea-pipeline handoff feature=$(cat .spur/run/idea-feature-id.txt) ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)" > .spur/memory/sessions/idea-checkpoint.md'
|
|
165
|
+
|
|
166
|
+
- id: cancelled
|
|
167
|
+
description: Terminal — pipeline cancelled by operator or error.
|
|
168
|
+
|
|
169
|
+
- id: failed
|
|
170
|
+
description: >
|
|
171
|
+
Terminal — pipeline failed after exhausting retry caps on cyclic edges
|
|
172
|
+
(feature-check → ac-generate or batch-create → decompose). The error
|
|
173
|
+
message in the run trace identifies which edge exceeded the cap.
|
|
174
|
+
|
|
175
|
+
transitions:
|
|
176
|
+
# ── start -> discovery ──
|
|
177
|
+
- from: start
|
|
178
|
+
to: discovery
|
|
179
|
+
description: Begin discovery — brainstorm the idea.
|
|
180
|
+
guard:
|
|
181
|
+
kind: always
|
|
182
|
+
|
|
183
|
+
# ── discovery -> feature-create ──
|
|
184
|
+
- from: discovery
|
|
185
|
+
to: feature-create
|
|
186
|
+
description: Discovery complete — create or select a feature.
|
|
187
|
+
guard:
|
|
188
|
+
kind: always
|
|
189
|
+
|
|
190
|
+
# ── feature-create -> ac-generate ──
|
|
191
|
+
- from: feature-create
|
|
192
|
+
to: ac-generate
|
|
193
|
+
description: Feature created — generate acceptance criteria.
|
|
194
|
+
guard:
|
|
195
|
+
kind: always
|
|
196
|
+
|
|
197
|
+
# ── ac-generate -> feature-check ──
|
|
198
|
+
- from: ac-generate
|
|
199
|
+
to: feature-check
|
|
200
|
+
description: AC generated — run feature check.
|
|
201
|
+
guard:
|
|
202
|
+
kind: always
|
|
203
|
+
|
|
204
|
+
# ── feature-check: pass + design route -> system-design, pass + skip route -> decompose, fail -> ac-generate ──
|
|
205
|
+
# Declaration order matters: system-design route tried first, then skip route, then fail.
|
|
206
|
+
- from: feature-check
|
|
207
|
+
to: system-design
|
|
208
|
+
description: Feature check passed, design route — run system design.
|
|
209
|
+
guard:
|
|
210
|
+
kind: shell
|
|
211
|
+
options:
|
|
212
|
+
command: '${vars.spurBin} feature check $(cat .spur/run/idea-feature-id.txt) --strict && (test "${vars.design}" = force || (test "${vars.design}" = auto && test "$(jq -r .needs_design .spur/run/idea-needs-design.json 2>/dev/null)" != false))'
|
|
213
|
+
- from: feature-check
|
|
214
|
+
to: decompose
|
|
215
|
+
description: Feature check passed, skip-design route — go directly to decompose.
|
|
216
|
+
guard:
|
|
217
|
+
kind: shell
|
|
218
|
+
options:
|
|
219
|
+
command: '${vars.spurBin} feature check $(cat .spur/run/idea-feature-id.txt) --strict && (test "${vars.design}" = skip || (test "${vars.design}" = auto && test "$(jq -r .needs_design .spur/run/idea-needs-design.json 2>/dev/null)" = false))'
|
|
220
|
+
- from: feature-check
|
|
221
|
+
to: ac-generate
|
|
222
|
+
description: "Feature check failed — revise AC (retry cap: 3)."
|
|
223
|
+
guard:
|
|
224
|
+
kind: shell
|
|
225
|
+
options:
|
|
226
|
+
command: '! ${vars.spurBin} feature check $(cat .spur/run/idea-feature-id.txt) --strict && test "$(cat .spur/run/idea-ac-retry-count 2>/dev/null || echo 0)" -lt 3 && echo $(( $(cat .spur/run/idea-ac-retry-count 2>/dev/null || echo 0) + 1 )) > .spur/run/idea-ac-retry-count'
|
|
227
|
+
- from: feature-check
|
|
228
|
+
to: failed
|
|
229
|
+
description: Feature check failed after 3 retries — escalate to failed.
|
|
230
|
+
guard:
|
|
231
|
+
kind: shell
|
|
232
|
+
options:
|
|
233
|
+
command: '! ${vars.spurBin} feature check $(cat .spur/run/idea-feature-id.txt) --strict && test "$(cat .spur/run/idea-ac-retry-count 2>/dev/null || echo 0)" -ge 3'
|
|
234
|
+
|
|
235
|
+
# ── system-design: -> design-approval (normal) OR -> decompose (auto + prior approval) ──
|
|
236
|
+
# Declaration order: auto-skip guard tried FIRST (like task-pipeline's review->verify pattern).
|
|
237
|
+
- from: system-design
|
|
238
|
+
to: decompose
|
|
239
|
+
description: profile=auto AND design_approved=true — skip design approval gate.
|
|
240
|
+
guard:
|
|
241
|
+
kind: shell
|
|
242
|
+
options:
|
|
243
|
+
command: 'test "${vars.profile}" = auto && test "${vars.design_approved}" = true'
|
|
244
|
+
- from: system-design
|
|
245
|
+
to: design-approval
|
|
246
|
+
description: System design done — gate on design approval.
|
|
247
|
+
guard:
|
|
248
|
+
kind: always
|
|
249
|
+
|
|
250
|
+
# ── design-approval -> decompose (after operator approval) ──
|
|
251
|
+
- from: design-approval
|
|
252
|
+
to: decompose
|
|
253
|
+
description: Design approved — proceed to decomposition.
|
|
254
|
+
guard:
|
|
255
|
+
kind: always
|
|
256
|
+
|
|
257
|
+
# ── decompose -> batch-create ──
|
|
258
|
+
- from: decompose
|
|
259
|
+
to: batch-create
|
|
260
|
+
description: Decomposition complete — create task batch.
|
|
261
|
+
guard:
|
|
262
|
+
kind: always
|
|
263
|
+
|
|
264
|
+
# ── batch-create: success -> handoff, failure -> decompose (retry cap: 3), failure+cap -> failed ──
|
|
265
|
+
- from: batch-create
|
|
266
|
+
to: handoff
|
|
267
|
+
description: Batch created — handoff.
|
|
268
|
+
guard:
|
|
269
|
+
kind: shell
|
|
270
|
+
options:
|
|
271
|
+
command: '${vars.spurBin} task batch-create --file .spur/run/idea-task-batch.json'
|
|
272
|
+
- from: batch-create
|
|
273
|
+
to: decompose
|
|
274
|
+
description: "Batch-create failed — revise decomposition (retry cap: 3)."
|
|
275
|
+
guard:
|
|
276
|
+
kind: shell
|
|
277
|
+
options:
|
|
278
|
+
command: '! ${vars.spurBin} task batch-create --file .spur/run/idea-task-batch.json && test "$(cat .spur/run/idea-decompose-retry-count 2>/dev/null || echo 0)" -lt 3 && echo $(( $(cat .spur/run/idea-decompose-retry-count 2>/dev/null || echo 0) + 1 )) > .spur/run/idea-decompose-retry-count'
|
|
279
|
+
- from: batch-create
|
|
280
|
+
to: failed
|
|
281
|
+
description: Batch-create failed after 3 retries — escalate to failed.
|
|
282
|
+
guard:
|
|
283
|
+
kind: shell
|
|
284
|
+
options:
|
|
285
|
+
command: '! ${vars.spurBin} task batch-create --file .spur/run/idea-task-batch.json && test "$(cat .spur/run/idea-decompose-retry-count 2>/dev/null || echo 0)" -ge 3'
|
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
# commit (Q3 — hybrid by risk).
|
|
13
13
|
#
|
|
14
14
|
# Shape: phasing(HITL) → feature-id → design-gen → design-approval(HITL) → handoff
|
|
15
|
-
# (phasing
|
|
15
|
+
# (phasing skipped when profile=auto; design-approval skipped when profile=auto
|
|
16
|
+
# AND design_approved=true — taste gates pause even in auto unless prior approval
|
|
17
|
+
# is encoded, per design doc HITL taxonomy).
|
|
16
18
|
#
|
|
17
19
|
# Vars (passed as a JSON object via `--vars`):
|
|
18
20
|
# slug — the design-doc slug, e.g. `--vars '{"slug":"auth-migration"}'`
|
|
@@ -78,8 +80,11 @@ states:
|
|
|
78
80
|
- id: design-approval
|
|
79
81
|
description: >
|
|
80
82
|
HITL gate — the highest-leverage gate in the pipeline. Loop until the operator
|
|
81
|
-
approves the design doc. Under profile=auto this state is
|
|
82
|
-
|
|
83
|
+
approves the design doc. Under profile=auto this state is skipped ONLY when
|
|
84
|
+
vars.design_approved=true (explicit prior approval); otherwise the taste gate
|
|
85
|
+
pauses even in auto mode (design doc HITL taxonomy). Loop back to design-gen
|
|
86
|
+
for revisions.
|
|
87
|
+
pause: true
|
|
83
88
|
onEnter:
|
|
84
89
|
- kind: hitl.confirm
|
|
85
90
|
options:
|
|
@@ -90,6 +95,11 @@ states:
|
|
|
90
95
|
Terminal — design doc approved; the drafted feature list is ready. Hand off to
|
|
91
96
|
sp:spur-dev (steps 7–12), which starts at `spur feature create`. The seam is the
|
|
92
97
|
drafted-feature-list file at docs/plans/${vars.slug}-drafted.md.
|
|
98
|
+
# Checkpoint write: record session state for resume (Phase 4, task 0171 R3)
|
|
99
|
+
onEnter:
|
|
100
|
+
- kind: shell
|
|
101
|
+
options:
|
|
102
|
+
command: 'mkdir -p .spur/memory/sessions && echo "checkpoint: planning-pipeline handoff slug=${vars.slug} ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)" > .spur/memory/sessions/${vars.slug}-checkpoint.md'
|
|
93
103
|
|
|
94
104
|
- id: cancelled
|
|
95
105
|
description: Terminal — planning abandoned by the operator.
|
|
@@ -131,16 +141,17 @@ transitions:
|
|
|
131
141
|
guard:
|
|
132
142
|
kind: always
|
|
133
143
|
|
|
134
|
-
# ── design-gen → design-approval, OR skip the HITL gate when profile=auto ──
|
|
135
|
-
# Auto-skip guard tried FIRST: under profile=auto
|
|
136
|
-
# without entering design-approval
|
|
144
|
+
# ── design-gen → design-approval, OR skip the HITL gate when profile=auto AND design_approved=true ──
|
|
145
|
+
# Auto-skip guard tried FIRST: under profile=auto with explicit prior approval,
|
|
146
|
+
# route straight to handoff without entering design-approval. Without prior
|
|
147
|
+
# approval, the taste gate pauses even in auto (design doc HITL taxonomy).
|
|
137
148
|
- from: design-gen
|
|
138
149
|
to: handoff
|
|
139
|
-
description: profile=auto — skip the design-approval HITL gate
|
|
150
|
+
description: profile=auto AND design_approved=true — skip the design-approval HITL gate.
|
|
140
151
|
guard:
|
|
141
152
|
kind: shell
|
|
142
153
|
options:
|
|
143
|
-
command: 'test "${vars.profile}" = auto'
|
|
154
|
+
command: 'test "${vars.profile}" = auto && test "${vars.design_approved}" = true'
|
|
144
155
|
- from: design-gen
|
|
145
156
|
to: design-approval
|
|
146
157
|
description: Design doc drafted — gate on human approval.
|
|
@@ -75,6 +75,15 @@ states:
|
|
|
75
75
|
- kind: shell
|
|
76
76
|
options:
|
|
77
77
|
command: "${vars.spurBin} task update ${vars.wbs} wip --no-lifecycle"
|
|
78
|
+
# Post-implement cleanup: auto-format any unformatted output the agent produced.
|
|
79
|
+
# The agent may leave files that don't pass `biome check`, which then fails
|
|
80
|
+
# `bun run lint` downstream. Running format here is cheap (< 1 s) and prevents
|
|
81
|
+
# the test stage from tripping on a purely mechanical formatting defect (dogfood
|
|
82
|
+
# bug-733 — omp left agent-run.test.ts unformatted, causing a spurious lint gate
|
|
83
|
+
# failure that the ## Testing section mis-attributed to "pre-existing gaps").
|
|
84
|
+
- kind: shell
|
|
85
|
+
options:
|
|
86
|
+
command: "bun run format"
|
|
78
87
|
|
|
79
88
|
- id: test
|
|
80
89
|
description: Test execution + coverage via /sp:dev-unit.
|
|
@@ -84,6 +93,13 @@ states:
|
|
|
84
93
|
agent: ${vars.agent}
|
|
85
94
|
input: /sp:dev-unit ${vars.wbs} --auto
|
|
86
95
|
timeoutMs: ${vars.stepTimeoutMs}
|
|
96
|
+
# Post-test gate: run the project's full lint gate (biome check + tsc). If the
|
|
97
|
+
# implement or test step left unformatted files or type errors, the run routes to
|
|
98
|
+
# `failed` rather than letting an agent-authored `## Testing` claim "all pass"
|
|
99
|
+
# against a red gate (dogfood bug-733).
|
|
100
|
+
- kind: shell
|
|
101
|
+
options:
|
|
102
|
+
command: "bun run lint"
|
|
87
103
|
|
|
88
104
|
- id: review
|
|
89
105
|
description: SECUA-framework code review via /sp:dev-review (Security, Efficiency, Correctness, Usability, Architecture).
|
|
@@ -147,6 +163,10 @@ states:
|
|
|
147
163
|
- kind: note
|
|
148
164
|
options:
|
|
149
165
|
message: "Pipeline complete for task ${vars.wbs} (done gate cleared)."
|
|
166
|
+
# Checkpoint write: record session state for resume (Phase 4, task 0171 R3)
|
|
167
|
+
- kind: shell
|
|
168
|
+
options:
|
|
169
|
+
command: 'mkdir -p .spur/memory/sessions && echo "checkpoint: task-pipeline done wbs=${vars.wbs} ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)" > .spur/memory/sessions/${vars.wbs}-checkpoint.md'
|
|
150
170
|
|
|
151
171
|
- id: failed
|
|
152
172
|
description: Terminal — precheck (or a gated step) failed; reported, not advanced.
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# Wrap-up pipeline — post-execution wrap-up for one task or a batch (design §wrapup-pipeline).
|
|
2
|
+
#
|
|
3
|
+
# Orchestration is configuration (ADR-022 / §3.2): this is YAML over the existing
|
|
4
|
+
# dual-workflow engine — zero new engine code. The pipeline NEVER mutates task status —
|
|
5
|
+
# wrap-up consumes completed tasks and produces learning/metrics/doc artifacts only.
|
|
6
|
+
# Feature transitions go through `spur feature update` so the feature-lifecycle guards
|
|
7
|
+
# apply identically. Branch cleanup is an irreversible HITL gate — it always pauses,
|
|
8
|
+
# even under --auto (Iron Law #6: irreversible action -> surface to human).
|
|
9
|
+
#
|
|
10
|
+
# Shape: start -> task-resolve -> doc-sync -> learning-capture -> metrics-record
|
|
11
|
+
# -> feature-transition (conditional: if vars.feature set)
|
|
12
|
+
# -> branch-cleanup (conditional: if vars.merge=true)
|
|
13
|
+
# -> done
|
|
14
|
+
# (task-resolve with empty list short-circuits to `skipped`).
|
|
15
|
+
#
|
|
16
|
+
# Vars (passed as a JSON object via `--vars`):
|
|
17
|
+
# tasks — JSON array of WBS strings (required), e.g. --vars '{"tasks":["0167"]}'
|
|
18
|
+
# feature — feature id to advance through legal lifecycle edges (optional)
|
|
19
|
+
# profile — set --vars '{"profile":"auto"}' to skip objective confirmations
|
|
20
|
+
# merge — set --vars '{"merge":"true"}' to run branch cleanup (irreversible HITL)
|
|
21
|
+
# spurBin — PATH-independent spur invocation (overridden by CLI at run start)
|
|
22
|
+
# agent — agent for agent.run steps (default: omp)
|
|
23
|
+
#
|
|
24
|
+
# Seeded by `spur init`; adapt the agent.run inputs to your project's command set.
|
|
25
|
+
|
|
26
|
+
"$schema": "@gobing-ai/spur/schemas/state-machine-workflow.schema.json"
|
|
27
|
+
kind: state-machine
|
|
28
|
+
name: wrapup-pipeline
|
|
29
|
+
description: "Post-execution wrap-up: doc-sync, learning-capture, metrics, feature-transition, branch-cleanup"
|
|
30
|
+
iterationBound: 10
|
|
31
|
+
initialState: start
|
|
32
|
+
terminalStates:
|
|
33
|
+
- done
|
|
34
|
+
- skipped
|
|
35
|
+
vars:
|
|
36
|
+
tasks: "[]"
|
|
37
|
+
feature: ""
|
|
38
|
+
profile: "standard"
|
|
39
|
+
merge: "false"
|
|
40
|
+
spurBin: "spur"
|
|
41
|
+
agent: "omp"
|
|
42
|
+
stepTimeoutMs: "600000"
|
|
43
|
+
|
|
44
|
+
states:
|
|
45
|
+
- id: start
|
|
46
|
+
description: >
|
|
47
|
+
Pipeline start. Task statuses are NOT mutated by wrap-up — wrap-up consumes
|
|
48
|
+
completed tasks and produces learning/metrics/doc artifacts only.
|
|
49
|
+
onEnter:
|
|
50
|
+
- kind: note
|
|
51
|
+
options:
|
|
52
|
+
message: "Wrap-up pipeline start for tasks: ${vars.tasks}. Task statuses are NOT mutated."
|
|
53
|
+
|
|
54
|
+
- id: task-resolve
|
|
55
|
+
description: >
|
|
56
|
+
Validate the task list passed by the wrapper is non-empty. The command wrapper
|
|
57
|
+
(dev-wrap/dev-wrapall) resolves the task list and passes it as vars.tasks;
|
|
58
|
+
this state validates it before proceeding. Empty list routes to `skipped`.
|
|
59
|
+
onEnter:
|
|
60
|
+
- kind: note
|
|
61
|
+
options:
|
|
62
|
+
message: "Resolving task list: ${vars.tasks}"
|
|
63
|
+
|
|
64
|
+
- id: doc-sync
|
|
65
|
+
description: >
|
|
66
|
+
Dispatch sp:doc-evolve once for the entire batch. Project-level doc drift repair
|
|
67
|
+
runs once, not per-task. The agent reads the batch's completed tasks, identifies
|
|
68
|
+
doc drift (04_DESIGN, 03_ARCHITECTURE, 00_ADR, docs/design/*), and repairs it
|
|
69
|
+
following the constitution's edit rules. Does NOT write task or feature corpus.
|
|
70
|
+
onEnter:
|
|
71
|
+
- kind: agent.run
|
|
72
|
+
options:
|
|
73
|
+
agent: ${vars.agent}
|
|
74
|
+
input: "Run sp:doc-evolve for completed tasks ${vars.tasks}. Repair doc drift in 04_DESIGN, 03_ARCHITECTURE, 00_ADR, docs/design/* following the constitution edit rules. Do not write task or feature corpus files."
|
|
75
|
+
timeoutMs: ${vars.stepTimeoutMs}
|
|
76
|
+
|
|
77
|
+
- id: learning-capture
|
|
78
|
+
description: >
|
|
79
|
+
Append working learnings to .spur/memory/learnings.md. The agent extracts insights
|
|
80
|
+
from the batch — conventions discovered, errors hit and resolved, patterns that
|
|
81
|
+
worked, gotchas for future tasks. Grouped by date and task WBS. Working scratchpad,
|
|
82
|
+
not a CLI-gated corpus artifact.
|
|
83
|
+
onEnter:
|
|
84
|
+
- kind: agent.run
|
|
85
|
+
options:
|
|
86
|
+
agent: ${vars.agent}
|
|
87
|
+
input: "Extract working learnings from tasks ${vars.tasks}. Append to .spur/memory/learnings.md grouped by date and task WBS. Include: conventions discovered, errors hit and resolved, patterns that worked, gotchas. This is a working scratchpad, not a validated corpus."
|
|
88
|
+
timeoutMs: ${vars.stepTimeoutMs}
|
|
89
|
+
|
|
90
|
+
- id: metrics-record
|
|
91
|
+
description: >
|
|
92
|
+
Append one JSONL row per task to .spur/memory/wrapup-metrics.jsonl. Each row
|
|
93
|
+
records: task WBS, feature id, status, verdict, timestamp. Append-only and
|
|
94
|
+
machine-readable.
|
|
95
|
+
onEnter:
|
|
96
|
+
- kind: agent.run
|
|
97
|
+
options:
|
|
98
|
+
agent: ${vars.agent}
|
|
99
|
+
input: "For each task in ${vars.tasks}, read the task file and append one JSONL row to .spur/memory/wrapup-metrics.jsonl with fields: wbs, feature_id, status, verdict (from .spur/run/<wbs>-verdict.json if available), timestamp. Append-only; do not overwrite existing rows."
|
|
100
|
+
timeoutMs: ${vars.stepTimeoutMs}
|
|
101
|
+
|
|
102
|
+
- id: feature-transition
|
|
103
|
+
description: >
|
|
104
|
+
If vars.feature is set, advance the feature through legal lifecycle edges only
|
|
105
|
+
via spur feature update. Never attempt backlog|active -> done directly. The
|
|
106
|
+
legal path: backlog -> active (always), active -> verifying (feature check
|
|
107
|
+
guard), verifying -> done (strict feature check guard). Task statuses are NOT
|
|
108
|
+
mutated. The agent checks current status and advances idempotently.
|
|
109
|
+
onEnter:
|
|
110
|
+
- kind: agent.run
|
|
111
|
+
options:
|
|
112
|
+
agent: ${vars.agent}
|
|
113
|
+
input: "Advance feature ${vars.feature} through legal lifecycle edges using spur feature update. Check current status with 'spur feature show ${vars.feature} --json'. If backlog, transition to active. If active, run 'spur feature check ${vars.feature}' then transition to verifying. If verifying, run 'spur feature check ${vars.feature} --strict' then transition to done. Never skip edges (no backlog->done directly). Report transitions made."
|
|
114
|
+
timeoutMs: ${vars.stepTimeoutMs}
|
|
115
|
+
|
|
116
|
+
- id: branch-cleanup
|
|
117
|
+
description: >
|
|
118
|
+
Irreversible HITL gate. If vars.merge=true, dispatch branch cleanup (merge or
|
|
119
|
+
delete). This gate ALWAYS pauses — even under --auto — because branch operations
|
|
120
|
+
are irreversible (Auto-Decision Principle #6). The operator must explicitly confirm.
|
|
121
|
+
pause: true
|
|
122
|
+
onEnter:
|
|
123
|
+
- kind: hitl.confirm
|
|
124
|
+
options:
|
|
125
|
+
prompt: "Branch cleanup for tasks ${vars.tasks}. This is IRREVERSIBLE (merge or delete). Confirm to proceed?"
|
|
126
|
+
|
|
127
|
+
- id: done
|
|
128
|
+
description: >
|
|
129
|
+
Terminal — wrap-up complete. Output summary: tasks wrapped, learnings captured,
|
|
130
|
+
metrics recorded, feature advanced (if applicable), branch cleaned (if applicable).
|
|
131
|
+
onEnter:
|
|
132
|
+
- kind: note
|
|
133
|
+
options:
|
|
134
|
+
message: "Wrap-up pipeline complete for tasks: ${vars.tasks}. Learnings at .spur/memory/learnings.md, metrics at .spur/memory/wrapup-metrics.jsonl."
|
|
135
|
+
# Checkpoint write: record session state for resume (Phase 4, task 0171 R3)
|
|
136
|
+
- kind: shell
|
|
137
|
+
options:
|
|
138
|
+
command: 'mkdir -p .spur/memory/sessions && echo "checkpoint: wrapup-pipeline done tasks=${vars.tasks} ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)" > .spur/memory/sessions/wrapup-checkpoint.md'
|
|
139
|
+
|
|
140
|
+
- id: skipped
|
|
141
|
+
description: Terminal — wrap-up skipped (empty task list or operator abort).
|
|
142
|
+
|
|
143
|
+
transitions:
|
|
144
|
+
# ── start -> task-resolve ──
|
|
145
|
+
- from: start
|
|
146
|
+
to: task-resolve
|
|
147
|
+
description: Begin wrap-up.
|
|
148
|
+
guard:
|
|
149
|
+
kind: always
|
|
150
|
+
|
|
151
|
+
# ── task-resolve: non-empty -> doc-sync, empty -> skipped ──
|
|
152
|
+
- from: task-resolve
|
|
153
|
+
to: doc-sync
|
|
154
|
+
description: Task list resolved and non-empty — begin doc-sync.
|
|
155
|
+
guard:
|
|
156
|
+
kind: shell
|
|
157
|
+
options:
|
|
158
|
+
command: 'test "$(echo "${vars.tasks}" | jq length)" -gt 0'
|
|
159
|
+
- from: task-resolve
|
|
160
|
+
to: skipped
|
|
161
|
+
description: Task list is empty — skip wrap-up.
|
|
162
|
+
guard:
|
|
163
|
+
kind: shell
|
|
164
|
+
options:
|
|
165
|
+
command: 'test "$(echo "${vars.tasks}" | jq length)" -eq 0'
|
|
166
|
+
|
|
167
|
+
# ── linear body: doc-sync -> learning-capture -> metrics-record ──
|
|
168
|
+
- from: doc-sync
|
|
169
|
+
to: learning-capture
|
|
170
|
+
description: Doc sync complete — capture learnings.
|
|
171
|
+
guard:
|
|
172
|
+
kind: always
|
|
173
|
+
- from: learning-capture
|
|
174
|
+
to: metrics-record
|
|
175
|
+
description: Learnings captured — record metrics.
|
|
176
|
+
guard:
|
|
177
|
+
kind: always
|
|
178
|
+
|
|
179
|
+
# ── metrics-record: conditional routing based on feature and merge ──
|
|
180
|
+
# Declaration order matters: feature-transition is tried first (if feature is set),
|
|
181
|
+
# then branch-cleanup (if merge=true but no feature), then done (neither).
|
|
182
|
+
- from: metrics-record
|
|
183
|
+
to: feature-transition
|
|
184
|
+
description: Feature id is set — advance feature through legal lifecycle edges.
|
|
185
|
+
guard:
|
|
186
|
+
kind: shell
|
|
187
|
+
options:
|
|
188
|
+
command: 'test -n "${vars.feature}"'
|
|
189
|
+
- from: metrics-record
|
|
190
|
+
to: branch-cleanup
|
|
191
|
+
description: No feature id, but merge is requested — go to branch cleanup.
|
|
192
|
+
guard:
|
|
193
|
+
kind: shell
|
|
194
|
+
options:
|
|
195
|
+
command: 'test -z "${vars.feature}" && test "${vars.merge}" = true'
|
|
196
|
+
- from: metrics-record
|
|
197
|
+
to: done
|
|
198
|
+
description: No feature id and no merge — wrap-up is done.
|
|
199
|
+
guard:
|
|
200
|
+
kind: shell
|
|
201
|
+
options:
|
|
202
|
+
command: 'test -z "${vars.feature}" && test "${vars.merge}" != true'
|
|
203
|
+
|
|
204
|
+
# ── feature-transition: -> branch-cleanup (if merge) or done ──
|
|
205
|
+
- from: feature-transition
|
|
206
|
+
to: branch-cleanup
|
|
207
|
+
description: Feature advanced — merge is requested.
|
|
208
|
+
guard:
|
|
209
|
+
kind: shell
|
|
210
|
+
options:
|
|
211
|
+
command: 'test "${vars.merge}" = true'
|
|
212
|
+
- from: feature-transition
|
|
213
|
+
to: done
|
|
214
|
+
description: Feature advanced — no merge requested.
|
|
215
|
+
guard:
|
|
216
|
+
kind: shell
|
|
217
|
+
options:
|
|
218
|
+
command: 'test "${vars.merge}" != true'
|
|
219
|
+
|
|
220
|
+
# ── branch-cleanup: HITL confirmed -> done ──
|
|
221
|
+
- from: branch-cleanup
|
|
222
|
+
to: done
|
|
223
|
+
description: Branch cleanup confirmed — wrap-up complete.
|
|
224
|
+
guard:
|
|
225
|
+
kind: always
|
package/spur.js
CHANGED
|
@@ -58665,18 +58665,16 @@ var init_task_check = __esm(() => {
|
|
|
58665
58665
|
runL3(doc2, entry, findings) {
|
|
58666
58666
|
const reqBody = doc2.getSection("Requirements");
|
|
58667
58667
|
if (reqBody !== null && !isPlaceholderBody(reqBody)) {
|
|
58668
|
-
const
|
|
58669
|
-
`);
|
|
58668
|
+
const blocks = reqBody.trim().split(/\n\s*\n/).filter((b) => b.trim().length > 0);
|
|
58670
58669
|
let numbered = 0;
|
|
58671
|
-
|
|
58672
|
-
|
|
58673
|
-
|
|
58674
|
-
|
|
58675
|
-
|
|
58676
|
-
numbered++;
|
|
58670
|
+
for (const block of blocks) {
|
|
58671
|
+
const firstLine = block.trimStart().split(`
|
|
58672
|
+
`)[0] ?? "";
|
|
58673
|
+
if (/^\s*[-*]?\s*(?:\[[ xX]\]\s*)?R\d+\.?\s/.test(firstLine)) {
|
|
58674
|
+
numbered++;
|
|
58677
58675
|
}
|
|
58678
58676
|
}
|
|
58679
|
-
if (numbered === 0 || numbered <
|
|
58677
|
+
if (numbered === 0 || numbered < blocks.length * 0.5) {
|
|
58680
58678
|
findings.push({
|
|
58681
58679
|
layer: "L3",
|
|
58682
58680
|
severity: "warning",
|
|
@@ -59966,6 +59964,7 @@ var init_team_service = __esm(() => {
|
|
|
59966
59964
|
});
|
|
59967
59965
|
|
|
59968
59966
|
// ../../packages/app/src/workflow/actions/agent-run.ts
|
|
59967
|
+
import { existsSync as existsSync4 } from "fs";
|
|
59969
59968
|
import { mkdir, writeFile } from "fs/promises";
|
|
59970
59969
|
import { dirname as dirname9, isAbsolute as isAbsolute2, join as join10 } from "path";
|
|
59971
59970
|
|
|
@@ -60012,6 +60011,7 @@ class AgentRunActionRunner {
|
|
|
60012
60011
|
if (continueFlag !== undefined)
|
|
60013
60012
|
flags.continue = continueFlag;
|
|
60014
60013
|
const answerFile = asOptionalString(options.answerFile);
|
|
60014
|
+
const expectFile = asOptionalString(options.expectFile);
|
|
60015
60015
|
const capture = asOptionalBoolean(options.capture) || answerFile !== undefined;
|
|
60016
60016
|
const agentLabel = agent ?? "<default>";
|
|
60017
60017
|
if (capture) {
|
|
@@ -60022,6 +60022,16 @@ class AgentRunActionRunner {
|
|
|
60022
60022
|
await mkdir(dirname9(target), { recursive: true });
|
|
60023
60023
|
await writeFile(target, answer, "utf8");
|
|
60024
60024
|
}
|
|
60025
|
+
if (ok2 && expectFile !== undefined) {
|
|
60026
|
+
const target = isAbsolute2(expectFile) ? expectFile : join10(cwd, expectFile);
|
|
60027
|
+
if (!existsSync4(target)) {
|
|
60028
|
+
return {
|
|
60029
|
+
ok: false,
|
|
60030
|
+
data: { exitCode: exitCode2, agent: agentLabel, answer },
|
|
60031
|
+
error: `agent.run (${agentLabel}) exited 0 but expected file is absent: ${expectFile}`
|
|
60032
|
+
};
|
|
60033
|
+
}
|
|
60034
|
+
}
|
|
60025
60035
|
return {
|
|
60026
60036
|
ok: ok2,
|
|
60027
60037
|
data: { exitCode: exitCode2, agent: agentLabel, answer },
|
|
@@ -60031,6 +60041,16 @@ class AgentRunActionRunner {
|
|
|
60031
60041
|
}
|
|
60032
60042
|
const exitCode = await this.agentService.run(input, flags);
|
|
60033
60043
|
const ok = exitCode === 0;
|
|
60044
|
+
if (ok && expectFile !== undefined) {
|
|
60045
|
+
const target = isAbsolute2(expectFile) ? expectFile : join10(cwd, expectFile);
|
|
60046
|
+
if (!existsSync4(target)) {
|
|
60047
|
+
return {
|
|
60048
|
+
ok: false,
|
|
60049
|
+
data: { exitCode, agent: agentLabel },
|
|
60050
|
+
error: `agent.run (${agentLabel}) exited 0 but expected file is absent: ${expectFile}`
|
|
60051
|
+
};
|
|
60052
|
+
}
|
|
60053
|
+
}
|
|
60034
60054
|
return {
|
|
60035
60055
|
ok,
|
|
60036
60056
|
data: { exitCode, agent: agentLabel },
|
|
@@ -68685,7 +68705,7 @@ init_src3();
|
|
|
68685
68705
|
init_src3();
|
|
68686
68706
|
init_loader();
|
|
68687
68707
|
init_src2();
|
|
68688
|
-
import { existsSync as
|
|
68708
|
+
import { existsSync as existsSync5 } from "fs";
|
|
68689
68709
|
import { join as join12 } from "path";
|
|
68690
68710
|
|
|
68691
68711
|
// src/workflow/resolve-spur-bin.ts
|
|
@@ -68708,11 +68728,11 @@ function resolveWorkflowPath(context3, profile) {
|
|
|
68708
68728
|
const bundledRoot = bundledConfigRoot();
|
|
68709
68729
|
if (bundledRoot !== null) {
|
|
68710
68730
|
const bundledPath = join12(bundledRoot, "workflows", `${profile.workflowName}.yaml`);
|
|
68711
|
-
if (
|
|
68731
|
+
if (existsSync5(bundledPath))
|
|
68712
68732
|
return bundledPath;
|
|
68713
68733
|
}
|
|
68714
68734
|
const projectPath = join12(context3.cwd, ".spur", "workflows", `${profile.workflowName}.yaml`);
|
|
68715
|
-
if (
|
|
68735
|
+
if (existsSync5(projectPath))
|
|
68716
68736
|
return projectPath;
|
|
68717
68737
|
return null;
|
|
68718
68738
|
}
|
|
@@ -68967,7 +68987,7 @@ import { join as join13, resolve as resolve4 } from "path";
|
|
|
68967
68987
|
var CLI_CONFIG = {
|
|
68968
68988
|
binaryName: "spur",
|
|
68969
68989
|
binaryLabel: "spur",
|
|
68970
|
-
binaryVersion: "0.2.
|
|
68990
|
+
binaryVersion: "0.2.12",
|
|
68971
68991
|
configDir: ".spur",
|
|
68972
68992
|
configFile: ".spur/config.yaml",
|
|
68973
68993
|
databaseFile: ".spur/spur.db"
|
|
@@ -77672,7 +77692,7 @@ async function readTargetStatus(context3, targetPath) {
|
|
|
77672
77692
|
init_src3();
|
|
77673
77693
|
init_loader();
|
|
77674
77694
|
init_src2();
|
|
77675
|
-
import { existsSync as
|
|
77695
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
|
|
77676
77696
|
import { join as join18 } from "path";
|
|
77677
77697
|
// schemas/section-matrix.schema.json
|
|
77678
77698
|
var section_matrix_schema_default = {
|
|
@@ -78495,7 +78515,7 @@ function loadTemplateContent(projectRoot, variant) {
|
|
|
78495
78515
|
if (templateMissSet.has(variant))
|
|
78496
78516
|
return;
|
|
78497
78517
|
const localPath = join18(projectRoot, ".spur", "tasks", "templates", `${variant}.md`);
|
|
78498
|
-
if (
|
|
78518
|
+
if (existsSync6(localPath)) {
|
|
78499
78519
|
const content = readFileSync6(localPath, "utf8");
|
|
78500
78520
|
templateContentCache.set(variant, content);
|
|
78501
78521
|
return content;
|
|
@@ -78503,7 +78523,7 @@ function loadTemplateContent(projectRoot, variant) {
|
|
|
78503
78523
|
const root = bundledConfigRoot();
|
|
78504
78524
|
if (root !== null) {
|
|
78505
78525
|
const templatePath = join18(root, "templates", "task", `${variant}.md`);
|
|
78506
|
-
if (
|
|
78526
|
+
if (existsSync6(templatePath)) {
|
|
78507
78527
|
const content = readFileSync6(templatePath, "utf8");
|
|
78508
78528
|
templateContentCache.set(variant, content);
|
|
78509
78529
|
return content;
|
|
@@ -78519,13 +78539,13 @@ function loadTemplateBodies(projectRoot, variant) {
|
|
|
78519
78539
|
return cached2;
|
|
78520
78540
|
let bodies = {};
|
|
78521
78541
|
const localPath = join18(projectRoot, ".spur", "tasks", "templates", `${variant}.md`);
|
|
78522
|
-
if (
|
|
78542
|
+
if (existsSync6(localPath)) {
|
|
78523
78543
|
bodies = extractTemplateBodies(readFileSync6(localPath, "utf8"));
|
|
78524
78544
|
} else {
|
|
78525
78545
|
const root = bundledConfigRoot();
|
|
78526
78546
|
if (root !== null) {
|
|
78527
78547
|
const templatePath = join18(root, "templates", "task", `${variant}.md`);
|
|
78528
|
-
if (
|
|
78548
|
+
if (existsSync6(templatePath)) {
|
|
78529
78549
|
bodies = extractTemplateBodies(readFileSync6(templatePath, "utf8"));
|
|
78530
78550
|
}
|
|
78531
78551
|
}
|
|
@@ -78560,7 +78580,7 @@ async function loadSectionMatrix(projectRoot) {
|
|
|
78560
78580
|
}
|
|
78561
78581
|
async function loadSectionMatrixUncached(projectRoot) {
|
|
78562
78582
|
const localPath = join18(projectRoot, ".spur", "tasks", "section-matrix.yaml");
|
|
78563
|
-
if (
|
|
78583
|
+
if (existsSync6(localPath)) {
|
|
78564
78584
|
const data = await loadStructuredSpurConfig(localPath, {
|
|
78565
78585
|
validateJsonSchema: true,
|
|
78566
78586
|
embeddedSchemas: EMBEDDED_SPUR_SCHEMAS
|
|
@@ -78570,7 +78590,7 @@ async function loadSectionMatrixUncached(projectRoot) {
|
|
|
78570
78590
|
const root = bundledConfigRoot();
|
|
78571
78591
|
if (root !== null) {
|
|
78572
78592
|
const matrixPath = join18(root, "tasks", "section-matrix.yaml");
|
|
78573
|
-
if (
|
|
78593
|
+
if (existsSync6(matrixPath)) {
|
|
78574
78594
|
const data = await loadStructuredSpurConfig(matrixPath, {
|
|
78575
78595
|
validateJsonSchema: true,
|
|
78576
78596
|
embeddedSchemas: EMBEDDED_SPUR_SCHEMAS
|