gsd-pi 2.35.0 → 2.36.0-dev.f887f4e

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 (164) hide show
  1. package/README.md +3 -1
  2. package/dist/cli.js +7 -2
  3. package/dist/resource-loader.d.ts +1 -1
  4. package/dist/resource-loader.js +13 -1
  5. package/dist/resources/extensions/async-jobs/await-tool.js +0 -2
  6. package/dist/resources/extensions/async-jobs/job-manager.js +0 -6
  7. package/dist/resources/extensions/bg-shell/output-formatter.js +1 -19
  8. package/dist/resources/extensions/bg-shell/process-manager.js +0 -4
  9. package/dist/resources/extensions/bg-shell/types.js +0 -2
  10. package/dist/resources/extensions/context7/index.js +5 -0
  11. package/dist/resources/extensions/get-secrets-from-user.js +2 -30
  12. package/dist/resources/extensions/google-search/index.js +5 -0
  13. package/dist/resources/extensions/gsd/auto-dispatch.js +43 -1
  14. package/dist/resources/extensions/gsd/auto-loop.js +17 -3
  15. package/dist/resources/extensions/gsd/auto-model-selection.js +15 -3
  16. package/dist/resources/extensions/gsd/auto-recovery.js +35 -0
  17. package/dist/resources/extensions/gsd/auto-start.js +35 -2
  18. package/dist/resources/extensions/gsd/auto.js +59 -4
  19. package/dist/resources/extensions/gsd/commands-handlers.js +2 -2
  20. package/dist/resources/extensions/gsd/commands-inspect.js +10 -3
  21. package/dist/resources/extensions/gsd/commands-rate.js +31 -0
  22. package/dist/resources/extensions/gsd/commands.js +43 -1
  23. package/dist/resources/extensions/gsd/doctor-environment.js +26 -17
  24. package/dist/resources/extensions/gsd/files.js +11 -2
  25. package/dist/resources/extensions/gsd/gitignore.js +54 -7
  26. package/dist/resources/extensions/gsd/guided-flow.js +8 -2
  27. package/dist/resources/extensions/gsd/health-widget-core.js +96 -0
  28. package/dist/resources/extensions/gsd/health-widget.js +97 -46
  29. package/dist/resources/extensions/gsd/index.js +26 -33
  30. package/dist/resources/extensions/gsd/migrate-external.js +55 -2
  31. package/dist/resources/extensions/gsd/milestone-ids.js +3 -2
  32. package/dist/resources/extensions/gsd/paths.js +74 -7
  33. package/dist/resources/extensions/gsd/post-unit-hooks.js +4 -1
  34. package/dist/resources/extensions/gsd/preferences-validation.js +16 -1
  35. package/dist/resources/extensions/gsd/preferences.js +12 -0
  36. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +2 -0
  37. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +2 -0
  38. package/dist/resources/extensions/gsd/roadmap-mutations.js +55 -0
  39. package/dist/resources/extensions/gsd/session-lock.js +53 -2
  40. package/dist/resources/extensions/gsd/state.js +2 -1
  41. package/dist/resources/extensions/gsd/templates/plan.md +8 -0
  42. package/dist/resources/extensions/gsd/worktree-resolver.js +12 -0
  43. package/dist/resources/extensions/remote-questions/remote-command.js +2 -22
  44. package/dist/resources/extensions/shared/mod.js +1 -1
  45. package/dist/resources/extensions/shared/sanitize.js +30 -0
  46. package/dist/resources/extensions/subagent/index.js +6 -14
  47. package/dist/resources/skills/core-web-vitals/SKILL.md +1 -1
  48. package/dist/resources/skills/create-gsd-extension/workflows/debug-extension.md +1 -1
  49. package/dist/resources/skills/github-workflows/SKILL.md +0 -2
  50. package/dist/resources/skills/web-quality-audit/SKILL.md +0 -2
  51. package/package.json +2 -1
  52. package/packages/pi-agent-core/dist/agent.d.ts +10 -2
  53. package/packages/pi-agent-core/dist/agent.d.ts.map +1 -1
  54. package/packages/pi-agent-core/dist/agent.js +19 -8
  55. package/packages/pi-agent-core/dist/agent.js.map +1 -1
  56. package/packages/pi-agent-core/src/agent.ts +31 -10
  57. package/packages/pi-ai/dist/providers/openai-responses.js +1 -1
  58. package/packages/pi-ai/dist/providers/openai-responses.js.map +1 -1
  59. package/packages/pi-ai/src/providers/openai-responses.ts +1 -1
  60. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  61. package/packages/pi-coding-agent/dist/core/agent-session.js +20 -4
  62. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  63. package/packages/pi-coding-agent/dist/core/resource-loader.d.ts.map +1 -1
  64. package/packages/pi-coding-agent/dist/core/resource-loader.js +13 -2
  65. package/packages/pi-coding-agent/dist/core/resource-loader.js.map +1 -1
  66. package/packages/pi-coding-agent/package.json +1 -1
  67. package/packages/pi-coding-agent/src/core/agent-session.ts +36 -12
  68. package/packages/pi-coding-agent/src/core/resource-loader.ts +13 -2
  69. package/pkg/package.json +1 -1
  70. package/src/resources/extensions/async-jobs/await-tool.ts +0 -2
  71. package/src/resources/extensions/async-jobs/job-manager.ts +0 -7
  72. package/src/resources/extensions/bg-shell/output-formatter.ts +0 -17
  73. package/src/resources/extensions/bg-shell/process-manager.ts +0 -4
  74. package/src/resources/extensions/bg-shell/types.ts +0 -12
  75. package/src/resources/extensions/context7/index.ts +7 -0
  76. package/src/resources/extensions/get-secrets-from-user.ts +2 -35
  77. package/src/resources/extensions/google-search/index.ts +7 -0
  78. package/src/resources/extensions/gsd/auto-dispatch.ts +49 -1
  79. package/src/resources/extensions/gsd/auto-loop.ts +22 -2
  80. package/src/resources/extensions/gsd/auto-model-selection.ts +23 -2
  81. package/src/resources/extensions/gsd/auto-recovery.ts +39 -0
  82. package/src/resources/extensions/gsd/auto-start.ts +42 -2
  83. package/src/resources/extensions/gsd/auto.ts +61 -3
  84. package/src/resources/extensions/gsd/commands-handlers.ts +2 -2
  85. package/src/resources/extensions/gsd/commands-inspect.ts +10 -3
  86. package/src/resources/extensions/gsd/commands-rate.ts +55 -0
  87. package/src/resources/extensions/gsd/commands.ts +43 -1
  88. package/src/resources/extensions/gsd/doctor-environment.ts +26 -16
  89. package/src/resources/extensions/gsd/files.ts +12 -2
  90. package/src/resources/extensions/gsd/gitignore.ts +54 -7
  91. package/src/resources/extensions/gsd/guided-flow.ts +8 -2
  92. package/src/resources/extensions/gsd/health-widget-core.ts +129 -0
  93. package/src/resources/extensions/gsd/health-widget.ts +103 -59
  94. package/src/resources/extensions/gsd/index.ts +30 -33
  95. package/src/resources/extensions/gsd/migrate-external.ts +47 -2
  96. package/src/resources/extensions/gsd/milestone-ids.ts +3 -2
  97. package/src/resources/extensions/gsd/paths.ts +73 -7
  98. package/src/resources/extensions/gsd/post-unit-hooks.ts +5 -1
  99. package/src/resources/extensions/gsd/preferences-validation.ts +16 -1
  100. package/src/resources/extensions/gsd/preferences.ts +14 -1
  101. package/src/resources/extensions/gsd/prompts/complete-milestone.md +2 -0
  102. package/src/resources/extensions/gsd/prompts/validate-milestone.md +2 -0
  103. package/src/resources/extensions/gsd/roadmap-mutations.ts +66 -0
  104. package/src/resources/extensions/gsd/session-lock.ts +59 -2
  105. package/src/resources/extensions/gsd/state.ts +2 -1
  106. package/src/resources/extensions/gsd/templates/plan.md +8 -0
  107. package/src/resources/extensions/gsd/tests/commands-inspect-open-db.test.ts +46 -0
  108. package/src/resources/extensions/gsd/tests/files-loadfile-eisdir.test.ts +20 -0
  109. package/src/resources/extensions/gsd/tests/gitignore-tracked-gsd.test.ts +214 -0
  110. package/src/resources/extensions/gsd/tests/health-widget.test.ts +158 -0
  111. package/src/resources/extensions/gsd/tests/paths.test.ts +113 -0
  112. package/src/resources/extensions/gsd/tests/preferences.test.ts +12 -2
  113. package/src/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +26 -0
  114. package/src/resources/extensions/gsd/tests/test-utils.ts +165 -0
  115. package/src/resources/extensions/gsd/tests/validate-directory.test.ts +15 -0
  116. package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +7 -0
  117. package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +32 -0
  118. package/src/resources/extensions/gsd/worktree-resolver.ts +11 -0
  119. package/src/resources/extensions/remote-questions/remote-command.ts +2 -23
  120. package/src/resources/extensions/shared/mod.ts +1 -1
  121. package/src/resources/extensions/shared/sanitize.ts +36 -0
  122. package/src/resources/extensions/subagent/index.ts +6 -12
  123. package/src/resources/skills/core-web-vitals/SKILL.md +1 -1
  124. package/src/resources/skills/create-gsd-extension/workflows/debug-extension.md +1 -1
  125. package/src/resources/skills/github-workflows/SKILL.md +0 -2
  126. package/src/resources/skills/web-quality-audit/SKILL.md +0 -2
  127. package/dist/resources/extensions/shared/wizard-ui.js +0 -478
  128. package/dist/resources/skills/swiftui/SKILL.md +0 -208
  129. package/dist/resources/skills/swiftui/references/animations.md +0 -921
  130. package/dist/resources/skills/swiftui/references/architecture.md +0 -1561
  131. package/dist/resources/skills/swiftui/references/layout-system.md +0 -1186
  132. package/dist/resources/skills/swiftui/references/navigation.md +0 -1492
  133. package/dist/resources/skills/swiftui/references/networking-async.md +0 -214
  134. package/dist/resources/skills/swiftui/references/performance.md +0 -1706
  135. package/dist/resources/skills/swiftui/references/platform-integration.md +0 -204
  136. package/dist/resources/skills/swiftui/references/state-management.md +0 -1443
  137. package/dist/resources/skills/swiftui/references/swiftdata.md +0 -297
  138. package/dist/resources/skills/swiftui/references/testing-debugging.md +0 -247
  139. package/dist/resources/skills/swiftui/references/uikit-appkit-interop.md +0 -218
  140. package/dist/resources/skills/swiftui/workflows/add-feature.md +0 -191
  141. package/dist/resources/skills/swiftui/workflows/build-new-app.md +0 -311
  142. package/dist/resources/skills/swiftui/workflows/debug-swiftui.md +0 -192
  143. package/dist/resources/skills/swiftui/workflows/optimize-performance.md +0 -197
  144. package/dist/resources/skills/swiftui/workflows/ship-app.md +0 -203
  145. package/dist/resources/skills/swiftui/workflows/write-tests.md +0 -235
  146. package/src/resources/extensions/shared/wizard-ui.ts +0 -551
  147. package/src/resources/skills/swiftui/SKILL.md +0 -208
  148. package/src/resources/skills/swiftui/references/animations.md +0 -921
  149. package/src/resources/skills/swiftui/references/architecture.md +0 -1561
  150. package/src/resources/skills/swiftui/references/layout-system.md +0 -1186
  151. package/src/resources/skills/swiftui/references/navigation.md +0 -1492
  152. package/src/resources/skills/swiftui/references/networking-async.md +0 -214
  153. package/src/resources/skills/swiftui/references/performance.md +0 -1706
  154. package/src/resources/skills/swiftui/references/platform-integration.md +0 -204
  155. package/src/resources/skills/swiftui/references/state-management.md +0 -1443
  156. package/src/resources/skills/swiftui/references/swiftdata.md +0 -297
  157. package/src/resources/skills/swiftui/references/testing-debugging.md +0 -247
  158. package/src/resources/skills/swiftui/references/uikit-appkit-interop.md +0 -218
  159. package/src/resources/skills/swiftui/workflows/add-feature.md +0 -191
  160. package/src/resources/skills/swiftui/workflows/build-new-app.md +0 -311
  161. package/src/resources/skills/swiftui/workflows/debug-swiftui.md +0 -192
  162. package/src/resources/skills/swiftui/workflows/optimize-performance.md +0 -197
  163. package/src/resources/skills/swiftui/workflows/ship-app.md +0 -203
  164. package/src/resources/skills/swiftui/workflows/write-tests.md +0 -235
