gsd-pi 2.16.0 → 2.18.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 (225) hide show
  1. package/README.md +39 -0
  2. package/dist/onboarding.js +2 -2
  3. package/dist/remote-questions-config.d.ts +10 -0
  4. package/dist/remote-questions-config.js +36 -0
  5. package/dist/resources/extensions/gsd/activity-log.ts +37 -7
  6. package/dist/resources/extensions/gsd/auto-dashboard.ts +4 -0
  7. package/dist/resources/extensions/gsd/auto-dispatch.ts +9 -3
  8. package/dist/resources/extensions/gsd/auto-prompts.ts +91 -42
  9. package/dist/resources/extensions/gsd/auto-recovery.ts +7 -2
  10. package/dist/resources/extensions/gsd/auto-worktree.ts +33 -4
  11. package/dist/resources/extensions/gsd/auto.ts +177 -25
  12. package/dist/resources/extensions/gsd/commands.ts +264 -23
  13. package/dist/resources/extensions/gsd/complexity.ts +236 -0
  14. package/dist/resources/extensions/gsd/dispatch-guard.ts +7 -19
  15. package/dist/resources/extensions/gsd/docs/preferences-reference.md +202 -2
  16. package/dist/resources/extensions/gsd/files.ts +129 -3
  17. package/dist/resources/extensions/gsd/git-service.ts +19 -8
  18. package/dist/resources/extensions/gsd/gitignore.ts +41 -2
  19. package/dist/resources/extensions/gsd/guided-flow.ts +247 -10
  20. package/dist/resources/extensions/gsd/index.ts +47 -3
  21. package/dist/resources/extensions/gsd/metrics.ts +44 -0
  22. package/dist/resources/extensions/gsd/native-git-bridge.ts +5 -0
  23. package/dist/resources/extensions/gsd/native-parser-bridge.ts +5 -0
  24. package/dist/resources/extensions/gsd/paths.ts +9 -0
  25. package/dist/resources/extensions/gsd/preferences.ts +181 -2
  26. package/dist/resources/extensions/gsd/prompts/execute-task.md +6 -5
  27. package/dist/resources/extensions/gsd/prompts/system.md +2 -0
  28. package/dist/resources/extensions/gsd/queue-order.ts +231 -0
  29. package/dist/resources/extensions/gsd/queue-reorder-ui.ts +263 -0
  30. package/dist/resources/extensions/gsd/routing-history.ts +290 -0
  31. package/dist/resources/extensions/gsd/state.ts +15 -3
  32. package/dist/resources/extensions/gsd/templates/knowledge.md +19 -0
  33. package/dist/resources/extensions/gsd/templates/preferences.md +14 -0
  34. package/dist/resources/extensions/gsd/tests/auto-recovery.test.ts +50 -0
  35. package/dist/resources/extensions/gsd/tests/auto-worktree.test.ts +20 -0
  36. package/dist/resources/extensions/gsd/tests/budget-prediction.test.ts +220 -0
  37. package/dist/resources/extensions/gsd/tests/complexity-routing.test.ts +294 -0
  38. package/dist/resources/extensions/gsd/tests/context-compression.test.ts +180 -0
  39. package/dist/resources/extensions/gsd/tests/derive-state-deps.test.ts +99 -0
  40. package/dist/resources/extensions/gsd/tests/git-service.test.ts +132 -0
  41. package/dist/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +79 -0
  42. package/dist/resources/extensions/gsd/tests/knowledge.test.ts +161 -0
  43. package/dist/resources/extensions/gsd/tests/memory-leak-guards.test.ts +87 -0
  44. package/dist/resources/extensions/gsd/tests/preferences-git.test.ts +28 -0
  45. package/dist/resources/extensions/gsd/tests/preferences-wizard-fields.test.ts +168 -0
  46. package/dist/resources/extensions/gsd/tests/queue-order.test.ts +204 -0
  47. package/dist/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +281 -0
  48. package/dist/resources/extensions/gsd/tests/routing-history.test.ts +87 -0
  49. package/dist/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +139 -0
  50. package/dist/resources/extensions/gsd/tests/stop-auto-remote.test.ts +130 -0
  51. package/dist/resources/extensions/gsd/tests/token-profile.test.ts +263 -0
  52. package/dist/resources/extensions/gsd/types.ts +28 -0
  53. package/dist/resources/extensions/gsd/worktree-manager.ts +8 -5
  54. package/dist/resources/extensions/gsd/worktree.ts +24 -2
  55. package/dist/resources/extensions/shared/next-action-ui.ts +16 -1
  56. package/package.json +1 -1
  57. package/packages/pi-ai/dist/models.generated.d.ts +493 -13
  58. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  59. package/packages/pi-ai/dist/models.generated.js +422 -62
  60. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  61. package/packages/pi-ai/dist/providers/google-shared.d.ts +12 -0
  62. package/packages/pi-ai/dist/providers/google-shared.d.ts.map +1 -1
  63. package/packages/pi-ai/dist/providers/google-shared.js +9 -22
  64. package/packages/pi-ai/dist/providers/google-shared.js.map +1 -1
  65. package/packages/pi-ai/dist/providers/google-shared.test.d.ts +2 -0
  66. package/packages/pi-ai/dist/providers/google-shared.test.d.ts.map +1 -0
  67. package/packages/pi-ai/dist/providers/google-shared.test.js +125 -0
  68. package/packages/pi-ai/dist/providers/google-shared.test.js.map +1 -0
  69. package/packages/pi-ai/src/models.generated.ts +422 -62
  70. package/packages/pi-ai/src/providers/google-shared.test.ts +137 -0
  71. package/packages/pi-ai/src/providers/google-shared.ts +10 -19
  72. package/packages/pi-coding-agent/dist/cli/args.d.ts +5 -0
  73. package/packages/pi-coding-agent/dist/cli/args.d.ts.map +1 -1
  74. package/packages/pi-coding-agent/dist/cli/args.js +21 -0
  75. package/packages/pi-coding-agent/dist/cli/args.js.map +1 -1
  76. package/packages/pi-coding-agent/dist/cli/list-models.d.ts +14 -3
  77. package/packages/pi-coding-agent/dist/cli/list-models.d.ts.map +1 -1
  78. package/packages/pi-coding-agent/dist/cli/list-models.js +52 -17
  79. package/packages/pi-coding-agent/dist/cli/list-models.js.map +1 -1
  80. package/packages/pi-coding-agent/dist/core/discovery-cache.d.ts +27 -0
  81. package/packages/pi-coding-agent/dist/core/discovery-cache.d.ts.map +1 -0
  82. package/packages/pi-coding-agent/dist/core/discovery-cache.js +79 -0
  83. package/packages/pi-coding-agent/dist/core/discovery-cache.js.map +1 -0
  84. package/packages/pi-coding-agent/dist/core/discovery-cache.test.d.ts +2 -0
  85. package/packages/pi-coding-agent/dist/core/discovery-cache.test.d.ts.map +1 -0
  86. package/packages/pi-coding-agent/dist/core/discovery-cache.test.js +140 -0
  87. package/packages/pi-coding-agent/dist/core/discovery-cache.test.js.map +1 -0
  88. package/packages/pi-coding-agent/dist/core/model-discovery.d.ts +35 -0
  89. package/packages/pi-coding-agent/dist/core/model-discovery.d.ts.map +1 -0
  90. package/packages/pi-coding-agent/dist/core/model-discovery.js +162 -0
  91. package/packages/pi-coding-agent/dist/core/model-discovery.js.map +1 -0
  92. package/packages/pi-coding-agent/dist/core/model-discovery.test.d.ts +2 -0
  93. package/packages/pi-coding-agent/dist/core/model-discovery.test.d.ts.map +1 -0
  94. package/packages/pi-coding-agent/dist/core/model-discovery.test.js +100 -0
  95. package/packages/pi-coding-agent/dist/core/model-discovery.test.js.map +1 -0
  96. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.d.ts +2 -0
  97. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.d.ts.map +1 -0
  98. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js +113 -0
  99. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js.map +1 -0
  100. package/packages/pi-coding-agent/dist/core/model-registry.d.ts +26 -0
  101. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  102. package/packages/pi-coding-agent/dist/core/model-registry.js +98 -0
  103. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  104. package/packages/pi-coding-agent/dist/core/models-json-writer.d.ts +62 -0
  105. package/packages/pi-coding-agent/dist/core/models-json-writer.d.ts.map +1 -0
  106. package/packages/pi-coding-agent/dist/core/models-json-writer.js +145 -0
  107. package/packages/pi-coding-agent/dist/core/models-json-writer.js.map +1 -0
  108. package/packages/pi-coding-agent/dist/core/models-json-writer.test.d.ts +2 -0
  109. package/packages/pi-coding-agent/dist/core/models-json-writer.test.d.ts.map +1 -0
  110. package/packages/pi-coding-agent/dist/core/models-json-writer.test.js +118 -0
  111. package/packages/pi-coding-agent/dist/core/models-json-writer.test.js.map +1 -0
  112. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +9 -0
  113. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  114. package/packages/pi-coding-agent/dist/core/settings-manager.js +11 -0
  115. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  116. package/packages/pi-coding-agent/dist/core/slash-commands.d.ts.map +1 -1
  117. package/packages/pi-coding-agent/dist/core/slash-commands.js +1 -0
  118. package/packages/pi-coding-agent/dist/core/slash-commands.js.map +1 -1
  119. package/packages/pi-coding-agent/dist/core/tools/edit-diff.d.ts +7 -7
  120. package/packages/pi-coding-agent/dist/core/tools/edit-diff.d.ts.map +1 -1
  121. package/packages/pi-coding-agent/dist/core/tools/edit-diff.js +209 -13
  122. package/packages/pi-coding-agent/dist/core/tools/edit-diff.js.map +1 -1
  123. package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.d.ts +2 -0
  124. package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.d.ts.map +1 -0
  125. package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.js +67 -0
  126. package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.js.map +1 -0
  127. package/packages/pi-coding-agent/dist/index.d.ts +5 -1
  128. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  129. package/packages/pi-coding-agent/dist/index.js +4 -1
  130. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  131. package/packages/pi-coding-agent/dist/main.d.ts.map +1 -1
  132. package/packages/pi-coding-agent/dist/main.js +17 -2
  133. package/packages/pi-coding-agent/dist/main.js.map +1 -1
  134. package/packages/pi-coding-agent/dist/modes/interactive/components/index.d.ts +1 -0
  135. package/packages/pi-coding-agent/dist/modes/interactive/components/index.d.ts.map +1 -1
  136. package/packages/pi-coding-agent/dist/modes/interactive/components/index.js +1 -0
  137. package/packages/pi-coding-agent/dist/modes/interactive/components/index.js.map +1 -1
  138. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js +1 -1
  139. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js.map +1 -1
  140. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts +25 -0
  141. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts.map +1 -0
  142. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js +121 -0
  143. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js.map +1 -0
  144. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +1 -0
  145. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  146. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +32 -0
  147. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  148. package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  149. package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.js +10 -0
  150. package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.js.map +1 -1
  151. package/packages/pi-coding-agent/src/cli/args.ts +21 -0
  152. package/packages/pi-coding-agent/src/cli/list-models.ts +70 -17
  153. package/packages/pi-coding-agent/src/core/discovery-cache.test.ts +170 -0
  154. package/packages/pi-coding-agent/src/core/discovery-cache.ts +97 -0
  155. package/packages/pi-coding-agent/src/core/model-discovery.test.ts +125 -0
  156. package/packages/pi-coding-agent/src/core/model-discovery.ts +231 -0
  157. package/packages/pi-coding-agent/src/core/model-registry-discovery.test.ts +135 -0
  158. package/packages/pi-coding-agent/src/core/model-registry.ts +107 -0
  159. package/packages/pi-coding-agent/src/core/models-json-writer.test.ts +145 -0
  160. package/packages/pi-coding-agent/src/core/models-json-writer.ts +188 -0
  161. package/packages/pi-coding-agent/src/core/settings-manager.ts +21 -0
  162. package/packages/pi-coding-agent/src/core/slash-commands.ts +1 -0
  163. package/packages/pi-coding-agent/src/core/tools/edit-diff.test.ts +85 -0
  164. package/packages/pi-coding-agent/src/core/tools/edit-diff.ts +245 -17
  165. package/packages/pi-coding-agent/src/index.ts +5 -0
  166. package/packages/pi-coding-agent/src/main.ts +19 -2
  167. package/packages/pi-coding-agent/src/modes/interactive/components/index.ts +1 -0
  168. package/packages/pi-coding-agent/src/modes/interactive/components/model-selector.ts +1 -1
  169. package/packages/pi-coding-agent/src/modes/interactive/components/provider-manager.ts +163 -0
  170. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +37 -0
  171. package/packages/pi-coding-agent/src/modes/interactive/theme/theme.ts +13 -0
  172. package/pkg/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  173. package/pkg/dist/modes/interactive/theme/theme.js +10 -0
  174. package/pkg/dist/modes/interactive/theme/theme.js.map +1 -1
  175. package/src/resources/extensions/gsd/activity-log.ts +37 -7
  176. package/src/resources/extensions/gsd/auto-dashboard.ts +4 -0
  177. package/src/resources/extensions/gsd/auto-dispatch.ts +9 -3
  178. package/src/resources/extensions/gsd/auto-prompts.ts +91 -42
  179. package/src/resources/extensions/gsd/auto-recovery.ts +7 -2
  180. package/src/resources/extensions/gsd/auto-worktree.ts +33 -4
  181. package/src/resources/extensions/gsd/auto.ts +177 -25
  182. package/src/resources/extensions/gsd/commands.ts +264 -23
  183. package/src/resources/extensions/gsd/complexity.ts +236 -0
  184. package/src/resources/extensions/gsd/dispatch-guard.ts +7 -19
  185. package/src/resources/extensions/gsd/docs/preferences-reference.md +202 -2
  186. package/src/resources/extensions/gsd/files.ts +129 -3
  187. package/src/resources/extensions/gsd/git-service.ts +19 -8
  188. package/src/resources/extensions/gsd/gitignore.ts +41 -2
  189. package/src/resources/extensions/gsd/guided-flow.ts +247 -10
  190. package/src/resources/extensions/gsd/index.ts +47 -3
  191. package/src/resources/extensions/gsd/metrics.ts +44 -0
  192. package/src/resources/extensions/gsd/native-git-bridge.ts +5 -0
  193. package/src/resources/extensions/gsd/native-parser-bridge.ts +5 -0
  194. package/src/resources/extensions/gsd/paths.ts +9 -0
  195. package/src/resources/extensions/gsd/preferences.ts +181 -2
  196. package/src/resources/extensions/gsd/prompts/execute-task.md +6 -5
  197. package/src/resources/extensions/gsd/prompts/system.md +2 -0
  198. package/src/resources/extensions/gsd/queue-order.ts +231 -0
  199. package/src/resources/extensions/gsd/queue-reorder-ui.ts +263 -0
  200. package/src/resources/extensions/gsd/routing-history.ts +290 -0
  201. package/src/resources/extensions/gsd/state.ts +15 -3
  202. package/src/resources/extensions/gsd/templates/knowledge.md +19 -0
  203. package/src/resources/extensions/gsd/templates/preferences.md +14 -0
  204. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +50 -0
  205. package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +20 -0
  206. package/src/resources/extensions/gsd/tests/budget-prediction.test.ts +220 -0
  207. package/src/resources/extensions/gsd/tests/complexity-routing.test.ts +294 -0
  208. package/src/resources/extensions/gsd/tests/context-compression.test.ts +180 -0
  209. package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +99 -0
  210. package/src/resources/extensions/gsd/tests/git-service.test.ts +132 -0
  211. package/src/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +79 -0
  212. package/src/resources/extensions/gsd/tests/knowledge.test.ts +161 -0
  213. package/src/resources/extensions/gsd/tests/memory-leak-guards.test.ts +87 -0
  214. package/src/resources/extensions/gsd/tests/preferences-git.test.ts +28 -0
  215. package/src/resources/extensions/gsd/tests/preferences-wizard-fields.test.ts +168 -0
  216. package/src/resources/extensions/gsd/tests/queue-order.test.ts +204 -0
  217. package/src/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +281 -0
  218. package/src/resources/extensions/gsd/tests/routing-history.test.ts +87 -0
  219. package/src/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +139 -0
  220. package/src/resources/extensions/gsd/tests/stop-auto-remote.test.ts +130 -0
  221. package/src/resources/extensions/gsd/tests/token-profile.test.ts +263 -0
  222. package/src/resources/extensions/gsd/types.ts +28 -0
  223. package/src/resources/extensions/gsd/worktree-manager.ts +8 -5
  224. package/src/resources/extensions/gsd/worktree.ts +24 -2
  225. package/src/resources/extensions/shared/next-action-ui.ts +16 -1
