gsd-pi 2.80.0-dev.2848d9009 → 2.80.0-dev.2be7caf18

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 (182) hide show
  1. package/README.md +3 -2
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/extensions/gsd/auto/loop.js +32 -1
  4. package/dist/resources/extensions/gsd/auto-prompts.js +13 -1
  5. package/dist/resources/extensions/gsd/auto.js +7 -2
  6. package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +27 -20
  7. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +21 -0
  8. package/dist/resources/extensions/gsd/crash-recovery.js +56 -10
  9. package/dist/resources/extensions/gsd/guided-flow.js +33 -6
  10. package/dist/resources/extensions/gsd/migrate/command.js +40 -1
  11. package/dist/resources/extensions/gsd/migration-auto-check.js +87 -0
  12. package/dist/resources/extensions/gsd/planning-path-scope.js +26 -0
  13. package/dist/resources/extensions/gsd/prompts/parallel-research-slices.md +1 -1
  14. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  15. package/dist/resources/extensions/gsd/prompts/quick-task.md +1 -5
  16. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +2 -2
  17. package/dist/resources/extensions/gsd/quick.js +34 -2
  18. package/dist/resources/extensions/gsd/tools/context-mode-tool-result.js +15 -0
  19. package/dist/resources/extensions/gsd/tools/exec-search-tool.js +5 -0
  20. package/dist/resources/extensions/gsd/tools/exec-tool.js +3 -15
  21. package/dist/resources/extensions/gsd/tools/plan-slice.js +9 -0
  22. package/dist/resources/extensions/gsd/tools/plan-task.js +9 -0
  23. package/dist/resources/extensions/gsd/tools/resume-tool.js +5 -0
  24. package/dist/resources/extensions/gsd/unit-context-composer.js +12 -3
  25. package/dist/resources/extensions/gsd/unit-runtime.js +11 -0
  26. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  27. package/dist/web/standalone/.next/BUILD_ID +1 -1
  28. package/dist/web/standalone/.next/app-path-routes-manifest.json +10 -10
  29. package/dist/web/standalone/.next/build-manifest.json +2 -2
  30. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  31. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  32. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  40. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/index.html +1 -1
  48. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app-paths-manifest.json +10 -10
  55. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  56. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  57. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  58. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  59. package/package.json +1 -1
  60. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  61. package/packages/mcp-server/dist/workflow-tools.js +22 -17
  62. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  63. package/packages/mcp-server/src/workflow-tools.test.ts +66 -0
  64. package/packages/mcp-server/src/workflow-tools.ts +30 -16
  65. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  66. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  67. package/packages/pi-coding-agent/dist/core/agent-session.js +3 -0
  68. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  69. package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js +3 -1
  70. package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js.map +1 -1
  71. package/packages/pi-coding-agent/dist/core/compaction/compaction.d.ts +11 -0
  72. package/packages/pi-coding-agent/dist/core/compaction/compaction.d.ts.map +1 -1
  73. package/packages/pi-coding-agent/dist/core/compaction/compaction.js +9 -0
  74. package/packages/pi-coding-agent/dist/core/compaction/compaction.js.map +1 -1
  75. package/packages/pi-coding-agent/dist/core/compaction-threshold.test.d.ts +2 -0
  76. package/packages/pi-coding-agent/dist/core/compaction-threshold.test.d.ts.map +1 -0
  77. package/packages/pi-coding-agent/dist/core/compaction-threshold.test.js +103 -0
  78. package/packages/pi-coding-agent/dist/core/compaction-threshold.test.js.map +1 -0
  79. package/packages/pi-coding-agent/dist/core/db-snapshot.d.ts +15 -0
  80. package/packages/pi-coding-agent/dist/core/db-snapshot.d.ts.map +1 -0
  81. package/packages/pi-coding-agent/dist/core/db-snapshot.js +66 -0
  82. package/packages/pi-coding-agent/dist/core/db-snapshot.js.map +1 -0
  83. package/packages/pi-coding-agent/dist/core/db-snapshot.test.d.ts +2 -0
  84. package/packages/pi-coding-agent/dist/core/db-snapshot.test.d.ts.map +1 -0
  85. package/packages/pi-coding-agent/dist/core/db-snapshot.test.js +24 -0
  86. package/packages/pi-coding-agent/dist/core/db-snapshot.test.js.map +1 -0
  87. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +1 -0
  88. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
  89. package/packages/pi-coding-agent/dist/core/extensions/runner.js +3 -0
  90. package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
  91. package/packages/pi-coding-agent/dist/core/extensions/runner.test.js +2 -0
  92. package/packages/pi-coding-agent/dist/core/extensions/runner.test.js.map +1 -1
  93. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +7 -0
  94. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  95. package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  96. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +20 -0
  97. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  98. package/packages/pi-coding-agent/dist/core/settings-manager.js +25 -0
  99. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  100. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts +1 -0
  101. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  102. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +3 -0
  103. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  104. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  105. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +13 -5
  106. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  107. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.js +53 -0
  108. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.js.map +1 -1
  109. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  110. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +3 -0
  111. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  112. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage-safety-guard.test.d.ts +2 -0
  113. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage-safety-guard.test.d.ts.map +1 -0
  114. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage-safety-guard.test.js +10 -0
  115. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage-safety-guard.test.js.map +1 -0
  116. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.d.ts.map +1 -1
  117. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.js +3 -2
  118. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.js.map +1 -1
  119. package/packages/pi-coding-agent/src/core/agent-session.ts +3 -0
  120. package/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts +3 -1
  121. package/packages/pi-coding-agent/src/core/compaction/compaction.ts +18 -0
  122. package/packages/pi-coding-agent/src/core/compaction-threshold.test.ts +121 -0
  123. package/packages/pi-coding-agent/src/core/db-snapshot.test.ts +32 -0
  124. package/packages/pi-coding-agent/src/core/db-snapshot.ts +66 -0
  125. package/packages/pi-coding-agent/src/core/extensions/runner.test.ts +2 -0
  126. package/packages/pi-coding-agent/src/core/extensions/runner.ts +3 -0
  127. package/packages/pi-coding-agent/src/core/extensions/types.ts +7 -0
  128. package/packages/pi-coding-agent/src/core/settings-manager.ts +39 -1
  129. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +4 -0
  130. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.test.ts +56 -0
  131. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +22 -7
  132. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +3 -0
  133. package/packages/pi-coding-agent/src/resources/extensions/memory/storage-safety-guard.test.ts +14 -0
  134. package/packages/pi-coding-agent/src/resources/extensions/memory/storage.ts +3 -2
  135. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  136. package/packages/pi-tui/dist/tui.d.ts.map +1 -1
  137. package/packages/pi-tui/dist/tui.js +18 -8
  138. package/packages/pi-tui/dist/tui.js.map +1 -1
  139. package/packages/pi-tui/src/tui.ts +20 -8
  140. package/packages/pi-tui/tsconfig.tsbuildinfo +1 -1
  141. package/src/resources/extensions/gsd/auto/loop.ts +50 -8
  142. package/src/resources/extensions/gsd/auto-prompts.ts +17 -1
  143. package/src/resources/extensions/gsd/auto.ts +11 -2
  144. package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +27 -19
  145. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +22 -0
  146. package/src/resources/extensions/gsd/crash-recovery.ts +67 -10
  147. package/src/resources/extensions/gsd/guided-flow.ts +39 -6
  148. package/src/resources/extensions/gsd/migrate/command.ts +47 -1
  149. package/src/resources/extensions/gsd/migration-auto-check.ts +129 -0
  150. package/src/resources/extensions/gsd/planning-path-scope.ts +35 -0
  151. package/src/resources/extensions/gsd/preferences-types.ts +1 -1
  152. package/src/resources/extensions/gsd/prompts/parallel-research-slices.md +1 -1
  153. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  154. package/src/resources/extensions/gsd/prompts/quick-task.md +1 -5
  155. package/src/resources/extensions/gsd/prompts/validate-milestone.md +2 -2
  156. package/src/resources/extensions/gsd/quick.ts +37 -2
  157. package/src/resources/extensions/gsd/tests/compaction-snapshot.test.ts +11 -0
  158. package/src/resources/extensions/gsd/tests/crash-handler-secondary.test.ts +55 -0
  159. package/src/resources/extensions/gsd/tests/crash-recovery-via-db.test.ts +22 -0
  160. package/src/resources/extensions/gsd/tests/exec-history.test.ts +15 -0
  161. package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +65 -0
  162. package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +48 -0
  163. package/src/resources/extensions/gsd/tests/migration-auto-check.test.ts +127 -0
  164. package/src/resources/extensions/gsd/tests/plan-slice.test.ts +50 -0
  165. package/src/resources/extensions/gsd/tests/plan-task.test.ts +21 -0
  166. package/src/resources/extensions/gsd/tests/prompt-path-audit.test.ts +40 -0
  167. package/src/resources/extensions/gsd/tests/quick-external-gsd.test.ts +40 -0
  168. package/src/resources/extensions/gsd/tests/smart-entry-complete.test.ts +20 -0
  169. package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +17 -0
  170. package/src/resources/extensions/gsd/tests/status-db-open.test.ts +9 -0
  171. package/src/resources/extensions/gsd/tests/unit-context-composer.test.ts +136 -4
  172. package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +7 -0
  173. package/src/resources/extensions/gsd/tools/context-mode-tool-result.ts +25 -0
  174. package/src/resources/extensions/gsd/tools/exec-search-tool.ts +7 -7
  175. package/src/resources/extensions/gsd/tools/exec-tool.ts +4 -23
  176. package/src/resources/extensions/gsd/tools/plan-slice.ts +13 -0
  177. package/src/resources/extensions/gsd/tools/plan-task.ts +10 -0
  178. package/src/resources/extensions/gsd/tools/resume-tool.ts +7 -7
  179. package/src/resources/extensions/gsd/unit-context-composer.ts +19 -4
  180. package/src/resources/extensions/gsd/unit-runtime.ts +14 -0
  181. /package/dist/web/standalone/.next/static/{vaQ47E8aNlbbNEDoeME6G → 3EDDd9ULaybSupPoA_vf-}/_buildManifest.js +0 -0
  182. /package/dist/web/standalone/.next/static/{vaQ47E8aNlbbNEDoeME6G → 3EDDd9ULaybSupPoA_vf-}/_ssgManifest.js +0 -0
