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,21 +3,34 @@
3
3
  // Exposes the Context Mode runtime tools in-process. Default-on; opt out with
4
4
  // `context_mode.enabled: false` in preferences.
5
5
  import { Type } from "@sinclair/typebox";
6
+ async function loadContextModePreferences(baseDir) {
7
+ const [{ loadEffectiveGSDPreferences }, { logWarning }] = await Promise.all([
8
+ import("../preferences.js"),
9
+ import("../workflow-logger.js"),
10
+ ]);
11
+ try {
12
+ return loadEffectiveGSDPreferences(baseDir)?.preferences ?? null;
13
+ }
14
+ catch (err) {
15
+ logWarning("tool", `Context Mode tool could not load preferences: ${err instanceof Error ? err.message : String(err)}`);
16
+ return null;
17
+ }
18
+ }
6
19
  export function registerExecTools(pi) {
7
20
  pi.registerTool({
8
21
  name: "gsd_exec",
9
22
  label: "Exec (Sandboxed)",
10
- description: "Run a short script (bash/node/python) in a subprocess. Full stdout/stderr persist to " +
23
+ description: "Run a short script (bash/node/python) in a subprocess. Capped stdout/stderr and metadata persist to " +
11
24
  ".gsd/exec/<id>.{stdout,stderr,meta.json}; only a short digest returns in context. Use " +
12
25
  "this instead of reading many files or emitting large tool outputs — e.g. have the script " +
13
26
  "count/grep/summarize and log the finding. Enabled by default; opt out via " +
14
27
  "preferences.context_mode.enabled=false.",
15
- promptSnippet: "Run a bash/node/python script in a sandbox; full output is saved to disk and only a digest returns",
28
+ promptSnippet: "Run a bash/node/python script in a sandbox; capped output is saved to disk and only a digest returns",
16
29
  promptGuidelines: [
17
30
  "Prefer gsd_exec for analyses that would otherwise read >3 files or produce large tool output.",
18
31
  "Write scripts that log the finding (counts, matches, summaries) rather than raw dumps.",
19
32
  "The digest is the last ~300 chars of stdout — size your log output accordingly.",
20
- "Need the full output? Read the stdout_path returned in details (file on local disk).",
33
+ "Need persisted output? Read the stdout_path returned in details (file on local disk).",
21
34
  ],
22
35
  parameters: Type.Object({
23
36
  runtime: Type.Union([Type.Literal("bash"), Type.Literal("node"), Type.Literal("python")], { description: "Interpreter: bash (-c), node (-e), or python3 (-c)." }),
@@ -30,21 +43,11 @@ export function registerExecTools(pi) {
30
43
  })),
31
44
  }),
32
45
  async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
33
- const [{ executeGsdExec }, { loadEffectiveGSDPreferences }, { logWarning }] = await Promise.all([
34
- import("../tools/exec-tool.js"),
35
- import("../preferences.js"),
36
- import("../workflow-logger.js"),
37
- ]);
38
- let prefs = null;
39
- try {
40
- prefs = loadEffectiveGSDPreferences();
41
- }
42
- catch (err) {
43
- logWarning("tool", `gsd_exec could not load preferences: ${err instanceof Error ? err.message : String(err)}`);
44
- }
46
+ const { executeGsdExec } = await import("../tools/exec-tool.js");
47
+ const baseDir = process.cwd();
45
48
  return executeGsdExec(params, {
46
- baseDir: process.cwd(),
47
- preferences: prefs?.preferences ?? null,
49
+ baseDir,
50
+ preferences: await loadContextModePreferences(baseDir),
48
51
  });
49
52
  },
50
53
  });
@@ -56,7 +59,7 @@ export function registerExecTools(pi) {
56
59
  promptSnippet: "Search prior gsd_exec runs by substring, runtime, or failing-only filter",
57
60
  promptGuidelines: [
58
61
  "Use this before re-running an expensive analysis — the prior run's stdout file may still answer.",
59
- "The preview shows the trailing ~300 chars of stdout; read stdout_path for the full transcript.",
62
+ "The preview shows the trailing ~300 chars of stdout; read stdout_path for persisted output.",
60
63
  ],
61
64
  parameters: Type.Object({
62
65
  query: Type.Optional(Type.String({ description: "Substring matched against id and purpose (case-insensitive)." })),
@@ -68,8 +71,10 @@ export function registerExecTools(pi) {
68
71
  }),
69
72
  async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
70
73
  const { executeExecSearch } = await import("../tools/exec-search-tool.js");
74
+ const baseDir = process.cwd();
71
75
  return executeExecSearch(params, {
72
- baseDir: process.cwd(),
76
+ baseDir,
77
+ preferences: await loadContextModePreferences(baseDir),
73
78
  });
74
79
  },
75
80
  });
