@gobing-ai/spur 0.3.47 → 0.3.49

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 (148) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/config.example.yaml +94 -15
  3. package/config/transition-shims.json +33 -0
  4. package/config/workflows/basic.yaml +2 -0
  5. package/config/workflows/docs-pipeline.yaml +2 -0
  6. package/config/workflows/feature-dev.yaml +8 -0
  7. package/config/workflows/idea-pipeline.yaml +10 -0
  8. package/config/workflows/planning-pipeline.yaml +4 -0
  9. package/config/workflows/pr-review.yaml +338 -0
  10. package/config/workflows/task-pipeline.yaml +8 -0
  11. package/config/workflows/wayfinder-resolution.yaml +4 -0
  12. package/config/workflows/wrapup-pipeline.yaml +18 -1
  13. package/package.json +8 -8
  14. package/plugins/sp/README.md +9 -6
  15. package/plugins/sp/agents/expert-spur.md +1 -0
  16. package/plugins/sp/commands/dev-arch.md +2 -1
  17. package/plugins/sp/commands/dev-brainstorm.md +2 -1
  18. package/plugins/sp/commands/dev-changelog.md +1 -0
  19. package/plugins/sp/commands/dev-daily.md +1 -0
  20. package/plugins/sp/commands/dev-debug.md +2 -1
  21. package/plugins/sp/commands/dev-dogfood.md +2 -1
  22. package/plugins/sp/commands/{dev-featurechange.md → dev-feature-change.md} +8 -10
  23. package/plugins/sp/commands/dev-find-conflict.md +2 -1
  24. package/plugins/sp/commands/dev-find-issue.md +36 -43
  25. package/plugins/sp/commands/dev-find-next.md +5 -4
  26. package/plugins/sp/commands/dev-fixall.md +1 -0
  27. package/plugins/sp/commands/dev-gitmsg.md +1 -0
  28. package/plugins/sp/commands/dev-gtd.md +12 -12
  29. package/plugins/sp/commands/dev-handover.md +1 -0
  30. package/plugins/sp/commands/dev-history-load.md +63 -0
  31. package/plugins/sp/commands/dev-idea.md +1 -0
  32. package/plugins/sp/commands/dev-next.md +2 -1
  33. package/plugins/sp/commands/dev-parallel.md +2 -1
  34. package/plugins/sp/commands/dev-plan.md +2 -1
  35. package/plugins/sp/commands/dev-pr-review.md +39 -0
  36. package/plugins/sp/commands/dev-refine.md +5 -3
  37. package/plugins/sp/commands/dev-refineall.md +2 -1
  38. package/plugins/sp/commands/dev-refresh.md +2 -1
  39. package/plugins/sp/commands/dev-reverse.md +2 -1
  40. package/plugins/sp/commands/dev-review.md +2 -1
  41. package/plugins/sp/commands/dev-run.md +3 -2
  42. package/plugins/sp/commands/dev-runall.md +3 -2
  43. package/plugins/sp/commands/dev-simplify.md +2 -1
  44. package/plugins/sp/commands/dev-unit.md +2 -1
  45. package/plugins/sp/commands/dev-verify.md +2 -1
  46. package/plugins/sp/commands/dev-verifyall.md +2 -1
  47. package/plugins/sp/commands/dev-wrap.md +7 -5
  48. package/plugins/sp/commands/dev-wrapall.md +7 -5
  49. package/plugins/sp/commands/rule-add.md +1 -0
  50. package/plugins/sp/commands/rule-refine.md +1 -0
  51. package/plugins/sp/commands/rule-scan.md +1 -0
  52. package/plugins/sp/commands/spur-init.md +1 -0
  53. package/plugins/sp/commands/workflow-add.md +1 -0
  54. package/plugins/sp/commands/workflow-refine.md +1 -0
  55. package/plugins/sp/hooks/careful-guard.ts +5 -80
  56. package/plugins/sp/hooks/destructive-policy.ts +146 -0
  57. package/plugins/sp/hooks/pi/guard-extension.ts +33 -46
  58. package/plugins/sp/hooks/task-file-policy.ts +31 -0
  59. package/plugins/sp/hooks/task-write-guard.ts +4 -0
  60. package/plugins/sp/plugin.json +1 -1
  61. package/plugins/sp/references/roles.md +106 -0
  62. package/plugins/sp/scripts/feature-sync-bounded.ts +28 -2
  63. package/plugins/sp/scripts/history-load.ts +400 -0
  64. package/plugins/sp/scripts/pr-reviewing.ts +867 -0
  65. package/plugins/sp/scripts/stage-registry-adapter.ts +66 -31
  66. package/plugins/sp/scripts/surface-drift-inventory.ts +908 -0
  67. package/plugins/sp/scripts/task-size-precheck.ts +30 -4
  68. package/plugins/sp/scripts/transition-shim-check.ts +238 -0
  69. package/plugins/sp/scripts/validate-commands.ts +33 -2
  70. package/plugins/sp/scripts/validate-flag-contracts.ts +5 -2
  71. package/plugins/sp/skills/code-implementation/SKILL.md +9 -1
  72. package/plugins/sp/skills/code-verification/SKILL.md +29 -28
  73. package/plugins/sp/skills/issue-finding/SKILL.md +123 -141
  74. package/plugins/sp/skills/issue-finding/examples/expected-findings.json +1 -1
  75. package/plugins/sp/skills/issue-finding/references/session-formats.md +87 -90
  76. package/plugins/sp/skills/next-feature/SKILL.md +6 -6
  77. package/plugins/sp/skills/next-feature/references/handoff-routing.md +5 -5
  78. package/plugins/sp/skills/next-feature/references/signal-derivation.md +7 -2
  79. package/plugins/sp/skills/next-router/SKILL.md +1 -1
  80. package/plugins/sp/skills/parallel-execution/references/dispatch-surface.md +40 -2
  81. package/plugins/sp/skills/pr-reviewing/SKILL.md +285 -0
  82. package/plugins/sp/skills/spur-cli/SKILL.md +3 -0
  83. package/plugins/sp/skills/spur-cli/references/agent.md +12 -7
  84. package/plugins/sp/skills/spur-cli/references/features/hierarchy-mece.md +5 -5
  85. package/plugins/sp/skills/spur-cli/references/features.md +1 -1
  86. package/plugins/sp/skills/spur-cli/references/team.md +10 -3
  87. package/plugins/sp/skills/spur-dev/SKILL.md +2 -0
  88. package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +17 -0
  89. package/plugins/sp/skills/spur-dev/references/cross-cutting.md +44 -23
  90. package/plugins/sp/skills/spur-dev/references/dev-operations.md +14 -11
  91. package/plugins/sp/skills/spur-dev/references/execution-workflow.md +14 -12
  92. package/plugins/sp/skills/spur-dev/references/flag-glossary.md +28 -7
  93. package/plugins/sp/skills/spur-dev/references/gate-checklists.md +2 -0
  94. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +12 -2
  95. package/schemas/spur-config.schema.json +47 -3
  96. package/spur.js +12223 -7716
  97. package/web/_astro/BoardApp.8hiqShQn.js +1 -0
  98. package/web/_astro/{BoardApp.DKyrGxdo.js → BoardApp.BjQUNhuj.js} +74 -74
  99. package/web/_astro/{TaskDetail.6-27_LMa.js → TaskDetail.CVBuD6dF.js} +1 -1
  100. package/web/_astro/{arc.Df-9AQvS.js → arc.BMMjdODi.js} +1 -1
  101. package/web/_astro/{architectureDiagram-3BPJPVTR.VAI_-paS.js → architectureDiagram-3BPJPVTR.BU5ShzXf.js} +1 -1
  102. package/web/_astro/{blockDiagram-GPEHLZMM.DFpUY1ue.js → blockDiagram-GPEHLZMM.Bj1iEqPD.js} +1 -1
  103. package/web/_astro/{c4Diagram-AAUBKEIU.CF8doOpg.js → c4Diagram-AAUBKEIU.vX8wepCL.js} +1 -1
  104. package/web/_astro/channel.EwdSemIC.js +1 -0
  105. package/web/_astro/{chunk-2J33WTMH.BnjK3fjt.js → chunk-2J33WTMH.BKAipTym.js} +1 -1
  106. package/web/_astro/{chunk-4BX2VUAB.x6ZDnJKq.js → chunk-4BX2VUAB.B68XkPG7.js} +1 -1
  107. package/web/_astro/{chunk-55IACEB6.zY-0uu7w.js → chunk-55IACEB6.BmeDLcrc.js} +1 -1
  108. package/web/_astro/{chunk-727SXJPM.BZxKg_Vi.js → chunk-727SXJPM.PDuBA3Kw.js} +1 -1
  109. package/web/_astro/{chunk-AQP2D5EJ.Cpi9G9Td.js → chunk-AQP2D5EJ.C7A044za.js} +1 -1
  110. package/web/_astro/{chunk-FMBD7UC4.DWTB-Pif.js → chunk-FMBD7UC4.BtzKKFqR.js} +1 -1
  111. package/web/_astro/{chunk-ND2GUHAM.BPDQbiOG.js → chunk-ND2GUHAM.BJuDeeOy.js} +1 -1
  112. package/web/_astro/{chunk-QZHKN3VN.BRWIcuoM.js → chunk-QZHKN3VN.DSeMDgcQ.js} +1 -1
  113. package/web/_astro/{classDiagram-4FO5ZUOK.mGTCZsDO.js → classDiagram-4FO5ZUOK.D53Q4tCw.js} +1 -1
  114. package/web/_astro/{classDiagram-v2-Q7XG4LA2.mGTCZsDO.js → classDiagram-v2-Q7XG4LA2.D53Q4tCw.js} +1 -1
  115. package/web/_astro/{cose-bilkent-S5V4N54A.D1GEut-z.js → cose-bilkent-S5V4N54A.c712AFRH.js} +1 -1
  116. package/web/_astro/{dagre-BM42HDAG.BV0XG9Do.js → dagre-BM42HDAG.D-idisph.js} +1 -1
  117. package/web/_astro/{diagram-2AECGRRQ.DzpYxsjo.js → diagram-2AECGRRQ.DLgnsJCU.js} +1 -1
  118. package/web/_astro/{diagram-5GNKFQAL.Cm9YzJh4.js → diagram-5GNKFQAL.BiaxBVqx.js} +1 -1
  119. package/web/_astro/{diagram-KO2AKTUF.BjhottUj.js → diagram-KO2AKTUF.C8HX1vd8.js} +1 -1
  120. package/web/_astro/{diagram-LMA3HP47.BFsQW5kb.js → diagram-LMA3HP47.CfqDLLes.js} +1 -1
  121. package/web/_astro/{diagram-OG6HWLK6.8pdpzSWO.js → diagram-OG6HWLK6.15SDiEed.js} +1 -1
  122. package/web/_astro/{erDiagram-TEJ5UH35.Bd7KUJmJ.js → erDiagram-TEJ5UH35.DksYtOYM.js} +1 -1
  123. package/web/_astro/{flowDiagram-I6XJVG4X.7LWffkaE.js → flowDiagram-I6XJVG4X.DR_Au-HV.js} +1 -1
  124. package/web/_astro/{ganttDiagram-6RSMTGT7.BeDcO5tI.js → ganttDiagram-6RSMTGT7.CHhHrffI.js} +1 -1
  125. package/web/_astro/{gitGraphDiagram-PVQCEYII.Ca4n730A.js → gitGraphDiagram-PVQCEYII.B2Xehvam.js} +1 -1
  126. package/web/_astro/{index.Dbvuw6d4.css → index.DAxu50UF.css} +1 -1
  127. package/web/_astro/{infoDiagram-5YYISTIA.B0OakQYb.js → infoDiagram-5YYISTIA.C9c3CNNN.js} +1 -1
  128. package/web/_astro/{ishikawaDiagram-YF4QCWOH.DSmNQe-1.js → ishikawaDiagram-YF4QCWOH.BibUHkh8.js} +1 -1
  129. package/web/_astro/{journeyDiagram-JHISSGLW.Cy5ruEUu.js → journeyDiagram-JHISSGLW.BYoVHiyO.js} +1 -1
  130. package/web/_astro/{kanban-definition-UN3LZRKU.CUJXub0p.js → kanban-definition-UN3LZRKU.CM1K5wHE.js} +1 -1
  131. package/web/_astro/{linear.DC1jCCXn.js → linear.SPpjJUb-.js} +1 -1
  132. package/web/_astro/{mermaid.core.DxVP99Ab.js → mermaid.core.BAgx3nnb.js} +4 -4
  133. package/web/_astro/{mindmap-definition-RKZ34NQL.D0MaV6sJ.js → mindmap-definition-RKZ34NQL.D35oPG1R.js} +1 -1
  134. package/web/_astro/{pieDiagram-4H26LBE5.DCC6_q32.js → pieDiagram-4H26LBE5.DiWuRwk7.js} +1 -1
  135. package/web/_astro/{quadrantDiagram-W4KKPZXB.BeUOAM7C.js → quadrantDiagram-W4KKPZXB.B9PBzTWn.js} +1 -1
  136. package/web/_astro/{requirementDiagram-4Y6WPE33.Dbl4MASO.js → requirementDiagram-4Y6WPE33.CYuuamFN.js} +1 -1
  137. package/web/_astro/{sankeyDiagram-5OEKKPKP.HsLg0VS4.js → sankeyDiagram-5OEKKPKP.W24UhhtD.js} +1 -1
  138. package/web/_astro/{sequenceDiagram-3UESZ5HK.DT7DJTnZ.js → sequenceDiagram-3UESZ5HK.BpbNjA51.js} +1 -1
  139. package/web/_astro/{stateDiagram-AJRCARHV.d_ju1Vr1.js → stateDiagram-AJRCARHV.DqVsHudf.js} +1 -1
  140. package/web/_astro/{stateDiagram-v2-BHNVJYJU.DMCAjMJ4.js → stateDiagram-v2-BHNVJYJU.CzwHYX81.js} +1 -1
  141. package/web/_astro/{timeline-definition-PNZ67QCA.DNOHr62_.js → timeline-definition-PNZ67QCA.Bc3B6djw.js} +1 -1
  142. package/web/_astro/{vennDiagram-CIIHVFJN.B7dUy-1W.js → vennDiagram-CIIHVFJN.C-D5rh8O.js} +1 -1
  143. package/web/_astro/{wardley-L42UT6IY.DEqOXvBh.js → wardley-L42UT6IY.D7PdYCqn.js} +1 -1
  144. package/web/_astro/{wardleyDiagram-YWT4CUSO.BCRb2p6x.js → wardleyDiagram-YWT4CUSO.CwmJKXF3.js} +1 -1
  145. package/web/_astro/{xychartDiagram-2RQKCTM6.NxVQLdBh.js → xychartDiagram-2RQKCTM6.avDYnLsb.js} +1 -1
  146. package/web/index.html +2 -2
  147. package/web/_astro/BoardApp.Ce6zJYAH.js +0 -1
  148. package/web/_astro/channel.Uhm9O3UV.js +0 -1
