gsd-pi 2.17.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 (153) 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-prompts.ts +20 -1
  7. package/dist/resources/extensions/gsd/auto-worktree.ts +33 -4
  8. package/dist/resources/extensions/gsd/auto.ts +123 -10
  9. package/dist/resources/extensions/gsd/commands.ts +245 -22
  10. package/dist/resources/extensions/gsd/dispatch-guard.ts +7 -19
  11. package/dist/resources/extensions/gsd/docs/preferences-reference.md +201 -2
  12. package/dist/resources/extensions/gsd/files.ts +123 -1
  13. package/dist/resources/extensions/gsd/guided-flow.ts +237 -4
  14. package/dist/resources/extensions/gsd/index.ts +47 -3
  15. package/dist/resources/extensions/gsd/paths.ts +9 -0
  16. package/dist/resources/extensions/gsd/preferences.ts +59 -1
  17. package/dist/resources/extensions/gsd/prompts/execute-task.md +6 -5
  18. package/dist/resources/extensions/gsd/prompts/system.md +2 -0
  19. package/dist/resources/extensions/gsd/queue-order.ts +231 -0
  20. package/dist/resources/extensions/gsd/queue-reorder-ui.ts +263 -0
  21. package/dist/resources/extensions/gsd/state.ts +15 -3
  22. package/dist/resources/extensions/gsd/templates/knowledge.md +19 -0
  23. package/dist/resources/extensions/gsd/templates/preferences.md +14 -0
  24. package/dist/resources/extensions/gsd/tests/auto-worktree.test.ts +20 -0
  25. package/dist/resources/extensions/gsd/tests/derive-state-deps.test.ts +99 -0
  26. package/dist/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +79 -0
  27. package/dist/resources/extensions/gsd/tests/knowledge.test.ts +161 -0
  28. package/dist/resources/extensions/gsd/tests/memory-leak-guards.test.ts +87 -0
  29. package/dist/resources/extensions/gsd/tests/preferences-wizard-fields.test.ts +168 -0
  30. package/dist/resources/extensions/gsd/tests/queue-order.test.ts +204 -0
  31. package/dist/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +281 -0
  32. package/dist/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +139 -0
  33. package/dist/resources/extensions/gsd/worktree-manager.ts +8 -5
  34. package/dist/resources/extensions/gsd/worktree.ts +22 -0
  35. package/dist/resources/extensions/shared/next-action-ui.ts +16 -1
  36. package/package.json +1 -1
  37. package/packages/pi-coding-agent/dist/cli/args.d.ts +5 -0
  38. package/packages/pi-coding-agent/dist/cli/args.d.ts.map +1 -1
  39. package/packages/pi-coding-agent/dist/cli/args.js +21 -0
  40. package/packages/pi-coding-agent/dist/cli/args.js.map +1 -1
  41. package/packages/pi-coding-agent/dist/cli/list-models.d.ts +14 -3
  42. package/packages/pi-coding-agent/dist/cli/list-models.d.ts.map +1 -1
  43. package/packages/pi-coding-agent/dist/cli/list-models.js +52 -17
  44. package/packages/pi-coding-agent/dist/cli/list-models.js.map +1 -1
  45. package/packages/pi-coding-agent/dist/core/discovery-cache.d.ts +27 -0
  46. package/packages/pi-coding-agent/dist/core/discovery-cache.d.ts.map +1 -0
  47. package/packages/pi-coding-agent/dist/core/discovery-cache.js +79 -0
  48. package/packages/pi-coding-agent/dist/core/discovery-cache.js.map +1 -0
  49. package/packages/pi-coding-agent/dist/core/discovery-cache.test.d.ts +2 -0
  50. package/packages/pi-coding-agent/dist/core/discovery-cache.test.d.ts.map +1 -0
  51. package/packages/pi-coding-agent/dist/core/discovery-cache.test.js +140 -0
  52. package/packages/pi-coding-agent/dist/core/discovery-cache.test.js.map +1 -0
  53. package/packages/pi-coding-agent/dist/core/model-discovery.d.ts +35 -0
  54. package/packages/pi-coding-agent/dist/core/model-discovery.d.ts.map +1 -0
  55. package/packages/pi-coding-agent/dist/core/model-discovery.js +162 -0
  56. package/packages/pi-coding-agent/dist/core/model-discovery.js.map +1 -0
  57. package/packages/pi-coding-agent/dist/core/model-discovery.test.d.ts +2 -0
  58. package/packages/pi-coding-agent/dist/core/model-discovery.test.d.ts.map +1 -0
  59. package/packages/pi-coding-agent/dist/core/model-discovery.test.js +100 -0
  60. package/packages/pi-coding-agent/dist/core/model-discovery.test.js.map +1 -0
  61. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.d.ts +2 -0
  62. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.d.ts.map +1 -0
  63. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js +113 -0
  64. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js.map +1 -0
  65. package/packages/pi-coding-agent/dist/core/model-registry.d.ts +26 -0
  66. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  67. package/packages/pi-coding-agent/dist/core/model-registry.js +98 -0
  68. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  69. package/packages/pi-coding-agent/dist/core/models-json-writer.d.ts +62 -0
  70. package/packages/pi-coding-agent/dist/core/models-json-writer.d.ts.map +1 -0
  71. package/packages/pi-coding-agent/dist/core/models-json-writer.js +145 -0
  72. package/packages/pi-coding-agent/dist/core/models-json-writer.js.map +1 -0
  73. package/packages/pi-coding-agent/dist/core/models-json-writer.test.d.ts +2 -0
  74. package/packages/pi-coding-agent/dist/core/models-json-writer.test.d.ts.map +1 -0
  75. package/packages/pi-coding-agent/dist/core/models-json-writer.test.js +118 -0
  76. package/packages/pi-coding-agent/dist/core/models-json-writer.test.js.map +1 -0
  77. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +9 -0
  78. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  79. package/packages/pi-coding-agent/dist/core/settings-manager.js +11 -0
  80. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  81. package/packages/pi-coding-agent/dist/core/slash-commands.d.ts.map +1 -1
  82. package/packages/pi-coding-agent/dist/core/slash-commands.js +1 -0
  83. package/packages/pi-coding-agent/dist/core/slash-commands.js.map +1 -1
  84. package/packages/pi-coding-agent/dist/index.d.ts +5 -1
  85. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  86. package/packages/pi-coding-agent/dist/index.js +4 -1
  87. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  88. package/packages/pi-coding-agent/dist/main.d.ts.map +1 -1
  89. package/packages/pi-coding-agent/dist/main.js +17 -2
  90. package/packages/pi-coding-agent/dist/main.js.map +1 -1
  91. package/packages/pi-coding-agent/dist/modes/interactive/components/index.d.ts +1 -0
  92. package/packages/pi-coding-agent/dist/modes/interactive/components/index.d.ts.map +1 -1
  93. package/packages/pi-coding-agent/dist/modes/interactive/components/index.js +1 -0
  94. package/packages/pi-coding-agent/dist/modes/interactive/components/index.js.map +1 -1
  95. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js +1 -1
  96. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js.map +1 -1
  97. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts +25 -0
  98. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts.map +1 -0
  99. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js +121 -0
  100. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js.map +1 -0
  101. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +1 -0
  102. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  103. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +32 -0
  104. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  105. package/packages/pi-coding-agent/src/cli/args.ts +21 -0
  106. package/packages/pi-coding-agent/src/cli/list-models.ts +70 -17
  107. package/packages/pi-coding-agent/src/core/discovery-cache.test.ts +170 -0
  108. package/packages/pi-coding-agent/src/core/discovery-cache.ts +97 -0
  109. package/packages/pi-coding-agent/src/core/model-discovery.test.ts +125 -0
  110. package/packages/pi-coding-agent/src/core/model-discovery.ts +231 -0
  111. package/packages/pi-coding-agent/src/core/model-registry-discovery.test.ts +135 -0
  112. package/packages/pi-coding-agent/src/core/model-registry.ts +107 -0
  113. package/packages/pi-coding-agent/src/core/models-json-writer.test.ts +145 -0
  114. package/packages/pi-coding-agent/src/core/models-json-writer.ts +188 -0
  115. package/packages/pi-coding-agent/src/core/settings-manager.ts +21 -0
  116. package/packages/pi-coding-agent/src/core/slash-commands.ts +1 -0
  117. package/packages/pi-coding-agent/src/index.ts +5 -0
  118. package/packages/pi-coding-agent/src/main.ts +19 -2
  119. package/packages/pi-coding-agent/src/modes/interactive/components/index.ts +1 -0
  120. package/packages/pi-coding-agent/src/modes/interactive/components/model-selector.ts +1 -1
  121. package/packages/pi-coding-agent/src/modes/interactive/components/provider-manager.ts +163 -0
  122. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +37 -0
  123. package/src/resources/extensions/gsd/activity-log.ts +37 -7
  124. package/src/resources/extensions/gsd/auto-prompts.ts +20 -1
  125. package/src/resources/extensions/gsd/auto-worktree.ts +33 -4
  126. package/src/resources/extensions/gsd/auto.ts +123 -10
  127. package/src/resources/extensions/gsd/commands.ts +245 -22
  128. package/src/resources/extensions/gsd/dispatch-guard.ts +7 -19
  129. package/src/resources/extensions/gsd/docs/preferences-reference.md +201 -2
  130. package/src/resources/extensions/gsd/files.ts +123 -1
  131. package/src/resources/extensions/gsd/guided-flow.ts +237 -4
  132. package/src/resources/extensions/gsd/index.ts +47 -3
  133. package/src/resources/extensions/gsd/paths.ts +9 -0
  134. package/src/resources/extensions/gsd/preferences.ts +59 -1
  135. package/src/resources/extensions/gsd/prompts/execute-task.md +6 -5
  136. package/src/resources/extensions/gsd/prompts/system.md +2 -0
  137. package/src/resources/extensions/gsd/queue-order.ts +231 -0
  138. package/src/resources/extensions/gsd/queue-reorder-ui.ts +263 -0
  139. package/src/resources/extensions/gsd/state.ts +15 -3
  140. package/src/resources/extensions/gsd/templates/knowledge.md +19 -0
  141. package/src/resources/extensions/gsd/templates/preferences.md +14 -0
  142. package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +20 -0
  143. package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +99 -0
  144. package/src/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +79 -0
  145. package/src/resources/extensions/gsd/tests/knowledge.test.ts +161 -0
  146. package/src/resources/extensions/gsd/tests/memory-leak-guards.test.ts +87 -0
  147. package/src/resources/extensions/gsd/tests/preferences-wizard-fields.test.ts +168 -0
  148. package/src/resources/extensions/gsd/tests/queue-order.test.ts +204 -0
  149. package/src/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +281 -0
  150. package/src/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +139 -0
  151. package/src/resources/extensions/gsd/worktree-manager.ts +8 -5
  152. package/src/resources/extensions/gsd/worktree.ts +22 -0
  153. 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) {
@@ -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")}`;
@@ -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"));
@@ -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);
@@ -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"));
@@ -578,11 +586,16 @@ 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
601
  const taskSummaryPath = `${relSlicePath(base, mid, sid)}/tasks/${tid}-SUMMARY.md`;
