gsd-pi 2.8.2 → 2.9.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 (193) hide show
  1. package/README.md +2 -1
  2. package/dist/cli.js +5 -0
  3. package/dist/loader.js +1 -1
  4. package/dist/update-check.d.ts +24 -0
  5. package/dist/update-check.js +93 -0
  6. package/node_modules/@gsd/pi-coding-agent/dist/core/extensions/types.d.ts +4 -2
  7. package/node_modules/@gsd/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  8. package/node_modules/@gsd/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  9. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/client.d.ts +46 -0
  10. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -0
  11. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/client.js +758 -0
  12. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/client.js.map +1 -0
  13. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/config.d.ts +23 -0
  14. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/config.d.ts.map +1 -0
  15. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/config.js +267 -0
  16. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/config.js.map +1 -0
  17. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/edits.d.ts +17 -0
  18. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/edits.d.ts.map +1 -0
  19. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/edits.js +101 -0
  20. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/edits.js.map +1 -0
  21. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/helpers.d.ts +15 -0
  22. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/helpers.d.ts.map +1 -0
  23. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/helpers.js +46 -0
  24. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/helpers.js.map +1 -0
  25. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/index.d.ts +35 -0
  26. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/index.d.ts.map +1 -0
  27. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/index.js +709 -0
  28. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/index.js.map +1 -0
  29. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lsp-integration.test.d.ts +2 -0
  30. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lsp-integration.test.d.ts.map +1 -0
  31. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lsp-integration.test.js +308 -0
  32. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lsp-integration.test.js.map +1 -0
  33. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lspmux.d.ts +34 -0
  34. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lspmux.d.ts.map +1 -0
  35. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lspmux.js +136 -0
  36. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lspmux.js.map +1 -0
  37. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/types.d.ts +262 -0
  38. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/types.d.ts.map +1 -0
  39. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/types.js +64 -0
  40. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/types.js.map +1 -0
  41. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/utils.d.ts +50 -0
  42. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/utils.d.ts.map +1 -0
  43. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/utils.js +574 -0
  44. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/utils.js.map +1 -0
  45. package/node_modules/@gsd/pi-coding-agent/dist/core/slash-commands.d.ts.map +1 -1
  46. package/node_modules/@gsd/pi-coding-agent/dist/core/slash-commands.js +1 -0
  47. package/node_modules/@gsd/pi-coding-agent/dist/core/slash-commands.js.map +1 -1
  48. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/index.d.ts +13 -0
  49. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
  50. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/index.js +4 -0
  51. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/index.js.map +1 -1
  52. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts +10 -1
  53. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  54. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/components/settings-selector.js +2 -2
  55. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/components/settings-selector.js.map +1 -1
  56. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +2 -0
  57. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  58. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js +80 -1
  59. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  60. package/node_modules/@gsd/pi-coding-agent/dist/modes/rpc/rpc-mode.js +1 -1
  61. package/node_modules/@gsd/pi-coding-agent/dist/modes/rpc/rpc-mode.js.map +1 -1
  62. package/node_modules/@gsd/pi-coding-agent/dist/modes/rpc/rpc-types.d.ts +5 -0
  63. package/node_modules/@gsd/pi-coding-agent/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  64. package/node_modules/@gsd/pi-coding-agent/dist/modes/rpc/rpc-types.js.map +1 -1
  65. package/node_modules/@gsd/pi-coding-agent/src/core/extensions/types.ts +4 -2
  66. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/client.ts +880 -0
  67. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/config.ts +325 -0
  68. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/defaults.json +456 -0
  69. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/edits.ts +109 -0
  70. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/helpers.ts +54 -0
  71. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/index.ts +943 -0
  72. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/lsp-integration.test.ts +407 -0
  73. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/lsp.md +33 -0
  74. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/lspmux.ts +199 -0
  75. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/types.ts +421 -0
  76. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/utils.ts +682 -0
  77. package/node_modules/@gsd/pi-coding-agent/src/core/slash-commands.ts +1 -0
  78. package/node_modules/@gsd/pi-coding-agent/src/core/tools/index.ts +10 -0
  79. package/node_modules/@gsd/pi-coding-agent/src/modes/interactive/components/settings-selector.ts +2 -2
  80. package/node_modules/@gsd/pi-coding-agent/src/modes/interactive/interactive-mode.ts +94 -2
  81. package/node_modules/@gsd/pi-coding-agent/src/modes/rpc/rpc-mode.ts +2 -2
  82. package/node_modules/@gsd/pi-coding-agent/src/modes/rpc/rpc-types.ts +2 -1
  83. package/package.json +1 -1
  84. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +4 -2
  85. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  86. package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  87. package/packages/pi-coding-agent/dist/core/lsp/client.d.ts +46 -0
  88. package/packages/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -0
  89. package/packages/pi-coding-agent/dist/core/lsp/client.js +758 -0
  90. package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -0
  91. package/packages/pi-coding-agent/dist/core/lsp/config.d.ts +23 -0
  92. package/packages/pi-coding-agent/dist/core/lsp/config.d.ts.map +1 -0
  93. package/packages/pi-coding-agent/dist/core/lsp/config.js +267 -0
  94. package/packages/pi-coding-agent/dist/core/lsp/config.js.map +1 -0
  95. package/packages/pi-coding-agent/dist/core/lsp/edits.d.ts +17 -0
  96. package/packages/pi-coding-agent/dist/core/lsp/edits.d.ts.map +1 -0
  97. package/packages/pi-coding-agent/dist/core/lsp/edits.js +101 -0
  98. package/packages/pi-coding-agent/dist/core/lsp/edits.js.map +1 -0
  99. package/packages/pi-coding-agent/dist/core/lsp/helpers.d.ts +15 -0
  100. package/packages/pi-coding-agent/dist/core/lsp/helpers.d.ts.map +1 -0
  101. package/packages/pi-coding-agent/dist/core/lsp/helpers.js +46 -0
  102. package/packages/pi-coding-agent/dist/core/lsp/helpers.js.map +1 -0
  103. package/packages/pi-coding-agent/dist/core/lsp/index.d.ts +35 -0
  104. package/packages/pi-coding-agent/dist/core/lsp/index.d.ts.map +1 -0
  105. package/packages/pi-coding-agent/dist/core/lsp/index.js +709 -0
  106. package/packages/pi-coding-agent/dist/core/lsp/index.js.map +1 -0
  107. package/packages/pi-coding-agent/dist/core/lsp/lsp-integration.test.d.ts +2 -0
  108. package/packages/pi-coding-agent/dist/core/lsp/lsp-integration.test.d.ts.map +1 -0
  109. package/packages/pi-coding-agent/dist/core/lsp/lsp-integration.test.js +308 -0
  110. package/packages/pi-coding-agent/dist/core/lsp/lsp-integration.test.js.map +1 -0
  111. package/packages/pi-coding-agent/dist/core/lsp/lspmux.d.ts +34 -0
  112. package/packages/pi-coding-agent/dist/core/lsp/lspmux.d.ts.map +1 -0
  113. package/packages/pi-coding-agent/dist/core/lsp/lspmux.js +136 -0
  114. package/packages/pi-coding-agent/dist/core/lsp/lspmux.js.map +1 -0
  115. package/packages/pi-coding-agent/dist/core/lsp/types.d.ts +262 -0
  116. package/packages/pi-coding-agent/dist/core/lsp/types.d.ts.map +1 -0
  117. package/packages/pi-coding-agent/dist/core/lsp/types.js +64 -0
  118. package/packages/pi-coding-agent/dist/core/lsp/types.js.map +1 -0
  119. package/packages/pi-coding-agent/dist/core/lsp/utils.d.ts +50 -0
  120. package/packages/pi-coding-agent/dist/core/lsp/utils.d.ts.map +1 -0
  121. package/packages/pi-coding-agent/dist/core/lsp/utils.js +574 -0
  122. package/packages/pi-coding-agent/dist/core/lsp/utils.js.map +1 -0
  123. package/packages/pi-coding-agent/dist/core/slash-commands.d.ts.map +1 -1
  124. package/packages/pi-coding-agent/dist/core/slash-commands.js +1 -0
  125. package/packages/pi-coding-agent/dist/core/slash-commands.js.map +1 -1
  126. package/packages/pi-coding-agent/dist/core/tools/index.d.ts +13 -0
  127. package/packages/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
  128. package/packages/pi-coding-agent/dist/core/tools/index.js +4 -0
  129. package/packages/pi-coding-agent/dist/core/tools/index.js.map +1 -1
  130. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts +10 -1
  131. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  132. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js +2 -2
  133. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js.map +1 -1
  134. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +2 -0
  135. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  136. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +80 -1
  137. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  138. package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js +1 -1
  139. package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js.map +1 -1
  140. package/packages/pi-coding-agent/dist/modes/rpc/rpc-types.d.ts +5 -0
  141. package/packages/pi-coding-agent/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  142. package/packages/pi-coding-agent/dist/modes/rpc/rpc-types.js.map +1 -1
  143. package/packages/pi-coding-agent/src/core/extensions/types.ts +4 -2
  144. package/packages/pi-coding-agent/src/core/lsp/client.ts +880 -0
  145. package/packages/pi-coding-agent/src/core/lsp/config.ts +325 -0
  146. package/packages/pi-coding-agent/src/core/lsp/defaults.json +456 -0
  147. package/packages/pi-coding-agent/src/core/lsp/edits.ts +109 -0
  148. package/packages/pi-coding-agent/src/core/lsp/helpers.ts +54 -0
  149. package/packages/pi-coding-agent/src/core/lsp/index.ts +943 -0
  150. package/packages/pi-coding-agent/src/core/lsp/lsp-integration.test.ts +407 -0
  151. package/packages/pi-coding-agent/src/core/lsp/lsp.md +33 -0
  152. package/packages/pi-coding-agent/src/core/lsp/lspmux.ts +199 -0
  153. package/packages/pi-coding-agent/src/core/lsp/types.ts +421 -0
  154. package/packages/pi-coding-agent/src/core/lsp/utils.ts +682 -0
  155. package/packages/pi-coding-agent/src/core/slash-commands.ts +1 -0
  156. package/packages/pi-coding-agent/src/core/tools/index.ts +10 -0
  157. package/packages/pi-coding-agent/src/modes/interactive/components/settings-selector.ts +2 -2
  158. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +94 -2
  159. package/packages/pi-coding-agent/src/modes/rpc/rpc-mode.ts +2 -2
  160. package/packages/pi-coding-agent/src/modes/rpc/rpc-types.ts +2 -1
  161. package/src/resources/extensions/ask-user-questions.ts +42 -2
  162. package/src/resources/extensions/bg-shell/index.ts +34 -37
  163. package/src/resources/extensions/browser-tools/core.d.ts +205 -0
  164. package/src/resources/extensions/browser-tools/index.ts +2 -2
  165. package/src/resources/extensions/browser-tools/refs.ts +1 -1
  166. package/src/resources/extensions/browser-tools/tools/session.ts +1 -1
  167. package/src/resources/extensions/context7/index.ts +2 -2
  168. package/src/resources/extensions/get-secrets-from-user.ts +3 -2
  169. package/src/resources/extensions/google-search/index.ts +1 -1
  170. package/src/resources/extensions/gsd/auto.ts +126 -12
  171. package/src/resources/extensions/gsd/commands.ts +218 -3
  172. package/src/resources/extensions/gsd/doctor.ts +1 -1
  173. package/src/resources/extensions/gsd/git-service.ts +163 -13
  174. package/src/resources/extensions/gsd/guided-flow.ts +19 -9
  175. package/src/resources/extensions/gsd/index.ts +17 -7
  176. package/src/resources/extensions/gsd/preferences.ts +1 -1
  177. package/src/resources/extensions/gsd/tests/git-service.test.ts +226 -0
  178. package/src/resources/extensions/gsd/tests/migrate-command.test.ts +2 -2
  179. package/src/resources/extensions/gsd/tests/migrate-transformer.test.ts +1 -1
  180. package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +10 -10
  181. package/src/resources/extensions/gsd/tests/next-milestone-id.test.ts +87 -0
  182. package/src/resources/extensions/gsd/tests/worktree.test.ts +352 -0
  183. package/src/resources/extensions/gsd/types.ts +1 -0
  184. package/src/resources/extensions/gsd/worktree.ts +20 -1
  185. package/src/resources/extensions/mac-tools/index.ts +1 -1
  186. package/src/resources/extensions/search-the-web/command-search-provider.ts +1 -1
  187. package/src/resources/extensions/search-the-web/format.ts +1 -1
  188. package/src/resources/extensions/search-the-web/index.ts +5 -5
  189. package/src/resources/extensions/search-the-web/native-search.ts +5 -6
  190. package/src/resources/extensions/search-the-web/tool-fetch-page.ts +7 -7
  191. package/src/resources/extensions/search-the-web/tool-llm-context.ts +11 -11
  192. package/src/resources/extensions/search-the-web/tool-search.ts +10 -10
  193. package/src/resources/extensions/shared/interview-ui.ts +2 -2