@@ -11,9 +11,9 @@ import { existsSync, statSync } from "node:fs";
11
11
  import { resolve } from "node:path";
12
12
 
13
13
  import type { ExtensionAPI, Theme } from "@gsd/pi-coding-agent";
14
- import { CURSOR_MARKER, Editor, type EditorTheme, Key, matchesKey, Text, truncateToWidth, wrapTextWithAnsi } from "@gsd/pi-tui";
14
+ import { Editor, type EditorTheme, Key, matchesKey, Text, truncateToWidth, wrapTextWithAnsi } from "@gsd/pi-tui";
15
15
  import { Type } from "@sinclair/typebox";
16
- import { makeUI, type ProgressStatus } from "./shared/mod.js";
16
+ import { makeUI, maskEditorLine, type ProgressStatus } from "./shared/mod.js";
17
17
  import { parseSecretsManifest, formatSecretsManifest } from "./gsd/files.js";
18
18
  import { resolveMilestoneFile } from "./gsd/paths.js";
19
19
  import type { SecretsManifestEntry } from "./gsd/types.js";
@@ -42,39 +42,6 @@ function maskPreview(value: string): string {
42
42
  return `${value.slice(0, 4)}${"*".repeat(Math.max(4, value.length - 8))}${value.slice(-4)}`;
43
43
  }
