@gobing-ai/spur 0.3.99 → 0.4.1

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 (172) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/config.global.yaml +17 -0
  3. package/config/decisions/gates.yaml +36 -0
  4. package/config/decisions/history-anatomy.yaml +35 -0
  5. package/config/decisions/idea-pipeline.yaml +38 -0
  6. package/config/decisions/task-pipeline.yaml +55 -0
  7. package/config/plugin-scripts.json +25 -0
  8. package/config/rules/boundary/sp-runtime-path.yaml +2 -0
  9. package/config/rules/docs/sourced-api-claims.yaml +60 -0
  10. package/config/rules/quality/coverage-gate.yaml +5 -3
  11. package/config/rules/recommended-pre-check.yaml +8 -3
  12. package/config/rules/strict/runtime-boundaries.yaml +11 -0
  13. package/config/rules/structure/protected-files.yaml +3 -0
  14. package/config/rules/structure/scratch-evidence-pointer.yaml +35 -0
  15. package/config/script-placement-baseline.json +2 -2
  16. package/config/workflows/feature-lifecycle.yaml +9 -2
  17. package/config/workflows/history-anatomy.yaml +22 -0
  18. package/config/workflows/idea-pipeline.yaml +75 -0
  19. package/config/workflows/task-pipeline.yaml +368 -49
  20. package/config/workflows/wrapup-pipeline.yaml +106 -13
  21. package/package.json +9 -9
  22. package/plugins/sp/README.md +25 -2
  23. package/plugins/sp/agents/super-planner.md +11 -0
  24. package/plugins/sp/agents/super-reviewer.md +39 -11
  25. package/plugins/sp/bunfig.toml +8 -0
  26. package/plugins/sp/commands/dev-brainstorm.md +29 -16
  27. package/plugins/sp/commands/dev-changelog.md +5 -4
  28. package/plugins/sp/commands/dev-find-way.md +80 -0
  29. package/plugins/sp/commands/dev-idea.md +6 -0
  30. package/plugins/sp/commands/dev-parallel.md +8 -2
  31. package/plugins/sp/commands/dev-plan.md +6 -0
  32. package/plugins/sp/commands/dev-review-session.md +14 -7
  33. package/plugins/sp/commands/dev-run.md +17 -4
  34. package/plugins/sp/commands/dev-runall.md +21 -10
  35. package/plugins/sp/hooks/agent-lifecycle.ts +155 -0
  36. package/plugins/sp/hooks/context-session-start.ts +1 -1
  37. package/plugins/sp/hooks/fleet-guest-stop.ts +180 -0
  38. package/plugins/sp/hooks/hooks.json +45 -1
  39. package/plugins/sp/hooks/pi/guard-extension.ts +21 -0
  40. package/plugins/sp/lib/history-anatomy.generated.mjs +5 -0
  41. package/plugins/sp/lib/idea-handoff.generated.mjs +72 -11
  42. package/plugins/sp/lib/inline-run-app.ts +28 -0
  43. package/plugins/sp/lib/inline-run.generated.d.mts +6 -0
  44. package/plugins/sp/lib/inline-run.generated.mjs +364 -46
  45. package/plugins/sp/lib/plan-projection.generated.d.mts +28 -0
  46. package/plugins/sp/lib/plan-projection.generated.mjs +255 -0
  47. package/plugins/sp/lib/quality-gate.generated.d.mts +47 -1
  48. package/plugins/sp/lib/quality-gate.generated.mjs +30489 -82
  49. package/plugins/sp/lib/residual-scan.generated.d.mts +19 -0
  50. package/plugins/sp/lib/residual-scan.generated.mjs +66 -0
  51. package/plugins/sp/lib/run-summary-core.ts +561 -0
  52. package/plugins/sp/lib/transcript.ts +275 -0
  53. package/plugins/sp/plugin.json +1 -1
  54. package/plugins/sp/references/roles.md +1 -1
  55. package/plugins/sp/scripts/batch-plan.mjs +335 -0
  56. package/plugins/sp/scripts/batch-plan.ts +100 -0
  57. package/plugins/sp/scripts/batch-preflight.mjs +51 -3
  58. package/plugins/sp/scripts/batch-preflight.ts +72 -1
  59. package/plugins/sp/scripts/commit-guard.mjs +216 -0
  60. package/plugins/sp/scripts/commit-guard.ts +249 -0
  61. package/plugins/sp/scripts/history-anatomy-cache.mjs +5 -0
  62. package/plugins/sp/scripts/inline-run-dispatch.mjs +130 -0
  63. package/plugins/sp/scripts/inline-run-dispatch.ts +135 -0
  64. package/plugins/sp/scripts/inline-run-setup.mjs +563 -91
  65. package/plugins/sp/scripts/inline-run-setup.ts +65 -97
  66. package/plugins/sp/scripts/persist-out-check.mjs +90 -41
  67. package/plugins/sp/scripts/persist-out-check.ts +112 -57
  68. package/plugins/sp/scripts/quality-gate.mjs +30407 -73
  69. package/plugins/sp/scripts/quality-gate.ts +21 -5
  70. package/plugins/sp/scripts/residual-scan.mjs +88 -38
  71. package/plugins/sp/scripts/residual-scan.ts +35 -38
  72. package/plugins/sp/scripts/run-summary.mjs +693 -0
  73. package/plugins/sp/scripts/run-summary.ts +105 -0
  74. package/plugins/sp/scripts/session-timeline.mjs +331 -0
  75. package/plugins/sp/scripts/session-timeline.ts +204 -0
  76. package/plugins/sp/scripts/wrapup-steps.mjs +150 -2
  77. package/plugins/sp/scripts/wrapup-steps.ts +203 -1
  78. package/plugins/sp/skills/brainstorm/SKILL.md +9 -47
  79. package/plugins/sp/skills/branch-workflow/SKILL.md +1 -0
  80. package/plugins/sp/skills/branch-workflow/references/worktree-patterns.md +1 -0
  81. package/plugins/sp/skills/code-implementation/SKILL.md +48 -1
  82. package/plugins/sp/skills/code-verification/SKILL.md +8 -4
  83. package/plugins/sp/skills/code-verification/references/secu-review.md +3 -0
  84. package/plugins/sp/skills/code-verification/references/verdict-schema.md +68 -0
  85. package/plugins/sp/skills/fleet-join/SKILL.md +63 -0
  86. package/plugins/sp/skills/session-review/SKILL.md +77 -24
  87. package/plugins/sp/skills/source-driven-development/SKILL.md +5 -0
  88. package/plugins/sp/skills/spec-decomposition/references/decomposition.md +5 -3
  89. package/plugins/sp/skills/spur-cli/SKILL.md +6 -2
  90. package/plugins/sp/skills/spur-cli/references/agent.md +124 -17
  91. package/plugins/sp/skills/spur-cli/references/decision.md +98 -0
  92. package/plugins/sp/skills/spur-cli/references/message.md +7 -7
  93. package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +41 -3
  94. package/plugins/sp/skills/spur-cli/references/tasks.md +10 -7
  95. package/plugins/sp/skills/spur-cli/references/workflows/authoring-workflows.md +7 -0
  96. package/plugins/sp/skills/spur-cli/references/workflows.md +7 -2
  97. package/plugins/sp/skills/spur-composer/SKILL.md +28 -2
  98. package/plugins/sp/skills/spur-dev/SKILL.md +18 -14
  99. package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +6 -3
  100. package/plugins/sp/skills/spur-dev/references/cross-cutting.md +49 -14
  101. package/plugins/sp/skills/spur-dev/references/dev-operations.md +51 -18
  102. package/plugins/sp/skills/spur-dev/references/execution-batch-continuation.md +68 -0
  103. package/plugins/sp/skills/spur-dev/references/execution-batch-report.md +194 -0
  104. package/plugins/sp/skills/spur-dev/references/execution-batch.md +113 -963
  105. package/plugins/sp/skills/spur-dev/references/execution-parallel-isolation.md +130 -0
  106. package/plugins/sp/skills/spur-dev/references/execution-workflow.md +20 -0
  107. package/plugins/sp/skills/spur-dev/references/execution-worktree-landing.md +471 -0
  108. package/plugins/sp/skills/spur-dev/references/execution-worktree-setup.md +379 -0
  109. package/plugins/sp/skills/spur-dev/references/flag-glossary.md +51 -9
  110. package/plugins/sp/skills/spur-dev/references/glossary.md +2 -2
  111. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +361 -156
  112. package/plugins/sp/skills/spur-dev/references/planning-workflow.md +6 -0
  113. package/plugins/sp/skills/spur-dev/references/structured-trace-emission.md +178 -0
  114. package/plugins/sp/skills/wayfinder/SKILL.md +10 -14
  115. package/schemas/decision-catalog.schema.json +226 -0
  116. package/schemas/spur-config.schema.json +25 -0
  117. package/schemas/state-machine-workflow.schema.json +23 -0
  118. package/spur.js +28165 -19051
  119. package/web/_astro/{BoardApp.i4eh_fHA.js → BoardApp.D66k_s_T.js} +1 -1
  120. package/web/_astro/BoardApp.D6sjAJr_.js +324 -0
  121. package/web/_astro/{TaskDetail.C5Cu8LdB.js → TaskDetail.BynLnMNc.js} +1 -1
  122. package/web/_astro/{arc.Bu5XpBEz.js → arc._BUadrdH.js} +1 -1
  123. package/web/_astro/{architectureDiagram-3BPJPVTR.9vHQTvhF.js → architectureDiagram-3BPJPVTR.Dh7xS1Pp.js} +1 -1
  124. package/web/_astro/{blockDiagram-GPEHLZMM.Cn2Xl7D6.js → blockDiagram-GPEHLZMM.DFDVpfXm.js} +1 -1
  125. package/web/_astro/{c4Diagram-AAUBKEIU.7o0P_QDo.js → c4Diagram-AAUBKEIU.V7_3_glh.js} +1 -1
  126. package/web/_astro/channel.DZVfto-r.js +1 -0
  127. package/web/_astro/{chunk-2J33WTMH.C3zZFsNt.js → chunk-2J33WTMH.De1yx7Jz.js} +1 -1
  128. package/web/_astro/{chunk-4BX2VUAB.CNy_M7fO.js → chunk-4BX2VUAB.BDhHFuxx.js} +1 -1
  129. package/web/_astro/{chunk-55IACEB6.2TiPnLBL.js → chunk-55IACEB6.DDLkWnmC.js} +1 -1
  130. package/web/_astro/{chunk-727SXJPM.5CGSyKAp.js → chunk-727SXJPM.CwfiLnA7.js} +1 -1
  131. package/web/_astro/{chunk-AQP2D5EJ.w-ZIgfsK.js → chunk-AQP2D5EJ.CtGDmUnM.js} +1 -1
  132. package/web/_astro/{chunk-FMBD7UC4.BkcEyhKa.js → chunk-FMBD7UC4.BOLjpw73.js} +1 -1
  133. package/web/_astro/{chunk-ND2GUHAM.mxKKbBG4.js → chunk-ND2GUHAM.BDhQls6O.js} +1 -1
  134. package/web/_astro/{chunk-QZHKN3VN.cyA0z9aT.js → chunk-QZHKN3VN.76qBynDF.js} +1 -1
  135. package/web/_astro/{classDiagram-4FO5ZUOK.CqtUzwhX.js → classDiagram-4FO5ZUOK.N0mpPHT0.js} +1 -1
  136. package/web/_astro/{classDiagram-v2-Q7XG4LA2.CqtUzwhX.js → classDiagram-v2-Q7XG4LA2.N0mpPHT0.js} +1 -1
  137. package/web/_astro/{cose-bilkent-S5V4N54A.BExPTopI.js → cose-bilkent-S5V4N54A.CKknFfjB.js} +1 -1
  138. package/web/_astro/{cynefin-OW5HDTMX.Bq2i10Cn.js → cynefin-OW5HDTMX.Cw3ZWBAh.js} +1 -1
  139. package/web/_astro/{dagre-BM42HDAG.By_HsOuv.js → dagre-BM42HDAG.D3yIKCRD.js} +1 -1
  140. package/web/_astro/{diagram-2AECGRRQ.BtU0b7hz.js → diagram-2AECGRRQ.D51dO83Z.js} +1 -1
  141. package/web/_astro/{diagram-5GNKFQAL.D00ztMdT.js → diagram-5GNKFQAL.Bb-FViDj.js} +1 -1
  142. package/web/_astro/{diagram-KO2AKTUF.DMVJWaUN.js → diagram-KO2AKTUF.D3mN8S6x.js} +1 -1
  143. package/web/_astro/{diagram-LMA3HP47.Cxqrjg5-.js → diagram-LMA3HP47.dMMyPcbJ.js} +1 -1
  144. package/web/_astro/{diagram-OG6HWLK6.2lz_1B5h.js → diagram-OG6HWLK6.AzbvzY4C.js} +1 -1
  145. package/web/_astro/{erDiagram-TEJ5UH35.BVQEuJ3T.js → erDiagram-TEJ5UH35.BR5I4OZb.js} +1 -1
  146. package/web/_astro/{flowDiagram-I6XJVG4X.Qlyo90ky.js → flowDiagram-I6XJVG4X.DFqvhGkn.js} +1 -1
  147. package/web/_astro/{ganttDiagram-6RSMTGT7.DRwb8HHZ.js → ganttDiagram-6RSMTGT7.ByJbhH6J.js} +1 -1
  148. package/web/_astro/{gitGraphDiagram-PVQCEYII.YfoPthfS.js → gitGraphDiagram-PVQCEYII.CInHjQR3.js} +1 -1
  149. package/web/_astro/index.COh_kBxN.css +1 -0
  150. package/web/_astro/{infoDiagram-5YYISTIA.DvFREck1.js → infoDiagram-5YYISTIA.Cm0dGVjJ.js} +1 -1
  151. package/web/_astro/{ishikawaDiagram-YF4QCWOH.BzCj9Q0-.js → ishikawaDiagram-YF4QCWOH.C3nOP6Sj.js} +1 -1
  152. package/web/_astro/{journeyDiagram-JHISSGLW.BDMT_AFb.js → journeyDiagram-JHISSGLW.4IvLa2MW.js} +1 -1
  153. package/web/_astro/{kanban-definition-UN3LZRKU.D6Fh3gj9.js → kanban-definition-UN3LZRKU.DS87Cl-k.js} +1 -1
  154. package/web/_astro/{linear.Dxw00fGQ.js → linear.MzmRrNER.js} +1 -1
  155. package/web/_astro/{mermaid.core.CsoxUWa0.js → mermaid.core.fK-WNswS.js} +4 -4
  156. package/web/_astro/{mindmap-definition-RKZ34NQL.CN8oeHLu.js → mindmap-definition-RKZ34NQL.BqipOHhY.js} +1 -1
  157. package/web/_astro/{pieDiagram-4H26LBE5.C7ADqQ_7.js → pieDiagram-4H26LBE5.0nBiPqFG.js} +1 -1
  158. package/web/_astro/{quadrantDiagram-W4KKPZXB.Cw9lKyHP.js → quadrantDiagram-W4KKPZXB.Szj7Zpx5.js} +1 -1
  159. package/web/_astro/{requirementDiagram-4Y6WPE33.B1ni-z4q.js → requirementDiagram-4Y6WPE33.D4UR6TWo.js} +1 -1
  160. package/web/_astro/{sankeyDiagram-5OEKKPKP.BhexdjVr.js → sankeyDiagram-5OEKKPKP.DAE7BdOs.js} +1 -1
  161. package/web/_astro/{sequenceDiagram-3UESZ5HK.C3Rm7dpQ.js → sequenceDiagram-3UESZ5HK.BP9ZVq25.js} +1 -1
  162. package/web/_astro/{stateDiagram-AJRCARHV.CkuMt27u.js → stateDiagram-AJRCARHV.BQW7mIxk.js} +1 -1
  163. package/web/_astro/{stateDiagram-v2-BHNVJYJU.Dc_aazUA.js → stateDiagram-v2-BHNVJYJU.PbK4nDih.js} +1 -1
  164. package/web/_astro/{timeline-definition-PNZ67QCA.ja7sUFhH.js → timeline-definition-PNZ67QCA.Ci_GvUpo.js} +1 -1
  165. package/web/_astro/{vennDiagram-CIIHVFJN.BxIshX_K.js → vennDiagram-CIIHVFJN.DNw1Y5qx.js} +1 -1
  166. package/web/_astro/{wardleyDiagram-YWT4CUSO.DfHlC0sh.js → wardleyDiagram-YWT4CUSO.CwlkaaNe.js} +1 -1
  167. package/web/_astro/{xychartDiagram-2RQKCTM6.DSnhHwCh.js → xychartDiagram-2RQKCTM6.MI6ZuiH_.js} +1 -1
  168. package/web/board-runtime.json +1 -1
  169. package/web/index.html +2 -2
  170. package/web/_astro/BoardApp.BVp-LYi6.js +0 -322
  171. package/web/_astro/channel.6IFIKBhu.js +0 -1
  172. package/web/_astro/index.CbNz17Sx.css +0 -1
