@gobing-ai/spur 0.3.43 → 0.3.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/templates/AGENTS.md +5 -0
  3. package/config/workflows/idea-pipeline.yaml +205 -33
  4. package/config/workflows/task-pipeline.yaml +1 -1
  5. package/package.json +1 -1
  6. package/plugins/sp/commands/dev-runall.md +3 -1
  7. package/plugins/sp/plugin.json +1 -1
  8. package/plugins/sp/skills/spur-cli/SKILL.md +3 -0
  9. package/plugins/sp/skills/spur-cli/references/features/hierarchy-mece.md +8 -3
  10. package/plugins/sp/skills/spur-cli/references/tasks.md +22 -0
  11. package/plugins/sp/skills/spur-dev/SKILL.md +12 -4
  12. package/plugins/sp/skills/spur-dev/references/dev-operations.md +1 -1
  13. package/plugins/sp/skills/spur-dev/references/execution-batch.md +12 -3
  14. package/plugins/sp/skills/spur-dev/references/flag-glossary.md +1 -1
  15. package/plugins/sp/skills/spur-dev/references/planning-workflow.md +53 -0
  16. package/spur.js +147 -67
  17. package/web/_astro/{BoardApp.8sc6Rb1t.js → BoardApp.BnhkWEOG.js} +99 -99
  18. package/web/_astro/BoardApp.CxOS4HSo.js +1 -0
  19. package/web/_astro/{TaskDetail.DOKJP-Kp.js → TaskDetail.B6yiT-U7.js} +1 -1
  20. package/web/_astro/{arc.CTqmqspU.js → arc.PbmgYm3_.js} +1 -1
  21. package/web/_astro/{architectureDiagram-3BPJPVTR.B0iBdehs.js → architectureDiagram-3BPJPVTR.BkOMSdmD.js} +1 -1
  22. package/web/_astro/{blockDiagram-GPEHLZMM.BHE5UFj5.js → blockDiagram-GPEHLZMM.BtomoUdy.js} +1 -1
  23. package/web/_astro/{c4Diagram-AAUBKEIU.DQApkgGM.js → c4Diagram-AAUBKEIU.UzrYwJnF.js} +1 -1
  24. package/web/_astro/channel.DRRsElzO.js +1 -0
  25. package/web/_astro/{chunk-2J33WTMH.fPrKLM8v.js → chunk-2J33WTMH.nNChLHkw.js} +1 -1
  26. package/web/_astro/{chunk-4BX2VUAB.DExvwBhn.js → chunk-4BX2VUAB.DLYAecPo.js} +1 -1
  27. package/web/_astro/{chunk-55IACEB6.EjNkjuD6.js → chunk-55IACEB6.t5bzuj1J.js} +1 -1
  28. package/web/_astro/{chunk-727SXJPM.Dl8xy5rT.js → chunk-727SXJPM.DBV62bIy.js} +1 -1
  29. package/web/_astro/{chunk-AQP2D5EJ.BSzMBgBs.js → chunk-AQP2D5EJ.BdaPhTPs.js} +1 -1
  30. package/web/_astro/{chunk-FMBD7UC4.CAeOHpbb.js → chunk-FMBD7UC4.eoZ88KMf.js} +1 -1
  31. package/web/_astro/{chunk-ND2GUHAM.Bbeu_ZOH.js → chunk-ND2GUHAM.QdNDyeSh.js} +1 -1
  32. package/web/_astro/{chunk-QZHKN3VN.Da_GaIAZ.js → chunk-QZHKN3VN.DPaToLfx.js} +1 -1
  33. package/web/_astro/{classDiagram-4FO5ZUOK.sMfFYSQp.js → classDiagram-4FO5ZUOK.By9BMe7b.js} +1 -1
  34. package/web/_astro/{classDiagram-v2-Q7XG4LA2.sMfFYSQp.js → classDiagram-v2-Q7XG4LA2.By9BMe7b.js} +1 -1
  35. package/web/_astro/{cose-bilkent-S5V4N54A.PS3IwOH4.js → cose-bilkent-S5V4N54A.DMypl6q_.js} +1 -1
  36. package/web/_astro/{dagre-BM42HDAG.EQCx8a6D.js → dagre-BM42HDAG.p26m9grg.js} +1 -1
  37. package/web/_astro/{diagram-2AECGRRQ.aEFQl8v8.js → diagram-2AECGRRQ.C6m3oTIu.js} +1 -1
  38. package/web/_astro/{diagram-5GNKFQAL.jyN4tqFX.js → diagram-5GNKFQAL.JCxivn2z.js} +1 -1
  39. package/web/_astro/{diagram-KO2AKTUF.DaTVHnme.js → diagram-KO2AKTUF.DifbEd3P.js} +1 -1
  40. package/web/_astro/{diagram-LMA3HP47.DWsGRLAN.js → diagram-LMA3HP47.Bffga-cf.js} +1 -1
  41. package/web/_astro/{diagram-OG6HWLK6.DYxmOViu.js → diagram-OG6HWLK6.CUKx5_Cx.js} +1 -1
  42. package/web/_astro/{erDiagram-TEJ5UH35.BKM75coa.js → erDiagram-TEJ5UH35.DaJ9KZR0.js} +1 -1
  43. package/web/_astro/{flowDiagram-I6XJVG4X.B7qpqDW5.js → flowDiagram-I6XJVG4X.Do6l5pzg.js} +1 -1
  44. package/web/_astro/{ganttDiagram-6RSMTGT7.CYBL-hwy.js → ganttDiagram-6RSMTGT7.DqBKgB-3.js} +1 -1
  45. package/web/_astro/{gitGraphDiagram-PVQCEYII.Cz4ly3Kq.js → gitGraphDiagram-PVQCEYII.B3VLSQ22.js} +1 -1
  46. package/web/_astro/{index.yAse9IaO.css → index.QfZ9SC3X.css} +1 -1
  47. package/web/_astro/{infoDiagram-5YYISTIA.CIS7cXrJ.js → infoDiagram-5YYISTIA.hK8ZC1oa.js} +1 -1
  48. package/web/_astro/{ishikawaDiagram-YF4QCWOH.DQ-pmaOG.js → ishikawaDiagram-YF4QCWOH.C8DXwh9Z.js} +1 -1
  49. package/web/_astro/{journeyDiagram-JHISSGLW.DVS6F7UM.js → journeyDiagram-JHISSGLW.mj5aHQDb.js} +1 -1
  50. package/web/_astro/{kanban-definition-UN3LZRKU.D8t-G-U6.js → kanban-definition-UN3LZRKU.Dwlhi49r.js} +1 -1
  51. package/web/_astro/{linear.IO8rTd_n.js → linear.7zBHdlwl.js} +1 -1
  52. package/web/_astro/{mermaid.core.CjVnvFNf.js → mermaid.core.AYdA0EJr.js} +4 -4
  53. package/web/_astro/{mindmap-definition-RKZ34NQL.DZoEMTnU.js → mindmap-definition-RKZ34NQL.BSnniHEq.js} +1 -1
  54. package/web/_astro/{pieDiagram-4H26LBE5.86r1QWnX.js → pieDiagram-4H26LBE5.B7EVpA3J.js} +1 -1
  55. package/web/_astro/{quadrantDiagram-W4KKPZXB.DzFA9uPk.js → quadrantDiagram-W4KKPZXB.rg4i8YOY.js} +1 -1
  56. package/web/_astro/{requirementDiagram-4Y6WPE33.dQvdvfMr.js → requirementDiagram-4Y6WPE33.C4pr792x.js} +1 -1
  57. package/web/_astro/{sankeyDiagram-5OEKKPKP.BvC-VNI8.js → sankeyDiagram-5OEKKPKP.DTXU996O.js} +1 -1
  58. package/web/_astro/{sequenceDiagram-3UESZ5HK.DDQNwHs8.js → sequenceDiagram-3UESZ5HK.D73DOjzi.js} +1 -1
  59. package/web/_astro/{stateDiagram-AJRCARHV.BKwYy-tQ.js → stateDiagram-AJRCARHV.CVkeSaEH.js} +1 -1
  60. package/web/_astro/{stateDiagram-v2-BHNVJYJU.KxCiRYYf.js → stateDiagram-v2-BHNVJYJU.DkBGwoN5.js} +1 -1
  61. package/web/_astro/{timeline-definition-PNZ67QCA.Bh9NDjOx.js → timeline-definition-PNZ67QCA.DnJ9Yh7G.js} +1 -1
  62. package/web/_astro/{vennDiagram-CIIHVFJN.DIM-n6us.js → vennDiagram-CIIHVFJN.DN1tvR4Y.js} +1 -1
  63. package/web/_astro/{wardley-L42UT6IY.DW15qOFQ.js → wardley-L42UT6IY.D54jJ-7-.js} +1 -1
  64. package/web/_astro/{wardleyDiagram-YWT4CUSO.UTIvjsG9.js → wardleyDiagram-YWT4CUSO.C2grUBbE.js} +1 -1
  65. package/web/_astro/{xychartDiagram-2RQKCTM6.CQYhkVDS.js → xychartDiagram-2RQKCTM6.D05okBnV.js} +1 -1
  66. package/web/index.html +2 -2
  67. package/web/_astro/BoardApp.ChKnY1JD.js +0 -1
  68. package/web/_astro/channel.B0q9WGrH.js +0 -1