44
44
 
45
- /**
46
- * Replace editor visible text with masked characters while preserving ANSI cursor/sequencer codes.
47
- */
48
- function maskEditorLine(line: string): string {
49
- // Keep border / metadata lines readable.
50
- if (line.startsWith("─")) {
51
- return line;
52
- }
53
-
54
- let output = "";
55
- let i = 0;
56
- while (i < line.length) {
57
- if (line.startsWith(CURSOR_MARKER, i)) {
58
- output += CURSOR_MARKER;
59
- i += CURSOR_MARKER.length;
60
- continue;
61
- }
62
-
63
- const ansiMatch = /^\x1b\[[0-9;]*m/.exec(line.slice(i));
64
- if (ansiMatch) {
65
- output += ansiMatch[0];
66
- i += ansiMatch[0].length;
67
- continue;
68
- }
69
-
70
- const ch = line[i] as string;
71
- output += ch === " " ? " " : "*";
72
- i += 1;
73
- }
74
-
75
- return output;
76
- }
77
-
78
45
  function shellEscapeSingle(value: string): string {
79
46
  return `'${value.replace(/'/g, `'\\''`)}'`;
80
47
  }
@@ -411,6 +411,13 @@ export default function (pi: ExtensionAPI) {
411
411
  },
412
412
  });
413
413
 
414
+ // ── Session cleanup ─────────────────────────────────────────────────────
415
+
416
+ pi.on("session_shutdown", async () => {
417
+ resultCache.clear();
418
+ client = null;
419
+ });
420
+
414
421
  // ── Startup notification ─────────────────────────────────────────────────
415
422
 
416
423
  pi.on("session_start", async (_event, ctx) => {
@@ -12,14 +12,16 @@
12
12
  import type { GSDState } from "./types.js";
13
13
  import type { GSDPreferences } from "./preferences.js";
14
14
  import type { UatType } from "./files.js";
15
- import { loadFile, extractUatType, loadActiveOverrides } from "./files.js";
15
+ import { loadFile, extractUatType, loadActiveOverrides, parseRoadmap } from "./files.js";
16
16
  import {
17
17
  resolveMilestoneFile,
18
18
  resolveMilestonePath,
19
19
  resolveSliceFile,
20
+ resolveSlicePath,
20
21
  resolveTaskFile,
21
22
  relSliceFile,
22
23
  buildMilestoneFileName,
24
+ buildSliceFileName,
23
25
  } from "./paths.js";
24
26
  import { existsSync, mkdirSync, writeFileSync } from "node:fs";
25
27
  import { join } from "node:path";
@@ -369,6 +371,30 @@ const DISPATCH_RULES: DispatchRule[] = [
369
371
  name: "validating-milestone → validate-milestone",
370
372
  match: async ({ state, mid, midTitle, basePath, prefs }) => {
371
373
  if (state.phase !== "validating-milestone") return null;
374
+
375
+ // Safety guard (#1368): verify all roadmap slices have SUMMARY files before
376
+ // allowing milestone validation. If any slice lacks a summary, the milestone
377
+ // is not genuinely complete — something skipped earlier slices.
378
+ const roadmapFile = resolveMilestoneFile(basePath, mid, "ROADMAP");
379
+ const roadmapContent = roadmapFile ? await loadFile(roadmapFile) : null;
380
+ if (roadmapContent) {
381
+ const roadmap = parseRoadmap(roadmapContent);
382
+ const missingSlices: string[] = [];
383
+ for (const slice of roadmap.slices) {
384
+ const summaryPath = resolveSliceFile(basePath, mid, slice.id, "SUMMARY");
385
+ if (!summaryPath || !existsSync(summaryPath)) {
386
+ missingSlices.push(slice.id);
387
+ }
388
+ }
389
+ if (missingSlices.length > 0) {
390
+ return {
391
+ action: "stop",
392
+ reason: `Cannot validate milestone ${mid}: slices ${missingSlices.join(", ")} are missing SUMMARY files. These slices may have been skipped.`,
393
+ level: "error",
394
+ };
395
+ }
396
+ }
397
+
372
398
  // Skip preference: write a minimal pass-through VALIDATION file
373
399
  if (prefs?.phases?.skip_milestone_validation) {
374
400
  const mDir = resolveMilestonePath(basePath, mid);
@@ -404,6 +430,28 @@ const DISPATCH_RULES: DispatchRule[] = [
404
430
  name: "completing-milestone → complete-milestone",
405
431
  match: async ({ state, mid, midTitle, basePath }) => {
406
432
  if (state.phase !== "completing-milestone") return null;
433
+
434
+ // Safety guard (#1368): verify all roadmap slices have SUMMARY files.
435
+ const roadmapFile = resolveMilestoneFile(basePath, mid, "ROADMAP");
436
+ const roadmapContent = roadmapFile ? await loadFile(roadmapFile) : null;
437
+ if (roadmapContent) {
438
+ const roadmap = parseRoadmap(roadmapContent);
439
+ const missingSlices: string[] = [];
440
+ for (const slice of roadmap.slices) {
441
+ const summaryPath = resolveSliceFile(basePath, mid, slice.id, "SUMMARY");
442
+ if (!summaryPath || !existsSync(summaryPath)) {
443
+ missingSlices.push(slice.id);
444
+ }
445
+ }
446
+ if (missingSlices.length > 0) {
447
+ return {
448
+ action: "stop",
449
+ reason: `Cannot complete milestone ${mid}: slices ${missingSlices.join(", ")} are missing SUMMARY files. Run /gsd doctor to diagnose.`,
450
+ level: "error",
451
+ };
452
+ }
453
+ }
454
+
407
455
  return {
408
456
  action: "dispatch",
409
457
  unitType: "complete-milestone",
@@ -221,6 +221,15 @@ export async function runUnit(
221
221
  s.pendingResolve = resolve;
222
222
  });
223
223
 
224
+ // Ensure cwd matches basePath before dispatch (#1389).
225
+ // async_bash and background jobs can drift cwd away from the worktree.
226
+ // Realigning here prevents commits from landing on the wrong branch.
227
+ try {
228
+ if (process.cwd() !== s.basePath) {
229
+ process.chdir(s.basePath);
230
+ }
231
+ } catch { /* non-fatal — chdir may fail if dir was removed */ }
232
+
224
233
  // ── Send the prompt ──
225
234
  debugLog("runUnit", { phase: "send-message", unitType, unitId });
226
235
 
@@ -344,6 +353,7 @@ export interface LoopDeps {
344
353
  getManifestStatus: (
345
354
  basePath: string,
346
355
  mid: string | undefined,
356
+ projectRoot?: string,
347
357
  ) => Promise<{ pending: unknown[] } | null>;
348
358
  collectSecretsFromManifest: (
349
359
  basePath: string,
@@ -446,6 +456,7 @@ export interface LoopDeps {
446
456
  prefs: GSDPreferences | undefined,
447
457
  verbose: boolean,
448
458
  startModel: { provider: string; id: string } | null,
459
+ retryContext?: { isRetry: boolean; previousTier?: string },
449
460
  ) => Promise<{ routing: { tier: string; modelDowngraded: boolean } | null }>;
450
461
  startUnitSupervision: (sctx: {
451
462
  s: AutoSession;
@@ -983,7 +994,7 @@ export async function autoLoop(
983
994
 
984
995
  // Secrets re-check gate
985
996
  try {
986
- const manifestStatus = await deps.getManifestStatus(s.basePath, mid);
997
+ const manifestStatus = await deps.getManifestStatus(s.basePath, mid, s.originalBasePath);
987
998
  if (manifestStatus && manifestStatus.pending.length > 0) {
988
999
  const result = await deps.collectSecretsFromManifest(
989
1000
  s.basePath,
@@ -1172,6 +1183,14 @@ export async function autoLoop(
1172
1183
  unitId,
1173
1184
  });
1174
1185
 
1186
+ // Detect retry and capture previous tier for escalation
1187
+ const isRetry = !!(
1188
+ s.currentUnit &&
1189
+ s.currentUnit.type === unitType &&
1190
+ s.currentUnit.id === unitId
1191
+ );
1192
+ const previousTier = s.currentUnitRouting?.tier;
1193
+
1175
1194
  // Closeout previous unit
1176
1195
  if (s.currentUnit) {
1177
1196
  await deps.closeoutUnit(
@@ -1325,7 +1344,7 @@ export async function autoLoop(
1325
1344
  );
1326
1345
  }
1327
1346
 
1328
- // Select and apply model
1347
+ // Select and apply model (with tier escalation on retry)
1329
1348
  const modelResult = await deps.selectAndApplyModel(
1330
1349
  ctx,
1331
1350
  pi,
@@ -1335,6 +1354,7 @@ export async function autoLoop(
1335
1354
  prefs,
1336
1355
  s.verbose,
1337
1356
  s.autoModeStartModel,
1357
+ { isRetry, previousTier },
1338
1358
  );
1339
1359
  s.currentUnitRouting =
1340
1360
  modelResult.routing as AutoSession["currentUnitRouting"];
@@ -7,8 +7,9 @@
7
7
  import type { ExtensionAPI, ExtensionContext } from "@gsd/pi-coding-agent";
8
8
  import type { GSDPreferences } from "./preferences.js";
9
9
  import { resolveModelWithFallbacksForUnit, resolveDynamicRoutingConfig } from "./preferences.js";
10
+ import type { ComplexityTier } from "./complexity-classifier.js";
10
11
  import { classifyUnitComplexity, tierLabel } from "./complexity-classifier.js";
11
- import { resolveModelForComplexity } from "./model-router.js";
12
+ import { resolveModelForComplexity, escalateTier } from "./model-router.js";
12
13
  import { getLedger, getProjectTotals } from "./metrics.js";
13
14
  import { unitPhaseLabel } from "./auto-dashboard.js";
14
15
 
@@ -33,6 +34,7 @@ export async function selectAndApplyModel(
33
34
  prefs: GSDPreferences | undefined,
34
35
  verbose: boolean,
35
36
  autoModeStartModel: { provider: string; id: string } | null,
37
+ retryContext?: { isRetry: boolean; previousTier?: string },
36
38
  ): Promise<ModelSelectionResult> {
37
39
  const modelConfig = resolveModelWithFallbacksForUnit(unitType);
38
40
  let routing: { tier: string; modelDowngraded: boolean } | null = null;
@@ -60,8 +62,27 @@ export async function selectAndApplyModel(
60
62
  const shouldClassify = !isHook || routingConfig.hooks !== false;
61
63
 
62
64
  if (shouldClassify) {
63
- const classification = classifyUnitComplexity(unitType, unitId, basePath, budgetPct);
65
+ let classification = classifyUnitComplexity(unitType, unitId, basePath, budgetPct);
64
66
  const availableModelIds = availableModels.map(m => m.id);
67
+
68
+ // Escalate tier on retry when escalate_on_failure is enabled (default: true)
69
+ if (
70
+ retryContext?.isRetry &&
71
+ retryContext.previousTier &&
72
+ routingConfig.escalate_on_failure !== false
73
+ ) {
74
+ const escalated = escalateTier(retryContext.previousTier as ComplexityTier);
75
+ if (escalated) {
76
+ classification = { ...classification, tier: escalated, reason: "escalated after failure" };
77
+ if (verbose) {
78
+ ctx.ui.notify(
79
+ `Tier escalation: ${retryContext.previousTier} → ${escalated} (retry after failure)`,
80
+ "info",
81
+ );
82
+ }
83
+ }
84
+ }
85
+
65
86
  const routingResult = resolveModelForComplexity(classification, modelConfig, routingConfig, availableModelIds);
66
87
 
67
88
  if (routingResult.wasDowngraded) {
@@ -8,6 +8,8 @@
8
8
  */
9
9
 
10
10
  import type { ExtensionContext } from "@gsd/pi-coding-agent";
11
+ import { parseUnitId } from "./unit-id.js";
12
+ import { atomicWriteSync } from "./atomic-write.js";
11
13
  import { clearUnitRuntimeRecord } from "./unit-runtime.js";
12
14
  import { clearParseCache, parseRoadmap, parsePlan } from "./files.js";
13
15
  import { isValidationTerminal } from "./state.js";
@@ -35,6 +37,7 @@ import {
35
37
  clearPathCache,
36
38
  resolveGsdRootFile,
37
39
  } from "./paths.js";
40
+ import { markSliceDoneInRoadmap } from "./roadmap-mutations.js";
38
41
  import {
39
42
  existsSync,
40
43
  mkdirSync,
@@ -499,6 +502,42 @@ export async function selfHealRuntimeRecords(
499
502
  for (const record of records) {
500
503
  const { unitType, unitId } = record;
501
504
 
505
+ // Case 0: complete-slice with SUMMARY + UAT but unchecked roadmap (#1350).
506
+ // If a complete-slice was interrupted after writing artifacts but before
507
+ // flipping the roadmap checkbox, the verification fails and the dispatch
508
+ // loop relaunches the same unit forever. Auto-fix the checkbox.
509
+ if (unitType === "complete-slice") {
510
+ const { milestone: mid, slice: sid } = parseUnitId(unitId);
511
+ if (mid && sid) {
512
+ const dir = resolveSlicePath(base, mid, sid);
513
+ if (dir) {
514
+ const summaryPath = join(dir, buildSliceFileName(sid, "SUMMARY"));
515
+ const uatPath = join(dir, buildSliceFileName(sid, "UAT"));
516
+ if (existsSync(summaryPath) && existsSync(uatPath)) {
517
+ const roadmapFile = resolveMilestoneFile(base, mid, "ROADMAP");
518
+ if (roadmapFile && existsSync(roadmapFile)) {
519
+ try {
520
+ const roadmapContent = readFileSync(roadmapFile, "utf-8");
521
+ const roadmap = parseRoadmap(roadmapContent);
522
+ const slice = (roadmap.slices ?? []).find(s => s.id === sid);
523
+ if (slice && !slice.done) {
524
+ // Auto-fix: flip the checkbox using shared utility
525
+ if (markSliceDoneInRoadmap(base, mid, sid)) {
526
+ ctx.ui.notify(
527
+ `Self-heal: marked ${sid} done in roadmap (SUMMARY + UAT exist but checkbox was stale).`,
528
+ "info",
529
+ );
530
+ }
531
+ }
532
+ } catch {
533
+ // Roadmap parse failure — don't block self-heal
534
+ }
535
+ }
536
+ }
537
+ }
538
+ }
539
+ }
540
+
502
541
  // Clear stale dispatched records (dispatched > 1h ago, process crashed)
503
542
  const age = now - (record.startedAt ?? 0);
504
543
  if (record.phase === "dispatched" && age > STALE_THRESHOLD_MS) {
@@ -20,6 +20,8 @@ import {
20
20
  resolveSkillDiscoveryMode,
21
21
  getIsolationMode,
22
22
  } from "./preferences.js";
23
+ import { ensureGsdSymlink } from "./repo-identity.js";
24
+ import { migrateToExternalState, recoverFailedMigration } from "./migrate-external.js";
23
25
  import { collectSecretsFromManifest } from "../get-secrets-from-user.js";
24
26
  import { gsdRoot, resolveMilestoneFile, milestonesDir } from "./paths.js";
25
27
  import { invalidateAllCaches } from "./cache.js";
@@ -92,6 +94,13 @@ export interface BootstrapDeps {
92
94
  * Returns false if the bootstrap aborted (e.g., guided flow returned,
93
95
  * concurrent session detected). Returns true when ready to dispatch.
94
96
  */
97
+
98
+ /** Guard: tracks consecutive bootstrap attempts that found phase === "complete".
99
+ * Prevents the recursive dialog loop described in #1348 where
100
+ * bootstrapAutoSession → showSmartEntry → checkAutoStartAfterDiscuss → startAuto
101
+ * cycles indefinitely when the discuss workflow doesn't produce a milestone. */
102
+ let _consecutiveCompleteBootstraps = 0;
103
+ const MAX_CONSECUTIVE_COMPLETE_BOOTSTRAPS = 2;
95
104
  export async function bootstrapAutoSession(
96
105
  s: AutoSession,
97
106
  ctx: ExtensionCommandContext,
@@ -128,7 +137,20 @@ export async function bootstrapAutoSession(
128
137
  nativeInit(base, mainBranch);
129
138
  }
130
139
 
131
- // Ensure .gitignore has baseline patterns
140
+ // Migrate legacy in-project .gsd/ to external state directory.
141
+ // Migration MUST run before ensureGitignore to avoid adding ".gsd" to
142
+ // .gitignore when .gsd/ is git-tracked (data-loss bug #1364).
143
+ recoverFailedMigration(base);
144
+ const migration = migrateToExternalState(base);
145
+ if (migration.error) {
146
+ ctx.ui.notify(`External state migration warning: ${migration.error}`, "warning");
147
+ }
148
+ // Ensure symlink exists (handles fresh projects and post-migration)
149
+ ensureGsdSymlink(base);
150
+
151
+ // Ensure .gitignore has baseline patterns.
152
+ // ensureGitignore checks for git-tracked .gsd/ files and skips the
153
+ // ".gsd" pattern if the project intentionally tracks .gsd/ in git.
132
154
  const gitPrefs = loadEffectiveGSDPreferences()?.preferences?.git;
133
155
  const commitDocs = gitPrefs?.commit_docs;
134
156
  const manageGitignore = gitPrefs?.manage_gitignore;
@@ -286,6 +308,20 @@ export async function bootstrapAutoSession(
286
308
  if (!hasSurvivorBranch) {
287
309
  // No active work — start a new milestone via discuss flow
288
310
  if (!state.activeMilestone || state.phase === "complete") {
311
+ // Guard against recursive dialog loop (#1348):
312
+ // If we've entered this branch multiple times in quick succession,
313
+ // the discuss workflow isn't producing a milestone. Break the cycle.
314
+ _consecutiveCompleteBootstraps++;
315
+ if (_consecutiveCompleteBootstraps > MAX_CONSECUTIVE_COMPLETE_BOOTSTRAPS) {
316
+ _consecutiveCompleteBootstraps = 0;
317
+ ctx.ui.notify(
318
+ "All milestones are complete and the discussion didn't produce a new one. " +
319
+ "Run /gsd to start a new milestone manually.",
320
+ "warning",
321
+ );
322
+ return releaseLockAndReturn();
323
+ }
324
+
289
325
  const { showSmartEntry } = await import("./guided-flow.js");
290
326
  await showSmartEntry(ctx, pi, base, { step: requestedStepMode });
291
327
 
@@ -296,6 +332,7 @@ export async function bootstrapAutoSession(
296
332
  postState.phase !== "complete" &&
297
333
  postState.phase !== "pre-planning"
298
334
  ) {
335
+ _consecutiveCompleteBootstraps = 0; // Successfully advanced past "complete"
299
336
  state = postState;
300
337
  } else if (
301
338
  postState.activeMilestone &&
@@ -352,6 +389,9 @@ export async function bootstrapAutoSession(
352
389
  return releaseLockAndReturn();
353
390
  }
354
391
 
392
+ // Successfully resolved an active milestone — reset the re-entry guard
393
+ _consecutiveCompleteBootstraps = 0;
394
+
355
395
  // ── Initialize session state ──
356
396
  s.active = true;
357
397
  s.stepMode = requestedStepMode;
@@ -484,7 +524,7 @@ export async function bootstrapAutoSession(
484
524
  // Secrets collection gate
485
525
  const mid = state.activeMilestone!.id;
486
526
  try {
487
- const manifestStatus = await getManifestStatus(base, mid);
527
+ const manifestStatus = await getManifestStatus(base, mid, s.originalBasePath || base);
488
528
  if (manifestStatus && manifestStatus.pending.length > 0) {
489
529
  const result = await collectSecretsFromManifest(base, mid, ctx);
490
530
  if (
@@ -127,7 +127,7 @@ import {
127
127
  formatTokenCount,
128
128
  } from "./metrics.js";
129
129
  import { join } from "node:path";
130
- import { readFileSync, existsSync, mkdirSync } from "node:fs";
130
+ import { readFileSync, existsSync, mkdirSync, writeFileSync, unlinkSync } from "node:fs";
131
131
  import { atomicWriteSync } from "./atomic-write.js";
132
132
  import {
133
133
  autoCommitCurrentBranch,
@@ -554,6 +554,13 @@ export async function stopAuto(
554
554
  resetRoutingHistory();
555
555
  resetHookState();
556
556
  if (s.basePath) clearPersistedHookState(s.basePath);
557
+
558
+ // Remove paused-session metadata if present (#1383)
559
+ try {
560
+ const pausedPath = join(gsdRoot(s.originalBasePath || s.basePath), "runtime", "paused-session.json");
561
+ if (existsSync(pausedPath)) unlinkSync(pausedPath);
562
+ } catch { /* non-fatal */ }
563
+
557
564
  s.active = false;
558
565
  s.paused = false;
559
566
  s.stepMode = false;
@@ -607,8 +614,32 @@ export async function pauseAuto(
607
614
 
608
615
  s.pausedSessionFile = ctx?.sessionManager?.getSessionFile() ?? null;
609
616
 
610
- if (lockBase()) clearLock(lockBase());
611
- if (lockBase()) releaseSessionLock(lockBase());
617
+ // Persist paused-session metadata so resume survives /exit (#1383).
618
+ // The fresh-start bootstrap checks for this file and restores worktree context.
619
+ try {
620
+ const pausedMeta = {
621
+ milestoneId: s.currentMilestoneId,
622
+ worktreePath: isInAutoWorktree(s.basePath) ? s.basePath : null,
623
+ originalBasePath: s.originalBasePath,
624
+ stepMode: s.stepMode,
625
+ pausedAt: new Date().toISOString(),
626
+ sessionFile: s.pausedSessionFile,
627
+ };
628
+ const runtimeDir = join(gsdRoot(s.originalBasePath || s.basePath), "runtime");
629
+ mkdirSync(runtimeDir, { recursive: true });
630
+ writeFileSync(
631
+ join(runtimeDir, "paused-session.json"),
632
+ JSON.stringify(pausedMeta, null, 2),
633
+ "utf-8",
634
+ );
635
+ } catch {
636
+ // Non-fatal — resume will still work via full bootstrap, just without worktree context
637
+ }
638
+
639
+ if (lockBase()) {
640
+ releaseSessionLock(lockBase());
641
+ clearLock(lockBase());
642
+ }
612
643
 
613
644
  deregisterSigtermHandler();
614
645
 
@@ -792,6 +823,30 @@ export async function startAuto(
792
823
  base = escapeStaleWorktree(base);
793
824
 
794
825
  // If resuming from paused state, just re-activate and dispatch next unit.
826
+ // Check persisted paused-session first (#1383) — survives /exit.
827
+ if (!s.paused) {
828
+ try {
829
+ const pausedPath = join(gsdRoot(base), "runtime", "paused-session.json");
830
+ if (existsSync(pausedPath)) {
831
+ const meta = JSON.parse(readFileSync(pausedPath, "utf-8"));
832
+ if (meta.milestoneId) {
833
+ s.currentMilestoneId = meta.milestoneId;
834
+ s.originalBasePath = meta.originalBasePath || base;
835
+ s.stepMode = meta.stepMode ?? requestedStepMode;
836
+ s.paused = true;
837
+ // Clean up the persisted file — we're consuming it
838
+ try { unlinkSync(pausedPath); } catch { /* non-fatal */ }
839
+ ctx.ui.notify(
840
+ `Resuming paused session for ${meta.milestoneId}${meta.worktreePath ? ` (worktree)` : ""}.`,
841
+ "info",
842
+ );
843
+ }
844
+ }
845
+ } catch {
846
+ // Malformed or missing — proceed with fresh bootstrap
847
+ }
848
+ }
849
+
795
850
  if (s.paused) {
796
851
  const resumeLock = acquireSessionLock(base);
797
852
  if (!resumeLock.acquired) {
@@ -1145,6 +1200,9 @@ export async function dispatchHookUnit(
1145
1200
  ctx.ui.setStatus("gsd-auto", s.stepMode ? "next" : "auto");
1146
1201
  ctx.ui.notify(`Running post-unit hook: ${hookName}`, "info");
1147
1202
 
1203
+ // Ensure cwd matches basePath before hook dispatch (#1389)
1204
+ try { if (process.cwd() !== s.basePath) process.chdir(s.basePath); } catch {}
1205
+
1148
1206
  debugLog("dispatchHookUnit", {
1149
1207
  phase: "send-message",
1150
1208
  promptLength: hookPrompt.length,
@@ -24,7 +24,7 @@ import { projectRoot } from "./commands.js";
24
24
  import { loadPrompt } from "./prompt-loader.js";
25
25
 
26
26
  export function dispatchDoctorHeal(pi: ExtensionAPI, scope: string | undefined, reportText: string, structuredIssues: string): void {
27
- const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".pi", "GSD-WORKFLOW.md");
27
+ const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".gsd", "agent", "GSD-WORKFLOW.md");
28
28
  const workflow = readFileSync(workflowPath, "utf-8");
29
29
  const prompt = loadPrompt("doctor-heal", {
30
30
  doctorSummary: reportText,
@@ -187,7 +187,7 @@ export async function handleTriage(ctx: ExtensionCommandContext, pi: ExtensionAP
187
187
  roadmapContext: roadmapContext || "(no active roadmap)",
188
188
  });
189
189
 
190
- const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".pi", "GSD-WORKFLOW.md");
190
+ const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".gsd", "agent", "GSD-WORKFLOW.md");
191
191
  const workflow = readFileSync(workflowPath, "utf-8");
192
192
 
193
193
  pi.sendMessage(
@@ -5,6 +5,9 @@
5
5
  */
6
6
 
7
7
  import type { ExtensionCommandContext } from "@gsd/pi-coding-agent";
8
+ import { existsSync } from "node:fs";
9
+ import { join } from "node:path";
10
+ import { gsdRoot } from "./paths.js";
8
11
  import { getErrorMessage } from "./error-utils.js";
9
12
 
10
13
  export interface InspectData {
@@ -44,11 +47,15 @@ export function formatInspectOutput(data: InspectData): string {
44
47
 
45
48
  export async function handleInspect(ctx: ExtensionCommandContext): Promise<void> {
46
49
  try {
47
- const { isDbAvailable, _getAdapter } = await import("./gsd-db.js");
50
+ const { isDbAvailable, _getAdapter, openDatabase } = await import("./gsd-db.js");
48
51
 
49
52
  if (!isDbAvailable()) {
50
- ctx.ui.notify("No GSD database available. Run /gsd auto to create one.", "info");
51
- return;
53
+ const gsdDir = gsdRoot(process.cwd());
54
+ const dbPath = join(gsdDir, "gsd.db");
55
+ if (!existsSync(gsdDir) || !existsSync(dbPath) || !openDatabase(dbPath)) {
56
+ ctx.ui.notify("No GSD database available. Run /gsd auto to create one.", "info");
57
+ return;
58
+ }
52
59
  }
53
60
 
54
61
  const adapter = _getAdapter();
@@ -0,0 +1,55 @@
1
+ /**
2
+ * /gsd rate — Submit feedback on the last unit's model tier assignment.
3
+ * Feeds into the adaptive routing history so future dispatches improve.
4
+ */
5
+
6
+ import type { ExtensionCommandContext } from "@gsd/pi-coding-agent";
7
+ import { loadLedgerFromDisk } from "./metrics.js";
8
+ import { recordFeedback, initRoutingHistory } from "./routing-history.js";
9
+ import type { ComplexityTier } from "./complexity-classifier.js";
10
+
11
+ const VALID_RATINGS = new Set(["over", "under", "ok"]);
12
+
13
+ export async function handleRate(
14
+ args: string,
15
+ ctx: ExtensionCommandContext,
16
+ basePath: string,
17
+ ): Promise<void> {
18
+ const rating = args.trim().toLowerCase();
19
+
20
+ if (!rating || !VALID_RATINGS.has(rating)) {
21
+ ctx.ui.notify(
22
+ "Usage: /gsd rate <over|ok|under>\n" +
23
+ " over — model was overpowered for that task (encourage cheaper)\n" +
24
+ " ok — model was appropriate\n" +
25
+ " under — model was too weak (encourage stronger)",
26
+ "info",
27
+ );
28
+ return;
29
+ }
30
+
31
+ const ledger = loadLedgerFromDisk(basePath);
32
+ if (!ledger || ledger.units.length === 0) {
33
+ ctx.ui.notify("No completed units found — nothing to rate.", "warning");
34
+ return;
35
+ }
36
+
37
+ const lastUnit = ledger.units[ledger.units.length - 1];
38
+ const tier = lastUnit.tier as ComplexityTier | undefined;
39
+
40
+ if (!tier) {
41
+ ctx.ui.notify(
42
+ "Last unit has no tier data (dynamic routing was not active). Rating skipped.",
43
+ "warning",
44
+ );
45
+ return;
46
+ }
47
+
48
+ initRoutingHistory(basePath);
49
+ recordFeedback(lastUnit.type, lastUnit.id, tier, rating as "over" | "under" | "ok");
50
+
51
+ ctx.ui.notify(
52
+ `Recorded "${rating}" for ${lastUnit.type}/${lastUnit.id} at tier ${tier}.`,
53
+ "info",
54
+ );
55
+ }