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

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 (155) hide show
  1. package/dist/resources/.managed-resources-content-hash +1 -1
  2. package/dist/resources/extensions/gsd/auto/phases.js +7 -2
  3. package/dist/resources/extensions/gsd/auto/session.js +3 -0
  4. package/dist/resources/extensions/gsd/auto-dispatch.js +3 -2
  5. package/dist/resources/extensions/gsd/auto-post-unit.js +7 -1
  6. package/dist/resources/extensions/gsd/auto-worktree.js +185 -40
  7. package/dist/resources/extensions/gsd/auto.js +62 -1
  8. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +1 -1
  9. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +17 -16
  10. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +67 -55
  11. package/dist/resources/extensions/gsd/db-writer.js +96 -16
  12. package/dist/resources/extensions/gsd/delegation-policy.js +155 -0
  13. package/dist/resources/extensions/gsd/gsd-db.js +194 -0
  14. package/dist/resources/extensions/gsd/guided-flow-queue.js +1 -1
  15. package/dist/resources/extensions/gsd/guided-flow.js +117 -25
  16. package/dist/resources/extensions/gsd/metrics.js +287 -1
  17. package/dist/resources/extensions/gsd/paths.js +79 -8
  18. package/dist/resources/extensions/gsd/prompts/complete-slice.md +4 -4
  19. package/dist/resources/extensions/gsd/prompts/execute-task.md +3 -3
  20. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +8 -1
  21. package/dist/resources/extensions/gsd/prompts/guided-discuss-project.md +22 -7
  22. package/dist/resources/extensions/gsd/prompts/guided-discuss-requirements.md +6 -2
  23. package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -1
  24. package/dist/resources/extensions/gsd/templates/project.md +10 -0
  25. package/dist/resources/extensions/gsd/workflow-mcp.js +2 -2
  26. package/dist/resources/extensions/gsd/workspace.js +59 -0
  27. package/dist/resources/extensions/gsd/worktree-resolver.js +15 -2
  28. package/dist/resources/extensions/gsd/write-intercept.js +3 -3
  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 +10 -10
  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/required-server-files.json +1 -1
  35. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  36. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  44. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/index.html +1 -1
  52. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app-paths-manifest.json +10 -10
  59. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  60. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  61. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  62. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  63. package/dist/web/standalone/server.js +1 -1
  64. package/package.json +1 -1
  65. package/packages/mcp-server/README.md +2 -11
  66. package/packages/mcp-server/dist/remote-questions.d.ts +27 -0
  67. package/packages/mcp-server/dist/remote-questions.d.ts.map +1 -1
  68. package/packages/mcp-server/dist/remote-questions.js +28 -0
  69. package/packages/mcp-server/dist/remote-questions.js.map +1 -1
  70. package/packages/mcp-server/dist/server.d.ts +28 -0
  71. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  72. package/packages/mcp-server/dist/server.js +94 -4
  73. package/packages/mcp-server/dist/server.js.map +1 -1
  74. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  75. package/packages/mcp-server/src/mcp-server.test.ts +226 -0
  76. package/packages/mcp-server/src/remote-questions.test.ts +103 -0
  77. package/packages/mcp-server/src/remote-questions.ts +35 -0
  78. package/packages/mcp-server/src/server.ts +129 -6
  79. package/packages/mcp-server/src/workflow-tools.ts +1 -1
  80. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  81. package/src/resources/extensions/gsd/auto/phases.ts +8 -2
  82. package/src/resources/extensions/gsd/auto/session.ts +4 -0
  83. package/src/resources/extensions/gsd/auto-dispatch.ts +10 -2
  84. package/src/resources/extensions/gsd/auto-post-unit.ts +8 -1
  85. package/src/resources/extensions/gsd/auto-worktree.ts +225 -47
  86. package/src/resources/extensions/gsd/auto.ts +79 -1
  87. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +1 -1
  88. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +17 -17
  89. package/src/resources/extensions/gsd/bootstrap/tests/write-gate-basepath.test.ts +103 -0
  90. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +80 -55
  91. package/src/resources/extensions/gsd/db-writer.ts +113 -17
  92. package/src/resources/extensions/gsd/delegation-policy.ts +197 -0
  93. package/src/resources/extensions/gsd/gsd-db.ts +184 -0
  94. package/src/resources/extensions/gsd/guided-flow-queue.ts +1 -1
  95. package/src/resources/extensions/gsd/guided-flow.ts +154 -25
  96. package/src/resources/extensions/gsd/metrics.ts +321 -1
  97. package/src/resources/extensions/gsd/paths.ts +67 -8
  98. package/src/resources/extensions/gsd/prompts/complete-slice.md +4 -4
  99. package/src/resources/extensions/gsd/prompts/execute-task.md +3 -3
  100. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +8 -1
  101. package/src/resources/extensions/gsd/prompts/guided-discuss-project.md +22 -7
  102. package/src/resources/extensions/gsd/prompts/guided-discuss-requirements.md +6 -2
  103. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -1
  104. package/src/resources/extensions/gsd/templates/project.md +10 -0
  105. package/src/resources/extensions/gsd/tests/auto-discuss-milestone-deadlock-4973.test.ts +14 -14
  106. package/src/resources/extensions/gsd/tests/auto-session-scope.test.ts +331 -0
  107. package/src/resources/extensions/gsd/tests/auto-worktree-registry.test.ts +176 -0
  108. package/src/resources/extensions/gsd/tests/db-writer-path-containment.test.ts +152 -0
  109. package/src/resources/extensions/gsd/tests/db-writer-root-artifact.test.ts +221 -0
  110. package/src/resources/extensions/gsd/tests/db-writer-scope.test.ts +230 -0
  111. package/src/resources/extensions/gsd/tests/delegation-policy.test.ts +151 -0
  112. package/src/resources/extensions/gsd/tests/dispatch-backgroundable-annotation.test.ts +55 -0
  113. package/src/resources/extensions/gsd/tests/draft-promotion.test.ts +3 -23
  114. package/src/resources/extensions/gsd/tests/gate-1b-orphan-discrimination.test.ts +193 -0
  115. package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound-corrections.test.ts +246 -0
  116. package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound.test.ts +218 -0
  117. package/src/resources/extensions/gsd/tests/gsd-db-failed-open-restore.test.ts +117 -0
  118. package/src/resources/extensions/gsd/tests/gsd-db-workspace-scope.test.ts +226 -0
  119. package/src/resources/extensions/gsd/tests/gsd-root-canonical.test.ts +66 -0
  120. package/src/resources/extensions/gsd/tests/gsd-root-home-guard.test.ts +68 -5
  121. package/src/resources/extensions/gsd/tests/guided-flow-prompt-consolidation.test.ts +4 -4
  122. package/src/resources/extensions/gsd/tests/integration/workspace-collapse-integration.test.ts +371 -0
  123. package/src/resources/extensions/gsd/tests/metrics-atomic-merge.test.ts +222 -0
  124. package/src/resources/extensions/gsd/tests/metrics-lock-hardening.test.ts +400 -0
  125. package/src/resources/extensions/gsd/tests/metrics-lock-not-acquired.test.ts +141 -0
  126. package/src/resources/extensions/gsd/tests/metrics-lock-retry-sleep.test.ts +287 -0
  127. package/src/resources/extensions/gsd/tests/metrics-prune-cache-invalidation.test.ts +149 -0
  128. package/src/resources/extensions/gsd/tests/metrics-scope.test.ts +378 -0
  129. package/src/resources/extensions/gsd/tests/originalbase-path-comparison.test.ts +329 -0
  130. package/src/resources/extensions/gsd/tests/path-cache-decoupled.test.ts +209 -0
  131. package/src/resources/extensions/gsd/tests/path-normalization-unified.test.ts +175 -0
  132. package/src/resources/extensions/gsd/tests/paths-cache.test.ts +170 -0
  133. package/src/resources/extensions/gsd/tests/pending-autostart-scope.test.ts +120 -0
  134. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +150 -7
  135. package/src/resources/extensions/gsd/tests/ready-phrase-no-files-4573.test.ts +74 -0
  136. package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +28 -16
  137. package/src/resources/extensions/gsd/tests/resume-missing-worktree-warning.test.ts +209 -0
  138. package/src/resources/extensions/gsd/tests/sync-layer-scope.test.ts +453 -0
  139. package/src/resources/extensions/gsd/tests/teardown-chdir-failure-clears-registry.test.ts +162 -0
  140. package/src/resources/extensions/gsd/tests/teardown-cleanup-parity.test.ts +102 -0
  141. package/src/resources/extensions/gsd/tests/teardown-failure-clears-registry.test.ts +186 -0
  142. package/src/resources/extensions/gsd/tests/tool-invocation-error-loop-break.test.ts +1 -1
  143. package/src/resources/extensions/gsd/tests/validator-scope-parity.test.ts +239 -0
  144. package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +2 -2
  145. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +9 -15
  146. package/src/resources/extensions/gsd/tests/workspace.test.ts +190 -0
  147. package/src/resources/extensions/gsd/tests/write-gate-predicates.test.ts +35 -35
  148. package/src/resources/extensions/gsd/tests/write-gate.test.ts +67 -52
  149. package/src/resources/extensions/gsd/tests/write-intercept.test.ts +1 -1
  150. package/src/resources/extensions/gsd/workflow-mcp.ts +2 -2
  151. package/src/resources/extensions/gsd/workspace.ts +95 -0
  152. package/src/resources/extensions/gsd/worktree-resolver.ts +16 -2
  153. package/src/resources/extensions/gsd/write-intercept.ts +3 -3
  154. /package/dist/web/standalone/.next/static/{HahrZrc_Xn4wumj0O1Ydp → AT5qi39nKXkdmQIOIoh0f}/_buildManifest.js +0 -0
  155. /package/dist/web/standalone/.next/static/{HahrZrc_Xn4wumj0O1Ydp → AT5qi39nKXkdmQIOIoh0f}/_ssgManifest.js +0 -0
