@gobing-ai/spur 0.3.40 → 0.3.42

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 +103 -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 +80 -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 +30 -0
  42. package/plugins/sp/commands/dev-wrapall.md +33 -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 +176 -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 +424 -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 +121 -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 +100 -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 +278 -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 +664 -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 +727 -0
  168. package/plugins/sp/skills/spur-dev/references/execution-workflow.md +350 -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 +88 -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 +6093 -5180
@@ -0,0 +1,363 @@
1
+ /**
2
+ * detect-pipeline-driving — @1.2 pipeline-driving testee detector (task 0277, W7).
3
+ *
4
+ * Pure function over the raw testee string → boolean. Used by the dogfood driver
5
+ * (Phase 1.0 refuse-ambiguous gate) and by unit tests so the matcher contract is
6
+ * machine-checked rather than agent-interpreted.
7
+ *
8
+ * Also ships a **CLI gate** (import.meta.main) so Phase 1.0 is a live shell call,
9
+ * not agent-only prose interpretation:
10
+ *
11
+ * bun plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts \
12
+ * --testee "/sp:dev-run 0125 --auto --next" [--max-retry-present] \
13
+ * [--steps "step1||step2"] [--json]
14
+ *
15
+ * Exit codes:
16
+ * 0 — proceed (may print implement-heavy advisory on stderr)
17
+ * 2 — refuse: pipeline-driving without explicit --max-retry
18
+ * 1 — usage / argument error
19
+ *
20
+ * Contract (0274 §3 dogfood-pipeline-detect, 0277 R1):
21
+ * - Word-boundary matchers with `-` treated as a word char (NOT a boundary).
22
+ * A token counts only when it is a distinct hyphen-word, not when it is a
23
+ * substring of a longer alphanumeric or hyphen run. This removes the
24
+ * leading-space dependency of the @1.1 prose detector.
25
+ * - Two token shapes, both with strict `[^\w-]` boundaries:
26
+ * (a) complete tokens — `--next`, `dev-run`, `dev-runall`, `dev-wrap`,
27
+ * `dev-wrapall`, `dev-idea` — matched as whole hyphen-words so the
28
+ * slash form `/sp:dev-run` matches on its `dev-run` tail whether or
29
+ * not it is preceded by a space, but `--next` does NOT match inside
30
+ * `--next-gen` (the trailing `-gen` breaks the boundary).
31
+ * (b) bare nouns — `run`, `runall`, `wrap`, `wrapall`, `idea` — matched
32
+ * only as standalone words, so `run` matches in
33
+ * `bun ... task run 0042` but NOT inside `runaway`/`prerun`, and
34
+ * `idea` does NOT match inside `idealist`/`ideal`.
35
+ * - Listing both shapes is what makes the detector leading-space invariant
36
+ * (the slash form is caught by its `dev-*` tail; the bare noun is caught
37
+ * when it appears as its own word) without letting `run` leak into every
38
+ * `-run-` identifier.
39
+ *
40
+ * Non-goals of detectPipelineDriving: decides ambiguity only. It does NOT decide
41
+ * observe vs fix — any explicit `--max-retry` value proceeds regardless.
42
+ * Implement-heavy advisory is a separate helper (detectImplementHeavy).
43
+ */
44
+
45
+ /**
46
+ * Tokens whose presence makes a testee pipeline-driving. Ordered for stable
47
+ * diagnostics. Two shapes per the contract above: complete `dev-*` / `--next`
48
+ * tokens first, then bare nouns.
49
+ */
50
+ const PIPELINE_TOKENS = [
51
+ '--next',
52
+ 'dev-runall',
53
+ 'dev-wrapall',
54
+ 'dev-run',
55
+ 'dev-wrap',
56
+ 'dev-idea',
57
+ 'runall',
58
+ 'wrapall',
59
+ 'run',
60
+ 'wrap',
61
+ 'idea',
62
+ ] as const;
63
+
64
+ /** Refuse message when pipeline-driving and `--max-retry` was not passed (exact string). */
65
+ export const PIPELINE_DRIVING_REFUSE_MESSAGE =
66
+ '⚠ pipeline-driving testee detected; pass --max-retry 0 (observe-only) or --max-retry N (fix mode, tree mutation acknowledged)';
67
+
68
+ /**
69
+ * Refuse message when the testee carries a mutating `--fix` mode (`--fix all` /
70
+ * `--fix blockers-first`) and `--max-retry` was not passed. A mutating fix mode
71
+ * mutates the working tree independent of pipeline-driving (task 0293 R1/R2):
72
+ * `--max-retry 0` bounds the **driver** only — the testee's own `--fix` pass
73
+ * still mutates the tree — so the message must not imply otherwise.
74
+ */
75
+ export const MUTATING_FIX_REFUSE_MESSAGE =
76
+ '⚠ mutating --fix mode detected (--fix all | --fix blockers-first); pass --max-retry 0 (observe-only for the driver; the testee still mutates the tree) or --max-retry N (fix mode, driver + testee both mutate)';
77
+
78
+ /** Advisory when pipeline-driving + implement-heavy derived steps (exact string). */
79
+ export const IMPLEMENT_HEAVY_ADVISORY_MESSAGE =
80
+ '⚠ implement-heavy pipeline dogfood: prefer --max-retry 0 (observe-only) or step-split; operator --max-retry N overrides';
81
+
82
+ /**
83
+ * Tokens that mark a derived step as implement-heavy (mutates product/code or
84
+ * drives a mutating pipeline leg). Deliberately excludes verify/review/unit-only
85
+ * surfaces even when they carry `--next`.
86
+ */
87
+ const IMPLEMENT_HEAVY_TOKENS = [
88
+ 'dev-runall',
89
+ 'dev-wrapall',
90
+ 'dev-run',
91
+ 'dev-wrap',
92
+ 'dev-idea',
93
+ 'runall',
94
+ 'wrapall',
95
+ // bare `run` / `wrap` / `idea` — only when not clearly a non-mutating surface
96
+ 'run',
97
+ 'wrap',
98
+ 'idea',
99
+ 'implement',
100
+ ] as const;
101
+
102
+ /**
103
+ * A verify/review surface stops being non-mutating the moment it carries a
104
+ * repair mode: `--fix all` / `--fix blockers-first` applies Edit/Write repairs
105
+ * to the working tree (0280 dogfood, finding P2). `--fix none` stays
106
+ * observational. Boundary-guarded so `--prefix all` / `--focus all` never match.
107
+ * Exported so the Phase 1.0 gate and tests share one matcher (task 0293 R1/R4).
108
+ */
109
+ export function hasMutatingFixMode(step: string): boolean {
110
+ return /(?<![\w-])--fix[=\s]+(all|blockers-first)(?![\w-])/i.test(step);
111
+ }
112
+
113
+ /**
114
+ * Match a token at a word boundary. A "word boundary" here is the position
115
+ * between a non-`[\w-]` char (or string start) and the token, and between the
116
+ * token and a non-`[\w-]` char (or string end). Treating `-` as a word char
117
+ * is what makes `--next` reject `--next-gen` and `dev-run` reject `dev-runner`.
118
+ */
119
+ function tokenMatches(testee: string, token: string): boolean {
120
+ const escaped = token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
121
+ // (?<![\w-]) — not preceded by a word char or hyphen.
122
+ // (?![\w-]) — not followed by a word char or hyphen.
123
+ const re = new RegExp(`(?<![\\w-])${escaped}(?![\\w-])`, 'i');
124
+ return re.test(testee);
125
+ }
126
+
127
+ /**
128
+ * Returns whether the testee string is pipeline-driving (contains any of the
129
+ * pipeline-driving tokens as a distinct hyphen-word). Machine-checked
130
+ * counterpart of the prose list in SKILL.md §Pipeline-driving detection and
131
+ * dev-dogfood.md.
132
+ */
133
+ export function detectPipelineDriving(testee: string): boolean {
134
+ if (typeof testee !== 'string' || testee.length === 0) {
135
+ return false;
136
+ }
137
+ return PIPELINE_TOKENS.some((token) => tokenMatches(testee, token));
138
+ }
139
+
140
+ /**
141
+ * True when a single step label (or the whole testee) is implement-heavy —
142
+ * it chains into real implementation / wrap / idea work, not verify-only.
143
+ *
144
+ * Non-mutating surfaces (`dev-verify`, `dev-review`, `dev-unit`, plain
145
+ * `dev-refine` without a further run) are never implement-heavy even if
146
+ * pipeline-driving via `--next` — UNLESS they carry a mutating repair mode
147
+ * (`--fix all` / `--fix blockers-first`), which turns the verify/review leg
148
+ * into a tree-mutating fix pass (0280 dogfood, finding P2).
149
+ */
150
+ export function isImplementHeavyStep(step: string): boolean {
151
+ if (typeof step !== 'string' || step.length === 0) return false;
152
+ // Explicit non-mutating surfaces win unless a mutating token co-occurs.
153
+ const nonMutatingOnly =
154
+ tokenMatches(step, 'dev-verify') || tokenMatches(step, 'dev-review') || tokenMatches(step, 'dev-unit');
155
+ const hasMutating = IMPLEMENT_HEAVY_TOKENS.some((token) => tokenMatches(step, token)) || hasMutatingFixMode(step);
156
+ if (nonMutatingOnly && !hasMutating) return false;
157
+ // refine alone is planning, not implement-heavy; refine+run/--next chain is.
158
+ if (tokenMatches(step, 'dev-refine') && !hasMutating && !tokenMatches(step, '--next')) {
159
+ return false;
160
+ }
161
+ if (tokenMatches(step, 'dev-refine') && tokenMatches(step, '--next')) {
162
+ // refine --next chains into run → implement-heavy
163
+ return true;
164
+ }
165
+ return hasMutating;
166
+ }
167
+
168
+ /**
169
+ * True when the testee is implement-heavy: it carries a pipeline-driving token,
170
+ * a mutating `--fix` mode, OR at least one derived step (or the testee itself
171
+ * when no steps given) is implement-heavy. Used for the W8 Phase 1 advisory
172
+ * after step derivation.
173
+ *
174
+ * Task 0293 R3: a mutating `--fix` mode alone is implement-heavy even with no
175
+ * pipeline token — the verify/review leg itself becomes a tree-mutating fix
176
+ * pass.
177
+ */
178
+ export function detectImplementHeavy(testee: string, derivedSteps: string[] = []): boolean {
179
+ if (isImplementHeavyStep(testee)) return true;
180
+ if (!detectPipelineDriving(testee)) return false;
181
+ return derivedSteps.some((step) => isImplementHeavyStep(step));
182
+ }
183
+
184
+ export interface GateResult {
185
+ pipelineDriving: boolean;
186
+ mutatingFix: boolean;
187
+ maxRetryPresent: boolean;
188
+ implementHeavy: boolean;
189
+ refuse: boolean;
190
+ advisory: boolean;
191
+ message: string | null;
192
+ exitCode: 0 | 1 | 2;
193
+ }
194
+
195
+ /**
196
+ * Phase 1.0 + W8 gate decision. Pure — no I/O. The CLI wrapper prints and exits.
197
+ *
198
+ * Refuse condition (task 0293 R1) is the union of two **independent** mutation
199
+ * sources, each gated on `!maxRetryPresent`:
200
+ *
201
+ * 1. pipeline-driving testee (`--next`, `dev-run`, …) — refuses because the
202
+ * testee chains lifecycle legs whose cumulative blast radius the driver
203
+ * cannot pre-attribute.
204
+ * 2. mutating `--fix` mode (`--fix all` / `--fix blockers-first`) — refuses
205
+ * because the testee itself applies Edit/Write repairs even with no
206
+ * pipeline token. `--max-retry 0` bounds the **driver** only; the testee
207
+ * still mutates (honesty note in MUTATING_FIX_REFUSE_MESSAGE).
208
+ *
209
+ * Pipeline-driving is reported first when both co-occur (its refuse message is
210
+ * the superset — chain + tree mutation).
211
+ */
212
+ export function evaluateDogfoodGate(
213
+ testee: string,
214
+ options: { maxRetryPresent?: boolean; steps?: string[] } = {},
215
+ ): GateResult {
216
+ const maxRetryPresent = options.maxRetryPresent === true;
217
+ const steps = options.steps ?? [];
218
+ const pipelineDriving = detectPipelineDriving(testee);
219
+ const mutatingFix = hasMutatingFixMode(testee);
220
+ const implementHeavy = detectImplementHeavy(testee, steps);
221
+
222
+ if (pipelineDriving && !maxRetryPresent) {
223
+ return {
224
+ pipelineDriving,
225
+ mutatingFix,
226
+ maxRetryPresent,
227
+ implementHeavy,
228
+ refuse: true,
229
+ advisory: false,
230
+ message: PIPELINE_DRIVING_REFUSE_MESSAGE,
231
+ exitCode: 2,
232
+ };
233
+ }
234
+
235
+ if (mutatingFix && !maxRetryPresent) {
236
+ return {
237
+ pipelineDriving,
238
+ mutatingFix,
239
+ maxRetryPresent,
240
+ implementHeavy,
241
+ refuse: true,
242
+ advisory: false,
243
+ message: MUTATING_FIX_REFUSE_MESSAGE,
244
+ exitCode: 2,
245
+ };
246
+ }
247
+
248
+ if (implementHeavy) {
249
+ return {
250
+ pipelineDriving,
251
+ mutatingFix,
252
+ maxRetryPresent,
253
+ implementHeavy,
254
+ refuse: false,
255
+ advisory: true,
256
+ message: IMPLEMENT_HEAVY_ADVISORY_MESSAGE,
257
+ exitCode: 0,
258
+ };
259
+ }
260
+
261
+ return {
262
+ pipelineDriving,
263
+ mutatingFix,
264
+ maxRetryPresent,
265
+ implementHeavy,
266
+ refuse: false,
267
+ advisory: false,
268
+ message: null,
269
+ exitCode: 0,
270
+ };
271
+ }
272
+
273
+ export { PIPELINE_TOKENS };
274
+
275
+ // ── CLI entry (live Phase 1.0 gate) ──────────────────────────────────────────
276
+
277
+ export interface CliArgs {
278
+ testee: string | null;
279
+ maxRetryPresent: boolean;
280
+ steps: string[];
281
+ json: boolean;
282
+ help: boolean;
283
+ }
284
+
285
+ /** Parse CLI argv (everything after the script name). Exported for unit tests. */
286
+ export function parseCliArgs(argv: string[]): CliArgs {
287
+ let testee: string | null = null;
288
+ let maxRetryPresent = false;
289
+ let steps: string[] = [];
290
+ let json = false;
291
+ let help = false;
292
+
293
+ for (let i = 0; i < argv.length; i++) {
294
+ const a = argv[i];
295
+ if (a === '--help' || a === '-h') {
296
+ help = true;
297
+ } else if (a === '--json') {
298
+ json = true;
299
+ } else if (a === '--max-retry-present') {
300
+ maxRetryPresent = true;
301
+ } else if (a === '--testee') {
302
+ testee = argv[++i] ?? null;
303
+ } else if (a === '--steps') {
304
+ const raw = argv[++i] ?? '';
305
+ steps =
306
+ raw.length === 0
307
+ ? []
308
+ : raw
309
+ .split('||')
310
+ .map((s) => s.trim())
311
+ .filter(Boolean);
312
+ } else if (a === '--') {
313
+ testee = argv.slice(i + 1).join(' ');
314
+ break;
315
+ } else if (!a.startsWith('-') && testee === null) {
316
+ testee = a;
317
+ }
318
+ }
319
+
320
+ return { testee, maxRetryPresent, steps, json, help };
321
+ }
322
+
323
+ export const CLI_USAGE = `Usage:
324
+ bun plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts \\
325
+ --testee "<testee string>" [--max-retry-present] [--steps "s1||s2"] [--json]
326
+
327
+ Exit codes:
328
+ 0 proceed (stdout may carry implement-heavy advisory)
329
+ 2 refuse — pipeline-driving OR mutating --fix mode without --max-retry
330
+ 1 usage error
331
+
332
+ Phase 1.0: run BEFORE deriving steps (omit --steps).
333
+ Phase 1 W8: re-run AFTER step derivation with --steps "label1||label2".`;
334
+
335
+ /**
336
+ * Pure CLI runner for tests: returns { exitCode, stdout, stderr } without
337
+ * process.exit / console I/O side effects.
338
+ */
339
+ export function runCli(argv: string[]): { exitCode: number; stdout: string; stderr: string } {
340
+ const { testee, maxRetryPresent, steps, json, help } = parseCliArgs(argv);
341
+ if (help) {
342
+ return { exitCode: 0, stdout: '', stderr: CLI_USAGE };
343
+ }
344
+ if (testee === null || testee.length === 0) {
345
+ return { exitCode: 1, stdout: '', stderr: CLI_USAGE };
346
+ }
347
+
348
+ const result = evaluateDogfoodGate(testee, { maxRetryPresent, steps });
349
+ if (json) {
350
+ return { exitCode: result.exitCode, stdout: `${JSON.stringify(result, null, 2)}\n`, stderr: '' };
351
+ }
352
+ if (result.message) {
353
+ return { exitCode: result.exitCode, stdout: `${result.message}\n`, stderr: '' };
354
+ }
355
+ return { exitCode: result.exitCode, stdout: '', stderr: '' };
356
+ }
357
+
358
+ if (import.meta.main) {
359
+ const { exitCode, stdout, stderr } = runCli(Bun.argv.slice(2));
360
+ if (stdout) process.stdout.write(stdout);
361
+ if (stderr) process.stderr.write(`${stderr}\n`);
362
+ process.exit(exitCode);
363
+ }
@@ -0,0 +1,168 @@
1
+ /**
2
+ * validate-report — @1.2 dogfood report contract checker (task 0276, W6).
3
+ *
4
+ * Pure function over a report markdown string → { ok, errors[] } with stable error
5
+ * codes, callable from tests and by agents finalizing a run. It validates the
6
+ * *complete-report* shape: the six unique section headings, Issues subheads, the
7
+ * mandatory summary footer with both delivery paths, frontmatter protocol string,
8
+ * and ledger↔declared-steps cardinality. Aborted/partial reports are out of scope —
9
+ * they legitimately lack the footer and Steps line.
10
+ *
11
+ * CLI (task 0278 R6): `bun …/validate-report.ts --file <report.md> [--json]`
12
+ */
13
+
14
+ import { readFileSync } from 'node:fs';
15
+
16
+ export interface ReportValidation {
17
+ ok: boolean;
18
+ errors: string[];
19
+ }
20
+
21
+ const REQUIRED_SECTIONS = [1, 2, 3, 4, 5, 6] as const;
22
+ const CANONICAL_PROTOCOL = 'sp:dogfood-testing@1.2';
23
+
24
+ function countSectionHeadings(markdown: string, section: number): number {
25
+ const re = new RegExp(`^### ${section}\\.`, 'gm');
26
+ return (markdown.match(re) ?? []).length;
27
+ }
28
+
29
+ function countLedgerDataRows(markdown: string): number | null {
30
+ const heading = markdown.match(/^### 3\. Monitor Ledger\s*$/m);
31
+ if (!heading || heading.index === undefined) return null;
32
+ const after = markdown.slice(heading.index + heading[0].length);
33
+ const nextHeading = after.search(/^### /m);
34
+ const body = nextHeading === -1 ? after : after.slice(0, nextHeading);
35
+ const rows = body
36
+ .split('\n')
37
+ .filter((line) => line.trim().startsWith('|'))
38
+ .filter((line) => !/^\|[\s:|-]+\|?\s*$/.test(line.trim()))
39
+ // drift:external rows are documentary (task 0296) — included in the table but
40
+ // subtracted from the executed-step count, per the @1.2 cardinality contract.
41
+ .filter((line) => !/^\|\s*drift:/.test(line.trim()));
42
+ // Minus the header row; what remains are data rows.
43
+ return Math.max(rows.length - 1, 0);
44
+ }
45
+
46
+ function declaredExecutedSteps(markdown: string): number | null {
47
+ const match = markdown.match(/\*\*Steps:\*\*\s*\d+\s+derived,\s*(\d+)\s+executed/);
48
+ return match ? Number.parseInt(match[1], 10) : null;
49
+ }
50
+
51
+ export function validateReport(markdown: string): ReportValidation {
52
+ const errors: string[] = [];
53
+
54
+ // Footer (W2/D1) — the mirrored footer block at the report end.
55
+ if (!markdown.includes('── Dogfood Summary ──')) errors.push('missing_footer');
56
+ if (!markdown.includes('[Live:')) errors.push('missing_live_path');
57
+ if (!markdown.includes('[Report:')) errors.push('missing_report_path');
58
+
59
+ // Six unique section headings (W2/D3).
60
+ for (const section of REQUIRED_SECTIONS) {
61
+ const count = countSectionHeadings(markdown, section);
62
+ if (count === 0) errors.push(`missing_section:${section}`);
63
+ if (count > 1) errors.push(`duplicate_section:${section}`);
64
+ }
65
+
66
+ // Issues subheads (W2/D3).
67
+ if (!markdown.includes('#### Fixed') || !markdown.includes('#### Unresolved')) {
68
+ errors.push('missing_issues_subheads');
69
+ }
70
+
71
+ // Protocol string (W1/D5) — colon form, exact version.
72
+ const protocolMatch = markdown.match(/^protocol:\s*(\S+)\s*$/m);
73
+ if (!protocolMatch || protocolMatch[1] !== CANONICAL_PROTOCOL) {
74
+ errors.push('protocol_string');
75
+ }
76
+
77
+ // Ledger cardinality (W3/D4) — data rows == declared executed steps.
78
+ const executed = declaredExecutedSteps(markdown);
79
+ if (executed === null) {
80
+ errors.push('missing_steps_declared');
81
+ } else {
82
+ const rows = countLedgerDataRows(markdown);
83
+ if (rows === null || rows !== executed) {
84
+ errors.push('ledger_cardinality');
85
+ }
86
+ }
87
+
88
+ return { ok: errors.length === 0, errors };
89
+ }
90
+
91
+ // ── CLI entry (Phase 4 self-validate — task 0278 R6) ─────────────────────────
92
+
93
+ export interface ValidateCliArgs {
94
+ file: string | null;
95
+ json: boolean;
96
+ help: boolean;
97
+ }
98
+
99
+ export function parseValidateCliArgs(argv: string[]): ValidateCliArgs {
100
+ let file: string | null = null;
101
+ let json = false;
102
+ let help = false;
103
+ for (let i = 0; i < argv.length; i++) {
104
+ const a = argv[i];
105
+ if (a === '--help' || a === '-h') help = true;
106
+ else if (a === '--json') json = true;
107
+ else if (a === '--file') file = argv[++i] ?? null;
108
+ else if (!a.startsWith('-') && file === null) file = a;
109
+ }
110
+ return { file, json, help };
111
+ }
112
+
113
+ export const VALIDATE_CLI_USAGE = `Usage:
114
+ bun plugins/sp/scripts/dogfood-testing/validate-report.ts --file <report.md> [--json]
115
+
116
+ Exit codes:
117
+ 0 report validates clean (complete-report shape)
118
+ 2 validation failed (errors on stdout / --json)
119
+ 1 usage error
120
+
121
+ Phase 4 finalize MUST run this before status: complete (task 0278 R6).
122
+ On exit 2: set status: aborted and list error codes under #### Unresolved.`;
123
+
124
+ export function runValidateCli(
125
+ argv: string[],
126
+ readFile: (path: string) => string,
127
+ ): { exitCode: number; stdout: string; stderr: string } {
128
+ const { file, json, help } = parseValidateCliArgs(argv);
129
+ if (help) return { exitCode: 0, stdout: '', stderr: VALIDATE_CLI_USAGE };
130
+ if (file === null || file.length === 0) {
131
+ return { exitCode: 1, stdout: '', stderr: VALIDATE_CLI_USAGE };
132
+ }
133
+ let markdown: string;
134
+ try {
135
+ markdown = readFile(file);
136
+ } catch (err) {
137
+ const msg = err instanceof Error ? err.message : String(err);
138
+ return { exitCode: 1, stdout: '', stderr: `Failed to read ${file}: ${msg}` };
139
+ }
140
+ const result = validateReport(markdown);
141
+ if (json) {
142
+ return {
143
+ exitCode: result.ok ? 0 : 2,
144
+ stdout: `${JSON.stringify(result, null, 2)}\n`,
145
+ stderr: '',
146
+ };
147
+ }
148
+ if (result.ok) {
149
+ return { exitCode: 0, stdout: 'ok\n', stderr: '' };
150
+ }
151
+ return {
152
+ exitCode: 2,
153
+ stdout: `${result.errors.join('\n')}\n`,
154
+ stderr: '',
155
+ };
156
+ }
157
+
158
+ /** CLI entry for Phase 4 self-validate (import.meta.main). */
159
+ export function mainCli(argv: string[] = Bun.argv.slice(2)): number {
160
+ const { exitCode, stdout, stderr } = runValidateCli(argv, (p) => readFileSync(p, 'utf8'));
161
+ if (stdout) process.stdout.write(stdout);
162
+ if (stderr) process.stderr.write(`${stderr}\n`);
163
+ return exitCode;
164
+ }
165
+
166
+ if (import.meta.main) {
167
+ process.exit(mainCli());
168
+ }