package/README.md CHANGED
@@ -361,7 +361,7 @@ The database is authoritative for milestones, slices, tasks, requirements, decis
361
361
 
362
362
  2. **Context pre-loading** — The dispatch prompt includes inlined task plans, slice plans, prior task summaries, dependency summaries, roadmap excerpts, and decisions register. The LLM starts with everything it needs instead of spending tool calls reading files.
363
363
 
364
- 3. **Context Mode** — Context Mode is enabled by default and gives every auto-mode unit guidance for preserving context. Agents are steered toward `gsd_exec` for noisy scans, builds, tests, and diagnostics; full stdout/stderr is saved under `.gsd/exec/` while only a short digest enters the conversation. `gsd_exec_search` lets agents reuse prior runs instead of repeating expensive checks, and `gsd_resume` reads `.gsd/last-snapshot.md` after compaction or resume. Opt out with `context_mode.enabled: false`; tune sandbox timeout/output caps with `context_mode.exec_timeout_ms`, `context_mode.exec_stdout_cap_bytes`, and `context_mode.exec_digest_chars`.
364
+ 3. **Context Mode** — Context Mode is enabled by default and gives eligible auto-mode units guidance for preserving context. Agents are steered toward `gsd_exec` for noisy scans, builds, tests, and diagnostics; capped stdout/stderr and metadata are saved under `.gsd/exec/` while only a short digest enters the conversation. `gsd_exec_search` lets agents reuse prior runs instead of repeating expensive checks, and `gsd_resume` reads a prior compaction snapshot from `.gsd/last-snapshot.md` when one exists. Opt out with `context_mode.enabled: false` to disable Context Mode guidance, snapshot injection, `gsd_exec`, `gsd_exec_search`, and `gsd_resume`; tune sandbox timeout/output caps and environment forwarding with `context_mode.exec_timeout_ms`, `context_mode.exec_stdout_cap_bytes`, `context_mode.exec_digest_chars`, and `context_mode.exec_env_allowlist`.
365
365
 
