@gobing-ai/spur 0.2.10 → 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/basic.yaml +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 +23 -4
- package/spur-cli/config/workflows/wrapup-pipeline.yaml +225 -0
- package/spur.js +194 -52
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",
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
$schema: "@gobing-ai/spur/schemas/state-machine-workflow.schema.json"
|
|
2
1
|
# Canonical basic workflow: implement -> check -> fix until the check passes or the
|
|
3
2
|
# iteration bound is exhausted. Authored for the @gobing-ai/ts-dual-workflow-engine
|
|
4
3
|
# state-machine schema (initialState / states[].id / onEnter / top-level transitions).
|
|
5
4
|
# See Architecture Section 5 and Design Section 3.3.
|
|
5
|
+
"$schema": "@gobing-ai/spur/schemas/state-machine-workflow.schema.json"
|
|
6
6
|
name: basic
|
|
7
7
|
kind: state-machine
|
|
8
8
|
description: The canonical implement-check-fix-until-pass loop
|
|
@@ -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).
|
|
@@ -97,10 +113,9 @@ states:
|
|
|
97
113
|
- id: approve
|
|
98
114
|
description: >
|
|
99
115
|
Human-in-the-loop approval gate. Under `profile=auto` this state is never entered
|
|
100
|
-
(review routes around it straight to verify).
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
full `spur workflow validate` stays green. The workspace schema already supports it.
|
|
116
|
+
(review routes around it straight to verify). In interactive mode this state pauses
|
|
117
|
+
the run for `spur workflow continue` (E3), making approval an explicit operator action.
|
|
118
|
+
pause: true
|
|
104
119
|
onEnter:
|
|
105
120
|
- kind: hitl.confirm
|
|
106
121
|
options:
|
|
@@ -148,6 +163,10 @@ states:
|
|
|
148
163
|
- kind: note
|
|
149
164
|
options:
|
|
150
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'
|
|
151
170
|
|
|
152
171
|
- id: failed
|
|
153
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
|
@@ -32493,7 +32493,40 @@ var init_template_renderer = () => {};
|
|
|
32493
32493
|
// ../../packages/config/src/loader.ts
|
|
32494
32494
|
import { existsSync as existsSync3 } from "fs";
|
|
32495
32495
|
import { homedir } from "os";
|
|
32496
|
-
import { join as join2 } from "path";
|
|
32496
|
+
import { dirname as dirname3, join as join2 } from "path";
|
|
32497
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
32498
|
+
function embeddedSchemasId(embeddedSchemas) {
|
|
32499
|
+
if (embeddedSchemas === undefined)
|
|
32500
|
+
return "disk";
|
|
32501
|
+
let id = embeddedSchemasIds.get(embeddedSchemas);
|
|
32502
|
+
if (id === undefined) {
|
|
32503
|
+
id = nextEmbeddedSchemasId;
|
|
32504
|
+
nextEmbeddedSchemasId += 1;
|
|
32505
|
+
embeddedSchemasIds.set(embeddedSchemas, id);
|
|
32506
|
+
}
|
|
32507
|
+
return `embedded:${id}`;
|
|
32508
|
+
}
|
|
32509
|
+
function cacheKey(configPath, opts, validateJsonSchema2) {
|
|
32510
|
+
return [
|
|
32511
|
+
configPath,
|
|
32512
|
+
validateJsonSchema2 ? "schema" : "zod",
|
|
32513
|
+
opts?.schemaManifestSpecifier ?? "@gobing-ai/spur/package.json",
|
|
32514
|
+
embeddedSchemasId(opts?.embeddedSchemas)
|
|
32515
|
+
].join("\x00");
|
|
32516
|
+
}
|
|
32517
|
+
function resolveSchemaSpecifier(specifier, manifestSpecifier) {
|
|
32518
|
+
if (specifier === manifestSpecifier) {
|
|
32519
|
+
const workspaceManifest = join2(LOADER_DIR, "..", "..", "..", "apps", "cli", "package.json");
|
|
32520
|
+
if (existsSync3(workspaceManifest))
|
|
32521
|
+
return workspaceManifest;
|
|
32522
|
+
}
|
|
32523
|
+
try {
|
|
32524
|
+
const resolved = import.meta.resolve(specifier);
|
|
32525
|
+
return resolved.startsWith("file:") ? fileURLToPath3(resolved) : resolved;
|
|
32526
|
+
} catch {
|
|
32527
|
+
return specifier;
|
|
32528
|
+
}
|
|
32529
|
+
}
|
|
32497
32530
|
function resolveConfigFile(cwd) {
|
|
32498
32531
|
const projectConfig = join2(cwd ?? process.cwd(), SPUR_CONFIG_DIR, SPUR_CONFIG_FILE);
|
|
32499
32532
|
if (existsSync3(projectConfig))
|
|
@@ -32518,11 +32551,21 @@ function makeEmbeddedReader(embeddedSchemas) {
|
|
|
32518
32551
|
};
|
|
32519
32552
|
}
|
|
32520
32553
|
async function loadSpurConfig(cwd = process.cwd(), opts) {
|
|
32521
|
-
const configPath =
|
|
32522
|
-
if (
|
|
32554
|
+
const configPath = resolveConfigFile(cwd);
|
|
32555
|
+
if (configPath === undefined) {
|
|
32523
32556
|
return spurConfigSchema.parse({});
|
|
32524
32557
|
}
|
|
32525
32558
|
const validateJsonSchema2 = opts?.validateJsonSchema ?? true;
|
|
32559
|
+
const key = cacheKey(configPath, opts, validateJsonSchema2);
|
|
32560
|
+
const cached2 = spurConfigCache.get(key);
|
|
32561
|
+
if (cached2 !== undefined)
|
|
32562
|
+
return cached2;
|
|
32563
|
+
const promise2 = loadSpurConfigFile(configPath, opts, validateJsonSchema2);
|
|
32564
|
+
spurConfigCache.set(key, promise2);
|
|
32565
|
+
promise2.catch(() => spurConfigCache.delete(key));
|
|
32566
|
+
return promise2;
|
|
32567
|
+
}
|
|
32568
|
+
async function loadSpurConfigFile(configPath, opts, validateJsonSchema2) {
|
|
32526
32569
|
let raw;
|
|
32527
32570
|
if (validateJsonSchema2) {
|
|
32528
32571
|
const embeddedSchemas = opts?.embeddedSchemas;
|
|
@@ -32531,7 +32574,7 @@ async function loadSpurConfig(cwd = process.cwd(), opts) {
|
|
|
32531
32574
|
if (embeddedSchemas !== undefined && specifier === manifestSpecifier) {
|
|
32532
32575
|
return `${EMBEDDED_PREFIX}/package.json`;
|
|
32533
32576
|
}
|
|
32534
|
-
return specifier;
|
|
32577
|
+
return resolveSchemaSpecifier(specifier, manifestSpecifier);
|
|
32535
32578
|
};
|
|
32536
32579
|
const fileSystem = embeddedSchemas ? { readFile: makeEmbeddedReader(embeddedSchemas) } : undefined;
|
|
32537
32580
|
raw = await loadStructuredConfig(configPath, {
|
|
@@ -32561,7 +32604,7 @@ async function loadStructuredSpurConfig(configPath, opts) {
|
|
|
32561
32604
|
if (embeddedSchemas !== undefined && specifier === manifestSpecifier) {
|
|
32562
32605
|
return `${EMBEDDED_PREFIX}/package.json`;
|
|
32563
32606
|
}
|
|
32564
|
-
return specifier;
|
|
32607
|
+
return resolveSchemaSpecifier(specifier, manifestSpecifier);
|
|
32565
32608
|
};
|
|
32566
32609
|
const fileSystem = embeddedSchemas ? { readFile: makeEmbeddedReader(embeddedSchemas) } : undefined;
|
|
32567
32610
|
return await loadStructuredConfig(configPath, {
|
|
@@ -32584,6 +32627,14 @@ function defaultPlanningFolders() {
|
|
|
32584
32627
|
};
|
|
32585
32628
|
}
|
|
32586
32629
|
async function resolvePlanningFolders(fs) {
|
|
32630
|
+
const cached2 = planningFoldersCache.get(fs);
|
|
32631
|
+
if (cached2 !== undefined)
|
|
32632
|
+
return cached2;
|
|
32633
|
+
const promise2 = resolvePlanningFoldersUncached(fs);
|
|
32634
|
+
planningFoldersCache.set(fs, promise2);
|
|
32635
|
+
return promise2;
|
|
32636
|
+
}
|
|
32637
|
+
async function resolvePlanningFoldersUncached(fs) {
|
|
32587
32638
|
const configPath = fs.resolve(join2(SPUR_CONFIG_DIR, SPUR_CONFIG_FILE));
|
|
32588
32639
|
if (!await fs.exists(configPath))
|
|
32589
32640
|
return defaultPlanningFolders();
|
|
@@ -32614,7 +32665,7 @@ async function resolvePlanningFolders(fs) {
|
|
|
32614
32665
|
return defaultPlanningFolders();
|
|
32615
32666
|
}
|
|
32616
32667
|
}
|
|
32617
|
-
var SPUR_CONFIG_DIR = ".spur", SPUR_CONFIG_FILE = "config.yaml", GLOBAL_CONFIG_FILE, EMBEDDED_PREFIX = "\x00embedded-spur";
|
|
32668
|
+
var SPUR_CONFIG_DIR = ".spur", SPUR_CONFIG_FILE = "config.yaml", GLOBAL_CONFIG_FILE, LOADER_DIR, nextEmbeddedSchemasId = 1, embeddedSchemasIds, spurConfigCache, planningFoldersCache, EMBEDDED_PREFIX = "\x00embedded-spur";
|
|
32618
32669
|
var init_loader = __esm(() => {
|
|
32619
32670
|
init_dist3();
|
|
32620
32671
|
init_dist2();
|
|
@@ -32622,6 +32673,10 @@ var init_loader = __esm(() => {
|
|
|
32622
32673
|
init_bundled_config();
|
|
32623
32674
|
init_template_renderer();
|
|
32624
32675
|
GLOBAL_CONFIG_FILE = join2(homedir(), ".config", "spur", "config.yaml");
|
|
32676
|
+
LOADER_DIR = dirname3(fileURLToPath3(import.meta.url));
|
|
32677
|
+
embeddedSchemasIds = new WeakMap;
|
|
32678
|
+
spurConfigCache = new Map;
|
|
32679
|
+
planningFoldersCache = new WeakMap;
|
|
32625
32680
|
});
|
|
32626
32681
|
|
|
32627
32682
|
// ../../node_modules/.bun/@gobing-ai+ts-runtime@0.4.2/node_modules/@gobing-ai/ts-runtime/dist/bun-sqlite.js
|
|
@@ -55614,7 +55669,7 @@ var init_db2 = __esm(() => {
|
|
|
55614
55669
|
|
|
55615
55670
|
// ../../packages/domain/src/planning/locks.ts
|
|
55616
55671
|
import { closeSync, fsyncSync, openSync, renameSync as renameSync2 } from "fs";
|
|
55617
|
-
import { dirname as
|
|
55672
|
+
import { dirname as dirname6, join as join5 } from "path";
|
|
55618
55673
|
function lockPathForEntity(folder, entityId) {
|
|
55619
55674
|
return join5(folder, `.${entityId}.lock`);
|
|
55620
55675
|
}
|
|
@@ -55701,7 +55756,7 @@ function fsyncPath(path8, swallowErrors = false) {
|
|
|
55701
55756
|
}
|
|
55702
55757
|
}
|
|
55703
55758
|
function atomicWrite(dest, content, entityId, fs3, projectName = "spur") {
|
|
55704
|
-
const dir =
|
|
55759
|
+
const dir = dirname6(dest);
|
|
55705
55760
|
const rand = Math.random().toString(36).slice(2, 10);
|
|
55706
55761
|
const tempPath = join5(dir, `.${projectName}.${entityId}.${rand}.tmp`);
|
|
55707
55762
|
fs3.writeFile(tempPath, content);
|
|
@@ -55710,7 +55765,7 @@ function atomicWrite(dest, content, entityId, fs3, projectName = "spur") {
|
|
|
55710
55765
|
fsyncPath(dir, true);
|
|
55711
55766
|
}
|
|
55712
55767
|
async function atomicWriteAsync(dest, content, entityId, fs3, projectName = "spur") {
|
|
55713
|
-
const dir =
|
|
55768
|
+
const dir = dirname6(dest);
|
|
55714
55769
|
const rand = Math.random().toString(36).slice(2, 10);
|
|
55715
55770
|
const tempPath = join5(dir, `.${projectName}.${entityId}.${rand}.tmp`);
|
|
55716
55771
|
await fs3.writeFile(tempPath, content);
|
|
@@ -55774,6 +55829,19 @@ function findHeadings(body, hashes) {
|
|
|
55774
55829
|
}
|
|
55775
55830
|
return hits;
|
|
55776
55831
|
}
|
|
55832
|
+
function normalizeYamlScalars(data) {
|
|
55833
|
+
const out = {};
|
|
55834
|
+
for (const [key, value] of Object.entries(data)) {
|
|
55835
|
+
if (value instanceof Date) {
|
|
55836
|
+
out[key] = value.toISOString();
|
|
55837
|
+
} else if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
55838
|
+
out[key] = normalizeYamlScalars(value);
|
|
55839
|
+
} else {
|
|
55840
|
+
out[key] = value;
|
|
55841
|
+
}
|
|
55842
|
+
}
|
|
55843
|
+
return out;
|
|
55844
|
+
}
|
|
55777
55845
|
|
|
55778
55846
|
class MarkdownDocument {
|
|
55779
55847
|
_domain;
|
|
@@ -55804,7 +55872,7 @@ class MarkdownDocument {
|
|
|
55804
55872
|
try {
|
|
55805
55873
|
const parsed = $parse(raw);
|
|
55806
55874
|
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
55807
|
-
data = parsed;
|
|
55875
|
+
data = normalizeYamlScalars(parsed);
|
|
55808
55876
|
}
|
|
55809
55877
|
} catch {}
|
|
55810
55878
|
frontmatter = { raw, data };
|
|
@@ -55971,8 +56039,19 @@ ${body.trimEnd()}
|
|
|
55971
56039
|
${content}
|
|
55972
56040
|
${END_MARKER}${after}`;
|
|
55973
56041
|
}
|
|
56042
|
+
yamlSafeValue(value) {
|
|
56043
|
+
if (value.startsWith('"') && value.endsWith('"'))
|
|
56044
|
+
return value;
|
|
56045
|
+
if (/[:{}[\],&*?|>!%@`#"'\\\n\r]/.test(value))
|
|
56046
|
+
return `"${value}"`;
|
|
56047
|
+
if (/^(null|true|false|yes|no|on|off)$/i.test(value))
|
|
56048
|
+
return `"${value}"`;
|
|
56049
|
+
if (/^\d/.test(value) && !/^\d{4}-\d{2}-\d{2}/.test(value))
|
|
56050
|
+
return `"${value}"`;
|
|
56051
|
+
return value;
|
|
56052
|
+
}
|
|
55974
56053
|
setFrontmatterField(key, value) {
|
|
55975
|
-
const fieldLine = `${key}: ${value}`;
|
|
56054
|
+
const fieldLine = `${key}: ${this.yamlSafeValue(value)}`;
|
|
55976
56055
|
if (this._frontmatter === null) {
|
|
55977
56056
|
const raw2 = fieldLine;
|
|
55978
56057
|
this._frontmatter = { raw: raw2, data: { [key]: value } };
|
|
@@ -58476,7 +58555,7 @@ var init_rule_service = __esm(() => {
|
|
|
58476
58555
|
});
|
|
58477
58556
|
|
|
58478
58557
|
// ../../packages/app/src/services/task-check.ts
|
|
58479
|
-
import { dirname as
|
|
58558
|
+
import { dirname as dirname7, join as join8 } from "path";
|
|
58480
58559
|
function isPlaceholderBody(body) {
|
|
58481
58560
|
const stripped = body.replace(/<!--[\s\S]*?-->/g, "").replace(/^\s*>\s*TBD\s*$/gim, "").trim();
|
|
58482
58561
|
return stripped.length === 0;
|
|
@@ -58526,6 +58605,29 @@ function isProseOnlyReview(body) {
|
|
|
58526
58605
|
return !stripped.split(`
|
|
58527
58606
|
`).some((l) => l.includes("|"));
|
|
58528
58607
|
}
|
|
58608
|
+
function hasAdjacentFileLineColumns(body) {
|
|
58609
|
+
const lines = body.split(`
|
|
58610
|
+
`);
|
|
58611
|
+
for (const line of lines) {
|
|
58612
|
+
if (!line.includes("|"))
|
|
58613
|
+
continue;
|
|
58614
|
+
const cells = line.split("|").map((c3) => c3.trim());
|
|
58615
|
+
if (cells.length < 3)
|
|
58616
|
+
continue;
|
|
58617
|
+
for (let i2 = 0;i2 < cells.length - 1; i2++) {
|
|
58618
|
+
const a2 = cells[i2];
|
|
58619
|
+
const b = cells[i2 + 1];
|
|
58620
|
+
if (a2 === undefined || b === undefined)
|
|
58621
|
+
continue;
|
|
58622
|
+
const fileText = a2.replace(/`/g, "");
|
|
58623
|
+
const fileCell = /\.(tsx?|jsx?|mjs|cjs|md|ya?ml|json|css|html|sql|sh|toml)$/i.test(fileText);
|
|
58624
|
+
const lineCell = /^\d+(-\d+)?$/.test(b);
|
|
58625
|
+
if (fileCell && lineCell)
|
|
58626
|
+
return true;
|
|
58627
|
+
}
|
|
58628
|
+
}
|
|
58629
|
+
return false;
|
|
58630
|
+
}
|
|
58529
58631
|
var TaskCheckService;
|
|
58530
58632
|
var init_task_check = __esm(() => {
|
|
58531
58633
|
init_src2();
|
|
@@ -58554,8 +58656,8 @@ var init_task_check = __esm(() => {
|
|
|
58554
58656
|
const entry = this.resolveMatrixEntry(variant, status);
|
|
58555
58657
|
this.runL2(doc2, entry, findings);
|
|
58556
58658
|
this.runL3(doc2, entry, findings);
|
|
58557
|
-
const tasksDir =
|
|
58558
|
-
const featuresDir = join8(
|
|
58659
|
+
const tasksDir = dirname7(filePath);
|
|
58660
|
+
const featuresDir = join8(dirname7(tasksDir), "features");
|
|
58559
58661
|
await this.runL4(doc2, fm, findings, featuresDir, tasksDir);
|
|
58560
58662
|
await this.runL4Rollup(doc2, wbs, status, findings, tasksDir);
|
|
58561
58663
|
return { wbs, ...this.summarizeWithStatus(status, findings, strict) };
|
|
@@ -58563,18 +58665,16 @@ var init_task_check = __esm(() => {
|
|
|
58563
58665
|
runL3(doc2, entry, findings) {
|
|
58564
58666
|
const reqBody = doc2.getSection("Requirements");
|
|
58565
58667
|
if (reqBody !== null && !isPlaceholderBody(reqBody)) {
|
|
58566
|
-
const
|
|
58567
|
-
`);
|
|
58668
|
+
const blocks = reqBody.trim().split(/\n\s*\n/).filter((b) => b.trim().length > 0);
|
|
58568
58669
|
let numbered = 0;
|
|
58569
|
-
|
|
58570
|
-
|
|
58571
|
-
|
|
58572
|
-
|
|
58573
|
-
|
|
58574
|
-
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++;
|
|
58575
58675
|
}
|
|
58576
58676
|
}
|
|
58577
|
-
if (numbered === 0 || numbered <
|
|
58677
|
+
if (numbered === 0 || numbered < blocks.length * 0.5) {
|
|
58578
58678
|
findings.push({
|
|
58579
58679
|
layer: "L3",
|
|
58580
58680
|
severity: "warning",
|
|
@@ -58586,7 +58686,8 @@ var init_task_check = __esm(() => {
|
|
|
58586
58686
|
const solBody = doc2.getSection("Solution");
|
|
58587
58687
|
if (solBody !== null && !isPlaceholderBody(solBody)) {
|
|
58588
58688
|
const hasFileLine = /`[^`]+?:\d+(-\d+)?`/.test(solBody) || /[^\s`]\.\w+:\d+/.test(solBody);
|
|
58589
|
-
|
|
58689
|
+
const hasTableFileLine = hasFileLine || hasAdjacentFileLineColumns(solBody);
|
|
58690
|
+
if (!hasTableFileLine) {
|
|
58590
58691
|
findings.push({
|
|
58591
58692
|
layer: "L3",
|
|
58592
58693
|
severity: "error",
|
|
@@ -58997,7 +59098,7 @@ function gitDiffU0(cwd) {
|
|
|
58997
59098
|
var init_task_record = () => {};
|
|
58998
59099
|
|
|
58999
59100
|
// ../../packages/app/src/services/task-service.ts
|
|
59000
|
-
import { dirname as
|
|
59101
|
+
import { dirname as dirname8, isAbsolute, relative as relative2, resolve as resolve2 } from "path";
|
|
59001
59102
|
function patchFrontmatterField(rendered, key, value) {
|
|
59002
59103
|
const existingRe = new RegExp(`^(?=\\s*${escapeRegex2(key)}:)`, "m");
|
|
59003
59104
|
if (existingRe.test(rendered)) {
|
|
@@ -59590,7 +59691,7 @@ ${block}` : block);
|
|
|
59590
59691
|
resolveListDir(folder) {
|
|
59591
59692
|
if (folder === undefined)
|
|
59592
59693
|
return this.ctx.tasksDir;
|
|
59593
|
-
const root = resolve2(
|
|
59694
|
+
const root = resolve2(dirname8(this.ctx.tasksDir));
|
|
59594
59695
|
const candidate = resolve2(root, folder);
|
|
59595
59696
|
const rel = relative2(root, candidate);
|
|
59596
59697
|
if (rel.startsWith("..") || isAbsolute(rel)) {
|
|
@@ -59863,8 +59964,9 @@ var init_team_service = __esm(() => {
|
|
|
59863
59964
|
});
|
|
59864
59965
|
|
|
59865
59966
|
// ../../packages/app/src/workflow/actions/agent-run.ts
|
|
59967
|
+
import { existsSync as existsSync4 } from "fs";
|
|
59866
59968
|
import { mkdir, writeFile } from "fs/promises";
|
|
59867
|
-
import { dirname as
|
|
59969
|
+
import { dirname as dirname9, isAbsolute as isAbsolute2, join as join10 } from "path";
|
|
59868
59970
|
|
|
59869
59971
|
class AgentRunActionRunner {
|
|
59870
59972
|
kind = KIND;
|
|
@@ -59909,6 +60011,7 @@ class AgentRunActionRunner {
|
|
|
59909
60011
|
if (continueFlag !== undefined)
|
|
59910
60012
|
flags.continue = continueFlag;
|
|
59911
60013
|
const answerFile = asOptionalString(options.answerFile);
|
|
60014
|
+
const expectFile = asOptionalString(options.expectFile);
|
|
59912
60015
|
const capture = asOptionalBoolean(options.capture) || answerFile !== undefined;
|
|
59913
60016
|
const agentLabel = agent ?? "<default>";
|
|
59914
60017
|
if (capture) {
|
|
@@ -59916,9 +60019,19 @@ class AgentRunActionRunner {
|
|
|
59916
60019
|
const ok2 = exitCode2 === 0;
|
|
59917
60020
|
if (answerFile !== undefined) {
|
|
59918
60021
|
const target = isAbsolute2(answerFile) ? answerFile : join10(cwd, answerFile);
|
|
59919
|
-
await mkdir(
|
|
60022
|
+
await mkdir(dirname9(target), { recursive: true });
|
|
59920
60023
|
await writeFile(target, answer, "utf8");
|
|
59921
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
|
+
}
|
|
59922
60035
|
return {
|
|
59923
60036
|
ok: ok2,
|
|
59924
60037
|
data: { exitCode: exitCode2, agent: agentLabel, answer },
|
|
@@ -59928,6 +60041,16 @@ class AgentRunActionRunner {
|
|
|
59928
60041
|
}
|
|
59929
60042
|
const exitCode = await this.agentService.run(input, flags);
|
|
59930
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
|
+
}
|
|
59931
60054
|
return {
|
|
59932
60055
|
ok,
|
|
59933
60056
|
data: { exitCode, agent: agentLabel },
|
|
@@ -61232,7 +61355,7 @@ init_loader();
|
|
|
61232
61355
|
init_dist4();
|
|
61233
61356
|
init_dist3();
|
|
61234
61357
|
import { appendFileSync as appendFileSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync4 } from "fs";
|
|
61235
|
-
import { dirname as
|
|
61358
|
+
import { dirname as dirname4 } from "path";
|
|
61236
61359
|
|
|
61237
61360
|
// ../../node_modules/.bun/@gobing-ai+ts-infra@0.4.2+be3e9d72efacb189/node_modules/@gobing-ai/ts-infra/dist/application/index.js
|
|
61238
61361
|
init_default_observers();
|
|
@@ -64634,7 +64757,7 @@ function validateAppConfig(validator, raw, section, filePath) {
|
|
|
64634
64757
|
function createFileSink(filePath) {
|
|
64635
64758
|
return (line) => {
|
|
64636
64759
|
try {
|
|
64637
|
-
mkdirSync2(
|
|
64760
|
+
mkdirSync2(dirname4(filePath), { recursive: true });
|
|
64638
64761
|
appendFileSync3(filePath, line);
|
|
64639
64762
|
} catch {}
|
|
64640
64763
|
};
|
|
@@ -66052,8 +66175,8 @@ var figlet = (() => {
|
|
|
66052
66175
|
})();
|
|
66053
66176
|
|
|
66054
66177
|
// ../../node_modules/.bun/figlet@1.11.0/node_modules/figlet/dist/node-figlet.mjs
|
|
66055
|
-
import { fileURLToPath as
|
|
66056
|
-
var __filename2 =
|
|
66178
|
+
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
66179
|
+
var __filename2 = fileURLToPath4(import.meta.url);
|
|
66057
66180
|
var __dirname2 = path7.dirname(__filename2);
|
|
66058
66181
|
var fontPath = path7.join(__dirname2, "/../fonts/");
|
|
66059
66182
|
var nodeFiglet = figlet;
|
|
@@ -68582,7 +68705,7 @@ init_src3();
|
|
|
68582
68705
|
init_src3();
|
|
68583
68706
|
init_loader();
|
|
68584
68707
|
init_src2();
|
|
68585
|
-
import { existsSync as
|
|
68708
|
+
import { existsSync as existsSync5 } from "fs";
|
|
68586
68709
|
import { join as join12 } from "path";
|
|
68587
68710
|
|
|
68588
68711
|
// src/workflow/resolve-spur-bin.ts
|
|
@@ -68605,11 +68728,11 @@ function resolveWorkflowPath(context3, profile) {
|
|
|
68605
68728
|
const bundledRoot = bundledConfigRoot();
|
|
68606
68729
|
if (bundledRoot !== null) {
|
|
68607
68730
|
const bundledPath = join12(bundledRoot, "workflows", `${profile.workflowName}.yaml`);
|
|
68608
|
-
if (
|
|
68731
|
+
if (existsSync5(bundledPath))
|
|
68609
68732
|
return bundledPath;
|
|
68610
68733
|
}
|
|
68611
68734
|
const projectPath = join12(context3.cwd, ".spur", "workflows", `${profile.workflowName}.yaml`);
|
|
68612
|
-
if (
|
|
68735
|
+
if (existsSync5(projectPath))
|
|
68613
68736
|
return projectPath;
|
|
68614
68737
|
return null;
|
|
68615
68738
|
}
|
|
@@ -68864,7 +68987,7 @@ import { join as join13, resolve as resolve4 } from "path";
|
|
|
68864
68987
|
var CLI_CONFIG = {
|
|
68865
68988
|
binaryName: "spur",
|
|
68866
68989
|
binaryLabel: "spur",
|
|
68867
|
-
binaryVersion: "0.2.
|
|
68990
|
+
binaryVersion: "0.2.12",
|
|
68868
68991
|
configDir: ".spur",
|
|
68869
68992
|
configFile: ".spur/config.yaml",
|
|
68870
68993
|
databaseFile: ".spur/spur.db"
|
|
@@ -69394,7 +69517,7 @@ init_src();
|
|
|
69394
69517
|
|
|
69395
69518
|
// ../server/src/serve.ts
|
|
69396
69519
|
init_src3();
|
|
69397
|
-
import { dirname as
|
|
69520
|
+
import { dirname as dirname10, isAbsolute as isAbsolute3, join as join16 } from "path";
|
|
69398
69521
|
init_dist3();
|
|
69399
69522
|
|
|
69400
69523
|
// ../server/src/bootstrap.ts
|
|
@@ -72635,15 +72758,15 @@ var getPattern = (label, next) => {
|
|
|
72635
72758
|
}
|
|
72636
72759
|
const match = label.match(/^\:([^\{\}]+)(?:\{(.+)\})?$/);
|
|
72637
72760
|
if (match) {
|
|
72638
|
-
const
|
|
72639
|
-
if (!patternCache[
|
|
72761
|
+
const cacheKey2 = `${label}#${next}`;
|
|
72762
|
+
if (!patternCache[cacheKey2]) {
|
|
72640
72763
|
if (match[2]) {
|
|
72641
|
-
patternCache[
|
|
72764
|
+
patternCache[cacheKey2] = next && next[0] !== ":" && next[0] !== "*" ? [cacheKey2, match[1], new RegExp(`^${match[2]}(?=/${next})`)] : [label, match[1], new RegExp(`^${match[2]}$`)];
|
|
72642
72765
|
} else {
|
|
72643
|
-
patternCache[
|
|
72766
|
+
patternCache[cacheKey2] = [label, match[1], true];
|
|
72644
72767
|
}
|
|
72645
72768
|
}
|
|
72646
|
-
return patternCache[
|
|
72769
|
+
return patternCache[cacheKey2];
|
|
72647
72770
|
}
|
|
72648
72771
|
return null;
|
|
72649
72772
|
};
|
|
@@ -77367,7 +77490,7 @@ var defaultDeps = {
|
|
|
77367
77490
|
async function resolveWebDistPath(configuredPath) {
|
|
77368
77491
|
const candidates = configuredPath && configuredPath.trim() !== "" ? [isAbsolute3(configuredPath) ? configuredPath : join16(process.cwd(), configuredPath)] : [
|
|
77369
77492
|
join16(process.cwd(), "dist/web"),
|
|
77370
|
-
join16(
|
|
77493
|
+
join16(dirname10(process.execPath), "../web"),
|
|
77371
77494
|
join16(import.meta.dir, "../../../dist/web")
|
|
77372
77495
|
];
|
|
77373
77496
|
for (const candidate of candidates) {
|
|
@@ -77569,7 +77692,7 @@ async function readTargetStatus(context3, targetPath) {
|
|
|
77569
77692
|
init_src3();
|
|
77570
77693
|
init_loader();
|
|
77571
77694
|
init_src2();
|
|
77572
|
-
import { existsSync as
|
|
77695
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
|
|
77573
77696
|
import { join as join18 } from "path";
|
|
77574
77697
|
// schemas/section-matrix.schema.json
|
|
77575
77698
|
var section_matrix_schema_default = {
|
|
@@ -78135,6 +78258,15 @@ ${result.content}`);
|
|
|
78135
78258
|
} else {
|
|
78136
78259
|
context3.output.write(`${result.ref.id}: ${result.fromStatus} \u2192 ${result.toStatus}`);
|
|
78137
78260
|
}
|
|
78261
|
+
if (status === "done" && !options.json) {
|
|
78262
|
+
const task2 = await svc.show(wbs);
|
|
78263
|
+
const featureId2 = task2.frontmatter.feature_id;
|
|
78264
|
+
if (!featureId2 || featureId2.length === 0) {
|
|
78265
|
+
context3.output.write(`\u24D8 Task ${wbs} is now done but has no feature_id.
|
|
78266
|
+
` + ` Link it with: spur task update ${wbs} --feature <id>
|
|
78267
|
+
` + " (Advisory only \u2014 the task is already done.)");
|
|
78268
|
+
}
|
|
78269
|
+
}
|
|
78138
78270
|
} else {
|
|
78139
78271
|
context3.output.error("Either <status>, --section/--from-file, or --feature/--priority is required");
|
|
78140
78272
|
context3.setExitCode(2);
|
|
@@ -78383,7 +78515,7 @@ function loadTemplateContent(projectRoot, variant) {
|
|
|
78383
78515
|
if (templateMissSet.has(variant))
|
|
78384
78516
|
return;
|
|
78385
78517
|
const localPath = join18(projectRoot, ".spur", "tasks", "templates", `${variant}.md`);
|
|
78386
|
-
if (
|
|
78518
|
+
if (existsSync6(localPath)) {
|
|
78387
78519
|
const content = readFileSync6(localPath, "utf8");
|
|
78388
78520
|
templateContentCache.set(variant, content);
|
|
78389
78521
|
return content;
|
|
@@ -78391,7 +78523,7 @@ function loadTemplateContent(projectRoot, variant) {
|
|
|
78391
78523
|
const root = bundledConfigRoot();
|
|
78392
78524
|
if (root !== null) {
|
|
78393
78525
|
const templatePath = join18(root, "templates", "task", `${variant}.md`);
|
|
78394
|
-
if (
|
|
78526
|
+
if (existsSync6(templatePath)) {
|
|
78395
78527
|
const content = readFileSync6(templatePath, "utf8");
|
|
78396
78528
|
templateContentCache.set(variant, content);
|
|
78397
78529
|
return content;
|
|
@@ -78407,13 +78539,13 @@ function loadTemplateBodies(projectRoot, variant) {
|
|
|
78407
78539
|
return cached2;
|
|
78408
78540
|
let bodies = {};
|
|
78409
78541
|
const localPath = join18(projectRoot, ".spur", "tasks", "templates", `${variant}.md`);
|
|
78410
|
-
if (
|
|
78542
|
+
if (existsSync6(localPath)) {
|
|
78411
78543
|
bodies = extractTemplateBodies(readFileSync6(localPath, "utf8"));
|
|
78412
78544
|
} else {
|
|
78413
78545
|
const root = bundledConfigRoot();
|
|
78414
78546
|
if (root !== null) {
|
|
78415
78547
|
const templatePath = join18(root, "templates", "task", `${variant}.md`);
|
|
78416
|
-
if (
|
|
78548
|
+
if (existsSync6(templatePath)) {
|
|
78417
78549
|
bodies = extractTemplateBodies(readFileSync6(templatePath, "utf8"));
|
|
78418
78550
|
}
|
|
78419
78551
|
}
|
|
@@ -78436,9 +78568,19 @@ async function runDoneGateCheck(context3, wbs, folderOverride) {
|
|
|
78436
78568
|
const result = await svc.check(`${tasksDir}/${fileName}`, wbs, { strict: false });
|
|
78437
78569
|
return result.pass;
|
|
78438
78570
|
}
|
|
78571
|
+
var sectionMatrixCache = new Map;
|
|
78439
78572
|
async function loadSectionMatrix(projectRoot) {
|
|
78573
|
+
const cached2 = sectionMatrixCache.get(projectRoot);
|
|
78574
|
+
if (cached2 !== undefined)
|
|
78575
|
+
return cached2;
|
|
78576
|
+
const promise2 = loadSectionMatrixUncached(projectRoot);
|
|
78577
|
+
sectionMatrixCache.set(projectRoot, promise2);
|
|
78578
|
+
promise2.catch(() => sectionMatrixCache.delete(projectRoot));
|
|
78579
|
+
return promise2;
|
|
78580
|
+
}
|
|
78581
|
+
async function loadSectionMatrixUncached(projectRoot) {
|
|
78440
78582
|
const localPath = join18(projectRoot, ".spur", "tasks", "section-matrix.yaml");
|
|
78441
|
-
if (
|
|
78583
|
+
if (existsSync6(localPath)) {
|
|
78442
78584
|
const data = await loadStructuredSpurConfig(localPath, {
|
|
78443
78585
|
validateJsonSchema: true,
|
|
78444
78586
|
embeddedSchemas: EMBEDDED_SPUR_SCHEMAS
|
|
@@ -78448,7 +78590,7 @@ async function loadSectionMatrix(projectRoot) {
|
|
|
78448
78590
|
const root = bundledConfigRoot();
|
|
78449
78591
|
if (root !== null) {
|
|
78450
78592
|
const matrixPath = join18(root, "tasks", "section-matrix.yaml");
|
|
78451
|
-
if (
|
|
78593
|
+
if (existsSync6(matrixPath)) {
|
|
78452
78594
|
const data = await loadStructuredSpurConfig(matrixPath, {
|
|
78453
78595
|
validateJsonSchema: true,
|
|
78454
78596
|
embeddedSchemas: EMBEDDED_SPUR_SCHEMAS
|
|
@@ -78826,7 +78968,7 @@ init_src3();
|
|
|
78826
78968
|
init_src();
|
|
78827
78969
|
init_src2();
|
|
78828
78970
|
init_dist3();
|
|
78829
|
-
import { dirname as
|
|
78971
|
+
import { dirname as dirname11, join as join19, resolve as resolve6 } from "path";
|
|
78830
78972
|
import { isatty as isatty3 } from "tty";
|
|
78831
78973
|
|
|
78832
78974
|
// ../../node_modules/.bun/@clack+core@1.4.1/node_modules/@clack/core/dist/index.mjs
|
|
@@ -80238,7 +80380,7 @@ async function createMigratedDbAdapter(cwd = process.cwd(), env = process.env, d
|
|
|
80238
80380
|
const configuredUrl = env.DATABASE_URL === undefined ? join19(cwd, CLI_CONFIG.databaseFile) : config3.database.url;
|
|
80239
80381
|
const url2 = dbUrl ?? configuredUrl;
|
|
80240
80382
|
if (url2 !== ":memory:") {
|
|
80241
|
-
await createNodeFileSystem().ensureDir(
|
|
80383
|
+
await createNodeFileSystem().ensureDir(dirname11(url2));
|
|
80242
80384
|
}
|
|
80243
80385
|
return createMigratedDb({ url: url2 });
|
|
80244
80386
|
}
|