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,3 +1,6 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
1
4
|
const MILESTONE_CONTEXT_RE = /M\d+(?:-[a-z0-9]{6})?-CONTEXT\.md$/;
|
|
2
5
|
const CONTEXT_MILESTONE_RE = /(?:^|[/\\])(M\d+(?:-[a-z0-9]{6})?)-CONTEXT\.md$/i;
|
|
3
6
|
const DEPTH_VERIFICATION_MILESTONE_RE = /depth_verification[_-](M\d+(?:-[a-z0-9]{6})?)/i;
|
|
@@ -65,6 +68,69 @@ const GATE_SAFE_TOOLS = new Set([
|
|
|
65
68
|
"search_and_read",
|
|
66
69
|
]);
|
|
67
70
|
|
|
71
|
+
export interface WriteGateSnapshot {
|
|
72
|
+
verifiedDepthMilestones: string[];
|
|
73
|
+
activeQueuePhase: boolean;
|
|
74
|
+
pendingGateId: string | null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function shouldPersistWriteGateSnapshot(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
78
|
+
return env.GSD_PERSIST_WRITE_GATE_STATE === "1";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function writeGateSnapshotPath(basePath: string = process.cwd()): string {
|
|
82
|
+
return join(basePath, ".gsd", "runtime", "write-gate-state.json");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function currentWriteGateSnapshot(): WriteGateSnapshot {
|
|
86
|
+
return {
|
|
87
|
+
verifiedDepthMilestones: [...verifiedDepthMilestones].sort(),
|
|
88
|
+
activeQueuePhase,
|
|
89
|
+
pendingGateId,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function persistWriteGateSnapshot(basePath: string = process.cwd()): void {
|
|
94
|
+
if (!shouldPersistWriteGateSnapshot()) return;
|
|
95
|
+
const path = writeGateSnapshotPath(basePath);
|
|
96
|
+
mkdirSync(join(basePath, ".gsd", "runtime"), { recursive: true });
|
|
97
|
+
const tempPath = `${path}.tmp`;
|
|
98
|
+
writeFileSync(tempPath, JSON.stringify(currentWriteGateSnapshot(), null, 2), "utf-8");
|
|
99
|
+
renameSync(tempPath, path);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function clearPersistedWriteGateSnapshot(basePath: string = process.cwd()): void {
|
|
103
|
+
if (!shouldPersistWriteGateSnapshot()) return;
|
|
104
|
+
const path = writeGateSnapshotPath(basePath);
|
|
105
|
+
try {
|
|
106
|
+
unlinkSync(path);
|
|
107
|
+
} catch {
|
|
108
|
+
// swallow
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function normalizeWriteGateSnapshot(value: unknown): WriteGateSnapshot {
|
|
113
|
+
const record = value && typeof value === "object" ? value as Record<string, unknown> : {};
|
|
114
|
+
const verified = Array.isArray(record.verifiedDepthMilestones)
|
|
115
|
+
? record.verifiedDepthMilestones.filter((item): item is string => typeof item === "string")
|
|
116
|
+
: [];
|
|
117
|
+
return {
|
|
118
|
+
verifiedDepthMilestones: [...new Set(verified)].sort(),
|
|
119
|
+
activeQueuePhase: record.activeQueuePhase === true,
|
|
120
|
+
pendingGateId: typeof record.pendingGateId === "string" ? record.pendingGateId : null,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function loadWriteGateSnapshot(basePath: string = process.cwd()): WriteGateSnapshot {
|
|
125
|
+
const path = writeGateSnapshotPath(basePath);
|
|
126
|
+
if (!existsSync(path)) return currentWriteGateSnapshot();
|
|
127
|
+
try {
|
|
128
|
+
return normalizeWriteGateSnapshot(JSON.parse(readFileSync(path, "utf-8")));
|
|
129
|
+
} catch {
|
|
130
|
+
return currentWriteGateSnapshot();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
68
134
|
export function isDepthVerified(): boolean {
|
|
69
135
|
return verifiedDepthMilestones.size > 0;
|
|
70
136
|
}
|
|
@@ -77,28 +143,40 @@ export function isMilestoneDepthVerified(milestoneId: string | null | undefined)
|
|
|
77
143
|
return verifiedDepthMilestones.has(milestoneId);
|
|
78
144
|
}
|
|
79
145
|
|
|
146
|
+
export function isMilestoneDepthVerifiedInSnapshot(
|
|
147
|
+
snapshot: WriteGateSnapshot,
|
|
148
|
+
milestoneId: string | null | undefined,
|
|
149
|
+
): boolean {
|
|
150
|
+
if (!milestoneId) return false;
|
|
151
|
+
return snapshot.verifiedDepthMilestones.includes(milestoneId);
|
|
152
|
+
}
|
|
153
|
+
|
|
80
154
|
export function isQueuePhaseActive(): boolean {
|
|
81
155
|
return activeQueuePhase;
|
|
82
156
|
}
|
|
83
157
|
|
|
84
158
|
export function setQueuePhaseActive(active: boolean): void {
|
|
85
159
|
activeQueuePhase = active;
|
|
160
|
+
persistWriteGateSnapshot();
|
|
86
161
|
}
|
|
87
162
|
|
|
88
163
|
export function resetWriteGateState(): void {
|
|
89
164
|
verifiedDepthMilestones.clear();
|
|
90
165
|
pendingGateId = null;
|
|
166
|
+
persistWriteGateSnapshot();
|
|
91
167
|
}
|
|
92
168
|
|
|
93
169
|
export function clearDiscussionFlowState(): void {
|
|
94
170
|
verifiedDepthMilestones.clear();
|
|
95
171
|
activeQueuePhase = false;
|
|
96
172
|
pendingGateId = null;
|
|
173
|
+
clearPersistedWriteGateSnapshot();
|
|
97
174
|
}
|
|
98
175
|
|
|
99
|
-
export function markDepthVerified(milestoneId?: string | null): void {
|
|
176
|
+
export function markDepthVerified(milestoneId?: string | null, basePath: string = process.cwd()): void {
|
|
100
177
|
if (!milestoneId) return;
|
|
101
178
|
verifiedDepthMilestones.add(milestoneId);
|
|
179
|
+
persistWriteGateSnapshot(basePath);
|
|
102
180
|
}
|
|
103
181
|
|
|
104
182
|
/**
|
|
@@ -130,6 +208,7 @@ function extractContextMilestoneId(inputPath: string): string | null {
|
|
|
130
208
|
*/
|
|
131
209
|
export function setPendingGate(gateId: string): void {
|
|
132
210
|
pendingGateId = gateId;
|
|
211
|
+
persistWriteGateSnapshot();
|
|
133
212
|
}
|
|
134
213
|
|
|
135
214
|
/**
|
|
@@ -137,6 +216,7 @@ export function setPendingGate(gateId: string): void {
|
|
|
137
216
|
*/
|
|
138
217
|
export function clearPendingGate(): void {
|
|
139
218
|
pendingGateId = null;
|
|
219
|
+
persistWriteGateSnapshot();
|
|
140
220
|
}
|
|
141
221
|
|
|
142
222
|
/**
|
|
@@ -154,11 +234,20 @@ export function getPendingGate(): string | null {
|
|
|
154
234
|
* Read-only tools and ask_user_questions itself are always allowed.
|
|
155
235
|
*/
|
|
156
236
|
export function shouldBlockPendingGate(
|
|
237
|
+
toolName: string,
|
|
238
|
+
milestoneId: string | null,
|
|
239
|
+
queuePhaseActive?: boolean,
|
|
240
|
+
): { block: boolean; reason?: string } {
|
|
241
|
+
return shouldBlockPendingGateInSnapshot(currentWriteGateSnapshot(), toolName, milestoneId, queuePhaseActive);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function shouldBlockPendingGateInSnapshot(
|
|
245
|
+
snapshot: WriteGateSnapshot,
|
|
157
246
|
toolName: string,
|
|
158
247
|
_milestoneId: string | null,
|
|
159
248
|
_queuePhaseActive?: boolean,
|
|
160
249
|
): { block: boolean; reason?: string } {
|
|
161
|
-
if (!pendingGateId) return { block: false };
|
|
250
|
+
if (!snapshot.pendingGateId) return { block: false };
|
|
162
251
|
|
|
163
252
|
if (GATE_SAFE_TOOLS.has(toolName)) return { block: false };
|
|
164
253
|
|
|
@@ -168,7 +257,7 @@ export function shouldBlockPendingGate(
|
|
|
168
257
|
return {
|
|
169
258
|
block: true,
|
|
170
259
|
reason: [
|
|
171
|
-
`HARD BLOCK: Discussion gate "${pendingGateId}" has not been confirmed by the user.`,
|
|
260
|
+
`HARD BLOCK: Discussion gate "${snapshot.pendingGateId}" has not been confirmed by the user.`,
|
|
172
261
|
`You MUST re-call ask_user_questions with the gate question before making any other tool calls.`,
|
|
173
262
|
`If the previous ask_user_questions call failed, errored, was cancelled, or the user's response`,
|
|
174
263
|
`did not match a provided option, you MUST re-ask — never rationalize past the block.`,
|
|
@@ -182,11 +271,20 @@ export function shouldBlockPendingGate(
|
|
|
182
271
|
* Read-only bash commands are allowed; mutating commands are blocked.
|
|
183
272
|
*/
|
|
184
273
|
export function shouldBlockPendingGateBash(
|
|
274
|
+
command: string,
|
|
275
|
+
milestoneId: string | null,
|
|
276
|
+
queuePhaseActive?: boolean,
|
|
277
|
+
): { block: boolean; reason?: string } {
|
|
278
|
+
return shouldBlockPendingGateBashInSnapshot(currentWriteGateSnapshot(), command, milestoneId, queuePhaseActive);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function shouldBlockPendingGateBashInSnapshot(
|
|
282
|
+
snapshot: WriteGateSnapshot,
|
|
185
283
|
command: string,
|
|
186
284
|
_milestoneId: string | null,
|
|
187
285
|
_queuePhaseActive?: boolean,
|
|
188
286
|
): { block: boolean; reason?: string } {
|
|
189
|
-
if (!pendingGateId) return { block: false };
|
|
287
|
+
if (!snapshot.pendingGateId) return { block: false };
|
|
190
288
|
|
|
191
289
|
// Allow read-only bash commands
|
|
192
290
|
if (BASH_READ_ONLY_RE.test(command)) return { block: false };
|
|
@@ -194,7 +292,7 @@ export function shouldBlockPendingGateBash(
|
|
|
194
292
|
return {
|
|
195
293
|
block: true,
|
|
196
294
|
reason: [
|
|
197
|
-
`HARD BLOCK: Discussion gate "${pendingGateId}" has not been confirmed by the user.`,
|
|
295
|
+
`HARD BLOCK: Discussion gate "${snapshot.pendingGateId}" has not been confirmed by the user.`,
|
|
198
296
|
`You MUST re-call ask_user_questions with the gate question before running mutating commands.`,
|
|
199
297
|
`If the previous ask_user_questions call failed, errored, was cancelled, or the user's response`,
|
|
200
298
|
`did not match a provided option, you MUST re-ask — never rationalize past the block.`,
|
|
@@ -275,6 +373,15 @@ export function shouldBlockContextArtifactSave(
|
|
|
275
373
|
artifactType: string,
|
|
276
374
|
milestoneId: string | null,
|
|
277
375
|
sliceId?: string | null,
|
|
376
|
+
): { block: boolean; reason?: string } {
|
|
377
|
+
return shouldBlockContextArtifactSaveInSnapshot(currentWriteGateSnapshot(), artifactType, milestoneId, sliceId);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export function shouldBlockContextArtifactSaveInSnapshot(
|
|
381
|
+
snapshot: WriteGateSnapshot,
|
|
382
|
+
artifactType: string,
|
|
383
|
+
milestoneId: string | null,
|
|
384
|
+
sliceId?: string | null,
|
|
278
385
|
): { block: boolean; reason?: string } {
|
|
279
386
|
if (artifactType !== "CONTEXT") return { block: false };
|
|
280
387
|
if (sliceId) return { block: false };
|
|
@@ -287,7 +394,7 @@ export function shouldBlockContextArtifactSave(
|
|
|
287
394
|
].join(" "),
|
|
288
395
|
};
|
|
289
396
|
}
|
|
290
|
-
if (
|
|
397
|
+
if (isMilestoneDepthVerifiedInSnapshot(snapshot, milestoneId)) return { block: false };
|
|
291
398
|
|
|
292
399
|
return {
|
|
293
400
|
block: true,
|
|
@@ -317,6 +424,15 @@ export function shouldBlockQueueExecution(
|
|
|
317
424
|
toolName: string,
|
|
318
425
|
input: string,
|
|
319
426
|
queuePhaseActive: boolean,
|
|
427
|
+
): { block: boolean; reason?: string } {
|
|
428
|
+
return shouldBlockQueueExecutionInSnapshot(currentWriteGateSnapshot(), toolName, input, queuePhaseActive);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
export function shouldBlockQueueExecutionInSnapshot(
|
|
432
|
+
snapshot: WriteGateSnapshot,
|
|
433
|
+
toolName: string,
|
|
434
|
+
input: string,
|
|
435
|
+
queuePhaseActive: boolean = snapshot.activeQueuePhase,
|
|
320
436
|
): { block: boolean; reason?: string } {
|
|
321
437
|
if (!queuePhaseActive) return { block: false };
|
|
322
438
|
|
|
@@ -194,6 +194,56 @@ function sortModelsForSelection(models: Model<any>[], currentModel: Model<any> |
|
|
|
194
194
|
});
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
+
function buildProviderModelGroups(
|
|
198
|
+
models: Model<any>[],
|
|
199
|
+
currentModel: Model<any> | undefined,
|
|
200
|
+
): Map<string, Model<any>[]> {
|
|
201
|
+
const byProvider = new Map<string, Model<any>[]>();
|
|
202
|
+
|
|
203
|
+
for (const model of sortModelsForSelection(models, currentModel)) {
|
|
204
|
+
let group = byProvider.get(model.provider);
|
|
205
|
+
if (!group) {
|
|
206
|
+
group = [];
|
|
207
|
+
byProvider.set(model.provider, group);
|
|
208
|
+
}
|
|
209
|
+
group.push(model);
|
|
210
|
+
}
|
|
211
|
+
return byProvider;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function selectModelByProvider(
|
|
215
|
+
title: string,
|
|
216
|
+
models: Model<any>[],
|
|
217
|
+
ctx: ExtensionCommandContext,
|
|
218
|
+
currentModel: Model<any> | undefined,
|
|
219
|
+
): Promise<Model<any> | undefined> {
|
|
220
|
+
const byProvider = buildProviderModelGroups(models, currentModel);
|
|
221
|
+
const providerOptions = Array.from(byProvider.entries()).map(([provider, group]) =>
|
|
222
|
+
`${provider} (${group.length} model${group.length === 1 ? "" : "s"})`,
|
|
223
|
+
);
|
|
224
|
+
providerOptions.push("(cancel)");
|
|
225
|
+
|
|
226
|
+
const providerChoice = await ctx.ui.select(`${title} — choose provider:`, providerOptions);
|
|
227
|
+
if (!providerChoice || typeof providerChoice !== "string" || providerChoice === "(cancel)") return undefined;
|
|
228
|
+
|
|
229
|
+
const providerName = providerChoice.replace(/ \(\d+ models?\)$/, "");
|
|
230
|
+
const providerModels = byProvider.get(providerName);
|
|
231
|
+
if (!providerModels || providerModels.length === 0) return undefined;
|
|
232
|
+
|
|
233
|
+
const optionToModel = new Map<string, Model<any>>();
|
|
234
|
+
const modelOptions = providerModels.map((model) => {
|
|
235
|
+
const isCurrent = currentModel && model.provider === currentModel.provider && model.id === currentModel.id;
|
|
236
|
+
const label = `${isCurrent ? "* " : ""}${model.id}`;
|
|
237
|
+
optionToModel.set(label, model);
|
|
238
|
+
return label;
|
|
239
|
+
});
|
|
240
|
+
modelOptions.push("(cancel)");
|
|
241
|
+
|
|
242
|
+
const modelChoice = await ctx.ui.select(`${title} — ${providerName}:`, modelOptions);
|
|
243
|
+
if (!modelChoice || typeof modelChoice !== "string" || modelChoice === "(cancel)") return undefined;
|
|
244
|
+
return optionToModel.get(modelChoice);
|
|
245
|
+
}
|
|
246
|
+
|
|
197
247
|
async function resolveRequestedModel(
|
|
198
248
|
query: string,
|
|
199
249
|
ctx: ExtensionCommandContext,
|
|
@@ -211,19 +261,7 @@ async function resolveRequestedModel(
|
|
|
211
261
|
|
|
212
262
|
if (partialMatches.length === 1) return partialMatches[0];
|
|
213
263
|
if (partialMatches.length === 0 || !ctx.hasUI) return undefined;
|
|
214
|
-
|
|
215
|
-
const sorted = sortModelsForSelection(partialMatches, ctx.model);
|
|
216
|
-
const optionToModel = new Map<string, Model<any>>();
|
|
217
|
-
const options = sorted.map((model) => {
|
|
218
|
-
const label = `${model.provider}/${model.id}`;
|
|
219
|
-
optionToModel.set(label, model);
|
|
220
|
-
return label;
|
|
221
|
-
});
|
|
222
|
-
options.push("(cancel)");
|
|
223
|
-
|
|
224
|
-
const choice = await ctx.ui.select(`Multiple models match "${query}" — choose one:`, options);
|
|
225
|
-
if (!choice || typeof choice !== "string" || choice === "(cancel)") return undefined;
|
|
226
|
-
return optionToModel.get(choice);
|
|
264
|
+
return selectModelByProvider(`Multiple models match "${query}"`, partialMatches, ctx, ctx.model);
|
|
227
265
|
}
|
|
228
266
|
|
|
229
267
|
async function handleModel(trimmedArgs: string, ctx: ExtensionCommandContext, pi: ExtensionAPI | undefined): Promise<void> {
|
|
@@ -247,18 +285,7 @@ async function handleModel(trimmedArgs: string, ctx: ExtensionCommandContext, pi
|
|
|
247
285
|
return;
|
|
248
286
|
}
|
|
249
287
|
|
|
250
|
-
|
|
251
|
-
const options = sortModelsForSelection(availableModels, ctx.model).map((model) => {
|
|
252
|
-
const isCurrent = ctx.model && model.provider === ctx.model.provider && model.id === ctx.model.id;
|
|
253
|
-
const label = `${isCurrent ? "* " : ""}${model.provider}/${model.id}`;
|
|
254
|
-
optionToModel.set(label, model);
|
|
255
|
-
return label;
|
|
256
|
-
});
|
|
257
|
-
options.push("(cancel)");
|
|
258
|
-
|
|
259
|
-
const choice = await ctx.ui.select("Select session model:", options);
|
|
260
|
-
if (!choice || typeof choice !== "string" || choice === "(cancel)") return;
|
|
261
|
-
targetModel = optionToModel.get(choice);
|
|
288
|
+
targetModel = await selectModelByProvider("Select session model:", availableModels, ctx, ctx.model);
|
|
262
289
|
} else {
|
|
263
290
|
targetModel = await resolveRequestedModel(trimmed, ctx);
|
|
264
291
|
}
|
|
@@ -8,7 +8,13 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
|
|
|
8
8
|
getArgumentCompletions: getGsdArgumentCompletions,
|
|
9
9
|
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
10
10
|
const { handleGSDCommand } = await import("./dispatcher.js");
|
|
11
|
-
await
|
|
11
|
+
const { setStderrLoggingEnabled } = await import("../workflow-logger.js");
|
|
12
|
+
const previousStderrSetting = setStderrLoggingEnabled(false);
|
|
13
|
+
try {
|
|
14
|
+
await handleGSDCommand(args, ctx, pi);
|
|
15
|
+
} finally {
|
|
16
|
+
setStderrLoggingEnabled(previousStderrSetting);
|
|
17
|
+
}
|
|
12
18
|
},
|
|
13
19
|
});
|
|
14
20
|
}
|
|
@@ -40,6 +40,10 @@ import { findMilestoneIds, nextMilestoneId, reserveMilestoneId, getReservedMiles
|
|
|
40
40
|
import { parkMilestone, discardMilestone } from "./milestone-actions.js";
|
|
41
41
|
import { selectAndApplyModel } from "./auto-model-selection.js";
|
|
42
42
|
import { DISCUSS_TOOLS_ALLOWLIST } from "./constants.js";
|
|
43
|
+
import {
|
|
44
|
+
getWorkflowTransportSupportError,
|
|
45
|
+
getRequiredWorkflowToolsForGuidedUnit,
|
|
46
|
+
} from "./workflow-mcp.js";
|
|
43
47
|
import {
|
|
44
48
|
runPreparation,
|
|
45
49
|
formatCodebaseBrief,
|
|
@@ -318,6 +322,26 @@ async function dispatchWorkflow(
|
|
|
318
322
|
routing: result.routing,
|
|
319
323
|
});
|
|
320
324
|
}
|
|
325
|
+
|
|
326
|
+
const compatibilityError = getWorkflowTransportSupportError(
|
|
327
|
+
result.appliedModel?.provider ?? ctx.model?.provider,
|
|
328
|
+
getRequiredWorkflowToolsForGuidedUnit(unitType),
|
|
329
|
+
{
|
|
330
|
+
projectRoot: process.cwd(),
|
|
331
|
+
surface: "guided flow",
|
|
332
|
+
unitType,
|
|
333
|
+
authMode: result.appliedModel?.provider
|
|
334
|
+
? ctx.modelRegistry.getProviderAuthMode(result.appliedModel.provider)
|
|
335
|
+
: ctx.model?.provider
|
|
336
|
+
? ctx.modelRegistry.getProviderAuthMode(ctx.model.provider)
|
|
337
|
+
: undefined,
|
|
338
|
+
baseUrl: result.appliedModel?.baseUrl ?? ctx.model?.baseUrl,
|
|
339
|
+
},
|
|
340
|
+
);
|
|
341
|
+
if (compatibilityError) {
|
|
342
|
+
ctx.ui.notify(compatibilityError, "error");
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
321
345
|
}
|
|
322
346
|
|
|
323
347
|
// Scope tools for discuss flows (#2949).
|
|
@@ -235,6 +235,20 @@ export async function showProjectInit(
|
|
|
235
235
|
// ── Step 9: Bootstrap .gsd/ ────────────────────────────────────────────────
|
|
236
236
|
bootstrapGsdDirectory(basePath, prefs, signals);
|
|
237
237
|
|
|
238
|
+
// Initialize SQLite database so GSD starts in full-capability mode (#3880).
|
|
239
|
+
// Without this, isDbAvailable() returns false and GSD enters degraded
|
|
240
|
+
// markdown-only mode until a tool handler happens to call ensureDbOpen().
|
|
241
|
+
let dbReady = false;
|
|
242
|
+
try {
|
|
243
|
+
const { ensureDbOpen } = await import("./bootstrap/dynamic-tools.js");
|
|
244
|
+
dbReady = await ensureDbOpen(basePath);
|
|
245
|
+
} catch {
|
|
246
|
+
// Swallowed — warning surfaced below
|
|
247
|
+
}
|
|
248
|
+
if (!dbReady) {
|
|
249
|
+
ctx.ui.notify("Warning: database initialization failed — GSD will run in degraded mode until the next /gsd invocation.", "warning");
|
|
250
|
+
}
|
|
251
|
+
|
|
238
252
|
// Ensure .gitignore
|
|
239
253
|
ensureGitignore(basePath);
|
|
240
254
|
untrackRuntimeFiles(basePath);
|
|
@@ -250,6 +264,25 @@ export async function showProjectInit(
|
|
|
250
264
|
// Non-fatal — codebase map generation failure should never block project init
|
|
251
265
|
}
|
|
252
266
|
|
|
267
|
+
// Write initial STATE.md so it exists before the first /gsd invocation.
|
|
268
|
+
// The explicit /gsd init path (ops.ts) returns without entering showSmartEntry(),
|
|
269
|
+
// which would otherwise generate STATE.md at guided-flow.ts:1358.
|
|
270
|
+
let stateReady = false;
|
|
271
|
+
try {
|
|
272
|
+
const { deriveState } = await import("./state.js");
|
|
273
|
+
const { buildStateMarkdown } = await import("./doctor.js");
|
|
274
|
+
const { saveFile } = await import("./files.js");
|
|
275
|
+
const { resolveGsdRootFile } = await import("./paths.js");
|
|
276
|
+
const state = await deriveState(basePath);
|
|
277
|
+
await saveFile(resolveGsdRootFile(basePath, "STATE"), buildStateMarkdown(state));
|
|
278
|
+
stateReady = true;
|
|
279
|
+
} catch {
|
|
280
|
+
// Swallowed — warning surfaced below
|
|
281
|
+
}
|
|
282
|
+
if (!stateReady) {
|
|
283
|
+
ctx.ui.notify("Warning: initial STATE.md generation failed — it will be created on the next /gsd invocation.", "warning");
|
|
284
|
+
}
|
|
285
|
+
|
|
253
286
|
ctx.ui.notify("GSD initialized. Starting your first milestone...", "info");
|
|
254
287
|
|
|
255
288
|
return { completed: true, bootstrapped: true };
|
|
@@ -433,6 +466,7 @@ function bootstrapGsdDirectory(
|
|
|
433
466
|
|
|
434
467
|
const gsd = gsdRoot(basePath);
|
|
435
468
|
mkdirSync(join(gsd, "milestones"), { recursive: true });
|
|
469
|
+
mkdirSync(join(gsd, "runtime"), { recursive: true });
|
|
436
470
|
|
|
437
471
|
// Write PREFERENCES.md from wizard answers
|
|
438
472
|
const preferencesContent = buildPreferencesFile(prefs);
|
|
@@ -74,3 +74,104 @@ test("model command resolves and persists exact provider-qualified selection", a
|
|
|
74
74
|
assert.deepEqual(applied, selectedModel);
|
|
75
75
|
assert.match(notices[0]!.message, /openai\/gpt-5\.4/);
|
|
76
76
|
});
|
|
77
|
+
|
|
78
|
+
test("interactive model picker chooses provider first, then model", async () => {
|
|
79
|
+
const selectedModel = { provider: "openai", id: "gpt-5.4" };
|
|
80
|
+
let applied: typeof selectedModel | null = null;
|
|
81
|
+
const selects: Array<{ title: string; options: string[] }> = [];
|
|
82
|
+
const notices: Array<{ message: string; type?: string }> = [];
|
|
83
|
+
|
|
84
|
+
const ctx = {
|
|
85
|
+
hasUI: true,
|
|
86
|
+
model: { provider: "anthropic", id: "claude-sonnet-4-6" },
|
|
87
|
+
modelRegistry: {
|
|
88
|
+
getAvailable: () => [
|
|
89
|
+
{ provider: "openai", id: "gpt-5.4" },
|
|
90
|
+
{ provider: "anthropic", id: "claude-opus-4-6" },
|
|
91
|
+
{ provider: "openai", id: "gpt-5.3-mini" },
|
|
92
|
+
{ provider: "anthropic", id: "claude-sonnet-4-6" },
|
|
93
|
+
],
|
|
94
|
+
},
|
|
95
|
+
ui: {
|
|
96
|
+
select: async (title: string, options: string[]) => {
|
|
97
|
+
selects.push({ title, options });
|
|
98
|
+
return selects.length === 1 ? "openai (2 models)" : "gpt-5.4";
|
|
99
|
+
},
|
|
100
|
+
notify: (message: string, type?: string) => {
|
|
101
|
+
notices.push({ message, type });
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
} as any;
|
|
105
|
+
|
|
106
|
+
const pi = {
|
|
107
|
+
setModel: async (model: typeof selectedModel) => {
|
|
108
|
+
applied = model;
|
|
109
|
+
return true;
|
|
110
|
+
},
|
|
111
|
+
} as any;
|
|
112
|
+
|
|
113
|
+
const handled = await handleCoreCommand("model", ctx, pi);
|
|
114
|
+
assert.equal(handled, true);
|
|
115
|
+
assert.deepEqual(selects, [
|
|
116
|
+
{
|
|
117
|
+
title: "Select session model: — choose provider:",
|
|
118
|
+
options: ["anthropic (2 models)", "openai (2 models)", "(cancel)"],
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
title: "Select session model: — openai:",
|
|
122
|
+
options: ["gpt-5.3-mini", "gpt-5.4", "(cancel)"],
|
|
123
|
+
},
|
|
124
|
+
]);
|
|
125
|
+
assert.deepEqual(applied, selectedModel);
|
|
126
|
+
assert.match(notices[0]!.message, /openai\/gpt-5\.4/);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("ambiguous typed model selection chooses provider first, then model", async () => {
|
|
130
|
+
const selectedModel = { provider: "github-copilot", id: "gpt-5" };
|
|
131
|
+
let applied: typeof selectedModel | null = null;
|
|
132
|
+
const selects: Array<{ title: string; options: string[] }> = [];
|
|
133
|
+
const notices: Array<{ message: string; type?: string }> = [];
|
|
134
|
+
|
|
135
|
+
const ctx = {
|
|
136
|
+
hasUI: true,
|
|
137
|
+
model: { provider: "anthropic", id: "claude-sonnet-4-6" },
|
|
138
|
+
modelRegistry: {
|
|
139
|
+
getAvailable: () => [
|
|
140
|
+
{ provider: "openai", id: "gpt-5" },
|
|
141
|
+
{ provider: "github-copilot", id: "gpt-5" },
|
|
142
|
+
{ provider: "openai", id: "gpt-5-mini" },
|
|
143
|
+
],
|
|
144
|
+
},
|
|
145
|
+
ui: {
|
|
146
|
+
select: async (title: string, options: string[]) => {
|
|
147
|
+
selects.push({ title, options });
|
|
148
|
+
return selects.length === 1 ? "github-copilot (1 model)" : "gpt-5";
|
|
149
|
+
},
|
|
150
|
+
notify: (message: string, type?: string) => {
|
|
151
|
+
notices.push({ message, type });
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
} as any;
|
|
155
|
+
|
|
156
|
+
const pi = {
|
|
157
|
+
setModel: async (model: typeof selectedModel) => {
|
|
158
|
+
applied = model;
|
|
159
|
+
return true;
|
|
160
|
+
},
|
|
161
|
+
} as any;
|
|
162
|
+
|
|
163
|
+
const handled = await handleCoreCommand("model gpt", ctx, pi);
|
|
164
|
+
assert.equal(handled, true);
|
|
165
|
+
assert.deepEqual(selects, [
|
|
166
|
+
{
|
|
167
|
+
title: "Multiple models match \"gpt\" — choose provider:",
|
|
168
|
+
options: ["github-copilot (1 model)", "openai (2 models)", "(cancel)"],
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
title: "Multiple models match \"gpt\" — github-copilot:",
|
|
172
|
+
options: ["gpt-5", "(cancel)"],
|
|
173
|
+
},
|
|
174
|
+
]);
|
|
175
|
+
assert.deepEqual(applied, selectedModel);
|
|
176
|
+
assert.match(notices[0]!.message, /github-copilot\/gpt-5/);
|
|
177
|
+
});
|
|
@@ -77,6 +77,36 @@ describe('ensure-db-open', () => {
|
|
|
77
77
|
}
|
|
78
78
|
});
|
|
79
79
|
|
|
80
|
+
test('ensureDbOpen: explicit basePath opens target project without cwd override', async () => {
|
|
81
|
+
const tmpDir = makeTmpDir();
|
|
82
|
+
const gsdDir = path.join(tmpDir, '.gsd');
|
|
83
|
+
fs.mkdirSync(gsdDir, { recursive: true });
|
|
84
|
+
fs.writeFileSync(path.join(gsdDir, 'DECISIONS.md'), `# Decisions
|
|
85
|
+
|
|
86
|
+
| # | When | Scope | Decision | Choice | Rationale | Revisable |
|
|
87
|
+
|---|------|-------|----------|--------|-----------|-----------|
|
|
88
|
+
| D777 | M001 | architecture | Use explicit basePath | BasePath | Avoid cwd coupling | Yes |
|
|
89
|
+
`);
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
closeDatabase();
|
|
93
|
+
} catch { /* ok */ }
|
|
94
|
+
|
|
95
|
+
const originalCwd = process.cwd();
|
|
96
|
+
try {
|
|
97
|
+
const { ensureDbOpen } = await import('../bootstrap/dynamic-tools.ts');
|
|
98
|
+
const result = await ensureDbOpen(tmpDir);
|
|
99
|
+
|
|
100
|
+
assert.ok(result === true, 'ensureDbOpen should honor explicit basePath');
|
|
101
|
+
assert.equal(process.cwd(), originalCwd, 'ensureDbOpen should not mutate process.cwd');
|
|
102
|
+
assert.ok(isDbAvailable(), 'DB should be available after explicit open');
|
|
103
|
+
assert.ok(getDecisionById('D777') !== null, 'explicit basePath DB should be opened');
|
|
104
|
+
} finally {
|
|
105
|
+
closeDatabase();
|
|
106
|
+
cleanupDir(tmpDir);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
80
110
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
81
111
|
// ensureDbOpen returns false when no .gsd/ exists
|
|
82
112
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -159,6 +189,42 @@ describe('ensure-db-open', () => {
|
|
|
159
189
|
}
|
|
160
190
|
});
|
|
161
191
|
|
|
192
|
+
test('ensureDbOpen: switches open database when basePath changes', async () => {
|
|
193
|
+
const firstDir = makeTmpDir();
|
|
194
|
+
const secondDir = makeTmpDir();
|
|
195
|
+
fs.mkdirSync(path.join(firstDir, '.gsd'), { recursive: true });
|
|
196
|
+
fs.mkdirSync(path.join(secondDir, '.gsd'), { recursive: true });
|
|
197
|
+
fs.writeFileSync(path.join(firstDir, '.gsd', 'DECISIONS.md'), `# Decisions
|
|
198
|
+
|
|
199
|
+
| # | When | Scope | Decision | Choice | Rationale | Revisable |
|
|
200
|
+
|---|------|-------|----------|--------|-----------|-----------|
|
|
201
|
+
| D101 | M001 | architecture | First DB | First | First rationale | Yes |
|
|
202
|
+
`);
|
|
203
|
+
fs.writeFileSync(path.join(secondDir, '.gsd', 'DECISIONS.md'), `# Decisions
|
|
204
|
+
|
|
205
|
+
| # | When | Scope | Decision | Choice | Rationale | Revisable |
|
|
206
|
+
|---|------|-------|----------|--------|-----------|-----------|
|
|
207
|
+
| D202 | M001 | architecture | Second DB | Second | Second rationale | Yes |
|
|
208
|
+
`);
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
closeDatabase();
|
|
212
|
+
} catch { /* ok */ }
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
const { ensureDbOpen } = await import('../bootstrap/dynamic-tools.ts');
|
|
216
|
+
assert.equal(await ensureDbOpen(firstDir), true);
|
|
217
|
+
assert.ok(getDecisionById('D101') !== null, 'first DB should be active');
|
|
218
|
+
assert.equal(await ensureDbOpen(secondDir), true);
|
|
219
|
+
assert.ok(getDecisionById('D202') !== null, 'second DB should be active after switch');
|
|
220
|
+
assert.equal(getDecisionById('D101'), null, 'first DB should no longer be active after switch');
|
|
221
|
+
} finally {
|
|
222
|
+
closeDatabase();
|
|
223
|
+
cleanupDir(firstDir);
|
|
224
|
+
cleanupDir(secondDir);
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
162
228
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
163
229
|
|
|
164
230
|
});
|