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
@@ -72,11 +72,21 @@ export async function postUnitPreVerification(pctx, opts) {
72
72
  const summaryContent = await loadFile(summaryPath);
73
73
  if (summaryContent) {
74
74
  const summary = parseSummary(summaryContent);
75
+ // Look up GitHub issue number for commit linking
76
+ let ghIssueNumber;
77
+ try {
78
+ const { getTaskIssueNumberForCommit } = await import("../github-sync/sync.js");
79
+ ghIssueNumber = getTaskIssueNumberForCommit(s.basePath, mid, sid, tid) ?? undefined;
80
+ }
81
+ catch {
82
+ // GitHub sync not available — skip
83
+ }
75
84
  taskContext = {
76
85
  taskId: `${sid}/${tid}`,
77
86
  taskTitle: summary.title?.replace(/^T\d+:\s*/, "") || tid,
78
87
  oneLiner: summary.oneLiner || undefined,
79
88
  keyFiles: summary.frontmatter.key_files?.filter(f => !f.includes("{{")) || undefined,
89
+ issueNumber: ghIssueNumber,
80
90
  };
81
91
  }
82
92
  }
@@ -94,6 +104,14 @@ export async function postUnitPreVerification(pctx, opts) {
94
104
  catch (e) {
95
105
  debugLog("postUnit", { phase: "auto-commit", error: String(e) });
96
106
  }
107
+ // GitHub sync (non-blocking, opt-in)
108
+ try {
109
+ const { runGitHubSync } = await import("../github-sync/sync.js");
110
+ await runGitHubSync(s.basePath, s.currentUnit.type, s.currentUnit.id);
111
+ }
112
+ catch (e) {
113
+ debugLog("postUnit", { phase: "github-sync", error: String(e) });
114
+ }
97
115
  // Doctor: fix mechanical bookkeeping (skipped for lightweight sidecars)
98
116
  if (!opts?.skipDoctor)
99
117
  try {
@@ -105,12 +123,18 @@ export async function postUnitPreVerification(pctx, opts) {
105
123
  if (report.fixesApplied.length > 0) {
106
124
  ctx.ui.notify(`Post-hook: applied ${report.fixesApplied.length} fix(es).`, "info");
107
125
  }
108
- // Proactive health tracking
109
- const summary = summarizeDoctorIssues(report.issues);
126
+ // Proactive health tracking — filter to current milestone to avoid
127
+ // cross-milestone stale errors inflating the escalation counter
128
+ const currentMilestoneId = s.currentUnit.id.split("/")[0];
129
+ const milestoneIssues = currentMilestoneId
130
+ ? report.issues.filter(i => i.unitId === currentMilestoneId ||
131
+ i.unitId.startsWith(`${currentMilestoneId}/`))
132
+ : report.issues;
133
+ const summary = summarizeDoctorIssues(milestoneIssues);
110
134
  recordHealthSnapshot(summary.errors, summary.warnings, report.fixesApplied.length);
111
135
  // Check if we should escalate to LLM-assisted heal
112
136
  if (summary.errors > 0) {
113
- const unresolvedErrors = report.issues
137
+ const unresolvedErrors = milestoneIssues
114
138
  .filter(i => i.severity === "error" && !i.fixable)
115
139
  .map(i => ({ code: i.code, message: i.message, unitId: i.unitId }));
116
140
  const escalation = checkHealEscalation(summary.errors, unresolvedErrors);
@@ -123,6 +147,7 @@ export async function postUnitPreVerification(pctx, opts) {
123
147
  const reportText = formatDoctorReport(report, { scope: doctorScope, includeWarnings: true });
124
148
  const structuredIssues = formatDoctorIssuesForPrompt(actionable);
125
149
  dispatchDoctorHeal(pi, doctorScope, reportText, structuredIssues);
150
+ return "dispatched";
126
151
  }
127
152
  catch (e) {
128
153
  debugLog("postUnit", { phase: "doctor-heal-dispatch", error: String(e) });
@@ -7,12 +7,19 @@
7
7
  */
8
8
  import { loadFile, parseContinue, parsePlan, parseRoadmap, parseSummary, extractUatType, loadActiveOverrides, formatOverridesSection } from "./files.js";
9
9
  import { loadPrompt, inlineTemplate } from "./prompt-loader.js";
10
- import { resolveMilestoneFile, resolveSliceFile, resolveSlicePath, resolveTasksDir, resolveTaskFiles, resolveTaskFile, relMilestoneFile, relSliceFile, relSlicePath, relMilestonePath, resolveGsdRootFile, relGsdRootFile, } from "./paths.js";
10
+ import { resolveMilestoneFile, resolveSliceFile, resolveSlicePath, resolveTasksDir, resolveTaskFiles, resolveTaskFile, relMilestoneFile, relSliceFile, relSlicePath, relMilestonePath, resolveGsdRootFile, relGsdRootFile, resolveRuntimeFile, } from "./paths.js";
11
11
  import { resolveSkillDiscoveryMode, resolveInlineLevel, loadEffectiveGSDPreferences } from "./preferences.js";
12
12
  import { join } from "node:path";
13
13
  import { existsSync } from "node:fs";
14
- import { computeBudgets, resolveExecutorContextWindow } from "./context-budget.js";
14
+ import { computeBudgets, resolveExecutorContextWindow, truncateAtSectionBoundary } from "./context-budget.js";
15
15
  import { formatDecisionsCompact, formatRequirementsCompact } from "./structured-data-formatter.js";
16
+ // ─── Preamble Cap ─────────────────────────────────────────────────────────────
17
+ const MAX_PREAMBLE_CHARS = 30_000;
18
+ function capPreamble(preamble) {
19
+ if (preamble.length <= MAX_PREAMBLE_CHARS)
20
+ return preamble;
21
+ return truncateAtSectionBoundary(preamble, MAX_PREAMBLE_CHARS).content;
22
+ }
16
23
  // ─── Executor Constraints ─────────────────────────────────────────────────────
17
24
  /**
18
25
  * Format executor context constraints for injection into the plan-slice prompt.
@@ -124,7 +131,6 @@ export async function inlineFileSmart(absPath, relPath, label, query, threshold
124
131
  return `### ${label}\nSource: \`${relPath}\`\n\n${content.trim()}`;
125
132
  }
126
133
  // For large files, truncate at section boundary
127
- const { truncateAtSectionBoundary } = await import("./context-budget.js");
128
134
  const truncated = truncateAtSectionBoundary(content, threshold).content;
129
135
  return `### ${label}\nSource: \`${relPath}\`\n\n${truncated}`;
130
136
  }
@@ -158,7 +164,6 @@ export async function inlineDependencySummaries(mid, sid, base, budgetChars) {
158
164
  }
159
165
  const result = sections.join("\n\n");
160
166
  if (budgetChars !== undefined && result.length > budgetChars) {
161
- const { truncateAtSectionBoundary } = await import("./context-budget.js");
162
167
  return truncateAtSectionBoundary(result, budgetChars).content;
163
168
  }
164
169
  return result;
@@ -525,7 +530,7 @@ export async function buildResearchMilestonePrompt(mid, midTitle, base) {
525
530
  if (knowledgeInlineRM)
526
531
  inlined.push(knowledgeInlineRM);
527
532
  inlined.push(inlineTemplate("research", "Research"));
528
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
533
+ const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
529
534
  const outputRelPath = relMilestoneFile(base, mid, "RESEARCH");
530
535
  return loadPrompt("research-milestone", {
531
536
  workingDirectory: base,
@@ -578,7 +583,7 @@ export async function buildPlanMilestonePrompt(mid, midTitle, base, level) {
578
583
  inlined.push(inlineTemplate("plan", "Slice Plan"));
579
584
  inlined.push(inlineTemplate("task-plan", "Task Plan"));
580
585
  }
581
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
586
+ const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
582
587
  const outputRelPath = relMilestoneFile(base, mid, "ROADMAP");
583
588
  const researchOutputPath = join(base, relMilestoneFile(base, mid, "RESEARCH"));
584
589
  const secretsOutputPath = join(base, relMilestoneFile(base, mid, "SECRETS"));
@@ -626,7 +631,7 @@ export async function buildResearchSlicePrompt(mid, _midTitle, sid, sTitle, base
626
631
  const overridesInline = formatOverridesSection(activeOverrides);
627
632
  if (overridesInline)
628
633
  inlined.unshift(overridesInline);
629
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
634
+ const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
630
635
  const outputRelPath = relSliceFile(base, mid, sid, "RESEARCH");
631
636
  return loadPrompt("research-slice", {
632
637
  workingDirectory: base,
@@ -672,7 +677,7 @@ export async function buildPlanSlicePrompt(mid, _midTitle, sid, sTitle, base, le
672
677
  const planOverridesInline = formatOverridesSection(planActiveOverrides);
673
678
  if (planOverridesInline)
674
679
  inlined.unshift(planOverridesInline);
675
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
680
+ const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
676
681
  // Build executor context constraints from the budget engine
677
682
  const executorContextConstraints = formatExecutorConstraints();
678
683
  const outputRelPath = relSliceFile(base, mid, sid, "PLAN");
@@ -760,11 +765,17 @@ export async function buildExecuteTaskPrompt(mid, sid, sTitle, tid, tTitle, base
760
765
  const carryForwardBudget = Math.floor(budgets.inlineContextBudgetChars * 0.4);
761
766
  let finalCarryForward = carryForwardSection;
762
767
  if (carryForwardSection.length > carryForwardBudget) {
763
- const { truncateAtSectionBoundary } = await import("./context-budget.js");
764
768
  finalCarryForward = truncateAtSectionBoundary(carryForwardSection, carryForwardBudget).content;
765
769
  }
770
+ // Inline RUNTIME.md if present
771
+ const runtimePath = resolveRuntimeFile(base);
772
+ const runtimeContent = existsSync(runtimePath) ? await loadFile(runtimePath) : null;
773
+ const runtimeContext = runtimeContent
774
+ ? `### Runtime Context\nSource: \`.gsd/RUNTIME.md\`\n\n${runtimeContent.trim()}`
775
+ : "";
766
776
  return loadPrompt("execute-task", {
767
777
  overridesSection,
778
+ runtimeContext,
768
779
  workingDirectory: base,
769
780
  milestoneId: mid, sliceId: sid, sliceTitle: sTitle, taskId: tid, taskTitle: tTitle,
770
781
  planPath: join(base, relSliceFile(base, mid, sid, "PLAN")),
@@ -819,7 +830,7 @@ export async function buildCompleteSlicePrompt(mid, _midTitle, sid, sTitle, base
819
830
  const completeOverridesInline = formatOverridesSection(completeActiveOverrides);
820
831
  if (completeOverridesInline)
821
832
  inlined.unshift(completeOverridesInline);
822
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
833
+ const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
823
834
  const sliceRel = relSlicePath(base, mid, sid);
824
835
  const sliceSummaryPath = join(base, `${sliceRel}/${sid}-SUMMARY.md`);
825
836
  const sliceUatPath = join(base, `${sliceRel}/${sid}-UAT.md`);
@@ -875,7 +886,7 @@ export async function buildCompleteMilestonePrompt(mid, midTitle, base, level) {
875
886
  if (contextInline)
876
887
  inlined.push(contextInline);
877
888
  inlined.push(inlineTemplate("milestone-summary", "Milestone Summary"));
878
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
889
+ const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
879
890
  const milestoneSummaryPath = join(base, `${relMilestonePath(base, mid)}/${mid}-SUMMARY.md`);
880
891
  return loadPrompt("complete-milestone", {
881
892
  workingDirectory: base,
@@ -942,7 +953,7 @@ export async function buildValidateMilestonePrompt(mid, midTitle, base, level) {
942
953
  const contextInline = await inlineFileOptional(contextPath, contextRel, "Milestone Context");
943
954
  if (contextInline)
944
955
  inlined.push(contextInline);
945
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
956
+ const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
946
957
  const validationOutputPath = join(base, `${relMilestonePath(base, mid)}/${mid}-VALIDATION.md`);
947
958
  const roadmapOutputPath = `${relMilestonePath(base, mid)}/${mid}-ROADMAP.md`;
948
959
  return loadPrompt("validate-milestone", {
@@ -990,7 +1001,7 @@ export async function buildReplanSlicePrompt(mid, midTitle, sid, sTitle, base) {
990
1001
  const replanOverridesInline = formatOverridesSection(replanActiveOverrides);
991
1002
  if (replanOverridesInline)
992
1003
  inlined.unshift(replanOverridesInline);
993
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
1004
+ const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
994
1005
  const replanPath = join(base, `${relSlicePath(base, mid, sid)}/${sid}-REPLAN.md`);
995
1006
  // Build capture context for replan prompt (captures that triggered this replan)
996
1007
  let captureContext = "(none)";
@@ -1030,7 +1041,7 @@ export async function buildRunUatPrompt(mid, sliceId, uatPath, uatContent, base)
1030
1041
  const projectInline = await inlineProjectFromDb(base);
1031
1042
  if (projectInline)
1032
1043
  inlined.push(projectInline);
1033
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
1044
+ const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
1034
1045
  const uatResultPath = join(base, relSliceFile(base, mid, sliceId, "UAT-RESULT"));
1035
1046
  const uatType = extractUatType(uatContent) ?? "human-experience";
1036
1047
  return loadPrompt("run-uat", {
@@ -1066,7 +1077,7 @@ export async function buildReassessRoadmapPrompt(mid, midTitle, completedSliceId
1066
1077
  const knowledgeInlineRA = await inlineGsdRootFile(base, "knowledge.md", "Project Knowledge");
1067
1078
  if (knowledgeInlineRA)
1068
1079
  inlined.push(knowledgeInlineRA);
1069
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
1080
+ const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
1070
1081
  const assessmentPath = join(base, relSliceFile(base, mid, completedSliceId, "ASSESSMENT"));
1071
1082
  // Build deferred captures context for reassess prompt
1072
1083
  let deferredCaptures = "(none)";
@@ -15,10 +15,10 @@ import { safeCopy, safeCopyRecursive } from "./safe-fs.js";
15
15
  import { gsdRoot } from "./paths.js";
16
16
  import { createWorktree, removeWorktree, worktreePath, } from "./worktree-manager.js";
17
17
  import { detectWorktreeName, nudgeGitBranchCache, } from "./worktree.js";
18
- import { MergeConflictError, readIntegrationBranch } from "./git-service.js";
18
+ import { MergeConflictError, readIntegrationBranch, RUNTIME_EXCLUSION_PATHS } from "./git-service.js";
19
19
  import { parseRoadmap } from "./files.js";
20
20
  import { loadEffectiveGSDPreferences } from "./preferences.js";
21
- import { nativeGetCurrentBranch, nativeWorkingTreeStatus, nativeAddAll, nativeCommit, nativeCheckoutBranch, nativeMergeSquash, nativeConflictFiles, nativeCheckoutTheirs, nativeAddPaths, nativeRmForce, nativeBranchDelete, nativeBranchExists, } from "./native-git-bridge.js";
21
+ import { nativeGetCurrentBranch, nativeWorkingTreeStatus, nativeAddAllWithExclusions, nativeCommit, nativeCheckoutBranch, nativeMergeSquash, nativeConflictFiles, nativeCheckoutTheirs, nativeAddPaths, nativeRmForce, nativeBranchDelete, nativeBranchExists, } from "./native-git-bridge.js";
22
22
  // ─── Module State ──────────────────────────────────────────────────────────
23
23
  /** Original project root before chdir into auto-worktree. */
24
24
  let originalBase = null;
@@ -656,7 +656,7 @@ function autoCommitDirtyState(cwd) {
656
656
  const status = nativeWorkingTreeStatus(cwd);
657
657
  if (!status)
658
658
  return false;
659
- nativeAddAll(cwd);
659
+ nativeAddAllWithExclusions(cwd, RUNTIME_EXCLUSION_PATHS);
660
660
  const result = nativeCommit(cwd, "chore: auto-commit before milestone merge");
661
661
  return result !== null;
662
662
  }
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * One command, one wizard. Routes to smart entry or status.
5
5
  */
6
+ import { importExtensionModule } from "@gsd/pi-coding-agent";
6
7
  import { existsSync, readFileSync, readdirSync, unlinkSync } from "node:fs";
7
8
  import { homedir } from "node:os";
8
9
  import { join } from "node:path";
@@ -529,7 +530,7 @@ export async function handleGSDCommand(args, ctx, pi) {
529
530
  return;
530
531
  }
531
532
  if (trimmed === "widget" || trimmed.startsWith("widget ")) {
532
- const { cycleWidgetMode, setWidgetMode, getWidgetMode } = await import("./auto-dashboard.js");
533
+ const { cycleWidgetMode, setWidgetMode, getWidgetMode } = await importExtensionModule(import.meta.url, "./auto-dashboard.js");
533
534
  const arg = trimmed.replace(/^widget\s*/, "").trim();
534
535
  if (arg === "full" || arg === "small" || arg === "min" || arg === "off") {
535
536
  setWidgetMode(arg);
@@ -257,10 +257,23 @@ async function markSliceDoneInRoadmap(basePath, milestoneId, sliceId, fixesAppli
257
257
  fixesApplied.push(`marked ${sliceId} done in ${roadmapPath}`);
258
258
  }
259
259
  }
260
+ async function markSliceUndoneInRoadmap(basePath, milestoneId, sliceId, fixesApplied) {
261
+ const roadmapPath = resolveMilestoneFile(basePath, milestoneId, "ROADMAP");
262
+ if (!roadmapPath)
263
+ return;
264
+ const content = await loadFile(roadmapPath);
265
+ if (!content)
266
+ return;
267
+ const updated = content.replace(new RegExp(`^(\\s*-\\s+)\\[x\\]\\s+\\*\\*${sliceId}:`, "m"), `$1[ ] **${sliceId}:`);
268
+ if (updated !== content) {
269
+ await saveFile(roadmapPath, updated);
270
+ fixesApplied.push(`unmarked ${sliceId} in ${roadmapPath} (premature completion)`);
271
+ }
272
+ }
260
273
  function matchesScope(unitId, scope) {
261
274
  if (!scope)
262
275
  return true;
263
- return unitId === scope || unitId.startsWith(`${scope}/`) || unitId.startsWith(`${scope}`);
276
+ return unitId === scope || unitId.startsWith(`${scope}/`);
264
277
  }
265
278
  function auditRequirements(content) {
266
279
  if (!content)
@@ -828,6 +841,12 @@ export async function runGSDDoctor(basePath, options) {
828
841
  file: relSliceFile(basePath, milestoneId, slice.id, "SUMMARY"),
829
842
  fixable: true,
830
843
  });
844
+ if (!allTasksDone) {
845
+ dryRunCanFix("slice_checked_missing_summary", `uncheck ${slice.id} in roadmap (tasks incomplete)`);
846
+ if (shouldFix("slice_checked_missing_summary")) {
847
+ await markSliceUndoneInRoadmap(basePath, milestoneId, slice.id, fixesApplied);
848
+ }
849
+ }
831
850
  }
832
851
  if (slice.done && !hasSliceUat) {
833
852
  issues.push({
@@ -1,9 +1,10 @@
1
+ import { importExtensionModule } from "@gsd/pi-coding-agent";
1
2
  export function registerExitCommand(pi, deps = {}) {
2
3
  pi.registerCommand("exit", {
3
4
  description: "Exit GSD gracefully",
4
5
  handler: async (_args, ctx) => {
5
6
  // Stop auto-mode first so locks and activity state are cleaned up before shutdown.
6
- const stopAuto = deps.stopAuto ?? (await import("./auto.js")).stopAuto;
7
+ const stopAuto = deps.stopAuto ?? (await importExtensionModule(import.meta.url, "./auto.js")).stopAuto;
7
8
  await stopAuto(ctx, pi, "Graceful exit");
8
9
  ctx.shutdown();
9
10
  },
@@ -692,6 +692,10 @@ export function extractUatType(content) {
692
692
  const rawValue = modeBullet.slice('UAT mode:'.length).trim().toLowerCase();
693
693
  if (rawValue.startsWith('artifact-driven'))
694
694
  return 'artifact-driven';
695
+ if (rawValue.startsWith('browser-executable'))
696
+ return 'browser-executable';
697
+ if (rawValue.startsWith('runtime-executable'))
698
+ return 'runtime-executable';
695
699
  if (rawValue.startsWith('live-runtime'))
696
700
  return 'live-runtime';
697
701
  if (rawValue.startsWith('human-experience'))
@@ -14,7 +14,7 @@ import { gsdRoot } from "./paths.js";
14
14
  import { GIT_NO_PROMPT_ENV } from "./git-constants.js";
15
15
  import { loadEffectiveGSDPreferences } from "./preferences.js";
16
16
  import { detectWorktreeName, SLICE_BRANCH_RE, } from "./worktree.js";
17
- import { nativeGetCurrentBranch, nativeDetectMainBranch, nativeBranchExists, nativeHasChanges, nativeAddAll, nativeResetPaths, nativeHasStagedChanges, nativeCommit, nativeRmCached, nativeUpdateRef, } from "./native-git-bridge.js";
17
+ import { nativeGetCurrentBranch, nativeDetectMainBranch, nativeBranchExists, nativeHasChanges, nativeAddAllWithExclusions, nativeHasStagedChanges, nativeCommit, nativeRmCached, nativeUpdateRef, } from "./native-git-bridge.js";
18
18
  import { GSDError, GSD_MERGE_CONFLICT, GSD_GIT_ERROR } from "./errors.js";
19
19
  import { getErrorMessage } from "./error-utils.js";
20
20
  export const VALID_BRANCH_NAME = /^[a-zA-Z0-9_\-\/.]+$/;
@@ -36,12 +36,19 @@ export function buildTaskCommitMessage(ctx) {
36
36
  : description;
37
37
  const subject = `${type}(${scope}): ${truncated}`;
38
38
  // Build body with key files if available
39
+ const bodyParts = [];
39
40
  if (ctx.keyFiles && ctx.keyFiles.length > 0) {
40
41
  const fileLines = ctx.keyFiles
41
42
  .slice(0, 8) // cap at 8 files to keep commit concise
42
43
  .map(f => `- ${f}`)
43
44
  .join("\n");
44
- return `${subject}\n\n${fileLines}`;
45
+ bodyParts.push(fileLines);
46
+ }
47
+ if (ctx.issueNumber) {
48
+ bodyParts.push(`Resolves #${ctx.issueNumber}`);
49
+ }
50
+ if (bodyParts.length > 0) {
51
+ return `${subject}\n\n${bodyParts.join("\n\n")}`;
45
52
  }
46
53
  return subject;
47
54
  }
@@ -254,7 +261,9 @@ export class GitServiceImpl {
254
261
  }
255
262
  this._runtimeFilesCleanedUp = true;
256
263
  }
257
- // Stage everything, then unstage excluded paths.
264
+ // Stage everything using pathspec exclusions so excluded paths are never
265
+ // hashed by git. The old approach of `git add -A` followed by unstaging
266
+ // hangs indefinitely on repos with large untracked artifact trees (#1605).
258
267
  //
259
268
  // Exclude only RUNTIME paths from staging — not the entire .gsd/ directory.
260
269
  // When .gsd/milestones/ files are already tracked in the index (projects
@@ -264,15 +273,9 @@ export class GitServiceImpl {
264
273
  // the second half of a milestone's artifacts are never committed (#1326).
265
274
  //
266
275
  // If .gsd/ IS in .gitignore (the default for external state projects),
267
- // git add -A already skips it and the reset is a harmless no-op.
268
- nativeAddAll(this.basePath);
269
- const runtimeExclusions = [...RUNTIME_EXCLUSION_PATHS, ...extraExclusions];
270
- for (const exclusion of runtimeExclusions) {
271
- try {
272
- nativeResetPaths(this.basePath, [exclusion]);
273
- }
274
- catch { /* path not staged — ignore */ }
275
- }
276
+ // git add -A already skips it and the exclusions are harmless no-ops.
277
+ const allExclusions = [...RUNTIME_EXCLUSION_PATHS, ...extraExclusions];
278
+ nativeAddAllWithExclusions(this.basePath, allExclusions);
276
279
  }
277
280
  /** Tracks whether runtime file cleanup has run this session. */
278
281
  _runtimeFilesCleanedUp = false;
@@ -433,6 +436,21 @@ export class GitServiceImpl {
433
436
  }
434
437
  }
435
438
  }
439
+ // ─── Draft PR Creation ─────────────────────────────────────────────────────
440
+ /**
441
+ * Create a draft pull request for a completed milestone using `gh pr create`.
442
+ * Returns the PR URL on success, or null on failure.
443
+ * Non-fatal: callers should treat failure as best-effort.
444
+ */
445
+ export function createDraftPR(basePath, milestoneId, title, body) {
446
+ try {
447
+ const result = execSync(`gh pr create --draft --title ${JSON.stringify(title)} --body ${JSON.stringify(body)}`, { cwd: basePath, encoding: "utf8", timeout: 30000, env: GIT_NO_PROMPT_ENV });
448
+ return result.trim();
449
+ }
450
+ catch {
451
+ return null;
452
+ }
453
+ }
436
454
  // ─── Factory ───────────────────────────────────────────────────────────────
437
455
  /** Create a GitServiceImpl with the current effective git preferences. */
438
456
  export function createGitService(basePath) {