gsd-pi 2.37.1-dev.d3ace49 → 2.38.0-dev.361f5e3

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 (168) hide show
  1. package/dist/app-paths.js +1 -1
  2. package/dist/cli.js +9 -0
  3. package/dist/extension-discovery.d.ts +5 -3
  4. package/dist/extension-discovery.js +14 -9
  5. package/dist/extension-registry.js +2 -2
  6. package/dist/remote-questions-config.js +2 -2
  7. package/dist/resources/extensions/browser-tools/package.json +3 -1
  8. package/dist/resources/extensions/cmux/index.js +55 -1
  9. package/dist/resources/extensions/context7/package.json +1 -1
  10. package/dist/resources/extensions/env-utils.js +29 -0
  11. package/dist/resources/extensions/get-secrets-from-user.js +5 -24
  12. package/dist/resources/extensions/github-sync/cli.js +284 -0
  13. package/dist/resources/extensions/github-sync/index.js +73 -0
  14. package/dist/resources/extensions/github-sync/mapping.js +67 -0
  15. package/dist/resources/extensions/github-sync/sync.js +424 -0
  16. package/dist/resources/extensions/github-sync/templates.js +118 -0
  17. package/dist/resources/extensions/github-sync/types.js +7 -0
  18. package/dist/resources/extensions/google-search/package.json +3 -1
  19. package/dist/resources/extensions/gsd/auto/session.js +6 -23
  20. package/dist/resources/extensions/gsd/auto-dispatch.js +7 -8
  21. package/dist/resources/extensions/gsd/auto-loop.js +149 -170
  22. package/dist/resources/extensions/gsd/auto-post-unit.js +92 -70
  23. package/dist/resources/extensions/gsd/auto-prompts.js +7 -31
  24. package/dist/resources/extensions/gsd/auto-start.js +13 -2
  25. package/dist/resources/extensions/gsd/auto-worktree-sync.js +13 -5
  26. package/dist/resources/extensions/gsd/auto.js +143 -96
  27. package/dist/resources/extensions/gsd/captures.js +9 -1
  28. package/dist/resources/extensions/gsd/commands-extensions.js +3 -2
  29. package/dist/resources/extensions/gsd/commands-handlers.js +16 -3
  30. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +1 -1
  31. package/dist/resources/extensions/gsd/commands.js +22 -2
  32. package/dist/resources/extensions/gsd/context-budget.js +2 -10
  33. package/dist/resources/extensions/gsd/detection.js +1 -2
  34. package/dist/resources/extensions/gsd/docs/preferences-reference.md +0 -2
  35. package/dist/resources/extensions/gsd/doctor-checks.js +82 -0
  36. package/dist/resources/extensions/gsd/doctor-environment.js +78 -0
  37. package/dist/resources/extensions/gsd/doctor-format.js +15 -0
  38. package/dist/resources/extensions/gsd/doctor-providers.js +27 -11
  39. package/dist/resources/extensions/gsd/doctor.js +184 -11
  40. package/dist/resources/extensions/gsd/export.js +1 -1
  41. package/dist/resources/extensions/gsd/files.js +2 -2
  42. package/dist/resources/extensions/gsd/forensics.js +1 -1
  43. package/dist/resources/extensions/gsd/git-service.js +8 -1
  44. package/dist/resources/extensions/gsd/index.js +2 -1
  45. package/dist/resources/extensions/gsd/migrate/parsers.js +1 -1
  46. package/dist/resources/extensions/gsd/package.json +1 -1
  47. package/dist/resources/extensions/gsd/preferences-models.js +0 -12
  48. package/dist/resources/extensions/gsd/preferences-types.js +1 -1
  49. package/dist/resources/extensions/gsd/preferences-validation.js +59 -11
  50. package/dist/resources/extensions/gsd/preferences.js +8 -5
  51. package/dist/resources/extensions/gsd/prompts/discuss.md +11 -14
  52. package/dist/resources/extensions/gsd/prompts/execute-task.md +2 -2
  53. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
  54. package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
  55. package/dist/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
  56. package/dist/resources/extensions/gsd/prompts/queue.md +4 -8
  57. package/dist/resources/extensions/gsd/prompts/reactive-execute.md +11 -8
  58. package/dist/resources/extensions/gsd/prompts/run-uat.md +25 -10
  59. package/dist/resources/extensions/gsd/prompts/workflow-start.md +2 -2
  60. package/dist/resources/extensions/gsd/repo-identity.js +21 -4
  61. package/dist/resources/extensions/gsd/resource-version.js +2 -1
  62. package/dist/resources/extensions/gsd/state.js +1 -1
  63. package/dist/resources/extensions/gsd/visualizer-data.js +1 -1
  64. package/dist/resources/extensions/gsd/worktree.js +35 -16
  65. package/dist/resources/extensions/remote-questions/status.js +2 -1
  66. package/dist/resources/extensions/remote-questions/store.js +2 -1
  67. package/dist/resources/extensions/search-the-web/provider.js +2 -1
  68. package/dist/resources/extensions/subagent/index.js +12 -3
  69. package/dist/resources/extensions/subagent/isolation.js +2 -1
  70. package/dist/resources/extensions/ttsr/rule-loader.js +2 -1
  71. package/dist/resources/extensions/universal-config/package.json +1 -1
  72. package/dist/welcome-screen.d.ts +12 -0
  73. package/dist/welcome-screen.js +53 -0
  74. package/package.json +1 -1
  75. package/packages/pi-coding-agent/dist/core/package-manager.d.ts.map +1 -1
  76. package/packages/pi-coding-agent/dist/core/package-manager.js +8 -4
  77. package/packages/pi-coding-agent/dist/core/package-manager.js.map +1 -1
  78. package/packages/pi-coding-agent/package.json +1 -1
  79. package/packages/pi-coding-agent/src/core/package-manager.ts +8 -4
  80. package/pkg/package.json +1 -1
  81. package/src/resources/extensions/cmux/index.ts +57 -1
  82. package/src/resources/extensions/env-utils.ts +31 -0
  83. package/src/resources/extensions/get-secrets-from-user.ts +5 -24
  84. package/src/resources/extensions/github-sync/cli.ts +364 -0
  85. package/src/resources/extensions/github-sync/index.ts +93 -0
  86. package/src/resources/extensions/github-sync/mapping.ts +81 -0
  87. package/src/resources/extensions/github-sync/sync.ts +556 -0
  88. package/src/resources/extensions/github-sync/templates.ts +183 -0
  89. package/src/resources/extensions/github-sync/tests/cli.test.ts +20 -0
  90. package/src/resources/extensions/github-sync/tests/commit-linking.test.ts +39 -0
  91. package/src/resources/extensions/github-sync/tests/mapping.test.ts +104 -0
  92. package/src/resources/extensions/github-sync/tests/templates.test.ts +110 -0
  93. package/src/resources/extensions/github-sync/types.ts +47 -0
  94. package/src/resources/extensions/gsd/auto/session.ts +7 -25
  95. package/src/resources/extensions/gsd/auto-dispatch.ts +6 -8
  96. package/src/resources/extensions/gsd/auto-loop.ts +207 -252
  97. package/src/resources/extensions/gsd/auto-post-unit.ts +69 -41
  98. package/src/resources/extensions/gsd/auto-prompts.ts +7 -33
  99. package/src/resources/extensions/gsd/auto-start.ts +18 -2
  100. package/src/resources/extensions/gsd/auto-worktree-sync.ts +15 -4
  101. package/src/resources/extensions/gsd/auto.ts +139 -101
  102. package/src/resources/extensions/gsd/captures.ts +10 -1
  103. package/src/resources/extensions/gsd/commands-extensions.ts +4 -2
  104. package/src/resources/extensions/gsd/commands-handlers.ts +17 -2
  105. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
  106. package/src/resources/extensions/gsd/commands.ts +24 -2
  107. package/src/resources/extensions/gsd/context-budget.ts +2 -12
  108. package/src/resources/extensions/gsd/detection.ts +2 -2
  109. package/src/resources/extensions/gsd/docs/preferences-reference.md +0 -2
  110. package/src/resources/extensions/gsd/doctor-checks.ts +75 -0
  111. package/src/resources/extensions/gsd/doctor-environment.ts +82 -1
  112. package/src/resources/extensions/gsd/doctor-format.ts +20 -0
  113. package/src/resources/extensions/gsd/doctor-providers.ts +26 -9
  114. package/src/resources/extensions/gsd/doctor-types.ts +16 -1
  115. package/src/resources/extensions/gsd/doctor.ts +177 -13
  116. package/src/resources/extensions/gsd/export.ts +1 -1
  117. package/src/resources/extensions/gsd/files.ts +2 -2
  118. package/src/resources/extensions/gsd/forensics.ts +1 -1
  119. package/src/resources/extensions/gsd/git-service.ts +13 -1
  120. package/src/resources/extensions/gsd/index.ts +3 -1
  121. package/src/resources/extensions/gsd/migrate/parsers.ts +1 -1
  122. package/src/resources/extensions/gsd/preferences-models.ts +0 -12
  123. package/src/resources/extensions/gsd/preferences-types.ts +4 -4
  124. package/src/resources/extensions/gsd/preferences-validation.ts +51 -11
  125. package/src/resources/extensions/gsd/preferences.ts +8 -5
  126. package/src/resources/extensions/gsd/prompts/discuss.md +11 -14
  127. package/src/resources/extensions/gsd/prompts/execute-task.md +2 -2
  128. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
  129. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
  130. package/src/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
  131. package/src/resources/extensions/gsd/prompts/queue.md +4 -8
  132. package/src/resources/extensions/gsd/prompts/reactive-execute.md +11 -8
  133. package/src/resources/extensions/gsd/prompts/run-uat.md +25 -10
  134. package/src/resources/extensions/gsd/prompts/workflow-start.md +2 -2
  135. package/src/resources/extensions/gsd/repo-identity.ts +23 -4
  136. package/src/resources/extensions/gsd/resource-version.ts +3 -1
  137. package/src/resources/extensions/gsd/state.ts +1 -1
  138. package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +21 -18
  139. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +16 -37
  140. package/src/resources/extensions/gsd/tests/cmux.test.ts +93 -0
  141. package/src/resources/extensions/gsd/tests/doctor-enhancements.test.ts +266 -0
  142. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +86 -3
  143. package/src/resources/extensions/gsd/tests/preferences.test.ts +2 -7
  144. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +59 -0
  145. package/src/resources/extensions/gsd/tests/repo-identity-worktree.test.ts +21 -1
  146. package/src/resources/extensions/gsd/tests/run-uat.test.ts +11 -3
  147. package/src/resources/extensions/gsd/tests/worktree.test.ts +47 -0
  148. package/src/resources/extensions/gsd/types.ts +0 -1
  149. package/src/resources/extensions/gsd/visualizer-data.ts +1 -1
  150. package/src/resources/extensions/gsd/worktree.ts +35 -15
  151. package/src/resources/extensions/remote-questions/status.ts +3 -1
  152. package/src/resources/extensions/remote-questions/store.ts +3 -1
  153. package/src/resources/extensions/search-the-web/provider.ts +2 -1
  154. package/src/resources/extensions/subagent/index.ts +12 -3
  155. package/src/resources/extensions/subagent/isolation.ts +3 -1
  156. package/src/resources/extensions/ttsr/rule-loader.ts +3 -1
  157. package/dist/resources/extensions/gsd/prompt-compressor.js +0 -393
  158. package/dist/resources/extensions/gsd/semantic-chunker.js +0 -254
  159. package/dist/resources/extensions/gsd/summary-distiller.js +0 -212
  160. package/src/resources/extensions/gsd/prompt-compressor.ts +0 -508
  161. package/src/resources/extensions/gsd/semantic-chunker.ts +0 -336
  162. package/src/resources/extensions/gsd/summary-distiller.ts +0 -258
  163. package/src/resources/extensions/gsd/tests/context-compression.test.ts +0 -193
  164. package/src/resources/extensions/gsd/tests/prompt-compressor.test.ts +0 -529
  165. package/src/resources/extensions/gsd/tests/semantic-chunker.test.ts +0 -426
  166. package/src/resources/extensions/gsd/tests/summary-distiller.test.ts +0 -323
  167. package/src/resources/extensions/gsd/tests/token-optimization-benchmark.test.ts +0 -1272
  168. package/src/resources/extensions/gsd/tests/token-optimization-prefs.test.ts +0 -164
