gsd-pi 2.78.1-dev.b0759e59b → 2.78.1-dev.e9d88a536

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 (203) hide show
  1. package/README.md +8 -5
  2. package/dist/headless-recover.d.ts +23 -0
  3. package/dist/headless-recover.js +93 -0
  4. package/dist/headless.js +9 -0
  5. package/dist/help-text.js +1 -0
  6. package/dist/resources/.managed-resources-content-hash +1 -1
  7. package/dist/resources/extensions/browser-tools/tools/intent.js +8 -1
  8. package/dist/resources/extensions/gsd/auto-dispatch.js +4 -56
  9. package/dist/resources/extensions/gsd/auto-post-unit.js +7 -27
  10. package/dist/resources/extensions/gsd/auto-start.js +1 -8
  11. package/dist/resources/extensions/gsd/auto-worktree.js +59 -176
  12. package/dist/resources/extensions/gsd/auto.js +24 -6
  13. package/dist/resources/extensions/gsd/bootstrap/dynamic-tools.js +9 -77
  14. package/dist/resources/extensions/gsd/commands-codebase.js +2 -2
  15. package/dist/resources/extensions/gsd/commands-handlers.js +5 -5
  16. package/dist/resources/extensions/gsd/commands-logs.js +2 -2
  17. package/dist/resources/extensions/gsd/commands-scan.js +2 -2
  18. package/dist/resources/extensions/gsd/commands-ship.js +2 -2
  19. package/dist/resources/extensions/gsd/commands-workflow-templates.js +5 -5
  20. package/dist/resources/extensions/gsd/db-writer.js +16 -85
  21. package/dist/resources/extensions/gsd/dispatch-guard.js +6 -10
  22. package/dist/resources/extensions/gsd/doctor-engine-checks.js +2 -2
  23. package/dist/resources/extensions/gsd/gsd-db.js +74 -8
  24. package/dist/resources/extensions/gsd/guided-flow.js +31 -8
  25. package/dist/resources/extensions/gsd/markdown-renderer.js +14 -51
  26. package/dist/resources/extensions/gsd/parallel-merge.js +14 -13
  27. package/dist/resources/extensions/gsd/parallel-monitor-overlay.js +5 -2
  28. package/dist/resources/extensions/gsd/paths.js +35 -1
  29. package/dist/resources/extensions/gsd/prompts/plan-milestone.md +6 -0
  30. package/dist/resources/extensions/gsd/queue-order.js +6 -1
  31. package/dist/resources/extensions/gsd/rethink.js +2 -2
  32. package/dist/resources/extensions/gsd/state.js +91 -372
  33. package/dist/resources/extensions/gsd/tools/complete-milestone.js +6 -5
  34. package/dist/resources/extensions/gsd/tools/complete-slice.js +7 -12
  35. package/dist/resources/extensions/gsd/tools/complete-task.js +19 -31
  36. package/dist/resources/extensions/gsd/tools/validate-milestone.js +7 -5
  37. package/dist/resources/extensions/gsd/workflow-manifest.js +2 -1
  38. package/dist/resources/extensions/gsd/workflow-mcp-auto-prep.js +3 -21
  39. package/dist/resources/extensions/gsd/workflow-reconcile.js +3 -3
  40. package/dist/resources/extensions/gsd/worktree-command.js +4 -3
  41. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  42. package/dist/web/standalone/.next/BUILD_ID +1 -1
  43. package/dist/web/standalone/.next/app-path-routes-manifest.json +12 -12
  44. package/dist/web/standalone/.next/build-manifest.json +2 -2
  45. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  46. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  47. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  55. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/api/boot/route.js.nft.json +1 -1
  63. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js.nft.json +1 -1
  64. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js.nft.json +1 -1
  65. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js.nft.json +1 -1
  66. package/dist/web/standalone/.next/server/app/api/captures/route.js.nft.json +1 -1
  67. package/dist/web/standalone/.next/server/app/api/cleanup/route.js.nft.json +1 -1
  68. package/dist/web/standalone/.next/server/app/api/doctor/route.js.nft.json +1 -1
  69. package/dist/web/standalone/.next/server/app/api/export-data/route.js.nft.json +1 -1
  70. package/dist/web/standalone/.next/server/app/api/files/route.js.nft.json +1 -1
  71. package/dist/web/standalone/.next/server/app/api/forensics/route.js.nft.json +1 -1
  72. package/dist/web/standalone/.next/server/app/api/git/route.js.nft.json +1 -1
  73. package/dist/web/standalone/.next/server/app/api/history/route.js.nft.json +1 -1
  74. package/dist/web/standalone/.next/server/app/api/hooks/route.js.nft.json +1 -1
  75. package/dist/web/standalone/.next/server/app/api/inspect/route.js.nft.json +1 -1
  76. package/dist/web/standalone/.next/server/app/api/knowledge/route.js.nft.json +1 -1
  77. package/dist/web/standalone/.next/server/app/api/live-state/route.js.nft.json +1 -1
  78. package/dist/web/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
  79. package/dist/web/standalone/.next/server/app/api/onboarding/route.js.nft.json +1 -1
  80. package/dist/web/standalone/.next/server/app/api/projects/route.js.nft.json +1 -1
  81. package/dist/web/standalone/.next/server/app/api/recovery/route.js.nft.json +1 -1
  82. package/dist/web/standalone/.next/server/app/api/session/browser/route.js.nft.json +1 -1
  83. package/dist/web/standalone/.next/server/app/api/session/command/route.js.nft.json +1 -1
  84. package/dist/web/standalone/.next/server/app/api/session/events/route.js.nft.json +1 -1
  85. package/dist/web/standalone/.next/server/app/api/session/manage/route.js.nft.json +1 -1
  86. package/dist/web/standalone/.next/server/app/api/settings-data/route.js.nft.json +1 -1
  87. package/dist/web/standalone/.next/server/app/api/skill-health/route.js.nft.json +1 -1
  88. package/dist/web/standalone/.next/server/app/api/steer/route.js.nft.json +1 -1
  89. package/dist/web/standalone/.next/server/app/api/switch-root/route.js.nft.json +1 -1
  90. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js.nft.json +1 -1
  91. package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js.nft.json +1 -1
  92. package/dist/web/standalone/.next/server/app/api/undo/route.js.nft.json +1 -1
  93. package/dist/web/standalone/.next/server/app/api/visualizer/route.js.nft.json +1 -1
  94. package/dist/web/standalone/.next/server/app/index.html +1 -1
  95. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  96. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  97. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  98. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  99. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  100. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  101. package/dist/web/standalone/.next/server/app-paths-manifest.json +12 -12
  102. package/dist/web/standalone/.next/server/chunks/6336.js +1 -0
  103. package/dist/web/standalone/.next/server/chunks/6897.js +1 -1
  104. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  105. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  106. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  107. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  108. package/package.json +1 -1
  109. package/packages/mcp-server/dist/workflow-tools.d.ts +6 -0
  110. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  111. package/packages/mcp-server/dist/workflow-tools.js +56 -2
  112. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  113. package/packages/mcp-server/src/parse-workflow-args.test.ts +80 -0
  114. package/packages/mcp-server/src/workflow-tools.ts +61 -2
  115. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  116. package/src/resources/extensions/browser-tools/tools/intent.ts +13 -2
  117. package/src/resources/extensions/gsd/auto-dispatch.ts +4 -60
  118. package/src/resources/extensions/gsd/auto-post-unit.ts +7 -26
  119. package/src/resources/extensions/gsd/auto-start.ts +1 -8
  120. package/src/resources/extensions/gsd/auto-worktree.ts +61 -204
  121. package/src/resources/extensions/gsd/auto.ts +23 -6
  122. package/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts +9 -84
  123. package/src/resources/extensions/gsd/commands-codebase.ts +2 -2
  124. package/src/resources/extensions/gsd/commands-handlers.ts +5 -5
  125. package/src/resources/extensions/gsd/commands-logs.ts +2 -2
  126. package/src/resources/extensions/gsd/commands-scan.ts +2 -2
  127. package/src/resources/extensions/gsd/commands-ship.ts +2 -2
  128. package/src/resources/extensions/gsd/commands-workflow-templates.ts +5 -5
  129. package/src/resources/extensions/gsd/db-writer.ts +16 -83
  130. package/src/resources/extensions/gsd/dispatch-guard.ts +6 -11
  131. package/src/resources/extensions/gsd/doctor-engine-checks.ts +2 -2
  132. package/src/resources/extensions/gsd/gsd-db.ts +85 -8
  133. package/src/resources/extensions/gsd/guided-flow.ts +35 -8
  134. package/src/resources/extensions/gsd/markdown-renderer.ts +13 -64
  135. package/src/resources/extensions/gsd/parallel-merge.ts +14 -13
  136. package/src/resources/extensions/gsd/parallel-monitor-overlay.ts +5 -2
  137. package/src/resources/extensions/gsd/paths.ts +55 -1
  138. package/src/resources/extensions/gsd/prompts/plan-milestone.md +6 -0
  139. package/src/resources/extensions/gsd/queue-order.ts +6 -1
  140. package/src/resources/extensions/gsd/rethink.ts +2 -2
  141. package/src/resources/extensions/gsd/state.ts +91 -389
  142. package/src/resources/extensions/gsd/tests/artifact-corruption-2630.test.ts +1 -0
  143. package/src/resources/extensions/gsd/tests/auto-paused-session-validation.test.ts +6 -0
  144. package/src/resources/extensions/gsd/tests/auto-remediate-slice-status.test.ts +21 -34
  145. package/src/resources/extensions/gsd/tests/complete-task-rollback-evidence.test.ts +6 -7
  146. package/src/resources/extensions/gsd/tests/complete-task.test.ts +8 -6
  147. package/src/resources/extensions/gsd/tests/completed-at-reconcile.test.ts +12 -27
  148. package/src/resources/extensions/gsd/tests/completed-units-metrics-sync.test.ts +18 -5
  149. package/src/resources/extensions/gsd/tests/db-path-worktree-symlink.test.ts +4 -4
  150. package/src/resources/extensions/gsd/tests/db-writer.test.ts +14 -16
  151. package/src/resources/extensions/gsd/tests/derive-state-crossval.test.ts +6 -5
  152. package/src/resources/extensions/gsd/tests/derive-state-db-disk-reconcile.test.ts +10 -38
  153. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +136 -56
  154. package/src/resources/extensions/gsd/tests/derive-state-draft.test.ts +3 -0
  155. package/src/resources/extensions/gsd/tests/derive-state-helpers.test.ts +119 -61
  156. package/src/resources/extensions/gsd/tests/derive-state.test.ts +4 -0
  157. package/src/resources/extensions/gsd/tests/dispatch-complete-milestone-guard.test.ts +6 -20
  158. package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +4 -5
  159. package/src/resources/extensions/gsd/tests/dispatcher-stuck-planning.test.ts +14 -15
  160. package/src/resources/extensions/gsd/tests/ensure-db-open.test.ts +11 -16
  161. package/src/resources/extensions/gsd/tests/escalation.test.ts +2 -1
  162. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +2 -1
  163. package/src/resources/extensions/gsd/tests/gsdroot-worktree-detection.test.ts +15 -36
  164. package/src/resources/extensions/gsd/tests/handler-worktree-write-isolation.test.ts +57 -0
  165. package/src/resources/extensions/gsd/tests/integration/parallel-merge.test.ts +15 -15
  166. package/src/resources/extensions/gsd/tests/integration/state-machine-edge-cases.test.ts +15 -5
  167. package/src/resources/extensions/gsd/tests/markdown-renderer.test.ts +14 -8
  168. package/src/resources/extensions/gsd/tests/md-importer.test.ts +2 -1
  169. package/src/resources/extensions/gsd/tests/memory-store.test.ts +3 -2
  170. package/src/resources/extensions/gsd/tests/park-milestone.test.ts +2 -0
  171. package/src/resources/extensions/gsd/tests/progressive-planning.test.ts +25 -16
  172. package/src/resources/extensions/gsd/tests/projection-regression.test.ts +1 -0
  173. package/src/resources/extensions/gsd/tests/ready-phrase-no-files-4573.test.ts +184 -0
  174. package/src/resources/extensions/gsd/tests/register-hooks-compaction-checkpoint.test.ts +6 -1
  175. package/src/resources/extensions/gsd/tests/replan-slice.test.ts +3 -0
  176. package/src/resources/extensions/gsd/tests/resolve-ts.mjs +4 -0
  177. package/src/resources/extensions/gsd/tests/rogue-file-detection.test.ts +3 -4
  178. package/src/resources/extensions/gsd/tests/slice-disk-reconcile.test.ts +10 -56
  179. package/src/resources/extensions/gsd/tests/stale-slice-rows.test.ts +15 -16
  180. package/src/resources/extensions/gsd/tests/state-corruption-2945.test.ts +1 -0
  181. package/src/resources/extensions/gsd/tests/state-machine-full-walkthrough.test.ts +23 -27
  182. package/src/resources/extensions/gsd/tests/steer-worktree-path.test.ts +13 -14
  183. package/src/resources/extensions/gsd/tests/stop-auto-remote.test.ts +4 -3
  184. package/src/resources/extensions/gsd/tests/sync-worktree-skip-current.test.ts +10 -33
  185. package/src/resources/extensions/gsd/tests/validate-milestone-write-order.test.ts +7 -8
  186. package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +9 -15
  187. package/src/resources/extensions/gsd/tests/workflow-logger-wiring.test.ts +12 -7
  188. package/src/resources/extensions/gsd/tests/workflow-mcp-auto-prep.test.ts +4 -4
  189. package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +24 -1
  190. package/src/resources/extensions/gsd/tests/worktree-db-same-file.test.ts +13 -0
  191. package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +65 -71
  192. package/src/resources/extensions/gsd/tests/worktree-sync-tasks.test.ts +26 -151
  193. package/src/resources/extensions/gsd/tools/complete-milestone.ts +7 -5
  194. package/src/resources/extensions/gsd/tools/complete-slice.ts +7 -14
  195. package/src/resources/extensions/gsd/tools/complete-task.ts +19 -34
  196. package/src/resources/extensions/gsd/tools/validate-milestone.ts +7 -5
  197. package/src/resources/extensions/gsd/workflow-manifest.ts +4 -1
  198. package/src/resources/extensions/gsd/workflow-mcp-auto-prep.ts +2 -18
  199. package/src/resources/extensions/gsd/workflow-reconcile.ts +3 -3
  200. package/src/resources/extensions/gsd/worktree-command.ts +4 -3
  201. package/dist/web/standalone/.next/server/chunks/8527.js +0 -1
  202. /package/dist/web/standalone/.next/static/{rk1EN3FQTE6Z1yalkW_GE → oZGTPvJBQX_IDKKnuV8Bt}/_buildManifest.js +0 -0
  203. /package/dist/web/standalone/.next/static/{rk1EN3FQTE6Z1yalkW_GE → oZGTPvJBQX_IDKKnuV8Bt}/_ssgManifest.js +0 -0
