gsd-pi 2.37.0 → 2.37.1-dev.3bbb0a9

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 (103) hide show
  1. package/README.md +21 -20
  2. package/dist/onboarding.js +1 -0
  3. package/dist/resources/extensions/cmux/package.json +7 -0
  4. package/dist/resources/extensions/gsd/auto-dispatch.js +67 -1
  5. package/dist/resources/extensions/gsd/auto-loop.js +18 -4
  6. package/dist/resources/extensions/gsd/auto-post-unit.js +14 -0
  7. package/dist/resources/extensions/gsd/auto-prompts.js +91 -2
  8. package/dist/resources/extensions/gsd/auto-recovery.js +37 -1
  9. package/dist/resources/extensions/gsd/auto.js +42 -5
  10. package/dist/resources/extensions/gsd/commands.js +80 -33
  11. package/dist/resources/extensions/gsd/doctor-providers.js +35 -1
  12. package/dist/resources/extensions/gsd/files.js +41 -0
  13. package/dist/resources/extensions/gsd/git-service.js +9 -1
  14. package/dist/resources/extensions/gsd/history.js +2 -1
  15. package/dist/resources/extensions/gsd/metrics.js +4 -2
  16. package/dist/resources/extensions/gsd/observability-validator.js +24 -0
  17. package/dist/resources/extensions/gsd/preferences-types.js +2 -1
  18. package/dist/resources/extensions/gsd/preferences-validation.js +42 -0
  19. package/dist/resources/extensions/gsd/prompts/plan-slice.md +2 -1
  20. package/dist/resources/extensions/gsd/prompts/reactive-execute.md +41 -0
  21. package/dist/resources/extensions/gsd/reactive-graph.js +227 -0
  22. package/dist/resources/extensions/gsd/session-lock.js +26 -6
  23. package/dist/resources/extensions/gsd/templates/task-plan.md +11 -3
  24. package/dist/resources/extensions/shared/format-utils.js +5 -41
  25. package/dist/resources/extensions/shared/layout-utils.js +46 -0
  26. package/dist/resources/extensions/shared/mod.js +2 -1
  27. package/package.json +2 -1
  28. package/packages/pi-ai/dist/env-api-keys.js +13 -0
  29. package/packages/pi-ai/dist/env-api-keys.js.map +1 -1
  30. package/packages/pi-ai/dist/models.generated.d.ts +172 -0
  31. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  32. package/packages/pi-ai/dist/models.generated.js +172 -0
  33. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  34. package/packages/pi-ai/dist/providers/anthropic-shared.d.ts +64 -0
  35. package/packages/pi-ai/dist/providers/anthropic-shared.d.ts.map +1 -0
  36. package/packages/pi-ai/dist/providers/anthropic-shared.js +668 -0
  37. package/packages/pi-ai/dist/providers/anthropic-shared.js.map +1 -0
  38. package/packages/pi-ai/dist/providers/anthropic-vertex.d.ts +5 -0
  39. package/packages/pi-ai/dist/providers/anthropic-vertex.d.ts.map +1 -0
  40. package/packages/pi-ai/dist/providers/anthropic-vertex.js +85 -0
  41. package/packages/pi-ai/dist/providers/anthropic-vertex.js.map +1 -0
  42. package/packages/pi-ai/dist/providers/anthropic.d.ts +4 -30
  43. package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
  44. package/packages/pi-ai/dist/providers/anthropic.js +47 -764
  45. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  46. package/packages/pi-ai/dist/providers/register-builtins.d.ts.map +1 -1
  47. package/packages/pi-ai/dist/providers/register-builtins.js +6 -0
  48. package/packages/pi-ai/dist/providers/register-builtins.js.map +1 -1
  49. package/packages/pi-ai/dist/types.d.ts +2 -2
  50. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  51. package/packages/pi-ai/dist/types.js.map +1 -1
  52. package/packages/pi-ai/package.json +1 -0
  53. package/packages/pi-ai/src/env-api-keys.ts +14 -0
  54. package/packages/pi-ai/src/models.generated.ts +172 -0
  55. package/packages/pi-ai/src/providers/anthropic-shared.ts +761 -0
  56. package/packages/pi-ai/src/providers/anthropic-vertex.ts +130 -0
  57. package/packages/pi-ai/src/providers/anthropic.ts +76 -868
  58. package/packages/pi-ai/src/providers/register-builtins.ts +7 -0
  59. package/packages/pi-ai/src/types.ts +2 -0
  60. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  61. package/packages/pi-coding-agent/dist/core/extensions/loader.js +8 -4
  62. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  63. package/packages/pi-coding-agent/dist/core/model-resolver.d.ts.map +1 -1
  64. package/packages/pi-coding-agent/dist/core/model-resolver.js +1 -0
  65. package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
  66. package/packages/pi-coding-agent/package.json +1 -1
  67. package/packages/pi-coding-agent/src/core/extensions/loader.ts +8 -4
  68. package/packages/pi-coding-agent/src/core/model-resolver.ts +1 -0
  69. package/pkg/package.json +1 -1
  70. package/src/resources/extensions/cmux/package.json +7 -0
  71. package/src/resources/extensions/gsd/auto-dispatch.ts +93 -0
  72. package/src/resources/extensions/gsd/auto-loop.ts +24 -6
  73. package/src/resources/extensions/gsd/auto-post-unit.ts +14 -0
  74. package/src/resources/extensions/gsd/auto-prompts.ts +125 -3
  75. package/src/resources/extensions/gsd/auto-recovery.ts +42 -0
  76. package/src/resources/extensions/gsd/auto.ts +56 -5
  77. package/src/resources/extensions/gsd/commands.ts +85 -31
  78. package/src/resources/extensions/gsd/doctor-providers.ts +38 -1
  79. package/src/resources/extensions/gsd/files.ts +45 -0
  80. package/src/resources/extensions/gsd/git-service.ts +12 -1
  81. package/src/resources/extensions/gsd/history.ts +2 -1
  82. package/src/resources/extensions/gsd/metrics.ts +4 -2
  83. package/src/resources/extensions/gsd/observability-validator.ts +27 -0
  84. package/src/resources/extensions/gsd/preferences-types.ts +5 -1
  85. package/src/resources/extensions/gsd/preferences-validation.ts +41 -0
  86. package/src/resources/extensions/gsd/prompts/plan-slice.md +2 -1
  87. package/src/resources/extensions/gsd/prompts/reactive-execute.md +41 -0
  88. package/src/resources/extensions/gsd/reactive-graph.ts +289 -0
  89. package/src/resources/extensions/gsd/session-lock.ts +41 -6
  90. package/src/resources/extensions/gsd/templates/task-plan.md +11 -3
  91. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +37 -1
  92. package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +19 -0
  93. package/src/resources/extensions/gsd/tests/cmux.test.ts +25 -1
  94. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +108 -3
  95. package/src/resources/extensions/gsd/tests/plan-quality-validator.test.ts +111 -0
  96. package/src/resources/extensions/gsd/tests/reactive-executor.test.ts +511 -0
  97. package/src/resources/extensions/gsd/tests/reactive-graph.test.ts +299 -0
  98. package/src/resources/extensions/gsd/tests/session-lock-regression.test.ts +45 -0
  99. package/src/resources/extensions/gsd/types.ts +43 -0
  100. package/src/resources/extensions/shared/format-utils.ts +5 -44
  101. package/src/resources/extensions/shared/layout-utils.ts +49 -0
  102. package/src/resources/extensions/shared/mod.ts +7 -4
  103. package/src/resources/extensions/shared/tests/format-utils.test.ts +5 -3
