gsd-pi 2.13.0 → 2.14.0

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 (99) hide show
  1. package/README.md +3 -3
  2. package/dist/cli.js +1 -0
  3. package/dist/loader.js +50 -6
  4. package/dist/resource-loader.d.ts +7 -6
  5. package/dist/resource-loader.js +15 -8
  6. package/dist/resources/extensions/gsd/auto-worktree.ts +29 -183
  7. package/dist/resources/extensions/gsd/auto.ts +252 -370
  8. package/dist/resources/extensions/gsd/commands.ts +118 -34
  9. package/dist/resources/extensions/gsd/doctor.ts +29 -4
  10. package/dist/resources/extensions/gsd/git-self-heal.ts +0 -71
  11. package/dist/resources/extensions/gsd/git-service.ts +8 -431
  12. package/dist/resources/extensions/gsd/gitignore.ts +11 -4
  13. package/dist/resources/extensions/gsd/guided-flow.ts +141 -5
  14. package/dist/resources/extensions/gsd/preferences.ts +18 -17
  15. package/dist/resources/extensions/gsd/prompts/discuss.md +35 -0
  16. package/dist/resources/extensions/gsd/prompts/queue.md +7 -1
  17. package/dist/resources/extensions/gsd/state.ts +26 -8
  18. package/dist/resources/extensions/gsd/templates/state.md +0 -1
  19. package/dist/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +3 -2
  20. package/dist/resources/extensions/gsd/tests/auto-worktree.test.ts +1 -1
  21. package/dist/resources/extensions/gsd/tests/derive-state.test.ts +35 -0
  22. package/dist/resources/extensions/gsd/tests/doctor-git.test.ts +22 -4
  23. package/dist/resources/extensions/gsd/tests/draft-promotion.test.ts +2 -1
  24. package/dist/resources/extensions/gsd/tests/git-self-heal.test.ts +8 -111
  25. package/dist/resources/extensions/gsd/tests/git-service.test.ts +11 -770
  26. package/dist/resources/extensions/gsd/tests/idle-recovery.test.ts +21 -113
  27. package/dist/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +16 -82
  28. package/dist/resources/extensions/gsd/tests/preferences-git.test.ts +29 -50
  29. package/dist/resources/extensions/gsd/tests/worktree-e2e.test.ts +17 -91
  30. package/dist/resources/extensions/gsd/tests/worktree-integration.test.ts +28 -55
  31. package/dist/resources/extensions/gsd/tests/worktree.test.ts +1 -426
  32. package/dist/resources/extensions/gsd/types.ts +0 -1
  33. package/dist/resources/extensions/gsd/worktree-manager.ts +7 -3
  34. package/dist/resources/extensions/gsd/worktree.ts +7 -65
  35. package/dist/resources/extensions/search-the-web/command-search-provider.ts +3 -1
  36. package/package.json +1 -1
  37. package/packages/pi-ai/dist/providers/google.d.ts.map +1 -1
  38. package/packages/pi-ai/dist/providers/google.js +12 -4
  39. package/packages/pi-ai/dist/providers/google.js.map +1 -1
  40. package/packages/pi-ai/dist/providers/mistral.d.ts.map +1 -1
  41. package/packages/pi-ai/dist/providers/mistral.js +10 -2
  42. package/packages/pi-ai/dist/providers/mistral.js.map +1 -1
  43. package/packages/pi-ai/src/providers/google.ts +20 -8
  44. package/packages/pi-ai/src/providers/mistral.ts +14 -2
  45. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts +3 -0
  46. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  47. package/packages/pi-coding-agent/dist/core/extensions/loader.js +10 -7
  48. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  49. package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.d.ts +1 -1
  50. package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.d.ts.map +1 -1
  51. package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.js +4 -1
  52. package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.js.map +1 -1
  53. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  54. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +12 -3
  55. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  56. package/packages/pi-coding-agent/src/core/extensions/loader.ts +13 -9
  57. package/packages/pi-coding-agent/src/modes/interactive/components/extension-input.ts +4 -1
  58. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +14 -3
  59. package/packages/pi-tui/dist/components/input.d.ts +1 -0
  60. package/packages/pi-tui/dist/components/input.d.ts.map +1 -1
  61. package/packages/pi-tui/dist/components/input.js +10 -0
  62. package/packages/pi-tui/dist/components/input.js.map +1 -1
  63. package/packages/pi-tui/src/components/input.ts +11 -0
  64. package/src/resources/extensions/gsd/auto-worktree.ts +29 -183
  65. package/src/resources/extensions/gsd/auto.ts +252 -370
  66. package/src/resources/extensions/gsd/commands.ts +118 -34
  67. package/src/resources/extensions/gsd/doctor.ts +29 -4
  68. package/src/resources/extensions/gsd/git-self-heal.ts +0 -71
  69. package/src/resources/extensions/gsd/git-service.ts +8 -431
  70. package/src/resources/extensions/gsd/gitignore.ts +11 -4
  71. package/src/resources/extensions/gsd/guided-flow.ts +141 -5
  72. package/src/resources/extensions/gsd/preferences.ts +18 -17
  73. package/src/resources/extensions/gsd/prompts/discuss.md +35 -0
  74. package/src/resources/extensions/gsd/prompts/queue.md +7 -1
  75. package/src/resources/extensions/gsd/state.ts +26 -8
  76. package/src/resources/extensions/gsd/templates/state.md +0 -1
  77. package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +3 -2
  78. package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +1 -1
  79. package/src/resources/extensions/gsd/tests/derive-state.test.ts +35 -0
  80. package/src/resources/extensions/gsd/tests/doctor-git.test.ts +22 -4
  81. package/src/resources/extensions/gsd/tests/draft-promotion.test.ts +2 -1
  82. package/src/resources/extensions/gsd/tests/git-self-heal.test.ts +8 -111
  83. package/src/resources/extensions/gsd/tests/git-service.test.ts +11 -770
  84. package/src/resources/extensions/gsd/tests/idle-recovery.test.ts +21 -113
  85. package/src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +16 -82
  86. package/src/resources/extensions/gsd/tests/preferences-git.test.ts +29 -50
  87. package/src/resources/extensions/gsd/tests/worktree-e2e.test.ts +17 -91
  88. package/src/resources/extensions/gsd/tests/worktree-integration.test.ts +28 -55
  89. package/src/resources/extensions/gsd/tests/worktree.test.ts +1 -426
  90. package/src/resources/extensions/gsd/types.ts +0 -1
  91. package/src/resources/extensions/gsd/worktree-manager.ts +7 -3
  92. package/src/resources/extensions/gsd/worktree.ts +7 -65
  93. package/src/resources/extensions/search-the-web/command-search-provider.ts +3 -1
  94. package/dist/resources/extensions/gsd/tests/auto-worktree-merge.test.ts +0 -282
  95. package/dist/resources/extensions/gsd/tests/isolation-resolver.test.ts +0 -107
  96. package/dist/resources/extensions/gsd/tests/orphaned-branch.test.ts +0 -353
  97. package/src/resources/extensions/gsd/tests/auto-worktree-merge.test.ts +0 -282
  98. package/src/resources/extensions/gsd/tests/isolation-resolver.test.ts +0 -107
  99. package/src/resources/extensions/gsd/tests/orphaned-branch.test.ts +0 -353
