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
package/README.md CHANGED
@@ -21,6 +21,25 @@ One command. Walk away. Come back to a built project with clean git history.
21
21
 
22
22
  ---
23
23
 
24
+ ## Documentation
25
+
26
+ Full documentation is available in the [`docs/`](./docs/) directory:
27
+
28
+ - **[Getting Started](./docs/getting-started.md)** — install, first run, basic usage
29
+ - **[Auto Mode](./docs/auto-mode.md)** — autonomous execution deep-dive
30
+ - **[Configuration](./docs/configuration.md)** — all preferences, models, git, and hooks
31
+ - **[Token Optimization](./docs/token-optimization.md)** — profiles, context compression, complexity routing (v2.17)
32
+ - **[Cost Management](./docs/cost-management.md)** — budgets, tracking, projections
33
+ - **[Git Strategy](./docs/git-strategy.md)** — worktree isolation, branching, merge behavior
34
+ - **[Working in Teams](./docs/working-in-teams.md)** — unique IDs, shared artifacts
35
+ - **[Skills](./docs/skills.md)** — bundled skills, discovery, custom authoring
36
+ - **[Commands Reference](./docs/commands.md)** — all commands and keyboard shortcuts
37
+ - **[Architecture](./docs/architecture.md)** — system design and dispatch pipeline
38
+ - **[Troubleshooting](./docs/troubleshooting.md)** — common issues, doctor, recovery
39
+ - **[Migration from v1](./docs/migration.md)** — `.planning` → `.gsd` migration
40
+
41
+ ---
42
+
24
43
  ## What Changed From v1
25
44
 
26
45
  The original GSD was a collection of markdown prompts installed into `~/.claude/commands/`. It relied entirely on the LLM reading those prompts and doing the right thing. That worked surprisingly well — but it had hard limits:
@@ -334,6 +353,26 @@ unique_milestone_ids: true
334
353
  | `skill_rules` | Situational rules for skill routing |
335
354
  | `unique_milestone_ids` | Uses unique milestone names to avoid clashes when working in teams of people |
336
355
 
356
+ ### Token Optimization (v2.17)
357
+
358
+ GSD 2.17 introduced a coordinated token optimization system that reduces usage by 40-60% on cost-sensitive workloads. Set a single preference to coordinate model selection, phase skipping, and context compression:
359
+
360
+ ```yaml
361
+ token_profile: budget # or balanced (default), quality
362
+ ```
363
+
364
+ | Profile | Savings | What It Does |
365
+ |---------|---------|-------------|
366
+ | `budget` | 40-60% | Cheap models, skip research/reassess, minimal context inlining |
367
+ | `balanced` | 10-20% | Default models, skip slice research, standard context |
368
+ | `quality` | 0% | All phases, all context, full model power |
369
+
370
+ **Complexity-based routing** automatically classifies tasks as simple/standard/complex and routes to appropriate models. Simple docs tasks get Haiku; complex architectural work gets Opus. The classification is heuristic (sub-millisecond, no LLM calls) and learns from outcomes via a persistent routing history.
371
+
372
+ **Budget pressure** graduates model downgrading as you approach your budget ceiling — 50%, 75%, and 90% thresholds progressively shift work to cheaper tiers.
373
+
374
+ See the full [Token Optimization Guide](./docs/token-optimization.md) for details.
375
+
337
376
  ### Bundled Tools
338
377
 
339
378
  GSD ships with 14 extensions, all loaded automatically:
@@ -633,7 +633,7 @@ async function runRemoteQuestionsStep(p, pc, authStorage) {
633
633
  });
634
634
  if (p.isCancel(channelId) || !channelId)
635
635
  return null;
636
- const { saveRemoteQuestionsConfig } = await import('./resources/extensions/remote-questions/remote-command.js');
636
+ const { saveRemoteQuestionsConfig } = await import('./remote-questions-config.js');
637
637
  saveRemoteQuestionsConfig('slack', channelId.trim());
638
638
  p.log.success(`Slack channel: ${pc.green(channelId.trim())}`);
