gsd-pi 2.38.0-dev.add4f78 → 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 (117) 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/discuss.md +11 -14
  33. package/dist/resources/extensions/gsd/prompts/execute-task.md +2 -2
  34. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
  35. package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
  36. package/dist/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
  37. package/dist/resources/extensions/gsd/prompts/queue.md +4 -8
  38. package/dist/resources/extensions/gsd/prompts/reactive-execute.md +11 -8
  39. package/dist/resources/extensions/gsd/prompts/run-uat.md +27 -10
  40. package/dist/resources/extensions/gsd/prompts/workflow-start.md +2 -2
  41. package/dist/resources/extensions/gsd/repo-identity.js +19 -3
  42. package/dist/resources/extensions/gsd/roadmap-mutations.js +24 -0
  43. package/dist/resources/extensions/mcp-client/index.js +14 -1
  44. package/package.json +1 -1
  45. package/packages/pi-ai/dist/utils/oauth/anthropic.js +2 -2
  46. package/packages/pi-ai/dist/utils/oauth/anthropic.js.map +1 -1
  47. package/packages/pi-ai/src/utils/oauth/anthropic.ts +2 -2
  48. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  49. package/packages/pi-coding-agent/dist/core/extensions/loader.js +205 -7
  50. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  51. package/packages/pi-coding-agent/src/core/extensions/loader.ts +223 -7
  52. package/src/resources/extensions/github-sync/cli.ts +364 -0
  53. package/src/resources/extensions/github-sync/index.ts +93 -0
  54. package/src/resources/extensions/github-sync/mapping.ts +81 -0
  55. package/src/resources/extensions/github-sync/sync.ts +556 -0
  56. package/src/resources/extensions/github-sync/templates.ts +183 -0
  57. package/src/resources/extensions/github-sync/tests/cli.test.ts +20 -0
  58. package/src/resources/extensions/github-sync/tests/commit-linking.test.ts +39 -0
  59. package/src/resources/extensions/github-sync/tests/mapping.test.ts +104 -0
  60. package/src/resources/extensions/github-sync/tests/templates.test.ts +110 -0
  61. package/src/resources/extensions/github-sync/types.ts +47 -0
  62. package/src/resources/extensions/gsd/auto/session.ts +3 -25
  63. package/src/resources/extensions/gsd/auto-dispatch.ts +1 -1
  64. package/src/resources/extensions/gsd/auto-loop.ts +382 -360
  65. package/src/resources/extensions/gsd/auto-post-unit.ts +29 -3
  66. package/src/resources/extensions/gsd/auto-prompts.ts +25 -45
  67. package/src/resources/extensions/gsd/auto-start.ts +11 -1
  68. package/src/resources/extensions/gsd/auto-worktree.ts +3 -3
  69. package/src/resources/extensions/gsd/auto.ts +139 -86
  70. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
  71. package/src/resources/extensions/gsd/commands.ts +2 -2
  72. package/src/resources/extensions/gsd/context-budget.ts +2 -12
  73. package/src/resources/extensions/gsd/docs/preferences-reference.md +0 -2
  74. package/src/resources/extensions/gsd/doctor-providers.ts +26 -9
  75. package/src/resources/extensions/gsd/doctor.ts +22 -1
  76. package/src/resources/extensions/gsd/exit-command.ts +2 -2
  77. package/src/resources/extensions/gsd/files.ts +3 -1
  78. package/src/resources/extensions/gsd/git-service.ts +20 -10
  79. package/src/resources/extensions/gsd/guided-flow.ts +110 -38
  80. package/src/resources/extensions/gsd/index.ts +21 -16
  81. package/src/resources/extensions/gsd/native-git-bridge.ts +37 -0
  82. package/src/resources/extensions/gsd/preferences-models.ts +0 -12
  83. package/src/resources/extensions/gsd/preferences-types.ts +4 -4
  84. package/src/resources/extensions/gsd/preferences-validation.ts +50 -10
  85. package/src/resources/extensions/gsd/preferences.ts +3 -2
  86. package/src/resources/extensions/gsd/prompts/discuss.md +11 -14
  87. package/src/resources/extensions/gsd/prompts/execute-task.md +2 -2
  88. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
  89. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
  90. package/src/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
  91. package/src/resources/extensions/gsd/prompts/queue.md +4 -8
  92. package/src/resources/extensions/gsd/prompts/reactive-execute.md +11 -8
  93. package/src/resources/extensions/gsd/prompts/run-uat.md +27 -10
  94. package/src/resources/extensions/gsd/prompts/workflow-start.md +2 -2
  95. package/src/resources/extensions/gsd/repo-identity.ts +20 -3
  96. package/src/resources/extensions/gsd/roadmap-mutations.ts +29 -0
  97. package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +21 -18
  98. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +122 -68
  99. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +86 -3
  100. package/src/resources/extensions/gsd/tests/preferences.test.ts +2 -7
  101. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +59 -0
  102. package/src/resources/extensions/gsd/tests/repo-identity-worktree.test.ts +21 -1
  103. package/src/resources/extensions/gsd/tests/run-uat.test.ts +11 -3
  104. package/src/resources/extensions/gsd/types.ts +0 -1
  105. package/src/resources/extensions/mcp-client/index.ts +17 -1
  106. package/dist/resources/extensions/gsd/prompt-compressor.js +0 -393
  107. package/dist/resources/extensions/gsd/semantic-chunker.js +0 -254
  108. package/dist/resources/extensions/gsd/summary-distiller.js +0 -212
  109. package/src/resources/extensions/gsd/prompt-compressor.ts +0 -508
  110. package/src/resources/extensions/gsd/semantic-chunker.ts +0 -336
  111. package/src/resources/extensions/gsd/summary-distiller.ts +0 -258
  112. package/src/resources/extensions/gsd/tests/context-compression.test.ts +0 -193
  113. package/src/resources/extensions/gsd/tests/prompt-compressor.test.ts +0 -529
  114. package/src/resources/extensions/gsd/tests/semantic-chunker.test.ts +0 -426
  115. package/src/resources/extensions/gsd/tests/summary-distiller.test.ts +0 -323
  116. package/src/resources/extensions/gsd/tests/token-optimization-benchmark.test.ts +0 -1272
  117. package/src/resources/extensions/gsd/tests/token-optimization-prefs.test.ts +0 -164