366
366
  4. **Git isolation** — When `git.isolation` is set to `worktree` or `branch`, each milestone runs on its own `milestone/<MID>` branch (in a worktree or in-place). All slice work commits sequentially — no branch switching, no merge conflicts. When the milestone completes, it's squash-merged to main as one clean commit. The default is `none` (work on the current branch), configurable via preferences. If `worktree` is configured in a repo with no committed `HEAD`, GSD temporarily behaves as `none` until the first commit exists because git worktrees need a committed start point.
367
367
 
@@ -659,10 +659,11 @@ auto_report: true
659
659
  | `unique_milestone_ids` | Uses unique milestone names to avoid clashes when working in teams of people |
660
660
  | `git.isolation` | `none` (default), `worktree`, or `branch` — enable worktree or branch isolation for milestone work. `worktree` requires a committed `HEAD`; zero-commit repos temporarily run as `none` |
661
661
  | `git.manage_gitignore` | Set `false` to prevent GSD from modifying `.gitignore` |
662
- | `context_mode.enabled` | Context Mode is default-on; set `false` to disable `gsd_exec`, exec history guidance, and resume snapshots |
662
+ | `context_mode.enabled` | Context Mode is default-on; set `false` to disable prompt guidance, snapshot injection, `gsd_exec`, `gsd_exec_search`, and `gsd_resume` |
663
663
  | `context_mode.exec_timeout_ms` | Timeout for sandboxed `gsd_exec` runs (default: 30000) |
664
664
  | `context_mode.exec_stdout_cap_bytes` | Persisted stdout cap for `gsd_exec` output (default: 1048576) |
665
665
  | `context_mode.exec_digest_chars` | Trailing stdout characters returned to the agent context (default: 300) |
666
+ | `context_mode.exec_env_allowlist` | Environment variables forwarded to sandboxed `gsd_exec` runs in addition to `PATH` and `HOME` |
666
667
  | `verification_commands` | Array of shell commands to run after task execution (e.g., `["npm run lint", "npm run test"]`) |
667
668
  | `verification_auto_fix` | Auto-retry on verification failures (default: true) |
668
669
  | `verification_max_retries` | Max retries for verification failures (default: 2) |
@@ -1 +1 @@
1
- 4b5e4f1bfb55a0a1
1
+ 8e6c04d763f9ba00
@@ -25,6 +25,8 @@ import { normalizeRealPath } from "../paths.js";
25
25
  import { decideCooldownRecovery, decideDispatchClaim, decideEngineDispatch, decideFinalizeResult, decideInfrastructureError, decideIterationErrorRecovery, decideMemoryPressure, decideModelPolicyBlocked, decideMinRequestInterval, decideWorkflowLoop, formatDispatchExceptionSummary, formatUnhandledDispatchErrorSummary, resolveUnitRequestTimestamp, shouldUseCustomEnginePath, } from "./workflow-kernel.js";
26
26
  import { hydrateCustomVerifyRetryCounts, saveCustomVerifyRetryCounts, } from "./custom-verify-retry-store.js";
27
27
  import { settleDispatchCompleted, settleDispatchFailed, } from "./workflow-dispatch-ledger.js";
28
+ import { emitOpenUnitEndForUnit } from "../crash-recovery.js";
29
+ import { writeUnitRuntimeRecord } from "../unit-runtime.js";
28
30
  import { openDispatchClaim } from "./workflow-dispatch-claim.js";
29
31
  import { completeWorkflowIteration } from "./workflow-iteration-completion.js";
30
32
  import { createWorkflowJournalReporter } from "./workflow-journal-reporter.js";
@@ -133,6 +135,24 @@ async function enforceMinRequestInterval(s, prefs) {
133
135
  await new Promise(r => setTimeout(r, decision.waitMs));
134
136
  }
135
137
  }