@@ -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);
@@ -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");
@@ -825,6 +842,8 @@ export async function buildReassessRoadmapPrompt(
825
842
  const decisionsInline = await inlineGsdRootFile(base, "decisions.md", "Decisions");
826
843
  if (decisionsInline) inlined.push(decisionsInline);
827
844
  }
845
+ const knowledgeInlineRA = await inlineGsdRootFile(base, "knowledge.md", "Project Knowledge");
846
+ if (knowledgeInlineRA) inlined.push(knowledgeInlineRA);
828
847
 
829
848
  const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
830
849
 
@@ -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
@@ -29,7 +29,7 @@ import {
29
29
  buildMilestoneFileName, buildSliceFileName, buildTaskFileName,
30
30
  } from "./paths.js";
31
31
  import { invalidateAllCaches } from "./cache.js";
32
- import { saveActivityLog } from "./activity-log.js";
32
+ import { saveActivityLog, clearActivityLogState } from "./activity-log.js";
33
33
  import { synthesizeCrashRecovery, getDeepDiagnostic } from "./session-forensics.js";
34
34
  import { writeLock, clearLock, readCrashLock, formatCrashInfo, isLockProcessAlive } from "./crash-recovery.js";
35
35
  import {
@@ -92,7 +92,9 @@ import {
92
92
  getAutoWorktreePath,
93
93
  getAutoWorktreeOriginalBase,
94
94
  mergeMilestoneToMain,
95
+ autoWorktreeBranch,
95
96
  } from "./auto-worktree.js";
97
+ import { pruneQueueOrder } from "./queue-order.js";
96
98
  import { showNextAction } from "../shared/next-action-ui.js";
97
99
  import {
98
100
  resolveExpectedArtifactPath,
@@ -196,6 +198,33 @@ function shouldUseWorktreeIsolation(): boolean {
196
198
  return true; // default: worktree
197
199
  }
198
200
 
201
+ /**
202
+ * Detect and escape a stale worktree cwd (#608).
203
+ *
204
+ * After milestone completion + merge, the worktree directory is removed but
205
+ * the process cwd may still point inside `.gsd/worktrees/<MID>/`.
206
+ * When a new session starts, `process.cwd()` is passed as `base` to startAuto
207
+ * and all subsequent writes land in the wrong directory. This function detects
208
+ * that scenario and chdir back to the project root.
209
+ *
210
+ * Returns the corrected base path.
211
+ */
212
+ function escapeStaleWorktree(base: string): string {
213
+ const marker = `${pathSep}.gsd${pathSep}worktrees${pathSep}`;
214
+ const idx = base.indexOf(marker);
215
+ if (idx === -1) return base;
216
+
217
+ // base is inside .gsd/worktrees/<something> — extract the project root
218
+ const projectRoot = base.slice(0, idx);
219
+ try {
220
+ process.chdir(projectRoot);
221
+ } catch {
222
+ // If chdir fails, return the original — caller will handle errors downstream
223
+ return base;
224
+ }
225
+ return projectRoot;
226
+ }
227
+
199
228
  /** Crash recovery prompt — set by startAuto, consumed by first dispatchNextUnit */
200
229
  let pendingCrashRecovery: string | null = null;
201
230
 
@@ -228,6 +257,9 @@ const DISPATCH_GAP_TIMEOUT_MS = 5_000; // 5 seconds
228
257
  /** SIGTERM handler registered while auto-mode is active — cleared on stop/pause. */
229
258
  let _sigtermHandler: (() => void) | null = null;
230
259
 
260
+ /** Tool calls currently being executed — prevents false idle detection during long-running tools. */
261
+ const inFlightTools = new Set<string>();
262
+
231
263
  type BudgetAlertLevel = 0 | 75 | 90 | 100;
232
264
 
233
265
  export function getBudgetAlertLevel(budgetPct: number): BudgetAlertLevel {
@@ -293,6 +325,22 @@ export function isAutoPaused(): boolean {
293
325
  return paused;
294
326
  }
295
327
 
328
+ /**
329
+ * Mark a tool execution as in-flight. Called from index.ts on tool_execution_start.
330
+ * Prevents the idle watchdog from declaring the agent idle while tools are executing.
331
+ */
332
+ export function markToolStart(toolCallId: string): void {
333
+ if (!active) return;
334
+ inFlightTools.add(toolCallId);
335
+ }
336
+
337
+ /**
338
+ * Mark a tool execution as completed. Called from index.ts on tool_execution_end.
339
+ */
340
+ export function markToolEnd(toolCallId: string): void {
341
+ inFlightTools.delete(toolCallId);
342
+ }
343
+
296
344
  /**
297
345
  * Return the base path to use for the auto.lock file.
298
346
  * Always uses the original project root (not the worktree) so that
@@ -345,6 +393,7 @@ function clearUnitTimeout(): void {
345
393
  clearInterval(idleWatchdogHandle);
346
394
  idleWatchdogHandle = null;
347
395
  }
396
+ inFlightTools.clear();
348
397
  clearDispatchGapWatchdog();
349
398
  }
350
399
 
@@ -426,14 +475,18 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI): Promi
426
475
  `Auto-worktree teardown failed: ${err instanceof Error ? err.message : String(err)}`,
427
476
  "warning",
428
477
  );
429
- // Force basePath back to original even if teardown failed
430
- if (originalBasePath) {
431
- basePath = originalBasePath;
432
- try { process.chdir(basePath); } catch { /* best-effort */ }
433
- }
434
478
  }
