gsd-pi 2.78.1-dev.e9d88a536 → 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 (212) 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 +14 -7
  8. package/dist/resources/extensions/gsd/auto/session.js +36 -0
  9. package/dist/resources/extensions/gsd/auto-dispatch.js +49 -4
  10. package/dist/resources/extensions/gsd/auto-post-unit.js +26 -12
  11. package/dist/resources/extensions/gsd/auto-worktree.js +185 -201
  12. package/dist/resources/extensions/gsd/auto.js +139 -49
  13. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +1 -1
  14. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +26 -20
  15. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +67 -55
  16. package/dist/resources/extensions/gsd/crash-recovery.js +160 -47
  17. package/dist/resources/extensions/gsd/db/auto-workers.js +227 -0
  18. package/dist/resources/extensions/gsd/db/command-queue.js +105 -0
  19. package/dist/resources/extensions/gsd/db/milestone-leases.js +210 -0
  20. package/dist/resources/extensions/gsd/db/runtime-kv.js +91 -0
  21. package/dist/resources/extensions/gsd/db/unit-dispatches.js +322 -0
  22. package/dist/resources/extensions/gsd/db-writer.js +96 -16
  23. package/dist/resources/extensions/gsd/delegation-policy.js +155 -0
  24. package/dist/resources/extensions/gsd/docs/COORDINATION.md +42 -0
  25. package/dist/resources/extensions/gsd/doctor-proactive.js +4 -0
  26. package/dist/resources/extensions/gsd/doctor-runtime-checks.js +22 -6
  27. package/dist/resources/extensions/gsd/doctor.js +12 -2
  28. package/dist/resources/extensions/gsd/gsd-db.js +355 -3
  29. package/dist/resources/extensions/gsd/guided-flow-queue.js +1 -1
  30. package/dist/resources/extensions/gsd/guided-flow.js +116 -26
  31. package/dist/resources/extensions/gsd/interrupted-session.js +18 -15
  32. package/dist/resources/extensions/gsd/metrics.js +287 -1
  33. package/dist/resources/extensions/gsd/paths.js +79 -8
  34. package/dist/resources/extensions/gsd/prompts/complete-slice.md +4 -4
  35. package/dist/resources/extensions/gsd/prompts/execute-task.md +3 -3
  36. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +8 -1
  37. package/dist/resources/extensions/gsd/prompts/guided-discuss-project.md +22 -7
  38. package/dist/resources/extensions/gsd/prompts/guided-discuss-requirements.md +6 -2
  39. package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -1
  40. package/dist/resources/extensions/gsd/state.js +21 -6
  41. package/dist/resources/extensions/gsd/templates/project.md +10 -0
  42. package/dist/resources/extensions/gsd/workflow-mcp.js +2 -2
  43. package/dist/resources/extensions/gsd/workspace.js +59 -0
  44. package/dist/resources/extensions/gsd/worktree-resolver.js +79 -2
  45. package/dist/resources/extensions/gsd/write-intercept.js +3 -3
  46. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  47. package/dist/web/standalone/.next/BUILD_ID +1 -1
  48. package/dist/web/standalone/.next/app-path-routes-manifest.json +14 -14
  49. package/dist/web/standalone/.next/build-manifest.json +2 -2
  50. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  51. package/dist/web/standalone/.next/required-server-files.json +1 -1
  52. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  53. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  61. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/index.html +1 -1
  69. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  72. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  73. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  74. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  75. package/dist/web/standalone/.next/server/app-paths-manifest.json +14 -14
  76. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  77. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  78. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  79. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  80. package/dist/web/standalone/server.js +1 -1
  81. package/package.json +1 -1
  82. package/packages/mcp-server/README.md +2 -11
  83. package/packages/mcp-server/dist/remote-questions.d.ts +27 -0
  84. package/packages/mcp-server/dist/remote-questions.d.ts.map +1 -1
  85. package/packages/mcp-server/dist/remote-questions.js +28 -0
  86. package/packages/mcp-server/dist/remote-questions.js.map +1 -1
  87. package/packages/mcp-server/dist/server.d.ts +28 -0
  88. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  89. package/packages/mcp-server/dist/server.js +94 -4
  90. package/packages/mcp-server/dist/server.js.map +1 -1
  91. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  92. package/packages/mcp-server/src/mcp-server.test.ts +226 -0
  93. package/packages/mcp-server/src/remote-questions.test.ts +103 -0
  94. package/packages/mcp-server/src/remote-questions.ts +35 -0
  95. package/packages/mcp-server/src/server.ts +129 -6
  96. package/packages/mcp-server/src/workflow-tools.ts +1 -1
  97. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  98. package/src/resources/extensions/gsd/auto/detect-stuck.ts +37 -5
  99. package/src/resources/extensions/gsd/auto/loop.ts +263 -41
  100. package/src/resources/extensions/gsd/auto/phases.ts +15 -7
  101. package/src/resources/extensions/gsd/auto/session.ts +40 -0
  102. package/src/resources/extensions/gsd/auto-dispatch.ts +63 -4
  103. package/src/resources/extensions/gsd/auto-post-unit.ts +27 -12
  104. package/src/resources/extensions/gsd/auto-worktree.ts +218 -225
  105. package/src/resources/extensions/gsd/auto.ts +166 -43
  106. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +1 -1
  107. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +26 -21
  108. package/src/resources/extensions/gsd/bootstrap/tests/write-gate-basepath.test.ts +103 -0
  109. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +80 -55
  110. package/src/resources/extensions/gsd/crash-recovery.ts +177 -43
  111. package/src/resources/extensions/gsd/db/auto-workers.ts +273 -0
  112. package/src/resources/extensions/gsd/db/command-queue.ts +149 -0
  113. package/src/resources/extensions/gsd/db/milestone-leases.ts +274 -0
  114. package/src/resources/extensions/gsd/db/runtime-kv.ts +127 -0
  115. package/src/resources/extensions/gsd/db/unit-dispatches.ts +446 -0
  116. package/src/resources/extensions/gsd/db-writer.ts +113 -17
  117. package/src/resources/extensions/gsd/delegation-policy.ts +197 -0
  118. package/src/resources/extensions/gsd/docs/COORDINATION.md +42 -0
  119. package/src/resources/extensions/gsd/doctor-proactive.ts +4 -0
  120. package/src/resources/extensions/gsd/doctor-runtime-checks.ts +24 -6
  121. package/src/resources/extensions/gsd/doctor.ts +10 -2
  122. package/src/resources/extensions/gsd/gsd-db.ts +354 -3
  123. package/src/resources/extensions/gsd/guided-flow-queue.ts +1 -1
  124. package/src/resources/extensions/gsd/guided-flow.ts +152 -26
  125. package/src/resources/extensions/gsd/interrupted-session.ts +19 -12
  126. package/src/resources/extensions/gsd/metrics.ts +321 -1
  127. package/src/resources/extensions/gsd/paths.ts +67 -8
  128. package/src/resources/extensions/gsd/prompts/complete-slice.md +4 -4
  129. package/src/resources/extensions/gsd/prompts/execute-task.md +3 -3
  130. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +8 -1
  131. package/src/resources/extensions/gsd/prompts/guided-discuss-project.md +22 -7
  132. package/src/resources/extensions/gsd/prompts/guided-discuss-requirements.md +6 -2
  133. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -1
  134. package/src/resources/extensions/gsd/state.ts +44 -6
  135. package/src/resources/extensions/gsd/templates/project.md +10 -0
  136. package/src/resources/extensions/gsd/tests/auto-discuss-milestone-deadlock-4973.test.ts +14 -14
  137. package/src/resources/extensions/gsd/tests/auto-loop-no-copy-artifacts.test.ts +72 -0
  138. package/src/resources/extensions/gsd/tests/auto-loop-symlink-worktree.test.ts +190 -0
  139. package/src/resources/extensions/gsd/tests/auto-session-scope.test.ts +331 -0
  140. package/src/resources/extensions/gsd/tests/auto-workers.test.ts +105 -0
  141. package/src/resources/extensions/gsd/tests/auto-worktree-registry.test.ts +176 -0
  142. package/src/resources/extensions/gsd/tests/command-queue.test.ts +141 -0
  143. package/src/resources/extensions/gsd/tests/crash-recovery-via-db.test.ts +203 -0
  144. package/src/resources/extensions/gsd/tests/crash-recovery.test.ts +169 -59
  145. package/src/resources/extensions/gsd/tests/db-writer-path-containment.test.ts +152 -0
  146. package/src/resources/extensions/gsd/tests/db-writer-root-artifact.test.ts +221 -0
  147. package/src/resources/extensions/gsd/tests/db-writer-scope.test.ts +230 -0
  148. package/src/resources/extensions/gsd/tests/delegation-policy.test.ts +151 -0
  149. package/src/resources/extensions/gsd/tests/detect-stuck-respects-retry.test.ts +173 -0
  150. package/src/resources/extensions/gsd/tests/dispatch-backgroundable-annotation.test.ts +55 -0
  151. package/src/resources/extensions/gsd/tests/draft-promotion.test.ts +3 -23
  152. package/src/resources/extensions/gsd/tests/gate-1b-orphan-discrimination.test.ts +193 -0
  153. package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound-corrections.test.ts +246 -0
  154. package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound.test.ts +218 -0
  155. package/src/resources/extensions/gsd/tests/gsd-db-failed-open-restore.test.ts +117 -0
  156. package/src/resources/extensions/gsd/tests/gsd-db-workspace-scope.test.ts +226 -0
  157. package/src/resources/extensions/gsd/tests/gsd-root-canonical.test.ts +66 -0
  158. package/src/resources/extensions/gsd/tests/gsd-root-home-guard.test.ts +68 -5
  159. package/src/resources/extensions/gsd/tests/guided-flow-prompt-consolidation.test.ts +4 -4
  160. package/src/resources/extensions/gsd/tests/integration/auto-worktree.test.ts +22 -12
  161. package/src/resources/extensions/gsd/tests/integration/doctor-proactive.test.ts +24 -10
  162. package/src/resources/extensions/gsd/tests/integration/doctor-runtime.test.ts +35 -23
  163. package/src/resources/extensions/gsd/tests/integration/workspace-collapse-integration.test.ts +369 -0
  164. package/src/resources/extensions/gsd/tests/interrupted-session-auto.test.ts +72 -25
  165. package/src/resources/extensions/gsd/tests/interrupted-session-ui.test.ts +72 -25
  166. package/src/resources/extensions/gsd/tests/memory-pressure-stuck-state.test.ts +9 -6
  167. package/src/resources/extensions/gsd/tests/metrics-atomic-merge.test.ts +222 -0
  168. package/src/resources/extensions/gsd/tests/metrics-lock-hardening.test.ts +400 -0
  169. package/src/resources/extensions/gsd/tests/metrics-lock-not-acquired.test.ts +141 -0
  170. package/src/resources/extensions/gsd/tests/metrics-lock-retry-sleep.test.ts +287 -0
  171. package/src/resources/extensions/gsd/tests/metrics-prune-cache-invalidation.test.ts +149 -0
  172. package/src/resources/extensions/gsd/tests/metrics-scope.test.ts +378 -0
  173. package/src/resources/extensions/gsd/tests/milestone-leases.test.ts +152 -0
  174. package/src/resources/extensions/gsd/tests/originalbase-path-comparison.test.ts +329 -0
  175. package/src/resources/extensions/gsd/tests/parallel-milestone-isolation.test.ts +106 -0
  176. package/src/resources/extensions/gsd/tests/path-cache-decoupled.test.ts +209 -0
  177. package/src/resources/extensions/gsd/tests/path-normalization-unified.test.ts +175 -0
  178. package/src/resources/extensions/gsd/tests/paths-cache.test.ts +170 -0
  179. package/src/resources/extensions/gsd/tests/paused-session-via-db.test.ts +119 -0
  180. package/src/resources/extensions/gsd/tests/pending-autostart-scope.test.ts +120 -0
  181. package/src/resources/extensions/gsd/tests/pipeline-variant-dispatch.test.ts +58 -0
  182. package/src/resources/extensions/gsd/tests/preferences-worktree-sync.test.ts +3 -17
  183. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +150 -7
  184. package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +138 -16
  185. package/src/resources/extensions/gsd/tests/resume-missing-worktree-warning.test.ts +209 -0
  186. package/src/resources/extensions/gsd/tests/runtime-kv.test.ts +120 -0
  187. package/src/resources/extensions/gsd/tests/skipped-validation-completion.test.ts +133 -28
  188. package/src/resources/extensions/gsd/tests/skipped-validation-db-atomicity.test.ts +17 -0
  189. package/src/resources/extensions/gsd/tests/stuck-state-via-db.test.ts +134 -0
  190. package/src/resources/extensions/gsd/tests/sync-layer-scope.test.ts +434 -0
  191. package/src/resources/extensions/gsd/tests/teardown-chdir-failure-clears-registry.test.ts +162 -0
  192. package/src/resources/extensions/gsd/tests/teardown-cleanup-parity.test.ts +98 -0
  193. package/src/resources/extensions/gsd/tests/teardown-failure-clears-registry.test.ts +186 -0
  194. package/src/resources/extensions/gsd/tests/tool-invocation-error-loop-break.test.ts +1 -1
  195. package/src/resources/extensions/gsd/tests/unit-dispatches.test.ts +247 -0
  196. package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +41 -1
  197. package/src/resources/extensions/gsd/tests/validator-scope-parity.test.ts +239 -0
  198. package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +2 -2
  199. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +9 -15
  200. package/src/resources/extensions/gsd/tests/workspace.test.ts +196 -0
  201. package/src/resources/extensions/gsd/tests/write-gate-predicates.test.ts +35 -35
  202. package/src/resources/extensions/gsd/tests/write-gate.test.ts +94 -71
  203. package/src/resources/extensions/gsd/tests/write-intercept.test.ts +1 -1
  204. package/src/resources/extensions/gsd/workflow-mcp.ts +2 -2
  205. package/src/resources/extensions/gsd/workspace.ts +95 -0
  206. package/src/resources/extensions/gsd/worktree-resolver.ts +78 -2
  207. package/src/resources/extensions/gsd/write-intercept.ts +3 -3
  208. package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +0 -213
  209. package/src/resources/extensions/gsd/tests/auto-stale-lock-self-kill.test.ts +0 -87
  210. package/src/resources/extensions/gsd/tests/stop-auto-remote.test.ts +0 -159
  211. /package/dist/web/standalone/.next/static/{oZGTPvJBQX_IDKKnuV8Bt → Y5UeGFkXTYM9WIQOWHkot}/_buildManifest.js +0 -0
  212. /package/dist/web/standalone/.next/static/{oZGTPvJBQX_IDKKnuV8Bt → Y5UeGFkXTYM9WIQOWHkot}/_ssgManifest.js +0 -0
