gsd-pi 2.80.0-dev.e146beb20 → 2.80.0-dev.e6c48c3af

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 (218) hide show
  1. package/README.md +4 -2
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/extensions/gsd/auto/phases.js +59 -21
  4. package/dist/resources/extensions/gsd/auto/resolve.js +17 -0
  5. package/dist/resources/extensions/gsd/auto/run-unit.js +17 -2
  6. package/dist/resources/extensions/gsd/auto-direct-dispatch.js +1 -1
  7. package/dist/resources/extensions/gsd/auto-prompts.js +13 -1
  8. package/dist/resources/extensions/gsd/auto-recovery.js +43 -1
  9. package/dist/resources/extensions/gsd/auto-supervisor.js +8 -1
  10. package/dist/resources/extensions/gsd/auto-timeout-recovery.js +2 -2
  11. package/dist/resources/extensions/gsd/auto.js +84 -5
  12. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +21 -2
  13. package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +27 -20
  14. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +75 -4
  15. package/dist/resources/extensions/gsd/clean-root-preflight.js +24 -6
  16. package/dist/resources/extensions/gsd/context-budget.js +37 -2
  17. package/dist/resources/extensions/gsd/db/unit-dispatches.js +39 -0
  18. package/dist/resources/extensions/gsd/db-base-schema.js +4 -2
  19. package/dist/resources/extensions/gsd/db-migration-steps.js +6 -0
  20. package/dist/resources/extensions/gsd/git-service.js +36 -4
  21. package/dist/resources/extensions/gsd/gsd-db.js +46 -13
  22. package/dist/resources/extensions/gsd/guided-flow.js +33 -4
  23. package/dist/resources/extensions/gsd/memory-store.js +69 -12
  24. package/dist/resources/extensions/gsd/migrate/command.js +40 -1
  25. package/dist/resources/extensions/gsd/migration-auto-check.js +87 -0
  26. package/dist/resources/extensions/gsd/pre-execution-checks.js +7 -0
  27. package/dist/resources/extensions/gsd/prompt-loader.js +28 -2
  28. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +16 -13
  29. package/dist/resources/extensions/gsd/prompts/parallel-research-slices.md +1 -1
  30. package/dist/resources/extensions/gsd/prompts/quick-task.md +1 -5
  31. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +2 -2
  32. package/dist/resources/extensions/gsd/quick.js +34 -2
  33. package/dist/resources/extensions/gsd/tools/context-mode-tool-result.js +15 -0
  34. package/dist/resources/extensions/gsd/tools/exec-search-tool.js +5 -0
  35. package/dist/resources/extensions/gsd/tools/exec-tool.js +3 -15
  36. package/dist/resources/extensions/gsd/tools/memory-tools.js +1 -0
  37. package/dist/resources/extensions/gsd/tools/resume-tool.js +5 -0
  38. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +1 -1
  39. package/dist/resources/extensions/gsd/unit-context-composer.js +12 -3
  40. package/dist/resources/extensions/gsd/unit-runtime.js +11 -0
  41. package/dist/resources/extensions/gsd/worktree-resolver.js +33 -17
  42. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  43. package/dist/web/standalone/.next/BUILD_ID +1 -1
  44. package/dist/web/standalone/.next/app-path-routes-manifest.json +16 -16
  45. package/dist/web/standalone/.next/build-manifest.json +2 -2
  46. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  47. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  48. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  56. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +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 +16 -16
  71. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  72. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  73. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  74. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  75. package/package.json +3 -3
  76. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  77. package/packages/mcp-server/dist/workflow-tools.js +22 -17
  78. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  79. package/packages/mcp-server/src/workflow-tools.test.ts +75 -2
  80. package/packages/mcp-server/src/workflow-tools.ts +30 -16
  81. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  82. package/packages/native/tsconfig.tsbuildinfo +1 -1
  83. package/packages/pi-coding-agent/dist/core/agent-session-abort-order.test.js +32 -0
  84. package/packages/pi-coding-agent/dist/core/agent-session-abort-order.test.js.map +1 -1
  85. package/packages/pi-coding-agent/dist/core/agent-session-tool-refresh.test.js +15 -0
  86. package/packages/pi-coding-agent/dist/core/agent-session-tool-refresh.test.js.map +1 -1
  87. package/packages/pi-coding-agent/dist/core/agent-session.d.ts +2 -0
  88. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  89. package/packages/pi-coding-agent/dist/core/agent-session.js +12 -3
  90. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  91. package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js +3 -1
  92. package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js.map +1 -1
  93. package/packages/pi-coding-agent/dist/core/compaction/compaction.d.ts +11 -0
  94. package/packages/pi-coding-agent/dist/core/compaction/compaction.d.ts.map +1 -1
  95. package/packages/pi-coding-agent/dist/core/compaction/compaction.js +9 -0
  96. package/packages/pi-coding-agent/dist/core/compaction/compaction.js.map +1 -1
  97. package/packages/pi-coding-agent/dist/core/compaction-threshold.test.d.ts +2 -0
  98. package/packages/pi-coding-agent/dist/core/compaction-threshold.test.d.ts.map +1 -0
  99. package/packages/pi-coding-agent/dist/core/compaction-threshold.test.js +103 -0
  100. package/packages/pi-coding-agent/dist/core/compaction-threshold.test.js.map +1 -0
  101. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +3 -0
  102. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
  103. package/packages/pi-coding-agent/dist/core/extensions/runner.js +3 -0
  104. package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
  105. package/packages/pi-coding-agent/dist/core/extensions/runner.test.js +2 -0
  106. package/packages/pi-coding-agent/dist/core/extensions/runner.test.js.map +1 -1
  107. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +12 -0
  108. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  109. package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  110. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +20 -0
  111. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  112. package/packages/pi-coding-agent/dist/core/settings-manager.js +25 -0
  113. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  114. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts +1 -0
  115. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  116. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +3 -0
  117. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  118. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  119. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +13 -5
  120. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  121. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.js +53 -0
  122. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.js.map +1 -1
  123. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  124. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +3 -0
  125. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  126. package/packages/pi-coding-agent/src/core/agent-session-abort-order.test.ts +36 -0
  127. package/packages/pi-coding-agent/src/core/agent-session-tool-refresh.test.ts +18 -0
  128. package/packages/pi-coding-agent/src/core/agent-session.ts +14 -3
  129. package/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts +3 -1
  130. package/packages/pi-coding-agent/src/core/compaction/compaction.ts +18 -0
  131. package/packages/pi-coding-agent/src/core/compaction-threshold.test.ts +121 -0
  132. package/packages/pi-coding-agent/src/core/extensions/runner.test.ts +2 -0
  133. package/packages/pi-coding-agent/src/core/extensions/runner.ts +5 -0
  134. package/packages/pi-coding-agent/src/core/extensions/types.ts +12 -0
  135. package/packages/pi-coding-agent/src/core/settings-manager.ts +39 -1
  136. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +4 -0
  137. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.test.ts +56 -0
  138. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +22 -7
  139. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +3 -0
  140. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  141. package/packages/pi-tui/dist/tui.d.ts.map +1 -1
  142. package/packages/pi-tui/dist/tui.js +18 -8
  143. package/packages/pi-tui/dist/tui.js.map +1 -1
  144. package/packages/pi-tui/src/tui.ts +20 -8
  145. package/packages/pi-tui/tsconfig.tsbuildinfo +1 -1
  146. package/src/resources/extensions/gsd/auto/loop-deps.ts +2 -2
  147. package/src/resources/extensions/gsd/auto/phases.ts +85 -35
  148. package/src/resources/extensions/gsd/auto/resolve.ts +23 -1
  149. package/src/resources/extensions/gsd/auto/run-unit.ts +22 -2
  150. package/src/resources/extensions/gsd/auto-direct-dispatch.ts +1 -1
  151. package/src/resources/extensions/gsd/auto-prompts.ts +17 -1
  152. package/src/resources/extensions/gsd/auto-recovery.ts +54 -0
  153. package/src/resources/extensions/gsd/auto-supervisor.ts +7 -0
  154. package/src/resources/extensions/gsd/auto-timeout-recovery.ts +2 -2
  155. package/src/resources/extensions/gsd/auto.ts +96 -4
  156. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +21 -1
  157. package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +27 -19
  158. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +88 -4
  159. package/src/resources/extensions/gsd/clean-root-preflight.ts +32 -7
  160. package/src/resources/extensions/gsd/context-budget.ts +44 -2
  161. package/src/resources/extensions/gsd/db/unit-dispatches.ts +41 -0
  162. package/src/resources/extensions/gsd/db-base-schema.ts +4 -2
  163. package/src/resources/extensions/gsd/db-migration-steps.ts +8 -0
  164. package/src/resources/extensions/gsd/git-service.ts +46 -8
  165. package/src/resources/extensions/gsd/gsd-db.ts +50 -13
  166. package/src/resources/extensions/gsd/guided-flow.ts +49 -4
  167. package/src/resources/extensions/gsd/memory-store.ts +77 -12
  168. package/src/resources/extensions/gsd/migrate/command.ts +47 -1
  169. package/src/resources/extensions/gsd/migration-auto-check.ts +129 -0
  170. package/src/resources/extensions/gsd/pre-execution-checks.ts +7 -0
  171. package/src/resources/extensions/gsd/preferences-types.ts +1 -1
  172. package/src/resources/extensions/gsd/prompt-loader.ts +27 -2
  173. package/src/resources/extensions/gsd/prompts/complete-milestone.md +16 -13
  174. package/src/resources/extensions/gsd/prompts/parallel-research-slices.md +1 -1
  175. package/src/resources/extensions/gsd/prompts/quick-task.md +1 -5
  176. package/src/resources/extensions/gsd/prompts/validate-milestone.md +2 -2
  177. package/src/resources/extensions/gsd/quick.ts +37 -2
  178. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +215 -1
  179. package/src/resources/extensions/gsd/tests/auto-phases-lifecycle.test.ts +56 -13
  180. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +14 -1
  181. package/src/resources/extensions/gsd/tests/auto-wrapup-inflight-guard.test.ts +166 -4
  182. package/src/resources/extensions/gsd/tests/clean-root-preflight.test.ts +15 -6
  183. package/src/resources/extensions/gsd/tests/compaction-snapshot.test.ts +14 -1
  184. package/src/resources/extensions/gsd/tests/context-budget.test.ts +10 -1
  185. package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +5 -1
  186. package/src/resources/extensions/gsd/tests/dispatch-rule-coverage.test.ts +313 -0
  187. package/src/resources/extensions/gsd/tests/exec-history.test.ts +15 -0
  188. package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +65 -0
  189. package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +54 -0
  190. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +239 -1
  191. package/src/resources/extensions/gsd/tests/memory-decay-factor.test.ts +90 -0
  192. package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +48 -0
  193. package/src/resources/extensions/gsd/tests/migration-auto-check.test.ts +127 -0
  194. package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +38 -0
  195. package/src/resources/extensions/gsd/tests/prompt-path-audit.test.ts +40 -0
  196. package/src/resources/extensions/gsd/tests/prompt-step-ordering.test.ts +19 -0
  197. package/src/resources/extensions/gsd/tests/quick-external-gsd.test.ts +40 -0
  198. package/src/resources/extensions/gsd/tests/schema-v27-v28-sequence.test.ts +156 -0
  199. package/src/resources/extensions/gsd/tests/signal-handlers.test.ts +27 -0
  200. package/src/resources/extensions/gsd/tests/stalled-tool-recovery.test.ts +49 -1
  201. package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +55 -0
  202. package/src/resources/extensions/gsd/tests/status-db-open.test.ts +9 -0
  203. package/src/resources/extensions/gsd/tests/unit-context-composer.test.ts +136 -4
  204. package/src/resources/extensions/gsd/tests/unit-dispatches.test.ts +30 -0
  205. package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +30 -0
  206. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +3 -0
  207. package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +63 -1
  208. package/src/resources/extensions/gsd/tools/context-mode-tool-result.ts +25 -0
  209. package/src/resources/extensions/gsd/tools/exec-search-tool.ts +7 -7
  210. package/src/resources/extensions/gsd/tools/exec-tool.ts +4 -23
  211. package/src/resources/extensions/gsd/tools/memory-tools.ts +1 -0
  212. package/src/resources/extensions/gsd/tools/resume-tool.ts +7 -7
  213. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +1 -1
  214. package/src/resources/extensions/gsd/unit-context-composer.ts +19 -4
  215. package/src/resources/extensions/gsd/unit-runtime.ts +11 -0
  216. package/src/resources/extensions/gsd/worktree-resolver.ts +36 -15
  217. /package/dist/web/standalone/.next/static/{y73quA-XdLo9n41nxphjW → 4dQ9NTZJ8pEvFwBgDUX93}/_buildManifest.js +0 -0
  218. /package/dist/web/standalone/.next/static/{y73quA-XdLo9n41nxphjW → 4dQ9NTZJ8pEvFwBgDUX93}/_ssgManifest.js +0 -0