@@ -280,119 +280,6 @@ function cleanup(base: string): void {
280
280
  }
281
281
  }
282
282
 
283
- // ═══ verifyExpectedArtifact: fix-merge ════════════════════════════════════════
284
-
285
- /** Create a real git repo for fix-merge tests */
286
- function createGitBase(): string {
287
- const base = mkdtempSync(join(tmpdir(), "gsd-fixmerge-test-"));
288
- execSync("git init -b main", { cwd: base, stdio: "ignore" });
289
- execSync("git config user.email test@test.com", { cwd: base, stdio: "ignore" });
290
- execSync("git config user.name Test", { cwd: base, stdio: "ignore" });
291
- writeFileSync(join(base, "README.md"), "init\n", "utf-8");
292
- execSync("git add -A && git commit -m init", { cwd: base, stdio: "ignore" });
293
- // Create .gsd structure for the fixture
294
- mkdirSync(join(base, ".gsd", "milestones", "M001", "slices", "S01", "tasks"), { recursive: true });
295
- return base;
296
- }
297
-
298
- {
299
- console.log("\n=== verifyExpectedArtifact: fix-merge — clean repo returns true ===");
300
- const base = createGitBase();
301
- try {
302
- const result = verifyExpectedArtifact("fix-merge", "M001/S01", base);
303
- assertTrue(result === true, "clean repo should verify as true");
304
- } finally {
305
- cleanup(base);
306
- }
307
- }
308
-
309
- {
310
- console.log("\n=== verifyExpectedArtifact: fix-merge — MERGE_HEAD present returns false ===");
311
- const base = createGitBase();
312
- try {
313
- writeFileSync(join(base, ".git", "MERGE_HEAD"), "abc123\n", "utf-8");
314
- const result = verifyExpectedArtifact("fix-merge", "M001/S01", base);
315
- assertTrue(result === false, "MERGE_HEAD present should return false");
316
- } finally {
317
- cleanup(base);
318
- }
319
- }
320
-
321
- {
322
- console.log("\n=== verifyExpectedArtifact: fix-merge — SQUASH_MSG present returns false ===");
323
- const base = createGitBase();
324
- try {
325
- writeFileSync(join(base, ".git", "SQUASH_MSG"), "squash msg\n", "utf-8");
326
- const result = verifyExpectedArtifact("fix-merge", "M001/S01", base);
327
- assertTrue(result === false, "SQUASH_MSG present should return false");
328
- } finally {
329
- cleanup(base);
330
- }
331
- }
332
-
333
- {
334
- console.log("\n=== verifyExpectedArtifact: fix-merge — real UU conflict returns false ===");
335
- const base = createGitBase();
336
- try {
337
- // Create a conflict: modify same file on two branches
338
- writeFileSync(join(base, "conflict.txt"), "main content\n", "utf-8");
339
- execSync('git add -A && git commit -m "main change"', { cwd: base, stdio: "ignore" });
340
- execSync("git checkout -b feature", { cwd: base, stdio: "ignore" });
341
- writeFileSync(join(base, "conflict.txt"), "feature content\n", "utf-8");
342
- execSync('git add -A && git commit -m "feature change"', { cwd: base, stdio: "ignore" });
343
- execSync("git checkout main", { cwd: base, stdio: "ignore" });
344
- writeFileSync(join(base, "conflict.txt"), "different main content\n", "utf-8");
345
- execSync('git add -A && git commit -m "diverge"', { cwd: base, stdio: "ignore" });
346
- try { execSync("git merge feature", { cwd: base, stdio: "ignore" }); } catch { /* expected conflict */ }
347
- const result = verifyExpectedArtifact("fix-merge", "M001/S01", base);
348
- assertTrue(result === false, "UU conflict should return false");
349
- } finally {
350
- execSync("git reset --hard HEAD", { cwd: base, stdio: "ignore" });
351
- cleanup(base);
352
- }
353
- }
354
-
355
- {
356
- console.log("\n=== verifyExpectedArtifact: fix-merge — real DU conflict returns false ===");
357
- const base = createGitBase();
358
- try {
359
- writeFileSync(join(base, "deleted.txt"), "content\n", "utf-8");
360
- execSync('git add -A && git commit -m "add file"', { cwd: base, stdio: "ignore" });
361
- execSync("git checkout -b feature2", { cwd: base, stdio: "ignore" });
362
- writeFileSync(join(base, "deleted.txt"), "modified on feature\n", "utf-8");
363
- execSync('git add -A && git commit -m "modify on feature"', { cwd: base, stdio: "ignore" });
364
- execSync("git checkout main", { cwd: base, stdio: "ignore" });
365
- execSync("git rm deleted.txt", { cwd: base, stdio: "ignore" });
366
- execSync('git commit -m "delete on main"', { cwd: base, stdio: "ignore" });
367
- try { execSync("git merge feature2", { cwd: base, stdio: "ignore" }); } catch { /* expected conflict */ }
368
- const result = verifyExpectedArtifact("fix-merge", "M001/S01", base);
369
- assertTrue(result === false, "DU conflict should return false");
370
- } finally {
371
- execSync("git reset --hard HEAD", { cwd: base, stdio: "ignore" });
372
- cleanup(base);
373
- }
374
- }
375
-
376
- {
377
- console.log("\n=== verifyExpectedArtifact: fix-merge — real AA conflict returns false ===");
378
- const base = createGitBase();
379
- try {
380
- execSync("git checkout -b branch-a", { cwd: base, stdio: "ignore" });
381
- writeFileSync(join(base, "both.txt"), "branch-a content\n", "utf-8");
382
- execSync('git add -A && git commit -m "add on branch-a"', { cwd: base, stdio: "ignore" });
383
- execSync("git checkout main", { cwd: base, stdio: "ignore" });
384
- execSync("git checkout -b branch-b", { cwd: base, stdio: "ignore" });
385
- writeFileSync(join(base, "both.txt"), "branch-b content\n", "utf-8");
386
- execSync('git add -A && git commit -m "add on branch-b"', { cwd: base, stdio: "ignore" });
387
- try { execSync("git merge branch-a", { cwd: base, stdio: "ignore" }); } catch { /* expected conflict */ }
388
- const result = verifyExpectedArtifact("fix-merge", "M001/S01", base);
389
- assertTrue(result === false, "AA conflict should return false");
390
- } finally {
391
- execSync("git reset --hard HEAD", { cwd: base, stdio: "ignore" });
392
- cleanup(base);
393
- }
394
- }
395
-
396
283
  // ═══ verifyExpectedArtifact: complete-slice roadmap check ════════════════════
