gsd-pi 2.38.0-dev.bc2e21e → 2.38.0-dev.d533afb

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 (99) hide show
  1. package/dist/resource-loader.js +34 -1
  2. package/dist/resources/extensions/github-sync/cli.js +284 -0
  3. package/dist/resources/extensions/github-sync/index.js +73 -0
  4. package/dist/resources/extensions/github-sync/mapping.js +67 -0
  5. package/dist/resources/extensions/github-sync/sync.js +424 -0
  6. package/dist/resources/extensions/github-sync/templates.js +118 -0
  7. package/dist/resources/extensions/github-sync/types.js +7 -0
  8. package/dist/resources/extensions/gsd/auto/session.js +3 -23
  9. package/dist/resources/extensions/gsd/auto-dispatch.js +1 -1
  10. package/dist/resources/extensions/gsd/auto-loop.js +292 -263
  11. package/dist/resources/extensions/gsd/auto-post-unit.js +28 -3
  12. package/dist/resources/extensions/gsd/auto-prompts.js +23 -43
  13. package/dist/resources/extensions/gsd/auto-start.js +7 -1
  14. package/dist/resources/extensions/gsd/auto-worktree.js +3 -3
  15. package/dist/resources/extensions/gsd/auto.js +143 -80
  16. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +1 -1
  17. package/dist/resources/extensions/gsd/commands.js +2 -1
  18. package/dist/resources/extensions/gsd/context-budget.js +2 -10
  19. package/dist/resources/extensions/gsd/docs/preferences-reference.md +0 -2
  20. package/dist/resources/extensions/gsd/doctor-providers.js +27 -11
  21. package/dist/resources/extensions/gsd/doctor.js +20 -1
  22. package/dist/resources/extensions/gsd/exit-command.js +2 -1
  23. package/dist/resources/extensions/gsd/files.js +4 -0
  24. package/dist/resources/extensions/gsd/git-service.js +15 -12
  25. package/dist/resources/extensions/gsd/guided-flow.js +82 -32
  26. package/dist/resources/extensions/gsd/index.js +22 -19
  27. package/dist/resources/extensions/gsd/native-git-bridge.js +37 -0
  28. package/dist/resources/extensions/gsd/preferences-models.js +0 -12
  29. package/dist/resources/extensions/gsd/preferences-types.js +1 -1
  30. package/dist/resources/extensions/gsd/preferences-validation.js +58 -10
  31. package/dist/resources/extensions/gsd/preferences.js +4 -2
  32. package/dist/resources/extensions/gsd/prompts/run-uat.md +2 -0
  33. package/dist/resources/extensions/gsd/repo-identity.js +19 -3
  34. package/dist/resources/extensions/gsd/roadmap-mutations.js +24 -0
  35. package/dist/resources/extensions/mcp-client/index.js +14 -1
  36. package/package.json +1 -1
  37. package/packages/pi-ai/dist/utils/oauth/anthropic.js +2 -2
  38. package/packages/pi-ai/dist/utils/oauth/anthropic.js.map +1 -1
  39. package/packages/pi-ai/src/utils/oauth/anthropic.ts +2 -2
  40. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  41. package/packages/pi-coding-agent/dist/core/extensions/loader.js +205 -7
  42. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  43. package/packages/pi-coding-agent/src/core/extensions/loader.ts +223 -7
  44. package/src/resources/extensions/github-sync/cli.ts +364 -0
  45. package/src/resources/extensions/github-sync/index.ts +93 -0
  46. package/src/resources/extensions/github-sync/mapping.ts +81 -0
  47. package/src/resources/extensions/github-sync/sync.ts +556 -0
  48. package/src/resources/extensions/github-sync/templates.ts +183 -0
  49. package/src/resources/extensions/github-sync/tests/cli.test.ts +20 -0
  50. package/src/resources/extensions/github-sync/tests/commit-linking.test.ts +39 -0
  51. package/src/resources/extensions/github-sync/tests/mapping.test.ts +104 -0
  52. package/src/resources/extensions/github-sync/tests/templates.test.ts +110 -0
  53. package/src/resources/extensions/github-sync/types.ts +47 -0
  54. package/src/resources/extensions/gsd/auto/session.ts +3 -25
  55. package/src/resources/extensions/gsd/auto-dispatch.ts +1 -1
  56. package/src/resources/extensions/gsd/auto-loop.ts +382 -360
  57. package/src/resources/extensions/gsd/auto-post-unit.ts +29 -3
  58. package/src/resources/extensions/gsd/auto-prompts.ts +25 -45
  59. package/src/resources/extensions/gsd/auto-start.ts +11 -1
  60. package/src/resources/extensions/gsd/auto-worktree.ts +3 -3
  61. package/src/resources/extensions/gsd/auto.ts +139 -86
  62. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
  63. package/src/resources/extensions/gsd/commands.ts +2 -2
  64. package/src/resources/extensions/gsd/context-budget.ts +2 -12
  65. package/src/resources/extensions/gsd/docs/preferences-reference.md +0 -2
  66. package/src/resources/extensions/gsd/doctor-providers.ts +26 -9
  67. package/src/resources/extensions/gsd/doctor.ts +22 -1
  68. package/src/resources/extensions/gsd/exit-command.ts +2 -2
  69. package/src/resources/extensions/gsd/files.ts +3 -1
  70. package/src/resources/extensions/gsd/git-service.ts +20 -10
  71. package/src/resources/extensions/gsd/guided-flow.ts +110 -38
  72. package/src/resources/extensions/gsd/index.ts +21 -16
  73. package/src/resources/extensions/gsd/native-git-bridge.ts +37 -0
  74. package/src/resources/extensions/gsd/preferences-models.ts +0 -12
  75. package/src/resources/extensions/gsd/preferences-types.ts +4 -4
  76. package/src/resources/extensions/gsd/preferences-validation.ts +50 -10
  77. package/src/resources/extensions/gsd/preferences.ts +3 -2
  78. package/src/resources/extensions/gsd/prompts/run-uat.md +2 -0
  79. package/src/resources/extensions/gsd/repo-identity.ts +20 -3
  80. package/src/resources/extensions/gsd/roadmap-mutations.ts +29 -0
  81. package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +21 -18
  82. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +122 -68
  83. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +86 -3
  84. package/src/resources/extensions/gsd/tests/preferences.test.ts +2 -7
  85. package/src/resources/extensions/gsd/tests/repo-identity-worktree.test.ts +21 -1
  86. package/src/resources/extensions/gsd/types.ts +0 -1
  87. package/src/resources/extensions/mcp-client/index.ts +17 -1
  88. package/dist/resources/extensions/gsd/prompt-compressor.js +0 -393
  89. package/dist/resources/extensions/gsd/semantic-chunker.js +0 -254
  90. package/dist/resources/extensions/gsd/summary-distiller.js +0 -212
  91. package/src/resources/extensions/gsd/prompt-compressor.ts +0 -508
  92. package/src/resources/extensions/gsd/semantic-chunker.ts +0 -336
  93. package/src/resources/extensions/gsd/summary-distiller.ts +0 -258
  94. package/src/resources/extensions/gsd/tests/context-compression.test.ts +0 -193
  95. package/src/resources/extensions/gsd/tests/prompt-compressor.test.ts +0 -529
  96. package/src/resources/extensions/gsd/tests/semantic-chunker.test.ts +0 -426
  97. package/src/resources/extensions/gsd/tests/summary-distiller.test.ts +0 -323
  98. package/src/resources/extensions/gsd/tests/token-optimization-benchmark.test.ts +0 -1272
  99. package/src/resources/extensions/gsd/tests/token-optimization-prefs.test.ts +0 -164
