@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
@@ -19,6 +19,16 @@ import { resolveTasksDir } from "./paths.js";
19
19
 
20
20
  /** Large enough for unbounded milestone-history git log scans in big repos. */
21
21
  const GIT_LOG_MAX_BUFFER = 16 * 1024 * 1024;
22
+ const LOG_FIELD_SEPARATOR = "\x1f";
23
+ const LOG_RECORD_SEPARATOR = "\x1e";
24
+
25
+ type CommitRecord = {
26
+ hash: string;
27
+ parents: string;
28
+ committedAt: string;
29
+ message: string;
30
+ files: string[];
31
+ };
22
32
 
23
33
  /**
24
34
  * Check whether a milestone produced implementation artifacts (non-`.gsd/`
@@ -198,7 +208,7 @@ function getChangedFilesFromMilestoneTaggedCommits(
198
208
  // Primary: path-scoped log against .gsd/milestones/<id>. Fast and unbounded
199
209
  // by depth when .gsd/ is tracked in git.
200
210
  const scoped = scanGsdTaggedCommits(basePath, milestoneId, [
201
- "log", "--format=%H%x1f%B%x1e", "HEAD", "--", `.gsd/milestones/${milestoneId}`,
211
+ "log", "--full-diff", "--name-only", "--format=%x1e%H%x1f%B%x1f", "HEAD", "--", `.gsd/milestones/${milestoneId}`,
202
212
  ]);
203
213
  if (!scoped.ok) return scoped;
204
214
  if (scoped.matched && classifyImplementationFiles(scoped.files) === "present") return scoped;
@@ -213,7 +223,7 @@ function getChangedFilesFromMilestoneTaggedCommits(
213
223
  // reintroducing the rolling-depth failure class removed in #4699 where
214
224
  // milestone evidence aged out behind unrelated activity.
215
225
  const unscoped = scanGsdTaggedCommits(basePath, milestoneId, [
216
- "log", "--format=%H%x1f%B%x1e", "HEAD",
226
+ "log", "--name-only", "--format=%x1e%H%x1f%B%x1f", "HEAD",
217
227
  ]);
218
228
  if (!unscoped.ok) return scoped.matched ? scoped : unscoped;
219
229
  if (!unscoped.matched) return scoped;
@@ -299,8 +309,7 @@ function backfillChangedFilesFromUntaggedMilestoneCommits(
299
309
  if (record.parents.trim().split(/\s+/).filter(Boolean).length > 1) continue;
300
310
  if (commitMessageHasGsdTrailer(record.message)) continue;
301
311
 
302
- const commitFiles = getChangedFilesForCommit(basePath, record.hash);
303
- const implementationFiles = commitFiles.map(normalizeRepoPath).filter(isImplementationPath);
312
+ const implementationFiles = record.files.map(normalizeRepoPath).filter(isImplementationPath);
304
313
  if (implementationFiles.length === 0) continue;
305
314
  if (!implementationFiles.some((file) => hintSet.has(file))) continue;
306
315
 
@@ -323,25 +332,30 @@ function backfillChangedFilesFromUntaggedMilestoneCommits(
323
332
  }
324
333
  }
325
334
 
326
- function getCommitRecords(basePath: string): Array<{ hash: string; parents: string; committedAt: string; message: string }> {
327
- const logOutput = execFileSync("git", ["log", "--format=%H%x1f%P%x1f%cI%x1f%B%x1e", "HEAD"], {
335
+ function getCommitRecords(basePath: string): CommitRecord[] {
336
+ const logOutput = execFileSync("git", ["log", "--name-only", "--format=%x1e%H%x1f%P%x1f%cI%x1f%B%x1f", "HEAD"], {
328
337
  cwd: basePath,
329
338
  stdio: ["ignore", "pipe", "pipe"],
330
339
  encoding: "utf-8",
331
340
  maxBuffer: GIT_LOG_MAX_BUFFER,
332
341
  });
333
342
  return logOutput
334
- .split("\x1e")
335
- .map((record) => record.trim())
343
+ .split(LOG_RECORD_SEPARATOR)
336
344
  .filter(Boolean)
337
345
  .flatMap((record) => {
338
- const parts = record.split("\x1f");
339
- if (parts.length < 4) return [];
340
- const [hash, parents, committedAt, ...messageParts] = parts;
341
- return [{ hash: hash.trim(), parents: parents.trim(), committedAt: committedAt.trim(), message: messageParts.join("\x1f") }];
346
+ const parts = record.split(LOG_FIELD_SEPARATOR);
347
+ if (parts.length < 5) return [];
348
+ const [hash, parents, committedAt] = parts;
349
+ const files = parseNameOnlyFiles(parts.at(-1) ?? "");
350
+ const message = parts.slice(3, -1).join(LOG_FIELD_SEPARATOR);
351
+ return [{ hash: hash.trim(), parents: parents.trim(), committedAt: committedAt.trim(), message, files }];
342
352
  });
343
353
  }
344
354
 
355
+ function parseNameOnlyFiles(rawFiles: string): string[] {
356
+ return rawFiles.split(/\r?\n/).map((file) => file.trim()).filter(Boolean);
357
+ }
358
+
345
359
  function isFullCommitSha(value: string): boolean {
346
360
  return /^[0-9a-f]{40}$/i.test(value);
347
361
  }
@@ -359,23 +373,23 @@ function scanGsdTaggedCommits(
359
373
  maxBuffer: GIT_LOG_MAX_BUFFER,
360
374
  });
361
375
  const records = logOutput
362
- .split("\x1e")
363
- .map((record) => record.trim())
376
+ .split(LOG_RECORD_SEPARATOR)
364
377
  .filter(Boolean)
365
378
  .flatMap((record) => {
366
- const sep = record.indexOf("\x1f");
367
- if (sep === -1) return [];
368
- const hash = record.slice(0, sep).trim();
369
- const message = record.slice(sep + 1);
370
- return [{ hash, message }];
379
+ const parts = record.split(LOG_FIELD_SEPARATOR);
380
+ if (parts.length < 3) return [];
381
+ const hash = parts[0].trim();
382
+ if (!hash) return [];
383
+ const files = parseNameOnlyFiles(parts.at(-1) ?? "");
384
+ const message = parts.slice(1, -1).join(LOG_FIELD_SEPARATOR);
385
+ return [{ message, files }];
371
386
  });
372
387
 
373
388
  const files = new Set<string>();
374
389
  let matched = false;
375
- for (const { hash, message } of records) {
390
+ for (const { message, files: commitFiles } of records) {
376
391
  if (!commitMessageHasGsdTrailer(message)) continue;
377
392
 
378
- const commitFiles = getChangedFilesForCommit(basePath, hash);
379
393
  if (!commitMatchesMilestone(basePath, message, milestoneId, commitFiles)) continue;
380
394
 
381
395
  matched = true;
@@ -10,8 +10,8 @@
10
10
  */
