gsd-pi 2.17.0 → 2.19.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 (217) 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 +14 -2
  7. package/dist/resources/extensions/gsd/auto-prompts.ts +65 -16
  8. package/dist/resources/extensions/gsd/auto-worktree.ts +33 -4
  9. package/dist/resources/extensions/gsd/auto.ts +399 -29
  10. package/dist/resources/extensions/gsd/captures.ts +384 -0
  11. package/dist/resources/extensions/gsd/commands.ts +382 -23
  12. package/dist/resources/extensions/gsd/complexity-classifier.ts +322 -0
  13. package/dist/resources/extensions/gsd/dashboard-overlay.ts +10 -0
  14. package/dist/resources/extensions/gsd/dispatch-guard.ts +7 -19
  15. package/dist/resources/extensions/gsd/docs/preferences-reference.md +201 -2
  16. package/dist/resources/extensions/gsd/files.ts +123 -1
  17. package/dist/resources/extensions/gsd/guided-flow.ts +237 -4
  18. package/dist/resources/extensions/gsd/index.ts +47 -3
  19. package/dist/resources/extensions/gsd/metrics.ts +48 -0
  20. package/dist/resources/extensions/gsd/model-cost-table.ts +65 -0
  21. package/dist/resources/extensions/gsd/model-router.ts +256 -0
  22. package/dist/resources/extensions/gsd/paths.ts +9 -0
  23. package/dist/resources/extensions/gsd/post-unit-hooks.ts +2 -1
  24. package/dist/resources/extensions/gsd/preferences.ts +132 -1
  25. package/dist/resources/extensions/gsd/prompt-loader.ts +45 -9
  26. package/dist/resources/extensions/gsd/prompts/execute-task.md +6 -5
  27. package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +6 -0
  28. package/dist/resources/extensions/gsd/prompts/replan-slice.md +8 -0
  29. package/dist/resources/extensions/gsd/prompts/system.md +2 -0
  30. package/dist/resources/extensions/gsd/prompts/triage-captures.md +62 -0
  31. package/dist/resources/extensions/gsd/queue-order.ts +231 -0
  32. package/dist/resources/extensions/gsd/queue-reorder-ui.ts +263 -0
  33. package/dist/resources/extensions/gsd/state.ts +15 -3
  34. package/dist/resources/extensions/gsd/templates/knowledge.md +19 -0
  35. package/dist/resources/extensions/gsd/templates/preferences.md +14 -0
  36. package/dist/resources/extensions/gsd/tests/auto-worktree.test.ts +20 -0
  37. package/dist/resources/extensions/gsd/tests/captures.test.ts +438 -0
  38. package/dist/resources/extensions/gsd/tests/complexity-classifier.test.ts +181 -0
  39. package/dist/resources/extensions/gsd/tests/derive-state-deps.test.ts +99 -0
  40. package/dist/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +434 -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/milestone-transition-worktree.test.ts +144 -0
  45. package/dist/resources/extensions/gsd/tests/model-cost-table.test.ts +69 -0
  46. package/dist/resources/extensions/gsd/tests/model-router.test.ts +167 -0
  47. package/dist/resources/extensions/gsd/tests/preferences-wizard-fields.test.ts +168 -0
  48. package/dist/resources/extensions/gsd/tests/queue-order.test.ts +204 -0
  49. package/dist/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +281 -0
  50. package/dist/resources/extensions/gsd/tests/remote-questions.test.ts +227 -1
  51. package/dist/resources/extensions/gsd/tests/routing-history.test.ts +215 -62
  52. package/dist/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +139 -0
  53. package/dist/resources/extensions/gsd/tests/triage-dispatch.test.ts +224 -0
  54. package/dist/resources/extensions/gsd/tests/triage-resolution.test.ts +215 -0
  55. package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +198 -0
  56. package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +255 -0
  57. package/dist/resources/extensions/gsd/triage-resolution.ts +200 -0
  58. package/dist/resources/extensions/gsd/triage-ui.ts +175 -0
  59. package/dist/resources/extensions/gsd/visualizer-data.ts +154 -0
  60. package/dist/resources/extensions/gsd/visualizer-overlay.ts +193 -0
  61. package/dist/resources/extensions/gsd/visualizer-views.ts +293 -0
  62. package/dist/resources/extensions/gsd/worktree-manager.ts +8 -5
  63. package/dist/resources/extensions/gsd/worktree.ts +22 -0
  64. package/dist/resources/extensions/remote-questions/discord-adapter.ts +33 -0
  65. package/dist/resources/extensions/remote-questions/format.ts +12 -6
  66. package/dist/resources/extensions/remote-questions/manager.ts +8 -0
  67. package/dist/resources/extensions/shared/next-action-ui.ts +16 -1
  68. package/package.json +1 -1
  69. package/packages/pi-coding-agent/dist/cli/args.d.ts +5 -0
  70. package/packages/pi-coding-agent/dist/cli/args.d.ts.map +1 -1
  71. package/packages/pi-coding-agent/dist/cli/args.js +21 -0
  72. package/packages/pi-coding-agent/dist/cli/args.js.map +1 -1
  73. package/packages/pi-coding-agent/dist/cli/list-models.d.ts +14 -3
  74. package/packages/pi-coding-agent/dist/cli/list-models.d.ts.map +1 -1
  75. package/packages/pi-coding-agent/dist/cli/list-models.js +52 -17
  76. package/packages/pi-coding-agent/dist/cli/list-models.js.map +1 -1
  77. package/packages/pi-coding-agent/dist/core/discovery-cache.d.ts +27 -0
  78. package/packages/pi-coding-agent/dist/core/discovery-cache.d.ts.map +1 -0
  79. package/packages/pi-coding-agent/dist/core/discovery-cache.js +79 -0
  80. package/packages/pi-coding-agent/dist/core/discovery-cache.js.map +1 -0
  81. package/packages/pi-coding-agent/dist/core/discovery-cache.test.d.ts +2 -0
  82. package/packages/pi-coding-agent/dist/core/discovery-cache.test.d.ts.map +1 -0
  83. package/packages/pi-coding-agent/dist/core/discovery-cache.test.js +140 -0
  84. package/packages/pi-coding-agent/dist/core/discovery-cache.test.js.map +1 -0
  85. package/packages/pi-coding-agent/dist/core/model-discovery.d.ts +35 -0
  86. package/packages/pi-coding-agent/dist/core/model-discovery.d.ts.map +1 -0
  87. package/packages/pi-coding-agent/dist/core/model-discovery.js +162 -0
  88. package/packages/pi-coding-agent/dist/core/model-discovery.js.map +1 -0
  89. package/packages/pi-coding-agent/dist/core/model-discovery.test.d.ts +2 -0
  90. package/packages/pi-coding-agent/dist/core/model-discovery.test.d.ts.map +1 -0
  91. package/packages/pi-coding-agent/dist/core/model-discovery.test.js +100 -0
  92. package/packages/pi-coding-agent/dist/core/model-discovery.test.js.map +1 -0
  93. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.d.ts +2 -0
  94. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.d.ts.map +1 -0
  95. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js +113 -0
  96. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js.map +1 -0
  97. package/packages/pi-coding-agent/dist/core/model-registry.d.ts +26 -0
  98. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  99. package/packages/pi-coding-agent/dist/core/model-registry.js +98 -0
  100. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  101. package/packages/pi-coding-agent/dist/core/models-json-writer.d.ts +62 -0
  102. package/packages/pi-coding-agent/dist/core/models-json-writer.d.ts.map +1 -0
  103. package/packages/pi-coding-agent/dist/core/models-json-writer.js +145 -0
  104. package/packages/pi-coding-agent/dist/core/models-json-writer.js.map +1 -0
  105. package/packages/pi-coding-agent/dist/core/models-json-writer.test.d.ts +2 -0
  106. package/packages/pi-coding-agent/dist/core/models-json-writer.test.d.ts.map +1 -0
  107. package/packages/pi-coding-agent/dist/core/models-json-writer.test.js +118 -0
  108. package/packages/pi-coding-agent/dist/core/models-json-writer.test.js.map +1 -0
  109. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +9 -0
  110. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  111. package/packages/pi-coding-agent/dist/core/settings-manager.js +11 -0
  112. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  113. package/packages/pi-coding-agent/dist/core/slash-commands.d.ts.map +1 -1
  114. package/packages/pi-coding-agent/dist/core/slash-commands.js +1 -0
  115. package/packages/pi-coding-agent/dist/core/slash-commands.js.map +1 -1
  116. package/packages/pi-coding-agent/dist/index.d.ts +5 -1
  117. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  118. package/packages/pi-coding-agent/dist/index.js +4 -1
  119. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  120. package/packages/pi-coding-agent/dist/main.d.ts.map +1 -1
  121. package/packages/pi-coding-agent/dist/main.js +17 -2
  122. package/packages/pi-coding-agent/dist/main.js.map +1 -1
  123. package/packages/pi-coding-agent/dist/modes/interactive/components/index.d.ts +1 -0
  124. package/packages/pi-coding-agent/dist/modes/interactive/components/index.d.ts.map +1 -1
  125. package/packages/pi-coding-agent/dist/modes/interactive/components/index.js +1 -0
  126. package/packages/pi-coding-agent/dist/modes/interactive/components/index.js.map +1 -1
  127. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js +1 -1
  128. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js.map +1 -1
  129. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts +25 -0
  130. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts.map +1 -0
  131. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js +121 -0
  132. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js.map +1 -0
  133. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +1 -0
  134. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  135. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +32 -0
  136. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  137. package/packages/pi-coding-agent/src/cli/args.ts +21 -0
  138. package/packages/pi-coding-agent/src/cli/list-models.ts +70 -17
  139. package/packages/pi-coding-agent/src/core/discovery-cache.test.ts +170 -0
  140. package/packages/pi-coding-agent/src/core/discovery-cache.ts +97 -0
  141. package/packages/pi-coding-agent/src/core/model-discovery.test.ts +125 -0
  142. package/packages/pi-coding-agent/src/core/model-discovery.ts +231 -0
  143. package/packages/pi-coding-agent/src/core/model-registry-discovery.test.ts +135 -0
  144. package/packages/pi-coding-agent/src/core/model-registry.ts +107 -0
  145. package/packages/pi-coding-agent/src/core/models-json-writer.test.ts +145 -0
  146. package/packages/pi-coding-agent/src/core/models-json-writer.ts +188 -0
  147. package/packages/pi-coding-agent/src/core/settings-manager.ts +21 -0
  148. package/packages/pi-coding-agent/src/core/slash-commands.ts +1 -0
  149. package/packages/pi-coding-agent/src/index.ts +5 -0
  150. package/packages/pi-coding-agent/src/main.ts +19 -2
  151. package/packages/pi-coding-agent/src/modes/interactive/components/index.ts +1 -0
  152. package/packages/pi-coding-agent/src/modes/interactive/components/model-selector.ts +1 -1
  153. package/packages/pi-coding-agent/src/modes/interactive/components/provider-manager.ts +163 -0
  154. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +37 -0
  155. package/src/resources/extensions/gsd/activity-log.ts +37 -7
  156. package/src/resources/extensions/gsd/auto-dashboard.ts +14 -2
  157. package/src/resources/extensions/gsd/auto-prompts.ts +65 -16
  158. package/src/resources/extensions/gsd/auto-worktree.ts +33 -4
  159. package/src/resources/extensions/gsd/auto.ts +399 -29
  160. package/src/resources/extensions/gsd/captures.ts +384 -0
  161. package/src/resources/extensions/gsd/commands.ts +382 -23
  162. package/src/resources/extensions/gsd/complexity-classifier.ts +322 -0
  163. package/src/resources/extensions/gsd/dashboard-overlay.ts +10 -0
  164. package/src/resources/extensions/gsd/dispatch-guard.ts +7 -19
  165. package/src/resources/extensions/gsd/docs/preferences-reference.md +201 -2
  166. package/src/resources/extensions/gsd/files.ts +123 -1
  167. package/src/resources/extensions/gsd/guided-flow.ts +237 -4
  168. package/src/resources/extensions/gsd/index.ts +47 -3
  169. package/src/resources/extensions/gsd/metrics.ts +48 -0
  170. package/src/resources/extensions/gsd/model-cost-table.ts +65 -0
  171. package/src/resources/extensions/gsd/model-router.ts +256 -0
  172. package/src/resources/extensions/gsd/paths.ts +9 -0
  173. package/src/resources/extensions/gsd/post-unit-hooks.ts +2 -1
  174. package/src/resources/extensions/gsd/preferences.ts +132 -1
  175. package/src/resources/extensions/gsd/prompt-loader.ts +45 -9
  176. package/src/resources/extensions/gsd/prompts/execute-task.md +6 -5
  177. package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +6 -0
  178. package/src/resources/extensions/gsd/prompts/replan-slice.md +8 -0
  179. package/src/resources/extensions/gsd/prompts/system.md +2 -0
  180. package/src/resources/extensions/gsd/prompts/triage-captures.md +62 -0
  181. package/src/resources/extensions/gsd/queue-order.ts +231 -0
  182. package/src/resources/extensions/gsd/queue-reorder-ui.ts +263 -0
  183. package/src/resources/extensions/gsd/state.ts +15 -3
  184. package/src/resources/extensions/gsd/templates/knowledge.md +19 -0
  185. package/src/resources/extensions/gsd/templates/preferences.md +14 -0
  186. package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +20 -0
  187. package/src/resources/extensions/gsd/tests/captures.test.ts +438 -0
  188. package/src/resources/extensions/gsd/tests/complexity-classifier.test.ts +181 -0
  189. package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +99 -0
  190. package/src/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +434 -0
  191. package/src/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +79 -0
  192. package/src/resources/extensions/gsd/tests/knowledge.test.ts +161 -0
  193. package/src/resources/extensions/gsd/tests/memory-leak-guards.test.ts +87 -0
  194. package/src/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +144 -0
  195. package/src/resources/extensions/gsd/tests/model-cost-table.test.ts +69 -0
  196. package/src/resources/extensions/gsd/tests/model-router.test.ts +167 -0
  197. package/src/resources/extensions/gsd/tests/preferences-wizard-fields.test.ts +168 -0
  198. package/src/resources/extensions/gsd/tests/queue-order.test.ts +204 -0
  199. package/src/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +281 -0
  200. package/src/resources/extensions/gsd/tests/remote-questions.test.ts +227 -1
  201. package/src/resources/extensions/gsd/tests/routing-history.test.ts +215 -62
  202. package/src/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +139 -0
  203. package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +224 -0
  204. package/src/resources/extensions/gsd/tests/triage-resolution.test.ts +215 -0
  205. package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +198 -0
  206. package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +255 -0
  207. package/src/resources/extensions/gsd/triage-resolution.ts +200 -0
  208. package/src/resources/extensions/gsd/triage-ui.ts +175 -0
  209. package/src/resources/extensions/gsd/visualizer-data.ts +154 -0
  210. package/src/resources/extensions/gsd/visualizer-overlay.ts +193 -0
  211. package/src/resources/extensions/gsd/visualizer-views.ts +293 -0
  212. package/src/resources/extensions/gsd/worktree-manager.ts +8 -5
  213. package/src/resources/extensions/gsd/worktree.ts +22 -0
  214. package/src/resources/extensions/remote-questions/discord-adapter.ts +33 -0
  215. package/src/resources/extensions/remote-questions/format.ts +12 -6
  216. package/src/resources/extensions/remote-questions/manager.ts +8 -0
  217. package/src/resources/extensions/shared/next-action-ui.ts +16 -1
