@opengsd/gsd-pi 1.3.0-dev.65546769 → 1.3.0-dev.eed73bea

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 (183) hide show
  1. package/dist/resources/.managed-resources-content-hash +1 -1
  2. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +11 -2
  3. package/dist/resources/extensions/google-cli/stream-adapter.js +82 -15
  4. package/dist/resources/extensions/gsd/auto/orchestrator.js +12 -3
  5. package/dist/resources/extensions/gsd/auto-dispatch.js +17 -14
  6. package/dist/resources/extensions/gsd/auto-prompts.js +43 -12
  7. package/dist/resources/extensions/gsd/auto-recovery.js +13 -6
  8. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +103 -13
  9. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +6 -1
  10. package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +2 -0
  11. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +8 -3
  12. package/dist/resources/extensions/gsd/bootstrap/system-context.js +46 -19
  13. package/dist/resources/extensions/gsd/bootstrap/tool-call-loop-guard.js +75 -1
  14. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +1 -1
  15. package/dist/resources/extensions/gsd/commands-context.js +19 -1
  16. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +16 -10
  17. package/dist/resources/extensions/gsd/commands-worktree.js +12 -10
  18. package/dist/resources/extensions/gsd/dashboard-overlay.js +32 -3
  19. package/dist/resources/extensions/gsd/db/queries.js +60 -0
  20. package/dist/resources/extensions/gsd/doctor-providers.js +92 -8
  21. package/dist/resources/extensions/gsd/exec-sandbox.js +45 -9
  22. package/dist/resources/extensions/gsd/forensics.js +2 -32
  23. package/dist/resources/extensions/gsd/git-service.js +4 -4
  24. package/dist/resources/extensions/gsd/guided-flow-queue.js +59 -5
  25. package/dist/resources/extensions/gsd/health-widget.js +55 -29
  26. package/dist/resources/extensions/gsd/markdown-renderer.js +6 -2
  27. package/dist/resources/extensions/gsd/memory-consolidation-scanner.js +44 -21
  28. package/dist/resources/extensions/gsd/milestone-implementation-evidence.js +26 -20
  29. package/dist/resources/extensions/gsd/quick.js +45 -2
  30. package/dist/resources/extensions/gsd/session-forensics.js +11 -1
  31. package/dist/resources/extensions/gsd/state-reconciliation/drift/stale-render.js +52 -3
  32. package/dist/resources/extensions/gsd/tools/complete-slice.js +34 -3
  33. package/dist/resources/extensions/gsd/tools/complete-task.js +78 -16
  34. package/dist/resources/extensions/gsd/tools/exec-tool.js +7 -2
  35. package/dist/resources/extensions/gsd/unit-context-composer.js +23 -7
  36. package/dist/resources/extensions/gsd/unit-registry.js +25 -3
  37. package/dist/resources/extensions/gsd/unmerged-milestone-guard.js +33 -3
  38. package/dist/resources/extensions/gsd/validation-block-guard.js +9 -4
  39. package/dist/resources/extensions/gsd/workspace-git-preflight.js +30 -1
  40. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  41. package/dist/web/standalone/.next/BUILD_ID +1 -1
  42. package/dist/web/standalone/.next/app-path-routes-manifest.json +14 -14
  43. package/dist/web/standalone/.next/build-manifest.json +3 -3
  44. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  45. package/dist/web/standalone/.next/react-loadable-manifest.json +1 -1
  46. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  47. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  55. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
  63. package/dist/web/standalone/.next/server/app/index.html +1 -1
  64. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app-paths-manifest.json +14 -14
  71. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  72. package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
  73. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  74. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  75. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  76. package/dist/web/standalone/.next/static/chunks/{796.e0bdc932325d7e03.js → 796.3976108148518f7d.js} +3 -3
  77. package/dist/web/standalone/.next/static/chunks/{webpack-f46ea08200a0227e.js → webpack-7c1d97e39be2da11.js} +1 -1
  78. package/package.json +1 -1
  79. package/packages/cloud-mcp-gateway/package.json +2 -2
  80. package/packages/contracts/dist/workflow.d.ts +1 -0
  81. package/packages/contracts/dist/workflow.d.ts.map +1 -1
  82. package/packages/contracts/dist/workflow.js +2 -0
  83. package/packages/contracts/dist/workflow.js.map +1 -1
  84. package/packages/contracts/package.json +1 -1
  85. package/packages/daemon/package.json +4 -4
  86. package/packages/gsd-agent-core/package.json +5 -5
  87. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  88. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.js +21 -9
  89. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  90. package/packages/gsd-agent-modes/package.json +7 -7
  91. package/packages/mcp-server/README.md +1 -1
  92. package/packages/mcp-server/dist/server.d.ts +1 -1
  93. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  94. package/packages/mcp-server/dist/server.js +3 -3
  95. package/packages/mcp-server/dist/server.js.map +1 -1
  96. package/packages/mcp-server/dist/workflow-tools.d.ts +13 -1
  97. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  98. package/packages/mcp-server/dist/workflow-tools.js +34 -20
  99. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  100. package/packages/mcp-server/package.json +4 -4
  101. package/packages/native/package.json +1 -1
  102. package/packages/pi-agent-core/package.json +1 -1
  103. package/packages/pi-ai/package.json +1 -1
  104. package/packages/pi-coding-agent/package.json +7 -7
  105. package/packages/pi-tui/package.json +2 -2
  106. package/packages/rpc-client/package.json +2 -2
  107. package/pkg/package.json +1 -1
  108. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +20 -2
  109. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +80 -0
  110. package/src/resources/extensions/google-cli/stream-adapter.ts +106 -19
  111. package/src/resources/extensions/gsd/auto/orchestrator.ts +25 -11
  112. package/src/resources/extensions/gsd/auto-dispatch.ts +18 -17
  113. package/src/resources/extensions/gsd/auto-prompts.ts +54 -12
  114. package/src/resources/extensions/gsd/auto-recovery.ts +13 -6
  115. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +125 -12
  116. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +6 -1
  117. package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +2 -0
  118. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +9 -3
  119. package/src/resources/extensions/gsd/bootstrap/system-context.ts +52 -18
  120. package/src/resources/extensions/gsd/bootstrap/tool-call-loop-guard.ts +82 -1
  121. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +1 -1
  122. package/src/resources/extensions/gsd/commands-context.ts +18 -1
  123. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +14 -9
  124. package/src/resources/extensions/gsd/commands-worktree.ts +12 -10
  125. package/src/resources/extensions/gsd/dashboard-overlay.ts +32 -3
  126. package/src/resources/extensions/gsd/db/queries.ts +79 -0
  127. package/src/resources/extensions/gsd/doctor-providers.ts +103 -9
  128. package/src/resources/extensions/gsd/exec-sandbox.ts +49 -9
  129. package/src/resources/extensions/gsd/forensics.ts +2 -33
  130. package/src/resources/extensions/gsd/git-service.ts +5 -5
  131. package/src/resources/extensions/gsd/guided-flow-queue.ts +82 -4
  132. package/src/resources/extensions/gsd/health-widget.ts +69 -32
  133. package/src/resources/extensions/gsd/markdown-renderer.ts +6 -1
  134. package/src/resources/extensions/gsd/memory-consolidation-scanner.ts +51 -19
  135. package/src/resources/extensions/gsd/milestone-implementation-evidence.ts +35 -21
  136. package/src/resources/extensions/gsd/quick.ts +43 -2
  137. package/src/resources/extensions/gsd/session-forensics.ts +11 -1
  138. package/src/resources/extensions/gsd/state-reconciliation/drift/stale-render.ts +76 -8
  139. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +111 -1
  140. package/src/resources/extensions/gsd/tests/commands-context.test.ts +26 -0
  141. package/src/resources/extensions/gsd/tests/commands-worktree-clean.test.ts +80 -0
  142. package/src/resources/extensions/gsd/tests/complete-slice.test.ts +11 -0
  143. package/src/resources/extensions/gsd/tests/complete-task-rollback-evidence.test.ts +48 -8
  144. package/src/resources/extensions/gsd/tests/complete-task.test.ts +75 -0
  145. package/src/resources/extensions/gsd/tests/dashboard-overlay.test.ts +55 -2
  146. package/src/resources/extensions/gsd/tests/dispatch-rule-coverage.test.ts +26 -1
  147. package/src/resources/extensions/gsd/tests/doctor-forensics-db-open-regression.test.ts +70 -2
  148. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +107 -0
  149. package/src/resources/extensions/gsd/tests/exec-graceful-kill.test.ts +38 -0
  150. package/src/resources/extensions/gsd/tests/exec-tool.test.ts +45 -1
  151. package/src/resources/extensions/gsd/tests/forensics-error-filter.test.ts +88 -0
  152. package/src/resources/extensions/gsd/tests/guided-discuss-milestone-prompt-rendering.test.ts +42 -0
  153. package/src/resources/extensions/gsd/tests/health-widget.test.ts +268 -3
  154. package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +119 -1
  155. package/src/resources/extensions/gsd/tests/integration/queue-active-milestone-context-budget.test.ts +93 -0
  156. package/src/resources/extensions/gsd/tests/integration/quick-branch-lifecycle.test.ts +56 -9
  157. package/src/resources/extensions/gsd/tests/knowledge-cold-start.test.ts +14 -0
  158. package/src/resources/extensions/gsd/tests/memory-consolidation-scanner.test.ts +78 -0
  159. package/src/resources/extensions/gsd/tests/orchestrator-logs.test.ts +43 -1
  160. package/src/resources/extensions/gsd/tests/parallel-research-dispatch.test.ts +26 -0
  161. package/src/resources/extensions/gsd/tests/pipeline-variant-dispatch.test.ts +1 -1
  162. package/src/resources/extensions/gsd/tests/prefs-wizard-coverage.test.ts +54 -1
  163. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +195 -1
  164. package/src/resources/extensions/gsd/tests/read-uat-gate-verdict.test.ts +185 -0
  165. package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +87 -0
  166. package/src/resources/extensions/gsd/tests/state-reconciliation-drift.test.ts +76 -0
  167. package/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +68 -0
  168. package/src/resources/extensions/gsd/tests/tool-param-optionality.test.ts +26 -0
  169. package/src/resources/extensions/gsd/tests/unit-context-composer.test.ts +193 -14
  170. package/src/resources/extensions/gsd/tests/unmerged-milestone-guard.test.ts +25 -0
  171. package/src/resources/extensions/gsd/tests/validation-block-guard.test.ts +79 -0
  172. package/src/resources/extensions/gsd/tests/verify-artifact-tightened.test.ts +66 -0
  173. package/src/resources/extensions/gsd/tests/workspace-git-preflight.test.ts +151 -2
  174. package/src/resources/extensions/gsd/tools/complete-slice.ts +30 -3
  175. package/src/resources/extensions/gsd/tools/complete-task.ts +86 -16
  176. package/src/resources/extensions/gsd/tools/exec-tool.ts +7 -3
  177. package/src/resources/extensions/gsd/unit-context-composer.ts +33 -7
  178. package/src/resources/extensions/gsd/unit-registry.ts +25 -3
  179. package/src/resources/extensions/gsd/unmerged-milestone-guard.ts +41 -5
  180. package/src/resources/extensions/gsd/validation-block-guard.ts +13 -7
  181. package/src/resources/extensions/gsd/workspace-git-preflight.ts +31 -0
  182. /package/dist/web/standalone/.next/static/{BTKtGFF1Y-hvVJEGhBRo9 → SzEuqWX37DR9MEpEuQjP1}/_buildManifest.js +0 -0
  183. /package/dist/web/standalone/.next/static/{BTKtGFF1Y-hvVJEGhBRo9 → SzEuqWX37DR9MEpEuQjP1}/_ssgManifest.js +0 -0