11
11
 
12
12
  import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent";
13
- import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
14
- import { isAbsolute, join, relative } from "node:path";
13
+ import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
14
+ import { isAbsolute, join, relative, resolve } from "node:path";
15
15
  import { QUICK_BRANCH_RE } from "./branch-patterns.js";
16
16
  import { loadPrompt } from "./prompt-loader.js";
17
17
  import { gsdRoot } from "./paths.js";
@@ -31,6 +31,7 @@ interface QuickReturnState {
31
31
  }
32
32
 
33
33
  let pendingQuickReturn: QuickReturnState | null = null;
34
+ const pendingQuickReturnMisses = new Map<string, string>();
34
35
 
35
36
  // ─── Quick Task Helpers ───────────────────────────────────────────────────────
36
37
 
@@ -189,12 +190,35 @@ export function buildQuickCommitInstruction(basePath: string, root: string): str
189
190
  ].join("\n");
190
191
  }
191
192
 
193
+ function readHeadBranchName(basePath: string): string | null {
194
+ try {
195
+ const gitPath = join(basePath, ".git");
196
+ if (!existsSync(gitPath)) return null;
197
+
198
+ let headPath: string;
199
+ if (lstatSync(gitPath).isDirectory()) {
200
+ headPath = join(gitPath, "HEAD");
201
+ } else {
202
+ const gitFile = readFileSync(gitPath, "utf-8").trim();
203
+ if (!gitFile.startsWith("gitdir: ")) return null;
204
+ headPath = join(resolve(basePath, gitFile.slice("gitdir: ".length)), "HEAD");
205
+ }
206
+
207
+ const head = readFileSync(headPath, "utf-8").trim();
208
+ if (!head.startsWith("ref: refs/heads/")) return null;
209
+ return head.slice("ref: refs/heads/".length);
210
+ } catch {
211
+ return null;
212
+ }
213
+ }
214
+
192
215
  function quickReturnStatePath(basePath: string): string {
193
216
  return join(gsdRoot(basePath), "runtime", "quick-return.json");
194
217
  }
