@gobing-ai/spur 0.3.23 → 0.3.24

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 (55) hide show
  1. package/package.json +1 -1
  2. package/spur.js +258 -25
  3. package/web/_astro/{BoardApp.b8HvN_Uy.js → BoardApp.Cuna9Z5I.js} +60 -60
  4. package/web/_astro/BoardApp.DSSZTsSW.js +1 -0
  5. package/web/_astro/{TaskDetail.BRr8ebLd.js → TaskDetail.Bso6Ccoa.js} +1 -1
  6. package/web/_astro/{arc.D9I3ghf3.js → arc.BpfaYkuC.js} +1 -1
  7. package/web/_astro/{architectureDiagram-3BPJPVTR.B60VjZ82.js → architectureDiagram-3BPJPVTR.Bsc64OWC.js} +1 -1
  8. package/web/_astro/{blockDiagram-GPEHLZMM.B8xiXsGb.js → blockDiagram-GPEHLZMM.CgrqZDLK.js} +1 -1
  9. package/web/_astro/{c4Diagram-AAUBKEIU.D8Q9jRJZ.js → c4Diagram-AAUBKEIU.5ROHxpqD.js} +1 -1
  10. package/web/_astro/channel.DKzvLld2.js +1 -0
  11. package/web/_astro/{chunk-2J33WTMH.BVcogAuP.js → chunk-2J33WTMH.BIuSLWP4.js} +1 -1
  12. package/web/_astro/{chunk-4BX2VUAB.pDodeppW.js → chunk-4BX2VUAB.u-fSGnxt.js} +1 -1
  13. package/web/_astro/{chunk-55IACEB6.5dYLJxJn.js → chunk-55IACEB6.pL62Yh8J.js} +1 -1
  14. package/web/_astro/{chunk-727SXJPM.C6GTI_n6.js → chunk-727SXJPM.Cxy6KODj.js} +1 -1
  15. package/web/_astro/{chunk-AQP2D5EJ.kgDX41bQ.js → chunk-AQP2D5EJ.D2XUILVx.js} +1 -1
  16. package/web/_astro/{chunk-FMBD7UC4.CSnAi74r.js → chunk-FMBD7UC4.DNnV20jb.js} +1 -1
  17. package/web/_astro/{chunk-ND2GUHAM.qQ8ldUUT.js → chunk-ND2GUHAM.Cc-L-_6y.js} +1 -1
  18. package/web/_astro/{chunk-QZHKN3VN.PzPFlVri.js → chunk-QZHKN3VN.Bw_kp1ef.js} +1 -1
  19. package/web/_astro/{classDiagram-4FO5ZUOK.C7RulRYW.js → classDiagram-4FO5ZUOK.CoE-GIzM.js} +1 -1
  20. package/web/_astro/{classDiagram-v2-Q7XG4LA2.C7RulRYW.js → classDiagram-v2-Q7XG4LA2.CoE-GIzM.js} +1 -1
  21. package/web/_astro/{cose-bilkent-S5V4N54A.Dw4HZHnT.js → cose-bilkent-S5V4N54A.CDsPYTeJ.js} +1 -1
  22. package/web/_astro/{dagre-BM42HDAG.MmGbZWM_.js → dagre-BM42HDAG.BUw8fm5M.js} +1 -1
  23. package/web/_astro/{diagram-2AECGRRQ.DugWYvZC.js → diagram-2AECGRRQ.Bx3Ndc4x.js} +1 -1
  24. package/web/_astro/{diagram-5GNKFQAL.BL7D0BMi.js → diagram-5GNKFQAL.DZOACNsA.js} +1 -1
  25. package/web/_astro/{diagram-KO2AKTUF.B9GQMjwM.js → diagram-KO2AKTUF.BVmZqHXi.js} +1 -1
  26. package/web/_astro/{diagram-LMA3HP47.CbHPvhbu.js → diagram-LMA3HP47.Dhw7Sl9e.js} +1 -1
  27. package/web/_astro/{diagram-OG6HWLK6.Bx6GcKpw.js → diagram-OG6HWLK6.xOqh3zFz.js} +1 -1
  28. package/web/_astro/{erDiagram-TEJ5UH35._sHJgXD7.js → erDiagram-TEJ5UH35.VX6HV9QU.js} +1 -1
  29. package/web/_astro/{flowDiagram-I6XJVG4X.D305L8m4.js → flowDiagram-I6XJVG4X.D5tzv1nF.js} +1 -1
  30. package/web/_astro/{ganttDiagram-6RSMTGT7.qjJ4kX00.js → ganttDiagram-6RSMTGT7.bRb0hclW.js} +1 -1
  31. package/web/_astro/{gitGraphDiagram-PVQCEYII.D-t4MzVO.js → gitGraphDiagram-PVQCEYII.BrXfUWT_.js} +1 -1
  32. package/web/_astro/index.BRj8-zmo.css +1 -0
  33. package/web/_astro/{infoDiagram-5YYISTIA.WwSNIljZ.js → infoDiagram-5YYISTIA.LIQUmEkh.js} +1 -1
  34. package/web/_astro/{ishikawaDiagram-YF4QCWOH.EWsUYdpd.js → ishikawaDiagram-YF4QCWOH.BMMwawXt.js} +1 -1
  35. package/web/_astro/{journeyDiagram-JHISSGLW.CqKE9hNP.js → journeyDiagram-JHISSGLW.F7Rw6EQ6.js} +1 -1
  36. package/web/_astro/{kanban-definition-UN3LZRKU.BDCCufoa.js → kanban-definition-UN3LZRKU.BnZBa0gE.js} +1 -1
  37. package/web/_astro/{linear.CyHEakPy.js → linear.DamosLmv.js} +1 -1
  38. package/web/_astro/{mermaid.core.B-9P_KDH.js → mermaid.core.BjIs-rhV.js} +4 -4
  39. package/web/_astro/{mindmap-definition-RKZ34NQL.bCxpjW2r.js → mindmap-definition-RKZ34NQL.CAJpo14X.js} +1 -1
  40. package/web/_astro/{pieDiagram-4H26LBE5.x_CRiGMu.js → pieDiagram-4H26LBE5.AcbGV8z3.js} +1 -1
  41. package/web/_astro/{quadrantDiagram-W4KKPZXB.CnbQkwo7.js → quadrantDiagram-W4KKPZXB.DFHo7-vl.js} +1 -1
  42. package/web/_astro/{requirementDiagram-4Y6WPE33.CfeJKVEP.js → requirementDiagram-4Y6WPE33.ZcAUCpFg.js} +1 -1
  43. package/web/_astro/{sankeyDiagram-5OEKKPKP.COYpTQlG.js → sankeyDiagram-5OEKKPKP.B3jEuSva.js} +1 -1
  44. package/web/_astro/{sequenceDiagram-3UESZ5HK.DVzoeimO.js → sequenceDiagram-3UESZ5HK.Cpz6c_VX.js} +1 -1
  45. package/web/_astro/{stateDiagram-AJRCARHV.BVL5te9q.js → stateDiagram-AJRCARHV.g_pKql00.js} +1 -1
  46. package/web/_astro/{stateDiagram-v2-BHNVJYJU.C_x1GbV0.js → stateDiagram-v2-BHNVJYJU.DFINnXL2.js} +1 -1
  47. package/web/_astro/{timeline-definition-PNZ67QCA.DtZPbOJL.js → timeline-definition-PNZ67QCA.D0m2aZaf.js} +1 -1
  48. package/web/_astro/{vennDiagram-CIIHVFJN.pexoUOut.js → vennDiagram-CIIHVFJN.Cyk_24V8.js} +1 -1
  49. package/web/_astro/{wardley-L42UT6IY.DLG5pKRN.js → wardley-L42UT6IY.CW7JQlsX.js} +1 -1
  50. package/web/_astro/{wardleyDiagram-YWT4CUSO.CnYrM4tH.js → wardleyDiagram-YWT4CUSO.CkaxqiW8.js} +1 -1
  51. package/web/_astro/{xychartDiagram-2RQKCTM6.kBrlce_Y.js → xychartDiagram-2RQKCTM6.DJorKgMY.js} +1 -1
  52. package/web/index.html +2 -2
  53. package/web/_astro/BoardApp.LygxZgOy.js +0 -1
  54. package/web/_astro/channel.BToTp7Lu.js +0 -1
  55. package/web/_astro/index.CmVh0AUD.css +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gobing-ai/spur",