435
479
  }
436
480
 
481
+ // Always restore cwd to project root on stop (#608).
482
+ // Even if isInAutoWorktree returned false (e.g., module state was already
483
+ // cleared by mergeMilestoneToMain), the process cwd may still be inside
484
+ // the worktree directory. Force it back to originalBasePath.
485
+ if (originalBasePath) {
486
+ basePath = originalBasePath;
487
+ try { process.chdir(basePath); } catch { /* best-effort */ }
488
+ }
489
+
437
490
  const ledger = getLedger();
438
491
  if (ledger && ledger.units.length > 0) {
439
492
  const totals = getProjectTotals(ledger.units);
@@ -458,12 +511,15 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI): Promi
458
511
  stepMode = false;
459
512
  unitDispatchCount.clear();
460
513
  unitRecoveryCount.clear();
514
+ inFlightTools.clear();
461
515
  lastBudgetAlertLevel = 0;
462
516
  unitLifetimeDispatches.clear();
463
517
  currentUnit = null;
464
518
  currentMilestoneId = null;
465
519
  originalBasePath = "";
520
+ completedUnits = [];
466
521
  clearSliceProgressCache();
522
+ clearActivityLogState();
467
523
  pendingCrashRecovery = null;
468
524
  _handlingAgentEnd = false;