@@ -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
  }
@@ -20,11 +20,17 @@ 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";
24
- import { compressToTarget } from "./prompt-compressor.js";
25
- import { distillSummaries } from "./summary-distiller.js";
23
+ import { computeBudgets, resolveExecutorContextWindow, truncateAtSectionBoundary } from "./context-budget.js";
26
24
  import { formatDecisionsCompact, formatRequirementsCompact } from "./structured-data-formatter.js";
27
- import { chunkByRelevance, formatChunks } from "./semantic-chunker.js";
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
+ }
28
34
 
29
35
  // ─── Executor Constraints ─────────────────────────────────────────────────────
30
36
 
@@ -159,16 +165,9 @@ export async function inlineFileSmart(
159
165
  return `### ${label}\nSource: \`${relPath}\`\n\n${content.trim()}`;
160
166
  }
161
167
 
162
- // Use semantic chunking for large files
163
- const result = chunkByRelevance(content, query, { maxChunks: 5, minScore: 0.05 });
164
-
165
- // If chunking didn't save much (< 20%), just include full content
166
- if (result.savingsPercent < 20) {
167
- return `### ${label}\nSource: \`${relPath}\`\n\n${content.trim()}`;
168
- }
169
-
170
- const formatted = formatChunks(result, relPath);
171
- return `### ${label} (${result.omittedChunks} sections omitted for relevance)\nSource: \`${relPath}\`\n\n${formatted}`;
168
+ // For large files, truncate at section boundary
169
+ const truncated = truncateAtSectionBoundary(content, threshold).content;
170
+ return `### ${label}\nSource: \`${relPath}\`\n\n${truncated}`;
172
171
  }
