gsd-pi 2.37.1 → 2.38.0-dev.add4f78

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 (159) hide show
  1. package/README.md +1 -1
  2. package/dist/app-paths.js +1 -1
  3. package/dist/cli.js +9 -0
  4. package/dist/extension-discovery.d.ts +5 -3
  5. package/dist/extension-discovery.js +14 -9
  6. package/dist/extension-registry.js +2 -2
  7. package/dist/onboarding.js +1 -0
  8. package/dist/remote-questions-config.js +2 -2
  9. package/dist/resources/extensions/browser-tools/package.json +3 -1
  10. package/dist/resources/extensions/cmux/index.js +55 -1
  11. package/dist/resources/extensions/context7/package.json +1 -1
  12. package/dist/resources/extensions/env-utils.js +29 -0
  13. package/dist/resources/extensions/get-secrets-from-user.js +5 -24
  14. package/dist/resources/extensions/google-search/package.json +3 -1
  15. package/dist/resources/extensions/gsd/auto/session.js +3 -0
  16. package/dist/resources/extensions/gsd/auto-dispatch.js +74 -9
  17. package/dist/resources/extensions/gsd/auto-loop.js +61 -31
  18. package/dist/resources/extensions/gsd/auto-post-unit.js +87 -69
  19. package/dist/resources/extensions/gsd/auto-prompts.js +91 -2
  20. package/dist/resources/extensions/gsd/auto-recovery.js +37 -1
  21. package/dist/resources/extensions/gsd/auto-start.js +6 -1
  22. package/dist/resources/extensions/gsd/auto-worktree-sync.js +13 -5
  23. package/dist/resources/extensions/gsd/auto.js +10 -26
  24. package/dist/resources/extensions/gsd/captures.js +9 -1
  25. package/dist/resources/extensions/gsd/commands-extensions.js +3 -2
  26. package/dist/resources/extensions/gsd/commands-handlers.js +16 -3
  27. package/dist/resources/extensions/gsd/commands.js +22 -2
  28. package/dist/resources/extensions/gsd/detection.js +1 -2
  29. package/dist/resources/extensions/gsd/doctor-checks.js +82 -0
  30. package/dist/resources/extensions/gsd/doctor-environment.js +78 -0
  31. package/dist/resources/extensions/gsd/doctor-format.js +15 -0
  32. package/dist/resources/extensions/gsd/doctor-providers.js +35 -1
  33. package/dist/resources/extensions/gsd/doctor.js +184 -11
  34. package/dist/resources/extensions/gsd/export.js +1 -1
  35. package/dist/resources/extensions/gsd/files.js +43 -2
  36. package/dist/resources/extensions/gsd/forensics.js +1 -1
  37. package/dist/resources/extensions/gsd/index.js +2 -1
  38. package/dist/resources/extensions/gsd/migrate/parsers.js +1 -1
  39. package/dist/resources/extensions/gsd/observability-validator.js +24 -0
  40. package/dist/resources/extensions/gsd/package.json +1 -1
  41. package/dist/resources/extensions/gsd/preferences-types.js +2 -1
  42. package/dist/resources/extensions/gsd/preferences-validation.js +43 -1
  43. package/dist/resources/extensions/gsd/preferences.js +4 -3
  44. package/dist/resources/extensions/gsd/prompts/plan-slice.md +2 -1
  45. package/dist/resources/extensions/gsd/prompts/reactive-execute.md +41 -0
  46. package/dist/resources/extensions/gsd/reactive-graph.js +227 -0
  47. package/dist/resources/extensions/gsd/repo-identity.js +2 -1
  48. package/dist/resources/extensions/gsd/resource-version.js +2 -1
  49. package/dist/resources/extensions/gsd/state.js +1 -1
  50. package/dist/resources/extensions/gsd/templates/task-plan.md +11 -3
  51. package/dist/resources/extensions/gsd/visualizer-data.js +1 -1
  52. package/dist/resources/extensions/gsd/worktree.js +35 -16
  53. package/dist/resources/extensions/remote-questions/status.js +2 -1
  54. package/dist/resources/extensions/remote-questions/store.js +2 -1
  55. package/dist/resources/extensions/search-the-web/provider.js +2 -1
  56. package/dist/resources/extensions/subagent/index.js +12 -3
  57. package/dist/resources/extensions/subagent/isolation.js +2 -1
  58. package/dist/resources/extensions/ttsr/rule-loader.js +2 -1
  59. package/dist/resources/extensions/universal-config/package.json +1 -1
  60. package/dist/welcome-screen.d.ts +12 -0
  61. package/dist/welcome-screen.js +53 -0
  62. package/package.json +2 -1
  63. package/packages/pi-ai/dist/env-api-keys.js +13 -0
  64. package/packages/pi-ai/dist/env-api-keys.js.map +1 -1
  65. package/packages/pi-ai/dist/models.generated.d.ts +172 -0
  66. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  67. package/packages/pi-ai/dist/models.generated.js +172 -0
  68. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  69. package/packages/pi-ai/dist/providers/anthropic-shared.d.ts +64 -0
  70. package/packages/pi-ai/dist/providers/anthropic-shared.d.ts.map +1 -0
  71. package/packages/pi-ai/dist/providers/anthropic-shared.js +668 -0
  72. package/packages/pi-ai/dist/providers/anthropic-shared.js.map +1 -0
  73. package/packages/pi-ai/dist/providers/anthropic-vertex.d.ts +5 -0
  74. package/packages/pi-ai/dist/providers/anthropic-vertex.d.ts.map +1 -0
  75. package/packages/pi-ai/dist/providers/anthropic-vertex.js +85 -0
  76. package/packages/pi-ai/dist/providers/anthropic-vertex.js.map +1 -0
  77. package/packages/pi-ai/dist/providers/anthropic.d.ts +4 -30
  78. package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
  79. package/packages/pi-ai/dist/providers/anthropic.js +47 -764
  80. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  81. package/packages/pi-ai/dist/providers/register-builtins.d.ts.map +1 -1
  82. package/packages/pi-ai/dist/providers/register-builtins.js +6 -0
  83. package/packages/pi-ai/dist/providers/register-builtins.js.map +1 -1
  84. package/packages/pi-ai/dist/types.d.ts +2 -2
  85. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  86. package/packages/pi-ai/dist/types.js.map +1 -1
  87. package/packages/pi-ai/package.json +1 -0
  88. package/packages/pi-ai/src/env-api-keys.ts +14 -0
  89. package/packages/pi-ai/src/models.generated.ts +172 -0
  90. package/packages/pi-ai/src/providers/anthropic-shared.ts +761 -0
  91. package/packages/pi-ai/src/providers/anthropic-vertex.ts +130 -0
  92. package/packages/pi-ai/src/providers/anthropic.ts +76 -868
  93. package/packages/pi-ai/src/providers/register-builtins.ts +7 -0
  94. package/packages/pi-ai/src/types.ts +2 -0
  95. package/packages/pi-coding-agent/dist/core/model-resolver.d.ts.map +1 -1
  96. package/packages/pi-coding-agent/dist/core/model-resolver.js +1 -0
  97. package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
  98. package/packages/pi-coding-agent/dist/core/package-manager.d.ts.map +1 -1
  99. package/packages/pi-coding-agent/dist/core/package-manager.js +8 -4
  100. package/packages/pi-coding-agent/dist/core/package-manager.js.map +1 -1
  101. package/packages/pi-coding-agent/package.json +1 -1
  102. package/packages/pi-coding-agent/src/core/model-resolver.ts +1 -0
  103. package/packages/pi-coding-agent/src/core/package-manager.ts +8 -4
  104. package/pkg/package.json +1 -1
  105. package/src/resources/extensions/cmux/index.ts +57 -1
  106. package/src/resources/extensions/env-utils.ts +31 -0
  107. package/src/resources/extensions/get-secrets-from-user.ts +5 -24
  108. package/src/resources/extensions/gsd/auto/session.ts +5 -1
  109. package/src/resources/extensions/gsd/auto-dispatch.ts +99 -8
  110. package/src/resources/extensions/gsd/auto-loop.ts +83 -64
  111. package/src/resources/extensions/gsd/auto-post-unit.ts +64 -40
  112. package/src/resources/extensions/gsd/auto-prompts.ts +125 -3
  113. package/src/resources/extensions/gsd/auto-recovery.ts +42 -0
  114. package/src/resources/extensions/gsd/auto-start.ts +7 -1
  115. package/src/resources/extensions/gsd/auto-worktree-sync.ts +15 -4
  116. package/src/resources/extensions/gsd/auto.ts +14 -29
  117. package/src/resources/extensions/gsd/captures.ts +10 -1
  118. package/src/resources/extensions/gsd/commands-extensions.ts +4 -2
  119. package/src/resources/extensions/gsd/commands-handlers.ts +17 -2
  120. package/src/resources/extensions/gsd/commands.ts +24 -2
  121. package/src/resources/extensions/gsd/detection.ts +2 -2
  122. package/src/resources/extensions/gsd/doctor-checks.ts +75 -0
  123. package/src/resources/extensions/gsd/doctor-environment.ts +82 -1
  124. package/src/resources/extensions/gsd/doctor-format.ts +20 -0
  125. package/src/resources/extensions/gsd/doctor-providers.ts +38 -1
  126. package/src/resources/extensions/gsd/doctor-types.ts +16 -1
  127. package/src/resources/extensions/gsd/doctor.ts +177 -13
  128. package/src/resources/extensions/gsd/export.ts +1 -1
  129. package/src/resources/extensions/gsd/files.ts +47 -2
  130. package/src/resources/extensions/gsd/forensics.ts +1 -1
  131. package/src/resources/extensions/gsd/index.ts +3 -1
  132. package/src/resources/extensions/gsd/migrate/parsers.ts +1 -1
  133. package/src/resources/extensions/gsd/observability-validator.ts +27 -0
  134. package/src/resources/extensions/gsd/preferences-types.ts +5 -1
  135. package/src/resources/extensions/gsd/preferences-validation.ts +42 -1
  136. package/src/resources/extensions/gsd/preferences.ts +5 -3
  137. package/src/resources/extensions/gsd/prompts/plan-slice.md +2 -1
  138. package/src/resources/extensions/gsd/prompts/reactive-execute.md +41 -0
  139. package/src/resources/extensions/gsd/reactive-graph.ts +289 -0
  140. package/src/resources/extensions/gsd/repo-identity.ts +3 -1
  141. package/src/resources/extensions/gsd/resource-version.ts +3 -1
  142. package/src/resources/extensions/gsd/state.ts +1 -1
  143. package/src/resources/extensions/gsd/templates/task-plan.md +11 -3
  144. package/src/resources/extensions/gsd/tests/cmux.test.ts +93 -0
  145. package/src/resources/extensions/gsd/tests/doctor-enhancements.test.ts +266 -0
  146. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +108 -3
  147. package/src/resources/extensions/gsd/tests/plan-quality-validator.test.ts +111 -0
  148. package/src/resources/extensions/gsd/tests/reactive-executor.test.ts +511 -0
  149. package/src/resources/extensions/gsd/tests/reactive-graph.test.ts +299 -0
  150. package/src/resources/extensions/gsd/tests/worktree.test.ts +47 -0
  151. package/src/resources/extensions/gsd/types.ts +43 -0
  152. package/src/resources/extensions/gsd/visualizer-data.ts +1 -1
  153. package/src/resources/extensions/gsd/worktree.ts +35 -15
  154. package/src/resources/extensions/remote-questions/status.ts +3 -1
  155. package/src/resources/extensions/remote-questions/store.ts +3 -1
  156. package/src/resources/extensions/search-the-web/provider.ts +2 -1
  157. package/src/resources/extensions/subagent/index.ts +12 -3
  158. package/src/resources/extensions/subagent/isolation.ts +3 -1
  159. package/src/resources/extensions/ttsr/rule-loader.ts +3 -1
@@ -0,0 +1,227 @@
1
+ /**
2
+ * Reactive Task Graph — derives dependency edges from task plan IO signatures.
3
+ *
4
+ * Pure functions that build a DAG from task IO intersections and resolve
5
+ * which tasks are currently ready for parallel dispatch. Used by the
6
+ * reactive-execute dispatch path (ADR-004).
7
+ *
8
+ * Graph derivation and resolution functions are pure (no filesystem access).
9
+ * The `loadSliceTaskIO` loader at the bottom is the only async/IO function.
10
+ */
11
+ import { loadFile, parsePlan, parseTaskPlanIO } from "./files.js";
12
+ import { resolveTasksDir, resolveTaskFiles } from "./paths.js";
13
+ import { join } from "node:path";
14
+ import { loadJsonFileOrNull, saveJsonFile } from "./json-persistence.js";
15
+ import { existsSync, unlinkSync } from "node:fs";
16
+ // ─── Graph Construction ───────────────────────────────────────────────────
17
+ /**
18
+ * Build a dependency graph from task IO signatures.
19
+ *
20
+ * A task T_b depends on T_a when any of T_b's inputFiles appear in T_a's
21
+ * outputFiles. Self-references are excluded.
22
+ *
23
+ * Tasks are returned in the same order as the input array.
24
+ */
25
+ export function deriveTaskGraph(tasks) {
26
+ // Build output → producer lookup
27
+ const outputToProducer = new Map();
28
+ for (const task of tasks) {
29
+ for (const outFile of task.outputFiles) {
30
+ const existing = outputToProducer.get(outFile);
31
+ if (existing) {
32
+ existing.push(task.id);
33
+ }
34
+ else {
35
+ outputToProducer.set(outFile, [task.id]);
36
+ }
37
+ }
38
+ }
39
+ return tasks.map((task) => {
40
+ const deps = new Set();
41
+ for (const inFile of task.inputFiles) {
42
+ const producers = outputToProducer.get(inFile);
43
+ if (producers) {
44
+ for (const pid of producers) {
45
+ if (pid !== task.id)
46
+ deps.add(pid);
47
+ }
48
+ }
49
+ }
50
+ return {
51
+ ...task,
52
+ dependsOn: [...deps].sort(),
53
+ };
54
+ });
55
+ }
56
+ // ─── Ready Set Resolution ─────────────────────────────────────────────────
57
+ /**
58
+ * Return task IDs whose dependencies are all in `completed`.
59
+ * Excludes tasks that are already done or in-flight.
60
+ */
61
+ export function getReadyTasks(graph, completed, inFlight) {
62
+ return graph
63
+ .filter((node) => {
64
+ if (node.done || completed.has(node.id) || inFlight.has(node.id))
65
+ return false;
66
+ return node.dependsOn.every((dep) => completed.has(dep));
67
+ })
68
+ .map((node) => node.id);
69
+ }
70
+ // ─── Conflict-Free Subset Selection ──────────────────────────────────────
71
+ /**
72
+ * Greedy selection of non-conflicting tasks up to `maxParallel`.
73
+ *
74
+ * Two tasks conflict if they share any outputFile. We also exclude tasks
75
+ * whose outputs overlap with `inFlightOutputs` (files being written by
76
+ * tasks currently in progress).
77
+ */
78
+ export function chooseNonConflictingSubset(readyIds, graph, maxParallel, inFlightOutputs) {
79
+ const nodeMap = new Map(graph.map((n) => [n.id, n]));
80
+ const claimed = new Set(inFlightOutputs);
81
+ const selected = [];
82
+ for (const id of readyIds) {
83
+ if (selected.length >= maxParallel)
84
+ break;
85
+ const node = nodeMap.get(id);
86
+ if (!node)
87
+ continue;
88
+ // Check for output overlap with already-selected or in-flight
89
+ const conflicts = node.outputFiles.some((f) => claimed.has(f));
90
+ if (conflicts)
91
+ continue;
92
+ // Claim this task's outputs
93
+ for (const f of node.outputFiles)
94
+ claimed.add(f);
95
+ selected.push(id);
96
+ }
97
+ return selected;
98
+ }
99
+ // ─── Graph Quality Checks ─────────────────────────────────────────────────
100
+ /**
101
+ * Returns true if any incomplete task has 0 inputFiles AND 0 outputFiles.
102
+ *
103
+ * An ambiguous graph means IO annotations are too sparse to derive reliable
104
+ * edges — the dispatcher should fall back to sequential execution.
105
+ */
106
+ export function isGraphAmbiguous(graph) {
107
+ return graph.some((node) => !node.done &&
108
+ node.inputFiles.length === 0 &&
109
+ node.outputFiles.length === 0);
110
+ }
111
+ /**
112
+ * Detect deadlock: no tasks are ready and none are in-flight, yet incomplete
113
+ * tasks remain. This indicates a circular dependency or impossible state.
114
+ */
115
+ export function detectDeadlock(graph, completed, inFlight) {
116
+ const incomplete = graph.filter((n) => !n.done && !completed.has(n.id) && !inFlight.has(n.id));
117
+ if (incomplete.length === 0)
118
+ return false; // all done
119
+ if (inFlight.size > 0)
120
+ return false; // something is running, wait for it
121
+ // Nothing in flight, but incomplete tasks remain — check if any are ready
122
+ const ready = getReadyTasks(graph, completed, inFlight);
123
+ return ready.length === 0;
124
+ }
125
+ // ─── Graph Metrics ────────────────────────────────────────────────────────
126
+ /** Compute summary metrics for logging. */
127
+ export function graphMetrics(graph) {
128
+ const completed = new Set(graph.filter((n) => n.done).map((n) => n.id));
129
+ const ready = getReadyTasks(graph, completed, new Set());
130
+ const edgeCount = graph.reduce((sum, n) => sum + n.dependsOn.length, 0);
131
+ return {
132
+ taskCount: graph.length,
133
+ edgeCount,
134
+ readySetSize: ready.length,
135
+ ambiguous: isGraphAmbiguous(graph),
136
+ };
137
+ }
138
+ // ─── IO Loader (async, filesystem) ────────────────────────────────────────
139
+ /**
140
+ * Load TaskIO for all tasks in a slice by reading the slice plan (for done
141
+ * status and task IDs) and individual task plan files (for IO sections).
142
+ *
143
+ * Returns [] when the slice plan or tasks directory doesn't exist.
144
+ */
145
+ export async function loadSliceTaskIO(basePath, mid, sid) {
146
+ const { resolveSliceFile } = await import("./paths.js");
147
+ const slicePlanPath = resolveSliceFile(basePath, mid, sid, "PLAN");
148
+ const planContent = slicePlanPath ? await loadFile(slicePlanPath) : null;
149
+ if (!planContent)
150
+ return [];
151
+ const plan = parsePlan(planContent);
152
+ const tDir = resolveTasksDir(basePath, mid, sid);
153
+ if (!tDir)
154
+ return [];
155
+ const results = [];
156
+ for (const taskEntry of plan.tasks) {
157
+ const planFiles = resolveTaskFiles(tDir, "PLAN");
158
+ const taskFileName = planFiles.find((f) => f.toUpperCase().startsWith(taskEntry.id.toUpperCase() + "-"));
159
+ if (!taskFileName) {
160
+ // Task plan file missing — include with empty IO (will trigger ambiguous)
161
+ results.push({
162
+ id: taskEntry.id,
163
+ title: taskEntry.title,
164
+ inputFiles: [],
165
+ outputFiles: [],
166
+ done: taskEntry.done,
167
+ });
168
+ continue;
169
+ }
170
+ const taskContent = await loadFile(join(tDir, taskFileName));
171
+ if (!taskContent) {
172
+ results.push({
173
+ id: taskEntry.id,
174
+ title: taskEntry.title,
175
+ inputFiles: [],
176
+ outputFiles: [],
177
+ done: taskEntry.done,
178
+ });
179
+ continue;
180
+ }
181
+ const io = parseTaskPlanIO(taskContent);
182
+ results.push({
183
+ id: taskEntry.id,
184
+ title: taskEntry.title,
185
+ inputFiles: io.inputFiles,
186
+ outputFiles: io.outputFiles,
187
+ done: taskEntry.done,
188
+ });
189
+ }
190
+ return results;
191
+ }
192
+ // ─── State Persistence ────────────────────────────────────────────────────
193
+ function reactiveStatePath(basePath, mid, sid) {
194
+ return join(basePath, ".gsd", "runtime", `${mid}-${sid}-reactive.json`);
195
+ }
196
+ function isReactiveState(data) {
197
+ if (!data || typeof data !== "object")
198
+ return false;
199
+ const d = data;
200
+ return typeof d.sliceId === "string" && Array.isArray(d.completed) && Array.isArray(d.dispatched);
201
+ }
202
+ /**
203
+ * Load persisted reactive execution state for a slice.
204
+ * Returns null when no state file exists or the file is invalid.
205
+ */
206
+ export function loadReactiveState(basePath, mid, sid) {
207
+ return loadJsonFileOrNull(reactiveStatePath(basePath, mid, sid), isReactiveState);
208
+ }
209
+ /**
210
+ * Save reactive execution state to disk.
211
+ */
212
+ export function saveReactiveState(basePath, mid, sid, state) {
213
+ saveJsonFile(reactiveStatePath(basePath, mid, sid), state);
214
+ }
215
+ /**
216
+ * Remove the reactive state file when a slice completes.
217
+ */
218
+ export function clearReactiveState(basePath, mid, sid) {
219
+ const path = reactiveStatePath(basePath, mid, sid);
220
+ try {
221
+ if (existsSync(path))
222
+ unlinkSync(path);
223
+ }
224
+ catch {
225
+ // Non-fatal
226
+ }
227
+ }
@@ -10,6 +10,7 @@ import { execFileSync } from "node:child_process";
10
10
  import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, rmSync, symlinkSync } from "node:fs";
11
11
  import { homedir } from "node:os";
12
12
  import { join, resolve } from "node:path";
13
+ const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
13
14
  // ─── Repo Identity ──────────────────────────────────────────────────────────
14
15
  /**
15
16
  * Get the git remote URL for "origin", or "" if no remote is configured.
@@ -105,7 +106,7 @@ export function repoIdentity(basePath) {
105
106
  * otherwise `~/.gsd/projects/<hash>`.
106
107
  */
107
108
  export function externalGsdRoot(basePath) {
108
- const base = process.env.GSD_STATE_DIR || join(homedir(), ".gsd");
109
+ const base = process.env.GSD_STATE_DIR || gsdHome;
109
110
  return join(base, "projects", repoIdentity(basePath));
110
111
  }
111
112
  // ─── Symlink Management ─────────────────────────────────────────────────────
@@ -9,6 +9,7 @@ import { loadJsonFileOrNull } from "./json-persistence.js";
9
9
  import { join } from "node:path";
10
10
  import { homedir } from "node:os";
11
11
  import { resolveProjectRoot } from "./worktree.js";
12
+ const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
12
13
  // ─── Resource Staleness ───────────────────────────────────────────────────
13
14
  /**
14
15
  * Read the resource version (semver) from the managed-resources manifest.
@@ -19,7 +20,7 @@ function isManifestWithVersion(data) {
19
20
  return data !== null && typeof data === "object" && "gsdVersion" in data && typeof data.gsdVersion === "string";
20
21
  }
21
22
  export function readResourceVersion() {
22
- const agentDir = process.env.GSD_CODING_AGENT_DIR || join(homedir(), ".gsd", "agent");
23
+ const agentDir = process.env.GSD_CODING_AGENT_DIR || join(gsdHome, "agent");
23
24
  const manifestPath = join(agentDir, "managed-resources.json");
24
25
  const manifest = loadJsonFileOrNull(manifestPath, isManifestWithVersion);
25
26
  return manifest?.gsdVersion ?? null;
@@ -3,7 +3,7 @@
3
3
  // Pure TypeScript, zero Pi dependencies.
4
4
  import { parseRoadmap, parsePlan, parseSummary, loadFile, parseRequirementCounts, parseContextDependsOn, } from './files.js';
5
5
  import { resolveMilestoneFile, resolveSlicePath, resolveSliceFile, resolveTaskFile, resolveTasksDir, resolveGsdRootFile, gsdRoot, } from './paths.js';
6
- import { findMilestoneIds } from './guided-flow.js';
6
+ import { findMilestoneIds } from './milestone-ids.js';
7
7
  import { nativeBatchParseGsdFiles } from './native-parser-bridge.js';
8
8
  import { join, resolve } from 'path';
9
9
  import { existsSync, readdirSync } from 'node:fs';
@@ -42,11 +42,19 @@ estimated_files: {{estimatedFiles}}
42
42
 
43
43
  ## Inputs
44
44
 
45
+ <!-- Every input MUST be a backtick-wrapped file path. These paths are machine-parsed to
46
+ derive task dependencies — vague descriptions without paths break dependency detection.
47
+ For the first task in a slice with no prior task outputs, list the existing source files
48
+ this task reads or modifies. -->
49
+
45
50
  - `{{filePath}}` — {{whatThisTaskNeedsFromPriorWork}}
46
- - {{priorTaskSummaryInsight}}
47
51
 
48
52
  ## Expected Output
49
53
 
50
- <!-- This task should produce a real increment toward making the slice goal/demo true. A full slice plan should not be able to mark every task complete while the claimed slice behavior still does not work at the stated proof level. -->
54
+ <!-- Every output MUST be a backtick-wrapped file path the specific files this task creates
55
+ or modifies. These paths are machine-parsed to derive task dependencies.
56
+ This task should produce a real increment toward making the slice goal/demo true. A full
57
+ slice plan should not be able to mark every task complete while the claimed slice behavior
58
+ still does not work at the stated proof level. -->
51
59
 
52
- - `{{filePath}}` — {{whatThisTaskShouldProduceOrModify}}
60
+ - `{{filePath}}` — {{whatThisTaskCreatesOrModifies}}
@@ -2,7 +2,7 @@
2
2
  import { existsSync, readFileSync, statSync } from 'node:fs';
3
3
  import { deriveState } from './state.js';
4
4
  import { parseRoadmap, parsePlan, parseSummary, loadFile } from './files.js';
5
- import { findMilestoneIds } from './guided-flow.js';
5
+ import { findMilestoneIds } from './milestone-ids.js';
6
6
  import { resolveMilestoneFile, resolveSliceFile, resolveGsdRootFile } from './paths.js';
7
7
  import { getLedger, getProjectTotals, aggregateByPhase, aggregateBySlice, aggregateByModel, aggregateByTier, formatTierSavings, loadLedgerFromDisk, } from './metrics.js';
8
8
  import { loadAllCaptures, countPendingCaptures } from './captures.js';
@@ -57,42 +57,61 @@ export function captureIntegrationBranch(basePath, milestoneId, options) {
57
57
  writeIntegrationBranch(basePath, milestoneId, current, options);
58
58
  }
59
59
  // ─── Pure Utility Functions (unchanged) ────────────────────────────────────
60
+ /**
61
+ * Find the worktrees segment in a path, supporting both direct
62
+ * (`/.gsd/worktrees/`) and symlink-resolved (`/.gsd/projects/<hash>/worktrees/`)
63
+ * layouts. When `.gsd` is a symlink to `~/.gsd/projects/<hash>`, resolved
64
+ * paths contain the intermediate `projects/<hash>/` segment that the old
65
+ * single-marker check missed.
66
+ */
67
+ function findWorktreeSegment(normalizedPath) {
68
+ // Direct layout: /.gsd/worktrees/<name>
69
+ const directMarker = "/.gsd/worktrees/";
70
+ const idx = normalizedPath.indexOf(directMarker);
71
+ if (idx !== -1) {
72
+ return { gsdIdx: idx, afterWorktrees: idx + directMarker.length };
73
+ }
74
+ // Symlink-resolved layout: /.gsd/projects/<hash>/worktrees/<name>
75
+ const symlinkRe = /\/\.gsd\/projects\/[a-f0-9]+\/worktrees\//;
76
+ const match = normalizedPath.match(symlinkRe);
77
+ if (match && match.index !== undefined) {
78
+ return { gsdIdx: match.index, afterWorktrees: match.index + match[0].length };
79
+ }
80
+ return null;
81
+ }
60
82
  /**
61
83
  * Detect the active worktree name from the current working directory.
62
84
  * Returns null if not inside a GSD worktree (.gsd/worktrees/<name>/).
63
85
  */
64
86
  export function detectWorktreeName(basePath) {
65
87
  const normalizedPath = basePath.replaceAll("\\", "/");
66
- const marker = "/.gsd/worktrees/";
67
- const idx = normalizedPath.indexOf(marker);
68
- if (idx === -1)
88
+ const seg = findWorktreeSegment(normalizedPath);
89
+ if (!seg)
69
90
  return null;
70
- const afterMarker = normalizedPath.slice(idx + marker.length);
91
+ const afterMarker = normalizedPath.slice(seg.afterWorktrees);
71
92
  const name = afterMarker.split("/")[0];
72
93
  return name || null;
73
94
  }
74
95
  /**
75
96
  * Resolve the project root from a path that may be inside a worktree.
76
- * If the path contains `/.gsd/worktrees/<name>/`, returns the portion
77
- * before `/.gsd/`. Otherwise returns the input unchanged.
97
+ * If the path contains a worktrees segment, returns the portion before
98
+ * `/.gsd/`. Otherwise returns the input unchanged.
78
99
  *
79
100
  * Use this in commands that call `process.cwd()` to ensure they always
80
101
  * operate against the real project root, not a worktree subdirectory.
81
102
  */
82
103
  export function resolveProjectRoot(basePath) {
83
104
  const normalizedPath = basePath.replaceAll("\\", "/");
84
- const marker = "/.gsd/worktrees/";
85
- const idx = normalizedPath.indexOf(marker);
86
- if (idx === -1)
105
+ const seg = findWorktreeSegment(normalizedPath);
106
+ if (!seg)
87
107
  return basePath;
88
- // Return the original path up to the .gsd/ marker (un-normalized)
89
- // Account for potential OS-specific separators
108
+ // Return the original path up to the /.gsd/ boundary
90
109
  const sep = basePath.includes("\\") ? "\\" : "/";
91
- const markerOs = `${sep}.gsd${sep}worktrees${sep}`;
92
- const idxOs = basePath.indexOf(markerOs);
93
- if (idxOs !== -1)
94
- return basePath.slice(0, idxOs);
95
- return basePath.slice(0, idx);
110
+ const gsdMarker = `${sep}.gsd${sep}`;
111
+ const gsdIdx = basePath.indexOf(gsdMarker);
112
+ if (gsdIdx !== -1)
113
+ return basePath.slice(0, gsdIdx);
114
+ return basePath.slice(0, seg.gsdIdx);
96
115
  }