@@ -8,7 +8,7 @@
8
8
  // Critical invariant: generated markdown must round-trip through
9
9
  // parseDecisionsTable() and parseRequirementsSections() with field fidelity.
10
10
 
11
- import { join, resolve } from 'node:path';
11
+ import { isAbsolute, join, relative, resolve } from 'node:path';
12
12
  import { readFileSync, existsSync, statSync } from 'node:fs';
13
13
  import type { Decision, Requirement } from './types.js';
14
14
  import { resolveGsdRootFile } from './paths.js';
@@ -18,6 +18,8 @@ import { logWarning, logError } from './workflow-logger.js';
18
18
  import { invalidateStateCache } from './state.js';
19
19
  import { clearPathCache } from './paths.js';
20
20
  import { clearParseCache } from './files.js';
21
+ import type { MilestoneScope, GsdWorkspace } from './workspace.js';
22
+ import { createWorkspace, scopeMilestone } from './workspace.js';
21
23
 
22
24
  // ─── Freeform Detection ───────────────────────────────────────────────────
23
25
 
@@ -715,28 +717,104 @@ export interface SaveArtifactOpts {
715
717
  }
716
718
 
717
719
  /**
718
- * Save an artifact to DB and write the corresponding markdown file to disk.
720
+ * Save a root-level artifact (no milestone) to DB and write to disk,
721
+ * routing path construction through workspace.contract.projectGsd directly.
722
+ * Use this instead of saveArtifactToDbByScope when milestone_id is absent.
723
+ */
724
+ export async function saveArtifactToDbForWorkspace(
725
+ workspace: GsdWorkspace,
726
+ opts: SaveArtifactOpts,
727
+ ): Promise<void> {
728
+ try {
729
+ const db = await import('./gsd-db.js');
730
+
731
+ const gsdDir = workspace.contract.projectGsd;
732
+ const fullPath = resolve(gsdDir, opts.path);
733
+
734
+ const rel0 = relative(gsdDir, fullPath);
735
+ if (rel0.startsWith('..') || isAbsolute(rel0)) {
736
+ throw new GSDError(GSD_IO_ERROR, `saveArtifactToDbForWorkspace: path escapes .gsd/ directory: ${opts.path}`);
737
+ }
738
+
739
+ let contentToPersist = opts.content;
740
+ if (opts.artifact_type === 'REQUIREMENTS' && opts.path === 'REQUIREMENTS.md') {
741
+ const activeRequirements = db.getActiveRequirements();
742
+ if (activeRequirements.length === 0) {
743
+ throw new GSDError(GSD_STALE_STATE, 'saveArtifactToDbForWorkspace: REQUIREMENTS final save requires active DB-backed requirements');
744
+ }
745
+ contentToPersist = generateRequirementsMd(activeRequirements);
746
+ }
747
+
748
+ let skipDiskWrite = false;
749
+ if (!isRootCanonicalArtifact(opts) && existsSync(fullPath)) {
750
+ const existingSize = statSync(fullPath).size;
751
+ const newSize = Buffer.byteLength(contentToPersist, 'utf-8');
752
+ if (existingSize > 0 && newSize < existingSize * 0.5) {
753
+ logWarning('projection', `new content (${newSize}B) is <50% of existing projection (${existingSize}B), preserving disk file while DB remains authoritative`, { fn: 'saveArtifactToDbForWorkspace', path: opts.path });
754
+ skipDiskWrite = true;
755
+ }
756
+ }
757
+
758
+ db.insertArtifact({
759
+ path: opts.path,
760
+ artifact_type: opts.artifact_type,
761
+ milestone_id: null,
762
+ slice_id: null,
763
+ task_id: null,
764
+ full_content: contentToPersist,
765
+ });
766
+
767
+ if (!skipDiskWrite) {
768
+ try {
769
+ await saveFile(fullPath, contentToPersist);
770
+ } catch (diskErr) {
771
+ logWarning('projection', 'artifact projection write failed; DB artifact remains committed', { fn: 'saveArtifactToDbForWorkspace', path: opts.path, error: String((diskErr as Error).message) });
772
+ }
773
+ }
774
+ invalidateStateCache();
775
+ clearPathCache();
776
+ clearParseCache();
777
+ } catch (err) {
778
+ logError('manifest', 'saveArtifactToDbForWorkspace failed', { fn: 'saveArtifactToDbForWorkspace', error: String((err as Error).message) });
779
+ throw err;
780
+ }
781
+ }
782
+
783
+ /**
784
+ * Save an artifact to DB and write the corresponding markdown file to disk,
785
+ * routing all path construction through the workspace contract.
786
+ *
719
787
  * The path is relative to .gsd/ (e.g. "milestones/M001/slices/S06/tasks/T01-SUMMARY.md").
720
- * The full file path is computed as basePath + '.gsd/' + path.
788
+ * The full file path is computed as scope.workspace.contract.projectGsd + '/' + path.
721
789
  */