@@ -11,6 +11,7 @@
11
11
  * - Optional search/tool integrations (Brave, Tavily, Jina, Context7)
12
12
  */
13
13
  import { existsSync, readFileSync } from "node:fs";
14
+ import { access, readFile } from "node:fs/promises";
14
15
  import { delimiter, join } from "node:path";
15
16
  import { AuthStorage } from "@gsd/pi-coding-agent";
16
17
  import { getEnvApiKey } from "@gsd/pi-ai";
@@ -133,15 +134,11 @@ const CLI_AUTH_PATH_CHECK_PROVIDERS = new Set([
133
134
  "google-gemini-cli",
134
135
  "google-antigravity",
135
136
  ]);
136
- /**
137
- * Check if a CLI provider's binary exists anywhere in PATH.
138
- * Fast filesystem scan — no subprocess, no network, sub-1ms.
139
- */
140
- function isCliBinaryInPath(providerId) {
137
+ let asyncCliBinaryPathCache = null;
138
+ function cliExecutableNames(providerId) {
141
139
  const binaries = CLI_BINARY_MAP[providerId];
142
140
  if (!binaries)
143
- return false;
144
- const pathDirs = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
141
+ return [];
145
142
  // On Windows, command shims are commonly installed as .cmd/.exe/.bat/.com.
146
143
  // Scan PATHEXT candidates in addition to the bare binary name.
147
144
  const executableNames = [...binaries];
@@ -160,8 +157,45 @@ function isCliBinaryInPath(providerId) {
160
157
  }
161
158
  }
162
159
  }
