gsd-pi 2.43.0-next.7-dev.8fab558 → 2.43.0-next.7-dev.4684f0e

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 (145) hide show
  1. package/dist/cli.js +13 -1
  2. package/dist/help-text.js +24 -0
  3. package/dist/resources/extensions/gsd/auto-post-unit.js +40 -23
  4. package/dist/resources/extensions/gsd/auto-recovery.js +7 -6
  5. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +65 -0
  6. package/dist/resources/extensions/gsd/commands-maintenance.js +20 -4
  7. package/dist/resources/extensions/gsd/db-writer.js +25 -3
  8. package/dist/resources/extensions/gsd/gsd-db.js +5 -2
  9. package/dist/resources/extensions/gsd/prompts/complete-slice.md +2 -2
  10. package/dist/resources/extensions/gsd/prompts/discuss.md +2 -2
  11. package/dist/resources/extensions/gsd/prompts/execute-task.md +2 -2
  12. package/dist/resources/extensions/gsd/prompts/plan-milestone.md +5 -7
  13. package/dist/resources/extensions/gsd/state.js +28 -18
  14. package/dist/resources/extensions/gsd/tools/complete-milestone.js +128 -0
  15. package/dist/resources/extensions/gsd/tools/plan-milestone.js +4 -2
  16. package/dist/resources/extensions/gsd/tools/plan-slice.js +4 -2
  17. package/dist/web/standalone/.next/BUILD_ID +1 -1
  18. package/dist/web/standalone/.next/app-path-routes-manifest.json +19 -19
  19. package/dist/web/standalone/.next/build-manifest.json +2 -2
  20. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  21. package/dist/web/standalone/.next/required-server-files.json +1 -1
  22. package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
  23. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  24. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  25. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  26. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  27. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  28. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  31. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/index.html +1 -1
  39. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app-paths-manifest.json +19 -19
  46. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  47. package/dist/web/standalone/.next/server/pages/500.html +2 -2
  48. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  49. package/dist/web/standalone/node_modules/node-pty/build/Makefile +2 -2
  50. package/dist/web/standalone/node_modules/node-pty/build/pty.target.mk +14 -14
  51. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api.target.mk +14 -14
  52. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_except.target.mk +14 -14
  53. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_maybe.target.mk +14 -14
  54. package/dist/web/standalone/server.js +1 -1
  55. package/package.json +1 -1
  56. package/packages/pi-coding-agent/dist/core/agent-session.d.ts +3 -3
  57. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  58. package/packages/pi-coding-agent/dist/core/agent-session.js +11 -34
  59. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  60. package/packages/pi-coding-agent/dist/core/compaction/branch-summarization.d.ts +2 -2
  61. package/packages/pi-coding-agent/dist/core/compaction/branch-summarization.d.ts.map +1 -1
  62. package/packages/pi-coding-agent/dist/core/compaction/branch-summarization.js.map +1 -1
  63. package/packages/pi-coding-agent/dist/core/compaction/compaction.d.ts +2 -2
  64. package/packages/pi-coding-agent/dist/core/compaction/compaction.d.ts.map +1 -1
  65. package/packages/pi-coding-agent/dist/core/compaction/compaction.js.map +1 -1
  66. package/packages/pi-coding-agent/dist/core/compaction-orchestrator.js +4 -4
  67. package/packages/pi-coding-agent/dist/core/compaction-orchestrator.js.map +1 -1
  68. package/packages/pi-coding-agent/dist/core/extensions/index.d.ts +1 -1
  69. package/packages/pi-coding-agent/dist/core/extensions/index.d.ts.map +1 -1
  70. package/packages/pi-coding-agent/dist/core/extensions/index.js.map +1 -1
  71. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  72. package/packages/pi-coding-agent/dist/core/extensions/loader.js +18 -0
  73. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  74. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +37 -0
  75. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  76. package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  77. package/packages/pi-coding-agent/dist/core/fallback-resolver.d.ts.map +1 -1
  78. package/packages/pi-coding-agent/dist/core/fallback-resolver.js +2 -3
  79. package/packages/pi-coding-agent/dist/core/fallback-resolver.js.map +1 -1
  80. package/packages/pi-coding-agent/dist/core/fallback-resolver.test.js +12 -2
  81. package/packages/pi-coding-agent/dist/core/fallback-resolver.test.js.map +1 -1
  82. package/packages/pi-coding-agent/dist/core/lifecycle-hooks.d.ts +38 -0
  83. package/packages/pi-coding-agent/dist/core/lifecycle-hooks.d.ts.map +1 -0
  84. package/packages/pi-coding-agent/dist/core/lifecycle-hooks.js +192 -0
  85. package/packages/pi-coding-agent/dist/core/lifecycle-hooks.js.map +1 -0
  86. package/packages/pi-coding-agent/dist/core/model-registry-auth-mode.test.d.ts +2 -0
  87. package/packages/pi-coding-agent/dist/core/model-registry-auth-mode.test.d.ts.map +1 -0
  88. package/packages/pi-coding-agent/dist/core/model-registry-auth-mode.test.js +255 -0
  89. package/packages/pi-coding-agent/dist/core/model-registry-auth-mode.test.js.map +1 -0
  90. package/packages/pi-coding-agent/dist/core/model-registry.d.ts +15 -0
  91. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  92. package/packages/pi-coding-agent/dist/core/model-registry.js +40 -3
  93. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  94. package/packages/pi-coding-agent/dist/core/package-commands.d.ts +25 -0
  95. package/packages/pi-coding-agent/dist/core/package-commands.d.ts.map +1 -0
  96. package/packages/pi-coding-agent/dist/core/package-commands.js +253 -0
  97. package/packages/pi-coding-agent/dist/core/package-commands.js.map +1 -0
  98. package/packages/pi-coding-agent/dist/core/package-commands.test.d.ts +2 -0
  99. package/packages/pi-coding-agent/dist/core/package-commands.test.d.ts.map +1 -0
  100. package/packages/pi-coding-agent/dist/core/package-commands.test.js +225 -0
  101. package/packages/pi-coding-agent/dist/core/package-commands.test.js.map +1 -0
  102. package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
  103. package/packages/pi-coding-agent/dist/core/sdk.js +4 -0
  104. package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
  105. package/packages/pi-coding-agent/dist/index.d.ts +3 -1
  106. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  107. package/packages/pi-coding-agent/dist/index.js +1 -0
  108. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  109. package/packages/pi-coding-agent/dist/main.d.ts.map +1 -1
  110. package/packages/pi-coding-agent/dist/main.js +11 -199
  111. package/packages/pi-coding-agent/dist/main.js.map +1 -1
  112. package/packages/pi-coding-agent/src/core/agent-session.ts +13 -37
  113. package/packages/pi-coding-agent/src/core/compaction/branch-summarization.ts +2 -2
  114. package/packages/pi-coding-agent/src/core/compaction/compaction.ts +3 -3
  115. package/packages/pi-coding-agent/src/core/compaction-orchestrator.ts +4 -4
  116. package/packages/pi-coding-agent/src/core/extensions/index.ts +5 -0
  117. package/packages/pi-coding-agent/src/core/extensions/loader.ts +23 -0
  118. package/packages/pi-coding-agent/src/core/extensions/types.ts +44 -0
  119. package/packages/pi-coding-agent/src/core/fallback-resolver.test.ts +15 -2
  120. package/packages/pi-coding-agent/src/core/fallback-resolver.ts +2 -3
  121. package/packages/pi-coding-agent/src/core/lifecycle-hooks.ts +274 -0
  122. package/packages/pi-coding-agent/src/core/model-registry-auth-mode.test.ts +288 -0
  123. package/packages/pi-coding-agent/src/core/model-registry.ts +39 -3
  124. package/packages/pi-coding-agent/src/core/package-commands.test.ts +240 -0
  125. package/packages/pi-coding-agent/src/core/package-commands.ts +310 -0
  126. package/packages/pi-coding-agent/src/core/sdk.ts +4 -0
  127. package/packages/pi-coding-agent/src/index.ts +7 -0
  128. package/packages/pi-coding-agent/src/main.ts +11 -232
  129. package/src/resources/extensions/gsd/auto-post-unit.ts +41 -20
  130. package/src/resources/extensions/gsd/auto-recovery.ts +7 -6
  131. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +69 -0
  132. package/src/resources/extensions/gsd/commands-maintenance.ts +20 -5
  133. package/src/resources/extensions/gsd/db-writer.ts +28 -3
  134. package/src/resources/extensions/gsd/gsd-db.ts +4 -2
  135. package/src/resources/extensions/gsd/prompts/complete-slice.md +2 -2
  136. package/src/resources/extensions/gsd/prompts/discuss.md +2 -2
  137. package/src/resources/extensions/gsd/prompts/execute-task.md +2 -2
  138. package/src/resources/extensions/gsd/prompts/plan-milestone.md +5 -7
  139. package/src/resources/extensions/gsd/state.ts +29 -18
  140. package/src/resources/extensions/gsd/tests/tool-naming.test.ts +2 -1
  141. package/src/resources/extensions/gsd/tools/complete-milestone.ts +176 -0
  142. package/src/resources/extensions/gsd/tools/plan-milestone.ts +7 -2
  143. package/src/resources/extensions/gsd/tools/plan-slice.ts +7 -2
  144. /package/dist/web/standalone/.next/static/{vIVEdIRyywg0fXAwrLj3b → ZYERjwjiaf3Mhj69oy-Ms}/_buildManifest.js +0 -0
  145. /package/dist/web/standalone/.next/static/{vIVEdIRyywg0fXAwrLj3b → ZYERjwjiaf3Mhj69oy-Ms}/_ssgManifest.js +0 -0