@@ -87,8 +92,10 @@ export function registerExecTools(pi) {
87
92
  parameters: Type.Object({}),
88
93
  async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
89
94
  const { executeResume } = await import("../tools/resume-tool.js");
95
+ const baseDir = process.cwd();
90
96
  return executeResume(params, {
91
- baseDir: process.cwd(),
97
+ baseDir,
98
+ preferences: await loadContextModePreferences(baseDir),
92
99
  });
93
100
  },
94
101
  });
@@ -23,6 +23,7 @@ import { approvalGateIdForUnit, isExplicitApprovalResponse, shouldPauseForUserAp
23
23
  // printed it before the TUI launched. Only re-print on /clear (subsequent sessions).
24
24
  let isFirstSession = true;
25
25
  let approvalQuestionAbortInFlight = false;
26
+ let deferredApprovalGate = null;
26
27
  async function deriveGsdState(basePath) {
27
28
  const { deriveState } = await import("../state.js");
28
29
  return deriveState(basePath);
@@ -52,6 +53,62 @@ async function applyDisabledModelProviderPolicy(ctx) {
52
53
  // Non-fatal: keep default provider visibility if preferences cannot be loaded.
53
54
  }
54
55
  }
56
+ /**
57
+ * Bridge `context_management.compaction_threshold_percent` from GSD preferences
58
+ * into the agent's runtime compaction settings (#5475). The preference is
59
+ * validated to (0.5, 0.95) at load time, but defense-in-depth normalization
60
+ * here protects against a stale or hand-edited prefs file. Calling with
61
+ * `undefined` clears any prior override so a removed preference does not leak.
62
+ */
63
+ async function applyCompactionThresholdOverride(ctx) {
64
+ try {
65
+ const { loadEffectiveGSDPreferences } = await import("../preferences.js");
66
+ const prefs = loadEffectiveGSDPreferences();
67
+ const raw = prefs?.preferences.context_management?.compaction_threshold_percent;
68
+ const value = typeof raw === "number" && Number.isFinite(raw) && raw > 0 && raw < 1 ? raw : undefined;
69
+ ctx.setCompactionThresholdOverride(value);
70
+ }
71
+ catch {
72
+ // Non-fatal: leave any existing override in place.
73
+ }
74
+ }
75
+ function clearDeferredApprovalGate(basePath) {
76
+ if (!basePath || deferredApprovalGate?.basePath === basePath) {
77
+ deferredApprovalGate = null;
78
+ }
79
+ }
80
+ function deferApprovalGate(gateId, basePath) {
81
+ deferredApprovalGate = { gateId, basePath };
82
+ }
83
+ function activateDeferredApprovalGate(basePath) {
84
+ if (deferredApprovalGate?.basePath !== basePath)
85
+ return;
86
+ setPendingGate(deferredApprovalGate.gateId, basePath);
87
+ deferredApprovalGate = null;
88
+ }
89
+ function isContextDraftSummarySave(toolName, input) {
90
+ if (toolName !== "gsd_summary_save" && toolName !== "summary_save")
91
+ return false;
92
+ if (!input || typeof input !== "object")
93
+ return false;
94
+ return input.artifact_type === "CONTEXT-DRAFT";
95
+ }
96
+ function shouldBlockDeferredApprovalTool(toolName, input, basePath) {
97
+ if (deferredApprovalGate?.basePath !== basePath)
98
+ return { block: false };
99
+ if (toolName === "ask_user_questions")
100
+ return { block: false };
101
+ if (isContextDraftSummarySave(toolName, input))
102
+ return { block: false };
103
+ return {
104
+ block: true,
105
+ reason: [
106
+ `HARD BLOCK: Approval question "${deferredApprovalGate.gateId}" has been shown to the user.`,
107
+ `Only CONTEXT-DRAFT persistence may finish in this same assistant turn.`,
108
+ `Wait for the user's answer before calling additional tools.`,
109
+ ].join(" "),
110
+ };
111
+ }
55
112
  export function resolveNotificationStoreBasePath(cwd = process.cwd()) {
56
113
  return resolveWorktreeProjectRoot(cwd);
57
114
  }
@@ -98,9 +155,11 @@ export function registerHooks(pi, ecosystemHandlers) {
98
155
  resetWriteGateState(process.cwd());
99
156
  resetToolCallLoopGuard();
100
157
  approvalQuestionAbortInFlight = false;
158
+ clearDeferredApprovalGate();
101
159
  await resetAskUserQuestionsTurnCache();
102
160
  await syncServiceTierStatus(ctx);
103
161
  await applyDisabledModelProviderPolicy(ctx);
162
+ await applyCompactionThresholdOverride(ctx);
104
163
  // Skip MCP auto-prep when running inside an auto-worktree (see session_switch below).
105
164
  const { isInAutoWorktree } = await import("../auto-worktree.js");
106
165
  if (!isInAutoWorktree(process.cwd())) {
@@ -145,10 +204,12 @@ export function registerHooks(pi, ecosystemHandlers) {
145
204
  initSessionNotifications(ctx);
146
205
  resetWriteGateState(process.cwd());
147
206
  resetToolCallLoopGuard();
207
+ clearDeferredApprovalGate();
148
208
  await resetAskUserQuestionsTurnCache();
149
209
  clearDiscussionFlowState(process.cwd());
150
210
  await syncServiceTierStatus(ctx);
151
211
  await applyDisabledModelProviderPolicy(ctx);
212
+ await applyCompactionThresholdOverride(ctx);
152
213
  // Skip MCP auto-prep when running inside an auto-worktree. The worktree
153
214
  // already has .mcp.json from createAutoWorktree, and re-running the writer
154
215
  // post-chdir rewrites the file mid-run (non-idempotent due to cwd-relative
@@ -180,6 +241,7 @@ export function registerHooks(pi, ecosystemHandlers) {
180
241
  markDepthVerified(milestoneId, beforeAgentBasePath);
181
242
  clearPendingGate(beforeAgentBasePath);
182
243
  }
244
+ clearDeferredApprovalGate(beforeAgentBasePath);
183
245
  // GSD's own context injection (existing behavior — unchanged).
184
246
  const { buildBeforeAgentStartResult } = await import("./system-context.js");
185
247
  const gsdResult = await buildBeforeAgentStartResult(event, ctx);
@@ -223,7 +285,12 @@ export function registerHooks(pi, ecosystemHandlers) {
223
285
  resetToolCallLoopGuard();
224
286
  await resetAskUserQuestionsTurnCache();
225
287
  const { handleAgentEnd } = await import("./agent-end-recovery.js");
226
- await handleAgentEnd(pi, event, ctx);
288
+ try {
289
+ await handleAgentEnd(pi, event, ctx);
290
+ }
291
+ finally {
292
+ activateDeferredApprovalGate(process.cwd());
293
+ }
227
294
  });
228
295
  // Squash-merge quick-task branch back to the original branch after the
229
296
  // agent turn completes (#2668). cleanupQuickBranch is a no-op when no
@@ -321,11 +388,12 @@ export function registerHooks(pi, ecosystemHandlers) {
321
388
  return;
322
389
  const gateId = approvalGateIdForUnit(unitType, unitId);
323
390
  if (gateId)
324
- setPendingGate(gateId, process.cwd());
391
+ deferApprovalGate(gateId, process.cwd());
325
392
  approvalQuestionAbortInFlight = true;
326
393
  ctx.ui.notify(`${unitType}${unitId ? ` ${unitId}` : ""} is waiting for your approval - pausing before more tool calls run.`, "info");
327
- // The pending gate set above blocks subsequent non-read-only tool calls
328
- // via the tool_call hook below, so we do not abort the in-flight stream.
394
+ // The durable pending gate is activated at agent_end so same-turn
395
+ // CONTEXT-DRAFT persistence can finish after the text boundary streams.
396
+ // The tool_call hook below still blocks non-draft tools in this turn.
329
397
  // Aborting mid-stream eats the model's question text on external CLI
330
398
  // providers (Claude Code SDK) because lastTextContent isn't populated
331
399
  // from in-flight builder state — the user only ever sees "Claude Code
@@ -356,6 +424,9 @@ export function registerHooks(pi, ecosystemHandlers) {
356
424
  if (loopCheck.block) {
357
425
  return { block: true, reason: loopCheck.reason };
358
426
  }
427
+ const deferredGateGuard = shouldBlockDeferredApprovalTool(toolName, event.input, discussionBasePath);
428
+ if (deferredGateGuard.block)
429
+ return deferredGateGuard;
359
430
  // ── Discussion gate enforcement: track pending gate questions ─────────
360
431
  // Only gate-shaped ask_user_questions calls should block execution.
361
432
  // The gate stays pending until the user selects the approval option.
@@ -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
  import { execFileSync } from "node:child_process";
@@ -98,7 +98,8 @@ export function preflightCleanRoot(basePath, milestoneId, notify) {
98
98
  *
99
99
  * Only called when preflightCleanRoot returned stashPushed=true.
100
100
  * Any pop error (e.g. conflict) is logged and notified but does NOT throw —
101
- * the merge already completed successfully.
101
+ * the merge already completed successfully. Callers must treat
102
+ * needsManualRecovery=true as a dirty workspace stop, not a clean completion.
102
103
  */
103
104
  export function postflightPopStash(basePath, milestoneId, stashMarker, notify) {
104
105
  let stashRef = null;
@@ -108,7 +109,11 @@ export function postflightPopStash(basePath, milestoneId, stashMarker, notify) {
108
109
  const msg = `No matching GSD preflight stash found for milestone ${milestoneId}; leaving stash list untouched.`;
109
110
  logWarning("preflight", msg);
110
111
  notify(msg, "warning");
111
- return;
112
+ return {
113
+ restored: false,
114
+ needsManualRecovery: true,
115
+ message: msg,
116
+ };
112
117
  }
113
118
  execFileSync("git", ["stash", "pop", stashRef], {
114
119
  cwd: basePath,
@@ -116,7 +121,14 @@ export function postflightPopStash(basePath, milestoneId, stashMarker, notify) {
116
121
  encoding: "utf-8",
117
122
  env: GIT_NO_PROMPT_ENV,
118
123
  });
119
- notify(`Restored stashed changes after milestone ${milestoneId} merge.`, "info");
124
+ const msg = `Restored stashed changes after milestone ${milestoneId} merge.`;
125
+ notify(msg, "info");
126
+ return {
127
+ restored: true,
128
+ needsManualRecovery: false,
129
+ message: msg,
130
+ stashRef,
131
+ };
120
132
  }
121
133
  catch (err) {
122
134
  // Pop conflicts mean the merged code collides with the stashed changes.
@@ -127,5 +139,11 @@ export function postflightPopStash(basePath, milestoneId, stashMarker, notify) {
127
139
  const msg = `git stash pop ${stashRef ?? ""}`.trim() + ` failed after merge of milestone ${milestoneId}: ${err instanceof Error ? err.message : String(err)}. ${restoreHint}`;
128
140
  logWarning("preflight", msg);
129
141
  notify(msg, "warning");
142
+ return {
143
+ restored: false,
144
+ needsManualRecovery: true,
145
+ message: msg,
146
+ ...(stashRef ? { stashRef } : {}),
147
+ };
130
148
  }
131
149
  }
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * @see D001 (module location), D002 (200K fallback), D003 (section-boundary truncation)
9
9
  */
10
- import { getCharsPerToken } from "./token-counter.js";
10
+ import { getCharsPerToken, isAccurateCountingAvailable, countTokensSync, } from "./token-counter.js";
11
11
  // ─── Budget ratio constants ──────────────────────────────────────────────────
12
12
  // Percentages of total context window allocated to each budget category.
13
13
  // These are applied after tokens→chars conversion.
@@ -23,6 +23,22 @@ const CHARS_PER_TOKEN = 4;
23
23
  const DEFAULT_CONTEXT_WINDOW = 200_000;
24
24
  /** Conservative effective context for Claude Code subscription routing (#4676) */
25
25
  const CLAUDE_CODE_EFFECTIVE_CONTEXT_WINDOW = 200_000;
26
+ /**
27
+ * Cached empirical chars-per-token from a tiktoken probe, keyed by provider.
28
+ * countTokensSync's fallback path is provider-aware, so we cache per-provider
29
+ * to preserve that distinction once the encoder warms. The cl100k_base encoder
30
+ * itself gives a stable ratio for ASCII English so a single probe per provider
31
+ * key is sufficient. Empty map means "not yet probed" or "encoder unavailable".
32
+ */
33
+ const _empiricalCharsPerTokenByProvider = new Map();
34
+ /**
35
+ * Test hook — clears the empirical chars-per-token cache so test cases that
36
+ * assert against the static char-ratio fallback aren't polluted by a prior
37
+ * tiktoken-warmed run in the same process. Production code must not call this.
38
+ */
39
+ export function _resetEmpiricalCacheForTest() {
40
+ _empiricalCharsPerTokenByProvider.clear();
41
+ }
26
42
  /** Percentage of context consumed before suggesting a continue-here checkpoint */
27
43
  const CONTINUE_THRESHOLD_PERCENT = 70;
28
44
  // ─── Task count bounds ───────────────────────────────────────────────────────
@@ -46,7 +62,26 @@ const TASK_COUNT_TIERS = [
46
62
  export function computeBudgets(contextWindow, provider) {
47
63
  const effectiveWindow = contextWindow > 0 ? contextWindow : DEFAULT_CONTEXT_WINDOW;
48
64
  const charsPerToken = provider ? getCharsPerToken(provider) : CHARS_PER_TOKEN;
49
- const totalChars = effectiveWindow * charsPerToken;
65
+ // Prefer the tiktoken encoder for total-char estimation when it has been
66
+ // warmed (initTokenCounter resolved). The cl100k_base ratio is stable for
67
+ // ASCII English, so probe once per provider and cache — computeBudgets is
68
+ // called multiple times per prompt build and the probe encode is otherwise
69
+ // wasted work.
70
+ let totalChars;
71
+ if (isAccurateCountingAvailable()) {
72
+ const providerKey = provider ?? "__default__";
73
+ let empirical = _empiricalCharsPerTokenByProvider.get(providerKey);
74
+ if (empirical === undefined) {
75
+ const probe = "the quick brown fox jumps over the lazy dog ".repeat(64);
76
+ const probeTokens = countTokensSync(probe, provider);
77
+ empirical = probeTokens > 0 ? probe.length / probeTokens : charsPerToken;
78
+ _empiricalCharsPerTokenByProvider.set(providerKey, empirical);
79
+ }
80
+ totalChars = effectiveWindow * empirical;
81
+ }
82
+ else {
83
+ totalChars = effectiveWindow * charsPerToken;
84
+ }
50
85
  return {
51
86
  summaryBudgetChars: Math.floor(totalChars * SUMMARY_RATIO),
52
87
  inlineContextBudgetChars: Math.floor(totalChars * INLINE_CONTEXT_RATIO),
@@ -253,6 +253,45 @@ export function markCanceled(dispatchId, reason) {
253
253
  SET status = 'canceled', ended_at = :ended_at, exit_reason = :reason
254
254
  WHERE id = :id AND status IN ('pending','claimed','running')`).run({ ":id": dispatchId, ":ended_at": now, ":reason": reason });
255
255
  }
256
+ /**
257
+ * Best-effort signal/crash cleanup: cancel the latest active dispatch owned by
258
+ * a worker when the process is exiting before the normal loop can settle it.
259
+ */
260
+ export function markLatestActiveForWorkerCanceled(workerId, reason) {
261
+ if (!isDbAvailable())
262
+ return false;
263
+ const now = new Date().toISOString();
264
+ const db = _getAdapter();
265
+ const result = transaction(() => {
266
+ return db.prepare(`UPDATE unit_dispatches
267
+ SET status = 'canceled', ended_at = :ended_at, exit_reason = :reason
268
+ WHERE id = (
269
+ SELECT id FROM unit_dispatches
270
+ WHERE worker_id = :worker_id
271
+ AND status IN ('pending','claimed','running')
272
+ ORDER BY id DESC
273
+ LIMIT 1
274
+ )`).run({
275
+ ":ended_at": now,
276
+ ":reason": reason,
277
+ ":worker_id": workerId,
278
+ });
279
+ });
280
+ const changes = typeof result.changes === "number"
281
+ ? result.changes
282
+ : 0;
283
+ if (changes <= 0)
284
+ return false;
285
+ insertAuditEvent({
286
+ eventId: randomUUID(),
287
+ traceId: workerId,
288
+ category: "orchestration",
289
+ type: "dispatch-canceled",
290
+ ts: now,
291
+ payload: { workerId, reason },
292
+ });
293
+ return true;
294
+ }
256
295
  /**
257
296
  * Fetch the most recent N dispatches for a unit. Used by recordDispatchClaim
258
297
  * callers to compute attempt_n and by detect-stuck.ts (B3) to consult
@@ -46,7 +46,8 @@ export function createBaseSchemaObjects(db, hooks) {
46
46
  slice_id TEXT DEFAULT NULL,
47
47
  task_id TEXT DEFAULT NULL,
48
48
  full_content TEXT NOT NULL DEFAULT '',
49
- imported_at TEXT NOT NULL DEFAULT ''
49
+ imported_at TEXT NOT NULL DEFAULT '',
50
+ content_hash TEXT DEFAULT NULL
50
51
  )
51
52
  `);
52
53
  db.exec(`
@@ -64,7 +65,8 @@ export function createBaseSchemaObjects(db, hooks) {
64
65
  hit_count INTEGER NOT NULL DEFAULT 0,
65
66
  scope TEXT NOT NULL DEFAULT 'project',
66
67
  tags TEXT NOT NULL DEFAULT '[]',
67
- structured_fields TEXT DEFAULT NULL
68
+ structured_fields TEXT DEFAULT NULL,
69
+ last_hit_at TEXT DEFAULT NULL
68
70
  )
69
71
  `);
70
72
  db.exec(`
@@ -379,6 +379,12 @@ export function applyMigrationV26MilestoneCommitAttributions(db) {
379
379
  `);
380
380
  db.exec("CREATE INDEX IF NOT EXISTS idx_milestone_commit_attr_milestone ON milestone_commit_attributions(milestone_id)");
381
381
  }
382
+ export function applyMigrationV27ArtifactHash(db) {
383
+ ensureColumn(db, "artifacts", "content_hash", "ALTER TABLE artifacts ADD COLUMN content_hash TEXT DEFAULT NULL");
384
+ }
385
+ export function applyMigrationV28MemoryLastHitAt(db) {
386
+ ensureColumn(db, "memories", "last_hit_at", "ALTER TABLE memories ADD COLUMN last_hit_at TEXT DEFAULT NULL");
387
+ }
382
388
  export function applyMigrationV22QualityGateRepair(db, hooks) {
383
389
  const qgInfo = db.prepare("PRAGMA table_info(quality_gates)").all();
384
390
  const taskIdCol = qgInfo.find((r) => r["name"] === "task_id");
@@ -13,6 +13,7 @@ import { isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
13
13
  import { gsdRoot } from "./paths.js";
14
14
  import { GIT_NO_PROMPT_ENV } from "./git-constants.js";
15
15
  import { loadEffectiveGSDPreferences } from "./preferences.js";
16
+ import { logWarning } from "./workflow-logger.js";
16
17
  import { detectWorktreeName, } from "./worktree.js";
17
18
  import { SLICE_BRANCH_RE, QUICK_BRANCH_RE, WORKFLOW_BRANCH_RE } from "./branch-patterns.js";
18
19
  import { nativeGetCurrentBranch, nativeDetectMainBranch, nativeBranchExists, nativeHasChanges, nativeAddAllWithExclusions, nativeHasStagedChanges, nativeCommit, nativeRmCached, nativeUpdateRef, nativeAddPaths, nativeResetSoft, nativeCommitSubject, _resetHasChangesCache, } from "./native-git-bridge.js";
@@ -535,14 +536,45 @@ export class GitServiceImpl {
535
536
  if (keyFiles.length === 0)
536
537
  return false;
537
538
  const allExclusions = [...RUNTIME_EXCLUSION_PATHS, ...extraExclusions];
538
- const paths = Array.from(new Set(keyFiles
539
+ const normalized = keyFiles
539
540
  .map(file => normalizeRepoRelativePath(this.basePath, file))
540
541
  .filter((file) => file !== null)
541
- .filter(file => !isExcludedScopedPath(file, allExclusions))));
542
+ .filter(file => !isExcludedScopedPath(file, allExclusions));
543
+ // Drop entries that don't exist on disk. The LLM occasionally lists files
544
+ // it intended to write but didn't (or names them with wrong casing/path).
545
+ // Pre-`b304f738b` `git add -A` swallowed these silently; the scoped
546
+ // pathspec form passes each path explicitly, so a single bad entry made
547
+ // the whole commit fail (see #5500). Filter so valid paths still commit.
548
+ const missing = [];
549
+ const existing = [];
550
+ for (const path of normalized) {
551
+ if (existsSync(join(this.basePath, path))) {
552
+ existing.push(path);
553
+ }
554
+ else {
555
+ missing.push(path);
556
+ }
557
+ }
558
+ if (missing.length > 0) {
559
+ logWarning("engine", `scoped stage: dropping ${missing.length} non-existent keyFile(s) from task commit: ${missing.join(", ")}`, { file: "git-service.ts" });
560
+ }
561
+ const paths = Array.from(new Set(existing));
542
562
  if (paths.length === 0)
543
563
  return false;
544
- nativeAddPaths(this.basePath, paths);
545
- return true;
564
+ try {
565
+ nativeAddPaths(this.basePath, paths);
566
+ return true;
567
+ }
568
+ catch (err) {
569
+ // Defense-in-depth: even after existence filtering, libgit2/git can
570
+ // still reject paths (gitignore matches, case-only differences on
571
+ // case-insensitive FS, submodule boundaries). Returning false lets
572
+ // autoCommit fall through to smartStage so the commit still goes out
573
+ // — restoring the resilience the unscoped path used to provide.
574
+ const msg = err instanceof Error ? err.message : String(err);
575
+ logWarning("engine", `scoped stage failed (${msg}); falling back to smartStage`, { file: "git-service.ts" });
576
+ return false;
577
+ }
546
578
  }
547
579
  /** Tracks whether runtime file cleanup has run this session. */
548
580
  _runtimeFilesCleanedUp = false;
@@ -22,6 +22,7 @@
22
22
  // intentionally independent store for cross-worktree claim races and is
23
23
  // excluded from this invariant.
24
24
  import { createRequire } from "node:module";
25
+ import { createHash } from "node:crypto";
25
26
  import { existsSync, copyFileSync, mkdirSync, realpathSync } from "node:fs";
26
27
  import { dirname } from "node:path";
27
28
  import { GSDError, GSD_STALE_STATE } from "./errors.js";
@@ -36,7 +37,7 @@ import { rowToActiveDecision, rowToActiveRequirement, rowToDecision, rowToRequir
36
37
  import { rowToGate } from "./db-gate-rows.js";
37
38
  import { rowToArtifact, rowToMilestone } from "./db-milestone-artifact-rows.js";
38
39
  import { backupDatabaseBeforeMigration } from "./db-migration-backup.js";
39
- import { applyMigrationV2Artifacts, applyMigrationV3Memories, applyMigrationV4DecisionMadeBy, applyMigrationV5HierarchyTables, applyMigrationV6SliceSummaries, applyMigrationV7Dependencies, applyMigrationV8PlanningFields, applyMigrationV9Ordering, applyMigrationV10ReplanTrigger, applyMigrationV11TaskPlanning, applyMigrationV12QualityGates, applyMigrationV13HotPathIndexes, applyMigrationV14SliceDependencies, applyMigrationV15AuditTables, applyMigrationV16EscalationSource, applyMigrationV17TaskEscalation, applyMigrationV18MemorySources, applyMigrationV19MemoryFts, applyMigrationV20MemoryRelations, applyMigrationV21StructuredMemories, applyMigrationV22QualityGateRepair, applyMigrationV23MilestoneQueue, applyMigrationV26MilestoneCommitAttributions, } from "./db-migration-steps.js";
40
+ import { applyMigrationV2Artifacts, applyMigrationV3Memories, applyMigrationV4DecisionMadeBy, applyMigrationV5HierarchyTables, applyMigrationV6SliceSummaries, applyMigrationV7Dependencies, applyMigrationV8PlanningFields, applyMigrationV9Ordering, applyMigrationV10ReplanTrigger, applyMigrationV11TaskPlanning, applyMigrationV12QualityGates, applyMigrationV13HotPathIndexes, applyMigrationV14SliceDependencies, applyMigrationV15AuditTables, applyMigrationV16EscalationSource, applyMigrationV17TaskEscalation, applyMigrationV18MemorySources, applyMigrationV19MemoryFts, applyMigrationV20MemoryRelations, applyMigrationV21StructuredMemories, applyMigrationV22QualityGateRepair, applyMigrationV23MilestoneQueue, applyMigrationV26MilestoneCommitAttributions, applyMigrationV27ArtifactHash, applyMigrationV28MemoryLastHitAt, } from "./db-migration-steps.js";
40
41
  import { isMemoriesFtsAvailableSchema, tryCreateMemoriesFtsSchema } from "./db-memory-fts-schema.js";
41
42
  import { createDbOpenState } from "./db-open-state.js";
42
43
  import { createRuntimeKvTableV25 } from "./db-runtime-kv-schema.js";
@@ -52,7 +53,7 @@ const providerLoader = createSqliteProviderLoader({
52
53
  nodeVersion: process.versions.node,
53
54
  writeStderr: (message) => process.stderr.write(message),
54
55
  });
55
- export const SCHEMA_VERSION = 26;
56
+ export const SCHEMA_VERSION = 28;
56
57
  function initSchema(db, fileBacked) {
57
58
  if (fileBacked)
58
59
  db.exec("PRAGMA journal_mode=WAL");
@@ -250,6 +251,14 @@ function migrateSchema(db) {
250
251
  applyMigrationV26MilestoneCommitAttributions(db);
251
252
  recordSchemaVersion(db, 26);
252
253
  }
254
+ if (currentVersion < 27) {
255
+ applyMigrationV27ArtifactHash(db);
256
+ recordSchemaVersion(db, 27);
257
+ }
258
+ if (currentVersion < 28) {
259
+ applyMigrationV28MemoryLastHitAt(db);
260
+ recordSchemaVersion(db, 28);
261
+ }
253
262
  db.exec("COMMIT");
254
263
  }
255
264
  catch (err) {
@@ -821,8 +830,9 @@ export function clearArtifacts() {
821
830
  export function insertArtifact(a) {
822
831
  if (!currentDb)
823
832
  throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
824
- currentDb.prepare(`INSERT OR REPLACE INTO artifacts (path, artifact_type, milestone_id, slice_id, task_id, full_content, imported_at)
825
- VALUES (:path, :artifact_type, :milestone_id, :slice_id, :task_id, :full_content, :imported_at)`).run({
833
+ const contentHash = createHash("sha256").update(a.full_content).digest("hex");
834
+ currentDb.prepare(`INSERT OR REPLACE INTO artifacts (path, artifact_type, milestone_id, slice_id, task_id, full_content, imported_at, content_hash)
835
+ VALUES (:path, :artifact_type, :milestone_id, :slice_id, :task_id, :full_content, :imported_at, :content_hash)`).run({
826
836
  ":path": a.path,
827
837
  ":artifact_type": a.artifact_type,
828
838
  ":milestone_id": a.milestone_id,
@@ -830,6 +840,7 @@ export function insertArtifact(a) {
830
840
  ":task_id": a.task_id,
831
841
  ":full_content": a.full_content,
832
842
  ":imported_at": new Date().toISOString(),
843
+ ":content_hash": contentHash,
833
844
  });
834
845
  }
835
846
  export function insertMilestone(m) {
@@ -1521,6 +1532,13 @@ export function reconcileWorktreeDb(mainDbPath, worktreeDbPath) {
1521
1532
  const hasEscalationAwaiting = wtTaskInfo.some((col) => col["name"] === "escalation_awaiting_review");
1522
1533
  const hasEscalationArtifact = wtTaskInfo.some((col) => col["name"] === "escalation_artifact_path");
1523
1534
  const hasEscalationOverride = wtTaskInfo.some((col) => col["name"] === "escalation_override_applied_at");
1535
+ const wtArtifactInfo = adapter.prepare("PRAGMA wt.table_info('artifacts')").all();
1536
+ const hasArtifactContentHash = wtArtifactInfo.some((col) => col["name"] === "content_hash");
1537
+ const wtMemoryInfo = adapter.prepare("PRAGMA wt.table_info('memories')").all();
1538
+ const hasMemoryScope = wtMemoryInfo.some((col) => col["name"] === "scope");
1539
+ const hasMemoryTags = wtMemoryInfo.some((col) => col["name"] === "tags");
1540
+ const hasMemoryStructuredFields = wtMemoryInfo.some((col) => col["name"] === "structured_fields");
1541
+ const hasMemoryLastHitAt = wtMemoryInfo.some((col) => col["name"] === "last_hit_at");
1524
1542
  const decConf = adapter.prepare(`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 ${hasMadeBy ? "m.made_by != w.made_by" : "'agent' != 'agent'"} OR m.superseded_by IS NOT w.superseded_by`).all();
1525
1543
  for (const row of decConf)
1526
1544
  conflicts.push(`decision ${row["id"]}: modified in both`);
@@ -1553,12 +1571,17 @@ export function reconcileWorktreeDb(mainDbPath, worktreeDbPath) {
1553
1571
  supporting_slices, validation, notes, full_content, superseded_by
1554
1572
  FROM wt.requirements
1555
1573
  `).run());
1574
+ // V27: preserve content_hash. If the worktree predates V27 (no column),
1575
+ // fall back to the main DB's existing hash so reconcile doesn't null
1576
+ // out integrity fingerprints on artifacts that were unchanged in wt.
1556
1577
  merged.artifacts = countChanges(adapter.prepare(`
1557
1578
  INSERT OR REPLACE INTO artifacts (
1558
- path, artifact_type, milestone_id, slice_id, task_id, full_content, imported_at
1579
+ path, artifact_type, milestone_id, slice_id, task_id, full_content, imported_at, content_hash
1559
1580
  )
1560
- SELECT path, artifact_type, milestone_id, slice_id, task_id, full_content, imported_at
1561
- FROM wt.artifacts
1581
+ SELECT w.path, w.artifact_type, w.milestone_id, w.slice_id, w.task_id, w.full_content, w.imported_at,
1582
+ ${hasArtifactContentHash ? "w.content_hash" : "m.content_hash"}
1583
+ FROM wt.artifacts w
1584
+ LEFT JOIN artifacts m ON m.path = w.path
1562
1585
  `).run());
1563
1586
  // Merge milestones — worktree may have updated status/planning fields.
1564
1587
  // Never downgrade status: complete > active > pre-planning (#4372).
@@ -1656,15 +1679,25 @@ export function reconcileWorktreeDb(mainDbPath, worktreeDbPath) {
1656
1679
  FROM wt.tasks w
1657
1680
  LEFT JOIN tasks m ON m.milestone_id = w.milestone_id AND m.slice_id = w.slice_id AND m.id = w.id
1658
1681
  `).run());
1659
- // Merge memories — keep worktree-learned insights
1682
+ // Merge memories — keep worktree-learned insights.
1683
+ // V18 (scope, tags), V21 (structured_fields), V28 (last_hit_at): for each
1684
+ // column the wt may not yet have (older worktree DB), fall back to the
1685
+ // main DB's existing value via LEFT JOIN so reconcile never silently
1686
+ // resets these fields to defaults on rows that already had them.
1660
1687
  merged.memories = countChanges(adapter.prepare(`
1661
1688
  INSERT OR REPLACE INTO memories (
1662
1689
  seq, id, category, content, confidence, source_unit_type, source_unit_id,
1663
- created_at, updated_at, superseded_by, hit_count
1690
+ created_at, updated_at, superseded_by, hit_count,
1691
+ scope, tags, structured_fields, last_hit_at
1664
1692
  )
1665
- SELECT seq, id, category, content, confidence, source_unit_type, source_unit_id,
1666
- created_at, updated_at, superseded_by, hit_count
1667
- FROM wt.memories
1693
+ SELECT w.seq, w.id, w.category, w.content, w.confidence, w.source_unit_type, w.source_unit_id,
1694
+ w.created_at, w.updated_at, w.superseded_by, w.hit_count,
1695
+ ${hasMemoryScope ? "w.scope" : "COALESCE(m.scope, 'project')"},
1696
+ ${hasMemoryTags ? "w.tags" : "COALESCE(m.tags, '[]')"},
1697
+ ${hasMemoryStructuredFields ? "w.structured_fields" : "m.structured_fields"},
1698
+ ${hasMemoryLastHitAt ? "w.last_hit_at" : "m.last_hit_at"}
1699
+ FROM wt.memories w
1700
+ LEFT JOIN memories m ON m.id = w.id
1668
1701
  `).run());
1669
1702
  // Merge verification evidence — append-only, use INSERT OR IGNORE to avoid duplicates
1670
1703
  merged.verification_evidence = countChanges(adapter.prepare(`
@@ -2423,7 +2456,7 @@ export function updateMemoryContentRow(id, content, confidence, updatedAt) {
2423
2456
  export function incrementMemoryHitCount(id, updatedAt) {
2424
2457
  if (!currentDb)
2425
2458
  throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
2426
- currentDb.prepare("UPDATE memories SET hit_count = hit_count + 1, updated_at = :updated_at WHERE id = :id").run({ ":updated_at": updatedAt, ":id": id });
2459
+ currentDb.prepare("UPDATE memories SET hit_count = hit_count + 1, updated_at = :updated_at, last_hit_at = :last_hit_at WHERE id = :id").run({ ":updated_at": updatedAt, ":last_hit_at": updatedAt, ":id": id });
2427
2460
  }
2428
2461
  export function supersedeMemoryRow(oldId, newId, updatedAt) {
2429
2462
  if (!currentDb)