722
- export async function saveArtifactToDb(
790
+ export async function saveArtifactToDbByScope(
791
+ scope: MilestoneScope,
723
792
  opts: SaveArtifactOpts,
724
- basePath: string,
725
793
  ): Promise<void> {
794
+ // Guard: an empty milestoneId produces malformed paths (milestoneDir = join(gsd, "milestones", "")).
795
+ // Callers that have no milestone should use saveArtifactToDbForWorkspace instead.
796
+ if (!scope.milestoneId) {
797
+ throw new GSDError(GSD_IO_ERROR, `saveArtifactToDbByScope: milestoneId is empty — use saveArtifactToDbForWorkspace for root artifacts`);
798
+ }
799
+
726
800
  try {
727
801
  const db = await import('./gsd-db.js');
728
802
 
803
+ // Use contract.projectGsd as the canonical .gsd directory — never a hand-rolled basePath join.
804
+ const gsdDir = scope.workspace.contract.projectGsd;
805
+ const fullPath = resolve(gsdDir, opts.path);
806
+
729
807
  // Guard against path traversal before any reads/writes
730
- const gsdDir = resolve(basePath, '.gsd');
731
- const fullPath = resolve(basePath, '.gsd', opts.path);
732
- if (!fullPath.startsWith(gsdDir)) {
733
- throw new GSDError(GSD_IO_ERROR, `saveArtifactToDb: path escapes .gsd/ directory: ${opts.path}`);
808
+ const rel1 = relative(gsdDir, fullPath);
809
+ if (rel1.startsWith('..') || isAbsolute(rel1)) {
810
+ throw new GSDError(GSD_IO_ERROR, `saveArtifactToDbByScope: path escapes .gsd/ directory: ${opts.path}`);
734
811
  }
812
+
735
813
  let contentToPersist = opts.content;
736
814
  if (opts.artifact_type === 'REQUIREMENTS' && opts.path === 'REQUIREMENTS.md') {
737
815
  const activeRequirements = db.getActiveRequirements();
738
816
  if (activeRequirements.length === 0) {
739
- throw new GSDError(GSD_STALE_STATE, 'saveArtifactToDb: REQUIREMENTS final save requires active DB-backed requirements');
817
+ throw new GSDError(GSD_STALE_STATE, 'saveArtifactToDbByScope: REQUIREMENTS final save requires active DB-backed requirements');
740
818
  }
741
819
  contentToPersist = generateRequirementsMd(activeRequirements);
742
820
  }
@@ -744,16 +822,13 @@ export async function saveArtifactToDb(
744
822
  // Shrinkage guard: if the projection file already exists and the new
745
823
  // content is significantly smaller (<50%), preserve the richer file on
746
824
  // 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.
825
+ // Root canonical artifacts are exempt (rendered from canonical DB state).
751
826
  let skipDiskWrite = false;
752
827
  if (!isRootCanonicalArtifact(opts) && existsSync(fullPath)) {
753
828
  const existingSize = statSync(fullPath).size;
754
829
  const newSize = Buffer.byteLength(contentToPersist, 'utf-8');
755
830
  if (existingSize > 0 && newSize < existingSize * 0.5) {
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 });
831
+ logWarning('projection', `new content (${newSize}B) is <50% of existing projection (${existingSize}B), preserving disk file while DB remains authoritative`, { fn: 'saveArtifactToDbByScope', path: opts.path });
757
832
  skipDiskWrite = true;
758
833
  }
759
834
  }
@@ -772,7 +847,7 @@ export async function saveArtifactToDb(
772
847
  try {
773
848
  await saveFile(fullPath, contentToPersist);
774
849
  } catch (diskErr) {
775
- logWarning('projection', 'artifact projection write failed; DB artifact remains committed', { fn: 'saveArtifactToDb', path: opts.path, error: String((diskErr as Error).message) });
850
+ logWarning('projection', 'artifact projection write failed; DB artifact remains committed', { fn: 'saveArtifactToDbByScope', path: opts.path, error: String((diskErr as Error).message) });
776
851
  }
777
852
  }
778
853
  // Invalidate file-read caches so deriveState() sees the updated markdown.
@@ -781,7 +856,28 @@ export async function saveArtifactToDb(
781
856
  clearPathCache();
782
857
  clearParseCache();
783
858
  } catch (err) {
784
- logError('manifest', 'saveArtifactToDb failed', { fn: 'saveArtifactToDb', error: String((err as Error).message) });
859
+ logError('manifest', 'saveArtifactToDbByScope failed', { fn: 'saveArtifactToDbByScope', error: String((err as Error).message) });
785
860
  throw err;
786
861
  }
787
862
  }
863
+
864
+ /**
865
+ * Save an artifact to DB and write the corresponding markdown file to disk.
866
+ * The path is relative to .gsd/ (e.g. "milestones/M001/slices/S06/tasks/T01-SUMMARY.md").
867
+ * The full file path is computed as basePath + '.gsd/' + path.
868
+ *
869
+ * @deprecated Use saveArtifactToDbByScope instead, which routes through the
870
+ * workspace contract for canonical path resolution.
871
+ * TODO(C-future): remove this legacy wrapper once all callers are migrated.
872
+ */
873
+ export async function saveArtifactToDb(
874
+ opts: SaveArtifactOpts,
875
+ basePath: string,
876
+ ): Promise<void> {
877
+ const workspace = createWorkspace(basePath);
878
+ const milestoneId = opts.milestone_id;
879
+ if (milestoneId) {
880
+ return saveArtifactToDbByScope(scopeMilestone(workspace, milestoneId), opts);
881
+ }
882
+ return saveArtifactToDbForWorkspace(workspace, opts);
883
+ }
@@ -0,0 +1,197 @@
1
+ // Delegation policy — codifies which GSD MCP tools are safe to run as
2
+ // background sub-agents while the foreground /gsd flow continues. Verdicts
3
+ // are derived from the round-1 and round-2 evaluations recorded in this
4
+ // branch's PR description; the rationale field on each entry preserves
5
+ // the reason so future changes have to revisit the analysis explicitly.
6
+ //
7
+ // Default-deny: unknown tools are never backgroundable.
8
+ //
9
+ // ─── Tool-name vs unit-type namespaces ───────────────────────────────────
10
+ // Entries are keyed by canonical MCP tool name (`gsd_*`). The optional
11
+ // `unitType` field is a *secondary* index for the dispatcher's convenience
12
+ // — it bridges this policy to `auto-dispatch.ts`' `DispatchAction.unitType`
13
+ // values. The two namespaces are not 1:1:
14
+ //
15
+ // - Some tools have no corresponding unit type (e.g. `gsd_doctor`,
16
+ // `gsd_plan_task`) and intentionally omit `unitType`.
17
+ // - Some unit types share a tool — e.g. `execute-task`, `execute-task-simple`,
18
+ // and `reactive-execute` all invoke `gsd_execute`. The current shape
19
+ // allows only one `unitType` per entry, so those units fall through to
20
+ // `getVerdictByUnitType() === null` (→ `backgroundable: false`) even
21
+ // though `gsd_execute` itself is GOOD. This is the intended default-deny
22
+ // posture until a future PR wires actual background dispatch and
23
+ // decides whether each unit-level orchestration is safe — the unit
24
+ // wraps a prompt, harness setup, and post-processing on top of the
25
+ // tool, and the tool's safety doesn't transfer automatically.
26
+ //
27
+ // Auto-dispatch produces 20 distinct unit types; only 5 are explicitly
28
+ // classified here. The other 15 default-deny:
29
+ // complete-milestone, complete-slice, discuss-milestone, discuss-project,
30
+ // discuss-requirements, execute-task, execute-task-simple, gate-evaluate,
31
+ // reactive-execute, refine-slice, research-decision, research-milestone,
32
+ // research-project, research-slice, rewrite-docs, run-uat
33
+ //
34
+ // Adding a `unitType` mapping (or a future `unitTypes: string[]`) to an
35
+ // existing entry is the place to lift any of these out of default-deny
36
+ // when the analysis has been done.
37
+
38
+ export type BackgroundabilityVerdict = "good" | "risky" | "no";
39
+
40
+ export interface DelegationPolicyEntry {
41
+ /** Canonical MCP tool name (the verb_object form, e.g. `gsd_plan_slice`). */
42
+ toolName: string;
43
+ /** Workflow unit type from auto-dispatch.ts, when one exists. */
44
+ unitType?: string;
45
+ verdict: BackgroundabilityVerdict;
46
+ /** One-line justification grounded in the evaluation findings. */
47
+ rationale: string;
48
+ /**
49
+ * Constraints the caller MUST satisfy when dispatching this unit in the
50
+ * background. Only populated for `good` and conditional `risky` entries.
51
+ */
52
+ constraints?: string[];
53
+ }
54
+
55
+ const POLICY: Record<string, DelegationPolicyEntry> = {
56
+ gsd_plan_slice: {
57
+ toolName: "gsd_plan_slice",
58
+ unitType: "plan-slice",
59
+ verdict: "good",
60
+ rationale:
61
+ "Self-contained, no user prompts, atomic DB tx; existing slice-parallel-orchestrator pattern transfers cleanly.",
62
+ constraints: [
63
+ "Lock the slice from further user discussion once dispatched (context is frozen at dispatch time).",
64
+ "Foreground must not derive state for that slice while the transaction is in flight.",
65
+ "Foreground must await background completion before any tool reads the planned tasks/gates.",
66
+ ],
67
+ },
68
+ gsd_execute: {
69
+ toolName: "gsd_execute",
70
+ // No `unitType` set on purpose — the underlying tool is safe, but the
71
+ // unit-level orchestrations that invoke it (`execute-task`,
72
+ // `execute-task-simple`, `reactive-execute`) wrap additional prompt and
73
+ // harness work whose safety is a separate analysis. Default-deny those
74
+ // units until that analysis is recorded; adding `unitType` here would
75
+ // promote them silently.
76
+ verdict: "good",
77
+ rationale:
78
+ "No DB writes; UUID-isolated stdout/stderr/meta files; existing reactive-execute parallel-subagent precedent.",
79
+ },
80
+ gsd_validate_milestone: {
81
+ toolName: "gsd_validate_milestone",
82
+ unitType: "validate-milestone",
83
+ verdict: "good",
84
+ rationale:
85
+ "Verdict pre-computed by parallel reviewers; atomic DB tx plus isolated VALIDATION.md write; no user interaction.",
86
+ },
87
+ gsd_reassess_roadmap: {
88
+ toolName: "gsd_reassess_roadmap",
89
+ unitType: "reassess-roadmap",
90
+ verdict: "good",
91
+ rationale:
92
+ "Narrower mutation scope than plan_milestone; structural guards prevent modification of completed slices.",
93
+ },
94
+ gsd_doctor: {
95
+ toolName: "gsd_doctor",
96
+ verdict: "risky",
97
+ rationale:
98
+ "Diagnostic-only mode (fix=false) is safe to background; fix=true writes STATE.md/ROADMAP.md without session-lock coordination and can race the foreground flow.",
99
+ constraints: [
100
+ "Background only with fix=false (diagnostic-only).",
101
+ "Apply fixes synchronously, only when no foreground unit is dispatched.",
102
+ ],
103
+ },
104
+ gsd_plan_milestone: {
105
+ toolName: "gsd_plan_milestone",
106
+ unitType: "plan-milestone",
107
+ verdict: "risky",
108
+ rationale:
109
+ "Inputs require CONTEXT.md from discuss-milestone, so initial questioning is already done by the time it can start; TOCTOU guards and projection coherence make concurrency unsafe.",
110
+ },
111
+ gsd_replan_slice: {
112
+ toolName: "gsd_replan_slice",
113
+ unitType: "replan-slice",
114
+ verdict: "risky",
115
+ rationale:
116
+ "Blocks the replanning→executing state transition on a gate that waits for S##-REPLAN.md; background failure leaves the flow stuck.",
117
+ },
118
+ gsd_plan_task: {
119
+ toolName: "gsd_plan_task",
120
+ verdict: "no",
121
+ rationale:
122
+ "plan-slice prompt explicitly forbids calling gsd_plan_task separately; per-task granularity multiplies manifest writes and projection re-renders with no payoff.",
123
+ },
124
+ };
125
+
126
+ // Alias map keyed on the secondary name; resolves to the canonical entry above.
127
+ // Sourced from packages/mcp-server/src/workflow-tools.ts alias registrations
128
+ // (gsd_milestone_validate, gsd_roadmap_reassess, gsd_slice_replan, gsd_task_plan).
129
+ const ALIASES: Record<string, string> = {
130
+ gsd_milestone_validate: "gsd_validate_milestone",
131
+ gsd_roadmap_reassess: "gsd_reassess_roadmap",
132
+ gsd_slice_replan: "gsd_replan_slice",
133
+ gsd_task_plan: "gsd_plan_task",
134
+ };
135
+
136
+ function resolveCanonical(name: string): string {
137
+ return ALIASES[name] ?? name;
138
+ }
139
+
140
+ export function getDelegationVerdict(toolName: string): DelegationPolicyEntry | null {
141
+ return POLICY[resolveCanonical(toolName)] ?? null;
142
+ }
143
+
144
+ export function isBackgroundable(toolName: string): boolean {
145
+ const entry = getDelegationVerdict(toolName);
146
+ return entry?.verdict === "good";
147
+ }
148
+
149
+ export function listBackgroundableTools(): string[] {
150
+ return Object.values(POLICY)
151
+ .filter((entry) => entry.verdict === "good")
152
+ .map((entry) => entry.toolName)
153
+ .sort();
154
+ }
155
+
156
+ export function getVerdictByUnitType(unitType: string): DelegationPolicyEntry | null {
157
+ for (const entry of Object.values(POLICY)) {
158
+ if (entry.unitType === unitType) return entry;
159
+ }
160
+ return null;
161
+ }
162
+
163
+ /**
164
+ * Minimal shape of a dispatch action that the annotator needs to operate on.
165
+ * Matches the `dispatch` and non-dispatch variants of auto-dispatch.ts'
166
+ * DispatchAction without depending on it (so this module stays free of
167
+ * workspace-package transitive imports).
168
+ */
169
+ export type AnnotatableDispatchAction =
170
+ | { action: "dispatch"; unitType: string; backgroundable?: boolean; [k: string]: unknown }
171
+ | { action: "stop"; [k: string]: unknown }
172
+ | { action: "skip"; [k: string]: unknown };
173
+
174
+ /**
175
+ * Annotates a dispatch action in place with `backgroundable: true` when its
176
+ * unitType has a `good` verdict in the policy. Stop/skip actions pass through
177
+ * unchanged. Default-deny: unknown unit types resolve to `false`.
178
+ *
179
+ * **Mutation contract.** The `backgroundable` field is written directly onto
180
+ * the passed action object. This is intentional — every dispatch path in
181
+ * `auto-dispatch.ts` constructs a fresh action object per `where(ctx)` /
182
+ * `evaluateDispatch(ctx)` invocation, so in-place mutation cannot leak across
183
+ * dispatch cycles. Future dispatch rules MUST follow that convention: never
184
+ * cache or share `DispatchAction` objects across calls. If you need to cache,
185
+ * either freeze the cached object (`Object.freeze`) and clone on read, or
186
+ * stop calling `annotateBackgroundable` on the shared instance. The annotator
187
+ * always recomputes from the policy on every call (no internal cache), so
188
+ * repeated invocations on the same object will overwrite stale values
189
+ * deterministically — see the `annotateBackgroundable recomputes on each call`
190
+ * test for the contract pin.
191
+ */
192
+ export function annotateBackgroundable<T extends AnnotatableDispatchAction>(action: T): T {
193
+ if (action.action !== "dispatch") return action;
194
+ const verdict = getVerdictByUnitType(action.unitType);
195
+ action.backgroundable = verdict?.verdict === "good";
196
+ return action;
197
+ }
@@ -0,0 +1,42 @@
1
+ # Auto-mode coordination is single-host
2
+
3
+ The DB-backed coordination tables introduced by Phase B (`workers`,
4
+ `milestone_leases`, `unit_dispatches`, `cancellation_requests`,
5
+ `command_queue`) and the supporting `runtime_kv` table from Phase C all
6
+ rely on **shared SQLite WAL on local disk**. They do not work across
7
+ machines.
8
+
9
+ ## Why single-host only
10
+
11
+ - SQLite WAL coordination — the locking primitives that make
12
+ `claimMilestoneLease`, `recordDispatchClaim`, and `claimNextCommand`
13
+ atomic — is local-disk only. Network filesystems (NFS, SMB, S3FS) and
14
+ fuse mounts break the lock semantics that the WAL relies on.
15
+ - Heartbeat TTL (`workers.last_heartbeat_at`) compares timestamps written
16
+ with SQLite wall-clock time (`datetime('now')`). Across machines without
17
+ wall-clock synchronization (for example NTP/chrony), TTL filtering can
18
+ produce phantom-active or premature-crashed verdicts. Monotonic clocks
19
+ are not used for these comparisons.
20
+ - Fencing tokens (`milestone_leases.fencing_token`) are monotonically
21
+ ordered by SQL within a single transaction. Cross-host races could
22
+ produce duplicate tokens if two SQLite processes opened the same DB
23
+ on a network mount.
24
+
25
+ ## What does work
26
+
27
+ - Multiple `gsd auto` worker processes on the **same machine**, sharing
28
+ the project's SQLite DB via WAL. The lease check refuses concurrent
29
+ claims on the same milestone; the dispatch ledger's partial unique
30
+ index refuses double-claims of the same unit.
31
+ - A single `gsd auto` worker plus arbitrary read-only consumers
32
+ (dashboards, doctors) on the same machine.
33
+ - Worktree-based parallelism on the same machine, where each worker
34
+ holds a different milestone lease.
35
+
36
+ ## Multi-host alternatives
37
+
38
+ If you need to coordinate `gsd auto` workers across machines, you need
39
+ a real coordinator: Postgres for the ledger + a leader-election service
40
+ (etcd, Consul) for the leases. That's out of scope for these phases.
41
+ The schema and module shapes here would need a non-trivial backend
42
+ swap before they could ride on top of either.
@@ -25,6 +25,7 @@ import { resolveMilestoneIntegrationBranch } from "./git-service.js";
25
25
  import { nativeIsRepo, nativeHasChanges, nativeLastCommitEpoch, nativeGetCurrentBranch, nativeAddTracked, nativeCommit } from "./native-git-bridge.js";
26
26
  import { loadEffectiveGSDPreferences } from "./preferences.js";
27
27
  import { runEnvironmentChecks } from "./doctor-environment.js";
28
+ import { ensureDbOpen } from "./bootstrap/dynamic-tools.js";
28
29
 
29
30
  // ── Health Score Tracking ──────────────────────────────────────────────────
30
31
 
@@ -219,6 +220,9 @@ export async function preDispatchHealthGate(basePath: string): Promise<PreDispat
219
220
  // If a stale lock exists, the crash recovery path should handle it,
220
221
  // not a new dispatch. This prevents double-dispatch after crashes.
221
222
  try {
223
+ if (existsSync(join(gsdRoot(basePath), "gsd.db"))) {
224
+ await ensureDbOpen(basePath);
225
+ }
222
226
  const lock = readCrashLock(basePath);
223
227
  if (lock && !isLockProcessAlive(lock)) {
224
228
  // Auto-clear it since we're about to dispatch anyway
@@ -8,6 +8,8 @@ import { deriveState, isGhostMilestone, isReusableGhostMilestone } from "./state
8
8
  import { saveFile } from "./files.js";
9
9
  import { nativeIsRepo, nativeForEachRef, nativeUpdateRef } from "./native-git-bridge.js";
10
10
  import { readCrashLock, isLockProcessAlive, clearLock } from "./crash-recovery.js";
11
+ import { getActiveAutoWorkers } from "./db/auto-workers.js";
12
+ import { normalizeRealPath } from "./paths.js";
11
13
  import { ensureGitignore, isGsdGitignored } from "./gitignore.js";
12
14
  import { readAllSessionStatuses, isSessionStale, removeSessionStatus } from "./session-status-io.js";
13
15
  import { recoverFailedMigration } from "./migrate-external.js";
@@ -35,6 +37,9 @@ export async function checkRuntimeHealth(
35
37
  const root = gsdRoot(basePath);
36
38
 
37
39
  // ── Stale crash lock ──────────────────────────────────────────────────
40
+ // Phase C pt 2: the lock state lives in the workers + unit_dispatches
41
+ // tables now, not auto.lock. readCrashLock synthesizes a LockData from
42
+ // the DB; isLockProcessAlive is a pure OS PID check.
38
43
  try {
39
44
  const lock = readCrashLock(basePath);
40
45
  if (lock) {
@@ -45,14 +50,14 @@ export async function checkRuntimeHealth(
45
50
  code: "stale_crash_lock",
46
51
  scope: "project",
47
52
  unitId: "project",
48
- message: `Stale auto.lock from PID ${lock.pid} (started ${lock.startedAt}, was executing ${lock.unitType} ${lock.unitId}) — process is no longer running`,
49
- file: ".gsd/auto.lock",
53
+ message: `Stale auto-mode worker (PID ${lock.pid}, started ${lock.startedAt}, was executing ${lock.unitType} ${lock.unitId}) — process is no longer running`,
54
+ file: "<workers table>",
50
55
  fixable: true,
51
56
  });
52
57
 
53
58
  if (shouldFix("stale_crash_lock")) {
54
59
  clearLock(basePath);
55
- fixesApplied.push("cleared stale auto.lock");
60
+ fixesApplied.push("cleared stale auto-mode worker state");
56
61
  }
57
62
  }
58
63
  }
@@ -70,9 +75,22 @@ export async function checkRuntimeHealth(
70
75
  if (existsSync(lockDir)) {
71
76
  const statRes = statSync(lockDir);
72
77
  if (statRes.isDirectory()) {
73
- // Check if any live process actually holds this lock
74
- const lock = readCrashLock(basePath);
75
- const lockHolderAlive = lock ? isLockProcessAlive(lock) : false;
78
+ // Phase C pt 2: "any live process holds the lock?" check now means
79
+ // "is any worker registered with status='active' AND a fresh
80
+ // heartbeat for this project?" readCrashLock returns null for
81
+ // healthy live workers (it surfaces stale ones only), so we must
82
+ // consult getActiveAutoWorkers directly.
83
+ const projectRoot = normalizeRealPath(basePath);
84
+ const activeWorkers = getActiveAutoWorkers().filter(
85
+ (w) => w.project_root_realpath === projectRoot && isLockProcessAlive({
86
+ pid: w.pid,
87
+ startedAt: w.started_at,
88
+ unitType: "starting",
89
+ unitId: "bootstrap",
90
+ unitStartedAt: w.started_at,
91
+ }),
92
+ );
93
+ const lockHolderAlive = activeWorkers.length > 0;
76
94
  if (!lockHolderAlive) {
77
95
  issues.push({
78
96
  severity: "error",
@@ -3,8 +3,8 @@ import { join } from "node:path";
3
3
 
4
4
  import { loadFile, parseSummary, saveFile, parseTaskPlanMustHaves, countMustHavesMentionedInSummary } from "./files.js";
5
5
  import { parseRoadmap as parseLegacyRoadmap, parsePlan as parseLegacyPlan } from "./parsers-legacy.js";
6
- import { isDbAvailable, getMilestoneSlices, getSliceTasks } from "./gsd-db.js";
7
- import { resolveMilestoneFile, resolveMilestonePath, resolveSliceFile, resolveSlicePath, resolveTaskFile, resolveTasksDir, milestonesDir, gsdRoot, relMilestoneFile, relSliceFile, relTaskFile, relSlicePath, relGsdRootFile, resolveGsdRootFile, relMilestonePath } from "./paths.js";
6
+ import { isDbAvailable, openDatabase, getMilestoneSlices, getSliceTasks } from "./gsd-db.js";
7
+ import { resolveMilestoneFile, resolveMilestonePath, resolveSliceFile, resolveSlicePath, resolveTaskFile, resolveTasksDir, milestonesDir, gsdRoot, relMilestoneFile, relSliceFile, relTaskFile, relSlicePath, relGsdRootFile, resolveGsdRootFile, relMilestonePath, resolveGsdPathContract } from "./paths.js";
8
8
  import { deriveState, isMilestoneComplete } from "./state.js";
9
9
  import { invalidateAllCaches } from "./cache.js";
10
10
  import { loadEffectiveGSDPreferences, type GSDPreferences } from "./preferences.js";
@@ -336,6 +336,14 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
336
336
  const dryRun = options?.dryRun === true;
337
337
  const fixLevel = options?.fixLevel ?? "all";
338
338
 
339
+ // CLI doctor can run before any tool handler has opened the DB. Runtime
340
+ // health checks need the existing project DB to surface DB-backed crash
341
+ // locks, paused sessions, and coordination rows.
342
+ const dbPath = resolveGsdPathContract(basePath).projectDb;
343
+ if (existsSync(dbPath)) {
344
+ try { openDatabase(dbPath); } catch { /* surfaced later as db_unavailable */ }
345
+ }
346
+
339
347
  // Issue codes that represent completion state transitions — creating summary
340
348
  // stubs, marking slices/milestones done in the roadmap. These belong to the
341
349
  // dispatch lifecycle (complete-slice, complete-milestone units), not to