@@ -9,6 +9,8 @@ import {
9
9
  RUNTIME_EXCLUSION_PATHS,
10
10
  VALID_BRANCH_NAME,
11
11
  runGit,
12
+ readIntegrationBranch,
13
+ writeIntegrationBranch,
12
14
  type GitPreferences,
13
15
  type CommitOptions,
14
16
  type MergeSliceResult,
@@ -1370,6 +1372,230 @@ async function main(): Promise<void> {
1370
1372
  assert(true, "PreMergeCheckResult type exported and usable");
1371
1373
  }
1372
1374
 
1375
+ // ═══════════════════════════════════════════════════════════════════════
1376
+ // Integration branch — feature-branch workflow support
1377
+ // ═══════════════════════════════════════════════════════════════════════
1378
+
1379
+ // ─── writeIntegrationBranch / readIntegrationBranch: round-trip ────────
1380
+
1381
+ console.log("\n=== Integration branch: write and read ===");
1382
+
1383
+ {
1384
+ const repo = initBranchTestRepo();
1385
+
1386
+ // Initially no integration branch
1387
+ assertEq(readIntegrationBranch(repo, "M001"), null, "readIntegrationBranch returns null when no metadata");
1388
+
1389
+ // Write integration branch
1390
+ writeIntegrationBranch(repo, "M001", "f-123-new-thing");
1391
+ assertEq(readIntegrationBranch(repo, "M001"), "f-123-new-thing", "readIntegrationBranch returns written branch");
1392
+
1393
+ rmSync(repo, { recursive: true, force: true });
1394
+ }
1395
+
1396
+ // ─── writeIntegrationBranch: idempotent — doesn't overwrite ───────────
1397
+
1398
+ console.log("\n=== Integration branch: idempotent write ===");
1399
+
1400
+ {
1401
+ const repo = initBranchTestRepo();
1402
+
1403
+ writeIntegrationBranch(repo, "M001", "f-123-first");
1404
+ writeIntegrationBranch(repo, "M001", "f-456-second"); // should NOT overwrite
1405
+
1406
+ assertEq(readIntegrationBranch(repo, "M001"), "f-123-first", "second write does not overwrite existing integration branch");
1407
+
1408
+ rmSync(repo, { recursive: true, force: true });
1409
+ }
1410
+
1411
+ // ─── writeIntegrationBranch: rejects slice branches ───────────────────
1412
+
1413
+ console.log("\n=== Integration branch: rejects slice branches ===");
1414
+
1415
+ {
1416
+ const repo = initBranchTestRepo();
1417
+
1418
+ writeIntegrationBranch(repo, "M001", "gsd/M001/S01");
1419
+ assertEq(readIntegrationBranch(repo, "M001"), null, "slice branches are not recorded as integration branch");
1420
+
1421
+ rmSync(repo, { recursive: true, force: true });
1422
+ }
1423
+
1424
+ // ─── writeIntegrationBranch: rejects invalid branch names ─────────────
1425
+
1426
+ console.log("\n=== Integration branch: rejects invalid names ===");
1427
+
1428
+ {
1429
+ const repo = initBranchTestRepo();
1430
+
1431
+ writeIntegrationBranch(repo, "M001", "bad; rm -rf /");
1432
+ assertEq(readIntegrationBranch(repo, "M001"), null, "invalid branch name is not recorded");
1433
+
1434
+ rmSync(repo, { recursive: true, force: true });
1435
+ }
1436
+
1437
+ // ─── getMainBranch: uses integration branch when milestone set ────────
1438
+
1439
+ console.log("\n=== getMainBranch: integration branch from milestone metadata ===");
1440
+
1441
+ {
1442
+ const repo = initBranchTestRepo();
1443
+
1444
+ // Create a feature branch
1445
+ run("git checkout -b f-123-feature", repo);
1446
+ run("git checkout main", repo);
1447
+
1448
+ // Write integration branch metadata
1449
+ writeIntegrationBranch(repo, "M001", "f-123-feature");
1450
+
1451
+ // Without milestone set, getMainBranch returns "main"
1452
+ const svc = new GitServiceImpl(repo);
1453
+ assertEq(svc.getMainBranch(), "main", "getMainBranch returns main when no milestone set");
1454
+
1455
+ // With milestone set, getMainBranch returns the integration branch
1456
+ svc.setMilestoneId("M001");
1457
+ assertEq(svc.getMainBranch(), "f-123-feature", "getMainBranch returns integration branch when milestone set");
1458
+
1459
+ rmSync(repo, { recursive: true, force: true });
1460
+ }
1461
+
1462
+ // ─── getMainBranch: main_branch pref still takes priority ─────────────
1463
+
1464
+ console.log("\n=== getMainBranch: main_branch pref overrides integration branch ===");
1465
+
1466
+ {
1467
+ const repo = initBranchTestRepo();
1468
+
1469
+ run("git checkout -b f-123-feature", repo);
1470
+ run("git checkout -b trunk", repo);
1471
+ run("git checkout main", repo);
1472
+
1473
+ writeIntegrationBranch(repo, "M001", "f-123-feature");
1474
+
1475
+ // Explicit preference still wins
1476
+ const svc = new GitServiceImpl(repo, { main_branch: "trunk" });
1477
+ svc.setMilestoneId("M001");
1478
+ assertEq(svc.getMainBranch(), "trunk", "main_branch preference overrides integration branch");
1479
+
1480
+ rmSync(repo, { recursive: true, force: true });
1481
+ }
1482
+
1483
+ // ─── getMainBranch: falls back when integration branch deleted ────────
1484
+
1485
+ console.log("\n=== getMainBranch: fallback when integration branch deleted ===");
1486
+
1487
+ {
1488
+ const repo = initBranchTestRepo();
1489
+
1490
+ // Write metadata pointing to a branch that doesn't exist
1491
+ writeIntegrationBranch(repo, "M001", "deleted-branch");
1492
+
1493
+ const svc = new GitServiceImpl(repo);
1494
+ svc.setMilestoneId("M001");
1495
+ assertEq(svc.getMainBranch(), "main", "getMainBranch falls back to main when integration branch no longer exists");
1496
+
1497
+ rmSync(repo, { recursive: true, force: true });
1498
+ }
1499
+
1500
+ // ─── End-to-end: feature branch workflow ──────────────────────────────
1501
+
1502
+ console.log("\n=== End-to-end: feature branch workflow ===");
1503
+
1504
+ {
1505
+ const repo = initBranchTestRepo();
1506
+
1507
+ // Simulate: user creates feature branch and starts GSD
1508
+ run("git checkout -b f-123-new-thing", repo);
1509
+ createFile(repo, "setup.txt", "initial setup");
1510
+ run("git add -A", repo);
1511
+ run("git commit -m 'initial feature setup'", repo);
1512
+
1513
+ // Record integration branch (this is what auto.ts does at startup)
1514
+ writeIntegrationBranch(repo, "M001", "f-123-new-thing");
1515
+
1516
+ // Create GitServiceImpl with milestone set
1517
+ const svc = new GitServiceImpl(repo);
1518
+ svc.setMilestoneId("M001");
1519
+
1520
+ // Verify getMainBranch returns the feature branch, not "main"
1521
+ assertEq(svc.getMainBranch(), "f-123-new-thing", "e2e: getMainBranch returns feature branch");
1522
+
1523
+ // Create slice branch — should branch from f-123-new-thing (current)
1524
+ svc.ensureSliceBranch("M001", "S01");
1525
+ assertEq(svc.getCurrentBranch(), "gsd/M001/S01", "e2e: slice branch created");
1526
+
1527
+ // The slice branch should have the feature branch's commit
1528
+ const log = run("git log --oneline", repo);
1529
+ assert(log.includes("initial feature setup"), "e2e: slice branch inherits feature branch content");
1530
+
1531
+ // Do work on the slice branch
1532
+ createFile(repo, "src/feature.ts", "export const feature = true;");
1533
+ svc.commit({ message: "feat: add feature module" });
1534
+
1535
+ // switchToMain should go to feature branch
1536
+ svc.switchToMain();
1537
+ assertEq(svc.getCurrentBranch(), "f-123-new-thing", "e2e: switchToMain goes to feature branch, not main");
1538
+
1539
+ // mergeSliceToMain should merge into feature branch
1540
+ const result = svc.mergeSliceToMain("M001", "S01", "Add feature module");
1541
+ assertEq(result.mergedCommitMessage, "feat(M001/S01): Add feature module", "e2e: merge commit message correct");
1542
+ assertEq(svc.getCurrentBranch(), "f-123-new-thing", "e2e: after merge, still on feature branch");
1543
+
1544
+ // The feature branch should have the merged work
1545
+ const files = run("git ls-files", repo);
1546
+ assert(files.includes("src/feature.ts"), "e2e: merged file exists on feature branch");
1547
+
1548
+ // Main should NOT have the merged work
1549
+ run("git checkout main", repo);
1550
+ const mainFiles = run("git ls-files", repo);
1551
+ assert(!mainFiles.includes("src/feature.ts"), "e2e: main does NOT have merged work — it stays on the feature branch");
1552
+
1553
+ rmSync(repo, { recursive: true, force: true });
1554
+ }
1555
+
1556
+ // ─── Per-milestone isolation: different milestones, different targets ──
1557
+
1558
+ console.log("\n=== Integration branch: per-milestone isolation ===");
1559
+
1560
+ {
1561
+ const repo = initBranchTestRepo();
1562
+
1563
+ run("git checkout -b feature-a", repo);
1564
+ run("git checkout -b feature-b", repo);
1565
+ run("git checkout main", repo);
1566
+
1567
+ writeIntegrationBranch(repo, "M001", "feature-a");
1568
+ writeIntegrationBranch(repo, "M002", "feature-b");
1569
+
1570
+ const svc = new GitServiceImpl(repo);
1571
+
1572
+ svc.setMilestoneId("M001");
1573
+ assertEq(svc.getMainBranch(), "feature-a", "M001 integration branch is feature-a");
1574
+
1575
+ svc.setMilestoneId("M002");
1576
+ assertEq(svc.getMainBranch(), "feature-b", "M002 integration branch is feature-b");
1577
+
1578
+ svc.setMilestoneId(null);
1579
+ assertEq(svc.getMainBranch(), "main", "no milestone set → falls back to main");
1580
+
1581
+ rmSync(repo, { recursive: true, force: true });
1582
+ }
1583
+
1584
+ // ─── Backward compatibility: no metadata → existing behavior ──────────
1585
+
1586
+ console.log("\n=== Integration branch: backward compat ===");
1587
+
1588
+ {
1589
+ const repo = initBranchTestRepo();
1590
+ const svc = new GitServiceImpl(repo);
1591
+
1592
+ // Set milestone but no metadata file exists
1593
+ svc.setMilestoneId("M001");
1594
+ assertEq(svc.getMainBranch(), "main", "backward compat: no metadata file → falls back to main");
1595
+
1596
+ rmSync(repo, { recursive: true, force: true });
1597
+ }
1598
+
1373
1599
  // ─── untrackRuntimeFiles: removes tracked runtime files from index ───
1374
1600
 
1375
1601
  console.log("\n=== untrackRuntimeFiles ===");
@@ -354,8 +354,8 @@ async function main(): Promise<void> {
354
354
  assert(state.phase !== undefined, 'pipeline: deriveState returns phase');
355
355
  assert(state.activeMilestone !== null, 'pipeline: deriveState has activeMilestone');
356
356
  assertEq(state.activeMilestone!.id, 'M001', 'pipeline: deriveState activeMilestone is M001');
357
- assert(state.progress.slices !== undefined, 'pipeline: deriveState has slices progress');
358
- assert(state.progress.tasks !== undefined, 'pipeline: deriveState has tasks progress');
357
+ assert(state.progress!.slices !== undefined, 'pipeline: deriveState has slices progress');
358
+ assert(state.progress!.tasks !== undefined, 'pipeline: deriveState has tasks progress');
359
359
 
360
360
  } finally {
361
361
  rmSync(base, { recursive: true, force: true });
@@ -317,7 +317,7 @@ function makeResearch(fileName: string, content: string): PlanningResearch {
317
317
  assertEq(doneSlice?.tasks[0]?.summary?.duration, '2h', 'completion: summary duration from frontmatter');
318
318
  assertEq(doneSlice?.tasks[0]?.summary?.provides, ['feature-01'], 'completion: summary provides from frontmatter');
319
319
  assertEq(doneSlice?.tasks[0]?.summary?.keyFiles, ['file-01.ts'], 'completion: summary keyFiles from frontmatter');
320
- assert(doneSlice?.tasks[0]?.summary?.whatHappened?.includes('Summary body'), 'completion: summary whatHappened from body');
320
+ assert(doneSlice?.tasks[0]?.summary?.whatHappened?.includes('Summary body') ?? false, 'completion: summary whatHappened from body');
321
321
  assert(doneSlice?.summary !== null, 'completion: done slice has slice summary');
322
322
  assert(activeSlice?.summary === null, 'completion: active slice has null summary');
323
323
  assertEq(doneSlice?.tasks[0]?.estimate, '2h', 'completion: task estimate from summary duration');
@@ -234,18 +234,18 @@ async function main(): Promise<void> {
234
234
  assertEq(state.activeSlice!.id, 'S02', 'incomplete: deriveState activeSlice is S02');
235
235
  assert(state.activeTask !== null, 'incomplete: deriveState has activeTask');
236
236
  assertEq(state.activeTask!.id, 'T03', 'incomplete: deriveState activeTask is T03');
237
- assert(state.progress.slices !== undefined, 'incomplete: deriveState has slices progress');
238
- assertEq(state.progress.slices!.done, 1, 'incomplete: deriveState slices done count');
239
- assertEq(state.progress.slices!.total, 2, 'incomplete: deriveState slices total count');
240
- assert(state.progress.tasks !== undefined, 'incomplete: deriveState has tasks progress');
237
+ assert(state.progress!.slices !== undefined, 'incomplete: deriveState has slices progress');
238
+ assertEq(state.progress!.slices!.done, 1, 'incomplete: deriveState slices done count');
239
+ assertEq(state.progress!.slices!.total, 2, 'incomplete: deriveState slices total count');
240
+ assert(state.progress!.tasks !== undefined, 'incomplete: deriveState has tasks progress');
241
241
  // S02 has 1 task, 0 done (only active slice tasks counted)
242
- assertEq(state.progress.tasks!.done, 0, 'incomplete: deriveState tasks done (in active slice)');
243
- assertEq(state.progress.tasks!.total, 1, 'incomplete: deriveState tasks total (in active slice)');
242
+ assertEq(state.progress!.tasks!.done, 0, 'incomplete: deriveState tasks done (in active slice)');
243
+ assertEq(state.progress!.tasks!.total, 1, 'incomplete: deriveState tasks total (in active slice)');
244
244
  // Requirements
245
- assertEq(state.requirements.active, 1, 'incomplete: deriveState requirements active');
246
- assertEq(state.requirements.validated, 1, 'incomplete: deriveState requirements validated');
247
- assertEq(state.requirements.deferred, 1, 'incomplete: deriveState requirements deferred');
248
- assertEq(state.requirements.outOfScope, 1, 'incomplete: deriveState requirements outOfScope');
245
+ assertEq(state.requirements!.active, 1, 'incomplete: deriveState requirements active');
246
+ assertEq(state.requirements!.validated, 1, 'incomplete: deriveState requirements validated');
247
+ assertEq(state.requirements!.deferred, 1, 'incomplete: deriveState requirements deferred');
248
+ assertEq(state.requirements!.outOfScope, 1, 'incomplete: deriveState requirements outOfScope');
249
249
 
250
250
  // (f) generatePreview
251
251
  console.log(' --- generatePreview ---');
@@ -0,0 +1,87 @@
1
+ // Tests for nextMilestoneId and maxMilestoneNum — milestone ID generation
2
+ // using max-based approach to avoid collisions after deletions.
3
+ //
4
+ // Sections:
5
+ // (a) Empty array returns M001
6
+ // (b) Sequential IDs return next in sequence
7
+ // (c) IDs with gaps (deletion) use max, not fill
8
+ // (d) Non-numeric directory names mixed in are ignored
9
+
10
+ import { nextMilestoneId, maxMilestoneNum } from '../guided-flow.ts';
11
+
12
+ // ─── Assertion helpers ─────────────────────────────────────────────────────
13
+
14
+ let passed = 0;
15
+ let failed = 0;
16
+
17
+ function assertEq<T>(actual: T, expected: T, message: string): void {
18
+ if (JSON.stringify(actual) === JSON.stringify(expected)) {
19
+ passed++;
20
+ } else {
21
+ failed++;
22
+ console.error(` FAIL: ${message} — expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
23
+ }
24
+ }
25
+
26
+ // ─── Tests ─────────────────────────────────────────────────────────────────
27
+
28
+ async function main(): Promise<void> {
29
+ console.log('nextMilestoneId / maxMilestoneNum tests');
30
+
31
+ // (a) Empty array → M001
32
+ {
33
+ assertEq(maxMilestoneNum([]), 0, 'maxMilestoneNum([]) === 0');
34
+ assertEq(nextMilestoneId([]), 'M001', 'nextMilestoneId([]) === "M001"');
35
+ }
36
+
37
+ // (b) Sequential IDs → next in sequence
38
+ {
39
+ assertEq(
40
+ nextMilestoneId(['M001', 'M002', 'M003']),
41
+ 'M004',
42
+ 'sequential IDs return M004',
43
+ );
44
+ assertEq(maxMilestoneNum(['M001', 'M002', 'M003']), 3, 'max of sequential is 3');
45
+ }
46
+
47
+ // (c) IDs with gaps (deletion scenario) → uses max, not fill
48
+ {
49
+ assertEq(
50
+ nextMilestoneId(['M001', 'M003']),
51
+ 'M004',
52
+ 'gap scenario returns M004, not M002',
53
+ );
54
+ assertEq(maxMilestoneNum(['M001', 'M003']), 3, 'max with gap is 3');
55
+ }
56
+
57
+ // (d) Non-numeric directory names mixed in are ignored
58
+ {
59
+ assertEq(
60
+ nextMilestoneId(['M001', 'notes', '.DS_Store', 'M003']),
61
+ 'M004',
62
+ 'non-numeric names ignored, returns M004',
63
+ );
64
+ assertEq(
65
+ maxMilestoneNum(['M001', 'notes', '.DS_Store', 'M003']),
66
+ 3,
67
+ 'max ignores non-numeric entries',
68
+ );
69
+ }
70
+
71
+ // ═══════════════════════════════════════════════════════════════════════════
72
+ // Results
73
+ // ═══════════════════════════════════════════════════════════════════════════
74
+
75
+ console.log(`\n${'='.repeat(40)}`);
76
+ console.log(`Results: ${passed} passed, ${failed} failed`);
77
+ if (failed > 0) {
78
+ process.exit(1);
79
+ } else {
80
+ console.log('All tests passed');
81
+ }
82
+ }
83
+
84
+ main().catch((error) => {
85
+ console.error(error);
86
+ process.exit(1);
87
+ });