397
284
  // Regression for #indefinite-hang: complete-slice must verify roadmap [x] or
398
285
  // the idempotency skip loops forever after a crash that wrote SUMMARY+UAT but
@@ -574,4 +461,25 @@ const ROADMAP_COMPLETE = `# M001: Test Milestone
574
461
  }
575
462
  }
576
463
 
464
+ // ═══ verifyExpectedArtifact: hook unit types ═════════════════════════════════
465
+
466
+ console.log("\n=== verifyExpectedArtifact: hook types always return true ===");
467
+
468
+ {
469
+ const base = createFixtureBase();
470
+ try {
471
+ // Hook units don't have standard artifacts — they should always pass
472
+ const result1 = verifyExpectedArtifact("hook/code-review", "M001/S01/T01", base);
473
+ assertTrue(result1, "hook/code-review should always return true");
474
+
475
+ const result2 = verifyExpectedArtifact("hook/simplify", "M001/S01/T02", base);
476
+ assertTrue(result2, "hook/simplify should always return true");
477
+
478
+ const result3 = verifyExpectedArtifact("hook/custom-hook", "M001/S01", base);
479
+ assertTrue(result3, "hook/custom-hook at slice level should return true");
480
+ } finally {
481
+ rmSync(base, { recursive: true, force: true });
482
+ }
483
+ }
484
+
577
485
  report();
@@ -6,7 +6,7 @@
6
6
  * Uses real filesystem and git fixtures — no mocking.
7
7
  */
8
8
 
9
- import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
9
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
10
10
  import { execSync } from 'node:child_process';
11
11
  import { join } from 'node:path';
12
12
  import { tmpdir } from 'node:os';
@@ -16,12 +16,8 @@ import { indexWorkspace } from '../workspace-index.ts';
16
16
  import { inlinePriorMilestoneSummary } from '../files.ts';
17
17
  import { getPriorSliceCompletionBlocker } from '../dispatch-guard.ts';
18
18
  import {
19
- ensureSliceBranch,
20
- getCurrentBranch,
21
19
  getSliceBranchName,
22
- mergeSliceToMain,
23
20
  parseSliceBranch,
24
- switchToMain,
25
21
  } from '../worktree.ts';
26
22
  import { clearPathCache } from '../paths.ts';
27
23
  import { createTestContext } from './test-helpers.ts';
@@ -481,84 +477,22 @@ Built the legacy feature successfully.
481
477
  }
482
478
  }
483
479
 
484
- // ─── Group 6: Branch operations with new-format IDs ─────────────────
485
- console.log('\n=== Group 6: Branch operations with new-format IDs ===');
480
+ // ─── Group 6: Branch name helpers with new-format IDs ───────────────
481
+ console.log('\n=== Group 6: Branch name helpers with new-format IDs ===');
486
482
  {
487
- const base = createGitRepo();
488
- try {
489
- // Need a milestone dir and initial commit for branch ops
490
- writeRoadmap(base, 'M001-abc123', `# M001-abc123: Branch Test
491
-
492
- **Vision:** Test branches
493
-
494
- ## Slices
495
- - [ ] **S01: Slice One** \`risk:low\` \`depends:[]\`
496
- > Branch test
497
- `);
498
- writePlan(base, 'M001-abc123', 'S01', `# S01: Slice One
499
-
500
- **Goal:** Test
501
- **Demo:** Branch works
502
-
503
- ## Tasks
504
- - [ ] **T01: Build** \`est:10m\`
505
- Build it.
506
- `);
507
- writeFileSync(join(base, 'README.md'), 'initial\n');
508
- run('git add .', base);
509
- run('git commit -m init', base);
510
-
511
- // Test getSliceBranchName with new-format ID
512
- assertEq(
513
- getSliceBranchName('M001-abc123', 'S01'),
514
- 'gsd/M001-abc123/S01',
515
- 'G6: getSliceBranchName returns gsd/M001-abc123/S01',
516
- );
517
-
518
- // Test parseSliceBranch with new-format branch name
519
- const parsed = parseSliceBranch('gsd/M001-abc123/S01');
520
- assertTrue(parsed !== null, 'G6: parseSliceBranch returns non-null for new-format');
521
- assertEq(parsed?.milestoneId, 'M001-abc123', 'G6: parsed milestoneId is M001-abc123');
522
- assertEq(parsed?.sliceId, 'S01', 'G6: parsed sliceId is S01');
523
- assertEq(parsed?.worktreeName, null, 'G6: parsed worktreeName is null (no worktree)');
524
-
525
- // Test ensureSliceBranch creates the branch
526
- const created = ensureSliceBranch(base, 'M001-abc123', 'S01');
527
- assertTrue(created, 'G6: ensureSliceBranch returns true (branch created)');
528
- assertEq(
529
- getCurrentBranch(base),
530
- 'gsd/M001-abc123/S01',
531
- 'G6: getCurrentBranch returns gsd/M001-abc123/S01',
532
- );
533
-
534
- // Idempotent: second ensure should not create
535
- const secondCreate = ensureSliceBranch(base, 'M001-abc123', 'S01');
536
- assertEq(secondCreate, false, 'G6: second ensureSliceBranch returns false');
537
-
538
- // Make a change on the slice branch, commit, then merge to main
539
- writeFileSync(join(base, 'feature.txt'), 'new feature from slice\n');
540
- run('git add feature.txt', base);
541
- run('git commit -m "feat: slice work"', base);
542
-
543
- // Switch to main and merge
544
- switchToMain(base);
545
- assertEq(getCurrentBranch(base), 'main', 'G6: back on main after switchToMain');
546
-
547
- const merge = mergeSliceToMain(base, 'M001-abc123', 'S01', 'Slice One');
548
- assertEq(merge.branch, 'gsd/M001-abc123/S01', 'G6: merge reports correct branch');
549
- assertEq(getCurrentBranch(base), 'main', 'G6: still on main after merge');
550
- assertTrue(merge.deletedBranch, 'G6: merge deleted the slice branch');
551
-
552
- // Verify the merged content exists on main
553
- const content = readFileSync(join(base, 'feature.txt'), 'utf-8');
554
- assertTrue(content.includes('new feature from slice'), 'G6: merged content on main');
555
-
556
- // Verify branch is gone
557
- const branches = run('git branch', base);
558
- assertTrue(!branches.includes('gsd/M001-abc123/S01'), 'G6: slice branch deleted after merge');
559
- } finally {
560
- cleanup(base);
561
- }
483
+ // Test getSliceBranchName with new-format ID
484
+ assertEq(
485
+ getSliceBranchName('M001-abc123', 'S01'),
486
+ 'gsd/M001-abc123/S01',
487
+ 'G6: getSliceBranchName returns gsd/M001-abc123/S01',
488
+ );
489
+
490
+ // Test parseSliceBranch with new-format branch name
491
+ const parsed = parseSliceBranch('gsd/M001-abc123/S01');
492
+ assertTrue(parsed !== null, 'G6: parseSliceBranch returns non-null for new-format');
493
+ assertEq(parsed?.milestoneId, 'M001-abc123', 'G6: parsed milestoneId is M001-abc123');
494
+ assertEq(parsed?.sliceId, 'S01', 'G6: parsed sliceId is S01');
495
+ assertEq(parsed?.worktreeName, null, 'G6: parsed worktreeName is null (no worktree)');
562
496
  }
563
497
 
564
498
  // ─── Summary ──────────────────────────────────────────────────────────
@@ -1,5 +1,6 @@
1
1
  /**
2
- * preferences-git.test.ts — Validates git.isolation and git.merge_to_main preference fields.
2
+ * preferences-git.test.ts — Validates that deprecated git.isolation and
3
+ * git.merge_to_main preference fields produce deprecation warnings.
3
4
  */
4
5
 
5
6
  import { createTestContext } from "./test-helpers.ts";
@@ -8,78 +9,56 @@ import { validatePreferences } from "../preferences.ts";
8
9
  const { assertEq, assertTrue, report } = createTestContext();
9
10
 
10
11
  async function main(): Promise<void> {
11
- console.log("\n=== git.isolation validation ===");
12
+ console.log("\n=== git.isolation deprecated ===");
12
13
 
13
- // Valid values
14
+ // Any value produces a deprecation warning
14
15
  {
15
- const { preferences, errors } = validatePreferences({ git: { isolation: "worktree" } });
16
- assertEq(errors.length, 0, "isolation: worktree — no errors");
17
- assertEq(preferences.git?.isolation, "worktree", "isolation: worktree — value preserved");
16
+ const { warnings } = validatePreferences({ git: { isolation: "worktree" } });
17
+ assertTrue(warnings.length > 0, "isolation: worktree — produces deprecation warning");
18
+ assertTrue(warnings[0].includes("deprecated"), "isolation: worktree — warning mentions deprecated");
18
19
  }
19
20
  {
20
- const { preferences, errors } = validatePreferences({ git: { isolation: "branch" } });
21
- assertEq(errors.length, 0, "isolation: branch — no errors");
22
- assertEq(preferences.git?.isolation, "branch", "isolation: branch — value preserved");
21
+ const { warnings } = validatePreferences({ git: { isolation: "branch" } });
22
+ assertTrue(warnings.length > 0, "isolation: branch — produces deprecation warning");
23
+ assertTrue(warnings[0].includes("deprecated"), "isolation: branch — warning mentions deprecated");
23
24
  }
24
25
 
25
- // Invalid values
26
+ // Undefined passes through without warning
26
27
  {
27
- const { errors } = validatePreferences({ git: { isolation: "invalid" } });
28
- assertTrue(errors.length > 0, "isolation: invalidproduces error");
29
- assertTrue(errors[0].includes("isolation"), "isolation: invalid — error mentions isolation");
30
- }
31
- {
32
- const { errors } = validatePreferences({ git: { isolation: 42 } });
33
- assertTrue(errors.length > 0, "isolation: number — produces error");
34
- }
35
-
36
- // Undefined passes through
37
- {
38
- const { preferences, errors } = validatePreferences({ git: { auto_push: true } });
39
- assertEq(errors.length, 0, "isolation: undefined — no errors");
28
+ const { preferences, warnings } = validatePreferences({ git: { auto_push: true } });
29
+ assertEq(warnings.length, 0, "isolation: undefinedno warnings");
40
30
  assertEq(preferences.git?.isolation, undefined, "isolation: undefined — not set");
41
31
  }
42
32
 
43
- console.log("\n=== git.merge_to_main validation ===");
44
-
45
- // Valid values
46
- {
47
- const { preferences, errors } = validatePreferences({ git: { merge_to_main: "milestone" } });
48
- assertEq(errors.length, 0, "merge_to_main: milestone — no errors");
49
- assertEq(preferences.git?.merge_to_main, "milestone", "merge_to_main: milestone — value preserved");
50
- }
51
- {
52
- const { preferences, errors } = validatePreferences({ git: { merge_to_main: "slice" } });
53
- assertEq(errors.length, 0, "merge_to_main: slice — no errors");
54
- assertEq(preferences.git?.merge_to_main, "slice", "merge_to_main: slice — value preserved");
55
- }
33
+ console.log("\n=== git.merge_to_main deprecated ===");
56
34
 
57
- // Invalid values
35
+ // Any value produces a deprecation warning
58
36
  {
59
- const { errors } = validatePreferences({ git: { merge_to_main: "invalid" } });
60
- assertTrue(errors.length > 0, "merge_to_main: invalid — produces error");
61
- assertTrue(errors[0].includes("merge_to_main"), "merge_to_main: invaliderror mentions merge_to_main");
37
+ const { warnings } = validatePreferences({ git: { merge_to_main: "milestone" } });
38
+ assertTrue(warnings.length > 0, "merge_to_main: milestone — produces deprecation warning");
39
+ assertTrue(warnings[0].includes("deprecated"), "merge_to_main: milestonewarning mentions deprecated");
62
40
  }
63
41
  {
64
- const { errors } = validatePreferences({ git: { merge_to_main: false } });
65
- assertTrue(errors.length > 0, "merge_to_main: boolean — produces error");
42
+ const { warnings } = validatePreferences({ git: { merge_to_main: "slice" } });
43
+ assertTrue(warnings.length > 0, "merge_to_main: slice — produces deprecation warning");
44
+ assertTrue(warnings[0].includes("deprecated"), "merge_to_main: slice — warning mentions deprecated");
66
45
  }
67
46
 
68
- // Undefined passes through
47
+ // Undefined passes through without warning
69
48
  {
70
- const { preferences, errors } = validatePreferences({ git: { auto_push: true } });
71
- assertEq(errors.length, 0, "merge_to_main: undefined — no errors");
49
+ const { preferences, warnings } = validatePreferences({ git: { auto_push: true } });
50
+ assertEq(warnings.length, 0, "merge_to_main: undefined — no warnings");
72
51
  assertEq(preferences.git?.merge_to_main, undefined, "merge_to_main: undefined — not set");
73
52
  }
74
53
 
75
- console.log("\n=== both fields together ===");
54
+ console.log("\n=== both deprecated fields together ===");
76
55
  {
77
- const { preferences, errors } = validatePreferences({
56
+ const { warnings } = validatePreferences({
78
57
  git: { isolation: "worktree", merge_to_main: "slice" },
79
58
  });
80
- assertEq(errors.length, 0, "both fields valid no errors");
81
- assertEq(preferences.git?.isolation, "worktree", "isolation preserved");
82
- assertEq(preferences.git?.merge_to_main, "slice", "merge_to_main preserved");
59
+ assertEq(warnings.length, 2, "both deprecated fields — 2 warnings");
60
+ assertTrue(warnings.some(w => w.includes("isolation")), "one warning mentions isolation");
61
+ assertTrue(warnings.some(w => w.includes("merge_to_main")), "one warning mentions merge_to_main");
83
62
  }
84
63
 
85
64
  report();
@@ -1,17 +1,15 @@
1
1
  /**
2
2
  * worktree-e2e.test.ts -- End-to-end tests for worktree-isolated git flow.
3
3
  *
4
- * Covers 5 cross-cutting groups not tested by individual slice tests:
4
+ * Covers cross-cutting groups not tested by individual slice tests:
5
5
  * 1. Full lifecycle chain (create -> slice commits -> merge to milestone -> merge to main)
6
- * 2. Preference gating (shouldUseWorktreeIsolation with overrides)
7
- * 3. merge_to_main mode resolution (getMergeToMainMode)
8
- * 4. Self-heal in merge context (abortAndReset, withMergeHeal)
9
- * 5. Doctor detection of orphaned worktrees
6
+ * 2. Self-heal: abortAndReset cleans up failed merges
7
+ * 3. Doctor detection of orphaned worktrees
10
8
  */
11
9
 
12
10
  import {
13
11
  mkdtempSync, mkdirSync, writeFileSync, rmSync,
14
- existsSync, realpathSync, readFileSync,
12
+ existsSync, realpathSync,
15
13
  } from "node:fs";
16
14
  import { join } from "node:path";
17
15
  import { tmpdir } from "node:os";
@@ -20,11 +18,9 @@ import { execSync } from "node:child_process";
20
18
  import {
21
19
  createAutoWorktree,
22
20
  mergeMilestoneToMain,
23
- mergeSliceToMilestone,
24
- shouldUseWorktreeIsolation,
25
21
  } from "../auto-worktree.ts";
26
22
  import { getSliceBranchName } from "../worktree.ts";
27
- import { abortAndReset, withMergeHeal, MergeConflictError } from "../git-self-heal.ts";
23
+ import { abortAndReset } from "../git-self-heal.ts";
28
24
  import { runGSDDoctor } from "../doctor.ts";
29
25
  import { createTestContext } from "./test-helpers.ts";
30
26
 
@@ -60,11 +56,11 @@ function makeRoadmap(
60
56
  }
61
57
 
62
58
  function addSliceToMilestone(
63
- repo: string,
59
+ _repo: string,
64
60
  wtPath: string,
65
61
  milestoneId: string,
66
62
  sliceId: string,
67
- sliceTitle: string,
63
+ _sliceTitle: string,
68
64
  commits: Array<{ file: string; content: string; message: string }>,
69
65
  ): void {
70
66
  const normalizedPath = wtPath.replaceAll("\\", "/");
@@ -81,7 +77,7 @@ function addSliceToMilestone(
81
77
  run(`git commit -m "${c.message}"`, wtPath);
82
78
  }
83
79
  run(`git checkout milestone/${milestoneId}`, wtPath);
84
- mergeSliceToMilestone(repo, milestoneId, sliceId, sliceTitle);
80
+ run(`git merge --no-ff ${sliceBranch} -m "merge ${sliceId}"`, wtPath);
85
81
  }
86
82
 
87
83
  async function main(): Promise<void> {
@@ -144,59 +140,10 @@ async function main(): Promise<void> {
144
140
  }
145
141
 
146
142
  // ================================================================
147
- // Group 2: Preference gating (shouldUseWorktreeIsolation)
148
- // ================================================================
149
- console.log("\n=== Preference gating ===");
150
- {
151
- const repo = createTempRepo();
152
- tempDirs.push(repo);
153
-
154
- // Override to branch mode
155
- const branchResult = shouldUseWorktreeIsolation(repo, { isolation: "branch" });
156
- assertEq(branchResult, false, "isolation=branch returns false");
157
-
158
- // Override to worktree mode
159
- const wtResult = shouldUseWorktreeIsolation(repo, { isolation: "worktree" });
160
- assertEq(wtResult, true, "isolation=worktree returns true");
161
-
162
- // Default (no legacy branches) returns true
163
- const defaultResult = shouldUseWorktreeIsolation(repo);
164
- assertEq(defaultResult, true, "new project defaults to worktree (true)");
165
- }
166
-
167
- // ================================================================
168
- // Group 3: merge_to_main mode resolution
169
- // ================================================================
170
- console.log("\n=== merge_to_main mode ===");
171
- {
172
- // getMergeToMainMode reads from loadEffectiveGSDPreferences — test via legacy branch detection
173
- // Instead, test that the function returns the default "milestone" when no prefs set
174
- // (Cannot inject overridePrefs — function signature doesn't accept them)
175
- // We verify the shouldUseWorktreeIsolation override path handles legacy detection
176
- const repo = createTempRepo();
177
- tempDirs.push(repo);
178
-
179
- // Create a legacy gsd/*/* branch to test legacy detection
180
- run("git checkout -b gsd/M001/S01", repo);
181
- writeFileSync(join(repo, "legacy.txt"), "legacy\n");
182
- run("git add .", repo);
183
- run("git commit -m legacy", repo);
184
- run("git checkout main", repo);
185
-
186
- const legacyResult = shouldUseWorktreeIsolation(repo);
187
- assertEq(legacyResult, false, "legacy gsd branches detected -> branch mode");
188
-
189
- // Explicit worktree override wins over legacy detection
190
- const overrideResult = shouldUseWorktreeIsolation(repo, { isolation: "worktree" });
191
- assertEq(overrideResult, true, "explicit worktree override wins over legacy");
192
- }
193
-
194
- // ================================================================
195
- // Group 4: Self-heal (abortAndReset, withMergeHeal)
143
+ // Group 2: Self-heal (abortAndReset)
196
144
  // ================================================================
197
145
  console.log("\n=== Self-heal ===");
198
146
  {
199
- // 4a: abortAndReset cleans up MERGE_HEAD
200
147
  const repo = createTempRepo();
201
148
  tempDirs.push(repo);
202
149
 
@@ -218,37 +165,13 @@ async function main(): Promise<void> {
218
165
  assertTrue(!existsSync(join(repo, ".git", "MERGE_HEAD")), "MERGE_HEAD removed after abort");
219
166
  assertTrue(abortResult.cleaned.length > 0, "abortAndReset reports cleaned items");
220
167
  }
221
- {
222
- // 4b: withMergeHeal throws MergeConflictError for real conflicts
223
- const repo = createTempRepo();
224
- tempDirs.push(repo);
225
-
226
- run("git checkout -b conflict-branch", repo);
227
- writeFileSync(join(repo, "file.txt"), "branch version\n");
228
- run("git add .", repo);
229
- run("git commit -m branch-ver", repo);
230
- run("git checkout main", repo);
231
- writeFileSync(join(repo, "file.txt"), "main version\n");
232
- run("git add .", repo);
233
- run("git commit -m main-ver", repo);
234
-
235
- let caughtError: unknown = null;
236
- try {
237
- withMergeHeal(repo, () => {
238
- execSync("git merge conflict-branch", { cwd: repo, stdio: "pipe" });
239
- });
240
- } catch (e) {
241
- caughtError = e;
242
- }
243
- assertTrue(caughtError instanceof MergeConflictError, "withMergeHeal throws MergeConflictError");
244
- if (caughtError instanceof MergeConflictError) {
245
- assertTrue(caughtError.conflictedFiles.length > 0, "MergeConflictError has conflictedFiles");
246
- }
247
- }
248
168
 
249
169
  // ================================================================
250
- // Group 5: Doctor detects orphaned worktrees
170
+ // Group 3: Doctor detects orphaned worktrees
171
+ // Skip on Windows: git worktree path resolution in temp dirs uses
172
+ // UNC/8.3 forms that don't match after normalization.
251
173
  // ================================================================
174
+ if (process.platform !== "win32") {
252
175
  console.log("\n=== Doctor: orphaned worktree detection ===");
253
176
  {
254
177
  // Build a repo with a completed milestone
@@ -279,7 +202,7 @@ Test
279
202
  _None_
280
203
  `);
281
204
  run("git add -A", repo);
282
- run("git commit -m 'add milestone'", repo);
205
+ run("git commit -m \"add milestone\"", repo);
283
206
 
284
207
  // Create orphaned worktree
285
208
  mkdirSync(join(repo, ".gsd", "worktrees"), { recursive: true });
@@ -302,6 +225,9 @@ _None_
302
225
  const wtList = run("git worktree list", repo);
303
226
  assertTrue(!wtList.includes("milestone/M001"), "worktree gone after doctor fix");
304
227
  }
228
+ } else {
229
+ console.log("\n=== Doctor: orphaned worktree detection (skipped on Windows) ===");
230
+ }
305
231
  } finally {
306
232
  process.chdir(savedCwd);
307
233
  for (const d of tempDirs) {