@@ -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) });
@@ -11,11 +11,15 @@ import { resolveMilestoneFile, resolveSliceFile, resolveSlicePath, resolveTasksD
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";
15
- import { compressToTarget } from "./prompt-compressor.js";
16
- import { distillSummaries } from "./summary-distiller.js";
14
+ import { computeBudgets, resolveExecutorContextWindow, truncateAtSectionBoundary } from "./context-budget.js";
17
15
  import { formatDecisionsCompact, formatRequirementsCompact } from "./structured-data-formatter.js";
18
- import { chunkByRelevance, formatChunks } from "./semantic-chunker.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
+ }
19
23
  // ─── Executor Constraints ─────────────────────────────────────────────────────
20
24
  /**
21
25
  * Format executor context constraints for injection into the plan-slice prompt.
@@ -126,14 +130,9 @@ export async function inlineFileSmart(absPath, relPath, label, query, threshold
126
130
  if (content.length <= threshold || !query) {
127
131
  return `### ${label}\nSource: \`${relPath}\`\n\n${content.trim()}`;
128
132
  }
129
- // Use semantic chunking for large files
130
- const result = chunkByRelevance(content, query, { maxChunks: 5, minScore: 0.05 });
131
- // If chunking didn't save much (< 20%), just include full content
132
- if (result.savingsPercent < 20) {
133
- return `### ${label}\nSource: \`${relPath}\`\n\n${content.trim()}`;
134
- }
135
- const formatted = formatChunks(result, relPath);
136
- return `### ${label} (${result.omittedChunks} sections omitted for relevance)\nSource: \`${relPath}\`\n\n${formatted}`;
133
+ // For large files, truncate at section boundary
134
+ const truncated = truncateAtSectionBoundary(content, threshold).content;
135
+ return `### ${label}\nSource: \`${relPath}\`\n\n${truncated}`;
137
136
  }
138
137
  /**
139
138
  * Load and inline dependency slice summaries (full content, not just paths).
@@ -165,21 +164,6 @@ export async function inlineDependencySummaries(mid, sid, base, budgetChars) {
165
164
  }
166
165
  const result = sections.join("\n\n");
167
166
  if (budgetChars !== undefined && result.length > budgetChars) {
168
- // For 3+ summaries, try distillation first (preserves more information)
169
- if (sections.length >= 3) {
170
- const rawSummaries = sections.map(s => {
171
- // Extract content after the header line
172
- const lines = s.split("\n");
173
- const contentStart = lines.findIndex(l => l.startsWith("Source:"));
174
- return contentStart >= 0 ? lines.slice(contentStart + 1).join("\n").trim() : s;
175
- });
176
- const distilled = distillSummaries(rawSummaries, budgetChars);
177
- if (distilled.content.length <= budgetChars) {
178
- return distilled.content;
179
- }
180
- }
181
- // Fall back to section-boundary truncation
182
- const { truncateAtSectionBoundary } = await import("./context-budget.js");
183
167
  return truncateAtSectionBoundary(result, budgetChars).content;
184
168
  }
185
169
  return result;
@@ -546,7 +530,7 @@ export async function buildResearchMilestonePrompt(mid, midTitle, base) {
546
530
  if (knowledgeInlineRM)
547
531
  inlined.push(knowledgeInlineRM);
548
532
  inlined.push(inlineTemplate("research", "Research"));
549
- 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")}`);
550
534
  const outputRelPath = relMilestoneFile(base, mid, "RESEARCH");
551
535
  return loadPrompt("research-milestone", {
552
536
  workingDirectory: base,
@@ -599,7 +583,7 @@ export async function buildPlanMilestonePrompt(mid, midTitle, base, level) {
599
583
  inlined.push(inlineTemplate("plan", "Slice Plan"));
600
584
  inlined.push(inlineTemplate("task-plan", "Task Plan"));
601
585
  }
602
- 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")}`);
603
587
  const outputRelPath = relMilestoneFile(base, mid, "ROADMAP");
604
588
  const researchOutputPath = join(base, relMilestoneFile(base, mid, "RESEARCH"));
605
589
  const secretsOutputPath = join(base, relMilestoneFile(base, mid, "SECRETS"));
@@ -647,7 +631,7 @@ export async function buildResearchSlicePrompt(mid, _midTitle, sid, sTitle, base
647
631
  const overridesInline = formatOverridesSection(activeOverrides);
648
632
  if (overridesInline)
649
633
  inlined.unshift(overridesInline);
650
- 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")}`);
651
635
  const outputRelPath = relSliceFile(base, mid, sid, "RESEARCH");
652
636
  return loadPrompt("research-slice", {
653
637
  workingDirectory: base,
@@ -693,7 +677,7 @@ export async function buildPlanSlicePrompt(mid, _midTitle, sid, sTitle, base, le
693
677
  const planOverridesInline = formatOverridesSection(planActiveOverrides);
694
678
  if (planOverridesInline)
695
679
  inlined.unshift(planOverridesInline);
696
- 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")}`);
697
681
  // Build executor context constraints from the budget engine
698
682
  const executorContextConstraints = formatExecutorConstraints();
699
683
  const outputRelPath = relSliceFile(base, mid, sid, "PLAN");
@@ -777,15 +761,11 @@ export async function buildExecuteTaskPrompt(mid, sid, sTitle, tid, tTitle, base
777
761
  const contextWindow = resolveExecutorContextWindow(undefined, prefs?.preferences);
778
762
  const budgets = computeBudgets(contextWindow);
779
763
  const verificationBudget = `~${Math.round(budgets.verificationBudgetChars / 1000)}K chars`;
780
- // Compress carry-forward section when it exceeds 40% of inline context budget.
781
- // Only compress when compression_strategy is "compress" (budget/balanced profiles).
764
+ // Truncate carry-forward section when it exceeds 40% of inline context budget.
782
765
  const carryForwardBudget = Math.floor(budgets.inlineContextBudgetChars * 0.4);
783
766
  let finalCarryForward = carryForwardSection;
784
767
  if (carryForwardSection.length > carryForwardBudget) {
785
- const { resolveCompressionStrategy } = await import("./preferences.js");
786
- if (resolveCompressionStrategy() === "compress") {
787
- finalCarryForward = compressToTarget(carryForwardSection, carryForwardBudget).content;
788
- }
768
+ finalCarryForward = truncateAtSectionBoundary(carryForwardSection, carryForwardBudget).content;
789
769
  }
790
770
  return loadPrompt("execute-task", {
791
771
  overridesSection,
@@ -843,7 +823,7 @@ export async function buildCompleteSlicePrompt(mid, _midTitle, sid, sTitle, base
843
823
  const completeOverridesInline = formatOverridesSection(completeActiveOverrides);
844
824
  if (completeOverridesInline)
845
825
  inlined.unshift(completeOverridesInline);
846
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
826
+ const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
847
827
  const sliceRel = relSlicePath(base, mid, sid);
848
828
  const sliceSummaryPath = join(base, `${sliceRel}/${sid}-SUMMARY.md`);
849
829
  const sliceUatPath = join(base, `${sliceRel}/${sid}-UAT.md`);
@@ -899,7 +879,7 @@ export async function buildCompleteMilestonePrompt(mid, midTitle, base, level) {
899
879
  if (contextInline)
900
880
  inlined.push(contextInline);
901
881
  inlined.push(inlineTemplate("milestone-summary", "Milestone Summary"));
902
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
882
+ const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
903
883
  const milestoneSummaryPath = join(base, `${relMilestonePath(base, mid)}/${mid}-SUMMARY.md`);
904
884
  return loadPrompt("complete-milestone", {
905
885
  workingDirectory: base,
@@ -966,7 +946,7 @@ export async function buildValidateMilestonePrompt(mid, midTitle, base, level) {
966
946
  const contextInline = await inlineFileOptional(contextPath, contextRel, "Milestone Context");
967
947
  if (contextInline)
968
948
  inlined.push(contextInline);
969
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
949
+ const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
970
950
  const validationOutputPath = join(base, `${relMilestonePath(base, mid)}/${mid}-VALIDATION.md`);
971
951
  const roadmapOutputPath = `${relMilestonePath(base, mid)}/${mid}-ROADMAP.md`;
972
952
  return loadPrompt("validate-milestone", {
@@ -1014,7 +994,7 @@ export async function buildReplanSlicePrompt(mid, midTitle, sid, sTitle, base) {
1014
994
  const replanOverridesInline = formatOverridesSection(replanActiveOverrides);
1015
995
  if (replanOverridesInline)
1016
996
  inlined.unshift(replanOverridesInline);
1017
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
997
+ const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
1018
998
  const replanPath = join(base, `${relSlicePath(base, mid, sid)}/${sid}-REPLAN.md`);
1019
999
  // Build capture context for replan prompt (captures that triggered this replan)
1020
1000
  let captureContext = "(none)";
@@ -1054,7 +1034,7 @@ export async function buildRunUatPrompt(mid, sliceId, uatPath, uatContent, base)
1054
1034
  const projectInline = await inlineProjectFromDb(base);
1055
1035
  if (projectInline)
1056
1036
  inlined.push(projectInline);
1057
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
1037
+ const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
1058
1038
  const uatResultPath = join(base, relSliceFile(base, mid, sliceId, "UAT-RESULT"));
1059
1039
  const uatType = extractUatType(uatContent) ?? "human-experience";
1060
1040
  return loadPrompt("run-uat", {
@@ -1090,7 +1070,7 @@ export async function buildReassessRoadmapPrompt(mid, midTitle, completedSliceId
1090
1070
  const knowledgeInlineRA = await inlineGsdRootFile(base, "knowledge.md", "Project Knowledge");
1091
1071
  if (knowledgeInlineRA)
1092
1072
  inlined.push(knowledgeInlineRA);
1093
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
1073
+ const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
1094
1074
  const assessmentPath = join(base, relSliceFile(base, mid, completedSliceId, "ASSESSMENT"));
1095
1075
  // Build deferred captures context for reassess prompt
1096
1076
  let deferredCaptures = "(none)";
@@ -11,7 +11,7 @@
11
11
  import { deriveState } from "./state.js";
12
12
  import { loadFile, getManifestStatus } from "./files.js";
13
13
  import { loadEffectiveGSDPreferences, resolveSkillDiscoveryMode, getIsolationMode, } from "./preferences.js";
14
- import { ensureGsdSymlink } from "./repo-identity.js";
14
+ import { ensureGsdSymlink, validateProjectId } from "./repo-identity.js";
15
15
  import { migrateToExternalState, recoverFailedMigration } from "./migrate-external.js";
16
16
  import { collectSecretsFromManifest } from "../get-secrets-from-user.js";
17
17
  import { gsdRoot, resolveMilestoneFile } from "./paths.js";
@@ -63,6 +63,12 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
63
63
  return false;
64
64
  }
65
65
  try {
66
+ // Validate GSD_PROJECT_ID early so the user gets immediate feedback
67
+ const customProjectId = process.env.GSD_PROJECT_ID;
68
+ if (customProjectId && !validateProjectId(customProjectId)) {
69
+ ctx.ui.notify(`GSD_PROJECT_ID must contain only alphanumeric characters, hyphens, and underscores. Got: "${customProjectId}"`, "error");
70
+ return releaseLockAndReturn();
71
+ }
66
72
  // Ensure git repo exists
67
73
  if (!nativeIsRepo(base)) {
68
74
  const mainBranch = loadEffectiveGSDPreferences()?.preferences?.git?.main_branch || "main";
@@ -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
  }
@@ -297,100 +297,163 @@ export async function stopAuto(ctx, pi, reason) {
297
297
  return;
298
298
  const loadedPreferences = loadEffectiveGSDPreferences()?.preferences;
299
299
  const reasonSuffix = reason ? ` — ${reason}` : "";
300
- clearUnitTimeout();
301
- if (lockBase())
302
- clearLock(lockBase());
303
- if (lockBase())
304
- releaseSessionLock(lockBase());
305
- clearSkillSnapshot();
306
- resetSkillTelemetry();
307
- // Remove SIGTERM handler registered at auto-mode start
308
- deregisterSigtermHandler();
309
- // ── Auto-worktree: exit worktree and reset s.basePath on stop ──
310
- if (s.currentMilestoneId) {
311
- const notifyCtx = ctx
312
- ? { notify: ctx.ui.notify.bind(ctx.ui) }
313
- : { notify: () => { } };
314
- buildResolver().exitMilestone(s.currentMilestoneId, notifyCtx, {
315
- preserveBranch: true,
316
- });
317
- }
318
- // ── DB cleanup: close the SQLite connection ──
319
- if (isDbAvailable()) {
300
+ try {
301
+ // ── Step 1: Timers and locks ──
320
302
  try {
321
- const { closeDatabase } = await import("./gsd-db.js");
322
- closeDatabase();
303
+ clearUnitTimeout();
304
+ if (lockBase())
305
+ clearLock(lockBase());
306
+ if (lockBase())
307
+ releaseSessionLock(lockBase());
323
308
  }
324
309
  catch (e) {
325
- debugLog("db-close-failed", {
326
- error: e instanceof Error ? e.message : String(e),
327
- });
310
+ debugLog("stop-cleanup-locks", { error: e instanceof Error ? e.message : String(e) });
328
311
  }
329
- }
330
- if (s.originalBasePath) {
331
- s.basePath = s.originalBasePath;
312
+ // ── Step 2: Skill state ──
332
313
  try {
333
- process.chdir(s.basePath);
314
+ clearSkillSnapshot();
315
+ resetSkillTelemetry();
334
316
  }
335
- catch {
336
- /* best-effort */
317
+ catch (e) {
318
+ debugLog("stop-cleanup-skills", { error: e instanceof Error ? e.message : String(e) });
337
319
  }