@@ -443,7 +443,137 @@ function runFeatureTransition(env, options = {}) {
443
443
  `);
444
444
  return { status: syncStatus, statusFile: relStatusFile, exitCode: 0 };
445
445
  }
446
- var WRAPUP_STEPS_USAGE = "usage: wrapup-steps.ts <resolve|route-reason|metrics|feature-transition> (env: __runId, tasks, mode, feature, featureGateCmd, spurBin)";
446
+ var DOC_SYNC_SNAPSHOT_SUFFIX = "wrapup-doc-sync-snapshot.txt";
447
+ var DOC_SYNC_SCOPE_STATUS_SUFFIX = "wrapup-doc-sync-scope.status";
448
+ var DOC_SYNC_SUPERSESSION_STATUS_SUFFIX = "wrapup-doc-supersession.status";
449
+ var SUPERSESSION_PIN = "repo-wide-tests/adr-supersession.test.ts";
450
+ var GIT_UNAVAILABLE_MARKER = "# git-unavailable:";
451
+ var SNAPSHOT_MISSING = "snapshot-missing";
452
+ function porcelainPath(line) {
453
+ const body = line.length >= 3 ? line.slice(3) : line;
454
+ const arrow = body.indexOf(" -> ");
455
+ const target = arrow >= 0 ? body.slice(arrow + 4) : body;
456
+ const trimmed = target.trim();
457
+ return trimmed.startsWith('"') && trimmed.endsWith('"') && trimmed.length >= 2 ? trimmed.slice(1, -1) : trimmed;
458
+ }
459
+ function isDocSyncAllowedPath(path, allowedExtra) {
460
+ if (allowedExtra.includes(path))
461
+ return true;
462
+ if (!path.startsWith("docs/"))
463
+ return false;
464
+ return !path.startsWith("docs/tasks") && !path.startsWith("docs/features");
465
+ }
466
+ function docSyncScopeViolations(snapshot, current, allowedExtra) {
467
+ const before = new Set(snapshot.split(`
468
+ `).filter((line) => line.trim() !== ""));
469
+ const paths = new Set;
470
+ for (const line of current.split(`
471
+ `)) {
472
+ if (line.trim() === "" || before.has(line))
473
+ continue;
474
+ const path = porcelainPath(line);
475
+ if (!isDocSyncAllowedPath(path, allowedExtra))
476
+ paths.add(path);
477
+ }
478
+ return [...paths].sort();
479
+ }
480
+ function gitPorcelain(cwd) {
481
+ const result = spawnSync("git", ["status", "--porcelain", "-uall"], {
482
+ cwd,
483
+ encoding: "utf8"
484
+ });
485
+ if (result.error !== undefined)
486
+ return { ok: false, listing: "", detail: String(result.error) };
487
+ if (result.status !== 0)
488
+ return { ok: false, listing: "", detail: (result.stderr ?? "").trim() };
489
+ return { ok: true, listing: result.stdout ?? "", detail: "" };
490
+ }
491
+ function wrapupRunPaths(env, cwd) {
492
+ const runId = env.__runId ?? "";
493
+ const rel = (suffix) => join(".spur", "run", `${runId}-${suffix}`);
494
+ return { rel, abs: (suffix) => cwd ? join(cwd, rel(suffix)) : rel(suffix) };
495
+ }
496
+ function docSyncAllowedExtra(env) {
497
+ const runId = env.__runId ?? "";
498
+ return [
499
+ join(".spur", "run", `${runId}-wrapup-learnings.md`),
500
+ join(".spur", "run", `${runId}-${DOC_SYNC_SNAPSHOT_SUFFIX}`),
501
+ join(".spur", "run", `${runId}-${DOC_SYNC_SCOPE_STATUS_SUFFIX}`)
502
+ ];
503
+ }
504
+ function runDocSyncSnapshot(env, options = {}) {
505
+ const cwd = options.cwd;
506
+ const paths = wrapupRunPaths(env, cwd);
507
+ mkdirSync(cwd ? join(cwd, ".spur", "run") : join(".spur", "run"), { recursive: true });
508
+ writeFileSync(paths.abs(DOC_SYNC_SNAPSHOT_SUFFIX), "");
509
+ const listing = gitPorcelain(cwd);
510
+ if (!listing.ok) {
511
+ writeFileSync(paths.abs(DOC_SYNC_SNAPSHOT_SUFFIX), `${GIT_UNAVAILABLE_MARKER} ${listing.detail}
512
+ `);
513
+ process.stderr.write(`doc-sync-snapshot: git status unavailable \u2014 scope check skips (${listing.detail})
514
+ `);
515
+ return { statusFile: paths.rel(DOC_SYNC_SNAPSHOT_SUFFIX), captured: false, exitCode: 0 };
516
+ }
517
+ writeFileSync(paths.abs(DOC_SYNC_SNAPSHOT_SUFFIX), listing.listing);
518
+ return { statusFile: paths.rel(DOC_SYNC_SNAPSHOT_SUFFIX), captured: true, exitCode: 0 };
519
+ }
520
+ function runDocSyncScope(env, options = {}) {
521
+ const cwd = options.cwd;
522
+ const paths = wrapupRunPaths(env, cwd);
523
+ mkdirSync(cwd ? join(cwd, ".spur", "run") : join(".spur", "run"), { recursive: true });
524
+ const statusFile = paths.rel(DOC_SYNC_SCOPE_STATUS_SUFFIX);
525
+ const write = (status, detail) => {
526
+ writeFileSync(paths.abs(DOC_SYNC_SCOPE_STATUS_SUFFIX), `${detail}
527
+ `);
528
+ if (status !== "PASS")
529
+ process.stderr.write(`doc-sync-scope: ${detail}
530
+ `);
531
+ return { status, statusFile, violations: status === "PASS" ? [] : [detail], exitCode: 0 };
532
+ };
533
+ const snapshot = readFileSyncSafe(paths.abs(DOC_SYNC_SNAPSHOT_SUFFIX));
534
+ if (snapshot === null) {
535
+ return write("scope-violation", `scope-violation: ${SNAPSHOT_MISSING}`);
536
+ }
537
+ if (snapshot.startsWith(GIT_UNAVAILABLE_MARKER)) {
538
+ process.stderr.write(`doc-sync-scope: entry snapshot recorded no git listing \u2014 scope check skipped
539
+ `);
540
+ return write("PASS", "PASS");
541
+ }
542
+ const current = gitPorcelain(cwd);
543
+ if (!current.ok) {
544
+ return write("scope-violation", `scope-violation: git-unavailable (${current.detail})`);
545
+ }
546
+ const violations = docSyncScopeViolations(snapshot, current.listing, docSyncAllowedExtra(env));
547
+ if (violations.length === 0)
548
+ return write("PASS", "PASS");
549
+ return write("scope-violation", `scope-violation: ${violations.join(" ")}`);
550
+ }
551
+ function runDocSupersessionCheck(env, options = {}) {
552
+ const cwd = options.cwd;
553
+ const paths = wrapupRunPaths(env, cwd);
554
+ mkdirSync(cwd ? join(cwd, ".spur", "run") : join(".spur", "run"), { recursive: true });
555
+ const statusFile = paths.rel(DOC_SYNC_SUPERSESSION_STATUS_SUFFIX);
556
+ const pinPath = cwd ? join(cwd, SUPERSESSION_PIN) : SUPERSESSION_PIN;
557
+ if (!existsSync(pinPath)) {
558
+ writeFileSync(paths.abs(DOC_SYNC_SUPERSESSION_STATUS_SUFFIX), `PASS
559
+ `);
560
+ return { status: "PASS", statusFile, exitCode: 0 };
561
+ }
562
+ const pin = spawnSync("bun", ["test", SUPERSESSION_PIN], { cwd, encoding: "utf8" });
563
+ if (pin.status === 0) {
564
+ writeFileSync(paths.abs(DOC_SYNC_SUPERSESSION_STATUS_SUFFIX), `PASS
565
+ `);
566
+ return { status: "PASS", statusFile, exitCode: 0 };
567
+ }
568
+ writeFileSync(paths.abs(DOC_SYNC_SUPERSESSION_STATUS_SUFFIX), `supersession-pin-failed: ${SUPERSESSION_PIN}
569
+ `);
570
+ const detail = `${pin.stdout ?? ""}${pin.stderr ?? ""}`.trim();
571
+ process.stderr.write(`doc-supersession: ${SUPERSESSION_PIN} failed (exit ${String(pin.status ?? 1)}) \u2014 the doc-sync step introduced or kept superseded index content:
572
+ ${detail.slice(-2000)}
573
+ `);
574
+ return { status: "supersession-pin-failed", statusFile, exitCode: 0 };
575
+ }
576
+ var WRAPUP_STEPS_USAGE = "usage: wrapup-steps.ts <resolve|route-reason|metrics|feature-transition|doc-sync-snapshot|doc-sync-scope|doc-supersession> (env: __runId, tasks, mode, feature, featureGateCmd, spurBin)";
447
577
  function main(argv, env = getEnvVars(), options = {}) {
448
578
  const sub = argv[0];
449
579
  if (sub === "resolve")
@@ -456,6 +586,12 @@ function main(argv, env = getEnvVars(), options = {}) {
456
586
  }
457
587
  if (sub === "feature-transition")
458
588
  return runFeatureTransition(env, options).exitCode;
589
+ if (sub === "doc-sync-snapshot")
590
+ return runDocSyncSnapshot(env, options).exitCode;
591
+ if (sub === "doc-sync-scope")
592
+ return runDocSyncScope(env, options).exitCode;
593
+ if (sub === "doc-supersession")
594
+ return runDocSupersessionCheck(env, options).exitCode;
459
595
  process.stderr.write(`${WRAPUP_STEPS_USAGE}
460
596
  `);
461
597
  return 2;
@@ -469,11 +605,23 @@ export {
469
605
  taskStatusOf,
470
606
  runMetrics,
471
607
  runFeatureTransition,
608
+ runDocSyncSnapshot,
609
+ runDocSyncScope,
610
+ runDocSupersessionCheck,
472
611
  resolveTasks,
473
612
  preflightFeature,
613
+ porcelainPath,
474
614
  main,
475
615
  jqPick,
616
+ isDocSyncAllowedPath,
617
+ docSyncScopeViolations,
476
618
  classifySync,
477
619
  WRAPUP_STEPS_USAGE,
478
- WBS_PATTERN
620
+ WBS_PATTERN,
621
+ SUPERSESSION_PIN,
622
+ SNAPSHOT_MISSING,
623
+ GIT_UNAVAILABLE_MARKER,
624
+ DOC_SYNC_SUPERSESSION_STATUS_SUFFIX,
625
+ DOC_SYNC_SNAPSHOT_SUFFIX,
626
+ DOC_SYNC_SCOPE_STATUS_SUFFIX
479
627
  };
@@ -14,6 +14,9 @@
14
14
  * - `.spur/memory/wrapup-metrics.jsonl` one row per task (metrics)
15
15
  * - `<runId>-wrapup-metrics.status` `PASS`/`FAIL` (metrics)
16
16
  * - `<runId>-wrapup-sync.status` `PASS`/`FAIL` (feature-transition)
17
+ * - `<runId>-wrapup-doc-sync-snapshot.txt` working listing at doc-sync entry (doc-sync-snapshot)
18
+ * - `<runId>-wrapup-doc-sync-scope.status` `PASS`/`scope-violation: <paths>` (doc-sync-scope)
19
+ * - `<runId>-wrapup-doc-supersession.status` `PASS`/`supersession-pin-failed: <pin>` (doc-supersession)
17
20
  *
18
21
  * Environment comes from the workflow vars: `__runId`, `tasks`, `feature`, `featureGateCmd`
19
22
  * and `spurBin` (split on whitespace into a command plus prefix args, so
@@ -668,8 +671,204 @@ export function runFeatureTransition(env: WrapupStepsEnv, options: WrapupStepsOp
668
671
  return { status: syncStatus, statusFile: relStatusFile, exitCode: 0 };
669
672
  }
670
673
 
674
+ /** Run-scoped snapshot of the working listing taken at doc-sync entry (1147 R1). */
675
+ export const DOC_SYNC_SNAPSHOT_SUFFIX = 'wrapup-doc-sync-snapshot.txt';
676
+ /** Run-scoped verdict of the post-doc-sync write-scope check (1147 R1). */
677
+ export const DOC_SYNC_SCOPE_STATUS_SUFFIX = 'wrapup-doc-sync-scope.status';
678
+ /** Run-scoped verdict of the post-doc-sync supersession-pin re-run (1147 R2). */
679
+ export const DOC_SYNC_SUPERSESSION_STATUS_SUFFIX = 'wrapup-doc-supersession.status';
680
+ /**
681
+ * The supersession pins doc-sync must not resurrect (1147 R2). Re-run right after doc-sync so a
682
+ * re-added delinked index row is attributed to doc-sync instead of to the later `doc-tripwire`
683
+ * backstop, which stays unchanged.
684
+ */
685
+ export const SUPERSESSION_PIN = 'repo-wide-tests/adr-supersession.test.ts';
686
+ /** Snapshot marker for "git could not list the tree" — the check then skips instead of guessing. */
687
+ export const GIT_UNAVAILABLE_MARKER = '# git-unavailable:';
688
+
689
+ /** Snapshot marker the scope check reports when doc-sync's entry snapshot is absent (fail closed). */
690
+ export const SNAPSHOT_MISSING = 'snapshot-missing';
691
+
692
+ /**
693
+ * One `git status --porcelain` line → the repo-relative path it names. A rename/copy line
694
+ * (`R old -> new`) reports its target, and a quoted path is unquoted (1147 R1).
695
+ */
696
+ export function porcelainPath(line: string): string {
697
+ const body = line.length >= 3 ? line.slice(3) : line;
698
+ const arrow = body.indexOf(' -> ');
699
+ const target = arrow >= 0 ? body.slice(arrow + 4) : body;
700
+ const trimmed = target.trim();
701
+ return trimmed.startsWith('"') && trimmed.endsWith('"') && trimmed.length >= 2 ? trimmed.slice(1, -1) : trimmed;
702
+ }
703
+
704
+ /**
705
+ * doc-sync's declared write scope (1147 R1): `docs/**` except the task/feature corpus, the
706
+ * learnings capture the step declares, and the guard's own snapshot/status bookkeeping (never
707
+ * doc-sync edits, and present in the listing on a repeated check).
708
+ */
709
+ export function isDocSyncAllowedPath(path: string, allowedExtra: readonly string[]): boolean {
710
+ if (allowedExtra.includes(path)) return true;
711
+ if (!path.startsWith('docs/')) return false;
712
+ return !path.startsWith('docs/tasks') && !path.startsWith('docs/features');
713
+ }
714
+
715
+ /**
716
+ * Paths doc-sync newly dirtied: the current porcelain listing minus the entry snapshot's lines.
717
+ * A line present in both listings is pre-existing dirt (a sibling task's uncommitted work) and is
718
+ * never attributed to this pass (1147 R1).
719
+ */
720
+ export function docSyncScopeViolations(snapshot: string, current: string, allowedExtra: readonly string[]): string[] {
721
+ const before = new Set(snapshot.split('\n').filter((line) => line.trim() !== ''));
722
+ const paths = new Set<string>();
723
+ for (const line of current.split('\n')) {
724
+ if (line.trim() === '' || before.has(line)) continue;
725
+ const path = porcelainPath(line);
726
+ if (!isDocSyncAllowedPath(path, allowedExtra)) paths.add(path);
727
+ }
728
+ return [...paths].sort();
729
+ }
730
+
731
+ /** `git status --porcelain -uall` — the listing both the snapshot and the scope check compare. */
732
+ function gitPorcelain(cwd?: string): { ok: boolean; listing: string; detail: string } {
733
+ const result = spawnSync('git', ['status', '--porcelain', '-uall'], {
734
+ cwd,
735
+ encoding: 'utf8',
736
+ });
737
+ if (result.error !== undefined) return { ok: false, listing: '', detail: String(result.error) };
738
+ if (result.status !== 0) return { ok: false, listing: '', detail: (result.stderr ?? '').trim() };
739
+ return { ok: true, listing: result.stdout ?? '', detail: '' };
740
+ }
741
+
742
+ interface WrapupRunPaths {
743
+ rel: (suffix: string) => string;
744
+ abs: (suffix: string) => string;
745
+ }
746
+
747
+ function wrapupRunPaths(env: WrapupStepsEnv, cwd?: string): WrapupRunPaths {
748
+ const runId = env.__runId ?? '';
749
+ const rel = (suffix: string): string => join('.spur', 'run', `${runId}-${suffix}`);
750
+ return { rel, abs: (suffix: string) => (cwd ? join(cwd, rel(suffix)) : rel(suffix)) };
751
+ }
752
+
753
+ /** The four path shapes this check knows are not doc-sync edits (1147 R1). */
754
+ function docSyncAllowedExtra(env: WrapupStepsEnv): string[] {
755
+ const runId = env.__runId ?? '';
756
+ return [
757
+ join('.spur', 'run', `${runId}-wrapup-learnings.md`),
758
+ join('.spur', 'run', `${runId}-${DOC_SYNC_SNAPSHOT_SUFFIX}`),
759
+ join('.spur', 'run', `${runId}-${DOC_SYNC_SCOPE_STATUS_SUFFIX}`),
760
+ ];
761
+ }
762
+
763
+ /** Outcome of `doc-sync-snapshot`: the recorded entry listing (or a git-unavailable marker). */
764
+ export interface DocSyncSnapshotResult {
765
+ statusFile: string;
766
+ /** True when the listing was captured; false when git could not list the tree. */
767
+ captured: boolean;
768
+ exitCode: number;
769
+ }
770
+
771
+ /**
772
+ * `doc-sync-snapshot` — record the working listing at doc-sync entry (1147 R1), before the model
773
+ * query runs, so the post-doc-sync check can attribute only this step's writes. Never aborts: a
774
+ * git failure is recorded in the snapshot (the check then skips rather than guessing).
775
+ */
776
+ export function runDocSyncSnapshot(env: WrapupStepsEnv, options: WrapupStepsOptions = {}): DocSyncSnapshotResult {
777
+ const cwd = options.cwd;
778
+ const paths = wrapupRunPaths(env, cwd);
779
+ mkdirSync(cwd ? join(cwd, '.spur', 'run') : join('.spur', 'run'), { recursive: true });
780
+ // Create the file BEFORE listing so it is present in both listings (never a false violation).
781
+ writeFileSync(paths.abs(DOC_SYNC_SNAPSHOT_SUFFIX), '');
782
+ const listing = gitPorcelain(cwd);
783
+ if (!listing.ok) {
784
+ writeFileSync(paths.abs(DOC_SYNC_SNAPSHOT_SUFFIX), `${GIT_UNAVAILABLE_MARKER} ${listing.detail}\n`);
785
+ process.stderr.write(`doc-sync-snapshot: git status unavailable — scope check skips (${listing.detail})\n`);
786
+ return { statusFile: paths.rel(DOC_SYNC_SNAPSHOT_SUFFIX), captured: false, exitCode: 0 };
787
+ }
788
+ writeFileSync(paths.abs(DOC_SYNC_SNAPSHOT_SUFFIX), listing.listing);
789
+ return { statusFile: paths.rel(DOC_SYNC_SNAPSHOT_SUFFIX), captured: true, exitCode: 0 };
790
+ }
791
+
792
+ /** Outcome of `doc-sync-scope`: the recorded verdict plus the offending paths it named. */
793
+ export interface DocSyncScopeResult {
794
+ status: 'PASS' | 'scope-violation';
795
+ statusFile: string;
796
+ violations: string[];
797
+ exitCode: number;
798
+ }
799
+
800
+ /**
801
+ * `doc-sync-scope` — the deterministic write-scope guard (1147 R1): diff the working listing
802
+ * against the entry snapshot and fail the step on any path outside the declared scope. The
803
+ * violating edits are reported, never reverted (the operator decides); the verdict lives in the
804
+ * status file so the workflow guard routes on it.
805
+ */
806
+ export function runDocSyncScope(env: WrapupStepsEnv, options: WrapupStepsOptions = {}): DocSyncScopeResult {
807
+ const cwd = options.cwd;
808
+ const paths = wrapupRunPaths(env, cwd);
809
+ mkdirSync(cwd ? join(cwd, '.spur', 'run') : join('.spur', 'run'), { recursive: true });
810
+ const statusFile = paths.rel(DOC_SYNC_SCOPE_STATUS_SUFFIX);
811
+ const write = (status: DocSyncScopeResult['status'], detail: string): DocSyncScopeResult => {
812
+ writeFileSync(paths.abs(DOC_SYNC_SCOPE_STATUS_SUFFIX), `${detail}\n`);
813
+ if (status !== 'PASS') process.stderr.write(`doc-sync-scope: ${detail}\n`);
814
+ return { status, statusFile, violations: status === 'PASS' ? [] : [detail], exitCode: 0 };
815
+ };
816
+
817
+ const snapshot = readFileSyncSafe(paths.abs(DOC_SYNC_SNAPSHOT_SUFFIX));
818
+ if (snapshot === null) {
819
+ return write('scope-violation', `scope-violation: ${SNAPSHOT_MISSING}`);
820
+ }
821
+ if (snapshot.startsWith(GIT_UNAVAILABLE_MARKER)) {
822
+ process.stderr.write('doc-sync-scope: entry snapshot recorded no git listing — scope check skipped\n');
823
+ return write('PASS', 'PASS');
824
+ }
825
+ const current = gitPorcelain(cwd);
826
+ if (!current.ok) {
827
+ return write('scope-violation', `scope-violation: git-unavailable (${current.detail})`);
828
+ }
829
+ const violations = docSyncScopeViolations(snapshot, current.listing, docSyncAllowedExtra(env));
830
+ if (violations.length === 0) return write('PASS', 'PASS');
831
+ return write('scope-violation', `scope-violation: ${violations.join(' ')}`);
832
+ }
833
+
834
+ /** Outcome of `doc-supersession`: the recorded verdict of the supersession-pin re-run. */
835
+ export interface DocSupersessionResult {
836
+ status: 'PASS' | 'supersession-pin-failed';
837
+ statusFile: string;
838
+ exitCode: number;
839
+ }
840
+
841
+ /**
842
+ * `doc-supersession` — re-run the supersession pins right after doc-sync (1147 R2). A doc-sync
843
+ * that resurrects a deliberately delinked index row fails HERE, named with the pin, instead of at
844
+ * the later repo-wide `doc-tripwire` backstop (which is never weakened). A project without the pin
845
+ * skips: this check must stay inert outside the spur source repo.
846
+ */
847
+ export function runDocSupersessionCheck(env: WrapupStepsEnv, options: WrapupStepsOptions = {}): DocSupersessionResult {
848
+ const cwd = options.cwd;
849
+ const paths = wrapupRunPaths(env, cwd);
850
+ mkdirSync(cwd ? join(cwd, '.spur', 'run') : join('.spur', 'run'), { recursive: true });
851
+ const statusFile = paths.rel(DOC_SYNC_SUPERSESSION_STATUS_SUFFIX);
852
+ const pinPath = cwd ? join(cwd, SUPERSESSION_PIN) : SUPERSESSION_PIN;
853
+ if (!existsSync(pinPath)) {
854
+ writeFileSync(paths.abs(DOC_SYNC_SUPERSESSION_STATUS_SUFFIX), 'PASS\n');
855
+ return { status: 'PASS', statusFile, exitCode: 0 };
856
+ }
857
+ const pin = spawnSync('bun', ['test', SUPERSESSION_PIN], { cwd, encoding: 'utf8' });
858
+ if (pin.status === 0) {
859
+ writeFileSync(paths.abs(DOC_SYNC_SUPERSESSION_STATUS_SUFFIX), 'PASS\n');
860
+ return { status: 'PASS', statusFile, exitCode: 0 };
861
+ }
862
+ writeFileSync(paths.abs(DOC_SYNC_SUPERSESSION_STATUS_SUFFIX), `supersession-pin-failed: ${SUPERSESSION_PIN}\n`);
863
+ const detail = `${pin.stdout ?? ''}${pin.stderr ?? ''}`.trim();
864
+ process.stderr.write(
865
+ `doc-supersession: ${SUPERSESSION_PIN} failed (exit ${String(pin.status ?? 1)}) — the doc-sync step introduced or kept superseded index content:\n${detail.slice(-2000)}\n`,
866
+ );
867
+ return { status: 'supersession-pin-failed', statusFile, exitCode: 0 };
868
+ }
869
+
671
870
  export const WRAPUP_STEPS_USAGE =
672
- 'usage: wrapup-steps.ts <resolve|route-reason|metrics|feature-transition> (env: __runId, tasks, mode, feature, featureGateCmd, spurBin)';
871
+ 'usage: wrapup-steps.ts <resolve|route-reason|metrics|feature-transition|doc-sync-snapshot|doc-sync-scope|doc-supersession> (env: __runId, tasks, mode, feature, featureGateCmd, spurBin)';
673
872
 
674
873
  export function main(argv: string[], env: WrapupStepsEnv = getEnvVars(), options: WrapupStepsOptions = {}): number {
675
874
  const sub = argv[0];
@@ -680,6 +879,9 @@ export function main(argv: string[], env: WrapupStepsEnv = getEnvVars(), options
680
879
  return 0;
681
880
  }
682
881
  if (sub === 'feature-transition') return runFeatureTransition(env, options).exitCode;
882
+ if (sub === 'doc-sync-snapshot') return runDocSyncSnapshot(env, options).exitCode;
883
+ if (sub === 'doc-sync-scope') return runDocSyncScope(env, options).exitCode;
884
+ if (sub === 'doc-supersession') return runDocSupersessionCheck(env, options).exitCode;
683
885
  process.stderr.write(`${WRAPUP_STEPS_USAGE}\n`);
684
886
  return 2;
685
887
  }
@@ -26,7 +26,6 @@ metadata:
26
26
  see_also:
27
27
  - sp:source-driven-development
28
28
  - sp:spur-cli
29
- - sp:wayfinder
30
29
  ---
31
30
 
32
31
  # sp:brainstorm — Structured Ideation Workflow
@@ -35,7 +34,6 @@ Generate solution options with trade-offs, recommendations, and confidence scori
35
34
 
36
35
  **Key distinction:**
37
36
  - **`sp:brainstorm`** = Ideation: generate approaches with trade-offs
38
- - **`sp:wayfinder`** = Wayfinding: chart a multi-session map when the destination itself is foggy
39
37
  - **research** = verify and synthesize information (delegate inline by default — see [inline-default contract](../spur-dev/references/cross-cutting.md#inline-default-execution-surface))
40
38
  - **`sp:spur-dev`** = Task creation: structured task breakdown (planning half)
41
39
  - **`sp:source-driven-development`** = Verification: source-first claim validation
@@ -65,14 +63,13 @@ Activate sp:brainstorm when:
65
63
  | "research approaches" | User wants evidence-backed options |
66
64
  | "what are my options?" | User wants multiple solutions |
67
65
  | "how should I approach X?" | User wants recommendation with reasoning |
68
- | "wayfind" / "chart a course" | User needs a multi-session investigation map — escalate to `sp:wayfinder` (Phase 2) |
69
66
 
70
67
  **NOT for:**
71
68
  - Pure research (delegate to research skills inline by default; escalate via `spur agent run` only on a subprocess trigger)
72
69
  - Task creation without ideation (use `sp:spur-dev` instead)
73
70
  - Fact-checking or verification only (use `sp:source-driven-development` instead)
74
71
  - Task file operations (use `sp:spur-cli` instead)
75
- - Multi-session investigation when the destination itself is foggy (use `sp:wayfinder` instead)
72
+ - Multi-session investigation when the destination itself is foggy (recommend `/sp:dev-find-way`; see [Scope Check](#scope-check-end-of-phase-1))
76
73
 
77
74
  ## Core Principles
78
75
 
@@ -317,51 +314,16 @@ non-deterministic intent into a reliable sequence, not bare forwarders (ADR-016)
317
314
 
318
315
  ---
319
316
 
320
- ## Wayfinding Escalation (Phase 2)
317
+ ## Scope Check (end of Phase 1)
321
318
 
322
- When the discovery interview (Phase 1 of `/sp:dev-brainstorm`) surfaces that **the destination itself is foggy** — the spec can't be written in one session because too many decisions are unresolved — brainstorm escalates to `sp:wayfinder` instead of proceeding to ideation.
319
+ Before ideation, ask: **"Can this be spec'd in one session, or is the destination itself still foggy?"**
323
320
 
324
- ### Scope Check
325
-
326
- At the end of Phase 1, before ideation begins, run this scope check:
327
-
328
- > **"Can this be spec'd in one session, or is the destination itself still foggy?"**
329
-
330
- **Signals that wayfinding is needed:**
331
- - The topic touches ≥3 subsystems or unknown boundaries
332
- - Key decisions depend on research not yet done
333
- - The operator can describe the goal but not the shape of the solution
334
- - Multiple "it depends" answers in the discovery interview
335
- - The operator uses fog language: "I'm not sure yet", "we need to explore", "it depends on what we find"
336
-
337
- **Signals that standard ideation suffices:**
338
- - The destination is clear; only the approach is in question
339
- - All key decisions can be made from existing knowledge
340
- - The operator can enumerate the constraints and trade-offs
341
-
342
- ### Escalation Path
343
-
344
- When the scope check indicates a foggy destination, offer the escalation:
345
-
346
- > *"This is a multi-session investigation. Want me to chart a wayfinder map so we can work through it one decision at a time?"*
347
-
348
- The operator **confirms** before wayfinding begins — never silently escalate. A 30-minute quick-answer need might touch a big domain without requiring a multi-session map.
349
-
350
- On confirmation, delegate to `sp:wayfinder` for the "Chart the map" mode. The resolved decision tree from Phase 1 seeds the map's **## Notes** and initial **## Not yet specified** sections.
351
-
352
- ### `--wayfind` Flag
353
-
354
- When `/sp:dev-brainstorm` is invoked with `--wayfind`, the scope check is **skipped** — the operator has pre-approved the escalation. After the discovery interview, proceed directly to `sp:wayfinder` charting without the confirmation prompt.
355
-
356
- Use `--wayfind` when:
357
- - The operator already knows this is a multi-session investigation
358
- - A previous session recommended wayfinding
359
- - The topic is explicitly exploratory ("explore the solution space for X")
360
-
361
- ### Integration with the Design Approval Gate
362
-
363
- A wayfinding escalation **replaces** the standard ideation output. The map feature (with its destination, notes, fog, and child tickets) is the artifact. The `needs_design` signal is not emitted — wayfinding defers design until the route to the destination is clear. When the last ticket resolves, the final session hands off to standard `sp:brainstorm` → `sp:spec-decomposition` with a now-clear destination.
321
+ Fog signals: the topic touches ≥3 subsystems or unknown boundaries; key decisions depend on research
322
+ not yet done; the operator can name the goal but not the solution's shape; repeated "it depends"
323
+ answers. When they dominate, **stop** — do not ideate toward a destination that isn't there. Report
324
+ the resolved decision tree and recommend `/sp:dev-find-way "<topic>"`, which owns multi-session
325
+ investigation maps. Brainstorm never charts a map itself.
364
326
 
365
327
  ---
366
328
 
367
- **Remember:** Ideation ≠ Research. Generate approaches with trade-offs. Delegate verification to `sp:source-driven-development`. Delegate synthesis/research inline (escalate to `spur agent run` only on a subprocess trigger). Delegate task creation to `sp:spur-dev`. When the destination itself is foggy, escalate to `sp:wayfinder` — never force a spec that isn't ready.
329
+ **Remember:** Ideation ≠ Research. Generate approaches with trade-offs. Delegate verification to `sp:source-driven-development`. Delegate synthesis/research inline (escalate to `spur agent run` only on a subprocess trigger). Delegate task creation to `sp:spur-dev`. When the destination itself is foggy, stop and recommend `/sp:dev-find-way` — never force a spec that isn't ready.
@@ -40,6 +40,7 @@ A git worktree gives you an isolated working directory for a branch without stas
40
40
  ```bash
41
41
  git worktree add ../<project>-<branch> <branch>
42
42
  cd ../<project>-<branch>
43
+ spur projects add . 2>/dev/null || true # Register in projects.json for the Board project switcher
43
44
  ```
44
45
 
45
46
  **When to use worktrees:**
@@ -28,6 +28,7 @@ A git worktree gives you an isolated working directory linked to the same reposi
28
28
  ```bash
29
29
  git worktree add ../project-hotfix hotfix/critical-fix
30
30
  cd ../project-hotfix && bun install --frozen-lockfile
31
+ spur projects add . 2>/dev/null || true # list it in the Board project switcher
31
32
  ```
32
33
 
33
34
  The worktree directory is created alongside the main project. Name it `<project>-<branch>` for clarity.
@@ -104,6 +104,13 @@ only burns wall clock and context budget.
104
104
 
105
105
  - **Run only targeted probes** to validate your changes: `bun test <file>`,
106
106
  `bun test <file> --test-name-pattern "<test>"`, or `bunx tsc --noEmit` on a single package.
107
+ - **Run Spur through this tree's own entry point, never a bare `spur` (task 1110 R2).** Use the
108
+ invocation the pipeline supplies (`${vars.spurBin}`, e.g. `bun apps/cli/src/index.ts`) from the
109
+ execution tree — a bare `spur` on `PATH` resolves to the published bundle in `~/.bun/bin` or
110
+ `~/node_modules`, so a guard, a `task check` or a `task record` silently runs a different build
111
+ against a different tree. Same rule for the working directory: each shell call starts fresh, so
112
+ re-select the execution tree inside the call (`cd -- "$TREE"` plus the identity check) instead of
113
+ relying on a `cd` a previous call left behind.
107
114
  - **NEVER run** `bun run test`, `bun run spur-check`, `bun run check`, or any other full-suite /
108
115
  project-gate command from inside implement. These belong to the pipeline's `test` hop.
109
116
  - **Full-suite budget: at most 2 per task** (task 0436 R2) — counted across the whole task run
@@ -117,7 +124,35 @@ only burns wall clock and context budget.
117
124
  - If a targeted probe reveals a failure you cannot fix within implement scope, note it in
118
125
  `## Solution` and let the `test` hop's fixall handle it — do not pre-empt the gate.
119
126
 
120
- ## Changed-path targeted checks (dependency-aware verification, task 0510 R3)
127
+ ### Public-surface probe (task 1135 R1/R6/R8)
128
+
129
+ Narrow unit tests prove the seam you refactored, not the surface a user calls. Before this stage
130
+ reports done it runs **one end-to-end probe per new or changed public surface** — a CLI verb/flag,
131
+ an HTTP route, or a plugin script entry:
132
+
133
+ - against a **throwaway project root** (a temp dir), never the repo corpus or the execution tree's
134
+ tracked files;
135
+ - bounded: 120 s per invocation, output capped at 8 KiB, no network beyond the CLI under test;
136
+ - recorded as acceptance evidence: the **exact command** and its **observed output**;
137
+ - a failing probe is evidence, never a reason to mutate the corpus.
138
+
139
+ A service-layer test suite does **not** satisfy this. The gap it closes is measured: 19 service tests
140
+ green while `task create --feature <verifying>` failed end-to-end (task 1135 Background). If the stage
141
+ changed no public surface, report the single line
142
+ `probe: not-applicable (no public surface changed)` instead.
143
+
144
+ **Changed-path static self-check (R8)** — over the changed paths only, before reporting done:
145
+
146
+ - `spur rule run` with the configured pre-check preset (catches a direct `process.env` read, the
147
+ `env-var-hygiene` rule, and its siblings);
148
+ - `bun run script-contract-check` when a plugin script was touched (a stale generated `.mjs` twin);
149
+ - the paired-script parity test when `package.json` changed (a half-updated `spur-check` /
150
+ `spur-check-new` chain).
151
+
152
+ The single full project check stays the pipeline `test` hop's job (see the section above); these are
153
+ changed-path checks, not that gate.
154
+
155
+ ## Changed-path targeted checks (dependency-aware verification, task 0510 R3 / task 1142 R3)
121
156
 
122
157
  "Run the narrow test first" needs a second half: *which* narrow tests, when a change to a shared
123
158
  surface can break a downstream consumer. The matrix below is dependency-aware — domain → app →
@@ -126,6 +161,12 @@ implement. **It augments narrow behavior tests; it never authorizes `bun run spu
126
161
  `bun run test`, or another full project check inside implement** — the pipeline's `test` hop owns
127
162
  that single full gate.
128
163
 
164
+ **Worker iteration check order (task 1142 R3):**
165
+ 1. The **changed-path matrix** below (narrow behavior tests + affected consumers);
166
+ 2. the resolved `quality-gate` script in `light` mode (source-repo probe, else the installed twin) (biome on changed files, per-workspace typecheck, rule presets `rules:pre`/`rules:post`, related tests).
167
+
168
+ Neither step takes the host-wide full-gate lock. The full gate is named only for the pipeline's `test` and `test-recheck` hops.
169
+
129
170
  | Changed surface | Required targeted tests | Required typechecks |
130
171
  | --- | --- | --- |
131
172
  | `packages/domain/src/**` public type/query | affected domain test; affected app service test; affected CLI command test | `@gobing-ai/spur-domain`, `@gobing-ai/spur-app`, `@gobing-ai/spur` |
@@ -143,6 +184,12 @@ that single full gate.
143
184
  creep into every plugin edit.
144
185
  - Run the applicable rows, then stop: the single full project check belongs to the pipeline's
145
186
  `test` hop (`task-pipeline.yaml` `${vars.qualityGateCmd}`).
187
+ - **Mirrored surfaces: an edit lands on all of them.** When the change is one instance of a repeated
188
+ pattern — the same wiring rule on two runbooks, a signature in both a generator and its generated
189
+ `.d.mts`, a command chain in both `spur-check` and `spur-check-new` — grep for the siblings and
190
+ update each in this pass. Two H1-batch cycles (2026-10-08) were spent re-finding exactly this:
191
+ task 1129 fixed the guard wiring on one runbook and left its sibling, and task 1127 changed
192
+ `spur-check` while `spur-check-new`'s chain-parity assertion still expected the old chain.
146
193
 
147
194
  ## Behavior
148
195
 
@@ -165,7 +165,7 @@ classifies it as external and never raises `L4.stale-line-anchor` for it (R1). D
165
165
  that lives in this repo — in-repo evidence MUST use the repo-relative backtick form
166
166
  `` `path:line` `` / `` `path:start-end` ``, and citing it in the external form still reports (R2).
167
167
 
168
- **Concrete anchors (0804 R9):** cite existing `file:line`s, never globs — `references/verdict-schema.md`.
168
+ **Concrete anchors (0804 R9):** existing `file:line`s only, never globs, always repo-root-relative — a basename is `L4.anchor-unresolved` at the `--as done` gate (`references/verdict-schema.md`).
169
169
 
170
170
  ### Step 5 — Acceptance Criteria guard
171
171
 
@@ -315,6 +315,11 @@ node "$RESIDUAL" fold <wbs>
315
315
  > for each dropped MET-matched scenario key and when the new rows match no feature scenario —
316
316
  > treat those warnings as a re-key instruction, not noise.
317
317
 
318
+ > **Keying rule for feature-linked tasks (1086/1089 re-audits).** Key AC rows by the exact feature
319
+ > scenario title (`R1 — …`): feature `Scenario:` titles are declared identities and satisfy L4
320
+ > scenario matching directly. Bare `AC1` fails `ac-identity` on Gherkin AC sections, and checklist
321
+ > labels truncate at the first colon (a `(req: …)` suffix), so the task's own AC title may not resolve.
322
+
318
323
  > **Do not write `## Review` directly, ever.** The `## Review` section is owned by the
319
324
  > `review` coordinator (`/sp:dev-review` → `sp:super-reviewer`), which merges
320
325
  > `functional-review` + `code-verification` review mode + `code-improvement` fragments. The
@@ -357,9 +362,8 @@ Confidence: HIGH
357
362
  The per-requirement traceability table MUST use `| Req | Status | Evidence |` (exactly this header, no `R#`/`R`/`Requirement` variant — `Status` in column 2 — and no extra columns between Req and Status). The Acceptance Criteria table MUST use `| AC | Status | Evidence Type | Evidence |`.
358
363
  The answer MUST carry exactly one `Confidence: HIGH|MEDIUM|LOW` line (task 1068): the verifier's stated confidence in the verdict — HIGH = fresh executable evidence + re-read anchors this run; MEDIUM = partial reliance on prior evidence, external/delegated outcomes, or an unstable tree; LOW = key evidence not re-executed. A missing or out-of-vocabulary line fails lint (`confidence-missing` / `confidence-value`) and blocks derivation; the value lands in the verdict artifact's `confidence` field and the recorded `## Testing` section.
359
364
  The answer MUST cite a source for external API/library claims in Requirement/AC evidence cells: a cell asserting a scoped-package (`@scope/name`), `node_modules/`, semver-version (`x.y.z`), or backticked function-call (`` `name(args)` ``) claim is rejected with `evidence-citation` unless the SAME cell carries a citation — a repo `path:line(-end)` (backticked or bare), the external named-origin form (@origin `path` line N), or a URL. Cross-cell references ("receipt above") do not count; marker-free receipts ("bun test 398 pass / 0 fail") need no citation.
360
- **MUST NOT:** place a `Severity` column between `Req` and `Status` in the authoring contract.
361
- The parser is tolerant of these variants (defense-in-depth), but the authoring contract is
362
- canonical.
365
+ **MUST NOT:** place a `Severity` column between `Req` and `Status`. The parser tolerates variants
366
+ as defense-in-depth; this contract is canonical.
363
367
 
364
368
  Under the pipeline, the verifier owns the answer file `.spur/run/<wbs>-verify-answer.txt` (0726
365
369
  R3): `Verdict: PARTIAL` first, append one row at a time, replace the verdict line only when all
@@ -90,6 +90,9 @@ Before declaring a task `done`, run this lightweight checklist. It catches the m
90
90
  - [ ] No `console.log` / `console.error` in production code (use the project logger).
91
91
  - [ ] No `--no-verify`, `--force`, or new suppression comments added to bypass gates.
92
92
  - [ ] Solution section contains `file:line` citations for every changed file.
93
+ - [ ] API/library claims in changed docs carry a source citation (URL or `path:line`) and an honest
94
+ confidence level (HIGH/MEDIUM/LOW) per source-driven-development; the deterministic advisory backstop is
95
+ the `cited-version-strings` / `cited-api-claims` rules (`.spur/rules/docs/sourced-api-claims.yaml`).
93
96
  - [ ] Review section has P1–P4 findings table (even if all rows are empty — the table itself proves review happened).
94
97
 
95
98
  This checklist runs as part of the verify step when the task reaches `testing` status. It does not replace the formal gates — it augments them with between-pipeline hygiene checks.