@gobing-ai/spur 0.3.52 → 0.3.54

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 (94) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/corpus-baseline.json +2881 -2881
  3. package/config/pipeline-budgets.json +42 -0
  4. package/config/plugin-scripts.json +73 -0
  5. package/config/workflow-composition-baseline.json +561 -0
  6. package/config/workflows/docs-pipeline.yaml +29 -13
  7. package/config/workflows/feature-dev.yaml +54 -2
  8. package/config/workflows/idea-pipeline.yaml +15 -0
  9. package/config/workflows/task-pipeline.yaml +77 -65
  10. package/config/workflows/wrapup-pipeline.yaml +40 -45
  11. package/package.json +9 -9
  12. package/plugins/README.md +1 -1
  13. package/plugins/sp/README.md +3 -3
  14. package/plugins/sp/agents/super-planner.md +1 -1
  15. package/plugins/sp/commands/dev-daily.md +1 -1
  16. package/plugins/sp/commands/dev-history-load.md +1 -1
  17. package/plugins/sp/plugin.json +1 -1
  18. package/plugins/sp/scripts/batch-preflight.mjs +160 -0
  19. package/plugins/sp/scripts/batch-preflight.ts +1 -1
  20. package/plugins/sp/scripts/daily-summary/daily-summary.mjs +479 -0
  21. package/plugins/sp/scripts/daily-summary/daily-summary.ts +75 -37
  22. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.mjs +204 -0
  23. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts +1 -1
  24. package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +141 -0
  25. package/plugins/sp/scripts/dogfood-testing/validate-report.ts +1 -1
  26. package/plugins/sp/scripts/feature-sync-bounded.mjs +290 -0
  27. package/plugins/sp/scripts/feature-sync-bounded.ts +9 -8
  28. package/plugins/sp/scripts/history-load.mjs +268 -0
  29. package/plugins/sp/scripts/pr-reviewing.mjs +685 -0
  30. package/plugins/sp/scripts/pr-reviewing.ts +1 -1
  31. package/plugins/sp/scripts/script-contract-check.ts +319 -0
  32. package/plugins/sp/skills/daily-summary/SKILL.md +3 -3
  33. package/plugins/sp/skills/dogfood-testing/SKILL.md +5 -5
  34. package/plugins/sp/skills/dogfood-testing/references/report-template.md +1 -1
  35. package/plugins/sp/skills/pr-reviewing/SKILL.md +3 -3
  36. package/plugins/sp/skills/spur-dev/SKILL.md +2 -1
  37. package/plugins/sp/skills/spur-dev/references/cross-cutting.md +3 -4
  38. package/plugins/sp/skills/spur-dev/references/execution-batch.md +3 -3
  39. package/plugins/sp/skills/spur-dev/references/gate-checklists.md +4 -4
  40. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +10 -1
  41. package/spur.js +4274 -1326
  42. package/web/_astro/{BoardApp.plVXYkKo.js → BoardApp.DCLSB3Zs.js} +63 -63
  43. package/web/_astro/BoardApp.Dx5gzAhb.js +1 -0
  44. package/web/_astro/{TaskDetail.5DIyVQ_N.js → TaskDetail.DN_RxS-2.js} +1 -1
  45. package/web/_astro/{arc.BnUgL7ho.js → arc.CldTHRg-.js} +1 -1
  46. package/web/_astro/{architectureDiagram-3BPJPVTR.B9YMJau-.js → architectureDiagram-3BPJPVTR.DQ4T9oeU.js} +1 -1
  47. package/web/_astro/{blockDiagram-GPEHLZMM.Dw2qgvWJ.js → blockDiagram-GPEHLZMM.CGDaXH8J.js} +1 -1
  48. package/web/_astro/{c4Diagram-AAUBKEIU.jTbU0-Nk.js → c4Diagram-AAUBKEIU.Cqsc2iub.js} +1 -1
  49. package/web/_astro/channel.CrBJYpxo.js +1 -0
  50. package/web/_astro/{chunk-2J33WTMH.Di46EtkU.js → chunk-2J33WTMH.CQSKsRmq.js} +1 -1
  51. package/web/_astro/{chunk-4BX2VUAB.DweHBwqQ.js → chunk-4BX2VUAB.Pv1HCtp8.js} +1 -1
  52. package/web/_astro/{chunk-55IACEB6.rRjTKaa7.js → chunk-55IACEB6.-9MseOIk.js} +1 -1
  53. package/web/_astro/{chunk-727SXJPM.BkEQIkSV.js → chunk-727SXJPM.M3OI1DF7.js} +1 -1
  54. package/web/_astro/{chunk-AQP2D5EJ.Cfe9IvAd.js → chunk-AQP2D5EJ.NfHka5Ca.js} +1 -1
  55. package/web/_astro/{chunk-FMBD7UC4.fI6tLbeN.js → chunk-FMBD7UC4.B2suYe3s.js} +1 -1
  56. package/web/_astro/{chunk-ND2GUHAM.BIjgpq0O.js → chunk-ND2GUHAM.BfJ7aucP.js} +1 -1
  57. package/web/_astro/{chunk-QZHKN3VN.ZwLnVxyz.js → chunk-QZHKN3VN._582hZVc.js} +1 -1
  58. package/web/_astro/{classDiagram-4FO5ZUOK.BJAGYidE.js → classDiagram-4FO5ZUOK._tttU_jk.js} +1 -1
  59. package/web/_astro/{classDiagram-v2-Q7XG4LA2.BJAGYidE.js → classDiagram-v2-Q7XG4LA2._tttU_jk.js} +1 -1
  60. package/web/_astro/{cose-bilkent-S5V4N54A.B5y3nz26.js → cose-bilkent-S5V4N54A.C4rVATLQ.js} +1 -1
  61. package/web/_astro/{dagre-BM42HDAG.DHmw0ZBC.js → dagre-BM42HDAG.8BLRi7f9.js} +1 -1
  62. package/web/_astro/{diagram-2AECGRRQ.10gapJfY.js → diagram-2AECGRRQ.D6lwUZos.js} +1 -1
  63. package/web/_astro/{diagram-5GNKFQAL.Gh33_drM.js → diagram-5GNKFQAL.Cl_rMiVT.js} +1 -1
  64. package/web/_astro/{diagram-KO2AKTUF.8ROWlfcX.js → diagram-KO2AKTUF.DhMExHLp.js} +1 -1
  65. package/web/_astro/{diagram-LMA3HP47.BsP3tKmg.js → diagram-LMA3HP47.3OAxzH_o.js} +1 -1
  66. package/web/_astro/{diagram-OG6HWLK6.DM2WFDhU.js → diagram-OG6HWLK6.Uh9RLucG.js} +1 -1
  67. package/web/_astro/{erDiagram-TEJ5UH35.royaC_lH.js → erDiagram-TEJ5UH35.izzElG0c.js} +1 -1
  68. package/web/_astro/{flowDiagram-I6XJVG4X.C9GEvIie.js → flowDiagram-I6XJVG4X.BlGwp5qM.js} +1 -1
  69. package/web/_astro/{ganttDiagram-6RSMTGT7.CXO4T2U0.js → ganttDiagram-6RSMTGT7.je7Vf8dN.js} +1 -1
  70. package/web/_astro/{gitGraphDiagram-PVQCEYII.Q02KzYH7.js → gitGraphDiagram-PVQCEYII.DNk0Ycop.js} +1 -1
  71. package/web/_astro/{index.DAxu50UF.css → index.V6Q7nhed.css} +1 -1
  72. package/web/_astro/{infoDiagram-5YYISTIA.DNfSDKJt.js → infoDiagram-5YYISTIA.K2HhGyWH.js} +1 -1
  73. package/web/_astro/{ishikawaDiagram-YF4QCWOH.BFttvtgJ.js → ishikawaDiagram-YF4QCWOH.BbbOW4IN.js} +1 -1
  74. package/web/_astro/{journeyDiagram-JHISSGLW.Cfm-2SbG.js → journeyDiagram-JHISSGLW.B1UaZKqN.js} +1 -1
  75. package/web/_astro/{kanban-definition-UN3LZRKU.BWGNW6fV.js → kanban-definition-UN3LZRKU.9tsw5QFd.js} +1 -1
  76. package/web/_astro/{linear.BprdUk2Y.js → linear.I9dvtu-j.js} +1 -1
  77. package/web/_astro/{mermaid.core.B2zRNxtX.js → mermaid.core.5bKJsMgf.js} +4 -4
  78. package/web/_astro/{mindmap-definition-RKZ34NQL.g9jij8sw.js → mindmap-definition-RKZ34NQL.BC5MqSn6.js} +1 -1
  79. package/web/_astro/{pieDiagram-4H26LBE5.CbzR4n5b.js → pieDiagram-4H26LBE5.B8GIMyxD.js} +1 -1
  80. package/web/_astro/{quadrantDiagram-W4KKPZXB.DxV3viWt.js → quadrantDiagram-W4KKPZXB.K_aZSM_u.js} +1 -1
  81. package/web/_astro/{requirementDiagram-4Y6WPE33.DvAb90JS.js → requirementDiagram-4Y6WPE33.B_V5kzfT.js} +1 -1
  82. package/web/_astro/{sankeyDiagram-5OEKKPKP.CIFz5t4D.js → sankeyDiagram-5OEKKPKP.DyV2quLS.js} +1 -1
  83. package/web/_astro/{sequenceDiagram-3UESZ5HK.DzlEQynd.js → sequenceDiagram-3UESZ5HK.BlKvPnBv.js} +1 -1
  84. package/web/_astro/{stateDiagram-AJRCARHV.Dph2V-ts.js → stateDiagram-AJRCARHV.C1lYUMdS.js} +1 -1
  85. package/web/_astro/{stateDiagram-v2-BHNVJYJU.wJhfrLAy.js → stateDiagram-v2-BHNVJYJU.DJWjr3QV.js} +1 -1
  86. package/web/_astro/{timeline-definition-PNZ67QCA.BrSiyubX.js → timeline-definition-PNZ67QCA.CYYiXVlv.js} +1 -1
  87. package/web/_astro/{vennDiagram-CIIHVFJN.COobPrUX.js → vennDiagram-CIIHVFJN.zYjQKa_S.js} +1 -1
  88. package/web/_astro/{wardley-L42UT6IY.CrjeTx8A.js → wardley-L42UT6IY.BGrWTY3D.js} +1 -1
  89. package/web/_astro/{wardleyDiagram-YWT4CUSO.B8_fTQ8E.js → wardleyDiagram-YWT4CUSO.CkXxtcI1.js} +1 -1
  90. package/web/_astro/{xychartDiagram-2RQKCTM6.BPpokgVv.js → xychartDiagram-2RQKCTM6.BhKH3KG3.js} +1 -1
  91. package/web/index.html +2 -2
  92. package/config/workflows/planning-pipeline.yaml +0 -249
  93. package/web/_astro/BoardApp.pdtYu-yW.js +0 -1
  94. package/web/_astro/channel.DEqvaz-I.js +0 -1