@@ -1,11 +1,12 @@
1
1
  import { existsSync } from "node:fs";
2
- import { join, sep } from "node:path";
2
+ import { dirname } from "node:path";
3
3
 
4
4
  import type { ExtensionAPI } from "@gsd/pi-coding-agent";
5
5
  import { createBashTool, createEditTool, createReadTool, createWriteTool } from "@gsd/pi-coding-agent";
6
6
 
7
7
  import { DEFAULT_BASH_TIMEOUT_SECS } from "../constants.js";
8
8
  import { setLogBasePath, logWarning } from "../workflow-logger.js";
9
+ import { resolveGsdPathContract } from "../paths.js";
9
10
 
10
11
  /**
11
12
  * Resolve the correct DB path for the current working directory.
@@ -14,75 +15,16 @@ import { setLogBasePath, logWarning } from "../workflow-logger.js";
14
15
  * returns `<basePath>/.gsd/gsd.db`.
15
16
  */
16
17
  export function resolveProjectRootDbPath(basePath: string): string {
17
- // Detect worktree: look for `.gsd/worktrees/` in the path segments.
18
- // A worktree path looks like: /project/root/.gsd/worktrees/M001/...
19
- // We need to resolve back to /project/root/.gsd/gsd.db
20
- const marker = `${sep}.gsd${sep}worktrees${sep}`;
21
- const idx = basePath.indexOf(marker);
22
- if (idx !== -1) {
23
- const projectRoot = basePath.slice(0, idx);
24
- return join(projectRoot, ".gsd", "gsd.db");
25
- }
26
-
27
- // Also handle forward-slash paths on all platforms
28
- const fwdMarker = "/.gsd/worktrees/";
29
- const fwdIdx = basePath.indexOf(fwdMarker);
30
- if (fwdIdx !== -1) {
31
- const projectRoot = basePath.slice(0, fwdIdx);
32
- return join(projectRoot, ".gsd", "gsd.db");
33
- }
34
-
35
- // External-state layout: ~/.gsd/projects/<hash>/worktrees/<MID>/...
36
- // Resolve to ~/.gsd/projects/<hash>/gsd.db (the canonical project DB) (#2952).
37
- // Must be checked before the generic symlink-resolved handler: both match
38
- // /.gsd/projects/<hash>/worktrees/ but require different resolution targets.
39
- const extRe = /[/\\]\.gsd[/\\]projects[/\\][a-f0-9]+[/\\]worktrees(?:[/\\]|$)/;
40
- const extMatch = extRe.exec(basePath);
41
- if (extMatch) {
42
- const matchStr = extMatch[0];
43
- // Find the "/worktrees" portion within the match and slice up to it
44
- const wtIdx = matchStr.search(/[/\\]worktrees(?:[/\\]|$)/);
45
- const projectStateRoot = basePath.slice(0, extMatch.index + wtIdx);
46
- return join(projectStateRoot, "gsd.db");
47
- }
48
-
49
- // Symlink-resolved layout: /.gsd/projects/<hash>/worktrees/M001/...
50
- // The project root is everything before /.gsd/projects/ (#2517)
51
- const symlinkMarker = `${sep}.gsd${sep}projects${sep}`;
52
- const symlinkIdx = basePath.indexOf(symlinkMarker);
53
- if (symlinkIdx !== -1) {
54
- const afterProjects = basePath.slice(symlinkIdx + symlinkMarker.length);
55
- // Expect: <hash>/worktrees/...
56
- const worktreeSeg = `${sep}worktrees${sep}`;
57
- if (afterProjects.includes(worktreeSeg)) {
58
- const projectRoot = basePath.slice(0, symlinkIdx);
59
- return join(projectRoot, ".gsd", "gsd.db");
60
- }
61
- }
62
-
63
- // Forward-slash variant for symlink-resolved layout
64
- const fwdSymlinkMarker = "/.gsd/projects/";
65
- const fwdSymlinkIdx = basePath.indexOf(fwdSymlinkMarker);
66
- if (fwdSymlinkIdx !== -1) {
67
- const afterProjects = basePath.slice(fwdSymlinkIdx + fwdSymlinkMarker.length);
68
- if (afterProjects.includes("/worktrees/")) {
69
- const projectRoot = basePath.slice(0, fwdSymlinkIdx);
70
- return join(projectRoot, ".gsd", "gsd.db");
71
- }
72
- }
73
-
74
-
75
- return join(basePath, ".gsd", "gsd.db");
18
+ return resolveGsdPathContract(basePath).projectDb;
76
19
  }
77
20
 
78
21
  export async function ensureDbOpen(basePath: string = process.cwd()): Promise<boolean> {
79
22
  try {
80
23
  const db = await import("../gsd-db.js");
81
- const dbPath = resolveProjectRootDbPath(basePath);
82
- const gsdDir = join(basePath, ".gsd");
83
-
84
- // Derive the project root from the DB path (strip .gsd/gsd.db)
85
- const projectRoot = join(dbPath, "..", "..");
24
+ const contract = resolveGsdPathContract(basePath);
25
+ const dbPath = contract.projectDb;
26
+ const gsdDir = contract.projectGsd;
27
+ const projectRoot = dirname(dirname(dbPath));
86
28
 
87
29
  // Open existing DB file (may be at project root for worktrees)
88
30
  if (existsSync(dbPath)) {
@@ -91,26 +33,9 @@ export async function ensureDbOpen(basePath: string = process.cwd()): Promise<bo
91
33
  return opened;
92
34
  }
93
35
 
94
- // No DB file — create + migrate from Markdown if .gsd/ has content
36
+ // No DB file — create an empty authoritative DB. Markdown migration is
37
+ // explicit-only; runtime startup must not import projections into state.
95
38
  if (existsSync(gsdDir)) {
96
- const hasDecisions = existsSync(join(gsdDir, "DECISIONS.md"));
97
- const hasRequirements = existsSync(join(gsdDir, "REQUIREMENTS.md"));
98
- const hasMilestones = existsSync(join(gsdDir, "milestones"));
99
- if (hasDecisions || hasRequirements || hasMilestones) {
100
- const opened = db.openDatabase(dbPath);
101
- if (opened) {
102
- setLogBasePath(projectRoot);
103
- try {
104
- const { migrateFromMarkdown } = await import("../md-importer.js");
105
- migrateFromMarkdown(basePath);
106
- } catch (err) {
107
- logWarning("bootstrap", `ensureDbOpen auto-migration failed: ${(err as Error).message}`);
108
- }
109
- }
110
- return opened;
111
- }
112
-
113
- // .gsd/ exists but has no Markdown content (fresh project) — create empty DB
114
39
  const opened = db.openDatabase(dbPath);
115
40
  if (opened) setLogBasePath(projectRoot);
116
41
  return opened;
@@ -16,7 +16,7 @@ import {
16
16
  } from "./codebase-generator.js";
17
17
  import { loadEffectiveGSDPreferences } from "./preferences.js";
18
18
  import type { CodebaseMapOptions } from "./codebase-generator.js";
19
- import { projectRoot } from "./commands/context.js";
19
+ import { currentDirectoryRoot } from "./commands/context.js";
20
20
 
21
21
  const USAGE =
22
22
  "Usage: /gsd codebase [generate|update|stats]\n\n" +
@@ -37,7 +37,7 @@ export async function handleCodebase(
37
37
  ctx: ExtensionCommandContext,
38
38
  _pi: ExtensionAPI,
39
39
  ): Promise<void> {
40
- const basePath = projectRoot();
40
+ const basePath = currentDirectoryRoot();
41
41
  const parts = args.trim().split(/\s+/);
42
42
  const sub = parts[0] ?? "";
43
43
 
@@ -24,7 +24,7 @@ import {
24
24
  } from "./doctor.js";
25
25
  import { isAutoActive, checkRemoteAutoSession } from "./auto.js";
26
26
  import { getAutoWorktreePath } from "./auto-worktree.js";
27
- import { projectRoot } from "./commands/context.js";
27
+ import { currentDirectoryRoot, projectRoot } from "./commands/context.js";
28
28
  import { loadPrompt } from "./prompt-loader.js";
29
29
 
30
30
  const UPDATE_REGISTRY_URL = "https://registry.npmjs.org/gsd-pi/latest";
@@ -203,7 +203,7 @@ export async function handleCapture(args: string, ctx: ExtensionCommandContext):
203
203
  return;
204
204
  }
205
205
 
206
- const basePath = projectRoot();
206
+ const basePath = currentDirectoryRoot();
207
207
 
208
208
  // Ensure .gsd/ exists — capture should work even without a milestone
209
209
  const gsdDir = gsdRoot(basePath);
@@ -270,7 +270,7 @@ export async function handleTriage(ctx: ExtensionCommandContext, pi: ExtensionAP
270
270
  }
271
271
 
272
272
  export async function handleSteer(change: string, ctx: ExtensionCommandContext, pi: ExtensionAPI): Promise<void> {
273
- const basePath = projectRoot();
273
+ const basePath = currentDirectoryRoot();
274
274
  const state = await deriveState(basePath);
275
275
  const mid = state.activeMilestone?.id ?? "none";
276
276
  const sid = state.activeSlice?.id ?? "none";
@@ -343,7 +343,7 @@ export async function handleKnowledge(args: string, ctx: ExtensionCommandContext
343
343
  }
344
344
 
345
345
  const type = typeArg as "rule" | "pattern" | "lesson";
346
- const basePath = projectRoot();
346
+ const basePath = currentDirectoryRoot();
347
347
  const state = await deriveState(basePath);
348
348
  const scope = state.activeMilestone?.id
349
349
  ? `${state.activeMilestone.id}${state.activeSlice ? `/${state.activeSlice.id}` : ""}`
@@ -372,7 +372,7 @@ Examples:
372
372
  }
373
373
 
374
374
  const [hookName, unitType, unitId] = parts;
375
- const basePath = projectRoot();
375
+ const basePath = currentDirectoryRoot();
376
376
 
377
377
  // Import the hook trigger function
378
378
  const { triggerHookManually, formatHookStatus, getHookStatus } = await import("./post-unit-hooks.js");
@@ -15,7 +15,7 @@ import { existsSync, readdirSync, readFileSync, statSync, unlinkSync } from "nod
15
15
  import { join } from "node:path";
16
16
  import { gsdRoot } from "./paths.js";
17
17
  import { loadJsonFileOrNull } from "./json-persistence.js";
18
- import { projectRoot } from "./commands/context.js";
18
+ import { currentDirectoryRoot } from "./commands/context.js";
19
19
 
20
20
  // ─── Types ──────────────────────────────────────────────────────────────────
21
21
 
@@ -244,7 +244,7 @@ function summarizeDebugLog(filePath: string): {
244
244
  // ─── Main Handler ───────────────────────────────────────────────────────────
245
245
 
246
246
  export async function handleLogs(args: string, ctx: ExtensionCommandContext): Promise<void> {
247
- const basePath = projectRoot();
247
+ const basePath = currentDirectoryRoot();
248
248
  const parts = args.trim().split(/\s+/).filter(Boolean);
249
249
  const subCmd = parts[0] ?? "";
250
250
 
@@ -20,7 +20,7 @@ import { existsSync, mkdirSync } from "node:fs";
20
20
  import { join, relative } from "node:path";
21
21
 
22
22
  import { loadPrompt } from "./prompt-loader.js";
23
- import { projectRoot } from "./commands/context.js";
23
+ import { currentDirectoryRoot } from "./commands/context.js";
24
24
 
25
25
  // ─── Constants ────────────────────────────────────────────────────────────────
26
26
 
@@ -87,7 +87,7 @@ export async function handleScan(
87
87
  ctx: ExtensionCommandContext,
88
88
  pi: ExtensionAPI,
89
89
  ): Promise<void> {
90
- const basePath = projectRoot();
90
+ const basePath = currentDirectoryRoot();
91
91
  const { focus } = parseScanArgs(args);
92
92
  const outputDir = join(basePath, ".gsd", "codebase");
93
93
  const outputPaths = buildScanOutputPaths(focus, basePath);
@@ -17,7 +17,7 @@ import { getLedger, getProjectTotals, aggregateByModel, formatCost, formatTokenC
17
17
  import { nativeGetCurrentBranch, nativeDetectMainBranch } from "./native-git-bridge.js";
18
18
  import { formatDuration } from "../shared/format-utils.js";
19
19
  import { parseEvalReviewFrontmatter, type Verdict } from "./eval-review-schema.js";
20
- import { projectRoot } from "./commands/context.js";
20
+ import { currentDirectoryRoot } from "./commands/context.js";
21
21
 
22
22
  function git(basePath: string, args: readonly string[]): string {
23
23
  return execFileSync("git", args, { cwd: basePath, encoding: "utf-8" }).trim();
@@ -223,7 +223,7 @@ export async function handleShip(
223
223
  ctx: ExtensionCommandContext,
224
224
  _pi: ExtensionAPI,
225
225
  ): Promise<void> {
226
- const basePath = projectRoot();
226
+ const basePath = currentDirectoryRoot();
227
227
  const dryRun = args.includes("--dry-run");
228
228
  const draft = args.includes("--draft");
229
229
  const force = args.includes("--force");
@@ -23,7 +23,7 @@ import { createGitService, runGit } from "./git-service.js";
23
23
  import { isAutoActive, isAutoPaused } from "./auto.js";
24
24
  import { getErrorMessage } from "./error-utils.js";
25
25
  import { resolvePlugin, type WorkflowPlugin } from "./workflow-plugins.js";
26
- import { projectRoot } from "./commands/context.js";
26
+ import { currentDirectoryRoot } from "./commands/context.js";
27
27
 
28
28
  // ─── Helpers ─────────────────────────────────────────────────────────────────
29
29
 
@@ -197,7 +197,7 @@ export async function handleStart(
197
197
  // ─── Resume detection ───────────────────────────────────────────────────
198
198
  // /gsd start --resume or /gsd start resume → resume in-progress workflow
199
199
  if (trimmed === "--resume" || trimmed === "resume") {
200
- const basePath = projectRoot();
200
+ const basePath = currentDirectoryRoot();
201
201
  const inProgress = findInProgressWorkflows(basePath);
202
202
  if (inProgress.length === 0) {
203
203
  ctx.ui.notify("No in-progress workflows found.", "info");
@@ -248,7 +248,7 @@ export async function handleStart(
248
248
 
249
249
  // Show in-progress workflows when /gsd start is called with no args
250
250
  if (!trimmed) {
251
- const basePath = projectRoot();
251
+ const basePath = currentDirectoryRoot();
252
252
  const inProgress = findInProgressWorkflows(basePath);
253
253
  if (inProgress.length > 0) {
254
254
  const wf = inProgress[0];
@@ -347,7 +347,7 @@ export async function handleStart(
347
347
 
348
348
  const templateId = match.id;
349
349
  const template = match.template;
350
- const basePath = projectRoot();
350
+ const basePath = currentDirectoryRoot();
351
351
  const date = new Date().toISOString().split("T")[0];
352
352
 
353
353
  // Load the workflow template content — prefer a project/global plugin
@@ -582,7 +582,7 @@ export function dispatchMarkdownPhasePlugin(
582
582
  }
583
583
 
584
584
  const templateId = plugin.name;
585
- const basePath = projectRoot();
585
+ const basePath = currentDirectoryRoot();
586
586
  const date = new Date().toISOString().split("T")[0];
587
587
  let workflowContent: string;
588
588
  try {
@@ -318,23 +318,6 @@ export async function saveRequirementToDb(
318
318
  LIMIT 1`,
319
319
  )
320
320
  .get({ ':description': fields.description });
321
- const previousRow: Requirement | null = existingRow
322
- ? {
323
- id: existingRow['id'] as string,
324
- class: existingRow['class'] as string,
325
- status: existingRow['status'] as string,
326
- description: existingRow['description'] as string,
327
- why: existingRow['why'] as string,
328
- source: existingRow['source'] as string,
329
- primary_owner: existingRow['primary_owner'] as string,
330
- supporting_slices: existingRow['supporting_slices'] as string,
331
- validation: existingRow['validation'] as string,
332
- notes: existingRow['notes'] as string,
333
- full_content: existingRow['full_content'] as string,
334
- superseded_by: (existingRow['superseded_by'] as string) ?? null,
335
- }
336
- : null;
337
-
338
321
  const row = adapter
339
322
  .prepare('SELECT MAX(CAST(SUBSTR(id, 2) AS INTEGER)) as max_num FROM requirements')
340
323
  .get();
@@ -361,9 +344,9 @@ export async function saveRequirementToDb(
361
344
  };
362
345
 
363
346
  db.upsertRequirement(requirement);
364
- return { id: nextId, isNew: !existingRow, previousRow };
347
+ return { id: nextId };
365
348
  });
366
- const { id, isNew, previousRow } = txResult;
349
+ const { id } = txResult;
367
350
 
368
351
  // Fetch all requirements for full file regeneration
369
352
  const adapter = db._getAdapter();
@@ -392,17 +375,7 @@ export async function saveRequirementToDb(
392
375
  try {
393
376
  await saveFile(filePath, md);
394
377
  } catch (diskErr) {
395
- logError('manifest', 'disk write failed, rolling back DB row', { fn: 'saveRequirementToDb', error: String((diskErr as Error).message) });
396
- try {
397
- if (isNew) {
398
- db.deleteRequirementById(id);
399
- } else if (previousRow) {
400
- db.upsertRequirement(previousRow);
401
- }
402
- } catch (rollbackErr) {
403
- logError('manifest', 'SPLIT BRAIN: disk write failed AND DB rollback failed — DB has orphaned row', { fn: 'saveRequirementToDb', id, error: String((rollbackErr as Error).message) });
404
- }
405
- throw diskErr;
378
+ logWarning('projection', 'REQUIREMENTS.md projection write failed; DB requirement remains committed', { fn: 'saveRequirementToDb', id, error: String((diskErr as Error).message) });
406
379
  }
407
380
  invalidateStateCache();
408
381
  clearPathCache();
@@ -538,13 +511,7 @@ export async function saveDecisionToDb(
538
511
  try {
539
512
  await saveFile(filePath, md);
540
513
  } catch (diskErr) {
541
- logError('manifest', 'disk write failed, rolling back DB row', { fn: 'saveDecisionToDb', error: String((diskErr as Error).message) });
542
- try {
543
- db.deleteDecisionById(id);
544
- } catch (rollbackErr) {
545
- logError('manifest', 'SPLIT BRAIN: disk write failed AND DB rollback failed — DB has orphaned row', { fn: 'saveDecisionToDb', id, error: String((rollbackErr as Error).message) });
546
- }
547
- throw diskErr;
514
+ logWarning('projection', 'DECISIONS.md projection write failed; DB decision remains committed', { fn: 'saveDecisionToDb', id, error: String((diskErr as Error).message) });
548
515
  }
549
516
  // #2661: When a decision defers a slice, update the slice status in the DB
550
517
  // so the dispatcher skips it. Without this, STATE.md and DECISIONS.md are
@@ -667,34 +634,7 @@ export async function updateRequirementInDb(
667
634
  try {
668
635
  const db = await import('./gsd-db.js');
669
636
 
670
- let existing = db.getRequirementById(id);
671
-
672
- // If requirement doesn't exist in DB, seed the entire requirements table
673
- // from REQUIREMENTS.md first (#3346). This handles the standard workflow
674
- // where requirements are authored in markdown during discussion but never
675
- // imported into the database — making gsd_requirement_update always fail
676
- // with "not_found" at milestone completion.
677
- if (!existing) {
678
- const reqFilePath = resolveGsdRootFile(basePath, 'REQUIREMENTS');
679
- try {
680
- const content = readFileSync(reqFilePath, 'utf-8');
681
- const { parseRequirementsSections } = await import('./md-importer.js');
682
- const parsed = parseRequirementsSections(content);
683
- if (parsed.length > 0) {
684
- logWarning('manifest', `Seeding ${parsed.length} requirements from REQUIREMENTS.md into DB (first update triggers import)`, { fn: 'updateRequirementInDb' });
685
- for (const req of parsed) {
686
- // Only seed if not already in DB (avoid overwriting concurrent inserts)
687
- if (!db.getRequirementById(req.id)) {
688
- db.upsertRequirement(req);
689
- }
690
- }
691
- // Re-check after seeding
692
- existing = db.getRequirementById(id);
693
- }
694
- } catch {
695
- // REQUIREMENTS.md missing or unparseable — fall through to skeleton
696
- }
697
- }
637
+ const existing = db.getRequirementById(id);
698
638
 
699
639
  const base: Requirement = existing ?? {
700
640
  id,
@@ -750,11 +690,7 @@ export async function updateRequirementInDb(
750
690
  try {
751
691
  await saveFile(filePath, md);
752
692
  } catch (diskErr) {
753
- logError('manifest', 'disk write failed, reverting DB row', { fn: 'updateRequirementInDb', error: String((diskErr as Error).message) });
754
- if (existing) {
755
- db.upsertRequirement(existing);
756
- }
757
- throw diskErr;
693
+ logWarning('projection', 'REQUIREMENTS.md projection write failed; DB requirement update remains committed', { fn: 'updateRequirementInDb', id, error: String((diskErr as Error).message) });
758
694
  }
759
695
  // Invalidate file-read caches so deriveState() sees the updated markdown.
760
696
  // Do NOT clear the artifacts table — we just wrote to it intentionally.
@@ -805,20 +741,19 @@ export async function saveArtifactToDb(
805
741
  contentToPersist = generateRequirementsMd(activeRequirements);
806
742
  }
807
743
 
808
- // Shrinkage guard: if the file already exists and the new content is
809
- // significantly smaller (<50%), preserve the richer file on disk and
810
- // store its content in the DB instead of the abbreviated version. Root
811
- // canonical artifacts are exempt because their content is rendered from
812
- // canonical DB state, and cleanup/consolidation is often intentionally much
813
- // smaller than a malformed accumulated file.
814
- let dbContent = contentToPersist;
744
+ // Shrinkage guard: if the projection file already exists and the new
745
+ // content is significantly smaller (<50%), preserve the richer file on
746
+ // disk, but keep the DB row authoritative with the caller-provided content.
747
+ // The disk file is a stale projection until the next explicit render.
748
+ // Root canonical artifacts are exempt because their content is rendered
749
+ // from canonical DB state, and cleanup/consolidation is often intentionally
750
+ // much smaller than a malformed accumulated file.
815
751
  let skipDiskWrite = false;
816
752
  if (!isRootCanonicalArtifact(opts) && existsSync(fullPath)) {
817
753
  const existingSize = statSync(fullPath).size;
818
754
  const newSize = Buffer.byteLength(contentToPersist, 'utf-8');
819
755
  if (existingSize > 0 && newSize < existingSize * 0.5) {
820
- logWarning('manifest', `new content (${newSize}B) is <50% of existing file (${existingSize}B), preserving disk file`, { fn: 'saveArtifactToDb', path: opts.path });
821
- dbContent = readFileSync(fullPath, 'utf-8');
756
+ logWarning('projection', `new content (${newSize}B) is <50% of existing projection (${existingSize}B), preserving disk file while DB remains authoritative`, { fn: 'saveArtifactToDb', path: opts.path });
822
757
  skipDiskWrite = true;
823
758
  }
824
759
  }
@@ -829,7 +764,7 @@ export async function saveArtifactToDb(
829
764
  milestone_id: opts.milestone_id ?? null,
830
765
  slice_id: opts.slice_id ?? null,
831
766
  task_id: opts.task_id ?? null,
832
- full_content: dbContent,
767
+ full_content: contentToPersist,
833
768
  });
834
769
 
835
770
  // Write the file to disk (only if we're not preserving a richer existing file)
@@ -837,9 +772,7 @@ export async function saveArtifactToDb(
837
772
  try {
838
773
  await saveFile(fullPath, contentToPersist);
839
774
  } catch (diskErr) {
840
- logError('manifest', 'disk write failed, rolling back DB row', { fn: 'saveArtifactToDb', error: String((diskErr as Error).message) });
841
- db.deleteArtifactByPath(opts.path);
842
- throw diskErr;
775
+ logWarning('projection', 'artifact projection write failed; DB artifact remains committed', { fn: 'saveArtifactToDb', path: opts.path, error: String((diskErr as Error).message) });
843
776
  }
844
777
  }
845
778
  // Invalidate file-read caches so deriveState() sees the updated markdown.
@@ -54,21 +54,16 @@ export function getPriorSliceCompletionBlocker(
54
54
  // completion, which is wrong when the SUMMARY is a failure-path report
55
55
  // (verification FAILED, blocker placeholder, etc.). Resolve as follows:
56
56
  // 1. When DB is available and status is closed → skip (authoritative).
57
- // 2. When SUMMARY exists but looks like a failure/blocker report →
58
- // do not short-circuit; fall through to the slice-level check so
59
- // the guard can still block dependents of an active milestone.
60
- // 3. Otherwise (SUMMARY without failure markers) → skip. Preserves
61
- // the #1716 contract where a completed milestone with unchecked
62
- // remediation slices is still treated as done.
63
- const summaryPath = resolveMilestoneFile(base, mid, "SUMMARY");
57
+ // 2. When DB is unavailable, legacy SUMMARY.md fallback may skip.
58
+ // DB-backed projects must not treat SUMMARY.md as authoritative.
64
59
  if (isDbAvailable()) {
65
60
  const milestoneRow = getMilestone(mid);
66
61
  if (milestoneRow && isClosedStatus(milestoneRow.status)) continue;
67
- }
68
- if (summaryPath) {
62
+ } else {
63
+ const summaryPath = resolveMilestoneFile(base, mid, "SUMMARY");
69
64
  let summaryContent: string | null = null;
70
- try { summaryContent = readFileSync(summaryPath, "utf-8"); } catch { /* ignore */ }
71
- if (!summaryContent || classifyMilestoneSummaryContent(summaryContent) !== "failure") {
65
+ try { summaryContent = summaryPath ? readFileSync(summaryPath, "utf-8") : null; } catch { /* ignore */ }
66
+ if (summaryContent && classifyMilestoneSummaryContent(summaryContent) !== "failure") {
72
67
  continue;
73
68
  }
74
69
  }
@@ -3,7 +3,7 @@ import { join } from "node:path";
3
3
 
4
4
  import type { DoctorIssue } from "./doctor-types.js";
5
5
  import { isDbAvailable, _getAdapter } from "./gsd-db.js";
6
- import { resolveMilestoneFile } from "./paths.js";
6
+ import { resolveGsdPathContract, resolveMilestoneFile } from "./paths.js";
7
7
  import { deriveState } from "./state.js";
8
8
  import { readEvents } from "./workflow-events.js";
9
9
  import { renderAllProjections } from "./workflow-projections.js";
@@ -13,7 +13,7 @@ export async function checkEngineHealth(
13
13
  issues: DoctorIssue[],
14
14
  fixesApplied: string[],
15
15
  ): Promise<void> {
16
- const dbPath = join(basePath, ".gsd", "gsd.db");
16
+ const dbPath = resolveGsdPathContract(basePath).projectDb;
17
17
 
18
18
  if (!isDbAvailable() && existsSync(dbPath)) {
19
19
  issues.push({