package/dist/cli.js CHANGED
@@ -1,4 +1,4 @@
1
- import { AuthStorage, DefaultResourceLoader, ModelRegistry, SettingsManager, SessionManager, createAgentSession, InteractiveMode, runPrintMode, runRpcMode, } from '@gsd/pi-coding-agent';
1
+ import { AuthStorage, DefaultResourceLoader, ModelRegistry, runPackageCommand, SettingsManager, SessionManager, createAgentSession, InteractiveMode, runPrintMode, runRpcMode, } from '@gsd/pi-coding-agent';
2
2
  import { readFileSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import { agentDir, sessionsDir, authFilePath } from './app-paths.js';
@@ -121,6 +121,18 @@ if (subcommand && process.argv.includes('--help')) {
121
121
  process.exit(0);
122
122
  }
123
123
  }
124
+ const packageCommand = await runPackageCommand({
125
+ appName: 'gsd',
126
+ args: process.argv.slice(2),
127
+ cwd: process.cwd(),
128
+ agentDir,
129
+ stdout: process.stdout,
130
+ stderr: process.stderr,
131
+ allowedCommands: new Set(['install', 'remove', 'list']),
132
+ });
133
+ if (packageCommand.handled) {
134
+ process.exit(packageCommand.exitCode);
135
+ }
124
136
  // `gsd config` — replay the setup wizard and exit