338
- }
339
- const ledger = getLedger();
340
- if (ledger && ledger.units.length > 0) {
341
- const totals = getProjectTotals(ledger.units);
342
- ctx?.ui.notify(`Auto-mode stopped${reasonSuffix}. Session: ${formatCost(totals.cost)} · ${formatTokenCount(totals.tokens.total)} tokens · ${ledger.units.length} units`, "info");
343
- }
344
- else {
345
- ctx?.ui.notify(`Auto-mode stopped${reasonSuffix}.`, "info");
346
- }
347
- if (s.basePath) {
320
+ // ── Step 3: SIGTERM handler ──
348
321
  try {
349
- await rebuildState(s.basePath);
322
+ deregisterSigtermHandler();
350
323
  }
351
324
  catch (e) {
352
- debugLog("stop-rebuild-state-failed", {
353
- error: e instanceof Error ? e.message : String(e),
354
- });
325
+ debugLog("stop-cleanup-sigterm", { error: e instanceof Error ? e.message : String(e) });
355
326
  }
356
- }
357
- clearCmuxSidebar(loadedPreferences);
358
- logCmuxEvent(loadedPreferences, `Auto-mode stopped${reasonSuffix || ""}.`, reason?.startsWith("Blocked:") ? "warning" : "info");
359
- if (isDebugEnabled()) {
360
- const logPath = writeDebugSummary();
361
- if (logPath) {
362
- ctx?.ui.notify(`Debug log written → ${logPath}`, "info");
327
+ // ── Step 4: Auto-worktree exit ──
328
+ try {
329
+ if (s.currentMilestoneId) {
330
+ const notifyCtx = ctx
331
+ ? { notify: ctx.ui.notify.bind(ctx.ui) }
332
+ : { notify: () => { } };
333
+ buildResolver().exitMilestone(s.currentMilestoneId, notifyCtx, {
334
+ preserveBranch: true,
335
+ });
336
+ }
337
+ }
338
+ catch (e) {
339
+ debugLog("stop-cleanup-worktree", { error: e instanceof Error ? e.message : String(e) });
340
+ }
341
+ // ── Step 5: DB cleanup ──
342
+ if (isDbAvailable()) {
343
+ try {
344
+ const { closeDatabase } = await import("./gsd-db.js");
345
+ closeDatabase();
346
+ }
347
+ catch (e) {
348
+ debugLog("db-close-failed", {
349
+ error: e instanceof Error ? e.message : String(e),
350
+ });
351
+ }
352
+ }
353
+ // ── Step 6: Restore basePath and chdir ──
354
+ try {
355
+ if (s.originalBasePath) {
356
+ s.basePath = s.originalBasePath;
357
+ try {
358
+ process.chdir(s.basePath);
359
+ }
360
+ catch {
361
+ /* best-effort */
362
+ }
363
+ }
364
+ }
365
+ catch (e) {
366
+ debugLog("stop-cleanup-basepath", { error: e instanceof Error ? e.message : String(e) });
367
+ }
368
+ // ── Step 7: Ledger notification ──
369
+ try {
370
+ const ledger = getLedger();
371
+ if (ledger && ledger.units.length > 0) {
372
+ const totals = getProjectTotals(ledger.units);
373
+ ctx?.ui.notify(`Auto-mode stopped${reasonSuffix}. Session: ${formatCost(totals.cost)} · ${formatTokenCount(totals.tokens.total)} tokens · ${ledger.units.length} units`, "info");
374
+ }
375
+ else {
376
+ ctx?.ui.notify(`Auto-mode stopped${reasonSuffix}.`, "info");
377
+ }
378
+ }
379
+ catch (e) {
380
+ debugLog("stop-cleanup-ledger", { error: e instanceof Error ? e.message : String(e) });
381
+ }
382
+ // ── Step 8: Rebuild state ──
383
+ if (s.basePath) {
384
+ try {
385
+ await rebuildState(s.basePath);
386
+ }
387
+ catch (e) {
388
+ debugLog("stop-rebuild-state-failed", {
389
+ error: e instanceof Error ? e.message : String(e),
390
+ });
391
+ }
392
+ }
393
+ // ── Step 9: Cmux sidebar / event log ──
394
+ try {
395
+ clearCmuxSidebar(loadedPreferences);
396
+ logCmuxEvent(loadedPreferences, `Auto-mode stopped${reasonSuffix || ""}.`, reason?.startsWith("Blocked:") ? "warning" : "info");
397
+ }
398
+ catch (e) {
399
+ debugLog("stop-cleanup-cmux", { error: e instanceof Error ? e.message : String(e) });
400
+ }
401
+ // ── Step 10: Debug summary ──
402
+ try {
403
+ if (isDebugEnabled()) {
404
+ const logPath = writeDebugSummary();
405
+ if (logPath) {
406
+ ctx?.ui.notify(`Debug log written → ${logPath}`, "info");
407
+ }
408
+ }
409
+ }
410
+ catch (e) {
411
+ debugLog("stop-cleanup-debug", { error: e instanceof Error ? e.message : String(e) });
412
+ }
413
+ // ── Step 11: Reset metrics, routing, hooks ──
414
+ try {
415
+ resetMetrics();
416
+ resetRoutingHistory();
417
+ resetHookState();
418
+ if (s.basePath)
419
+ clearPersistedHookState(s.basePath);
420
+ }
421
+ catch (e) {
422
+ debugLog("stop-cleanup-metrics", { error: e instanceof Error ? e.message : String(e) });
423
+ }
424
+ // ── Step 12: Remove paused-session metadata (#1383) ──
425
+ try {
426
+ const pausedPath = join(gsdRoot(s.originalBasePath || s.basePath), "runtime", "paused-session.json");
427
+ if (existsSync(pausedPath))
428
+ unlinkSync(pausedPath);
429
+ }
430
+ catch { /* non-fatal */ }
431
+ // ── Step 13: Restore original model (before reset clears IDs) ──
432
+ try {
433
+ if (pi && ctx && s.originalModelId && s.originalModelProvider) {
434
+ const original = ctx.modelRegistry.find(s.originalModelProvider, s.originalModelId);
435
+ if (original)
436
+ await pi.setModel(original);
437
+ }
438
+ }
439
+ catch (e) {
440
+ debugLog("stop-cleanup-model", { error: e instanceof Error ? e.message : String(e) });
363
441
  }
364
442
  }
365
- resetMetrics();
366
- resetRoutingHistory();
367
- resetHookState();
368
- if (s.basePath)
369
- clearPersistedHookState(s.basePath);
370
- // Remove paused-session metadata if present (#1383)
371
- try {
372
- const pausedPath = join(gsdRoot(s.originalBasePath || s.basePath), "runtime", "paused-session.json");
373
- if (existsSync(pausedPath))
374
- unlinkSync(pausedPath);
375
- }
376
- catch { /* non-fatal */ }
377
- // Restore original model before reset() clears the IDs
378
- if (pi && ctx && s.originalModelId && s.originalModelProvider) {
379
- const original = ctx.modelRegistry.find(s.originalModelProvider, s.originalModelId);
380
- if (original)
381
- await pi.setModel(original);
443
+ finally {
444
+ // ── Critical invariants: these MUST execute regardless of errors ──
445
+ // External cleanup (not covered by session reset)
446
+ clearInFlightTools();
447
+ clearSliceProgressCache();
448
+ clearActivityLogState();
449
+ resetProactiveHealing();
450
+ // UI cleanup
451
+ ctx?.ui.setStatus("gsd-auto", undefined);
452
+ ctx?.ui.setWidget("gsd-progress", undefined);
453
+ ctx?.ui.setFooter(undefined);
454
+ // Reset all session state in one call
455
+ s.reset();
382
456
  }
383
- // External cleanup (not covered by session reset)
384
- clearInFlightTools();
385
- clearSliceProgressCache();
386
- clearActivityLogState();
387
- resetProactiveHealing();
388
- // UI cleanup
389
- ctx?.ui.setStatus("gsd-auto", undefined);
390
- ctx?.ui.setWidget("gsd-progress", undefined);
391
- ctx?.ui.setFooter(undefined);
392
- // Reset all session state in one call
393
- s.reset();
394
457
  }
395
458
  /**
396
459
  * Pause auto-mode without destroying state. Context is preserved.
@@ -631,7 +631,7 @@ export function serializePreferencesToFrontmatter(prefs) {
631
631
  "dynamic_routing", "token_profile", "phases", "parallel",
632
632
  "auto_visualize", "auto_report",
633
633
  "verification_commands", "verification_auto_fix", "verification_max_retries",
634
- "search_provider", "compression_strategy", "context_selection",
634
+ "search_provider", "context_selection",
635
635
  ];
636
636
  const seen = new Set();
637
637
  for (const key of orderedKeys) {
@@ -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);
@@ -8,7 +8,6 @@
8
8
  * @see D001 (module location), D002 (200K fallback), D003 (section-boundary truncation)
9
9
  */
10
10
  import { getCharsPerToken } from "./token-counter.js";
11
- import { compressToTarget } from "./prompt-compressor.js";
12
11
  // ─── Budget ratio constants ──────────────────────────────────────────────────
13
12
  // Percentages of total context window allocated to each budget category.
14
13
  // These are applied after tokens→chars conversion.
@@ -132,20 +131,13 @@ export function resolveExecutorContextWindow(registry, preferences, sessionConte
132
131
  return DEFAULT_CONTEXT_WINDOW;
133
132
  }
134
133
  /**
135
- * Smart context reduction: compress first, then truncate if still over budget.
136
- * Returns the content within budget with maximum information preservation.
134
+ * Reduce content to fit within budget using section-boundary truncation.
137
135
  */
138
136
  export function reduceToFit(content, budgetChars) {
139
137
  if (!content || content.length <= budgetChars) {
140
138
  return { content, droppedSections: 0 };
141
139
  }
142
- // Step 1: Try compression
143
- const compressed = compressToTarget(content, budgetChars);
144
- if (compressed.compressedChars <= budgetChars) {
145
- return { content: compressed.content, droppedSections: 0 };
146
- }
147
- // Step 2: Truncate the compressed content at section boundaries
148
- return truncateAtSectionBoundary(compressed.content, budgetChars);
140
+ return truncateAtSectionBoundary(content, budgetChars);
149
141
  }
150
142
  // ─── Internal helpers ────────────────────────────────────────────────────────
151
143
  /**
@@ -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: