gsd-pi 2.67.0-dev.1cd1e0f → 2.67.0-dev.2142d3e
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/extensions/claude-code-cli/stream-adapter.js +3 -0
- package/dist/resources/extensions/gsd/auto/phases.js +17 -0
- package/dist/resources/extensions/gsd/auto-direct-dispatch.js +12 -0
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +11 -435
- package/dist/resources/extensions/gsd/bootstrap/dynamic-tools.js +1 -4
- package/dist/resources/extensions/gsd/bootstrap/query-tools.js +7 -64
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +88 -8
- package/dist/resources/extensions/gsd/commands/handlers/core.js +38 -24
- package/dist/resources/extensions/gsd/commands/index.js +8 -1
- package/dist/resources/extensions/gsd/guided-flow.js +16 -0
- package/dist/resources/extensions/gsd/init-wizard.js +34 -0
- package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +508 -0
- package/dist/resources/extensions/gsd/workflow-logger.js +18 -3
- package/dist/resources/extensions/gsd/workflow-mcp.js +190 -0
- 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/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/package.json +1 -1
- package/packages/mcp-server/README.md +38 -0
- package/packages/mcp-server/src/server.ts +6 -2
- package/packages/mcp-server/src/workflow-tools.test.ts +976 -0
- package/packages/mcp-server/src/workflow-tools.ts +986 -0
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +3 -0
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +121 -0
- package/src/resources/extensions/gsd/auto/phases.ts +25 -0
- package/src/resources/extensions/gsd/auto-direct-dispatch.ts +20 -0
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +22 -435
- package/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts +1 -5
- package/src/resources/extensions/gsd/bootstrap/query-tools.ts +7 -72
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +122 -6
- package/src/resources/extensions/gsd/commands/handlers/core.ts +52 -25
- package/src/resources/extensions/gsd/commands/index.ts +7 -1
- package/src/resources/extensions/gsd/guided-flow.ts +24 -0
- package/src/resources/extensions/gsd/init-wizard.ts +34 -0
- package/src/resources/extensions/gsd/tests/core-overlay-fallback.test.ts +101 -0
- package/src/resources/extensions/gsd/tests/ensure-db-open.test.ts +66 -0
- package/src/resources/extensions/gsd/tests/init-bootstrap-completeness.test.ts +121 -0
- package/src/resources/extensions/gsd/tests/workflow-logger.test.ts +16 -0
- package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +301 -0
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +625 -0
- package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +629 -0
- package/src/resources/extensions/gsd/workflow-logger.ts +19 -3
- package/src/resources/extensions/gsd/workflow-mcp.ts +233 -0
- /package/dist/web/standalone/.next/static/{PHqEommYRR8CRn3i84CGM → xR6qurkuYSvyjBjRyJLxG}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{PHqEommYRR8CRn3i84CGM → xR6qurkuYSvyjBjRyJLxG}/_ssgManifest.js +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// GSD2 — Read-only query tools exposing DB state to the LLM via the WAL connection
|
|
2
2
|
import { Type } from "@sinclair/typebox";
|
|
3
|
-
import {
|
|
3
|
+
import { ensureDbOpen } from "./dynamic-tools.js";
|
|
4
|
+
import { executeMilestoneStatus } from "../tools/workflow-tool-executors.js";
|
|
4
5
|
export function registerQueryTools(pi) {
|
|
5
6
|
pi.registerTool({
|
|
6
7
|
name: "gsd_milestone_status",
|
|
@@ -16,72 +17,14 @@ export function registerQueryTools(pi) {
|
|
|
16
17
|
milestoneId: Type.String({ description: "Milestone ID to query (e.g. M001)" }),
|
|
17
18
|
}),
|
|
18
19
|
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
// ensureDbOpen() only creates/migrates when .gsd/ has content (#3644).
|
|
22
|
-
const { ensureDbOpen } = await import("./dynamic-tools.js");
|
|
23
|
-
const dbAvailable = await ensureDbOpen();
|
|
24
|
-
const { getMilestone, getSliceStatusSummary, getSliceTaskCounts, _getAdapter, } = await import("../gsd-db.js");
|
|
25
|
-
if (!dbAvailable) {
|
|
26
|
-
return {
|
|
27
|
-
content: [{ type: "text", text: "Error: GSD database is not available." }],
|
|
28
|
-
details: { operation: "milestone_status", error: "db_unavailable" },
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
// Wrap all reads in a single transaction for snapshot consistency.
|
|
32
|
-
// SQLite WAL mode guarantees reads within a transaction see a single
|
|
33
|
-
// consistent snapshot, preventing torn reads from concurrent writes.
|
|
34
|
-
const adapter = _getAdapter();
|
|
35
|
-
adapter.exec("BEGIN"); // eslint-disable-line -- SQLite exec, not child_process
|
|
36
|
-
try {
|
|
37
|
-
const milestone = getMilestone(params.milestoneId);
|
|
38
|
-
if (!milestone) {
|
|
39
|
-
adapter.exec("COMMIT"); // eslint-disable-line
|
|
40
|
-
return {
|
|
41
|
-
content: [{ type: "text", text: `Milestone ${params.milestoneId} not found in database.` }],
|
|
42
|
-
details: { operation: "milestone_status", milestoneId: params.milestoneId, found: false },
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
const sliceStatuses = getSliceStatusSummary(params.milestoneId);
|
|
46
|
-
const slices = sliceStatuses.map((s) => {
|
|
47
|
-
const counts = getSliceTaskCounts(params.milestoneId, s.id);
|
|
48
|
-
return {
|
|
49
|
-
id: s.id,
|
|
50
|
-
status: s.status,
|
|
51
|
-
taskCounts: counts,
|
|
52
|
-
};
|
|
53
|
-
});
|
|
54
|
-
adapter.exec("COMMIT"); // eslint-disable-line
|
|
55
|
-
const result = {
|
|
56
|
-
milestoneId: milestone.id,
|
|
57
|
-
title: milestone.title,
|
|
58
|
-
status: milestone.status,
|
|
59
|
-
createdAt: milestone.created_at,
|
|
60
|
-
completedAt: milestone.completed_at,
|
|
61
|
-
sliceCount: slices.length,
|
|
62
|
-
slices,
|
|
63
|
-
};
|
|
64
|
-
return {
|
|
65
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
66
|
-
details: { operation: "milestone_status", milestoneId: milestone.id, sliceCount: slices.length },
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
catch (txErr) {
|
|
70
|
-
try {
|
|
71
|
-
adapter.exec("ROLLBACK");
|
|
72
|
-
}
|
|
73
|
-
catch { /* swallow */ } // eslint-disable-line
|
|
74
|
-
throw txErr;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
catch (err) {
|
|
78
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
79
|
-
logWarning("tool", `gsd_milestone_status tool failed: ${msg}`);
|
|
20
|
+
const dbAvailable = await ensureDbOpen();
|
|
21
|
+
if (!dbAvailable) {
|
|
80
22
|
return {
|
|
81
|
-
content: [{ type: "text", text:
|
|
82
|
-
details: { operation: "milestone_status", error:
|
|
23
|
+
content: [{ type: "text", text: "Error: GSD database is not available. Cannot read milestone status." }],
|
|
24
|
+
details: { operation: "milestone_status", error: "db_unavailable" },
|
|
83
25
|
};
|
|
84
26
|
}
|
|
27
|
+
return executeMilestoneStatus(params);
|
|
85
28
|
},
|
|
86
29
|
});
|
|
87
30
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
1
3
|
const MILESTONE_CONTEXT_RE = /M\d+(?:-[a-z0-9]{6})?-CONTEXT\.md$/;
|
|
2
4
|
const CONTEXT_MILESTONE_RE = /(?:^|[/\\])(M\d+(?:-[a-z0-9]{6})?)-CONTEXT\.md$/i;
|
|
3
5
|
const DEPTH_VERIFICATION_MILESTONE_RE = /depth_verification[_-](M\d+(?:-[a-z0-9]{6})?)/i;
|
|
@@ -57,6 +59,61 @@ const GATE_SAFE_TOOLS = new Set([
|
|
|
57
59
|
"search-the-web", "resolve_library", "get_library_docs", "fetch_page",
|
|
58
60
|
"search_and_read",
|
|
59
61
|
]);
|
|
62
|
+
function shouldPersistWriteGateSnapshot(env = process.env) {
|
|
63
|
+
return env.GSD_PERSIST_WRITE_GATE_STATE === "1";
|
|
64
|
+
}
|
|
65
|
+
function writeGateSnapshotPath(basePath = process.cwd()) {
|
|
66
|
+
return join(basePath, ".gsd", "runtime", "write-gate-state.json");
|
|
67
|
+
}
|
|
68
|
+
function currentWriteGateSnapshot() {
|
|
69
|
+
return {
|
|
70
|
+
verifiedDepthMilestones: [...verifiedDepthMilestones].sort(),
|
|
71
|
+
activeQueuePhase,
|
|
72
|
+
pendingGateId,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function persistWriteGateSnapshot(basePath = process.cwd()) {
|
|
76
|
+
if (!shouldPersistWriteGateSnapshot())
|
|
77
|
+
return;
|
|
78
|
+
const path = writeGateSnapshotPath(basePath);
|
|
79
|
+
mkdirSync(join(basePath, ".gsd", "runtime"), { recursive: true });
|
|
80
|
+
const tempPath = `${path}.tmp`;
|
|
81
|
+
writeFileSync(tempPath, JSON.stringify(currentWriteGateSnapshot(), null, 2), "utf-8");
|
|
82
|
+
renameSync(tempPath, path);
|
|
83
|
+
}
|
|
84
|
+
function clearPersistedWriteGateSnapshot(basePath = process.cwd()) {
|
|
85
|
+
if (!shouldPersistWriteGateSnapshot())
|
|
86
|
+
return;
|
|
87
|
+
const path = writeGateSnapshotPath(basePath);
|
|
88
|
+
try {
|
|
89
|
+
unlinkSync(path);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// swallow
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function normalizeWriteGateSnapshot(value) {
|
|
96
|
+
const record = value && typeof value === "object" ? value : {};
|
|
97
|
+
const verified = Array.isArray(record.verifiedDepthMilestones)
|
|
98
|
+
? record.verifiedDepthMilestones.filter((item) => typeof item === "string")
|
|
99
|
+
: [];
|
|
100
|
+
return {
|
|
101
|
+
verifiedDepthMilestones: [...new Set(verified)].sort(),
|
|
102
|
+
activeQueuePhase: record.activeQueuePhase === true,
|
|
103
|
+
pendingGateId: typeof record.pendingGateId === "string" ? record.pendingGateId : null,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
export function loadWriteGateSnapshot(basePath = process.cwd()) {
|
|
107
|
+
const path = writeGateSnapshotPath(basePath);
|
|
108
|
+
if (!existsSync(path))
|
|
109
|
+
return currentWriteGateSnapshot();
|
|
110
|
+
try {
|
|
111
|
+
return normalizeWriteGateSnapshot(JSON.parse(readFileSync(path, "utf-8")));
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return currentWriteGateSnapshot();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
60
117
|
export function isDepthVerified() {
|
|
61
118
|
return verifiedDepthMilestones.size > 0;
|
|
62
119
|
}
|
|
@@ -68,25 +125,34 @@ export function isMilestoneDepthVerified(milestoneId) {
|
|
|
68
125
|
return false;
|
|
69
126
|
return verifiedDepthMilestones.has(milestoneId);
|
|
70
127
|
}
|
|
128
|
+
export function isMilestoneDepthVerifiedInSnapshot(snapshot, milestoneId) {
|
|
129
|
+
if (!milestoneId)
|
|
130
|
+
return false;
|
|
131
|
+
return snapshot.verifiedDepthMilestones.includes(milestoneId);
|
|
132
|
+
}
|
|
71
133
|
export function isQueuePhaseActive() {
|
|
72
134
|
return activeQueuePhase;
|
|
73
135
|
}
|
|
74
136
|
export function setQueuePhaseActive(active) {
|
|
75
137
|
activeQueuePhase = active;
|
|
138
|
+
persistWriteGateSnapshot();
|
|
76
139
|
}
|
|
77
140
|
export function resetWriteGateState() {
|
|
78
141
|
verifiedDepthMilestones.clear();
|
|
79
142
|
pendingGateId = null;
|
|
143
|
+
persistWriteGateSnapshot();
|
|
80
144
|
}
|
|
81
145
|
export function clearDiscussionFlowState() {
|
|
82
146
|
verifiedDepthMilestones.clear();
|
|
83
147
|
activeQueuePhase = false;
|
|
84
148
|
pendingGateId = null;
|
|
149
|
+
clearPersistedWriteGateSnapshot();
|
|
85
150
|
}
|
|
86
|
-
export function markDepthVerified(milestoneId) {
|
|
151
|
+
export function markDepthVerified(milestoneId, basePath = process.cwd()) {
|
|
87
152
|
if (!milestoneId)
|
|
88
153
|
return;
|
|
89
154
|
verifiedDepthMilestones.add(milestoneId);
|
|
155
|
+
persistWriteGateSnapshot(basePath);
|
|
90
156
|
}
|
|
91
157
|
/**
|
|
92
158
|
* Check whether a question ID matches a recognized gate pattern.
|
|
@@ -114,12 +180,14 @@ function extractContextMilestoneId(inputPath) {
|
|
|
114
180
|
*/
|
|
115
181
|
export function setPendingGate(gateId) {
|
|
116
182
|
pendingGateId = gateId;
|
|
183
|
+
persistWriteGateSnapshot();
|
|
117
184
|
}
|
|
118
185
|
/**
|
|
119
186
|
* Clear the pending gate (called when the user confirms).
|
|
120
187
|
*/
|
|
121
188
|
export function clearPendingGate() {
|
|
122
189
|
pendingGateId = null;
|
|
190
|
+
persistWriteGateSnapshot();
|
|
123
191
|
}
|
|
124
192
|
/**
|
|
125
193
|
* Get the currently pending gate, if any.
|
|
@@ -134,8 +202,11 @@ export function getPendingGate() {
|
|
|
134
202
|
* Returns { block: true, reason } if the tool should be blocked.
|
|
135
203
|
* Read-only tools and ask_user_questions itself are always allowed.
|
|
136
204
|
*/
|
|
137
|
-
export function shouldBlockPendingGate(toolName,
|
|
138
|
-
|
|
205
|
+
export function shouldBlockPendingGate(toolName, milestoneId, queuePhaseActive) {
|
|
206
|
+
return shouldBlockPendingGateInSnapshot(currentWriteGateSnapshot(), toolName, milestoneId, queuePhaseActive);
|
|
207
|
+
}
|
|
208
|
+
export function shouldBlockPendingGateInSnapshot(snapshot, toolName, _milestoneId, _queuePhaseActive) {
|
|
209
|
+
if (!snapshot.pendingGateId)
|
|
139
210
|
return { block: false };
|
|
140
211
|
if (GATE_SAFE_TOOLS.has(toolName))
|
|
141
212
|
return { block: false };
|
|
@@ -145,7 +216,7 @@ export function shouldBlockPendingGate(toolName, _milestoneId, _queuePhaseActive
|
|
|
145
216
|
return {
|
|
146
217
|
block: true,
|
|
147
218
|
reason: [
|
|
148
|
-
`HARD BLOCK: Discussion gate "${pendingGateId}" has not been confirmed by the user.`,
|
|
219
|
+
`HARD BLOCK: Discussion gate "${snapshot.pendingGateId}" has not been confirmed by the user.`,
|
|
149
220
|
`You MUST re-call ask_user_questions with the gate question before making any other tool calls.`,
|
|
150
221
|
`If the previous ask_user_questions call failed, errored, was cancelled, or the user's response`,
|
|
151
222
|
`did not match a provided option, you MUST re-ask — never rationalize past the block.`,
|
|
@@ -157,8 +228,11 @@ export function shouldBlockPendingGate(toolName, _milestoneId, _queuePhaseActive
|
|
|
157
228
|
* Check whether a bash command should be blocked because a discussion gate is pending.
|
|
158
229
|
* Read-only bash commands are allowed; mutating commands are blocked.
|
|
159
230
|
*/
|
|
160
|
-
export function shouldBlockPendingGateBash(command,
|
|
161
|
-
|
|
231
|
+
export function shouldBlockPendingGateBash(command, milestoneId, queuePhaseActive) {
|
|
232
|
+
return shouldBlockPendingGateBashInSnapshot(currentWriteGateSnapshot(), command, milestoneId, queuePhaseActive);
|
|
233
|
+
}
|
|
234
|
+
export function shouldBlockPendingGateBashInSnapshot(snapshot, command, _milestoneId, _queuePhaseActive) {
|
|
235
|
+
if (!snapshot.pendingGateId)
|
|
162
236
|
return { block: false };
|
|
163
237
|
// Allow read-only bash commands
|
|
164
238
|
if (BASH_READ_ONLY_RE.test(command))
|
|
@@ -166,7 +240,7 @@ export function shouldBlockPendingGateBash(command, _milestoneId, _queuePhaseAct
|
|
|
166
240
|
return {
|
|
167
241
|
block: true,
|
|
168
242
|
reason: [
|
|
169
|
-
`HARD BLOCK: Discussion gate "${pendingGateId}" has not been confirmed by the user.`,
|
|
243
|
+
`HARD BLOCK: Discussion gate "${snapshot.pendingGateId}" has not been confirmed by the user.`,
|
|
170
244
|
`You MUST re-call ask_user_questions with the gate question before running mutating commands.`,
|
|
171
245
|
`If the previous ask_user_questions call failed, errored, was cancelled, or the user's response`,
|
|
172
246
|
`did not match a provided option, you MUST re-ask — never rationalize past the block.`,
|
|
@@ -232,6 +306,9 @@ export function shouldBlockContextWrite(toolName, inputPath, milestoneId, _queue
|
|
|
232
306
|
* require the milestone to be depth-verified first.
|
|
233
307
|
*/
|
|
234
308
|
export function shouldBlockContextArtifactSave(artifactType, milestoneId, sliceId) {
|
|
309
|
+
return shouldBlockContextArtifactSaveInSnapshot(currentWriteGateSnapshot(), artifactType, milestoneId, sliceId);
|
|
310
|
+
}
|
|
311
|
+
export function shouldBlockContextArtifactSaveInSnapshot(snapshot, artifactType, milestoneId, sliceId) {
|
|
235
312
|
if (artifactType !== "CONTEXT")
|
|
236
313
|
return { block: false };
|
|
237
314
|
if (sliceId)
|
|
@@ -245,7 +322,7 @@ export function shouldBlockContextArtifactSave(artifactType, milestoneId, sliceI
|
|
|
245
322
|
].join(" "),
|
|
246
323
|
};
|
|
247
324
|
}
|
|
248
|
-
if (
|
|
325
|
+
if (isMilestoneDepthVerifiedInSnapshot(snapshot, milestoneId))
|
|
249
326
|
return { block: false };
|
|
250
327
|
return {
|
|
251
328
|
block: true,
|
|
@@ -271,6 +348,9 @@ export function shouldBlockContextArtifactSave(artifactType, milestoneId, sliceI
|
|
|
271
348
|
* @returns { block, reason } — block=true if the call should be rejected.
|
|
272
349
|
*/
|
|
273
350
|
export function shouldBlockQueueExecution(toolName, input, queuePhaseActive) {
|
|
351
|
+
return shouldBlockQueueExecutionInSnapshot(currentWriteGateSnapshot(), toolName, input, queuePhaseActive);
|
|
352
|
+
}
|
|
353
|
+
export function shouldBlockQueueExecutionInSnapshot(snapshot, toolName, input, queuePhaseActive = snapshot.activeQueuePhase) {
|
|
274
354
|
if (!queuePhaseActive)
|
|
275
355
|
return { block: false };
|
|
276
356
|
// Always-safe tools (read-only, discussion, planning)
|
|
@@ -168,6 +168,42 @@ function sortModelsForSelection(models, currentModel) {
|
|
|
168
168
|
return a.id.localeCompare(b.id);
|
|
169
169
|
});
|
|
170
170
|
}
|
|
171
|
+
function buildProviderModelGroups(models, currentModel) {
|
|
172
|
+
const byProvider = new Map();
|
|
173
|
+
for (const model of sortModelsForSelection(models, currentModel)) {
|
|
174
|
+
let group = byProvider.get(model.provider);
|
|
175
|
+
if (!group) {
|
|
176
|
+
group = [];
|
|
177
|
+
byProvider.set(model.provider, group);
|
|
178
|
+
}
|
|
179
|
+
group.push(model);
|
|
180
|
+
}
|
|
181
|
+
return byProvider;
|
|
182
|
+
}
|
|
183
|
+
async function selectModelByProvider(title, models, ctx, currentModel) {
|
|
184
|
+
const byProvider = buildProviderModelGroups(models, currentModel);
|
|
185
|
+
const providerOptions = Array.from(byProvider.entries()).map(([provider, group]) => `${provider} (${group.length} model${group.length === 1 ? "" : "s"})`);
|
|
186
|
+
providerOptions.push("(cancel)");
|
|
187
|
+
const providerChoice = await ctx.ui.select(`${title} — choose provider:`, providerOptions);
|
|
188
|
+
if (!providerChoice || typeof providerChoice !== "string" || providerChoice === "(cancel)")
|
|
189
|
+
return undefined;
|
|
190
|
+
const providerName = providerChoice.replace(/ \(\d+ models?\)$/, "");
|
|
191
|
+
const providerModels = byProvider.get(providerName);
|
|
192
|
+
if (!providerModels || providerModels.length === 0)
|
|
193
|
+
return undefined;
|
|
194
|
+
const optionToModel = new Map();
|
|
195
|
+
const modelOptions = providerModels.map((model) => {
|
|
196
|
+
const isCurrent = currentModel && model.provider === currentModel.provider && model.id === currentModel.id;
|
|
197
|
+
const label = `${isCurrent ? "* " : ""}${model.id}`;
|
|
198
|
+
optionToModel.set(label, model);
|
|
199
|
+
return label;
|
|
200
|
+
});
|
|
201
|
+
modelOptions.push("(cancel)");
|
|
202
|
+
const modelChoice = await ctx.ui.select(`${title} — ${providerName}:`, modelOptions);
|
|
203
|
+
if (!modelChoice || typeof modelChoice !== "string" || modelChoice === "(cancel)")
|
|
204
|
+
return undefined;
|
|
205
|
+
return optionToModel.get(modelChoice);
|
|
206
|
+
}
|
|
171
207
|
async function resolveRequestedModel(query, ctx) {
|
|
172
208
|
const { resolveModelId } = await import("../../auto-model-selection.js");
|
|
173
209
|
const models = ctx.modelRegistry.getAvailable();
|
|
@@ -181,18 +217,7 @@ async function resolveRequestedModel(query, ctx) {
|
|
|
181
217
|
return partialMatches[0];
|
|
182
218
|
if (partialMatches.length === 0 || !ctx.hasUI)
|
|
183
219
|
return undefined;
|
|
184
|
-
|
|
185
|
-
const optionToModel = new Map();
|
|
186
|
-
const options = sorted.map((model) => {
|
|
187
|
-
const label = `${model.provider}/${model.id}`;
|
|
188
|
-
optionToModel.set(label, model);
|
|
189
|
-
return label;
|
|
190
|
-
});
|
|
191
|
-
options.push("(cancel)");
|
|
192
|
-
const choice = await ctx.ui.select(`Multiple models match "${query}" — choose one:`, options);
|
|
193
|
-
if (!choice || typeof choice !== "string" || choice === "(cancel)")
|
|
194
|
-
return undefined;
|
|
195
|
-
return optionToModel.get(choice);
|
|
220
|
+
return selectModelByProvider(`Multiple models match "${query}"`, partialMatches, ctx, ctx.model);
|
|
196
221
|
}
|
|
197
222
|
async function handleModel(trimmedArgs, ctx, pi) {
|
|
198
223
|
const availableModels = ctx.modelRegistry.getAvailable();
|
|
@@ -212,18 +237,7 @@ async function handleModel(trimmedArgs, ctx, pi) {
|
|
|
212
237
|
ctx.ui.notify(`Current model: ${current}\nUsage: /gsd model <provider/model|model-id>`, "info");
|
|
213
238
|
return;
|
|
214
239
|
}
|
|
215
|
-
|
|
216
|
-
const options = sortModelsForSelection(availableModels, ctx.model).map((model) => {
|
|
217
|
-
const isCurrent = ctx.model && model.provider === ctx.model.provider && model.id === ctx.model.id;
|
|
218
|
-
const label = `${isCurrent ? "* " : ""}${model.provider}/${model.id}`;
|
|
219
|
-
optionToModel.set(label, model);
|
|
220
|
-
return label;
|
|
221
|
-
});
|
|
222
|
-
options.push("(cancel)");
|
|
223
|
-
const choice = await ctx.ui.select("Select session model:", options);
|
|
224
|
-
if (!choice || typeof choice !== "string" || choice === "(cancel)")
|
|
225
|
-
return;
|
|
226
|
-
targetModel = optionToModel.get(choice);
|
|
240
|
+
targetModel = await selectModelByProvider("Select session model:", availableModels, ctx, ctx.model);
|
|
227
241
|
}
|
|
228
242
|
else {
|
|
229
243
|
targetModel = await resolveRequestedModel(trimmed, ctx);
|
|
@@ -5,7 +5,14 @@ export function registerGSDCommand(pi) {
|
|
|
5
5
|
getArgumentCompletions: getGsdArgumentCompletions,
|
|
6
6
|
handler: async (args, ctx) => {
|
|
7
7
|
const { handleGSDCommand } = await import("./dispatcher.js");
|
|
8
|
-
await
|
|
8
|
+
const { setStderrLoggingEnabled } = await import("../workflow-logger.js");
|
|
9
|
+
const previousStderrSetting = setStderrLoggingEnabled(false);
|
|
10
|
+
try {
|
|
11
|
+
await handleGSDCommand(args, ctx, pi);
|
|
12
|
+
}
|
|
13
|
+
finally {
|
|
14
|
+
setStderrLoggingEnabled(previousStderrSetting);
|
|
15
|
+
}
|
|
9
16
|
},
|
|
10
17
|
});
|
|
11
18
|
}
|
|
@@ -34,6 +34,7 @@ import { findMilestoneIds, nextMilestoneId, reserveMilestoneId, getReservedMiles
|
|
|
34
34
|
import { parkMilestone, discardMilestone } from "./milestone-actions.js";
|
|
35
35
|
import { selectAndApplyModel } from "./auto-model-selection.js";
|
|
36
36
|
import { DISCUSS_TOOLS_ALLOWLIST } from "./constants.js";
|
|
37
|
+
import { getWorkflowTransportSupportError, getRequiredWorkflowToolsForGuidedUnit, } from "./workflow-mcp.js";
|
|
37
38
|
import { runPreparation, formatCodebaseBrief, formatPriorContextBrief, formatEcosystemBrief, } from "./preparation.js";
|
|
38
39
|
// ─── Preparation result storage ─────────────────────────────────────────────
|
|
39
40
|
// Stores the most recent preparation result for injection into discuss prompts.
|
|
@@ -259,6 +260,21 @@ async function dispatchWorkflow(pi, note, customType = "gsd-run", ctx, unitType)
|
|
|
259
260
|
routing: result.routing,
|
|
260
261
|
});
|
|
261
262
|
}
|
|
263
|
+
const compatibilityError = getWorkflowTransportSupportError(result.appliedModel?.provider ?? ctx.model?.provider, getRequiredWorkflowToolsForGuidedUnit(unitType), {
|
|
264
|
+
projectRoot: process.cwd(),
|
|
265
|
+
surface: "guided flow",
|
|
266
|
+
unitType,
|
|
267
|
+
authMode: result.appliedModel?.provider
|
|
268
|
+
? ctx.modelRegistry.getProviderAuthMode(result.appliedModel.provider)
|
|
269
|
+
: ctx.model?.provider
|
|
270
|
+
? ctx.modelRegistry.getProviderAuthMode(ctx.model.provider)
|
|
271
|
+
: undefined,
|
|
272
|
+
baseUrl: result.appliedModel?.baseUrl ?? ctx.model?.baseUrl,
|
|
273
|
+
});
|
|
274
|
+
if (compatibilityError) {
|
|
275
|
+
ctx.ui.notify(compatibilityError, "error");
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
262
278
|
}
|
|
263
279
|
// Scope tools for discuss flows (#2949).
|
|
264
280
|
// Providers with grammar-based constrained decoding (xAI/Grok) return
|
|
@@ -187,6 +187,20 @@ export async function showProjectInit(ctx, pi, basePath, detection) {
|
|
|
187
187
|
}
|
|
188
188
|
// ── Step 9: Bootstrap .gsd/ ────────────────────────────────────────────────
|
|
189
189
|
bootstrapGsdDirectory(basePath, prefs, signals);
|
|
190
|
+
// Initialize SQLite database so GSD starts in full-capability mode (#3880).
|
|
191
|
+
// Without this, isDbAvailable() returns false and GSD enters degraded
|
|
192
|
+
// markdown-only mode until a tool handler happens to call ensureDbOpen().
|
|
193
|
+
let dbReady = false;
|
|
194
|
+
try {
|
|
195
|
+
const { ensureDbOpen } = await import("./bootstrap/dynamic-tools.js");
|
|
196
|
+
dbReady = await ensureDbOpen(basePath);
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
// Swallowed — warning surfaced below
|
|
200
|
+
}
|
|
201
|
+
if (!dbReady) {
|
|
202
|
+
ctx.ui.notify("Warning: database initialization failed — GSD will run in degraded mode until the next /gsd invocation.", "warning");
|
|
203
|
+
}
|
|
190
204
|
// Ensure .gitignore
|
|
191
205
|
ensureGitignore(basePath);
|
|
192
206
|
untrackRuntimeFiles(basePath);
|
|
@@ -201,6 +215,25 @@ export async function showProjectInit(ctx, pi, basePath, detection) {
|
|
|
201
215
|
catch {
|
|
202
216
|
// Non-fatal — codebase map generation failure should never block project init
|
|
203
217
|
}
|
|
218
|
+
// Write initial STATE.md so it exists before the first /gsd invocation.
|
|
219
|
+
// The explicit /gsd init path (ops.ts) returns without entering showSmartEntry(),
|
|
220
|
+
// which would otherwise generate STATE.md at guided-flow.ts:1358.
|
|
221
|
+
let stateReady = false;
|
|
222
|
+
try {
|
|
223
|
+
const { deriveState } = await import("./state.js");
|
|
224
|
+
const { buildStateMarkdown } = await import("./doctor.js");
|
|
225
|
+
const { saveFile } = await import("./files.js");
|
|
226
|
+
const { resolveGsdRootFile } = await import("./paths.js");
|
|
227
|
+
const state = await deriveState(basePath);
|
|
228
|
+
await saveFile(resolveGsdRootFile(basePath, "STATE"), buildStateMarkdown(state));
|
|
229
|
+
stateReady = true;
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
// Swallowed — warning surfaced below
|
|
233
|
+
}
|
|
234
|
+
if (!stateReady) {
|
|
235
|
+
ctx.ui.notify("Warning: initial STATE.md generation failed — it will be created on the next /gsd invocation.", "warning");
|
|
236
|
+
}
|
|
204
237
|
ctx.ui.notify("GSD initialized. Starting your first milestone...", "info");
|
|
205
238
|
return { completed: true, bootstrapped: true };
|
|
206
239
|
}
|
|
@@ -348,6 +381,7 @@ function bootstrapGsdDirectory(basePath, prefs, signals) {
|
|
|
348
381
|
assertSafeDirectory(basePath);
|
|
349
382
|
const gsd = gsdRoot(basePath);
|
|
350
383
|
mkdirSync(join(gsd, "milestones"), { recursive: true });
|
|
384
|
+
mkdirSync(join(gsd, "runtime"), { recursive: true });
|
|
351
385
|
// Write PREFERENCES.md from wizard answers
|
|
352
386
|
const preferencesContent = buildPreferencesFile(prefs);
|
|
353
387
|
writeFileSync(join(gsd, "PREFERENCES.md"), preferencesContent, "utf-8");
|