138
+ function closeOutCrashedUnit(s, iterData, err) {
139
+ const summary = formatDispatchExceptionSummary({ error: err });
140
+ try {
141
+ emitOpenUnitEndForUnit(s.basePath, iterData.unitType, iterData.unitId, "cancelled", {
142
+ message: summary,
143
+ category: "unit-exception",
144
+ isTransient: false,
145
+ });
146
+ writeUnitRuntimeRecord(s.basePath, iterData.unitType, iterData.unitId, s.currentUnit?.startedAt ?? Date.now(), {
147
+ phase: "crashed",
148
+ lastProgressAt: Date.now(),
149
+ lastProgressKind: "unit-exception",
150
+ });
151
+ }
152
+ catch (closeoutErr) {
153
+ logWarning("dispatch", `unit crash closeout failed: ${closeoutErr instanceof Error ? closeoutErr.message : String(closeoutErr)}`);
154
+ }
155
+ }
136
156
  /**
137
157
  * Main auto-mode execution loop. Iterates: derive → dispatch → guards →
138
158
  * runUnit → finalize → repeat. Exits when s.active becomes false or a
@@ -392,7 +412,17 @@ export async function autoLoop(ctx, pi, s, deps, options) {
392
412
  }
393
413
  // ── Unit execution (shared with dev path) ──
394
414
  await enforceMinRequestInterval(s, prefs);
395
- const unitPhaseResult = await runUnitPhaseViaContract(dispatchContract, ic, iterData, loopState, undefined, unitDispatchDeps);
415
+ let unitPhaseResult;
416
+ try {
417
+ unitPhaseResult = await runUnitPhaseViaContract(dispatchContract, ic, iterData, loopState, undefined, unitDispatchDeps);
418
+ }
419
+ catch (err) {
420
+ if (err instanceof ModelPolicyDispatchBlockedError) {
421
+ throw err;
422
+ }
423
+ closeOutCrashedUnit(s, iterData, err);
424
+ throw err;
425
+ }
396
426
  if (unitPhaseResult.action === "next") {
397
427
  const requestTimestamp = resolveUnitRequestTimestamp(unitPhaseResult.data);
398
428
  if (requestTimestamp !== undefined)
@@ -580,6 +610,7 @@ export async function autoLoop(ctx, pi, s, deps, options) {
580
610
  if (err instanceof ModelPolicyDispatchBlockedError) {
581
611
  throw err;
582
612
  }
613
+ closeOutCrashedUnit(s, iterData, err);
583
614
  dispatchSettled = settleDispatchFailed(dispatchId, formatDispatchExceptionSummary({ error: err }), {
584
615
  markFailed: markDispatchFailed,
585
616
  logWriteFailure: logDispatchLedgerWriteFailure,
@@ -21,6 +21,7 @@ import { assertGateCoverage, getGatesForTurn, } from "./gate-registry.js";
21
21
  import { formatDecisionsCompact, formatRequirementsCompact } from "./structured-data-formatter.js";
22
22
  import { readPhaseAnchor, formatAnchorForPrompt } from "./phase-anchor.js";
23
23
  import { composeContextModeInstructions, composeInlinedContext } from "./unit-context-composer.js";
24
+ import { readCompactionSnapshot } from "./compaction-snapshot.js";
24
25
  import { logWarning } from "./workflow-logger.js";
25
26
  import { inlineGraphSubgraph } from "./graph-context.js";
26
27
  import { buildExtractionStepsBlock } from "./commands-extract-learnings.js";
@@ -178,8 +179,19 @@ function renderContextModeForPrompt(unitType, base, renderMode = "standalone") {
178
179
  renderMode,
179
180
  });
180
181
  }
181
- function prependContextModeToBlock(unitType, base, block, renderMode = "standalone") {
182
+ function renderContextModeBlockForPrompt(unitType, base, renderMode = "standalone") {
182
183
  const contextMode = renderContextModeForPrompt(unitType, base, renderMode);
184
+ if (!contextMode)
185
+ return "";
186
+ if (renderMode === "nested")
187
+ return contextMode;
188
+ const snapshot = readCompactionSnapshot(base);
189
+ if (!snapshot?.trim())
190
+ return contextMode;
191
+ return `${contextMode}\n\n## Context Snapshot\nSource: \`.gsd/last-snapshot.md\`\n\n${snapshot.trimEnd()}`;
192
+ }
193
+ function prependContextModeToBlock(unitType, base, block, renderMode = "standalone") {
194
+ const contextMode = renderContextModeBlockForPrompt(unitType, base, renderMode);
183
195
  if (!contextMode)
184
196
  return block;
185
197
  if (!block.trim())
@@ -112,11 +112,12 @@ const STATE_REBUILD_MIN_INTERVAL_MS = 30_000;
112
112
  * the DB is unavailable (e.g. fresh project before init) we skip registration
113
113
  * silently rather than blocking session start.
114
114
  */
115
- function registerAutoWorkerForSession(session) {
115
+ function registerAutoWorkerForSession(session, projectRootOverride) {
116
116
  if (session.workerId)
117
117
  return; // already registered (e.g. resume re-runs)
118
118
  try {
119
- const projectRootRealpath = normalizeRealPath(session.scope?.workspace.projectRoot
119
+ const projectRootRealpath = normalizeRealPath(projectRootOverride
120
+ ?? session.scope?.workspace.projectRoot
120
121
  ?? (session.originalBasePath || session.basePath));
121
122
  session.workerId = registerAutoWorker({ projectRootRealpath });
122
123
  }
@@ -1631,6 +1632,10 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
1631
1632
  lockBase,
1632
1633
  buildResolver,
1633
1634
  };
1635
+ // Register the worker before bootstrap enters a milestone worktree.
1636
+ // This ensures enterMilestone can claim a lease and seed dispatch claims
1637
+ // for crash-recovery fidelity (#5405).
1638
+ registerAutoWorkerForSession(s, base);
1634
1639
  const ready = await bootstrapAutoSession(s, ctx, pi, base, verboseMode, requestedStepMode, bootstrapDeps, freshStartAssessment);
1635
1640
  if (!ready)
1636
1641
  return;
@@ -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
  });
