@gobing-ai/spur 0.3.33 → 0.3.34

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.
@@ -74,7 +74,7 @@ agent:
74
74
  # agent: pi
75
75
  # tier: capable-1
76
76
 
77
- # `default-by-phase` is a deprecated backward-compat shim (ADR-033). Prefer
77
+ # `default-by-phase` is a REMOVED (0452). Prefer
78
78
  # executor `tier` + stage-registry model_policy. When present, a matching
79
79
  # phase mapping is still authoritative (fail-fast; no fall back to default)
80
80
  # and shadows stage routing for that phase.
@@ -74,6 +74,16 @@ transitions:
74
74
  guard:
75
75
  kind: always
76
76
 
77
+ # Reopen: done → active (design §7.5 — done is re-enterable; cancelled is not)
78
+ - from: done
79
+ to: active
80
+ description: >
81
+ Reopen completed feature for follow-up work (mandatory History entry).
82
+ Mirrors task-lifecycle done→wip. Prefer spur feature sync --force when
83
+ non-terminal tasks are already linked.
84
+ guard:
85
+ kind: always
86
+
77
87
  # Blocked ↔ active — bidirectional
78
88
  - from: active
79
89
  to: blocked
@@ -113,6 +113,21 @@ states:
113
113
  # hook at run start — see the 0062 follow-up note; there is no link-writing
114
114
  # CLI verb to call from a shell step, so it cannot live in pure YAML.
115
115
  message: "Pipeline start for task ${vars.wbs}."
116
+ # R1 (0453): auto-profile precheck reopens a done feature before task check.
117
+ # Under profile=auto, resolve feature_id, sync (preferred) or update to active.
118
+ # Under non-auto, leave R4 message to guide the operator.
119
+ - kind: shell
120
+ options:
121
+ command: >-
122
+ if [ "$profile" = auto ]; then
123
+ FID=$($spurBin task show $wbs --json 2>/dev/null |
124
+ jq -r '.feature_id // .frontmatter.feature_id // empty' 2>/dev/null);
125
+ if [ -n "$FID" ]; then
126
+ $spurBin feature sync "$FID" --force --json 2>/dev/null ||
127
+ $spurBin feature update "$FID" active 2>/dev/null || true;
128
+ fi;
129
+ fi;
130
+ exit 0
116
131
 
117
132
  - id: implement
118
133
  description: >
@@ -181,8 +196,6 @@ states:
181
196
  ATTEMPT_FILE=".spur/run/$wbs-test-fix-attempt" &&
182
197
  echo 0 > "$ATTEMPT_FILE" &&
183
198
  set +e &&
184
- # sh -c re-parses multi-command gates (&&, ;); do NOT use bare `$qualityGateCmd`
185
- # word-split (breaks on &&). qualityGateCmd is pipeline-owned trusted config only.
186
199
  ( sh -c "$qualityGateCmd" ); gate_rc=$?; set -e &&
187
200
  if [ "$gate_rc" -eq 0 ]; then
188
201
  printf 'PASS\n' > "$STATUS_FILE";
@@ -223,7 +236,6 @@ states:
223
236
  mkdir -p .spur/run &&
224
237
  STATUS_FILE=".spur/run/$wbs-test-gate.status" &&
225
238
  set +e &&
226
- # See test probe: sh -c for multi-command; qualityGateCmd is trusted config only.
227
239
  ( sh -c "$qualityGateCmd" ); gate_rc=$?; set -e &&
228
240
  if [ "$gate_rc" -eq 0 ]; then
229
241
  printf 'PASS\n' > "$STATUS_FILE";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gobing-ai/spur",
3
- "version": "0.3.33",
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.",
3
+ "version": "0.3.34",
4
+ "description": "Spur CLI \u2014 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",
7
7
  "cli",
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "https://github.com/gobing-ai/spur/schemas/spur-config.schema.json",
4
4
  "title": "Spur Project Configuration",
5
- "description": "Schema for .spur/config.yaml the single project configuration surface consumed by spur and (future) spur-server.",
5
+ "description": "Schema for .spur/config.yaml \u2014 the single project configuration surface consumed by spur and (future) spur-server.",
6
6
  "type": "object",
7
7
  "required": ["version", "name"],
8
8
  "properties": {
@@ -107,7 +107,7 @@
107
107
  },
108
108
  "executors": {
109
109
  "type": "array",
110
- "description": "Named executor profiles: each pairs a canonical agent with an optional model override. Referenced by name from `default` and `default-by-phase`.",
110
+ "description": "Named executor profiles: each pairs a canonical agent with an optional model override. Referenced by name from `agent.default` and stage model_policy (default-by-phase removed 0452).",
111
111
  "items": {
112
112
  "type": "object",
113
113
  "required": ["name", "agent"],
@@ -130,18 +130,11 @@
130
130
  "tier": {
131
131
  "type": "string",
132
132
  "enum": ["cheap", "standard", "capable-1", "capable-2", "capable-3"],
133
- "description": "Capability tier for stage-registry adaptive model routing (ADR-033, 0343). Live values: cheap | standard | capable-1 | capable-2 | capable-3 (1=low output quality, 3=high within the capable band). A stage starts on the cheapest eligible executor meeting its model_policy min_tier. Bare legacy `capable` is accepted at runtime (zod preprocess capable-1) during the deprecation window but is not part of this editor enum."
133
+ "description": "Capability tier for stage-registry adaptive model routing (ADR-033, 0343). Live values: cheap | standard | capable-1 | capable-2 | capable-3 (1=low output quality, 3=high within the capable band). A stage starts on the cheapest eligible executor meeting its model_policy min_tier. Bare legacy `capable` is accepted at runtime (zod preprocess \u2192 capable-1) during the deprecation window but is not part of this editor enum."
134
134
  }
135
135
  }
136
136
  }
137
137
  },
138
- "default-by-phase": {
139
- "type": "object",
140
- "description": "DEPRECATED (ADR-033). Backward-compat map of dev phase (e.g. dev-run, dev-review) to executor selector. Prefer executor `tier` + stage-registry model_policy. When present, a matching phase mapping is still authoritative (fail-fast; no fall back to `default`) and shadows stage routing for that phase.",
141
- "additionalProperties": {
142
- "type": "string"
143
- }
144
- },
145
138
  "team": {
146
139
  "type": "object",
147
140
  "description": "Declarative agent teams keyed by team id (the map key is the teamId). Each team materializes a roster of agent specs (feature M). Mirrors @gobing-ai/spur-config TeamConfigSchema (Zod is SSOT).",
@@ -169,7 +162,10 @@
169
162
  "description": "Roster of member references (>= 1). A bare string is shorthand for { executor: <string> }; the composed agent id is <teamId>-<id ?? executor>.",
170
163
  "items": {
171
164
  "oneOf": [
172
- { "type": "string", "minLength": 1 },
165
+ {
166
+ "type": "string",
167
+ "minLength": 1
168
+ },
173
169
  {
174
170
  "type": "object",
175
171
  "required": ["executor"],
@@ -177,27 +173,41 @@
177
173
  "executor": {
178
174
  "type": "string",
179
175
  "minLength": 1,
180
- "description": "Executor name resolved against agent.executors first, else a raw canonical agent type."
176
+ "description": "Executor name \u2014 resolved against agent.executors first, else a raw canonical agent type."
181
177
  },
182
178
  "id": {
183
179
  "type": "string",
184
180
  "minLength": 1,
185
181
  "description": "Local member id; composed id is <teamId>-<id ?? executor> (must match ^[a-z][a-z0-9_-]{1,63}$)."
186
182
  },
187
- "purpose": { "type": "string" },
183
+ "purpose": {
184
+ "type": "string"
185
+ },
188
186
  "workspace": {
189
187
  "type": "string",
190
188
  "minLength": 1,
191
189
  "description": "Per-member workspace override. A leading `~` is tilde-expanded at load."
192
190
  },
193
- "model": { "type": "string", "minLength": 1 },
194
- "autonomy": { "type": "string" },
195
- "systemPrompt": { "type": "string" },
191
+ "model": {
192
+ "type": "string",
193
+ "minLength": 1
194
+ },
195
+ "autonomy": {
196
+ "type": "string"
197
+ },
198
+ "systemPrompt": {
199
+ "type": "string"
200
+ },
196
201
  "command": {
197
202
  "type": "array",
198
- "items": { "type": "string", "minLength": 1 }
203
+ "items": {
204
+ "type": "string",
205
+ "minLength": 1
206
+ }
199
207
  },
200
- "autostart": { "type": "boolean" }
208
+ "autostart": {
209
+ "type": "boolean"
210
+ }
201
211
  }
202
212
  }
203
213
  ]
@@ -213,7 +223,9 @@
213
223
  "properties": {
214
224
  "paths": {
215
225
  "type": "array",
216
- "items": { "type": "string" },
226
+ "items": {
227
+ "type": "string"
228
+ },
217
229
  "description": "Glob paths to rule files."
218
230
  }
219
231
  }
@@ -223,7 +235,9 @@
223
235
  "properties": {
224
236
  "paths": {
225
237
  "type": "array",
226
- "items": { "type": "string" },
238
+ "items": {
239
+ "type": "string"
240
+ },
227
241
  "description": "Glob paths to workflow directories."
228
242
  }
229
243
  }
@@ -239,11 +253,11 @@
239
253
  },