125
137
  if (cliFlags.messages[0] === 'config') {
126
138
  const authStorage = AuthStorage.create(authFilePath);
package/dist/help-text.js CHANGED
@@ -29,6 +29,27 @@ const SUBCOMMAND_HELP = {
29
29
  '',
30
30
  'Compare with --continue (-c) which always resumes the most recent session.',
31
31
  ].join('\n'),
32
+ install: [
33
+ 'Usage: gsd install <source> [-l, --local]',
34
+ '',
35
+ 'Install a package/extension source and run declared lifecycle hooks.',
36
+ '',
37
+ 'Examples:',
38
+ ' gsd install npm:@foo/bar',
39
+ ' gsd install git:github.com/user/repo',
40
+ ' gsd install https://github.com/user/repo',
41
+ ' gsd install ./local/path',
42
+ ].join('\n'),
43
+ remove: [
44
+ 'Usage: gsd remove <source> [-l, --local]',
45
+ '',
46
+ 'Remove an installed package source and its settings entry.',
47
+ ].join('\n'),
48
+ list: [
49
+ 'Usage: gsd list',
50
+ '',
51
+ 'List installed package sources from user and project settings.',
52
+ ].join('\n'),
32
53
  worktree: [
33
54
  'Usage: gsd worktree <command> [args]',
34
55
  '',
@@ -122,6 +143,9 @@ export function printHelp(version) {
122
143
  process.stdout.write(' --help, -h Print this help and exit\n');
123
144
  process.stdout.write('\nSubcommands:\n');
124
145
  process.stdout.write(' config Re-run the setup wizard\n');
146
+ process.stdout.write(' install <source> Install a package/extension source\n');
147
+ process.stdout.write(' remove <source> Remove an installed package source\n');
148
+ process.stdout.write(' list List installed package sources\n');
125
149
  process.stdout.write(' update Update GSD to the latest version\n');
126
150
  process.stdout.write(' sessions List and resume a past session\n');
127
151
  process.stdout.write(' worktree <cmd> Manage worktrees (list, merge, clean, remove)\n');
@@ -13,7 +13,7 @@
13
13
  import { deriveState } from "./state.js";
14
14
  import { loadFile, parseSummary, resolveAllOverrides } from "./files.js";
15
15
  import { loadPrompt } from "./prompt-loader.js";
16
- import { resolveSliceFile, resolveSlicePath, resolveTaskFile, resolveMilestoneFile, resolveTasksDir, buildTaskFileName, gsdRoot, } from "./paths.js";
16
+ import { resolveSliceFile, resolveTaskFile, resolveMilestoneFile, resolveTasksDir, buildTaskFileName, gsdRoot, } from "./paths.js";
17
17
  import { invalidateAllCaches } from "./cache.js";
18
18
  import { closeoutUnit } from "./auto-unit-closeout.js";
19
19
  import { autoCommitCurrentBranch, } from "./worktree.js";
@@ -22,13 +22,13 @@ import { writeUnitRuntimeRecord, clearUnitRuntimeRecord } from "./unit-runtime.j
22
22
  import { runGSDDoctor, rebuildState, summarizeDoctorIssues } from "./doctor.js";
23
23
  import { recordHealthSnapshot, checkHealEscalation } from "./doctor-proactive.js";
24
24
  import { syncStateToProjectRoot } from "./auto-worktree-sync.js";
25
- import { isDbAvailable, getTask, getSlice, getMilestone, updateTaskStatus } from "./gsd-db.js";
25
+ import { isDbAvailable, getTask, getSlice, getMilestone, updateTaskStatus, _getAdapter } from "./gsd-db.js";
26
26
  import { renderPlanCheckboxes } from "./markdown-renderer.js";
27
27
  import { consumeSignal } from "./session-status-io.js";
28
28
  import { checkPostUnitHooks, isRetryPending, consumeRetryTrigger, persistHookState, resolveHookArtifactPath, } from "./post-unit-hooks.js";
29
29
  import { hasPendingCaptures, loadPendingCaptures } from "./captures.js";
30
30
  import { debugLog } from "./debug-logger.js";
31
- import { existsSync, unlinkSync, readFileSync, writeFileSync } from "node:fs";
31
+ import { existsSync, unlinkSync } from "node:fs";
32
32
  import { join } from "node:path";
33
33
  import { atomicWriteSync } from "./atomic-write.js";
34
34
  import { _resetHasChangesCache } from "./native-git-bridge.js";
@@ -101,6 +101,39 @@ export function detectRogueFileWrites(unitType, unitId, basePath) {
101
101
  if (!hasPlanningState) {
102
102
  rogues.push({ path: planPath, unitType, unitId });
103
103
  }
104
+ // Also check for rogue REPLAN.md
105
+ const replanPath = resolveSliceFile(basePath, mid, sid, "REPLAN");
106
+ if (replanPath && existsSync(replanPath) && !hasPlanningState) {
107
+ rogues.push({ path: replanPath, unitType, unitId });
108
+ }
109
+ }
110
+ else if (unitType === "reassess-roadmap") {
111
+ const [mid, sid] = parts;
112
+ if (!mid || !sid)
113
+ return [];
114
+ const assessPath = resolveSliceFile(basePath, mid, sid, "ASSESSMENT");
115
+ if (!assessPath || !existsSync(assessPath))
116
+ return [];
117
+ // Assessment file exists on disk — check if DB knows about it via the artifacts table
118
+ const adapter = _getAdapter();
119
+ if (adapter) {
120
+ const row = adapter.prepare(`SELECT 1 FROM artifacts WHERE path LIKE :pattern AND artifact_type = 'ASSESSMENT' LIMIT 1`).get({ ":pattern": `%${sid}-ASSESSMENT.md` });
121
+ if (!row) {
122
+ rogues.push({ path: assessPath, unitType, unitId });
123
+ }
124
+ }
125
+ }
126
+ else if (unitType === "plan-task") {
127
+ const [mid, sid, tid] = parts;
128
+ if (!mid || !sid || !tid)
129
+ return [];
130
+ const taskPlanPath = resolveTaskFile(basePath, mid, sid, tid, "PLAN");
131
+ if (!taskPlanPath || !existsSync(taskPlanPath))
132
+ return [];
133
+ const dbRow = getTask(mid, sid, tid);
134
+ if (!dbRow) {
135
+ rogues.push({ path: taskPlanPath, unitType, unitId });
136
+ }
104
137
  }
105
138
  return rogues;
106
139
  }
@@ -468,26 +501,10 @@ export async function postUnitPostVerification(pctx) {
468
501
  updateTaskStatus(mid, sid, tid, "pending");
469
502
  await renderPlanCheckboxes(s.basePath, mid, sid);
470
503
  }
471
- catch {
472
- // DB may be unavailable — fall back to direct file-based uncheck
473
- try {
474
- const slicePath = resolveSlicePath(s.basePath, mid, sid);
475
- if (slicePath) {
476
- const { readdirSync } = await import("node:fs");
477
- const planCandidates = readdirSync(slicePath)
478
- .filter((f) => f.includes("PLAN") && (f.startsWith(sid) || f.startsWith(`${sid}-`)));
479
- if (planCandidates.length > 0) {
480
- const planFile = join(slicePath, planCandidates[0]);
481
- let content = readFileSync(planFile, "utf-8");
482
- const regex = new RegExp(`^(\\s*-\\s*)\\[x\\](\\s*\\**${tid}\\**[:\\s])`, "mi");
483
- if (regex.test(content)) {
484
- content = content.replace(regex, "$1[ ]$2");
485
- writeFileSync(planFile, content, "utf-8");
486
- }
487
- }
488
- }
489
- }
490
- catch { /* non-fatal: file-based fallback failure */ }
504
+ catch (dbErr) {
505
+ // DB unavailable — fail explicitly rather than silently reverting to markdown mutation.
506
+ // Use 'gsd recover' to rebuild DB state from disk if needed.
507
+ process.stderr.write(`gsd: retry state-reset failed (DB unavailable): ${dbErr.message}. Run 'gsd recover' to reconcile.\n`);
491
508
  }
492
509
  }
493
510
  // 2. Delete SUMMARY.md for the task
@@ -284,10 +284,10 @@ export function verifyExpectedArtifact(unitType, unitId, base) {
284
284
  return false;
285
285
  }
286
286
  else if (!isDbAvailable()) {
287
- // DB unavailable fall back to plan heading check (format detection,
288
- // not reconciliation). Heading-style entries (### T01 --) count as
289
- // verified because the summary file existence (checked above) is the
290
- // real signal.
287
+ // LEGACY: Pre-migration fallback for projects without DB.
288
+ // Fall back to plan heading check (format detection, not reconciliation).
289
+ // Heading-style entries (### T01 --) count as verified because the
290
+ // summary file existence (checked above) is the real signal.
291
291
  const planAbs = resolveSliceFile(base, mid, sid, "PLAN");
292
292
  if (planAbs && existsSync(planAbs)) {
293
293
  const planContent = readFileSync(planAbs, "utf-8");
@@ -320,7 +320,7 @@ export function verifyExpectedArtifact(unitType, unitId, base) {
320
320
  taskIds = tasks.map(t => t.id);
321
321
  }
322
322
  if (!taskIds) {
323
- // DB unavailable or no tasks in DB — parse plan file for task IDs
323
+ // LEGACY: DB unavailable or no tasks in DB — parse plan file for task IDs
324
324
  const planContent = readFileSync(absPath, "utf-8");
325
325
  const plan = parseLegacyPlan(planContent);
326
326
  if (plan.tasks.length > 0)
@@ -362,7 +362,8 @@ export function verifyExpectedArtifact(unitType, unitId, base) {
362
362
  return false;
363
363
  }
364
364
  else if (!isDbAvailable()) {
365
- // DB unavailable fall back to roadmap checkbox check via parsers-legacy
365
+ // LEGACY: Pre-migration fallback for projects without DB.
366
+ // Fall back to roadmap checkbox check via parsers-legacy
366
367
  const roadmapFile = resolveMilestoneFile(base, mid, "ROADMAP");
367
368
  if (roadmapFile && existsSync(roadmapFile)) {
368
369
  try {
@@ -667,6 +667,71 @@ export function registerDbTools(pi) {
667
667
  };
668
668
  pi.registerTool(sliceCompleteTool);
669
669
  registerAlias(pi, sliceCompleteTool, "gsd_complete_slice", "gsd_slice_complete");
670
+ // ─── gsd_complete_milestone ────────────────────────────────────────────
671
+ const milestoneCompleteExecute = async (_toolCallId, params, _signal, _onUpdate, _ctx) => {
672
+ const dbAvailable = await ensureDbOpen();
673
+ if (!dbAvailable) {
674
+ return {
675
+ content: [{ type: "text", text: "Error: GSD database is not available. Cannot complete milestone." }],
676
+ details: { operation: "complete_milestone", error: "db_unavailable" },
677
+ };
678
+ }
679
+ try {
680
+ const { handleCompleteMilestone } = await import("../tools/complete-milestone.js");
681
+ const result = await handleCompleteMilestone(params, process.cwd());
682
+ if ("error" in result) {
683
+ return {
684
+ content: [{ type: "text", text: `Error completing milestone: ${result.error}` }],
685
+ details: { operation: "complete_milestone", error: result.error },
686
+ };
687
+ }
688
+ return {
689
+ content: [{ type: "text", text: `Completed milestone ${result.milestoneId}. Summary written to ${result.summaryPath}` }],
690
+ details: {
691
+ operation: "complete_milestone",
692
+ milestoneId: result.milestoneId,
693
+ summaryPath: result.summaryPath,
694
+ },
695
+ };
696
+ }
697
+ catch (err) {
698
+ const msg = err instanceof Error ? err.message : String(err);
699
+ process.stderr.write(`gsd-db: complete_milestone tool failed: ${msg}\n`);
700
+ return {
701
+ content: [{ type: "text", text: `Error completing milestone: ${msg}` }],
702
+ details: { operation: "complete_milestone", error: msg },
703
+ };
704
+ }
705
+ };
706
+ const milestoneCompleteTool = {
707
+ name: "gsd_complete_milestone",
708
+ label: "Complete Milestone",
709
+ description: "Record a completed milestone to the GSD database, render MILESTONE-SUMMARY.md to disk — all in one atomic operation. " +
710
+ "Validates all slices are complete before proceeding.",
711
+ promptSnippet: "Complete a GSD milestone (DB write + summary render)",
712
+ promptGuidelines: [
713
+ "Use gsd_complete_milestone when all slices in a milestone are finished and the milestone needs to be recorded.",
714
+ "All slices in the milestone must have status 'complete' — the handler validates this before proceeding.",
715
+ "On success, returns summaryPath where the MILESTONE-SUMMARY.md was written.",
716
+ ],
717
+ parameters: Type.Object({
718
+ milestoneId: Type.String({ description: "Milestone ID (e.g. M001)" }),
719
+ title: Type.String({ description: "Milestone title" }),
720
+ oneLiner: Type.String({ description: "One-sentence summary of what the milestone achieved" }),
721
+ narrative: Type.String({ description: "Detailed narrative of what happened during the milestone" }),
722
+ successCriteriaResults: Type.String({ description: "Markdown detailing how each success criterion was met or not met" }),
723
+ definitionOfDoneResults: Type.String({ description: "Markdown detailing how each definition-of-done item was met" }),
724
+ requirementOutcomes: Type.String({ description: "Markdown detailing requirement status transitions with evidence" }),
725
+ keyDecisions: Type.Array(Type.String(), { description: "Key architectural/pattern decisions made during the milestone" }),
726
+ keyFiles: Type.Array(Type.String(), { description: "Key files created or modified during the milestone" }),
727
+ lessonsLearned: Type.Array(Type.String(), { description: "Lessons learned during the milestone" }),
728
+ followUps: Type.Optional(Type.String({ description: "Follow-up items for future milestones" })),
729
+ deviations: Type.Optional(Type.String({ description: "Deviations from the original plan" })),
730
+ }),
731
+ execute: milestoneCompleteExecute,
732
+ };
733
+ pi.registerTool(milestoneCompleteTool);
734
+ registerAlias(pi, milestoneCompleteTool, "gsd_milestone_complete", "gsd_complete_milestone");
670
735
  // ─── gsd_replan_slice (gsd_slice_replan alias) ─────────────────────────
671
736
  const replanSliceExecute = async (_toolCallId, params, _signal, _onUpdate, _ctx) => {
672
737
  const dbAvailable = await ensureDbOpen();
@@ -43,12 +43,29 @@ export async function handleCleanupBranches(ctx, basePath) {
43
43
  const { loadFile } = await import("./files.js");
44
44
  const { parseRoadmap } = await import("./parsers-legacy.js");
45
45
  const { isMilestoneComplete } = await import("./state.js");
46
+ const { isDbAvailable, getMilestone } = await import("./gsd-db.js");
46
47
  const attachedBranches = new Set(listWorktrees(basePath).map((wt) => wt.branch));
47
48
  const milestoneBranches = nativeBranchList(basePath, "milestone/*");
48
49
  for (const branch of milestoneBranches) {
49
50
  if (attachedBranches.has(branch))
50
51
  continue;
51
52
  const milestoneId = branch.replace(/^milestone\//, "");
53
+ // DB-first: check milestone status directly
54
+ if (isDbAvailable()) {
55
+ const dbRow = getMilestone(milestoneId);
56
+ if (dbRow) {
57
+ if (dbRow.status !== "complete" && dbRow.status !== "done")
58
+ continue;
59
+ // Milestone is complete per DB — proceed to delete branch
60
+ try {
61
+ nativeBranchDelete(basePath, branch, true);
62
+ deletedStaleMilestones++;
63
+ }
64
+ catch { /* non-fatal */ }
65
+ continue;
66
+ }
67
+ }
68
+ // Filesystem fallback
52
69
  const roadmapPath = resolveMilestoneFile(basePath, milestoneId, "ROADMAP");
53
70
  if (!roadmapPath)
54
71
  continue;
@@ -427,15 +444,14 @@ export async function handleRecover(ctx, basePath) {
427
444
  return;
428
445
  }
429
446
  try {
430
- // 1. Delete hierarchy rows inside a transaction
447
+ // 1. Delete + re-populate inside a single transaction for atomicity
431
448
  const db = _getAdapter();
432
- dbTransaction(() => {
449
+ const counts = dbTransaction(() => {
433
450
  db.exec("DELETE FROM tasks");
434
451
  db.exec("DELETE FROM slices");
435
452
  db.exec("DELETE FROM milestones");
453
+ return migrateHierarchyToDb(basePath);
436
454
  });
437
- // 2. Re-populate from rendered markdown on disk
438
- const counts = migrateHierarchyToDb(basePath);
439
455
  // 3. Invalidate state cache so deriveState() picks up fresh DB data
440
456
  invalidateStateCache();
441
457
  // 4. Derive state to verify sanity
@@ -265,7 +265,14 @@ export async function saveDecisionToDb(fields, basePath) {
265
265
  // Table format or no existing file — full regeneration (original behavior)
266
266
  md = generateDecisionsMd(allDecisions);
267
267
  }
268
- await saveFile(filePath, md);
268
+ try {
269
+ await saveFile(filePath, md);
270
+ }
271
+ catch (diskErr) {
272
+ process.stderr.write(`gsd-db: saveDecisionToDb — disk write failed, rolling back DB row: ${diskErr.message}\n`);
273
+ adapter?.prepare('DELETE FROM decisions WHERE id = :id').run({ ':id': id });
274
+ throw diskErr;
275
+ }
269
276
  // Invalidate file-read caches so deriveState() sees the updated markdown.
270
277
  // Do NOT clear the artifacts table — we just wrote to it intentionally.
271
278
  invalidateStateCache();
@@ -322,7 +329,14 @@ export async function updateRequirementInDb(id, updates, basePath) {
322
329
  const nonSuperseded = allRequirements.filter(r => r.superseded_by == null);
323
330
  const md = generateRequirementsMd(nonSuperseded);
324
331
  const filePath = resolveGsdRootFile(basePath, 'REQUIREMENTS');
325
- await saveFile(filePath, md);
332
+ try {
333
+ await saveFile(filePath, md);
334
+ }
335
+ catch (diskErr) {
336
+ process.stderr.write(`gsd-db: updateRequirementInDb — disk write failed, reverting DB row: ${diskErr.message}\n`);
337
+ db.upsertRequirement(existing);
338
+ throw diskErr;
339
+ }
326
340
  // Invalidate file-read caches so deriveState() sees the updated markdown.
327
341
  // Do NOT clear the artifacts table — we just wrote to it intentionally.
328
342
  invalidateStateCache();
@@ -356,7 +370,15 @@ export async function saveArtifactToDb(opts, basePath) {
356
370
  if (!fullPath.startsWith(gsdDir)) {
357
371
  throw new GSDError(GSD_IO_ERROR, `saveArtifactToDb: path escapes .gsd/ directory: ${opts.path}`);
358
372
  }
359
- await saveFile(fullPath, opts.content);
373
+ try {
374
+ await saveFile(fullPath, opts.content);
375
+ }
376
+ catch (diskErr) {
377
+ process.stderr.write(`gsd-db: saveArtifactToDb — disk write failed, rolling back DB row: ${diskErr.message}\n`);
378
+ const rollbackAdapter = db._getAdapter();
379
+ rollbackAdapter?.prepare('DELETE FROM artifacts WHERE path = :path').run({ ':path': opts.path });
380
+ throw diskErr;
381
+ }
360
382
  // Invalidate file-read caches so deriveState() sees the updated markdown.
361
383
  // Do NOT clear the artifacts table — we just wrote to it intentionally.
362
384
  invalidateStateCache();
@@ -109,6 +109,9 @@ const SCHEMA_VERSION = 10;
109
109
  function initSchema(db, fileBacked) {
110
110
  if (fileBacked)
111
111
  db.exec("PRAGMA journal_mode=WAL");
112
+ if (fileBacked)
113
+ db.exec("PRAGMA busy_timeout = 5000");
114
+ db.exec("PRAGMA foreign_keys = ON");
112
115
  db.exec("BEGIN");
113
116
  try {
114
117
  db.exec(`
@@ -219,7 +222,7 @@ function initSchema(db, fileBacked) {
219
222
  proof_level TEXT NOT NULL DEFAULT '',
220
223
  integration_closure TEXT NOT NULL DEFAULT '',
221
224
  observability_impact TEXT NOT NULL DEFAULT '',
222
- sequence INTEGER DEFAULT 0,
225
+ sequence INTEGER DEFAULT 0, -- DEAD CODE: no tool exposes sequence — always 0
223
226
  replan_triggered_at TEXT DEFAULT NULL,
224
227
  PRIMARY KEY (milestone_id, id),
225
228
  FOREIGN KEY (milestone_id) REFERENCES milestones(id)
@@ -250,7 +253,7 @@ function initSchema(db, fileBacked) {
250
253
  inputs TEXT NOT NULL DEFAULT '[]',
251
254
  expected_output TEXT NOT NULL DEFAULT '[]',
252
255
  observability_impact TEXT NOT NULL DEFAULT '',
253
- sequence INTEGER DEFAULT 0,
256
+ sequence INTEGER DEFAULT 0, -- DEAD CODE: no tool exposes sequence — always 0
254
257
  PRIMARY KEY (milestone_id, slice_id, id),
255
258
  FOREIGN KEY (milestone_id, slice_id) REFERENCES slices(milestone_id, id)
256
259
  )
@@ -24,7 +24,7 @@ Then:
24
24
  3. Run all slice-level verification checks defined in the slice plan. All must pass before marking the slice done. If any fail, fix them first.
25
25
  4. If the slice plan includes observability/diagnostic surfaces, confirm they work. Skip this for simple slices that don't have observability sections.
26
26
  5. If `.gsd/REQUIREMENTS.md` exists, update it based on what this slice actually proved. Move requirements between Active, Validated, Deferred, Blocked, or Out of Scope only when the evidence from execution supports that change.
27
- 6. Call the `gsd_slice_complete` tool (alias: `gsd_complete_slice`) to record the slice as complete. The tool validates all tasks are complete, writes the slice summary to `{{sliceSummaryPath}}`, UAT to `{{sliceUatPath}}`, and toggles the `{{sliceId}}` checkbox in `{{roadmapPath}}` — all atomically. Read the summary and UAT templates at `~/.gsd/agent/extensions/gsd/templates/` to understand the expected structure, then pass the following parameters:
27
+ 6. Call the `gsd_slice_complete` tool (alias: `gsd_complete_slice`) to record the slice as complete. The tool validates all tasks are complete, updates the slice status in the DB, renders the summary to `{{sliceSummaryPath}}`, UAT to `{{sliceUatPath}}`, and re-renders `{{roadmapPath}}` — all atomically. Read the summary and UAT templates at `~/.gsd/agent/extensions/gsd/templates/` to understand the expected structure, then pass the following parameters:
28
28
 
29
29
  **Identity:** `sliceId`, `milestoneId`, `sliceTitle`
30
30
 
@@ -45,6 +45,6 @@ Then:
45
45
  9. Do not run git commands — the system commits your changes and handles any merge after this unit succeeds.
46
46
  10. Update `.gsd/PROJECT.md` if it exists — refresh current state if needed.
47
47
 
48
- **You MUST call `gsd_slice_complete` before finishing.** The tool handles writing `{{sliceSummaryPath}}`, `{{sliceUatPath}}`, and toggling the `{{roadmapPath}}` checkbox atomically. You must still review decisions and knowledge manually (steps 7-8).
48
+ **You MUST call `gsd_slice_complete` before finishing.** The tool handles writing `{{sliceSummaryPath}}`, `{{sliceUatPath}}`, and updating `{{roadmapPath}}` atomically. You must still review decisions and knowledge manually (steps 7-8).
49
49
 
50
50
  When done, say: "Slice {{sliceId}} complete."
@@ -202,7 +202,7 @@ Once the user is satisfied, in a single pass:
202
202
  When writing context.md, preserve the user's exact terminology, emphasis, and specific framing from the discussion. Do not paraphrase user nuance into generic summaries. If the user said "craft feel," write "craft feel" — not "high-quality user experience." If they emphasized a specific constraint or negative requirement, carry that emphasis through verbatim. The context file is downstream agents' only window into this conversation — flattening specifics into generics loses the signal that shaped every decision.
203
203
 
204
204
  4. Write `{{contextPath}}` — use the **Context** output template below. Preserve key risks, unknowns, existing codebase constraints, integration points, and relevant requirements surfaced during discussion.
205
- 5. Write `{{roadmapPath}}` use the **Roadmap** output template below. Decompose into demoable vertical slices with checkboxes, risk, depends, demo sentences, proof strategy, verification classes, milestone definition of done, requirement coverage, and a boundary map. If the milestone crosses multiple runtime boundaries, include an explicit final integration slice that proves the assembled system works end-to-end in a real environment.
205
+ 5. Call `gsd_plan_milestone` to create the roadmap. Decompose into demoable vertical slices with risk, depends, demo sentences, proof strategy, verification classes, milestone definition of done, requirement coverage, and a boundary map. If the milestone crosses multiple runtime boundaries, include an explicit final integration slice that proves the assembled system works end-to-end in a real environment. Use the **Roadmap** output template below to structure the tool call parameters.
206
206
  6. Seed `.gsd/DECISIONS.md` — use the **Decisions** output template below. Append rows for any architectural or pattern decisions made during discussion.
207
207
  7. {{commitInstruction}}
208
208
 
@@ -222,7 +222,7 @@ Once the user confirms the milestone split:
222
222
  #### Phase 2: Primary milestone
223
223
 
224
224
  5. Write a full `CONTEXT.md` for the primary milestone (the one discussed in depth).
225
- 6. Write a `ROADMAP.md` for **only the primary milestone** — detail-planning later milestones now is waste because the codebase will change. Include requirement coverage and a milestone definition of done.
225
+ 6. Call `gsd_plan_milestone` for **only the primary milestone** — detail-planning later milestones now is waste because the codebase will change. Include requirement coverage and a milestone definition of done.
226
226
 
227
227
  #### MANDATORY: depends_on Frontmatter in CONTEXT.md
228
228
 
@@ -63,7 +63,7 @@ Then:
63
63
  11. **Blocker discovery:** If execution reveals that the remaining slice plan is fundamentally invalid — not just a bug or minor deviation, but a plan-invalidating finding like a wrong API, missing capability, or architectural mismatch — set `blocker_discovered: true` in the task summary frontmatter and describe the blocker clearly in the summary narrative. Do NOT set `blocker_discovered: true` for ordinary debugging, minor deviations, or issues that can be fixed within the current task or the remaining plan. This flag triggers an automatic replan of the slice.
64
64
  12. If you made an architectural, pattern, library, or observability decision during this task that downstream work should know about, append it to `.gsd/DECISIONS.md` (read the template at `~/.gsd/agent/extensions/gsd/templates/decisions.md` if the file doesn't exist yet). Not every task produces decisions — only append when a meaningful choice was made.
65
65
  13. If you discover a non-obvious rule, recurring gotcha, or useful pattern during execution, append it to `.gsd/KNOWLEDGE.md`. Only add entries that would save future agents from repeating your investigation. Don't add obvious things.
66
- 14. Call the `gsd_task_complete` tool (alias: `gsd_complete_task`) to record the task completion. This single tool call atomically writes the summary file to `{{taskSummaryPath}}`, toggles the `[ ]` `[x]` checkbox in `{{planPath}}`, and persists the task row to the DB. Read the summary template at `~/.gsd/agent/extensions/gsd/templates/task-summary.md` to understand the expected structure — but pass the content as tool parameters, not as a file write. The tool parameters are:
66
+ 14. Call the `gsd_task_complete` tool (alias: `gsd_complete_task`) to record the task completion. This single tool call atomically updates the task status in the DB, renders the summary file to `{{taskSummaryPath}}`, and re-renders the plan file at `{{planPath}}`. Read the summary template at `~/.gsd/agent/extensions/gsd/templates/task-summary.md` to understand the expected structure — but pass the content as tool parameters, not as a file write. The tool parameters are:
67
67
  - `taskId`: "{{taskId}}"
68
68
  - `sliceId`: "{{sliceId}}"
69
69
  - `milestoneId`: "{{milestoneId}}"
@@ -80,6 +80,6 @@ Then:
80
80
 
81
81
  All work stays in your working directory: `{{workingDirectory}}`.
82
82
 
83
- **You MUST call `gsd_task_complete` before finishing.** The tool handles writing `{{taskSummaryPath}}` and toggling the checkbox in `{{planPath}}` — do not write the summary file or toggle the checkbox manually.
83
+ **You MUST call `gsd_task_complete` before finishing.** The tool handles writing `{{taskSummaryPath}}` and updating the plan file at `{{planPath}}` — do not write the summary file or modify the plan file manually.
84
84
 
85
85
  When done, say: "Task {{taskId}} complete."
@@ -80,15 +80,13 @@ Apply these when decomposing and ordering slices:
80
80
 
81
81
  ## Single-Slice Fast Path
82
82
 
83
- If the roadmap has only one slice, also write the slice plan and task plans inline during this unit — don't leave them for a separate planning session.
83
+ If the roadmap has only one slice, also plan the slice and its tasks inline during this unit — don't leave them for a separate planning session.
84
84
 
85
- 1. Use the **Slice Plan** and **Task Plan** output templates from the inlined context above
86
- 2. `mkdir -p {{milestonePath}}/slices/S01/tasks`
87
- 3. Write the S01 plan file at `{{milestonePath}}/slices/S01/S01-PLAN.md`
88
- 4. Write individual task plans at `{{milestonePath}}/slices/S01/tasks/T01-PLAN.md`, etc.
89
- 5. For simple slices, keep the plan lean — omit Proof Level, Integration Closure, and Observability sections if they would all be "none". Executable verification commands are sufficient.
85
+ 1. After `gsd_plan_milestone` returns, immediately call `gsd_plan_slice` for S01 with the full task breakdown
86
+ 2. Use the **Slice Plan** and **Task Plan** output templates from the inlined context above to structure the tool call parameters
87
+ 3. For simple slices, keep the plan lean omit Proof Level, Integration Closure, and Observability sections if they would all be "none". Executable verification commands are sufficient.
90
88
 
91
- This eliminates a separate research-slice + plan-slice cycle when the work is straightforward.
89
+ Do **not** write plan files manually — use the DB-backed tools so state stays consistent.
92
90
 
93
91
  ## Secret Forecasting
94
92
 
@@ -1,5 +1,5 @@
1
1
  // GSD Extension — State Derivation
2
- // Reads roadmap + plan files to determine current position.
2
+ // DB-primary state derivation with filesystem fallback for unmigrated projects.
3
3
  // Pure TypeScript, zero Pi dependencies.
4
4
  import { parseRoadmap, parsePlan, } from './parsers-legacy.js';
5
5
  import { parseSummary, loadFile, parseRequirementCounts, parseContextDependsOn, } from './files.js';
@@ -68,40 +68,48 @@ export function invalidateStateCache() {
68
68
  * Returns the ID of the first incomplete milestone, or null if all are complete.
69
69
  */
70
70
  export async function getActiveMilestoneId(basePath) {
71
- const milestoneIds = findMilestoneIds(basePath);
72
71
  // Parallel worker isolation
73
72
  const milestoneLock = process.env.GSD_MILESTONE_LOCK;
74
73
  if (milestoneLock) {
74
+ const milestoneIds = findMilestoneIds(basePath);
75
75
  if (!milestoneIds.includes(milestoneLock))
76
76
  return null;
77
- // Locked milestone that is parked should not be active
78
77
  const lockedParked = resolveMilestoneFile(basePath, milestoneLock, "PARKED");
79
78
  if (lockedParked)
80
79
  return null;
81
80
  return milestoneLock;
82
81
  }
82
+ // DB-first: query milestones table for the first non-complete, non-parked milestone
83
+ if (isDbAvailable()) {
84
+ const allMilestones = getAllMilestones();
85
+ if (allMilestones.length > 0) {
86
+ const sorted = [...allMilestones].sort((a, b) => a.id.localeCompare(b.id));
87
+ for (const m of sorted) {
88
+ if (m.status === "complete" || m.status === "done" || m.status === "parked")
89
+ continue;
90
+ return m.id;
91
+ }
92
+ return null;
93
+ }
94
+ }
95
+ // Filesystem fallback for unmigrated projects or empty DB
96
+ const milestoneIds = findMilestoneIds(basePath);
83
97
  for (const mid of milestoneIds) {
84
- // Skip parked milestones — they are not eligible for active status
85
98
  const parkedFile = resolveMilestoneFile(basePath, mid, "PARKED");
86
99
  if (parkedFile)
87
100
  continue;
88
101
  const roadmapFile = resolveMilestoneFile(basePath, mid, "ROADMAP");
89
102
  const content = roadmapFile ? await loadFile(roadmapFile) : null;
90
103
  if (!content) {
91
- // No roadmap — but if a summary exists, the milestone is already complete
92
104
  const summaryFile = resolveMilestoneFile(basePath, mid, "SUMMARY");
93
105
  if (summaryFile)
94
- continue; // completed milestone, skip
106
+ continue;
95
107
  if (isGhostMilestone(basePath, mid))
96
- continue; // ghost dir — skip
97
- return mid; // No roadmap and no summary — milestone is incomplete
98
- // Note: draft-awareness (CONTEXT-DRAFT.md) is handled in deriveState(), not here.
99
- // A draft milestone is still "active" — this function only determines which milestone is current.
108
+ continue;
109
+ return mid;
100
110
  }
101
111
  const roadmap = parseRoadmap(content);
102
112
  if (!isMilestoneComplete(roadmap)) {
103
- // Summary is the terminal artifact — if it exists, the milestone is
104
- // complete even when roadmap checkboxes weren't ticked (#864).
105
113
  const summaryFile = resolveMilestoneFile(basePath, mid, "SUMMARY");
106
114
  if (!summaryFile)
107
115
  return mid;
@@ -110,13 +118,12 @@ export async function getActiveMilestoneId(basePath) {
110
118
  return null;
111
119
  }
112
120
  /**
113
- * Reconstruct GSD state from files on disk.
114
- * This is the source of truth — STATE.md is just a cache of this output.
121
+ * Reconstruct GSD state from DB (primary) or filesystem (fallback).
122
+ * STATE.md is a rendered cache of this output.
115
123
  *
116
- * Uses native batch parsing when available: a single Rust call reads and parses
117
- * every .md file under .gsd/, populating an in-memory cache that replaces all
118
- * individual loadFile() calls during milestone/slice/task traversal.
119
- * Falls back to sequential JS file reads when the native module is absent.
124
+ * When DB is available, queries milestone/slice/task tables directly.
125
+ * Falls back to filesystem parsing for unmigrated projects or when DB
126
+ * has zero milestones (e.g. first run before migration).
120
127
  */
121
128
  export async function deriveState(basePath) {
122
129
  // Return cached result if within the TTL window for the same basePath
@@ -590,6 +597,9 @@ export async function deriveStateFromDb(basePath) {
590
597
  progress: { milestones: milestoneProgress, slices: sliceProgress, tasks: taskProgress },
591
598
  };
592
599
  }
600
+ // LEGACY: Filesystem-based state derivation for unmigrated projects.
601
+ // DB-backed projects use deriveStateFromDb() above. Target: extract to
602
+ // state-legacy.ts when all projects are DB-backed.
593
603
  export async function _deriveStateImpl(basePath) {
594
604
  const milestoneIds = findMilestoneIds(basePath);
595
605
  // ── Parallel worker isolation ──────────────────────────────────────────