@@ -14,6 +14,7 @@ import {
14
14
  type AgentEndEvent,
15
15
  type LoopDeps,
16
16
  } from "../auto-loop.js";
17
+ import type { SessionLockStatus } from "../session-lock.js";
17
18
 
18
19
  // ─── Helpers ─────────────────────────────────────────────────────────────────
19
20
 
@@ -341,7 +342,7 @@ function makeMockDeps(
341
342
  preDispatchHealthGate: async () => ({ proceed: true, fixesApplied: [] }),
342
343
  syncProjectRootToWorktree: () => {},
343
344
  checkResourcesStale: () => null,
344
- validateSessionLock: () => true,
345
+ validateSessionLock: () => ({ valid: true } as SessionLockStatus),
345
346
  updateSessionLock: () => {
346
347
  callLog.push("updateSessionLock");
347
348
  },
@@ -532,6 +533,41 @@ test("autoLoop exits on terminal complete state", async (t) => {
532
533
  );
533
534
  });
534
535
 
536
+ test("autoLoop passes structured session-lock failure details to the handler", async () => {
537
+ _resetPendingResolve();
538
+
539
+ const ctx = makeMockCtx();
540
+ ctx.ui.setStatus = () => {};
541
+ const pi = makeMockPi();
542
+ const s = makeLoopSession();
543
+ let observedLockStatus: SessionLockStatus | undefined;
544
+
545
+ const deps = makeMockDeps({
546
+ validateSessionLock: () =>
547
+ ({
548
+ valid: false,
549
+ failureReason: "compromised",
550
+ expectedPid: process.pid,
551
+ }) as SessionLockStatus,
552
+ handleLostSessionLock: (_ctx, lockStatus) => {
553
+ observedLockStatus = lockStatus;
554
+ deps.callLog.push("handleLostSessionLock");
555
+ },
556
+ });
557
+
558
+ await autoLoop(ctx, pi, s, deps);
559
+
560
+ assert.deepEqual(observedLockStatus, {
561
+ valid: false,
562
+ failureReason: "compromised",
563
+ expectedPid: process.pid,
564
+ });
565
+ assert.ok(
566
+ !deps.callLog.includes("resolveDispatch"),
567
+ "should stop before dispatch after lock validation fails",
568
+ );
569
+ });
570
+
535
571
  test("autoLoop exits on terminal blocked state", async (t) => {
536
572
  _resetPendingResolve();
537
573
 
@@ -153,6 +153,25 @@ async function main(): Promise<void> {
153
153
  // After teardown, originalBase should be null
154
154
  assertEq(getAutoWorktreeOriginalBase(), null, "no split-brain: originalBase cleared");
155
155
 
156
+ // ─── #1526: getMainBranch returns milestone branch in auto-worktree ──
157
+ console.log("\n=== #1526: getMainBranch() returns milestone/<MID> in auto-worktree ===");
158
+ {
159
+ const { GitServiceImpl } = await import("../git-service.ts");
160
+
161
+ // Create worktree
162
+ const wtPath = createAutoWorktree(tempDir, "M005");
163
+ // Don't set main_branch pref so getMainBranch falls through to worktree detection
164
+ const gitService = new GitServiceImpl(wtPath);
165
+ gitService.setMilestoneId("M005");
166
+
167
+ // Verify getMainBranch returns the milestone branch
168
+ const mainBranch = gitService.getMainBranch();
169
+ assertEq(mainBranch, "milestone/M005", "getMainBranch returns milestone/<MID> in auto-worktree");
170
+
171
+ // Cleanup
172
+ teardownAutoWorktree(tempDir, "M005");
173
+ }
174
+
156
175
  // ─── #778: reconcile plan checkboxes on re-attach ─────────────────
157
176
  console.log("\n=== #778: reconcile plan checkboxes on re-attach ===");
158
177
  {
@@ -1,5 +1,8 @@
1
- import test from "node:test";
1
+ import test, { describe } from "node:test";
2
2
  import assert from "node:assert/strict";
3
+ import * as fs from "node:fs";
4
+ import * as path from "node:path";
5
+ import { fileURLToPath } from "node:url";
3
6
  import {
4
7
  buildCmuxProgress,
5
8
  buildCmuxStatusLabel,
@@ -96,3 +99,24 @@ test("buildCmuxStatusLabel and progress prefer deepest active unit", () => {
96
99
  assert.equal(buildCmuxStatusLabel(state), "M001 S02/T03 · executing");
97
100
  assert.deepEqual(buildCmuxProgress(state), { value: 0.4, label: "2/5 tasks" });
98
101
  });
102
+
103
+ describe("cmux extension discovery opt-out", () => {
104
+ test("cmux directory has package.json with pi manifest to prevent auto-discovery as extension", () => {
105
+ const cmuxDir = path.resolve(
106
+ path.dirname(fileURLToPath(import.meta.url)),
107
+ "../../cmux",
108
+ );
109
+ const pkgPath = path.join(cmuxDir, "package.json");
110
+ assert.ok(fs.existsSync(pkgPath), `${pkgPath} must exist`);
111
+
112
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
113
+ assert.ok(
114
+ pkg.pi !== undefined && typeof pkg.pi === "object",
115
+ 'package.json must have a "pi" field to opt out of extension auto-discovery',
116
+ );
117
+ assert.ok(
118
+ !pkg.pi.extensions?.length,
119
+ "pi.extensions must be empty or absent — cmux is a library, not an extension",
120
+ );
121
+ });
122
+ });
@@ -184,7 +184,7 @@ test("runProviderChecks detects Anthropic key from ANTHROPIC_API_KEY env var", (
184
184
  // Isolate from real HOME so loadEffectiveGSDPreferences returns null (default → anthropic)
185
185
  // and auth.json lookups hit an empty directory.
186
186
  const tmpHome = realpathSync(mkdtempSync(join(tmpdir(), "gsd-providers-env-test-")));
187
- withEnv({ ANTHROPIC_API_KEY: "sk-ant-test-key", HOME: tmpHome }, () => {
187
+ withEnv({ ANTHROPIC_API_KEY: "sk-ant-test-key", ANTHROPIC_OAUTH_TOKEN: undefined, HOME: tmpHome }, () => {
188
188
  try {
189
189
  const results = runProviderChecks();
190
190
  const anthropic = results.find(r => r.name === "anthropic");
@@ -199,7 +199,15 @@ test("runProviderChecks detects Anthropic key from ANTHROPIC_API_KEY env var", (
199
199
 
200
200
  test("runProviderChecks returns error for Anthropic when no key present", () => {
201
201
  const tmpHome = realpathSync(mkdtempSync(join(tmpdir(), "gsd-providers-test-")));
202
- withEnv({ ANTHROPIC_API_KEY: undefined, HOME: tmpHome }, () => {
202
+ withEnv({
203
+ ANTHROPIC_API_KEY: undefined,
204
+ ANTHROPIC_OAUTH_TOKEN: undefined,
205
+ // Clear cross-provider routing env vars (GitHub Copilot can serve Claude models)
206
+ COPILOT_GITHUB_TOKEN: undefined,
207
+ GH_TOKEN: undefined,
208
+ GITHUB_TOKEN: undefined,
209
+ HOME: tmpHome,
210
+ }, () => {
203
211
  try {
204
212
  const results = runProviderChecks();
205
213
  const anthropic = results.find(r => r.name === "anthropic");
@@ -275,7 +283,7 @@ test("runProviderChecks detects key from auth.json", () => {
275
283
  });
276
284
 
277
285
  test("runProviderChecks ignores empty placeholder keys in auth.json", () => {
278
- withEnv({ ANTHROPIC_API_KEY: undefined }, () => {
286
+ withEnv({ ANTHROPIC_API_KEY: undefined, ANTHROPIC_OAUTH_TOKEN: undefined, COPILOT_GITHUB_TOKEN: undefined, GH_TOKEN: undefined, GITHUB_TOKEN: undefined }, () => {
279
287
  const tmpHome = realpathSync(mkdtempSync(join(tmpdir(), "gsd-providers-test-")));
280
288
  const agentDir = join(tmpHome, ".gsd", "agent");
281
289
  mkdirSync(agentDir, { recursive: true });
@@ -296,3 +304,100 @@ test("runProviderChecks ignores empty placeholder keys in auth.json", () => {
296
304
  rmSync(tmpHome, { recursive: true, force: true });
297
305
  });
298
306
  });
307
+
308
+ // ─── runProviderChecks — cross-provider routing ──────────────────────────────
309
+
310
+ test("runProviderChecks reports ok for Anthropic when GitHub Copilot env var is set", () => {
311
+ const tmpHome = realpathSync(mkdtempSync(join(tmpdir(), "gsd-providers-copilot-test-")));
312
+ withEnv({
313
+ ANTHROPIC_API_KEY: undefined,
314
+ ANTHROPIC_OAUTH_TOKEN: undefined,
315
+ COPILOT_GITHUB_TOKEN: "ghu_copilot-token",
316
+ GH_TOKEN: undefined,
317
+ GITHUB_TOKEN: undefined,
318
+ HOME: tmpHome,
319
+ }, () => {
320
+ try {
321
+ const results = runProviderChecks();
322
+ const anthropic = results.find(r => r.name === "anthropic");
323
+ assert.ok(anthropic, "anthropic result should exist");
324
+ assert.equal(anthropic!.status, "ok", "should be ok when Copilot auth is available");
325
+ assert.ok(anthropic!.message.includes("GitHub Copilot"), "should mention cross-provider source");
326
+ } finally {
327
+ rmSync(tmpHome, { recursive: true, force: true });
328
+ }
329
+ });
330
+ });
331
+
332
+ test("runProviderChecks reports ok for Anthropic via GITHUB_TOKEN cross-provider routing", () => {
333
+ const tmpHome = realpathSync(mkdtempSync(join(tmpdir(), "gsd-providers-ghtoken-test-")));
334
+ withEnv({
335
+ ANTHROPIC_API_KEY: undefined,
336
+ ANTHROPIC_OAUTH_TOKEN: undefined,
337
+ COPILOT_GITHUB_TOKEN: undefined,
338
+ GH_TOKEN: undefined,
339
+ GITHUB_TOKEN: "ghp_github-token",
340
+ HOME: tmpHome,
341
+ }, () => {
342
+ try {
343
+ const results = runProviderChecks();
344
+ const anthropic = results.find(r => r.name === "anthropic");
345
+ assert.ok(anthropic, "anthropic result should exist");
346
+ assert.equal(anthropic!.status, "ok", "should be ok when GITHUB_TOKEN provides Copilot access");
347
+ } finally {
348
+ rmSync(tmpHome, { recursive: true, force: true });
349
+ }
350
+ });
351
+ });
352
+
353
+ test("runProviderChecks detects ANTHROPIC_OAUTH_TOKEN as valid Anthropic auth", () => {
354
+ const tmpHome = realpathSync(mkdtempSync(join(tmpdir(), "gsd-providers-oauth-test-")));
355
+ withEnv({
356
+ ANTHROPIC_API_KEY: undefined,
357
+ ANTHROPIC_OAUTH_TOKEN: "oauth-token-test",
358
+ COPILOT_GITHUB_TOKEN: undefined,
359
+ GH_TOKEN: undefined,
360
+ GITHUB_TOKEN: undefined,
361
+ HOME: tmpHome,
362
+ }, () => {
363
+ try {
364
+ const results = runProviderChecks();
365
+ const anthropic = results.find(r => r.name === "anthropic");
366
+ assert.ok(anthropic, "anthropic result should exist");
367
+ assert.equal(anthropic!.status, "ok", "should be ok when ANTHROPIC_OAUTH_TOKEN is set");
368
+ assert.ok(anthropic!.message.includes("env"), "should report env source");
369
+ } finally {
370
+ rmSync(tmpHome, { recursive: true, force: true });
371
+ }
372
+ });
373
+ });
374
+
375
+ test("runProviderChecks reports ok via Copilot auth.json for Anthropic", () => {
376
+ withEnv({
377
+ ANTHROPIC_API_KEY: undefined,
378
+ ANTHROPIC_OAUTH_TOKEN: undefined,
379
+ COPILOT_GITHUB_TOKEN: undefined,
380
+ GH_TOKEN: undefined,
381
+ GITHUB_TOKEN: undefined,
382
+ }, () => {
383
+ const tmpHome = realpathSync(mkdtempSync(join(tmpdir(), "gsd-providers-copilot-auth-test-")));
384
+ const agentDir = join(tmpHome, ".gsd", "agent");
385
+ mkdirSync(agentDir, { recursive: true });
386
+
387
+ // GitHub Copilot OAuth in auth.json
388
+ const authData = {
389
+ "github-copilot": { type: "oauth", apiKey: "ghu_copilot-key", expires: Date.now() + 3_600_000 },
390
+ };
391
+ writeFileSync(join(agentDir, "auth.json"), JSON.stringify(authData));
392
+
393
+ withEnv({ HOME: tmpHome }, () => {
394
+ const results = runProviderChecks();
395
+ const anthropic = results.find(r => r.name === "anthropic");
396
+ assert.ok(anthropic, "anthropic result should exist");
397
+ assert.equal(anthropic!.status, "ok", "should be ok when Copilot is authenticated in auth.json");
398
+ assert.ok(anthropic!.message.includes("GitHub Copilot"), "should mention Copilot as source");
399
+ });
400
+
401
+ rmSync(tmpHome, { recursive: true, force: true });
402
+ });
403
+ });
@@ -360,4 +360,115 @@ console.log('\n=== Clean slice plan: no plan-quality issues ===');
360
360
  assertEq(planQualityIssues.length, 0, 'clean slice plan produces no empty_task_entry issues');
361
361
  }
362
362
 
363
+ // ═══════════════════════════════════════════════════════════════════════════
364
+ // validateTaskPlanContent — missing output file paths
365
+ // ═══════════════════════════════════════════════════════════════════════════
366
+
367
+ console.log('\n=== validateTaskPlanContent: missing output file paths ===');
368
+ {
369
+ const content = `# T01: Some Task
370
+
371
+ ## Description
372
+
373
+ Do something.
374
+
375
+ ## Steps
376
+
377
+ 1. Do the thing
378
+
379
+ ## Verification
380
+
381
+ - Check it works
382
+
383
+ ## Expected Output
384
+
385
+ This task produces the main output.
386
+ `;
387
+
388
+ const issues = validateTaskPlanContent('T01-PLAN.md', content);
389
+ const outputIssues = issues.filter(i => i.ruleId === 'missing_output_file_paths');
390
+ assertTrue(outputIssues.length >= 1, 'Expected Output without file paths triggers missing_output_file_paths');
391
+ }
392
+
393
+ console.log('\n=== validateTaskPlanContent: valid output file paths ===');
394
+ {
395
+ const content = `# T01: Some Task
396
+
397
+ ## Description
398
+
399
+ Do something.
400
+
401
+ ## Steps
402
+
403
+ 1. Do the thing
404
+
405
+ ## Verification
406
+
407
+ - Check it works
408
+
409
+ ## Expected Output
410
+
411
+ - \`src/types.ts\` — New type definitions
412
+ `;
413
+
414
+ const issues = validateTaskPlanContent('T01-PLAN.md', content);
415
+ const outputIssues = issues.filter(i => i.ruleId === 'missing_output_file_paths');
416
+ assertEq(outputIssues.length, 0, 'Expected Output with file paths does not trigger warning');
417
+ }
418
+
419
+ console.log('\n=== validateTaskPlanContent: missing input file paths (info severity) ===');
420
+ {
421
+ const content = `# T01: Some Task
422
+
423
+ ## Description
424
+
425
+ Do something.
426
+
427
+ ## Steps
428
+
429
+ 1. Do the thing
430
+
431
+ ## Verification
432
+
433
+ - Check it works
434
+
435
+ ## Inputs
436
+
437
+ Prior task summary insights about the architecture.
438
+
439
+ ## Expected Output
440
+
441
+ - \`src/output.ts\` — Output file
442
+ `;
443
+
444
+ const issues = validateTaskPlanContent('T01-PLAN.md', content);
445
+ const inputIssues = issues.filter(i => i.ruleId === 'missing_input_file_paths');
446
+ assertTrue(inputIssues.length >= 1, 'Inputs without file paths triggers missing_input_file_paths');
447
+ if (inputIssues.length > 0) {
448
+ assertEq(inputIssues[0].severity, 'info', 'missing_input_file_paths is info severity (not warning)');
449
+ }
450
+ }
451
+
452
+ console.log('\n=== validateTaskPlanContent: no Expected Output section at all ===');
453
+ {
454
+ const content = `# T01: Some Task
455
+
456
+ ## Description
457
+
458
+ Do something.
459
+
460
+ ## Steps
461
+
462
+ 1. Do the thing
463
+
464
+ ## Verification
465
+
466
+ - Check it works
467
+ `;
468
+
469
+ const issues = validateTaskPlanContent('T01-PLAN.md', content);
470
+ const outputIssues = issues.filter(i => i.ruleId === 'missing_output_file_paths');
471
+ assertTrue(outputIssues.length >= 1, 'Missing Expected Output section triggers missing_output_file_paths');
472
+ }
473
+
363
474
  report();