@@ -8,6 +8,7 @@
8
8
  * - User annotations (learnings, issues, pending)
9
9
  */
10
10
 
11
+ import { spawn, spawnSync } from 'node:child_process';
11
12
  import { existsSync, mkdirSync, readlinkSync, writeFileSync } from 'node:fs';
12
13
  import { dirname, isAbsolute, join, resolve } from 'node:path';
13
14
  import { logger } from './logger';
@@ -149,26 +150,28 @@ Examples:
149
150
 
150
151
  // ─── Date Helpers ─────────────────────────────────────────────────────────────
151
152
 
153
+ // ─── Date Helpers ─────────────────────────────────────────────────────────────
154
+
152
155
  /** Return today's date as YYYY-MM-DD in the system (git) timezone. */
153
156
  export function todayLocal(): string {
154
157
  // Spawn 'date' to get system timezone date, since JS runtime
155
158
  // may have a different TZ (e.g. bun test forces UTC).
156
- const proc = Bun.spawnSync(['date', '+%Y-%m-%d']);
157
- return proc.stdout.toString().trim();
159
+ const proc = spawnSync('date', ['+%Y-%m-%d'], { encoding: 'utf8' });
160
+ return (proc.stdout ?? '').trim();
158
161
  }
159
162
 
160
163
  /** Return yesterday's date as YYYY-MM-DD in the system (git) timezone. */