@@ -76,7 +76,13 @@ bun run apps/cli/src/index.ts feature check <id> --strict --json
76
76
 
77
77
  - **Abort shape.** A non-zero check aborts the batch immediately: verdict `aborted`, zero attempted
78
78
  tasks, and the structured feature findings (the `--json` finding list) reported verbatim. This is
79
- the same abort vocabulary as cycle / unknown selector (Step 4/Step 5).
79
+ the same abort vocabulary as cycle / unknown selector (Step 4/Step 5). **Scoped to structural
80
+ findings:** `L4.scenario-unverified` is the expected state of any not-yet-run feature (its
81
+ covering tasks have no PASS verdicts yet) and `--strict` elevates it to error, so it is reported
82
+ verbatim but does **not** abort the batch; every other error finding (L1–L3 structural,
83
+ `L4.malformed-verdict-artifact`, `L4.uncovered-feature-scenario`) still aborts. The terminal
84
+ feature-transition gate enforces scenario verification after the batch runs, so pre-run unverified
85
+ scenarios are transient, not defects (dogfood 2026-08-11, feature I2).
80
86
  - **Exactly once.** The check runs once per batch, before `task list`; it is not re-run per task.
81
87
  - **Non-feature exclusion.** Explicit WBS lists, status pseudo-lists, and `ready` selectors add no
82
88
  feature check — only an effective `feature:<id>` selector is feature-derived. When explicit
@@ -84,7 +90,10 @@ bun run apps/cli/src/index.ts feature check <id> --strict --json
84
90
  - **Why.** `FeatureCheckService` emits `L3.scope-delineation` (Scope lacking an In/Out split) as a
85
91
  **warning**; feature-scoped batches never ran a strict check before freezing, so the late feature
86
92
  transition became the first blocking check. A selector-local strict preflight catches a known
87
- strict finding before any task pipeline action without changing corpus-wide severity.
93
+ strict finding before any task pipeline action without changing corpus-wide severity. A not-yet-run
94
+ feature always fails `L4.scenario-unverified` under `--strict`, so that class is
95
+ reported-not-aborting per the Abort shape above — the preflight targets defects, not the
96
+ expected pre-run state.
88
97
  - **Scope.** This preflight is advisory to severity policy: it does not alter `FeatureCheckService`,
89
98
  `L3.scope-delineation` severity, `feature sync`, or batch-create.
90
99
 
@@ -672,7 +681,7 @@ Resume, merge, or discard:
672
681
  discard: git worktree remove <worktree-path> && git branch -D <branch>