173
172
 
174
173
  /**
@@ -202,21 +201,6 @@ export async function inlineDependencySummaries(
202
201
 
203
202
  const result = sections.join("\n\n");
204
203
  if (budgetChars !== undefined && result.length > budgetChars) {
205
- // For 3+ summaries, try distillation first (preserves more information)
206
- if (sections.length >= 3) {
207
- const rawSummaries = sections.map(s => {
208
- // Extract content after the header line
209
- const lines = s.split("\n");
210
- const contentStart = lines.findIndex(l => l.startsWith("Source:"));
211
- return contentStart >= 0 ? lines.slice(contentStart + 1).join("\n").trim() : s;
212
- });
213
- const distilled = distillSummaries(rawSummaries, budgetChars);
214
- if (distilled.content.length <= budgetChars) {
215
- return distilled.content;
216
- }
217
- }
218
- // Fall back to section-boundary truncation
219
- const { truncateAtSectionBoundary } = await import("./context-budget.js");
220
204
  return truncateAtSectionBoundary(result, budgetChars).content;
221
205
  }
222
206
  return result;
@@ -634,7 +618,7 @@ export async function buildResearchMilestonePrompt(mid: string, midTitle: string
634
618
  if (knowledgeInlineRM) inlined.push(knowledgeInlineRM);
635
619
  inlined.push(inlineTemplate("research", "Research"));
636
620
 
637
- 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")}`);
638
622
 
639
623
  const outputRelPath = relMilestoneFile(base, mid, "RESEARCH");
640
624
  return loadPrompt("research-milestone", {
@@ -684,7 +668,7 @@ export async function buildPlanMilestonePrompt(mid: string, midTitle: string, ba
684
668
  inlined.push(inlineTemplate("task-plan", "Task Plan"));
685
669
  }
686
670
 
687
- 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")}`);
688
672
 
689
673
  const outputRelPath = relMilestoneFile(base, mid, "ROADMAP");
690
674
  const researchOutputPath = join(base, relMilestoneFile(base, mid, "RESEARCH"));
@@ -733,7 +717,7 @@ export async function buildResearchSlicePrompt(
733
717
  const overridesInline = formatOverridesSection(activeOverrides);
734
718
  if (overridesInline) inlined.unshift(overridesInline);
735
719
 
736
- 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")}`);
737
721
 
738
722
  const outputRelPath = relSliceFile(base, mid, sid, "RESEARCH");
739
723
  return loadPrompt("research-slice", {
@@ -781,7 +765,7 @@ export async function buildPlanSlicePrompt(
781
765
  const planOverridesInline = formatOverridesSection(planActiveOverrides);
782
766
  if (planOverridesInline) inlined.unshift(planOverridesInline);
783
767
 
784
- 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")}`);
785
769
 
786
770
  // Build executor context constraints from the budget engine
787
771
  const executorContextConstraints = formatExecutorConstraints();
@@ -900,15 +884,11 @@ export async function buildExecuteTaskPrompt(
900
884
  const budgets = computeBudgets(contextWindow);
901
885
  const verificationBudget = `~${Math.round(budgets.verificationBudgetChars / 1000)}K chars`;
902
886
 
903
- // Compress carry-forward section when it exceeds 40% of inline context budget.
904
- // Only compress when compression_strategy is "compress" (budget/balanced profiles).
887
+ // Truncate carry-forward section when it exceeds 40% of inline context budget.
905
888
  const carryForwardBudget = Math.floor(budgets.inlineContextBudgetChars * 0.4);
906
889
  let finalCarryForward = carryForwardSection;
907
890
  if (carryForwardSection.length > carryForwardBudget) {
908
- const { resolveCompressionStrategy } = await import("./preferences.js");
909
- if (resolveCompressionStrategy() === "compress") {
910
- finalCarryForward = compressToTarget(carryForwardSection, carryForwardBudget).content;
911
- }
891
+ finalCarryForward = truncateAtSectionBoundary(carryForwardSection, carryForwardBudget).content;
912
892
  }
913
893
 
914
894
  return loadPrompt("execute-task", {
@@ -971,7 +951,7 @@ export async function buildCompleteSlicePrompt(
971
951
  const completeOverridesInline = formatOverridesSection(completeActiveOverrides);
972
952
  if (completeOverridesInline) inlined.unshift(completeOverridesInline);
973
953
 
974
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
954
+ const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
975
955
 
976
956
  const sliceRel = relSlicePath(base, mid, sid);
977
957
  const sliceSummaryPath = join(base, `${sliceRel}/${sid}-SUMMARY.md`);
@@ -1030,7 +1010,7 @@ export async function buildCompleteMilestonePrompt(
1030
1010
  if (contextInline) inlined.push(contextInline);
1031
1011
  inlined.push(inlineTemplate("milestone-summary", "Milestone Summary"));
1032
1012
 
1033
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
1013
+ const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
1034
1014
 
1035
1015
  const milestoneSummaryPath = join(base, `${relMilestonePath(base, mid)}/${mid}-SUMMARY.md`);
1036
1016
 
@@ -1101,7 +1081,7 @@ export async function buildValidateMilestonePrompt(
1101
1081
  const contextInline = await inlineFileOptional(contextPath, contextRel, "Milestone Context");
1102
1082
  if (contextInline) inlined.push(contextInline);
1103
1083
 
1104
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
1084
+ const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
1105
1085
 
1106
1086
  const validationOutputPath = join(base, `${relMilestonePath(base, mid)}/${mid}-VALIDATION.md`);
1107
1087
  const roadmapOutputPath = `${relMilestonePath(base, mid)}/${mid}-ROADMAP.md`;
@@ -1155,7 +1135,7 @@ export async function buildReplanSlicePrompt(
1155
1135
  const replanOverridesInline = formatOverridesSection(replanActiveOverrides);
1156
1136
  if (replanOverridesInline) inlined.unshift(replanOverridesInline);
1157
1137
 
1158
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
1138
+ const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
1159
1139
 
1160
1140
  const replanPath = join(base, `${relSlicePath(base, mid, sid)}/${sid}-REPLAN.md`);
1161
1141
 
@@ -1203,7 +1183,7 @@ export async function buildRunUatPrompt(
1203
1183
  const projectInline = await inlineProjectFromDb(base);
1204
1184
  if (projectInline) inlined.push(projectInline);
1205
1185
 
1206
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
1186
+ const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
1207
1187
 
1208
1188
  const uatResultPath = join(base, relSliceFile(base, mid, sliceId, "UAT-RESULT"));
1209
1189
  const uatType = extractUatType(uatContent) ?? "human-experience";
@@ -1242,7 +1222,7 @@ export async function buildReassessRoadmapPrompt(
1242
1222
  const knowledgeInlineRA = await inlineGsdRootFile(base, "knowledge.md", "Project Knowledge");
1243
1223
  if (knowledgeInlineRA) inlined.push(knowledgeInlineRA);
1244
1224
 
1245
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
1225
+ const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
1246
1226
 
1247
1227
  const assessmentPath = join(base, relSliceFile(base, mid, completedSliceId, "ASSESSMENT"));
1248
1228
 
@@ -20,7 +20,7 @@ import {
20
20
  resolveSkillDiscoveryMode,
21
21
  getIsolationMode,
22
22
  } from "./preferences.js";
23
- import { ensureGsdSymlink } from "./repo-identity.js";
23
+ import { ensureGsdSymlink, validateProjectId } from "./repo-identity.js";
24
24
  import { migrateToExternalState, recoverFailedMigration } from "./migrate-external.js";
25
25
  import { collectSecretsFromManifest } from "../get-secrets-from-user.js";
26
26
  import { gsdRoot, resolveMilestoneFile, milestonesDir } from "./paths.js";
@@ -130,6 +130,16 @@ export async function bootstrapAutoSession(
130
130
  }
131
131
 
132
132
  try {
133
+ // Validate GSD_PROJECT_ID early so the user gets immediate feedback
134
+ const customProjectId = process.env.GSD_PROJECT_ID;
135
+ if (customProjectId && !validateProjectId(customProjectId)) {
136
+ ctx.ui.notify(
137
+ `GSD_PROJECT_ID must contain only alphanumeric characters, hyphens, and underscores. Got: "${customProjectId}"`,
138
+ "error",
139
+ );
140
+ return releaseLockAndReturn();
141
+ }
142
+
133
143
  // Ensure git repo exists
134
144
  if (!nativeIsRepo(base)) {
135
145
  const mainBranch =
@@ -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",
@@ -536,114 +536,167 @@ export async function stopAuto(
536
536
  if (!s.active && !s.paused) return;
537
537
  const loadedPreferences = loadEffectiveGSDPreferences()?.preferences;
538
538
  const reasonSuffix = reason ? ` — ${reason}` : "";
539
- clearUnitTimeout();
540
- if (lockBase()) clearLock(lockBase());
541
- if (lockBase()) releaseSessionLock(lockBase());
542
- clearSkillSnapshot();
543
- resetSkillTelemetry();
544
539
 
545
- // Remove SIGTERM handler registered at auto-mode start
546
- deregisterSigtermHandler();
540
+ try {
541
+ // ── Step 1: Timers and locks ──
542
+ try {
543
+ clearUnitTimeout();
544
+ if (lockBase()) clearLock(lockBase());
545
+ if (lockBase()) releaseSessionLock(lockBase());
546
+ } catch (e) {
547
+ debugLog("stop-cleanup-locks", { error: e instanceof Error ? e.message : String(e) });
548
+ }
547
549
 
548
- // ── Auto-worktree: exit worktree and reset s.basePath on stop ──
549
- if (s.currentMilestoneId) {
550
- const notifyCtx = ctx
551
- ? { notify: ctx.ui.notify.bind(ctx.ui) }
552
- : { notify: () => {} };
553
- buildResolver().exitMilestone(s.currentMilestoneId, notifyCtx, {
554
- preserveBranch: true,
555
- });
556
- }
550
+ // ── Step 2: Skill state ──
551
+ try {
552
+ clearSkillSnapshot();
553
+ resetSkillTelemetry();
554
+ } catch (e) {
555
+ debugLog("stop-cleanup-skills", { error: e instanceof Error ? e.message : String(e) });
556
+ }
557
557
 
558
- // ── DB cleanup: close the SQLite connection ──
559
- if (isDbAvailable()) {
558
+ // ── Step 3: SIGTERM handler ──
560
559
  try {
561
- const { closeDatabase } = await import("./gsd-db.js");
562
- closeDatabase();
560
+ deregisterSigtermHandler();
563
561
  } catch (e) {
564
- debugLog("db-close-failed", {
565
- error: e instanceof Error ? e.message : String(e),
566
- });
562
+ debugLog("stop-cleanup-sigterm", { error: e instanceof Error ? e.message : String(e) });
567
563
  }
568
- }
569
564
 
570
- if (s.originalBasePath) {
571
- s.basePath = s.originalBasePath;
565
+ // ── Step 4: Auto-worktree exit ──
572
566
  try {
573
- process.chdir(s.basePath);
574
- } catch {
575
- /* best-effort */
567
+ if (s.currentMilestoneId) {
568
+ const notifyCtx = ctx
569
+ ? { notify: ctx.ui.notify.bind(ctx.ui) }
570
+ : { notify: () => {} };
571
+ buildResolver().exitMilestone(s.currentMilestoneId, notifyCtx, {
572
+ preserveBranch: true,
573
+ });
574
+ }
575
+ } catch (e) {
576
+ debugLog("stop-cleanup-worktree", { error: e instanceof Error ? e.message : String(e) });
576
577
  }
577
- }
578
578
 
579
- const ledger = getLedger();
580
- if (ledger && ledger.units.length > 0) {
581
- const totals = getProjectTotals(ledger.units);
582
- ctx?.ui.notify(
583
- `Auto-mode stopped${reasonSuffix}. Session: ${formatCost(totals.cost)} · ${formatTokenCount(totals.tokens.total)} tokens · ${ledger.units.length} units`,
584
- "info",
585
- );
586
- } else {
587
- ctx?.ui.notify(`Auto-mode stopped${reasonSuffix}.`, "info");
588
- }
579
+ // ── Step 5: DB cleanup ──
580
+ if (isDbAvailable()) {
581
+ try {
582
+ const { closeDatabase } = await import("./gsd-db.js");
583
+ closeDatabase();
584
+ } catch (e) {
585
+ debugLog("db-close-failed", {
586
+ error: e instanceof Error ? e.message : String(e),
587
+ });
588
+ }
589
+ }
589
590
 
590
- if (s.basePath) {
591
+ // ── Step 6: Restore basePath and chdir ──
591
592
  try {
592
- await rebuildState(s.basePath);
593
+ if (s.originalBasePath) {
594
+ s.basePath = s.originalBasePath;
595
+ try {
596
+ process.chdir(s.basePath);
597
+ } catch {
598
+ /* best-effort */
599
+ }
600
+ }
593
601
  } catch (e) {
594
- debugLog("stop-rebuild-state-failed", {
595
- error: e instanceof Error ? e.message : String(e),
596
- });
602
+ debugLog("stop-cleanup-basepath", { error: e instanceof Error ? e.message : String(e) });
597
603
  }
598
- }
599
604
 