@@ -26,6 +26,8 @@
26
26
  import { execFileSync } from 'node:child_process';
27
27
  import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
28
28
  import { join } from 'node:path';
29
+ import { fileURLToPath } from 'node:url';
30
+ import { STAGE_FLOOR_TIER, TIER_ORDER } from './stage-registry-adapter';
29
31
 
30
32
  // ─── Regex (sync with packages/app/src/services/task-size-precheck.ts) ───────
31
33
 
@@ -44,8 +46,17 @@ const CHECKLIST_ITEM_RE = /^\s*-\s*\[[ xX]\]/m;
44
46
  const LARGE_TASK_REQS = 5;
45
47
  const LARGE_TASK_PLAN_ITEMS = 8;
46
48
 
47
- /** Capability tiers strong enough for a large task. Anything else blocks. */
48
- const CAPABLE_TIERS = new Set(['capable-1', 'capable-2', 'capable-3']);
49
+ /**
50
+ * Capability tiers strong enough for a large task (R3, task 0487). The floor is
51
+ * the `review` stage's Layer-1 tier — `reviewer` per `references/roles.md`,
52
+ * read via the stage-registry adapter (0538 R4: no tier literal here; roles.md
53
+ * is the pointer). Tiers at or above the floor pass. An unreachable roles.md
54
+ * degrades to the pre-reconcile band — fail-closed for a safety gate.
55
+ */
56
+ const CAPABLE_TIERS: ReadonlySet<string> = (() => {
57
+ const floor = STAGE_FLOOR_TIER.get('review') ?? 'capable-1';
58
+ return new Set(TIER_ORDER.slice(Math.max(0, TIER_ORDER.indexOf(floor))));
59
+ })();
49
60
 
