gsd-pi 2.38.0-dev.63ad7e5 → 2.38.0-dev.7209774
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/resource-loader.js +34 -1
- package/dist/resources/extensions/browser-tools/index.js +3 -1
- package/dist/resources/extensions/browser-tools/tools/verify.js +97 -0
- package/dist/resources/extensions/github-sync/cli.js +284 -0
- package/dist/resources/extensions/github-sync/index.js +73 -0
- package/dist/resources/extensions/github-sync/mapping.js +67 -0
- package/dist/resources/extensions/github-sync/sync.js +424 -0
- package/dist/resources/extensions/github-sync/templates.js +118 -0
- package/dist/resources/extensions/github-sync/types.js +7 -0
- package/dist/resources/extensions/gsd/auto-dispatch.js +1 -1
- package/dist/resources/extensions/gsd/auto-loop.js +593 -516
- package/dist/resources/extensions/gsd/auto-post-unit.js +28 -3
- package/dist/resources/extensions/gsd/auto-prompts.js +26 -15
- package/dist/resources/extensions/gsd/auto-worktree.js +3 -3
- package/dist/resources/extensions/gsd/commands.js +2 -1
- package/dist/resources/extensions/gsd/doctor.js +20 -1
- package/dist/resources/extensions/gsd/exit-command.js +2 -1
- package/dist/resources/extensions/gsd/files.js +4 -0
- package/dist/resources/extensions/gsd/git-service.js +30 -12
- package/dist/resources/extensions/gsd/guided-flow.js +82 -32
- package/dist/resources/extensions/gsd/index.js +22 -19
- package/dist/resources/extensions/gsd/native-git-bridge.js +37 -0
- package/dist/resources/extensions/gsd/paths.js +3 -0
- package/dist/resources/extensions/gsd/preferences-types.js +1 -0
- package/dist/resources/extensions/gsd/preferences-validation.js +58 -0
- package/dist/resources/extensions/gsd/preferences.js +3 -0
- package/dist/resources/extensions/gsd/prompts/execute-task.md +2 -0
- package/dist/resources/extensions/gsd/prompts/run-uat.md +2 -0
- package/dist/resources/extensions/gsd/roadmap-mutations.js +24 -0
- package/dist/resources/extensions/gsd/templates/runtime.md +21 -0
- package/dist/resources/extensions/mcp-client/index.js +14 -1
- package/package.json +1 -1
- package/packages/pi-ai/dist/utils/oauth/anthropic.js +2 -2
- package/packages/pi-ai/dist/utils/oauth/anthropic.js.map +1 -1
- package/packages/pi-ai/src/utils/oauth/anthropic.ts +2 -2
- package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.js +205 -7
- package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
- package/packages/pi-coding-agent/src/core/extensions/loader.ts +223 -7
- package/src/resources/extensions/browser-tools/index.ts +3 -0
- package/src/resources/extensions/browser-tools/tools/verify.ts +117 -0
- package/src/resources/extensions/github-sync/cli.ts +364 -0
- package/src/resources/extensions/github-sync/index.ts +93 -0
- package/src/resources/extensions/github-sync/mapping.ts +81 -0
- package/src/resources/extensions/github-sync/sync.ts +556 -0
- package/src/resources/extensions/github-sync/templates.ts +183 -0
- package/src/resources/extensions/github-sync/tests/cli.test.ts +20 -0
- package/src/resources/extensions/github-sync/tests/commit-linking.test.ts +39 -0
- package/src/resources/extensions/github-sync/tests/mapping.test.ts +104 -0
- package/src/resources/extensions/github-sync/tests/templates.test.ts +110 -0
- package/src/resources/extensions/github-sync/types.ts +47 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +1 -1
- package/src/resources/extensions/gsd/auto-loop.ts +472 -434
- package/src/resources/extensions/gsd/auto-post-unit.ts +29 -3
- package/src/resources/extensions/gsd/auto-prompts.ts +29 -15
- package/src/resources/extensions/gsd/auto-worktree.ts +3 -3
- package/src/resources/extensions/gsd/commands.ts +2 -2
- package/src/resources/extensions/gsd/doctor.ts +22 -1
- package/src/resources/extensions/gsd/exit-command.ts +2 -2
- package/src/resources/extensions/gsd/files.ts +3 -1
- package/src/resources/extensions/gsd/git-service.ts +44 -10
- package/src/resources/extensions/gsd/guided-flow.ts +110 -38
- package/src/resources/extensions/gsd/index.ts +21 -16
- package/src/resources/extensions/gsd/native-git-bridge.ts +37 -0
- package/src/resources/extensions/gsd/paths.ts +4 -0
- package/src/resources/extensions/gsd/preferences-types.ts +4 -0
- package/src/resources/extensions/gsd/preferences-validation.ts +50 -0
- package/src/resources/extensions/gsd/preferences.ts +3 -0
- package/src/resources/extensions/gsd/prompts/execute-task.md +2 -0
- package/src/resources/extensions/gsd/prompts/run-uat.md +2 -0
- package/src/resources/extensions/gsd/roadmap-mutations.ts +29 -0
- package/src/resources/extensions/gsd/templates/runtime.md +21 -0
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +111 -37
- package/src/resources/extensions/gsd/types.ts +8 -0
- package/src/resources/extensions/gsd/verification-evidence.ts +16 -0
- package/src/resources/extensions/mcp-client/index.ts +17 -1
|
@@ -34,6 +34,7 @@ import { showConfirm } from "../shared/mod.js";
|
|
|
34
34
|
import { debugLog } from "./debug-logger.js";
|
|
35
35
|
import { findMilestoneIds, nextMilestoneId } from "./milestone-ids.js";
|
|
36
36
|
import { parkMilestone, discardMilestone } from "./milestone-actions.js";
|
|
37
|
+
import { resolveModelWithFallbacksForUnit } from "./preferences-models.js";
|
|
37
38
|
|
|
38
39
|
// ─── Re-exports (preserve public API for existing importers) ────────────────
|
|
39
40
|
export {
|
|
@@ -190,8 +191,40 @@ type UIContext = ExtensionContext;
|
|
|
190
191
|
/**
|
|
191
192
|
* Read GSD-WORKFLOW.md and dispatch it to the LLM with a contextual note.
|
|
192
193
|
* This is the only way the wizard triggers work — everything else is the LLM's job.
|
|
194
|
+
*
|
|
195
|
+
* When a unitType is provided, resolves the user's model preference for that
|
|
196
|
+
* phase (e.g., models.planning → "plan-milestone") and applies it before
|
|
197
|
+
* dispatching. This ensures guided-flow dispatches respect the same
|
|
198
|
+
* per-phase model preferences that auto-mode uses.
|
|
193
199
|
*/
|
|
194
|
-
function dispatchWorkflow(
|
|
200
|
+
async function dispatchWorkflow(
|
|
201
|
+
pi: ExtensionAPI,
|
|
202
|
+
note: string,
|
|
203
|
+
customType = "gsd-run",
|
|
204
|
+
ctx?: ExtensionContext,
|
|
205
|
+
unitType?: string,
|
|
206
|
+
): Promise<void> {
|
|
207
|
+
// Apply model preference for this unit type (if configured)
|
|
208
|
+
if (ctx && unitType) {
|
|
209
|
+
const modelConfig = resolveModelWithFallbacksForUnit(unitType);
|
|
210
|
+
if (modelConfig) {
|
|
211
|
+
const availableModels = ctx.modelRegistry.getAvailable();
|
|
212
|
+
const modelsToTry = [modelConfig.primary, ...modelConfig.fallbacks];
|
|
213
|
+
|
|
214
|
+
for (const modelId of modelsToTry) {
|
|
215
|
+
// Resolve model from available models (same logic as auto-model-selection)
|
|
216
|
+
const model = resolveAvailableModel(modelId, availableModels, ctx.model?.provider);
|
|
217
|
+
if (!model) continue;
|
|
218
|
+
|
|
219
|
+
const ok = await pi.setModel(model, { persist: false });
|
|
220
|
+
if (ok) {
|
|
221
|
+
debugLog("guided-flow-model-applied", { unitType, model: `${model.provider}/${model.id}` });
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
195
228
|
const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".gsd", "agent", "GSD-WORKFLOW.md");
|
|
196
229
|
const workflow = readFileSync(workflowPath, "utf-8");
|
|
197
230
|
|
|
@@ -205,6 +238,45 @@ function dispatchWorkflow(pi: ExtensionAPI, note: string, customType = "gsd-run"
|
|
|
205
238
|
);
|
|
206
239
|
}
|
|
207
240
|
|
|
241
|
+
/**
|
|
242
|
+
* Resolve a model ID string to a model object from available models.
|
|
243
|
+
* Handles "provider/model" and bare ID formats.
|
|
244
|
+
*/
|
|
245
|
+
function resolveAvailableModel<T extends { id: string; provider: string }>(
|
|
246
|
+
modelId: string,
|
|
247
|
+
availableModels: T[],
|
|
248
|
+
currentProvider: string | undefined,
|
|
249
|
+
): T | undefined {
|
|
250
|
+
const slashIdx = modelId.indexOf("/");
|
|
251
|
+
|
|
252
|
+
if (slashIdx !== -1) {
|
|
253
|
+
const maybeProvider = modelId.substring(0, slashIdx);
|
|
254
|
+
const id = modelId.substring(slashIdx + 1);
|
|
255
|
+
|
|
256
|
+
const knownProviders = new Set(availableModels.map(m => m.provider.toLowerCase()));
|
|
257
|
+
if (knownProviders.has(maybeProvider.toLowerCase())) {
|
|
258
|
+
const match = availableModels.find(
|
|
259
|
+
m => m.provider.toLowerCase() === maybeProvider.toLowerCase()
|
|
260
|
+
&& m.id.toLowerCase() === id.toLowerCase(),
|
|
261
|
+
);
|
|
262
|
+
if (match) return match;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Try matching the full string as a model ID (OpenRouter-style)
|
|
266
|
+
const lower = modelId.toLowerCase();
|
|
267
|
+
return availableModels.find(
|
|
268
|
+
m => m.id.toLowerCase() === lower
|
|
269
|
+
|| `${m.provider}/${m.id}`.toLowerCase() === lower,
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Bare ID — prefer current provider, then first available
|
|
274
|
+
const exactProviderMatch = availableModels.find(
|
|
275
|
+
m => m.id === modelId && m.provider === currentProvider,
|
|
276
|
+
);
|
|
277
|
+
return exactProviderMatch ?? availableModels.find(m => m.id === modelId);
|
|
278
|
+
}
|
|
279
|
+
|
|
208
280
|
/**
|
|
209
281
|
* Build the discuss-and-plan prompt for a new milestone.
|
|
210
282
|
* Used by all three "new milestone" paths (first ever, no active, all complete).
|
|
@@ -301,8 +373,8 @@ export async function showHeadlessMilestoneCreation(
|
|
|
301
373
|
// Set pending auto start (auto-mode triggers on "Milestone X ready." via checkAutoStartAfterDiscuss)
|
|
302
374
|
pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId };
|
|
303
375
|
|
|
304
|
-
// Dispatch
|
|
305
|
-
dispatchWorkflow(pi, prompt);
|
|
376
|
+
// Dispatch — headless milestone creation is a planning activity
|
|
377
|
+
await dispatchWorkflow(pi, prompt, "gsd-run", ctx, "plan-milestone");
|
|
306
378
|
}
|
|
307
379
|
|
|
308
380
|
|
|
@@ -467,21 +539,21 @@ export async function showDiscuss(
|
|
|
467
539
|
? `${basePrompt}\n\n## Prior Discussion (Draft Seed)\n\n${draftContent}`
|
|
468
540
|
: basePrompt;
|
|
469
541
|
pendingAutoStart = { ctx, pi, basePath, milestoneId: mid, step: false };
|
|
470
|
-
dispatchWorkflow(pi, seed, "gsd-discuss");
|
|
542
|
+
await dispatchWorkflow(pi, seed, "gsd-discuss", ctx, "plan-milestone");
|
|
471
543
|
} else if (choice === "discuss_fresh") {
|
|
472
544
|
const discussMilestoneTemplates = inlineTemplate("context", "Context");
|
|
473
545
|
const structuredQuestionsAvailable = pi.getActiveTools().includes("ask_user_questions") ? "true" : "false";
|
|
474
546
|
pendingAutoStart = { ctx, pi, basePath, milestoneId: mid, step: false };
|
|
475
|
-
dispatchWorkflow(pi, loadPrompt("guided-discuss-milestone", {
|
|
547
|
+
await dispatchWorkflow(pi, loadPrompt("guided-discuss-milestone", {
|
|
476
548
|
milestoneId: mid, milestoneTitle, inlinedTemplates: discussMilestoneTemplates, structuredQuestionsAvailable,
|
|
477
549
|
commitInstruction: buildDocsCommitInstruction(`docs(${mid}): milestone context from discuss`),
|
|
478
|
-
}), "gsd-discuss");
|
|
550
|
+
}), "gsd-discuss", ctx, "plan-milestone");
|
|
479
551
|
} else if (choice === "skip_milestone") {
|
|
480
552
|
const milestoneIds = findMilestoneIds(basePath);
|
|
481
553
|
const uniqueMilestoneIds = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
|
|
482
554
|
const nextId = nextMilestoneId(milestoneIds, uniqueMilestoneIds);
|
|
483
555
|
pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId, step: false };
|
|
484
|
-
dispatchWorkflow(pi, buildDiscussPrompt(nextId, `New milestone ${nextId}.`, basePath));
|
|
556
|
+
await dispatchWorkflow(pi, buildDiscussPrompt(nextId, `New milestone ${nextId}.`, basePath), "gsd-run", ctx, "plan-milestone");
|
|
485
557
|
}
|
|
486
558
|
return;
|
|
487
559
|
}
|
|
@@ -580,7 +652,7 @@ export async function showDiscuss(
|
|
|
580
652
|
}
|
|
581
653
|
|
|
582
654
|
const prompt = await buildDiscussSlicePrompt(mid, chosen.id, chosen.title, basePath, { rediscuss: isRediscuss });
|
|
583
|
-
dispatchWorkflow(pi, prompt, "gsd-discuss");
|
|
655
|
+
await dispatchWorkflow(pi, prompt, "gsd-discuss", ctx, "plan-slice");
|
|
584
656
|
|
|
585
657
|
// Wait for the discuss session to finish, then loop back to the picker
|
|
586
658
|
await ctx.waitForIdle();
|
|
@@ -722,10 +794,10 @@ async function handleMilestoneActions(
|
|
|
722
794
|
const uniqueMilestoneIds = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
|
|
723
795
|
const nextId = nextMilestoneId(milestoneIds, uniqueMilestoneIds);
|
|
724
796
|
pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId, step: stepMode };
|
|
725
|
-
dispatchWorkflow(pi, buildDiscussPrompt(nextId,
|
|
797
|
+
await dispatchWorkflow(pi, buildDiscussPrompt(nextId,
|
|
726
798
|
`New milestone ${nextId}.`,
|
|
727
799
|
basePath
|
|
728
|
-
));
|
|
800
|
+
), "gsd-run", ctx, "plan-milestone");
|
|
729
801
|
return true;
|
|
730
802
|
}
|
|
731
803
|
|
|
@@ -866,10 +938,10 @@ export async function showSmartEntry(
|
|
|
866
938
|
if (isFirst) {
|
|
867
939
|
// First ever — skip wizard, just ask directly
|
|
868
940
|
pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId, step: stepMode };
|
|
869
|
-
dispatchWorkflow(pi, buildDiscussPrompt(nextId,
|
|
941
|
+
await dispatchWorkflow(pi, buildDiscussPrompt(nextId,
|
|
870
942
|
`New project, milestone ${nextId}. Do NOT read or explore .gsd/ — it's empty scaffolding.`,
|
|
871
943
|
basePath
|
|
872
|
-
));
|
|
944
|
+
), "gsd-run", ctx, "plan-milestone");
|
|
873
945
|
} else {
|
|
874
946
|
const choice = await showNextAction(ctx, {
|
|
875
947
|
title: "GSD — Get Shit Done",
|
|
@@ -887,10 +959,10 @@ export async function showSmartEntry(
|
|
|
887
959
|
|
|
888
960
|
if (choice === "new_milestone") {
|
|
889
961
|
pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId, step: stepMode };
|
|
890
|
-
dispatchWorkflow(pi, buildDiscussPrompt(nextId,
|
|
962
|
+
await dispatchWorkflow(pi, buildDiscussPrompt(nextId,
|
|
891
963
|
`New milestone ${nextId}.`,
|
|
892
964
|
basePath
|
|
893
|
-
));
|
|
965
|
+
), "gsd-run", ctx, "plan-milestone");
|
|
894
966
|
}
|
|
895
967
|
}
|
|
896
968
|
return;
|
|
@@ -926,10 +998,10 @@ export async function showSmartEntry(
|
|
|
926
998
|
const nextId = nextMilestoneId(milestoneIds, uniqueMilestoneIds);
|
|
927
999
|
|
|
928
1000
|
pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId, step: stepMode };
|
|
929
|
-
dispatchWorkflow(pi, buildDiscussPrompt(nextId,
|
|
1001
|
+
await dispatchWorkflow(pi, buildDiscussPrompt(nextId,
|
|
930
1002
|
`New milestone ${nextId}.`,
|
|
931
1003
|
basePath
|
|
932
|
-
));
|
|
1004
|
+
), "gsd-run", ctx, "plan-milestone");
|
|
933
1005
|
} else if (choice === "status") {
|
|
934
1006
|
const { fireStatusViaCommand } = await import("./commands.js");
|
|
935
1007
|
await fireStatusViaCommand(ctx);
|
|
@@ -977,24 +1049,24 @@ export async function showSmartEntry(
|
|
|
977
1049
|
? `${basePrompt}\n\n## Prior Discussion (Draft Seed)\n\n${draftContent}`
|
|
978
1050
|
: basePrompt;
|
|
979
1051
|
pendingAutoStart = { ctx, pi, basePath, milestoneId, step: stepMode };
|
|
980
|
-
dispatchWorkflow(pi, seed, "gsd-discuss");
|
|
1052
|
+
await dispatchWorkflow(pi, seed, "gsd-discuss", ctx, "plan-milestone");
|
|
981
1053
|
} else if (choice === "discuss_fresh") {
|
|
982
1054
|
const discussMilestoneTemplates = inlineTemplate("context", "Context");
|
|
983
1055
|
const structuredQuestionsAvailable = pi.getActiveTools().includes("ask_user_questions") ? "true" : "false";
|
|
984
1056
|
pendingAutoStart = { ctx, pi, basePath, milestoneId, step: stepMode };
|
|
985
|
-
dispatchWorkflow(pi, loadPrompt("guided-discuss-milestone", {
|
|
1057
|
+
await dispatchWorkflow(pi, loadPrompt("guided-discuss-milestone", {
|
|
986
1058
|
milestoneId, milestoneTitle, inlinedTemplates: discussMilestoneTemplates, structuredQuestionsAvailable,
|
|
987
1059
|
commitInstruction: buildDocsCommitInstruction(`docs(${milestoneId}): milestone context from discuss`),
|
|
988
|
-
}), "gsd-discuss");
|
|
1060
|
+
}), "gsd-discuss", ctx, "plan-milestone");
|
|
989
1061
|
} else if (choice === "skip_milestone") {
|
|
990
1062
|
const milestoneIds = findMilestoneIds(basePath);
|
|
991
1063
|
const uniqueMilestoneIds = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
|
|
992
1064
|
const nextId = nextMilestoneId(milestoneIds, uniqueMilestoneIds);
|
|
993
1065
|
pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId, step: stepMode };
|
|
994
|
-
dispatchWorkflow(pi, buildDiscussPrompt(nextId,
|
|
1066
|
+
await dispatchWorkflow(pi, buildDiscussPrompt(nextId,
|
|
995
1067
|
`New milestone ${nextId}.`,
|
|
996
1068
|
basePath
|
|
997
|
-
));
|
|
1069
|
+
), "gsd-run", ctx, "plan-milestone");
|
|
998
1070
|
}
|
|
999
1071
|
return;
|
|
1000
1072
|
}
|
|
@@ -1051,25 +1123,25 @@ export async function showSmartEntry(
|
|
|
1051
1123
|
inlineTemplate("secrets-manifest", "Secrets Manifest"),
|
|
1052
1124
|
].join("\n\n---\n\n");
|
|
1053
1125
|
const secretsOutputPath = relMilestoneFile(basePath, milestoneId, "SECRETS");
|
|
1054
|
-
dispatchWorkflow(pi, loadPrompt("guided-plan-milestone", {
|
|
1126
|
+
await dispatchWorkflow(pi, loadPrompt("guided-plan-milestone", {
|
|
1055
1127
|
milestoneId, milestoneTitle, secretsOutputPath, inlinedTemplates: planMilestoneTemplates,
|
|
1056
|
-
}));
|
|
1128
|
+
}), "gsd-run", ctx, "plan-milestone");
|
|
1057
1129
|
} else if (choice === "discuss") {
|
|
1058
1130
|
const discussMilestoneTemplates = inlineTemplate("context", "Context");
|
|
1059
1131
|
const structuredQuestionsAvailable = pi.getActiveTools().includes("ask_user_questions") ? "true" : "false";
|
|
1060
|
-
dispatchWorkflow(pi, loadPrompt("guided-discuss-milestone", {
|
|
1132
|
+
await dispatchWorkflow(pi, loadPrompt("guided-discuss-milestone", {
|
|
1061
1133
|
milestoneId, milestoneTitle, inlinedTemplates: discussMilestoneTemplates, structuredQuestionsAvailable,
|
|
1062
1134
|
commitInstruction: buildDocsCommitInstruction(`docs(${milestoneId}): milestone context from discuss`),
|
|
1063
|
-
}));
|
|
1135
|
+
}), "gsd-run", ctx, "plan-milestone");
|
|
1064
1136
|
} else if (choice === "skip_milestone") {
|
|
1065
1137
|
const milestoneIds = findMilestoneIds(basePath);
|
|
1066
1138
|
const uniqueMilestoneIds = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
|
|
1067
1139
|
const nextId = nextMilestoneId(milestoneIds, uniqueMilestoneIds);
|
|
1068
1140
|
pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId, step: stepMode };
|
|
1069
|
-
dispatchWorkflow(pi, buildDiscussPrompt(nextId,
|
|
1141
|
+
await dispatchWorkflow(pi, buildDiscussPrompt(nextId,
|
|
1070
1142
|
`New milestone ${nextId}.`,
|
|
1071
1143
|
basePath
|
|
1072
|
-
));
|
|
1144
|
+
), "gsd-run", ctx, "plan-milestone");
|
|
1073
1145
|
} else if (choice === "discard_milestone") {
|
|
1074
1146
|
const confirmed = await showConfirm(ctx, {
|
|
1075
1147
|
title: "Discard milestone?",
|
|
@@ -1181,16 +1253,16 @@ export async function showSmartEntry(
|
|
|
1181
1253
|
inlineTemplate("plan", "Slice Plan"),
|
|
1182
1254
|
inlineTemplate("task-plan", "Task Plan"),
|
|
1183
1255
|
].join("\n\n---\n\n");
|
|
1184
|
-
dispatchWorkflow(pi, loadPrompt("guided-plan-slice", {
|
|
1256
|
+
await dispatchWorkflow(pi, loadPrompt("guided-plan-slice", {
|
|
1185
1257
|
milestoneId, sliceId, sliceTitle, inlinedTemplates: planSliceTemplates,
|
|
1186
|
-
}));
|
|
1258
|
+
}), "gsd-run", ctx, "plan-slice");
|
|
1187
1259
|
} else if (choice === "discuss") {
|
|
1188
|
-
dispatchWorkflow(pi, await buildDiscussSlicePrompt(milestoneId, sliceId, sliceTitle, basePath, { rediscuss: hasContext }));
|
|
1260
|
+
await dispatchWorkflow(pi, await buildDiscussSlicePrompt(milestoneId, sliceId, sliceTitle, basePath, { rediscuss: hasContext }), "gsd-run", ctx, "plan-slice");
|
|
1189
1261
|
} else if (choice === "research") {
|
|
1190
1262
|
const researchTemplates = inlineTemplate("research", "Research");
|
|
1191
|
-
dispatchWorkflow(pi, loadPrompt("guided-research-slice", {
|
|
1263
|
+
await dispatchWorkflow(pi, loadPrompt("guided-research-slice", {
|
|
1192
1264
|
milestoneId, sliceId, sliceTitle, inlinedTemplates: researchTemplates,
|
|
1193
|
-
}));
|
|
1265
|
+
}), "gsd-run", ctx, "research-slice");
|
|
1194
1266
|
} else if (choice === "status") {
|
|
1195
1267
|
const { fireStatusViaCommand } = await import("./commands.js");
|
|
1196
1268
|
await fireStatusViaCommand(ctx);
|
|
@@ -1232,9 +1304,9 @@ export async function showSmartEntry(
|
|
|
1232
1304
|
inlineTemplate("slice-summary", "Slice Summary"),
|
|
1233
1305
|
inlineTemplate("uat", "UAT"),
|
|
1234
1306
|
].join("\n\n---\n\n");
|
|
1235
|
-
dispatchWorkflow(pi, loadPrompt("guided-complete-slice", {
|
|
1307
|
+
await dispatchWorkflow(pi, loadPrompt("guided-complete-slice", {
|
|
1236
1308
|
workingDirectory: basePath, milestoneId, sliceId, sliceTitle, inlinedTemplates: completeSliceTemplates,
|
|
1237
|
-
}));
|
|
1309
|
+
}), "gsd-run", ctx, "complete-slice");
|
|
1238
1310
|
} else if (choice === "status") {
|
|
1239
1311
|
const { fireStatusViaCommand } = await import("./commands.js");
|
|
1240
1312
|
await fireStatusViaCommand(ctx);
|
|
@@ -1297,14 +1369,14 @@ export async function showSmartEntry(
|
|
|
1297
1369
|
|
|
1298
1370
|
if (choice === "execute") {
|
|
1299
1371
|
if (hasInterrupted) {
|
|
1300
|
-
dispatchWorkflow(pi, loadPrompt("guided-resume-task", {
|
|
1372
|
+
await dispatchWorkflow(pi, loadPrompt("guided-resume-task", {
|
|
1301
1373
|
milestoneId, sliceId,
|
|
1302
|
-
}));
|
|
1374
|
+
}), "gsd-run", ctx, "execute-task");
|
|
1303
1375
|
} else {
|
|
1304
1376
|
const executeTaskTemplates = inlineTemplate("task-summary", "Task Summary");
|
|
1305
|
-
dispatchWorkflow(pi, loadPrompt("guided-execute-task", {
|
|
1377
|
+
await dispatchWorkflow(pi, loadPrompt("guided-execute-task", {
|
|
1306
1378
|
milestoneId, sliceId, taskId, taskTitle, inlinedTemplates: executeTaskTemplates,
|
|
1307
|
-
}));
|
|
1379
|
+
}), "gsd-run", ctx, "execute-task");
|
|
1308
1380
|
}
|
|
1309
1381
|
} else if (choice === "status") {
|
|
1310
1382
|
const { fireStatusViaCommand } = await import("./commands.js");
|
|
@@ -92,6 +92,23 @@ function warnDeprecatedAgentInstructions(): void {
|
|
|
92
92
|
// ── Depth verification state ──────────────────────────────────────────────
|
|
93
93
|
let depthVerificationDone = false;
|
|
94
94
|
|
|
95
|
+
// ── DB lazy-open helper ───────────────────────────────────────────────────
|
|
96
|
+
// In manual sessions (no auto-mode), the DB is never opened by bootstrapAutoSession.
|
|
97
|
+
// This helper ensures the DB is lazily opened on first tool call that needs it.
|
|
98
|
+
async function ensureDbOpen(): Promise<boolean> {
|
|
99
|
+
try {
|
|
100
|
+
const db = await import("./gsd-db.js");
|
|
101
|
+
if (db.isDbAvailable()) return true;
|
|
102
|
+
const dbPath = join(process.cwd(), ".gsd", "gsd.db");
|
|
103
|
+
if (existsSync(dbPath)) {
|
|
104
|
+
return db.openDatabase(dbPath);
|
|
105
|
+
}
|
|
106
|
+
return false;
|
|
107
|
+
} catch {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
95
112
|
// ── Queue phase tracking ──────────────────────────────────────────────────
|
|
96
113
|
// When true, the LLM is in a queue flow writing CONTEXT.md files.
|
|
97
114
|
// The write-gate applies during queue flows just like discussion flows.
|
|
@@ -300,12 +317,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
300
317
|
when_context: Type.Optional(Type.String({ description: "When/context for the decision (e.g. milestone ID)" })),
|
|
301
318
|
}),
|
|
302
319
|
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
303
|
-
//
|
|
304
|
-
|
|
305
|
-
try {
|
|
306
|
-
const db = await import("./gsd-db.js");
|
|
307
|
-
dbAvailable = db.isDbAvailable();
|
|
308
|
-
} catch { /* dynamic import failed */ }
|
|
320
|
+
// Ensure DB is open (lazy-open on first tool call in manual sessions)
|
|
321
|
+
const dbAvailable = await ensureDbOpen();
|
|
309
322
|
|
|
310
323
|
if (!dbAvailable) {
|
|
311
324
|
return {
|
|
@@ -367,11 +380,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
367
380
|
supporting_slices: Type.Optional(Type.String({ description: "Supporting slices" })),
|
|
368
381
|
}),
|
|
369
382
|
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
370
|
-
|
|
371
|
-
try {
|
|
372
|
-
const db = await import("./gsd-db.js");
|
|
373
|
-
dbAvailable = db.isDbAvailable();
|
|
374
|
-
} catch { /* dynamic import failed */ }
|
|
383
|
+
const dbAvailable = await ensureDbOpen();
|
|
375
384
|
|
|
376
385
|
if (!dbAvailable) {
|
|
377
386
|
return {
|
|
@@ -441,11 +450,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
441
450
|
content: Type.String({ description: "The full markdown content of the artifact" }),
|
|
442
451
|
}),
|
|
443
452
|
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
444
|
-
|
|
445
|
-
try {
|
|
446
|
-
const db = await import("./gsd-db.js");
|
|
447
|
-
dbAvailable = db.isDbAvailable();
|
|
448
|
-
} catch { /* dynamic import failed */ }
|
|
453
|
+
const dbAvailable = await ensureDbOpen();
|
|
449
454
|
|
|
450
455
|
if (!dbAvailable) {
|
|
451
456
|
return {
|
|
@@ -671,6 +671,43 @@ export function nativeAddAll(basePath: string): void {
|
|
|
671
671
|
gitFileExec(basePath, ["add", "-A"]);
|
|
672
672
|
}
|
|
673
673
|
|
|
674
|
+
/**
|
|
675
|
+
* Stage all files with pathspec exclusions (git add -A -- ':!pattern' ...).
|
|
676
|
+
* Excluded paths are never hashed by git, preventing hangs on large
|
|
677
|
+
* untracked artifact trees (57GB+, 11K+ files). See #1605.
|
|
678
|
+
*
|
|
679
|
+
* Falls back to plain `git add -A` when no exclusions are provided.
|
|
680
|
+
* Always uses the CLI path (not libgit2) because libgit2's add_all
|
|
681
|
+
* does not support pathspec exclusion syntax.
|
|
682
|
+
*
|
|
683
|
+
* When excluded paths are already covered by .gitignore, git may exit
|
|
684
|
+
* with code 1 and an "ignored by .gitignore" warning. This is harmless
|
|
685
|
+
* (the staging succeeds for all non-ignored files) and is suppressed.
|
|
686
|
+
*/
|
|
687
|
+
export function nativeAddAllWithExclusions(basePath: string, exclusions: readonly string[]): void {
|
|
688
|
+
if (exclusions.length === 0) {
|
|
689
|
+
nativeAddAll(basePath);
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
const pathspecs = exclusions.map(e => `:!${e}`);
|
|
693
|
+
try {
|
|
694
|
+
execFileSync("git", ["add", "-A", "--", ...pathspecs], {
|
|
695
|
+
cwd: basePath,
|
|
696
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
697
|
+
encoding: "utf-8",
|
|
698
|
+
env: GIT_NO_PROMPT_ENV,
|
|
699
|
+
});
|
|
700
|
+
} catch (err: unknown) {
|
|
701
|
+
// git exits 1 when pathspec exclusions reference paths already covered
|
|
702
|
+
// by .gitignore. The staging itself succeeds — only suppress that case.
|
|
703
|
+
const stderr = (err as { stderr?: string })?.stderr ?? "";
|
|
704
|
+
if (stderr.includes("ignored by one of your .gitignore files")) {
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
throw new GSDError(GSD_GIT_ERROR, `git add -A with exclusions failed in ${basePath}: ${getErrorMessage(err)}`);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
674
711
|
/**
|
|
675
712
|
* Stage specific files.
|
|
676
713
|
* Native: libgit2 index add.
|
|
@@ -356,6 +356,10 @@ export function milestonesDir(basePath: string): string {
|
|
|
356
356
|
return join(gsdRoot(basePath), "milestones");
|
|
357
357
|
}
|
|
358
358
|
|
|
359
|
+
export function resolveRuntimeFile(basePath: string): string {
|
|
360
|
+
return join(gsdRoot(basePath), "RUNTIME.md");
|
|
361
|
+
}
|
|
362
|
+
|
|
359
363
|
export function resolveGsdRootFile(basePath: string, key: GSDRootFileKey): string {
|
|
360
364
|
const root = gsdRoot(basePath);
|
|
361
365
|
const canonical = join(root, GSD_ROOT_FILES[key]);
|
|
@@ -20,6 +20,7 @@ import type {
|
|
|
20
20
|
ReactiveExecutionConfig,
|
|
21
21
|
} from "./types.js";
|
|
22
22
|
import type { DynamicRoutingConfig } from "./model-router.js";
|
|
23
|
+
import type { GitHubSyncConfig } from "../github-sync/types.js";
|
|
23
24
|
|
|
24
25
|
// ─── Workflow Modes ──────────────────────────────────────────────────────────
|
|
25
26
|
|
|
@@ -86,6 +87,7 @@ export const KNOWN_PREFERENCE_KEYS = new Set<string>([
|
|
|
86
87
|
"context_selection",
|
|
87
88
|
"widget_mode",
|
|
88
89
|
"reactive_execution",
|
|
90
|
+
"github",
|
|
89
91
|
]);
|
|
90
92
|
|
|
91
93
|
/** Canonical list of all dispatch unit types. */
|
|
@@ -215,6 +217,8 @@ export interface GSDPreferences {
|
|
|
215
217
|
widget_mode?: "full" | "small" | "min" | "off";
|
|
216
218
|
/** Reactive (graph-derived parallel) task execution within slices. Disabled by default. */
|
|
217
219
|
reactive_execution?: ReactiveExecutionConfig;
|
|
220
|
+
/** GitHub sync configuration. Opt-in: syncs GSD events to GitHub Issues, Milestones, and PRs. */
|
|
221
|
+
github?: GitHubSyncConfig;
|
|
218
222
|
}
|
|
219
223
|
|
|
220
224
|
export interface LoadedGSDPreferences {
|
|
@@ -696,5 +696,55 @@ export function validatePreferences(preferences: GSDPreferences): {
|
|
|
696
696
|
}
|
|
697
697
|
}
|
|
698
698
|
|
|
699
|
+
// ─── GitHub Sync ────────────────────────────────────────────────────────
|
|
700
|
+
if (preferences.github !== undefined) {
|
|
701
|
+
if (typeof preferences.github === "object" && preferences.github !== null) {
|
|
702
|
+
const gh = preferences.github as unknown as Record<string, unknown>;
|
|
703
|
+
const validGh: Record<string, unknown> = {};
|
|
704
|
+
|
|
705
|
+
if (gh.enabled !== undefined) {
|
|
706
|
+
if (typeof gh.enabled === "boolean") validGh.enabled = gh.enabled;
|
|
707
|
+
else errors.push("github.enabled must be a boolean");
|
|
708
|
+
}
|
|
709
|
+
if (gh.repo !== undefined) {
|
|
710
|
+
if (typeof gh.repo === "string" && gh.repo.includes("/")) validGh.repo = gh.repo;
|
|
711
|
+
else errors.push('github.repo must be a string in "owner/repo" format');
|
|
712
|
+
}
|
|
713
|
+
if (gh.project !== undefined) {
|
|
714
|
+
const p = typeof gh.project === "number" ? gh.project : Number(gh.project);
|
|
715
|
+
if (Number.isFinite(p) && p > 0) validGh.project = Math.floor(p);
|
|
716
|
+
else errors.push("github.project must be a positive number");
|
|
717
|
+
}
|
|
718
|
+
if (gh.labels !== undefined) {
|
|
719
|
+
if (Array.isArray(gh.labels) && gh.labels.every((l: unknown) => typeof l === "string")) {
|
|
720
|
+
validGh.labels = gh.labels;
|
|
721
|
+
} else {
|
|
722
|
+
errors.push("github.labels must be an array of strings");
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
if (gh.auto_link_commits !== undefined) {
|
|
726
|
+
if (typeof gh.auto_link_commits === "boolean") validGh.auto_link_commits = gh.auto_link_commits;
|
|
727
|
+
else errors.push("github.auto_link_commits must be a boolean");
|
|
728
|
+
}
|
|
729
|
+
if (gh.slice_prs !== undefined) {
|
|
730
|
+
if (typeof gh.slice_prs === "boolean") validGh.slice_prs = gh.slice_prs;
|
|
731
|
+
else errors.push("github.slice_prs must be a boolean");
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
const knownGhKeys = new Set(["enabled", "repo", "project", "labels", "auto_link_commits", "slice_prs"]);
|
|
735
|
+
for (const key of Object.keys(gh)) {
|
|
736
|
+
if (!knownGhKeys.has(key)) {
|
|
737
|
+
warnings.push(`unknown github key "${key}" — ignored`);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
if (Object.keys(validGh).length > 0) {
|
|
742
|
+
validated.github = validGh as unknown as import("../github-sync/types.js").GitHubSyncConfig;
|
|
743
|
+
}
|
|
744
|
+
} else {
|
|
745
|
+
errors.push("github must be an object");
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
699
749
|
return { preferences: validated, errors, warnings };
|
|
700
750
|
}
|
|
@@ -271,6 +271,9 @@ function mergePreferences(base: GSDPreferences, override: GSDPreferences): GSDPr
|
|
|
271
271
|
context_selection: override.context_selection ?? base.context_selection,
|
|
272
272
|
auto_visualize: override.auto_visualize ?? base.auto_visualize,
|
|
273
273
|
auto_report: override.auto_report ?? base.auto_report,
|
|
274
|
+
github: (base.github || override.github)
|
|
275
|
+
? { ...(base.github ?? {}), ...(override.github ?? {}) } as import("../github-sync/types.js").GitHubSyncConfig
|
|
276
|
+
: undefined,
|
|
274
277
|
};
|
|
275
278
|
}
|
|
276
279
|
|
|
@@ -25,6 +25,8 @@ You are the UAT runner. Execute every check defined in `{{uatPath}}` as deeply a
|
|
|
25
25
|
### Automation rules by mode
|
|
26
26
|
|
|
27
27
|
- `artifact-driven` — verify with shell commands, scripts, file reads, and artifact structure checks.
|
|
28
|
+
- `browser-executable` — use browser tools to navigate to the target URL and verify expected behavior. Capture screenshots as evidence. Record pass/fail with specific assertions.
|
|
29
|
+
- `runtime-executable` — execute the specified command or script. Capture stdout/stderr as evidence. Record pass/fail based on exit code and output.
|
|
28
30
|
- `live-runtime` — exercise the real runtime path. Start or connect to the app/service if needed, use browser/runtime/network checks, and verify observable behavior.
|
|
29
31
|
- `mixed` — run all automatable artifact-driven and live-runtime checks. Separate any remaining human-only checks explicitly.
|
|
30
32
|
- `human-experience` — automate setup, preconditions, screenshots, logs, and objective checks, but do **not** invent subjective PASS results. Mark taste-based, experiential, or purely human-judgment checks as `NEEDS-HUMAN` and use an overall verdict of `PARTIAL` unless every required check was objective and passed.
|
|
@@ -39,6 +39,35 @@ export function markSliceDoneInRoadmap(basePath: string, mid: string, sid: strin
|
|
|
39
39
|
return true;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Mark a slice as not done ([ ]) in the milestone roadmap.
|
|
44
|
+
* Idempotent — no-op if already unchecked or if the slice isn't found.
|
|
45
|
+
*
|
|
46
|
+
* @returns true if the roadmap was modified, false if no change was needed
|
|
47
|
+
*/
|
|
48
|
+
export function markSliceUndoneInRoadmap(basePath: string, mid: string, sid: string): boolean {
|
|
49
|
+
const roadmapFile = resolveMilestoneFile(basePath, mid, "ROADMAP");
|
|
50
|
+
if (!roadmapFile) return false;
|
|
51
|
+
|
|
52
|
+
let content: string;
|
|
53
|
+
try {
|
|
54
|
+
content = readFileSync(roadmapFile, "utf-8");
|
|
55
|
+
} catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const updated = content.replace(
|
|
60
|
+
new RegExp(`^(\\s*-\\s+)\\[x\\]\\s+\\*\\*${sid}:`, "m"),
|
|
61
|
+
`$1[ ] **${sid}:`,
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
if (updated === content) return false;
|
|
65
|
+
|
|
66
|
+
atomicWriteSync(roadmapFile, updated);
|
|
67
|
+
clearParseCache();
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
|
|
42
71
|
/**
|
|
43
72
|
* Mark a task as done ([x]) in the slice plan.
|
|
44
73
|
* Idempotent — no-op if already checked or if the task isn't found.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Runtime Context
|
|
2
|
+
|
|
3
|
+
## Stack
|
|
4
|
+
- **Language:** (e.g., TypeScript, Python, Go)
|
|
5
|
+
- **Framework:** (e.g., Next.js, FastAPI, Gin)
|
|
6
|
+
- **Build:** (e.g., npm run build, cargo build)
|
|
7
|
+
- **Test:** (e.g., npm run test, pytest)
|
|
8
|
+
- **Lint:** (e.g., npm run lint, ruff check)
|
|
9
|
+
|
|
10
|
+
## Environment
|
|
11
|
+
- **Node version:** (e.g., 20.x)
|
|
12
|
+
- **Package manager:** (e.g., npm, pnpm, yarn)
|
|
13
|
+
- **Required env vars:** (list any needed for local dev)
|
|
14
|
+
|
|
15
|
+
## Dev Server
|
|
16
|
+
- **Start command:** (e.g., npm run dev)
|
|
17
|
+
- **Default port:** (e.g., 3000)
|
|
18
|
+
- **Health check:** (e.g., curl http://localhost:3000/health)
|
|
19
|
+
|
|
20
|
+
## Notes
|
|
21
|
+
(Any runtime-specific context the executor needs to know)
|