@@ -0,0 +1,281 @@
1
+ /**
2
+ * End-to-end integration tests for the Queue Reorder feature.
3
+ *
4
+ * Verifies the full chain: QUEUE-ORDER.json + findMilestoneIds() + deriveState()
5
+ * + depends_on removal from CONTEXT.md files.
6
+ *
7
+ * These tests simulate what happens when a user reorders milestones and confirms:
8
+ * 1. QUEUE-ORDER.json is written with the new order
9
+ * 2. depends_on is removed from CONTEXT.md frontmatter
10
+ * 3. deriveState() picks the correct milestone as active
11
+ * 4. A fresh deriveState() call (simulating new session) also works
12
+ */
13
+
14
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync, readFileSync, existsSync } from 'node:fs';
15
+ import { join } from 'node:path';
16
+ import { tmpdir } from 'node:os';
17
+
18
+ import { deriveState, invalidateStateCache } from '../state.ts';
19
+ import { findMilestoneIds } from '../guided-flow.ts';
20
+ import { saveQueueOrder, loadQueueOrder } from '../queue-order.ts';
21
+ import { parseContextDependsOn } from '../files.ts';
22
+ import { createTestContext } from './test-helpers.ts';
23
+
24
+ const { assertEq, assertTrue, report } = createTestContext();
25
+
26
+ // ─── Fixture Helpers ───────────────────────────────────────────────────────
27
+
28
+ function createFixtureBase(): string {
29
+ const base = mkdtempSync(join(tmpdir(), 'gsd-reorder-e2e-'));
30
+ mkdirSync(join(base, '.gsd', 'milestones'), { recursive: true });
31
+ return base;
32
+ }
33
+
34
+ function cleanup(base: string): void {
35
+ rmSync(base, { recursive: true, force: true });
36
+ }
37
+
38
+ function writeMilestoneDir(base: string, mid: string): void {
39
+ mkdirSync(join(base, '.gsd', 'milestones', mid), { recursive: true });
40
+ }
41
+
42
+ function writeContext(base: string, mid: string, frontmatter: string, body: string = ''): void {
43
+ const dir = join(base, '.gsd', 'milestones', mid);
44
+ mkdirSync(dir, { recursive: true });
45
+ const fm = frontmatter ? `---\n${frontmatter}\n---\n\n` : '';
46
+ writeFileSync(join(dir, `${mid}-CONTEXT.md`), `${fm}# ${mid}: Test\n\n${body}`);
47
+ }
48
+
49
+ function writeCompleteMilestone(base: string, mid: string): void {
50
+ const dir = join(base, '.gsd', 'milestones', mid);
51
+ mkdirSync(dir, { recursive: true });
52
+ writeFileSync(join(dir, `${mid}-ROADMAP.md`), `# ${mid}: Complete
53
+
54
+ **Vision:** Done.
55
+
56
+ ## Slices
57
+
58
+ - [x] **S01: Done** \`risk:low\` \`depends:[]\`
59
+ > After this: Done.
60
+ `);
61
+ writeFileSync(join(dir, `${mid}-SUMMARY.md`), `# ${mid} Summary\n\nComplete.`);
62
+ }
63
+
64
+ function readContextFile(base: string, mid: string): string {
65
+ return readFileSync(join(base, '.gsd', 'milestones', mid, `${mid}-CONTEXT.md`), 'utf-8');
66
+ }
67
+
68
+ // ═══════════════════════════════════════════════════════════════════════════
69
+ // Test: Queue order changes milestone activation
70
+ // ═══════════════════════════════════════════════════════════════════════════
71
+
72
+ console.log('\n=== E2E: queue-order changes active milestone ===');
73
+ {
74
+ const base = createFixtureBase();
75
+ try {
76
+ // Setup: M007 complete, M008 and M009 pending (no context, no roadmap)
77
+ writeCompleteMilestone(base, 'M007');
78
+ writeMilestoneDir(base, 'M008');
79
+ writeContext(base, 'M008', '', 'Multi-Session Parallel Orchestration');
80
+ writeMilestoneDir(base, 'M009');
81
+ writeContext(base, 'M009', '', 'Context-Budget Visibility');
82
+
83
+ // Without custom order: M008 comes first (numeric sort)
84
+ invalidateStateCache();
85
+ const stateBefore = await deriveState(base);
86
+ assertEq(stateBefore.activeMilestone?.id, 'M008', 'before reorder: M008 is active');
87
+
88
+ // Save custom order: M009 before M008
89
+ saveQueueOrder(base, ['M009', 'M008']);
90
+
91
+ // With custom order: M009 should be active
92
+ invalidateStateCache();
93
+ const stateAfter = await deriveState(base);
94
+ assertEq(stateAfter.activeMilestone?.id, 'M009', 'after reorder: M009 is active');
95
+
96
+ // findMilestoneIds respects the order
97
+ const ids = findMilestoneIds(base);
98
+ const m008Idx = ids.indexOf('M008');
99
+ const m009Idx = ids.indexOf('M009');
100
+ assertTrue(m009Idx < m008Idx, 'findMilestoneIds: M009 comes before M008');
101
+
102
+ } finally {
103
+ cleanup(base);
104
+ }
105
+ }
106
+
107
+ // ═══════════════════════════════════════════════════════════════════════════
108
+ // Test: Reorder + depends_on removal = correct state
109
+ // ═══════════════════════════════════════════════════════════════════════════
110
+
111
+ console.log('\n=== E2E: reorder with depends_on removal ===');
112
+ {
113
+ const base = createFixtureBase();
114
+ try {
115
+ // Setup: M007 complete, M008 depends_on M009, M009 no deps
116
+ writeCompleteMilestone(base, 'M007');
117
+ writeContext(base, 'M008', 'depends_on: [M009]', 'Multi-Session Parallel');
118
+ writeContext(base, 'M009', '', 'Context-Budget Visibility');
119
+
120
+ // Before: M008 depends on M009, so deriveState skips M008, M009 is active
121
+ invalidateStateCache();
122
+ const stateBefore = await deriveState(base);
123
+ assertEq(stateBefore.activeMilestone?.id, 'M009', 'before: M009 active (M008 dep-blocked)');
124
+
125
+ // Simulate reorder confirm: save order M009→M008, remove depends_on from M008
126
+ saveQueueOrder(base, ['M009', 'M008']);
127
+
128
+ // Remove depends_on from M008-CONTEXT.md (simulating what handleQueueReorder does)
129
+ const contextContent = readContextFile(base, 'M008');
130
+ const newContent = contextContent.replace(/---\ndepends_on: \[M009\]\n---\n\n/, '');
131
+ writeFileSync(join(base, '.gsd', 'milestones', 'M008', 'M008-CONTEXT.md'), newContent);
132
+
133
+ // Verify: depends_on is gone
134
+ const updatedContent = readContextFile(base, 'M008');
135
+ const deps = parseContextDependsOn(updatedContent);
136
+ assertEq(deps.length, 0, 'depends_on removed from M008-CONTEXT.md');
137
+
138
+ // Verify: deriveState still picks M009 (it's first in queue order)
139
+ invalidateStateCache();
140
+ const stateAfter = await deriveState(base);
141
+ assertEq(stateAfter.activeMilestone?.id, 'M009', 'after: M009 still active (first in queue)');
142
+
143
+ // Verify: M008 is now pending (not dep-blocked)
144
+ const m008Entry = stateAfter.registry.find(m => m.id === 'M008');
145
+ assertEq(m008Entry?.status, 'pending', 'M008 is pending (not dep-blocked)');
146
+ assertTrue(!m008Entry?.dependsOn || m008Entry.dependsOn.length === 0, 'M008 has no dependsOn');
147
+
148
+ } finally {
149
+ cleanup(base);
150
+ }
151
+ }
152
+
153
+ // ═══════════════════════════════════════════════════════════════════════════
154
+ // Test: Fresh deriveState (simulating new session) respects queue order
155
+ // ═══════════════════════════════════════════════════════════════════════════
156
+
157
+ console.log('\n=== E2E: fresh session respects queue order ===');
158
+ {
159
+ const base = createFixtureBase();
160
+ try {
161
+ writeCompleteMilestone(base, 'M007');
162
+ writeContext(base, 'M008', '', 'Parallel Orchestration');
163
+ writeContext(base, 'M009', '', 'Budget Visibility');
164
+
165
+ // Save queue order
166
+ saveQueueOrder(base, ['M009', 'M008']);
167
+
168
+ // Simulate fresh session — invalidate all caches
169
+ invalidateStateCache();
170
+
171
+ // Derive state — should read QUEUE-ORDER.json from disk
172
+ const state = await deriveState(base);
173
+ assertEq(state.activeMilestone?.id, 'M009', 'fresh session: M009 is active');
174
+
175
+ // Verify queue order persisted
176
+ const order = loadQueueOrder(base);
177
+ assertEq(order, ['M009', 'M008'], 'QUEUE-ORDER.json persisted correctly');
178
+
179
+ } finally {
180
+ cleanup(base);
181
+ }
182
+ }
183
+
184
+ // ═══════════════════════════════════════════════════════════════════════════
185
+ // Test: Queue order with newly added milestones
186
+ // ═══════════════════════════════════════════════════════════════════════════
187
+
188
+ console.log('\n=== E2E: new milestones appended to queue ===');
189
+ {
190
+ const base = createFixtureBase();
191
+ try {
192
+ writeCompleteMilestone(base, 'M007');
193
+ writeContext(base, 'M008', '', 'Parallel');
194
+ writeContext(base, 'M009', '', 'Visibility');
195
+
196
+ // Custom order only has M009, M008
197
+ saveQueueOrder(base, ['M009', 'M008']);
198
+
199
+ // Add M010 (not in queue order)
200
+ writeContext(base, 'M010', '', 'New feature');
201
+
202
+ invalidateStateCache();
203
+ const ids = findMilestoneIds(base);
204
+
205
+ // M009 first, M008 second, M010 appended at end
206
+ const m009Idx = ids.indexOf('M009');
207
+ const m008Idx = ids.indexOf('M008');
208
+ const m010Idx = ids.indexOf('M010');
209
+ assertTrue(m009Idx < m008Idx, 'M009 before M008');
210
+ assertTrue(m008Idx < m010Idx, 'M008 before M010 (new milestone appended)');
211
+
212
+ // M009 is still active (first non-complete in queue order)
213
+ const state = await deriveState(base);
214
+ assertEq(state.activeMilestone?.id, 'M009', 'M009 still active after M010 added');
215
+
216
+ } finally {
217
+ cleanup(base);
218
+ }
219
+ }
220
+
221
+ // ═══════════════════════════════════════════════════════════════════════════
222
+ // Test: No queue order file = default numeric sort (backward compat)
223
+ // ═══════════════════════════════════════════════════════════════════════════
224
+
225
+ console.log('\n=== E2E: backward compat without QUEUE-ORDER.json ===');
226
+ {
227
+ const base = createFixtureBase();
228
+ try {
229
+ writeCompleteMilestone(base, 'M007');
230
+ writeContext(base, 'M008', '', 'Parallel');
231
+ writeContext(base, 'M009', '', 'Visibility');
232
+
233
+ // No QUEUE-ORDER.json — default numeric sort
234
+ invalidateStateCache();
235
+ const state = await deriveState(base);
236
+ assertEq(state.activeMilestone?.id, 'M008', 'no queue order: M008 active (numeric)');
237
+
238
+ const ids = findMilestoneIds(base);
239
+ assertTrue(ids.indexOf('M008') < ids.indexOf('M009'), 'default sort: M008 before M009');
240
+
241
+ } finally {
242
+ cleanup(base);
243
+ }
244
+ }
245
+
246
+ // ═══════════════════════════════════════════════════════════════════════════
247
+ // Test: depends_on inline array format removal
248
+ // ═══════════════════════════════════════════════════════════════════════════
249
+
250
+ console.log('\n=== E2E: depends_on inline format preserved after partial removal ===');
251
+ {
252
+ const base = createFixtureBase();
253
+ try {
254
+ writeCompleteMilestone(base, 'M007');
255
+ // M008 depends on both M009 and M010
256
+ writeContext(base, 'M008', 'depends_on: [M009, M010]', 'Parallel');
257
+ writeContext(base, 'M009', '', 'Visibility');
258
+ writeContext(base, 'M010', '', 'Other');
259
+
260
+ // Verify both deps are parsed
261
+ const contentBefore = readContextFile(base, 'M008');
262
+ const depsBefore = parseContextDependsOn(contentBefore);
263
+ assertEq(depsBefore.length, 2, 'M008 has 2 deps before');
264
+
265
+ // Simulate removing only M009 dep (keep M010)
266
+ const content = readContextFile(base, 'M008');
267
+ const updated = content.replace('depends_on: [M009, M010]', 'depends_on: [M010]');
268
+ writeFileSync(join(base, '.gsd', 'milestones', 'M008', 'M008-CONTEXT.md'), updated);
269
+
270
+ // Verify only M010 remains
271
+ const contentAfter = readContextFile(base, 'M008');
272
+ const depsAfter = parseContextDependsOn(contentAfter);
273
+ assertEq(depsAfter.length, 1, 'M008 has 1 dep after removal');
274
+ assertEq(depsAfter[0], 'M010', 'remaining dep is M010');
275
+
276
+ } finally {
277
+ cleanup(base);
278
+ }
279
+ }
280
+
281
+ report();
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Routing History — structural tests for adaptive learning module.
3
+ *
4
+ * Verifies routing-history.ts exports and structure from #579.
5
+ * Uses source-level checks to avoid @gsd/pi-coding-agent import chain.
6
+ */
7
+
8
+ import test from "node:test";
9
+ import assert from "node:assert/strict";
10
+ import { readFileSync } from "node:fs";
11
+ import { join, dirname } from "node:path";
12
+ import { fileURLToPath } from "node:url";
13
+
14
+ const __dirname = dirname(fileURLToPath(import.meta.url));
15
+ const historySrc = readFileSync(join(__dirname, "..", "routing-history.ts"), "utf-8");
16
+
17
+ // ═══════════════════════════════════════════════════════════════════════════
18
+ // Module Exports
19
+ // ═══════════════════════════════════════════════════════════════════════════
20
+
21
+ test("routing-history: exports initRoutingHistory", () => {
22
+ assert.ok(historySrc.includes("export function initRoutingHistory"), "should export initRoutingHistory");
23
+ });
24
+
25
+ test("routing-history: exports recordOutcome", () => {
26
+ assert.ok(historySrc.includes("export function recordOutcome"), "should export recordOutcome");
27
+ });
28
+
29
+ test("routing-history: exports recordFeedback", () => {
30
+ assert.ok(historySrc.includes("export function recordFeedback"), "should export recordFeedback");
31
+ });
32
+
33
+ test("routing-history: exports getAdaptiveTierAdjustment", () => {
34
+ assert.ok(historySrc.includes("export function getAdaptiveTierAdjustment"), "should export getAdaptiveTierAdjustment");
35
+ });
36
+
37
+ test("routing-history: exports resetRoutingHistory", () => {
38
+ assert.ok(historySrc.includes("export function resetRoutingHistory"), "should export resetRoutingHistory");
39
+ });
40
+
41
+ // ═══════════════════════════════════════════════════════════════════════════
42
+ // Design Constants
43
+ // ═══════════════════════════════════════════════════════════════════════════
44
+
45
+ test("routing-history: uses rolling window of 50 entries", () => {
46
+ assert.ok(historySrc.includes("ROLLING_WINDOW = 50"), "should use 50-entry rolling window");
47
+ });
48
+
49
+ test("routing-history: failure threshold is 20%", () => {
50
+ assert.ok(historySrc.includes("FAILURE_THRESHOLD = 0.20"), "should use 20% failure threshold");
51
+ });
52
+
53
+ test("routing-history: feedback weight is 2x", () => {
54
+ assert.ok(historySrc.includes("FEEDBACK_WEIGHT = 2"), "feedback should count 2x");
55
+ });
56
+
57
+ // ═══════════════════════════════════════════════════════════════════════════
58
+ // Type Structure
59
+ // ═══════════════════════════════════════════════════════════════════════════
60
+
61
+ test("routing-history: imports ComplexityTier from types.ts", () => {
62
+ assert.ok(
63
+ historySrc.includes('from "./types.js"') && historySrc.includes("ComplexityTier"),
64
+ "should import ComplexityTier from types.ts",
65
+ );
66
+ });
67
+
68
+ test("routing-history: defines RoutingHistoryData interface", () => {
69
+ assert.ok(historySrc.includes("interface RoutingHistoryData"), "should define RoutingHistoryData");
70
+ });
71
+
72
+ test("routing-history: defines FeedbackEntry interface", () => {
73
+ assert.ok(historySrc.includes("interface FeedbackEntry"), "should define FeedbackEntry");
74
+ });
75
+
76
+ // ═══════════════════════════════════════════════════════════════════════════
77
+ // Persistence
78
+ // ═══════════════════════════════════════════════════════════════════════════
79
+
80
+ test("routing-history: persists to routing-history.json", () => {
81
+ assert.ok(historySrc.includes("routing-history.json"), "should persist to routing-history.json");
82
+ });
83
+
84
+ test("routing-history: has save and load functions", () => {
85
+ assert.ok(historySrc.includes("saveHistory") || historySrc.includes("function save"), "should have save");
86
+ assert.ok(historySrc.includes("loadHistory") || historySrc.includes("function load"), "should have load");
87
+ });
@@ -0,0 +1,139 @@
1
+ /**
2
+ * stale-worktree-cwd.test.ts — Tests for #608 fix.
3
+ *
4
+ * Verifies that when process.cwd() is inside a stale .gsd/worktrees/ path,
5
+ * startAuto escapes back to the project root before proceeding.
6
+ */
7
+
8
+ import test from "node:test";
9
+ import assert from "node:assert/strict";
10
+ import { mkdtempSync, mkdirSync, rmSync, existsSync, realpathSync, writeFileSync } from "node:fs";
11
+ import { join, sep } from "node:path";
12
+ import { tmpdir } from "node:os";
13
+ import { execSync } from "node:child_process";
14
+
15
+ import {
16
+ createAutoWorktree,
17
+ teardownAutoWorktree,
18
+ mergeMilestoneToMain,
19
+ } from "../auto-worktree.ts";
20
+
21
+ function run(command: string, cwd: string): string {
22
+ return execSync(command, { cwd, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" }).trim();
23
+ }
24
+
25
+ function createTempRepo(): string {
26
+ const dir = realpathSync(mkdtempSync(join(tmpdir(), "stale-wt-test-")));
27
+ run("git init", dir);
28
+ run("git config user.email test@test.com", dir);
29
+ run("git config user.name Test", dir);
30
+ writeFileSync(join(dir, "README.md"), "# test\n");
31
+ run("git add .", dir);
32
+ run("git commit -m init", dir);
33
+ run("git branch -M main", dir);
34
+ return dir;
35
+ }
36
+
37
+ // ─── escapeStaleWorktree is called by startAuto, test the detection logic ────
38
+
39
+ test("detects stale worktree path and extracts project root", () => {
40
+ // Simulate the path pattern: /project/.gsd/worktrees/M004/...
41
+ const projectRoot = "/Users/test/myproject";
42
+ const stalePath = `${projectRoot}${sep}.gsd${sep}worktrees${sep}M004`;
43
+
44
+ const marker = `${sep}.gsd${sep}worktrees${sep}`;
45
+ const idx = stalePath.indexOf(marker);
46
+
47
+ assert.ok(idx !== -1, "marker found in stale path");
48
+ assert.equal(stalePath.slice(0, idx), projectRoot, "project root extracted correctly");
49
+ });
50
+
51
+ test("does not trigger on normal project path", () => {
52
+ const normalPath = "/Users/test/myproject";
53
+ const marker = `${sep}.gsd${sep}worktrees${sep}`;
54
+ const idx = normalPath.indexOf(marker);
55
+
56
+ assert.equal(idx, -1, "marker not found in normal path");
57
+ });
58
+
59
+ // ─── Integration: mergeMilestoneToMain restores cwd ─────────────────────────
60
+
61
+ test("mergeMilestoneToMain restores cwd to project root", () => {
62
+ const savedCwd = process.cwd();
63
+ let tempDir = "";
64
+
65
+ try {
66
+ tempDir = createTempRepo();
67
+
68
+ // Create milestone planning artifacts
69
+ const msDir = join(tempDir, ".gsd", "milestones", "M050");
70
+ mkdirSync(msDir, { recursive: true });
71
+ writeFileSync(join(msDir, "CONTEXT.md"), "# M050 Context\n");
72
+ const roadmap = [
73
+ "# M050: Test Milestone",
74
+ "**Vision**: testing",
75
+ "## Success Criteria",
76
+ "- It works",
77
+ "## Slices",
78
+ "- [x] S01 — First slice",
79
+ ].join("\n");
80
+ writeFileSync(join(msDir, "ROADMAP.md"), roadmap);
81
+ run("git add .", tempDir);
82
+ run("git commit -m \"add milestone\"", tempDir);
83
+
84
+ // Create auto-worktree (enters the worktree dir)
85
+ const wtPath = createAutoWorktree(tempDir, "M050");
86
+ assert.equal(process.cwd(), wtPath, "cwd is in worktree after create");
87
+
88
+ // Add a change in the worktree
89
+ writeFileSync(join(wtPath, "feature.txt"), "new feature\n");
90
+ run("git add .", wtPath);
91
+ run("git commit -m \"feat: add feature\"", wtPath);
92
+
93
+ // Merge back — should restore cwd to tempDir
94
+ mergeMilestoneToMain(tempDir, "M050", roadmap);
95
+
96
+ assert.equal(process.cwd(), tempDir, "cwd restored to project root after merge");
97
+ assert.ok(!existsSync(wtPath), "worktree directory removed after merge");
98
+ } finally {
99
+ process.chdir(savedCwd);
100
+ if (tempDir && existsSync(tempDir)) {
101
+ rmSync(tempDir, { recursive: true, force: true });
102
+ }
103
+ }
104
+ });
105
+
106
+ // ─── Integration: stale worktree directory is detectable ────────────────────
107
+
108
+ test("process.cwd() inside removed worktree is recoverable", () => {
109
+ const savedCwd = process.cwd();
110
+ let tempDir = "";
111
+
112
+ try {
113
+ tempDir = createTempRepo();
114
+
115
+ // Create a .gsd/worktrees/M099 directory to simulate stale state
116
+ const staleWtDir = join(tempDir, ".gsd", "worktrees", "M099");
117
+ mkdirSync(staleWtDir, { recursive: true });
118
+
119
+ // Enter the stale directory
120
+ process.chdir(staleWtDir);
121
+ const cwdBefore = process.cwd();
122
+ assert.ok(cwdBefore.includes(`${sep}.gsd${sep}worktrees${sep}`), "cwd is inside worktree dir");
123
+
124
+ // Simulate escapeStaleWorktree logic
125
+ const marker = `${sep}.gsd${sep}worktrees${sep}`;
126
+ const idx = cwdBefore.indexOf(marker);
127
+ assert.ok(idx !== -1, "marker found");
128
+
129
+ const projectRoot = cwdBefore.slice(0, idx);
130
+ process.chdir(projectRoot);
131
+
132
+ assert.equal(process.cwd(), tempDir, "successfully escaped to project root");
133
+ } finally {
134
+ process.chdir(savedCwd);
135
+ if (tempDir && existsSync(tempDir)) {
136
+ rmSync(tempDir, { recursive: true, force: true });
137
+ }
138
+ }
139
+ });
@@ -0,0 +1,130 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdirSync, rmSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+ import { randomUUID } from "node:crypto";
7
+ import { fork } from "node:child_process";
8
+
9
+ import { writeFileSync } from "node:fs";
10
+ import {
11
+ writeLock,
12
+ readCrashLock,
13
+ clearLock,
14
+ isLockProcessAlive,
15
+ } from "../crash-recovery.ts";
16
+ import { stopAutoRemote } from "../auto.ts";
17
+
18
+ function makeTmpBase(): string {
19
+ const base = join(tmpdir(), `gsd-test-${randomUUID()}`);
20
+ mkdirSync(join(base, ".gsd"), { recursive: true });
21
+ return base;
22
+ }
23
+
24
+ function cleanup(base: string): void {
25
+ try { rmSync(base, { recursive: true, force: true }); } catch { /* */ }
26
+ }
27
+
28
+ // ─── stopAutoRemote ──────────────────────────────────────────────────────
29
+
30
+ test("stopAutoRemote returns found:false when no lock file exists", () => {
31
+ const base = makeTmpBase();
32
+ try {
33
+ const result = stopAutoRemote(base);
34
+ assert.equal(result.found, false);
35
+ assert.equal(result.pid, undefined);
36
+ assert.equal(result.error, undefined);
37
+ } finally {
38
+ cleanup(base);
39
+ }
40
+ });
41
+
42
+ test("stopAutoRemote cleans up stale lock (dead PID) and returns found:false", () => {
43
+ const base = makeTmpBase();
44
+ try {
45
+ // Write a lock with a PID that doesn't exist
46
+ writeLock(base, "execute-task", "M001/S01/T01", 3);
47
+ // Overwrite PID to a dead one
48
+ const lock = readCrashLock(base)!;
49
+ const staleData = { ...lock, pid: 999999999 };
50
+ writeFileSync(join(base, ".gsd", "auto.lock"), JSON.stringify(staleData, null, 2), "utf-8");
51
+
52
+ const result = stopAutoRemote(base);
53
+ assert.equal(result.found, false, "stale lock should not be found as running");
54
+
55
+ // Lock should be cleaned up
56
+ assert.equal(readCrashLock(base), null, "stale lock should be removed");
57
+ } finally {
58
+ cleanup(base);
59
+ }
60
+ });
61
+
62
+ test("stopAutoRemote sends SIGTERM to a live process and returns found:true", async () => {
63
+ const base = makeTmpBase();
64
+
65
+ // Spawn a child process that sleeps, acting as a fake auto-mode session
66
+ const child = fork(
67
+ "-e",
68
+ ["process.on('SIGTERM', () => process.exit(0)); setTimeout(() => process.exit(1), 30000);"],
69
+ { stdio: "ignore", detached: false },
70
+ );
71
+
72
+ try {
73
+ // Wait for child to be ready
74
+ await new Promise((resolve) => setTimeout(resolve, 200));
75
+
76
+ // Write lock with child's PID
77
+ const lockData = {
78
+ pid: child.pid,
79
+ startedAt: new Date().toISOString(),
80
+ unitType: "execute-task",
81
+ unitId: "M001/S01/T01",
82
+ unitStartedAt: new Date().toISOString(),
83
+ completedUnits: 0,
84
+ };
85
+ writeFileSync(join(base, ".gsd", "auto.lock"), JSON.stringify(lockData, null, 2), "utf-8");
86
+
87
+ const result = stopAutoRemote(base);
88
+ assert.equal(result.found, true, "should find running auto-mode");
89
+ assert.equal(result.pid, child.pid, "should return the PID");
90
+
91
+ // Wait for child to exit (it should receive SIGTERM)
92
+ const exitCode = await new Promise<number | null>((resolve) => {
93
+ child.on("exit", (code) => resolve(code));
94
+ setTimeout(() => resolve(null), 5000);
95
+ });
96
+ // On Windows, SIGTERM is not interceptable — the process exits with code 1
97
+ // rather than running the handler. Accept either clean exit (0) or forced (1).
98
+ assert.ok(exitCode !== null, "child should have exited after SIGTERM");
99
+ if (process.platform !== "win32") {
100
+ assert.equal(exitCode, 0, "child should have exited cleanly via SIGTERM");
101
+ }
102
+ } finally {
103
+ try { child.kill("SIGKILL"); } catch { /* already dead */ }
104
+ cleanup(base);
105
+ }
106
+ });
107
+
108
+ // ─── Lock path: original project root vs worktree ────────────────────────
109
+
110
+ test("lock file should be discoverable at project root, not worktree path", () => {
111
+ const projectRoot = makeTmpBase();
112
+ const worktreePath = join(projectRoot, ".gsd", "worktrees", "M001");
113
+ mkdirSync(join(worktreePath, ".gsd"), { recursive: true });
114
+
115
+ try {
116
+ // Simulate: auto-mode writes lock to project root (the fix)
117
+ writeLock(projectRoot, "execute-task", "M001/S01/T01", 0);
118
+
119
+ // Second terminal checks project root — should find the lock
120
+ const lock = readCrashLock(projectRoot);
121
+ assert.ok(lock, "lock should be found at project root");
122
+ assert.equal(lock!.unitType, "execute-task");
123
+
124
+ // Worktree path should NOT have a lock
125
+ const worktreeLock = readCrashLock(worktreePath);
126
+ assert.equal(worktreeLock, null, "lock should NOT exist at worktree path");
127
+ } finally {
128
+ cleanup(projectRoot);
129
+ }
130
+ });