@@ -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
+ }
@@ -25,6 +25,7 @@ import { existsSync, copyFileSync, mkdirSync, realpathSync } from "node:fs";
25
25
  import { dirname } from "node:path";
26
26
  import type { Decision, Requirement, GateRow, GateId, GateScope, GateStatus, GateVerdict } from "./types.js";
27
27
  import { GSDError, GSD_STALE_STATE } from "./errors.js";
28
+ import type { GsdWorkspace, MilestoneScope } from "./workspace.js";
28
29
  import { getGateIdsForTurn, type OwnerTurn } from "./gate-registry.js";
29
30
  import { logError, logWarning } from "./workflow-logger.js";
30
31
  // Type-only import to avoid a circular runtime dep. The runtime side of
@@ -1259,6 +1260,182 @@ let _exitHandlerRegistered = false;
1259
1260
  let _dbOpenAttempted = false;
1260
1261
  let _lastDbError: Error | null = null;
1261
1262
  let _lastDbPhase: "open" | "initSchema" | "vacuum-recovery" | null = null;
1263
+ /**
1264
+ * Identity key of the workspace whose connection is currently active
1265
+ * (currentDb). Set by openDatabaseByWorkspace(); null when the active
1266
+ * connection was opened via the legacy openDatabase(path) path.
1267
+ */
1268
+ let _currentIdentityKey: string | null = null;
1269
+
1270
+ /**
1271
+ * Workspace-scoped connection cache.
1272
+ * Key: GsdWorkspace.identityKey (realpath-normalized project root).
1273
+ * Value: the DB path and open adapter for that workspace.
1274
+ *
1275
+ * Sibling worktrees of the same project share the same identityKey (set by
1276
+ * createWorkspace) and therefore reuse the same cached connection, preserving
1277
+ * shared-WAL semantics. Different projects get distinct cache entries.
1278
+ *
1279
+ * NOTE: Only one connection is "active" at a time (currentDb/currentPath).
1280
+ * The cache allows fast re-activation of a previously opened connection when
1281
+ * callers switch between known workspaces via openDatabaseByWorkspace().
1282
+ */
1283
+ const _dbCache = new Map<string, { dbPath: string; db: DbAdapter }>();
1284
+
1285
+ /** Test helper: expose the internal cache for inspection. Not for production use. */
1286
+ export function _getDbCache(): ReadonlyMap<string, { dbPath: string; db: DbAdapter }> {
1287
+ return _dbCache;
1288
+ }
1289
+
1290
+ /**
1291
+ * Close and evict every entry in the workspace connection cache, then call
1292
+ * closeDatabase() to close the active connection.
1293
+ *
1294
+ * Use this for test teardown or process-shutdown paths where every open
1295
+ * connection must be flushed. Normal callers should use closeDatabase() or
1296
+ * closeDatabaseByWorkspace() instead.
1297
+ */
1298
+ export function closeAllDatabases(): void {
1299
+ // Close all non-active cached connections first.
1300
+ for (const [key, entry] of _dbCache) {
1301
+ if (entry.db === currentDb) continue; // handled by closeDatabase() below
1302
+ _dbCache.delete(key);
1303
+ try { entry.db.exec("PRAGMA wal_checkpoint(TRUNCATE)"); } catch { /* best-effort */ }
1304
+ try { entry.db.exec("PRAGMA incremental_vacuum(64)"); } catch { /* best-effort */ }
1305
+ try { entry.db.close(); } catch { /* best-effort */ }
1306
+ }
1307
+ closeDatabase();
1308
+ }
1309
+
1310
+ /**
1311
+ * Open (or reuse) the database connection scoped to the given workspace.
1312
+ *
1313
+ * Uses workspace.identityKey as the cache key, so sibling worktrees of the
1314
+ * same project resolve to the same connection. On a cache hit the existing
1315
+ * adapter is reactivated as the current connection without re-opening the
1316
+ * file. On a cache miss, delegates to openDatabase() for the full
1317
+ * open + schema-init + migration flow, then caches the result.
1318
+ *
1319
+ * When switching to a different workspace, the previously active connection
1320
+ * is preserved in the cache (not closed), so callers can switch back to it
1321
+ * cheaply via a subsequent openDatabaseByWorkspace() call.
1322
+ *
1323
+ * @param workspace A GsdWorkspace created by createWorkspace().
1324
+ * @returns true if the connection is open and ready, false otherwise.
1325
+ */
1326
+ export function openDatabaseByWorkspace(workspace: GsdWorkspace): boolean {
1327
+ const key = workspace.identityKey;
1328
+ const dbPath = workspace.contract.projectDb;
1329
+
1330
+ const cached = _dbCache.get(key);
1331
+ if (cached) {
1332
+ // Reactivate the cached connection as the current singleton.
1333
+ currentDb = cached.db;
1334
+ currentPath = cached.dbPath;
1335
+ currentPid = process.pid;
1336
+ _dbOpenAttempted = true;
1337
+ _currentIdentityKey = key;
1338
+ return true;
1339
+ }
1340
+
1341
+ // Cache miss — need to open a new connection.
1342
+ //
1343
+ // If there is a currently active workspace connection, stash it in the
1344
+ // cache under its identity key before calling openDatabase(), because
1345
+ // openDatabase() will call closeDatabase() when the path changes (which
1346
+ // would destroy the existing adapter). By nulling out currentDb first,
1347
+ // we prevent openDatabase() from closing the live adapter.
1348
+ let oldDb: typeof currentDb = null;
1349
+ let oldPath: typeof currentPath = null;
1350
+ let oldPid: typeof currentPid = 0;
1351
+ let oldKey: typeof _currentIdentityKey = null;
1352
+
1353
+ if (currentDb !== null && _currentIdentityKey !== null) {
1354
+ // Snapshot the old globals so we can restore them on failure.
1355
+ oldDb = currentDb;
1356
+ oldPath = currentPath;
1357
+ oldPid = currentPid;
1358
+ oldKey = _currentIdentityKey;
1359
+ // Save the current connection so it stays alive in the cache.
1360
+ _dbCache.set(_currentIdentityKey, {
1361
+ dbPath: currentPath!,
1362
+ db: currentDb,
1363
+ });
1364
+ // Detach from globals so openDatabase() opens fresh without closing it.
1365
+ currentDb = null;
1366
+ currentPath = null;
1367
+ currentPid = 0;
1368
+ _currentIdentityKey = null;
1369
+ }
1370
+
1371
+ // Run the full open/schema/migration flow for the new workspace.
1372
+ // openDatabase() can throw on corrupt DB or permission error — catch so we
1373
+ // can restore the previous connection rather than leaving globals null.
1374
+ let opened: boolean;
1375
+ try {
1376
+ opened = openDatabase(dbPath);
1377
+ } catch (err) {
1378
+ // Failed to open the new DB. Restore the previous workspace connection so
1379
+ // the caller's workspace remains active (it is still safe in _dbCache).
1380
+ if (oldDb !== null) {
1381
+ currentDb = oldDb;
1382
+ currentPath = oldPath;
1383
+ currentPid = oldPid;
1384
+ _currentIdentityKey = oldKey;
1385
+ }
1386
+ throw err;
1387
+ }
1388
+ if (opened && currentDb) {
1389
+ _dbCache.set(key, { dbPath, db: currentDb });
1390
+ _currentIdentityKey = key;
1391
+ } else if (!opened && oldDb !== null) {
1392
+ // Restore the previous connection so the caller's workspace remains active.
1393
+ // The failed attempt left no live adapter, so the globals stayed null.
1394
+ currentDb = oldDb;
1395
+ currentPath = oldPath;
1396
+ currentPid = oldPid;
1397
+ _currentIdentityKey = oldKey;
1398
+ }
1399
+ return opened;
1400
+ }
1401
+
1402
+ /**
1403
+ * Open (or reuse) the database connection scoped to the workspace in a
1404
+ * MilestoneScope. Thin delegation to openDatabaseByWorkspace().
1405
+ */
1406
+ export function openDatabaseByScope(scope: MilestoneScope): boolean {
1407
+ return openDatabaseByWorkspace(scope.workspace);
1408
+ }
1409
+
1410
+ /**
1411
+ * Close the database connection for the given workspace and remove it from
1412
+ * the cache. If the workspace's connection is currently active (currentDb),
1413
+ * performs a full closeDatabase() including WAL checkpoint. Otherwise only
1414
+ * removes the cache entry (the adapter was already replaced by a later open).
1415
+ */
1416
+ export function closeDatabaseByWorkspace(workspace: GsdWorkspace): void {
1417
+ const key = workspace.identityKey;
1418
+ const cached = _dbCache.get(key);
1419
+ if (!cached) return;
1420
+
1421
+ _dbCache.delete(key);
1422
+
1423
+ if (currentDb === cached.db) {
1424
+ // This workspace's connection is the active one — full close.
1425
+ closeDatabase();
1426
+ } else {
1427
+ // Connection was displaced by a later open; close the adapter directly.
1428
+ try {
1429
+ cached.db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
1430
+ } catch (e) { logWarning("db", `WAL checkpoint (byWorkspace) failed: ${(e as Error).message}`); }
1431
+ try {
1432
+ cached.db.exec("PRAGMA incremental_vacuum(64)");
1433
+ } catch (e) { logWarning("db", `incremental vacuum (byWorkspace) failed: ${(e as Error).message}`); }
1434
+ try {
1435
+ cached.db.close();
1436
+ } catch (e) { logWarning("db", `database close (byWorkspace) failed: ${(e as Error).message}`); }
1437
+ }
1438
+ }
1262
1439
 