160
+ return executableNames;
161
+ }
162
+ /**
163
+ * Check if a CLI provider's binary exists anywhere in PATH.
164
+ * Fast filesystem scan — no subprocess, no network, sub-1ms.
165
+ */
166
+ function isCliBinaryInPath(providerId) {
167
+ const cached = asyncCliBinaryPathCache?.get(providerId);
168
+ if (cached !== undefined)
169
+ return cached;
170
+ const executableNames = cliExecutableNames(providerId);
171
+ if (executableNames.length === 0)
172
+ return false;
173
+ const pathDirs = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
163
174
  return pathDirs.some(dir => executableNames.some(name => existsSync(join(dir, name))));
164
175
  }
176
+ async function isCliBinaryInPathAsync(providerId) {
177
+ const executableNames = cliExecutableNames(providerId);
178
+ if (executableNames.length === 0)
179
+ return false;
180
+ const pathDirs = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
181
+ const candidates = pathDirs.flatMap(dir => executableNames.map(name => join(dir, name)));
182
+ if (candidates.length === 0)
183
+ return false;
184
+ try {
185
+ await Promise.any(candidates.map(candidate => access(candidate)));
186
+ return true;
187
+ }
188
+ catch {
189
+ return false;
190
+ }
191
+ }
192
+ async function loadCliBinaryPathCache() {
193
+ const entries = await Promise.all(Object.keys(CLI_BINARY_MAP).map(async (providerId) => [
194
+ providerId,
195
+ await isCliBinaryInPathAsync(providerId),
196
+ ]));
197
+ return new Map(entries);
198
+ }
165
199
  function modelsJsonPaths() {
166
200
  const home = homedir();
167
201
  return [
@@ -170,7 +204,11 @@ function modelsJsonPaths() {
170
204
  join(home, ".pi", "agent", "models.json"),
171
205
  ];
172
206
  }
207
+ let asyncModelsJsonApiKeyCache = null;
173
208
  function hasModelsJsonApiKey(providerId) {
209
+ if (asyncModelsJsonApiKeyCache) {
210
+ return asyncModelsJsonApiKeyCache.has(providerId);
211
+ }
174
212
  for (const path of modelsJsonPaths()) {
175
213
  if (!existsSync(path))
176
214
  continue;
@@ -187,7 +225,25 @@ function hasModelsJsonApiKey(providerId) {
187
225
  }
188
226
  return false;
189
227
  }
190
- function resolveKey(providerId) {
228
+ async function loadModelsJsonApiKeyCache() {
229
+ const providersWithKeys = new Set();
230
+ for (const path of modelsJsonPaths()) {
231
+ try {
232
+ const parsed = JSON.parse(await readFile(path, "utf-8"));
233
+ for (const [providerId, provider] of Object.entries(parsed.providers ?? {})) {
234
+ const apiKey = provider.apiKey;
235
+ if (typeof apiKey === "string" && apiKey.trim().length > 0) {
236
+ providersWithKeys.add(providerId);
237
+ }
238
+ }
239
+ }
240
+ catch {
241
+ // Missing or malformed models.json should not break dashboard health checks.
242
+ }
243
+ }
244
+ return providersWithKeys;
245
+ }
246
+ function resolveKeyFromAuthOrEnv(providerId) {
191
247
  const info = PROVIDER_REGISTRY.find(p => p.id === providerId);
192
248
  if (providerId === "anthropic-vertex" && process.env.ANTHROPIC_VERTEX_PROJECT_ID) {
193
249
  return { found: true, source: "env", backedOff: false };
@@ -234,6 +290,12 @@ function resolveKey(providerId) {
234
290
  if (info?.envVar && process.env[info.envVar]) {
235
291
  return { found: true, source: "env", backedOff: false };
236
292
  }
293
+ return null;
294
+ }
295
+ function resolveKey(providerId) {
296
+ const direct = resolveKeyFromAuthOrEnv(providerId);
297
+ if (direct)
298
+ return direct;
237
299
  if (hasModelsJsonApiKey(providerId)) {
238
300
  return { found: true, source: "models.json", backedOff: false };
239
301
  }
@@ -432,6 +494,28 @@ export function runProviderChecks() {
432
494
  results.push(...checkOptionalProviders());
433
495
  return results;
434
496
  }
497
+ /**
498
+ * Non-blocking equivalent of `runProviderChecks` for the health-widget
499
+ * background refresh. PATH checks and custom models.json discovery use async
500
+ * filesystem APIs so periodic widget refreshes do not stall the input loop.
501
+ */
502
+ export async function runProviderChecksAsync() {
503
+ const [cliCache, modelsJsonCache] = await Promise.all([
504
+ loadCliBinaryPathCache(),
505
+ loadModelsJsonApiKeyCache(),
506
+ ]);
507
+ const previousCliCache = asyncCliBinaryPathCache;
508
+ const previousModelsJsonCache = asyncModelsJsonApiKeyCache;
509
+ asyncCliBinaryPathCache = cliCache;
510
+ asyncModelsJsonApiKeyCache = modelsJsonCache;
511
+ try {
512
+ return runProviderChecks();
513
+ }
514
+ finally {
515
+ asyncCliBinaryPathCache = previousCliCache;
516
+ asyncModelsJsonApiKeyCache = previousModelsJsonCache;
517
+ }
518
+ }
435
519
  /**
436
520
  * Format provider check results as a human-readable report string.
437
521
  */
@@ -180,10 +180,22 @@ export function runExecSandbox(request, opts) {
180
180
  const effectiveGraceMs = opts.kill_grace_ms ?? SIGKILL_GRACE_MS;
181
181
  const effectiveForceResolveDelay = opts.force_resolve_delay_ms ?? (effectiveGraceMs + HARD_DEADLINE_MS);
182
182
  let timedOut = false;
183
+ let aborted = false;
183
184
  let settled = false;
185
+ let killInitiated = false;
186
+ let timer;
184
187
  let forceResolveTimer;
185
- const timer = setTimeout(() => {
186
- timedOut = true;
188
+ let abortListener;
189
+ const removeAbortListener = () => {
190
+ if (opts.signal && abortListener) {
191
+ opts.signal.removeEventListener("abort", abortListener);
192
+ abortListener = undefined;
193
+ }
194
+ };
195
+ const initiateKill = () => {
196
+ if (killInitiated)
197
+ return;
198
+ killInitiated = true;
187
199
  // killProcessTree handles both platforms and kills the whole tree: on Unix
188
200
  // it signals the process group (SIGTERM -> grace -> SIGKILL); on Windows it
189
201
  // force-kills the tree via taskkill /F /T. Using child.kill("SIGTERM") here
@@ -201,14 +213,14 @@ export function runExecSandbox(request, opts) {
201
213
  finalize(null, "SIGKILL", true);
202
214
  }, effectiveForceResolveDelay);
203
215
  forceResolveTimer.unref?.();
204
- }, timeoutMs);
205
- timer.unref?.();
216
+ };
206
217
  const finalize = (exitCode, signal, forceResolved = false) => {
207
218
  if (settled)
208
219
  return;
209
220
  settled = true;
210
221
  clearTimeout(timer);
211
222
  clearTimeout(forceResolveTimer);
223
+ removeAbortListener();
212
224
  const duration = Date.now() - started;
213
225
  const stdoutBuf = Buffer.concat(stdoutChunks);
214
226
  const stderrBuf = Buffer.concat(stderrChunks);
@@ -219,17 +231,20 @@ export function runExecSandbox(request, opts) {
219
231
  const digestBody = tail(stdoutBuf, opts.digest_chars);
220
232
  const digest = digestBody.length > 0
221
233
  ? digestBody
222
- : timedOut
223
- ? "[no stdout — timed out]"
224
- : stderrBuf.length > 0
225
- ? `[no stdout — tail of stderr]\n${tail(stderrBuf, opts.digest_chars)}`
226
- : "[no output]";
234
+ : aborted
235
+ ? "[no stdout — aborted]"
236
+ : timedOut
237
+ ? "[no stdout — timed out]"
238
+ : stderrBuf.length > 0
239
+ ? `[no stdout — tail of stderr]\n${tail(stderrBuf, opts.digest_chars)}`
240
+ : "[no output]";
227
241
  const result = {
228
242
  id,
229
243
  runtime: request.runtime,
230
244
  exit_code: exitCode,
231
245
  signal,
232
246
  timed_out: timedOut,
247
+ aborted,
233
248
  force_resolved: forceResolved,
234
249
  duration_ms: duration,
235
250
  stdout_bytes: stdoutBytes,
@@ -244,6 +259,26 @@ export function runExecSandbox(request, opts) {
244
259
  writeMeta(metaPath, result, request, now);
245
260
  resolveP(result);
246
261
  };
262
+ timer = setTimeout(() => {
263
+ timedOut = true;
264
+ initiateKill();
265
+ }, timeoutMs);
266
+ timer.unref?.();
267
+ if (opts.signal) {
268
+ abortListener = () => {
269
+ if (settled || timedOut)
270
+ return;
271
+ aborted = true;
272
+ clearTimeout(timer);
273
+ initiateKill();
274
+ };
275
+ if (opts.signal.aborted) {
276
+ abortListener();
277
+ }
278
+ else {
279
+ opts.signal.addEventListener("abort", abortListener, { once: true });
280
+ }
281
+ }
247
282
  child.on("error", (err) => {
248
283
  const message = err instanceof Error ? err.message : String(err);
249
284
  const line = `child error: ${message}\n`;
@@ -274,6 +309,7 @@ function writeMeta(path, result, request, now) {
274
309
  exit_code: result.exit_code,
275
310
  signal: result.signal,
276
311
  timed_out: result.timed_out,
312
+ aborted: result.aborted === true,
277
313
  force_resolved: result.force_resolved,
278
314
  duration_ms: result.duration_ms,
279
315
  stdout_bytes: result.stdout_bytes,
@@ -22,8 +22,7 @@ import { deriveState } from "./state.js";
22
22
  import { isAutoActive } from "./auto.js";
23
23
  import { loadPrompt } from "./prompt-loader.js";
24
24
  import { gsdRoot } from "./paths.js";
25
- import { isDbAvailable, getAllMilestones, getMilestoneSlices, getSliceTasks } from "./gsd-db.js";
26
- import { isClosedStatus } from "./status-guards.js";
25
+ import { isDbAvailable, getHierarchyCompletionCounts } from "./gsd-db.js";
27
26
  import { formatDuration } from "../shared/format-utils.js";
28
27
  import { getAutoWorktreePath } from "./auto-worktree.js";
29
28
  import { clearGSDPreferencesCache, loadEffectiveGSDPreferences, loadGlobalGSDPreferences, getGlobalGSDPreferencesPath } from "./preferences.js";
@@ -593,36 +592,7 @@ function loadCompletedKeys(basePath) {
593
592
  function getDbCompletionCounts() {
594
593
  if (!isDbAvailable())
595
594
  return null;
596
- const milestones = getAllMilestones();
597
- let completedMilestones = 0;
598
- let totalSlices = 0;
599
- let completedSlices = 0;
600
- let totalTasks = 0;
601
- let completedTasks = 0;
602
- for (const m of milestones) {
603
- if (isClosedStatus(m.status))
604
- completedMilestones++;
605
- const slices = getMilestoneSlices(m.id);
606
- for (const s of slices) {
607
- totalSlices++;
608
- if (isClosedStatus(s.status))
609
- completedSlices++;
610
- const tasks = getSliceTasks(m.id, s.id);
611
- for (const t of tasks) {
612
- totalTasks++;
613
- if (isClosedStatus(t.status))
614
- completedTasks++;
615
- }
616
- }
617
- }
618
- return {
619
- milestones: completedMilestones,
620
- milestonesTotal: milestones.length,
621
- slices: completedSlices,
622
- slicesTotal: totalSlices,
623
- tasks: completedTasks,
624
- tasksTotal: totalTasks,
625
- };
595
+ return getHierarchyCompletionCounts();
626
596
  }
627
597
  // ─── Anomaly Detectors ───────────────────────────────────────────────────────
628
598
  /**
@@ -225,6 +225,7 @@ export const RUNTIME_EXCLUSION_PATHS = [
225
225
  ".gsd/event-log.jsonl",
226
226
  ".gsd/DISCUSSION-MANIFEST.json",
227
227
  ];
228
+ const runtimeFilesCleanedUpRepos = new Set();
228
229
  // ─── Integration Branch Metadata ───────────────────────────────────────────
229
230
  /**
230
231
  * Path to the milestone metadata file that stores the integration branch.
@@ -565,7 +566,8 @@ export class GitServiceImpl {
565
566
  // and the worktree is torn down. This prevents a mid-execution behavioral
566
567
  // discontinuity where the first half of a milestone has .gsd/ artifacts
567
568
  // committed but the second half doesn't (#1326).
568
- if (!this._runtimeFilesCleanedUp) {
569
+ const cleanupRepoKey = resolve(this.basePath);
570
+ if (!runtimeFilesCleanedUpRepos.has(cleanupRepoKey)) {
569
571
  let cleaned = false;
570
572
  for (const exclusion of RUNTIME_EXCLUSION_PATHS) {
571
573
  const removed = nativeRmCached(this.basePath, [exclusion]);
@@ -575,7 +577,7 @@ export class GitServiceImpl {
575
577
  if (cleaned) {
576
578
  nativeCommit(this.basePath, "chore: untrack .gsd/ runtime files from git index", { allowEmpty: false });
577
579
  }
578
- this._runtimeFilesCleanedUp = true;
580
+ runtimeFilesCleanedUpRepos.add(cleanupRepoKey);
579
581
  }
580
582
  // Stage everything using pathspec exclusions so excluded paths are never
581
583
  // hashed by git. The old approach of `git add -A` followed by unstaging
@@ -683,8 +685,6 @@ export class GitServiceImpl {
683
685
  return false;
684
686
  }
685
687
  }
686
- /** Tracks whether runtime file cleanup has run this session. */
687
- _runtimeFilesCleanedUp = false;
688
688
  /**
689
689
  * Stage files (smart staging) and commit.
690
690
  * Returns the commit message string on success, or null if nothing to commit.
@@ -12,7 +12,7 @@ import { loadFile } from "./files.js";
12
12
  import { loadPrompt, inlineTemplate } from "./prompt-loader.js";
13
13
  import { deriveState } from "./state.js";
14
14
  import { invalidateAllCaches } from "./cache.js";
15
- import { gsdRoot, resolveMilestoneFile, resolveGsdRootFile, relGsdRootFile, } from "./paths.js";
15
+ import { gsdRoot, resolveMilestoneFile, resolveGsdRootFile, relGsdRootFile, relMilestoneFile, } from "./paths.js";
16
16
  import { readFileSync, writeFileSync, existsSync } from "node:fs";
17
17
  import { atomicWriteSync } from "./atomic-write.js";
18
18
  import { nativeAddPaths, nativeCommit } from "./native-git-bridge.js";
@@ -20,6 +20,9 @@ import { loadEffectiveGSDPreferences } from "./preferences.js";
20
20
  import { saveQueueOrder } from "./queue-order.js";
21
21
  import { findMilestoneIds, nextMilestoneId } from "./milestone-ids.js";
22
22
  import { isFutureMilestoneStatus } from "./status-guards.js";
23
+ const QUEUE_ARTIFACT_EXCERPT_MAX_CHARS = 20_000;
24
+ const QUEUE_EXISTING_MILESTONES_CONTEXT_MAX_CHARS = 120_000;
25
+ const QUEUE_CONTEXT_SECTION_SEPARATOR = "\n\n---\n\n";
23
26
  // ─── Queue Entry Point ──────────────────────────────────────────────────────
24
27
  /**
25
28
  * Queue future milestones via conversational intake.
@@ -215,7 +218,7 @@ export async function buildExistingMilestonesContext(basePath, milestoneIds, sta
215
218
  if (contextFile) {
216
219
  const content = await loadFile(contextFile);
217
220
  if (content) {
218
- parts.push(`\n**Context:**\n${content.trim()}`);
221
+ parts.push(`\n**Context:**\n${summarizeArtifactForQueue(content, relMilestoneFile(basePath, mid, "CONTEXT"))}`);
219
222
  }
220
223
  }
221
224
  else {
@@ -224,7 +227,7 @@ export async function buildExistingMilestonesContext(basePath, milestoneIds, sta
224
227
  if (draftFile) {
225
228
  const draftContent = await loadFile(draftFile);
226
229
  if (draftContent) {
227
- parts.push(`\n**Draft context available:**\n${draftContent.trim()}`);
230
+ parts.push(`\n**Draft context available:**\n${summarizeArtifactForQueue(draftContent, relMilestoneFile(basePath, mid, "CONTEXT-DRAFT"))}`);
228
231
  }
229
232
  }
230
233
  }
@@ -235,7 +238,7 @@ export async function buildExistingMilestonesContext(basePath, milestoneIds, sta
235
238
  if (roadmapFile) {
236
239
  const content = await loadFile(roadmapFile);
237
240
  if (content) {
238
- parts.push(`\n**Roadmap:**\n${content.trim()}`);
241
+ parts.push(`\n**Roadmap:**\n${summarizeArtifactForQueue(content, relMilestoneFile(basePath, mid, "ROADMAP"))}`);
239
242
  }
240
243
  }
241
244
  }
@@ -249,7 +252,58 @@ export async function buildExistingMilestonesContext(basePath, milestoneIds, sta
249
252
  sections.push(`### Previous Queue Entries\nSource: \`${relGsdRootFile("QUEUE")}\`\n\n${queueContent.trim()}`);
250
253
  }
251
254
  }
252
- return sections.join("\n\n---\n\n");
255
+ return capExistingMilestonesContext(sections);
256
+ }
257
+ function summarizeArtifactForQueue(content, sourcePath, cap = QUEUE_ARTIFACT_EXCERPT_MAX_CHARS) {
258
+ const trimmed = content.trim();
259
+ if (trimmed.length <= cap) {
260
+ return `Source: \`${sourcePath}\`\n\n${trimmed}`;
261
+ }
262
+ const excerpt = trimmed.slice(0, cap).trimEnd();
263
+ const omittedChars = trimmed.length - excerpt.length;
264
+ return [
265
+ `Source: \`${sourcePath}\``,
266
+ "",
267
+ excerpt,
268
+ "",
269
+ `[Truncated ${omittedChars} chars. Read \`${sourcePath}\` for full content.]`,
270
+ ].join("\n");
271
+ }
272
+ function capExistingMilestonesContext(sections, cap = QUEUE_EXISTING_MILESTONES_CONTEXT_MAX_CHARS) {
273
+ const fullContext = sections.join(QUEUE_CONTEXT_SECTION_SEPARATOR);
274
+ if (fullContext.length <= cap)
275
+ return fullContext;
276
+ const notice = `[Existing milestones context truncated to ${cap} chars. Read source paths in this prompt or the corresponding .gsd artifacts for full details.]`;
277
+ const compactSections = sections.map(compactSectionForQueueBudget);
278
+ const selected = [];
279
+ for (let i = 0; i < sections.length; i++) {
280
+ const withFullSection = [
281
+ ...selected,
282
+ sections[i],
283
+ ...compactSections.slice(i + 1),
284
+ notice,
285
+ ].join(QUEUE_CONTEXT_SECTION_SEPARATOR);
286
+ selected.push(withFullSection.length <= cap ? sections[i] : compactSections[i]);
287
+ }
288
+ const capped = [...selected, notice].join(QUEUE_CONTEXT_SECTION_SEPARATOR);
289
+ if (capped.length <= cap)
290
+ return capped;
291
+ return `${capped.slice(0, Math.max(0, cap - notice.length - 2)).trimEnd()}\n\n${notice}`;
292
+ }
293
+ function compactSectionForQueueBudget(section) {
294
+ const lines = section.split("\n");
295
+ const compact = [];
296
+ if (lines[0])
297
+ compact.push(lines[0]);
298
+ const statusLine = lines.find(line => line.startsWith("**Status:**"));
299
+ if (statusLine)
300
+ compact.push(statusLine);
301
+ const sourceLines = lines.filter(line => line.startsWith("Source: `"));
302
+ if (sourceLines.length > 0) {
303
+ compact.push("", "**Sources:**", ...sourceLines);
304
+ compact.push("", "[Artifact excerpts omitted due to total queue/rethink context budget.]");
305
+ }
306
+ return compact.join("\n");
253
307
  }
254
308
  // ─── Internal Helpers ───────────────────────────────────────────────────────
255
309
  /**
@@ -1,29 +1,61 @@
1
1
  // Project/App: gsd-pi
2
2
  // File Purpose: Always-on ambient health signal rendered below the editor.
3
- import { runProviderChecks, summariseProviderIssues } from "./doctor-providers.js";
3
+ import { execFile } from "node:child_process";
4
+ import { runProviderChecks, runProviderChecksAsync, summariseProviderIssues } from "./doctor-providers.js";
4
5
  import { runEnvironmentChecks, runEnvironmentChecksAsync } from "./doctor-environment.js";
5
6
  import { loadEffectiveGSDPreferences } from "./preferences.js";
6
- import { nativeIsRepo, nativeLastCommitEpoch, nativeGetCurrentBranch, nativeCommitSubject } from "./native-git-bridge.js";
7
+ import { GIT_NO_PROMPT_ENV } from "./git-constants.js";
7
8
  import { loadLedgerFromDisk, getProjectTotals } from "./metrics.js";
8
9
  import { projectRoot } from "./commands/context.js";
9
10
  import { buildHealthLines, detectHealthWidgetProjectState, } from "./health-widget-core.js";
10
11
  export const HEALTH_WIDGET_ACTIVE_HINTS = " /gsd auto to run · /gsd status to inspect · /gsd report for snapshots · /gsd notifications for history · /gsd help";
12
+ const LAST_COMMIT_LOOKUP_TIMEOUT_MS = 3_000;
13
+ const REFRESH_INTERVAL_MS = 60_000;
14
+ const PROJECT_STATE_CACHE_TTL_MS = REFRESH_INTERVAL_MS;
11
15
  // ── Data loader ────────────────────────────────────────────────────────────────
12
- // Last-commit lookup is subprocess-backed (native-git-bridge → git spawns),
13
- // so it is treated like the other expensive checks: skipped on first paint,
14
- // run only by the background refresh.
15
- function loadLastCommitInfo(basePath) {
16
+ const projectStateCache = new Map();
17
+ export function getCachedProjectState(basePath, force) {
18
+ const now = Date.now();
19
+ const cached = projectStateCache.get(basePath);
20
+ if (!force && cached && now - cached.computedAt <= PROJECT_STATE_CACHE_TTL_MS) {
21
+ return cached.state;
22
+ }
23
+ const state = detectHealthWidgetProjectState(basePath);
24
+ projectStateCache.set(basePath, { state, computedAt: now });
25
+ return state;
26
+ }
27
+ function runHealthWidgetGit(basePath, args) {
28
+ return new Promise((resolve) => {
29
+ const child = execFile("git", args, {
30
+ cwd: basePath,
31
+ timeout: LAST_COMMIT_LOOKUP_TIMEOUT_MS,
32
+ encoding: "utf-8",
33
+ env: GIT_NO_PROMPT_ENV,
34
+ }, (err, stdout) => resolve(err ? null : String(stdout).trimEnd()));
35
+ child.on("error", () => resolve(null));
36
+ });
37
+ }
38
+ async function loadLastCommitInfoAsync(basePath) {
16
39
  try {
17
- if (nativeIsRepo(basePath)) {
18
- const branch = nativeGetCurrentBranch(basePath);
19
- const epoch = nativeLastCommitEpoch(basePath, branch || "HEAD");
20
- if (epoch > 0) {
21
- return { epoch, message: nativeCommitSubject(basePath, branch || "HEAD") || null };
22
- }
40
+ if ((await runHealthWidgetGit(basePath, ["rev-parse", "--git-dir"])) === null) {
41
+ return { epoch: null, message: null };
23
42
  }
43
+ const branch = await runHealthWidgetGit(basePath, ["branch", "--show-current"]);
44
+ const ref = branch || "HEAD";
45
+ const raw = await runHealthWidgetGit(basePath, ["log", "-1", "--format=%ct%x00%s", ref]);
46
+ if (!raw)
47
+ return { epoch: null, message: null };
48
+ const separator = raw.indexOf("\0");
49
+ const epochText = separator >= 0 ? raw.slice(0, separator) : raw;
50
+ const epoch = parseInt(epochText.trim(), 10) || 0;
51
+ if (epoch <= 0)
52
+ return { epoch: null, message: null };
53
+ const message = separator >= 0 ? raw.slice(separator + 1).trim() : "";
54
+ return { epoch, message: message || null };
55
+ }
56
+ catch {
57
+ return { epoch: null, message: null };
24
58
  }
25
- catch { /* non-fatal */ }
26
- return { epoch: null, message: null };
27
59
  }
28
60
  function loadHealthWidgetData(basePath, options) {
29
61
  // `includeChecks` gates the expensive subprocess-backed checks (provider +
@@ -38,7 +70,7 @@ function loadHealthWidgetData(basePath, options) {
38
70
  let environmentWarningCount = 0;
39
71
  let lastCommitEpoch = null;
40
72
  let lastCommitMessage = null;
41
- const projectState = detectHealthWidgetProjectState(basePath);
73
+ const projectState = getCachedProjectState(basePath, options?.forceProjectState);
42
74
  try {
43
75
  const prefs = loadEffectiveGSDPreferences();
44
76
  budgetCeiling = prefs?.preferences?.budget_ceiling;
@@ -66,12 +98,6 @@ function loadHealthWidgetData(basePath, options) {
66
98
  }
67
99
  catch { /* non-fatal */ }
68
100
  }
69
- // ── Last commit info ── (git spawns — gated like the other expensive checks)
70
- if (includeChecks) {
71
- const commit = loadLastCommitInfo(basePath);
72
- lastCommitEpoch = commit.epoch;
73
- lastCommitMessage = commit.message;
74
- }
75
101
  return {
76
102
  projectState,
77
103
  budgetCeiling,
@@ -85,17 +111,15 @@ function loadHealthWidgetData(basePath, options) {
85
111
  };
86
112
  }
87
113
  // Non-blocking variant used by the widget's background refresh: the cheap fields
88
- // come from the synchronous snapshot, then provider + environment checks are
89
- // layered in off the event-loop critical path (env checks run concurrently via
90
- // runEnvironmentChecksAsync). Keeps the always-on widget from stalling the UI on
91
- // its initial enrichment or its 60s refresh.
114
+ // come from the synchronous snapshot, then provider, environment, and last-commit
115
+ // checks are layered in off the event-loop critical path.
92
116
  async function loadHealthWidgetDataAsync(basePath) {
93
117
  const data = loadHealthWidgetData(basePath, { includeChecks: false });
94
118
  let providerIssue = data.providerIssue;
95
119
  let environmentErrorCount = 0;
96
120
  let environmentWarningCount = 0;
97
121
  try {
98
- providerIssue = summariseProviderIssues(runProviderChecks());
122
+ providerIssue = summariseProviderIssues(await runProviderChecksAsync());
99
123
  }
100
124
  catch { /* non-fatal */ }
101
125
  try {
@@ -108,7 +132,7 @@ async function loadHealthWidgetDataAsync(basePath) {
108
132
  }
109
133
  }
110
134
  catch { /* non-fatal */ }
111
- const commit = loadLastCommitInfo(basePath);
135
+ const commit = await loadLastCommitInfoAsync(basePath);
112
136
  return {
113
137
  ...data,
114
138
  providerIssue,
@@ -120,7 +144,6 @@ async function loadHealthWidgetDataAsync(basePath) {
120
144
  };
121
145
  }
122
146
  // ── Widget init ────────────────────────────────────────────────────────────────
123
- const REFRESH_INTERVAL_MS = 60_000;
124
147
  /**
125
148
  * Initialize the always-on gsd-health widget (belowEditor).
126
149
  * Call once from the extension entry point after context is available.
@@ -129,13 +152,16 @@ export function initHealthWidget(ctx) {
129
152
  if (!ctx.hasUI)
130
153
  return;
131
154
  const basePath = projectRoot();
155
+ // Re-init must reflect filesystem changes immediately; the TTL cache is for
156
+ // interval refreshes, not this one-off synchronous paint.
157
+ projectStateCache.delete(basePath);
132
158
  // String-array fallback — used in RPC mode (factory is a no-op there).
133
159
  // Skip the expensive provider/environment doctor checks here: this runs
134
160
  // synchronously on the interactive-startup path, where running them would
135
161
  // block first paint by ~0.9s (lsof/docker probes, otherwise run again
136
162
  // immediately by the factory below). The factory's async refresh fills in
137
163
  // real health once the screen is up.
138
- const initialData = loadHealthWidgetData(basePath, { includeChecks: false });
164
+ const initialData = loadHealthWidgetData(basePath, { includeChecks: false, forceProjectState: true });
139
165
  ctx.ui.setWidget("gsd-health", buildHealthLines(initialData), { placement: "belowEditor" });
140
166
  // Factory-based widget for TUI mode — replaces the string-array above
141
167
  ctx.ui.setWidget("gsd-health", (_tui, _theme) => {
@@ -12,7 +12,7 @@ import { readFileSync, existsSync, mkdirSync, statSync, unlinkSync } from "node:
12
12
  import { logWarning } from "./workflow-logger.js";
13
13
  import { isClosedStatus } from "./status-guards.js";
14
14
  import { dirname, join, relative } from "node:path";
15
- import { getAllMilestones, getMilestone, getMilestoneScopedArtifacts, getSliceScopedArtifacts, getMilestoneSlices, getSliceTasks, getTask, getSlice, insertArtifact, deleteArtifactByPath, getGateResults, } from "./gsd-db.js";
15
+ import { getAllMilestones, getMilestone, getMilestoneScopedArtifacts, getSliceScopedArtifacts, getMilestoneSlices, getSliceTasks, getTask, getSlice, insertArtifact, deleteArtifactByPath, getGateResults, isDbAvailable, } from "./gsd-db.js";
16
16
  import { resolveSliceFile, resolveSlicePath, resolveTaskFile, resolveTasksDir, gsdProjectionRoot, gsdRoot, buildMilestoneFileName, buildTaskFileName, } from "./paths.js";
17
17
  import { saveFile, clearParseCache, registerCacheClearCallback } from "./files.js";
18
18
  import { parseRoadmap, parsePlan } from "./parsers-legacy.js";
@@ -604,11 +604,15 @@ function isAutoRecoveryPlaceholderPlan(content) {
604
604
  * projection (the 4S/0T-vs-5S/13T drift class). The artifacts table is an
605
605
  * output sink, never a render input.
606
606
  *
607
- * @returns true if the plan was written, false on skip/error
607
+ * @returns true if the plan was written, false when the DB slice has no tasks
608
+ * @throws when the DB connection is unavailable or the render write fails
608
609
  */
609
610
  export async function renderPlanCheckboxes(basePath, milestoneId, sliceId, outputPath) {
610
611
  const tasks = getSliceTasks(milestoneId, sliceId);
611
612
  if (tasks.length === 0) {
613
+ if (!isDbAvailable()) {
614
+ throw new Error(`database unavailable while rendering plan checkboxes for ${milestoneId}/${sliceId}`);
615
+ }
612
616
  process.stderr.write(`markdown-renderer: no tasks found for ${milestoneId}/${sliceId}\n`);
613
617
  return false;
614
618
  }