195
218
 
196
219
  function persistPendingReturn(state: QuickReturnState): void {
197
220
  pendingQuickReturn = state;
221
+ pendingQuickReturnMisses.delete(state.basePath);
198
222
  mkdirSync(join(gsdRoot(state.basePath), "runtime"), { recursive: true });
199
223
  writeFileSync(quickReturnStatePath(state.basePath), JSON.stringify(state) + "\n", "utf-8");
200
224
  }
@@ -203,6 +227,13 @@ function readPendingReturn(basePath: string): QuickReturnState | null {
203
227
  if (pendingQuickReturn && pendingQuickReturn.basePath === basePath) {
204
228
  return pendingQuickReturn;
205
229
  }
230
+ if (pendingQuickReturnMisses.has(basePath)) {
231
+ const statePath = quickReturnStatePath(basePath);
232
+ if (!existsSync(statePath) && readHeadBranchName(basePath) === pendingQuickReturnMisses.get(basePath)) {
233
+ return null;
234
+ }
235
+ pendingQuickReturnMisses.delete(basePath);
236
+ }
206
237
 
207
238
  try {
208
239
  const raw = readFileSync(quickReturnStatePath(basePath), "utf-8");
@@ -216,6 +247,7 @@ function readPendingReturn(basePath: string): QuickReturnState | null {
216
247
  && typeof parsed.description === "string"
217
248
  ) {
218
249
  pendingQuickReturn = parsed as QuickReturnState;
250
+ pendingQuickReturnMisses.delete(basePath);
219
251
  return pendingQuickReturn;
220
252
  }
221
253
  } catch {
@@ -225,9 +257,14 @@ function readPendingReturn(basePath: string): QuickReturnState | null {
225
257
  const inferred = inferQuickReturnFromBranch(basePath);
226
258
  if (inferred) {
227
259
  pendingQuickReturn = inferred;
260
+ pendingQuickReturnMisses.delete(basePath);
228
261
  return inferred;
229
262
  }
230
263
 
264
+ const branchAtMiss = readHeadBranchName(basePath);
265
+ if (branchAtMiss) {
266
+ pendingQuickReturnMisses.set(basePath, branchAtMiss);
267
+ }
231
268
  return null;
232
269
  }
233
270
 
@@ -235,6 +272,10 @@ function clearPendingReturn(basePath: string): void {
235
272
  if (pendingQuickReturn?.basePath === basePath) {
236
273
  pendingQuickReturn = null;
237
274
  }
275
+ const branchAtMiss = readHeadBranchName(basePath);
276
+ if (branchAtMiss) {
277
+ pendingQuickReturnMisses.set(basePath, branchAtMiss);
278
+ }
238
279
  rmSync(quickReturnStatePath(basePath), { force: true });
239
280
  }
240
281
 
@@ -223,11 +223,21 @@ export function extractTrace(entries: unknown[]): ExecutionTrace {
223
223
 
224
224
  // Flush any pending tool calls that never got results (crash mid-tool)
225
225
  for (const [, pending] of pendingTools) {
226
+ const missingResultError = `Tool call ${pending.name} started but no toolResult was recorded`;
226
227
  toolCalls.push({
227
228
  name: pending.name,
228
229
  input: redactInput(pending.name, pending.input),
229
- isError: false,
230
+ result: "missing tool result (stream/tool-call abort before execution)",
231
+ isError: true,
230
232
  });
233
+ errors.push(missingResultError);
234
+
235
+ // Mark the matching commandsRun entry as failed so it is consistent with
236
+ // the isError: true on the tool call above (bash/bg_shell only).
237
+ if (pending.name === "bash" || pending.name === "bg_shell") {
238
+ const lastCmd = findLast(commandsRun, c => c.command === String(pending.input.command));
239
+ if (lastCmd) lastCmd.failed = true;
240
+ }
231
241
  }
232
242
 
233
243
  return {
@@ -6,6 +6,9 @@
6
6
  // had zero callers in production code — wiring it through
7
7
  // reconcileBeforeDispatch closes that gap.
8
8
 
9
+ import { existsSync } from "node:fs";
10
+ import { join } from "node:path";
11
+
9
12
  import {
10
13
  detectStaleRenders,
11
14
  renderPlanCheckboxes,
@@ -13,8 +16,20 @@ import {
13
16
  renderSliceSummary,
14
17
  renderTaskSummary,
15
18
  } from "../../markdown-renderer.js";
16
- import { getMilestone, getMilestoneSlices, getSlice, getSliceTasks, setSliceSummaryMd } from "../../gsd-db.js";
17
- import { resolveSliceFile } from "../../paths.js";
19
+ import {
20
+ getMilestone,
21
+ getMilestoneSlices,
22
+ getSlice,
23
+ getSliceTasks,
24
+ isDbAvailable,
25
+ setSliceSummaryMd,
26
+ } from "../../gsd-db.js";
27
+ import {
28
+ getWorkflowDatabasePath,
29
+ openWorkflowDatabasePath,
30
+ refreshWorkflowDatabaseFromDisk,
31
+ } from "../../db-workspace.js";
32
+ import { gsdRoot, resolveSliceFile } from "../../paths.js";
18
33
  import type { GSDState } from "../../types.js";
19
34
  import { logWarning } from "../../workflow-logger.js";
20
35
  import type { DriftContext, DriftHandler, DriftRecord } from "../types.js";
@@ -107,10 +122,44 @@ function resolveRoadmapMilestoneIdFromPath(normPath: string): string {
107
122
  return fileMatch?.[1] ?? milestoneMatch[1];
108
123
  }
109
124
 
125
+ function expectedDbPathForStaleRenderRepair(basePath: string): string {
126
+ return join(gsdRoot(basePath), "gsd.db");
127
+ }
128
+
129
+ function ensureDbForStaleRenderRepair(basePath: string): boolean {
130
+ const dbPath = expectedDbPathForStaleRenderRepair(basePath);
131
+ if (isDbAvailable() && getWorkflowDatabasePath() === dbPath) return true;
132
+ if (!existsSync(dbPath)) return false;
133
+ try {
134
+ return openWorkflowDatabasePath(dbPath);
135
+ } catch (err) {
136
+ logWarning("reconcile", `stale-render repair could not reopen DB: ${(err as Error).message}`);
137
+ return false;
138
+ }
139
+ }
140
+
141
+ function retryDbForStaleRenderRepair(basePath: string): boolean {
142
+ const dbPath = expectedDbPathForStaleRenderRepair(basePath);
143
+ if (!existsSync(dbPath)) return false;
144
+ try {
145
+ if (isDbAvailable() && getWorkflowDatabasePath() === dbPath && refreshWorkflowDatabaseFromDisk()) {
146
+ return true;
147
+ }
148
+ return openWorkflowDatabasePath(dbPath);
149
+ } catch (err) {
150
+ logWarning("reconcile", `stale-render repair could not reopen DB: ${(err as Error).message}`);
151
+ return false;
152
+ }
153
+ }
154
+
110
155
  async function repairStaleRenderFromBasePath(
111
156
  record: StaleRenderDrift,
112
157
  basePath: string,
113
158
  ): Promise<void> {
159
+ if (!ensureDbForStaleRenderRepair(basePath)) {
160
+ throw new Error(`stale-render drift: database unavailable for repair (${basePath})`);
161
+ }
162
+
114
163
  const normPath = record.renderPath.replace(/\\/g, "/");
115
164
  const reason = record.reason;
116
165
 
@@ -136,12 +185,31 @@ async function repairStaleRenderFromBasePath(
136
185
  const sliceId = pathMatch[2] && pathMatch[3] && /^\d+$/.test(pathMatch[2])
137
186
  ? `S${String(parseInt(pathMatch[3]!, 10)).padStart(2, "0")}`
138
187
  : pathMatch[2]!;
139
- const wrote = await renderPlanCheckboxes(
140
- basePath,
141
- milestoneId,
142
- sliceId,
143
- record.renderPath,
144
- );
188
+ let wrote = false;
189
+ try {
190
+ wrote = await renderPlanCheckboxes(
191
+ basePath,
192
+ milestoneId,
193
+ sliceId,
194
+ record.renderPath,
195
+ );
196
+ } catch (err) {
197
+ if (!retryDbForStaleRenderRepair(basePath)) throw err;
198
+ wrote = await renderPlanCheckboxes(
199
+ basePath,
200
+ milestoneId,
201
+ sliceId,
202
+ record.renderPath,
203
+ );
204
+ }
205
+ if (!wrote && retryDbForStaleRenderRepair(basePath)) {
206
+ wrote = await renderPlanCheckboxes(
207
+ basePath,
208
+ milestoneId,
209
+ sliceId,
210
+ record.renderPath,
211
+ );
212
+ }
145
213
  if (!wrote) {
146
214
  throw new Error(
147
215
  `stale-render drift: plan re-render wrote nothing for ${milestoneId}/${pathMatch[2]} ` +
@@ -2,7 +2,7 @@
2
2
  // File Purpose: Tests auto-mode artifact verification and recovery behavior.
3
3
  import test, { afterEach } from "node:test";
4
4
  import assert from "node:assert/strict";
5
- import { mkdtempSync, mkdirSync, rmSync, writeFileSync, existsSync, readFileSync } from "node:fs";
5
+ import { chmodSync, mkdtempSync, mkdirSync, rmSync, writeFileSync, existsSync, readFileSync } from "node:fs";
6
6
  import { join } from "node:path";
7
7
  import { tmpdir } from "node:os";
8
8
  import { randomUUID } from "node:crypto";
@@ -964,6 +964,45 @@ function makeGitBase(): string {
964
964
  return base;
965
965
  }
966
966
 
967
+ function shellQuote(value: string): string {
968
+ return `'${value.replace(/'/g, "'\\''")}'`;
969
+ }
970
+
971
+ function withLoggedGitCommands<T>(base: string, action: () => T): { result: T; commands: string[] } {
972
+ const realGit = execFileSync("which", ["git"], {
973
+ encoding: "utf-8",
974
+ stdio: ["ignore", "pipe", "pipe"],
975
+ }).trim().split(/\r?\n/)[0];
976
+ if (!realGit) throw new Error("Unable to resolve git executable for invocation logging test");
977
+ const binDir = join(base, ".git-wrapper-bin");
978
+ const logFile = join(base, "git-invocations.log");
979
+ mkdirSync(binDir, { recursive: true });
980
+ const wrapper = join(binDir, "git");
981
+ writeFileSync(
982
+ wrapper,
983
+ [
984
+ "#!/usr/bin/env bash",
985
+ `printf '%s\\n' "$1" >> ${shellQuote(logFile)}`,
986
+ `exec ${shellQuote(realGit)} "$@"`,
987
+ "",
988
+ ].join("\n"),
989
+ );
990
+ chmodSync(wrapper, 0o755);
991
+
992
+ const originalPath = process.env.PATH;
993
+ process.env.PATH = originalPath ? `${binDir}:${originalPath}` : binDir;
994
+ try {
995
+ const result = action();
996
+ const commands = existsSync(logFile)
997
+ ? readFileSync(logFile, "utf-8").split(/\r?\n/).filter(Boolean)
998
+ : [];
999
+ return { result, commands };
1000
+ } finally {
1001
+ if (originalPath === undefined) delete process.env.PATH;
1002
+ else process.env.PATH = originalPath;
1003
+ }
1004
+ }
1005
+
967
1006
  test("hasImplementationArtifacts returns false when only .gsd/ files committed (#1703)", () => {
968
1007
  const base = makeGitBase();
969
1008
  try {
@@ -1344,6 +1383,77 @@ test("hasImplementationArtifacts finds implementation commits when .gsd/ is giti
1344
1383
  }
1345
1384
  });
1346
1385
 
1386
+ test("hasImplementationArtifacts scans GSD-tagged history without per-commit diff-tree forks (#892)", { skip: process.platform === "win32" }, () => {
1387
+ const base = makeGitBase();
1388
+ try {
1389
+ writeFileSync(join(base, ".git", "info", "exclude"), ".gsd/\n");
1390
+ mkdirSync(join(base, ".gsd", "milestones", "M001", "slices", "S01", "tasks"), { recursive: true });
1391
+ writeFileSync(
1392
+ join(base, ".gsd", "milestones", "M001", "slices", "S01", "tasks", "T01-SUMMARY.md"),
1393
+ "# Summary",
1394
+ );
1395
+
1396
+ mkdirSync(join(base, "src"), { recursive: true });
1397
+ for (let i = 0; i < 3; i++) {
1398
+ writeFileSync(join(base, "src", `feature-${i}.ts`), `export const feature${i} = true;\n`);
1399
+ execFileSync("git", ["add", "src"], { cwd: base, stdio: "ignore" });
1400
+ execFileSync(
1401
+ "git",
1402
+ ["commit", "-m", `feat: materialize M001 evidence ${i}\n\nGSD-Task: S01/T01`],
1403
+ { cwd: base, stdio: "ignore" },
1404
+ );
1405
+ }
1406
+
1407
+ const { result, commands } = withLoggedGitCommands(base, () => hasImplementationArtifacts(base, "M001"));
1408
+ assert.equal(result, "present", "milestone-tagged commits should still prove implementation evidence");
1409
+ assert.ok(commands.includes("log"), "milestone evidence fallback should scan history with git log");
1410
+ assert.equal(commands.filter((command) => command === "diff-tree").length, 0);
1411
+ } finally {
1412
+ cleanup(base);
1413
+ }
1414
+ });
1415
+
1416
+ test("hasImplementationArtifacts backfill scans commit records without per-commit diff-tree forks (#892)", { skip: process.platform === "win32" }, () => {
1417
+ const base = makeGitBase();
1418
+ try {
1419
+ mkdirSync(join(base, ".gsd"), { recursive: true });
1420
+ openDatabase(join(base, ".gsd", "gsd.db"));
1421
+ insertMilestone({ id: "M001", title: "Milestone One", status: "active" });
1422
+ insertSlice({
1423
+ id: "S01",
1424
+ milestoneId: "M001",
1425
+ title: "Slice One",
1426
+ status: "complete",
1427
+ risk: "low",
1428
+ depends: [],
1429
+ });
1430
+ insertTask({
1431
+ id: "T01",
1432
+ sliceId: "S01",
1433
+ milestoneId: "M001",
1434
+ title: "Task One",
1435
+ status: "complete",
1436
+ keyFiles: ["src/expected-0.ts", "src/expected-1.ts"],
1437
+ planning: { files: ["src/expected-0.ts", "src/expected-1.ts"] },
1438
+ });
1439
+
1440
+ mkdirSync(join(base, "src"), { recursive: true });
1441
+ for (let i = 0; i < 2; i++) {
1442
+ writeFileSync(join(base, "src", `expected-${i}.ts`), `export const expected${i} = true;\n`);
1443
+ execFileSync("git", ["add", "src"], { cwd: base, stdio: "ignore" });
1444
+ execFileSync("git", ["commit", "-m", `feat: untagged implementation ${i}`], { cwd: base, stdio: "ignore" });
1445
+ }
1446
+
1447
+ const { result, commands } = withLoggedGitCommands(base, () => hasImplementationArtifacts(base, "M001"));
1448
+ assert.equal(result, "present", "completed task file hints should still backfill untagged commits");
1449
+ assert.equal(getMilestoneCommitAttributionShas("M001").length, 2);
1450
+ assert.ok(commands.includes("log"), "backfill should scan commit records with git log");
1451
+ assert.equal(commands.filter((command) => command === "diff-tree").length, 0);
1452
+ } finally {
1453
+ cleanup(base);
1454
+ }
1455
+ });
1456
+
1347
1457
  test("hasImplementationArtifacts binds GSD-Task trailer to milestone via DB state when .gsd/ is gitignored", () => {
1348
1458
  const base = makeGitBase();
1349
1459
  try {
@@ -101,6 +101,32 @@ test("analyzeSessionContext buckets injections, tool results, and loaded skills"
101
101
  assert.equal(result.subagentSpawns, 1);
102
102
  });
103
103
 
104
+ test("analyzeSessionContext bounds large assistant tool-call arguments", () => {
105
+ const result = analyzeSessionContext(sessionEntries({
106
+ role: "assistant",
107
+ content: [
108
+ {
109
+ type: "toolCall",
110
+ id: "tc-save",
111
+ name: "gsd_summary_save",
112
+ arguments: {
113
+ path: ".gsd/milestones/M001/M001-SUMMARY.md",
114
+ artifact_type: "SUMMARY",
115
+ content: "x".repeat(20_000),
116
+ },
117
+ },
118
+ ],
119
+ timestamp: TS,
120
+ }), PROVIDER);
121
+
122
+ const assistant = result.conversationSections.find((section) => section.label === "Assistant responses");
123
+ assert.ok(assistant);
124
+ assert.ok(
125
+ assistant.tokens < 200,
126
+ `assistant tool-call arguments should be redacted before token counting, got ${assistant.tokens} tokens`,
127
+ );
128
+ });
129
+
104
130
  test("formatContextReport lists skills and subagents", () => {
105
131
  const report = buildContextBreakdown({
106
132
  modelLabel: "claude-code/claude-sonnet-4-6",
@@ -1,10 +1,22 @@
1
1
  import test from "node:test";
2
2
  import assert from "node:assert/strict";
3
+ import { execFileSync } from "node:child_process";
4
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
5
+ import { tmpdir } from "node:os";
6
+ import { join } from "node:path";
3
7
 
4
8
  import {
5
9
  formatCleanKeepReason,
10
+ handleWorktree,
6
11
  type WorktreeStatus,
7
12
  } from "../commands-worktree.ts";
13
+ import { withCommandCwd } from "../commands/context.ts";
14
+ import { createWorktree } from "../worktree-manager.ts";
15
+ import {
16
+ disableDebug,
17
+ enableDebug,
18
+ getDebugCounters,
19
+ } from "../debug-logger.ts";
8
20
 
9
21
  function mkStatus(over: Partial<WorktreeStatus>): WorktreeStatus {
10
22
  const name = over.name ?? "feat-x";
@@ -22,6 +34,45 @@ function mkStatus(over: Partial<WorktreeStatus>): WorktreeStatus {
22
34
  };
23
35
  }
24
36
 
37
+ function git(cwd: string, args: string[]): string {
38
+ return execFileSync("git", args, {
39
+ cwd,
40
+ stdio: ["ignore", "pipe", "pipe"],
41
+ encoding: "utf-8",
42
+ }).trim();
43
+ }
44
+
45
+ function makeRepo(): string {
46
+ const base = mkdtempSync(join(tmpdir(), "gsd-worktree-command-"));
47
+ git(base, ["init", "-b", "main"]);
48
+ git(base, ["config", "user.name", "Test User"]);
49
+ git(base, ["config", "user.email", "test@example.com"]);
50
+ mkdirSync(join(base, ".gsd"), { recursive: true });
51
+ writeFileSync(join(base, "README.md"), "# Test\n", "utf-8");
52
+ git(base, ["add", "."]);
53
+ git(base, ["commit", "-m", "chore: init"]);
54
+ return base;
55
+ }
56
+
57
+ function createCommittedWorktree(base: string, name: string): void {
58
+ const wt = createWorktree(base, name);
59
+ writeFileSync(join(wt.path, `${name}.txt`), `${name}\n`, "utf-8");
60
+ git(wt.path, ["add", "."]);
61
+ git(wt.path, ["commit", "-m", `feat: ${name}`]);
62
+ }
63
+
64
+ function createMockCtx() {
65
+ const notifications: { message: string; level: string }[] = [];
66
+ return {
67
+ notifications,
68
+ ui: {
69
+ notify(message: string, level: string) {
70
+ notifications.push({ message, level });
71
+ },
72
+ },
73
+ };
74
+ }
75
+
25
76
  test("clean keep reason shows uncommitted-only worktrees clearly", () => {
26
77
  const reason = formatCleanKeepReason(mkStatus({ uncommitted: true }));
27
78
  assert.equal(reason, "uncommitted changes");
@@ -46,3 +97,32 @@ test("clean keep reason uses singular form for a single changed file", () => {
46
97
  const reason = formatCleanKeepReason(mkStatus({ filesChanged: 1, uncommitted: false }));
47
98
  assert.equal(reason, "1 changed file");
48
99
  });
100
+
101
+ test("worktree list detects main branch once for the command", async (t) => {
102
+ if (process.env.GSD_ENABLE_NATIVE_GSD_GIT === "1") {
103
+ t.skip("git invocation regression is specific to the CLI fallback path");
104
+ return;
105
+ }
106
+
107
+ const base = makeRepo();
108
+ try {
109
+ createCommittedWorktree(base, "feature-a");
110
+ createCommittedWorktree(base, "feature-b");
111
+
112
+ const ctx = createMockCtx();
113
+ enableDebug(base);
114
+ try {
115
+ await withCommandCwd(base, async () => {
116
+ await handleWorktree("list", ctx as any);
117
+ });
118
+
119
+ assert.equal(ctx.notifications.length, 1);
120
+ assert.match(ctx.notifications[0].message, /Worktrees — 2/);
121
+ assert.equal(getDebugCounters().gitInvocations, 12);
122
+ } finally {
123
+ disableDebug();
124
+ }
125
+ } finally {
126
+ rmSync(base, { recursive: true, force: true });
127
+ }
128
+ });
@@ -203,6 +203,10 @@ console.log('\n=== complete-slice: handler happy path ===');
203
203
  insertSlice({ id: 'S02', milestoneId: 'M001', title: 'Second Slice', risk: 'low', depends: ['S01'], demo: 'advanced stuff', sequence: 2 });
204
204
  insertTask({ id: 'T01', sliceId: 'S01', milestoneId: 'M001', status: 'complete', title: 'Task 1' });
205
205
  insertTask({ id: 'T02', sliceId: 'S01', milestoneId: 'M001', status: 'complete', title: 'Task 2' });
206
+ insertTask({ id: 'T99', sliceId: 'S02', milestoneId: 'M001', status: 'complete', title: 'Sibling Task' });
207
+ const siblingSummaryPath = path.join(path.dirname(roadmapPath), 'T99-SUMMARY.md');
208
+ const siblingSummaryBefore = '# Existing sibling task summary\n\nDo not rewrite this file.\n';
209
+ fs.writeFileSync(siblingSummaryPath, siblingSummaryBefore);
206
210
 
207
211
  const params = makeValidSliceParams();
208
212
  const result = await handleCompleteSlice(params, basePath);
@@ -267,6 +271,13 @@ console.log('\n=== complete-slice: handler happy path ===');
267
271
  // (e) Verify slice status is complete in DB
268
272
  assertEq(sliceAfter!.status, 'complete', 'slice status should be complete in DB');
269
273
  assertTrue(sliceAfter!.completed_at !== null, 'completed_at should be set in DB');
274
+
275
+ // (f) Verify unrelated completed task summaries are not re-rendered during slice completion
276
+ assertEq(
277
+ fs.readFileSync(siblingSummaryPath, 'utf-8'),
278
+ siblingSummaryBefore,
279
+ 'complete-slice should not re-render sibling completed task summaries',
280
+ );
270
281
  }
271
282
 
272
283
  cleanupDir(basePath);