gsd-pi 2.78.1-dev.d8826a445 → 2.78.1-dev.eccf86e27

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 (121) hide show
  1. package/README.md +5 -7
  2. package/dist/help-text.js +1 -1
  3. package/dist/resource-loader.js +6 -1
  4. package/dist/resources/.managed-resources-content-hash +1 -1
  5. package/dist/resources/extensions/gsd/auto/detect-stuck.js +41 -5
  6. package/dist/resources/extensions/gsd/auto/loop.js +235 -36
  7. package/dist/resources/extensions/gsd/auto/phases.js +7 -5
  8. package/dist/resources/extensions/gsd/auto/session.js +33 -0
  9. package/dist/resources/extensions/gsd/auto-dispatch.js +46 -2
  10. package/dist/resources/extensions/gsd/auto-post-unit.js +19 -11
  11. package/dist/resources/extensions/gsd/auto-worktree.js +26 -187
  12. package/dist/resources/extensions/gsd/auto.js +79 -50
  13. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +9 -4
  14. package/dist/resources/extensions/gsd/crash-recovery.js +160 -47
  15. package/dist/resources/extensions/gsd/db/auto-workers.js +227 -0
  16. package/dist/resources/extensions/gsd/db/command-queue.js +105 -0
  17. package/dist/resources/extensions/gsd/db/milestone-leases.js +210 -0
  18. package/dist/resources/extensions/gsd/db/runtime-kv.js +91 -0
  19. package/dist/resources/extensions/gsd/db/unit-dispatches.js +322 -0
  20. package/dist/resources/extensions/gsd/docs/COORDINATION.md +42 -0
  21. package/dist/resources/extensions/gsd/doctor-proactive.js +4 -0
  22. package/dist/resources/extensions/gsd/doctor-runtime-checks.js +22 -6
  23. package/dist/resources/extensions/gsd/doctor.js +12 -2
  24. package/dist/resources/extensions/gsd/gsd-db.js +161 -3
  25. package/dist/resources/extensions/gsd/guided-flow.js +6 -2
  26. package/dist/resources/extensions/gsd/interrupted-session.js +18 -15
  27. package/dist/resources/extensions/gsd/state.js +21 -6
  28. package/dist/resources/extensions/gsd/worktree-resolver.js +64 -0
  29. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  30. package/dist/web/standalone/.next/BUILD_ID +1 -1
  31. package/dist/web/standalone/.next/app-path-routes-manifest.json +12 -12
  32. package/dist/web/standalone/.next/build-manifest.json +2 -2
  33. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  34. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  35. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  43. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/index.html +1 -1
  51. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app-paths-manifest.json +12 -12
  58. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  59. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  60. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  61. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  62. package/package.json +1 -1
  63. package/src/resources/extensions/gsd/auto/detect-stuck.ts +37 -5
  64. package/src/resources/extensions/gsd/auto/loop.ts +263 -41
  65. package/src/resources/extensions/gsd/auto/phases.ts +7 -5
  66. package/src/resources/extensions/gsd/auto/session.ts +36 -0
  67. package/src/resources/extensions/gsd/auto-dispatch.ts +53 -2
  68. package/src/resources/extensions/gsd/auto-post-unit.ts +19 -11
  69. package/src/resources/extensions/gsd/auto-worktree.ts +26 -211
  70. package/src/resources/extensions/gsd/auto.ts +89 -44
  71. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +9 -4
  72. package/src/resources/extensions/gsd/crash-recovery.ts +177 -43
  73. package/src/resources/extensions/gsd/db/auto-workers.ts +273 -0
  74. package/src/resources/extensions/gsd/db/command-queue.ts +149 -0
  75. package/src/resources/extensions/gsd/db/milestone-leases.ts +274 -0
  76. package/src/resources/extensions/gsd/db/runtime-kv.ts +127 -0
  77. package/src/resources/extensions/gsd/db/unit-dispatches.ts +446 -0
  78. package/src/resources/extensions/gsd/docs/COORDINATION.md +42 -0
  79. package/src/resources/extensions/gsd/doctor-proactive.ts +4 -0
  80. package/src/resources/extensions/gsd/doctor-runtime-checks.ts +24 -6
  81. package/src/resources/extensions/gsd/doctor.ts +10 -2
  82. package/src/resources/extensions/gsd/gsd-db.ts +170 -3
  83. package/src/resources/extensions/gsd/guided-flow.ts +6 -2
  84. package/src/resources/extensions/gsd/interrupted-session.ts +19 -12
  85. package/src/resources/extensions/gsd/state.ts +44 -6
  86. package/src/resources/extensions/gsd/tests/auto-loop-no-copy-artifacts.test.ts +72 -0
  87. package/src/resources/extensions/gsd/tests/auto-loop-symlink-worktree.test.ts +190 -0
  88. package/src/resources/extensions/gsd/tests/auto-workers.test.ts +105 -0
  89. package/src/resources/extensions/gsd/tests/command-queue.test.ts +141 -0
  90. package/src/resources/extensions/gsd/tests/crash-recovery-via-db.test.ts +203 -0
  91. package/src/resources/extensions/gsd/tests/crash-recovery.test.ts +169 -59
  92. package/src/resources/extensions/gsd/tests/detect-stuck-respects-retry.test.ts +173 -0
  93. package/src/resources/extensions/gsd/tests/integration/auto-worktree.test.ts +22 -12
  94. package/src/resources/extensions/gsd/tests/integration/doctor-proactive.test.ts +24 -10
  95. package/src/resources/extensions/gsd/tests/integration/doctor-runtime.test.ts +35 -23
  96. package/src/resources/extensions/gsd/tests/integration/workspace-collapse-integration.test.ts +3 -5
  97. package/src/resources/extensions/gsd/tests/interrupted-session-auto.test.ts +72 -25
  98. package/src/resources/extensions/gsd/tests/interrupted-session-ui.test.ts +72 -25
  99. package/src/resources/extensions/gsd/tests/memory-pressure-stuck-state.test.ts +9 -6
  100. package/src/resources/extensions/gsd/tests/milestone-leases.test.ts +152 -0
  101. package/src/resources/extensions/gsd/tests/parallel-milestone-isolation.test.ts +106 -0
  102. package/src/resources/extensions/gsd/tests/paused-session-via-db.test.ts +119 -0
  103. package/src/resources/extensions/gsd/tests/pipeline-variant-dispatch.test.ts +58 -0
  104. package/src/resources/extensions/gsd/tests/preferences-worktree-sync.test.ts +3 -17
  105. package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +110 -0
  106. package/src/resources/extensions/gsd/tests/runtime-kv.test.ts +120 -0
  107. package/src/resources/extensions/gsd/tests/skipped-validation-completion.test.ts +133 -28
  108. package/src/resources/extensions/gsd/tests/skipped-validation-db-atomicity.test.ts +17 -0
  109. package/src/resources/extensions/gsd/tests/stuck-state-via-db.test.ts +134 -0
  110. package/src/resources/extensions/gsd/tests/sync-layer-scope.test.ts +7 -26
  111. package/src/resources/extensions/gsd/tests/teardown-cleanup-parity.test.ts +4 -8
  112. package/src/resources/extensions/gsd/tests/unit-dispatches.test.ts +247 -0
  113. package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +41 -1
  114. package/src/resources/extensions/gsd/tests/workspace.test.ts +15 -9
  115. package/src/resources/extensions/gsd/tests/write-gate.test.ts +31 -23
  116. package/src/resources/extensions/gsd/worktree-resolver.ts +62 -0
  117. package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +0 -213
  118. package/src/resources/extensions/gsd/tests/auto-stale-lock-self-kill.test.ts +0 -87
  119. package/src/resources/extensions/gsd/tests/stop-auto-remote.test.ts +0 -159
  120. /package/dist/web/standalone/.next/static/{AT5qi39nKXkdmQIOIoh0f → Y5UeGFkXTYM9WIQOWHkot}/_buildManifest.js +0 -0
  121. /package/dist/web/standalone/.next/static/{AT5qi39nKXkdmQIOIoh0f → Y5UeGFkXTYM9WIQOWHkot}/_ssgManifest.js +0 -0
