gsd-pi 2.82.0-dev.dfbc5f58f → 2.82.0-dev.e7a7f1ed5

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 (182) hide show
  1. package/README.md +1 -1
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +1 -1
  4. package/dist/resources/extensions/gsd/auto/phases.js +73 -30
  5. package/dist/resources/extensions/gsd/auto-dashboard.js +66 -1
  6. package/dist/resources/extensions/gsd/auto-direct-dispatch.js +1 -0
  7. package/dist/resources/extensions/gsd/auto-dispatch.js +10 -16
  8. package/dist/resources/extensions/gsd/auto-recovery.js +40 -13
  9. package/dist/resources/extensions/gsd/auto-start.js +3 -3
  10. package/dist/resources/extensions/gsd/auto-verification.js +17 -4
  11. package/dist/resources/extensions/gsd/auto-worktree.js +65 -9
  12. package/dist/resources/extensions/gsd/auto.js +7 -2
  13. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +27 -6
  14. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +4 -2
  15. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +7 -2
  16. package/dist/resources/extensions/gsd/crash-recovery.js +16 -4
  17. package/dist/resources/extensions/gsd/db/milestone-leases.js +24 -0
  18. package/dist/resources/extensions/gsd/doctor-git-checks.js +46 -1
  19. package/dist/resources/extensions/gsd/git-service.js +6 -2
  20. package/dist/resources/extensions/gsd/gsd-db.js +20 -6
  21. package/dist/resources/extensions/gsd/guided-flow-queue.js +4 -3
  22. package/dist/resources/extensions/gsd/guided-flow.js +95 -116
  23. package/dist/resources/extensions/gsd/guided-unit-context.js +23 -0
  24. package/dist/resources/extensions/gsd/migration-auto-check.js +12 -17
  25. package/dist/resources/extensions/gsd/pending-auto-start.js +52 -0
  26. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
  27. package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  28. package/dist/resources/extensions/gsd/prompts/discuss-headless.md +8 -8
  29. package/dist/resources/extensions/gsd/prompts/discuss.md +9 -9
  30. package/dist/resources/extensions/gsd/prompts/guided-discuss-project.md +4 -4
  31. package/dist/resources/extensions/gsd/prompts/guided-discuss-requirements.md +3 -3
  32. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  33. package/dist/resources/extensions/gsd/prompts/queue.md +4 -4
  34. package/dist/resources/extensions/gsd/prompts/refine-slice.md +1 -1
  35. package/dist/resources/extensions/gsd/prompts/rewrite-docs.md +1 -1
  36. package/dist/resources/extensions/gsd/queue-reorder-ui.js +30 -13
  37. package/dist/resources/extensions/gsd/smart-entry-routing.js +36 -0
  38. package/dist/resources/extensions/gsd/state-reconciliation/drift/project-md.js +9 -14
  39. package/dist/resources/extensions/gsd/state-reconciliation/drift/roadmap.js +19 -24
  40. package/dist/resources/extensions/gsd/status-guards.js +7 -0
  41. package/dist/resources/extensions/gsd/workflow-mcp.js +17 -1
  42. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  43. package/dist/web/standalone/.next/BUILD_ID +1 -1
  44. package/dist/web/standalone/.next/app-path-routes-manifest.json +9 -9
  45. package/dist/web/standalone/.next/build-manifest.json +2 -2
  46. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  47. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  48. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  56. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/api/browse-directories/route.js +1 -1
  64. package/dist/web/standalone/.next/server/app/index.html +1 -1
  65. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app-paths-manifest.json +9 -9
  72. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  73. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  74. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  75. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  76. package/package.json +1 -1
  77. package/packages/pi-ai/dist/providers/google-gemini-cli.d.ts.map +1 -1
  78. package/packages/pi-ai/dist/providers/google-gemini-cli.js +5 -0
  79. package/packages/pi-ai/dist/providers/google-gemini-cli.js.map +1 -1
  80. package/packages/pi-ai/dist/providers/google-gemini-cli.test.d.ts +2 -0
  81. package/packages/pi-ai/dist/providers/google-gemini-cli.test.d.ts.map +1 -0
  82. package/packages/pi-ai/dist/providers/google-gemini-cli.test.js +41 -0
  83. package/packages/pi-ai/dist/providers/google-gemini-cli.test.js.map +1 -0
  84. package/packages/pi-ai/src/providers/google-gemini-cli.test.ts +49 -0
  85. package/packages/pi-ai/src/providers/google-gemini-cli.ts +7 -0
  86. package/packages/pi-ai/tsconfig.tsbuildinfo +1 -1
  87. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.d.ts.map +1 -1
  88. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js +24 -6
  89. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js.map +1 -1
  90. package/packages/pi-coding-agent/src/modes/interactive/components/footer.ts +23 -7
  91. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  92. package/packages/pi-tui/dist/__tests__/terminal.test.d.ts +2 -0
  93. package/packages/pi-tui/dist/__tests__/terminal.test.d.ts.map +1 -0
  94. package/packages/pi-tui/dist/__tests__/terminal.test.js +103 -0
  95. package/packages/pi-tui/dist/__tests__/terminal.test.js.map +1 -0
  96. package/packages/pi-tui/dist/terminal.d.ts +2 -0
  97. package/packages/pi-tui/dist/terminal.d.ts.map +1 -1
  98. package/packages/pi-tui/dist/terminal.js +12 -0
  99. package/packages/pi-tui/dist/terminal.js.map +1 -1
  100. package/packages/pi-tui/src/__tests__/terminal.test.ts +121 -0
  101. package/packages/pi-tui/src/terminal.ts +11 -0
  102. package/packages/pi-tui/tsconfig.tsbuildinfo +1 -1
  103. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +1 -1
  104. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +9 -0
  105. package/src/resources/extensions/gsd/auto/phases.ts +83 -37
  106. package/src/resources/extensions/gsd/auto-dashboard.ts +72 -1
  107. package/src/resources/extensions/gsd/auto-direct-dispatch.ts +1 -0
  108. package/src/resources/extensions/gsd/auto-dispatch.ts +10 -16
  109. package/src/resources/extensions/gsd/auto-recovery.ts +45 -11
  110. package/src/resources/extensions/gsd/auto-start.ts +2 -3
  111. package/src/resources/extensions/gsd/auto-verification.ts +22 -2
  112. package/src/resources/extensions/gsd/auto-worktree.ts +74 -9
  113. package/src/resources/extensions/gsd/auto.ts +8 -2
  114. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +36 -6
  115. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +4 -2
  116. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +8 -3
  117. package/src/resources/extensions/gsd/crash-recovery.ts +16 -2
  118. package/src/resources/extensions/gsd/db/milestone-leases.ts +26 -0
  119. package/src/resources/extensions/gsd/doctor-git-checks.ts +45 -1
  120. package/src/resources/extensions/gsd/doctor-types.ts +1 -0
  121. package/src/resources/extensions/gsd/git-service.ts +6 -3
  122. package/src/resources/extensions/gsd/gsd-db.ts +18 -6
  123. package/src/resources/extensions/gsd/guided-flow-queue.ts +4 -3
  124. package/src/resources/extensions/gsd/guided-flow.ts +128 -133
  125. package/src/resources/extensions/gsd/guided-unit-context.ts +30 -0
  126. package/src/resources/extensions/gsd/migration-auto-check.ts +15 -23
  127. package/src/resources/extensions/gsd/pending-auto-start.ts +79 -0
  128. package/src/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
  129. package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  130. package/src/resources/extensions/gsd/prompts/discuss-headless.md +8 -8
  131. package/src/resources/extensions/gsd/prompts/discuss.md +9 -9
  132. package/src/resources/extensions/gsd/prompts/guided-discuss-project.md +4 -4
  133. package/src/resources/extensions/gsd/prompts/guided-discuss-requirements.md +3 -3
  134. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  135. package/src/resources/extensions/gsd/prompts/queue.md +4 -4
  136. package/src/resources/extensions/gsd/prompts/refine-slice.md +1 -1
  137. package/src/resources/extensions/gsd/prompts/rewrite-docs.md +1 -1
  138. package/src/resources/extensions/gsd/queue-reorder-ui.ts +31 -13
  139. package/src/resources/extensions/gsd/smart-entry-routing.ts +77 -0
  140. package/src/resources/extensions/gsd/state-reconciliation/drift/project-md.ts +12 -15
  141. package/src/resources/extensions/gsd/state-reconciliation/drift/roadmap.ts +17 -25
  142. package/src/resources/extensions/gsd/status-guards.ts +8 -0
  143. package/src/resources/extensions/gsd/tests/auto-dashboard.test.ts +71 -0
  144. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +2 -0
  145. package/src/resources/extensions/gsd/tests/auto-paused-ui-cleanup.test.ts +29 -1
  146. package/src/resources/extensions/gsd/tests/auto-phases-lifecycle.test.ts +53 -2
  147. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +76 -5
  148. package/src/resources/extensions/gsd/tests/auto-stop-notification.test.ts +20 -0
  149. package/src/resources/extensions/gsd/tests/checkout-branch-stash-guard.test.ts +87 -0
  150. package/src/resources/extensions/gsd/tests/clear-stale-autostart.test.ts +11 -2
  151. package/src/resources/extensions/gsd/tests/complete-slice.test.ts +5 -9
  152. package/src/resources/extensions/gsd/tests/crash-recovery-via-db.test.ts +43 -0
  153. package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +2 -0
  154. package/src/resources/extensions/gsd/tests/db-authority-regression.test.ts +208 -0
  155. package/src/resources/extensions/gsd/tests/dispatch-complete-milestone-guard.test.ts +27 -0
  156. package/src/resources/extensions/gsd/tests/doctor-empty-worktree.test.ts +65 -0
  157. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +11 -0
  158. package/src/resources/extensions/gsd/tests/guided-discuss-project-prompt-rendering.test.ts +2 -0
  159. package/src/resources/extensions/gsd/tests/guided-dispatch-root.test.ts +106 -0
  160. package/src/resources/extensions/gsd/tests/guided-flow-session-isolation.test.ts +59 -11
  161. package/src/resources/extensions/gsd/tests/guided-tool-contract.test.ts +65 -0
  162. package/src/resources/extensions/gsd/tests/headless-milestone-parity.test.ts +7 -7
  163. package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +9 -0
  164. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +46 -0
  165. package/src/resources/extensions/gsd/tests/merge-db-cycle.test.ts +179 -0
  166. package/src/resources/extensions/gsd/tests/migration-auto-check.test.ts +26 -18
  167. package/src/resources/extensions/gsd/tests/pending-autostart-scope.test.ts +29 -5
  168. package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +2 -0
  169. package/src/resources/extensions/gsd/tests/prefs-wizard-coverage.test.ts +59 -0
  170. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +37 -1
  171. package/src/resources/extensions/gsd/tests/queue-reorder-ui.test.ts +54 -0
  172. package/src/resources/extensions/gsd/tests/remediation-completion-guard.test.ts +43 -0
  173. package/src/resources/extensions/gsd/tests/run-uat-replay-cap.test.ts +2 -3
  174. package/src/resources/extensions/gsd/tests/smart-entry-routing.test.ts +113 -0
  175. package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +22 -1
  176. package/src/resources/extensions/gsd/tests/state-reconciliation-drift.test.ts +119 -23
  177. package/src/resources/extensions/gsd/tests/status-guards.test.ts +13 -1
  178. package/src/resources/extensions/gsd/tests/validate-milestone-stuck-guard.test.ts +29 -2
  179. package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +18 -0
  180. package/src/resources/extensions/gsd/workflow-mcp.ts +18 -1
  181. /package/dist/web/standalone/.next/static/{q0WYuDVbHeFFYbdd-fei2 → 4dSwdrs__8NwCZggxP9KF}/_buildManifest.js +0 -0
  182. /package/dist/web/standalone/.next/static/{q0WYuDVbHeFFYbdd-fei2 → 4dSwdrs__8NwCZggxP9KF}/_ssgManifest.js +0 -0
