@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
@@ -48,6 +48,8 @@ export interface ExecSandboxOptions {
48
48
  now?: () => Date;
49
49
  /** Optional override for id generation (tests). */
50
50
  generateId?: () => string;
51
+ /** Optional request cancellation signal. Aborting kills the child process tree. */
52
+ signal?: AbortSignal;
51
53
  /**
52
54
  * Grace period (ms) between SIGTERM and SIGKILL on timeout.
53
55
  * Defaults to SIGKILL_GRACE_MS. Exposed as a test seam.
@@ -67,6 +69,8 @@ export interface ExecSandboxResult {
67
69
  exit_code: number | null;
68
70
  signal: NodeJS.Signals | null;
69
71
  timed_out: boolean;
72
+ /** True when an external AbortSignal terminated the child process tree. */
73
+ aborted?: boolean;
70
74
  /**
71
75
  * True when the result came from the hard-deadline force-resolve (a non-closing
72
76
  * D-state child that never emitted 'close') rather than an observed process exit.
@@ -260,11 +264,23 @@ export function runExecSandbox(
260
264
  const effectiveForceResolveDelay = opts.force_resolve_delay_ms ?? (effectiveGraceMs + HARD_DEADLINE_MS);
261
265
 
262
266
  let timedOut = false;
267
+ let aborted = false;
263
268
  let settled = false;
269
+ let killInitiated = false;
270
+ let timer: NodeJS.Timeout | undefined;
264
271
  let forceResolveTimer: NodeJS.Timeout | undefined;
272
+ let abortListener: (() => void) | undefined;
265
273
 
266
- const timer = setTimeout(() => {
267
- timedOut = true;
274
+ const removeAbortListener = () => {
275
+ if (opts.signal && abortListener) {
276
+ opts.signal.removeEventListener("abort", abortListener);
277
+ abortListener = undefined;
278
+ }
279
+ };
280
+
281
+ const initiateKill = () => {
282
+ if (killInitiated) return;
283
+ killInitiated = true;
268
284
  // killProcessTree handles both platforms and kills the whole tree: on Unix
269
285
  // it signals the process group (SIGTERM -> grace -> SIGKILL); on Windows it
270
286
  // force-kills the tree via taskkill /F /T. Using child.kill("SIGTERM") here
@@ -281,14 +297,14 @@ export function runExecSandbox(
281
297
  finalize(null, "SIGKILL", true);
282
298
  }, effectiveForceResolveDelay);
283
299
  forceResolveTimer.unref?.();
284
- }, timeoutMs);
285
- timer.unref?.();
300
+ };
286
301
 
287
302
  const finalize = (exitCode: number | null, signal: NodeJS.Signals | null, forceResolved = false) => {
288
303
  if (settled) return;
289
304
  settled = true;
290
305
  clearTimeout(timer);
291
306
  clearTimeout(forceResolveTimer);
307
+ removeAbortListener();
292
308
  const duration = Date.now() - started;
293
309
  const stdoutBuf = Buffer.concat(stdoutChunks);
294
310
  const stderrBuf = Buffer.concat(stderrChunks);
@@ -301,11 +317,13 @@ export function runExecSandbox(
301
317
  const digest =
302
318
  digestBody.length > 0
303
319
  ? digestBody
304
- : timedOut
305
- ? "[no stdout — timed out]"
306
- : stderrBuf.length > 0
307
- ? `[no stdout — tail of stderr]\n${tail(stderrBuf, opts.digest_chars)}`
308
- : "[no output]";
320
+ : aborted
321
+ ? "[no stdout — aborted]"
322
+ : timedOut
323
+ ? "[no stdout — timed out]"
324
+ : stderrBuf.length > 0
325
+ ? `[no stdout — tail of stderr]\n${tail(stderrBuf, opts.digest_chars)}`
326
+ : "[no output]";
309
327
 
310
328
  const result: ExecSandboxResult = {
311
329
  id,
@@ -313,6 +331,7 @@ export function runExecSandbox(
313
331
  exit_code: exitCode,
314
332
  signal,
315
333
  timed_out: timedOut,
334
+ aborted,
316
335
  force_resolved: forceResolved,
317
336
  duration_ms: duration,
318
337
  stdout_bytes: stdoutBytes,
@@ -328,6 +347,26 @@ export function runExecSandbox(
328
347
  resolveP(result);
329
348
  };
330
349
 
350
+ timer = setTimeout(() => {
351
+ timedOut = true;
352
+ initiateKill();
353
+ }, timeoutMs);
354
+ timer.unref?.();
355
+
356
+ if (opts.signal) {
357
+ abortListener = () => {
358
+ if (settled || timedOut) return;
359
+ aborted = true;
360
+ clearTimeout(timer);
361
+ initiateKill();
362
+ };
363
+ if (opts.signal.aborted) {
364
+ abortListener();
365
+ } else {
366
+ opts.signal.addEventListener("abort", abortListener, { once: true });
367
+ }
368
+ }
369
+
331
370
  child.on("error", (err) => {
332
371
  const message = err instanceof Error ? err.message : String(err);
333
372
  const line = `child error: ${message}\n`;
@@ -364,6 +403,7 @@ function writeMeta(
364
403
  exit_code: result.exit_code,
365
404
  signal: result.signal,
366
405
  timed_out: result.timed_out,
406
+ aborted: result.aborted === true,
367
407
  force_resolved: result.force_resolved,
368
408
  duration_ms: result.duration_ms,
369
409
  stdout_bytes: result.stdout_bytes,
@@ -28,8 +28,7 @@ import { deriveState } from "./state.js";
28
28
  import { isAutoActive } from "./auto.js";
29
29
  import { loadPrompt } from "./prompt-loader.js";
30
30
  import { gsdRoot } from "./paths.js";
31
- import { isDbAvailable, getAllMilestones, getMilestoneSlices, getSliceTasks } from "./gsd-db.js";
32
- import { isClosedStatus } from "./status-guards.js";
31
+ import { isDbAvailable, getHierarchyCompletionCounts } from "./gsd-db.js";
33
32
  import { formatDuration } from "../shared/format-utils.js";
34
33
  import { getAutoWorktreePath } from "./auto-worktree.js";
35
34
  import { clearGSDPreferencesCache, loadEffectiveGSDPreferences, loadGlobalGSDPreferences, getGlobalGSDPreferencesPath } from "./preferences.js";
@@ -759,37 +758,7 @@ function loadCompletedKeys(basePath: string): string[] {
759
758
  function getDbCompletionCounts(): DbCompletionCounts | null {
760
759
  if (!isDbAvailable()) return null;
761
760
 
762
- const milestones = getAllMilestones();
763
- let completedMilestones = 0;
764
- let totalSlices = 0;
765
- let completedSlices = 0;
766
- let totalTasks = 0;
767
- let completedTasks = 0;
768
-
769
- for (const m of milestones) {
770
- if (isClosedStatus(m.status)) completedMilestones++;
771
-
772
- const slices = getMilestoneSlices(m.id);
773
- for (const s of slices) {
774
- totalSlices++;
775
- if (isClosedStatus(s.status)) completedSlices++;
776
-
777
- const tasks = getSliceTasks(m.id, s.id);
778
- for (const t of tasks) {
779
- totalTasks++;
780
- if (isClosedStatus(t.status)) completedTasks++;
781
- }
782
- }
783
- }
784
-
785
- return {
786
- milestones: completedMilestones,
787
- milestonesTotal: milestones.length,
788
- slices: completedSlices,
789
- slicesTotal: totalSlices,
790
- tasks: completedTasks,
791
- tasksTotal: totalTasks,
792
- };
761
+ return getHierarchyCompletionCounts();
793
762
  }
794
763
 
795
764
  // ─── Anomaly Detectors ───────────────────────────────────────────────────────
@@ -378,6 +378,8 @@ export const RUNTIME_EXCLUSION_PATHS: readonly string[] = [
378
378
  ".gsd/DISCUSSION-MANIFEST.json",
379
379
  ];
380
380
 
381
+ const runtimeFilesCleanedUpRepos = new Set<string>();
382
+
381
383
  // ─── Integration Branch Metadata ───────────────────────────────────────────
382
384
 
383
385
  /**
@@ -755,7 +757,8 @@ export class GitServiceImpl {
755
757
  // and the worktree is torn down. This prevents a mid-execution behavioral
756
758
  // discontinuity where the first half of a milestone has .gsd/ artifacts
757
759
  // committed but the second half doesn't (#1326).
758
- if (!this._runtimeFilesCleanedUp) {
760
+ const cleanupRepoKey = resolve(this.basePath);
761
+ if (!runtimeFilesCleanedUpRepos.has(cleanupRepoKey)) {
759
762
  let cleaned = false;
760
763
  for (const exclusion of RUNTIME_EXCLUSION_PATHS) {
761
764
  const removed = nativeRmCached(this.basePath, [exclusion]);
@@ -764,7 +767,7 @@ export class GitServiceImpl {
764
767
  if (cleaned) {
765
768
  nativeCommit(this.basePath, "chore: untrack .gsd/ runtime files from git index", { allowEmpty: false });
766
769
  }
767
- this._runtimeFilesCleanedUp = true;
770
+ runtimeFilesCleanedUpRepos.add(cleanupRepoKey);
768
771
  }
769
772
 
770
773
  // Stage everything using pathspec exclusions so excluded paths are never
@@ -893,9 +896,6 @@ export class GitServiceImpl {
893
896
  }
894
897
  }
895
898
 
896
- /** Tracks whether runtime file cleanup has run this session. */
897
- private _runtimeFilesCleanedUp = false;
898
-
899
899
  /**
900
900
  * Stage files (smart staging) and commit.
901
901
  * Returns the commit message string on success, or null if nothing to commit.
@@ -20,6 +20,7 @@ import { invalidateAllCaches } from "./cache.js";
20
20
  import {
21
21
  gsdRoot, resolveMilestoneFile, resolveSliceFile,
22
22
  resolveGsdRootFile, relGsdRootFile, relSliceFile,
23
+ relMilestoneFile,
23
24
  } from "./paths.js";
24
25
  import { readFileSync, writeFileSync, existsSync } from "node:fs";
25
26
  import { atomicWriteSync } from "./atomic-write.js";
@@ -29,6 +30,10 @@ import { loadQueueOrder, sortByQueueOrder, saveQueueOrder } from "./queue-order.
29
30
  import { findMilestoneIds, nextMilestoneId } from "./milestone-ids.js";
30
31
  import { isFutureMilestoneStatus } from "./status-guards.js";
31
32
 
33
+ const QUEUE_ARTIFACT_EXCERPT_MAX_CHARS = 20_000;
34
+ const QUEUE_EXISTING_MILESTONES_CONTEXT_MAX_CHARS = 120_000;
35
+ const QUEUE_CONTEXT_SECTION_SEPARATOR = "\n\n---\n\n";
36
+
32
37
  // ─── Queue Entry Point ──────────────────────────────────────────────────────
33
38
 
34
39
  /**
@@ -280,7 +285,9 @@ export async function buildExistingMilestonesContext(
280
285
  if (contextFile) {
281
286
  const content = await loadFile(contextFile);
282
287
  if (content) {
283
- parts.push(`\n**Context:**\n${content.trim()}`);
288
+ parts.push(
289
+ `\n**Context:**\n${summarizeArtifactForQueue(content, relMilestoneFile(basePath, mid, "CONTEXT"))}`,
290
+ );
284
291
  }
285
292
  } else {
286
293
  // No full CONTEXT.md — check for CONTEXT-DRAFT.md (draft seed from prior discussion)
@@ -288,7 +295,9 @@ export async function buildExistingMilestonesContext(
288
295
  if (draftFile) {
289
296
  const draftContent = await loadFile(draftFile);
290
297
  if (draftContent) {
291
- parts.push(`\n**Draft context available:**\n${draftContent.trim()}`);
298
+ parts.push(
299
+ `\n**Draft context available:**\n${summarizeArtifactForQueue(draftContent, relMilestoneFile(basePath, mid, "CONTEXT-DRAFT"))}`,
300
+ );
292
301
  }
293
302
  }
294
303
  }
@@ -300,7 +309,9 @@ export async function buildExistingMilestonesContext(
300
309
  if (roadmapFile) {
301
310
  const content = await loadFile(roadmapFile);
302
311
  if (content) {
303
- parts.push(`\n**Roadmap:**\n${content.trim()}`);
312
+ parts.push(
313
+ `\n**Roadmap:**\n${summarizeArtifactForQueue(content, relMilestoneFile(basePath, mid, "ROADMAP"))}`,
314
+ );
304
315
  }
305
316
  }
306
317
  }
@@ -317,7 +328,74 @@ export async function buildExistingMilestonesContext(
317
328
  }
318
329
  }
319
330
 
320
- return sections.join("\n\n---\n\n");
331
+ return capExistingMilestonesContext(sections);
332
+ }
333
+
334
+ function summarizeArtifactForQueue(
335
+ content: string,
336
+ sourcePath: string,
337
+ cap = QUEUE_ARTIFACT_EXCERPT_MAX_CHARS,
338
+ ): string {
339
+ const trimmed = content.trim();
340
+ if (trimmed.length <= cap) {
341
+ return `Source: \`${sourcePath}\`\n\n${trimmed}`;
342
+ }
343
+
344
+ const excerpt = trimmed.slice(0, cap).trimEnd();
345
+ const omittedChars = trimmed.length - excerpt.length;
346
+ return [
347
+ `Source: \`${sourcePath}\``,
348
+ "",
349
+ excerpt,
350
+ "",
351
+ `[Truncated ${omittedChars} chars. Read \`${sourcePath}\` for full content.]`,
352
+ ].join("\n");
353
+ }
354
+
355
+ function capExistingMilestonesContext(
356
+ sections: string[],
357
+ cap = QUEUE_EXISTING_MILESTONES_CONTEXT_MAX_CHARS,
358
+ ): string {
359
+ const fullContext = sections.join(QUEUE_CONTEXT_SECTION_SEPARATOR);
360
+ if (fullContext.length <= cap) return fullContext;
361
+
362
+ const notice = `[Existing milestones context truncated to ${cap} chars. Read source paths in this prompt or the corresponding .gsd artifacts for full details.]`;
363
+ const compactSections = sections.map(compactSectionForQueueBudget);
364
+ const selected: string[] = [];
365
+
366
+ for (let i = 0; i < sections.length; i++) {
367
+ const withFullSection = [
368
+ ...selected,
369
+ sections[i],
370
+ ...compactSections.slice(i + 1),
371
+ notice,
372
+ ].join(QUEUE_CONTEXT_SECTION_SEPARATOR);
373
+
374
+ selected.push(withFullSection.length <= cap ? sections[i] : compactSections[i]);
375
+ }
376
+
377
+ const capped = [...selected, notice].join(QUEUE_CONTEXT_SECTION_SEPARATOR);
378
+ if (capped.length <= cap) return capped;
379
+
380
+ return `${capped.slice(0, Math.max(0, cap - notice.length - 2)).trimEnd()}\n\n${notice}`;
381
+ }
382
+
383
+ function compactSectionForQueueBudget(section: string): string {
384
+ const lines = section.split("\n");
385
+ const compact: string[] = [];
386
+
387
+ if (lines[0]) compact.push(lines[0]);
388
+
389
+ const statusLine = lines.find(line => line.startsWith("**Status:**"));
390
+ if (statusLine) compact.push(statusLine);
391
+
392
+ const sourceLines = lines.filter(line => line.startsWith("Source: `"));
393
+ if (sourceLines.length > 0) {
394
+ compact.push("", "**Sources:**", ...sourceLines);
395
+ compact.push("", "[Artifact excerpts omitted due to total queue/rethink context budget.]");
396
+ }
397
+
398
+ return compact.join("\n");
321
399
  }
322
400
 
323
401
  // ─── Internal Helpers ───────────────────────────────────────────────────────
@@ -2,11 +2,12 @@
2
2
  // File Purpose: Always-on ambient health signal rendered below the editor.
3
3
 
4
4
  import type { ExtensionContext } from "@gsd/pi-coding-agent";
5
+ import { execFile } from "node:child_process";
5
6
  import type { GSDState } from "./types.js";
6
- import { runProviderChecks, summariseProviderIssues } from "./doctor-providers.js";
7
+ import { runProviderChecks, runProviderChecksAsync, summariseProviderIssues } from "./doctor-providers.js";
7
8
  import { runEnvironmentChecks, runEnvironmentChecksAsync } from "./doctor-environment.js";
8
9
  import { loadEffectiveGSDPreferences } from "./preferences.js";
9
- import { nativeIsRepo, nativeLastCommitEpoch, nativeGetCurrentBranch, nativeCommitSubject } from "./native-git-bridge.js";
10
+ import { GIT_NO_PROMPT_ENV } from "./git-constants.js";
10
11
  import { loadLedgerFromDisk, getProjectTotals } from "./metrics.js";
11
12
  import { describeNextUnit, estimateTimeRemaining, updateSliceProgressCache } from "./auto-dashboard.js";
12
13
  import { projectRoot } from "./commands/context.js";
@@ -15,32 +16,75 @@ import {
15
16
  buildHealthLines,
16
17
  detectHealthWidgetProjectState,
17
18
  type HealthWidgetData,
19
+ type HealthWidgetProjectState,
18
20
  } from "./health-widget-core.js";
19
21
 
20
22
  export const HEALTH_WIDGET_ACTIVE_HINTS =
21
23
  " /gsd auto to run · /gsd status to inspect · /gsd report for snapshots · /gsd notifications for history · /gsd help";
22
24
 
25
+ const LAST_COMMIT_LOOKUP_TIMEOUT_MS = 3_000;
26
+ const REFRESH_INTERVAL_MS = 60_000;
27
+ const PROJECT_STATE_CACHE_TTL_MS = REFRESH_INTERVAL_MS;
28
+
23
29
  // ── Data loader ────────────────────────────────────────────────────────────────
24
30
 
25
- // Last-commit lookup is subprocess-backed (native-git-bridge git spawns),
26
- // so it is treated like the other expensive checks: skipped on first paint,
27
- // run only by the background refresh.
28
- function loadLastCommitInfo(basePath: string): { epoch: number | null; message: string | null } {
31
+ const projectStateCache = new Map<string, { state: HealthWidgetProjectState; computedAt: number }>();
32
+
33
+ export function getCachedProjectState(basePath: string, force?: boolean): HealthWidgetProjectState {
34
+ const now = Date.now();
35
+ const cached = projectStateCache.get(basePath);
36
+ if (!force && cached && now - cached.computedAt <= PROJECT_STATE_CACHE_TTL_MS) {
37
+ return cached.state;
38
+ }
39
+
40
+ const state = detectHealthWidgetProjectState(basePath);
41
+ projectStateCache.set(basePath, { state, computedAt: now });
42
+ return state;
43
+ }
44
+
45
+ function runHealthWidgetGit(basePath: string, args: string[]): Promise<string | null> {
46
+ return new Promise((resolve) => {
47
+ const child = execFile(
48
+ "git",
49
+ args,
50
+ {
51
+ cwd: basePath,
52
+ timeout: LAST_COMMIT_LOOKUP_TIMEOUT_MS,
53
+ encoding: "utf-8",
54
+ env: GIT_NO_PROMPT_ENV,
55
+ },
56
+ (err, stdout) => resolve(err ? null : String(stdout).trimEnd()),
57
+ );
58
+ child.on("error", () => resolve(null));
59
+ });
60
+ }
61
+
62
+ async function loadLastCommitInfoAsync(basePath: string): Promise<{ epoch: number | null; message: string | null }> {
29
63
  try {
30
- if (nativeIsRepo(basePath)) {
31
- const branch = nativeGetCurrentBranch(basePath);
32
- const epoch = nativeLastCommitEpoch(basePath, branch || "HEAD");
33
- if (epoch > 0) {
34
- return { epoch, message: nativeCommitSubject(basePath, branch || "HEAD") || null };
35
- }
64
+ if ((await runHealthWidgetGit(basePath, ["rev-parse", "--git-dir"])) === null) {
65
+ return { epoch: null, message: null };
36
66
  }
37
- } catch { /* non-fatal */ }
38
- return { epoch: null, message: null };
67
+
68
+ const branch = await runHealthWidgetGit(basePath, ["branch", "--show-current"]);
69
+ const ref = branch || "HEAD";
70
+ const raw = await runHealthWidgetGit(basePath, ["log", "-1", "--format=%ct%x00%s", ref]);
71
+ if (!raw) return { epoch: null, message: null };
72
+
73
+ const separator = raw.indexOf("\0");
74
+ const epochText = separator >= 0 ? raw.slice(0, separator) : raw;
75
+ const epoch = parseInt(epochText.trim(), 10) || 0;
76
+ if (epoch <= 0) return { epoch: null, message: null };
77
+
78
+ const message = separator >= 0 ? raw.slice(separator + 1).trim() : "";
79
+ return { epoch, message: message || null };
80
+ } catch {
81
+ return { epoch: null, message: null };
82
+ }
39
83
  }
