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.
Files changed (76) hide show
  1. package/dist/resource-loader.js +34 -1
  2. package/dist/resources/extensions/browser-tools/index.js +3 -1
  3. package/dist/resources/extensions/browser-tools/tools/verify.js +97 -0
  4. package/dist/resources/extensions/github-sync/cli.js +284 -0
  5. package/dist/resources/extensions/github-sync/index.js +73 -0
  6. package/dist/resources/extensions/github-sync/mapping.js +67 -0
  7. package/dist/resources/extensions/github-sync/sync.js +424 -0
  8. package/dist/resources/extensions/github-sync/templates.js +118 -0
  9. package/dist/resources/extensions/github-sync/types.js +7 -0
  10. package/dist/resources/extensions/gsd/auto-dispatch.js +1 -1
  11. package/dist/resources/extensions/gsd/auto-loop.js +593 -516
  12. package/dist/resources/extensions/gsd/auto-post-unit.js +28 -3
  13. package/dist/resources/extensions/gsd/auto-prompts.js +26 -15
  14. package/dist/resources/extensions/gsd/auto-worktree.js +3 -3
  15. package/dist/resources/extensions/gsd/commands.js +2 -1
  16. package/dist/resources/extensions/gsd/doctor.js +20 -1
  17. package/dist/resources/extensions/gsd/exit-command.js +2 -1
  18. package/dist/resources/extensions/gsd/files.js +4 -0
  19. package/dist/resources/extensions/gsd/git-service.js +30 -12
  20. package/dist/resources/extensions/gsd/guided-flow.js +82 -32
  21. package/dist/resources/extensions/gsd/index.js +22 -19
  22. package/dist/resources/extensions/gsd/native-git-bridge.js +37 -0
  23. package/dist/resources/extensions/gsd/paths.js +3 -0
  24. package/dist/resources/extensions/gsd/preferences-types.js +1 -0
  25. package/dist/resources/extensions/gsd/preferences-validation.js +58 -0
  26. package/dist/resources/extensions/gsd/preferences.js +3 -0
  27. package/dist/resources/extensions/gsd/prompts/execute-task.md +2 -0
  28. package/dist/resources/extensions/gsd/prompts/run-uat.md +2 -0
  29. package/dist/resources/extensions/gsd/roadmap-mutations.js +24 -0
  30. package/dist/resources/extensions/gsd/templates/runtime.md +21 -0
  31. package/dist/resources/extensions/mcp-client/index.js +14 -1
  32. package/package.json +1 -1
  33. package/packages/pi-ai/dist/utils/oauth/anthropic.js +2 -2
  34. package/packages/pi-ai/dist/utils/oauth/anthropic.js.map +1 -1
  35. package/packages/pi-ai/src/utils/oauth/anthropic.ts +2 -2
  36. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  37. package/packages/pi-coding-agent/dist/core/extensions/loader.js +205 -7
  38. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  39. package/packages/pi-coding-agent/src/core/extensions/loader.ts +223 -7
  40. package/src/resources/extensions/browser-tools/index.ts +3 -0
  41. package/src/resources/extensions/browser-tools/tools/verify.ts +117 -0
  42. package/src/resources/extensions/github-sync/cli.ts +364 -0
  43. package/src/resources/extensions/github-sync/index.ts +93 -0
  44. package/src/resources/extensions/github-sync/mapping.ts +81 -0
  45. package/src/resources/extensions/github-sync/sync.ts +556 -0
  46. package/src/resources/extensions/github-sync/templates.ts +183 -0
  47. package/src/resources/extensions/github-sync/tests/cli.test.ts +20 -0
  48. package/src/resources/extensions/github-sync/tests/commit-linking.test.ts +39 -0
  49. package/src/resources/extensions/github-sync/tests/mapping.test.ts +104 -0
  50. package/src/resources/extensions/github-sync/tests/templates.test.ts +110 -0
  51. package/src/resources/extensions/github-sync/types.ts +47 -0
  52. package/src/resources/extensions/gsd/auto-dispatch.ts +1 -1
  53. package/src/resources/extensions/gsd/auto-loop.ts +472 -434
  54. package/src/resources/extensions/gsd/auto-post-unit.ts +29 -3
  55. package/src/resources/extensions/gsd/auto-prompts.ts +29 -15
  56. package/src/resources/extensions/gsd/auto-worktree.ts +3 -3
  57. package/src/resources/extensions/gsd/commands.ts +2 -2
  58. package/src/resources/extensions/gsd/doctor.ts +22 -1
  59. package/src/resources/extensions/gsd/exit-command.ts +2 -2
  60. package/src/resources/extensions/gsd/files.ts +3 -1
  61. package/src/resources/extensions/gsd/git-service.ts +44 -10
  62. package/src/resources/extensions/gsd/guided-flow.ts +110 -38
  63. package/src/resources/extensions/gsd/index.ts +21 -16
  64. package/src/resources/extensions/gsd/native-git-bridge.ts +37 -0
  65. package/src/resources/extensions/gsd/paths.ts +4 -0
  66. package/src/resources/extensions/gsd/preferences-types.ts +4 -0
  67. package/src/resources/extensions/gsd/preferences-validation.ts +50 -0
  68. package/src/resources/extensions/gsd/preferences.ts +3 -0
  69. package/src/resources/extensions/gsd/prompts/execute-task.md +2 -0
  70. package/src/resources/extensions/gsd/prompts/run-uat.md +2 -0
  71. package/src/resources/extensions/gsd/roadmap-mutations.ts +29 -0
  72. package/src/resources/extensions/gsd/templates/runtime.md +21 -0
  73. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +111 -37
  74. package/src/resources/extensions/gsd/types.ts +8 -0
  75. package/src/resources/extensions/gsd/verification-evidence.ts +16 -0
  76. 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(pi: ExtensionAPI, note: string, customType = "gsd-run"): void {
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
- // Check DB availability
304
- let dbAvailable = false;
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
- let dbAvailable = false;
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
- let dbAvailable = false;
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
 
@@ -10,6 +10,8 @@ A researcher explored the codebase and a planner decomposed the work — you are
10
10
 
11
11
  {{overridesSection}}
12
12
 
13
+ {{runtimeContext}}
14
+
13
15
  {{resumeSection}}
14
16
 
15
17
  {{carryForwardSection}}
@@ -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)