@@ -208,30 +208,25 @@ test("git fields comprehensive validation", () => {
208
208
  assert.equal(preferences.git?.isolation, "branch");
209
209
  });
210
210
 
211
- test("auto_visualize, auto_report, compression_strategy, context_selection validate correctly", () => {
211
+ test("auto_visualize, auto_report, context_selection validate correctly", () => {
212
212
  const { preferences, errors } = validatePreferences({
213
213
  auto_visualize: true,
214
214
  auto_report: false,
215
- compression_strategy: "compress",
216
215
  context_selection: "smart",
217
216
  });
218
217
  assert.equal(errors.length, 0);
219
218
  assert.equal(preferences.auto_visualize, true);
220
219
  assert.equal(preferences.auto_report, false);
221
- assert.equal(preferences.compression_strategy, "compress");
222
220
  assert.equal(preferences.context_selection, "smart");
223
221
  });
224
222
 
225
- test("auto_visualize, auto_report, compression_strategy, context_selection reject invalid values", () => {
223
+ test("auto_visualize, auto_report, context_selection reject invalid values", () => {
226
224
  const { errors: e1 } = validatePreferences({ auto_visualize: "yes" as never });
227
225
  assert.ok(e1.some(e => e.includes("auto_visualize")));
228
226
 
229
227
  const { errors: e2 } = validatePreferences({ auto_report: 1 as never });
230
228
  assert.ok(e2.some(e => e.includes("auto_report")));
231
229
 
232
- const { errors: e3 } = validatePreferences({ compression_strategy: "shrink" as never });
233
- assert.ok(e3.some(e => e.includes("compression_strategy")));
234
-
235
230
  const { errors: e4 } = validatePreferences({ context_selection: "partial" as never });
236
231
  assert.ok(e4.some(e => e.includes("context_selection")));
237
232
  });
@@ -0,0 +1,59 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { readFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+
6
+ const promptsDir = join(process.cwd(), "src/resources/extensions/gsd/prompts");
7
+
8
+ function readPrompt(name: string): string {
9
+ return readFileSync(join(promptsDir, `${name}.md`), "utf-8");
10
+ }
11
+
12
+ test("reactive-execute prompt keeps task summaries with subagents and avoids batch commits", () => {
13
+ const prompt = readPrompt("reactive-execute");
14
+ assert.match(prompt, /subagent-written summary as authoritative/i);
15
+ assert.match(prompt, /Do NOT create a batch commit/i);
16
+ assert.doesNotMatch(prompt, /\*\*Write task summaries\*\*/i);
17
+ assert.doesNotMatch(prompt, /\*\*Commit\*\* all changes/i);
18
+ });
19
+
20
+ test("run-uat prompt branches on dynamic UAT mode and supports runtime evidence", () => {
21
+ const prompt = readPrompt("run-uat");
22
+ assert.match(prompt, /\*\*Detected UAT mode:\*\*\s*`\{\{uatType\}\}`/);
23
+ assert.match(prompt, /uatType:\s*\{\{uatType\}\}/);
24
+ assert.match(prompt, /live-runtime/);
25
+ assert.match(prompt, /browser\/runtime\/network/i);
26
+ assert.match(prompt, /NEEDS-HUMAN/);
27
+ assert.doesNotMatch(prompt, /uatType:\s*artifact-driven/);
28
+ });
29
+
30
+ test("workflow-start prompt defaults to autonomy instead of per-phase confirmation", () => {
31
+ const prompt = readPrompt("workflow-start");
32
+ assert.match(prompt, /Keep moving by default/i);
33
+ assert.match(prompt, /Decision gates, not ceremony/i);
34
+ assert.doesNotMatch(prompt, /confirm with the user before proceeding/i);
35
+ assert.doesNotMatch(prompt, /Gate between phases/i);
36
+ });
37
+
38
+ test("discuss prompt allows implementation questions when they materially matter", () => {
39
+ const prompt = readPrompt("discuss");
40
+ assert.match(prompt, /Lead with experience, but ask implementation when it materially matters/i);
41
+ assert.match(prompt, /one gate, not two/i);
42
+ assert.doesNotMatch(prompt, /Questions must be about the experience, not the implementation/i);
43
+ });
44
+
45
+ test("guided discussion prompts avoid wrap-up prompts after every round", () => {
46
+ const milestonePrompt = readPrompt("guided-discuss-milestone");
47
+ const slicePrompt = readPrompt("guided-discuss-slice");
48
+ assert.match(milestonePrompt, /Do \*\*not\*\* ask a meta "ready to wrap up\?" question after every round/i);
49
+ assert.match(slicePrompt, /Do \*\*not\*\* ask a meta "ready to wrap up\?" question after every round/i);
50
+ assert.doesNotMatch(milestonePrompt, /I think I have a solid picture of this milestone\. Ready to wrap up/i);
51
+ assert.doesNotMatch(slicePrompt, /I think I have a solid picture of this slice\. Ready to wrap up/i);
52
+ });
53
+
54
+ test("guided-resume-task prompt preserves recovery state until work is superseded", () => {
55
+ const prompt = readPrompt("guided-resume-task");
56
+ assert.match(prompt, /Do \*\*not\*\* delete the continue file immediately/i);
57
+ assert.match(prompt, /successfully completed or you have written a newer summary\/continue artifact/i);
58
+ assert.doesNotMatch(prompt, /Delete the continue file after reading it/i);
59
+ });
@@ -3,7 +3,7 @@ import { join } from "node:path";
3
3
  import { tmpdir } from "node:os";
4
4
  import { execSync } from "node:child_process";
5
5
 
6
- import { externalGsdRoot, ensureGsdSymlink } from "../repo-identity.ts";
6
+ import { repoIdentity, externalGsdRoot, ensureGsdSymlink, validateProjectId } from "../repo-identity.ts";
7
7
  import { createTestContext } from "./test-helpers.ts";
8
8
 
9
9
  const { assertEq, assertTrue, report } = createTestContext();
@@ -57,6 +57,26 @@ async function main(): Promise<void> {
57
57
  assertEq(preservedDirState, join(worktreePath, ".gsd"), "worktree .gsd directory is left in place for sync-based refresh");
58
58
  assertTrue(lstatSync(join(worktreePath, ".gsd")).isDirectory(), "worktree .gsd directory remains a directory");
59
59
  assertTrue(existsSync(join(worktreePath, ".gsd", "milestones", "stale.txt")), "existing worktree .gsd directory contents remain available for sync logic");
60
+
61
+ console.log("\n=== GSD_PROJECT_ID overrides computed repo hash ===");
62
+ process.env.GSD_PROJECT_ID = "my-project";
63
+ assertEq(repoIdentity(base), "my-project", "repoIdentity returns GSD_PROJECT_ID when set");
64
+ assertEq(externalGsdRoot(base), join(stateDir, "projects", "my-project"), "externalGsdRoot uses GSD_PROJECT_ID");
65
+ delete process.env.GSD_PROJECT_ID;
66
+
67
+ console.log("\n=== GSD_PROJECT_ID falls back to hash when unset ===");
68
+ const hashIdentity = repoIdentity(base);
69
+ assertTrue(/^[0-9a-f]{12}$/.test(hashIdentity), "repoIdentity returns 12-char hex hash when GSD_PROJECT_ID is unset");
70
+
71
+ console.log("\n=== validateProjectId rejects invalid values ===");
72
+ for (const invalid of ["has spaces", "path/traversal", "dot..dot", "back\\slash"]) {
73
+ assertTrue(!validateProjectId(invalid), `validateProjectId rejects invalid value: "${invalid}"`);
74
+ }
75
+
76
+ console.log("\n=== validateProjectId accepts valid values ===");
77
+ for (const valid of ["my-project", "foo_bar", "abc123", "A-Z_0-9"]) {
78
+ assertTrue(validateProjectId(valid), `validateProjectId accepts valid value: "${valid}"`);
79
+ }
60
80
  } finally {
61
81
  delete process.env.GSD_STATE_DIR;
62
82
  rmSync(base, { recursive: true, force: true });
@@ -210,7 +210,7 @@ async function main(): Promise<void> {
210
210
  const sliceId = 'S01';
211
211
  const uatPath = '.gsd/milestones/M001/slices/S01/S01-UAT.md';
212
212
  const uatResultPath = '.gsd/milestones/M001/slices/S01/S01-UAT-RESULT.md';
213
- const uatType = 'artifact-driven';
213
+ const uatType = 'live-runtime';
214
214
  const inlinedContext = '<!-- no context -->';
215
215
 
216
216
  let promptResult: string | undefined;
@@ -246,13 +246,21 @@ async function main(): Promise<void> {
246
246
  promptResult?.includes(uatResultPath) ?? false,
247
247
  `prompt contains uatResultPath value after substitution`,
248
248
  );
249
+ assertTrue(
250
+ promptResult?.includes(`Detected UAT mode:** \`${uatType}\``) ?? false,
251
+ `prompt contains detected dynamic uatType value "${uatType}" after substitution`,
252
+ );
253
+ assertTrue(
254
+ promptResult?.includes(`uatType: ${uatType}`) ?? false,
255
+ `prompt contains dynamic uatType frontmatter value "${uatType}" after substitution`,
256
+ );
249
257
  assertTrue(
250
258
  !/\{\{[^}]+\}\}/.test(promptResult ?? ''),
251
259
  'no unreplaced {{...}} tokens remain after variable substitution',
252
260
  );
253
261
  assertTrue(
254
- /artifact|execute|run/i.test(promptResult ?? ''),
255
- 'prompt contains artifact-driven execution language (artifact/execute/run)',
262
+ /browser|runtime|execute|run/i.test(promptResult ?? ''),
263
+ 'prompt contains runtime execution language (browser/runtime/execute/run)',
256
264
  );
257
265
  assertTrue(
258
266
  !/surfaced for human review/i.test(promptResult ?? ''),
@@ -11,6 +11,7 @@ import {
11
11
  getMainBranch,
12
12
  getSliceBranchName,
13
13
  parseSliceBranch,
14
+ resolveProjectRoot,
14
15
  setActiveMilestoneId,
15
16
  SLICE_BRANCH_RE,
16
17
  } from "../worktree.ts";
@@ -165,6 +166,52 @@ async function main(): Promise<void> {
165
166
  rmSync(repo, { recursive: true, force: true });
166
167
  }
167
168
 
169
+ // ── detectWorktreeName: symlink-resolved paths ───────────────────────────
170
+ console.log("\n=== detectWorktreeName (symlink-resolved paths) ===");
171
+ assertEq(
172
+ detectWorktreeName("/Users/fran/.gsd/projects/89e1c9ad49bf/worktrees/M001"),
173
+ "M001",
174
+ "detects milestone in symlink-resolved path",
175
+ );
176
+ assertEq(
177
+ detectWorktreeName("/Users/fran/.gsd/projects/abc123/worktrees/M002/subdir"),
178
+ "M002",
179
+ "detects milestone with trailing subdir in symlink-resolved path",
180
+ );
181
+ assertEq(
182
+ detectWorktreeName("/Users/fran/.gsd/projects/abc123"),
183
+ null,
184
+ "returns null for project root without worktrees segment",
185
+ );
186
+ assertEq(
187
+ detectWorktreeName("/foo/.gsd/worktrees/M001"),
188
+ "M001",
189
+ "still detects direct layout path",
190
+ );
191
+
192
+ // ── resolveProjectRoot: symlink-resolved paths ──────────────────────────
193
+ console.log("\n=== resolveProjectRoot (symlink-resolved paths) ===");
194
+ assertEq(
195
+ resolveProjectRoot("/Users/fran/.gsd/projects/89e1c9ad49bf/worktrees/M001"),
196
+ "/Users/fran",
197
+ "resolves to user home for symlink-resolved path",
198
+ );
199
+ assertEq(
200
+ resolveProjectRoot("/foo/.gsd/worktrees/M001"),
201
+ "/foo",
202
+ "still resolves direct layout path",
203
+ );
204
+ assertEq(
205
+ resolveProjectRoot("/some/repo"),
206
+ "/some/repo",
207
+ "returns unchanged for non-worktree path",
208
+ );
209
+ assertEq(
210
+ resolveProjectRoot("/data/.gsd/projects/deadbeef/worktrees/M003/nested"),
211
+ "/data",
212
+ "resolves correctly with nested subdirs after worktree name",
213
+ );
214
+
168
215
  rmSync(base, { recursive: true, force: true });
169
216
  report();
170
217
  }
@@ -423,7 +423,6 @@ export interface Requirement {
423
423
 
424
424
  // ─── Parallel Orchestration Types ────────────────────────────────────────
425
425
 
426
- export type CompressionStrategy = "truncate" | "compress";
427
426
  export type ContextSelectionMode = "full" | "smart";
428
427
 
429
428
  export type MergeStrategy = "per-slice" | "per-milestone";
@@ -3,7 +3,7 @@
3
3
  import { existsSync, readFileSync, statSync } from 'node:fs';
4
4
  import { deriveState } from './state.js';
5
5
  import { parseRoadmap, parsePlan, parseSummary, loadFile } from './files.js';
6
- import { findMilestoneIds } from './guided-flow.js';
6
+ import { findMilestoneIds } from './milestone-ids.js';
7
7
  import { resolveMilestoneFile, resolveSliceFile, resolveGsdRootFile } from './paths.js';
8
8
  import {
9
9
  getLedger,
@@ -67,40 +67,60 @@ export function captureIntegrationBranch(basePath: string, milestoneId: string,
67
67
 
68
68
  // ─── Pure Utility Functions (unchanged) ────────────────────────────────────
69
69
 
70
+ /**
71
+ * Find the worktrees segment in a path, supporting both direct
72
+ * (`/.gsd/worktrees/`) and symlink-resolved (`/.gsd/projects/<hash>/worktrees/`)
73
+ * layouts. When `.gsd` is a symlink to `~/.gsd/projects/<hash>`, resolved
74
+ * paths contain the intermediate `projects/<hash>/` segment that the old
75
+ * single-marker check missed.
76
+ */
77
+ function findWorktreeSegment(normalizedPath: string): { gsdIdx: number; afterWorktrees: number } | null {
78
+ // Direct layout: /.gsd/worktrees/<name>
79
+ const directMarker = "/.gsd/worktrees/";
80
+ const idx = normalizedPath.indexOf(directMarker);
81
+ if (idx !== -1) {
82
+ return { gsdIdx: idx, afterWorktrees: idx + directMarker.length };
83
+ }
84
+ // Symlink-resolved layout: /.gsd/projects/<hash>/worktrees/<name>
85
+ const symlinkRe = /\/\.gsd\/projects\/[a-f0-9]+\/worktrees\//;
86
+ const match = normalizedPath.match(symlinkRe);
87
+ if (match && match.index !== undefined) {
88
+ return { gsdIdx: match.index, afterWorktrees: match.index + match[0].length };
89
+ }
90
+ return null;
91
+ }
92
+
70
93
  /**
71
94
  * Detect the active worktree name from the current working directory.
72
95
  * Returns null if not inside a GSD worktree (.gsd/worktrees/<name>/).
73
96
  */
74
97
  export function detectWorktreeName(basePath: string): string | null {
75
98
  const normalizedPath = basePath.replaceAll("\\", "/");
76
- const marker = "/.gsd/worktrees/";
77
- const idx = normalizedPath.indexOf(marker);
78
- if (idx === -1) return null;
79
- const afterMarker = normalizedPath.slice(idx + marker.length);
99
+ const seg = findWorktreeSegment(normalizedPath);
100
+ if (!seg) return null;
101
+ const afterMarker = normalizedPath.slice(seg.afterWorktrees);
80
102
  const name = afterMarker.split("/")[0];
81
103
  return name || null;
82
104
  }
83
105
 
84
106
  /**
85
107
  * Resolve the project root from a path that may be inside a worktree.
86
- * If the path contains `/.gsd/worktrees/<name>/`, returns the portion
87
- * before `/.gsd/`. Otherwise returns the input unchanged.
108
+ * If the path contains a worktrees segment, returns the portion before
109
+ * `/.gsd/`. Otherwise returns the input unchanged.
88
110
  *
89
111
  * Use this in commands that call `process.cwd()` to ensure they always
90
112
  * operate against the real project root, not a worktree subdirectory.
91
113
  */
92
114
  export function resolveProjectRoot(basePath: string): string {
93
115
  const normalizedPath = basePath.replaceAll("\\", "/");
94
- const marker = "/.gsd/worktrees/";
95
- const idx = normalizedPath.indexOf(marker);
96
- if (idx === -1) return basePath;
97
- // Return the original path up to the .gsd/ marker (un-normalized)
98
- // Account for potential OS-specific separators
116
+ const seg = findWorktreeSegment(normalizedPath);
117
+ if (!seg) return basePath;
118
+ // Return the original path up to the /.gsd/ boundary
99
119
  const sep = basePath.includes("\\") ? "\\" : "/";
100
- const markerOs = `${sep}.gsd${sep}worktrees${sep}`;
101
- const idxOs = basePath.indexOf(markerOs);
102
- if (idxOs !== -1) return basePath.slice(0, idxOs);
103
- return basePath.slice(0, idx);
120
+ const gsdMarker = `${sep}.gsd${sep}`;
121
+ const gsdIdx = basePath.indexOf(gsdMarker);
122
+ if (gsdIdx !== -1) return basePath.slice(0, gsdIdx);
123
+ return basePath.slice(0, seg.gsdIdx);
104
124
  }
105
125
 
106
126
  /**
@@ -7,6 +7,8 @@ import { join } from "node:path";
7
7
  import { homedir } from "node:os";
8
8
  import { readPromptRecord } from "./store.js";
9
9
 
10
+ const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
11
+
10
12
  export interface LatestPromptSummary {
11
13
  id: string;
12
14
  status: string;
@@ -14,7 +16,7 @@ export interface LatestPromptSummary {
14
16
  }
15
17
 
16
18
  export function getLatestPromptSummary(): LatestPromptSummary | null {
17
- const runtimeDir = join(homedir(), ".gsd", "runtime", "remote-questions");
19
+ const runtimeDir = join(gsdHome, "runtime", "remote-questions");
18
20
  if (!existsSync(runtimeDir)) return null;
19
21
  const files = readdirSync(runtimeDir).filter((f) => f.endsWith(".json"));
20
22
  if (files.length === 0) return null;
@@ -7,8 +7,10 @@ import { join } from "node:path";
7
7
  import { homedir } from "node:os";
8
8
  import type { RemotePrompt, RemotePromptRecord, RemotePromptRef, RemoteAnswer, RemotePromptStatus } from "./types.js";
9
9
 
10
+ const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
11
+
10
12
  function runtimeDir(): string {
11
- return join(homedir(), ".gsd", "runtime", "remote-questions");
13
+ return join(gsdHome, "runtime", "remote-questions");
12
14
  }
13
15
 
14
16
  function recordPath(id: string): string {
@@ -17,7 +17,8 @@ import { resolveSearchProviderFromPreferences } from '../gsd/preferences.js'
17
17
  // Compute authFilePath locally instead of importing from app-paths.ts,
18
18
  // because extensions are copied to ~/.gsd/agent/extensions/ at runtime
19
19
  // where the relative import '../../../app-paths.ts' doesn't resolve.
20
- const authFilePath = join(homedir(), '.gsd', 'agent', 'auth.json')
20
+ const gsdHome = process.env.GSD_HOME || join(homedir(), '.gsd')
21
+ const authFilePath = join(gsdHome, 'agent', 'auth.json')
21
22
 
22
23
  export type SearchProvider = 'tavily' | 'brave' | 'ollama'
23
24
  export type SearchProviderPreference = SearchProvider | 'auto'
@@ -452,7 +452,7 @@ async function runSingleAgent(
452
452
 
453
453
  async function runSingleAgentInCmuxSplit(
454
454
  cmuxClient: CmuxClient,
455
- direction: "right" | "down",
455
+ directionOrSurfaceId: "right" | "down" | string,
456
456
  defaultCwd: string,
457
457
  agents: AgentConfig[],
458
458
  agentName: string,
@@ -503,7 +503,12 @@ async function runSingleAgentInCmuxSplit(
503
503
  const stdoutPath = path.join(tmpOutputDir, "stdout.jsonl");
504
504
  const stderrPath = path.join(tmpOutputDir, "stderr.log");
505
505
  const exitPath = path.join(tmpOutputDir, "exit.code");
506
- const cmuxSurfaceId = await cmuxClient.createSplit(direction);
506
+ // Accept either a pre-created surface ID or a direction to create a new split
507
+ const isDirection = directionOrSurfaceId === "right" || directionOrSurfaceId === "down"
508
+ || directionOrSurfaceId === "left" || directionOrSurfaceId === "up";
509
+ const cmuxSurfaceId = isDirection
510
+ ? await cmuxClient.createSplit(directionOrSurfaceId as "right" | "down" | "left" | "up")
511
+ : directionOrSurfaceId;
507
512
  if (!cmuxSurfaceId) {
508
513
  return runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, signal, onUpdate, makeDetails);
509
514
  }
@@ -806,12 +811,16 @@ export default function (pi: ExtensionAPI) {
806
811
  const MAX_RETRIES = 1; // Retry failed tasks once
807
812
  const batchId = crypto.randomUUID();
808
813
  const batchSize = params.tasks.length;
814
+ // Pre-create a grid layout for cmux splits so agents get a clean tiled arrangement
815
+ const gridSurfaces = cmuxSplitsEnabled
816
+ ? await cmuxClient.createGridLayout(Math.min(batchSize, MAX_CONCURRENCY))
817
+ : [];
809
818
  const results = await mapWithConcurrencyLimit(params.tasks, MAX_CONCURRENCY, async (t, index) => {
810
819
  const workerId = registerWorker(t.agent, t.task, index, batchSize, batchId);
811
820
  const runTask = () => cmuxSplitsEnabled
812
821
  ? runSingleAgentInCmuxSplit(
813
822
  cmuxClient,
814
- index % 2 === 0 ? "right" : "down",
823
+ gridSurfaces[index] ?? (index % 2 === 0 ? "right" : "down"),
815
824
  ctx.cwd,
816
825
  agents,
817
826
  t.agent,
@@ -57,8 +57,10 @@ function encodeCwd(cwd: string): string {
57
57
  return cwd.replace(/\//g, "--");
58
58
  }
59
59
 
60
+ const gsdHome = process.env.GSD_HOME || path.join(os.homedir(), ".gsd");
61
+
60
62
  function getIsolationBaseDir(cwd: string, taskId: string): string {
61
- return path.join(os.homedir(), ".gsd", "wt", encodeCwd(cwd), taskId);
63
+ return path.join(gsdHome, "wt", encodeCwd(cwd), taskId);
62
64
  }
63
65
 
64
66
  // Track active isolation dirs for cleanup on exit
@@ -8,6 +8,8 @@
8
8
  import { readdirSync, readFileSync, existsSync } from "node:fs";
9
9
  import { join, basename } from "node:path";
10
10
  import { homedir } from "node:os";
11
+
12
+ const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
11
13
  import type { Rule } from "./ttsr-manager.js";
12
14
  import { splitFrontmatter, parseFrontmatterMap } from "../shared/frontmatter.js";
13
15
 
@@ -59,7 +61,7 @@ function scanDir(dir: string): Rule[] {
59
61
  * Project rules override global rules with the same name.
60
62
  */
61
63
  export function loadRules(cwd: string): Rule[] {
62
- const globalDir = join(homedir(), ".gsd", "agent", "rules");
64
+ const globalDir = join(gsdHome, "agent", "rules");
63
65
  const projectDir = join(cwd, ".gsd", "rules");
64
66
 
65
67
  const globalRules = scanDir(globalDir);