240
254
  "tasks": {
241
255
  "type": "object",
242
- "description": "Task-folder registration (design §9). Absorbs the legacy docs/.tasks/config.json folders + baseCounter concepts. Mirrors @gobing-ai/spur-config tasksConfigSchema (Zod is SSOT).",
256
+ "description": "Task-folder registration (design \u00a79). Absorbs the legacy docs/.tasks/config.json folders + baseCounter concepts. Mirrors @gobing-ai/spur-config tasksConfigSchema (Zod is SSOT).",
243
257
  "properties": {
244
258
  "folders": {
245
259
  "type": "object",
246
- "description": "Map of task-folder path folder config.",
260
+ "description": "Map of task-folder path \u2192 folder config.",
247
261
  "additionalProperties": {
248
262
  "type": "object",
249
263
  "properties": {
@@ -275,7 +289,7 @@
275
289
  },
276
290
  "features": {
277
291
  "type": "object",
278
- "description": "Feature directory location (design §9). Mirrors @gobing-ai/spur-config featuresConfigSchema (Zod is SSOT).",
292
+ "description": "Feature directory location (design \u00a79). Mirrors @gobing-ai/spur-config featuresConfigSchema (Zod is SSOT).",
279
293
  "properties": {
280
294
  "dir": {
281
295
  "type": "string",
package/spur.js CHANGED
@@ -22521,7 +22521,7 @@ var init_figures = __esm(() => {
22521
22521
  replacements = Object.entries(specialMainSymbols);
22522
22522
  });
22523
22523
 
22524
- // ../../node_modules/.bun/yoctocolors@2.2.0/node_modules/yoctocolors/base.js
22524
+ // ../../node_modules/.bun/yoctocolors@2.1.2/node_modules/yoctocolors/base.js
22525
22525
  import tty from "tty";
22526
22526
  var hasColors, format = (open, close) => {
22527
22527
  if (!hasColors) {
@@ -22547,7 +22547,7 @@ var hasColors, format = (open, close) => {
22547
22547
  result += string4.slice(lastIndex) + closeCode;
22548
22548
  return result;
22549
22549
  };
22550
- }, reset, bold, dim, italic, underline, underlineDouble, underlineCurly, underlineDotted, underlineDashed, overline, inverse, hidden, strikethrough, black, red, green, yellow, blue, magenta, cyan, white, gray, bgBlack, bgRed, bgGreen, bgYellow, bgBlue, bgMagenta, bgCyan, bgWhite, bgGray, redBright, greenBright, yellowBright, blueBright, magentaBright, cyanBright, whiteBright, bgRedBright, bgGreenBright, bgYellowBright, bgBlueBright, bgMagentaBright, bgCyanBright, bgWhiteBright, underlineBlack, underlineRed, underlineGreen, underlineYellow, underlineBlue, underlineMagenta, underlineCyan, underlineWhite, underlineGray, underlineRedBright, underlineGreenBright, underlineYellowBright, underlineBlueBright, underlineMagentaBright, underlineCyanBright, underlineWhiteBright;
22550
+ }, reset, bold, dim, italic, underline, overline, inverse, hidden, strikethrough, black, red, green, yellow, blue, magenta, cyan, white, gray, bgBlack, bgRed, bgGreen, bgYellow, bgBlue, bgMagenta, bgCyan, bgWhite, bgGray, redBright, greenBright, yellowBright, blueBright, magentaBright, cyanBright, whiteBright, bgRedBright, bgGreenBright, bgYellowBright, bgBlueBright, bgMagentaBright, bgCyanBright, bgWhiteBright;
22551
22551
  var init_base = __esm(() => {
22552
22552
  hasColors = tty?.WriteStream?.prototype?.hasColors?.() ?? false;
22553
22553
  reset = format(0, 0);
@@ -22555,10 +22555,6 @@ var init_base = __esm(() => {
22555
22555
  dim = format(2, 22);
22556
22556
  italic = format(3, 23);
22557
22557
  underline = format(4, 24);
22558
- underlineDouble = format("4:2", 24);
22559
- underlineCurly = format("4:3", 24);
22560
- underlineDotted = format("4:4", 24);
22561
- underlineDashed = format("4:5", 24);
22562
22558
  overline = format(53, 55);
22563
22559
  inverse = format(7, 27);
22564
22560
  hidden = format(8, 28);
@@ -22595,25 +22591,9 @@ var init_base = __esm(() => {
22595
22591
  bgMagentaBright = format(105, 49);
22596
22592
  bgCyanBright = format(106, 49);
22597
22593
  bgWhiteBright = format(107, 49);
22598
- underlineBlack = format("58;5;0", 59);
22599
- underlineRed = format("58;5;1", 59);
22600
- underlineGreen = format("58;5;2", 59);
22601
- underlineYellow = format("58;5;3", 59);
22602
- underlineBlue = format("58;5;4", 59);
22603
- underlineMagenta = format("58;5;5", 59);
22604
- underlineCyan = format("58;5;6", 59);
22605
- underlineWhite = format("58;5;7", 59);
22606
- underlineGray = format("58;5;8", 59);
22607
- underlineRedBright = format("58;5;9", 59);
22608
- underlineGreenBright = format("58;5;10", 59);
22609
- underlineYellowBright = format("58;5;11", 59);
22610
- underlineBlueBright = format("58;5;12", 59);
22611
- underlineMagentaBright = format("58;5;13", 59);
22612
- underlineCyanBright = format("58;5;14", 59);
22613
- underlineWhiteBright = format("58;5;15", 59);
22614
- });
22615
-
22616
- // ../../node_modules/.bun/yoctocolors@2.2.0/node_modules/yoctocolors/index.js
22594
+ });
22595
+
22596
+ // ../../node_modules/.bun/yoctocolors@2.1.2/node_modules/yoctocolors/index.js
22617
22597
  var init_yoctocolors = __esm(() => {
22618
22598
  init_base();
22619
22599
  });
@@ -37381,7 +37361,6 @@ var init_src = __esm(() => {
37381
37361
  AgentConfigSchema = exports_external.object({
37382
37362
  default: exports_external.string().optional(),
37383
37363
  executors: exports_external.array(AgentExecutorConfigSchema).optional(),
37384
- "default-by-phase": exports_external.record(exports_external.string(), exports_external.string()).optional(),
37385
37364
  team: exports_external.record(exports_external.string(), TeamConfigSchema).optional(),
37386
37365
  output: AgentOutputConfigSchema.optional(),
37387
37366
  sessionAffinity: exports_external.boolean().optional()
@@ -61342,13 +61321,6 @@ class AgentService {
61342
61321
  ...observer !== undefined ? { observer } : {}
61343
61322
  };
61344
61323
  }
61345
- hasWarnedDeprecation = false;
61346
- warnDeprecationOnce(message) {
61347
- if (!this.hasWarnedDeprecation) {
61348
- this.ctx.output.error(`Warning: ${message}`);
61349
- this.hasWarnedDeprecation = true;
61350
- }
61351
- }
61352
61324
  async resolveAgent(prompt, flags, doctorRunner) {
61353
61325
  const raw = stringFlag(flags, "agent", "auto");
61354
61326
  if (raw === "auto")
@@ -61360,12 +61332,6 @@ class AgentService {
61360
61332
  async resolveAgentAuto(prompt, flags, doctorRunner) {
61361
61333
  const config4 = this.ctx.agentConfig;
61362
61334
  const phase = extractPhase(prompt);
61363
- const phaseMap = config4?.["default-by-phase"];
61364
- const phaseSelector = phase !== undefined ? phaseMap?.[phase] : undefined;
61365
- if (phaseSelector !== undefined && phase !== undefined) {
61366
- this.warnDeprecationOnce(`default-by-phase is deprecated; use stage model_policy instead. Phase '${phase}' mapped to '${phaseSelector}'.`);
61367
- return this.resolveExecutorSelector(phaseSelector, doctorRunner, "phase", phase);
61368
- }
61369
61335
  const stageFlag = stringFlag(flags, "stage", "");
61370
61336
  const targetStageId = stageFlag !== "" ? stageFlag : phase !== undefined ? phase : undefined;
61371
61337
  if (targetStageId !== undefined) {
@@ -62758,8 +62724,10 @@ var init_feature_check = __esm(() => {
62758
62724
  await this.checkChildrenLimit(featureId2, options.featuresDir, findings);
62759
62725
  }
62760
62726
  const dogfoodDir = options?.dogfoodDir ?? (options?.featuresDir ? join9(dirname8(options.featuresDir), "dogfood") : undefined);
62761
- const runDir = options?.runDir ?? (options?.tasksDir ? defaultVerdictRunDir(options.tasksDir) : undefined);
62762
- await this.runL4(doc2, featureId2, status, options?.tasksDir, dogfoodDir, runDir, findings);
62727
+ const primaryTasksDir = options?.tasksDir ?? options?.tasksDirs?.[0];
62728
+ const runDir = options?.runDir ?? (primaryTasksDir ? defaultVerdictRunDir(primaryTasksDir) : undefined);
62729
+ const taskScanDirs = options?.tasksDirs && options.tasksDirs.length > 0 ? options.tasksDirs : options?.tasksDir ? [options.tasksDir] : [];
62730
+ await this.runL4(doc2, featureId2, status, taskScanDirs, dogfoodDir, runDir, findings);
62763
62731
  return { id: featureId2, ...this.summarizeWithStatus(status, findings, strict, options?.severityOverrides) };
62764
62732
  }
62765
62733
  runL3(doc2, findings) {
@@ -62871,49 +62839,49 @@ var init_feature_check = __esm(() => {
62871
62839
  });
62872
62840
  }
62873
62841
  }
62874
- async runL4(doc2, featureId2, status, tasksDir, dogfoodDir, runDir, findings) {
62875
- if (tasksDir === undefined)
62842
+ async runL4(doc2, featureId2, status, tasksDirs, dogfoodDir, runDir, findings) {
62843
+ if (tasksDirs.length === 0)
62876
62844
  return;
62877
62845
  let linkedTasks = 0;
62878
62846
  const incompleteTasks = [];
62879
62847
  const linkedTaskAc = [];
62880
62848
  const linkedTaskSolutions = [];
62881
62849
  const linkedTaskRecords = [];
62882
- try {
62883
- const entries = await this.fs.readDir(tasksDir);
62884
- for (const entry of entries) {
62885
- if (!/^\d{4}_.+\.md$/.test(entry))
62886
- continue;
62887
- try {
62888
- const raw = await this.fs.readFile(`${tasksDir}/${entry}`);
62889
- const taskDoc = MarkdownDocument.parse(raw, "task");
62890
- const tfm = taskDoc.frontmatterData ?? {};
62891
- const tfid = tfm.feature_id ?? tfm["feature-id"];
62892
- if (tfid !== featureId2)
62850
+ for (const tasksDir of tasksDirs) {
62851
+ try {
62852
+ const entries = await this.fs.readDir(tasksDir);
62853
+ for (const entry of entries) {
62854
+ if (!/^\d{4}_.+\.md$/.test(entry))
62893
62855
  continue;
62894
- linkedTasks += 1;
62895
- const tStatus = tfm.status ?? "backlog";
62896
- const wbs = entry.match(/^(\d{4})_/)?.[1] ?? entry;
62897
- if (tStatus !== "done" && tStatus !== "cancelled") {
62898
- incompleteTasks.push(wbs);
62856
+ try {
62857
+ const raw = await this.fs.readFile(`${tasksDir}/${entry}`);
62858
+ const taskDoc = MarkdownDocument.parse(raw, "task");
62859
+ const tfm = taskDoc.frontmatterData ?? {};
62860
+ const tfid = tfm.feature_id ?? tfm["feature-id"];
62861
+ if (tfid !== featureId2)
62862
+ continue;
62863
+ linkedTasks += 1;
62864
+ const tStatus = tfm.status ?? "backlog";
62865
+ const wbs = entry.match(/^(\d{4})_/)?.[1] ?? entry;
62866
+ if (tStatus !== "done" && tStatus !== "cancelled") {
62867
+ incompleteTasks.push(wbs);
62868
+ }
62869
+ const tac = stripAcFence(taskDoc.getSection("Acceptance Criteria") ?? "");
62870
+ if (tac.trim().length > 0)
62871
+ linkedTaskAc.push(tac);
62872
+ linkedTaskSolutions.push(taskDoc.getSection("Solution") ?? "");
62873
+ linkedTaskRecords.push({ wbs, status: tStatus, ac: tac });
62874
+ } catch {
62875
+ findings.push({
62876
+ layer: "L4",
62877
+ code: FINDING_CODES.L4_LINKED_TASK_PARSE_FAILED,
62878
+ severity: "warning",
62879
+ section: "",
62880
+ message: `Linked task file "${entry}" failed to parse \u2014 dangling feature_id edge`
62881
+ });
62899
62882
  }
62900
- const tac = stripAcFence(taskDoc.getSection("Acceptance Criteria") ?? "");
62901
- if (tac.trim().length > 0)
62902
- linkedTaskAc.push(tac);
62903
- linkedTaskSolutions.push(taskDoc.getSection("Solution") ?? "");
62904
- linkedTaskRecords.push({ wbs, status: tStatus, ac: tac });
62905
- } catch {
62906
- findings.push({
62907
- layer: "L4",
62908
- code: FINDING_CODES.L4_LINKED_TASK_PARSE_FAILED,
62909
- severity: "warning",
62910
- section: "",
62911
- message: `Linked task file "${entry}" failed to parse \u2014 dangling feature_id edge`
62912
- });
62913
62883
  }
62914
- }
62915
- } catch {
62916
- return;
62884
+ } catch {}
62917
62885
  }
62918
62886
  const acBody = stripAcFence(doc2.getSection("Acceptance Criteria") ?? "");
62919
62887
  const hasScenarios = /^\s*Scenario:/m.test(acBody);
@@ -63018,7 +62986,9 @@ var init_feature_check = __esm(() => {
63018
62986
  artifacts2.set(wbs, artifact);
63019
62987
  const diagnosticParts = [];
63020
62988
  if (artifact.diagnostics.artifactError !== undefined) {
63021
- diagnosticParts.push(artifact.diagnostics.artifactError);
62989
+ if (artifact.diagnostics.artifactError !== "artifact is missing") {
62990
+ diagnosticParts.push(artifact.diagnostics.artifactError);
62991
+ }
63022
62992
  }
63023
62993
  if (artifact.diagnostics.rejectedRowCount > 0) {
63024
62994
  diagnosticParts.push(`${artifact.diagnostics.rejectedRowCount} rejected coverage row(s)`);
@@ -63381,9 +63351,13 @@ class FeatureService {
63381
63351
  }
63382
63352
  const checkL4Gate = async () => {
63383
63353
  const checkSvc = new FeatureCheckService(this.ctx.fs);
63354
+ const tasksDirs = this.ctx.foldersConfig ? Object.keys(this.ctx.foldersConfig.folders).map((p) => this.ctx.fs.resolve(p)) : [this.ctx.tasksDir];
63355
+ if (!tasksDirs.includes(this.ctx.tasksDir))
63356
+ tasksDirs.unshift(this.ctx.tasksDir);
63384
63357
  const res = await checkSvc.check(feature.filePath, featureId2, {
63385
63358
  featuresDir: this.ctx.featuresDir,
63386
63359
  tasksDir: this.ctx.tasksDir,
63360
+ tasksDirs,
63387
63361
  runDir: defaultVerdictRunDir(this.ctx.tasksDir)
63388
63362
  });
63389
63363
  const l4Errors = res.findings.filter((f) => f.layer === "L4" && f.severity === "error");
@@ -66349,12 +66323,13 @@ var init_task_check = __esm(() => {
66349
66323
  const featureTerminal = featureStatus === "done" || featureStatus === "cancelled";
66350
66324
  const taskTerminal = status === "done" || status === "cancelled";
66351
66325
  if (featureTerminal && !taskTerminal) {
66326
+ const reopenGuide = featureStatus === "done" ? `Reopen: \`spur feature update ${featureId2} active\` (or \`spur feature sync ${featureId2} --force\` when non-terminal tasks are already linked). Alternatively re-parent: \`spur task update <wbs> --feature <otherId>\`.` : `Feature is cancelled \u2014 re-parent or unlink this live task (\`spur task update <wbs> --feature <otherId>\`).`;
66352
66327
  findings.push({
66353
66328
  layer: "L4",
66354
66329
  code: FINDING_CODES.L4_FEATURE_TERMINAL,
66355
66330
  severity: "error",
66356
66331
  section: "",
66357
- message: `Feature "${featureId2}" is ${featureStatus} \u2014 remove or re-parent this task`
66332
+ message: `Feature "${featureId2}" is ${featureStatus} \u2014 live tasks cannot stay linked without action. ${reopenGuide}`
66358
66333
  });
66359
66334
  }
66360
66335
  await this.checkAcCoverage(doc2, featurePath, featureId2, findings);
@@ -67937,31 +67912,45 @@ function extractRequirements(text4) {
67937
67912
  const lines = text4.split(`
67938
67913
  `);
67939
67914
  let inTable = false;
67915
+ let colMap = null;
67940
67916
  for (const line of lines) {
67941
67917
  const trimmed = line.trim();
67942
67918
  if (!trimmed.startsWith("|"))
67943
67919
  continue;
67944
67920
  const cells = trimmed.split("|").map((c3) => c3.trim()).filter(Boolean);
67945
67921
  if (!inTable && cells.length >= 2) {
67946
- const h0 = (cells[0] ?? "").toLowerCase();
67947
- const h1 = (cells[1] ?? "").toLowerCase();
67948
- if ((h0.includes("req") || h0 === "requirement") && (h1.includes("status") || h1 === "verdict")) {
67949
- inTable = true;
67950
- continue;
67922
+ const h0 = (cells[0] ?? "").toLowerCase().trim();
67923
+ const h0IsId = h0.includes("req") || h0 === "requirement" || h0 === "r#" || h0 === "r" || /^r\d+$/.test(h0);
67924
+ if (h0IsId) {
67925
+ const statusIdx = cells.findIndex((c3) => {
67926
+ const x = c3.toLowerCase().trim();
67927
+ return x.includes("status") || x === "verdict";
67928
+ });
67929
+ if (statusIdx >= 0) {
67930
+ const evidenceIdx = cells.findIndex((c3) => c3.toLowerCase().includes("evidence"));
67931
+ colMap = {
67932
+ id: 0,
67933
+ status: statusIdx,
67934
+ evidence: evidenceIdx >= 0 ? evidenceIdx : undefined
67935
+ };
67936
+ inTable = true;
67937
+ continue;
67938
+ }
67951
67939
  }
67952
67940
  }
67953
67941
  if (/^[-:]+$/.test(cells[0] ?? ""))
67954
67942
  continue;
67955
- if (inTable && cells.length >= 2) {
67943
+ if (inTable && colMap && cells.length >= 2) {
67956
67944
  const first = (cells[0] ?? "").toLowerCase();
67957
67945
  const second = (cells[1] ?? "").toLowerCase();
67958
67946
  if ((first === "ac" || first.includes("acceptance") || first === "check" || first === "name") && (second.includes("status") || second === "verdict")) {
67959
67947
  inTable = false;
67948
+ colMap = null;
67960
67949
  continue;
67961
67950
  }
67962
- const id = cells[0] ?? "";
67963
- const statusRaw = (cells[1] ?? "").toUpperCase();
67964
- const evidence = cells.length >= 3 ? cells[2] ?? "" : "";
67951
+ const id = cells[colMap.id] ?? "";
67952
+ const statusRaw = (cells[colMap.status] ?? "").toUpperCase();
67953
+ const evidence = colMap.evidence !== undefined ? cells[colMap.evidence] ?? "" : "";
67965
67954
  const status = normalizeStatus(statusRaw);
67966
67955
  if (status !== null && id.length > 0) {
67967
67956
  reqs.push({ id, status, evidence });
@@ -69126,42 +69115,45 @@ import { dirname as dirname12, isAbsolute as isAbsolute2, join as join15 } from
69126
69115
  class AgentRunActionRunner {
69127
69116
  observabilityBus;
69128
69117
  steeringController;
69118
+ agentConfig;
69129
69119
  kind = KIND;
69130
69120
  agentService;
69131
- constructor(agentService, observabilityBus, steeringController) {
69121
+ constructor(agentService, observabilityBus, steeringController, agentConfig = {}) {
69132
69122
  this.observabilityBus = observabilityBus;
69133
69123
  this.steeringController = steeringController;
69124
+ this.agentConfig = agentConfig;
69134
69125
  this.agentService = agentService;
69135
69126
  }
69136
69127
  async execute(options, context4) {
69137
69128
  const input = asOptionalString(options.input);
69138
69129
  const agent = asOptionalString(options.agent);
69139
- const cfg = context4.config;
69140
- const dispatchAgent = agent === "inline" ? cfg?.agent?.default ?? undefined : agent;
69130
+ const dispatchAgent = agent === "inline" ? this.agentConfig.default ?? undefined : agent;
69141
69131
  const model = asOptionalString(options.model);
69142
69132
  const mode = asOptionalString(options.mode) ?? "text";
69143
69133
  const cwd = asOptionalString(options.cwd) ?? context4.workdir ?? ".";
69144
69134
  const sessionAffinityVar = context4.vars.sessionAffinity;
69145
- const affinityDisabled = sessionAffinityVar === "false" || typeof sessionAffinityVar === "boolean" && !sessionAffinityVar || options.sessionAffinity === false || cfg?.agent?.sessionAffinity === false;
69135
+ const affinityDisabled = sessionAffinityVar === "false" || typeof sessionAffinityVar === "boolean" && !sessionAffinityVar || options.sessionAffinity === false || this.agentConfig.sessionAffinity === false;
69146
69136
  const affinityOn = !affinityDisabled;
69147
69137
  const agentLabel = dispatchAgent ?? "<default>";
69148
- const targetAgentDir = dispatchAgent ?? cfg?.agent?.default ?? "omp";
69138
+ const targetAgentDir = dispatchAgent ?? this.agentConfig.default ?? "omp";
69149
69139
  const prevAgent = asOptionalString(context4.vars.__agentSessionAgent);
69150
69140
  let sessionDir = asOptionalString(context4.vars.__agentSessionDir);
69151
69141
  if (affinityOn) {
69152
69142
  if (!sessionDir || prevAgent && prevAgent !== targetAgentDir) {
69153
69143
  sessionDir = join15(cwd, ".spur", "run", context4.runId, "agent-sessions", targetAgentDir);
69154
69144
  }
69145
+ } else {
69146
+ if (!sessionDir) {
69147
+ sessionDir = join15(cwd, ".spur", "run", context4.runId, "agent-sessions", targetAgentDir);
69148
+ }
69155
69149
  }
69156
69150
  const storedSessionId = asOptionalString(context4.vars.__agentSessionId);
69157
69151
  let continueFlag = asOptionalBoolean(options.continue);
69158
69152
  const latch = context4.vars.__agentSession;
69159
69153
  const latchAutoContinued = continueFlag === undefined && latch === "open";
69160
69154
  if (latchAutoContinued) {
69161
- if (affinityOn) {
69155
+ if (!affinityOn) {
69162
69156
  continueFlag = true;
69163
- } else {
69164
- continueFlag = undefined;
69165
69157
  }
69166
69158
  }
69167
69159
  if (input === undefined && !continueFlag) {
@@ -69178,9 +69170,9 @@ class AgentRunActionRunner {
69178
69170
  flags.mode = mode;
69179
69171
  if (cwd !== "")
69180
69172
  flags.cwd = cwd;
69181
- if (affinityOn && sessionDir) {
69173
+ if (sessionDir) {
69182
69174
  flags.sessionDir = sessionDir;
69183
- if (storedSessionId) {
69175
+ if (affinityOn && storedSessionId) {
69184
69176
  flags.sessionId = storedSessionId;
69185
69177
  }
69186
69178
  }
@@ -69269,11 +69261,11 @@ class AgentRunActionRunner {
69269
69261
  }
69270
69262
  }
69271
69263
  const stepLabel = context4.stateOrNodeId;
69272
- if (ok && requireDiff === true && !await gitHasNonCorpusChanges(cwd)) {
69264
+ if (ok && requireDiff === true && !await gitHasNonCorpusChanges(cwd, this.agentConfig.excludeGlobs)) {
69273
69265
  return {
69274
69266
  ok: false,
69275
69267
  data: buildResultData(exitCode, agentLabel, capture, answer, invocation),
69276
- error: `agent.run '${stepLabel}' (${agentLabel}) exited 0 but produced zero non-corpus file changes \u2014 empty implement (no-op). The implement agent must change at least one file outside docs/tasks3|docs/features; fix the implement input and re-run the pipeline.`
69268
+ error: `agent.run '${stepLabel}' (${agentLabel}) exited 0 but produced zero non-corpus file changes \u2014 empty implement (no-op). The implement agent must change at least one file outside the configured task/feature folders; fix the implement input and re-run the pipeline.`
69277
69269
  };
69278
69270
  }
69279
69271
  if (!ok) {
@@ -69281,19 +69273,24 @@ class AgentRunActionRunner {
69281
69273
  }
69282
69274
  const partialWorkHint = `partial work (if any) preserved at .spur/run/${context4.runId}-${stepLabel}-partial.md; resume from that tree per the timed-out-implement runbook (plugins/sp/skills/spur-dev/references/execution-workflow.md)`;
69283
69275
  const error51 = ok ? undefined : traced.signal !== undefined ? timeoutMs !== undefined ? `agent.run '${stepLabel}' (${agentLabel}) terminated by signal ${traced.signal} (configured timeout: ${timeoutMs}ms; timeout or cancellation); ${partialWorkHint}` : `agent.run '${stepLabel}' (${agentLabel}) was cancelled by signal ${traced.signal}; ${partialWorkHint}` : traced.message !== undefined ? `agent.run '${stepLabel}' (${agentLabel}) dispatch failed: ${traced.message}` : `agent.run '${stepLabel}' (${agentLabel}) exited with code ${exitCode}; ${partialWorkHint}`;
69276
+ const resolvedAgent = invocation?.agent ?? targetAgentDir;
69277
+ let resolvedSessionDir = sessionDir;
69278
+ if (ok && affinityOn && resolvedAgent !== targetAgentDir && !context4.vars.__agentSessionDir) {
69279
+ resolvedSessionDir = join15(cwd, ".spur", "run", context4.runId, "agent-sessions", resolvedAgent);
69280
+ }
69284
69281
  let discoveredSessionId = storedSessionId;
69285
- if (ok && affinityOn && sessionDir && !discoveredSessionId) {
69286
- discoveredSessionId = await discoverSessionId(sessionDir);
69282
+ if (ok && affinityOn && resolvedSessionDir && !discoveredSessionId) {
69283
+ discoveredSessionId = await discoverSessionId(resolvedSessionDir);
69287
69284
  }
69288
- if (ok && affinityOn && sessionDir) {
69285
+ if (ok && affinityOn && resolvedSessionDir) {
69289
69286
  try {
69290
69287
  const sidecarPath = join15(cwd, ".spur", "run", `${context4.runId}-agent-session.json`);
69291
69288
  const fs3 = createNodeFileSystem3(cwd);
69292
69289
  await fs3.ensureDir(dirname12(sidecarPath));
69293
69290
  await fs3.writeFile(sidecarPath, JSON.stringify({
69294
- agent: agentLabel,
69291
+ agent: resolvedAgent,
69295
69292
  sessionId: discoveredSessionId ?? null,
69296
- sessionDir,
69293
+ sessionDir: resolvedSessionDir,
69297
69294
  openedAt: new Date().toISOString()
69298
69295
  }, null, 2));
69299
69296
  } catch {}
@@ -69304,9 +69301,9 @@ class AgentRunActionRunner {
69304
69301
  error: error51,
69305
69302
  setVars: ok ? {
69306
69303
  __agentSession: resumeRetried ? "no-resume" : "open",
69307
- ...affinityOn && sessionDir ? { __agentSessionDir: sessionDir } : {},
69304
+ ...affinityOn && resolvedSessionDir ? { __agentSessionDir: resolvedSessionDir } : {},
69308
69305
  ...affinityOn && (discoveredSessionId || storedSessionId) ? { __agentSessionId: discoveredSessionId || storedSessionId } : {},
69309
- ...affinityOn ? { __agentSessionAgent: targetAgentDir } : {},
69306
+ ...affinityOn ? { __agentSessionAgent: resolvedAgent } : {},
69310
69307
  ...steeringNote !== undefined ? { __steeringNote: steeringNote } : {}
69311
69308
  } : undefined
69312
69309
  };
@@ -69321,9 +69318,12 @@ async function discoverSessionId(sessionDir) {
69321
69318
  const entries = await fs3.readDir(sessionDir);
69322
69319
  if (entries.length === 0)
69323
69320
  return;
69321
+ const jsonEntries = entries.filter((e) => e.toLowerCase().endsWith(".json"));
69322
+ if (jsonEntries.length === 0)
69323
+ return;
69324
69324
  let newestFile;
69325
69325
  let newestMtime = 0;
69326
- for (const entry of entries) {
69326
+ for (const entry of jsonEntries) {
69327
69327
  const fullPath = join15(sessionDir, entry);
69328
69328
  const st = await fs3.stat(fullPath);
69329
69329
  if (st?.isFile() && st.mtimeMs > newestMtime) {
@@ -69442,11 +69442,12 @@ async function gitDiffStat(cwd) {
69442
69442
  return "";
69443
69443
  }
69444
69444
  }
69445
- async function gitHasNonCorpusChanges(cwd) {
69445
+ async function gitHasNonCorpusChanges(cwd, excludeGlobs = ["docs/tasks3/*", "docs/features/*"]) {
69446
69446
  try {
69447
+ const excludes = excludeGlobs.map((g) => `:(exclude)${g}`);
69447
69448
  const result = await new NodeProcessExecutor3().run({
69448
69449
  command: "git",
69449
- args: ["status", "--porcelain", "--", ".", ":(exclude)docs/tasks3/*", ":(exclude)docs/features/*"],
69450
+ args: ["status", "--porcelain", "--", ".", ...excludes],
69450
69451
  cwd,
69451
69452
  maxOutput: 1024 * 1024,
69452
69453
  forceBuffered: true,
@@ -70161,7 +70162,7 @@ var KIND12 = "shell";
70161
70162
  // ../../packages/app/src/workflow/builtins.ts
70162
70163
  function registerSpurBuiltins(host, options) {
70163
70164
  const fileSystem = options.fileSystem ?? createNodeFileSystem3();
70164
- host.registerAction(new AgentRunActionRunner(options.agentService, options.observabilityBus, options.steeringController), "builtin");
70165
+ host.registerAction(new AgentRunActionRunner(options.agentService, options.observabilityBus, options.steeringController, options.agentConfig), "builtin");
70165
70166
  host.registerAction(new StreamingShellActionRunner(options.processExecutor ?? new NodeProcessExecutor3, options.observabilityBus), "builtin");
70166
70167
  host.registerGuard(new EnvShellGuardRunner(options.processExecutor ?? new NodeProcessExecutor3), "builtin");
70167
70168
  host.registerAction(new RuleCheckActionRunner(options.ruleService), "builtin");
@@ -70271,6 +70272,30 @@ class WorkflowAppService {
70271
70272
  validateSchema: opts.validateSchema !== false,
70272
70273
  ...embedded !== undefined ? embedded : {}
70273
70274
  });
70275
+ const shellErrors = [];
70276
+ const commands = collectShellCommands(workflow);
70277
+ const executor = new NodeProcessExecutor3;
70278
+ for (const { stateId, kind, index: index2, command } of commands) {
70279
+ if (!command || command.trim().length === 0)
70280
+ continue;
70281
+ try {
70282
+ const result = await executor.run({
70283
+ command: "sh",
70284
+ args: ["-n", "-c", command],
70285
+ rejectOnError: false
70286
+ });
70287
+ if (result.exitCode !== 0) {
70288
+ const stderr = (result.stderr ?? "").trim();
70289
+ const location = stateId ? `${stateId}/${kind}[${index2}]` : `${kind}[${index2}]`;
70290
+ shellErrors.push(`Shell syntax error at ${location}: ${stderr || "non-zero exit"}`);
70291
+ }
70292
+ } catch {
70293
+ shellErrors.push(`Shell syntax check failed at ${stateId}/${kind}[${index2}]: process error`);
70294
+ }
70295
+ }
70296
+ if (shellErrors.length > 0) {
70297
+ return { ok: false, valid: false, file: file2, errors: shellErrors };
70298
+ }
70274
70299
  return { ok: true, valid: true, workflow };
70275
70300
  } catch (error51) {
70276
70301
  return {
@@ -70573,6 +70598,23 @@ class WorkflowAppService {
70573
70598
  const processExec = this.ctx.processExecutor?.();
70574
70599
  const host = createDefaultWorkflowEngineHost(processExec !== undefined ? { processExecutor: processExec } : {});
70575
70600
  const bus = this.ctx.observabilityBus?.();
70601
+ let agentSlice = {};
70602
+ try {
70603
+ const agent = (await loadSpurConfig(this.ctx.cwd)).agent;
70604
+ agentSlice = {
70605
+ ...agent?.default !== undefined ? { default: agent.default } : {},
70606
+ ...agent?.sessionAffinity !== undefined ? { sessionAffinity: agent.sessionAffinity } : {}
70607
+ };
70608
+ } catch {}
70609
+ try {
70610
+ const fs3 = createNodeFileSystem3(this.ctx.cwd);
70611
+ const { foldersConfig, featuresDir } = await resolvePlanningFolders(fs3);
70612
+ const folderGlobs = Object.keys(foldersConfig.folders).map((k) => `${k}/*`);
70613
+ agentSlice = {
70614
+ ...agentSlice,
70615
+ excludeGlobs: [...folderGlobs, `${featuresDir}/*`]
70616
+ };
70617
+ } catch {}
70576
70618
  registerSpurBuiltins(host, {
70577
70619
  agentService: this.ctx.agentService(),
70578
70620
  ruleService: this.ctx.ruleService(),
@@ -70581,7 +70623,8 @@ class WorkflowAppService {
70581
70623
  hostAllowlist: this.ctx.hostAllowlist?.(),
70582
70624
  ...bus !== undefined ? { observabilityBus: bus } : {},
70583
70625
  ...processExec !== undefined ? { processExecutor: processExec } : {},
70584
- ...opts.steeringController !== undefined ? { steeringController: opts.steeringController } : {}
70626
+ ...opts.steeringController !== undefined ? { steeringController: opts.steeringController } : {},
70627
+ agentConfig: agentSlice
70585
70628
  });
70586
70629
  const db2 = await this.ctx.getDb();
70587
70630
  let persistence2 = new DbWorkflowPersistenceAdapter(db2);
@@ -70592,6 +70635,55 @@ class WorkflowAppService {
70592
70635
  return new WorkflowService(host, adapter);
70593
70636
  }
70594
70637
  }
70638
+ function collectShellCommands(def) {
70639
+ const entries = [];
70640
+ const visitAction = (stateId, action, idx) => {
70641
+ if (action.kind === "shell") {
70642
+ const cmd = action.options?.command;
70643
+ if (typeof cmd === "string" && cmd.length > 0) {
70644
+ entries.push({ stateId, kind: "action", index: idx, command: cmd });
70645
+ }
70646
+ }
70647
+ };
70648
+ const visitGuard = (stateId, guard) => {
70649
+ if (guard.kind === "shell") {
70650
+ const cmd = guard.options?.command;
70651
+ if (typeof cmd === "string" && cmd.length > 0) {
70652
+ entries.push({ stateId, kind: "guard", index: 0, command: cmd });
70653
+ }
70654
+ }
70655
+ };
70656
+ if (def.kind === "transition-flow" || def.kind === undefined) {
70657
+ const flowDef = def;
70658
+ for (const node of flowDef.nodes ?? []) {
70659
+ if (node.action)
70660
+ visitAction(node.id, node.action, 0);
70661
+ }
70662
+ for (const edge of flowDef.edges ?? []) {
70663
+ if (edge.condition)
70664
+ visitGuard(edge.from, edge.condition);
70665
+ }
70666
+ } else {
70667
+ const smDef = def;
70668
+ for (const state of smDef.states ?? []) {
70669
+ if (state.onEnter) {
70670
+ state.onEnter.forEach((action, i2) => {
70671
+ visitAction(state.id, action, i2);
70672
+ });
70673
+ }
70674
+ if (state.onExit) {
70675
+ state.onExit.forEach((action, i2) => {
70676
+ visitAction(state.id, action, i2);
70677
+ });
70678
+ }
70679
+ }
70680
+ for (const trans of smDef.transitions ?? []) {
70681
+ if (trans.guard)
70682
+ visitGuard(`${trans.from}\u2192${trans.to}`, trans.guard);
70683
+ }
70684
+ }
70685
+ return entries;
70686
+ }
70595
70687
  async function fileExists(path9) {
70596
70688
  const fs3 = createNodeFileSystem3();
70597
70689
  return await fs3.exists(path9);
@@ -78982,6 +79074,9 @@ function registerFeatureCommand(program2, context4) {
78982
79074
  const resolved = await resolvePlanningFolders(context4.fs);
78983
79075
  const featuresDir = options.folder ?? context4.fs.resolve(resolved.featuresDir);
78984
79076
  const tasksDir = context4.fs.resolve(resolved.tasksDir);
79077
+ const tasksDirs = Object.keys(resolved.foldersConfig.folders).map((p) => context4.fs.resolve(p));
79078
+ if (!tasksDirs.includes(tasksDir))
79079
+ tasksDirs.unshift(tasksDir);
78985
79080
  const svc = new FeatureCheckService(context4.fs);
78986
79081
  const json3 = options.json === true;
78987
79082
  const strict = options.strict === true;
@@ -79000,6 +79095,7 @@ function registerFeatureCommand(program2, context4) {
79000
79095
  strict,
79001
79096
  featuresDir,
79002
79097
  tasksDir,
79098
+ tasksDirs,
79003
79099
  runDir: context4.fs.resolve(".spur/run"),
79004
79100
  severityOverrides: resolved.severityOverrides,
79005
79101
  asStatus: options.as
@@ -79095,6 +79191,9 @@ async function assertFeatureCheckPass(context4, id, folderOverride, strict, asSt
79095
79191
  const resolved = await resolvePlanningFolders(context4.fs);
79096
79192
  const featuresDir = folderOverride ?? context4.fs.resolve(resolved.featuresDir);
79097
79193
  const tasksDir = context4.fs.resolve(resolved.tasksDir);
79194
+ const tasksDirs = Object.keys(resolved.foldersConfig.folders).map((p) => context4.fs.resolve(p));
79195
+ if (!tasksDirs.includes(tasksDir))
79196
+ tasksDirs.unshift(tasksDir);
79098
79197
  const entries = await context4.fs.readDir(featuresDir);
79099
79198
  const fileName = entries.find((name) => name.match(new RegExp(`^${id}_.+\\.md$`)));
79100
79199
  if (fileName === undefined) {
@@ -79104,6 +79203,7 @@ async function assertFeatureCheckPass(context4, id, folderOverride, strict, asSt
79104
79203
  strict,
79105
79204
  featuresDir,
79106
79205
  tasksDir,
79206
+ tasksDirs,
79107
79207
  asStatus
79108
79208
  });
79109
79209
  if (!result.pass) {
@@ -79134,9 +79234,13 @@ function registerHistoryCommand(program2, context4) {
79134
79234
  const summary = await svc.analyze(options.since || undefined);
79135
79235
  context4.output.write(options.json ? toJson2(summary) : formatSummary(summary));
79136
79236
  });
79137
- noun.command("report").description("Reserved richer report surface; currently prints a TODO marker.").option("--json", "Output machine-readable JSON where supported").action(async (options) => {
79138
- const message = "TODO: spur history report is reserved for the richer report surface.";
79139
- context4.output.write(options.json ? toJson2({ status: "todo", message }) : message);
79237
+ noun.command("report").description("Reserved richer report surface; not yet implemented (see docs/04_DESIGN.md \xA7spur history report).").option("--json", "Output machine-readable JSON where supported").action(async (options) => {
79238
+ const message = `spur history report is reserved but not yet implemented (P8).
79239
+ ` + `See docs/04_DESIGN.md for the planned surface.
79240
+ ` + `Workaround: use "spur history analyze" for aggregate summaries, or
79241
+ ` + '"spur history import" to load session data, then query the database directly.';
79242
+ context4.output.write(options.json ? toJson2({ status: "not-implemented", message }) : message);
79243
+ context4.setExitCode(0);
79140
79244
  });
79141
79245
  }
79142
79246
  function formatImportResult(r) {
@@ -79164,7 +79268,7 @@ import { join as join19, resolve as resolve6 } from "path";
79164
79268
  var CLI_CONFIG = {
79165
79269
  binaryName: "spur",
79166
79270
  binaryLabel: "spur",
79167
- binaryVersion: "0.3.33",
79271
+ binaryVersion: "0.3.34",
79168
79272
  configDir: ".spur",
79169
79273
  configFile: ".spur/config.yaml",
79170
79274
  databaseFile: ".spur/spur.db"
@@ -86131,7 +86235,9 @@ function createFeatureHandlers(ctx) {
86131
86235
  }),
86132
86236
  sync: os2.feature.sync.handler(async ({ input }) => {
86133
86237
  if (input.direction === "push") {
86134
- throw new Error("Push sync (feature->tasks cascade) is not implemented");
86238
+ throw new HTTPException(501, {
86239
+ message: "Push sync (feature->tasks cascade) is not implemented; use the pull direction or spur feature sync CLI"
86240
+ });
86135
86241
  }
86136
86242
  const svc = ctx.featureService();
86137
86243
  const res = await svc.syncFeature(input.id);
@@ -89835,7 +89941,7 @@ var spur_config_schema_default = {
89835
89941
  },
89836
89942
  executors: {
89837
89943
  type: "array",
89838
- description: "Named executor profiles: each pairs a canonical agent with an optional model override. Referenced by name from `default` and `default-by-phase`.",
89944
+ description: "Named executor profiles: each pairs a canonical agent with an optional model override. Referenced by name from `agent.default` and stage model_policy (default-by-phase removed 0452).",
89839
89945
  items: {
89840
89946
  type: "object",
89841
89947
  required: ["name", "agent"],
@@ -89863,13 +89969,6 @@ var spur_config_schema_default = {
89863
89969
  }
89864
89970
  }
89865
89971
  },
89866
- "default-by-phase": {
89867
- type: "object",
89868
- description: "DEPRECATED (ADR-033). Backward-compat map of dev phase (e.g. dev-run, dev-review) to executor selector. Prefer executor `tier` + stage-registry model_policy. When present, a matching phase mapping is still authoritative (fail-fast; no fall back to `default`) and shadows stage routing for that phase.",
89869
- additionalProperties: {
89870
- type: "string"
89871
- }
89872
- },
89873
89972
  team: {
89874
89973
  type: "object",
89875
89974
  description: "Declarative agent teams keyed by team id (the map key is the teamId). Each team materializes a roster of agent specs (feature M). Mirrors @gobing-ai/spur-config TeamConfigSchema (Zod is SSOT).",
@@ -89897,7 +89996,10 @@ var spur_config_schema_default = {
89897
89996
  description: "Roster of member references (>= 1). A bare string is shorthand for { executor: <string> }; the composed agent id is <teamId>-<id ?? executor>.",
89898
89997
  items: {
89899
89998
  oneOf: [
89900
- { type: "string", minLength: 1 },
89999
+ {
90000
+ type: "string",
90001
+ minLength: 1
90002
+ },
89901
90003
  {
89902
90004
  type: "object",
89903
90005
  required: ["executor"],
@@ -89912,20 +90014,34 @@ var spur_config_schema_default = {
89912
90014
  minLength: 1,
89913
90015
  description: "Local member id; composed id is <teamId>-<id ?? executor> (must match ^[a-z][a-z0-9_-]{1,63}$)."
89914
90016
  },
89915
- purpose: { type: "string" },
90017
+ purpose: {
90018
+ type: "string"
90019
+ },
89916
90020
  workspace: {
89917
90021
  type: "string",
89918
90022
  minLength: 1,
89919
90023
  description: "Per-member workspace override. A leading `~` is tilde-expanded at load."
89920
90024
  },
89921
- model: { type: "string", minLength: 1 },
89922
- autonomy: { type: "string" },
89923
- systemPrompt: { type: "string" },
90025
+ model: {
90026
+ type: "string",
90027
+ minLength: 1
90028
+ },
90029
+ autonomy: {
90030
+ type: "string"
90031
+ },
90032
+ systemPrompt: {
90033
+ type: "string"
90034
+ },
89924
90035
  command: {
89925
90036
  type: "array",
89926
- items: { type: "string", minLength: 1 }
90037
+ items: {
90038
+ type: "string",
90039
+ minLength: 1
90040
+ }
89927
90041
  },
89928
- autostart: { type: "boolean" }
90042
+ autostart: {
90043
+ type: "boolean"
90044
+ }
89929
90045
  }
89930
90046
  }
89931
90047
  ]
@@ -89941,7 +90057,9 @@ var spur_config_schema_default = {
89941
90057
  properties: {
89942
90058
  paths: {
89943
90059
  type: "array",
89944
- items: { type: "string" },
90060
+ items: {
90061
+ type: "string"
90062
+ },
89945
90063
  description: "Glob paths to rule files."
89946
90064
  }
89947
90065
  }
@@ -89951,7 +90069,9 @@ var spur_config_schema_default = {
89951
90069
  properties: {
89952
90070
  paths: {
89953
90071
  type: "array",
89954
- items: { type: "string" },
90072
+ items: {
90073
+ type: "string"
90074
+ },
89955
90075
  description: "Glob paths to workflow directories."
89956
90076
  }
89957
90077
  }
package/web/index.html CHANGED
@@ -1,2 +1,2 @@
1
1
  <!DOCTYPE html><html lang="en"> <head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Spur Board</title><script type="module">(function(){var e=localStorage.getItem("spur-theme"),t=e||(window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light");document.documentElement.setAttribute("data-theme",t)})();</script><link rel="stylesheet" href="/_astro/index.yAse9IaO.css">
2
- <link rel="stylesheet" href="/_astro/BoardApp.t7x-dYDo.css"></head> <body> <style>astro-island,astro-slot,astro-static-slot{display:contents}</style><script>(()=>{var e=async t=>{await(await t())()};(self.Astro||(self.Astro={})).only=e;window.dispatchEvent(new Event("astro:only"));})();</script><script>(()=>{var g=Object.defineProperty;var w=(c,s,d)=>s in c?g(c,s,{enumerable:!0,configurable:!0,writable:!0,value:d}):c[s]=d;var l=(c,s,d)=>w(c,typeof s!="symbol"?s+"":s,d);var E=new Set(["__proto__","constructor","prototype"]);{let c={0:t=>y(t),1:t=>d(t),2:t=>new RegExp(t),3:t=>new Date(t),4:t=>new Map(d(t)),5:t=>new Set(d(t)),6:t=>BigInt(t),7:t=>new URL(t),8:t=>new Uint8Array(t),9:t=>new Uint16Array(t),10:t=>new Uint32Array(t),11:t=>Number.POSITIVE_INFINITY*t},s=t=>{let[p,e]=t;return p in c?c[p](e):void 0},d=t=>t.map(s),y=t=>typeof t!="object"||t===null?t:Object.fromEntries(Object.entries(t).map(([p,e])=>[p,s(e)]));class f extends HTMLElement{constructor(){super(...arguments);l(this,"Component");l(this,"hydrator");l(this,"hydrate",async()=>{var b;if(!this.hydrator||!this.isConnected)return;let e=(b=this.parentElement)==null?void 0:b.closest("astro-island[ssr]");if(e){e.addEventListener("astro:hydrate",this.hydrate,{once:!0});return}let n=this.querySelectorAll("astro-slot"),r={},i=this.querySelectorAll("template[data-astro-template]");for(let o of i){let a=o.closest(this.tagName);a!=null&&a.isSameNode(this)&&(r[o.getAttribute("data-astro-template")||"default"]=o.innerHTML,o.remove())}for(let o of n){let a=o.closest(this.tagName);a!=null&&a.isSameNode(this)&&(r[o.getAttribute("name")||"default"]=o.innerHTML)}let u;try{u=this.hasAttribute("props")?y(JSON.parse(this.getAttribute("props"))):{}}catch(o){let a=this.getAttribute("component-url")||"<unknown>",v=this.getAttribute("component-export");throw v&&(a+=` (export ${v})`),console.error(`[hydrate] Error parsing props for component ${a}`,this.getAttribute("props"),o),o}let h;await this.hydrator(this)(this.Component,u,r,{client:this.getAttribute("client")}),this.removeAttribute("ssr"),this.dispatchEvent(new CustomEvent("astro:hydrate"))});l(this,"unmount",()=>{this.isConnected||this.dispatchEvent(new CustomEvent("astro:unmount"))})}disconnectedCallback(){document.removeEventListener("astro:after-swap",this.unmount),document.addEventListener("astro:after-swap",this.unmount,{once:!0})}connectedCallback(){if(!this.hasAttribute("await-children")||document.readyState==="interactive"||document.readyState==="complete")this.childrenConnectedCallback();else{let e=()=>{document.removeEventListener("DOMContentLoaded",e),n.disconnect(),this.childrenConnectedCallback()},n=new MutationObserver(()=>{var r;((r=this.lastChild)==null?void 0:r.nodeType)===Node.COMMENT_NODE&&this.lastChild.nodeValue==="astro:end"&&(this.lastChild.remove(),e())});n.observe(this,{childList:!0}),document.addEventListener("DOMContentLoaded",e)}}async childrenConnectedCallback(){let e=this.getAttribute("before-hydration-url");e&&await import(e),this.start()}getRetryImportUrl(e){let n=new URL(e,document.baseURI),r=`astro-retry=${Date.now()}`,i=n.hash.replace(/^#/,"");return n.hash=i?`${i}&${r}`:r,n.toString()}async importWithRetry(e){try{return await import(e)}catch(n){return await new Promise(r=>setTimeout(r,1e3)),import(this.getRetryImportUrl(e))}}handleHydrationError(e){let n=this.getAttribute("component-url"),r=new CustomEvent("astro:hydration-error",{cancelable:!0,bubbles:!0,composed:!0,detail:{error:e,componentUrl:n}});this.dispatchEvent(r)&&console.error(`[astro-island] Error hydrating ${n}`,e)}async start(){let e=JSON.parse(this.getAttribute("opts")),n=this.getAttribute("client");if(Astro[n]===void 0){window.addEventListener(`astro:${n}`,()=>this.start(),{once:!0});return}try{await Astro[n](async()=>{let r=this.getAttribute("renderer-url");try{let[i,{default:u}]=await Promise.all([this.importWithRetry(this.getAttribute("component-url")),r?this.importWithRetry(r):Promise.resolve({default:()=>()=>{}})]),h=this.getAttribute("component-export")||"default";if(h.includes(".")){this.Component=i;for(let m of h.split(".")){if(E.has(m)||!this.Component||typeof this.Component!="object"&&typeof this.Component!="function"||!Object.hasOwn(this.Component,m))throw new Error(`Invalid component export path: ${h}`);this.Component=this.Component[m]}}else{if(E.has(h))throw new Error(`Invalid component export path: ${h}`);this.Component=i[h]}return this.hydrator=u,this.hydrate}catch(i){return this.handleHydrationError(i),()=>{}}},e,this)}catch(r){this.handleHydrationError(r)}}attributeChangedCallback(){this.hydrate()}}l(f,"observedAttributes",["props"]),customElements.get("astro-island")||customElements.define("astro-island",f)}})();</script><astro-island uid="WVsOk" component-url="/_astro/BoardApp.CBVEjvSW.js" component-export="default" renderer-url="/_astro/client.CdFpTatq.js" props="{}" ssr client="only" opts="{&quot;name&quot;:&quot;BoardApp&quot;,&quot;value&quot;:&quot;react&quot;}"></astro-island> </body></html>
2
+ <link rel="stylesheet" href="/_astro/BoardApp.t7x-dYDo.css"></head> <body> <style>astro-island,astro-slot,astro-static-slot{display:contents}</style><script>(()=>{var e=async t=>{await(await t())()};(self.Astro||(self.Astro={})).only=e;window.dispatchEvent(new Event("astro:only"));})();</script><script>(()=>{var g=Object.defineProperty;var w=(c,s,d)=>s in c?g(c,s,{enumerable:!0,configurable:!0,writable:!0,value:d}):c[s]=d;var l=(c,s,d)=>w(c,typeof s!="symbol"?s+"":s,d);var E=new Set(["__proto__","constructor","prototype"]);{let c={0:t=>y(t),1:t=>d(t),2:t=>new RegExp(t),3:t=>new Date(t),4:t=>new Map(d(t)),5:t=>new Set(d(t)),6:t=>BigInt(t),7:t=>new URL(t),8:t=>new Uint8Array(t),9:t=>new Uint16Array(t),10:t=>new Uint32Array(t),11:t=>Number.POSITIVE_INFINITY*t},s=t=>{let[p,e]=t;return p in c?c[p](e):void 0},d=t=>t.map(s),y=t=>typeof t!="object"||t===null?t:Object.fromEntries(Object.entries(t).map(([p,e])=>[p,s(e)]));class f extends HTMLElement{constructor(){super(...arguments);l(this,"Component");l(this,"hydrator");l(this,"hydrate",async()=>{var b;if(!this.hydrator||!this.isConnected)return;let e=(b=this.parentElement)==null?void 0:b.closest("astro-island[ssr]");if(e){e.addEventListener("astro:hydrate",this.hydrate,{once:!0});return}let n=this.querySelectorAll("astro-slot"),r={},i=this.querySelectorAll("template[data-astro-template]");for(let o of i){let a=o.closest(this.tagName);a!=null&&a.isSameNode(this)&&(r[o.getAttribute("data-astro-template")||"default"]=o.innerHTML,o.remove())}for(let o of n){let a=o.closest(this.tagName);a!=null&&a.isSameNode(this)&&(r[o.getAttribute("name")||"default"]=o.innerHTML)}let u;try{u=this.hasAttribute("props")?y(JSON.parse(this.getAttribute("props"))):{}}catch(o){let a=this.getAttribute("component-url")||"<unknown>",v=this.getAttribute("component-export");throw v&&(a+=` (export ${v})`),console.error(`[hydrate] Error parsing props for component ${a}`,this.getAttribute("props"),o),o}let h;await this.hydrator(this)(this.Component,u,r,{client:this.getAttribute("client")}),this.removeAttribute("ssr"),this.dispatchEvent(new CustomEvent("astro:hydrate"))});l(this,"unmount",()=>{this.isConnected||this.dispatchEvent(new CustomEvent("astro:unmount"))})}disconnectedCallback(){document.removeEventListener("astro:after-swap",this.unmount),document.addEventListener("astro:after-swap",this.unmount,{once:!0})}connectedCallback(){if(!this.hasAttribute("await-children")||document.readyState==="interactive"||document.readyState==="complete")this.childrenConnectedCallback();else{let e=()=>{document.removeEventListener("DOMContentLoaded",e),n.disconnect(),this.childrenConnectedCallback()},n=new MutationObserver(()=>{var r;((r=this.lastChild)==null?void 0:r.nodeType)===Node.COMMENT_NODE&&this.lastChild.nodeValue==="astro:end"&&(this.lastChild.remove(),e())});n.observe(this,{childList:!0}),document.addEventListener("DOMContentLoaded",e)}}async childrenConnectedCallback(){let e=this.getAttribute("before-hydration-url");e&&await import(e),this.start()}getRetryImportUrl(e){let n=new URL(e,document.baseURI),r=`astro-retry=${Date.now()}`,i=n.hash.replace(/^#/,"");return n.hash=i?`${i}&${r}`:r,n.toString()}async importWithRetry(e){try{return await import(e)}catch(n){return await new Promise(r=>setTimeout(r,1e3)),import(this.getRetryImportUrl(e))}}handleHydrationError(e){let n=this.getAttribute("component-url"),r=new CustomEvent("astro:hydration-error",{cancelable:!0,bubbles:!0,composed:!0,detail:{error:e,componentUrl:n}});this.dispatchEvent(r)&&console.error(`[astro-island] Error hydrating ${n}`,e)}async start(){let e=JSON.parse(this.getAttribute("opts")),n=this.getAttribute("client");if(Astro[n]===void 0){window.addEventListener(`astro:${n}`,()=>this.start(),{once:!0});return}try{await Astro[n](async()=>{let r=this.getAttribute("renderer-url");try{let[i,{default:u}]=await Promise.all([this.importWithRetry(this.getAttribute("component-url")),r?this.importWithRetry(r):Promise.resolve({default:()=>()=>{}})]),h=this.getAttribute("component-export")||"default";if(h.includes(".")){this.Component=i;for(let m of h.split(".")){if(E.has(m)||!this.Component||typeof this.Component!="object"&&typeof this.Component!="function"||!Object.hasOwn(this.Component,m))throw new Error(`Invalid component export path: ${h}`);this.Component=this.Component[m]}}else{if(E.has(h))throw new Error(`Invalid component export path: ${h}`);this.Component=i[h]}return this.hydrator=u,this.hydrate}catch(i){return this.handleHydrationError(i),()=>{}}},e,this)}catch(r){this.handleHydrationError(r)}}attributeChangedCallback(){this.hydrate()}}l(f,"observedAttributes",["props"]),customElements.get("astro-island")||customElements.define("astro-island",f)}})();</script><astro-island uid="Z1fJt2d" component-url="/_astro/BoardApp.CBVEjvSW.js" component-export="default" renderer-url="/_astro/client.CdFpTatq.js" props="{}" ssr client="only" opts="{&quot;name&quot;:&quot;BoardApp&quot;,&quot;value&quot;:&quot;react&quot;}"></astro-island> </body></html>