40
84
 
41
85
  function loadHealthWidgetData(
42
86
  basePath: string,
43
- options?: { includeChecks?: boolean },
87
+ options?: { includeChecks?: boolean; forceProjectState?: boolean },
44
88
  ): HealthWidgetData {
45
89
  // `includeChecks` gates the expensive subprocess-backed checks (provider +
46
90
  // environment doctor: `lsof`, `docker`, `node --version`, ...). The initial
@@ -55,7 +99,7 @@ function loadHealthWidgetData(
55
99
  let lastCommitEpoch: number | null = null;
56
100
  let lastCommitMessage: string | null = null;
57
101
 
58
- const projectState = detectHealthWidgetProjectState(basePath);
102
+ const projectState = getCachedProjectState(basePath, options?.forceProjectState);
59
103
 
60
104
  try {
61
105
  const prefs = loadEffectiveGSDPreferences();
@@ -83,13 +127,6 @@ function loadHealthWidgetData(
83
127
  } catch { /* non-fatal */ }
84
128
  }
85
129
 
86
- // ── Last commit info ── (git spawns — gated like the other expensive checks)
87
- if (includeChecks) {
88
- const commit = loadLastCommitInfo(basePath);
89
- lastCommitEpoch = commit.epoch;
90
- lastCommitMessage = commit.message;
91
- }
92
-
93
130
  return {
94
131
  projectState,
95
132
  budgetCeiling,
@@ -104,10 +141,8 @@ function loadHealthWidgetData(
104
141
  }
105
142
 
106
143
  // Non-blocking variant used by the widget's background refresh: the cheap fields
107
- // come from the synchronous snapshot, then provider + environment checks are
108
- // layered in off the event-loop critical path (env checks run concurrently via
109
- // runEnvironmentChecksAsync). Keeps the always-on widget from stalling the UI on
110
- // its initial enrichment or its 60s refresh.
144
+ // come from the synchronous snapshot, then provider, environment, and last-commit
145
+ // checks are layered in off the event-loop critical path.
111
146
  async function loadHealthWidgetDataAsync(basePath: string): Promise<HealthWidgetData> {
112
147
  const data = loadHealthWidgetData(basePath, { includeChecks: false });
113
148
  let providerIssue = data.providerIssue;
@@ -115,7 +150,7 @@ async function loadHealthWidgetDataAsync(basePath: string): Promise<HealthWidget
115
150
  let environmentWarningCount = 0;
116
151
 
117
152
  try {
118
- providerIssue = summariseProviderIssues(runProviderChecks());
153
+ providerIssue = summariseProviderIssues(await runProviderChecksAsync());
119
154
  } catch { /* non-fatal */ }
120
155
 
121
156
  try {
@@ -126,7 +161,7 @@ async function loadHealthWidgetDataAsync(basePath: string): Promise<HealthWidget
126
161
  }
127
162
  } catch { /* non-fatal */ }
128
163
 
129
- const commit = loadLastCommitInfo(basePath);
164
+ const commit = await loadLastCommitInfoAsync(basePath);
130
165
 
131
166
  return {
132
167
  ...data,
@@ -141,8 +176,6 @@ async function loadHealthWidgetDataAsync(basePath: string): Promise<HealthWidget
141
176
 
142
177
  // ── Widget init ────────────────────────────────────────────────────────────────
143
178
 
144
- const REFRESH_INTERVAL_MS = 60_000;
145
-
146
179
  /**
147
180
  * Initialize the always-on gsd-health widget (belowEditor).
148
181
  * Call once from the extension entry point after context is available.
@@ -152,13 +185,17 @@ export function initHealthWidget(ctx: ExtensionContext): void {
152
185
 
153
186
  const basePath = projectRoot();
154
187
 
188
+ // Re-init must reflect filesystem changes immediately; the TTL cache is for
189
+ // interval refreshes, not this one-off synchronous paint.
190
+ projectStateCache.delete(basePath);
191
+
155
192
  // String-array fallback — used in RPC mode (factory is a no-op there).
156
193
  // Skip the expensive provider/environment doctor checks here: this runs
157
194
  // synchronously on the interactive-startup path, where running them would
158
195
  // block first paint by ~0.9s (lsof/docker probes, otherwise run again
159
196
  // immediately by the factory below). The factory's async refresh fills in
160
197
  // real health once the screen is up.
161
- const initialData = loadHealthWidgetData(basePath, { includeChecks: false });
198
+ const initialData = loadHealthWidgetData(basePath, { includeChecks: false, forceProjectState: true });
162
199
  ctx.ui.setWidget("gsd-health", buildHealthLines(initialData), { placement: "belowEditor" });
163
200
 
164
201
  // Factory-based widget for TUI mode — replaces the string-array above
@@ -25,6 +25,7 @@ import {
25
25
  insertArtifact,
26
26
  deleteArtifactByPath,
27
27
  getGateResults,
28
+ isDbAvailable,
28
29
  } from "./gsd-db.js";
29
30
  import type { MilestoneRow, ArtifactRow } from "./db-milestone-artifact-rows.js";
30
31
  import type { SliceRow, TaskRow } from "./db-task-slice-rows.js";
@@ -737,7 +738,8 @@ function isAutoRecoveryPlaceholderPlan(content: string): boolean {
737
738
  * projection (the 4S/0T-vs-5S/13T drift class). The artifacts table is an
738
739
  * output sink, never a render input.
739
740
  *
740
- * @returns true if the plan was written, false on skip/error
741
+ * @returns true if the plan was written, false when the DB slice has no tasks
742
+ * @throws when the DB connection is unavailable or the render write fails
741
743
  */
742
744
  export async function renderPlanCheckboxes(
743
745
  basePath: string,
@@ -747,6 +749,9 @@ export async function renderPlanCheckboxes(
747
749
  ): Promise<boolean> {
748
750
  const tasks = getSliceTasks(milestoneId, sliceId);
749
751
  if (tasks.length === 0) {
752
+ if (!isDbAvailable()) {
753
+ throw new Error(`database unavailable while rendering plan checkboxes for ${milestoneId}/${sliceId}`);
754
+ }
750
755
  process.stderr.write(
751
756
  `markdown-renderer: no tasks found for ${milestoneId}/${sliceId}\n`,
752
757
  );
@@ -142,23 +142,53 @@ function getActiveDecisions(): DecisionRow[] {
142
142
  }
143
143
 
144
144
  /**
145
- * True when a memory row has a `structured_fields` JSON payload containing
146
- * the given `markerKey: "value"` pair. Matches the LIKE pattern used by
147
- * `backfillDecisionsToMemories` so the scanner is consistent with the
148
- * backfill's idempotency check.
145
+ * Source IDs already present in memory `structured_fields`. Collected with one
146
+ * pass over the memories table so the scanner does not perform a non-indexable
147
+ * LIKE probe for every decision and KNOWLEDGE.md row.
149
148
  */
150
- function memoryHasSourceMarker(markerKey: string, value: string): boolean {
151
- if (!isDbAvailable()) return false;
149
+ interface MemorySourceMarkers {
150
+ decisionIds: Set<string>;
151
+ knowledgeIds: Set<string>;
152
+ }
153
+
154
+ function emptyMemorySourceMarkers(): MemorySourceMarkers {
155
+ return { decisionIds: new Set(), knowledgeIds: new Set() };
156
+ }
157
+
158
+ function getMemorySourceMarkers(): MemorySourceMarkers {
159
+ const markers = emptyMemorySourceMarkers();
160
+ if (!isDbAvailable()) return markers;
152
161
  const adapter = _getAdapter();
153
- if (!adapter) return false;
162
+ if (!adapter) return markers;
163
+ try {
164
+ const rows = adapter
165
+ .prepare("SELECT structured_fields FROM memories WHERE structured_fields IS NOT NULL")
166
+ .all() as Array<Record<string, unknown>>;
167
+ for (const row of rows) {
168
+ collectMemorySourceMarker(markers, row["structured_fields"]);
169
+ }
170
+ } catch {
171
+ return markers;
172
+ }
173
+ return markers;
174
+ }
175
+
176
+ function collectMemorySourceMarker(markers: MemorySourceMarkers, raw: unknown): void {
177
+ if (typeof raw !== "string" || raw.length === 0) return;
154
178
  try {
155
- const pattern = `%"${markerKey}":"${value}"%`;
156
- const row = adapter
157
- .prepare("SELECT 1 FROM memories WHERE structured_fields LIKE :pattern LIMIT 1")
158
- .get({ ":pattern": pattern });
159
- return row !== undefined;
179
+ const parsed = JSON.parse(raw);
180
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return;
181
+ const fields = parsed as Record<string, unknown>;
182
+ const decisionId = fields["sourceDecisionId"];
183
+ if (typeof decisionId === "string" && decisionId.length > 0) {
184
+ markers.decisionIds.add(decisionId);
185
+ }
186
+ const knowledgeId = fields["sourceKnowledgeId"];
187
+ if (typeof knowledgeId === "string" && knowledgeId.length > 0) {
188
+ markers.knowledgeIds.add(knowledgeId);
189
+ }
160
190
  } catch {
161
- return false;
191
+ return;
162
192
  }
163
193
  }
164
194
 
@@ -174,10 +204,14 @@ const SAMPLE_LIMIT = 5;
174
204
  export function scanConsolidationGaps(basePath: string): ConsolidationGapReport {
175
205
  // ── Decisions ────────────────────────────────────────────────────────
176
206
  const decisions = getActiveDecisions();
207
+ const knowledgeRows = parseKnowledgeRows(knowledgeMdContent(basePath));
208
+ const memorySourceMarkers = decisions.length > 0 || knowledgeRows.length > 0
209
+ ? getMemorySourceMarkers()
210
+ : emptyMemorySourceMarkers();
177
211
  const decisionSamples: DecisionsSurfaceReport["samples"] = [];
178
212
  let decisionMigrated = 0;
179
213
  for (const decision of decisions) {
180
- if (memoryHasSourceMarker("sourceDecisionId", decision.id)) {
214
+ if (memorySourceMarkers.decisionIds.has(decision.id)) {
181
215
  decisionMigrated += 1;
182
216
  continue;
183
217
  }
@@ -190,16 +224,14 @@ export function scanConsolidationGaps(basePath: string): ConsolidationGapReport
190
224
  }
191
225
 
192
226
  // ── KNOWLEDGE.md ─────────────────────────────────────────────────────
193
- const knowledgeRows = parseKnowledgeRows(knowledgeMdContent(basePath));
194
227
  const knowledgeByTable = { rules: 0, patterns: 0, lessons: 0 };
195
228
  const knowledgeSamples: KnowledgeSurfaceReport["samples"] = [];
196
229
  let knowledgeMigrated = 0;
197
230
  for (const row of knowledgeRows) {
198
231
  knowledgeByTable[row.table] += 1;
199
- // Phase 6 will introduce a `sourceKnowledgeId` marker as part of the
200
- // KNOWLEDGE.md backfill. Until that path ships, this check returns
201
- // false for every row, which is the honest state of the consolidation.
202
- if (memoryHasSourceMarker("sourceKnowledgeId", row.id)) {
232
+ // KNOWLEDGE.md backfill writes `sourceKnowledgeId`; rows without that
233
+ // marker are still reported as consolidation gaps.
234
+ if (memorySourceMarkers.knowledgeIds.has(row.id)) {
203
235
  knowledgeMigrated += 1;
204
236
  continue;
205
237
  }