673
682
  ```
674
683
 
675
- The report reuses the [`--next` chain contract](flag-glossary.md#-next-chain-contract) halt-report
684
+ The report reuses the [`--next` chain contract](flag-glossary.md#--next-chain-contract) halt-report
676
685
  shape (halt cause + where + why), not new vocabulary. Retention is the right default: these batches
677
686
  are long and already resumable via `--continue`; auto-deleting is data loss, auto-merging is a
678
687
  partial result presented as a whole. The answer to "what happens if it fails" is "nothing happens,
@@ -366,7 +366,7 @@ swallowed as the name. `--worktree=<name>` is the unambiguous spelling. `/sp:dev
366
366
  the flag (single step; not worth the worktree cost), and `--worktree --mode parallel` is rejected
367
367
  (per-task parallel isolation stays task 0142). The full lifecycle — name resolution, dirty-tree
368
368
  precheck, creation or adoption, crash-safe marker, merge-or-retain, and `--continue` re-entry — is
369
- specified in [execution-batch.md § Worktree isolation](execution-batch.md#worktree-isolation---worktree).
369
+ specified in [execution-batch.md § Worktree isolation](execution-batch.md#worktree-isolation---worktree-name).
370
370
  Portable `git worktree` commands only; the git mechanics are reused from
371
371
  [worktree-patterns.md](../../branch-workflow/references/worktree-patterns.md).
372
372
 
@@ -217,6 +217,59 @@ fires, author the doc and **report** the chosen slug and a one-line rationale ("
217
217
  `docs/design/<slug>.md` — new `spur <noun>` command + config key"); when it does not fire, report the
218
218
  skip and why. Do not pause to ask; the operator reviews the satellite afterward.
219
219
 
220
+ ## Step 5.6: Idea pipeline (sp:dev-idea) — planning handoff contracts
221
+
222
+ `/sp:dev-idea` runs the same planning half through the idea-pipeline workflow definition
223
+ (`.spur/workflows/idea-pipeline.yaml` — symlinked to the tracked SSOT). Two
224
+ artifacts are the contract between the workflow states and the operator:
225
+
226
+ **Goal/Scope intent (feature-create).** The agent writes body-only intent files
227
+ `.spur/run/<runId>-idea-goal.md` and `.spur/run/<runId>-idea-scope.md`, then persists both through
228
+ the corpus CLI (a missing/empty artifact stops the state before decomposition can begin):
229
+
230
+ ```bash
231
+ spur feature update <id> --section Goal --from-file .spur/run/<runId>-idea-goal.md
232
+ spur feature update <id> --section Scope --from-file .spur/run/<runId>-idea-scope.md
233
+ ```
234
+
235
+ - **Goal is intent only** — a short statement of what the feature achieves. Task breakdowns,
236
+ checklists, and how-to steps **never** enter Goal.
237
+ - **Scope carries explicit boundaries** — in-scope and out-of-scope bullets.
238
+
239
+ **Design review artifact (system-design / design-approval).** One run-scoped file,
240
+ `.spur/run/<runId>-idea-design-review.md`, with fixed headings `## Proposed design`,
241
+ `## Operator feedback`, and `## Reconciliation`:
242
+
243
+ - **First pass:** system-design writes the proposed design under `## Proposed design`.
244
+ - **Rejection:** before answering `no` at the design-approval gate, the operator edits
245
+ `## Operator feedback` with the concrete issue(s).
246
+ - **Retry:** system-design reads that feedback, revises the design/ADR artifacts, records the
247
+ changes under `## Reconciliation`, and — when the feedback invalidates an Acceptance Criteria
248
+ scenario — updates the feature AC through `spur feature update <id> --section
249
+ "Acceptance Criteria" --from-file <file>` (never direct feature-file edits).
250
+ - **Exit gate:** every design exit into decomposition (auto-approved and interactive-approved)
251
+ re-runs `spur feature check <id>`, so stale or invalidated AC cannot proceed.
252
+
253
+ **Task ordering (decompose / handoff-finalize).** Decomposition also emits the private
254
+ run-scoped order sidecar `.spur/run/<runId>-idea-task-order.json` — a JSON array of
255
+ `{ name, depends_on_names[] }` (one entry per batch item, names matching batch item `name`s
256
+ exactly; `[]` when no ordering exists). It is private workflow data, not part of
257
+ `task-batch.schema.json`. A post-decompose validation fails the run on any ambiguous or
258
+ missing title match. After batch creation, `handoff-finalize`:
259
+
260
+ 1. Zips batch item names to the created WBS values from the captured
261
+ `.spur/run/<runId>-idea-batch-create-result.json` (`task batch-create --json` output;
262
+ `wbs[]` is in input order).
263
+ 2. Applies every non-empty `depends_on_names` list through
264
+ `spur task deps <wbs> set <dep-wbs...> --json` — a mapping or CLI error fails the run
265
+ before handoff.
266
+ 3. Refreshes the feature roster with `spur feature refresh --feature <id> --json`.
267
+ 4. Checks each created task (`spur task check <wbs> --json`) and writes
268
+ `.spur/run/<runId>-idea-handoff.md` with exactly **one** next command:
269
+ `/sp:dev-refineall --feature <id> --auto --depth ready` when any task is unready
270
+ (runall is then omitted), otherwise `/sp:dev-runall --feature <id> --auto`. The terminal
271
+ handoff note points at this report.
272
+
220
273
  ## Step 6: Refine before execute (the spec-completion gate)
221
274
 
222
275
  `batch-create` accepts optional `design` / `plan` / `acceptance_criteria` fields (plus
package/spur.js CHANGED
@@ -51037,7 +51037,7 @@ function stripScenarioPrefixes(title) {
51037
51037
  let previous;
51038
51038
  do {
51039
51039
  previous = out;
51040
- out = out.replace(/^\[[^\]]*\]\s*/, "").replace(/^Scenario:\s*/i, "").replace(/^R\d+\s*[:\-\u2014]?\s*/, "").trim();
51040
+ out = out.replace(/^\[[^\]]*\]\s*/, "").replace(/\s*\[[^\]]*\]\s*$/, "").replace(/^Scenario:\s*/i, "").replace(/^R\d+\s*[:\-\u2014]?\s*/, "").trim();
51041
51041
  } while (out !== previous);
51042
51042
  return out;
51043
51043
  }
@@ -51132,6 +51132,19 @@ function stripAcFence(body) {
51132
51132
  return out.join(`
51133
51133
  `);
51134
51134
  }
51135
+ function looksLikeGherkinAc(body) {
51136
+ return /^\s*(?:Feature:|Scenario:|Scenario Outline:)/m.test(body);
51137
+ }
51138
+ function normalizeAcFence(body) {
51139
+ const trimmed = body.trim();
51140
+ if (trimmed === "" || /^\s*```/m.test(trimmed))
51141
+ return body;
51142
+ if (!looksLikeGherkinAc(trimmed))
51143
+ return body;
51144
+ return `\`\`\`gherkin
51145
+ ${trimmed}
51146
+ \`\`\``;
51147
+ }
51135
51148
 
51136
51149
  // ../../packages/domain/src/bdd/scaffold.ts
51137
51150
  function renderScenarioStub(scenario) {
@@ -60303,6 +60316,11 @@ async function applyCliMigrations(adapter, migrations = CLI_MIGRATIONS) {
60303
60316
  const existing = await adapter.queryFirst('SELECT id FROM "__spur_cli_migrations" WHERE id = ?', migration.id);
60304
60317
  if (existing != null)
60305
60318
  continue;
60319
+ if (migration.id === "0009_spur_cli_history_message_run_idx" && !await tableExists(adapter, "history_message")) {
60320
+ for (const statement of splitSqlStatements(HISTORY_IMPORT_SCHEMA_SQL)) {
60321
+ await adapter.exec(statement);
60322
+ }
60323
+ }
60306
60324
  const addColumnGuard = migration.addColumnIfMissing;
60307
60325
  const shouldApplySql = addColumnGuard === undefined || !await columnExists(adapter, addColumnGuard.table, addColumnGuard.column);
60308
60326
  if (shouldApplySql) {
@@ -60332,6 +60350,10 @@ async function columnExists(adapter, table3, column2) {
60332
60350
  const rows = await adapter.queryAll(`PRAGMA table_info("${table3}")`);
60333
60351
  return rows.some((row) => row.name === column2);
60334
60352
  }
60353
+ async function tableExists(adapter, table3) {
60354
+ const row = await adapter.queryFirst("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?", table3);
60355
+ return row != null;
60356
+ }
60335
60357
  function splitSqlStatements(sql4) {
60336
60358
  return sql4.split(";").map((statement) => statement.trim()).filter(Boolean);
60337
60359
  }
@@ -62829,12 +62851,14 @@ __export(exports_src, {
62829
62851
  normalizeTaskStatus: () => normalizeTaskStatus,
62830
62852
  normalizeFeatureStatus: () => normalizeFeatureStatus,
62831
62853
  normalizeCapabilityTier: () => normalizeCapabilityTier,
62854
+ normalizeAcFence: () => normalizeAcFence,
62832
62855
  messageRollup: () => messageRollup,
62833
62856
  mergeStubs: () => mergeStubs,
62834
62857
  matrixEntrySchema: () => matrixEntrySchema,
62835
62858
  matchEtlPayloads: () => matchEtlPayloads,
62836
62859
  matchEtlForAction: () => matchEtlForAction,
62837
62860
  loops: () => loops,
62861
+ looksLikeGherkinAc: () => looksLikeGherkinAc,
62838
62862
  loadSqlMigrations: () => loadSqlMigrations,
62839
62863
  loadAllEtlPayloads: () => loadAllEtlPayloads,
62840
62864
  isTierEligible: () => isTierEligible,
@@ -66898,7 +66922,8 @@ class FeatureService {
66898
66922
  const slug = basename2(f.filePath).replace(/^[A-Z][1-9]*_/, "").replace(/\.md$/, "");
66899
66923
  const newPath = `${this.ctx.featuresDir}/${newId}_${slug}.md`;
66900
66924
  removed.push({ path: f.filePath, content: raw });
66901
- await atomicWriteAsync(newPath, doc2.serialize(), newId, this.ctx.fs, this.ctx.projectName ?? "spur");
66925
+ const movedContent = doc2.serialize().replace(/^#\s+[A-Z][1-9]*:/m, `# ${newId}:`);
66926
+ await atomicWriteAsync(newPath, movedContent, newId, this.ctx.fs, this.ctx.projectName ?? "spur");
66902
66927
  created.push(newPath);
66903
66928
  if (newPath !== f.filePath)
66904
66929
  await this.ctx.fs.deleteFile(f.filePath);
@@ -67913,7 +67938,8 @@ function applyMutation(doc2, mutation) {
67913
67938
  break;
67914
67939
  case "updateSection":
67915
67940
  if (mutation.sectionName !== undefined && mutation.sectionBody !== undefined) {
67916
- doc2.replaceSection(mutation.sectionName, mutation.sectionBody);
67941
+ const body = mutation.sectionName.toLowerCase() === "acceptance criteria" ? normalizeAcFence(mutation.sectionBody) : mutation.sectionBody;
67942
+ doc2.replaceSection(mutation.sectionName, body);
67917
67943
  }
67918
67944
  break;
67919
67945
  case "updateBody":
@@ -70087,7 +70113,7 @@ function renderCreatedTaskContent(params) {
70087
70113
  sectionPatches.Plan = (params.plan ?? "").trim();
70088
70114
  }
70089
70115
  if ((params.acceptanceCriteria ?? "").trim() !== "") {
70090
- sectionPatches["Acceptance Criteria"] = (params.acceptanceCriteria ?? "").trim();
70116
+ sectionPatches["Acceptance Criteria"] = normalizeAcFence((params.acceptanceCriteria ?? "").trim());
70091
70117
  }
70092
70118
  if (Object.keys(sectionPatches).length > 0) {
70093
70119
  const doc2 = MarkdownDocument.parse(content, "task");
@@ -70758,7 +70784,7 @@ ${issues}`);
70758
70784
  taskBodies.Plan = (item.plan ?? "").trim();
70759
70785
  }
70760
70786
  if ((item.acceptance_criteria ?? "").trim() !== "") {
70761
- taskBodies["Acceptance Criteria"] = (item.acceptance_criteria ?? "").trim();
70787
+ taskBodies["Acceptance Criteria"] = normalizeAcFence((item.acceptance_criteria ?? "").trim());
70762
70788
  }
70763
70789
  const content = buildTaskSkeleton({
70764
70790
  wbs,
@@ -71279,7 +71305,7 @@ var init_task_verdict = __esm(() => {
71279
71305
  });
71280
71306
 
71281
71307
  // ../../packages/app/src/services/team-service.ts
71282
- import { join as join16 } from "path";
71308
+ import { join as join16, resolve as resolve6 } from "path";
71283
71309
 
71284
71310
  class TeamService {
71285
71311
  ctx;
@@ -71487,13 +71513,18 @@ class TeamService {
71487
71513
  const specs = await loadAgentSpecs(this.configDir);
71488
71514
  const config4 = await this.loadTeamConfig();
71489
71515
  const teams = new Map;
71516
+ const configTeamIds = new Set;
71490
71517
  if (config4?.agent?.team) {
71491
71518
  for (const [teamId, teamConfig] of Object.entries(config4.agent.team)) {
71519
+ configTeamIds.add(teamId);
71520
+ const workDir = this.resolveWorkspaceDir(teamConfig.work_dir);
71492
71521
  teams.set(teamId, {
71493
71522
  teamId,
71494
71523
  name: teamConfig.name,
71495
71524
  members: [],
71496
- specs: []
71525
+ specs: [],
71526
+ workDir,
71527
+ isCurrentProject: this.isCurrentProject(workDir)
71497
71528
  });
71498
71529
  }
71499
71530
  }
@@ -71510,7 +71541,9 @@ class TeamService {
71510
71541
  teamId,
71511
71542
  name: teamId,
71512
71543
  members: [],
71513
- specs: [spec]
71544
+ specs: [spec],
71545
+ workDir: null,
71546
+ isCurrentProject: false
71514
71547
  });
71515
71548
  }
71516
71549
  } else {
@@ -71522,9 +71555,18 @@ class TeamService {
71522
71555
  teamId: "__untethered__",
71523
71556
  name: "Untethered",
71524
71557
  members: [],
71525
- specs: untethered
71558
+ specs: untethered,
71559
+ workDir: null,
71560
+ isCurrentProject: false
71526
71561
  });
71527
71562
  }
71563
+ for (const team of Array.from(teams.values())) {
71564
+ if (configTeamIds.has(team.teamId))
71565
+ continue;
71566
+ const common4 = this.commonWorkspace(team.specs);
71567
+ team.workDir = common4.workDir;
71568
+ team.isCurrentProject = common4.isCurrentProject;
71569
+ }
71528
71570
  return Array.from(teams.values());
71529
71571
  }
71530
71572
  async materializeTeam(teamId, opts) {
@@ -71613,6 +71655,23 @@ class TeamService {
71613
71655
  });
71614
71656
  return result;
71615
71657
  }
71658
+ resolveWorkspaceDir(p) {
71659
+ return resolve6(this.ctx.cwd, p);
71660
+ }
71661
+ isCurrentProject(workDir) {
71662
+ return resolve6(this.ctx.cwd, workDir) === resolve6(this.ctx.cwd, this.ctx.cwd);
71663
+ }
71664
+ commonWorkspace(specs) {
71665
+ if (specs.length === 0)
71666
+ return { workDir: null, isCurrentProject: false };
71667
+ const first = specs[0]?.workspace;
71668
+ if (typeof first !== "string" || first.length === 0)
71669
+ return { workDir: null, isCurrentProject: false };
71670
+ if (!specs.every((s2) => s2.workspace === first))
71671
+ return { workDir: null, isCurrentProject: false };
71672
+ const workDir = this.resolveWorkspaceDir(first);
71673
+ return { workDir, isCurrentProject: this.isCurrentProject(workDir) };
71674
+ }
71616
71675
  async loadTeamConfig() {
71617
71676
  try {
71618
71677
  return await loadSpurConfig(this.ctx.cwd);
@@ -72050,16 +72109,16 @@ class WorkflowSteeringController {
72050
72109
  active.state = "boundary";
72051
72110
  active.version += 1;
72052
72111
  this.publishState(active);
72053
- return await new Promise((resolve6) => {
72112
+ return await new Promise((resolve7) => {
72054
72113
  const settle = (decision) => {
72055
72114
  clearTimeout(timer);
72056
- resolve6(decision);
72115
+ resolve7(decision);
72057
72116
  };
72058
72117
  const timer = setTimeout(() => {
72059
72118
  if (active.resolve !== settle)
72060
72119
  return;
72061
72120
  active.resolve = undefined;
72062
- this.acceptDefault(active, resolve6);
72121
+ this.acceptDefault(active, resolve7);
72063
72122
  }, active.policy.timeoutMs);
72064
72123
  timer.unref?.();
72065
72124
  active.resolve = settle;
@@ -72157,7 +72216,7 @@ class WorkflowSteeringController {
72157
72216
  deadlineAt: new Date(Date.now() + deadlineMs).toISOString()
72158
72217
  });
72159
72218
  }
72160
- acceptDefault(active, resolve6) {
72219
+ acceptDefault(active, resolve7) {
72161
72220
  const command = {
72162
72221
  commandId: crypto.randomUUID(),
72163
72222
  runId: active.runId,
@@ -72182,7 +72241,7 @@ class WorkflowSteeringController {
72182
72241
  at: new Date().toISOString()
72183
72242
  };
72184
72243
  this.onAck?.(ack);
72185
- resolve6({ operation: "continue" });
72244
+ resolve7({ operation: "continue" });
72186
72245
  }
72187
72246
  accept(command, active, note) {
72188
72247
  return this.publish({
@@ -73555,7 +73614,7 @@ var init_builtins2 = __esm(() => {
73555
73614
 
73556
73615
  // ../../packages/app/src/services/workflow-service.ts
73557
73616
  import { homedir as homedir5 } from "os";
73558
- import { join as join19, resolve as resolve6 } from "path";
73617
+ import { join as join19, resolve as resolve7 } from "path";
73559
73618
  function signalSubprocess(pid) {
73560
73619
  if (!Number.isInteger(pid) || pid <= 1)
73561
73620
  return false;
@@ -73625,7 +73684,7 @@ class WorkflowAppService {
73625
73684
  };
73626
73685
  }
73627
73686
  async validate(file2, opts = {}) {
73628
- const absolute = resolve6(this.ctx.cwd, file2);
73687
+ const absolute = resolve7(this.ctx.cwd, file2);
73629
73688
  if (!await fileExists(absolute)) {
73630
73689
  return { ok: false, valid: false, file: file2, errors: [`File not found: ${absolute}`] };
73631
73690
  }
@@ -73684,7 +73743,7 @@ class WorkflowAppService {
73684
73743
  ...opts.vars ?? {},
73685
73744
  __runId: runId
73686
73745
  };
73687
- const absolute = resolve6(this.ctx.cwd, file2);
73746
+ const absolute = resolve7(this.ctx.cwd, file2);
73688
73747
  const embedded = this.embeddedSchemaOptions();
73689
73748
  const workflow = await loadWorkflowDef(absolute, {
73690
73749
  validateSchema: true,
@@ -73720,7 +73779,7 @@ class WorkflowAppService {
73720
73779
  return;
73721
73780
  let workflowName;
73722
73781
  try {
73723
- const def = await loadWorkflowDef(resolve6(this.ctx.cwd, file2), { validateSchema: false });
73782
+ const def = await loadWorkflowDef(resolve7(this.ctx.cwd, file2), { validateSchema: false });
73724
73783
  workflowName = def.name;
73725
73784
  } catch {
73726
73785
  return;
@@ -73845,7 +73904,7 @@ class WorkflowAppService {
73845
73904
  if (entry === undefined)
73846
73905
  return null;
73847
73906
  for (const layer of listing.layers) {
73848
- const abs = resolve6(layer.path, entry.path);
73907
+ const abs = resolve7(layer.path, entry.path);
73849
73908
  if (await fileExists(abs)) {
73850
73909
  try {
73851
73910
  return await loadWorkflowDef(abs, { validateSchema: false });
@@ -73861,7 +73920,7 @@ class WorkflowAppService {
73861
73920
  const entries = [];
73862
73921
  const scannedPaths = new Set;
73863
73922
  for (const relPath of workflowPaths) {
73864
- const absPath = resolve6(projectRoot, relPath);
73923
+ const absPath = resolve7(projectRoot, relPath);
73865
73924
  layers.push({ id: "project", path: absPath });
73866
73925
  scannedPaths.add(absPath);
73867
73926
  try {
@@ -73870,7 +73929,7 @@ class WorkflowAppService {
73870
73929
  } catch {}
73871
73930
  }
73872
73931
  for (const relPath of workflowPaths) {
73873
- const absPath = resolve6(globalRoot, relPath);
73932
+ const absPath = resolve7(globalRoot, relPath);
73874
73933
  if (scannedPaths.has(absPath))
73875
73934
  continue;
73876
73935
  layers.push({ id: "global", path: absPath });
@@ -74126,7 +74185,7 @@ async function scanWorkflowFiles(rootPath, source) {
74126
74185
  async function walk3(dirPath) {
74127
74186
  const names = await fs3.readDir(dirPath);
74128
74187
  for (const name of names) {
74129
- const fullPath = resolve6(dirPath, name);
74188
+ const fullPath = resolve7(dirPath, name);
74130
74189
  const st = await fs3.stat(fullPath);
74131
74190
  if (!st)
74132
74191
  continue;
@@ -82132,15 +82191,15 @@ function parseLoopPoll(raw) {
82132
82191
  return Number.isFinite(n2) && n2 > 0 ? n2 : DEFAULT_LOOP_POLL_MS;
82133
82192
  }
82134
82193
  function loopSleep(ms, signal) {
82135
- return new Promise((resolve7) => {
82194
+ return new Promise((resolve8) => {
82136
82195
  if (signal?.aborted) {
82137
- resolve7();
82196
+ resolve8();
82138
82197
  return;
82139
82198
  }
82140
- const timer = setTimeout(resolve7, ms);
82199
+ const timer = setTimeout(resolve8, ms);
82141
82200
  signal?.addEventListener("abort", () => {
82142
82201
  clearTimeout(timer);
82143
- resolve7();
82202
+ resolve8();
82144
82203
  }, { once: true });
82145
82204
  });
82146
82205
  }
@@ -82200,7 +82259,7 @@ init_loader();
82200
82259
  init_src2();
82201
82260
  import { existsSync as existsSync10 } from "fs";
82202
82261
  import { homedir as homedir6 } from "os";
82203
- import { join as join21, resolve as resolve7 } from "path";
82262
+ import { join as join21, resolve as resolve8 } from "path";
82204
82263
 
82205
82264
  // src/workflow/resolve-spur-bin.ts
82206
82265
  import { basename as basename4 } from "path";
@@ -82221,7 +82280,7 @@ function resolveSpurBin() {
82221
82280
  var GLOBAL_CONFIG_DIR = join21(".config", "spur");
82222
82281
  function globalConfigRoot(context4) {
82223
82282
  const override = context4.env.SPUR_GLOBAL_RULES_DIR;
82224
- return override !== undefined && override.length > 0 ? resolve7(context4.cwd, override) : join21(homedir6(), GLOBAL_CONFIG_DIR);
82283
+ return override !== undefined && override.length > 0 ? resolve8(context4.cwd, override) : join21(homedir6(), GLOBAL_CONFIG_DIR);
82225
82284
  }
82226
82285
  function resolveWorkflowPath(context4, profile) {
82227
82286
  const bundledRoot = bundledConfigRoot();
@@ -82626,7 +82685,7 @@ import { createRequire } from "module";
82626
82685
  var CLI_CONFIG = {
82627
82686
  binaryName: "spur",
82628
82687
  binaryLabel: "spur",
82629
- binaryVersion: "0.3.43",
82688
+ binaryVersion: "0.3.44",
82630
82689
  configDir: ".spur",
82631
82690
  configFile: ".spur/config.yaml",
82632
82691
  databaseFile: ".spur/spur.db"
@@ -82878,7 +82937,7 @@ init_loader();
82878
82937
  init_src2();
82879
82938
  init_dist10();
82880
82939
  import { homedir as homedir7 } from "os";
82881
- import { join as join22, resolve as resolve8 } from "path";
82940
+ import { join as join22, resolve as resolve9 } from "path";
82882
82941
 
82883
82942
  // src/config/scaffold-manifest.ts
82884
82943
  var SCAFFOLD_MANIFEST = [
@@ -82957,7 +83016,7 @@ If \`.spur/context/\` is absent, proceed normally. Never block work on its absen
82957
83016
  `;
82958
83017
  function globalRulesRoot(context4) {
82959
83018
  const override = context4.env.SPUR_GLOBAL_RULES_DIR;
82960
- return override !== undefined && override.length > 0 ? resolve8(context4.cwd, override) : join22(homedir7(), GLOBAL_RULES_DIR2);
83019
+ return override !== undefined && override.length > 0 ? resolve9(context4.cwd, override) : join22(homedir7(), GLOBAL_RULES_DIR2);
82961
83020
  }
82962
83021
  async function writeIfNew(context4, path9, content, force, result) {
82963
83022
  if (!force && await context4.fs.exists(path9)) {
@@ -82988,7 +83047,7 @@ async function seedGlobalConfig(context4) {
82988
83047
  if (source === null)
82989
83048
  return 0;
82990
83049
  const globalOverride = context4.env.SPUR_GLOBAL_RULES_DIR;
82991
- const target = globalOverride !== undefined && globalOverride.length > 0 ? resolve8(context4.cwd, globalOverride) : join22(homedir7(), GLOBAL_CONFIG_DIR2);
83050
+ const target = globalOverride !== undefined && globalOverride.length > 0 ? resolve9(context4.cwd, globalOverride) : join22(homedir7(), GLOBAL_CONFIG_DIR2);
82992
83051
  let written = 0;
82993
83052
  for (const relPath of listBundledConfigFiles()) {
82994
83053
  if (relPath === GLOBAL_CONFIG_EXAMPLE)
@@ -83261,15 +83320,15 @@ async function runMessageWatch(svc, output2, options, runtime = {}) {
83261
83320
  }
83262
83321
  }
83263
83322
  function defaultSleep(ms, signal) {
83264
- return new Promise((resolve9) => {
83323
+ return new Promise((resolve10) => {
83265
83324
  if (signal?.aborted) {
83266
- resolve9();
83325
+ resolve10();
83267
83326
  return;
83268
83327
  }
83269
- const timer = setTimeout(resolve9, ms);
83328
+ const timer = setTimeout(resolve10, ms);
83270
83329
  signal?.addEventListener("abort", () => {
83271
83330
  clearTimeout(timer);
83272
- resolve9();
83331
+ resolve10();
83273
83332
  }, { once: true });
83274
83333
  });
83275
83334
  }
@@ -83297,12 +83356,12 @@ function registerMigrateCommand(program2, context4) {
83297
83356
  // src/commands/projects.ts
83298
83357
  init_src3();
83299
83358
  init_dist5();
83300
- import { basename as basename5, resolve as resolve9 } from "path";
83359
+ import { basename as basename5, resolve as resolve10 } from "path";
83301
83360
  function registerProjectsCommand(program2, context4) {
83302
83361
  const projectsCmd = program2.command("projects").summary("manage the Spur multi-project registry");
83303
83362
  projectsCmd.command("add").argument("<path>", "Project root directory path").option("--name <name>", "Display name for the project").option("--json", "Output JSON response").action(async (pathArg, options) => {
83304
83363
  try {
83305
- const absolutePath = resolve9(context4.cwd, pathArg);
83364
+ const absolutePath = resolve10(context4.cwd, pathArg);
83306
83365
  if (!await context4.fs.exists(absolutePath)) {
83307
83366
  throw new Error(`Directory does not exist: ${absolutePath}`);
83308
83367
  }
@@ -83988,8 +84047,8 @@ class EventPublisher {
83988
84047
  if (bufferedEvents.length > 0) {
83989
84048
  return { done: false, value: bufferedEvents.shift() };
83990
84049
  }
83991
- return new Promise((resolve10, reject) => {
83992
- pullResolvers.push([resolve10, reject]);
84050
+ return new Promise((resolve11, reject) => {
84051
+ pullResolvers.push([resolve11, reject]);
83993
84052
  });
83994
84053
  }, async () => {
83995
84054
  unsubscribe();
@@ -90514,6 +90573,8 @@ var teamModule = {
90514
90573
  const enriched = teams.map((team) => ({
90515
90574
  teamId: team.teamId,
90516
90575
  name: team.name,
90576
+ workDir: team.workDir,
90577
+ isCurrentProject: team.isCurrentProject,
90517
90578
  members: team.specs.map((spec) => {
90518
90579
  const proc = processes.find((p2) => p2.agentId === spec.id);
90519
90580
  const configModel = spec.config?.model;
@@ -94549,32 +94610,51 @@ ${result.content}`);
94549
94610
  const svc = await makeCheckService(context4);
94550
94611
  const planningFolders = await resolvePlanningFolders(context4.fs);
94551
94612
  const activeFolder = planningFolders.foldersConfig.active_folder;
94552
- const tasksDir = options.folder ?? context4.fs.resolve(activeFolder);
94553
- const entries = await context4.fs.readDir(tasksDir);
94554
- const wbsPattern = wbs ? [wbs] : entries.filter((n2) => /^\d{4}_.+\.md$/.test(n2)).map((n2) => n2.match(/^(\d{4})_/)?.[1]).filter((n2) => n2 !== undefined);
94613
+ const tasksDir = context4.fs.resolve(options.folder ?? activeFolder);
94614
+ const printResult = (result) => {
94615
+ if (json3)
94616
+ return;
94617
+ context4.output.write(`
94618
+ ${result.wbs} (${result.status}): ${result.pass ? "PASS" : "FAIL"}`);
94619
+ for (const f of result.findings) {
94620
+ const tag = f.severity === "error" ? "ERR" : "WARN";
94621
+ context4.output.write(` [${tag}] ${f.layer} ${f.section}: ${f.message}`);
94622
+ }
94623
+ if (result.missingSections.length > 0) {
94624
+ context4.output.write(` Missing: ${result.missingSections.join(", ")}`);
94625
+ }
94626
+ };
94555
94627
  const results = [];
94556
- for (const w of wbsPattern) {
94557
- const fileName = entries.find((n2) => n2.startsWith(`${w}_`) && n2.endsWith(".md"));
94558
- if (!fileName) {
94559
- context4.output.error(`Task ${w} not found`);
94628
+ if (wbs) {
94629
+ const locator = options.folder !== undefined ? TaskLocator.forSingleDir(context4.fs, tasksDir) : await makeTaskLocator(context4);
94630
+ const hit = await locator.findByWbs(wbs);
94631
+ if (hit === null) {
94632
+ context4.output.error(`Task ${wbs} not found`);
94560
94633
  context4.setExitCode(1);
94561
- continue;
94634
+ } else {
94635
+ const result = await svc.check(hit.filePath, wbs, {
94636
+ strict,
94637
+ severityOverrides: planningFolders.severityOverrides
94638
+ });
94639
+ results.push(result);
94640
+ printResult(result);
94562
94641
  }
94563
- const result = await svc.check(`${tasksDir}/${fileName}`, w, {
94564
- strict,
94565
- severityOverrides: planningFolders.severityOverrides
94566
- });
94567
- results.push(result);
94568
- if (!json3) {
94569
- context4.output.write(`
94570
- ${result.wbs} (${result.status}): ${result.pass ? "PASS" : "FAIL"}`);
94571
- for (const f of result.findings) {
94572
- const tag = f.severity === "error" ? "ERR" : "WARN";
94573
- context4.output.write(` [${tag}] ${f.layer} ${f.section}: ${f.message}`);
94574
- }
94575
- if (result.missingSections.length > 0) {
94576
- context4.output.write(` Missing: ${result.missingSections.join(", ")}`);
94642
+ } else {
94643
+ const entries = await context4.fs.readDir(tasksDir);
94644
+ const wbsPattern = entries.filter((n2) => /^\d{4}_.+\.md$/.test(n2)).map((n2) => n2.match(/^(\d{4})_/)?.[1]).filter((n2) => n2 !== undefined);
94645
+ for (const w of wbsPattern) {
94646
+ const fileName = entries.find((n2) => n2.startsWith(`${w}_`) && n2.endsWith(".md"));
94647
+ if (!fileName) {
94648
+ context4.output.error(`Task ${w} not found`);
94649
+ context4.setExitCode(1);
94650
+ continue;
94577
94651
  }
94652
+ const result = await svc.check(`${tasksDir}/${fileName}`, w, {
94653
+ strict,
94654
+ severityOverrides: planningFolders.severityOverrides
94655
+ });
94656
+ results.push(result);
94657
+ printResult(result);
94578
94658
  }
94579
94659
  }
94580
94660
  if (!wbs) {
@@ -94697,7 +94777,7 @@ ${result.wbs} (${result.status}): ${result.pass ? "PASS" : "FAIL"}`);
94697
94777
  }
94698
94778
  async function makeService2(context4, folderOverride, noLifecycle = false) {
94699
94779
  const foldersConfig = (await resolvePlanningFolders(context4.fs)).foldersConfig;
94700
- const tasksDir = folderOverride ?? context4.fs.resolve(foldersConfig.active_folder);
94780
+ const tasksDir = context4.fs.resolve(folderOverride ?? foldersConfig.active_folder);
94701
94781
  const lifecycle = noLifecycle ? undefined : makeLifecycleAdapter(context4, TASK_LIFECYCLE_PROFILE);
94702
94782
  const writeService = new PlanningWriteService({
94703
94783
  fs: context4.fs,
@@ -95090,7 +95170,7 @@ init_dist7();
95090
95170
  init_dist6();
95091
95171
  init_dist5();
95092
95172
  import { closeSync as closeSync5, fstatSync as fstatSync3, openSync as openSync5, readSync as readSync3 } from "fs";
95093
- import { join as join30, resolve as resolve10 } from "path";
95173
+ import { join as join30, resolve as resolve11 } from "path";
95094
95174
  import { createInterface } from "readline";
95095
95175
  import { setTimeout as sleep3 } from "timers/promises";
95096
95176
  function shQuote2(value2) {
@@ -95289,7 +95369,7 @@ Monitor with: spur workflow trace ${runId2} --follow`);
95289
95369
  let planPreview;
95290
95370
  if (options.plan !== false) {
95291
95371
  try {
95292
- const def = await loadWorkflowDef(resolve10(context4.cwd, file2), { validateSchema: false });
95372
+ const def = await loadWorkflowDef(resolve11(context4.cwd, file2), { validateSchema: false });
95293
95373
  planPreview = renderRunPlan(def);
95294
95374
  } catch {}
95295
95375
  }
@@ -95753,7 +95833,7 @@ init_src3();
95753
95833
  init_src();
95754
95834
  init_src2();
95755
95835
  init_dist5();
95756
- import { dirname as dirname19, join as join31, resolve as resolve11 } from "path";
95836
+ import { dirname as dirname19, join as join31, resolve as resolve12 } from "path";
95757
95837
  import { isatty as isatty4 } from "tty";
95758
95838
 
95759
95839
  // ../../node_modules/.bun/@clack+core@1.4.1/node_modules/@clack/core/dist/index.mjs
@@ -97136,7 +97216,7 @@ class DefaultHitlResponder {
97136
97216
 
97137
97217
  // src/context.ts
97138
97218
  function createCliContext(options) {
97139
- const cwd = resolve11(options.cwd ?? process.cwd());
97219
+ const cwd = resolve12(options.cwd ?? process.cwd());
97140
97220
  const env = options.env ?? process.env;
97141
97221
  const fs3 = createNodeFileSystem3(cwd);
97142
97222
  let dbPromise;