469
525
  ctx?.ui.setStatus("gsd-auto", undefined);
@@ -519,6 +575,11 @@ export async function startAuto(
519
575
  ): Promise<void> {
520
576
  const requestedStepMode = options?.step ?? false;
521
577
 
578
+ // Escape stale worktree cwd from a previous milestone (#608).
579
+ // After milestone merge + worktree removal, the process cwd may still point
580
+ // inside .gsd/worktrees/<MID>/ — detect and chdir back to project root.
581
+ base = escapeStaleWorktree(base);
582
+
522
583
  // If resuming from paused state, just re-activate and dispatch next unit.
523
584
  // The conversation is still intact — no need to reinitialize everything.
524
585
  if (paused) {
@@ -569,17 +630,17 @@ export async function startAuto(
569
630
  ctx.ui.setFooter(hideFooter);
570
631
  ctx.ui.notify(stepMode ? "Step-mode resumed." : "Auto-mode resumed.", "info");
571
632
  // Restore hook state from disk in case session was interrupted
572
- restoreHookState(base);
633
+ restoreHookState(basePath);
573
634
  // Rebuild disk state before resuming — user interaction during pause may have changed files
574
- try { await rebuildState(base); } catch { /* non-fatal */ }
635
+ try { await rebuildState(basePath); } catch { /* non-fatal */ }
575
636
  try {
576
- const report = await runGSDDoctor(base, { fix: true });
637
+ const report = await runGSDDoctor(basePath, { fix: true });
577
638
  if (report.fixesApplied.length > 0) {
578
639
  ctx.ui.notify(`Resume: applied ${report.fixesApplied.length} fix(es) to state.`, "info");
579
640
  }
580
641
  } catch { /* non-fatal */ }
581
642
  // Self-heal: clear stale runtime records where artifacts already exist
582
- await selfHealRuntimeRecords(base, ctx, completedKeySet);
643
+ await selfHealRuntimeRecords(basePath, ctx, completedKeySet);
583
644
  invalidateAllCaches();
584
645
  await dispatchNextUnit(ctx, pi);
585
646
  return;
@@ -1251,6 +1312,11 @@ async function dispatchNextUnit(
1251
1312
  unitLifetimeDispatches.clear();
1252
1313
  // Capture integration branch for the new milestone and update git service
1253
1314
  captureIntegrationBranch(originalBasePath || basePath, mid, { commitDocs: loadEffectiveGSDPreferences()?.preferences?.git?.commit_docs });
1315
+ // Prune completed milestone from queue order file
1316
+ const pendingIds = state.registry
1317
+ .filter(m => m.status !== "complete")
1318
+ .map(m => m.id);
1319
+ pruneQueueOrder(basePath, pendingIds);
1254
1320
  }
1255
1321
  if (mid) {
1256
1322
  currentMilestoneId = mid;
@@ -1331,6 +1397,39 @@ async function dispatchNextUnit(
1331
1397
  `Milestone merge failed: ${err instanceof Error ? err.message : String(err)}`,
1332
1398
  "warning",
1333
1399
  );
1400
+ // Ensure cwd is restored even if merge failed partway through (#608).
1401
+ // mergeMilestoneToMain may have chdir'd but then thrown, leaving us
1402
+ // in an indeterminate location.
1403
+ if (originalBasePath) {
1404
+ basePath = originalBasePath;
1405
+ try { process.chdir(basePath); } catch { /* best-effort */ }
1406
+ }
1407
+ }
1408
+ } else if (currentMilestoneId && !isInAutoWorktree(basePath)) {
1409
+ // Branch isolation mode (#603): no worktree, but we may be on a milestone/* branch.
1410
+ // Squash-merge back to the integration branch (or main) before stopping.
1411
+ try {
1412
+ const currentBranch = getCurrentBranch(basePath);
1413
+ const milestoneBranch = autoWorktreeBranch(currentMilestoneId);
1414
+ if (currentBranch === milestoneBranch) {
1415
+ const roadmapPath = resolveMilestoneFile(basePath, currentMilestoneId, "ROADMAP");
1416
+ if (roadmapPath) {
1417
+ const roadmapContent = readFileSync(roadmapPath, "utf-8");
1418
+ // mergeMilestoneToMain handles: auto-commit, checkout integration branch,
1419
+ // squash merge, commit, optional push, branch deletion.
1420
+ const mergeResult = mergeMilestoneToMain(basePath, currentMilestoneId, roadmapContent);
1421
+ gitService = new GitServiceImpl(basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
1422
+ ctx.ui.notify(
1423
+ `Milestone ${currentMilestoneId} merged (branch mode).${mergeResult.pushed ? " Pushed to remote." : ""}`,
1424
+ "info",
1425
+ );
1426
+ }
1427
+ }
1428
+ } catch (err) {
1429
+ ctx.ui.notify(
1430
+ `Milestone merge failed (branch mode): ${err instanceof Error ? err.message : String(err)}`,
1431
+ "warning",
1432
+ );
1334
1433
  }
1335
1434
  }
1336
1435
  sendDesktopNotification("GSD", `Milestone ${mid} complete!`, "success", "milestone");
@@ -1757,6 +1856,10 @@ async function dispatchNextUnit(
1757
1856
  startedAt: currentUnit.startedAt,
1758
1857
  finishedAt: Date.now(),
1759
1858
  });
1859
+ // Cap to last 200 entries to prevent unbounded growth (#611)
1860
+ if (completedUnits.length > 200) {
1861
+ completedUnits = completedUnits.slice(-200);
1862
+ }
1760
1863
  clearUnitRuntimeRecord(basePath, currentUnit.type, currentUnit.id);
1761
1864
  unitDispatchCount.delete(`${currentUnit.type}/${currentUnit.id}`);
1762
1865
  unitRecoveryCount.delete(`${currentUnit.type}/${currentUnit.id}`);
@@ -1957,6 +2060,16 @@ async function dispatchNextUnit(
1957
2060
  if (!runtime) return;
1958
2061
  if (Date.now() - runtime.lastProgressAt < idleTimeoutMs) return;
1959
2062
 
2063
+ // Agent has tool calls currently executing (await_job, long bash, etc.) —
2064
+ // not idle, just waiting for tool completion.
2065
+ if (inFlightTools.size > 0) {
2066
+ writeUnitRuntimeRecord(basePath, unitType, unitId, currentUnit.startedAt, {
2067
+ lastProgressAt: Date.now(),
2068
+ lastProgressKind: "tool-in-flight",
2069
+ });
2070
+ return;
2071
+ }
2072
+
1960
2073
  // Before triggering recovery, check if the agent is actually producing
1961
2074
  // work on disk. `git status --porcelain` is cheap and catches any
1962
2075
  // staged/unstaged/untracked changes the agent made since lastProgressAt.