@@ -52,6 +52,25 @@ async function applyDisabledModelProviderPolicy(ctx) {
52
52
  // Non-fatal: keep default provider visibility if preferences cannot be loaded.
53
53
  }
54
54
  }
55
+ /**
56
+ * Bridge `context_management.compaction_threshold_percent` from GSD preferences
57
+ * into the agent's runtime compaction settings (#5475). The preference is
58
+ * validated to (0.5, 0.95) at load time, but defense-in-depth normalization
59
+ * here protects against a stale or hand-edited prefs file. Calling with
60
+ * `undefined` clears any prior override so a removed preference does not leak.
61
+ */
62
+ async function applyCompactionThresholdOverride(ctx) {
63
+ try {
64
+ const { loadEffectiveGSDPreferences } = await import("../preferences.js");
65
+ const prefs = loadEffectiveGSDPreferences();
66
+ const raw = prefs?.preferences.context_management?.compaction_threshold_percent;
67
+ const value = typeof raw === "number" && Number.isFinite(raw) && raw > 0 && raw < 1 ? raw : undefined;
68
+ ctx.setCompactionThresholdOverride(value);
69
+ }
70
+ catch {
71
+ // Non-fatal: leave any existing override in place.
72
+ }
73
+ }
55
74
  export function resolveNotificationStoreBasePath(cwd = process.cwd()) {
56
75
  return resolveWorktreeProjectRoot(cwd);
57
76
  }
@@ -101,6 +120,7 @@ export function registerHooks(pi, ecosystemHandlers) {
101
120
  await resetAskUserQuestionsTurnCache();
102
121
  await syncServiceTierStatus(ctx);
103
122
  await applyDisabledModelProviderPolicy(ctx);
123
+ await applyCompactionThresholdOverride(ctx);
104
124
  // Skip MCP auto-prep when running inside an auto-worktree (see session_switch below).
105
125
  const { isInAutoWorktree } = await import("../auto-worktree.js");
106
126
  if (!isInAutoWorktree(process.cwd())) {
@@ -149,6 +169,7 @@ export function registerHooks(pi, ecosystemHandlers) {
149
169
  clearDiscussionFlowState(process.cwd());
150
170
  await syncServiceTierStatus(ctx);
151
171
  await applyDisabledModelProviderPolicy(ctx);
172
+ await applyCompactionThresholdOverride(ctx);
152
173
  // Skip MCP auto-prep when running inside an auto-worktree. The worktree
153
174
  // already has .mcp.json from createAutoWorktree, and re-running the writer
154
175
  // post-chdir rewrites the file mid-run (non-idempotent due to cwd-relative
@@ -29,6 +29,7 @@ import { _getAdapter, isDbAvailable } from "./gsd-db.js";
29
29
  import { gsdRoot, normalizeRealPath } from "./paths.js";
30
30
  import { atomicWriteSync } from "./atomic-write.js";
31
31
  import { effectiveLockFile } from "./session-lock.js";
32
+ import { isInFlightRuntimePhase, listUnitRuntimeRecords } from "./unit-runtime.js";
32
33
  const SESSION_FILE_KV_KEY = "session_file";
33
34
  function lockPath(basePath) {
34
35
  return join(gsdRoot(basePath), effectiveLockFile());
@@ -72,9 +73,37 @@ function getLatestDispatchForWorker(workerId) {
72
73
  LIMIT 1`).get({ ":worker_id": workerId });
73
74
  return row ?? null;
74
75
  }
75
- function workerToLockData(worker) {
76
+ function latestInFlightRuntimeRecord(basePath) {
77
+ const records = listUnitRuntimeRecords(basePath).filter((record) => isInFlightRuntimePhase(record.phase));
78
+ if (records.length === 0)
79
+ return null;
80
+ return records.sort((a, b) => {
81
+ const bTime = b.updatedAt || b.startedAt || 0;
82
+ const aTime = a.updatedAt || a.startedAt || 0;
83
+ return bTime - aTime;
84
+ })[0] ?? null;
85
+ }
86
+ function runtimeRecordToLockData(worker, record, sessionFile) {
87
+ const startedAt = Number.isFinite(record.startedAt)
88
+ ? new Date(record.startedAt).toISOString()
89
+ : worker.started_at;
90
+ return {
91
+ pid: worker.pid,
92
+ startedAt: worker.started_at,
93
+ unitType: record.unitType,
94
+ unitId: record.unitId,
95
+ unitStartedAt: startedAt,
96
+ sessionFile,
97
+ };
98
+ }
99
+ function workerToLockData(basePath, worker) {
76
100
  const dispatch = getLatestDispatchForWorker(worker.worker_id);
77
101
  const sessionFile = getRuntimeKv("worker", worker.worker_id, SESSION_FILE_KV_KEY) ?? undefined;
102
+ if (!dispatch) {
103
+ const runtimeRecord = latestInFlightRuntimeRecord(basePath);
104
+ if (runtimeRecord)
105
+ return runtimeRecordToLockData(worker, runtimeRecord, sessionFile);
106
+ }
78
107
  return {
79
108
  pid: worker.pid,
80
109
  startedAt: worker.started_at,
@@ -172,7 +201,7 @@ export function readCrashLock(basePath) {
172
201
  const projectRoot = normalizeRealPath(basePath);
173
202
  const stale = findStaleWorkerForProject(projectRoot);
174
203
  if (stale)
175
- return workerToLockData(stale);
204
+ return workerToLockData(basePath, stale);
176
205
  }
177
206
  catch {
178
207
  // Fall through to the legacy lock-file compatibility path.
@@ -232,18 +261,32 @@ export function formatCrashInfo(lock) {
232
261
  export function emitCrashRecoveredUnitEnd(basePath, lock) {
233
262
  if (!lock.unitType || !lock.unitId || lock.unitType === "starting")
234
263
  return;
264
+ emitOpenUnitEndForUnit(basePath, lock.unitType, lock.unitId, "crash-recovered");
265
+ }
266
+ export function emitOpenUnitEndForUnit(basePath, unitType, unitId, status, errorContext) {
235
267
  try {
236
268
  const all = queryJournal(basePath);
237
- const starts = all.filter((e) => e.eventType === "unit-start" && e.data?.unitId === lock.unitId);
269
+ const starts = all.filter((e) => e.eventType === "unit-start" &&
270
+ e.data?.unitType === unitType &&
271
+ e.data?.unitId === unitId);
238
272
  if (starts.length === 0)
239
- return;
240
- const lastStart = starts[starts.length - 1];
273
+ return false;
274
+ const lastStart = [...starts].reverse().find((start) => {
275
+ return !all.some((e) => e.eventType === "unit-end" &&
276
+ e.data?.unitType === unitType &&
277
+ e.data?.unitId === unitId &&
278
+ e.causedBy?.flowId === start.flowId &&
279
+ e.causedBy?.seq === start.seq);
280
+ });
281
+ if (!lastStart)
282
+ return false;
241
283
  const alreadyClosed = all.some((e) => e.eventType === "unit-end" &&
242
- e.data?.unitId === lock.unitId &&
284
+ e.data?.unitType === unitType &&
285
+ e.data?.unitId === unitId &&
243
286
  e.causedBy?.flowId === lastStart.flowId &&
244
287
  e.causedBy?.seq === lastStart.seq);
245
288
  if (alreadyClosed)
246
- return;
289
+ return false;
247
290
  const maxSeq = all
248
291
  .filter((e) => e.flowId === lastStart.flowId)
249
292
  .reduce((max, e) => Math.max(max, e.seq), lastStart.seq);
@@ -253,16 +296,19 @@ export function emitCrashRecoveredUnitEnd(basePath, lock) {
253
296
  seq: maxSeq + 1,
254
297
  eventType: "unit-end",
255
298
  data: {
256
- unitType: lock.unitType,
257
- unitId: lock.unitId,
258
- status: "crash-recovered",
299
+ unitType,
300
+ unitId,
301
+ status,
259
302
  artifactVerified: false,
303
+ ...(errorContext ? { errorContext } : {}),
260
304
  },
261
305
  causedBy: { flowId: lastStart.flowId, seq: lastStart.seq },
262
306
  });
307
+ return true;
263
308
  }
264
309
  catch {
265
310
  // Never throw from crash recovery path.
311
+ return false;
266
312
  }
267
313
  }
268
314
  /**
@@ -16,7 +16,7 @@ import { invalidateAllCaches } from "./cache.js";
16
16
  import { startAutoDetached } from "./auto.js";
17
17
  import { clearLock } from "./crash-recovery.js";
18
18
  import { assessInterruptedSession, formatInterruptedSessionRunningMessage, formatInterruptedSessionSummary, } from "./interrupted-session.js";
19
- import { listUnitRuntimeRecords, clearUnitRuntimeRecord } from "./unit-runtime.js";
19
+ import { listUnitRuntimeRecords, clearUnitRuntimeRecord, isInFlightRuntimePhase } from "./unit-runtime.js";
20
20
  import { resolveExpectedArtifactPath } from "./auto.js";
21
21
  import { gsdHome } from "./gsd-home.js";
22
22
  import { gsdRoot, milestonesDir, resolveMilestoneFile, resolveMilestonePath, resolveSliceFile, resolveSlicePath, resolveGsdRootFile, relGsdRootFile, relMilestoneFile, relSliceFile, clearPathCache, } from "./paths.js";
@@ -71,6 +71,19 @@ export function verifyExpectedArtifactForScope(scope, unitType, unitId) {
71
71
  export function resolveExpectedArtifactPathForScope(scope, unitType, unitId) {
72
72
  return resolveExpectedArtifactPath(unitType, unitId, scope.workspace.projectRoot);
73
73
  }
74
+ async function runQuickTaskChoice(ctx, pi) {
75
+ if (!ctx.hasUI) {
76
+ ctx.ui.notify("Run /gsd quick <task> for small bounded work, or /gsd do <task> for natural-language routing.", "info");
77
+ return;
78
+ }
79
+ const task = (await ctx.ui.input("Quick task", "Describe the small task to run with /gsd quick"))?.trim();
80
+ if (!task) {
81
+ ctx.ui.notify("Quick task cancelled.", "info");
82
+ return;
83
+ }
84
+ const { handleQuick } = await import("./quick.js");
85
+ await handleQuick(task, ctx, pi);
86
+ }
74
87
  /**
75
88
  * Scope-based overload of isGhostMilestone.
76
89
  * Binds basePath and milestoneId from the scope, ensuring path resolution
@@ -1469,8 +1482,8 @@ function selfHealRuntimeRecords(basePath, ctx) {
1469
1482
  cleared++;
1470
1483
  continue;
1471
1484
  }
1472
- // Clear records stuck in dispatched or timeout phase (process died mid-unit)
1473
- if (phase === "dispatched" || phase === "timeout") {
1485
+ // Clear records stuck in an in-flight phase (process died mid-unit).
1486
+ if (isInFlightRuntimePhase(phase)) {
1474
1487
  clearUnitRuntimeRecord(basePath, unitType, unitId);
1475
1488
  cleared++;
1476
1489
  }
@@ -1695,6 +1708,20 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
1695
1708
  return;
1696
1709
  }
1697
1710
  }
1711
+ if (interrupted.classification !== "recoverable") {
1712
+ try {
1713
+ const { autoImportMarkdownHierarchyIfDbMismatch } = await import("./migration-auto-check.js");
1714
+ const result = await autoImportMarkdownHierarchyIfDbMismatch(basePath);
1715
+ if (result.action === "imported") {
1716
+ ctx.ui.notify(`Recovered migrated planning state into gsd.db (${result.reason}): ${result.afterDb.milestones} milestone(s), ${result.afterDb.slices} slice(s), ${result.afterDb.tasks} task(s).`, "info");
1717
+ }
1718
+ }
1719
+ catch (err) {
1720
+ const message = err instanceof Error ? err.message : String(err);
1721
+ ctx.ui.notify(`GSD could not auto-import existing planning state into gsd.db: ${message}`, "warning");
1722
+ logWarning("guided", `planning state auto-import failed: ${message}`, { file: "guided-flow.ts" });
1723
+ }
1724
+ }
1698
1725
  // Always derive from the project root — the assessment may have derived
1699
1726
  // state from a worktree path that was cleaned up in the stale branch above.
1700
1727
  const state = await deriveState(basePath);
@@ -1795,7 +1822,7 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
1795
1822
  notYetMessage: "Run /gsd when ready.",
1796
1823
  });
1797
1824
  if (choice === "quick_task") {
1798
- ctx.ui.notify("Run /gsd quick <task> for small bounded work, or /gsd do <task> for natural-language routing.", "info");
1825
+ await runQuickTaskChoice(ctx, pi);
1799
1826
  }
1800
1827
  else if (choice === "new_milestone") {
1801
1828
  setPendingAutoStart(basePath, { ctx, pi, basePath, milestoneId: nextId, step: stepMode });
@@ -1837,7 +1864,7 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
1837
1864
  notYetMessage: "Run /gsd when ready.",
1838
1865
  });
1839
1866
  if (choice === "quick_task") {
1840
- ctx.ui.notify("Run /gsd quick <task> for small bounded work, or /gsd do <task> for natural-language routing.", "info");
1867
+ await runQuickTaskChoice(ctx, pi);
1841
1868
  }
1842
1869
  else if (choice === "new_milestone") {
1843
1870
  const milestoneIds = findMilestoneIds(basePath);
@@ -1968,7 +1995,7 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
1968
1995
  notYetMessage: "Run /gsd when ready.",
1969
1996
  });
1970
1997
  if (choice === "quick_task") {
1971
- ctx.ui.notify("Run /gsd quick <task> for small bounded work, or /gsd do <task> for natural-language routing.", "info");
1998
+ await runQuickTaskChoice(ctx, pi);
1972
1999
  }
1973
2000
  else if (choice === "plan") {
1974
2001
  setPendingAutoStart(basePath, { ctx, pi, basePath, milestoneId, step: stepMode });
@@ -15,6 +15,43 @@ import { fileURLToPath } from "node:url";
15
15
  import { showNextAction } from "../../shared/tui.js";
16
16
  import { validatePlanningDirectory, parsePlanningDirectory, transformToGSD, generatePreview, writeGSDDirectory, } from "./index.js";
17
17
  import { homedir } from "node:os";
18
+ import { ensureDbOpen } from "../bootstrap/dynamic-tools.js";
19
+ import { clearEngineHierarchy, transaction } from "../gsd-db.js";
20
+ import { migrateFromMarkdown } from "../md-importer.js";
21
+ import { invalidateStateCache } from "../state.js";
22
+ function assertMigrationImportMatchesPreview(imported, preview) {
23
+ const mismatches = [];
24
+ if (imported.hierarchy.milestones !== preview.milestoneCount) {
25
+ mismatches.push(`milestones ${imported.hierarchy.milestones}/${preview.milestoneCount}`);
26
+ }
27
+ if (imported.hierarchy.slices !== preview.totalSlices) {
28
+ mismatches.push(`slices ${imported.hierarchy.slices}/${preview.totalSlices}`);
29
+ }
30
+ if (imported.hierarchy.tasks !== preview.totalTasks) {
31
+ mismatches.push(`tasks ${imported.hierarchy.tasks}/${preview.totalTasks}`);
32
+ }
33
+ if (imported.requirements !== preview.requirements.total) {
34
+ mismatches.push(`requirements ${imported.requirements}/${preview.requirements.total}`);
35
+ }
36
+ if (mismatches.length > 0) {
37
+ throw new Error(`migration DB import verification failed: ${mismatches.join(", ")}`);
38
+ }
39
+ }
40
+ export async function importWrittenMigrationToDb(basePath, preview) {
41
+ const opened = await ensureDbOpen(basePath);
42
+ if (!opened) {
43
+ throw new Error(`failed to open or create the GSD database at ${basePath}`);
44
+ }
45
+ const counts = transaction(() => {
46
+ clearEngineHierarchy();
47
+ const imported = migrateFromMarkdown(basePath);
48
+ if (preview)
49
+ assertMigrationImportMatchesPreview(imported, preview);
50
+ return imported;
51
+ });
52
+ invalidateStateCache();
53
+ return counts;
54
+ }
18
55
  /** Format preview stats for embedding in the review prompt. */
19
56
  function formatPreviewStats(preview) {
20
57
  const lines = [
@@ -126,12 +163,14 @@ export async function handleMigrate(args, ctx, pi) {
126
163
  ctx.ui.notify("Writing .gsd directory…", "info");
127
164
  const result = await writeGSDDirectory(project, process.cwd());
128
165
  const gsdPath = gsdRoot(process.cwd());
129
- ctx.ui.notify(`✓ Migration complete ${result.paths.length} file(s) written to .gsd/`, "info");
166
+ const imported = await importWrittenMigrationToDb(process.cwd(), preview);
167
+ ctx.ui.notify(`✓ Migration complete — ${result.paths.length} file(s) written to .gsd/ and ${imported.hierarchy.milestones}M/${imported.hierarchy.slices}S/${imported.hierarchy.tasks}T imported to the database`, "info");
130
168
  // ── Post-write review offer ────────────────────────────────────────────────
131
169
  const reviewChoice = await showNextAction(ctx, {
132
170
  title: "Migration written",
133
171
  summary: [
134
172
  `${result.paths.length} files written to .gsd/`,
173
+ `${imported.hierarchy.milestones} milestone(s), ${imported.hierarchy.slices} slice(s), and ${imported.hierarchy.tasks} task(s) imported to gsd.db`,
135
174
  "",
136
175
  "The agent can now review the migrated output against GSD-2 standards —",
137
176
  "checking structure, content quality, deriveState() round-trip, and",
@@ -0,0 +1,87 @@
1
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
2
+ import { ensureDbOpen } from "./bootstrap/dynamic-tools.js";
3
+ import { clearEngineHierarchy, getAllMilestones, getMilestoneSlices, getSliceTasks, isDbAvailable, transaction, } from "./gsd-db.js";
4
+ import { migrateHierarchyToDb } from "./md-importer.js";
5
+ import { parsePlan, parseRoadmap } from "./parsers-legacy.js";
6
+ import { milestonesDir, resolveMilestoneFile, resolveSliceFile, } from "./paths.js";
7
+ import { invalidateStateCache } from "./state.js";
8
+ function zeroCounts() {
9
+ return { milestones: 0, slices: 0, tasks: 0 };
10
+ }
11
+ function sameCounts(a, b) {
12
+ return a.milestones === b.milestones && a.slices === b.slices && a.tasks === b.tasks;
13
+ }
14
+ export function countMarkdownHierarchy(basePath) {
15
+ const root = milestonesDir(basePath);
16
+ if (!existsSync(root))
17
+ return zeroCounts();
18
+ const counts = zeroCounts();
19
+ for (const entry of readdirSync(root, { withFileTypes: true })) {
20
+ if (!entry.isDirectory() || !/^M\d+/.test(entry.name))
21
+ continue;
22
+ counts.milestones++;
23
+ const roadmapPath = resolveMilestoneFile(basePath, entry.name, "ROADMAP");
24
+ if (!roadmapPath || !existsSync(roadmapPath))
25
+ continue;
26
+ const roadmap = parseRoadmap(readFileSync(roadmapPath, "utf-8"));
27
+ counts.slices += roadmap.slices.length;
28
+ for (const slice of roadmap.slices) {
29
+ const planPath = resolveSliceFile(basePath, entry.name, slice.id, "PLAN");
30
+ if (!planPath || !existsSync(planPath))
31
+ continue;
32
+ const plan = parsePlan(readFileSync(planPath, "utf-8"));
33
+ counts.tasks += plan.tasks.length;
34
+ }
35
+ }
36
+ return counts;
37
+ }
38
+ export function countDbHierarchy() {
39
+ if (!isDbAvailable())
40
+ return zeroCounts();
41
+ const counts = zeroCounts();
42
+ const milestones = getAllMilestones();
43
+ counts.milestones = milestones.length;
44
+ for (const milestone of milestones) {
45
+ const slices = getMilestoneSlices(milestone.id);
46
+ counts.slices += slices.length;
47
+ for (const slice of slices) {
48
+ counts.tasks += getSliceTasks(milestone.id, slice.id).length;
49
+ }
50
+ }
51
+ return counts;
52
+ }
53
+ export async function autoImportMarkdownHierarchyIfDbMismatch(basePath) {
54
+ const markdown = countMarkdownHierarchy(basePath);
55
+ if (sameCounts(markdown, zeroCounts())) {
56
+ return {
57
+ action: "none",
58
+ reason: "no-markdown",
59
+ markdown,
60
+ beforeDb: zeroCounts(),
61
+ afterDb: zeroCounts(),
62
+ };
63
+ }
64
+ const opened = await ensureDbOpen(basePath);
65
+ if (!opened || !isDbAvailable()) {
66
+ throw new Error(`failed to open or create the GSD database at ${basePath}`);
67
+ }
68
+ const beforeDb = countDbHierarchy();
69
+ if (sameCounts(markdown, beforeDb)) {
70
+ return { action: "none", reason: "in-sync", markdown, beforeDb, afterDb: beforeDb };
71
+ }
72
+ const reason = sameCounts(beforeDb, zeroCounts()) ? "db-empty" : "count-mismatch";
73
+ const imported = transaction(() => {
74
+ clearEngineHierarchy();
75
+ return migrateHierarchyToDb(basePath);
76
+ });
77
+ invalidateStateCache();
78
+ const afterDb = {
79
+ milestones: imported.milestones,
80
+ slices: imported.slices,
81
+ tasks: imported.tasks,
82
+ };
83
+ if (!sameCounts(markdown, afterDb)) {
84
+ throw new Error(`migration auto-import verification failed: markdown ${markdown.milestones}M/${markdown.slices}S/${markdown.tasks}T, db ${afterDb.milestones}M/${afterDb.slices}S/${afterDb.tasks}T`);
85
+ }
86
+ return { action: "imported", reason, markdown, beforeDb, afterDb };
87
+ }