3
- "version": "0.3.23",
3
+ "version": "0.3.24",
4
4
  "description": "Spur CLI — local-first harness for mainstream coding agents: constraint checking, workflow orchestration, agent health, and history analytics. Bun-native; exposes the `spur` command.",
5
5
  "keywords": [
6
6
  "spur",
package/spur.js CHANGED
@@ -37077,6 +37077,8 @@ var init_finding_codes = __esm(() => {
37077
37077
  "L2.forbidden-section",
37078
37078
  "L2.disallowed-section",
37079
37079
  "L3.requirements-format",
37080
+ "L3.requirements-empty",
37081
+ "L3.ac-empty",
37080
37082
  "L3.solution-file-line",
37081
37083
  "L3.review-priority-table",
37082
37084
  "L3.testing-coverage",
@@ -37108,7 +37110,8 @@ var init_finding_codes = __esm(() => {
37108
37110
  "L4.uncovered-task-scenario",
37109
37111
  "L4.uncovered-feature-scenario",
37110
37112
  "L4.verifying-incomplete-tasks",
37111
- "L4.dogfood-missing"
37113
+ "L4.dogfood-missing",
37114
+ "L4.scenario-unverified"
37112
37115
  ];
37113
37116
  FINDING_CODES = {
37114
37117
  L1_MARKDOWN_PARSE: "L1.markdown-parse",
@@ -37117,6 +37120,8 @@ var init_finding_codes = __esm(() => {
37117
37120
  L2_FORBIDDEN_SECTION: "L2.forbidden-section",
37118
37121
  L2_DISALLOWED_SECTION: "L2.disallowed-section",
37119
37122
  L3_REQUIREMENTS_FORMAT: "L3.requirements-format",
37123
+ L3_REQUIREMENTS_EMPTY: "L3.requirements-empty",
37124
+ L3_AC_EMPTY: "L3.ac-empty",
37120
37125
  L3_SOLUTION_FILE_LINE: "L3.solution-file-line",
37121
37126
  L3_REVIEW_PRIORITY_TABLE: "L3.review-priority-table",
37122
37127
  L3_TESTING_COVERAGE: "L3.testing-coverage",
@@ -37148,7 +37153,8 @@ var init_finding_codes = __esm(() => {
37148
37153
  L4_UNCOVERED_TASK_SCENARIO: "L4.uncovered-task-scenario",
37149
37154
  L4_UNCOVERED_FEATURE_SCENARIO: "L4.uncovered-feature-scenario",
37150
37155
  L4_VERIFYING_INCOMPLETE_TASKS: "L4.verifying-incomplete-tasks",
37151
- L4_DOGFOOD_MISSING: "L4.dogfood-missing"
37156
+ L4_DOGFOOD_MISSING: "L4.dogfood-missing",
37157
+ L4_SCENARIO_UNVERIFIED: "L4.scenario-unverified"
37152
37158
  };
37153
37159
  });
37154
37160
 
@@ -60050,7 +60056,7 @@ class AgentService {
60050
60056
  const raw = stringFlag(flags, "agent", "auto");
60051
60057
  if (raw === "auto")
60052
60058
  return this.resolveAgentAuto(prompt, flags, doctorRunner);
60053
- return this.resolveAgentExplicit(raw, doctorRunner, "explicit");
60059
+ return this.resolveExecutorSelector(raw, doctorRunner, "explicit");
60054
60060
  }
60055
60061
  async resolveAgentAuto(prompt, flags, doctorRunner) {
60056
60062
  const config4 = this.ctx.agentConfig;
@@ -60175,16 +60181,6 @@ class AgentService {
60175
60181
  return usable.result;
60176
60182
  return { ok: true, agent: canonical, source };
60177
60183
  }
60178
- async resolveAgentExplicit(name, doctorRunner, source) {
60179
- const canonical = resolveAgentName(name);
60180
- if (canonical === undefined) {
60181
- return { ok: false, exitCode: 2, message: `Unknown agent: ${name}` };
60182
- }
60183
- const usable = await this.checkUsable(canonical, doctorRunner);
60184
- if (!usable.ok)
60185
- return usable.result;
60186
- return { ok: true, agent: canonical, source };
60187
- }
60188
60184
  async checkUsable(canonical, doctorRunner) {
60189
60185
  const result = await doctorRunner.runOne(canonical);
60190
60186
  if (!result.installed) {
@@ -61245,7 +61241,8 @@ var init_feature_check = __esm(() => {
61245
61241
  await this.checkChildrenLimit(featureId2, options.featuresDir, findings);
61246
61242
  }
61247
61243
  const dogfoodDir = options?.dogfoodDir ?? (options?.featuresDir ? join7(dirname8(options.featuresDir), "dogfood") : undefined);
61248
- await this.runL4(doc2, featureId2, status, options?.tasksDir, dogfoodDir, findings);
61244
+ const runDir = options?.runDir ?? (options?.tasksDir ? join7(dirname8(options.tasksDir), ".spur", "run") : undefined);
61245
+ await this.runL4(doc2, featureId2, status, options?.tasksDir, dogfoodDir, runDir, findings);
61249
61246
  return { id: featureId2, ...this.summarizeWithStatus(status, findings, strict, options?.severityOverrides) };
61250
61247
  }
61251
61248
  runL3(doc2, findings) {
@@ -61377,13 +61374,14 @@ var init_feature_check = __esm(() => {
61377
61374
  });
61378
61375
  }
61379
61376
  }
61380
- async runL4(doc2, featureId2, status, tasksDir, dogfoodDir, findings) {
61377
+ async runL4(doc2, featureId2, status, tasksDir, dogfoodDir, runDir, findings) {
61381
61378
  if (tasksDir === undefined)
61382
61379
  return;
61383
61380
  let linkedTasks = 0;
61384
61381
  const incompleteTasks = [];
61385
61382
  const linkedTaskAc = [];
61386
61383
  const linkedTaskSolutions = [];
61384
+ const linkedTaskRecords = [];
61387
61385
  try {
61388
61386
  const entries = await this.fs.readDir(tasksDir);
61389
61387
  for (const entry of entries) {
@@ -61398,13 +61396,15 @@ var init_feature_check = __esm(() => {
61398
61396
  continue;
61399
61397
  linkedTasks += 1;
61400
61398
  const tStatus = tfm.status ?? "backlog";
61399
+ const wbs = entry.match(/^(\d{4})_/)?.[1] ?? entry;
61401
61400
  if (tStatus !== "done" && tStatus !== "cancelled") {
61402
- incompleteTasks.push(entry.match(/^(\d{4})_/)?.[1] ?? entry);
61401
+ incompleteTasks.push(wbs);
61403
61402
  }
61404
61403
  const tac = stripAcFence(taskDoc.getSection("Acceptance Criteria") ?? "");
61405
61404
  if (tac.trim().length > 0)
61406
61405
  linkedTaskAc.push(tac);
61407
61406
  linkedTaskSolutions.push(taskDoc.getSection("Solution") ?? "");
61407
+ linkedTaskRecords.push({ wbs, status: tStatus, ac: tac });
61408
61408
  } catch {
61409
61409
  findings.push({
61410
61410
  layer: "L4",
@@ -61446,6 +61446,7 @@ var init_feature_check = __esm(() => {
61446
61446
  });
61447
61447
  }
61448
61448
  }
61449
+ await this.checkScenarioSatisfaction(acBody, linkedTaskRecords, runDir, findings);
61449
61450
  if (status === "verifying" && incompleteTasks.length > 0) {
61450
61451
  findings.push({
61451
61452
  layer: "L4",
@@ -61481,6 +61482,77 @@ var init_feature_check = __esm(() => {
61481
61482
  }
61482
61483
  }
61483
61484
  }
61485
+ async checkScenarioSatisfaction(featureAc, linkedTasks, runDir, findings) {
61486
+ if (linkedTasks.length === 0 || runDir === undefined)
61487
+ return;
61488
+ const parsed = parseFeature(featureAc);
61489
+ if (parsed === null)
61490
+ return;
61491
+ if (parsed.scenarios.length === 0)
61492
+ return;
61493
+ const scenarioAliases = parsed.scenarios.map((s, i2) => ({
61494
+ title: s.name,
61495
+ normalized: normalizeTitle(s.name),
61496
+ alias: `AC-${i2 + 1}`
61497
+ }));
61498
+ const covers = {};
61499
+ for (const sc of scenarioAliases) {
61500
+ const linked = [];
61501
+ for (const task of linkedTasks) {
61502
+ const taskCov = checkAcCoverage(`Feature: x
61503
+ Scenario: ${sc.title}
61504
+ Given x`, task.ac);
61505
+ if (!taskCov.orphans.includes(sc.title)) {
61506
+ linked.push({ wbs: task.wbs, status: task.status });
61507
+ }
61508
+ }
61509
+ covers[sc.title] = linked;
61510
+ }
61511
+ for (const sc of scenarioAliases) {
61512
+ const linked = covers[sc.title] ?? [];
61513
+ if (linked.length === 0)
61514
+ continue;
61515
+ const verified = await this.isScenarioVerified(sc, linked, runDir);
61516
+ if (!verified) {
61517
+ findings.push({
61518
+ layer: "L4",
61519
+ code: FINDING_CODES.L4_SCENARIO_UNVERIFIED,
61520
+ severity: "warning",
61521
+ section: "Acceptance Criteria",
61522
+ message: `Feature scenario "${sc.title}" is linked but unverified: covering task(s) ` + `${linked.map((l) => l.wbs).join(", ")} have no PASS verdict with MET requirement`
61523
+ });
61524
+ }
61525
+ }
61526
+ }
61527
+ async isScenarioVerified(sc, linked, runDir) {
61528
+ for (const task of linked) {
61529
+ if (task.status !== "done")
61530
+ continue;
61531
+ const artifact = await this.readVerdictArtifact(runDir, task.wbs);
61532
+ if (artifact === null)
61533
+ continue;
61534
+ if (artifact.verdict !== "PASS")
61535
+ continue;
61536
+ const matched = artifact.requirements.find((r) => normalizeTitle(r.id) === sc.normalized || normalizeTitle(r.alias ?? r.id) === sc.normalized || r.id === sc.alias);
61537
+ if (matched !== undefined && matched.status === "MET")
61538
+ return true;
61539
+ }
61540
+ return false;
61541
+ }
61542
+ async readVerdictArtifact(runDir, wbs) {
61543
+ try {
61544
+ const raw = await this.fs.readFile(join7(runDir, `${wbs}-verdict.json`));
61545
+ const parsed = JSON.parse(raw);
61546
+ if (typeof parsed.verdict !== "string")
61547
+ return null;
61548
+ if (!Array.isArray(parsed.requirements))
61549
+ return null;
61550
+ const requirements = parsed.requirements.filter((r) => typeof r?.id === "string" && typeof r?.status === "string").map((r) => ({ id: r.id, status: r.status }));
61551
+ return { verdict: parsed.verdict, requirements };
61552
+ } catch {
61553
+ return null;
61554
+ }
61555
+ }
61484
61556
  };
61485
61557
  });
61486
61558
 
@@ -63754,7 +63826,7 @@ var init_task_check = __esm(() => {
63754
63826
  this.runL3(doc2, entry, status, findings);
63755
63827
  const tasksDir = dirname9(filePath);
63756
63828
  const featuresDir = join9(dirname9(tasksDir), "features");
63757
- await this.runL4(doc2, fm, findings, featuresDir, tasksDir);
63829
+ await this.runL4(doc2, fm, status, findings, featuresDir, tasksDir);
63758
63830
  await this.runL4Rollup(doc2, wbs, status, findings, tasksDir);
63759
63831
  if (status !== "done" && status !== "cancelled") {
63760
63832
  await this.runL4Readiness(doc2, fm, wbs, status, findings, tasksDir);
@@ -63762,6 +63834,26 @@ var init_task_check = __esm(() => {
63762
63834
  return { wbs, ...this.summarizeWithStatus(status, findings, strict, options?.severityOverrides) };
63763
63835
  }
63764
63836
  runL3(doc2, entry, status, findings) {
63837
+ const reqBodyRaw = doc2.getSection("Requirements");
63838
+ if (reqBodyRaw !== null && isPlaceholderBody(reqBodyRaw)) {
63839
+ findings.push({
63840
+ layer: "L3",
63841
+ code: FINDING_CODES.L3_REQUIREMENTS_EMPTY,
63842
+ severity: "error",
63843
+ section: "Requirements",
63844
+ message: "Requirements is placeholder-only \u2014 a task with no requirements is unverifiable"
63845
+ });
63846
+ }
63847
+ const acBodyRaw = doc2.getSection("Acceptance Criteria");
63848
+ if (acBodyRaw !== null && isPlaceholderBody(stripAcFence(acBodyRaw))) {
63849
+ findings.push({
63850
+ layer: "L3",
63851
+ code: FINDING_CODES.L3_AC_EMPTY,
63852
+ severity: "error",
63853
+ section: "Acceptance Criteria",
63854
+ message: "Acceptance Criteria is placeholder-only \u2014 populate with real scenarios"
63855
+ });
63856
+ }
63765
63857
  const reqBody = doc2.getSection("Requirements");
63766
63858
  if (reqBody !== null && !isPlaceholderBody(reqBody)) {
63767
63859
  const blocks = reqBody.trim().split(/\n\s*\n/).filter((b) => b.trim().length > 0);
@@ -63853,7 +63945,7 @@ var init_task_check = __esm(() => {
63853
63945
  }
63854
63946
  }
63855
63947
  }
63856
- async runL4(doc2, fm, findings, featuresDir, tasksDir) {
63948
+ async runL4(doc2, fm, status, findings, featuresDir, tasksDir) {
63857
63949
  const template = typeof fm.template === "string" ? fm.template : DEFAULT_TASK_VARIANT;
63858
63950
  if (template === DEFAULT_TASK_VARIANT) {
63859
63951
  const designBody = doc2.getSection("Design");
@@ -63882,7 +63974,9 @@ var init_task_check = __esm(() => {
63882
63974
  });
63883
63975
  } else {
63884
63976
  const featureStatus = await this.readFeatureStatus(featurePath);
63885
- if (featureStatus === "done" || featureStatus === "cancelled") {
63977
+ const featureTerminal = featureStatus === "done" || featureStatus === "cancelled";
63978
+ const taskTerminal = status === "done" || status === "cancelled";
63979
+ if (featureTerminal && !taskTerminal) {
63886
63980
  findings.push({
63887
63981
  layer: "L4",
63888
63982
  code: FINDING_CODES.L4_FEATURE_TERMINAL,
@@ -64586,6 +64680,26 @@ class TaskService {
64586
64680
  const templateBodies = this.ctx.resolveTemplateBodies?.(variant) ?? {};
64587
64681
  return { ...templateBodies, ...taskBodies };
64588
64682
  }
64683
+ async findDuplicateFollowUp(featureId2, title, withinSec) {
64684
+ const siblings = await this.list({ featureId: featureId2 });
64685
+ const now = Date.now();
64686
+ const lowerTitle = title.toLowerCase();
64687
+ for (const t of siblings) {
64688
+ if ((t.name ?? "").toLowerCase() !== lowerTitle)
64689
+ continue;
64690
+ const createdAt = t.frontmatter?.created_at;
64691
+ if (typeof createdAt !== "string")
64692
+ continue;
64693
+ const createdMs = Date.parse(createdAt);
64694
+ if (Number.isNaN(createdMs))
64695
+ continue;
64696
+ const ageSec = (now - createdMs) / 1000;
64697
+ if (ageSec >= 0 && ageSec <= withinSec) {
64698
+ return { wbs: t.wbs, name: t.name };
64699
+ }
64700
+ }
64701
+ return null;
64702
+ }
64589
64703
  async create(params) {
64590
64704
  const folder = this.ctx.tasksDir;
64591
64705
  let background = "";
@@ -64595,6 +64709,12 @@ class TaskService {
64595
64709
  const variant = params.template ?? (params.featureId !== undefined ? "feature-impl" : DEFAULT_TASK_VARIANT);
64596
64710
  const status = params.status ?? (params.featureId !== undefined ? "todo" : "backlog");
64597
64711
  return this.writeService.createAllocated(folder, async () => {
64712
+ if (params.dedupeWithinSec !== undefined && params.featureId !== undefined) {
64713
+ const collision = await this.findDuplicateFollowUp(params.featureId, params.title, params.dedupeWithinSec);
64714
+ if (collision !== null) {
64715
+ throw new DuplicateFollowUpError(collision.wbs, collision.name, params.title);
64716
+ }
64717
+ }
64598
64718
  const wbs = await this.allocateWbs();
64599
64719
  const slug = this.slugify(params.title);
64600
64720
  const filePath = this.resolveTaskPath(wbs, slug);
@@ -65223,7 +65343,7 @@ ${block}` : block);
65223
65343
  return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60);
65224
65344
  }
65225
65345
  }
65226
- var DependencyMutationError, SectionMutationError, ROSTER_START = "<!-- AUTO-GENERATED by spur task refresh-roster -->", ROSTER_END = "<!-- END AUTO-GENERATED -->", ROSTER_REGION_RE, TASK_ACTION_COMMANDS, DEFAULT_CREATION_SECTIONS;
65346
+ var DependencyMutationError, SectionMutationError, DuplicateFollowUpError, ROSTER_START = "<!-- AUTO-GENERATED by spur task refresh-roster -->", ROSTER_END = "<!-- END AUTO-GENERATED -->", ROSTER_REGION_RE, TASK_ACTION_COMMANDS, DEFAULT_CREATION_SECTIONS;
65227
65347
  var init_task_service = __esm(() => {
65228
65348
  init_src2();
65229
65349
  init_task_locator();
@@ -65244,6 +65364,18 @@ var init_task_service = __esm(() => {
65244
65364
  this.code = code;
65245
65365
  }
65246
65366
  };
65367
+ DuplicateFollowUpError = class DuplicateFollowUpError extends Error {
65368
+ existingWbs;
65369
+ existingName;
65370
+ attemptedName;
65371
+ constructor(existingWbs, existingName, attemptedName) {
65372
+ super(`duplicate-follow-up: task ${existingWbs} ("${existingName}") already exists under this feature ` + `with an identical name. Reuse ${existingWbs} or pass --allow-duplicate-name to override.`);
65373
+ this.name = "DuplicateFollowUpError";
65374
+ this.existingWbs = existingWbs;
65375
+ this.existingName = existingName;
65376
+ this.attemptedName = attemptedName;
65377
+ }
65378
+ };
65247
65379
  ROSTER_REGION_RE = /<!--[ \t]*AUTO-GENERATED[^>]*-->[\s\S]*?<!--[ \t]*END[ \t]*AUTO-GENERATED[^>]*-->/g;
65248
65380
  TASK_ACTION_COMMANDS = {
65249
65381
  refine: (wbs) => `/sp:dev-refine ${wbs} --auto`,
@@ -65446,6 +65578,50 @@ function extractChecks(_text, taskCheckPassed, acceptanceCriteria) {
65446
65578
  }
65447
65579
  return checks4;
65448
65580
  }
65581
+ function classifyTaskOutcome(taskStatus, verdict) {
65582
+ const status = String(taskStatus).toLowerCase();
65583
+ if (NOT_STARTED_STATUSES[status]) {
65584
+ return "NOT-STARTED";
65585
+ }
65586
+ const v = String(verdict ?? "UNKNOWN").toUpperCase();
65587
+ if (v === "PASS" || v === "PARTIAL" || v === "FAIL")
65588
+ return v;
65589
+ return "UNKNOWN";
65590
+ }
65591
+ function aggregateBatchVerdicts(results) {
65592
+ const notStarted = results.filter((r) => r.outcome === "NOT-STARTED");
65593
+ const rolledUp = results.filter((r) => r.outcome !== "NOT-STARTED");
65594
+ const counts = {
65595
+ PASS: rolledUp.filter((r) => r.outcome === "PASS").length,
65596
+ PARTIAL: rolledUp.filter((r) => r.outcome === "PARTIAL").length,
65597
+ FAIL: rolledUp.filter((r) => r.outcome === "FAIL").length,
65598
+ UNKNOWN: rolledUp.filter((r) => r.outcome === "UNKNOWN").length
65599
+ };
65600
+ let verdict;
65601
+ if (rolledUp.length === 0) {
65602
+ verdict = "UNKNOWN";
65603
+ } else if (counts.FAIL > 0) {
65604
+ verdict = "FAIL";
65605
+ } else if (counts.PARTIAL > 0 || counts.UNKNOWN > 0) {
65606
+ verdict = "PARTIAL";
65607
+ } else {
65608
+ verdict = "PASS";
65609
+ }
65610
+ const summary = `${counts.PASS} PASS, ${counts.PARTIAL} PARTIAL, ${counts.FAIL} FAIL, ${notStarted.length} NOT-STARTED (excluded)`;
65611
+ return { verdict, rolledUp, notStarted, summary };
65612
+ }
65613
+ var NOT_STARTED_STATUSES;
65614
+ var init_task_verdict = __esm(() => {
65615
+ NOT_STARTED_STATUSES = {
65616
+ backlog: true,
65617
+ todo: true,
65618
+ wip: false,
65619
+ testing: false,
65620
+ blocked: true,
65621
+ done: false,
65622
+ cancelled: false
65623
+ };
65624
+ });
65449
65625
 
65450
65626
  // ../../packages/app/src/services/team-service.ts
65451
65627
  import { join as join10 } from "path";
@@ -67538,8 +67714,10 @@ __export(exports_src2, {
67538
67714
  deriveVerdict: () => deriveVerdict,
67539
67715
  createPsProcessInspector: () => createPsProcessInspector,
67540
67716
  computeAggregate: () => computeAggregate,
67717
+ classifyTaskOutcome: () => classifyTaskOutcome,
67541
67718
  clampToolUseLimit: () => clampToolUseLimit,
67542
67719
  bridgeEventBus: () => bridgeEventBus,
67720
+ aggregateBatchVerdicts: () => aggregateBatchVerdicts,
67543
67721
  WorkflowAppService: () => WorkflowAppService,
67544
67722
  UnsupportedProcessPlatformError: () => UnsupportedProcessPlatformError,
67545
67723
  TokenLedgerWatcher: () => TokenLedgerWatcher,
@@ -67588,6 +67766,7 @@ __export(exports_src2, {
67588
67766
  FeatureCheckService: () => FeatureCheckService,
67589
67767
  FINDING_CODES: () => FINDING_CODES,
67590
67768
  FEATURE_LIFECYCLE_PROFILE: () => FEATURE_LIFECYCLE_PROFILE,
67769
+ DuplicateFollowUpError: () => DuplicateFollowUpError,
67591
67770
  DependencyMutationError: () => DependencyMutationError,
67592
67771
  DEFAULT_FEATURE_MATRIX: () => DEFAULT_FEATURE_MATRIX,
67593
67772
  CorpusMigrator: () => CorpusMigrator,
@@ -67619,6 +67798,7 @@ var init_src3 = __esm(() => {
67619
67798
  init_task_record();
67620
67799
  init_task_scaffold();
67621
67800
  init_task_service();
67801
+ init_task_verdict();
67622
67802
  init_team_service();
67623
67803
  init_token_ledger_service();
67624
67804
  init_token_ledger_watcher();
@@ -75628,7 +75808,7 @@ import { join as join15, resolve as resolve4 } from "path";
75628
75808
  var CLI_CONFIG = {
75629
75809
  binaryName: "spur",
75630
75810
  binaryLabel: "spur",
75631
- binaryVersion: "0.3.23",
75811
+ binaryVersion: "0.3.24",
75632
75812
  configDir: ".spur",
75633
75813
  configFile: ".spur/config.yaml",
75634
75814
  databaseFile: ".spur/spur.db"
@@ -86201,7 +86381,7 @@ function renderMigrationReport(report, dryRun, corpusDir) {
86201
86381
  }
86202
86382
  function registerTaskCommand(program2, context4) {
86203
86383
  const task = program2.command("task").summary("manage tasks");
86204
- task.command("create").summary("Create a new task with race-safe WBS allocation.").argument("<title>", "Task title").option("--feature <id>", "Feature ID for traceability and Goal\u2192Background derivation").option("--parent <wbs>", "Parent WBS for sub-task grouping").option("--template <variant>", `Template variant (${TASK_VARIANTS.join("|")})`).option("--folder <path>", "Custom tasks folder").option("--json", "Output machine-readable JSON").action(async (title2, options) => {
86384
+ task.command("create").summary("Create a new task with race-safe WBS allocation.").argument("<title>", "Task title").option("--feature <id>", "Feature ID for traceability and Goal\u2192Background derivation").option("--parent <wbs>", "Parent WBS for sub-task grouping").option("--template <variant>", `Template variant (${TASK_VARIANTS.join("|")})`).option("--folder <path>", "Custom tasks folder").option("--dedupe-within <seconds>", "Refuse creation if an existing task under the same feature has an identical name created within the last N seconds (task 0341 R4)", (v) => Number.parseInt(v, 10)).option("--allow-duplicate-name", "Override the --dedupe-within guard (creates anyway)").option("--json", "Output machine-readable JSON").action(async (title2, options) => {
86205
86385
  if (options.template !== undefined && !TASK_VARIANTS.includes(options.template)) {
86206
86386
  context4.output.error(`Unknown template variant "${options.template}". Valid: ${TASK_VARIANTS.join(", ")}`);
86207
86387
  context4.setExitCode(2);
@@ -86213,7 +86393,8 @@ function registerTaskCommand(program2, context4) {
86213
86393
  title: title2,
86214
86394
  featureId: options.feature,
86215
86395
  parentWbs: options.parent,
86216
- template: options.template
86396
+ template: options.template,
86397
+ dedupeWithinSec: options.allowDuplicateName ? undefined : options.dedupeWithin
86217
86398
  });
86218
86399
  if (options.json) {
86219
86400
  context4.output.write(toJson2(result));
@@ -86221,8 +86402,13 @@ function registerTaskCommand(program2, context4) {
86221
86402
  context4.output.write(`Created task ${result.ref.id}: ${result.ref.filePath}`);
86222
86403
  }
86223
86404
  } catch (err) {
86224
- context4.output.error(String(err));
86225
- context4.setExitCode(1);
86405
+ if (err instanceof DuplicateFollowUpError) {
86406
+ context4.output.error(err.message);
86407
+ context4.setExitCode(3);
86408
+ } else {
86409
+ context4.output.error(String(err));
86410
+ context4.setExitCode(1);
86411
+ }
86226
86412
  }
86227
86413
  });
86228
86414
  task.command("show").summary("Show a task by WBS.").argument("<wbs>", "Task WBS number").option("--folder <path>", "Custom tasks folder").option("--json", "Output machine-readable JSON").action(async (wbs, options) => {
@@ -86605,6 +86791,53 @@ ${result.content}`);
86605
86791
  context4.setExitCode(1);
86606
86792
  }
86607
86793
  });
86794
+ task.command("verifyall-aggregate").summary("Aggregate per-task verify outcomes into a deterministic batch verdict (verifyall).").description("Reads a JSON array of per-task outcomes ({wbs,outcome}) and emits the batch " + "verdict with NOT-STARTED tasks excluded from the rollup. Replaces agent-discretion " + "rollup prose (dev-operations.md \xA73a) with deterministic code (task 0341).").option("--from-file <path>", "Path to JSON array of {wbs,outcome[,reason]} rows").option("--json", "Output machine-readable JSON").action(async (options) => {
86795
+ const inputPath = options.fromFile ?? ".spur/run/verifyall-batch-input.json";
86796
+ let raw2;
86797
+ try {
86798
+ raw2 = readFileSync8(inputPath, "utf-8");
86799
+ } catch {
86800
+ context4.output.error(`Batch input file not found: ${inputPath}`);
86801
+ context4.setExitCode(1);
86802
+ return;
86803
+ }
86804
+ let rows;
86805
+ try {
86806
+ const parsed = JSON.parse(raw2);
86807
+ if (!Array.isArray(parsed)) {
86808
+ throw new Error("expected a JSON array");
86809
+ }
86810
+ rows = parsed;
86811
+ } catch (err) {
86812
+ context4.output.error(`Invalid batch input JSON: ${String(err)}`);
86813
+ context4.setExitCode(1);
86814
+ return;
86815
+ }
86816
+ const results = rows.map((r2) => {
86817
+ const outcome = r2.outcome.toUpperCase();
86818
+ if (!["PASS", "PARTIAL", "FAIL", "NOT-STARTED", "UNKNOWN"].includes(outcome)) {
86819
+ throw new Error(`Invalid outcome for ${r2.wbs}: ${r2.outcome}`);
86820
+ }
86821
+ return {
86822
+ wbs: r2.wbs,
86823
+ outcome,
86824
+ reason: r2.reason
86825
+ };
86826
+ });
86827
+ const aggregation = aggregateBatchVerdicts(results);
86828
+ if (options.json) {
86829
+ context4.output.write(JSON.stringify(aggregation, null, 2));
86830
+ } else {
86831
+ context4.output.write(`Batch verdict: ${aggregation.verdict}`);
86832
+ context4.output.write(aggregation.summary);
86833
+ if (aggregation.notStarted.length > 0) {
86834
+ context4.output.write(`NOT-STARTED (excluded from rollup): ${aggregation.notStarted.map((r2) => r2.wbs).join(", ")}`);
86835
+ }
86836
+ }
86837
+ if (aggregation.verdict === "FAIL") {
86838
+ context4.setExitCode(1);
86839
+ }
86840
+ });
86608
86841
  task.command("check").summary("Validate a task file through the four-layer check (design \xA73).").argument("[wbs]", "Task WBS number (validates all tasks in the folder when omitted)").option("--strict", "Elevate ALL warnings to failures").option("--strict-core", "Gate variant: fail only on hard-core errors (the testing\u2192done guard)").option("--folder <path>", "Custom tasks folder").option("--json", "Output machine-readable JSON").action(async (wbs, options) => {
86609
86842
  const svc = await makeCheckService(context4);
86610
86843
  const json3 = options.json === true;