161
164
  export function yesterdayLocal(): string {
162
165
  // Use portable epoch math via date command
163
- const epochProc = Bun.spawnSync(['date', '+%s']);
164
- const epoch = parseInt(epochProc.stdout.toString().trim(), 10);
166
+ const epochProc = spawnSync('date', ['+%s'], { encoding: 'utf8' });
167
+ const epoch = parseInt((epochProc.stdout ?? '').trim(), 10);
165
168
  const yesterdayEpoch = epoch - 86400;
166
169
  // Try BSD -r first, then GNU -d @
167
- let proc = Bun.spawnSync(['date', '-r', String(yesterdayEpoch), '+%Y-%m-%d']);
168
- if (proc.exitCode !== 0) {
169
- proc = Bun.spawnSync(['date', '-d', `@${yesterdayEpoch}`, '+%Y-%m-%d']);
170
+ let proc = spawnSync('date', ['-r', String(yesterdayEpoch), '+%Y-%m-%d'], { encoding: 'utf8' });
171
+ if ((proc.status ?? 1) !== 0) {
172
+ proc = spawnSync('date', ['-d', `@${yesterdayEpoch}`, '+%Y-%m-%d'], { encoding: 'utf8' });
170
173
  }
171
- return proc.stdout.toString().trim();
174
+ return (proc.stdout ?? '').trim();
172
175
  }
173
176
 
174
177
  export function getDateRange(dateStr: string): { start: string; end: string } {
@@ -181,40 +184,72 @@ export function getDateRange(dateStr: string): { start: string; end: string } {
181
184
  return { start, end };
182
185
  }
183
186
 
187
+ // ─── Subprocess Spawner ──────────────────────────────────────────────────────
188
+
189
+ export interface ProcessSpawnResult {
190
+ stdout: string;
191
+ stderr: string;
192
+ exitCode: number;
193
+ }
194
+
195
+ export type ProcessSpawner = (cmd: string, args: string[], env?: NodeJS.ProcessEnv) => Promise<ProcessSpawnResult>;
196
+
197
+ export const defaultProcessSpawner: ProcessSpawner = (cmd, args, env) => {
198
+ return new Promise((resolve, reject) => {
199
+ try {
200
+ const proc = spawn(cmd, args, {
201
+ stdio: ['ignore', 'pipe', 'pipe'],
202
+ env: env ?? process.env,
203
+ });
204
+ const stdoutChunks: Buffer[] = [];
205
+ const stderrChunks: Buffer[] = [];
206
+ proc.stdout.on('data', (chunk) => stdoutChunks.push(Buffer.from(chunk)));
207
+ proc.stderr.on('data', (chunk) => stderrChunks.push(Buffer.from(chunk)));
208
+ proc.on('close', (exitCode) => {
209
+ resolve({
210
+ stdout: Buffer.concat(stdoutChunks).toString('utf8'),
211
+ stderr: Buffer.concat(stderrChunks).toString('utf8'),
212
+ exitCode: exitCode ?? 0,
213
+ });
214
+ });
215
+ proc.on('error', (err) => {
216
+ reject(err);
217
+ });
218
+ } catch (err) {
219
+ reject(err);
220
+ }
221
+ });
222
+ };
223
+
224
+ let processSpawner: ProcessSpawner = defaultProcessSpawner;
225
+
226
+ export function setProcessSpawner(next?: ProcessSpawner): void {
227
+ processSpawner = next ?? defaultProcessSpawner;
228
+ }
229
+
184
230
  // ─── Ccusage Integration ─────────────────────────────────────────────────────
185
231
 
186
232
  export async function getCcusageData(date: string): Promise<CcusageData | null> {
187
233
  try {
188
234
  // Check if ccusage is available
189
235
  const env = { ...process.env };
190
- const ccusageCheck = Bun.spawn(['ccusage', '--version'], {
191
- stdout: 'pipe',
192
- stderr: 'pipe',
193
- env,
194
- });
195
- await new Response(ccusageCheck.stdout).text();
196
- await ccusageCheck.exited;
236
+ const ccusageCheck = await processSpawner('ccusage', ['--version'], env);
237
+ if (ccusageCheck.exitCode !== 0) {
238
+ return null;
239
+ }
197
240
 
198
241
  // Get daily data for the date
199
242
  const since = `${date}T00:00:00`;
200
243
  const until = `${date}T23:59:59`;
201
244
 
202
- const proc = Bun.spawn(['ccusage', 'daily', '--since', since, '--until', until, '--json'], {
203
- stdout: 'pipe',
204
- stderr: 'pipe',
205
- env,
206
- });
207
-
208
- const output = await new Response(proc.stdout).text();
209
- const exitCode = await proc.exited;
245
+ const proc = await processSpawner('ccusage', ['daily', '--since', since, '--until', until, '--json'], env);
210
246
 
211
- if (exitCode !== 0) {
212
- const errorOutput = await new Response(proc.stderr).text();
213
- logger.warn(`ccusage error: ${errorOutput}`);
247
+ if (proc.exitCode !== 0) {
248
+ logger.warn(`ccusage error: ${proc.stderr}`);
214
249
  return null;
215
250
  }
216
251
 
217
- const data = JSON.parse(output) as CcusageData;
252
+ const data = JSON.parse(proc.stdout) as CcusageData;
218
253
  return data;
219
254
  } catch (error) {
220
255
  logger.warn(`Failed to get ccusage data: ${error}`);
@@ -228,21 +263,24 @@ export async function getGitCommits(date: string): Promise<GitCommit[]> {
228
263
  try {
229
264
  const { start, end } = getDateRange(date);
230
265
 
231
- const proc = Bun.spawn(
232
- ['git', 'log', '--since', start, '--until', end, '--pretty=format:%H|%ad|%s', '--date=iso', '--numstat'],
233
- { stdout: 'pipe', stderr: 'pipe' },
234
- );
235
-
236
- const output = await new Response(proc.stdout).text();
237
- const exitCode = await proc.exited;
238
-
239
- if (exitCode !== 0) {
266
+ const proc = await processSpawner('git', [
267
+ 'log',
268
+ '--since',
269
+ start,
270
+ '--until',
271
+ end,
272
+ '--pretty=format:%H|%ad|%s',
273
+ '--date=iso',
274
+ '--numstat',
275
+ ]);
276
+
277
+ if (proc.exitCode !== 0) {
240
278
  logger.warn('Failed to get git commits');
241
279
  return [];
242
280
  }
243
281
 
244
282
  const commits: GitCommit[] = [];
245
- const lines = output.trim().split('\n');
283
+ const lines = proc.stdout.trim().split('\n');
246
284
 
247
285
  let currentCommit: Partial<GitCommit> | null = null;
248
286
 
@@ -0,0 +1,204 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
3
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
4
+
5
+ // plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts
6
+ var PIPELINE_TOKENS = [
7
+ "--next",
8
+ "dev-runall",
9
+ "dev-wrapall",
10
+ "dev-run",
11
+ "dev-wrap",
12
+ "dev-idea",
13
+ "runall",
14
+ "wrapall",
15
+ "run",
16
+ "wrap",
17
+ "idea"
18
+ ];
19
+ var PIPELINE_DRIVING_REFUSE_MESSAGE = "⚠ pipeline-driving testee detected; pass --max-retry 0 (observe-only) or --max-retry N (fix mode, tree mutation acknowledged)";
20
+ var MUTATING_FIX_REFUSE_MESSAGE = "⚠ 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)";
21
+ var IMPLEMENT_HEAVY_ADVISORY_MESSAGE = "⚠ implement-heavy pipeline dogfood: prefer --max-retry 0 (observe-only) or step-split; operator --max-retry N overrides";
22
+ var IMPLEMENT_HEAVY_TOKENS = [
23
+ "dev-runall",
24
+ "dev-wrapall",
25
+ "dev-run",
26
+ "dev-wrap",
27
+ "dev-idea",
28
+ "runall",
29
+ "wrapall",
30
+ "run",
31
+ "wrap",
32
+ "idea",
33
+ "implement"
34
+ ];
35
+ function hasMutatingFixMode(step) {
36
+ return /(?<![\w-])--fix[=\s]+(all|blockers-first)(?![\w-])/i.test(step);
37
+ }
38
+ function tokenMatches(testee, token) {
39
+ const escaped = token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
40
+ const re = new RegExp(`(?<![\\w-])${escaped}(?![\\w-])`, "i");
41
+ return re.test(testee);
42
+ }
43
+ function detectPipelineDriving(testee) {
44
+ if (typeof testee !== "string" || testee.length === 0) {
45
+ return false;
46
+ }
47
+ return PIPELINE_TOKENS.some((token) => tokenMatches(testee, token));
48
+ }
49
+ function isImplementHeavyStep(step) {
50
+ if (typeof step !== "string" || step.length === 0)
51
+ return false;
52
+ const nonMutatingOnly = tokenMatches(step, "dev-verify") || tokenMatches(step, "dev-review") || tokenMatches(step, "dev-unit");
53
+ const hasMutating = IMPLEMENT_HEAVY_TOKENS.some((token) => tokenMatches(step, token)) || hasMutatingFixMode(step);
54
+ if (nonMutatingOnly && !hasMutating)
55
+ return false;
56
+ if (tokenMatches(step, "dev-refine") && !hasMutating && !tokenMatches(step, "--next")) {
57
+ return false;
58
+ }
59
+ if (tokenMatches(step, "dev-refine") && tokenMatches(step, "--next")) {
60
+ return true;
61
+ }
62
+ return hasMutating;
63
+ }
64
+ function detectImplementHeavy(testee, derivedSteps = []) {
65
+ if (isImplementHeavyStep(testee))
66
+ return true;
67
+ if (!detectPipelineDriving(testee))
68
+ return false;
69
+ return derivedSteps.some((step) => isImplementHeavyStep(step));
70
+ }
71
+ function evaluateDogfoodGate(testee, options = {}) {
72
+ const maxRetryPresent = options.maxRetryPresent === true;
73
+ const steps = options.steps ?? [];
74
+ const pipelineDriving = detectPipelineDriving(testee);
75
+ const mutatingFix = hasMutatingFixMode(testee);
76
+ const implementHeavy = detectImplementHeavy(testee, steps);
77
+ if (pipelineDriving && !maxRetryPresent) {
78
+ return {
79
+ pipelineDriving,
80
+ mutatingFix,
81
+ maxRetryPresent,
82
+ implementHeavy,
83
+ refuse: true,
84
+ advisory: false,
85
+ message: PIPELINE_DRIVING_REFUSE_MESSAGE,
86
+ exitCode: 2
87
+ };
88
+ }
89
+ if (mutatingFix && !maxRetryPresent) {
90
+ return {
91
+ pipelineDriving,
92
+ mutatingFix,
93
+ maxRetryPresent,
94
+ implementHeavy,
95
+ refuse: true,
96
+ advisory: false,
97
+ message: MUTATING_FIX_REFUSE_MESSAGE,
98
+ exitCode: 2
99
+ };
100
+ }
101
+ if (implementHeavy) {
102
+ return {
103
+ pipelineDriving,
104
+ mutatingFix,
105
+ maxRetryPresent,
106
+ implementHeavy,
107
+ refuse: false,
108
+ advisory: true,
109
+ message: IMPLEMENT_HEAVY_ADVISORY_MESSAGE,
110
+ exitCode: 0
111
+ };
112
+ }
113
+ return {
114
+ pipelineDriving,
115
+ mutatingFix,
116
+ maxRetryPresent,
117
+ implementHeavy,
118
+ refuse: false,
119
+ advisory: false,
120
+ message: null,
121
+ exitCode: 0
122
+ };
123
+ }
124
+ function parseCliArgs(argv) {
125
+ let testee = null;
126
+ let maxRetryPresent = false;
127
+ let steps = [];
128
+ let json = false;
129
+ let help = false;
130
+ for (let i = 0;i < argv.length; i++) {
131
+ const a = argv[i];
132
+ if (a === "--help" || a === "-h") {
133
+ help = true;
134
+ } else if (a === "--json") {
135
+ json = true;
136
+ } else if (a === "--max-retry-present") {
137
+ maxRetryPresent = true;
138
+ } else if (a === "--testee") {
139
+ testee = argv[++i] ?? null;
140
+ } else if (a === "--steps") {
141
+ const raw = argv[++i] ?? "";
142
+ steps = raw.length === 0 ? [] : raw.split("||").map((s) => s.trim()).filter(Boolean);
143
+ } else if (a === "--") {
144
+ testee = argv.slice(i + 1).join(" ");
145
+ break;
146
+ } else if (!a.startsWith("-") && testee === null) {
147
+ testee = a;
148
+ }
149
+ }
150
+ return { testee, maxRetryPresent, steps, json, help };
151
+ }
152
+ var CLI_USAGE = `Usage:
153
+ bun plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts \\
154
+ --testee "<testee string>" [--max-retry-present] [--steps "s1||s2"] [--json]
155
+
156
+ Exit codes:
157
+ 0 proceed (stdout may carry implement-heavy advisory)
158
+ 2 refuse — pipeline-driving OR mutating --fix mode without --max-retry
159
+ 1 usage error
160
+
161
+ Phase 1.0: run BEFORE deriving steps (omit --steps).
162
+ Phase 1 W8: re-run AFTER step derivation with --steps "label1||label2".`;
163
+ function runCli(argv) {
164
+ const { testee, maxRetryPresent, steps, json, help } = parseCliArgs(argv);
165
+ if (help) {
166
+ return { exitCode: 0, stdout: "", stderr: CLI_USAGE };
167
+ }
168
+ if (testee === null || testee.length === 0) {
169
+ return { exitCode: 1, stdout: "", stderr: CLI_USAGE };
170
+ }
171
+ const result = evaluateDogfoodGate(testee, { maxRetryPresent, steps });
172
+ if (json) {
173
+ return { exitCode: result.exitCode, stdout: `${JSON.stringify(result, null, 2)}
174
+ `, stderr: "" };
175
+ }
176
+ if (result.message) {
177
+ return { exitCode: result.exitCode, stdout: `${result.message}
178
+ `, stderr: "" };
179
+ }
180
+ return { exitCode: result.exitCode, stdout: "", stderr: "" };
181
+ }
182
+ {
183
+ const { exitCode, stdout, stderr } = runCli(process.argv.slice(2));
184
+ if (stdout)
185
+ process.stdout.write(stdout);
186
+ if (stderr)
187
+ process.stderr.write(`${stderr}
188
+ `);
189
+ process.exit(exitCode);
190
+ }
191
+ export {
192
+ runCli,
193
+ parseCliArgs,
194
+ isImplementHeavyStep,
195
+ hasMutatingFixMode,
196
+ evaluateDogfoodGate,
197
+ detectPipelineDriving,
198
+ detectImplementHeavy,
199
+ PIPELINE_TOKENS,
200
+ PIPELINE_DRIVING_REFUSE_MESSAGE,
201
+ MUTATING_FIX_REFUSE_MESSAGE,
202
+ IMPLEMENT_HEAVY_ADVISORY_MESSAGE,
203
+ CLI_USAGE
204
+ };
@@ -356,7 +356,7 @@ export function runCli(argv: string[]): { exitCode: number; stdout: string; stde
356
356
  }
357
357
 
358
358
  if (import.meta.main) {
359
- const { exitCode, stdout, stderr } = runCli(Bun.argv.slice(2));
359
+ const { exitCode, stdout, stderr } = runCli(process.argv.slice(2));
360
360
  if (stdout) process.stdout.write(stdout);
361
361
  if (stderr) process.stderr.write(`${stderr}\n`);
362
362
  process.exit(exitCode);
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
3
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
4
+
5
+ // plugins/sp/scripts/dogfood-testing/validate-report.ts
6
+ import { readFileSync } from "node:fs";
7
+ var REQUIRED_SECTIONS = [1, 2, 3, 4, 5, 6];
8
+ var CANONICAL_PROTOCOL = "sp:dogfood-testing@1.2";
9
+ function countSectionHeadings(markdown, section) {
10
+ const re = new RegExp(`^### ${section}\\.`, "gm");
11
+ return (markdown.match(re) ?? []).length;
12
+ }
13
+ function countLedgerDataRows(markdown) {
14
+ const heading = markdown.match(/^### 3\. Monitor Ledger\s*$/m);
15
+ if (!heading || heading.index === undefined)
16
+ return null;
17
+ const after = markdown.slice(heading.index + heading[0].length);
18
+ const nextHeading = after.search(/^### /m);
19
+ const body = nextHeading === -1 ? after : after.slice(0, nextHeading);
20
+ const rows = body.split(`
21
+ `).filter((line) => line.trim().startsWith("|")).filter((line) => !/^\|[\s:|-]+\|?\s*$/.test(line.trim())).filter((line) => !/^\|\s*drift:/.test(line.trim()));
22
+ return Math.max(rows.length - 1, 0);
23
+ }
24
+ function declaredExecutedSteps(markdown) {
25
+ const match = markdown.match(/\*\*Steps:\*\*\s*\d+\s+derived,\s*(\d+)\s+executed/);
26
+ return match ? Number.parseInt(match[1], 10) : null;
27
+ }
28
+ function validateReport(markdown) {
29
+ const errors = [];
30
+ if (!markdown.includes("── Dogfood Summary ──"))
31
+ errors.push("missing_footer");
32
+ if (!markdown.includes("[Live:"))
33
+ errors.push("missing_live_path");
34
+ if (!markdown.includes("[Report:"))
35
+ errors.push("missing_report_path");
36
+ for (const section of REQUIRED_SECTIONS) {
37
+ const count = countSectionHeadings(markdown, section);
38
+ if (count === 0)
39
+ errors.push(`missing_section:${section}`);
40
+ if (count > 1)
41
+ errors.push(`duplicate_section:${section}`);
42
+ }
43
+ if (!markdown.includes("#### Fixed") || !markdown.includes("#### Unresolved")) {
44
+ errors.push("missing_issues_subheads");
45
+ }
46
+ const protocolMatch = markdown.match(/^protocol:\s*(\S+)\s*$/m);
47
+ if (!protocolMatch || protocolMatch[1] !== CANONICAL_PROTOCOL) {
48
+ errors.push("protocol_string");
49
+ }
50
+ const executed = declaredExecutedSteps(markdown);
51
+ if (executed === null) {
52
+ errors.push("missing_steps_declared");
53
+ } else {
54
+ const rows = countLedgerDataRows(markdown);
55
+ if (rows === null || rows !== executed) {
56
+ errors.push("ledger_cardinality");
57
+ }
58
+ }
59
+ return { ok: errors.length === 0, errors };
60
+ }
61
+ function parseValidateCliArgs(argv) {
62
+ let file = null;
63
+ let json = false;
64
+ let help = false;
65
+ for (let i = 0;i < argv.length; i++) {
66
+ const a = argv[i];
67
+ if (a === "--help" || a === "-h")
68
+ help = true;
69
+ else if (a === "--json")
70
+ json = true;
71
+ else if (a === "--file")
72
+ file = argv[++i] ?? null;
73
+ else if (!a.startsWith("-") && file === null)
74
+ file = a;
75
+ }
76
+ return { file, json, help };
77
+ }
78
+ var VALIDATE_CLI_USAGE = `Usage:
79
+ bun plugins/sp/scripts/dogfood-testing/validate-report.ts --file <report.md> [--json]
80
+
81
+ Exit codes:
82
+ 0 report validates clean (complete-report shape)
83
+ 2 validation failed (errors on stdout / --json)
84
+ 1 usage error
85
+
86
+ Phase 4 finalize MUST run this before status: complete (task 0278 R6).
87
+ On exit 2: set status: aborted and list error codes under #### Unresolved.`;
88
+ function runValidateCli(argv, readFile) {
89
+ const { file, json, help } = parseValidateCliArgs(argv);
90
+ if (help)
91
+ return { exitCode: 0, stdout: "", stderr: VALIDATE_CLI_USAGE };
92
+ if (file === null || file.length === 0) {
93
+ return { exitCode: 1, stdout: "", stderr: VALIDATE_CLI_USAGE };
94
+ }
95
+ let markdown;
96
+ try {
97
+ markdown = readFile(file);
98
+ } catch (err) {
99
+ const msg = err instanceof Error ? err.message : String(err);
100
+ return { exitCode: 1, stdout: "", stderr: `Failed to read ${file}: ${msg}` };
101
+ }
102
+ const result = validateReport(markdown);
103
+ if (json) {
104
+ return {
105
+ exitCode: result.ok ? 0 : 2,
106
+ stdout: `${JSON.stringify(result, null, 2)}
107
+ `,
108
+ stderr: ""
109
+ };
110
+ }
111
+ if (result.ok) {
112
+ return { exitCode: 0, stdout: `ok
113
+ `, stderr: "" };
114
+ }
115
+ return {
116
+ exitCode: 2,
117
+ stdout: `${result.errors.join(`
118
+ `)}
119
+ `,
120
+ stderr: ""
121
+ };
122
+ }
123
+ function mainCli(argv = process.argv.slice(2)) {
124
+ const { exitCode, stdout, stderr } = runValidateCli(argv, (p) => readFileSync(p, "utf8"));
125
+ if (stdout)
126
+ process.stdout.write(stdout);
127
+ if (stderr)
128
+ process.stderr.write(`${stderr}
129
+ `);
130
+ return exitCode;
131
+ }
132
+ {
133
+ process.exit(mainCli());
134
+ }
135
+ export {
136
+ validateReport,
137
+ runValidateCli,
138
+ parseValidateCliArgs,
139
+ mainCli,
140
+ VALIDATE_CLI_USAGE
141
+ };
@@ -156,7 +156,7 @@ export function runValidateCli(
156
156
  }
157
157
 
158
158
  /** CLI entry for Phase 4 self-validate (import.meta.main). */
159
- export function mainCli(argv: string[] = Bun.argv.slice(2)): number {
159
+ export function mainCli(argv: string[] = process.argv.slice(2)): number {
160
160
  const { exitCode, stdout, stderr } = runValidateCli(argv, (p) => readFileSync(p, 'utf8'));
161
161
  if (stdout) process.stdout.write(stdout);
162
162
  if (stderr) process.stderr.write(`${stderr}\n`);