1263
1440
  export function getDbProvider(): ProviderName | null {
1264
1441
  loadProvider();
@@ -1389,6 +1566,13 @@ export function closeDatabase(): void {
1389
1566
  try {
1390
1567
  currentDb.close();
1391
1568
  } catch (e) { logWarning("db", `database close failed: ${(e as Error).message}`); }
1569
+ // If this connection was workspace-tracked, evict it from the cache so
1570
+ // subsequent openDatabaseByWorkspace() calls re-open rather than reactivate
1571
+ // a closed adapter.
1572
+ if (_currentIdentityKey !== null) {
1573
+ _dbCache.delete(_currentIdentityKey);
1574
+ _currentIdentityKey = null;
1575
+ }
1392
1576
  currentDb = null;
1393
1577
  currentPath = null;
1394
1578
  currentPid = 0;
@@ -194,7 +194,7 @@ export async function showQueueAdd(
194
194
 
195
195
  // ── Dispatch the queue prompt ───────────────────────────────────────
196
196
  // Activate the queue phase so the write-gate applies to CONTEXT.md writes
197
- setQueuePhaseActive(true);
197
+ setQueuePhaseActive(true, basePath);
198
198
 
199
199
  const queueInlinedTemplates = inlineTemplate("context", "Context");
200
200
  const prompt = loadPrompt("queue", {