gsd-pi 2.37.1 → 2.38.0-dev.96dc7fb

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 (116) hide show
  1. package/README.md +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/onboarding.js +1 -0
  6. package/dist/resources/extensions/browser-tools/package.json +3 -1
  7. package/dist/resources/extensions/cmux/index.js +55 -1
  8. package/dist/resources/extensions/context7/package.json +1 -1
  9. package/dist/resources/extensions/google-search/package.json +3 -1
  10. package/dist/resources/extensions/gsd/auto-dispatch.js +67 -1
  11. package/dist/resources/extensions/gsd/auto-loop.js +7 -1
  12. package/dist/resources/extensions/gsd/auto-post-unit.js +14 -0
  13. package/dist/resources/extensions/gsd/auto-prompts.js +91 -2
  14. package/dist/resources/extensions/gsd/auto-recovery.js +37 -1
  15. package/dist/resources/extensions/gsd/auto-start.js +6 -1
  16. package/dist/resources/extensions/gsd/auto-worktree-sync.js +11 -4
  17. package/dist/resources/extensions/gsd/captures.js +9 -1
  18. package/dist/resources/extensions/gsd/commands-handlers.js +16 -3
  19. package/dist/resources/extensions/gsd/commands.js +20 -1
  20. package/dist/resources/extensions/gsd/doctor-checks.js +82 -0
  21. package/dist/resources/extensions/gsd/doctor-environment.js +78 -0
  22. package/dist/resources/extensions/gsd/doctor-format.js +15 -0
  23. package/dist/resources/extensions/gsd/doctor-providers.js +35 -1
  24. package/dist/resources/extensions/gsd/doctor.js +184 -11
  25. package/dist/resources/extensions/gsd/files.js +41 -0
  26. package/dist/resources/extensions/gsd/observability-validator.js +24 -0
  27. package/dist/resources/extensions/gsd/package.json +1 -1
  28. package/dist/resources/extensions/gsd/preferences-types.js +2 -1
  29. package/dist/resources/extensions/gsd/preferences-validation.js +42 -0
  30. package/dist/resources/extensions/gsd/prompts/plan-slice.md +2 -1
  31. package/dist/resources/extensions/gsd/prompts/reactive-execute.md +41 -0
  32. package/dist/resources/extensions/gsd/reactive-graph.js +227 -0
  33. package/dist/resources/extensions/gsd/templates/task-plan.md +11 -3
  34. package/dist/resources/extensions/gsd/worktree.js +35 -16
  35. package/dist/resources/extensions/subagent/index.js +12 -3
  36. package/dist/resources/extensions/universal-config/package.json +1 -1
  37. package/dist/welcome-screen.d.ts +12 -0
  38. package/dist/welcome-screen.js +53 -0
  39. package/package.json +2 -1
  40. package/packages/pi-ai/dist/env-api-keys.js +13 -0
  41. package/packages/pi-ai/dist/env-api-keys.js.map +1 -1
  42. package/packages/pi-ai/dist/models.generated.d.ts +172 -0
  43. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  44. package/packages/pi-ai/dist/models.generated.js +172 -0
  45. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  46. package/packages/pi-ai/dist/providers/anthropic-shared.d.ts +64 -0
  47. package/packages/pi-ai/dist/providers/anthropic-shared.d.ts.map +1 -0
  48. package/packages/pi-ai/dist/providers/anthropic-shared.js +668 -0
  49. package/packages/pi-ai/dist/providers/anthropic-shared.js.map +1 -0
  50. package/packages/pi-ai/dist/providers/anthropic-vertex.d.ts +5 -0
  51. package/packages/pi-ai/dist/providers/anthropic-vertex.d.ts.map +1 -0
  52. package/packages/pi-ai/dist/providers/anthropic-vertex.js +85 -0
  53. package/packages/pi-ai/dist/providers/anthropic-vertex.js.map +1 -0
  54. package/packages/pi-ai/dist/providers/anthropic.d.ts +4 -30
  55. package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
  56. package/packages/pi-ai/dist/providers/anthropic.js +47 -764
  57. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  58. package/packages/pi-ai/dist/providers/register-builtins.d.ts.map +1 -1
  59. package/packages/pi-ai/dist/providers/register-builtins.js +6 -0
  60. package/packages/pi-ai/dist/providers/register-builtins.js.map +1 -1
  61. package/packages/pi-ai/dist/types.d.ts +2 -2
  62. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  63. package/packages/pi-ai/dist/types.js.map +1 -1
  64. package/packages/pi-ai/package.json +1 -0
  65. package/packages/pi-ai/src/env-api-keys.ts +14 -0
  66. package/packages/pi-ai/src/models.generated.ts +172 -0
  67. package/packages/pi-ai/src/providers/anthropic-shared.ts +761 -0
  68. package/packages/pi-ai/src/providers/anthropic-vertex.ts +130 -0
  69. package/packages/pi-ai/src/providers/anthropic.ts +76 -868
  70. package/packages/pi-ai/src/providers/register-builtins.ts +7 -0
  71. package/packages/pi-ai/src/types.ts +2 -0
  72. package/packages/pi-coding-agent/dist/core/model-resolver.d.ts.map +1 -1
  73. package/packages/pi-coding-agent/dist/core/model-resolver.js +1 -0
  74. package/packages/pi-coding-agent/dist/core/model-resolver.js.map +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/model-resolver.ts +1 -0
  80. package/packages/pi-coding-agent/src/core/package-manager.ts +8 -4
  81. package/pkg/package.json +1 -1
  82. package/src/resources/extensions/cmux/index.ts +57 -1
  83. package/src/resources/extensions/gsd/auto-dispatch.ts +93 -0
  84. package/src/resources/extensions/gsd/auto-loop.ts +13 -1
  85. package/src/resources/extensions/gsd/auto-post-unit.ts +14 -0
  86. package/src/resources/extensions/gsd/auto-prompts.ts +125 -3
  87. package/src/resources/extensions/gsd/auto-recovery.ts +42 -0
  88. package/src/resources/extensions/gsd/auto-start.ts +7 -1
  89. package/src/resources/extensions/gsd/auto-worktree-sync.ts +12 -3
  90. package/src/resources/extensions/gsd/captures.ts +10 -1
  91. package/src/resources/extensions/gsd/commands-handlers.ts +17 -2
  92. package/src/resources/extensions/gsd/commands.ts +21 -1
  93. package/src/resources/extensions/gsd/doctor-checks.ts +75 -0
  94. package/src/resources/extensions/gsd/doctor-environment.ts +82 -1
  95. package/src/resources/extensions/gsd/doctor-format.ts +20 -0
  96. package/src/resources/extensions/gsd/doctor-providers.ts +38 -1
  97. package/src/resources/extensions/gsd/doctor-types.ts +16 -1
  98. package/src/resources/extensions/gsd/doctor.ts +177 -13
  99. package/src/resources/extensions/gsd/files.ts +45 -0
  100. package/src/resources/extensions/gsd/observability-validator.ts +27 -0
  101. package/src/resources/extensions/gsd/preferences-types.ts +5 -1
  102. package/src/resources/extensions/gsd/preferences-validation.ts +41 -0
  103. package/src/resources/extensions/gsd/prompts/plan-slice.md +2 -1
  104. package/src/resources/extensions/gsd/prompts/reactive-execute.md +41 -0
  105. package/src/resources/extensions/gsd/reactive-graph.ts +289 -0
  106. package/src/resources/extensions/gsd/templates/task-plan.md +11 -3
  107. package/src/resources/extensions/gsd/tests/cmux.test.ts +93 -0
  108. package/src/resources/extensions/gsd/tests/doctor-enhancements.test.ts +266 -0
  109. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +108 -3
  110. package/src/resources/extensions/gsd/tests/plan-quality-validator.test.ts +111 -0
  111. package/src/resources/extensions/gsd/tests/reactive-executor.test.ts +511 -0
  112. package/src/resources/extensions/gsd/tests/reactive-graph.test.ts +299 -0
  113. package/src/resources/extensions/gsd/tests/worktree.test.ts +47 -0
  114. package/src/resources/extensions/gsd/types.ts +43 -0
  115. package/src/resources/extensions/gsd/worktree.ts +35 -15
  116. package/src/resources/extensions/subagent/index.ts +12 -3