@@ -3,13 +3,13 @@
3
3
  *
4
4
  * #2909: Adds a fast-path git status check before milestone completion merges.
5
5
  * When the working tree is dirty the user is warned and changes are auto-stashed
6
- * so the merge can proceed cleanly. After the merge completes, postflightPopStash
7
- * restores the stashed changes.
6
+ * so the merge can proceed cleanly. After the merge completes, postflightPopStash
7
+ * restores the stashed changes and reports whether manual recovery is needed.
8
8
  *
9
9
  * Design constraints (from Trek-e approval):
10
10
  * - Warn the user before stashing (no silent surprises)
11
11
  * - git stash push / git stash pop only — no custom stash management layer
12
- * - Stash/pop errors are logged but MUST NOT block the merge
12
+ * - Stash/pop errors are logged but MUST NOT block the merge itself
13
13
  * - Fast-path status check — clean trees pay no extra cost
14
14
  */
15
15
 
@@ -27,6 +27,13 @@ export interface PreflightResult {
27
27
  summary: string;
28
28
  }
29
29
 
30
+ export interface PostflightResult {
31
+ restored: boolean;
32
+ needsManualRecovery: boolean;
33
+ message: string;
34
+ stashRef?: string;
35
+ }
36
+
30
37
  function findPreflightStashRef(basePath: string, milestoneId: string, stashMarker?: string): string | null {
31
38
  const markerPrefix = `gsd-preflight-stash:${milestoneId}:`;
32
39
  let fallbackRef: string | null = null;
@@ -112,14 +119,15 @@ export function preflightCleanRoot(
112
119
  *
113
120
  * Only called when preflightCleanRoot returned stashPushed=true.
114
121
  * Any pop error (e.g. conflict) is logged and notified but does NOT throw —
115
- * the merge already completed successfully.
122
+ * the merge already completed successfully. Callers must treat
123
+ * needsManualRecovery=true as a dirty workspace stop, not a clean completion.
116
124
  */
117
125
  export function postflightPopStash(
118
126
  basePath: string,
119
127
  milestoneId: string,
120
128
  stashMarker: string | undefined,
121
129
  notify: (message: string, level: "info" | "warning" | "error") => void,
122
- ): void {
130
+ ): PostflightResult {
123
131
  let stashRef: string | null = null;
124
132
  try {
125
133
  stashRef = findPreflightStashRef(basePath, milestoneId, stashMarker);
@@ -127,7 +135,11 @@ export function postflightPopStash(
127
135
  const msg = `No matching GSD preflight stash found for milestone ${milestoneId}; leaving stash list untouched.`;
128
136
  logWarning("preflight", msg);
129
137
  notify(msg, "warning");
130
- return;
138
+ return {
139
+ restored: false,
140
+ needsManualRecovery: true,
141
+ message: msg,
142
+ };
131
143
  }
132
144
  execFileSync("git", ["stash", "pop", stashRef], {
133
145
  cwd: basePath,
@@ -135,7 +147,14 @@ export function postflightPopStash(
135
147
  encoding: "utf-8",
136
148
  env: GIT_NO_PROMPT_ENV,
137
149
  });
138
- notify(`Restored stashed changes after milestone ${milestoneId} merge.`, "info");
150
+ const msg = `Restored stashed changes after milestone ${milestoneId} merge.`;
151
+ notify(msg, "info");
152
+ return {
153
+ restored: true,
154
+ needsManualRecovery: false,
155
+ message: msg,
156
+ stashRef,
157
+ };
139
158
  } catch (err) {
140
159
  // Pop conflicts mean the merged code collides with the stashed changes.
141
160
  // Log a warning — the user needs to resolve manually, but the merge succeeded.
@@ -145,5 +164,11 @@ export function postflightPopStash(
145
164
  const msg = `git stash pop ${stashRef ?? ""}`.trim() + ` failed after merge of milestone ${milestoneId}: ${err instanceof Error ? err.message : String(err)}. ${restoreHint}`;
146
165
  logWarning("preflight", msg);
147
166
  notify(msg, "warning");
167
+ return {
168
+ restored: false,
169
+ needsManualRecovery: true,
170
+ message: msg,
171
+ ...(stashRef ? { stashRef } : {}),
172
+ };
148
173
  }
149
174
  }
@@ -8,7 +8,12 @@
8
8
  * @see D001 (module location), D002 (200K fallback), D003 (section-boundary truncation)
9
9
  */
10
10
 
11
- import { type TokenProvider, getCharsPerToken } from "./token-counter.js";
11
+ import {
12
+ type TokenProvider,
13
+ getCharsPerToken,
14
+ isAccurateCountingAvailable,
15
+ countTokensSync,
16
+ } from "./token-counter.js";
12
17
 
13
18
  // ─── Budget ratio constants ──────────────────────────────────────────────────
14
19
  // Percentages of total context window allocated to each budget category.
@@ -32,6 +37,24 @@ const DEFAULT_CONTEXT_WINDOW = 200_000;
32
37
  /** Conservative effective context for Claude Code subscription routing (#4676) */
33
38
  const CLAUDE_CODE_EFFECTIVE_CONTEXT_WINDOW = 200_000;
34
39
 
40
+ /**
41
+ * Cached empirical chars-per-token from a tiktoken probe, keyed by provider.
42
+ * countTokensSync's fallback path is provider-aware, so we cache per-provider
43
+ * to preserve that distinction once the encoder warms. The cl100k_base encoder
44
+ * itself gives a stable ratio for ASCII English so a single probe per provider
45
+ * key is sufficient. Empty map means "not yet probed" or "encoder unavailable".
46
+ */
47
+ const _empiricalCharsPerTokenByProvider = new Map<string, number>();
48
+
49
+ /**
50
+ * Test hook — clears the empirical chars-per-token cache so test cases that
51
+ * assert against the static char-ratio fallback aren't polluted by a prior
52
+ * tiktoken-warmed run in the same process. Production code must not call this.
53
+ */
54
+ export function _resetEmpiricalCacheForTest(): void {
55
+ _empiricalCharsPerTokenByProvider.clear();
56
+ }
57
+
35
58
  /** Percentage of context consumed before suggesting a continue-here checkpoint */
36
59
  const CONTINUE_THRESHOLD_PERCENT = 70;
37
60
 
@@ -101,7 +124,26 @@ export interface MinimalPreferences {
101
124
  export function computeBudgets(contextWindow: number, provider?: TokenProvider): BudgetAllocation {
102
125
  const effectiveWindow = contextWindow > 0 ? contextWindow : DEFAULT_CONTEXT_WINDOW;
103
126
  const charsPerToken = provider ? getCharsPerToken(provider) : CHARS_PER_TOKEN;
104
- const totalChars = effectiveWindow * charsPerToken;
127
+
128
+ // Prefer the tiktoken encoder for total-char estimation when it has been
129
+ // warmed (initTokenCounter resolved). The cl100k_base ratio is stable for
130
+ // ASCII English, so probe once per provider and cache — computeBudgets is
131
+ // called multiple times per prompt build and the probe encode is otherwise
132
+ // wasted work.
133
+ let totalChars: number;
134
+ if (isAccurateCountingAvailable()) {
135
+ const providerKey = provider ?? "__default__";
136
+ let empirical = _empiricalCharsPerTokenByProvider.get(providerKey);
137
+ if (empirical === undefined) {
138
+ const probe = "the quick brown fox jumps over the lazy dog ".repeat(64);
139
+ const probeTokens = countTokensSync(probe, provider);
140
+ empirical = probeTokens > 0 ? probe.length / probeTokens : charsPerToken;
141
+ _empiricalCharsPerTokenByProvider.set(providerKey, empirical);
142
+ }
143
+ totalChars = effectiveWindow * empirical;
144
+ } else {
145
+ totalChars = effectiveWindow * charsPerToken;
146
+ }
105
147
 
106
148
  return {
107
149
  summaryBudgetChars: Math.floor(totalChars * SUMMARY_RATIO),
@@ -359,6 +359,47 @@ export function markCanceled(dispatchId: number, reason: string): void {
359
359
  ).run({ ":id": dispatchId, ":ended_at": now, ":reason": reason });
360
360
  }
361
361
 
362
+ /**
363
+ * Best-effort signal/crash cleanup: cancel the latest active dispatch owned by
364
+ * a worker when the process is exiting before the normal loop can settle it.
365
+ */
366
+ export function markLatestActiveForWorkerCanceled(workerId: string, reason: string): boolean {
367
+ if (!isDbAvailable()) return false;
368
+ const now = new Date().toISOString();
369
+ const db = _getAdapter()!;
370
+ const result = transaction(() => {
371
+ return db.prepare(
372
+ `UPDATE unit_dispatches
373
+ SET status = 'canceled', ended_at = :ended_at, exit_reason = :reason
374
+ WHERE id = (
375
+ SELECT id FROM unit_dispatches
376
+ WHERE worker_id = :worker_id
377
+ AND status IN ('pending','claimed','running')
378
+ ORDER BY id DESC
379
+ LIMIT 1
380
+ )`,
381
+ ).run({
382
+ ":ended_at": now,
383
+ ":reason": reason,
384
+ ":worker_id": workerId,
385
+ });
386
+ });
387
+ const changes =
388
+ typeof (result as { changes?: unknown }).changes === "number"
389
+ ? (result as { changes: number }).changes
390
+ : 0;
391
+ if (changes <= 0) return false;
392
+ insertAuditEvent({
393
+ eventId: randomUUID(),
394
+ traceId: workerId,
395
+ category: "orchestration",
396
+ type: "dispatch-canceled",
397
+ ts: now,
398
+ payload: { workerId, reason },
399
+ });
400
+ return true;
401
+ }
402
+
362
403
  /**
363
404
  * Fetch the most recent N dispatches for a unit. Used by recordDispatchClaim
364
405
  * callers to compute attempt_n and by detect-stuck.ts (B3) to consult
@@ -57,7 +57,8 @@ export function createBaseSchemaObjects(db: DbAdapter, hooks: BaseSchemaHooks):
57
57
  slice_id TEXT DEFAULT NULL,
58
58
  task_id TEXT DEFAULT NULL,
59
59
  full_content TEXT NOT NULL DEFAULT '',
60
- imported_at TEXT NOT NULL DEFAULT ''
60
+ imported_at TEXT NOT NULL DEFAULT '',
61
+ content_hash TEXT DEFAULT NULL
61
62
  )
62
63
  `);
63
64
 
@@ -76,7 +77,8 @@ export function createBaseSchemaObjects(db: DbAdapter, hooks: BaseSchemaHooks):
76
77
  hit_count INTEGER NOT NULL DEFAULT 0,
77
78
  scope TEXT NOT NULL DEFAULT 'project',
78
79
  tags TEXT NOT NULL DEFAULT '[]',
79
- structured_fields TEXT DEFAULT NULL
80
+ structured_fields TEXT DEFAULT NULL,
81
+ last_hit_at TEXT DEFAULT NULL
80
82
  )
81
83
  `);
82
84
 
@@ -416,6 +416,14 @@ export function applyMigrationV26MilestoneCommitAttributions(db: DbAdapter): voi
416
416
  db.exec("CREATE INDEX IF NOT EXISTS idx_milestone_commit_attr_milestone ON milestone_commit_attributions(milestone_id)");
417
417
  }
418
418
 
419
+ export function applyMigrationV27ArtifactHash(db: DbAdapter): void {
420
+ ensureColumn(db, "artifacts", "content_hash", "ALTER TABLE artifacts ADD COLUMN content_hash TEXT DEFAULT NULL");
421
+ }
422
+
423
+ export function applyMigrationV28MemoryLastHitAt(db: DbAdapter): void {
424
+ ensureColumn(db, "memories", "last_hit_at", "ALTER TABLE memories ADD COLUMN last_hit_at TEXT DEFAULT NULL");
425
+ }
426
+
419
427
  export interface MigrationV22Hooks {
420
428
  copyQualityGateRowsToRepairedTable(db: DbAdapter): void;
421
429
  }
@@ -14,6 +14,7 @@ import { isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
14
14
  import { gsdRoot } from "./paths.js";
15
15
  import { GIT_NO_PROMPT_ENV } from "./git-constants.js";
16
16
  import { loadEffectiveGSDPreferences } from "./preferences.js";
17
+ import { logWarning } from "./workflow-logger.js";
17
18
 
18
19
 
19
20
  import {
@@ -722,16 +723,53 @@ export class GitServiceImpl {
722
723
  if (keyFiles.length === 0) return false;
723
724
 
724
725
  const allExclusions = [...RUNTIME_EXCLUSION_PATHS, ...extraExclusions];
725
- const paths = Array.from(new Set(
726
- keyFiles
727
- .map(file => normalizeRepoRelativePath(this.basePath, file))
728
- .filter((file): file is string => file !== null)
729
- .filter(file => !isExcludedScopedPath(file, allExclusions)),
730
- ));
726
+ const normalized = keyFiles
727
+ .map(file => normalizeRepoRelativePath(this.basePath, file))
728
+ .filter((file): file is string => file !== null)
729
+ .filter(file => !isExcludedScopedPath(file, allExclusions));
730
+
731
+ // Drop entries that don't exist on disk. The LLM occasionally lists files
732
+ // it intended to write but didn't (or names them with wrong casing/path).
733
+ // Pre-`b304f738b` `git add -A` swallowed these silently; the scoped
734
+ // pathspec form passes each path explicitly, so a single bad entry made
735
+ // the whole commit fail (see #5500). Filter so valid paths still commit.
736
+ const missing: string[] = [];
737
+ const existing: string[] = [];
738
+ for (const path of normalized) {
739
+ if (existsSync(join(this.basePath, path))) {
740
+ existing.push(path);
741
+ } else {
742
+ missing.push(path);
743
+ }
744
+ }
745
+ if (missing.length > 0) {
746
+ logWarning(
747
+ "engine",
748
+ `scoped stage: dropping ${missing.length} non-existent keyFile(s) from task commit: ${missing.join(", ")}`,
749
+ { file: "git-service.ts" },
750
+ );
751
+ }
752
+
753
+ const paths = Array.from(new Set(existing));
731
754
  if (paths.length === 0) return false;
732
755
 
733
- nativeAddPaths(this.basePath, paths);
734
- return true;
756
+ try {
757
+ nativeAddPaths(this.basePath, paths);
758
+ return true;
759
+ } catch (err) {
760
+ // Defense-in-depth: even after existence filtering, libgit2/git can
761
+ // still reject paths (gitignore matches, case-only differences on
762
+ // case-insensitive FS, submodule boundaries). Returning false lets
763
+ // autoCommit fall through to smartStage so the commit still goes out
764
+ // — restoring the resilience the unscoped path used to provide.
765
+ const msg = err instanceof Error ? err.message : String(err);
766
+ logWarning(
767
+ "engine",
768
+ `scoped stage failed (${msg}); falling back to smartStage`,
769
+ { file: "git-service.ts" },
770
+ );
771
+ return false;
772
+ }
735
773
  }
736
774
 
737
775
  /** Tracks whether runtime file cleanup has run this session. */
@@ -23,6 +23,7 @@
23
23
  // excluded from this invariant.
24
24
 
25
25
  import { createRequire } from "node:module";
26
+ import { createHash } from "node:crypto";
26
27
  import { existsSync, copyFileSync, mkdirSync, realpathSync } from "node:fs";
27
28
  import { dirname } from "node:path";
28
29
  import type { Decision, Requirement, GateRow, GateId, GateScope, GateStatus, GateVerdict } from "./types.js";
@@ -78,6 +79,8 @@ import {
78
79
  applyMigrationV22QualityGateRepair,
79
80
  applyMigrationV23MilestoneQueue,
80
81
  applyMigrationV26MilestoneCommitAttributions,
82
+ applyMigrationV27ArtifactHash,
83
+ applyMigrationV28MemoryLastHitAt,
81
84
  } from "./db-migration-steps.js";
82
85
  import { isMemoriesFtsAvailableSchema, tryCreateMemoriesFtsSchema } from "./db-memory-fts-schema.js";
83
86
  import { createDbOpenState, type DbOpenPhase } from "./db-open-state.js";
@@ -106,7 +109,7 @@ const providerLoader = createSqliteProviderLoader({
106
109
  writeStderr: (message: string) => process.stderr.write(message),
107
110
  });
108
111
 
109
- export const SCHEMA_VERSION = 26;
112
+ export const SCHEMA_VERSION = 28;
110
113
 
111
114
  function initSchema(db: DbAdapter, fileBacked: boolean): void {
112
115
  if (fileBacked) db.exec("PRAGMA journal_mode=WAL");
@@ -335,6 +338,16 @@ function migrateSchema(db: DbAdapter): void {
335
338
  recordSchemaVersion(db, 26);
336
339
  }
337
340
 
341
+ if (currentVersion < 27) {
342
+ applyMigrationV27ArtifactHash(db);
343
+ recordSchemaVersion(db, 27);
344
+ }
345
+
346
+ if (currentVersion < 28) {
347
+ applyMigrationV28MemoryLastHitAt(db);
348
+ recordSchemaVersion(db, 28);
349
+ }
350
+
338
351
  db.exec("COMMIT");
339
352
  } catch (err) {
340
353
  db.exec("ROLLBACK");
@@ -913,9 +926,10 @@ export function insertArtifact(a: {
913
926
  full_content: string;
914
927
  }): void {
915
928
  if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
929
+ const contentHash = createHash("sha256").update(a.full_content).digest("hex");
916
930
  currentDb.prepare(
917
- `INSERT OR REPLACE INTO artifacts (path, artifact_type, milestone_id, slice_id, task_id, full_content, imported_at)
918
- VALUES (:path, :artifact_type, :milestone_id, :slice_id, :task_id, :full_content, :imported_at)`,
931
+ `INSERT OR REPLACE INTO artifacts (path, artifact_type, milestone_id, slice_id, task_id, full_content, imported_at, content_hash)
932
+ VALUES (:path, :artifact_type, :milestone_id, :slice_id, :task_id, :full_content, :imported_at, :content_hash)`,
919
933
  ).run({
920
934
  ":path": a.path,
921
935
  ":artifact_type": a.artifact_type,
@@ -924,6 +938,7 @@ export function insertArtifact(a: {
924
938
  ":task_id": a.task_id,
925
939
  ":full_content": a.full_content,
926
940
  ":imported_at": new Date().toISOString(),
941
+ ":content_hash": contentHash,
927
942
  });
928
943
  }
929
944
 
@@ -1795,6 +1810,13 @@ export function reconcileWorktreeDb(
1795
1810
  const hasEscalationAwaiting = wtTaskInfo.some((col) => col["name"] === "escalation_awaiting_review");
1796
1811
  const hasEscalationArtifact = wtTaskInfo.some((col) => col["name"] === "escalation_artifact_path");
1797
1812
  const hasEscalationOverride = wtTaskInfo.some((col) => col["name"] === "escalation_override_applied_at");
1813
+ const wtArtifactInfo = adapter.prepare("PRAGMA wt.table_info('artifacts')").all();
1814
+ const hasArtifactContentHash = wtArtifactInfo.some((col) => col["name"] === "content_hash");
1815
+ const wtMemoryInfo = adapter.prepare("PRAGMA wt.table_info('memories')").all();
1816
+ const hasMemoryScope = wtMemoryInfo.some((col) => col["name"] === "scope");
1817
+ const hasMemoryTags = wtMemoryInfo.some((col) => col["name"] === "tags");
1818
+ const hasMemoryStructuredFields = wtMemoryInfo.some((col) => col["name"] === "structured_fields");
1819
+ const hasMemoryLastHitAt = wtMemoryInfo.some((col) => col["name"] === "last_hit_at");
1798
1820
 
1799
1821
  const decConf = adapter.prepare(
1800
1822
  `SELECT m.id FROM decisions m INNER JOIN wt.decisions w ON m.id = w.id WHERE m.decision != w.decision OR m.choice != w.choice OR m.rationale != w.rationale OR ${
@@ -1842,12 +1864,17 @@ export function reconcileWorktreeDb(
1842
1864
  FROM wt.requirements
1843
1865
  `).run());
1844
1866
 
1867
+ // V27: preserve content_hash. If the worktree predates V27 (no column),
1868
+ // fall back to the main DB's existing hash so reconcile doesn't null
1869
+ // out integrity fingerprints on artifacts that were unchanged in wt.
1845
1870
  merged.artifacts = countChanges(adapter.prepare(`
1846
1871
  INSERT OR REPLACE INTO artifacts (
1847
- path, artifact_type, milestone_id, slice_id, task_id, full_content, imported_at
1872
+ path, artifact_type, milestone_id, slice_id, task_id, full_content, imported_at, content_hash
1848
1873
  )
1849
- SELECT path, artifact_type, milestone_id, slice_id, task_id, full_content, imported_at
1850
- FROM wt.artifacts
1874
+ SELECT w.path, w.artifact_type, w.milestone_id, w.slice_id, w.task_id, w.full_content, w.imported_at,
1875
+ ${hasArtifactContentHash ? "w.content_hash" : "m.content_hash"}
1876
+ FROM wt.artifacts w
1877
+ LEFT JOIN artifacts m ON m.path = w.path
1851
1878
  `).run());
1852
1879
 
1853
1880
  // Merge milestones — worktree may have updated status/planning fields.
@@ -1949,15 +1976,25 @@ export function reconcileWorktreeDb(
1949
1976
  LEFT JOIN tasks m ON m.milestone_id = w.milestone_id AND m.slice_id = w.slice_id AND m.id = w.id
1950
1977
  `).run());
1951
1978
 
1952
- // Merge memories — keep worktree-learned insights
1979
+ // Merge memories — keep worktree-learned insights.
1980
+ // V18 (scope, tags), V21 (structured_fields), V28 (last_hit_at): for each
1981
+ // column the wt may not yet have (older worktree DB), fall back to the
1982
+ // main DB's existing value via LEFT JOIN so reconcile never silently
1983
+ // resets these fields to defaults on rows that already had them.
1953
1984
  merged.memories = countChanges(adapter.prepare(`
1954
1985
  INSERT OR REPLACE INTO memories (
1955
1986
  seq, id, category, content, confidence, source_unit_type, source_unit_id,
1956
- created_at, updated_at, superseded_by, hit_count
1987
+ created_at, updated_at, superseded_by, hit_count,
1988
+ scope, tags, structured_fields, last_hit_at
1957
1989
  )
1958
- SELECT seq, id, category, content, confidence, source_unit_type, source_unit_id,
1959
- created_at, updated_at, superseded_by, hit_count
1960
- FROM wt.memories
1990
+ SELECT w.seq, w.id, w.category, w.content, w.confidence, w.source_unit_type, w.source_unit_id,
1991
+ w.created_at, w.updated_at, w.superseded_by, w.hit_count,
1992
+ ${hasMemoryScope ? "w.scope" : "COALESCE(m.scope, 'project')"},
1993
+ ${hasMemoryTags ? "w.tags" : "COALESCE(m.tags, '[]')"},
1994
+ ${hasMemoryStructuredFields ? "w.structured_fields" : "m.structured_fields"},
1995
+ ${hasMemoryLastHitAt ? "w.last_hit_at" : "m.last_hit_at"}
1996
+ FROM wt.memories w
1997
+ LEFT JOIN memories m ON m.id = w.id
1961
1998
  `).run());
1962
1999
 
1963
2000
  // Merge verification evidence — append-only, use INSERT OR IGNORE to avoid duplicates
@@ -3062,8 +3099,8 @@ export function updateMemoryContentRow(
3062
3099
  export function incrementMemoryHitCount(id: string, updatedAt: string): void {
3063
3100
  if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
3064
3101
  currentDb.prepare(
3065
- "UPDATE memories SET hit_count = hit_count + 1, updated_at = :updated_at WHERE id = :id",
3066
- ).run({ ":updated_at": updatedAt, ":id": id });
3102
+ "UPDATE memories SET hit_count = hit_count + 1, updated_at = :updated_at, last_hit_at = :last_hit_at WHERE id = :id",
3103
+ ).run({ ":updated_at": updatedAt, ":last_hit_at": updatedAt, ":id": id });
3067
3104
  }
3068
3105
 
3069
3106
  export function supersedeMemoryRow(oldId: string, newId: string, updatedAt: string): void {
@@ -87,6 +87,34 @@ import { logWarning } from "./workflow-logger.js";
87
87
  import { deleteRuntimeKv } from "./db/runtime-kv.js";
88
88
  import { PAUSED_SESSION_KV_KEY } from "./interrupted-session.js";
89
89
 
90
+ type AutoStartOptions = Parameters<typeof startAutoDetached>[4];
91
+ type AutoStartLauncher = typeof startAutoDetached;
92
+
93
+ function scheduleAutoStartAfterIdle(
94
+ ctx: ExtensionCommandContext,
95
+ pi: ExtensionAPI,
96
+ basePath: string,
97
+ verboseMode: boolean,
98
+ options?: AutoStartOptions,
99
+ launch: AutoStartLauncher = startAutoDetached,
100
+ ): void {
101
+ const waitForIdle =
102
+ typeof (ctx as { waitForIdle?: unknown }).waitForIdle === "function"
103
+ ? ctx.waitForIdle.bind(ctx)
104
+ : async () => {};
105
+ void waitForIdle()
106
+ .then(() => {
107
+ setTimeout(() => launch(ctx, pi, basePath, verboseMode, options), 0);
108
+ })
109
+ .catch((err) => {
110
+ const message = err instanceof Error ? err.message : String(err);
111
+ ctx.ui.notify(`Auto-start failed while waiting for the prior turn to settle: ${message}`, "error");
112
+ logWarning("guided", `auto-start idle wait failed: ${message}`);
113
+ });
114
+ }
115
+
116
+ export const _scheduleAutoStartAfterIdleForTest = scheduleAutoStartAfterIdle;
117
+
90
118
  // ─── Scope-based validator wrappers ──────────────────────────────────────────
91
119
  // These thin wrappers accept a MilestoneScope so callers that already hold a
92
120
  // pinned scope never have to re-derive (basePath, milestoneId) separately.
@@ -446,7 +474,7 @@ async function dispatchNextDeepProjectSetupStage(entry: PendingDeepProjectSetupE
446
474
 
447
475
  if (!hasPendingDeepStage(prefs, entry.basePath)) {
448
476
  pendingDeepProjectSetupMap.delete(entry.basePath);
449
- startAutoDetached(entry.ctx, entry.pi, entry.basePath, false, { step: entry.step });
477
+ scheduleAutoStartAfterIdle(entry.ctx, entry.pi, entry.basePath, false, { step: entry.step });
450
478
  return true;
451
479
  }
452
480
 
@@ -479,7 +507,7 @@ async function dispatchNextDeepProjectSetupStage(entry: PendingDeepProjectSetupE
479
507
  entry.ctx.ui.notify(result.reason, result.level);
480
508
  } else if (hasPendingDeepStage(prefs, entry.basePath)) {
481
509
  pendingDeepProjectSetupMap.delete(entry.basePath);
482
- startAutoDetached(entry.ctx, entry.pi, entry.basePath, false, { step: entry.step });
510
+ scheduleAutoStartAfterIdle(entry.ctx, entry.pi, entry.basePath, false, { step: entry.step });
483
511
  return true;
484
512
  }
485
513
  return false;
@@ -487,7 +515,7 @@ async function dispatchNextDeepProjectSetupStage(entry: PendingDeepProjectSetupE
487
515
 
488
516
  if (!USER_DRIVEN_DEEP_SETUP_UNITS.has(result.unitType)) {
489
517
  pendingDeepProjectSetupMap.delete(entry.basePath);
490
- startAutoDetached(entry.ctx, entry.pi, entry.basePath, false, { step: entry.step });
518
+ scheduleAutoStartAfterIdle(entry.ctx, entry.pi, entry.basePath, false, { step: entry.step });
491
519
  return true;
492
520
  }
493
521
 
@@ -693,7 +721,7 @@ export function checkAutoStartAfterDiscuss(): boolean {
693
721
 
694
722
  pendingAutoStartMap.delete(basePath);
695
723
  ctx.ui.notify(`Milestone ${milestoneId} ready.`, "success");
696
- startAutoDetached(ctx, pi, basePath, false, { step });
724
+ scheduleAutoStartAfterIdle(ctx, pi, basePath, false, { step });
697
725
  return true;
698
726
  }
699
727
 
@@ -2058,6 +2086,23 @@ export async function showSmartEntry(
2058
2086
  }
2059
2087
  }
2060
2088
 
2089
+ if (interrupted.classification !== "recoverable") {
2090
+ try {
2091
+ const { autoImportMarkdownHierarchyIfDbMismatch } = await import("./migration-auto-check.js");
2092
+ const result = await autoImportMarkdownHierarchyIfDbMismatch(basePath);
2093
+ if (result.action === "imported") {
2094
+ ctx.ui.notify(
2095
+ `Recovered migrated planning state into gsd.db (${result.reason}): ${result.afterDb.milestones} milestone(s), ${result.afterDb.slices} slice(s), ${result.afterDb.tasks} task(s).`,
2096
+ "info",
2097
+ );
2098
+ }
2099
+ } catch (err) {
2100
+ const message = err instanceof Error ? err.message : String(err);
2101
+ ctx.ui.notify(`GSD could not auto-import existing planning state into gsd.db: ${message}`, "warning");
2102
+ logWarning("guided", `planning state auto-import failed: ${message}`, { file: "guided-flow.ts" });
2103
+ }
2104
+ }
2105
+
2061
2106
  // Always derive from the project root — the assessment may have derived
2062
2107
  // state from a worktree path that was cleaned up in the stale branch above.
2063
2108
  const state = await deriveState(basePath);