600
- clearCmuxSidebar(loadedPreferences);
601
- logCmuxEvent(
602
- loadedPreferences,
603
- `Auto-mode stopped${reasonSuffix || ""}.`,
604
- reason?.startsWith("Blocked:") ? "warning" : "info",
605
- );
605
+ // ── Step 7: Ledger notification ──
606
+ try {
607
+ const ledger = getLedger();
608
+ if (ledger && ledger.units.length > 0) {
609
+ const totals = getProjectTotals(ledger.units);
610
+ ctx?.ui.notify(
611
+ `Auto-mode stopped${reasonSuffix}. Session: ${formatCost(totals.cost)} · ${formatTokenCount(totals.tokens.total)} tokens · ${ledger.units.length} units`,
612
+ "info",
613
+ );
614
+ } else {
615
+ ctx?.ui.notify(`Auto-mode stopped${reasonSuffix}.`, "info");
616
+ }
617
+ } catch (e) {
618
+ debugLog("stop-cleanup-ledger", { error: e instanceof Error ? e.message : String(e) });
619
+ }
606
620
 
607
- if (isDebugEnabled()) {
608
- const logPath = writeDebugSummary();
609
- if (logPath) {
610
- ctx?.ui.notify(`Debug log written → ${logPath}`, "info");
621
+ // ── Step 8: Rebuild state ──
622
+ if (s.basePath) {
623
+ try {
624
+ await rebuildState(s.basePath);
625
+ } catch (e) {
626
+ debugLog("stop-rebuild-state-failed", {
627
+ error: e instanceof Error ? e.message : String(e),
628
+ });
629
+ }
611
630
  }
612
- }
613
631
 
614
- resetMetrics();
615
- resetRoutingHistory();
616
- resetHookState();
617
- if (s.basePath) clearPersistedHookState(s.basePath);
632
+ // ── Step 9: Cmux sidebar / event log ──
633
+ try {
634
+ clearCmuxSidebar(loadedPreferences);
635
+ logCmuxEvent(
636
+ loadedPreferences,
637
+ `Auto-mode stopped${reasonSuffix || ""}.`,
638
+ reason?.startsWith("Blocked:") ? "warning" : "info",
639
+ );
640
+ } catch (e) {
641
+ debugLog("stop-cleanup-cmux", { error: e instanceof Error ? e.message : String(e) });
642
+ }
618
643
 
619
- // Remove paused-session metadata if present (#1383)
620
- try {
621
- const pausedPath = join(gsdRoot(s.originalBasePath || s.basePath), "runtime", "paused-session.json");
622
- if (existsSync(pausedPath)) unlinkSync(pausedPath);
623
- } catch { /* non-fatal */ }
624
-
625
- // Restore original model before reset() clears the IDs
626
- if (pi && ctx && s.originalModelId && s.originalModelProvider) {
627
- const original = ctx.modelRegistry.find(
628
- s.originalModelProvider,
629
- s.originalModelId,
630
- );
631
- if (original) await pi.setModel(original);
632
- }
644
+ // ── Step 10: Debug summary ──
645
+ try {
646
+ if (isDebugEnabled()) {
647
+ const logPath = writeDebugSummary();
648
+ if (logPath) {
649
+ ctx?.ui.notify(`Debug log written → ${logPath}`, "info");
650
+ }
651
+ }
652
+ } catch (e) {
653
+ debugLog("stop-cleanup-debug", { error: e instanceof Error ? e.message : String(e) });
654
+ }
633
655
 
634
- // External cleanup (not covered by session reset)
635
- clearInFlightTools();
636
- clearSliceProgressCache();
637
- clearActivityLogState();
638
- resetProactiveHealing();
656
+ // ── Step 11: Reset metrics, routing, hooks ──
657
+ try {
658
+ resetMetrics();
659
+ resetRoutingHistory();
660
+ resetHookState();
661
+ if (s.basePath) clearPersistedHookState(s.basePath);
662
+ } catch (e) {
663
+ debugLog("stop-cleanup-metrics", { error: e instanceof Error ? e.message : String(e) });
664
+ }
639
665
 
640
- // UI cleanup
641
- ctx?.ui.setStatus("gsd-auto", undefined);
642
- ctx?.ui.setWidget("gsd-progress", undefined);
643
- ctx?.ui.setFooter(undefined);
666
+ // ── Step 12: Remove paused-session metadata (#1383) ──
667
+ try {
668
+ const pausedPath = join(gsdRoot(s.originalBasePath || s.basePath), "runtime", "paused-session.json");
669
+ if (existsSync(pausedPath)) unlinkSync(pausedPath);
670
+ } catch { /* non-fatal */ }
644
671
 
645
- // Reset all session state in one call
646
- s.reset();
672
+ // ── Step 13: Restore original model (before reset clears IDs) ──
673
+ try {
674
+ if (pi && ctx && s.originalModelId && s.originalModelProvider) {
675
+ const original = ctx.modelRegistry.find(
676
+ s.originalModelProvider,
677
+ s.originalModelId,
678
+ );
679
+ if (original) await pi.setModel(original);
680
+ }
681
+ } catch (e) {
682
+ debugLog("stop-cleanup-model", { error: e instanceof Error ? e.message : String(e) });
683
+ }
684
+ } finally {
685
+ // ── Critical invariants: these MUST execute regardless of errors ──
686
+ // External cleanup (not covered by session reset)
687
+ clearInFlightTools();
688
+ clearSliceProgressCache();
689
+ clearActivityLogState();
690
+ resetProactiveHealing();
691
+
692
+ // UI cleanup
693
+ ctx?.ui.setStatus("gsd-auto", undefined);
694
+ ctx?.ui.setWidget("gsd-progress", undefined);
695
+ ctx?.ui.setFooter(undefined);
696
+
697
+ // Reset all session state in one call
698
+ s.reset();
699
+ }
647
700
  }
