gsd-pi 2.11.0 → 2.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/dist/cli.js +18 -1
  2. package/dist/onboarding.js +3 -0
  3. package/dist/resource-loader.d.ts +2 -0
  4. package/dist/resource-loader.js +36 -1
  5. package/dist/resources/extensions/bg-shell/index.ts +51 -7
  6. package/dist/resources/extensions/gsd/auto-worktree.ts +509 -0
  7. package/dist/resources/extensions/gsd/auto.ts +381 -13
  8. package/dist/resources/extensions/gsd/commands.ts +9 -3
  9. package/dist/resources/extensions/gsd/doctor.ts +254 -3
  10. package/dist/resources/extensions/gsd/git-self-heal.ts +198 -0
  11. package/dist/resources/extensions/gsd/git-service.ts +11 -0
  12. package/dist/resources/extensions/gsd/guided-flow.ts +81 -9
  13. package/dist/resources/extensions/gsd/post-unit-hooks.ts +449 -0
  14. package/dist/resources/extensions/gsd/preferences.ts +209 -1
  15. package/dist/resources/extensions/gsd/prompt-loader.ts +28 -1
  16. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
  17. package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -3
  18. package/dist/resources/extensions/gsd/prompts/discuss.md +10 -8
  19. package/dist/resources/extensions/gsd/prompts/execute-task.md +4 -2
  20. package/dist/resources/extensions/gsd/prompts/guided-complete-slice.md +3 -1
  21. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +3 -1
  22. package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +3 -1
  23. package/dist/resources/extensions/gsd/prompts/guided-execute-task.md +3 -1
  24. package/dist/resources/extensions/gsd/prompts/guided-plan-milestone.md +4 -2
  25. package/dist/resources/extensions/gsd/prompts/guided-plan-slice.md +3 -1
  26. package/dist/resources/extensions/gsd/prompts/guided-research-slice.md +3 -1
  27. package/dist/resources/extensions/gsd/prompts/plan-milestone.md +9 -12
  28. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -3
  29. package/dist/resources/extensions/gsd/prompts/queue.md +3 -1
  30. package/dist/resources/extensions/gsd/prompts/research-milestone.md +1 -1
  31. package/dist/resources/extensions/gsd/prompts/research-slice.md +1 -1
  32. package/dist/resources/extensions/gsd/prompts/system.md +32 -29
  33. package/dist/resources/extensions/gsd/templates/context.md +1 -1
  34. package/dist/resources/extensions/gsd/templates/state.md +3 -3
  35. package/dist/resources/extensions/gsd/tests/auto-worktree-merge.test.ts +282 -0
  36. package/dist/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +259 -0
  37. package/dist/resources/extensions/gsd/tests/auto-worktree.test.ts +147 -0
  38. package/dist/resources/extensions/gsd/tests/doctor-git.test.ts +246 -0
  39. package/dist/resources/extensions/gsd/tests/doctor.test.ts +115 -1
  40. package/dist/resources/extensions/gsd/tests/git-self-heal.test.ts +234 -0
  41. package/dist/resources/extensions/gsd/tests/isolation-resolver.test.ts +107 -0
  42. package/dist/resources/extensions/gsd/tests/post-unit-hooks.test.ts +297 -0
  43. package/dist/resources/extensions/gsd/tests/preferences-git.test.ts +88 -0
  44. package/dist/resources/extensions/gsd/tests/preferences-hooks.test.ts +226 -0
  45. package/dist/resources/extensions/gsd/tests/regex-hardening.test.ts +12 -0
  46. package/dist/resources/extensions/gsd/tests/worktree-e2e.test.ts +315 -0
  47. package/dist/resources/extensions/gsd/types.ts +109 -0
  48. package/dist/resources/extensions/gsd/worktree-manager.ts +6 -4
  49. package/dist/resources/extensions/search-the-web/command-search-provider.ts +8 -4
  50. package/dist/resources/extensions/search-the-web/native-search.ts +15 -10
  51. package/dist/resources/extensions/search-the-web/provider.ts +19 -2
  52. package/dist/resources/extensions/search-the-web/tool-fetch-page.ts +62 -0
  53. package/dist/resources/extensions/search-the-web/tool-llm-context.ts +62 -3
  54. package/dist/resources/extensions/search-the-web/tool-search.ts +62 -3
  55. package/dist/wizard.js +1 -0
  56. package/package.json +1 -1
  57. package/packages/pi-agent-core/dist/agent-loop.d.ts.map +1 -1
  58. package/packages/pi-agent-core/dist/agent-loop.js +169 -55
  59. package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
  60. package/packages/pi-agent-core/dist/agent.d.ts +13 -1
  61. package/packages/pi-agent-core/dist/agent.d.ts.map +1 -1
  62. package/packages/pi-agent-core/dist/agent.js +16 -0
  63. package/packages/pi-agent-core/dist/agent.js.map +1 -1
  64. package/packages/pi-agent-core/dist/types.d.ts +91 -1
  65. package/packages/pi-agent-core/dist/types.d.ts.map +1 -1
  66. package/packages/pi-agent-core/dist/types.js.map +1 -1
  67. package/packages/pi-agent-core/src/agent-loop.ts +273 -63
  68. package/packages/pi-agent-core/src/agent.ts +24 -0
  69. package/packages/pi-agent-core/src/types.ts +98 -0
  70. package/packages/pi-ai/dist/env-api-keys.js +1 -0
  71. package/packages/pi-ai/dist/env-api-keys.js.map +1 -1
  72. package/packages/pi-ai/dist/models.generated.d.ts +314 -0
  73. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  74. package/packages/pi-ai/dist/models.generated.js +236 -0
  75. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  76. package/packages/pi-ai/dist/types.d.ts +1 -1
  77. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  78. package/packages/pi-ai/dist/types.js.map +1 -1
  79. package/packages/pi-ai/src/env-api-keys.ts +1 -0
  80. package/packages/pi-ai/src/models.generated.ts +236 -0
  81. package/packages/pi-ai/src/types.ts +2 -1
  82. package/packages/pi-coding-agent/dist/cli/args.d.ts.map +1 -1
  83. package/packages/pi-coding-agent/dist/cli/args.js +2 -1
  84. package/packages/pi-coding-agent/dist/cli/args.js.map +1 -1
  85. package/packages/pi-coding-agent/dist/core/agent-session.d.ts +10 -0
  86. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  87. package/packages/pi-coding-agent/dist/core/agent-session.js +69 -8
  88. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  89. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +4 -1
  90. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
  91. package/packages/pi-coding-agent/dist/core/extensions/runner.js +2 -1
  92. package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
  93. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +5 -0
  94. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  95. package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  96. package/packages/pi-coding-agent/dist/core/model-resolver.d.ts.map +1 -1
  97. package/packages/pi-coding-agent/dist/core/model-resolver.js +1 -0
  98. package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
  99. package/packages/pi-coding-agent/dist/core/sdk.js +3 -3
  100. package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
  101. package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
  102. package/packages/pi-coding-agent/dist/core/system-prompt.js +6 -0
  103. package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
  104. package/packages/pi-coding-agent/src/cli/args.ts +2 -1
  105. package/packages/pi-coding-agent/src/core/agent-session.ts +76 -7
  106. package/packages/pi-coding-agent/src/core/extensions/runner.ts +2 -1
  107. package/packages/pi-coding-agent/src/core/extensions/types.ts +2 -0
  108. package/packages/pi-coding-agent/src/core/model-resolver.ts +1 -0
  109. package/packages/pi-coding-agent/src/core/sdk.ts +3 -3
  110. package/packages/pi-coding-agent/src/core/system-prompt.ts +9 -0
  111. package/packages/pi-tui/dist/components/editor.d.ts +11 -0
  112. package/packages/pi-tui/dist/components/editor.d.ts.map +1 -1
  113. package/packages/pi-tui/dist/components/editor.js +64 -6
  114. package/packages/pi-tui/dist/components/editor.js.map +1 -1
  115. package/packages/pi-tui/src/components/editor.ts +71 -6
  116. package/src/resources/extensions/bg-shell/index.ts +51 -7
  117. package/src/resources/extensions/gsd/auto-worktree.ts +509 -0
  118. package/src/resources/extensions/gsd/auto.ts +381 -13
  119. package/src/resources/extensions/gsd/commands.ts +9 -3
  120. package/src/resources/extensions/gsd/doctor.ts +254 -3
  121. package/src/resources/extensions/gsd/git-self-heal.ts +198 -0
  122. package/src/resources/extensions/gsd/git-service.ts +11 -0
  123. package/src/resources/extensions/gsd/guided-flow.ts +81 -9
  124. package/src/resources/extensions/gsd/post-unit-hooks.ts +449 -0
  125. package/src/resources/extensions/gsd/preferences.ts +209 -1
  126. package/src/resources/extensions/gsd/prompt-loader.ts +28 -1
  127. package/src/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
  128. package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -3
  129. package/src/resources/extensions/gsd/prompts/discuss.md +10 -8
  130. package/src/resources/extensions/gsd/prompts/execute-task.md +4 -2
  131. package/src/resources/extensions/gsd/prompts/guided-complete-slice.md +3 -1
  132. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +3 -1
  133. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +3 -1
  134. package/src/resources/extensions/gsd/prompts/guided-execute-task.md +3 -1
  135. package/src/resources/extensions/gsd/prompts/guided-plan-milestone.md +4 -2
  136. package/src/resources/extensions/gsd/prompts/guided-plan-slice.md +3 -1
  137. package/src/resources/extensions/gsd/prompts/guided-research-slice.md +3 -1
  138. package/src/resources/extensions/gsd/prompts/plan-milestone.md +9 -12
  139. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -3
  140. package/src/resources/extensions/gsd/prompts/queue.md +3 -1
  141. package/src/resources/extensions/gsd/prompts/research-milestone.md +1 -1
  142. package/src/resources/extensions/gsd/prompts/research-slice.md +1 -1
  143. package/src/resources/extensions/gsd/prompts/system.md +32 -29
  144. package/src/resources/extensions/gsd/templates/context.md +1 -1
  145. package/src/resources/extensions/gsd/templates/state.md +3 -3
  146. package/src/resources/extensions/gsd/tests/auto-worktree-merge.test.ts +282 -0
  147. package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +259 -0
  148. package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +147 -0
  149. package/src/resources/extensions/gsd/tests/doctor-git.test.ts +246 -0
  150. package/src/resources/extensions/gsd/tests/doctor.test.ts +115 -1
  151. package/src/resources/extensions/gsd/tests/git-self-heal.test.ts +234 -0
  152. package/src/resources/extensions/gsd/tests/isolation-resolver.test.ts +107 -0
  153. package/src/resources/extensions/gsd/tests/post-unit-hooks.test.ts +297 -0
  154. package/src/resources/extensions/gsd/tests/preferences-git.test.ts +88 -0
  155. package/src/resources/extensions/gsd/tests/preferences-hooks.test.ts +226 -0
  156. package/src/resources/extensions/gsd/tests/regex-hardening.test.ts +12 -0
  157. package/src/resources/extensions/gsd/tests/worktree-e2e.test.ts +315 -0
  158. package/src/resources/extensions/gsd/types.ts +109 -0
  159. package/src/resources/extensions/gsd/worktree-manager.ts +6 -4
  160. package/src/resources/extensions/search-the-web/command-search-provider.ts +8 -4
  161. package/src/resources/extensions/search-the-web/native-search.ts +15 -10
  162. package/src/resources/extensions/search-the-web/provider.ts +19 -2
  163. package/src/resources/extensions/search-the-web/tool-fetch-page.ts +62 -0
  164. package/src/resources/extensions/search-the-web/tool-llm-context.ts +62 -3
  165. package/src/resources/extensions/search-the-web/tool-search.ts +62 -3