@@ -0,0 +1,168 @@
1
+ /**
2
+ * preferences-wizard-fields.test.ts — Validates that all wizard-configurable
3
+ * preference fields are properly validated and round-trip through the schema.
4
+ */
5
+
6
+ import { createTestContext } from "./test-helpers.ts";
7
+ import { validatePreferences } from "../preferences.ts";
8
+ import type { GSDPreferences } from "../preferences.ts";
9
+
10
+ const { assertEq, assertTrue, report } = createTestContext();
11
+
12
+ async function main(): Promise<void> {
13
+ console.log("\n=== budget fields validate correctly ===");
14
+
15
+ {
16
+ const { preferences, errors } = validatePreferences({
17
+ budget_ceiling: 25.50,
18
+ budget_enforcement: "warn",
19
+ context_pause_threshold: 80,
20
+ });
21
+ assertEq(errors.length, 0, "valid budget fields produce no errors");
22
+ assertEq(preferences.budget_ceiling, 25.50, "budget_ceiling passes through");
23
+ assertEq(preferences.budget_enforcement, "warn", "budget_enforcement passes through");
24
+ assertEq(preferences.context_pause_threshold, 80, "context_pause_threshold passes through");
25
+ }
26
+
27
+ {
28
+ const { preferences, errors } = validatePreferences({
29
+ budget_enforcement: "pause",
30
+ });
31
+ assertEq(errors.length, 0, "budget_enforcement 'pause' is valid");
32
+ assertEq(preferences.budget_enforcement, "pause", "pause passes through");
33
+ }
34
+
35
+ {
36
+ const { preferences, errors } = validatePreferences({
37
+ budget_enforcement: "halt",
38
+ });
39
+ assertEq(errors.length, 0, "budget_enforcement 'halt' is valid");
40
+ assertEq(preferences.budget_enforcement, "halt", "halt passes through");
41
+ }
42
+
43
+ {
44
+ const { errors } = validatePreferences({
45
+ budget_enforcement: "invalid",
46
+ } as unknown as GSDPreferences);
47
+ assertTrue(errors.some(e => e.includes("budget_enforcement")), "invalid budget_enforcement rejected");
48
+ }
49
+
50
+ console.log("\n=== notification fields validate correctly ===");
51
+
52
+ {
53
+ const { preferences, errors } = validatePreferences({
54
+ notifications: {
55
+ enabled: true,
56
+ on_complete: false,
57
+ on_error: true,
58
+ on_budget: true,
59
+ on_milestone: false,
60
+ on_attention: true,
61
+ },
62
+ });
63
+ assertEq(errors.length, 0, "valid notifications produce no errors");
64
+ assertEq(preferences.notifications?.enabled, true, "notifications.enabled passes through");
65
+ assertEq(preferences.notifications?.on_complete, false, "notifications.on_complete passes through");
66
+ assertEq(preferences.notifications?.on_milestone, false, "notifications.on_milestone passes through");
67
+ }
68
+
69
+ {
70
+ const { errors } = validatePreferences({
71
+ notifications: "invalid",
72
+ } as unknown as GSDPreferences);
73
+ assertTrue(errors.some(e => e.includes("notifications")), "invalid notifications rejected");
74
+ }
75
+
76
+ console.log("\n=== git fields validate correctly ===");
77
+
78
+ {
79
+ const { preferences, errors } = validatePreferences({
80
+ git: {
81
+ auto_push: true,
82
+ push_branches: false,
83
+ remote: "upstream",
84
+ snapshots: true,
85
+ pre_merge_check: "auto",
86
+ commit_type: "feat",
87
+ main_branch: "develop",
88
+ merge_strategy: "squash",
89
+ isolation: "branch",
90
+ },
91
+ });
92
+ assertEq(errors.length, 0, "valid git fields produce no errors");
93
+ assertEq(preferences.git?.auto_push, true, "git.auto_push passes through");
94
+ assertEq(preferences.git?.push_branches, false, "git.push_branches passes through");
95
+ assertEq(preferences.git?.remote, "upstream", "git.remote passes through");
96
+ assertEq(preferences.git?.snapshots, true, "git.snapshots passes through");
97
+ assertEq(preferences.git?.pre_merge_check, "auto", "git.pre_merge_check passes through");
98
+ assertEq(preferences.git?.commit_type, "feat", "git.commit_type passes through");
99
+ assertEq(preferences.git?.main_branch, "develop", "git.main_branch passes through");
100
+ assertEq(preferences.git?.merge_strategy, "squash", "git.merge_strategy passes through");
101
+ assertEq(preferences.git?.isolation, "branch", "git.isolation passes through");
102
+ }
103
+
104
+ console.log("\n=== uat_dispatch validates correctly ===");
105
+
106
+ {
107
+ const { preferences, errors } = validatePreferences({ uat_dispatch: true });
108
+ assertEq(errors.length, 0, "valid uat_dispatch produces no errors");
109
+ assertEq(preferences.uat_dispatch, true, "uat_dispatch true passes through");
110
+ }
111
+
112
+ {
113
+ const { preferences, errors } = validatePreferences({ uat_dispatch: false });
114
+ assertEq(errors.length, 0, "valid uat_dispatch false produces no errors");
115
+ assertEq(preferences.uat_dispatch, false, "uat_dispatch false passes through");
116
+ }
117
+
118
+ console.log("\n=== unique_milestone_ids validates correctly ===");
119
+
120
+ {
121
+ const { preferences, errors } = validatePreferences({ unique_milestone_ids: true });
122
+ assertEq(errors.length, 0, "valid unique_milestone_ids produces no errors");
123
+ assertEq(preferences.unique_milestone_ids, true, "unique_milestone_ids passes through");
124
+ }
125
+
126
+ console.log("\n=== all wizard fields together produce no errors ===");
127
+
128
+ {
129
+ const fullPrefs: GSDPreferences = {
130
+ version: 1,
131
+ models: { research: "claude-opus-4-6", planning: "claude-sonnet-4-6" },
132
+ auto_supervisor: { soft_timeout_minutes: 15, idle_timeout_minutes: 5, hard_timeout_minutes: 25 },
133
+ git: {
134
+ main_branch: "main",
135
+ auto_push: true,
136
+ push_branches: false,
137
+ remote: "origin",
138
+ snapshots: true,
139
+ pre_merge_check: "auto",
140
+ commit_type: "feat",
141
+ merge_strategy: "squash",
142
+ isolation: "worktree",
143
+ },
144
+ skill_discovery: "suggest",
145
+ unique_milestone_ids: false,
146
+ budget_ceiling: 50,
147
+ budget_enforcement: "pause",
148
+ context_pause_threshold: 75,
149
+ notifications: {
150
+ enabled: true,
151
+ on_complete: true,
152
+ on_error: true,
153
+ on_budget: true,
154
+ on_milestone: true,
155
+ on_attention: true,
156
+ },
157
+ uat_dispatch: false,
158
+ };
159
+ const { errors, warnings } = validatePreferences(fullPrefs);
160
+ const unknownWarnings = warnings.filter(w => w.includes("unknown"));
161
+ assertEq(errors.length, 0, "full wizard prefs produce no errors");
162
+ assertEq(unknownWarnings.length, 0, "full wizard prefs produce no unknown-key warnings");
163
+ }
164
+
165
+ report();
166
+ }
167
+
168
+ main();
@@ -0,0 +1,204 @@
1
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync, existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { tmpdir } from 'node:os';
4
+
5
+ import {
6
+ loadQueueOrder,
7
+ saveQueueOrder,
8
+ sortByQueueOrder,
9
+ pruneQueueOrder,
10
+ validateQueueOrder,
11
+ } from '../queue-order.ts';
12
+ import { createTestContext } from './test-helpers.ts';
13
+
14
+ const { assertEq, assertTrue, report } = createTestContext();
15
+
16
+ // ─── Fixture Helpers ───────────────────────────────────────────────────────
17
+
18
+ function createFixtureBase(): string {
19
+ const base = mkdtempSync(join(tmpdir(), 'gsd-queue-order-'));
20
+ mkdirSync(join(base, '.gsd'), { recursive: true });
21
+ return base;
22
+ }
23
+
24
+ function cleanup(base: string): void {
25
+ rmSync(base, { recursive: true, force: true });
26
+ }
27
+
28
+ // ═══════════════════════════════════════════════════════════════════════════
29
+ // sortByQueueOrder
30
+ // ═══════════════════════════════════════════════════════════════════════════
31
+
32
+ console.log('\n=== sortByQueueOrder ===');
33
+
34
+ // Null order → default milestoneIdSort
35
+ {
36
+ const result = sortByQueueOrder(['M003', 'M001', 'M002'], null);
37
+ assertEq(result, ['M001', 'M002', 'M003'], 'null order falls back to numeric sort');
38
+ }
39
+
40
+ // Custom order → exact sequence
41
+ {
42
+ const result = sortByQueueOrder(['M001', 'M002', 'M003'], ['M003', 'M001', 'M002']);
43
+ assertEq(result, ['M003', 'M001', 'M002'], 'custom order produces exact sequence');
44
+ }
45
+
46
+ // Custom order with new IDs → appended at end in numeric order
47
+ {
48
+ const result = sortByQueueOrder(['M001', 'M002', 'M003', 'M004'], ['M003', 'M001']);
49
+ assertEq(result, ['M003', 'M001', 'M002', 'M004'], 'new IDs appended in numeric order');
50
+ }
51
+
52
+ // Custom order with deleted IDs → silently skipped
53
+ {
54
+ const result = sortByQueueOrder(['M001', 'M003'], ['M003', 'M002', 'M001']);
55
+ assertEq(result, ['M003', 'M001'], 'deleted IDs in order are skipped');
56
+ }
57
+
58
+ // Empty custom order → all IDs in numeric order
59
+ {
60
+ const result = sortByQueueOrder(['M002', 'M001'], []);
61
+ assertEq(result, ['M001', 'M002'], 'empty custom order falls back to numeric sort');
62
+ }
63
+
64
+ // ═══════════════════════════════════════════════════════════════════════════
65
+ // loadQueueOrder / saveQueueOrder
66
+ // ═══════════════════════════════════════════════════════════════════════════
67
+
68
+ console.log('\n=== loadQueueOrder / saveQueueOrder ===');
69
+
70
+ // Load returns null when file doesn't exist
71
+ {
72
+ const base = createFixtureBase();
73
+ assertEq(loadQueueOrder(base), null, 'returns null when file missing');
74
+ cleanup(base);
75
+ }
76
+
77
+ // Save then load round-trip
78
+ {
79
+ const base = createFixtureBase();
80
+ saveQueueOrder(base, ['M003', 'M001', 'M002']);
81
+ const loaded = loadQueueOrder(base);
82
+ assertEq(loaded, ['M003', 'M001', 'M002'], 'round-trip preserves order');
83
+
84
+ // Verify file contains updatedAt
85
+ const raw = JSON.parse(readFileSync(join(base, '.gsd', 'QUEUE-ORDER.json'), 'utf-8'));
86
+ assertTrue(typeof raw.updatedAt === 'string' && raw.updatedAt.length > 0, 'file contains updatedAt');
87
+
88
+ cleanup(base);
89
+ }
90
+
91
+ // Load returns null on corrupt JSON
92
+ {
93
+ const base = createFixtureBase();
94
+ writeFileSync(join(base, '.gsd', 'QUEUE-ORDER.json'), 'not json');
95
+ assertEq(loadQueueOrder(base), null, 'returns null on corrupt JSON');
96
+ cleanup(base);
97
+ }
98
+
99
+ // Load returns null when order field is not an array
100
+ {
101
+ const base = createFixtureBase();
102
+ writeFileSync(join(base, '.gsd', 'QUEUE-ORDER.json'), '{"order": "invalid"}');
103
+ assertEq(loadQueueOrder(base), null, 'returns null when order is not array');
104
+ cleanup(base);
105
+ }
106
+
107
+ // ═══════════════════════════════════════════════════════════════════════════
108
+ // pruneQueueOrder
109
+ // ═══════════════════════════════════════════════════════════════════════════
110
+
111
+ console.log('\n=== pruneQueueOrder ===');
112
+
113
+ // Prune removes invalid IDs
114
+ {
115
+ const base = createFixtureBase();
116
+ saveQueueOrder(base, ['M001', 'M002', 'M003']);
117
+ pruneQueueOrder(base, ['M001', 'M003']);
118
+ assertEq(loadQueueOrder(base), ['M001', 'M003'], 'prune removes invalid IDs');
119
+ cleanup(base);
120
+ }
121
+
122
+ // Prune no-ops when file doesn't exist
123
+ {
124
+ const base = createFixtureBase();
125
+ pruneQueueOrder(base, ['M001']); // should not throw
126
+ assertTrue(!existsSync(join(base, '.gsd', 'QUEUE-ORDER.json')), 'prune does not create file');
127
+ cleanup(base);
128
+ }
129
+
130
+ // Prune no-ops when all IDs are valid
131
+ {
132
+ const base = createFixtureBase();
133
+ saveQueueOrder(base, ['M001', 'M002']);
134
+ pruneQueueOrder(base, ['M001', 'M002', 'M003']);
135
+ assertEq(loadQueueOrder(base), ['M001', 'M002'], 'prune is no-op when all valid');
136
+ cleanup(base);
137
+ }
138
+
139
+ // ═══════════════════════════════════════════════════════════════════════════
140
+ // validateQueueOrder
141
+ // ═══════════════════════════════════════════════════════════════════════════
142
+
143
+ console.log('\n=== validateQueueOrder ===');
144
+
145
+ // Valid order with no dependencies
146
+ {
147
+ const depsMap = new Map<string, string[]>();
148
+ const result = validateQueueOrder(['M001', 'M002'], depsMap, new Set());
149
+ assertTrue(result.valid, 'valid when no dependencies');
150
+ assertEq(result.violations.length, 0, 'no violations');
151
+ assertEq(result.redundant.length, 0, 'no redundancies');
152
+ }
153
+
154
+ // Dependency violation: M002 before M001, but M002 depends on M001
155
+ {
156
+ const depsMap = new Map<string, string[]>([['M002', ['M001']]]);
157
+ const result = validateQueueOrder(['M002', 'M001'], depsMap, new Set());
158
+ assertTrue(!result.valid, 'invalid when dep violated');
159
+ assertEq(result.violations.length, 1, 'one violation');
160
+ assertEq(result.violations[0].type, 'would_block', 'violation type is would_block');
161
+ assertEq(result.violations[0].milestone, 'M002', 'violation milestone is M002');
162
+ assertEq(result.violations[0].dependsOn, 'M001', 'violation dep is M001');
163
+ }
164
+
165
+ // Redundant dependency: M002 depends on M001, M001 comes first in order
166
+ {
167
+ const depsMap = new Map<string, string[]>([['M002', ['M001']]]);
168
+ const result = validateQueueOrder(['M001', 'M002'], depsMap, new Set());
169
+ assertTrue(result.valid, 'valid when dep satisfied by position');
170
+ assertEq(result.redundant.length, 1, 'one redundancy');
171
+ assertEq(result.redundant[0].milestone, 'M002', 'redundant milestone is M002');
172
+ }
173
+
174
+ // Completed dep is always satisfied
175
+ {
176
+ const depsMap = new Map<string, string[]>([['M002', ['M001']]]);
177
+ const result = validateQueueOrder(['M002'], depsMap, new Set(['M001']));
178
+ assertTrue(result.valid, 'valid when dep is already completed');
179
+ assertEq(result.violations.length, 0, 'no violations for completed dep');
180
+ }
181
+
182
+ // Missing dependency
183
+ {
184
+ const depsMap = new Map<string, string[]>([['M002', ['M099']]]);
185
+ const result = validateQueueOrder(['M001', 'M002'], depsMap, new Set());
186
+ assertTrue(!result.valid, 'invalid when dep does not exist');
187
+ assertEq(result.violations[0].type, 'missing_dep', 'violation type is missing_dep');
188
+ }
189
+
190
+ // Circular dependency
191
+ {
192
+ const depsMap = new Map<string, string[]>([
193
+ ['M001', ['M002']],
194
+ ['M002', ['M001']],
195
+ ]);
196
+ const result = validateQueueOrder(['M001', 'M002'], depsMap, new Set());
197
+ assertTrue(!result.valid, 'invalid on circular dependency');
198
+ const circularViolation = result.violations.find(v => v.type === 'circular');
199
+ assertTrue(!!circularViolation, 'circular violation detected');
200
+ }
201
+
202
+ // ═══════════════════════════════════════════════════════════════════════════
203
+
204
+ report();
@@ -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();