639
639
  return 'Slack';
@@ -736,7 +736,7 @@ async function runDiscordChannelStep(p, pc, token) {
736
736
  channelId = channelChoice;
737
737
  }
738
738
  // Save remote questions config
739
- const { saveRemoteQuestionsConfig } = await import('./resources/extensions/remote-questions/remote-command.js');
739
+ const { saveRemoteQuestionsConfig } = await import('./remote-questions-config.js');
740
740
  saveRemoteQuestionsConfig('discord', channelId);
741
741
  const channelName = channels.find(ch => ch.id === channelId)?.name;
742
742
  p.log.success(`Discord channel: ${pc.green(channelName ? `#${channelName}` : channelId)}`);
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Remote Questions Config Helper
3
+ *
4
+ * Extracted from remote-questions extension so onboarding.ts can import
5
+ * it without crossing the compiled/uncompiled boundary. The extension
6
+ * files in src/resources/ are shipped as raw .ts and loaded via jiti,
7
+ * but onboarding.ts is compiled by tsc — dynamic imports from compiled
8
+ * JS to uncompiled .ts fail at runtime (#592).
9
+ */
10
+ export declare function saveRemoteQuestionsConfig(channel: "slack" | "discord", channelId: string): void;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Remote Questions Config Helper
3
+ *
4
+ * Extracted from remote-questions extension so onboarding.ts can import
5
+ * it without crossing the compiled/uncompiled boundary. The extension
6
+ * files in src/resources/ are shipped as raw .ts and loaded via jiti,
7
+ * but onboarding.ts is compiled by tsc — dynamic imports from compiled
8
+ * JS to uncompiled .ts fail at runtime (#592).
9
+ */
10
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
11
+ import { dirname } from "node:path";
12
+ import { getGlobalGSDPreferencesPath } from "./resources/extensions/gsd/preferences.js";
13
+ export function saveRemoteQuestionsConfig(channel, channelId) {
14
+ const prefsPath = getGlobalGSDPreferencesPath();
15
+ const block = [
16
+ "remote_questions:",
17
+ ` channel: ${channel}`,
18
+ ` channel_id: "${channelId}"`,
19
+ " timeout_minutes: 5",
20
+ " poll_interval_seconds: 5",
21
+ ].join("\n");
22
+ const content = existsSync(prefsPath) ? readFileSync(prefsPath, "utf-8") : "";
23
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
24
+ let next = content;
25
+ if (fmMatch) {
26
+ let frontmatter = fmMatch[1];
27
+ const regex = /remote_questions:[\s\S]*?(?=\n[a-zA-Z_]|\n---|$)/;
28
+ frontmatter = regex.test(frontmatter) ? frontmatter.replace(regex, block) : `${frontmatter.trimEnd()}\n${block}`;
29
+ next = `---\n${frontmatter}\n---${content.slice(fmMatch[0].length)}`;
30
+ }
31
+ else {
32
+ next = `---\n${block}\n---\n\n${content}`;
33
+ }
34
+ mkdirSync(dirname(prefsPath), { recursive: true });
35
+ writeFileSync(prefsPath, next, "utf-8");
36
+ }
@@ -8,7 +8,7 @@
8
8
  * Diagnostic extraction is handled by session-forensics.ts.
9
9
  */
10
10
 
11
- import { writeFileSync, mkdirSync, readdirSync, unlinkSync, statSync, openSync, closeSync, constants } from "node:fs";
11
+ import { writeFileSync, writeSync, mkdirSync, readdirSync, unlinkSync, statSync, openSync, closeSync, constants } from "node:fs";
12
12
  import { createHash } from "node:crypto";
13
13
  import { join } from "node:path";
14
14
 
@@ -23,6 +23,15 @@ interface ActivityLogState {
23
23
 
24
24
  const activityLogState = new Map<string, ActivityLogState>();
25
25
 
26
+ /**
27
+ * Clear accumulated activity log state (#611).
28
+ * Call when auto-mode stops to prevent unbounded memory growth
29
+ * from lastSnapshotKeyByUnit maps accumulating across units.
30
+ */
31
+ export function clearActivityLogState(): void {
32
+ activityLogState.clear();
33
+ }
34
+
26
35
  function scanNextSequence(activityDir: string): number {
27
36
  let maxSeq = 0;
28
37
  try {
@@ -46,9 +55,21 @@ function getActivityState(activityDir: string): ActivityLogState {
46
55
  return state;
47
56
  }
48
57
 
49
- function snapshotKey(unitType: string, unitId: string, content: string): string {
50
- const digest = createHash("sha1").update(content).digest("hex");
51
- return `${unitType}\0${unitId}\0${digest}`;
58
+ /**
59
+ * Build a lightweight dedup key from session entries without serializing
60
+ * the entire content to a string (#611). Uses entry count + hash of
61
+ * the last few entries as a fingerprint instead of hashing megabytes.
62
+ */
63
+ function snapshotKey(unitType: string, unitId: string, entries: unknown[]): string {
64
+ const hash = createHash("sha1");
65
+ hash.update(`${unitType}\0${unitId}\0${entries.length}\0`);
66
+ // Hash only the last 3 entries as a fingerprint — if the session grew,
67
+ // the count change alone detects it; if content changed, the tail hash catches it.
68
+ const tail = entries.slice(-3);
69
+ for (const entry of tail) {
70
+ hash.update(JSON.stringify(entry));
71
+ }
72
+ return hash.digest("hex");
52
73
  }
53
74
 
54
75
  function nextActivityFilePath(
@@ -91,14 +112,23 @@ export function saveActivityLog(
91
112
  mkdirSync(activityDir, { recursive: true });
92
113
 
93
114
  const safeUnitId = unitId.replace(/\//g, "-");
94
- const content = `${entries.map(entry => JSON.stringify(entry)).join("\n")}\n`;
95
115
  const state = getActivityState(activityDir);
96
116
  const unitKey = `${unitType}\0${safeUnitId}`;
97
- const key = snapshotKey(unitType, safeUnitId, content);
117
+ // Use lightweight fingerprint instead of serializing all entries (#611)
118
+ const key = snapshotKey(unitType, safeUnitId, entries);
98
119
  if (state.lastSnapshotKeyByUnit.get(unitKey) === key) return;
99
120
 
100
121
  const filePath = nextActivityFilePath(activityDir, state, unitType, safeUnitId);
101
- writeFileSync(filePath, content, "utf-8");
122
+ // Stream entries to disk line-by-line instead of building one massive string (#611).
123
+ // For large sessions, the single-string approach allocated hundreds of MB.
124
+ const fd = openSync(filePath, "w");
125
+ try {
126
+ for (const entry of entries) {
127
+ writeSync(fd, JSON.stringify(entry) + "\n");
128
+ }
129
+ } finally {
130
+ closeSync(fd);
131
+ }
102
132
  state.nextSeq += 1;
103
133
  state.lastSnapshotKeyByUnit.set(unitKey, key);
104
134
  } catch (e) {
@@ -10,7 +10,7 @@ import type { ExtensionContext, ExtensionCommandContext } from "@gsd/pi-coding-a
10
10
  import type { GSDState } from "./types.js";
11
11
  import { getCurrentBranch } from "./worktree.js";
12
12
  import { getActiveHook } from "./post-unit-hooks.js";
13
- import { getLedger, getProjectTotals, formatCost, formatTokenCount } from "./metrics.js";
13
+ import { getLedger, getProjectTotals, formatCost, formatTokenCount, formatTierSavings } from "./metrics.js";
14
14
  import {
15
15
  resolveMilestoneFile,
16
16
  resolveSliceFile,
@@ -39,6 +39,8 @@ export interface AutoDashboardData {
39
39
  projectedRemainingCost?: number;
40
40
  /** Whether token profile has been auto-downgraded due to budget prediction */
41
41
  profileDowngraded?: boolean;
42
+ /** Number of pending captures awaiting triage (0 if none or file missing) */
43
+ pendingCaptureCount: number;
42
44
  }
43
45
 
44
46
  // ─── Unit Description Helpers ─────────────────────────────────────────────────
@@ -239,6 +241,7 @@ export function updateProgressWidget(
239
241
  unitId: string,
240
242
  state: GSDState,
241
243
  accessors: WidgetStateAccessors,
244
+ tierBadge?: string,
242
245
  ): void {
243
246
  if (!ctx.hasUI) return;
244
247
 
@@ -319,7 +322,8 @@ export function updateProgressWidget(
319
322
 
320
323
  const target = task ? `${task.id}: ${task.title}` : unitId;
321
324
  const actionLeft = `${pad}${theme.fg("accent", "▸")} ${theme.fg("accent", verb)} ${theme.fg("text", target)}`;
322
- const phaseBadge = theme.fg("dim", phaseLabel);
325
+ const tierTag = tierBadge ? theme.fg("dim", `[${tierBadge}] `) : "";
326
+ const phaseBadge = `${tierTag}${theme.fg("dim", phaseLabel)}`;
323
327
  lines.push(rightAlign(actionLeft, phaseBadge, width));
324
328
  lines.push("");
325
329
 
@@ -414,6 +418,14 @@ export function updateProgressWidget(
414
418
  ? `${modelPhase}${theme.fg("dim", modelDisplay)}`
415
419
  : "";
416
420
  lines.push(rightAlign(`${pad}${sLeft}`, sRight, width));
421
+
422
+ // Dynamic routing savings summary
423
+ if (mLedger && mLedger.units.some(u => u.tier)) {
424
+ const savings = formatTierSavings(mLedger.units);
425
+ if (savings) {
426
+ lines.push(truncateToWidth(theme.fg("dim", `${pad}${savings}`), width));
427
+ }
428
+ }
417
429
  }
418
430
 
419
431
  const hintParts: string[] = [];
@@ -89,7 +89,7 @@ export async function inlineDependencySummaries(
89
89
  export async function inlineGsdRootFile(
90
90
  base: string, filename: string, label: string,
91
91
  ): Promise<string | null> {
92
- const key = filename.replace(/\.md$/i, "").toUpperCase() as "PROJECT" | "DECISIONS" | "QUEUE" | "STATE" | "REQUIREMENTS";
92
+ const key = filename.replace(/\.md$/i, "").toUpperCase() as "PROJECT" | "DECISIONS" | "QUEUE" | "STATE" | "REQUIREMENTS" | "KNOWLEDGE";
93
93
  const absPath = resolveGsdRootFile(base, key);
94
94
  if (!existsSync(absPath)) return null;
95
95
  return inlineFileOptional(absPath, relGsdRootFile(key), label);
@@ -377,6 +377,8 @@ export async function buildResearchMilestonePrompt(mid: string, midTitle: string
377
377
  if (requirementsInline) inlined.push(requirementsInline);
378
378
  const decisionsInline = await inlineGsdRootFile(base, "decisions.md", "Decisions");
379
379
  if (decisionsInline) inlined.push(decisionsInline);
380
+ const knowledgeInlineRM = await inlineGsdRootFile(base, "knowledge.md", "Project Knowledge");
381
+ if (knowledgeInlineRM) inlined.push(knowledgeInlineRM);
380
382
  inlined.push(inlineTemplate("research", "Research"));
381
383
 
382
384
  const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
@@ -387,7 +389,7 @@ export async function buildResearchMilestonePrompt(mid: string, midTitle: string
387
389
  milestoneId: mid, milestoneTitle: midTitle,
388
390
  milestonePath: relMilestonePath(base, mid),
389
391
  contextPath: contextRel,
390
- outputPath: outputRelPath,
392
+ outputPath: join(base, outputRelPath),
391
393
  inlinedContext,
392
394
  ...buildSkillDiscoveryVars(),
393
395
  });
@@ -413,6 +415,8 @@ export async function buildPlanMilestonePrompt(mid: string, midTitle: string, ba
413
415
  if (requirementsInline) inlined.push(requirementsInline);
414
416
  const decisionsInline = inlineLevel !== "minimal" ? await inlineGsdRootFile(base, "decisions.md", "Decisions") : null;
415
417
  if (decisionsInline) inlined.push(decisionsInline);
418
+ const knowledgeInlinePM = await inlineGsdRootFile(base, "knowledge.md", "Project Knowledge");
419
+ if (knowledgeInlinePM) inlined.push(knowledgeInlinePM);
416
420
  inlined.push(inlineTemplate("roadmap", "Roadmap"));
417
421
  if (inlineLevel === "full") {
418
422
  inlined.push(inlineTemplate("decisions", "Decisions"));
@@ -428,14 +432,14 @@ export async function buildPlanMilestonePrompt(mid: string, midTitle: string, ba
428
432
  const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
429
433
 
430
434
  const outputRelPath = relMilestoneFile(base, mid, "ROADMAP");
431
- const secretsOutputPath = relMilestoneFile(base, mid, "SECRETS");
435
+ const secretsOutputPath = join(base, relMilestoneFile(base, mid, "SECRETS"));
432
436
  return loadPrompt("plan-milestone", {
433
437
  workingDirectory: base,
434
438
  milestoneId: mid, milestoneTitle: midTitle,
435
439
  milestonePath: relMilestonePath(base, mid),
436
440
  contextPath: contextRel,
437
441
  researchPath: researchRel,
438
- outputPath: outputRelPath,
442
+ outputPath: join(base, outputRelPath),
439
443
  secretsOutputPath,
440
444
  inlinedContext,
441
445
  });
@@ -461,6 +465,8 @@ export async function buildResearchSlicePrompt(
461
465
  if (decisionsInline) inlined.push(decisionsInline);
462
466
  const requirementsInline = await inlineGsdRootFile(base, "requirements.md", "Requirements");
463
467
  if (requirementsInline) inlined.push(requirementsInline);
468
+ const knowledgeInlineRS = await inlineGsdRootFile(base, "knowledge.md", "Project Knowledge");
469
+ if (knowledgeInlineRS) inlined.push(knowledgeInlineRS);
464
470
  inlined.push(inlineTemplate("research", "Research"));
465
471
 
466
472
  const depContent = await inlineDependencySummaries(mid, sid, base);
@@ -478,7 +484,7 @@ export async function buildResearchSlicePrompt(
478
484
  roadmapPath: roadmapRel,
479
485
  contextPath: contextRel,
480
486
  milestoneResearchPath: milestoneResearchRel,
481
- outputPath: outputRelPath,
487
+ outputPath: join(base, outputRelPath),
482
488
  inlinedContext,
483
489
  dependencySummaries: depContent,
484
490
  ...buildSkillDiscoveryVars(),
@@ -504,6 +510,8 @@ export async function buildPlanSlicePrompt(
504
510
  const requirementsInline = await inlineGsdRootFile(base, "requirements.md", "Requirements");
505
511
  if (requirementsInline) inlined.push(requirementsInline);
506
512
  }
513
+ const knowledgeInlinePS = await inlineGsdRootFile(base, "knowledge.md", "Project Knowledge");
514
+ if (knowledgeInlinePS) inlined.push(knowledgeInlinePS);
507
515
  inlined.push(inlineTemplate("plan", "Slice Plan"));
508
516
  if (inlineLevel === "full") {
509
517
  inlined.push(inlineTemplate("task-plan", "Task Plan"));
@@ -523,7 +531,7 @@ export async function buildPlanSlicePrompt(
523
531
  slicePath: relSlicePath(base, mid, sid),
524
532
  roadmapPath: roadmapRel,
525
533
  researchPath: researchRel,
526
- outputPath: outputRelPath,
534
+ outputPath: join(base, outputRelPath),
527
535
  inlinedContext,
528
536
  dependencySummaries: depContent,
529
537
  });
@@ -578,14 +586,19 @@ export async function buildExecuteTaskPrompt(
578
586
  ? priorSummaries.slice(-1)
579
587
  : priorSummaries;
580
588
  const carryForwardSection = await buildCarryForwardSection(effectivePriorSummaries, base);
589
+
590
+ // Inline project knowledge if available
591
+ const knowledgeInlineET = await inlineGsdRootFile(base, "knowledge.md", "Project Knowledge");
592
+
581
593
  const inlinedTemplates = inlineLevel === "minimal"
582
594
  ? inlineTemplate("task-summary", "Task Summary")
583
595
  : [
584
596
  inlineTemplate("task-summary", "Task Summary"),
585
597
  inlineTemplate("decisions", "Decisions"),
598
+ ...(knowledgeInlineET ? [knowledgeInlineET] : []),
586
599
  ].join("\n\n---\n\n");
587
600
 
588
- const taskSummaryPath = `${relSlicePath(base, mid, sid)}/tasks/${tid}-SUMMARY.md`;
601
+ const taskSummaryPath = join(base, `${relSlicePath(base, mid, sid)}/tasks/${tid}-SUMMARY.md`);
589
602
 
590
603
  const activeOverrides = await loadActiveOverrides(base);
591
604
  const overridesSection = formatOverridesSection(activeOverrides);
@@ -594,7 +607,7 @@ export async function buildExecuteTaskPrompt(
594
607
  overridesSection,
595
608
  workingDirectory: base,
596
609
  milestoneId: mid, sliceId: sid, sliceTitle: sTitle, taskId: tid, taskTitle: tTitle,
597
- planPath: relSliceFile(base, mid, sid, "PLAN"),
610
+ planPath: join(base, relSliceFile(base, mid, sid, "PLAN")),
598
611
  slicePath: relSlicePath(base, mid, sid),
599
612
  taskPlanPath: taskPlanRelPath,
600
613
  taskPlanInline,
@@ -624,6 +637,8 @@ export async function buildCompleteSlicePrompt(
624
637
  const requirementsInline = await inlineGsdRootFile(base, "requirements.md", "Requirements");
625
638
  if (requirementsInline) inlined.push(requirementsInline);
626
639
  }
640
+ const knowledgeInlineCS = await inlineGsdRootFile(base, "knowledge.md", "Project Knowledge");
641
+ if (knowledgeInlineCS) inlined.push(knowledgeInlineCS);
627
642
 
628
643
  // Inline all task summaries for this slice
629
644
  const tDir = resolveTasksDir(base, mid, sid);
@@ -650,14 +665,14 @@ export async function buildCompleteSlicePrompt(
650
665
  const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
651
666
 
652
667
  const sliceRel = relSlicePath(base, mid, sid);
653
- const sliceSummaryPath = `${sliceRel}/${sid}-SUMMARY.md`;
654
- const sliceUatPath = `${sliceRel}/${sid}-UAT.md`;
668
+ const sliceSummaryPath = join(base, `${sliceRel}/${sid}-SUMMARY.md`);
669
+ const sliceUatPath = join(base, `${sliceRel}/${sid}-UAT.md`);
655
670
 
656
671
  return loadPrompt("complete-slice", {
657
672
  workingDirectory: base,
658
673
  milestoneId: mid, sliceId: sid, sliceTitle: sTitle,
659
674
  slicePath: sliceRel,
660
- roadmapPath: roadmapRel,
675
+ roadmapPath: join(base, roadmapRel),
661
676
  inlinedContext,
662
677
  sliceSummaryPath,
663
678
  sliceUatPath,
@@ -697,6 +712,8 @@ export async function buildCompleteMilestonePrompt(
697
712
  const projectInline = await inlineGsdRootFile(base, "project.md", "Project");
698
713
  if (projectInline) inlined.push(projectInline);
699
714
  }
715
+ const knowledgeInlineCM = await inlineGsdRootFile(base, "knowledge.md", "Project Knowledge");
716
+ if (knowledgeInlineCM) inlined.push(knowledgeInlineCM);
700
717
  // Inline milestone context file (milestone-level, not GSD root)
701
718
  const contextPath = resolveMilestoneFile(base, mid, "CONTEXT");
702
719
  const contextRel = relMilestoneFile(base, mid, "CONTEXT");
@@ -706,7 +723,7 @@ export async function buildCompleteMilestonePrompt(
706
723
 
707
724
  const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
708
725
 
709
- const milestoneSummaryPath = `${relMilestonePath(base, mid)}/${mid}-SUMMARY.md`;
726
+ const milestoneSummaryPath = join(base, `${relMilestonePath(base, mid)}/${mid}-SUMMARY.md`);
710
727
 
711
728
  return loadPrompt("complete-milestone", {
712
729
  workingDirectory: base,
@@ -758,7 +775,21 @@ export async function buildReplanSlicePrompt(
758
775
 
759
776
  const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
760
777
 
761
- const replanPath = `${relSlicePath(base, mid, sid)}/${sid}-REPLAN.md`;
778
+ const replanPath = join(base, `${relSlicePath(base, mid, sid)}/${sid}-REPLAN.md`);
779
+
780
+ // Build capture context for replan prompt (captures that triggered this replan)
781
+ let captureContext = "(none)";
782
+ try {
783
+ const { loadReplanCaptures } = await import("./triage-resolution.js");
784
+ const replanCaptures = loadReplanCaptures(base);
785
+ if (replanCaptures.length > 0) {
786
+ captureContext = replanCaptures.map(c =>
787
+ `- **${c.id}**: "${c.text}" — ${c.rationale ?? "no rationale"}`
788
+ ).join("\n");
789
+ }
790
+ } catch {
791
+ // Non-fatal — captures module may not be available
792
+ }
762
793
 
763
794
  return loadPrompt("replan-slice", {
764
795
  workingDirectory: base,
@@ -766,10 +797,11 @@ export async function buildReplanSlicePrompt(
766
797
  sliceId: sid,
767
798
  sliceTitle: sTitle,
768
799
  slicePath: relSlicePath(base, mid, sid),
769
- planPath: slicePlanRel,
800
+ planPath: join(base, slicePlanRel),
770
801
  blockerTaskId,
771
802
  inlinedContext,
772
803
  replanPath,
804
+ captureContext,
773
805
  });
774
806
  }
775
807
 
@@ -791,7 +823,7 @@ export async function buildRunUatPrompt(
791
823
 
792
824
  const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
793
825
 
794
- const uatResultPath = relSliceFile(base, mid, sliceId, "UAT-RESULT");
826
+ const uatResultPath = join(base, relSliceFile(base, mid, sliceId, "UAT-RESULT"));
795
827
  const uatType = extractUatType(uatContent) ?? "human-experience";
796
828
 
797
829
  return loadPrompt("run-uat", {
@@ -825,10 +857,26 @@ export async function buildReassessRoadmapPrompt(
825
857
  const decisionsInline = await inlineGsdRootFile(base, "decisions.md", "Decisions");
826
858
  if (decisionsInline) inlined.push(decisionsInline);
827
859
  }
860
+ const knowledgeInlineRA = await inlineGsdRootFile(base, "knowledge.md", "Project Knowledge");
861
+ if (knowledgeInlineRA) inlined.push(knowledgeInlineRA);
828
862
 
829
863
  const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
830
864
 
831
- const assessmentPath = relSliceFile(base, mid, completedSliceId, "ASSESSMENT");
865
+ const assessmentPath = join(base, relSliceFile(base, mid, completedSliceId, "ASSESSMENT"));
866
+
867
+ // Build deferred captures context for reassess prompt
868
+ let deferredCaptures = "(none)";
869
+ try {
870
+ const { loadDeferredCaptures } = await import("./triage-resolution.js");
871
+ const deferred = loadDeferredCaptures(base);
872
+ if (deferred.length > 0) {
873
+ deferredCaptures = deferred.map(c =>
874
+ `- **${c.id}**: "${c.text}" — ${c.rationale ?? "deferred during triage"}`
875
+ ).join("\n");
876
+ }
877
+ } catch {
878
+ // Non-fatal — captures module may not be available
879
+ }
832
880
 
833
881
  return loadPrompt("reassess-roadmap", {
834
882
  workingDirectory: base,
@@ -839,6 +887,7 @@ export async function buildReassessRoadmapPrompt(
839
887
  completedSliceSummaryPath: summaryRel,
840
888
  assessmentPath,
841
889
  inlinedContext,
890
+ deferredCaptures,
842
891
  });
843
892
  }
844
893
 
@@ -14,8 +14,10 @@ import {
14
14
  removeWorktree,
15
15
  worktreePath,
16
16
  } from "./worktree-manager.js";
17
+ import { detectWorktreeName } from "./worktree.js";
17
18
  import {
18
19
  MergeConflictError,
20
+ readIntegrationBranch,
19
21
  } from "./git-service.js";
20
22
  import { parseRoadmap } from "./files.js";
21
23
  import { loadEffectiveGSDPreferences } from "./preferences.js";
@@ -90,7 +92,12 @@ export function autoWorktreeBranch(milestoneId: string): string {
90
92
  */
91
93
  export function createAutoWorktree(basePath: string, milestoneId: string): string {
92
94
  const branch = autoWorktreeBranch(milestoneId);
93
- const info = createWorktree(basePath, milestoneId, { branch });
95
+
96
+ // Use the integration branch recorded in META.json as the start point.
97
+ // This ensures the worktree branch is created from the branch the user
98
+ // was on when they started the milestone (e.g. f-setup-gsd-2), not main.
99
+ const integrationBranch = readIntegrationBranch(basePath, milestoneId) ?? undefined;
100
+ const info = createWorktree(basePath, milestoneId, { branch, startPoint: integrationBranch });
94
101
 
95
102
  // Copy .gsd/ planning artifacts from the source repo into the new worktree.
96
103
  // Worktrees are fresh git checkouts — untracked files don't carry over.
@@ -224,6 +231,27 @@ export function getAutoWorktreeOriginalBase(): string | null {
224
231
  return originalBase;
225
232
  }
226
233
 
234
+ export function getActiveAutoWorktreeContext(): {
235
+ originalBase: string;
236
+ worktreeName: string;
237
+ branch: string;
238
+ } | null {
239
+ if (!originalBase) return null;
240
+ const cwd = process.cwd();
241
+ const resolvedBase = existsSync(originalBase) ? realpathSync(originalBase) : originalBase;
242
+ const wtDir = join(resolvedBase, ".gsd", "worktrees");
243
+ if (!cwd.startsWith(wtDir)) return null;
244
+ const worktreeName = detectWorktreeName(cwd);
245
+ if (!worktreeName) return null;
246
+ const branch = nativeGetCurrentBranch(cwd);
247
+ if (!branch.startsWith("milestone/")) return null;
248
+ return {
249
+ originalBase,
250
+ worktreeName,
251
+ branch,
252
+ };
253
+ }
254
+
227
255
  // ─── Merge Milestone -> Main ───────────────────────────────────────────────
228
256
 
229
257
  /**
@@ -279,11 +307,12 @@ export function mergeMilestoneToMain(
279
307
  const previousCwd = process.cwd();
280
308
  process.chdir(originalBasePath_);
281
309
 
282
- // 4. Resolve main branch from preferences
310
+ // 4. Resolve integration branch prefer milestone metadata, fall back to preferences / "main"
283
311
  const prefs = loadEffectiveGSDPreferences()?.preferences?.git ?? {};
284
- const mainBranch = prefs.main_branch || "main";
312
+ const integrationBranch = readIntegrationBranch(originalBasePath_, milestoneId);
313
+ const mainBranch = integrationBranch ?? prefs.main_branch ?? "main";
285
314
 
286
- // 5. Checkout main
315
+ // 5. Checkout integration branch
287
316
  nativeCheckoutBranch(originalBasePath_, mainBranch);
288
317
 
289
318
  // 6. Build rich commit message