@gobing-ai/spur 0.3.47 → 0.3.48

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 (134) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/config.example.yaml +42 -11
  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/task-pipeline.yaml +8 -0
  10. package/config/workflows/wayfinder-resolution.yaml +4 -0
  11. package/config/workflows/wrapup-pipeline.yaml +18 -1
  12. package/package.json +2 -2
  13. package/plugins/sp/agents/expert-spur.md +1 -0
  14. package/plugins/sp/commands/dev-arch.md +2 -1
  15. package/plugins/sp/commands/dev-brainstorm.md +2 -1
  16. package/plugins/sp/commands/dev-changelog.md +1 -0
  17. package/plugins/sp/commands/dev-daily.md +1 -0
  18. package/plugins/sp/commands/dev-debug.md +2 -1
  19. package/plugins/sp/commands/dev-dogfood.md +2 -1
  20. package/plugins/sp/commands/dev-featurechange.md +1 -0
  21. package/plugins/sp/commands/dev-find-conflict.md +2 -1
  22. package/plugins/sp/commands/dev-find-issue.md +20 -32
  23. package/plugins/sp/commands/dev-find-next.md +2 -1
  24. package/plugins/sp/commands/dev-fixall.md +1 -0
  25. package/plugins/sp/commands/dev-gitmsg.md +1 -0
  26. package/plugins/sp/commands/dev-gtd.md +1 -0
  27. package/plugins/sp/commands/dev-handover.md +1 -0
  28. package/plugins/sp/commands/dev-idea.md +1 -0
  29. package/plugins/sp/commands/dev-next.md +2 -1
  30. package/plugins/sp/commands/dev-parallel.md +2 -1
  31. package/plugins/sp/commands/dev-plan.md +2 -1
  32. package/plugins/sp/commands/dev-refine.md +5 -3
  33. package/plugins/sp/commands/dev-refineall.md +2 -1
  34. package/plugins/sp/commands/dev-refresh.md +2 -1
  35. package/plugins/sp/commands/dev-reverse.md +2 -1
  36. package/plugins/sp/commands/dev-review.md +2 -1
  37. package/plugins/sp/commands/dev-run.md +3 -2
  38. package/plugins/sp/commands/dev-runall.md +3 -2
  39. package/plugins/sp/commands/dev-simplify.md +2 -1
  40. package/plugins/sp/commands/dev-unit.md +2 -1
  41. package/plugins/sp/commands/dev-verify.md +2 -1
  42. package/plugins/sp/commands/dev-verifyall.md +2 -1
  43. package/plugins/sp/commands/dev-wrap.md +7 -5
  44. package/plugins/sp/commands/dev-wrapall.md +7 -5
  45. package/plugins/sp/commands/rule-add.md +1 -0
  46. package/plugins/sp/commands/rule-refine.md +1 -0
  47. package/plugins/sp/commands/rule-scan.md +1 -0
  48. package/plugins/sp/commands/spur-init.md +1 -0
  49. package/plugins/sp/commands/workflow-add.md +1 -0
  50. package/plugins/sp/commands/workflow-refine.md +1 -0
  51. package/plugins/sp/hooks/careful-guard.ts +5 -80
  52. package/plugins/sp/hooks/destructive-policy.ts +146 -0
  53. package/plugins/sp/hooks/pi/guard-extension.ts +33 -46
  54. package/plugins/sp/hooks/task-file-policy.ts +31 -0
  55. package/plugins/sp/hooks/task-write-guard.ts +4 -0
  56. package/plugins/sp/plugin.json +1 -1
  57. package/plugins/sp/references/roles.md +93 -0
  58. package/plugins/sp/scripts/feature-sync-bounded.ts +28 -2
  59. package/plugins/sp/scripts/stage-registry-adapter.ts +66 -31
  60. package/plugins/sp/scripts/surface-drift-inventory.ts +908 -0
  61. package/plugins/sp/scripts/task-size-precheck.ts +30 -4
  62. package/plugins/sp/scripts/transition-shim-check.ts +238 -0
  63. package/plugins/sp/scripts/validate-flag-contracts.ts +5 -2
  64. package/plugins/sp/skills/code-verification/SKILL.md +2 -0
  65. package/plugins/sp/skills/issue-finding/SKILL.md +123 -142
  66. package/plugins/sp/skills/issue-finding/examples/expected-findings.json +1 -1
  67. package/plugins/sp/skills/issue-finding/references/session-formats.md +85 -90
  68. package/plugins/sp/skills/next-router/SKILL.md +1 -1
  69. package/plugins/sp/skills/parallel-execution/references/dispatch-surface.md +40 -2
  70. package/plugins/sp/skills/spur-cli/SKILL.md +3 -0
  71. package/plugins/sp/skills/spur-cli/references/agent.md +12 -7
  72. package/plugins/sp/skills/spur-cli/references/team.md +10 -3
  73. package/plugins/sp/skills/spur-dev/SKILL.md +2 -0
  74. package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +17 -0
  75. package/plugins/sp/skills/spur-dev/references/cross-cutting.md +44 -23
  76. package/plugins/sp/skills/spur-dev/references/dev-operations.md +14 -11
  77. package/plugins/sp/skills/spur-dev/references/execution-workflow.md +14 -12
  78. package/plugins/sp/skills/spur-dev/references/flag-glossary.md +14 -3
  79. package/plugins/sp/skills/spur-dev/references/gate-checklists.md +2 -0
  80. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +12 -2
  81. package/schemas/spur-config.schema.json +27 -3
  82. package/spur.js +11637 -7590
  83. package/web/_astro/BoardApp.8hiqShQn.js +1 -0
  84. package/web/_astro/{BoardApp.DKyrGxdo.js → BoardApp.BjQUNhuj.js} +74 -74
  85. package/web/_astro/{TaskDetail.6-27_LMa.js → TaskDetail.CVBuD6dF.js} +1 -1
  86. package/web/_astro/{arc.Df-9AQvS.js → arc.BMMjdODi.js} +1 -1
  87. package/web/_astro/{architectureDiagram-3BPJPVTR.VAI_-paS.js → architectureDiagram-3BPJPVTR.BU5ShzXf.js} +1 -1
  88. package/web/_astro/{blockDiagram-GPEHLZMM.DFpUY1ue.js → blockDiagram-GPEHLZMM.Bj1iEqPD.js} +1 -1
  89. package/web/_astro/{c4Diagram-AAUBKEIU.CF8doOpg.js → c4Diagram-AAUBKEIU.vX8wepCL.js} +1 -1
  90. package/web/_astro/channel.EwdSemIC.js +1 -0
  91. package/web/_astro/{chunk-2J33WTMH.BnjK3fjt.js → chunk-2J33WTMH.BKAipTym.js} +1 -1
  92. package/web/_astro/{chunk-4BX2VUAB.x6ZDnJKq.js → chunk-4BX2VUAB.B68XkPG7.js} +1 -1
  93. package/web/_astro/{chunk-55IACEB6.zY-0uu7w.js → chunk-55IACEB6.BmeDLcrc.js} +1 -1
  94. package/web/_astro/{chunk-727SXJPM.BZxKg_Vi.js → chunk-727SXJPM.PDuBA3Kw.js} +1 -1
  95. package/web/_astro/{chunk-AQP2D5EJ.Cpi9G9Td.js → chunk-AQP2D5EJ.C7A044za.js} +1 -1
  96. package/web/_astro/{chunk-FMBD7UC4.DWTB-Pif.js → chunk-FMBD7UC4.BtzKKFqR.js} +1 -1
  97. package/web/_astro/{chunk-ND2GUHAM.BPDQbiOG.js → chunk-ND2GUHAM.BJuDeeOy.js} +1 -1
  98. package/web/_astro/{chunk-QZHKN3VN.BRWIcuoM.js → chunk-QZHKN3VN.DSeMDgcQ.js} +1 -1
  99. package/web/_astro/{classDiagram-4FO5ZUOK.mGTCZsDO.js → classDiagram-4FO5ZUOK.D53Q4tCw.js} +1 -1
  100. package/web/_astro/{classDiagram-v2-Q7XG4LA2.mGTCZsDO.js → classDiagram-v2-Q7XG4LA2.D53Q4tCw.js} +1 -1
  101. package/web/_astro/{cose-bilkent-S5V4N54A.D1GEut-z.js → cose-bilkent-S5V4N54A.c712AFRH.js} +1 -1
  102. package/web/_astro/{dagre-BM42HDAG.BV0XG9Do.js → dagre-BM42HDAG.D-idisph.js} +1 -1
  103. package/web/_astro/{diagram-2AECGRRQ.DzpYxsjo.js → diagram-2AECGRRQ.DLgnsJCU.js} +1 -1
  104. package/web/_astro/{diagram-5GNKFQAL.Cm9YzJh4.js → diagram-5GNKFQAL.BiaxBVqx.js} +1 -1
  105. package/web/_astro/{diagram-KO2AKTUF.BjhottUj.js → diagram-KO2AKTUF.C8HX1vd8.js} +1 -1
  106. package/web/_astro/{diagram-LMA3HP47.BFsQW5kb.js → diagram-LMA3HP47.CfqDLLes.js} +1 -1
  107. package/web/_astro/{diagram-OG6HWLK6.8pdpzSWO.js → diagram-OG6HWLK6.15SDiEed.js} +1 -1
  108. package/web/_astro/{erDiagram-TEJ5UH35.Bd7KUJmJ.js → erDiagram-TEJ5UH35.DksYtOYM.js} +1 -1
  109. package/web/_astro/{flowDiagram-I6XJVG4X.7LWffkaE.js → flowDiagram-I6XJVG4X.DR_Au-HV.js} +1 -1
  110. package/web/_astro/{ganttDiagram-6RSMTGT7.BeDcO5tI.js → ganttDiagram-6RSMTGT7.CHhHrffI.js} +1 -1
  111. package/web/_astro/{gitGraphDiagram-PVQCEYII.Ca4n730A.js → gitGraphDiagram-PVQCEYII.B2Xehvam.js} +1 -1
  112. package/web/_astro/{index.Dbvuw6d4.css → index.DAxu50UF.css} +1 -1
  113. package/web/_astro/{infoDiagram-5YYISTIA.B0OakQYb.js → infoDiagram-5YYISTIA.C9c3CNNN.js} +1 -1
  114. package/web/_astro/{ishikawaDiagram-YF4QCWOH.DSmNQe-1.js → ishikawaDiagram-YF4QCWOH.BibUHkh8.js} +1 -1
  115. package/web/_astro/{journeyDiagram-JHISSGLW.Cy5ruEUu.js → journeyDiagram-JHISSGLW.BYoVHiyO.js} +1 -1
  116. package/web/_astro/{kanban-definition-UN3LZRKU.CUJXub0p.js → kanban-definition-UN3LZRKU.CM1K5wHE.js} +1 -1
  117. package/web/_astro/{linear.DC1jCCXn.js → linear.SPpjJUb-.js} +1 -1
  118. package/web/_astro/{mermaid.core.DxVP99Ab.js → mermaid.core.BAgx3nnb.js} +4 -4
  119. package/web/_astro/{mindmap-definition-RKZ34NQL.D0MaV6sJ.js → mindmap-definition-RKZ34NQL.D35oPG1R.js} +1 -1
  120. package/web/_astro/{pieDiagram-4H26LBE5.DCC6_q32.js → pieDiagram-4H26LBE5.DiWuRwk7.js} +1 -1
  121. package/web/_astro/{quadrantDiagram-W4KKPZXB.BeUOAM7C.js → quadrantDiagram-W4KKPZXB.B9PBzTWn.js} +1 -1
  122. package/web/_astro/{requirementDiagram-4Y6WPE33.Dbl4MASO.js → requirementDiagram-4Y6WPE33.CYuuamFN.js} +1 -1
  123. package/web/_astro/{sankeyDiagram-5OEKKPKP.HsLg0VS4.js → sankeyDiagram-5OEKKPKP.W24UhhtD.js} +1 -1
  124. package/web/_astro/{sequenceDiagram-3UESZ5HK.DT7DJTnZ.js → sequenceDiagram-3UESZ5HK.BpbNjA51.js} +1 -1
  125. package/web/_astro/{stateDiagram-AJRCARHV.d_ju1Vr1.js → stateDiagram-AJRCARHV.DqVsHudf.js} +1 -1
  126. package/web/_astro/{stateDiagram-v2-BHNVJYJU.DMCAjMJ4.js → stateDiagram-v2-BHNVJYJU.CzwHYX81.js} +1 -1
  127. package/web/_astro/{timeline-definition-PNZ67QCA.DNOHr62_.js → timeline-definition-PNZ67QCA.Bc3B6djw.js} +1 -1
  128. package/web/_astro/{vennDiagram-CIIHVFJN.B7dUy-1W.js → vennDiagram-CIIHVFJN.C-D5rh8O.js} +1 -1
  129. package/web/_astro/{wardley-L42UT6IY.DEqOXvBh.js → wardley-L42UT6IY.D7PdYCqn.js} +1 -1
  130. package/web/_astro/{wardleyDiagram-YWT4CUSO.BCRb2p6x.js → wardleyDiagram-YWT4CUSO.CwmJKXF3.js} +1 -1
  131. package/web/_astro/{xychartDiagram-2RQKCTM6.NxVQLdBh.js → xychartDiagram-2RQKCTM6.avDYnLsb.js} +1 -1
  132. package/web/index.html +2 -2
  133. package/web/_astro/BoardApp.Ce6zJYAH.js +0 -1
  134. 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());
@@ -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 });
@@ -507,6 +507,8 @@ Do **not** use this skill for:
507
507
  and the per-requirement aggregation rule.
508
508
  - [references/secu-review.md](references/secu-review.md) — the SECUA dimensions and finding-severity
509
509
  rubric.
510
+ - `plugins/sp/references/roles.md` — the Layer-1 role→tier table; this skill's verify/review modes
511
+ run under the `reviewer` role (0538 R4: this file points at roles.md, it does not restate tiers).
510
512
  - `.spur/workflows/task-pipeline.yaml` — the `verify → record` gate that consumes the verdict.
511
513
  - **`sp:spur-dev`** — the execution-half umbrella that drives the pipeline this skill gates.
512
514
  - [references/code-improvement.md](references/code-improvement.md) — architecture-improvement lens