97
116
  /**
98
117
  * Get the slice branch name, namespaced by worktree when inside one.
@@ -5,8 +5,9 @@ import { existsSync, readdirSync } from "node:fs";
5
5
  import { join } from "node:path";
6
6
  import { homedir } from "node:os";
7
7
  import { readPromptRecord } from "./store.js";
8
+ const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
8
9
  export function getLatestPromptSummary() {
9
- const runtimeDir = join(homedir(), ".gsd", "runtime", "remote-questions");
10
+ const runtimeDir = join(gsdHome, "runtime", "remote-questions");
10
11
  if (!existsSync(runtimeDir))
11
12
  return null;
12
13
  const files = readdirSync(runtimeDir).filter((f) => f.endsWith(".json"));
@@ -4,8 +4,9 @@
4
4
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
5
  import { join } from "node:path";
6
6
  import { homedir } from "node:os";
7
+ const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
7
8
  function runtimeDir() {
8
- return join(homedir(), ".gsd", "runtime", "remote-questions");
9
+ return join(gsdHome, "runtime", "remote-questions");
9
10
  }
10
11
  function recordPath(id) {
11
12
  return join(runtimeDir(), `${id}.json`);
@@ -15,7 +15,8 @@ import { resolveSearchProviderFromPreferences } from '../gsd/preferences.js';
15
15
  // Compute authFilePath locally instead of importing from app-paths.ts,
16
16
  // because extensions are copied to ~/.gsd/agent/extensions/ at runtime
17
17
  // where the relative import '../../../app-paths.ts' doesn't resolve.
18
- const authFilePath = join(homedir(), '.gsd', 'agent', 'auth.json');
18
+ const gsdHome = process.env.GSD_HOME || join(homedir(), '.gsd');
19
+ const authFilePath = join(gsdHome, 'agent', 'auth.json');
19
20
  const VALID_PREFERENCES = new Set(['tavily', 'brave', 'ollama', 'auto']);
20
21
  const PREFERENCE_KEY = 'search_provider';
21
22
  /** Returns the Tavily API key from the environment, or empty string if not set. */
