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.
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/gsd/auto/phases.js +7 -2
- package/dist/resources/extensions/gsd/auto/session.js +3 -0
- package/dist/resources/extensions/gsd/auto-dispatch.js +3 -2
- package/dist/resources/extensions/gsd/auto-post-unit.js +7 -1
- package/dist/resources/extensions/gsd/auto-worktree.js +185 -40
- package/dist/resources/extensions/gsd/auto.js +62 -1
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +1 -1
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +17 -16
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +67 -55
- package/dist/resources/extensions/gsd/db-writer.js +96 -16
- package/dist/resources/extensions/gsd/delegation-policy.js +155 -0
- package/dist/resources/extensions/gsd/gsd-db.js +194 -0
- package/dist/resources/extensions/gsd/guided-flow-queue.js +1 -1
- package/dist/resources/extensions/gsd/guided-flow.js +117 -25
- package/dist/resources/extensions/gsd/metrics.js +287 -1
- package/dist/resources/extensions/gsd/paths.js +79 -8
- package/dist/resources/extensions/gsd/prompts/complete-slice.md +4 -4
- package/dist/resources/extensions/gsd/prompts/execute-task.md +3 -3
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +8 -1
- package/dist/resources/extensions/gsd/prompts/guided-discuss-project.md +22 -7
- package/dist/resources/extensions/gsd/prompts/guided-discuss-requirements.md +6 -2
- package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -1
- package/dist/resources/extensions/gsd/templates/project.md +10 -0
- package/dist/resources/extensions/gsd/workflow-mcp.js +2 -2
- package/dist/resources/extensions/gsd/workspace.js +59 -0
- package/dist/resources/extensions/gsd/worktree-resolver.js +15 -2
- package/dist/resources/extensions/gsd/write-intercept.js +3 -3
- package/dist/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +10 -10
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/required-server-files.json +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +10 -10
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/server.js +1 -1
- package/package.json +1 -1
- package/packages/mcp-server/README.md +2 -11
- package/packages/mcp-server/dist/remote-questions.d.ts +27 -0
- package/packages/mcp-server/dist/remote-questions.d.ts.map +1 -1
- package/packages/mcp-server/dist/remote-questions.js +28 -0
- package/packages/mcp-server/dist/remote-questions.js.map +1 -1
- package/packages/mcp-server/dist/server.d.ts +28 -0
- package/packages/mcp-server/dist/server.d.ts.map +1 -1
- package/packages/mcp-server/dist/server.js +94 -4
- package/packages/mcp-server/dist/server.js.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/src/mcp-server.test.ts +226 -0
- package/packages/mcp-server/src/remote-questions.test.ts +103 -0
- package/packages/mcp-server/src/remote-questions.ts +35 -0
- package/packages/mcp-server/src/server.ts +129 -6
- package/packages/mcp-server/src/workflow-tools.ts +1 -1
- package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
- package/src/resources/extensions/gsd/auto/phases.ts +8 -2
- package/src/resources/extensions/gsd/auto/session.ts +4 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +10 -2
- package/src/resources/extensions/gsd/auto-post-unit.ts +8 -1
- package/src/resources/extensions/gsd/auto-worktree.ts +225 -47
- package/src/resources/extensions/gsd/auto.ts +79 -1
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +1 -1
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +17 -17
- package/src/resources/extensions/gsd/bootstrap/tests/write-gate-basepath.test.ts +103 -0
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +80 -55
- package/src/resources/extensions/gsd/db-writer.ts +113 -17
- package/src/resources/extensions/gsd/delegation-policy.ts +197 -0
- package/src/resources/extensions/gsd/gsd-db.ts +184 -0
- package/src/resources/extensions/gsd/guided-flow-queue.ts +1 -1
- package/src/resources/extensions/gsd/guided-flow.ts +154 -25
- package/src/resources/extensions/gsd/metrics.ts +321 -1
- package/src/resources/extensions/gsd/paths.ts +67 -8
- package/src/resources/extensions/gsd/prompts/complete-slice.md +4 -4
- package/src/resources/extensions/gsd/prompts/execute-task.md +3 -3
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +8 -1
- package/src/resources/extensions/gsd/prompts/guided-discuss-project.md +22 -7
- package/src/resources/extensions/gsd/prompts/guided-discuss-requirements.md +6 -2
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -1
- package/src/resources/extensions/gsd/templates/project.md +10 -0
- package/src/resources/extensions/gsd/tests/auto-discuss-milestone-deadlock-4973.test.ts +14 -14
- package/src/resources/extensions/gsd/tests/auto-session-scope.test.ts +331 -0
- package/src/resources/extensions/gsd/tests/auto-worktree-registry.test.ts +176 -0
- package/src/resources/extensions/gsd/tests/db-writer-path-containment.test.ts +152 -0
- package/src/resources/extensions/gsd/tests/db-writer-root-artifact.test.ts +221 -0
- package/src/resources/extensions/gsd/tests/db-writer-scope.test.ts +230 -0
- package/src/resources/extensions/gsd/tests/delegation-policy.test.ts +151 -0
- package/src/resources/extensions/gsd/tests/dispatch-backgroundable-annotation.test.ts +55 -0
- package/src/resources/extensions/gsd/tests/draft-promotion.test.ts +3 -23
- package/src/resources/extensions/gsd/tests/gate-1b-orphan-discrimination.test.ts +193 -0
- package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound-corrections.test.ts +246 -0
- package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound.test.ts +218 -0
- package/src/resources/extensions/gsd/tests/gsd-db-failed-open-restore.test.ts +117 -0
- package/src/resources/extensions/gsd/tests/gsd-db-workspace-scope.test.ts +226 -0
- package/src/resources/extensions/gsd/tests/gsd-root-canonical.test.ts +66 -0
- package/src/resources/extensions/gsd/tests/gsd-root-home-guard.test.ts +68 -5
- package/src/resources/extensions/gsd/tests/guided-flow-prompt-consolidation.test.ts +4 -4
- package/src/resources/extensions/gsd/tests/integration/workspace-collapse-integration.test.ts +371 -0
- package/src/resources/extensions/gsd/tests/metrics-atomic-merge.test.ts +222 -0
- package/src/resources/extensions/gsd/tests/metrics-lock-hardening.test.ts +400 -0
- package/src/resources/extensions/gsd/tests/metrics-lock-not-acquired.test.ts +141 -0
- package/src/resources/extensions/gsd/tests/metrics-lock-retry-sleep.test.ts +287 -0
- package/src/resources/extensions/gsd/tests/metrics-prune-cache-invalidation.test.ts +149 -0
- package/src/resources/extensions/gsd/tests/metrics-scope.test.ts +378 -0
- package/src/resources/extensions/gsd/tests/originalbase-path-comparison.test.ts +329 -0
- package/src/resources/extensions/gsd/tests/path-cache-decoupled.test.ts +209 -0
- package/src/resources/extensions/gsd/tests/path-normalization-unified.test.ts +175 -0
- package/src/resources/extensions/gsd/tests/paths-cache.test.ts +170 -0
- package/src/resources/extensions/gsd/tests/pending-autostart-scope.test.ts +120 -0
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +150 -7
- package/src/resources/extensions/gsd/tests/ready-phrase-no-files-4573.test.ts +74 -0
- package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +28 -16
- package/src/resources/extensions/gsd/tests/resume-missing-worktree-warning.test.ts +209 -0
- package/src/resources/extensions/gsd/tests/sync-layer-scope.test.ts +453 -0
- package/src/resources/extensions/gsd/tests/teardown-chdir-failure-clears-registry.test.ts +162 -0
- package/src/resources/extensions/gsd/tests/teardown-cleanup-parity.test.ts +102 -0
- package/src/resources/extensions/gsd/tests/teardown-failure-clears-registry.test.ts +186 -0
- package/src/resources/extensions/gsd/tests/tool-invocation-error-loop-break.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/validator-scope-parity.test.ts +239 -0
- package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +9 -15
- package/src/resources/extensions/gsd/tests/workspace.test.ts +190 -0
- package/src/resources/extensions/gsd/tests/write-gate-predicates.test.ts +35 -35
- package/src/resources/extensions/gsd/tests/write-gate.test.ts +67 -52
- package/src/resources/extensions/gsd/tests/write-intercept.test.ts +1 -1
- package/src/resources/extensions/gsd/workflow-mcp.ts +2 -2
- package/src/resources/extensions/gsd/workspace.ts +95 -0
- package/src/resources/extensions/gsd/worktree-resolver.ts +16 -2
- package/src/resources/extensions/gsd/write-intercept.ts +3 -3
- /package/dist/web/standalone/.next/static/{HahrZrc_Xn4wumj0O1Ydp → AT5qi39nKXkdmQIOIoh0f}/_buildManifest.js +0 -0
- /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", {
|