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
@@ -150,6 +150,12 @@ export class Editor implements Component, Focusable {
150
150
  private autocompletePrefix: string = "";
151
151
  private autocompleteMaxVisible: number = 5;
152
152
 
153
+ // Debounce for @ file autocomplete to prevent blocking the event loop
154
+ // with synchronous fuzzyFind calls on every keystroke
155
+ private autocompleteDebounceTimer: ReturnType<typeof setTimeout> | null = null;
156
+ private lastAutocompleteLookupPrefix: string | null = null;
157
+ private static readonly AUTOCOMPLETE_DEBOUNCE_MS = 150;
158
+
153
159
  // Paste tracking for large pastes
154
160
  private pastes: Map<number, string> = new Map();
155
161
  private pasteCounter: number = 0;
@@ -965,9 +971,10 @@ export class Editor implements Component, Focusable {
965
971
  if (this.isInSlashCommandContext(textBeforeCursor)) {
966
972
  this.tryTriggerAutocomplete();
967
973
  }
968
- // Check if we're in an @ file reference context
974
+ // Check if we're in an @ file reference context (debounce to avoid
975
+ // blocking the event loop with synchronous fuzzyFind on every keystroke)
969
976
  else if (textBeforeCursor.match(/(?:^|[\s])@[^\s]*$/)) {
970
- this.tryTriggerAutocomplete();
977
+ this.debouncedTriggerAutocomplete();
971
978
  }
972
979
  }
973
980
  } else {
@@ -975,6 +982,23 @@ export class Editor implements Component, Focusable {
975
982
  }
976
983
  }
977
984
 
985
+ /**
986
+ * Debounced version of tryTriggerAutocomplete for @ file reference context.
987
+ * Prevents synchronous fuzzyFind calls from blocking the event loop on every keystroke.
988
+ */
989
+ private debouncedTriggerAutocomplete(): void {
990
+ if (this.autocompleteDebounceTimer) {
991
+ clearTimeout(this.autocompleteDebounceTimer);
992
+ this.autocompleteDebounceTimer = null;
993
+ }
994
+
995
+ this.autocompleteDebounceTimer = setTimeout(() => {
996
+ this.autocompleteDebounceTimer = null;
997
+ this.tryTriggerAutocomplete();
998
+ this.tui.requestRender();
999
+ }, Editor.AUTOCOMPLETE_DEBOUNCE_MS);
1000
+ }
1001
+
978
1002
  private handlePaste(pastedText: string): void {
979
1003
  this.historyIndex = -1; // Exit history browsing mode
980
1004
  this.lastAction = null;
@@ -1133,9 +1157,9 @@ export class Editor implements Component, Focusable {
1133
1157
  if (this.isInSlashCommandContext(textBeforeCursor)) {
1134
1158
  this.tryTriggerAutocomplete();
1135
1159
  }
1136
- // @ file reference context
1160
+ // @ file reference context (debounced to avoid blocking event loop)
1137
1161
  else if (textBeforeCursor.match(/(?:^|[\s])@[^\s]*$/)) {
1138
- this.tryTriggerAutocomplete();
1162
+ this.debouncedTriggerAutocomplete();
1139
1163
  }
1140
1164
  }
1141
1165
  }
@@ -1440,9 +1464,9 @@ export class Editor implements Component, Focusable {
1440
1464
  if (this.isInSlashCommandContext(textBeforeCursor)) {
1441
1465
  this.tryTriggerAutocomplete();
1442
1466
  }
1443
- // @ file reference context
1467
+ // @ file reference context (debounced to avoid blocking event loop)
1444
1468
  else if (textBeforeCursor.match(/(?:^|[\s])@[^\s]*$/)) {
1445
- this.tryTriggerAutocomplete();
1469
+ this.debouncedTriggerAutocomplete();
1446
1470
  }
1447
1471
  }
1448
1472
  }
@@ -2020,6 +2044,15 @@ https://github.com/EsotericSoftware/spine-runtimes/actions/runs/19536643416/job/
2020
2044
  this.autocompleteState = null;
2021
2045
  this.autocompleteList = undefined;
2022
2046
  this.autocompletePrefix = "";
2047
+ this.clearAutocompleteDebounce();
2048
+ }
2049
+
2050
+ private clearAutocompleteDebounce(): void {
2051
+ if (this.autocompleteDebounceTimer) {
2052
+ clearTimeout(this.autocompleteDebounceTimer);
2053
+ this.autocompleteDebounceTimer = null;
2054
+ }
2055
+ this.lastAutocompleteLookupPrefix = null;
2023
2056
  }
2024
2057
 
2025
2058
  public isShowingAutocomplete(): boolean {
@@ -2034,6 +2067,38 @@ https://github.com/EsotericSoftware/spine-runtimes/actions/runs/19536643416/job/
2034
2067
  return;
2035
2068
  }
2036
2069
 
2070
+ // Check if we're in an @ file reference context — these trigger expensive
2071
+ // synchronous fuzzyFind calls that block the event loop. Debounce them so
2072
+ // rapid typing doesn't cascade into dozens of blocking searches.
2073
+ const currentLine = this.state.lines[this.state.cursorLine] || "";
2074
+ const textBeforeCursor = currentLine.slice(0, this.state.cursorCol);
2075
+ if (this.autocompletePrefix.startsWith("@") || textBeforeCursor.match(/(?:^|[\s])@[^\s]*$/)) {
2076
+ this.debouncedUpdateAutocompleteSuggestions();
2077
+ return;
2078
+ }
2079
+
2080
+ this.applyAutocompleteSuggestions();
2081
+ }
2082
+
2083
+ private debouncedUpdateAutocompleteSuggestions(): void {
2084
+ // Clear any pending debounce
2085
+ if (this.autocompleteDebounceTimer) {
2086
+ clearTimeout(this.autocompleteDebounceTimer);
2087
+ this.autocompleteDebounceTimer = null;
2088
+ }
2089
+
2090
+ this.autocompleteDebounceTimer = setTimeout(() => {
2091
+ this.autocompleteDebounceTimer = null;
2092
+ // Guard: autocomplete may have been cancelled during debounce wait
2093
+ if (!this.autocompleteState || !this.autocompleteProvider) return;
2094
+ this.applyAutocompleteSuggestions();
2095
+ this.tui.requestRender();
2096
+ }, Editor.AUTOCOMPLETE_DEBOUNCE_MS);
2097
+ }
2098
+
2099
+ private applyAutocompleteSuggestions(): void {
2100
+ if (!this.autocompleteProvider) return;
2101
+
2037
2102
  const suggestions = this.autocompleteProvider.getSuggestions(
2038
2103
  this.state.lines,
2039
2104
  this.state.cursorLine,
@@ -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