@@ -45,7 +45,7 @@ describe("headless milestone bootstrap — parity with interactive flow", () =>
45
45
 
46
46
  // Match only the actual dispatchWorkflow call — comments in the body
47
47
  // may mention "plan-milestone" as part of the fix rationale.
48
- const dispatchMatches = [...fnBody.matchAll(/dispatchWorkflow\([^)]*,\s*"([^"]+)"\s*\)/g)];
48
+ const dispatchMatches = [...fnBody.matchAll(/dispatchWorkflow\([\s\S]*?,\s*"([^"]+)"\s*,\s*\{\s*basePath\s*\}\s*\)/g)];
49
49
  assert.strictEqual(
50
50
  dispatchMatches.length,
51
51
  1,
@@ -65,15 +65,15 @@ describe("headless milestone bootstrap — parity with interactive flow", () =>
65
65
  /### Ready-phrase pre-condition \(NON-BYPASSABLE\)/.test(section),
66
66
  "single-milestone ready-phrase section must be present",
67
67
  );
68
- // All four required artifacts must appear as checkboxes, not a prose list.
68
+ // All four required outcomes must appear as checkboxes, not a prose list.
69
69
  for (const artifact of [
70
- "`.gsd/PROJECT.md`",
71
- "`.gsd/REQUIREMENTS.md`",
70
+ "PROJECT artifact",
71
+ "REQUIREMENTS artifact",
72
72
  "`{{contextPath}}`",
73
73
  "`gsd_plan_milestone`",
74
74
  ]) {
75
75
  assert.ok(
76
- new RegExp(`- \\[ \\] [A-Za-z]+ ${artifact.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`).test(section),
76
+ new RegExp(`- \\[ \\] [^\\n]*${artifact.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`).test(section),
77
77
  `single-milestone pre-condition must include a checkbox for ${artifact}`,
78
78
  );
79
79
  }
@@ -103,8 +103,8 @@ describe("headless milestone bootstrap — parity with interactive flow", () =>
103
103
  "multi-milestone ready-phrase section must be present",
104
104
  );
105
105
  for (const artifact of [
106
- "`.gsd/PROJECT.md`",
107
- "`.gsd/REQUIREMENTS.md`",
106
+ "PROJECT artifact",
107
+ "REQUIREMENTS artifact",
108
108
  "`gsd_plan_milestone`",
109
109
  "`.gsd/DISCUSSION-MANIFEST.json`",
110
110
  ]) {
@@ -1168,6 +1168,15 @@ describe('git-service', async () => {
1168
1168
  rmSync(repo, { recursive: true, force: true });
1169
1169
  });
1170
1170
 
1171
+ test('Integration branch: rejects milestone branches', () => {
1172
+ const repo = initBranchTestRepo();
1173
+
1174
+ writeIntegrationBranch(repo, "M001", "milestone/M001");
1175
+ assert.deepStrictEqual(readIntegrationBranch(repo, "M001"), null, "milestone branches are not recorded as integration branch");
1176
+
1177
+ rmSync(repo, { recursive: true, force: true });
1178
+ });
1179
+
1171
1180
  // ─── writeIntegrationBranch: still records legitimate branches ────────
1172
1181
 
1173
1182
  test('Integration branch: records non-ephemeral gsd branches', () => {
@@ -22,6 +22,7 @@ import type { IterationContext, LoopState, PreDispatchData, IterationData } from
22
22
  import type { SessionLockStatus } from "../session-lock.js";
23
23
  import { runDispatch, runUnitPhase, runPreDispatch, runFinalize } from "../auto/phases.js";
24
24
  import { readUnitRuntimeRecord } from "../unit-runtime.js";
25
+ import { ModelPolicyDispatchBlockedError } from "../auto-model-selection.js";
25
26
  import {
26
27
  closeDatabase,
27
28
  insertMilestone,
@@ -160,6 +161,8 @@ function makeIC(
160
161
  pi: {
161
162
  sendMessage: () => {},
162
163
  setModel: async () => true,
164
+ getThinkingLevel: () => "off",
165
+ setThinkingLevel: () => {},
163
166
  } as any,
164
167
  s: makeSession(),
165
168
  deps,
@@ -868,6 +871,49 @@ test("runUnitPhase increments unitDispatchCount for repeated artifact-missing re
868
871
  assert.equal(ic.s.unitDispatchCount.get("execute-task/M001/S01/T01"), 2);
869
872
  });
870
873
 
874
+ test("runUnitPhase pre-dispatch model validation failures do not emit unit-start or dispatch runtime state", async (t) => {
875
+ const capture = createEventCapture();
876
+ const base = mkdtempSync(join(tmpdir(), `gsd-pre-dispatch-block-${randomUUID()}`));
877
+ t.after(() => rmSync(base, { recursive: true, force: true }));
878
+
879
+ const deps = makeMockDeps(capture, {
880
+ selectAndApplyModel: async () => {
881
+ throw new ModelPolicyDispatchBlockedError("execute-task", "M001/S01/T01", []);
882
+ },
883
+ });
884
+ const ic = makeIC(deps, {
885
+ s: {
886
+ ...makeSession(),
887
+ basePath: base,
888
+ } as any,
889
+ });
890
+ const iterData: IterationData = {
891
+ unitType: "execute-task",
892
+ unitId: "M001/S01/T01",
893
+ prompt: "do stuff",
894
+ finalPrompt: "do stuff",
895
+ pauseAfterUatDispatch: false,
896
+ state: { phase: "executing", activeMilestone: { id: "M001" }, activeSlice: { id: "S01" }, registry: [], blockers: [] } as any,
897
+ mid: "M001",
898
+ midTitle: "Test",
899
+ isRetry: false,
900
+ previousTier: undefined,
901
+ };
902
+ const loopState: LoopState = { recentUnits: [{ key: "execute-task/M001/S01/T01" }], stuckRecoveryAttempts: 0, consecutiveFinalizeTimeouts: 0 };
903
+
904
+ await assert.rejects(() => runUnitPhase(ic, iterData, loopState), ModelPolicyDispatchBlockedError);
905
+ await assert.rejects(() => runUnitPhase(ic, iterData, loopState), ModelPolicyDispatchBlockedError);
906
+
907
+ const startEvents = capture.events.filter(e => e.eventType === "unit-start");
908
+ assert.equal(startEvents.length, 0, "pre-dispatch validation failures must not emit unit-start");
909
+ assert.equal(ic.s.unitDispatchCount.get("execute-task/M001/S01/T01") ?? 0, 0, "dispatch count must not increment on pre-dispatch validation failure");
910
+ assert.equal(
911
+ readUnitRuntimeRecord(base, "execute-task", "M001/S01/T01"),
912
+ null,
913
+ "pre-dispatch validation failures must not persist a dispatched runtime record",
914
+ );
915
+ });
916
+
871
917
  test("all events from a mock iteration have monotonically increasing seq and same flowId", async () => {
872
918
  const capture = createEventCapture();
873
919
  const { resolveAgentEnd, _resetPendingResolve } = await import("../auto/resolve.js");
@@ -0,0 +1,179 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import {
4
+ chmodSync,
5
+ existsSync,
6
+ mkdtempSync,
7
+ mkdirSync,
8
+ readFileSync,
9
+ realpathSync,
10
+ rmSync,
11
+ writeFileSync,
12
+ } from "node:fs";
13
+ import { tmpdir } from "node:os";
14
+ import { delimiter, join } from "node:path";
15
+ import { execFileSync } from "node:child_process";
16
+
17
+ import { mergeMilestoneToMain } from "../auto-worktree.ts";
18
+ import { closeDatabase, openDatabase } from "../gsd-db.ts";
19
+ import { GIT_NO_PROMPT_ENV } from "../git-constants.js";
20
+ import { _clearGsdRootCache } from "../paths.ts";
21
+ import { _resetServiceCache } from "../worktree.ts";
22
+ import { worktreePath } from "../worktree-manager.ts";
23
+
24
+ function git(args: string[], cwd: string): string {
25
+ return execFileSync("git", args, {
26
+ cwd,
27
+ stdio: ["ignore", "pipe", "pipe"],
28
+ encoding: "utf-8",
29
+ }).trim();
30
+ }
31
+
32
+ function withPlatform<T>(platform: NodeJS.Platform, fn: () => T): T {
33
+ const descriptor = Object.getOwnPropertyDescriptor(process, "platform");
34
+ Object.defineProperty(process, "platform", { value: platform });
35
+ try {
36
+ return fn();
37
+ } finally {
38
+ if (descriptor) {
39
+ Object.defineProperty(process, "platform", descriptor);
40
+ }
41
+ }
42
+ }
43
+
44
+ function realGitPath(): string {
45
+ const gitExecPath = execFileSync("git", ["--exec-path"], {
46
+ encoding: "utf-8",
47
+ }).trim();
48
+ return join(gitExecPath, process.platform === "win32" ? "git.exe" : "git");
49
+ }
50
+
51
+ function installGitShim(bin: string, probePath: string): void {
52
+ const shim = join(bin, "git-proxy.cjs");
53
+ writeFileSync(
54
+ shim,
55
+ `
56
+ const { appendFileSync, existsSync } = require("node:fs");
57
+ const { join } = require("node:path");
58
+ const { spawnSync } = require("node:child_process");
59
+
60
+ const realGit = ${JSON.stringify(realGitPath())};
61
+ const probePath = ${JSON.stringify(probePath)};
62
+ const args = process.argv.slice(2);
63
+
64
+ if (args[0] === "merge" && args[1] === "--squash") {
65
+ const sidecars = [
66
+ join(process.cwd(), ".gsd", "gsd.db-wal"),
67
+ join(process.cwd(), ".gsd", "gsd.db-shm"),
68
+ ];
69
+ const locked = sidecars.find((path) => existsSync(path));
70
+ if (locked) {
71
+ appendFileSync(probePath, "blocked:" + locked + "\\n");
72
+ console.error("error: local changes would be overwritten by merge");
73
+ console.error("\\t" + locked);
74
+ process.exit(1);
75
+ }
76
+ appendFileSync(probePath, "clean\\n");
77
+ }
78
+
79
+ const result = spawnSync(realGit, args, { stdio: "inherit", env: process.env });
80
+ process.exit(result.status ?? 1);
81
+ `,
82
+ "utf-8",
83
+ );
84
+
85
+ if (process.platform === "win32") {
86
+ writeFileSync(join(bin, "git.cmd"), `@echo off\r\nnode "%~dp0git-proxy.cjs" %*\r\n`, "utf-8");
87
+ } else {
88
+ const executable = join(bin, "git");
89
+ writeFileSync(executable, `#!/bin/sh\nexec node "${shim}" "$@"\n`, "utf-8");
90
+ chmodSync(executable, 0o755);
91
+ }
92
+ }
93
+
94
+ function createRepo(root: string): { repo: string; worktree: string } {
95
+ const repo = join(root, "repo");
96
+ mkdirSync(repo, { recursive: true });
97
+ git(["init"], repo);
98
+ git(["config", "user.email", "test@test.com"], repo);
99
+ git(["config", "user.name", "Test"], repo);
100
+ writeFileSync(join(repo, ".gitignore"), ".gsd/\n", "utf-8");
101
+ writeFileSync(join(repo, "README.md"), "# test\n", "utf-8");
102
+ git(["add", "."], repo);
103
+ git(["commit", "-m", "init"], repo);
104
+ git(["branch", "-M", "main"], repo);
105
+
106
+ git(["checkout", "-b", "milestone/M001"], repo);
107
+ writeFileSync(join(repo, "feature.txt"), "milestone change\n", "utf-8");
108
+ mkdirSync(join(repo, ".gsd"), { recursive: true });
109
+ writeFileSync(join(repo, ".gsd", "gsd.db-shm"), "milestone placeholder\n", "utf-8");
110
+ git(["add", "feature.txt"], repo);
111
+ git(["add", "-f", ".gsd/gsd.db-shm"], repo);
112
+ git(["commit", "-m", "feat: milestone change"], repo);
113
+ git(["checkout", "main"], repo);
114
+
115
+ const wt = worktreePath(repo, "M001");
116
+ mkdirSync(join(repo, ".gsd", "worktrees"), { recursive: true });
117
+ git(["worktree", "add", wt, "milestone/M001"], repo);
118
+ return { repo, worktree: wt };
119
+ }
120
+
121
+ test("mergeMilestoneToMain keeps the Windows DB cycle closed through squash merge", () => {
122
+ const savedCwd = process.cwd();
123
+ const originalPath = process.env.PATH ?? "";
124
+ const gitEnv = GIT_NO_PROMPT_ENV as NodeJS.ProcessEnv;
125
+ const originalGitEnvPath = gitEnv.PATH;
126
+ const originalHome = process.env.HOME;
127
+ const originalGsdHome = process.env.GSD_HOME;
128
+
129
+ const root = realpathSync(mkdtempSync(join(tmpdir(), "gsd-db-cycle-")));
130
+ const fakeHome = join(root, "home");
131
+ const bin = join(root, "bin");
132
+ const probePath = join(root, "merge-probe.txt");
133
+ mkdirSync(fakeHome, { recursive: true });
134
+ mkdirSync(bin, { recursive: true });
135
+ installGitShim(bin, probePath);
136
+
137
+ try {
138
+ process.env.HOME = fakeHome;
139
+ process.env.GSD_HOME = join(fakeHome, ".gsd");
140
+ _clearGsdRootCache();
141
+ _resetServiceCache();
142
+
143
+ const { repo, worktree } = createRepo(root);
144
+ mkdirSync(join(repo, ".gsd"), { recursive: true });
145
+
146
+ withPlatform("win32", () => {
147
+ assert.equal(openDatabase(join(repo, ".gsd", "gsd.db")), true);
148
+ assert.equal(existsSync(join(repo, ".gsd", "gsd.db-shm")), true);
149
+
150
+ process.env.PATH = `${bin}${delimiter}${originalPath}`;
151
+ gitEnv.PATH = process.env.PATH;
152
+ process.chdir(worktree);
153
+
154
+ const result = mergeMilestoneToMain(repo, "M001", "# M001: Windows DB cycle\n");
155
+ assert.equal(result.codeFilesChanged, true);
156
+ });
157
+
158
+ assert.equal(git(["show", "HEAD:feature.txt"], repo), "milestone change");
159
+ assert.equal(readFileSync(probePath, "utf-8"), "clean\n");
160
+ } finally {
161
+ closeDatabase();
162
+ process.chdir(savedCwd);
163
+ process.env.PATH = originalPath;
164
+ gitEnv.PATH = originalGitEnvPath;
165
+ if (originalHome === undefined) {
166
+ delete process.env.HOME;
167
+ } else {
168
+ process.env.HOME = originalHome;
169
+ }
170
+ if (originalGsdHome === undefined) {
171
+ delete process.env.GSD_HOME;
172
+ } else {
173
+ process.env.GSD_HOME = originalGsdHome;
174
+ }
175
+ _clearGsdRootCache();
176
+ _resetServiceCache();
177
+ if (existsSync(root)) rmSync(root, { recursive: true, force: true });
178
+ }
179
+ });
@@ -6,13 +6,15 @@ import test from "node:test";
6
6
 
7
7
  import { ensureDbOpen } from "../bootstrap/dynamic-tools.ts";
8
8
  import {
9
- _getAdapter,
10
9
  closeDatabase,
11
10
  getAllMilestones,
11
+ insertMilestone,
12
+ insertSlice,
13
+ insertTask,
12
14
  getSliceTasks,
13
15
  } from "../gsd-db.ts";
14
16
  import {
15
- autoImportMarkdownHierarchyIfDbMismatch,
17
+ checkMarkdownHierarchyAgainstDb,
16
18
  countMarkdownHierarchy,
17
19
  } from "../migration-auto-check.ts";
18
20
  import { writeGSDDirectory } from "../migrate/writer.ts";
@@ -70,7 +72,7 @@ function projectFixture(): GSDProject {
70
72
  };
71
73
  }
72
74
 
73
- test("migration auto-check imports markdown hierarchy when DB is empty", async () => {
75
+ test("migration auto-check preserves empty DB and reports explicit recovery", async () => {
74
76
  const base = makeBase();
75
77
  try {
76
78
  await writeGSDDirectory(projectFixture(), base);
@@ -79,32 +81,35 @@ test("migration auto-check imports markdown hierarchy when DB is empty", async (
79
81
  assert.equal(await ensureDbOpen(base), true);
80
82
  assert.equal(getAllMilestones().length, 0, "fresh authoritative DB starts empty");
81
83
 
82
- const result = await autoImportMarkdownHierarchyIfDbMismatch(base);
83
- assert.equal(result.action, "imported");
84
+ const result = await checkMarkdownHierarchyAgainstDb(base);
85
+ assert.equal(result.action, "recovery-required");
84
86
  assert.equal(result.reason, "db-empty");
85
- assert.deepEqual(result.afterDb, { milestones: 1, slices: 1, tasks: 1 });
86
- assert.equal(getAllMilestones().length, 1);
87
- assert.equal(getSliceTasks("M001", "S01").length, 1);
87
+ assert.deepEqual(result.afterDb, { milestones: 0, slices: 0, tasks: 0 });
88
+ assert.equal(result.recoveryCommand, "gsd recover");
89
+ assert.match(result.message ?? "", /will not import markdown automatically/);
90
+ assert.equal(getAllMilestones().length, 0);
91
+ assert.equal(getSliceTasks("M001", "S01").length, 0);
88
92
  } finally {
89
93
  cleanup(base);
90
94
  }
91
95
  });
92
96
 
93
- test("migration auto-check repairs DB hierarchy count mismatch", async () => {
97
+ test("migration auto-check preserves DB on hierarchy count mismatch", async () => {
94
98
  const base = makeBase();
95
99
  try {
96
100
  await writeGSDDirectory(projectFixture(), base);
97
- await autoImportMarkdownHierarchyIfDbMismatch(base);
98
-
99
- _getAdapter()!.prepare("DELETE FROM tasks WHERE milestone_id = ? AND slice_id = ? AND id = ?").run("M001", "S01", "T01");
101
+ assert.equal(await ensureDbOpen(base), true);
102
+ insertMilestone({ id: "M001", title: "Legacy Milestone", status: "active" });
103
+ insertSlice({ id: "S01", milestoneId: "M001", title: "Legacy Slice", status: "pending", risk: "medium", depends: [], demo: "Legacy slice demo", sequence: 1 });
100
104
  assert.equal(getSliceTasks("M001", "S01").length, 0, "test fixture simulates stale DB task count");
101
105
 
102
- const result = await autoImportMarkdownHierarchyIfDbMismatch(base);
103
- assert.equal(result.action, "imported");
106
+ const result = await checkMarkdownHierarchyAgainstDb(base);
107
+ assert.equal(result.action, "recovery-required");
104
108
  assert.equal(result.reason, "count-mismatch");
105
109
  assert.deepEqual(result.beforeDb, { milestones: 1, slices: 1, tasks: 0 });
106
- assert.deepEqual(result.afterDb, { milestones: 1, slices: 1, tasks: 1 });
107
- assert.equal(getSliceTasks("M001", "S01").length, 1);
110
+ assert.deepEqual(result.afterDb, { milestones: 1, slices: 1, tasks: 0 });
111
+ assert.equal(result.recoveryCommand, "gsd recover");
112
+ assert.equal(getSliceTasks("M001", "S01").length, 0);
108
113
  } finally {
109
114
  cleanup(base);
110
115
  }
@@ -114,9 +119,12 @@ test("migration auto-check leaves matching DB hierarchy alone", async () => {
114
119
  const base = makeBase();
115
120
  try {
116
121
  await writeGSDDirectory(projectFixture(), base);
117
- await autoImportMarkdownHierarchyIfDbMismatch(base);
122
+ assert.equal(await ensureDbOpen(base), true);
123
+ insertMilestone({ id: "M001", title: "Legacy Milestone", status: "active" });
124
+ insertSlice({ id: "S01", milestoneId: "M001", title: "Legacy Slice", status: "pending", risk: "medium", depends: [], demo: "Legacy slice demo", sequence: 1 });
125
+ insertTask({ id: "T01", sliceId: "S01", milestoneId: "M001", title: "Legacy Task", status: "pending" });
118
126
 
119
- const result = await autoImportMarkdownHierarchyIfDbMismatch(base);
127
+ const result = await checkMarkdownHierarchyAgainstDb(base);
120
128
  assert.equal(result.action, "none");
121
129
  assert.equal(result.reason, "in-sync");
122
130
  assert.deepEqual(result.markdown, { milestones: 1, slices: 1, tasks: 1 });
@@ -11,6 +11,7 @@ import {
11
11
  clearPendingAutoStart,
12
12
  _getPendingAutoStart,
13
13
  } from "../guided-flow.ts";
14
+ import type { PendingAutoStartInput } from "../pending-auto-start.ts";
14
15
 
15
16
  // ─── Helpers ─────────────────────────────────────────────────────────────────
16
17
 
@@ -20,6 +21,15 @@ function makeProjectDir(): string {
20
21
  return dir;
21
22
  }
22
23
 
24
+ function pendingInput(basePath: string, milestoneId: string) {
25
+ return {
26
+ basePath,
27
+ milestoneId,
28
+ ctx: { ui: { notify: () => undefined } } as any,
29
+ pi: { sendMessage: () => undefined } as any,
30
+ };
31
+ }
32
+
23
33
  // ─── Tests ───────────────────────────────────────────────────────────────────
24
34
 
25
35
  describe("pendingAutoStart scope pinning (C1)", () => {
@@ -38,7 +48,7 @@ describe("pendingAutoStart scope pinning (C1)", () => {
38
48
  });
39
49
 
40
50
  test("setPendingAutoStart stores a scope whose paths derive from the basePath at reservation time", () => {
41
- setPendingAutoStart(base, { basePath: base, milestoneId: "M001" });
51
+ setPendingAutoStart(base, pendingInput(base, "M001"));
42
52
 
43
53
  const entry = _getPendingAutoStart(base);
44
54
  assert.ok(entry, "entry should exist");
@@ -54,8 +64,22 @@ describe("pendingAutoStart scope pinning (C1)", () => {
54
64
  assert.equal(entry.scope.stateFile(), expectedState);
55
65
  });
56
66
 
67
+ test("setPendingAutoStart rejects entries without ctx and pi before storing them", () => {
68
+ assert.throws(
69
+ () =>
70
+ setPendingAutoStart(base, {
71
+ basePath: base,
72
+ milestoneId: "M001",
73
+ } as PendingAutoStartInput),
74
+ /requires ctx and pi/,
75
+ "pending entries must include the handles later used by auto-start recovery",
76
+ );
77
+
78
+ assert.equal(_getPendingAutoStart(base), null);
79
+ });
80
+
57
81
  test("scope paths are unaffected by process.chdir after reservation", (t) => {
58
- setPendingAutoStart(base, { basePath: base, milestoneId: "M002" });
82
+ setPendingAutoStart(base, pendingInput(base, "M002"));
59
83
 
60
84
  const entry = _getPendingAutoStart(base);
61
85
  assert.ok(entry, "entry should exist");
@@ -82,7 +106,7 @@ describe("pendingAutoStart scope pinning (C1)", () => {
82
106
 
83
107
  test("scope identityKey matches the realpath of the original basePath even with trailing slash", () => {
84
108
  const baseWithSlash = base + "/";
85
- setPendingAutoStart(base, { basePath: baseWithSlash, milestoneId: "M003" });
109
+ setPendingAutoStart(base, pendingInput(baseWithSlash, "M003"));
86
110
 
87
111
  const entry = _getPendingAutoStart(base);
88
112
  assert.ok(entry, "entry should exist");
@@ -96,7 +120,7 @@ describe("pendingAutoStart scope pinning (C1)", () => {
96
120
  });
97
121
 
98
122
  test("clearPendingAutoStart removes the entry", () => {
99
- setPendingAutoStart(base, { basePath: base, milestoneId: "M001" });
123
+ setPendingAutoStart(base, pendingInput(base, "M001"));
100
124
 
101
125
  const before = _getPendingAutoStart(base);
102
126
  assert.ok(before, "entry should exist before clear");
@@ -108,7 +132,7 @@ describe("pendingAutoStart scope pinning (C1)", () => {
108
132
  });
109
133
 
110
134
  test("_getPendingAutoStart with no basePath argument returns the sole entry", () => {
111
- setPendingAutoStart(base, { basePath: base, milestoneId: "M001" });
135
+ setPendingAutoStart(base, pendingInput(base, "M001"));
112
136
 
113
137
  // No argument — should return the sole entry
114
138
  const entry = _getPendingAutoStart();
@@ -65,7 +65,9 @@ test("plan-slice prompt: DB-backed tool names survive template substitution", ()
65
65
  const result = loadPrompt("plan-slice", { ...BASE_VARS, commitInstruction: "Do not commit." });
66
66
  assert.ok(result.includes("gsd_plan_slice"), "gsd_plan_slice should appear in rendered prompt");
67
67
  assert.ok(result.includes("gsd_plan_task"), "gsd_plan_task should appear in rendered prompt");
68
+ assert.ok(result.includes("gsd_decision_save"), "structural decisions should use DB-backed decision tool");
68
69
  assert.ok(result.includes("canonical write path"), "canonical write path language should survive substitution");
70
+ assert.doesNotMatch(result, /append them to `.gsd\/DECISIONS\.md`/);
69
71
  });
70
72
 
71
73
  test("plan-slice prompt: compact planning gates survive template substitution", () => {
@@ -141,3 +141,62 @@ test("category summaries expose the wizard menu surface for configured prefs", (
141
141
  assert.match(summaries.integrations, /remote: C123/);
142
142
  assert.match(summaries.verification, /1 cmd/);
143
143
  });
144
+
145
+ test("models wizard offers discovered models for enabled providers", async () => {
146
+ const dir = mkdtempSync(join(tmpdir(), "gsd-prefs-wizard-"));
147
+ const prefsPath = join(dir, "PREFERENCES.md");
148
+ const choices = [
149
+ "Models",
150
+ "local (2 models)",
151
+ "discovered-model",
152
+ "(keep current)",
153
+ "(keep current)",
154
+ "(keep current)",
155
+ "(keep current)",
156
+ "(keep current)",
157
+ "(keep current)",
158
+ "(keep current)",
159
+ ];
160
+ const ctx = {
161
+ modelRegistry: {
162
+ getAvailable: () => [{ provider: "local", id: "baseline-model" }],
163
+ getAllWithDiscovered: () => [
164
+ { provider: "local", id: "baseline-model" },
165
+ { provider: "local", id: "discovered-model" },
166
+ { provider: "disabled", id: "hidden-model" },
167
+ ],
168
+ },
169
+ ui: {
170
+ notify() {},
171
+ select: async (label: string, options: string[]) => {
172
+ const choice = choices.shift();
173
+ if (!choice && label === "GSD Preferences") return "── Save & Exit ──";
174
+ if (!choice && options.includes("(keep current)")) return "(keep current)";
175
+ if (!choice && options.includes("Done")) return "Done";
176
+ assert.ok(choice, `Unexpected prompt: ${label}`);
177
+ if (choice === "Models") {
178
+ const modelsOption = options.find((option) => option.startsWith("Models"));
179
+ assert.ok(modelsOption, "Expected Models category option");
180
+ return modelsOption;
181
+ }
182
+ assert.ok(options.includes(choice), `"${choice}" must be offered by "${label}"`);
183
+ assert.ok(!options.includes("hidden-model"), "models from disabled providers must not be offered");
184
+ return choice;
185
+ },
186
+ input: async () => null,
187
+ },
188
+ waitForIdle: async () => {},
189
+ reload: async () => {},
190
+ } as any;
191
+
192
+ try {
193
+ await handlePrefsWizard(ctx, "project", {}, { pathOverride: prefsPath });
194
+
195
+ assert.equal(choices.length, 0, "Expected all queued wizard choices to be consumed");
196
+ const saved = readFileSync(prefsPath, "utf-8");
197
+ assert.match(saved, /research:\s+local\/discovered-model/);
198
+ assert.doesNotMatch(saved, /hidden-model/);
199
+ } finally {
200
+ rmSync(dir, { recursive: true, force: true });
201
+ }
202
+ });
@@ -10,7 +10,12 @@ import assert from "node:assert/strict";
10
10
  import { classifyError, isTransient, isTransientNetworkError } from "../error-classifier.ts";
11
11
  import { pauseAutoForProviderError } from "../provider-error-pause.ts";
12
12
  import { resumeAutoAfterProviderDelay } from "../bootstrap/provider-error-resume.ts";
13
- import { MAX_TRANSIENT_AUTO_RESUMES, resetTransientRetryState } from "../bootstrap/agent-end-recovery.ts";
13
+ import {
14
+ MAX_TRANSIENT_AUTO_RESUMES,
15
+ isTerminalDeletedWorktreeProviderError,
16
+ resetTransientRetryState,
17
+ shouldDeferTransientErrorToCoreRetry,
18
+ } from "../bootstrap/agent-end-recovery.ts";
14
19
  import { _buildCancelledUnitStopReason } from "../auto/phases.ts";
15
20
  import { getNextFallbackModel } from "../preferences.ts";
16
21
  // Zero-import module — imported by path rather than through the package
@@ -399,6 +404,25 @@ test("pauseAutoForProviderError falls back to indefinite pause when not rate lim
399
404
  ]);
400
405
  });
401
406
 
407
+ test("isTerminalDeletedWorktreeProviderError matches removed auto-worktree paths only", () => {
408
+ assert.equal(
409
+ isTerminalDeletedWorktreeProviderError('Path "/Users/dev/.gsd/projects/abc123/worktrees/M005" does not exist'),
410
+ true,
411
+ );
412
+ assert.equal(
413
+ isTerminalDeletedWorktreeProviderError('Path "/Users/dev/app/.gsd/worktrees/M005" does not exist'),
414
+ true,
415
+ );
416
+ assert.equal(
417
+ isTerminalDeletedWorktreeProviderError('Path "/Users/dev/app/src/file.ts" does not exist'),
418
+ false,
419
+ );
420
+ assert.equal(
421
+ isTerminalDeletedWorktreeProviderError('Path "/Users/dev/.gsd/projects/abc123/worktrees/M005" failed with EACCES'),
422
+ false,
423
+ );
424
+ });
425
+
402
426
  // ── resumeAutoAfterProviderDelay ────────────────────────────────────────────
403
427
 
404
428
  test("resumeAutoAfterProviderDelay restarts paused auto-mode from the recorded base path", async () => {
@@ -659,3 +683,15 @@ test("agent-session retryable error regex matches server_error (underscore)", ()
659
683
  // "temporarily backed off" must NOT be matched (intentional exclusion #3429)
660
684
  assert.ok(!RETRYABLE_ERROR_RE.test("temporarily backed off"));
661
685
  });
686
+
687
+ test("exhausted retry errors are not deferred back to core retry handling", () => {
688
+ const cls = classifyError("Retry failed after 3 attempts: 500 empty_stream: upstream stream closed before first payload");
689
+ assert.equal(cls.kind, "server");
690
+ assert.equal(
691
+ shouldDeferTransientErrorToCoreRetry(
692
+ cls,
693
+ "Retry failed after 3 attempts: 500 empty_stream: upstream stream closed before first payload",
694
+ ),
695
+ false,
696
+ );
697
+ });
@@ -0,0 +1,54 @@
1
+ import { describe, test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+
4
+ import { showQueueReorder } from "../queue-reorder-ui.ts";
5
+
6
+ const fakeTheme = {
7
+ fg: (_color: string, text: string) => text,
8
+ bold: (text: string) => text,
9
+ };
10
+
11
+ describe("queue-reorder-ui", () => {
12
+ test("keeps cursor visible while scrolling long queue with arrow keys (#4656)", async () => {
13
+ const originalRowsDescriptor = Object.getOwnPropertyDescriptor(process.stdout, "rows");
14
+ Object.defineProperty(process.stdout, "rows", { value: 20, configurable: true });
15
+
16
+ try {
17
+ const pending = Array.from({ length: 20 }, (_, idx) => ({
18
+ id: `M${String(idx + 1).padStart(3, "0")}`,
19
+ title: `Milestone ${idx + 1}`,
20
+ }));
21
+
22
+ let resolved: { order: string[]; depsToRemove: Array<{ milestone: string; dep: string }> } | null = null;
23
+ let lastRender: string[] = [];
24
+
25
+ const ctx = {
26
+ hasUI: true,
27
+ ui: {
28
+ custom: async (factory: any) => {
29
+ const component = factory({ requestRender() {} }, fakeTheme, null, (value: any) => {
30
+ resolved = value;
31
+ });
32
+
33
+ for (let i = 0; i < 15; i++) component.handleInput("\u001b[B");
34
+ lastRender = component.render(100);
35
+ component.handleInput("\r");
36
+ return resolved;
37
+ },
38
+ },
39
+ } as any;
40
+
41
+ await showQueueReorder(ctx, [], pending);
42
+
43
+ const joined = lastRender.join("\n");
44
+ assert.ok(joined.includes("M016"), "selected item should stay visible after scrolling");
45
+ assert.ok(lastRender.length <= 16, `overlay should fit terminal max-height, got ${lastRender.length}`);
46
+ } finally {
47
+ if (originalRowsDescriptor) {
48
+ Object.defineProperty(process.stdout, "rows", originalRowsDescriptor);
49
+ } else {
50
+ delete (process.stdout as { rows?: number }).rows;
51
+ }
52
+ }
53
+ });
54
+ });