@@ -0,0 +1,299 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import {
4
+ deriveTaskGraph,
5
+ getReadyTasks,
6
+ chooseNonConflictingSubset,
7
+ isGraphAmbiguous,
8
+ detectDeadlock,
9
+ graphMetrics,
10
+ } from "../reactive-graph.ts";
11
+ import { parseTaskPlanIO } from "../files.ts";
12
+ import type { TaskIO, DerivedTaskNode } from "../types.ts";
13
+
14
+ // ─── parseTaskPlanIO ──────────────────────────────────────────────────────
15
+
16
+ test("parseTaskPlanIO extracts backtick-wrapped file paths from Inputs and Expected Output", () => {
17
+ const content = `---
18
+ estimated_steps: 3
19
+ estimated_files: 2
20
+ ---
21
+
22
+ # T01: Setup Models
23
+
24
+ **Slice:** S01 — Core Setup
25
+ **Milestone:** M001
26
+
27
+ ## Description
28
+
29
+ Create the core data models.
30
+
31
+ ## Steps
32
+
33
+ 1. Create types file
34
+ 2. Create models file
35
+
36
+ ## Must-Haves
37
+
38
+ - [ ] Type definitions complete
39
+
40
+ ## Verification
41
+
42
+ - Run type checker
43
+
44
+ ## Inputs
45
+
46
+ - \`src/types.ts\` — Existing type definitions from prior work
47
+ - \`src/config.json\` — Configuration schema
48
+
49
+ ## Expected Output
50
+
51
+ - \`src/models.ts\` — New data model definitions
52
+ - \`src/models.test.ts\` — Unit tests for models
53
+ `;
54
+
55
+ const io = parseTaskPlanIO(content);
56
+ assert.deepEqual(io.inputFiles, ["src/types.ts", "src/config.json"]);
57
+ assert.deepEqual(io.outputFiles, ["src/models.ts", "src/models.test.ts"]);
58
+ });
59
+
60
+ test("parseTaskPlanIO returns empty arrays for missing sections", () => {
61
+ const content = `# T01: Something\n\n## Description\n\nNo IO sections here.\n`;
62
+ const io = parseTaskPlanIO(content);
63
+ assert.deepEqual(io.inputFiles, []);
64
+ assert.deepEqual(io.outputFiles, []);
65
+ });
66
+
67
+ test("parseTaskPlanIO ignores non-file-path backtick tokens", () => {
68
+ const content = `# T01: Test
69
+
70
+ ## Inputs
71
+
72
+ - \`true\` — a boolean flag
73
+ - \`src/index.ts\` — main entry
74
+ - \`npm run test\` — a command, not a file
75
+
76
+ ## Expected Output
77
+
78
+ - \`dist/bundle.js\` — compiled output
79
+ - \`false\` — not a file
80
+ `;
81
+
82
+ const io = parseTaskPlanIO(content);
83
+ assert.deepEqual(io.inputFiles, ["src/index.ts"]);
84
+ assert.deepEqual(io.outputFiles, ["dist/bundle.js"]);
85
+ });
86
+
87
+ test("parseTaskPlanIO handles multiple backtick tokens on one line", () => {
88
+ const content = `# T01: Multi
89
+
90
+ ## Inputs
91
+
92
+ - \`src/a.ts\` and \`src/b.ts\` — both needed
93
+
94
+ ## Expected Output
95
+
96
+ - \`src/c.ts\` — output
97
+ `;
98
+ const io = parseTaskPlanIO(content);
99
+ assert.deepEqual(io.inputFiles, ["src/a.ts", "src/b.ts"]);
100
+ assert.deepEqual(io.outputFiles, ["src/c.ts"]);
101
+ });
102
+
103
+ // ─── deriveTaskGraph ──────────────────────────────────────────────────────
104
+
105
+ test("deriveTaskGraph: linear chain T01→T02→T03", () => {
106
+ const tasks: TaskIO[] = [
107
+ { id: "T01", title: "First", inputFiles: [], outputFiles: ["src/a.ts"], done: false },
108
+ { id: "T02", title: "Second", inputFiles: ["src/a.ts"], outputFiles: ["src/b.ts"], done: false },
109
+ { id: "T03", title: "Third", inputFiles: ["src/b.ts"], outputFiles: ["src/c.ts"], done: false },
110
+ ];
111
+
112
+ const graph = deriveTaskGraph(tasks);
113
+ assert.deepEqual(graph[0].dependsOn, []);
114
+ assert.deepEqual(graph[1].dependsOn, ["T01"]);
115
+ assert.deepEqual(graph[2].dependsOn, ["T02"]);
116
+ });
117
+
118
+ test("deriveTaskGraph: diamond dependency", () => {
119
+ const tasks: TaskIO[] = [
120
+ { id: "T01", title: "Base", inputFiles: [], outputFiles: ["src/base.ts"], done: false },
121
+ { id: "T02", title: "Left", inputFiles: ["src/base.ts"], outputFiles: ["src/left.ts"], done: false },
122
+ { id: "T03", title: "Right", inputFiles: ["src/base.ts"], outputFiles: ["src/right.ts"], done: false },
123
+ { id: "T04", title: "Merge", inputFiles: ["src/left.ts", "src/right.ts"], outputFiles: ["src/final.ts"], done: false },
124
+ ];
125
+
126
+ const graph = deriveTaskGraph(tasks);
127
+ assert.deepEqual(graph[0].dependsOn, []);
128
+ assert.deepEqual(graph[1].dependsOn, ["T01"]);
129
+ assert.deepEqual(graph[2].dependsOn, ["T01"]);
130
+ assert.deepEqual(graph[3].dependsOn, ["T02", "T03"]);
131
+ });
132
+
133
+ test("deriveTaskGraph: fully independent tasks", () => {
134
+ const tasks: TaskIO[] = [
135
+ { id: "T01", title: "A", inputFiles: [], outputFiles: ["src/a.ts"], done: false },
136
+ { id: "T02", title: "B", inputFiles: [], outputFiles: ["src/b.ts"], done: false },
137
+ { id: "T03", title: "C", inputFiles: [], outputFiles: ["src/c.ts"], done: false },
138
+ ];
139
+
140
+ const graph = deriveTaskGraph(tasks);
141
+ assert.deepEqual(graph[0].dependsOn, []);
142
+ assert.deepEqual(graph[1].dependsOn, []);
143
+ assert.deepEqual(graph[2].dependsOn, []);
144
+ });
145
+
146
+ test("deriveTaskGraph: self-referencing output→input is excluded", () => {
147
+ const tasks: TaskIO[] = [
148
+ { id: "T01", title: "Self", inputFiles: ["src/a.ts"], outputFiles: ["src/a.ts"], done: false },
149
+ ];
150
+
151
+ const graph = deriveTaskGraph(tasks);
152
+ assert.deepEqual(graph[0].dependsOn, []);
153
+ });
154
+
155
+ // ─── getReadyTasks ────────────────────────────────────────────────────────
156
+
157
+ test("getReadyTasks: partially completed graph", () => {
158
+ const tasks: TaskIO[] = [
159
+ { id: "T01", title: "Base", inputFiles: [], outputFiles: ["src/a.ts"], done: true },
160
+ { id: "T02", title: "Dep", inputFiles: ["src/a.ts"], outputFiles: ["src/b.ts"], done: false },
161
+ { id: "T03", title: "Blocked", inputFiles: ["src/b.ts"], outputFiles: ["src/c.ts"], done: false },
162
+ ];
163
+ const graph = deriveTaskGraph(tasks);
164
+ const ready = getReadyTasks(graph, new Set(["T01"]), new Set());
165
+ assert.deepEqual(ready, ["T02"]);
166
+ });
167
+
168
+ test("getReadyTasks: nothing complete → only root tasks ready", () => {
169
+ const tasks: TaskIO[] = [
170
+ { id: "T01", title: "Root", inputFiles: [], outputFiles: ["src/a.ts"], done: false },
171
+ { id: "T02", title: "Dep", inputFiles: ["src/a.ts"], outputFiles: ["src/b.ts"], done: false },
172
+ ];
173
+ const graph = deriveTaskGraph(tasks);
174
+ const ready = getReadyTasks(graph, new Set(), new Set());
175
+ assert.deepEqual(ready, ["T01"]);
176
+ });
177
+
178
+ test("getReadyTasks: all complete → empty", () => {
179
+ const tasks: TaskIO[] = [
180
+ { id: "T01", title: "Done", inputFiles: [], outputFiles: ["src/a.ts"], done: true },
181
+ ];
182
+ const graph = deriveTaskGraph(tasks);
183
+ const ready = getReadyTasks(graph, new Set(["T01"]), new Set());
184
+ assert.deepEqual(ready, []);
185
+ });
186
+
187
+ test("getReadyTasks: in-flight tasks excluded", () => {
188
+ const tasks: TaskIO[] = [
189
+ { id: "T01", title: "A", inputFiles: [], outputFiles: ["src/a.ts"], done: false },
190
+ { id: "T02", title: "B", inputFiles: [], outputFiles: ["src/b.ts"], done: false },
191
+ ];
192
+ const graph = deriveTaskGraph(tasks);
193
+ const ready = getReadyTasks(graph, new Set(), new Set(["T01"]));
194
+ assert.deepEqual(ready, ["T02"]);
195
+ });
196
+
197
+ // ─── chooseNonConflictingSubset ───────────────────────────────────────────
198
+
199
+ test("chooseNonConflictingSubset: output conflicts", () => {
200
+ const tasks: TaskIO[] = [
201
+ { id: "T01", title: "A", inputFiles: [], outputFiles: ["src/shared.ts"], done: false },
202
+ { id: "T02", title: "B", inputFiles: [], outputFiles: ["src/shared.ts"], done: false },
203
+ { id: "T03", title: "C", inputFiles: [], outputFiles: ["src/other.ts"], done: false },
204
+ ];
205
+ const graph = deriveTaskGraph(tasks);
206
+ const selected = chooseNonConflictingSubset(["T01", "T02", "T03"], graph, 3, new Set());
207
+ // T01 claims shared.ts, T02 conflicts, T03 is fine
208
+ assert.deepEqual(selected, ["T01", "T03"]);
209
+ });
210
+
211
+ test("chooseNonConflictingSubset: respects maxParallel", () => {
212
+ const tasks: TaskIO[] = [
213
+ { id: "T01", title: "A", inputFiles: [], outputFiles: ["src/a.ts"], done: false },
214
+ { id: "T02", title: "B", inputFiles: [], outputFiles: ["src/b.ts"], done: false },
215
+ { id: "T03", title: "C", inputFiles: [], outputFiles: ["src/c.ts"], done: false },
216
+ ];
217
+ const graph = deriveTaskGraph(tasks);
218
+ const selected = chooseNonConflictingSubset(["T01", "T02", "T03"], graph, 2, new Set());
219
+ assert.deepEqual(selected, ["T01", "T02"]);
220
+ });
221
+
222
+ test("chooseNonConflictingSubset: respects inFlightOutputs", () => {
223
+ const tasks: TaskIO[] = [
224
+ { id: "T01", title: "A", inputFiles: [], outputFiles: ["src/a.ts"], done: false },
225
+ { id: "T02", title: "B", inputFiles: [], outputFiles: ["src/b.ts"], done: false },
226
+ ];
227
+ const graph = deriveTaskGraph(tasks);
228
+ const selected = chooseNonConflictingSubset(["T01", "T02"], graph, 4, new Set(["src/a.ts"]));
229
+ assert.deepEqual(selected, ["T02"]);
230
+ });
231
+
232
+ // ─── isGraphAmbiguous ─────────────────────────────────────────────────────
233
+
234
+ test("isGraphAmbiguous: task with no IO → ambiguous", () => {
235
+ const graph: DerivedTaskNode[] = [
236
+ { id: "T01", title: "A", inputFiles: [], outputFiles: [], done: false, dependsOn: [] },
237
+ { id: "T02", title: "B", inputFiles: ["src/a.ts"], outputFiles: ["src/b.ts"], done: false, dependsOn: [] },
238
+ ];
239
+ assert.equal(isGraphAmbiguous(graph), true);
240
+ });
241
+
242
+ test("isGraphAmbiguous: all tasks have IO → not ambiguous", () => {
243
+ const graph: DerivedTaskNode[] = [
244
+ { id: "T01", title: "A", inputFiles: [], outputFiles: ["src/a.ts"], done: false, dependsOn: [] },
245
+ { id: "T02", title: "B", inputFiles: ["src/a.ts"], outputFiles: ["src/b.ts"], done: false, dependsOn: ["T01"] },
246
+ ];
247
+ assert.equal(isGraphAmbiguous(graph), false);
248
+ });
249
+
250
+ test("isGraphAmbiguous: done tasks with no IO are ignored", () => {
251
+ const graph: DerivedTaskNode[] = [
252
+ { id: "T01", title: "A", inputFiles: [], outputFiles: [], done: true, dependsOn: [] },
253
+ { id: "T02", title: "B", inputFiles: [], outputFiles: ["src/b.ts"], done: false, dependsOn: [] },
254
+ ];
255
+ assert.equal(isGraphAmbiguous(graph), false);
256
+ });
257
+
258
+ // ─── detectDeadlock ───────────────────────────────────────────────────────
259
+
260
+ test("detectDeadlock: circular dependency detected", () => {
261
+ // T01 depends on T02, T02 depends on T01 — deadlock
262
+ const graph: DerivedTaskNode[] = [
263
+ { id: "T01", title: "A", inputFiles: ["src/b.ts"], outputFiles: ["src/a.ts"], done: false, dependsOn: ["T02"] },
264
+ { id: "T02", title: "B", inputFiles: ["src/a.ts"], outputFiles: ["src/b.ts"], done: false, dependsOn: ["T01"] },
265
+ ];
266
+ assert.equal(detectDeadlock(graph, new Set(), new Set()), true);
267
+ });
268
+
269
+ test("detectDeadlock: normal blocked-waiting-for-in-flight → not deadlock", () => {
270
+ const graph: DerivedTaskNode[] = [
271
+ { id: "T01", title: "A", inputFiles: [], outputFiles: ["src/a.ts"], done: false, dependsOn: [] },
272
+ { id: "T02", title: "B", inputFiles: ["src/a.ts"], outputFiles: ["src/b.ts"], done: false, dependsOn: ["T01"] },
273
+ ];
274
+ // T01 is in-flight, T02 is waiting → not deadlock
275
+ assert.equal(detectDeadlock(graph, new Set(), new Set(["T01"])), false);
276
+ });
277
+
278
+ test("detectDeadlock: all complete → not deadlock", () => {
279
+ const graph: DerivedTaskNode[] = [
280
+ { id: "T01", title: "A", inputFiles: [], outputFiles: ["src/a.ts"], done: true, dependsOn: [] },
281
+ ];
282
+ assert.equal(detectDeadlock(graph, new Set(["T01"]), new Set()), false);
283
+ });
284
+
285
+ // ─── graphMetrics ─────────────────────────────────────────────────────────
286
+
287
+ test("graphMetrics computes correct values", () => {
288
+ const tasks: TaskIO[] = [
289
+ { id: "T01", title: "A", inputFiles: [], outputFiles: ["src/a.ts"], done: true },
290
+ { id: "T02", title: "B", inputFiles: ["src/a.ts"], outputFiles: ["src/b.ts"], done: false },
291
+ { id: "T03", title: "C", inputFiles: [], outputFiles: ["src/c.ts"], done: false },
292
+ ];
293
+ const graph = deriveTaskGraph(tasks);
294
+ const metrics = graphMetrics(graph);
295
+ assert.equal(metrics.taskCount, 3);
296
+ assert.equal(metrics.edgeCount, 1); // T02 depends on T01
297
+ assert.equal(metrics.readySetSize, 2); // T02 (T01 done) and T03 (no deps)
298
+ assert.equal(metrics.ambiguous, false);
299
+ });
@@ -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
  }
