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
@@ -121,11 +121,21 @@ export async function postUnitPreVerification(pctx: PostUnitContext, opts?: PreV
121
121
  const summaryContent = await loadFile(summaryPath);
122
122
  if (summaryContent) {
123
123
  const summary = parseSummary(summaryContent);
124
+ // Look up GitHub issue number for commit linking
125
+ let ghIssueNumber: number | undefined;
126
+ try {
127
+ const { getTaskIssueNumberForCommit } = await import("../github-sync/sync.js");
128
+ ghIssueNumber = getTaskIssueNumberForCommit(s.basePath, mid, sid, tid) ?? undefined;
129
+ } catch {
130
+ // GitHub sync not available — skip
131
+ }
132
+
124
133
  taskContext = {
125
134
  taskId: `${sid}/${tid}`,
126
135
  taskTitle: summary.title?.replace(/^T\d+:\s*/, "") || tid,
127
136
  oneLiner: summary.oneLiner || undefined,
128
137
  keyFiles: summary.frontmatter.key_files?.filter(f => !f.includes("{{")) || undefined,
138
+ issueNumber: ghIssueNumber,
129
139
  };
130
140
  }
131
141
  } catch (e) {
@@ -143,6 +153,14 @@ export async function postUnitPreVerification(pctx: PostUnitContext, opts?: PreV
143
153
  debugLog("postUnit", { phase: "auto-commit", error: String(e) });
144
154
  }
145
155
 
156
+ // GitHub sync (non-blocking, opt-in)
157
+ try {
158
+ const { runGitHubSync } = await import("../github-sync/sync.js");
159
+ await runGitHubSync(s.basePath, s.currentUnit.type, s.currentUnit.id);
160
+ } catch (e) {
161
+ debugLog("postUnit", { phase: "github-sync", error: String(e) });
162
+ }
163
+
146
164
  // Doctor: fix mechanical bookkeeping (skipped for lightweight sidecars)
147
165
  if (!opts?.skipDoctor) try {
148
166
  const scopeParts = s.currentUnit.id.split("/").slice(0, 2);
@@ -154,13 +172,20 @@ export async function postUnitPreVerification(pctx: PostUnitContext, opts?: PreV
154
172
  ctx.ui.notify(`Post-hook: applied ${report.fixesApplied.length} fix(es).`, "info");
155
173
  }
156
174
 
157
- // Proactive health tracking
158
- const summary = summarizeDoctorIssues(report.issues);
175
+ // Proactive health tracking — filter to current milestone to avoid
176
+ // cross-milestone stale errors inflating the escalation counter
177
+ const currentMilestoneId = s.currentUnit.id.split("/")[0];
178
+ const milestoneIssues = currentMilestoneId
179
+ ? report.issues.filter(i =>
180
+ i.unitId === currentMilestoneId ||
181
+ i.unitId.startsWith(`${currentMilestoneId}/`))
182
+ : report.issues;
183
+ const summary = summarizeDoctorIssues(milestoneIssues);
159
184
  recordHealthSnapshot(summary.errors, summary.warnings, report.fixesApplied.length);
160
185
 
161
186
  // Check if we should escalate to LLM-assisted heal
162
187
  if (summary.errors > 0) {
163
- const unresolvedErrors = report.issues
188
+ const unresolvedErrors = milestoneIssues
164
189
  .filter(i => i.severity === "error" && !i.fixable)
165
190
  .map(i => ({ code: i.code, message: i.message, unitId: i.unitId }));
166
191
  const escalation = checkHealEscalation(summary.errors, unresolvedErrors);
@@ -176,6 +201,7 @@ export async function postUnitPreVerification(pctx: PostUnitContext, opts?: PreV
176
201
  const reportText = formatDoctorReport(report, { scope: doctorScope, includeWarnings: true });
177
202
  const structuredIssues = formatDoctorIssuesForPrompt(actionable);
178
203
  dispatchDoctorHeal(pi, doctorScope, reportText, structuredIssues);
204
+ return "dispatched";
179
205
  } catch (e) {
180
206
  debugLog("postUnit", { phase: "doctor-heal-dispatch", error: String(e) });
181
207
  }
@@ -13,16 +13,25 @@ import {
13
13
  resolveMilestoneFile, resolveSliceFile, resolveSlicePath,
14
14
  resolveTasksDir, resolveTaskFiles, resolveTaskFile,
15
15
  relMilestoneFile, relSliceFile, relSlicePath, relMilestonePath,
16
- resolveGsdRootFile, relGsdRootFile,
16
+ resolveGsdRootFile, relGsdRootFile, resolveRuntimeFile,
17
17
  } from "./paths.js";
18
18
  import { resolveSkillDiscoveryMode, resolveInlineLevel, loadEffectiveGSDPreferences } from "./preferences.js";
19
19
  import type { GSDState, InlineLevel } from "./types.js";
20
20
  import type { GSDPreferences } from "./preferences.js";
21
21
  import { join } from "node:path";
22
22
  import { existsSync } from "node:fs";
23
- import { computeBudgets, resolveExecutorContextWindow } from "./context-budget.js";
23
+ import { computeBudgets, resolveExecutorContextWindow, truncateAtSectionBoundary } from "./context-budget.js";
24
24
  import { formatDecisionsCompact, formatRequirementsCompact } from "./structured-data-formatter.js";
25
25
 
26
+ // ─── Preamble Cap ─────────────────────────────────────────────────────────────
27
+
28
+ const MAX_PREAMBLE_CHARS = 30_000;
29
+
30
+ function capPreamble(preamble: string): string {
31
+ if (preamble.length <= MAX_PREAMBLE_CHARS) return preamble;
32
+ return truncateAtSectionBoundary(preamble, MAX_PREAMBLE_CHARS).content;
33
+ }
34
+
26
35
  // ─── Executor Constraints ─────────────────────────────────────────────────────
27
36
 
28
37
  /**
@@ -157,7 +166,6 @@ export async function inlineFileSmart(
157
166
  }
158
167
 
159
168
  // For large files, truncate at section boundary
160
- const { truncateAtSectionBoundary } = await import("./context-budget.js");
161
169
  const truncated = truncateAtSectionBoundary(content, threshold).content;
162
170
  return `### ${label}\nSource: \`${relPath}\`\n\n${truncated}`;
163
171
  }
@@ -193,7 +201,6 @@ export async function inlineDependencySummaries(
193
201
 
194
202
  const result = sections.join("\n\n");
195
203
  if (budgetChars !== undefined && result.length > budgetChars) {
196
- const { truncateAtSectionBoundary } = await import("./context-budget.js");
197
204
  return truncateAtSectionBoundary(result, budgetChars).content;
198
205
  }
199
206
  return result;
@@ -611,7 +618,7 @@ export async function buildResearchMilestonePrompt(mid: string, midTitle: string
611
618
  if (knowledgeInlineRM) inlined.push(knowledgeInlineRM);
612
619
  inlined.push(inlineTemplate("research", "Research"));
613
620
 
614
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
621
+ const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
615
622
 
616
623
  const outputRelPath = relMilestoneFile(base, mid, "RESEARCH");
617
624
  return loadPrompt("research-milestone", {
@@ -661,7 +668,7 @@ export async function buildPlanMilestonePrompt(mid: string, midTitle: string, ba
661
668
  inlined.push(inlineTemplate("task-plan", "Task Plan"));
662
669
  }
663
670
 
664
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
671
+ const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
665
672
 
666
673
  const outputRelPath = relMilestoneFile(base, mid, "ROADMAP");
667
674
  const researchOutputPath = join(base, relMilestoneFile(base, mid, "RESEARCH"));
@@ -710,7 +717,7 @@ export async function buildResearchSlicePrompt(
710
717
  const overridesInline = formatOverridesSection(activeOverrides);
711
718
  if (overridesInline) inlined.unshift(overridesInline);
712
719
 
713
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
720
+ const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
714
721
 
715
722
  const outputRelPath = relSliceFile(base, mid, sid, "RESEARCH");
716
723
  return loadPrompt("research-slice", {
@@ -758,7 +765,7 @@ export async function buildPlanSlicePrompt(
758
765
  const planOverridesInline = formatOverridesSection(planActiveOverrides);
759
766
  if (planOverridesInline) inlined.unshift(planOverridesInline);
760
767
 
761
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
768
+ const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
762
769
 
763
770
  // Build executor context constraints from the budget engine
764
771
  const executorContextConstraints = formatExecutorConstraints();
@@ -881,12 +888,19 @@ export async function buildExecuteTaskPrompt(
881
888
  const carryForwardBudget = Math.floor(budgets.inlineContextBudgetChars * 0.4);
882
889
  let finalCarryForward = carryForwardSection;
883
890
  if (carryForwardSection.length > carryForwardBudget) {
884
- const { truncateAtSectionBoundary } = await import("./context-budget.js");
885
891
  finalCarryForward = truncateAtSectionBoundary(carryForwardSection, carryForwardBudget).content;
886
892
  }
887
893
 
894
+ // Inline RUNTIME.md if present
895
+ const runtimePath = resolveRuntimeFile(base);
896
+ const runtimeContent = existsSync(runtimePath) ? await loadFile(runtimePath) : null;
897
+ const runtimeContext = runtimeContent
898
+ ? `### Runtime Context\nSource: \`.gsd/RUNTIME.md\`\n\n${runtimeContent.trim()}`
899
+ : "";
900
+
888
901
  return loadPrompt("execute-task", {
889
902
  overridesSection,
903
+ runtimeContext,
890
904
  workingDirectory: base,
891
905
  milestoneId: mid, sliceId: sid, sliceTitle: sTitle, taskId: tid, taskTitle: tTitle,
892
906
  planPath: join(base, relSliceFile(base, mid, sid, "PLAN")),
@@ -945,7 +959,7 @@ export async function buildCompleteSlicePrompt(
945
959
  const completeOverridesInline = formatOverridesSection(completeActiveOverrides);
946
960
  if (completeOverridesInline) inlined.unshift(completeOverridesInline);
947
961
 
948
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
962
+ const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
949
963
 
950
964
  const sliceRel = relSlicePath(base, mid, sid);
951
965
  const sliceSummaryPath = join(base, `${sliceRel}/${sid}-SUMMARY.md`);
@@ -1004,7 +1018,7 @@ export async function buildCompleteMilestonePrompt(
1004
1018
  if (contextInline) inlined.push(contextInline);
1005
1019
  inlined.push(inlineTemplate("milestone-summary", "Milestone Summary"));
1006
1020
 
1007
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
1021
+ const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
1008
1022
 
1009
1023
  const milestoneSummaryPath = join(base, `${relMilestonePath(base, mid)}/${mid}-SUMMARY.md`);
1010
1024
 
@@ -1075,7 +1089,7 @@ export async function buildValidateMilestonePrompt(
1075
1089
  const contextInline = await inlineFileOptional(contextPath, contextRel, "Milestone Context");
1076
1090
  if (contextInline) inlined.push(contextInline);
1077
1091
 
1078
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
1092
+ const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
1079
1093
 
1080
1094
  const validationOutputPath = join(base, `${relMilestonePath(base, mid)}/${mid}-VALIDATION.md`);
1081
1095
  const roadmapOutputPath = `${relMilestonePath(base, mid)}/${mid}-ROADMAP.md`;
@@ -1129,7 +1143,7 @@ export async function buildReplanSlicePrompt(
1129
1143
  const replanOverridesInline = formatOverridesSection(replanActiveOverrides);
1130
1144
  if (replanOverridesInline) inlined.unshift(replanOverridesInline);
1131
1145
 
1132
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
1146
+ const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
1133
1147
 
1134
1148
  const replanPath = join(base, `${relSlicePath(base, mid, sid)}/${sid}-REPLAN.md`);
1135
1149
 
@@ -1177,7 +1191,7 @@ export async function buildRunUatPrompt(
1177
1191
  const projectInline = await inlineProjectFromDb(base);
1178
1192
  if (projectInline) inlined.push(projectInline);
1179
1193
 
1180
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
1194
+ const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
1181
1195
 
1182
1196
  const uatResultPath = join(base, relSliceFile(base, mid, sliceId, "UAT-RESULT"));
1183
1197
  const uatType = extractUatType(uatContent) ?? "human-experience";
@@ -1216,7 +1230,7 @@ export async function buildReassessRoadmapPrompt(
1216
1230
  const knowledgeInlineRA = await inlineGsdRootFile(base, "knowledge.md", "Project Knowledge");
1217
1231
  if (knowledgeInlineRA) inlined.push(knowledgeInlineRA);
1218
1232
 
1219
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
1233
+ const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
1220
1234
 
1221
1235
  const assessmentPath = join(base, relSliceFile(base, mid, completedSliceId, "ASSESSMENT"));
1222
1236
 
@@ -37,13 +37,13 @@ import {
37
37
  resolveGitHeadPath,
38
38
  nudgeGitBranchCache,
39
39
  } from "./worktree.js";
40
- import { MergeConflictError, readIntegrationBranch } from "./git-service.js";
40
+ import { MergeConflictError, readIntegrationBranch, RUNTIME_EXCLUSION_PATHS } from "./git-service.js";
41
41
  import { parseRoadmap } from "./files.js";
42
42
  import { loadEffectiveGSDPreferences } from "./preferences.js";
43
43
  import {
44
44
  nativeGetCurrentBranch,
45
45
  nativeWorkingTreeStatus,
46
- nativeAddAll,
46
+ nativeAddAllWithExclusions,
47
47
  nativeCommit,
48
48
  nativeCheckoutBranch,
49
49
  nativeMergeSquash,
@@ -768,7 +768,7 @@ function autoCommitDirtyState(cwd: string): boolean {
768
768
  try {
769
769
  const status = nativeWorkingTreeStatus(cwd);
770
770
  if (!status) return false;
771
- nativeAddAll(cwd);
771
+ nativeAddAllWithExclusions(cwd, RUNTIME_EXCLUSION_PATHS);
772
772
  const result = nativeCommit(
773
773
  cwd,
774
774
  "chore: auto-commit before milestone merge",
@@ -4,7 +4,7 @@
4
4
  * One command, one wizard. Routes to smart entry or status.
5
5
  */
6
6
 
7
- import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent";
7
+ import { importExtensionModule, type ExtensionAPI, type ExtensionCommandContext } from "@gsd/pi-coding-agent";
8
8
  import type { GSDState } from "./types.js";
9
9
  import { existsSync, readFileSync, readdirSync, unlinkSync } from "node:fs";
10
10
  import { homedir } from "node:os";
@@ -585,7 +585,7 @@ export async function handleGSDCommand(
585
585
  }
586
586
 
587
587
  if (trimmed === "widget" || trimmed.startsWith("widget ")) {
588
- const { cycleWidgetMode, setWidgetMode, getWidgetMode } = await import("./auto-dashboard.js");
588
+ const { cycleWidgetMode, setWidgetMode, getWidgetMode } = await importExtensionModule<typeof import("./auto-dashboard.js")>(import.meta.url, "./auto-dashboard.js");
589
589
  const arg = trimmed.replace(/^widget\s*/, "").trim();
590
590
  if (arg === "full" || arg === "small" || arg === "min" || arg === "off") {
591
591
  setWidgetMode(arg);
@@ -280,9 +280,24 @@ async function markSliceDoneInRoadmap(basePath: string, milestoneId: string, sli
280
280
  }
281
281
  }
282
282
 
283
+ async function markSliceUndoneInRoadmap(basePath: string, milestoneId: string, sliceId: string, fixesApplied: string[]): Promise<void> {
284
+ const roadmapPath = resolveMilestoneFile(basePath, milestoneId, "ROADMAP");
285
+ if (!roadmapPath) return;
286
+ const content = await loadFile(roadmapPath);
287
+ if (!content) return;
288
+ const updated = content.replace(
289
+ new RegExp(`^(\\s*-\\s+)\\[x\\]\\s+\\*\\*${sliceId}:`, "m"),
290
+ `$1[ ] **${sliceId}:`,
291
+ );
292
+ if (updated !== content) {
293
+ await saveFile(roadmapPath, updated);
294
+ fixesApplied.push(`unmarked ${sliceId} in ${roadmapPath} (premature completion)`);
295
+ }
296
+ }
297
+
283
298
  function matchesScope(unitId: string, scope?: string): boolean {
284
299
  if (!scope) return true;
285
- return unitId === scope || unitId.startsWith(`${scope}/`) || unitId.startsWith(`${scope}`);
300
+ return unitId === scope || unitId.startsWith(`${scope}/`);
286
301
  }
287
302
 
288
303
  function auditRequirements(content: string | null): DoctorIssue[] {
@@ -863,6 +878,12 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
863
878
  file: relSliceFile(basePath, milestoneId, slice.id, "SUMMARY"),
864
879
  fixable: true,
865
880
  });
881
+ if (!allTasksDone) {
882
+ dryRunCanFix("slice_checked_missing_summary", `uncheck ${slice.id} in roadmap (tasks incomplete)`);
883
+ if (shouldFix("slice_checked_missing_summary")) {
884
+ await markSliceUndoneInRoadmap(basePath, milestoneId, slice.id, fixesApplied);
885
+ }
886
+ }
866
887
  }
867
888
 
868
889
  if (slice.done && !hasSliceUat) {
@@ -1,4 +1,4 @@
1
- import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent";
1
+ import { importExtensionModule, type ExtensionAPI, type ExtensionCommandContext } from "@gsd/pi-coding-agent";
2
2
 
3
3
  type StopAutoFn = (ctx: ExtensionCommandContext, pi: ExtensionAPI, reason?: string) => Promise<void>;
4
4
 
@@ -10,7 +10,7 @@ export function registerExitCommand(
10
10
  description: "Exit GSD gracefully",
11
11
  handler: async (_args: string, ctx: ExtensionCommandContext) => {
12
12
  // Stop auto-mode first so locks and activity state are cleaned up before shutdown.
13
- const stopAuto = deps.stopAuto ?? (await import("./auto.js")).stopAuto;
13
+ const stopAuto = deps.stopAuto ?? (await importExtensionModule<typeof import("./auto.js")>(import.meta.url, "./auto.js")).stopAuto;
14
14
  await stopAuto(ctx, pi, "Graceful exit");
15
15
  ctx.shutdown();
16
16
  },
@@ -775,7 +775,7 @@ export function parseTaskPlanIO(content: string): { inputFiles: string[]; output
775
775
  * The four UAT classification types recognised by GSD auto-mode.
776
776
  * `undefined` is returned (not this union) when no type can be determined.
777
777
  */
778
- export type UatType = 'artifact-driven' | 'live-runtime' | 'human-experience' | 'mixed';
778
+ export type UatType = 'artifact-driven' | 'live-runtime' | 'human-experience' | 'mixed' | 'browser-executable' | 'runtime-executable';
779
779
 
780
780
  /**
781
781
  * Extract the UAT type from a UAT file's raw content.
@@ -799,6 +799,8 @@ export function extractUatType(content: string): UatType | undefined {
799
799
  const rawValue = modeBullet.slice('UAT mode:'.length).trim().toLowerCase();
800
800
 
801
801
  if (rawValue.startsWith('artifact-driven')) return 'artifact-driven';
802
+ if (rawValue.startsWith('browser-executable')) return 'browser-executable';
803
+ if (rawValue.startsWith('runtime-executable')) return 'runtime-executable';
802
804
  if (rawValue.startsWith('live-runtime')) return 'live-runtime';
803
805
  if (rawValue.startsWith('human-experience')) return 'human-experience';
804
806
  if (rawValue.startsWith('mixed')) return 'mixed';
@@ -24,7 +24,7 @@ import {
24
24
  nativeDetectMainBranch,
25
25
  nativeBranchExists,
26
26
  nativeHasChanges,
27
- nativeAddAll,
27
+ nativeAddAllWithExclusions,
28
28
  nativeResetPaths,
29
29
  nativeHasStagedChanges,
30
30
  nativeCommit,
@@ -95,6 +95,8 @@ export interface TaskCommitContext {
95
95
  oneLiner?: string;
96
96
  /** Files modified by this task (from task summary frontmatter) */
97
97
  keyFiles?: string[];
98
+ /** GitHub issue number — appends "Resolves #N" trailer when set. */
99
+ issueNumber?: number;
98
100
  }
99
101
 
100
102
  /**
@@ -118,12 +120,22 @@ export function buildTaskCommitMessage(ctx: TaskCommitContext): string {
118
120
  const subject = `${type}(${scope}): ${truncated}`;
119
121
 
120
122
  // Build body with key files if available
123
+ const bodyParts: string[] = [];
124
+
121
125
  if (ctx.keyFiles && ctx.keyFiles.length > 0) {
122
126
  const fileLines = ctx.keyFiles
123
127
  .slice(0, 8) // cap at 8 files to keep commit concise
124
128
  .map(f => `- ${f}`)
125
129
  .join("\n");
126
- return `${subject}\n\n${fileLines}`;
130
+ bodyParts.push(fileLines);
131
+ }
132
+
133
+ if (ctx.issueNumber) {
134
+ bodyParts.push(`Resolves #${ctx.issueNumber}`);
135
+ }
136
+
137
+ if (bodyParts.length > 0) {
138
+ return `${subject}\n\n${bodyParts.join("\n\n")}`;
127
139
  }
128
140
 
129
141
  return subject;
@@ -373,7 +385,9 @@ export class GitServiceImpl {
373
385
  this._runtimeFilesCleanedUp = true;
374
386
  }
375
387
 
376
- // Stage everything, then unstage excluded paths.
388
+ // Stage everything using pathspec exclusions so excluded paths are never
389
+ // hashed by git. The old approach of `git add -A` followed by unstaging
390
+ // hangs indefinitely on repos with large untracked artifact trees (#1605).
377
391
  //
378
392
  // Exclude only RUNTIME paths from staging — not the entire .gsd/ directory.
379
393
  // When .gsd/milestones/ files are already tracked in the index (projects
@@ -383,13 +397,9 @@ export class GitServiceImpl {
383
397
  // the second half of a milestone's artifacts are never committed (#1326).
384
398
  //
385
399
  // If .gsd/ IS in .gitignore (the default for external state projects),
386
- // git add -A already skips it and the reset is a harmless no-op.
387
- nativeAddAll(this.basePath);
388
-
389
- const runtimeExclusions = [...RUNTIME_EXCLUSION_PATHS, ...extraExclusions];
390
- for (const exclusion of runtimeExclusions) {
391
- try { nativeResetPaths(this.basePath, [exclusion]); } catch { /* path not staged — ignore */ }
392
- }
400
+ // git add -A already skips it and the exclusions are harmless no-ops.
401
+ const allExclusions = [...RUNTIME_EXCLUSION_PATHS, ...extraExclusions];
402
+ nativeAddAllWithExclusions(this.basePath, allExclusions);
393
403
  }
394
404
 
395
405
  /** Tracks whether runtime file cleanup has run this session. */
@@ -574,6 +584,30 @@ export class GitServiceImpl {
574
584
 
575
585
  }
576
586
 
587
+ // ─── Draft PR Creation ─────────────────────────────────────────────────────
588
+
589
+ /**
590
+ * Create a draft pull request for a completed milestone using `gh pr create`.
591
+ * Returns the PR URL on success, or null on failure.
592
+ * Non-fatal: callers should treat failure as best-effort.
593
+ */
594
+ export function createDraftPR(
595
+ basePath: string,
596
+ milestoneId: string,
597
+ title: string,
598
+ body: string,
599
+ ): string | null {
600
+ try {
601
+ const result = execSync(
602
+ `gh pr create --draft --title ${JSON.stringify(title)} --body ${JSON.stringify(body)}`,
603
+ { cwd: basePath, encoding: "utf8", timeout: 30000, env: GIT_NO_PROMPT_ENV },
604
+ );
605
+ return result.trim();
606
+ } catch {
607
+ return null;
608
+ }
609
+ }
610
+
577
611
  // ─── Factory ───────────────────────────────────────────────────────────────
578
612
 
579
613
  /** Create a GitServiceImpl with the current effective git preferences. */