50
61
  // ─── CLI ─────────────────────────────────────────────────────────────────────
51
62
 
@@ -56,6 +67,21 @@ function usage(): never {
56
67
  process.exit(1);
57
68
  }
58
69
 
70
+ /**
71
+ * Resolve the spur CLI command in a monorepo-safe way:
72
+ * --spur-bin > SPUR_BIN > monorepo-local CLI entry > PATH `spur`.
73
+ * The plugin's own CI always passes an explicit --spur-bin; this fallback chain
74
+ * keeps ad-hoc invocations from silently hitting a stale PATH install.
75
+ */
76
+ function defaultSpurBin(): string {
77
+ if (process.env.SPUR_BIN) return process.env.SPUR_BIN;
78
+ // scripts/ -> plugins/sp/ -> <repo>/apps/cli/src/index.ts (fileURLToPath — raw pathname breaks
79
+ // on %-encoded paths, e.g. spaces in the checkout directory)
80
+ const local = fileURLToPath(new URL('../../../apps/cli/src/index.ts', import.meta.url));
81
+ if (existsSync(local)) return `bun ${local}`;
82
+ return 'spur';
83
+ }
84
+
59
85
  function parseArgs(argv: string[]): {
60
86
  wbs: string;
61
87
  spurBin: string;
@@ -63,7 +89,7 @@ function parseArgs(argv: string[]): {
63
89
  maxPlanItems: number;
64
90
  executor: string;
65
91
  } {
66
- let spurBin = process.env.SPUR_BIN ?? 'spur';
92
+ let spurBin = defaultSpurBin();
67
93
  let wbs = '';
68
94
  let maxReqs = Number(process.env.MAX_IMPLEMENT_REQS) || 5;
69
95
  let maxPlanItems = Number(process.env.MAX_IMPLEMENT_PLAN_ITEMS) || 8;
@@ -73,7 +99,7 @@ function parseArgs(argv: string[]): {
73
99
  while (i < argv.length) {
74
100
  const arg = argv[i];
75
101
  if (arg === '--spur-bin') {
76
- spurBin = argv[i + 1] ?? 'spur';
102
+ spurBin = argv[i + 1] ?? defaultSpurBin();
77
103
  i += 2;
78
104
  } else if (arg === '--max-reqs') {
79
105
  maxReqs = Number(argv[i + 1]) || 5;
@@ -0,0 +1,238 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * transition-shim-check — two-sided gate over `config/transition-shims.json`
4
+ * (task 0541, feature B2).
5
+ *
6
+ * A transition shim is a compatibility path that must survive the agent-role
7
+ * transition. Every shim carries a source marker comment `@transition-shim(<id>)`
8
+ * so it is a grep target and a review signal, and a manifest entry in
9
+ * `config/transition-shims.json` records who owns it and when it can be removed.
10
+ *
11
+ * The gate is deliberately two-sided, matching `config/corpus-baseline.json`
12
+ * semantics (corpus-check.ts): a marker with no manifest entry FAILS, and a
13
+ * manifest entry whose marker no longer appears in source FAILS. Without the
14
+ * second half the manifest would rot into a permanent suppression list — the
15
+ * exact invisible-debt pattern this gate exists to end.
16
+ *
17
+ * Marker convention (docs/04_DESIGN.md §2.5):
18
+ * // @transition-shim(<id>) — <one line on what this keeps working>
19
+ * <id> matches ^[a-z0-9][a-z0-9-]*$ (lowercase kebab).
20
+ * Markers live in production source roots (apps, packages, plugins, config,
21
+ * scripts, tooling). Docs and test fixtures are not scanned — prose examples
22
+ * and gate-fixture marker text must not trip the gate.
23
+ *
24
+ * The manifest is the removal worklist: emptying it is the definition of the
25
+ * transition being complete (R4).
26
+ *
27
+ * Ships with the plugin to arbitrary projects, so it stays node-builtin-only —
28
+ * no workspace imports.
29
+ *
30
+ * Usage:
31
+ * bun plugins/sp/scripts/transition-shim-check.ts
32
+ * [--manifest <path>] default: config/transition-shims.json
33
+ * [--roots <a,b,c>] default: apps,packages,plugins,config,scripts,tooling
34
+ *
35
+ * Exit code: 0 when every manifest entry is present in source and every source
36
+ * marker is registered; 1 on any violation (unregistered marker, stale entry,
37
+ * or incomplete entry). Violations are printed to stderr; a summary to stdout.
38
+ */
39
+
40
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
41
+ import { join, relative, resolve } from 'node:path';
42
+
43
+ const MARKER_RE = /@transition-shim\(\s*([a-z0-9][a-z0-9-]*)\s*\)/g;
44
+ const ID_RE = /^[a-z0-9][a-z0-9-]*$/;
45
+
46
+ /** Directories never scanned for markers (build output, vendored, VCS internals).
47
+ * `tests`/`test` are excluded too: a test fixture that mentions a marker id is
48
+ * data exercising the gate, not a compatibility path — shims live in production
49
+ * source, and only there do they need tracking. */
50
+ const SKIP_DIRS = new Set([
51
+ 'node_modules',
52
+ '.git',
53
+ 'dist',
54
+ 'coverage',
55
+ '.spur',
56
+ 'build',
57
+ '.next',
58
+ '.venv',
59
+ '__pycache__',
60
+ 'vendors',
61
+ '_legacy_reference',
62
+ 'artifacts',
63
+ 'tests',
64
+ 'test',
65
+ ]);
66
+
67
+ const DEFAULT_ROOTS = ['apps', 'packages', 'plugins', 'config', 'scripts', 'tooling'];
68
+
69
+ /** One manifest record — every field is required (R1). */
70
+ interface ManifestEntry {
71
+ id: string;
72
+ wbs: string;
73
+ file: string;
74
+ keepsWorking: string;
75
+ removalCondition: string;
76
+ }
77
+
78
+ interface Violation {
79
+ kind: 'unregistered' | 'stale' | 'incomplete';
80
+ id: string;
81
+ message: string;
82
+ }
83
+
84
+ function parseArgs(argv: string[]): { manifest: string; roots: string[]; cwd: string } {
85
+ let manifest = 'config/transition-shims.json';
86
+ let roots = DEFAULT_ROOTS;
87
+ for (let i = 0; i < argv.length; i++) {
88
+ if (argv[i] === '--manifest') manifest = argv[++i] ?? manifest;
89
+ else if (argv[i] === '--roots') roots = (argv[++i] ?? '').split(',').filter(Boolean);
90
+ }
91
+ return { manifest, roots, cwd: process.cwd() };
92
+ }
93
+
94
+ /** Walk a root and return marker id -> repo-relative files. */
95
+ function scanMarkers(cwd: string, roots: string[]): Map<string, Set<string>> {
96
+ const found = new Map<string, Set<string>>();
97
+ const walk = (dir: string): void => {
98
+ let entries: string[];
99
+ try {
100
+ entries = readdirSync(dir);
101
+ } catch {
102
+ return; // unreadable or missing root — skip
103
+ }
104
+ for (const name of entries) {
105
+ if (SKIP_DIRS.has(name)) continue;
106
+ const abs = join(dir, name);
107
+ let st: ReturnType<typeof statSync>;
108
+ try {
109
+ st = statSync(abs);
110
+ } catch {
111
+ continue;
112
+ }
113
+ if (st.isDirectory()) {
114
+ walk(abs);
115
+ continue;
116
+ }
117
+ if (!st.isFile()) continue;
118
+ let text: string;
119
+ try {
120
+ text = readFileSync(abs, 'utf8');
121
+ } catch {
122
+ continue; // binary or unreadable — markers are text only
123
+ }
124
+ for (const m of text.matchAll(MARKER_RE)) {
125
+ const id = m[1] as string;
126
+ const file = relative(cwd, abs);
127
+ const set = found.get(id) ?? new Set<string>();
128
+ set.add(file);
129
+ found.set(id, set);
130
+ }
131
+ }
132
+ };
133
+ for (const root of roots) walk(resolve(cwd, root));
134
+ return found;
135
+ }
136
+
137
+ /** Load the manifest; a missing file degrades to no entries (corpus-check parity). */
138
+ function loadManifest(path: string): { entries: ManifestEntry[]; raw: string | null } {
139
+ if (!existsSync(path)) return { entries: [], raw: null };
140
+ const raw = readFileSync(path, 'utf8');
141
+ let parsed: unknown;
142
+ try {
143
+ parsed = JSON.parse(raw);
144
+ } catch (error) {
145
+ return { entries: [], raw: `malformed JSON at ${path}: ${String(error)}` };
146
+ }
147
+ const entries = (parsed as { entries?: unknown[] }).entries;
148
+ if (!Array.isArray(entries)) return { entries: [], raw: `${path}: missing "entries" array` };
149
+ return { entries: entries as ManifestEntry[], raw: null };
150
+ }
151
+
152
+ function validateEntry(e: ManifestEntry, violations: Violation[]): void {
153
+ const field = (key: keyof ManifestEntry, value: unknown, why: string): void => {
154
+ if (typeof value !== 'string' || value.trim() === '') {
155
+ violations.push({
156
+ kind: 'incomplete',
157
+ id: typeof e.id === 'string' ? e.id : '<missing>',
158
+ message: `manifest entry ${JSON.stringify(e.id ?? null)} is missing required field "${key}"${why}`,
159
+ });
160
+ }
161
+ };
162
+ field('id', e.id, '');
163
+ field('wbs', e.wbs, ` in entry ${JSON.stringify(e.id ?? null)}`);
164
+ field('file', e.file, ` in entry ${JSON.stringify(e.id ?? null)}`);
165
+ field('keepsWorking', e.keepsWorking, ` in entry ${JSON.stringify(e.id ?? null)}`);
166
+ field('removalCondition', e.removalCondition, ` in entry ${JSON.stringify(e.id ?? null)}`);
167
+ if (typeof e.id === 'string' && e.id !== '' && !ID_RE.test(e.id)) {
168
+ violations.push({
169
+ kind: 'incomplete',
170
+ id: e.id,
171
+ message: `manifest entry id ${JSON.stringify(e.id)} is not lowercase-kebab (^[a-z0-9][a-z0-9-]*$)`,
172
+ });
173
+ }
174
+ }
175
+
176
+ function run(): number {
177
+ const { manifest, roots, cwd } = parseArgs(process.argv.slice(2));
178
+ const manifestPath = resolve(cwd, manifest);
179
+ const { entries, raw } = loadManifest(manifestPath);
180
+ const violations: Violation[] = [];
181
+ const incomplete = new Set<string>();
182
+
183
+ if (raw !== null) {
184
+ violations.push({ kind: 'incomplete', id: '<manifest>', message: raw });
185
+ // A manifest that cannot be parsed has no valid entries to compare against.
186
+ for (const v of violations) console.error(`transition-shim-check: FAIL - ${v.message}`);
187
+ console.error('transition-shim-check: FAIL - manifest unreadable; refusing to pass');
188
+ return 1;
189
+ }
190
+
191
+ for (const e of entries) {
192
+ const before = violations.length;
193
+ validateEntry(e, violations);
194
+ if (violations.length > before) incomplete.add(String(e.id));
195
+ }
196
+
197
+ const markers = scanMarkers(cwd, roots);
198
+ const observedIds = new Set(markers.keys());
199
+ const manifestIds = new Set(entries.filter((e) => !incomplete.has(String(e.id))).map((e) => e.id));
200
+
201
+ for (const [id, files] of markers) {
202
+ if (manifestIds.has(id)) continue;
203
+ const file = [...files].sort().join(', ');
204
+ violations.push({
205
+ kind: 'unregistered',
206
+ id,
207
+ message: `new unregistered shim @transition-shim(${id}) in ${file} — add an entry to ${manifest} or remove the marker`,
208
+ });
209
+ }
210
+ for (const e of entries) {
211
+ if (incomplete.has(e.id)) continue;
212
+ if (observedIds.has(e.id)) continue;
213
+ violations.push({
214
+ kind: 'stale',
215
+ id: e.id,
216
+ message: `stale manifest entry ${e.id} (wbs ${e.wbs}) — @transition-shim(${e.id}) no longer appears in source; remove the entry, its removal condition was: ${e.removalCondition}`,
217
+ });
218
+ }
219
+
220
+ const unregistered = violations.filter((v) => v.kind === 'unregistered');
221
+ const stale = violations.filter((v) => v.kind === 'stale');
222
+ const incompleteCount = violations.filter((v) => v.kind === 'incomplete').length;
223
+ const ok = violations.length === 0;
224
+
225
+ const noun = entries.length === 1 ? 'entry' : 'entries';
226
+ console.log(
227
+ `transition-shim-check: ${markers.size} marker(s) observed, ${entries.length} manifest ${noun} baselined, ` +
228
+ `${unregistered.length} new, ${stale.length} stale, ${incompleteCount} incomplete — ${ok ? 'PASS' : 'FAIL'}`,
229
+ );
230
+ if (ok) return 0;
231
+
232
+ for (const v of violations) {
233
+ console.error(`transition-shim-check: ${v.kind === 'incomplete' ? 'FAIL' : `FAIL (${v.kind})`} - ${v.message}`);
234
+ }
235
+ return 1;
236
+ }
237
+
238
+ process.exit(run());
@@ -8,7 +8,8 @@
8
8
  *
9
9
  * Gates:
10
10
  * (a) heading whitelist — H1 title + the per-contract ordered section headings
11
- * (b) frontmatter schema — description, argument-hint, allowed-tools; dev-only extras
11
+ * (b) frontmatter schema — description, argument-hint, allowed-tools; dev-only extras;
12
+ * real-YAML re-parse so malformed blocks cannot ship an empty description
12
13
  * (c) target resolution — sp:<skill> refs, workflow files, procedure anchors
13
14
  * (d) allowed-tools coherence — Skill present iff body contains Skill() call
14
15
  * (e) dev-command argument contract — syntax-only hint, Argument Flags table columns,
@@ -19,6 +20,7 @@
19
20
 
20
21
  import { existsSync, readdirSync, readFileSync } from 'node:fs';
21
22
  import { join, resolve } from 'node:path';
23
+ import { parse as parseYaml } from 'yaml';
22
24
 
23
25
  // ─── Types ──────────────────────────────────────────────────────────────────
24
26
 
@@ -53,6 +55,8 @@ interface ParsedCommand {
53
55
  readonly description: string | undefined;
54
56
  readonly argumentHint: string | undefined;
55
57
  readonly allowedTools: string[] | undefined;
58
+ /** Set when the frontmatter fails a real YAML parse or yields an empty description. */
59
+ readonly frontmatterYamlProblem: string | undefined;
56
60
  readonly body: string;
57
61
  }
58
62
 
@@ -68,6 +72,30 @@ function parseCommand(filePath: string, name: string): ParsedCommand {
68
72
  const argumentHint = extractYamlField(fm, 'argument-hint');
69
73
  const allowedTools = extractYamlList(fm, 'allowed-tools');
70
74
 
75
+ // The regex extractions above feed the per-field gates, but they cannot see
76
+ // malformed YAML: `description: >-` followed by unindented keys reads as a
77
+ // present description to the regex while a real YAML parser folds the next
78
+ // keys into the block scalar — the bug that shipped an empty description to
79
+ // superskill install for dev-feature-change. Gate (b) re-checks with a real
80
+ // parse so that class cannot regress.
81
+ let frontmatterYamlProblem: string | undefined;
82
+ if (fm.trim() !== '') {
83
+ try {
84
+ const parsed: unknown = parseYaml(fm);
85
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
86
+ frontmatterYamlProblem = 'frontmatter is not a YAML mapping';
87
+ } else {
88
+ const parsedDescription = (parsed as Record<string, unknown>).description;
89
+ if (typeof parsedDescription !== 'string' || parsedDescription.trim() === '') {
90
+ frontmatterYamlProblem = 'frontmatter description is empty after YAML parsing';
91
+ }
92
+ }
93
+ } catch (error) {
94
+ const detail = error instanceof Error ? error.message.split('\n')[0] : String(error);
95
+ frontmatterYamlProblem = `frontmatter is not valid YAML: ${detail}`;
96
+ }
97
+ }
98
+
71
99
  const lines = body.split('\n');
72
100
  const title = lines[0]?.startsWith('# ') ? lines[0].slice(2).trim() : '';
73
101
 
@@ -90,7 +118,7 @@ function parseCommand(filePath: string, name: string): ParsedCommand {
90
118
  }
91
119
  }
92
120
 
93
- return { name, title, headings, description, argumentHint, allowedTools, body };
121
+ return { name, title, headings, description, argumentHint, allowedTools, frontmatterYamlProblem, body };
94
122
  }
95
123
 
96
124
  /** Extract a plain YAML string field (handles quoted and unquoted). */
@@ -212,6 +240,9 @@ function checkHeadingWhitelist(cmd: ParsedCommand): readonly Violation[] {
212
240
 
213
241
  function checkFrontmatterSchema(cmd: ParsedCommand): readonly Violation[] {
214
242
  const violations: Violation[] = [];
243
+ if (cmd.frontmatterYamlProblem) {
244
+ violations.push({ command: cmd.name, gate: 'b', message: cmd.frontmatterYamlProblem });
245
+ }
215
246
  if (!cmd.description) {
216
247
  violations.push({ command: cmd.name, gate: 'b', message: 'missing frontmatter description' });
217
248
  }
@@ -406,10 +406,13 @@ export function extractTriggerTable(crossCuttingRaw: string): string[] | null {
406
406
  function adrAgentClaims(adrRaw: string): Map<string, SurfaceBehavior> | null {
407
407
  if (adrRaw.includes('## ADR-047')) {
408
408
  const out = new Map<string, SurfaceBehavior>();
409
+ // G5 amendment (feature G5 / task 0565): explicit inline is host-session-only — headless
410
+ // surfaces reject it with the stable special error; 0508 native-subagent eligibility
411
+ // applies to omitted --agent only, never explicit inline.
409
412
  out.set('inline', {
410
- surfaces: new Set(['inline', 'subprocess']),
413
+ surfaces: new Set(['inline']),
411
414
  conditional: false,
412
- defaultWhenOmitted: true,
415
+ defaultWhenOmitted: false,
413
416
  });
414
417
  out.set('auto', { surfaces: new Set(['subprocess']), conditional: false });
415
418
  out.set('<name>', { surfaces: new Set(['inline', 'subprocess']), conditional: true });
@@ -89,6 +89,14 @@ only burns wall clock and context budget.
89
89
  `bun test <file> --test-name-pattern "<test>"`, or `bunx tsc --noEmit` on a single package.
90
90
  - **NEVER run** `bun run test`, `bun run spur-check`, `bun run check`, or any other full-suite /
91
91
  project-gate command from inside implement. These belong to the pipeline's `test` hop.
92
+ - **Full-suite budget: at most 2 per task** (task 0436 R2) — counted across the whole task run
93
+ (implement probes + the pipeline `test` hop + verify/recheck), not per step. When a check fails,
94
+ run the narrow target (`bun test <file> --test-name-pattern <test>`) to green before any full
95
+ suite; reach for the second full run only when the narrow target cannot reproduce the failure.
96
+ - **Consolidate dogfood runs**: one combined real-data execution that exercises all scenarios,
97
+ not N near-identical `--dry-run`/real invocations of the same script. If you find yourself
98
+ rerunning the same dogfood command with one flag changed, stop and fold the variants into a
99
+ single run — repeated identical commands are loop-detector findings and pure cost.
92
100
  - If a targeted probe reveals a failure you cannot fix within implement scope, note it in
93
101
  `## Solution` and let the `test` hop's fixall handle it — do not pre-empt the gate.
94
102
 
@@ -146,7 +154,7 @@ reproduce → isolate → minimal fix → regression guard.
146
154
  ## Common Rationalizations
147
155
 
148
156
  | Rationalization | Reality |
149
- |---|---|
157
+ | --- | --- |
150
158
  | "The spec is clear — I don't need to read the callers." | Code that looks orthogonal is how regressions ship (R5). Read the exports you touch and their immediate callers before writing. |
151
159
  | "I'll add the tests in a follow-up." | Untested production code is unverified code. The task's test step is not optional; behavior ships with its test. |
152
160
  | "This abstraction will be useful later." | Speculative abstraction is complexity without a caller (R2). Build for the requirement in front of you; add the seam when the second use arrives. |
@@ -239,6 +239,11 @@ printf '...' > /tmp/<wbs>-testing.md
239
239
  spur task update <wbs> --section Testing --from-file /tmp/<wbs>-testing.md
240
240
  ```
241
241
 
242
+ > **Corrections: the answer file is the source of truth.** `spur task record` re-transcribes
243
+ > `## Testing` from the verdict artifact, overwriting `--section Testing` writes — direct section
244
+ > fixes are futile. Fix `.spur/run/<wbs>-verify-answer.txt` → `spur task verdict <wbs>
245
+ > --from-answer <file>` → re-record. `--section` is initial authorship only.
246
+
242
247
  > **Do not write `## Review` directly in verify mode.** The `## Review` section is owned by the
243
248
  > `review` step (`/sp:dev-review`), which dispatches `functional-review` + `code-verification`
244
249
  > review mode + `code-improvement`. The `record` step backfills `## Review` from the verdict
@@ -283,9 +288,7 @@ Verdict: PASS
283
288
  | P4 | — | — | No P1–P3 findings; verify verdict PASS |
284
289
  ```
285
290
 
286
- The per-requirement traceability table MUST use `| Req | Status | Evidence |` (exactly this header, no `R#`/`R`/`Requirement` variant, and no extra columns between Req and Status). The Acceptance Criteria table MUST use `| AC | Status | Evidence Type | Evidence |`.
287
-
288
- **MUST NOT:** use `| R# | ... |` as the sole id header without `Status` in column 2.
291
+ 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 |`.
289
292
  **MUST NOT:** place a `Severity` column between `Req` and `Status` in the authoring contract.
290
293
  The parser is tolerant of these variants (defense-in-depth), but the authoring contract is
291
294
  canonical.
@@ -348,8 +351,7 @@ mutation is discoverable from the tracked task file alone, without diffing untra
348
351
  ### Step 13 — Shippable readiness gate (feature-level)
349
352
 
350
353
  Per-task PASS is **not** the same as “this feature is ready to ship.” After Steps 11–12, when the
351
- gate is **active**, evaluate feature AC satisfaction via the existing CLI (do not invent a second
352
- framework).
354
+ gate is **active**, evaluate feature AC satisfaction via the existing CLI.
353
355
 
354
356
  **When active**
355
357
 
@@ -371,16 +373,17 @@ framework).
371
373
  `docs/.spur/run` or other nested `.spur` trees). Ephemeral scratch may use `/tmp` or
372
374
  `/private/tmp`. Requirement / AC row `id`s in the verdict MUST match feature scenario titles
373
375
  (or `AC-N` aliases) so satisfaction can mark MET.
374
- 2. Run:
376
+ 2. Run (0568 R6 monorepo-safe: SPUR_BIN env > local CLI > PATH):
375
377
 
376
378
  ```bash
377
- spur feature check <featureId> --json
378
- spur task list --feature <featureId> --json
379
+ SPUR_BIN="${SPUR_BIN:-$([ -f apps/cli/src/index.ts ] && echo 'bun apps/cli/src/index.ts' || echo spur)}"
380
+ $SPUR_BIN feature check <featureId> --json
381
+ $SPUR_BIN task list --feature <featureId> --json
379
382
  ```
380
383
 
381
384
  3. Classify **Shippable: PASS** only if **all** of:
382
385
  - No finding whose code/message indicates **linked but unverified** scenarios
383
- (`L4_SCENARIO_UNVERIFIED` / “linked but unverified”).
386
+ (`L4_SCENARIO_UNVERIFIED`).
384
387
  - No **orphan / uncovered** feature scenarios (`L4_ORPHAN_SCENARIOS`,
385
388
  `L4_UNCOVERED_FEATURE_SCENARIO` / no covering task).
386
389
  - No **incomplete** linked tasks: every task with this `feature_id` is `done` or `cancelled`
@@ -424,16 +427,14 @@ Full flag matrix and ops notes: [spur-dev/references/dev-operations.md](../spur-
424
427
 
425
428
  ### Step 14 — Report
426
429
 
427
- Show the per-task verdict, the per-requirement table, the gate outcome (cleared / blocked), and the
428
- **Shippable:** line from Step 13 when applicable. Under the pipeline the task verdict is consumed by
429
- the done-gate; for a direct `/sp:dev-verify` invocation the full report is the operator's summary.
430
+ Show the per-task verdict, per-requirement table, gate outcome (cleared/blocked), and the
431
+ **Shippable:** line from Step 13 when applicable. Under the pipeline the done-gate consumes the
432
+ verdict; for a direct `/sp:dev-verify` invocation the full report is the operator's summary.
430
433
 
431
- **`--next` on an already-terminal task (no-op surfacing).** When `--next` is invoked on a task
432
- already at `done` or `cancelled`, the transition cannot fire. The verify report line MUST state the
433
- no-op itself (e.g. `--next: no-op - task already terminal (<status>)`) rather than relying solely
434
- on the CLI print (documented in `dev-verify.md`'s `--next` chain section). The CLI print is the
435
- machine signal; the report line is the operator-visible summary - both must agree so a terminal-task
436
- re-audit cannot be misread as a successful `testing -> done` transition.
434
+ **`--next` on a terminal task (no-op surfacing).** The transition cannot fire; the verify report
435
+ line MUST state the no-op itself (e.g. `--next: no-op - task already terminal (<status>)`). The CLI
436
+ print is the machine signal, the report line the operator summary both must agree so a terminal
437
+ re-audit is never misread as a successful `testing -> done` (dev-verify.md `--next` chain).
437
438
 
438
439
  ---
439
440
 
@@ -507,18 +508,18 @@ Do **not** use this skill for:
507
508
  and the per-requirement aggregation rule.
508
509
  - [references/secu-review.md](references/secu-review.md) — the SECUA dimensions and finding-severity
509
510
  rubric.
511
+ - `plugins/sp/references/roles.md` — the Layer-1 role→tier table; this skill's verify/review modes
512
+ run under the `reviewer` role (0538 R4: this file points at roles.md, it does not restate tiers).
510
513
  - `.spur/workflows/task-pipeline.yaml` — the `verify → record` gate that consumes the verdict.
511
514
  - **`sp:spur-dev`** — the execution-half umbrella that drives the pipeline this skill gates.
512
515
  - [references/code-improvement.md](references/code-improvement.md) — architecture-improvement lens
513
516
  for module depth, seam placement, locality, coupling, and testability.
514
- - **`sp:functional-review`** — a peer review skill for requirements traceability (R{n} → file:line
515
- evidence, per-requirement MET/PARTIAL/UNMET, `FunctionalVerdict`). When the `review` dimension
516
- needs functional traceability (not just SECUA), dispatch this skill; see
517
- [../functional-review/SKILL.md](../functional-review/SKILL.md).
518
- - **`sp:code-improvement`** — a peer review skill for architectural deepening (5 signals: shallow
519
- module, tight coupling, wrong seam, weak locality, poor test surface; severity
520
- blocker/major/minor/advisory). When review findings expose structural friction rather than a
521
- localized defect, dispatch this skill; see [../code-improvement/SKILL.md](../code-improvement/SKILL.md).
517
+ - **`sp:functional-review`** — requirements-traceability peer review (R{n} → file:line,
518
+ per-requirement MET/PARTIAL/UNMET). Dispatch when the `review` dimension needs functional
519
+ traceability, not just SECUA: [../functional-review/SKILL.md](../functional-review/SKILL.md).
520
+ - **`sp:code-improvement`** — architectural-deepening peer review (5 signals, severity
521
+ blocker/major/minor/advisory). Dispatch for structural friction rather than localized
522
+ defects: [../code-improvement/SKILL.md](../code-improvement/SKILL.md).
522
523
 
523
524
  ---
524
525
 
@@ -531,5 +532,5 @@ directly: `Skill(skill="sp:code-verification", args="verify <wbs> --fix all")`.
531
532
 
532
533
  ### Codex / OpenClaw / OpenCode / Antigravity
533
534
 
534
- Run `spur` CLI via Bash; parse `--json`. Invoke this skill directly for the verification logic the
535
- skill is the SSOT; the commands are thin wrappers.
535
+ Run `spur` CLI via Bash; parse `--json`. Invoke the skill directly the skill is the SSOT; the
536
+ commands are thin wrappers.