@@ -436,3 +436,46 @@ export interface ParallelConfig {
436
436
  merge_strategy: MergeStrategy;
437
437
  auto_merge: AutoMergeMode;
438
438
  }
439
+
440
+ // ─── Reactive Task Execution Types ───────────────────────────────────────
441
+
442
+ /** IO signature extracted from a single task plan's Inputs/Expected Output sections. */
443
+ export interface TaskIO {
444
+ id: string; // e.g. "T01"
445
+ title: string;
446
+ inputFiles: string[];
447
+ outputFiles: string[];
448
+ done: boolean;
449
+ }
450
+
451
+ /** A task node with derived dependency edges from input/output intersection. */
452
+ export interface DerivedTaskNode extends TaskIO {
453
+ /** IDs of tasks whose outputFiles overlap with this task's inputFiles. */
454
+ dependsOn: string[];
455
+ }
456
+
457
+ /** Configuration for reactive (graph-derived parallel) task execution within a slice. */
458
+ export interface ReactiveExecutionConfig {
459
+ enabled: boolean;
460
+ /** Maximum number of tasks to dispatch in parallel. Clamped to 1–8. */
461
+ max_parallel: number;
462
+ /** Isolation mode for parallel tasks within a slice. Currently only "same-tree" is supported. */
463
+ isolation_mode: "same-tree";
464
+ }
465
+
466
+ /** Per-slice reactive execution runtime state, persisted to disk. */
467
+ export interface ReactiveExecutionState {
468
+ sliceId: string;
469
+ /** Task IDs that have been verified as completed. */
470
+ completed: string[];
471
+ /** Task IDs dispatched in the current/most recent reactive batch. */
472
+ dispatched: string[];
473
+ /** Snapshot of the graph at last dispatch. */
474
+ graphSnapshot: {
475
+ taskCount: number;
476
+ edgeCount: number;
477
+ readySetSize: number;
478
+ ambiguous: boolean;
479
+ };
480
+ updatedAt: string;
481
+ }
@@ -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
  /**
@@ -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,