package/dist/cli.js CHANGED
@@ -2,12 +2,27 @@ import { AuthStorage, DefaultResourceLoader, ModelRegistry, SettingsManager, Ses
2
2
  import { existsSync, readdirSync, renameSync, readFileSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import { agentDir, sessionsDir, authFilePath } from './app-paths.js';
5
- import { initResources, buildResourceLoader } from './resource-loader.js';
5
+ import { initResources, buildResourceLoader, getNewerManagedResourceVersion } from './resource-loader.js';
6
6
  import { ensureManagedTools } from './tool-bootstrap.js';
7
7
  import { loadStoredEnvKeys } from './wizard.js';
8
8
  import { getPiDefaultModelAndProvider, migratePiCredentials } from './pi-migration.js';
9
9
  import { shouldRunOnboarding, runOnboarding } from './onboarding.js';
10
10
  import { checkForUpdates } from './update-check.js';
11
+ function exitIfManagedResourcesAreNewer(currentAgentDir) {
12
+ const currentVersion = process.env.GSD_VERSION || '0.0.0';
13
+ const managedVersion = getNewerManagedResourceVersion(currentAgentDir, currentVersion);
14
+ if (!managedVersion) {
15
+ return;
16
+ }
17
+ const yellow = '\x1b[33m';
18
+ const dim = '\x1b[2m';
19
+ const reset = '\x1b[0m';
20
+ const bold = '\x1b[1m';
21
+ process.stderr.write(`[gsd] ${yellow}Version mismatch detected${reset}\n` +
22
+ `[gsd] Synced resources are from ${bold}v${managedVersion}${reset}, but this \`gsd\` binary is ${dim}v${currentVersion}${reset}.\n` +
23
+ `[gsd] Run ${bold}npm install -g gsd-pi@latest${reset} or ${bold}gsd update${reset}, then try again.\n`);
24
+ process.exit(1);
25
+ }
11
26
  function parseCliArgs(argv) {
12
27
  const flags = { extensions: [], messages: [] };
13
28
  const args = argv.slice(2); // skip node + script
@@ -199,6 +214,7 @@ if (isPrintMode) {
199
214
  appendSystemPrompt = cliFlags.appendSystemPrompt;
200
215
  }
201
216
  }
217
+ exitIfManagedResourcesAreNewer(agentDir);
202
218
  initResources(agentDir);
203
219
  const resourceLoader = new DefaultResourceLoader({
204
220
  agentDir,
@@ -272,6 +288,7 @@ if (existsSync(sessionsDir)) {
272
288
  const sessionManager = cliFlags.continue
273
289
  ? SessionManager.continueRecent(cwd, projectSessionsDir)
274
290
  : SessionManager.create(cwd, projectSessionsDir);
291
+ exitIfManagedResourcesAreNewer(agentDir);
275
292
  initResources(agentDir);
276
293
  const resourceLoader = buildResourceLoader(agentDir);
277
294
  await resourceLoader.reload();
@@ -48,6 +48,7 @@ const LLM_PROVIDER_IDS = [
48
48
  'xai',
49
49
  'openrouter',
50
50
  'mistral',
51
+ 'ollama-cloud',
51
52
  'custom-openai',
52
53
  ];
53
54
  /** API key prefix validation — loose checks to catch obvious mistakes */
@@ -61,6 +62,7 @@ const OTHER_PROVIDERS = [
61
62
  { value: 'xai', label: 'xAI (Grok)' },
62
63
  { value: 'openrouter', label: 'OpenRouter' },
63
64
  { value: 'mistral', label: 'Mistral' },
65
+ { value: 'ollama-cloud', label: 'Ollama Cloud' },
64
66
  { value: 'custom-openai', label: 'Custom (OpenAI-compatible)' },
65
67
  ];
66
68
  // ─── Dynamic imports ──────────────────────────────────────────────────────────
@@ -755,6 +757,7 @@ export function loadStoredEnvKeys(authStorage) {
755
757
  ['slack_bot', 'SLACK_BOT_TOKEN'],
756
758
  ['discord_bot', 'DISCORD_BOT_TOKEN'],
757
759
  ['groq', 'GROQ_API_KEY'],
760
+ ['ollama-cloud', 'OLLAMA_API_KEY'],
758
761
  ['custom-openai', 'CUSTOM_OPENAI_API_KEY'],
759
762
  ];
760
763
  for (const [provider, envVar] of providers) {
@@ -1,5 +1,7 @@
1
1
  import { DefaultResourceLoader } from '@gsd/pi-coding-agent';
2
2
  export declare function discoverExtensionEntryPaths(extensionsDir: string): string[];
3
+ export declare function readManagedResourceVersion(agentDir: string): string | null;
4
+ export declare function getNewerManagedResourceVersion(agentDir: string, currentVersion: string): string | null;
3
5
  /**
4
6
  * Syncs all bundled resources to agentDir (~/.gsd/agent/) on every launch.
5
7
  *
@@ -1,8 +1,9 @@
1
1
  import { DefaultResourceLoader } from '@gsd/pi-coding-agent';
2
2
  import { homedir } from 'node:os';
3
- import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync } from 'node:fs';
3
+ import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
4
4
  import { dirname, join, relative, resolve } from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
+ import { compareSemver } from './update-check.js';
6
7
  // Resolve resources directory — prefer dist/resources/ (stable, set at build time)
7
8
  // over src/resources/ (live working tree, changes with git branch).
8
9
  //
@@ -16,6 +17,7 @@ const distResources = join(packageRoot, 'dist', 'resources');
16
17
  const srcResources = join(packageRoot, 'src', 'resources');
17
18
  const resourcesDir = existsSync(distResources) ? distResources : srcResources;
18
19
  const bundledExtensionsDir = join(resourcesDir, 'extensions');
20
+ const resourceVersionManifestName = 'managed-resources.json';
19
21
  function isExtensionFile(name) {
20
22
  return name.endsWith('.ts') || name.endsWith('.js');
21
23
  }
@@ -70,6 +72,38 @@ function getExtensionKey(entryPath, extensionsDir) {
70
72
  const relPath = relative(extensionsDir, entryPath);
71
73
  return relPath.split(/[\\/]/)[0];
72
74
  }
75
+ function getManagedResourceManifestPath(agentDir) {
76
+ return join(agentDir, resourceVersionManifestName);
77
+ }
78
+ function getBundledGsdVersion() {
79
+ try {
80
+ const pkg = JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf-8'));
81
+ return typeof pkg?.version === 'string' ? pkg.version : '0.0.0';
82
+ }
83
+ catch {
84
+ return process.env.GSD_VERSION || '0.0.0';
85
+ }
86
+ }
87
+ function writeManagedResourceManifest(agentDir) {
88
+ const manifest = { gsdVersion: getBundledGsdVersion() };
89
+ writeFileSync(getManagedResourceManifestPath(agentDir), JSON.stringify(manifest));
90
+ }
91
+ export function readManagedResourceVersion(agentDir) {
92
+ try {
93
+ const manifest = JSON.parse(readFileSync(getManagedResourceManifestPath(agentDir), 'utf-8'));
94
+ return typeof manifest?.gsdVersion === 'string' ? manifest.gsdVersion : null;
95
+ }
96
+ catch {
97
+ return null;
98
+ }
99
+ }
100
+ export function getNewerManagedResourceVersion(agentDir, currentVersion) {
101
+ const managedVersion = readManagedResourceVersion(agentDir);
102
+ if (!managedVersion) {
103
+ return null;
104
+ }
105
+ return compareSemver(managedVersion, currentVersion) > 0 ? managedVersion : null;
106
+ }
73
107
  /**
74
108
  * Syncs all bundled resources to agentDir (~/.gsd/agent/) on every launch.
75
109
  *
@@ -101,6 +135,7 @@ export function initResources(agentDir) {
101
135
  if (existsSync(srcSkills)) {
102
136
  cpSync(srcSkills, destSkills, { recursive: true, force: true });
103
137
  }
138
+ writeManagedResourceManifest(agentDir);
104
139
  }
105
140
  /**
106
141
  * Constructs a DefaultResourceLoader that loads extensions from both
@@ -574,6 +574,7 @@ interface StartOptions {
574
574
  type?: ProcessType;
575
575
  readyPattern?: string;
576
576
  readyPort?: number;
577
+ readyTimeout?: number;
577
578
  group?: string;
578
579
  env?: Record<string, string>;
579
580
  }
@@ -689,7 +690,7 @@ function startProcess(opts: StartOptions): BgProcess {
689
690
 
690
691
  // Port probing for server-type processes
691
692
  if (bg.readyPort) {
692
- startPortProbing(bg, bg.readyPort);
693
+ startPortProbing(bg, bg.readyPort, opts.readyTimeout);
693
694
  }
694
695
 
695
696
  // Shell sessions are ready immediately after spawn
@@ -707,9 +708,17 @@ function startProcess(opts: StartOptions): BgProcess {
707
708
 
708
709
  // ── Port Probing Loop ──────────────────────────────────────────────────────
709
710
 
710
- function startPortProbing(bg: BgProcess, port: number): void {
711
+ function startPortProbing(bg: BgProcess, port: number, customTimeout?: number): void {
712
+ const timeout = customTimeout || DEFAULT_READY_TIMEOUT;
711
713
  const interval = setInterval(async () => {
712
- if (!bg.alive || bg.status !== "starting") {
714
+ if (!bg.alive) {
715
+ clearInterval(interval);
716
+ const stderrLines = bg.output.filter(l => l.stream === "stderr").slice(-10).map(l => l.line);
717
+ const detail = `Process exited (code ${bg.exitCode}) before port ${port} opened${stderrLines.length > 0 ? ` — ${stderrLines.join("; ").slice(0, 200)}` : ""}`;
718
+ addEvent(bg, { type: "port_timeout", detail, data: { port, exitCode: bg.exitCode } });
719
+ return;
720
+ }
721
+ if (bg.status !== "starting") {
713
722
  clearInterval(interval);
714
723
  return;
715
724
  }
@@ -722,8 +731,18 @@ function startPortProbing(bg: BgProcess, port: number): void {
722
731
  }
723
732
  }, READY_POLL_INTERVAL);
724
733
 
725
- // Stop probing after timeout
726
- setTimeout(() => clearInterval(interval), DEFAULT_READY_TIMEOUT);
734
+ // Stop probing after timeout — transition to error state so the process
735
+ // doesn't stay in "starting" forever (fixes #428)
736
+ setTimeout(() => {
737
+ clearInterval(interval);
738
+ if (bg.alive && bg.status === "starting") {
739
+ const stderrLines = bg.output.filter(l => l.stream === "stderr").slice(-10).map(l => l.line);
740
+ const detail = `Port ${port} not open after ${timeout}ms${stderrLines.length > 0 ? ` — ${stderrLines.join("; ").slice(0, 200)}` : ""}`;
741
+ bg.status = "error";
742
+ addEvent(bg, { type: "port_timeout", detail, data: { port, timeout } });
743
+ pushAlert(bg, `Port ${port} readiness timeout after ${timeout / 1000}s`);
744
+ }
745
+ }, timeout);
727
746
  }
728
747
 
729
748
  // ── Process Kill ───────────────────────────────────────────────────────────
@@ -864,9 +883,19 @@ async function waitForReady(bg: BgProcess, timeout: number, signal?: AbortSignal
864
883
  return { ready: false, detail: "Cancelled" };
865
884
  }
866
885
  if (!bg.alive) {
886
+ const stderrLines = bg.output.filter(l => l.stream === "stderr").slice(-5).map(l => l.line);
887
+ const stderrContext = stderrLines.length > 0 ? `\nstderr:\n${stderrLines.join("\n").slice(0, 500)}` : "";
888
+ return {
889
+ ready: false,
890
+ detail: `Process exited before becoming ready (code ${bg.exitCode})${bg.recentErrors.length > 0 ? ` — ${bg.recentErrors.slice(-1)[0]}` : ""}${stderrContext}`,
891
+ };
892
+ }
893
+ if (bg.status === "error") {
894
+ const stderrLines = bg.output.filter(l => l.stream === "stderr").slice(-5).map(l => l.line);
895
+ const stderrContext = stderrLines.length > 0 ? `\nstderr:\n${stderrLines.join("\n").slice(0, 500)}` : "";
867
896
  return {
868
897
  ready: false,
869
- detail: `Process exited before becoming ready (code ${bg.exitCode})${bg.recentErrors.length > 0 ? ` ${bg.recentErrors.slice(-1)[0]}` : ""}`,
898
+ detail: `Process entered error state${bg.readyPort ? ` (port ${bg.readyPort} never opened)` : ""}${stderrContext}`,
870
899
  };
871
900
  }
872
901
  if (bg.status === "ready") {
@@ -887,7 +916,9 @@ async function waitForReady(bg: BgProcess, timeout: number, signal?: AbortSignal
887
916
  }
888
917
  }
889
918
 
890
- return { ready: false, detail: `Timed out after ${timeout}ms waiting for ready signal` };
919
+ const stderrLines = bg.output.filter(l => l.stream === "stderr").slice(-5).map(l => l.line);
920
+ const stderrContext = stderrLines.length > 0 ? `\nstderr:\n${stderrLines.join("\n").slice(0, 500)}` : "";
921
+ return { ready: false, detail: `Timed out after ${timeout}ms waiting for ready signal${stderrContext}` };
891
922
  }
892
923
 
893
924
  // ── Query Shell Environment ────────────────────────────────────────────────
@@ -1234,6 +1265,15 @@ export default function (pi: ExtensionAPI) {
1234
1265
  cleanupAll();
1235
1266
  });
1236
1267
 
1268
+ // Register signal handlers to clean up bg processes on unexpected exit (fixes #428)
1269
+ // This prevents orphan processes and helps the parent restore terminal state
1270
+ const signalCleanup = () => {
1271
+ cleanupAll();
1272
+ };
1273
+ process.on("SIGTERM", signalCleanup);
1274
+ process.on("SIGINT", signalCleanup);
1275
+ process.on("beforeExit", signalCleanup);
1276
+
1237
1277
  // ── Compaction Awareness: Survive Context Resets ───────────────────
1238
1278
 
1239
1279
  /** Build a compact state summary of all alive processes for context re-injection */
@@ -1424,6 +1464,9 @@ export default function (pi: ExtensionAPI) {
1424
1464
  ready_port: Type.Optional(
1425
1465
  Type.Number({ description: "Port to probe for readiness (for start). When open, process is considered ready." }),
1426
1466
  ),
1467
+ ready_timeout: Type.Optional(
1468
+ Type.Number({ description: "Max milliseconds to wait for ready_port/ready_pattern before marking as error (default: 30000)" }),
1469
+ ),
1427
1470
  group: Type.Optional(
1428
1471
  Type.String({ description: "Group name for related processes (for start, group_status)" }),
1429
1472
  ),
@@ -1449,6 +1492,7 @@ export default function (pi: ExtensionAPI) {
1449
1492
  type: params.type as ProcessType | undefined,
1450
1493
  readyPattern: params.ready_pattern,
1451
1494
  readyPort: params.ready_port,
1495
+ readyTimeout: params.ready_timeout,
1452
1496
  group: params.group,
1453
1497
  });
1454
1498