@gobing-ai/spur 0.3.41 → 0.3.43

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 (185) hide show
  1. package/.claude-plugin/marketplace.json +15 -0
  2. package/README.md +15 -1
  3. package/config/templates/AGENTS.md +5 -3
  4. package/config/workflows/task-pipeline.yaml +113 -12
  5. package/package.json +15 -12
  6. package/plugins/README.md +655 -0
  7. package/plugins/sp/README.md +621 -0
  8. package/plugins/sp/agents/expert-spur.md +109 -0
  9. package/plugins/sp/agents/super-coder.md +126 -0
  10. package/plugins/sp/agents/super-planner.md +283 -0
  11. package/plugins/sp/agents/super-reviewer.md +154 -0
  12. package/plugins/sp/commands/dev-arch.md +30 -0
  13. package/plugins/sp/commands/dev-brainstorm.md +40 -0
  14. package/plugins/sp/commands/dev-changelog.md +28 -0
  15. package/plugins/sp/commands/dev-daily.md +29 -0
  16. package/plugins/sp/commands/dev-debug.md +29 -0
  17. package/plugins/sp/commands/dev-dogfood.md +33 -0
  18. package/plugins/sp/commands/dev-featurechange.md +130 -0
  19. package/plugins/sp/commands/dev-find-conflict.md +51 -0
  20. package/plugins/sp/commands/dev-find-issue.md +76 -0
  21. package/plugins/sp/commands/dev-find-next.md +60 -0
  22. package/plugins/sp/commands/dev-fixall.md +33 -0
  23. package/plugins/sp/commands/dev-gitmsg.md +28 -0
  24. package/plugins/sp/commands/dev-gtd.md +97 -0
  25. package/plugins/sp/commands/dev-handover.md +26 -0
  26. package/plugins/sp/commands/dev-idea.md +50 -0
  27. package/plugins/sp/commands/dev-next.md +31 -0
  28. package/plugins/sp/commands/dev-parallel.md +30 -0
  29. package/plugins/sp/commands/dev-plan.md +46 -0
  30. package/plugins/sp/commands/dev-refine.md +48 -0
  31. package/plugins/sp/commands/dev-refineall.md +65 -0
  32. package/plugins/sp/commands/dev-refresh.md +32 -0
  33. package/plugins/sp/commands/dev-reverse.md +32 -0
  34. package/plugins/sp/commands/dev-review.md +31 -0
  35. package/plugins/sp/commands/dev-run.md +63 -0
  36. package/plugins/sp/commands/dev-runall.md +83 -0
  37. package/plugins/sp/commands/dev-simplify.md +39 -0
  38. package/plugins/sp/commands/dev-unit.md +29 -0
  39. package/plugins/sp/commands/dev-verify.md +48 -0
  40. package/plugins/sp/commands/dev-verifyall.md +72 -0
  41. package/plugins/sp/commands/dev-wrap.md +48 -0
  42. package/plugins/sp/commands/dev-wrapall.md +51 -0
  43. package/plugins/sp/commands/rule-add.md +18 -0
  44. package/plugins/sp/commands/rule-refine.md +18 -0
  45. package/plugins/sp/commands/rule-scan.md +18 -0
  46. package/plugins/sp/commands/spur-init.md +22 -0
  47. package/plugins/sp/commands/workflow-add.md +18 -0
  48. package/plugins/sp/commands/workflow-refine.md +18 -0
  49. package/plugins/sp/hooks/agent-hint.ts +60 -0
  50. package/plugins/sp/hooks/careful-guard.ts +147 -0
  51. package/plugins/sp/hooks/context-post-tool.ts +325 -0
  52. package/plugins/sp/hooks/context-session-start.ts +182 -0
  53. package/plugins/sp/hooks/context-session-stop.ts +95 -0
  54. package/plugins/sp/hooks/hooks.json +51 -0
  55. package/plugins/sp/hooks/pi/guard-extension.ts +341 -0
  56. package/plugins/sp/hooks/task-write-guard.ts +84 -0
  57. package/plugins/sp/plugin.json +8 -0
  58. package/plugins/sp/scripts/batch-preflight.ts +204 -0
  59. package/plugins/sp/scripts/daily-summary/daily-summary.ts +606 -0
  60. package/plugins/sp/scripts/daily-summary/logger.ts +28 -0
  61. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts +363 -0
  62. package/plugins/sp/scripts/dogfood-testing/validate-report.ts +168 -0
  63. package/plugins/sp/scripts/feature-sync-bounded.ts +451 -0
  64. package/plugins/sp/scripts/stage-registry-adapter.ts +1341 -0
  65. package/plugins/sp/scripts/task-size-precheck.ts +202 -0
  66. package/plugins/sp/scripts/validate-commands.ts +651 -0
  67. package/plugins/sp/scripts/validate-flag-contracts.ts +875 -0
  68. package/plugins/sp/skills/brainstorm/SKILL.md +361 -0
  69. package/plugins/sp/skills/brainstorm/agents/openai.yaml +14 -0
  70. package/plugins/sp/skills/brainstorm/examples/ideation-example.md +209 -0
  71. package/plugins/sp/skills/brainstorm/metadata.openclaw +7 -0
  72. package/plugins/sp/skills/brainstorm/references/grilling-interview.md +68 -0
  73. package/plugins/sp/skills/brainstorm/references/workflows.md +255 -0
  74. package/plugins/sp/skills/branch-workflow/SKILL.md +109 -0
  75. package/plugins/sp/skills/branch-workflow/references/branch-lifecycle.md +69 -0
  76. package/plugins/sp/skills/branch-workflow/references/worktree-patterns.md +100 -0
  77. package/plugins/sp/skills/code-implementation/SKILL.md +203 -0
  78. package/plugins/sp/skills/code-implementation/references/debugging.md +64 -0
  79. package/plugins/sp/skills/code-implementation/references/implementation-patterns.md +113 -0
  80. package/plugins/sp/skills/code-improvement/SKILL.md +231 -0
  81. package/plugins/sp/skills/code-improvement/references/deepening-signals.md +205 -0
  82. package/plugins/sp/skills/code-review/SKILL.md +112 -0
  83. package/plugins/sp/skills/code-review/references/receiving-code-review.md +70 -0
  84. package/plugins/sp/skills/code-review/references/review-lenses.md +117 -0
  85. package/plugins/sp/skills/code-review/references/self-review-checklist.md +53 -0
  86. package/plugins/sp/skills/code-review/references/verification-before-completion.md +68 -0
  87. package/plugins/sp/skills/code-simplification/SKILL.md +225 -0
  88. package/plugins/sp/skills/code-testing/SKILL.md +108 -0
  89. package/plugins/sp/skills/code-testing/references/stacks/bun-ts.md +165 -0
  90. package/plugins/sp/skills/code-testing/references/stacks/go.md +88 -0
  91. package/plugins/sp/skills/code-testing/references/stacks/python.md +117 -0
  92. package/plugins/sp/skills/code-testing/references/test-loop-breaker.md +37 -0
  93. package/plugins/sp/skills/code-testing/references/test-output-discipline.md +52 -0
  94. package/plugins/sp/skills/code-testing/references/unit-testing.md +212 -0
  95. package/plugins/sp/skills/code-verification/SKILL.md +535 -0
  96. package/plugins/sp/skills/code-verification/references/code-improvement.md +39 -0
  97. package/plugins/sp/skills/code-verification/references/secu-review.md +94 -0
  98. package/plugins/sp/skills/code-verification/references/verdict-schema.md +141 -0
  99. package/plugins/sp/skills/conflict-finding/SKILL.md +254 -0
  100. package/plugins/sp/skills/conflict-finding/references/authority-resolution.md +258 -0
  101. package/plugins/sp/skills/conflict-finding/references/comparison-protocol.md +277 -0
  102. package/plugins/sp/skills/conflict-finding/references/finding-contract.md +346 -0
  103. package/plugins/sp/skills/conflict-finding/references/remediation-routing.md +276 -0
  104. package/plugins/sp/skills/daily-summary/SKILL.md +171 -0
  105. package/plugins/sp/skills/daily-summary/agents/openai.yaml +13 -0
  106. package/plugins/sp/skills/doc-evolve/SKILL.md +175 -0
  107. package/plugins/sp/skills/doc-evolve/references/operations.md +93 -0
  108. package/plugins/sp/skills/dogfood-testing/SKILL.md +608 -0
  109. package/plugins/sp/skills/dogfood-testing/references/monitor-ledger.md +211 -0
  110. package/plugins/sp/skills/dogfood-testing/references/report-template.md +351 -0
  111. package/plugins/sp/skills/doubt-driven-development/SKILL.md +136 -0
  112. package/plugins/sp/skills/functional-review/SKILL.md +364 -0
  113. package/plugins/sp/skills/functional-review/references/verdict-schema.md +133 -0
  114. package/plugins/sp/skills/indexed-context/SKILL.md +164 -0
  115. package/plugins/sp/skills/issue-finding/SKILL.md +442 -0
  116. package/plugins/sp/skills/issue-finding/agents/openai.yaml +15 -0
  117. package/plugins/sp/skills/issue-finding/examples/expected-findings.json +27 -0
  118. package/plugins/sp/skills/issue-finding/examples/session-test-loop.jsonl +23 -0
  119. package/plugins/sp/skills/issue-finding/references/session-formats.md +151 -0
  120. package/plugins/sp/skills/next-feature/SKILL.md +125 -0
  121. package/plugins/sp/skills/next-feature/references/handoff-routing.md +92 -0
  122. package/plugins/sp/skills/next-feature/references/proposal-contract.md +49 -0
  123. package/plugins/sp/skills/next-feature/references/ranking-rubric.md +52 -0
  124. package/plugins/sp/skills/next-feature/references/signal-derivation.md +67 -0
  125. package/plugins/sp/skills/next-router/SKILL.md +181 -0
  126. package/plugins/sp/skills/next-router/references/messages.md +90 -0
  127. package/plugins/sp/skills/next-router/references/routing-table.md +216 -0
  128. package/plugins/sp/skills/parallel-execution/SKILL.md +148 -0
  129. package/plugins/sp/skills/parallel-execution/references/dispatch-surface.md +109 -0
  130. package/plugins/sp/skills/parallel-execution/references/fan-out-patterns.md +101 -0
  131. package/plugins/sp/skills/parallel-execution/references/result-synthesis.md +94 -0
  132. package/plugins/sp/skills/reverse-engineering/SKILL.md +375 -0
  133. package/plugins/sp/skills/reverse-engineering/agents/openai.yaml +18 -0
  134. package/plugins/sp/skills/source-driven-development/SKILL.md +124 -0
  135. package/plugins/sp/skills/spec-decomposition/SKILL.md +117 -0
  136. package/plugins/sp/skills/spec-decomposition/references/decomposition.md +539 -0
  137. package/plugins/sp/skills/spur-cli/SKILL.md +133 -0
  138. package/plugins/sp/skills/spur-cli/references/agent.md +179 -0
  139. package/plugins/sp/skills/spur-cli/references/features/acceptance-criteria.md +82 -0
  140. package/plugins/sp/skills/spur-cli/references/features/hierarchy-mece.md +177 -0
  141. package/plugins/sp/skills/spur-cli/references/features/roadmap-priority.md +84 -0
  142. package/plugins/sp/skills/spur-cli/references/features/verbs.md +122 -0
  143. package/plugins/sp/skills/spur-cli/references/features.md +234 -0
  144. package/plugins/sp/skills/spur-cli/references/init.md +119 -0
  145. package/plugins/sp/skills/spur-cli/references/message.md +98 -0
  146. package/plugins/sp/skills/spur-cli/references/rules/authoring-rules.md +212 -0
  147. package/plugins/sp/skills/spur-cli/references/rules/fine-tuning.md +148 -0
  148. package/plugins/sp/skills/spur-cli/references/rules/operations.md +228 -0
  149. package/plugins/sp/skills/spur-cli/references/rules/validation-and-extension.md +129 -0
  150. package/plugins/sp/skills/spur-cli/references/rules.md +209 -0
  151. package/plugins/sp/skills/spur-cli/references/serve.md +58 -0
  152. package/plugins/sp/skills/spur-cli/references/tasks/l3-guard-cheatsheet.md +98 -0
  153. package/plugins/sp/skills/spur-cli/references/tasks/section-editing.md +95 -0
  154. package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +361 -0
  155. package/plugins/sp/skills/spur-cli/references/tasks.md +280 -0
  156. package/plugins/sp/skills/spur-cli/references/team.md +133 -0
  157. package/plugins/sp/skills/spur-cli/references/workflows/authoring-workflows.md +226 -0
  158. package/plugins/sp/skills/spur-cli/references/workflows/operations.md +211 -0
  159. package/plugins/sp/skills/spur-cli/references/workflows/validation-and-extension.md +129 -0
  160. package/plugins/sp/skills/spur-cli/references/workflows.md +355 -0
  161. package/plugins/sp/skills/spur-dev/SKILL.md +224 -0
  162. package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +196 -0
  163. package/plugins/sp/skills/spur-dev/references/cross-cutting.md +680 -0
  164. package/plugins/sp/skills/spur-dev/references/decision-brief.md +80 -0
  165. package/plugins/sp/skills/spur-dev/references/dev-operations.md +546 -0
  166. package/plugins/sp/skills/spur-dev/references/done-housekeeping.md +164 -0
  167. package/plugins/sp/skills/spur-dev/references/execution-batch.md +790 -0
  168. package/plugins/sp/skills/spur-dev/references/execution-workflow.md +352 -0
  169. package/plugins/sp/skills/spur-dev/references/feature-link-helper.md +191 -0
  170. package/plugins/sp/skills/spur-dev/references/flag-glossary.md +417 -0
  171. package/plugins/sp/skills/spur-dev/references/gate-checklists.md +171 -0
  172. package/plugins/sp/skills/spur-dev/references/glossary.md +95 -0
  173. package/plugins/sp/skills/spur-dev/references/idea-evaluation.md +80 -0
  174. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +120 -0
  175. package/plugins/sp/skills/spur-dev/references/planning-workflow.md +302 -0
  176. package/plugins/sp/skills/spur-dev/references/product-planning.md +206 -0
  177. package/plugins/sp/skills/spur-dev/references/section-batching.md +24 -0
  178. package/plugins/sp/skills/sys-architecture/SKILL.md +155 -0
  179. package/plugins/sp/skills/sys-architecture/references/decision-method.md +137 -0
  180. package/plugins/sp/skills/sys-architecture/references/upkeep-survey.md +75 -0
  181. package/plugins/sp/skills/sys-debugging/SKILL.md +228 -0
  182. package/plugins/sp/skills/sys-debugging/references/debugging-protocol.md +62 -0
  183. package/plugins/sp/skills/test-driven-development/SKILL.md +248 -0
  184. package/plugins/sp/skills/wayfinder/SKILL.md +292 -0
  185. package/spur.js +6035 -4963