@@ -360,7 +360,7 @@ async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, si
360
360
  }
361
361
  }
362
362
  }
363
- async function runSingleAgentInCmuxSplit(cmuxClient, direction, defaultCwd, agents, agentName, task, cwd, step, signal, onUpdate, makeDetails) {
363
+ async function runSingleAgentInCmuxSplit(cmuxClient, directionOrSurfaceId, defaultCwd, agents, agentName, task, cwd, step, signal, onUpdate, makeDetails) {
364
364
  const agent = agents.find((a) => a.name === agentName);
365
365
  if (!agent) {
366
366
  return runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, signal, onUpdate, makeDetails);
@@ -397,7 +397,12 @@ async function runSingleAgentInCmuxSplit(cmuxClient, direction, defaultCwd, agen
397
397
  const stdoutPath = path.join(tmpOutputDir, "stdout.jsonl");
398
398
  const stderrPath = path.join(tmpOutputDir, "stderr.log");
399
399
  const exitPath = path.join(tmpOutputDir, "exit.code");
400
- const cmuxSurfaceId = await cmuxClient.createSplit(direction);
400
+ // Accept either a pre-created surface ID or a direction to create a new split
401
+ const isDirection = directionOrSurfaceId === "right" || directionOrSurfaceId === "down"
402
+ || directionOrSurfaceId === "left" || directionOrSurfaceId === "up";
403
+ const cmuxSurfaceId = isDirection
404
+ ? await cmuxClient.createSplit(directionOrSurfaceId)
405
+ : directionOrSurfaceId;
401
406
  if (!cmuxSurfaceId) {
402
407
  return runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, signal, onUpdate, makeDetails);
403
408
  }
@@ -656,10 +661,14 @@ export default function (pi) {
656
661
  const MAX_RETRIES = 1; // Retry failed tasks once
657
662
  const batchId = crypto.randomUUID();
658
663
  const batchSize = params.tasks.length;
664
+ // Pre-create a grid layout for cmux splits so agents get a clean tiled arrangement
665
+ const gridSurfaces = cmuxSplitsEnabled
666
+ ? await cmuxClient.createGridLayout(Math.min(batchSize, MAX_CONCURRENCY))
667
+ : [];
659
668
  const results = await mapWithConcurrencyLimit(params.tasks, MAX_CONCURRENCY, async (t, index) => {
660
669
  const workerId = registerWorker(t.agent, t.task, index, batchSize, batchId);
661
670
  const runTask = () => cmuxSplitsEnabled
662
- ? runSingleAgentInCmuxSplit(cmuxClient, index % 2 === 0 ? "right" : "down", ctx.cwd, agents, t.agent, t.task, t.cwd, undefined, signal, (partial) => {
671
+ ? runSingleAgentInCmuxSplit(cmuxClient, gridSurfaces[index] ?? (index % 2 === 0 ? "right" : "down"), ctx.cwd, agents, t.agent, t.task, t.cwd, undefined, signal, (partial) => {
663
672
  if (partial.details?.results[0]) {
664
673
  allResults[index] = partial.details.results[0];
665
674
  emitParallelUpdate();
@@ -17,8 +17,9 @@ const execFile = promisify(execFileCb);
17
17
  function encodeCwd(cwd) {
18
18
  return cwd.replace(/\//g, "--");
19
19
  }
20
+ const gsdHome = process.env.GSD_HOME || path.join(os.homedir(), ".gsd");
20
21
  function getIsolationBaseDir(cwd, taskId) {
21
- return path.join(os.homedir(), ".gsd", "wt", encodeCwd(cwd), taskId);
22
+ return path.join(gsdHome, "wt", encodeCwd(cwd), taskId);
22
23
  }
23
24
  // Track active isolation dirs for cleanup on exit
24
25
  const activeIsolations = new Set();
@@ -8,6 +8,7 @@
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
+ const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
11
12
  import { splitFrontmatter, parseFrontmatterMap } from "../shared/frontmatter.js";
12
13
  function parseRuleFile(filePath) {
13
14
  let content;
@@ -56,7 +57,7 @@ function scanDir(dir) {
56
57
  * Project rules override global rules with the same name.
57
58
  */
58
59
  export function loadRules(cwd) {
59
- const globalDir = join(homedir(), ".gsd", "agent", "rules");
60
+ const globalDir = join(gsdHome, "agent", "rules");
60
61
  const projectDir = join(cwd, ".gsd", "rules");
61
62
  const globalRules = scanDir(globalDir);
62
63
  const projectRules = scanDir(projectDir);
@@ -5,7 +5,7 @@
5
5
  "type": "module",
6
6
  "pi": {
7
7
  "extensions": [
8
- "./index.ts"
8
+ "./index.js"
9
9
  ]
10
10
  }
11
11
  }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * GSD Welcome Screen
3
+ *
4
+ * Rendered to stderr before the TUI takes over.
5
+ * No box, no panels — logo with metadata alongside, dim hint below.
6
+ */
7
+ export interface WelcomeScreenOptions {
8
+ version: string;
9
+ modelName?: string;
10
+ provider?: string;
11
+ }
12
+ export declare function printWelcomeScreen(opts: WelcomeScreenOptions): void;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * GSD Welcome Screen
3
+ *
4
+ * Rendered to stderr before the TUI takes over.
5
+ * No box, no panels — logo with metadata alongside, dim hint below.
6
+ */
7
+ import os from 'node:os';
8
+ import chalk from 'chalk';
9
+ import { GSD_LOGO } from './logo.js';
10
+ function getShortCwd() {
11
+ const cwd = process.cwd();
12
+ const home = os.homedir();
13
+ return cwd.startsWith(home) ? '~' + cwd.slice(home.length) : cwd;
14
+ }
15
+ export function printWelcomeScreen(opts) {
16
+ if (!process.stderr.isTTY)
17
+ return;
18
+ const { version, modelName, provider } = opts;
19
+ const shortCwd = getShortCwd();
20
+ // Info lines to sit alongside the logo (one per logo row)
21
+ const modelLine = [modelName, provider].filter(Boolean).join(' · ');
22
+ const INFO = [
23
+ ` ${chalk.bold('Get Shit Done')} ${chalk.dim('v' + version)}`,
24
+ undefined,
25
+ modelLine ? ` ${chalk.dim(modelLine)}` : undefined,
26
+ ` ${chalk.dim(shortCwd)}`,
27
+ undefined,
28
+ undefined,
29
+ ];
30
+ const lines = [''];
31
+ for (let i = 0; i < GSD_LOGO.length; i++) {
32
+ lines.push(chalk.cyan(GSD_LOGO[i]) + (INFO[i] ?? ''));
33
+ }
34
+ // Tool status + hint — dim, aligned under the info text
35
+ const pad = ' '.repeat(28) + ' '; // aligns with the info text column
36
+ const toolParts = [];
37
+ if (process.env.BRAVE_API_KEY)
38
+ toolParts.push('Brave ✓');
39
+ if (process.env.BRAVE_ANSWERS_KEY)
40
+ toolParts.push('Answers ✓');
41
+ if (process.env.JINA_API_KEY)
42
+ toolParts.push('Jina ✓');
43
+ if (process.env.TAVILY_API_KEY)
44
+ toolParts.push('Tavily ✓');
45
+ if (process.env.CONTEXT7_API_KEY)
46
+ toolParts.push('Context7 ✓');
47
+ if (toolParts.length > 0) {
48
+ lines.push(chalk.dim(pad + ['Web search loaded', ...toolParts].join(' · ')));
49
+ }
50
+ lines.push(chalk.dim(pad + '/gsd to begin · /gsd help for all commands'));
51
+ lines.push('');
52
+ process.stderr.write(lines.join('\n') + '\n');
53
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gsd-pi",
3
- "version": "2.37.1",
3
+ "version": "2.38.0-dev.add4f78",
4
4
  "description": "GSD — Get Shit Done coding agent",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -84,6 +84,7 @@
84
84
  },
85
85
  "dependencies": {
86
86
  "@anthropic-ai/sdk": "^0.73.0",
87
+ "@anthropic-ai/vertex-sdk": "^0.14.4",
87
88
  "@aws-sdk/client-bedrock-runtime": "^3.983.0",
88
89
  "@clack/prompts": "^1.1.0",
89
90
  "@google/genai": "^1.40.0",
@@ -53,6 +53,19 @@ export function getEnvApiKey(provider) {
53
53
  if (provider === "anthropic") {
54
54
  return process.env.ANTHROPIC_OAUTH_TOKEN || process.env.ANTHROPIC_API_KEY;
55
55
  }
56
+ // Anthropic on Vertex AI uses Application Default Credentials.
57
+ // Detected via ANTHROPIC_VERTEX_PROJECT_ID (same env var as Claude Code).
58
+ if (provider === "anthropic-vertex") {
59
+ const hasProject = !!process.env.ANTHROPIC_VERTEX_PROJECT_ID;
60
+ if (hasProject) {
61
+ return "<authenticated>";
62
+ }
63
+ // Fall back to Google Cloud project env vars
64
+ const hasGoogleProject = !!(process.env.GOOGLE_CLOUD_PROJECT || process.env.GCLOUD_PROJECT);
65
+ if (hasGoogleProject && hasVertexAdcCredentials()) {
66
+ return "<authenticated>";
67
+ }
68
+ }
56
69
  // Vertex AI uses Application Default Credentials, not API keys.
57
70
  // Auth is configured via `gcloud auth application-default login`.
58
71
  if (provider === "google-vertex") {