648
701
 
649
702
  /**
@@ -745,7 +745,7 @@ export function serializePreferencesToFrontmatter(prefs: Record<string, unknown>
745
745
  "dynamic_routing", "token_profile", "phases", "parallel",
746
746
  "auto_visualize", "auto_report",
747
747
  "verification_commands", "verification_auto_fix", "verification_max_retries",
748
- "search_provider", "compression_strategy", "context_selection",
748
+ "search_provider", "context_selection",
749
749
  ];
750
750
 
751
751
  const seen = new Set<string>();
@@ -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);
@@ -9,7 +9,6 @@
9
9
  */
10
10
 
11
11
  import { type TokenProvider, getCharsPerToken } from "./token-counter.js";
12
- import { compressToTarget } from "./prompt-compressor.js";
13
12
 
14
13
  // ─── Budget ratio constants ──────────────────────────────────────────────────
15
14
  // Percentages of total context window allocated to each budget category.
@@ -202,22 +201,13 @@ export function resolveExecutorContextWindow(
202
201
  }
203
202
 
204
203
  /**
205
- * Smart context reduction: compress first, then truncate if still over budget.
206
- * Returns the content within budget with maximum information preservation.
204
+ * Reduce content to fit within budget using section-boundary truncation.
207
205
  */
208
206
  export function reduceToFit(content: string, budgetChars: number): TruncationResult {
209
207
  if (!content || content.length <= budgetChars) {
210
208
  return { content, droppedSections: 0 };
211
209
  }
212
-
213
- // Step 1: Try compression
214
- const compressed = compressToTarget(content, budgetChars);
215
- if (compressed.compressedChars <= budgetChars) {
216
- return { content: compressed.content, droppedSections: 0 };
217
- }
218
-
219
- // Step 2: Truncate the compressed content at section boundaries
220
- return truncateAtSectionBoundary(compressed.content, budgetChars);
210
+ return truncateAtSectionBoundary(content, budgetChars);
221
211
  }
222
212
 
223
213
  // ─── Internal helpers ────────────────────────────────────────────────────────
@@ -194,8 +194,6 @@ Setting `prefer_skills: []` does **not** disable skill discovery — it just mea
194
194
 
195
195
  - `search_provider`: `"brave"`, `"tavily"`, `"ollama"`, `"native"`, or `"auto"` — selects the search backend for research phases. `"native"` forces Anthropic's built-in web search only; provider values force that backend and disable native search; `"auto"` uses the default heuristic. Default: `"auto"`.
196
196
 
197
- - `compression_strategy`: `"truncate"` or `"compress"` — controls how context that exceeds the budget is reduced. `"truncate"` (default) drops sections from the end. `"compress"` applies heuristic compression before truncating, preserving more content at the cost of some fidelity. Default: `"truncate"`.
198
-
199
197
  - `context_selection`: `"full"` or `"smart"` — controls how files are inlined into context. `"full"` inlines entire files; `"smart"` uses semantic chunking to include only the most relevant sections. Default is derived from `token_profile`.
200
198
 
201
199
  - `parallel`: configures parallel orchestration for running multiple slices concurrently. Keys: