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
package/README.md CHANGED
@@ -629,7 +629,7 @@ GSD isn't locked to one provider. It runs on the [Pi SDK](https://github.com/bad
629
629
 
630
630
  ### Built-in Providers
631
631
 
632
- Anthropic, OpenAI, Google (Gemini), OpenRouter, GitHub Copilot, Amazon Bedrock, Azure OpenAI, Google Vertex, Groq, Cerebras, Mistral, xAI, HuggingFace, Vercel AI Gateway, and more.
632
+ Anthropic, Anthropic (Vertex AI), OpenAI, Google (Gemini), OpenRouter, GitHub Copilot, Amazon Bedrock, Azure OpenAI, Google Vertex, Groq, Cerebras, Mistral, xAI, HuggingFace, Vercel AI Gateway, and more.
633
633
 
634
634
  ### OAuth / Max Plans
635
635
 
package/dist/app-paths.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { homedir } from 'os';
2
2
  import { join } from 'path';
3
- export const appRoot = join(homedir(), '.gsd');
3
+ export const appRoot = process.env.GSD_HOME || join(homedir(), '.gsd');
4
4
  export const agentDir = join(appRoot, 'agent');
5
5
  export const sessionsDir = join(appRoot, 'sessions');
6
6
  export const authFilePath = join(agentDir, 'auth.json');
package/dist/cli.js CHANGED
@@ -505,6 +505,15 @@ if (enabledModelPatterns && enabledModelPatterns.length > 0) {
505
505
  session.setScopedModels(scopedModels);
506
506
  }
507
507
  }
508
+ // Welcome screen — shown on every fresh interactive session before TUI takes over
509
+ {
510
+ const { printWelcomeScreen } = await import('./welcome-screen.js');
511
+ printWelcomeScreen({
512
+ version: process.env.GSD_VERSION || '0.0.0',
513
+ modelName: settingsManager.getDefaultModel() || undefined,
514
+ provider: settingsManager.getDefaultProvider() || undefined,
515
+ });
516
+ }
508
517
  const interactiveMode = new InteractiveMode(session);
509
518
  markStartup('InteractiveMode');
510
519
  printStartupTimings();
@@ -1,9 +1,11 @@
1
1
  /**
2
2
  * Resolves the entry-point file(s) for a single extension directory.
3
3
  *
4
- * 1. If the directory contains a package.json with a `pi.extensions` array,
5
- * each entry is resolved relative to the directory and returned (if it exists).
6
- * 2. Otherwise falls back to `index.ts` → `index.js`.
4
+ * 1. If the directory contains a package.json with a `pi` manifest object,
5
+ * the manifest is authoritative:
6
+ * - `pi.extensions` array resolve each entry relative to the directory.
7
+ * - `pi: {}` (no extensions) → return empty (library opt-out, e.g. cmux).
8
+ * 2. Only when no `pi` manifest exists does it fall back to `index.ts` → `index.js`.
7
9
  */
8
10
  export declare function resolveExtensionEntries(dir: string): string[];
9
11
  /**
@@ -6,24 +6,29 @@ function isExtensionFile(name) {
6
6
  /**
7
7
  * Resolves the entry-point file(s) for a single extension directory.
8
8
  *
9
- * 1. If the directory contains a package.json with a `pi.extensions` array,
10
- * each entry is resolved relative to the directory and returned (if it exists).
11
- * 2. Otherwise falls back to `index.ts` → `index.js`.
9
+ * 1. If the directory contains a package.json with a `pi` manifest object,
10
+ * the manifest is authoritative:
11
+ * - `pi.extensions` array resolve each entry relative to the directory.
12
+ * - `pi: {}` (no extensions) → return empty (library opt-out, e.g. cmux).
13
+ * 2. Only when no `pi` manifest exists does it fall back to `index.ts` → `index.js`.
12
14
  */
13
15
  export function resolveExtensionEntries(dir) {
14
16
  const packageJsonPath = join(dir, 'package.json');
15
17
  if (existsSync(packageJsonPath)) {
16
18
  try {
17
19
  const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
18
- const declared = pkg?.pi?.extensions;
19
- if (Array.isArray(declared)) {
20
- const resolved = declared
20
+ if (pkg?.pi && typeof pkg.pi === 'object') {
21
+ // When a pi manifest exists, it is authoritative — don't fall through
22
+ // to index.ts/index.js auto-detection. This allows library directories
23
+ // (like cmux) to opt out by declaring "pi": {} with no extensions.
24
+ const declared = pkg.pi.extensions;
25
+ if (!Array.isArray(declared) || declared.length === 0) {
26
+ return [];
27
+ }
28
+ return declared
21
29
  .filter((entry) => typeof entry === 'string')
22
30
  .map((entry) => resolve(dir, entry))
23
31
  .filter((entry) => existsSync(entry));
24
- if (resolved.length > 0) {
25
- return resolved;
26
- }
27
32
  }
28
33
  }
29
34
  catch {
@@ -6,7 +6,7 @@
6
6
  * The only way an extension stops loading is an explicit `gsd extensions disable <id>`.
7
7
  */
8
8
  import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync } from "node:fs";
9
- import { homedir } from "node:os";
9
+ import { appRoot } from "./app-paths.js";
10
10
  import { dirname, join } from "node:path";
11
11
  // ─── Validation ─────────────────────────────────────────────────────────────
12
12
  function isRegistry(data) {
@@ -26,7 +26,7 @@ function isManifest(data) {
26
26
  }
27
27
  // ─── Registry Path ──────────────────────────────────────────────────────────
28
28
  export function getRegistryPath() {
29
- return join(homedir(), ".gsd", "extensions", "registry.json");
29
+ return join(appRoot, "extensions", "registry.json");
30
30
  }
31
31
  // ─── Registry I/O ───────────────────────────────────────────────────────────
32
32
  function defaultRegistry() {
@@ -38,6 +38,7 @@ const TOOL_KEYS = [
38
38
  /** Known LLM provider IDs that, if authed, mean the user doesn't need onboarding */
39
39
  const LLM_PROVIDER_IDS = [
40
40
  'anthropic',
41
+ 'anthropic-vertex',
41
42
  'openai',
42
43
  'github-copilot',
43
44
  'openai-codex',
@@ -9,12 +9,12 @@
9
9
  */
10
10
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
11
11
  import { dirname, join } from "node:path";
12
- import { homedir } from "node:os";
12
+ import { appRoot } from "./app-paths.js";
13
13
  // Inlined from preferences.ts to avoid crossing the compiled/uncompiled
14
14
  // boundary — this file is compiled by tsc, but preferences.ts is loaded
15
15
  // via jiti at runtime. Importing it as .js fails because no .js exists
16
16
  // in dist/. See #592, #1110.
17
- const GLOBAL_PREFERENCES_PATH = join(homedir(), ".gsd", "preferences.md");
17
+ const GLOBAL_PREFERENCES_PATH = join(appRoot, "preferences.md");
18
18
  export function saveRemoteQuestionsConfig(channel, channelId) {
19
19
  const prefsPath = GLOBAL_PREFERENCES_PATH;
20
20
  const block = [
@@ -7,7 +7,9 @@
7
7
  "test": "node --test tests/*.test.mjs"
8
8
  },
9
9
  "pi": {
10
- "extensions": ["./index.ts"]
10
+ "extensions": [
11
+ "./index.js"
12
+ ]
11
13
  },
12
14
  "peerDependencies": {
13
15
  "playwright": ">=1.40.0",
@@ -237,11 +237,14 @@ export class CmuxClient {
237
237
  return extractSurfaceIds(parsed);
238
238
  }
239
239
  async createSplit(direction) {
240
+ return this.createSplitFrom(this.config.surfaceId, direction);
241
+ }
242
+ async createSplitFrom(sourceSurfaceId, direction) {
240
243
  if (!this.config.splits)
241
244
  return null;
242
245
  const before = new Set(await this.listSurfaceIds());
243
246
  const args = ["new-split", direction];
244
- const scopedArgs = this.appendSurface(this.appendWorkspace(args), this.config.surfaceId);
247
+ const scopedArgs = this.appendSurface(this.appendWorkspace(args), sourceSurfaceId);
245
248
  await this.runAsync(scopedArgs);
246
249
  const after = await this.listSurfaceIds();
247
250
  for (const id of after) {
@@ -250,6 +253,57 @@ export class CmuxClient {
250
253
  }
251
254
  return null;
252
255
  }
256
+ /**
257
+ * Create a grid of surfaces for parallel agent execution.
258
+ *
259
+ * Layout strategy (gsd stays in the original surface):
260
+ * 1 agent: [gsd | A]
261
+ * 2 agents: [gsd | A]
262
+ * [ | B]
263
+ * 3 agents: [gsd | A]
264
+ * [ C | B]
265
+ * 4 agents: [gsd | A]
266
+ * [ C | B] (D splits from B downward)
267
+ * [ | D]
268
+ *
269
+ * Returns surface IDs in order, or empty array on failure.
270
+ */
271
+ async createGridLayout(count) {
272
+ if (!this.config.splits || count <= 0)
273
+ return [];
274
+ const surfaces = [];
275
+ // First split: create right column from the gsd surface
276
+ const rightCol = await this.createSplitFrom(this.config.surfaceId, "right");
277
+ if (!rightCol)
278
+ return [];
279
+ surfaces.push(rightCol);
280
+ if (count === 1)
281
+ return surfaces;
282
+ // Second split: split right column down → bottom-right
283
+ const bottomRight = await this.createSplitFrom(rightCol, "down");
284
+ if (!bottomRight)
285
+ return surfaces;
286
+ surfaces.push(bottomRight);
287
+ if (count === 2)
288
+ return surfaces;
289
+ // Third split: split gsd surface down → bottom-left
290
+ const bottomLeft = await this.createSplitFrom(this.config.surfaceId, "down");
291
+ if (!bottomLeft)
292
+ return surfaces;
293
+ surfaces.push(bottomLeft);
294
+ if (count === 3)
295
+ return surfaces;
296
+ // Fourth+: split subsequent surfaces down from the last created
297
+ let lastSurface = bottomRight;
298
+ for (let i = 3; i < count; i++) {
299
+ const next = await this.createSplitFrom(lastSurface, "down");
300
+ if (!next)
301
+ break;
302
+ surfaces.push(next);
303
+ lastSurface = next;
304
+ }
305
+ return surfaces;
306
+ }
253
307
  async sendSurface(surfaceId, text) {
254
308
  const payload = text.endsWith("\n") ? text : `${text}\n`;
255
309
  const stdout = await this.runAsync(["send-surface", "--surface", surfaceId, payload]);
@@ -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,29 @@
1
+ // GSD Extension — Environment variable utilities
2
+ // Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
3
+ //
4
+ // Pure utility for checking existing env keys in .env files and process.env.
5
+ // Extracted from get-secrets-from-user.ts to avoid pulling in @gsd/pi-tui
6
+ // when only env-checking is needed (e.g. from files.ts during report generation).
7
+ import { readFile } from "node:fs/promises";
8
+ /**
9
+ * Check which keys already exist in a .env file or process.env.
10
+ * Returns the subset of `keys` that are already set.
11
+ */
12
+ export async function checkExistingEnvKeys(keys, envFilePath) {
13
+ let fileContent = "";
14
+ try {
15
+ fileContent = await readFile(envFilePath, "utf8");
16
+ }
17
+ catch {
18
+ // ENOENT or other read error — proceed with empty content
19
+ }
20
+ const existing = [];
21
+ for (const key of keys) {
22
+ const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
23
+ const regex = new RegExp(`^${escaped}\\s*=`, "m");
24
+ if (regex.test(fileContent) || key in process.env) {
25
+ existing.push(key);
26
+ }
27
+ }
28
+ return existing;
29
+ }
@@ -46,30 +46,11 @@ async function writeEnvKey(filePath, key, value) {
46
46
  await writeFile(filePath, content, "utf8");
47
47
  }
48
48
  // ─── Exported utilities ───────────────────────────────────────────────────────
49
- /**
50
- * Check which keys already exist in the .env file or process.env.
51
- * Returns the subset of `keys` that are already set.
52
- * Handles ENOENT gracefully (still checks process.env).
53
- * Empty-string values count as existing.
54
- */
55
- export async function checkExistingEnvKeys(keys, envFilePath) {
56
- let fileContent = "";
57
- try {
58
- fileContent = await readFile(envFilePath, "utf8");
59
- }
60
- catch {
61
- // ENOENT or other read error — proceed with empty content
62
- }
63
- const existing = [];
64
- for (const key of keys) {
65
- const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
66
- const regex = new RegExp(`^${escaped}\\s*=`, "m");
67
- if (regex.test(fileContent) || key in process.env) {
68
- existing.push(key);
69
- }
70
- }
71
- return existing;
72
- }
49
+ // Re-export from env-utils.ts so existing consumers still work.
50
+ // The implementation lives in env-utils.ts to avoid pulling @gsd/pi-tui
51
+ // into modules that only need env-checking (e.g. files.ts during reports).
52
+ import { checkExistingEnvKeys } from "./env-utils.js";
53
+ export { checkExistingEnvKeys };
73
54
  /**
74
55
  * Detect the write destination based on project files in basePath.
75
56
  * Priority: vercel.json → convex/ dir → fallback "dotenv".
@@ -4,6 +4,8 @@
4
4
  "version": "1.0.0",
5
5
  "type": "module",
6
6
  "pi": {
7
- "extensions": ["./index.ts"]
7
+ "extensions": [
8
+ "./index.js"
9
+ ]
8
10
  }
9
11
  }
@@ -60,6 +60,8 @@ export class AutoSession {
60
60
  lastStateRebuildAt = 0;
61
61
  // ── Sidecar queue ─────────────────────────────────────────────────────
62
62
  sidecarQueue = [];
63
+ // ── Dispatch circuit breakers ──────────────────────────────────────
64
+ rewriteAttemptCount = 0;
63
65
  // ── Metrics ──────────────────────────────────────────────────────────────
64
66
  autoStartTime = 0;
65
67
  lastPromptCharCount;
@@ -160,6 +162,7 @@ export class AutoSession {
160
162
  this.lastBaselineCharCount = undefined;
161
163
  this.pendingQuickTasks = [];
162
164
  this.sidecarQueue = [];
165
+ this.rewriteAttemptCount = 0;
163
166
  // Signal handler
164
167
  this.sigtermHandler = null;
165
168
  // Loop promise state
@@ -12,7 +12,7 @@ import { loadFile, loadActiveOverrides, parseRoadmap } from "./files.js";
12
12
  import { resolveMilestoneFile, resolveMilestonePath, resolveSliceFile, resolveTaskFile, relSliceFile, buildMilestoneFileName, } from "./paths.js";
13
13
  import { existsSync, mkdirSync, writeFileSync } from "node:fs";
14
14
  import { join } from "node:path";
15
- import { buildResearchMilestonePrompt, buildPlanMilestonePrompt, buildResearchSlicePrompt, buildPlanSlicePrompt, buildExecuteTaskPrompt, buildCompleteSlicePrompt, buildCompleteMilestonePrompt, buildValidateMilestonePrompt, buildReplanSlicePrompt, buildRunUatPrompt, buildReassessRoadmapPrompt, buildRewriteDocsPrompt, checkNeedsReassessment, checkNeedsRunUat, } from "./auto-prompts.js";
15
+ import { buildResearchMilestonePrompt, buildPlanMilestonePrompt, buildResearchSlicePrompt, buildPlanSlicePrompt, buildExecuteTaskPrompt, buildCompleteSlicePrompt, buildCompleteMilestonePrompt, buildValidateMilestonePrompt, buildReplanSlicePrompt, buildRunUatPrompt, buildReassessRoadmapPrompt, buildRewriteDocsPrompt, buildReactiveExecutePrompt, checkNeedsReassessment, checkNeedsRunUat, } from "./auto-prompts.js";
16
16
  function missingSliceStop(mid, phase) {
17
17
  return {
18
18
  action: "stop",
@@ -22,25 +22,24 @@ function missingSliceStop(mid, phase) {
22
22
  }
23
23
  // ─── Rewrite Circuit Breaker ──────────────────────────────────────────────
24
24
  const MAX_REWRITE_ATTEMPTS = 3;
25
- let rewriteAttemptCount = 0;
26
- export function resetRewriteCircuitBreaker() {
27
- rewriteAttemptCount = 0;
28
- }
29
25
  // ─── Rules ────────────────────────────────────────────────────────────────
30
26
  const DISPATCH_RULES = [
31
27
  {
32
28
  name: "rewrite-docs (override gate)",
33
- match: async ({ mid, midTitle, state, basePath }) => {
29
+ match: async ({ mid, midTitle, state, basePath, session }) => {
34
30
  const pendingOverrides = await loadActiveOverrides(basePath);
35
31
  if (pendingOverrides.length === 0)
36
32
  return null;
37
- if (rewriteAttemptCount >= MAX_REWRITE_ATTEMPTS) {
33
+ const count = session?.rewriteAttemptCount ?? 0;
34
+ if (count >= MAX_REWRITE_ATTEMPTS) {
38
35
  const { resolveAllOverrides } = await import("./files.js");
39
36
  await resolveAllOverrides(basePath);
40
- rewriteAttemptCount = 0;
37
+ if (session)
38
+ session.rewriteAttemptCount = 0;
41
39
  return null;
42
40
  }
43
- rewriteAttemptCount++;
41
+ if (session)
42
+ session.rewriteAttemptCount++;
44
43
  const unitId = state.activeSlice ? `${mid}/${state.activeSlice.id}` : mid;
45
44
  return {
46
45
  action: "dispatch",
@@ -223,6 +222,72 @@ const DISPATCH_RULES = [
223
222
  };
224
223
  },
225
224
  },
225
+ {
226
+ name: "executing → reactive-execute (parallel dispatch)",
227
+ match: async ({ state, mid, midTitle, basePath, prefs }) => {
228
+ if (state.phase !== "executing" || !state.activeTask)
229
+ return null;
230
+ if (!state.activeSlice)
231
+ return null; // fall through
232
+ // Only activate when reactive_execution is explicitly enabled
233
+ const reactiveConfig = prefs?.reactive_execution;
234
+ if (!reactiveConfig?.enabled)
235
+ return null;
236
+ const sid = state.activeSlice.id;
237
+ const sTitle = state.activeSlice.title;
238
+ const maxParallel = reactiveConfig.max_parallel ?? 2;
239
+ // Dry-run mode: max_parallel=1 means graph is derived and logged but
240
+ // execution remains sequential
241
+ if (maxParallel <= 1)
242
+ return null;
243
+ try {
244
+ const { loadSliceTaskIO, deriveTaskGraph, isGraphAmbiguous, getReadyTasks, chooseNonConflictingSubset, graphMetrics, } = await import("./reactive-graph.js");
245
+ const taskIO = await loadSliceTaskIO(basePath, mid, sid);
246
+ if (taskIO.length < 2)
247
+ return null; // single task, no point
248
+ const graph = deriveTaskGraph(taskIO);
249
+ // Ambiguous graph → fall through to sequential
250
+ if (isGraphAmbiguous(graph))
251
+ return null;
252
+ const completed = new Set(graph.filter((n) => n.done).map((n) => n.id));
253
+ const readyIds = getReadyTasks(graph, completed, new Set());
254
+ // Only activate reactive dispatch when >1 task is ready
255
+ if (readyIds.length <= 1)
256
+ return null;
257
+ const selected = chooseNonConflictingSubset(readyIds, graph, maxParallel, new Set());
258
+ if (selected.length <= 1)
259
+ return null;
260
+ // Log graph metrics for observability
261
+ const metrics = graphMetrics(graph);
262
+ process.stderr.write(`gsd-reactive: ${mid}/${sid} graph — tasks:${metrics.taskCount} edges:${metrics.edgeCount} ` +
263
+ `ready:${metrics.readySetSize} dispatching:${selected.length} ambiguous:${metrics.ambiguous}\n`);
264
+ // Persist dispatched batch so verification and recovery can check
265
+ // exactly which tasks were sent.
266
+ const { saveReactiveState } = await import("./reactive-graph.js");
267
+ saveReactiveState(basePath, mid, sid, {
268
+ sliceId: sid,
269
+ completed: [...completed],
270
+ dispatched: selected,
271
+ graphSnapshot: metrics,
272
+ updatedAt: new Date().toISOString(),
273
+ });
274
+ // Encode selected task IDs in unitId for artifact verification.
275
+ // Format: M001/S01/reactive+T02,T03
276
+ const batchSuffix = selected.join(",");
277
+ return {
278
+ action: "dispatch",
279
+ unitType: "reactive-execute",
280
+ unitId: `${mid}/${sid}/reactive+${batchSuffix}`,
281
+ prompt: await buildReactiveExecutePrompt(mid, midTitle, sid, sTitle, selected, basePath),
282
+ };
283
+ }
284
+ catch (err) {
285
+ // Non-fatal — fall through to sequential execution
286
+ process.stderr.write(`gsd-reactive: graph derivation failed: ${err.message}\n`);
287
+ return null;
288
+ }
289
+ },
290
+ },
226
291
  {
227
292
  name: "executing → execute-task (recover missing task plan → plan-slice)",
228
293
  match: async ({ state, mid, midTitle, basePath }) => {
@@ -18,6 +18,13 @@ import { debugLog } from "./debug-logger.js";
18
18
  * generous headroom including retries and sidecar work.
19
19
  */
20
20
  const MAX_LOOP_ITERATIONS = 500;
21
+ /** Data-driven budget threshold notifications (75/80/90%). The 100% case is
22
+ * handled inline because it requires break/pause/stop control flow. */
23
+ const BUDGET_THRESHOLDS = [
24
+ { pct: 90, label: "Budget 90%", notifyLevel: "warning", cmuxLevel: "warning" },
25
+ { pct: 80, label: "Approaching budget ceiling — 80%", notifyLevel: "warning", cmuxLevel: "warning" },
26
+ { pct: 75, label: "Budget 75%", notifyLevel: "info", cmuxLevel: "progress" },
27
+ ];
21
28
  // ─── Session-scoped promise state ───────────────────────────────────────────
22
29
  //
23
30
  // pendingResolve and pendingAgentEndQueue live on AutoSession (not module-level)
@@ -57,9 +64,10 @@ export function resolveAgentEnd(event) {
57
64
  debugLog("resolveAgentEnd", {
58
65
  status: "queued",
59
66
  queueLength: s.pendingAgentEndQueue.length + 1,
67
+ unitId: s.currentUnit?.id,
60
68
  warning: "agent_end arrived between loop iterations — queued for next runUnit",
61
69
  });
62
- s.pendingAgentEndQueue.push(event);
70
+ s.pendingAgentEndQueue.push({ ...event, unitId: s.currentUnit?.id });
63
71
  }
64
72
  }
65
73
  export function isSessionSwitchInFlight() {
@@ -111,14 +119,35 @@ export async function runUnit(ctx, pi, s, unitType, unitId, prompt, _prefs) {
111
119
  ];
112
120
  }
113
121
  if (s.pendingAgentEndQueue.length > 0) {
114
- const queued = s.pendingAgentEndQueue.shift();
122
+ // Find an event matching this unit; discard stale events from other units
123
+ const matchIdx = s.pendingAgentEndQueue.findIndex((e) => !e.unitId || e.unitId === unitId);
124
+ if (matchIdx >= 0) {
125
+ // Discard any stale events before the match
126
+ if (matchIdx > 0) {
127
+ debugLog("runUnit", {
128
+ phase: "discarded-stale-events",
129
+ count: matchIdx,
130
+ unitType,
131
+ unitId,
132
+ });
133
+ }
134
+ const queued = s.pendingAgentEndQueue.splice(0, matchIdx + 1).pop();
135
+ debugLog("runUnit", {
136
+ phase: "drained-queued-event",
137
+ unitType,
138
+ unitId,
139
+ queueRemaining: s.pendingAgentEndQueue.length,
140
+ });
141
+ return { status: "completed", event: queued };
142
+ }
143
+ // No matching event — discard all stale events and proceed to new session
115
144
  debugLog("runUnit", {
116
- phase: "drained-queued-event",
145
+ phase: "discarded-all-stale-events",
146
+ count: s.pendingAgentEndQueue.length,
117
147
  unitType,
118
148
  unitId,
119
- queueRemaining: s.pendingAgentEndQueue.length,
120
149
  });
121
- return { status: "completed", event: queued };
150
+ s.pendingAgentEndQueue = [];
122
151
  }
123
152
  // ── Session creation with timeout ──
124
153
  debugLog("runUnit", { phase: "session-create", unitType, unitId });
@@ -373,7 +402,7 @@ export async function autoLoop(ctx, pi, s, deps) {
373
402
  await deps.closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt, deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id));
374
403
  }
375
404
  const incomplete = state.registry.filter((m) => m.status !== "complete" && m.status !== "parked");
376
- if (incomplete.length === 0) {
405
+ if (incomplete.length === 0 && state.registry.length > 0) {
377
406
  // All milestones complete — merge milestone branch before stopping
378
407
  if (s.currentMilestoneId) {
379
408
  deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
@@ -382,6 +411,12 @@ export async function autoLoop(ctx, pi, s, deps) {
382
411
  deps.logCmuxEvent(deps.loadEffectiveGSDPreferences()?.preferences, "All milestones complete.", "success");
383
412
  await deps.stopAuto(ctx, pi, "All milestones complete");
384
413
  }
414
+ else if (incomplete.length === 0 && state.registry.length === 0) {
415
+ // Empty registry — no milestones visible, likely a path resolution bug
416
+ const diag = `basePath=${s.basePath}, phase=${state.phase}`;
417
+ ctx.ui.notify(`No milestones visible in current scope. Possible path resolution issue.\n Diagnostic: ${diag}`, "error");
418
+ await deps.stopAuto(ctx, pi, `No milestones found — check basePath resolution`);
419
+ }
385
420
  else if (state.phase === "blocked") {
386
421
  const blockerMsg = `Blocked: ${state.blockers.join(", ")}`;
387
422
  await deps.stopAuto(ctx, pi, blockerMsg);
@@ -487,29 +522,20 @@ export async function autoLoop(ctx, pi, s, deps) {
487
522
  deps.sendDesktopNotification("GSD", msg, "warning", "budget");
488
523
  deps.logCmuxEvent(prefs, msg, "warning");
489
524
  }
490
- else if (newBudgetAlertLevel === 90) {
491
- s.lastBudgetAlertLevel =
492
- newBudgetAlertLevel;
493
- ctx.ui.notify(`Budget 90%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`, "warning");
494
- deps.sendDesktopNotification("GSD", `Budget 90%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`, "warning", "budget");
495
- deps.logCmuxEvent(prefs, `Budget 90%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`, "warning");
496
- }
497
- else if (newBudgetAlertLevel === 80) {
498
- s.lastBudgetAlertLevel =
499
- newBudgetAlertLevel;
500
- ctx.ui.notify(`Approaching budget ceiling — 80%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`, "warning");
501
- deps.sendDesktopNotification("GSD", `Approaching budget ceiling — 80%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`, "warning", "budget");
502
- deps.logCmuxEvent(prefs, `Budget 80%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`, "warning");
503
- }
504
- else if (newBudgetAlertLevel === 75) {
505
- s.lastBudgetAlertLevel =
506
- newBudgetAlertLevel;
507
- ctx.ui.notify(`Budget 75%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`, "info");
508
- deps.sendDesktopNotification("GSD", `Budget 75%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`, "info", "budget");
509
- deps.logCmuxEvent(prefs, `Budget 75%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`, "progress");
510
- }
511
- else if (budgetAlertLevel === 0) {
512
- s.lastBudgetAlertLevel = 0;
525
+ else {
526
+ // Data-driven 75/80/90% threshold notifications
527
+ const threshold = BUDGET_THRESHOLDS.find((t) => newBudgetAlertLevel === t.pct);
528
+ if (threshold) {
529
+ s.lastBudgetAlertLevel =
530
+ newBudgetAlertLevel;
531
+ const msg = `${threshold.label}: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`;
532
+ ctx.ui.notify(msg, threshold.notifyLevel);
533
+ deps.sendDesktopNotification("GSD", msg, threshold.notifyLevel, "budget");
534
+ deps.logCmuxEvent(prefs, msg, threshold.cmuxLevel);
535
+ }
536
+ else if (budgetAlertLevel === 0) {
537
+ s.lastBudgetAlertLevel = 0;
538
+ }
513
539
  }
514
540
  }
515
541
  else {
@@ -557,6 +583,7 @@ export async function autoLoop(ctx, pi, s, deps) {
557
583
  midTitle: midTitle,
558
584
  state,
559
585
  prefs,
586
+ session: s,
560
587
  });
561
588
  if (dispatchResult.action === "stop") {
562
589
  if (s.currentUnit) {
@@ -916,8 +943,11 @@ export async function autoLoop(ctx, pi, s, deps) {
916
943
  sidecarBroke = true;
917
944
  break;
918
945
  }
919
- // Run pre-verification for the sidecar unit
920
- const sidecarPreResult = await deps.postUnitPreVerification(postUnitCtx);
946
+ // Run pre-verification for the sidecar unit (lightweight path)
947
+ const sidecarPreOpts = item.kind === "hook"
948
+ ? { skipSettleDelay: true, skipDoctor: true, skipStateRebuild: true, skipWorktreeSync: true }
949
+ : { skipSettleDelay: true, skipStateRebuild: true };
950
+ const sidecarPreResult = await deps.postUnitPreVerification(postUnitCtx, sidecarPreOpts);
921
951
  if (sidecarPreResult === "dispatched") {
922
952
  // Pre-verification caused stop/pause
923
953
  debugLog("autoLoop", {