@@ -0,0 +1,1341 @@
1
+ /**
2
+ * stage-registry-adapter — dev-next golden-path adapter over the canonical
3
+ * stage registry (feature O, spec ticket 0282/0283, task 0307).
4
+ *
5
+ * Provides a programmatic bridge between the dev-next status-aware facade and
6
+ * the stage-registry schema. Defines the actual registered stage records,
7
+ * implements the TABLE A/B/C resolution algorithm, and exports a pure-function
8
+ * resolution API that any agent or CLI can call.
9
+ *
10
+ * Self-contained: no @gobing-ai/spur-domain dependency (plugins/sp is outside
11
+ * the workspace). Types are defined inline, mirroring the domain schema.
12
+ *
13
+ * CLI usage:
14
+ * bun plugins/sp/scripts/stage-registry-adapter.ts --wbs 0307 [--dry-run]
15
+ * bun plugins/sp/scripts/stage-registry-adapter.ts --wbs 0307 --auto
16
+ * bun plugins/sp/scripts/stage-registry-adapter.ts --feature O
17
+ * bun plugins/sp/scripts/stage-registry-adapter.ts --list-stages
18
+ * bun plugins/sp/scripts/stage-registry-adapter.ts --help
19
+ */
20
+
21
+ // ─── Inline type definitions (mirrors packages/domain/src/stage-registry/) ─
22
+
23
+ export type SchemaVersion = { major: number; minor: number };
24
+ export const CURRENT_SCHEMA_VERSION: SchemaVersion = { major: 1, minor: 0 };
25
+ export const AUTHORITY_LANES = ['registry', 'workflow', 'skill', 'cli', 'adapter'] as const;
26
+ export type AuthorityLane = (typeof AUTHORITY_LANES)[number];
27
+ // Mirrors packages/domain/src/stage-registry/schema.ts MUTATION_CLASSES. Pinned by
28
+ // tests/stage-registry-parity.test.ts — the plugin cannot import the domain package
29
+ // (it installs into foreign repos), so the copy is guarded rather than eliminated.
30
+ export const MUTATION_CLASSES = [
31
+ 'none',
32
+ 'corpus',
33
+ 'code',
34
+ 'tests',
35
+ 'verdict',
36
+ 'learnings',
37
+ 'driver',
38
+ 'irreversible',
39
+ ] as const;
40
+ export type MutationClass = (typeof MUTATION_CLASSES)[number];
41
+ export const EXECUTION_KINDS = ['inline', 'subprocess', 'deterministic', 'hitl', 'irreversible'] as const;
42
+ export type ExecutionKind = (typeof EXECUTION_KINDS)[number];
43
+ export const ARTIFACT_DIRECTIONS = ['input', 'output'] as const;
44
+ export type ArtifactDirection = (typeof ARTIFACT_DIRECTIONS)[number];
45
+ export const CONTEXT_LAYER_NAMES = [
46
+ 'harness-policy',
47
+ 'project-authority',
48
+ 'stage-contract',
49
+ 'task-state',
50
+ 'indexed-evidence',
51
+ 'run-state',
52
+ 'tool-observations',
53
+ ] as const;
54
+ export type ContextLayerName = (typeof CONTEXT_LAYER_NAMES)[number];
55
+
56
+ export interface StageArtifact {
57
+ kind: string;
58
+ direction: ArtifactDirection;
59
+ description?: string;
60
+ required?: boolean;
61
+ }
62
+
63
+ export interface StageGate {
64
+ name: string;
65
+ timing: 'pre' | 'post' | 'transition';
66
+ min_verdict?: 'pass' | 'partial' | 'fail';
67
+ description?: string;
68
+ }
69
+
70
+ export interface StageRetryPolicy {
71
+ max_attempts: number;
72
+ terminal_stop: 'block' | 'escalate' | 'fail';
73
+ timeout_seconds?: number;
74
+ }
75
+
76
+ /** Capability tiers (0343): cheap | standard | capable-1 | capable-2 | capable-3. */
77
+ export type AdapterCapabilityTier = 'cheap' | 'standard' | 'capable-1' | 'capable-2' | 'capable-3';
78
+
79
+ /**
80
+ * Objective escalation triggers (task 0405). Self-contained mirror of the
81
+ * domain `objectiveEscalationTriggerSchema` in
82
+ * `packages/domain/src/stage-registry/schema.ts` — the authority. Update both
83
+ * together; `resource-exhaustion` covers rate-limit / quota / token-budget
84
+ * failures as one class.
85
+ */
86
+ export type ObjectiveEscalationTrigger =
87
+ | 'gate-fail'
88
+ | 'timeout'
89
+ | 'insufficient-evidence'
90
+ | 'retry-exhausted'
91
+ | 'resource-exhaustion';
92
+
93
+ export interface StageModelPolicy {
94
+ min_tier: AdapterCapabilityTier;
95
+ fallback: Array<{
96
+ tier: AdapterCapabilityTier;
97
+ trigger: ObjectiveEscalationTrigger;
98
+ }>;
99
+ override_key?: string;
100
+ }
101
+
102
+ export interface StageContextLayer {
103
+ layer: ContextLayerName;
104
+ required: true;
105
+ }
106
+
107
+ export interface StageEvent {
108
+ name: string;
109
+ description?: string;
110
+ }
111
+
112
+ export interface ExecutionVariantInline {
113
+ kind: 'inline';
114
+ current_agent_allowed: true;
115
+ may_reuse_captured_layers?: boolean;
116
+ }
117
+
118
+ export interface ExecutionVariantSubprocess {
119
+ kind: 'subprocess';
120
+ current_agent_allowed: false;
121
+ via: 'spur-agent-run';
122
+ }
123
+
124
+ export interface ExecutionVariantDeterministic {
125
+ kind: 'deterministic';
126
+ current_agent_allowed: false;
127
+ executor: 'cli' | 'script';
128
+ }
129
+
130
+ export interface ExecutionVariantHitl {
131
+ kind: 'hitl';
132
+ current_agent_allowed: true;
133
+ gate_timing: 'pre' | 'post' | 'both';
134
+ }
135
+
136
+ export interface ExecutionVariantIrreversible {
137
+ kind: 'irreversible';
138
+ requires_operator_intent: true;
139
+ current_agent_allowed: boolean;
140
+ rollback_disclaimer: string;
141
+ }
142
+
143
+ export type ExecutionVariant =
144
+ | ExecutionVariantInline
145
+ | ExecutionVariantSubprocess
146
+ | ExecutionVariantDeterministic
147
+ | ExecutionVariantHitl
148
+ | ExecutionVariantIrreversible;
149
+
150
+ export interface StageRecord {
151
+ schema_version: SchemaVersion;
152
+ id: string;
153
+ aliases?: string[];
154
+ description: string;
155
+ artifacts: StageArtifact[];
156
+ reasoning_skill: string;
157
+ required_references?: string[];
158
+ gates?: StageGate[];
159
+ mutation_class: MutationClass;
160
+ retry: StageRetryPolicy;
161
+ model_policy: StageModelPolicy;
162
+ context_layers?: StageContextLayer[];
163
+ observability?: StageEvent[];
164
+ execution: ExecutionVariant;
165
+ }
166
+
167
+ export type TaskStatus = 'backlog' | 'todo' | 'wip' | 'testing' | 'blocked' | 'done' | 'cancelled' | string;
168
+ export type FeatureStatus = 'backlog' | 'active' | 'verifying' | 'blocked' | 'done' | 'cancelled' | string;
169
+ export const TASK_STATUSES = ['backlog', 'todo', 'wip', 'testing', 'blocked', 'done', 'cancelled'] as const;
170
+ export const FEATURE_STATUSES = ['backlog', 'active', 'verifying', 'blocked', 'done', 'cancelled'] as const;
171
+
172
+ // ─── Resolution result types ────────────────────────────────────────────
173
+
174
+ export interface StageResolution {
175
+ stage: StageRecord | null;
176
+ target: string;
177
+ status: string;
178
+ tableRow: string | null;
179
+ reason: string;
180
+ reasonKind: 'dispatch' | 'blocked' | 'multi-candidate' | 'no-route' | 'error' | 'usage';
181
+ requiresConfirmation: boolean;
182
+ chain: boolean;
183
+ dispatchCommand: string | null;
184
+ blocker?: string;
185
+ nextObservableOutcome?: string;
186
+ candidates?: Array<{ command: string; reason: string }>;
187
+ }
188
+ export interface TaskSignal {
189
+ wbs: string;
190
+ status: TaskStatus;
191
+ dependencies?: Array<{ wbs: string; status: TaskStatus }>;
192
+ feature_id?: string | null;
193
+ hasCheckpoint?: boolean;
194
+ }
195
+ export interface FeatureSignal {
196
+ id: string;
197
+ status: FeatureStatus;
198
+ tasks?: TaskSignal[];
199
+ }
200
+
201
+ export interface ResolutionInput {
202
+ target: string;
203
+ wbs?: string;
204
+ feature?: FeatureSignal;
205
+ task?: TaskSignal;
206
+ dryRun?: boolean;
207
+ once?: boolean;
208
+ auto?: boolean;
209
+ fullMode?: boolean;
210
+ }
211
+
212
+ export interface StageLookupEntry {
213
+ stage_id: string;
214
+ command: string;
215
+ skill: string;
216
+ }
217
+
218
+ // ─── Registry of all canonical stage records ────────────────────────────
219
+
220
+ const inlineInline = (reuse?: boolean): ExecutionVariantInline => ({
221
+ kind: 'inline',
222
+ current_agent_allowed: true,
223
+ may_reuse_captured_layers: reuse,
224
+ });
225
+
226
+ const inlineDeterministic = (executor: 'cli' | 'script' = 'cli'): ExecutionVariantDeterministic => ({
227
+ kind: 'deterministic',
228
+ current_agent_allowed: false,
229
+ executor,
230
+ });
231
+
232
+ const inlineHitl = (timing: 'pre' | 'post' | 'both'): ExecutionVariantHitl => ({
233
+ kind: 'hitl',
234
+ current_agent_allowed: true,
235
+ gate_timing: timing,
236
+ });
237
+ const defaultRetry: StageRetryPolicy = { max_attempts: 3, terminal_stop: 'block', timeout_seconds: 300 };
238
+ const standardModel: StageModelPolicy = {
239
+ min_tier: 'standard',
240
+ fallback: [{ tier: 'capable-1', trigger: 'gate-fail' }],
241
+ };
242
+ const capableModel: StageModelPolicy = {
243
+ min_tier: 'capable-1',
244
+ fallback: [],
245
+ };
246
+ /** Plan/create: capable-first (Design authored into tasks by default). */
247
+ const planModel: StageModelPolicy = {
248
+ min_tier: 'capable-2',
249
+ fallback: [{ tier: 'capable-3', trigger: 'gate-fail' }],
250
+ };
251
+ /** Refine: cheap fallback when Design/AC/Plan still blank after create. */
252
+ const refineModel: StageModelPolicy = {
253
+ min_tier: 'standard',
254
+ fallback: [{ tier: 'capable-2', trigger: 'gate-fail' }],
255
+ };
256
+
257
+ const layer = (name: ContextLayerName): StageContextLayer => ({ layer: name, required: true });
258
+ const event = (name: string, description?: string): StageEvent => ({ name, description });
259
+
260
+ /**
261
+ * Complete set of registered canonical stage records.
262
+ * Maps one-to-one with /sp:dev-* operations from dev-operations.md.
263
+ */
264
+ export const REGISTERED_STAGES: StageRecord[] = [
265
+ {
266
+ schema_version: CURRENT_SCHEMA_VERSION,
267
+ id: 'refine',
268
+ aliases: [],
269
+ description: 'dev-refine: Q&A refinement, section filling, AC tightening',
270
+ artifacts: [
271
+ { kind: 'task-section', direction: 'input', description: 'Background/Requirements sections' },
272
+ { kind: 'task-section', direction: 'output', description: 'Q&A/Design/Plan/AC sections' },
273
+ ],
274
+ reasoning_skill: 'sp:spur-dev',
275
+ required_references: ['references/dev-operations.md', 'spur-dev/references/decision-brief.md'],
276
+ gates: [
277
+ { name: 'refine-skip-gate', timing: 'pre', description: 'Skip sections that already meet L3' },
278
+ { name: 'l4-advisory', timing: 'post', min_verdict: 'pass', description: 'L4 advisory surface' },
279
+ ],
280
+ mutation_class: 'corpus',
281
+ retry: defaultRetry,
282
+ model_policy: refineModel,
283
+ context_layers: [layer('project-authority'), layer('task-state'), layer('stage-contract')],
284
+ observability: [event('stage-started'), event('feature-created'), event('batch-created')],
285
+ execution: inlineInline(true),
286
+ },
287
+ {
288
+ schema_version: CURRENT_SCHEMA_VERSION,
289
+ id: 'plan',
290
+ aliases: ['dev-plan'],
291
+ description: 'dev-plan: feature intake -> AC generation -> decomposition -> batch-create',
292
+ artifacts: [
293
+ { kind: 'feature-frontmatter', direction: 'input', required: true },
294
+ { kind: 'task-batch', direction: 'output', required: true },
295
+ ],
296
+ reasoning_skill: 'sp:spur-dev',
297
+ gates: [
298
+ { name: 'feature-check', timing: 'post', min_verdict: 'pass' },
299
+ { name: 'batch-create', timing: 'post', min_verdict: 'pass' },
300
+ ],
301
+ mutation_class: 'corpus',
302
+ retry: defaultRetry,
303
+ model_policy: planModel,
304
+ context_layers: [layer('project-authority'), layer('task-state'), layer('stage-contract')],
305
+ observability: [event('stage-started'), event('feature-created'), event('batch-created')],
306
+ execution: inlineInline(true),
307
+ },
308
+ {
309
+ schema_version: CURRENT_SCHEMA_VERSION,
310
+ id: 'implement',
311
+ description: 'dev-run --mode implement: code edits in worktree',
312
+ artifacts: [
313
+ { kind: 'worktree-diff', direction: 'output', required: true },
314
+ { kind: 'task-section', direction: 'input', description: 'Solution section constraints' },
315
+ ],
316
+ reasoning_skill: 'sp:code-implementation',
317
+ gates: [],
318
+ mutation_class: 'code',
319
+ retry: defaultRetry,
320
+ model_policy: standardModel,
321
+ context_layers: [layer('task-state'), layer('run-state')],
322
+ observability: [event('stage-started'), event('code-modified')],
323
+ execution: inlineInline(),
324
+ },
325
+ {
326
+ schema_version: CURRENT_SCHEMA_VERSION,
327
+ // Adaptive-routing stage for *coverage gap-fill* (router C3/C5 → /sp:dev-unit).
328
+ // Distinct from task-pipeline hops `test` / `test-fix` / `test-recheck`, which are
329
+ // the project quality gate (shell qualityGateCmd + bounded /sp:dev-fixall).
330
+ id: 'test',
331
+ description: 'coverage gap-fill via /sp:dev-unit (C3/C5); pipeline quality gate is test/test-fix/test-recheck',
332
+ aliases: ['unit', 'coverage'],
333
+ artifacts: [
334
+ { kind: 'test-file', direction: 'output', required: true },
335
+ { kind: 'coverage-report', direction: 'output', required: false },
336
+ ],
337
+ reasoning_skill: 'sp:code-testing',
338
+ gates: [{ name: 'coverage-floor', timing: 'post', min_verdict: 'pass', description: '≥90% function coverage' }],
339
+ mutation_class: 'tests',
340
+ retry: defaultRetry,
341
+ model_policy: standardModel,
342
+ context_layers: [layer('task-state'), layer('run-state')],
343
+ observability: [event('stage-started'), event('gate-passed'), event('coverage-measured')],
344
+ execution: inlineInline(),
345
+ },
346
+ {
347
+ schema_version: CURRENT_SCHEMA_VERSION,
348
+ // Pipeline quality-gate family (task-pipeline test hop). Adaptive routing for
349
+ // red lint/type/suite gates uses fixall (TABLE C2 already points here).
350
+ id: 'quality-gate',
351
+ description: 'task-pipeline quality gate: shell qualityGateCmd + bounded /sp:dev-fixall',
352
+ aliases: ['test-fix', 'test-recheck', 'gate'],
353
+ artifacts: [{ kind: 'coverage-report', direction: 'output', required: false }],
354
+ reasoning_skill: 'sp:spur-dev',
355
+ gates: [
356
+ { name: 'project-quality-gate', timing: 'post', min_verdict: 'pass', description: 'qualityGateCmd exit 0' },
357
+ ],
358
+ mutation_class: 'code',
359
+ retry: { max_attempts: 2, terminal_stop: 'block', timeout_seconds: 600 },
360
+ model_policy: standardModel,
361
+ context_layers: [layer('task-state'), layer('run-state')],
362
+ observability: [event('stage-started'), event('gate-passed')],
363
+ execution: inlineInline(),
364
+ },
365
+
366
+ {
367
+ schema_version: CURRENT_SCHEMA_VERSION,
368
+ id: 'verify',
369
+ description: 'dev-verify: SECUA review + requirements traceability',
370
+ artifacts: [
371
+ { kind: 'verdict-artifact', direction: 'output', required: true },
372
+ { kind: 'task-section', direction: 'output', description: 'Testing/Review sections' },
373
+ ],
374
+ reasoning_skill: 'sp:code-verification',
375
+ gates: [
376
+ { name: 'verdict-artifact', timing: 'post', min_verdict: 'pass' },
377
+ { name: 'strict-core', timing: 'post', description: 'L3 core findings must pass' },
378
+ ],
379
+ mutation_class: 'verdict',
380
+ retry: { max_attempts: 2, terminal_stop: 'escalate', timeout_seconds: 600 },
381
+ model_policy: capableModel,
382
+ context_layers: [layer('task-state'), layer('run-state'), layer('indexed-evidence')],
383
+ observability: [event('stage-started'), event('verdict-emitted'), event('gate-passed')],
384
+ execution: inlineInline(),
385
+ },
386
+ {
387
+ schema_version: CURRENT_SCHEMA_VERSION,
388
+ id: 'wrap',
389
+ aliases: ['dev-wrap'],
390
+ description: 'dev-wrap: learnings/doc-sync/feature transition',
391
+ artifacts: [
392
+ { kind: 'learning-entry', direction: 'output' },
393
+ { kind: 'task-section', direction: 'output', description: 'Testing/Review updated' },
394
+ ],
395
+ reasoning_skill: 'sp:spur-dev',
396
+ gates: [
397
+ {
398
+ name: 'task-check',
399
+ timing: 'pre',
400
+ min_verdict: 'pass',
401
+ description: 'Task check must PASS before close',
402
+ },
403
+ ],
404
+ mutation_class: 'learnings',
405
+ retry: { max_attempts: 2, terminal_stop: 'block', timeout_seconds: 180 },
406
+ model_policy: standardModel,
407
+ context_layers: [layer('task-state'), layer('indexed-evidence')],
408
+ observability: [event('stage-started'), event('learnings-written'), event('doc-synced')],
409
+ execution: inlineHitl('both'),
410
+ },
411
+ {
412
+ schema_version: CURRENT_SCHEMA_VERSION,
413
+ id: 'review',
414
+ description: 'dev-review: multi-dimensional code review (functional/SECUA/architecture)',
415
+ artifacts: [{ kind: 'review-findings', direction: 'output', required: true }],
416
+ reasoning_skill: 'sp:code-verification',
417
+ gates: [{ name: 'review-guard', timing: 'post', min_verdict: 'pass', description: 'No P1 findings blocking' }],
418
+ mutation_class: 'verdict',
419
+ retry: { max_attempts: 2, terminal_stop: 'block', timeout_seconds: 300 },
420
+ model_policy: standardModel,
421
+ context_layers: [layer('task-state'), layer('run-state')],
422
+ observability: [event('stage-started'), event('findings-produced')],
423
+ execution: inlineInline(),
424
+ },
425
+ {
426
+ schema_version: CURRENT_SCHEMA_VERSION,
427
+ id: 'dogfood',
428
+ description: 'dev-dogfood: end-to-end driver test of a skill/command/CLI',
429
+ artifacts: [
430
+ { kind: 'dogfood-report', direction: 'output', required: true },
431
+ { kind: 'monitor-ledger', direction: 'output' },
432
+ ],
433
+ reasoning_skill: 'sp:dogfood-testing',
434
+ required_references: ['references/monitor-ledger.md', 'references/report-template.md'],
435
+ gates: [
436
+ { name: 'detect-pipeline-driving', timing: 'pre', description: 'Refuse dogfood when driving a pipeline' },
437
+ {
438
+ name: 'report-validate',
439
+ timing: 'post',
440
+ min_verdict: 'pass',
441
+ description: 'Report must pass schema validation',
442
+ },
443
+ ],
444
+ mutation_class: 'driver',
445
+ retry: { max_attempts: 3, terminal_stop: 'block', timeout_seconds: 600 },
446
+ model_policy: capableModel,
447
+ context_layers: [layer('task-state'), layer('run-state')],
448
+ observability: [event('stage-started'), event('gate-passed'), event('report-emitted')],
449
+ execution: inlineInline(),
450
+ },
451
+ {
452
+ schema_version: CURRENT_SCHEMA_VERSION,
453
+ id: 'handover',
454
+ description: 'dev-handover: structured handover document when blocked',
455
+ artifacts: [{ kind: 'handover-doc', direction: 'output', required: true }],
456
+ reasoning_skill: 'inline',
457
+ gates: [],
458
+ mutation_class: 'corpus',
459
+ retry: { max_attempts: 1, terminal_stop: 'block', timeout_seconds: 120 },
460
+ model_policy: standardModel,
461
+ context_layers: [layer('task-state')],
462
+ observability: [event('stage-started')],
463
+ execution: inlineDeterministic('cli'),
464
+ },
465
+ {
466
+ schema_version: CURRENT_SCHEMA_VERSION,
467
+ id: 'fixall',
468
+ description: 'dev-fixall: lint + type + test fix loop',
469
+ artifacts: [
470
+ { kind: 'lint-report', direction: 'output' },
471
+ { kind: 'test-report', direction: 'output' },
472
+ ],
473
+ reasoning_skill: 'inline',
474
+ gates: [],
475
+ mutation_class: 'code',
476
+ retry: { max_attempts: 1, terminal_stop: 'block', timeout_seconds: 120 },
477
+ model_policy: standardModel,
478
+ context_layers: [layer('task-state')],
479
+ observability: [event('stage-started')],
480
+ execution: inlineDeterministic('script'),
481
+ },
482
+ {
483
+ schema_version: CURRENT_SCHEMA_VERSION,
484
+ id: 'brainstorm',
485
+ description: 'dev-brainstorm: structured ideation with trade-off analysis',
486
+ artifacts: [{ kind: 'brainstorm-outline', direction: 'output', required: true }],
487
+ reasoning_skill: 'sp:brainstorm',
488
+ gates: [],
489
+ mutation_class: 'corpus',
490
+ retry: { max_attempts: 2, terminal_stop: 'block', timeout_seconds: 300 },
491
+ model_policy: standardModel,
492
+ context_layers: [layer('project-authority'), layer('task-state')],
493
+ observability: [event('stage-started')],
494
+ execution: inlineInline(),
495
+ },
496
+ {
497
+ schema_version: CURRENT_SCHEMA_VERSION,
498
+ id: 'changelog',
499
+ description: 'dev-changelog: generate changelog from git commits',
500
+ artifacts: [{ kind: 'changelog-entry', direction: 'output' }],
501
+ reasoning_skill: 'inline',
502
+ gates: [],
503
+ mutation_class: 'none',
504
+ retry: { max_attempts: 1, terminal_stop: 'block', timeout_seconds: 60 },
505
+ model_policy: { min_tier: 'cheap', fallback: [] },
506
+ context_layers: [],
507
+ observability: [event('stage-started')],
508
+ execution: inlineDeterministic('cli'),
509
+ },
510
+ ];
511
+
512
+ // ─── Stage lookup ────────────────────────────────────────────────────────
513
+
514
+ export const STAGE_BY_ID = new Map<string, StageRecord>(REGISTERED_STAGES.map((s) => [s.id, s]));
515
+ for (const s of REGISTERED_STAGES) {
516
+ for (const a of s.aliases ?? []) {
517
+ STAGE_BY_ID.set(a, s);
518
+ }
519
+ }
520
+
521
+ export function getStage(id: string): StageRecord | undefined {
522
+ return STAGE_BY_ID.get(id);
523
+ }
524
+
525
+ export function listStages(): StageLookupEntry[] {
526
+ const COMMAND_BY_ID: Record<string, string> = {
527
+ refine: '/sp:dev-refine',
528
+ plan: '/sp:dev-plan',
529
+ implement: '/sp:dev-run --mode implement',
530
+ // Coverage gap-fill competency (not the pipeline quality-gate hops).
531
+ test: '/sp:dev-unit',
532
+ // Pipeline test/test-fix/test-recheck family — shell gate + fixall.
533
+ 'quality-gate': '/sp:dev-fixall',
534
+ verify: '/sp:dev-verify',
535
+ wrap: '/sp:dev-wrap',
536
+ review: '/sp:dev-review',
537
+ dogfood: '/sp:dev-dogfood',
538
+ handover: 'inline (dev-handover)',
539
+ fixall: 'inline (dev-fixall)',
540
+ brainstorm: '/sp:dev-brainstorm',
541
+ changelog: 'inline (dev-changelog)',
542
+ };
543
+ return REGISTERED_STAGES.map((s) => ({
544
+ stage_id: s.id,
545
+ command: COMMAND_BY_ID[s.id] ?? `/sp:dev-${s.id}`,
546
+ skill: s.reasoning_skill,
547
+ }));
548
+ }
549
+
550
+ // ─── TABLE A: task status → dispatch (routing-table.md §1) ──────────────
551
+
552
+ interface TableARow {
553
+ condition: (input: ResolutionInput) => boolean;
554
+ dispatch: string | null;
555
+ rowId: string;
556
+ chain: boolean;
557
+ stop: boolean;
558
+ stopReason?: string;
559
+ probe: boolean;
560
+ requiresConfirmation: boolean;
561
+ }
562
+
563
+ function statusEquals(s: string): (input: ResolutionInput) => boolean {
564
+ return (input) => input.task?.status === s;
565
+ }
566
+
567
+ function statusAndDepsSatisfied(s: string): (input: ResolutionInput) => boolean {
568
+ return (input) => {
569
+ if (input.task?.status !== s) return false;
570
+ const deps = input.task?.dependencies ?? [];
571
+ if (deps.length === 0) return true;
572
+ return deps.every((d) => d.status === 'done');
573
+ };
574
+ }
575
+
576
+ // Exported for tests/routing-table-parity.test.ts (C2): the markdown routing table
577
+ // and these rows are two live representations of one contract and must be pinned.
578
+ export const TABLE_A: TableARow[] = [
579
+ // A1 — backlog → refine
580
+ {
581
+ condition: statusEquals('backlog'),
582
+ dispatch: '/sp:dev-refine {wbs} --auto --next',
583
+ rowId: 'A1',
584
+ chain: true,
585
+ stop: false,
586
+ probe: true,
587
+ requiresConfirmation: false,
588
+ },
589
+ // A2 — todo with unmet deps → STOP
590
+ {
591
+ condition: (input) => {
592
+ if (input.task?.status !== 'todo') return false;
593
+ const deps = input.task?.dependencies ?? [];
594
+ return deps.length > 0 && deps.some((d) => d.status !== 'done');
595
+ },
596
+ dispatch: null,
597
+ rowId: 'A2',
598
+ chain: false,
599
+ stop: true,
600
+ stopReason: 'blocked by open dependencies',
601
+ probe: false,
602
+ requiresConfirmation: false,
603
+ },
604
+ // A3 — todo with satisfied deps → run --mode implement --next.
605
+ // The explicit mode is load-bearing (bug-742, routing-table.md §1 A3): without
606
+ // it the pipeline step can recursively launch full mode, and the dispatch also
607
+ // fails to match the implement pattern in STAGE_BY_DISPATCH, resolving to a null
608
+ // stage record. Pinned against routing-table.md by tests/routing-table-parity.test.ts.
609
+ {
610
+ condition: statusAndDepsSatisfied('todo'),
611
+ dispatch: '/sp:dev-run {wbs} --mode implement --auto --next',
612
+ rowId: 'A3',
613
+ chain: true,
614
+ stop: false,
615
+ probe: true,
616
+ requiresConfirmation: false,
617
+ },
618
+ // A4 — wip with checkpoint → continue
619
+ {
620
+ condition: (input) => input.task?.status === 'wip' && input.task?.hasCheckpoint === true,
621
+ dispatch: '/sp:dev-run {wbs} --continue',
622
+ rowId: 'A4',
623
+ chain: false,
624
+ stop: false,
625
+ probe: false,
626
+ requiresConfirmation: false,
627
+ },
628
+ // A5 — wip no checkpoint → implement --next
629
+ {
630
+ condition: statusEquals('wip'),
631
+ dispatch: '/sp:dev-run {wbs} --mode implement --auto --next',
632
+ rowId: 'A5',
633
+ chain: true,
634
+ stop: false,
635
+ probe: true,
636
+ requiresConfirmation: false,
637
+ },
638
+ // A6 — testing → verify
639
+ {
640
+ condition: statusEquals('testing'),
641
+ dispatch: '/sp:dev-verify {wbs} --auto --next',
642
+ rowId: 'A6',
643
+ chain: true,
644
+ stop: false,
645
+ probe: true,
646
+ requiresConfirmation: false,
647
+ },
648
+ // A7 — blocked → handover
649
+ {
650
+ condition: statusEquals('blocked'),
651
+ dispatch: '/sp:dev-handover <blocker>',
652
+ rowId: 'A7',
653
+ chain: false,
654
+ stop: true,
655
+ stopReason: 'blocked — handover document needed',
656
+ probe: false,
657
+ requiresConfirmation: true,
658
+ },
659
+ // A8 — done → wrap
660
+ {
661
+ condition: statusEquals('done'),
662
+ dispatch: '/sp:dev-wrap {wbs}',
663
+ rowId: 'A8',
664
+ chain: false,
665
+ stop: false,
666
+ probe: false,
667
+ requiresConfirmation: true,
668
+ },
669
+ // A9 — cancelled → STOP
670
+ {
671
+ condition: statusEquals('cancelled'),
672
+ dispatch: null,
673
+ rowId: 'A9',
674
+ chain: false,
675
+ stop: true,
676
+ stopReason: 'cancelled — nothing to advance',
677
+ probe: false,
678
+ requiresConfirmation: false,
679
+ },
680
+ ];
681
+
682
+ // ─── TABLE B: feature-level routing (routing-table.md §2) ────────────────
683
+
684
+ interface TableBRow {
685
+ condition: (input: ResolutionInput) => boolean;
686
+ dispatch: ((input: ResolutionInput) => string | null) | null;
687
+ rowId: string;
688
+ chain: boolean;
689
+ stop: boolean;
690
+ stopReason?: string;
691
+ requiresConfirmation: boolean;
692
+ }
693
+
694
+ /** Exported for tests/routing-table-parity.test.ts (C2). */
695
+ export const TABLE_B: TableBRow[] = [
696
+ // B0 — unknown feature
697
+ {
698
+ condition: (input) => input.feature == null,
699
+ dispatch: null,
700
+ rowId: 'B0',
701
+ chain: false,
702
+ stop: true,
703
+ stopReason: 'unknown feature id',
704
+ requiresConfirmation: false,
705
+ },
706
+ // B1 — cancelled
707
+ {
708
+ condition: (input) => input.feature?.status === 'cancelled',
709
+ dispatch: null,
710
+ rowId: 'B1',
711
+ chain: false,
712
+ stop: true,
713
+ stopReason: 'feature cancelled',
714
+ requiresConfirmation: false,
715
+ },
716
+ // B2 — done
717
+ {
718
+ condition: (input) => input.feature?.status === 'done',
719
+ dispatch: null,
720
+ rowId: 'B2',
721
+ chain: false,
722
+ stop: true,
723
+ stopReason: 'feature already done',
724
+ requiresConfirmation: false,
725
+ },
726
+ // B3 — frontier task exists → recurse TABLE A
727
+ {
728
+ condition: (input) => {
729
+ const tasks = input.feature?.tasks ?? [];
730
+ return tasks.some((t) => ['backlog', 'todo', 'wip', 'testing', 'blocked'].includes(t.status));
731
+ },
732
+ dispatch: null, // Handled specially by resolveStage — picks frontier task
733
+ rowId: 'B3',
734
+ chain: false,
735
+ stop: false,
736
+ requiresConfirmation: false,
737
+ },
738
+ // B4 — no frontier, feature backlog, AC invalid
739
+ {
740
+ condition: (input) => {
741
+ if (input.feature?.status !== 'backlog') return false;
742
+ const open = (input.feature?.tasks ?? []).filter((t) =>
743
+ ['backlog', 'todo', 'wip', 'testing', 'blocked'].includes(t.status),
744
+ );
745
+ return open.length === 0;
746
+ },
747
+ dispatch: null, // stop row — resolveFeature returns before invoking dispatch
748
+ rowId: 'B4',
749
+ chain: false,
750
+ stop: true,
751
+ stopReason: 'feature needs description and AC decomposition',
752
+ requiresConfirmation: true,
753
+ },
754
+ // B5 — no frontier, valid AC but zero tasks
755
+ {
756
+ condition: (input) => {
757
+ if (
758
+ input.feature?.status === 'blocked' ||
759
+ input.feature?.status === 'cancelled' ||
760
+ input.feature?.status === 'done'
761
+ )
762
+ return false;
763
+ const tasks = input.feature?.tasks ?? [];
764
+ const open = tasks.filter((t) => !['done', 'cancelled'].includes(t.status));
765
+ return open.length === 0 && tasks.length === 0;
766
+ },
767
+ dispatch: null, // stop row — resolveFeature returns before invoking dispatch
768
+ rowId: 'B5',
769
+ chain: false,
770
+ stop: true,
771
+ stopReason: 'no tasks created yet — run dev-plan to decompose',
772
+ requiresConfirmation: true,
773
+ },
774
+ // B6 — all tasks done, feature active/verifying → wrapall
775
+ {
776
+ condition: (input) => {
777
+ if (!input.feature) return false;
778
+ if (!['active', 'verifying'].includes(input.feature.status)) return false;
779
+ const tasks = input.feature?.tasks ?? [];
780
+ return tasks.length > 0 && tasks.every((t) => ['done', 'cancelled'].includes(t.status));
781
+ },
782
+ dispatch: (input) => `/sp:dev-wrapall --feature ${input.feature?.id ?? ''}`,
783
+ rowId: 'B6',
784
+ chain: false,
785
+ stop: false,
786
+ requiresConfirmation: true,
787
+ },
788
+ // B7 — mixed cancelled/done only
789
+ {
790
+ condition: (input) => {
791
+ if (!input.feature) return false;
792
+ const tasks = input.feature?.tasks ?? [];
793
+ if (tasks.length === 0) return false;
794
+ return tasks.every((t) => ['done', 'cancelled'].includes(t.status));
795
+ },
796
+ dispatch: null,
797
+ rowId: 'B7',
798
+ chain: false,
799
+ stop: true,
800
+ stopReason: 'all tasks are done or cancelled — no action needed',
801
+ requiresConfirmation: false,
802
+ },
803
+ // B8 — blocked
804
+ {
805
+ condition: (input) => input.feature?.status === 'blocked',
806
+ dispatch: null,
807
+ rowId: 'B8',
808
+ chain: false,
809
+ stop: true,
810
+ stopReason: 'feature is blocked — resolve blocker first',
811
+ requiresConfirmation: false,
812
+ },
813
+ ];
814
+
815
+ // ─── TABLE C light-gate short-circuit (routing-table.md §3) ─────────────
816
+
817
+ // Note: no `condition` field — TABLE C rows encode external runtime checks
818
+ // (lint failures, test failures, rule findings) that this pure adapter cannot
819
+ // evaluate. Rows are only consumed via C_REDIRECT_TABLE lookup, never matched.
820
+ interface TableCRow {
821
+ redirectDispatch: string;
822
+ rowId: string;
823
+ probeRows: string[];
824
+ }
825
+
826
+ const TABLE_C: TableCRow[] = [
827
+ {
828
+ // C1 — spur task check L3 findings: external check, not evaluable here
829
+ redirectDispatch: '/sp:dev-refine {wbs} --auto',
830
+ rowId: 'C1',
831
+ probeRows: ['A1', 'A3', 'A5'],
832
+ },
833
+ {
834
+ // C2 — lint failures: requires runtime check
835
+ redirectDispatch: '/sp:dev-fixall',
836
+ rowId: 'C2',
837
+ probeRows: ['A3', 'A5'],
838
+ },
839
+ {
840
+ // C3 — test failures: requires runtime check
841
+ redirectDispatch: '/sp:dev-unit {wbs} --auto',
842
+ rowId: 'C3',
843
+ probeRows: ['A5', 'A6'],
844
+ },
845
+ {
846
+ // C4 — rule findings: requires spur rule run
847
+ redirectDispatch: null as unknown as string, // HITL stop
848
+ rowId: 'C4',
849
+ probeRows: ['A3', 'A5', 'A6'],
850
+ },
851
+ {
852
+ // C5 — verdict FAIL pointing at coverage
853
+ redirectDispatch: '/sp:dev-unit {wbs}',
854
+ rowId: 'C5',
855
+ probeRows: ['A6'],
856
+ },
857
+ ];
858
+
859
+ // ─── Table C helpers ────────────────────────────────────────────────────
860
+
861
+ const C_REDIRECT_TABLE: Record<string, TableCRow> = {};
862
+ for (const row of TABLE_C) {
863
+ for (const pr of row.probeRows) {
864
+ C_REDIRECT_TABLE[pr] = row;
865
+ }
866
+ }
867
+
868
+ export function getTableCRedirect(rowId: string): TableCRow | undefined {
869
+ return C_REDIRECT_TABLE[rowId];
870
+ }
871
+
872
+ // ─── Core resolution ────────────────────────────────────────────────────
873
+
874
+ /**
875
+ * Pick the best frontier task from a feature's task list (TABLE B3 algorithm).
876
+ */
877
+ export function pickFrontierTask(tasks: TaskSignal[]): TaskSignal | null {
878
+ const OPEN_STATUSES: Record<string, number> = {
879
+ todo: 0,
880
+ backlog: 1,
881
+ wip: 2,
882
+ testing: 3,
883
+ blocked: 4,
884
+ };
885
+ const candidates = tasks.filter((t) => {
886
+ const rank = OPEN_STATUSES[t.status];
887
+ if (rank === undefined) return false;
888
+ // Exclude tasks with blocked-by-dep (not all dependencies done)
889
+ const deps = t.dependencies ?? [];
890
+ if (deps.length > 0 && deps.some((d) => d.status !== 'done')) return false;
891
+ return true;
892
+ });
893
+ if (candidates.length === 0) return null;
894
+ candidates.sort((a, b) => {
895
+ const rankA = OPEN_STATUSES[a.status] ?? 99;
896
+ const rankB = OPEN_STATUSES[b.status] ?? 99;
897
+ if (rankA !== rankB) return rankA - rankB;
898
+ return a.wbs.localeCompare(b.wbs);
899
+ });
900
+ return candidates[0] ?? null;
901
+ }
902
+
903
+ /**
904
+ * Build the unmet dependencies list for the A2 block message.
905
+ */
906
+ export function unmetDependencies(task: TaskSignal): string[] {
907
+ return (task.dependencies ?? []).filter((d) => d.status !== 'done').map((d) => d.wbs);
908
+ }
909
+
910
+ /**
911
+ * Resolve a task WBS or feature id to the next canonical stage.
912
+ *
913
+ * Implements the TABLE A/B/C algorithm from routing-table.md.
914
+ * Returns a StageResolution describing the selected stage (or stop reason).
915
+ */
916
+ export function resolveStage(input: ResolutionInput): StageResolution {
917
+ // Feature mode
918
+ if (input.feature != null) {
919
+ return resolveFeature(input);
920
+ }
921
+
922
+ // Task mode — TABLE A
923
+ if (input.task != null) {
924
+ return resolveTask(input);
925
+ }
926
+
927
+ return {
928
+ stage: null,
929
+ target: input.target,
930
+ status: 'unknown',
931
+ tableRow: null,
932
+ reason: 'no target resolved — pass a task WBS or feature id',
933
+ reasonKind: 'usage',
934
+ requiresConfirmation: false,
935
+ chain: false,
936
+ dispatchCommand: null,
937
+ };
938
+ }
939
+
940
+ function resolveTask(input: ResolutionInput): StageResolution {
941
+ if (input.task == null) throw new Error('resolveTask called without task');
942
+ const task = input.task;
943
+ const wbs = task.wbs;
944
+
945
+ // Apply TABLE A rows in order (A4 fires before A5 for wip+checkpoint;
946
+ // A5 catches wip without checkpoint since A4's condition is narrower)
947
+ for (const row of TABLE_A) {
948
+ if (!row.condition(input)) continue;
949
+
950
+ // Build the dispatch command
951
+ let dispatchCmd: string | null = row.dispatch;
952
+ if (dispatchCmd) {
953
+ dispatchCmd = dispatchCmd.replace(/\{wbs\}/g, wbs);
954
+ }
955
+
956
+ // Apply flag forwarding
957
+ if (input.once && dispatchCmd) {
958
+ dispatchCmd = dispatchCmd.replace(/ --next/g, '');
959
+ }
960
+ if (input.auto && dispatchCmd && !dispatchCmd.includes('--auto')) {
961
+ dispatchCmd += ' --auto';
962
+ }
963
+ if (input.fullMode && dispatchCmd) {
964
+ if (dispatchCmd.includes('--mode implement') || dispatchCmd.includes('--next')) {
965
+ dispatchCmd = `/sp:dev-run ${wbs} --mode full`;
966
+ }
967
+ }
968
+
969
+ // Map dispatch command → canonical stage id, first match wins.
970
+ //
971
+ // Patterns, not substrings: the run routes carry the WBS between the verb
972
+ // and the flag (`/sp:dev-run 0104 --mode implement`), so the old
973
+ // `'run --mode'` substring key could never match and every A3/A4/A5
974
+ // dispatch resolved to a null stage record — silently dropping the
975
+ // implement stage's gates, retry and model policy from the resolution.
976
+ const STAGE_BY_DISPATCH: Array<{ pattern: RegExp; stage: string }> = [
977
+ { pattern: /dev-refine/, stage: 'refine' },
978
+ { pattern: /dev-verify/, stage: 'verify' },
979
+ { pattern: /dev-wrap/, stage: 'wrap' },
980
+ { pattern: /dev-handover/, stage: 'handover' },
981
+ { pattern: /dev-run\b[^\n]*--mode/, stage: 'implement' },
982
+ { pattern: /dev-run\b[^\n]*--continue/, stage: 'implement' },
983
+ { pattern: /dev-unit/, stage: 'test' },
984
+ { pattern: /dev-fixall/, stage: 'fixall' },
985
+ // Pipeline quality-gate hops share the fixall repair surface.
986
+ { pattern: /test-fix|test-recheck|quality-gate/, stage: 'quality-gate' },
987
+ ];
988
+ let stageId: string | undefined;
989
+ if (dispatchCmd) {
990
+ for (const { pattern, stage: sid } of STAGE_BY_DISPATCH) {
991
+ if (pattern.test(dispatchCmd)) {
992
+ stageId = sid;
993
+ break;
994
+ }
995
+ }
996
+ }
997
+ const stage = stageId ? (getStage(stageId) ?? null) : null;
998
+
999
+ if (row.stop) {
1000
+ const stopReason = row.stopReason ?? 'no route';
1001
+ return {
1002
+ stage: null,
1003
+ target: wbs,
1004
+ status: task.status,
1005
+ tableRow: row.rowId,
1006
+ reason: stopReason,
1007
+ reasonKind: row.rowId === 'A2' ? 'blocked' : 'no-route',
1008
+ requiresConfirmation: row.requiresConfirmation,
1009
+ chain: row.chain,
1010
+ dispatchCommand: null,
1011
+ blocker: row.rowId === 'A2' ? `unmet deps: ${unmetDependencies(task).join(', ')}` : undefined,
1012
+ nextObservableOutcome:
1013
+ row.rowId === 'A2'
1014
+ ? 'resolve open dependencies'
1015
+ : row.rowId === 'A7'
1016
+ ? 'handover document created'
1017
+ : row.rowId === 'A9'
1018
+ ? 'no action needed'
1019
+ : undefined,
1020
+ };
1021
+ }
1022
+
1023
+ const OUTCOME_BY_ROW: Record<string, string> = {
1024
+ A1: 'refined task with filled sections',
1025
+ A3: 'code changes implemented',
1026
+ A4: 'implementation resumed from checkpoint',
1027
+ A5: 'implementation continued',
1028
+ A6: 'verification verdict (PASS/PARTIAL/FAIL)',
1029
+ A8: 'learnings recorded, doc synced',
1030
+ };
1031
+
1032
+ const REASON_BY_ROW: Record<string, string> = {
1033
+ A1: 'backlog — needs refinement',
1034
+ A3: 'todo — ready to implement',
1035
+ A4: 'wip — resume from checkpoint',
1036
+ A5: 'wip — continue implementing',
1037
+ A6: 'testing — verify results',
1038
+ A8: 'done — wrap up',
1039
+ };
1040
+
1041
+ return {
1042
+ stage,
1043
+ target: wbs,
1044
+ status: task.status,
1045
+ tableRow: row.rowId,
1046
+ reason: REASON_BY_ROW[row.rowId] ?? 'dispatch',
1047
+ reasonKind: 'dispatch',
1048
+ requiresConfirmation: row.requiresConfirmation,
1049
+ chain: row.chain,
1050
+ dispatchCommand: dispatchCmd,
1051
+ nextObservableOutcome: OUTCOME_BY_ROW[row.rowId],
1052
+ };
1053
+ }
1054
+
1055
+ // No row matched
1056
+ return {
1057
+ stage: null,
1058
+ target: wbs,
1059
+ status: task.status,
1060
+ tableRow: null,
1061
+ reason: `no route for ${wbs} (status=${task.status})`,
1062
+ reasonKind: 'no-route',
1063
+ requiresConfirmation: false,
1064
+ chain: false,
1065
+ dispatchCommand: null,
1066
+ };
1067
+ }
1068
+ function resolveFeature(input: ResolutionInput): StageResolution {
1069
+ if (input.feature == null) throw new Error('resolveFeature called without feature');
1070
+ const feature = input.feature;
1071
+
1072
+ for (const row of TABLE_B) {
1073
+ if (!row.condition(input)) continue;
1074
+
1075
+ // B3: frontier task → recurse TABLE A on the picked task
1076
+ if (row.rowId === 'B3') {
1077
+ const frontier = pickFrontierTask(feature.tasks ?? []);
1078
+ if (!frontier) {
1079
+ return {
1080
+ stage: null,
1081
+ target: feature.id,
1082
+ status: feature.status,
1083
+ tableRow: 'B3',
1084
+ reason: 'no frontier task found after condition matched — inconsistency',
1085
+ reasonKind: 'error',
1086
+ requiresConfirmation: false,
1087
+ chain: false,
1088
+ dispatchCommand: null,
1089
+ };
1090
+ }
1091
+ const taskInput: ResolutionInput = {
1092
+ target: frontier.wbs,
1093
+ task: frontier,
1094
+ feature: input.feature,
1095
+ dryRun: input.dryRun,
1096
+ once: input.once,
1097
+ auto: input.auto,
1098
+ fullMode: input.fullMode,
1099
+ };
1100
+ return resolveTask(taskInput);
1101
+ }
1102
+
1103
+ if (row.stop) {
1104
+ return {
1105
+ stage: null,
1106
+ target: feature.id,
1107
+ status: feature.status,
1108
+ tableRow: row.rowId,
1109
+ reason: row.stopReason ?? 'stop',
1110
+ reasonKind: 'blocked',
1111
+ requiresConfirmation: row.requiresConfirmation,
1112
+ chain: false,
1113
+ dispatchCommand: null,
1114
+ blocker: row.stopReason,
1115
+ };
1116
+ }
1117
+
1118
+ const dispatchCmd = row.dispatch?.(input);
1119
+ const stage = dispatchCmd?.includes('wrapall') ? getStage('wrap') : null;
1120
+
1121
+ return {
1122
+ stage: stage ?? null,
1123
+ target: feature.id,
1124
+ status: feature.status,
1125
+ tableRow: row.rowId,
1126
+ reason: row.rowId === 'B6' ? 'all tasks done — wrap up feature' : 'feature route',
1127
+ reasonKind: 'dispatch',
1128
+ requiresConfirmation: row.requiresConfirmation,
1129
+ chain: false,
1130
+ dispatchCommand: dispatchCmd,
1131
+ nextObservableOutcome: row.rowId === 'B6' ? 'feature transition or wrap' : undefined,
1132
+ };
1133
+ }
1134
+
1135
+ return {
1136
+ stage: null,
1137
+ target: feature.id,
1138
+ status: feature.status,
1139
+ tableRow: null,
1140
+ reason: `no route for feature ${feature.id}`,
1141
+ reasonKind: 'no-route',
1142
+ requiresConfirmation: false,
1143
+ chain: false,
1144
+ dispatchCommand: null,
1145
+ };
1146
+ }
1147
+
1148
+ // ─── Utility: display stage help ────────────────────────────────────────
1149
+
1150
+ export function renderHelp(): string {
1151
+ const lines: string[] = [
1152
+ 'stage-registry-adapter — dev-next golden-path adapter',
1153
+ '',
1154
+ 'Usage:',
1155
+ ' bun plugins/sp/scripts/stage-registry-adapter.ts \\',
1156
+ ' --wbs <wbs> [--dry-run] [--auto] [--once] [--full]',
1157
+ ' bun plugins/sp/scripts/stage-registry-adapter.ts \\',
1158
+ ' --feature <id> [--dry-run] [--auto]',
1159
+ ' bun plugins/sp/scripts/stage-registry-adapter.ts --list-stages',
1160
+ ' bun plugins/sp/scripts/stage-registry-adapter.ts --help',
1161
+ '',
1162
+ 'Options:',
1163
+ ' --wbs <wbs> Task WBS to resolve (digits)',
1164
+ ' --feature <id> Feature id to resolve (e.g. O)',
1165
+ ' --dry-run Print plan without dispatching',
1166
+ ' --auto Forward --auto to dispatched commands',
1167
+ ' --once Suppress --next chain',
1168
+ ' --full Use --mode full for implement',
1169
+ ' --list-stages List all registered stages',
1170
+ ' --help Show this message',
1171
+ '',
1172
+ 'Registered stages:',
1173
+ ];
1174
+ for (const s of REGISTERED_STAGES) {
1175
+ const aliases = s.aliases?.length ? ` (${s.aliases.join(', ')})` : '';
1176
+ lines.push(` ${s.id}${aliases} — ${s.description}`);
1177
+ }
1178
+ return lines.join('\n');
1179
+ }
1180
+
1181
+ // ─── CLI entry point ────────────────────────────────────────────────────
1182
+
1183
+ export interface CliArgs {
1184
+ wbs?: string;
1185
+ feature?: string;
1186
+ taskStatus?: string;
1187
+ dryRun: boolean;
1188
+ auto: boolean;
1189
+ once: boolean;
1190
+ full: boolean;
1191
+ listStages: boolean;
1192
+ help: boolean;
1193
+ }
1194
+
1195
+ /** Result of a CLI invocation. */
1196
+ export interface CliResult {
1197
+ exitCode: number;
1198
+ stdout: string;
1199
+ stderr: string;
1200
+ }
1201
+
1202
+ export function parseCliArgs(argv: string[]): CliArgs {
1203
+ const args = argv;
1204
+ let wbs: string | undefined;
1205
+ let feature: string | undefined;
1206
+ let taskStatus: string | undefined;
1207
+ let dryRun = false;
1208
+ let auto = false;
1209
+ let once = false;
1210
+ let full = false;
1211
+ let listStages = false;
1212
+ let help = false;
1213
+ for (let i = 0; i < args.length; i++) {
1214
+ const arg = args[i];
1215
+ if (arg == null) continue;
1216
+ if (arg === '--wbs') {
1217
+ wbs = args[++i];
1218
+ } else if (arg === '--feature') {
1219
+ feature = args[++i];
1220
+ } else if (arg === '--task-status') {
1221
+ taskStatus = args[++i];
1222
+ } else if (arg === '--dry-run') {
1223
+ dryRun = true;
1224
+ } else if (arg === '--auto') {
1225
+ auto = true;
1226
+ } else if (arg === '--once') {
1227
+ once = true;
1228
+ } else if (arg === '--full') {
1229
+ full = true;
1230
+ } else if (arg === '--list-stages') {
1231
+ listStages = true;
1232
+ } else if (arg === '--help') {
1233
+ help = true;
1234
+ }
1235
+ }
1236
+
1237
+ return { wbs, feature, taskStatus, dryRun, auto, once, full, listStages, help };
1238
+ }
1239
+
1240
+ /**
1241
+ * Format a resolved stage result as human-readable output lines.
1242
+ * Exported for testing — separates presentation from I/O.
1243
+ */
1244
+ export function formatStageResult(result: StageResolution): string[] {
1245
+ const lines = [
1246
+ `dev-next: ${result.reasonKind === 'dispatch' ? 'dispatch' : result.reasonKind}`,
1247
+ ` target: ${result.target} status=${result.status} table=${result.tableRow ?? '\u2014'}`,
1248
+ ` reason: ${result.reason}`,
1249
+ ];
1250
+ if (result.blocker) {
1251
+ lines.push(` blocker: ${result.blocker}`);
1252
+ }
1253
+ if (result.dispatchCommand) {
1254
+ lines.push(` dispatch: ${result.dispatchCommand}`);
1255
+ }
1256
+ if (result.chain) {
1257
+ lines.push(' chain: yes');
1258
+ }
1259
+ if (result.requiresConfirmation) {
1260
+ lines.push(' confirmation: required');
1261
+ }
1262
+ if (result.nextObservableOutcome) {
1263
+ lines.push(` next outcome: ${result.nextObservableOutcome}`);
1264
+ }
1265
+ return lines;
1266
+ }
1267
+
1268
+ export function runCli(argv: string[], opts?: { resolve?: (input: ResolutionInput) => StageResolution }): CliResult {
1269
+ const parsed = parseCliArgs(argv);
1270
+ const resolve = opts?.resolve ?? resolveStage;
1271
+
1272
+ if (parsed.help) {
1273
+ return { exitCode: 0, stdout: renderHelp(), stderr: '' };
1274
+ }
1275
+
1276
+ if (parsed.listStages) {
1277
+ const stagesList = listStages();
1278
+ const stageLines = stagesList.map((s) => `${s.stage_id}\t${s.command}\t${s.skill}`);
1279
+ return { exitCode: 0, stdout: `${stageLines.join('\n')}\n`, stderr: '' };
1280
+ }
1281
+
1282
+ if (!parsed.wbs && !parsed.feature) {
1283
+ return {
1284
+ exitCode: 1,
1285
+ stdout: '',
1286
+ stderr: `error: specify --wbs <wbs> or --feature <id>\n\n${renderHelp()}`,
1287
+ };
1288
+ }
1289
+
1290
+ // Build resolution input from CLI args (note: no live corpus access in CLI mode)
1291
+ const input: ResolutionInput = {
1292
+ target: parsed.wbs ?? parsed.feature ?? '',
1293
+ wbs: parsed.wbs,
1294
+ dryRun: parsed.dryRun,
1295
+ once: parsed.once,
1296
+ auto: parsed.auto,
1297
+ fullMode: parsed.full,
1298
+ task: parsed.wbs ? { wbs: parsed.wbs, status: parsed.taskStatus ?? 'unknown', dependencies: [] } : undefined,
1299
+ feature: parsed.feature ? { id: parsed.feature, status: 'active', tasks: [] } : undefined,
1300
+ };
1301
+
1302
+ try {
1303
+ const result = resolve(input);
1304
+ const lines = formatStageResult(result);
1305
+ return { exitCode: result.reasonKind === 'dispatch' ? 0 : 2, stdout: `${lines.join('\n')}\n`, stderr: '' };
1306
+ } catch (e) {
1307
+ return { exitCode: 1, stdout: '', stderr: `error: ${(e as Error).message}` };
1308
+ }
1309
+ }
1310
+
1311
+ /**
1312
+ * Entry-point boot — runs the CLI using process.argv. Extracted so tests can
1313
+ * call it directly without spawning a subprocess (which would not contribute
1314
+ * to the test isolate's V8 coverage counters).
1315
+ *
1316
+ * No `import.meta.main` guard here; the test calls this directly with
1317
+ * `process.exit` swapped for a spy. Production entry point is
1318
+ * `plugins/sp/scripts/main.ts` which calls this when `import.meta.main`.
1319
+ */
1320
+ export function bootMain(
1321
+ argv: string[] = process.argv,
1322
+ opts?: {
1323
+ run?: (a: string[]) => CliResult;
1324
+ exit?: (code: number) => void;
1325
+ stdout?: { write: (data: string) => void };
1326
+ stderr?: { write: (data: string) => void };
1327
+ },
1328
+ ): void {
1329
+ const cliRunner = opts?.run ?? runCli;
1330
+ const doExit = opts?.exit ?? process.exit;
1331
+ const stdout = opts?.stdout ?? process.stdout;
1332
+ const stderr = opts?.stderr ?? process.stderr;
1333
+ const result = cliRunner(argv);
1334
+ if (result.stdout) stdout.write(result.stdout);
1335
+ if (result.stderr) stderr.write(result.stderr);
1336
+ doExit(result.exitCode);
1337
+ }
1338
+
1339
+ if (import.meta.main) {
1340
+ bootMain();
1341
+ }