@@ -768,14 +768,16 @@ export async function postUnitPreVerification(pctx: PostUnitContext, opts?: PreV
768
768
  if (s.currentUnit.type === "triage-captures") {
769
769
  try {
770
770
  const { executeTriageResolutions } = await import("./triage-resolution.js");
771
- const state = await deriveState(s.basePath);
771
+ const state = await deriveState(s.canonicalProjectRoot);
772
772
  const mid = state.activeMilestone?.id ?? "";
773
773
  const sid = state.activeSlice?.id ?? "";
774
774
 
775
775
  // executeTriageResolutions handles defer milestone creation even
776
776
  // without an active milestone/slice (the "all milestones complete"
777
777
  // scenario from #1562). inject/replan/quick-task still require mid+sid.
778
- const triageResult = executeTriageResolutions(s.basePath, mid, sid);
778
+ // Phase C: write to canonical project root. copyPlanningArtifacts
779
+ // has been deleted, so triage writes land where readers consult.
780
+ const triageResult = executeTriageResolutions(s.canonicalProjectRoot, mid, sid);
779
781
 
780
782
  if (triageResult.injected > 0) {
781
783
  ctx.ui.notify(
@@ -940,10 +942,14 @@ export async function postUnitPreVerification(pctx: PostUnitContext, opts?: PreV
940
942
  try {
941
943
  const { milestone: mid, slice: sid } = parseUnitId(s.currentUnit.id);
942
944
  if (mid && sid) {
943
- const regenerated = await regenerateIfMissing(s.basePath, mid, sid, "PLAN");
945
+ // Phase C: write to the canonical project root (#5236 scope)
946
+ // so non-symlinked worktrees no longer maintain a separate
947
+ // local .gsd/ projection. copyPlanningArtifacts has been
948
+ // deleted; reads + writes converge at projectRoot.
949
+ const regenerated = await regenerateIfMissing(s.canonicalProjectRoot, mid, sid, "PLAN");
944
950
  if (regenerated) {
945
951
  // Re-check after regeneration
946
- triggerArtifactVerified = verifyExpectedArtifact(s.currentUnit.type, s.currentUnit.id, s.basePath);
952
+ triggerArtifactVerified = verifyExpectedArtifact(s.currentUnit.type, s.currentUnit.id, s.canonicalProjectRoot);
947
953
  if (triggerArtifactVerified) {
948
954
  invalidateAllCaches();
949
955
  }
@@ -1178,7 +1184,7 @@ export async function postUnitPostVerification(pctx: PostUnitContext): Promise<"
1178
1184
  if (mid && sid && tid) {
1179
1185
  try {
1180
1186
  updateTaskStatus(mid, sid, tid, "pending");
1181
- await renderPlanCheckboxes(s.basePath, mid, sid);
1187
+ await renderPlanCheckboxes(s.canonicalProjectRoot, mid, sid);
1182
1188
  } catch (dbErr) {
1183
1189
  // DB unavailable — fail explicitly rather than silently reverting to markdown mutation.
1184
1190
  // Use 'gsd recover' to rebuild DB state from disk if needed.
@@ -1188,7 +1194,8 @@ export async function postUnitPostVerification(pctx: PostUnitContext): Promise<"
1188
1194
 
1189
1195
  // 2. Delete SUMMARY.md for the task
1190
1196
  if (mid && sid && tid) {
1191
- const tasksDir = resolveTasksDir(s.basePath, mid, sid);
1197
+ // Phase C: read+delete via canonical project root.
1198
+ const tasksDir = resolveTasksDir(s.canonicalProjectRoot, mid, sid);
1192
1199
  if (tasksDir) {
1193
1200
  const summaryFile = join(tasksDir, buildTaskFileName(tid, "SUMMARY"));
1194
1201
  if (existsSync(summaryFile)) {
@@ -1199,7 +1206,7 @@ export async function postUnitPostVerification(pctx: PostUnitContext): Promise<"
1199
1206
 
1200
1207
  // 3. Delete the retry_on artifact (e.g. NEEDS-REWORK.md)
1201
1208
  if (trigger.retryArtifact) {
1202
- const retryArtifactPath = resolveHookArtifactPath(s.basePath, trigger.unitId, trigger.retryArtifact);
1209
+ const retryArtifactPath = resolveHookArtifactPath(s.canonicalProjectRoot, trigger.unitId, trigger.retryArtifact);
1203
1210
  if (existsSync(retryArtifactPath)) {
1204
1211
  unlinkSync(retryArtifactPath);
1205
1212
  }
@@ -1477,16 +1484,17 @@ export async function postUnitPostVerification(pctx: PostUnitContext): Promise<"
1477
1484
  if (hasPendingCaptures(s.basePath)) {
1478
1485
  const pending = loadPendingCaptures(s.basePath);
1479
1486
  if (pending.length > 0) {
1480
- const state = await deriveState(s.basePath);
1487
+ const readRoot = s.canonicalProjectRoot;
1488
+ const state = await deriveState(readRoot);
1481
1489
  const mid = state.activeMilestone?.id;
1482
1490
  const sid = state.activeSlice?.id;
1483
1491
 
1484
1492
  if (mid && sid) {
1485
1493
  let currentPlan = "";
1486
1494
  let roadmapContext = "";
1487
- const planFile = resolveSliceFile(s.basePath, mid, sid, "PLAN");
1495
+ const planFile = resolveSliceFile(readRoot, mid, sid, "PLAN");
1488
1496
  if (planFile) currentPlan = (await loadFile(planFile)) ?? "";
1489
- const roadmapFile = resolveMilestoneFile(s.basePath, mid, "ROADMAP");
1497
+ const roadmapFile = resolveMilestoneFile(readRoot, mid, "ROADMAP");
1490
1498
  if (roadmapFile) roadmapContext = (await loadFile(roadmapFile)) ?? "";
1491
1499
 
1492
1500
  const capturesList = pending.map(c =>
@@ -1554,7 +1562,7 @@ export async function postUnitPostVerification(pctx: PostUnitContext): Promise<"
1554
1562
  // exits the loop, leaving the user with no hint to /clear and /gsd again.
1555
1563
  if (s.stepMode) {
1556
1564
  try {
1557
- const nextState = await deriveState(s.basePath);
1565
+ const nextState = await deriveState(s.canonicalProjectRoot);
1558
1566
  ctx.ui.notify(buildStepCompleteMessage(nextState), "info");
1559
1567
  } catch (e) {
1560
1568
  debugLog("postUnit", { phase: "step-wizard-notify", error: String(e) });
@@ -268,9 +268,12 @@ function getActiveWorkspace(): GsdWorkspace | null {
268
268
 
269
269
  function clearProjectRootStateFiles(basePath: string, milestoneId: string): void {
270
270
  const gsdDir = gsdRoot(basePath);
271
+ // Phase C pt 2: auto.lock removed from this list — the file is gone
272
+ // (migrated to the workers + unit_dispatches + runtime_kv tables). The
273
+ // remaining transient files (STATE.md, {MID}-META.json) are still
274
+ // worth removing on teardown.
271
275
  const transientFiles = [
272
276
  join(gsdDir, "STATE.md"),
273
- join(gsdDir, "auto.lock"),
274
277
  join(gsdDir, "milestones", milestoneId, `${milestoneId}-META.json`),
275
278
  ];
276
279
 
@@ -1128,137 +1131,13 @@ export function enterBranchModeForMilestone(
1128
1131
  * Forward-merge plan checkbox state from the project root into a freshly
1129
1132
  * re-attached worktree (#778).
1130
1133
  *
1131
- * When auto-mode stops via crash (not graceful stop), the milestone branch
1132
- * HEAD may be behind the filesystem state at the project root because
1133
- * syncStateToProjectRoot() runs after every task completion but the final
1134
- * git commit may not have happened before the crash. On restart the worktree
1135
- * is re-attached to the branch HEAD, which has [ ] for the crashed task,
1136
- * causing verifyExpectedArtifact() to fail and triggering an infinite
1137
- * dispatch/skip loop.
1138
- *
1139
- * Fix: after re-attaching, read every *.md plan file in the milestone
1140
- * directory at the project root and apply any [x] checkbox states that are
1141
- * ahead of the worktree version (forward-only: never downgrade [x] → [ ]).
1142
- *
1143
- * This is forward-only compatibility for legacy projection copies. The DB
1144
- * remains authoritative; this never downgrades checked boxes in a local
1145
- * worktree projection.
1146
- */
1147
- /**
1148
- * Scope-typed variant of reconcilePlanCheckboxes.
1149
- *
1150
- * Takes an explicit (rootScope, worktreeScope) pair. milestoneId is taken
1151
- * from rootScope. Asserts both scopes belong to the same workspace identity
1152
- * to prevent silent mismatch bugs.
1153
- */
1154
- export function reconcilePlanCheckboxesByScope(
1155
- rootScope: MilestoneScope,
1156
- worktreeScope: MilestoneScope,
1157
- ): void {
1158
- if (rootScope.workspace.identityKey !== worktreeScope.workspace.identityKey) {
1159
- throw new Error(
1160
- `reconcilePlanCheckboxesByScope: scope identity mismatch — ` +
1161
- `rootScope.identityKey="${rootScope.workspace.identityKey}" ` +
1162
- `worktreeScope.identityKey="${worktreeScope.workspace.identityKey}"`,
1163
- );
1164
- }
1165
- if (rootScope.milestoneId !== worktreeScope.milestoneId) {
1166
- throw new Error(
1167
- `reconcilePlanCheckboxesByScope: milestoneId mismatch — ` +
1168
- `rootScope.milestoneId="${rootScope.milestoneId}" worktreeScope.milestoneId="${worktreeScope.milestoneId}"`,
1169
- );
1170
- }
1171
- const projectRoot = rootScope.workspace.projectRoot;
1172
- const wtPath = worktreeScope.workspace.worktreeRoot ?? worktreeScope.workspace.projectRoot;
1173
- const milestoneId = rootScope.milestoneId;
1174
- reconcilePlanCheckboxes(projectRoot, wtPath, milestoneId);
1175
- }
1176
-
1177
- /**
1178
- * @deprecated Use reconcilePlanCheckboxesByScope instead.
1179
- * TODO(C-future): remove once all callers migrated.
1134
+ * Phase C: deleted. Writers in workflow-projections.ts, triage-resolution.ts,
1135
+ * rule-registry.ts, and auto-post-unit.ts now route through
1136
+ * s.canonicalProjectRoot, so non-symlinked worktrees no longer need a local
1137
+ * .gsd/ projection the project-root .gsd/ is the only authoritative source
1138
+ * for both reads and writes. copyPlanningArtifacts and reconcilePlanCheckboxes
1139
+ * (both formerly here) became dead.
1180
1140
  */
1181
- function reconcilePlanCheckboxes(
1182
- projectRoot: string,
1183
- wtPath: string,
1184
- milestoneId: string,
1185
- ): void {
1186
- const srcMilestone = join(projectRoot, ".gsd", "milestones", milestoneId);
1187
- const dstMilestone = join(wtPath, ".gsd", "milestones", milestoneId);
1188
- if (!existsSync(srcMilestone) || !existsSync(dstMilestone)) return;
1189
-
1190
- // Walk all markdown files in the milestone directory (plans, summaries, etc.)
1191
- function walkMd(dir: string): string[] {
1192
- const results: string[] = [];
1193
- try {
1194
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
1195
- const full = join(dir, entry.name);
1196
- if (entry.isDirectory()) {
1197
- results.push(...walkMd(full));
1198
- } else if (entry.isFile() && entry.name.endsWith(".md")) {
1199
- results.push(full);
1200
- }
1201
- }
1202
- } catch (err) {
1203
- /* non-fatal */
1204
- logWarning("worktree", `walkMd directory read failed: ${err instanceof Error ? err.message : String(err)}`);
1205
- }
1206
- return results;
1207
- }
1208
-
1209
- for (const srcFile of walkMd(srcMilestone)) {
1210
- const rel = srcFile.slice(srcMilestone.length);
1211
- const dstFile = dstMilestone + rel;
1212
- if (!existsSync(dstFile)) continue; // only reconcile existing files
1213
-
1214
- let srcContent: string;
1215
- let dstContent: string;
1216
- try {
1217
- srcContent = readFileSync(srcFile, "utf-8");
1218
- dstContent = readFileSync(dstFile, "utf-8");
1219
- } catch (e) {
1220
- logWarning("worktree", `reconcilePlanCheckboxes read failed: ${(e as Error).message}`);
1221
- continue;
1222
- }
1223
-
1224
- if (srcContent === dstContent) continue;
1225
-
1226
- // Extract all checked task IDs from the source (project root)
1227
- // Pattern: - [x] **T<id>: or - [x] **S<id>: (case-insensitive x)
1228
- const checkedRe = /^- \[[xX]\] \*\*([TS]\d+):/gm;
1229
- const srcChecked = new Set<string>();
1230
- for (const m of srcContent.matchAll(checkedRe)) srcChecked.add(m[1]);
1231
-
1232
- if (srcChecked.size === 0) continue;
1233
-
1234
- // Forward-apply: replace [ ] → [x] for any IDs that are checked in src
1235
- let updated = dstContent;
1236
- let changed = false;
1237
- for (const id of srcChecked) {
1238
- const escapedId = id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1239
- const uncheckedRe = new RegExp(
1240
- `^(- )\\[ \\]( \\*\\*${escapedId}:)`,
1241
- "gm",
1242
- );
1243
- if (uncheckedRe.test(updated)) {
1244
- updated = updated.replace(
1245
- new RegExp(`^(- )\\[ \\]( \\*\\*${escapedId}:)`, "gm"),
1246
- "$1[x]$2",
1247
- );
1248
- changed = true;
1249
- }
1250
- }
1251
-
1252
- if (changed) {
1253
- try {
1254
- atomicWriteSync(dstFile, updated, "utf-8");
1255
- } catch (err) {
1256
- /* non-fatal */
1257
- logWarning("worktree", `plan checkbox reconcile write failed: ${err instanceof Error ? err.message : String(err)}`);
1258
- }
1259
- }
1260
- }
1261
- }
1262
1141
 
1263
1142
  export function createAutoWorktree(
1264
1143
  basePath: string,
@@ -1316,32 +1195,15 @@ export function createAutoWorktree(
1316
1195
  });
1317
1196
  }
1318
1197
 
1319
- // Copy .gsd/ planning artifacts from the source repo into the new worktree.
1320
- // Worktrees are fresh git checkouts untracked files don't carry over.
1321
- // Planning artifacts may be untracked if the project's .gitignore had a
1322
- // blanket .gsd/ rule (pre-v2.14.0). Without this copy, auto-mode loops
1323
- // on plan-slice because the plan file doesn't exist in the worktree.
1324
- //
1325
- // IMPORTANT: Skip when re-attaching to an existing branch (#759).
1326
- // The branch checkout already has committed artifacts with correct state
1327
- // (e.g. [x] for completed slices). Copying from the project root would
1328
- // overwrite them with stale data ([ ] checkboxes) because the root is
1329
- // not always fully synced.
1330
- if (!branchExists) {
1331
- copyPlanningArtifacts(basePath, info.path);
1332
- } else {
1333
- // Re-attaching to an existing branch: forward-merge any plan checkpoint
1334
- // state from the project root into the worktree (#778).
1335
- //
1336
- // If auto-mode stopped via crash, the milestone branch HEAD may lag behind
1337
- // the project root filesystem because syncStateToProjectRoot() ran after
1338
- // task completion but the auto-commit never fired. On restart the worktree
1339
- // is re-created from the branch HEAD (which has [ ] for the crashed task),
1340
- // causing verifyExpectedArtifact() to return false → stale-key eviction →
1341
- // infinite dispatch/skip loop. Reconciling here ensures the worktree sees
1342
- // the same [x] state that syncStateToProjectRoot() wrote to the root.
1343
- reconcilePlanCheckboxes(basePath, info.path, milestoneId);
1344
- }
1198
+ // Phase C: copyPlanningArtifacts and reconcilePlanCheckboxes were
1199
+ // deleted. Both addressed the same problem (worktree-local .gsd/
1200
+ // projection lagging behind project-root state) by maintaining a stale
1201
+ // copy. Now that auto-mode writers in workflow-projections.ts,
1202
+ // triage-resolution.ts, rule-registry.ts, and auto-post-unit.ts route
1203
+ // through s.canonicalProjectRoot, the worktree never needs a local
1204
+ // .gsd/ both reads and writes converge on the project-root .gsd/.
1205
+ // The original concerns (#759, #778) no longer apply because there is
1206
+ // no second copy to drift.
1345
1207
 
1346
1208
  // Run user-configured post-create hook (#597) — e.g. copy .env, symlink assets
1347
1209
  const hookError = runWorktreePostCreateHook(basePath, info.path);
@@ -1368,60 +1230,13 @@ export function createAutoWorktree(
1368
1230
  return info.path;
1369
1231
  }
1370
1232
 
1371
- /**
1372
- * Copy .gsd/ planning artifacts from source repo to a new worktree.
1373
- * Copies milestones/, DECISIONS.md, REQUIREMENTS.md, PROJECT.md, QUEUE.md,
1374
- * STATE.md, KNOWLEDGE.md, and OVERRIDES.md.
1375
- * Skips runtime files (auto.lock, metrics.json, etc.) and the worktrees/ dir.
1376
- * Best-effort failures are non-fatal since auto-mode can recreate artifacts.
1377
- */
1378
- function copyPlanningArtifacts(srcBase: string, wtPath: string): void {
1379
- const srcGsd = join(srcBase, ".gsd");
1380
- const dstGsd = join(wtPath, ".gsd");
1381
- if (!existsSync(srcGsd)) return;
1382
- if (isSamePath(srcGsd, dstGsd)) return;
1383
-
1384
- // Copy milestones/ directory (planning files, roadmaps, plans, research)
1385
- safeCopyRecursive(join(srcGsd, "milestones"), join(dstGsd, "milestones"), {
1386
- force: true,
1387
- filter: (src) => !src.endsWith("-META.json"),
1388
- });
1389
-
1390
- // Copy top-level planning files
1391
- for (const file of [
1392
- "DECISIONS.md",
1393
- "REQUIREMENTS.md",
1394
- "PROJECT.md",
1395
- "QUEUE.md",
1396
- "STATE.md",
1397
- "KNOWLEDGE.md",
1398
- "OVERRIDES.md",
1399
- "mcp.json",
1400
- ]) {
1401
- safeCopy(join(srcGsd, file), join(dstGsd, file), { force: true });
1402
- }
1403
-
1404
- // Seed canonical PREFERENCES.md when available; fall back to legacy lowercase.
1405
- if (existsSync(join(srcGsd, PROJECT_PREFERENCES_FILE))) {
1406
- safeCopy(
1407
- join(srcGsd, PROJECT_PREFERENCES_FILE),
1408
- join(dstGsd, PROJECT_PREFERENCES_FILE),
1409
- { force: true },
1410
- );
1411
- } else if (existsSync(join(srcGsd, LEGACY_PROJECT_PREFERENCES_FILE))) {
1412
- safeCopy(
1413
- join(srcGsd, LEGACY_PROJECT_PREFERENCES_FILE),
1414
- join(dstGsd, LEGACY_PROJECT_PREFERENCES_FILE),
1415
- { force: true },
1416
- );
1417
- }
1418
-
1419
- // Shared WAL (R012): worktrees use the project root's DB directly.
1420
- // No longer copy gsd.db into the worktree — the DB path resolver in
1421
- // ensureDbOpen() detects the worktree location and opens the root DB.
1422
- // Compat note: reconcileWorktreeDb() in mergeMilestoneToMain handles
1423
- // worktrees that already have a local gsd.db from before this change.
1424
- }
1233
+ // Phase C: copyPlanningArtifacts removed. Planning artifacts now live
1234
+ // only at the project root .gsd/; auto-mode writers (workflow-projections,
1235
+ // triage-resolution, rule-registry, regenerateIfMissing,
1236
+ // resolveHookArtifactPath) all route through s.canonicalProjectRoot.
1237
+ // Worktrees are pure git checkouts they no longer maintain a parallel
1238
+ // .gsd/ projection. The gsd.db has always lived at the project root via
1239
+ // the shared-WAL R012 contract; that is unchanged.
1425
1240
 
1426
1241
  /**
1427
1242
  * Teardown an auto-worktree: chdir back to original base, then remove
@@ -22,8 +22,14 @@ import type { GSDState } from "./types.js";
22
22
  import {
23
23
  assessInterruptedSession,
24
24
  readPausedSessionMetadata,
25
+ PAUSED_SESSION_KV_KEY,
25
26
  type InterruptedSessionAssessment,
27
+ type PausedSessionMetadata,
26
28
  } from "./interrupted-session.js";
29
+ import {
30
+ setRuntimeKv,
31
+ deleteRuntimeKv,
32
+ } from "./db/runtime-kv.js";
27
33
  import { getManifestStatus } from "./files.js";
28
34
  export { inlinePriorMilestoneSummary } from "./files.js";
29
35
  import { collectSecretsFromManifest } from "../get-secrets-from-user.js";
@@ -244,6 +250,7 @@ import type {
244
250
  CurrentUnit,
245
251
  UnitRouting,
246
252
  StartModel,
253
+ AutoSession,
247
254
  } from "./auto/session.js";
248
255
  export {
249
256
  STUB_RECOVERY_THRESHOLD,
@@ -257,6 +264,9 @@ export type {
257
264
  import { autoSession as s } from "./auto-runtime-state.js";
258
265
  import { gsdHome } from "./gsd-home.js";
259
266
  import { createWorkspace, scopeMilestone } from "./workspace.js";
267
+ import { registerAutoWorker, markWorkerStopping } from "./db/auto-workers.js";
268
+ import { releaseMilestoneLease } from "./db/milestone-leases.js";
269
+ import { normalizeRealPath } from "./paths.js";
260
270
 
261
271
  // ── ENCAPSULATION INVARIANT ─────────────────────────────────────────────────
262
272
  // ALL mutable auto-mode state lives in the AutoSession class (auto/session.ts).
@@ -274,6 +284,28 @@ import { createWorkspace, scopeMilestone } from "./workspace.js";
274
284
  /** Throttle STATE.md rebuilds — at most once per 30 seconds */
275
285
  const STATE_REBUILD_MIN_INTERVAL_MS = 30_000;
276
286
 
287
+ /**
288
+ * Phase B — register this auto-mode process in the workers table so other
289
+ * workers and janitors can detect liveness via heartbeat. Best-effort: if
290
+ * the DB is unavailable (e.g. fresh project before init) we skip registration
291
+ * silently rather than blocking session start.
292
+ */
293
+ function registerAutoWorkerForSession(session: AutoSession): void {
294
+ if (session.workerId) return; // already registered (e.g. resume re-runs)
295
+ try {
296
+ const projectRootRealpath = normalizeRealPath(
297
+ session.scope?.workspace.projectRoot
298
+ ?? (session.originalBasePath || session.basePath),
299
+ );
300
+ session.workerId = registerAutoWorker({ projectRootRealpath });
301
+ } catch (err) {
302
+ debugLog("autoLoop", {
303
+ phase: "register-worker-failed",
304
+ error: err instanceof Error ? err.message : String(err),
305
+ });
306
+ }
307
+ }
308
+
277
309
  function captureProjectRootEnv(projectRoot: string): void {
278
310
  if (!s.projectRootEnvCaptured) {
279
311
  s.hadProjectRootEnv = Object.prototype.hasOwnProperty.call(process.env, "GSD_PROJECT_ROOT");
@@ -911,6 +943,21 @@ export async function stopAuto(
911
943
  debugLog("stop-cleanup-locks", { error: e instanceof Error ? e.message : String(e) });
912
944
  }
913
945
 
946
+ // ── Step 1b: Coordination cleanup (Phase B) ──
947
+ // Release any active milestone lease so other workers don't have to
948
+ // wait for TTL expiry, then mark this worker as stopping. Best-effort:
949
+ // DB unavailability or stale state must not block shutdown.
950
+ try {
951
+ if (s.workerId && s.currentMilestoneId && s.milestoneLeaseToken) {
952
+ releaseMilestoneLease(s.workerId, s.currentMilestoneId, s.milestoneLeaseToken);
953
+ }
954
+ if (s.workerId) {
955
+ markWorkerStopping(s.workerId);
956
+ }
957
+ } catch (e) {
958
+ debugLog("stop-cleanup-coordination", { error: e instanceof Error ? e.message : String(e) });
959
+ }
960
+
914
961
  // ── Step 1b: Flush queued follow-up messages (#3512) ──
915
962
  // Late async notifications (async_job_result, gsd-auto-wrapup) can trigger
916
963
  // extra LLM turns after stop. Flush them the same way run-unit.ts does.
@@ -1095,11 +1142,11 @@ export async function stopAuto(
1095
1142
  }
1096
1143
 
1097
1144
  // ── Step 12: Remove paused-session metadata (#1383) ──
1145
+ // Phase C pt 2: deleteRuntimeKv replaces unlinkSync(paused-session.json).
1098
1146
  try {
1099
- const pausedPath = join(gsdRoot(s.originalBasePath || s.basePath), "runtime", "paused-session.json");
1100
- if (existsSync(pausedPath)) unlinkSync(pausedPath);
1147
+ deleteRuntimeKv("global", "", PAUSED_SESSION_KV_KEY);
1101
1148
  } catch (err) { /* non-fatal */
1102
- logWarning("engine", `file unlink failed: ${err instanceof Error ? err.message : String(err)}`, { file: "auto.ts" });
1149
+ logWarning("engine", `paused-session DB delete failed: ${err instanceof Error ? err.message : String(err)}`, { file: "auto.ts" });
1103
1150
  }
1104
1151
 
1105
1152
  // ── Step 13: Restore original model + thinking (before reset clears IDs) ──
@@ -1200,10 +1247,12 @@ export async function pauseAuto(
1200
1247
  s.pausedSessionFile = normalizeSessionFilePath(ctx?.sessionManager?.getSessionFile() ?? null);
1201
1248
 
1202
1249
  // Persist paused-session metadata so resume survives /exit (#1383).
1203
- // The fresh-start bootstrap checks for this file and restores worktree context.
1250
+ // Phase C pt 2: persisted to runtime_kv (global scope, key
1251
+ // PAUSED_SESSION_KV_KEY) instead of runtime/paused-session.json. The
1252
+ // fresh-start bootstrap below reads from the same key.
1204
1253
  try {
1205
- const pausedMeta = {
1206
- milestoneId: s.currentMilestoneId,
1254
+ const pausedMeta: PausedSessionMetadata = {
1255
+ milestoneId: s.currentMilestoneId ?? undefined,
1207
1256
  worktreePath: isInAutoWorktree(s.basePath) ? s.basePath : null,
1208
1257
  originalBasePath: s.originalBasePath,
1209
1258
  stepMode: s.stepMode,
@@ -1211,20 +1260,15 @@ export async function pauseAuto(
1211
1260
  sessionFile: s.pausedSessionFile,
1212
1261
  unitType: s.currentUnit?.type ?? undefined,
1213
1262
  unitId: s.currentUnit?.id ?? undefined,
1214
- activeEngineId: s.activeEngineId,
1263
+ activeEngineId: s.activeEngineId ?? undefined,
1215
1264
  activeRunDir: s.activeRunDir,
1216
1265
  autoStartTime: s.autoStartTime,
1217
1266
  milestoneLock: s.sessionMilestoneLock ?? undefined,
1218
1267
  };
1219
- const runtimeDir = join(gsdRoot(s.originalBasePath || s.basePath), "runtime");
1220
- atomicWriteSync(
1221
- join(runtimeDir, "paused-session.json"),
1222
- JSON.stringify(pausedMeta, null, 2),
1223
- "utf-8",
1224
- );
1268
+ setRuntimeKv("global", "", PAUSED_SESSION_KV_KEY, pausedMeta);
1225
1269
  } catch (err) {
1226
1270
  // Non-fatal — resume will still work via full bootstrap, just without worktree context
1227
- logWarning("engine", `paused-session file write failed: ${err instanceof Error ? err.message : String(err)}`, { file: "auto.ts" });
1271
+ logWarning("engine", `paused-session DB write failed: ${err instanceof Error ? err.message : String(err)}`, { file: "auto.ts" });
1228
1272
  }
1229
1273
 
1230
1274
  // Close out the current unit so its runtime record doesn't stay at "dispatched"
@@ -1507,8 +1551,10 @@ export async function startAuto(
1507
1551
  ctx.ui.notify("Recovered unfinished migration (.gsd.migrating → .gsd).", "info");
1508
1552
  }
1509
1553
 
1510
- const freshStartAssessment = interruptedAssessment
1511
- ?? await assessInterruptedSession(base);
1554
+ const freshStartAssessment = await (interruptedAssessment
1555
+ ?? (() => {
1556
+ return ensureDbOpen(base).then(() => assessInterruptedSession(base));
1557
+ })());
1512
1558
 
1513
1559
  if (freshStartAssessment.classification === "running") {
1514
1560
  const pid = freshStartAssessment.lock?.pid;
@@ -1523,10 +1569,20 @@ export async function startAuto(
1523
1569
 
1524
1570
  // If resuming from paused state, just re-activate and dispatch next unit.
1525
1571
  // Check persisted paused-session first (#1383) — survives /exit.
1572
+ // Phase C pt 2: persisted in runtime_kv (global scope) instead of
1573
+ // runtime/paused-session.json. The `clearPausedSession` helper
1574
+ // replaces every prior unlinkSync(pausedPath) call.
1575
+ const clearPausedSession = (logTag: string): void => {
1576
+ try {
1577
+ deleteRuntimeKv("global", "", PAUSED_SESSION_KV_KEY);
1578
+ } catch (err) {
1579
+ logWarning("session", `${logTag}: ${err instanceof Error ? err.message : String(err)}`, { file: "auto.ts" });
1580
+ }
1581
+ };
1582
+
1526
1583
  if (!s.paused) {
1527
1584
  try {
1528
1585
  const meta = freshStartAssessment.pausedSession ?? readPausedSessionMetadata(base);
1529
- const pausedPath = join(gsdRoot(base), "runtime", "paused-session.json");
1530
1586
  if (meta?.activeEngineId && meta.activeEngineId !== "dev") {
1531
1587
  // Custom workflow resume — restore engine state
1532
1588
  s.activeEngineId = meta.activeEngineId;
@@ -1536,11 +1592,6 @@ export async function startAuto(
1536
1592
  s.autoStartTime = meta.autoStartTime || Date.now();
1537
1593
  s.sessionMilestoneLock = meta.milestoneLock ?? null;
1538
1594
  s.paused = true;
1539
- try { unlinkSync(pausedPath); } catch (e) {
1540
- if ((e as NodeJS.ErrnoException).code !== "ENOENT") {
1541
- logWarning("session", `pause file cleanup failed: ${e instanceof Error ? e.message : String(e)}`, { file: "auto.ts" });
1542
- }
1543
- }
1544
1595
  ctx.ui.notify(
1545
1596
  `Resuming paused custom workflow${meta.activeRunDir ? ` (${meta.activeRunDir})` : ""}.`,
1546
1597
  "info",
@@ -1581,11 +1632,7 @@ export async function startAuto(
1581
1632
  }
1582
1633
  }
1583
1634
  if (!mDir || summaryIsTerminal) {
1584
- try { unlinkSync(pausedPath); } catch (err) {
1585
- if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
1586
- logWarning("session", `pause file cleanup failed: ${err instanceof Error ? err.message : String(err)}`, { file: "auto.ts" });
1587
- }
1588
- }
1635
+ clearPausedSession("paused-session DB cleanup failed (milestone gone/complete)");
1589
1636
  ctx.ui.notify(
1590
1637
  `Paused milestone ${meta.milestoneId} is ${!mDir ? "missing" : "already complete"}. Starting fresh.`,
1591
1638
  "info",
@@ -1616,22 +1663,15 @@ export async function startAuto(
1616
1663
  : (s.originalBasePath || base);
1617
1664
  rebuildScope(rawForScope, s.currentMilestoneId);
1618
1665
  }
1619
- try { unlinkSync(pausedPath); } catch (e) {
1620
- if ((e as NodeJS.ErrnoException).code !== "ENOENT") {
1621
- logWarning("session", `pause file cleanup failed: ${e instanceof Error ? e.message : String(e)}`, { file: "auto.ts" });
1622
- }
1623
- }
1624
1666
  ctx.ui.notify(
1625
1667
  `Resuming paused session for ${meta.milestoneId}${meta.worktreePath && existsSync(meta.worktreePath) ? ` (worktree)` : ""}.`,
1626
1668
  "info",
1627
1669
  );
1628
1670
  }
1629
- } else if (existsSync(pausedPath)) {
1630
- try { unlinkSync(pausedPath); } catch (e) {
1631
- if ((e as NodeJS.ErrnoException).code !== "ENOENT") {
1632
- logWarning("session", `stale pause file cleanup failed: ${e instanceof Error ? e.message : String(e)}`, { file: "auto.ts" });
1633
- }
1634
- }
1671
+ } else if (meta) {
1672
+ // Stale paused-session metadata that the assessment chose not to
1673
+ // resume clean it up so the next bootstrap starts fresh.
1674
+ clearPausedSession("stale paused-session DB cleanup failed");
1635
1675
  }
1636
1676
  }
1637
1677
  } catch (err) {
@@ -1811,19 +1851,23 @@ export async function startAuto(
1811
1851
  s.pausedSessionFile = null;
1812
1852
  }
1813
1853
 
1854
+ captureProjectRootEnv(s.originalBasePath || s.basePath);
1855
+ registerAutoWorkerForSession(s);
1814
1856
  updateSessionLock(
1815
1857
  lockBase(),
1816
1858
  "resuming",
1817
1859
  s.currentMilestoneId ?? "unknown",
1818
1860
  );
1819
- writeLock(
1820
- lockBase(),
1821
- "resuming",
1822
- s.currentMilestoneId ?? "unknown",
1823
- );
1861
+ if (s.workerId) {
1862
+ writeLock(
1863
+ lockBase(),
1864
+ "resuming",
1865
+ s.currentMilestoneId ?? "unknown",
1866
+ );
1867
+ clearPausedSession("paused-session DB cleanup failed (resume activation)");
1868
+ }
1824
1869
  pi.events.emit(CMUX_CHANNELS.LOG, { preferences: loadEffectiveGSDPreferences(s.basePath || undefined)?.preferences, message: s.stepMode ? "Step-mode resumed." : "Auto-mode resumed.", level: "progress" });
1825
1870
 
1826
- captureProjectRootEnv(s.originalBasePath || s.basePath);
1827
1871
  startAutoCommandPolling(s.basePath);
1828
1872
  await runAutoLoopWithUok({
1829
1873
  ctx,
@@ -1862,6 +1906,7 @@ export async function startAuto(
1862
1906
  rebuildScope(s.basePath, s.currentMilestoneId);
1863
1907
 
1864
1908
  captureProjectRootEnv(s.originalBasePath || s.basePath);
1909
+ registerAutoWorkerForSession(s);
1865
1910
  try {
1866
1911
  pi.events.emit(CMUX_CHANNELS.SIDEBAR, { action: "sync" as const, preferences: loadEffectiveGSDPreferences(s.basePath || undefined)?.preferences, state: await deriveState(s.basePath) });
1867
1912
  } catch (err) {
@@ -571,8 +571,13 @@ export function registerHooks(
571
571
  const currentPendingGate = getPendingGate();
572
572
  if (currentPendingGate) {
573
573
  if (details?.cancelled || !details?.response) {
574
- // Gate stays pending. Return a hard instruction as the tool result so
575
- // the model cannot reinterpret a cancelled prompt as prior approval.
574
+ // Gate stays pending. Direct the agent to the most reliable recovery
575
+ // path re-calling ask_user_questions with the same gate id — without
576
+ // misrepresenting the plain-text path. The plain-text path also works
577
+ // (isExplicitApprovalResponse on the next before_agent_start clears
578
+ // the gate when the user replies with an approval keyword), but the
579
+ // structured re-ask is more deterministic and gives the user a clear UI.
580
+ resetToolCallLoopGuard();
576
581
  return {
577
582
  content: [{
578
583
  type: "text" as const,
@@ -580,8 +585,8 @@ export function registerHooks(
580
585
  `HARD BLOCK: approval gate "${currentPendingGate}" is still pending.`,
581
586
  "No user response was received for the confirmation question.",
582
587
  "Do not infer approval from earlier or prior messages.",
583
- "Do not proceed, write files, save artifacts, or call more tools.",
584
- "Ask the user to confirm in plain chat, then stop and wait for their next message.",
588
+ "Do not proceed, write files, save artifacts, or call other tools.",
589
+ `Re-call ask_user_questions with the same gate question id ("${currentPendingGate}") and wait for the user's response.`,
585
590
  ].join(" "),
586
591
  }],
587
592
  };