gsd-pi 2.65.0-dev.d0517ff → 2.66.0

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 (188) hide show
  1. package/dist/resources/extensions/gsd/auto/finalize-timeout.js +2 -0
  2. package/dist/resources/extensions/gsd/auto/loop.js +2 -2
  3. package/dist/resources/extensions/gsd/auto/phases.js +48 -5
  4. package/dist/resources/extensions/gsd/auto/types.js +2 -0
  5. package/dist/resources/extensions/gsd/auto-dashboard.js +2 -1
  6. package/dist/resources/extensions/gsd/auto-start.js +134 -2
  7. package/dist/resources/extensions/gsd/bootstrap/system-context.js +3 -1
  8. package/dist/resources/extensions/gsd/commands/handlers/core.js +3 -2
  9. package/dist/resources/extensions/gsd/files.js +17 -0
  10. package/dist/resources/extensions/gsd/notification-overlay.js +1 -1
  11. package/dist/resources/extensions/gsd/notification-widget.js +2 -1
  12. package/dist/resources/extensions/gsd/parallel-monitor-overlay.js +1 -1
  13. package/dist/resources/extensions/gsd/pre-execution-checks.js +16 -2
  14. package/dist/resources/extensions/gsd/prompts/system.md +2 -2
  15. package/dist/resources/extensions/subagent/agents.js +19 -5
  16. package/dist/web/standalone/.next/BUILD_ID +1 -1
  17. package/dist/web/standalone/.next/app-path-routes-manifest.json +13 -13
  18. package/dist/web/standalone/.next/build-manifest.json +3 -3
  19. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  20. package/dist/web/standalone/.next/required-server-files.json +3 -3
  21. package/dist/web/standalone/.next/server/app/_global-error/page.js +3 -3
  22. package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  23. package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
  24. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  25. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  26. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  27. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  28. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_not-found/page.js +2 -2
  32. package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  33. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  34. package/dist/web/standalone/.next/server/app/_not-found.rsc +3 -3
  35. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
  36. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +3 -3
  38. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/api/boot/route.js +1 -1
  42. package/dist/web/standalone/.next/server/app/api/boot/route_client-reference-manifest.js +1 -1
  43. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js +1 -1
  44. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route_client-reference-manifest.js +1 -1
  45. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js +1 -1
  46. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route_client-reference-manifest.js +1 -1
  47. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js +2 -2
  48. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route_client-reference-manifest.js +1 -1
  49. package/dist/web/standalone/.next/server/app/api/browse-directories/route.js +1 -1
  50. package/dist/web/standalone/.next/server/app/api/browse-directories/route_client-reference-manifest.js +1 -1
  51. package/dist/web/standalone/.next/server/app/api/captures/route.js +1 -1
  52. package/dist/web/standalone/.next/server/app/api/captures/route_client-reference-manifest.js +1 -1
  53. package/dist/web/standalone/.next/server/app/api/cleanup/route.js +1 -1
  54. package/dist/web/standalone/.next/server/app/api/cleanup/route_client-reference-manifest.js +1 -1
  55. package/dist/web/standalone/.next/server/app/api/dev-mode/route.js +1 -1
  56. package/dist/web/standalone/.next/server/app/api/dev-mode/route_client-reference-manifest.js +1 -1
  57. package/dist/web/standalone/.next/server/app/api/doctor/route.js +1 -1
  58. package/dist/web/standalone/.next/server/app/api/doctor/route_client-reference-manifest.js +1 -1
  59. package/dist/web/standalone/.next/server/app/api/experimental/route.js +2 -2
  60. package/dist/web/standalone/.next/server/app/api/experimental/route_client-reference-manifest.js +1 -1
  61. package/dist/web/standalone/.next/server/app/api/export-data/route.js +1 -1
  62. package/dist/web/standalone/.next/server/app/api/export-data/route_client-reference-manifest.js +1 -1
  63. package/dist/web/standalone/.next/server/app/api/files/route.js +1 -1
  64. package/dist/web/standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
  65. package/dist/web/standalone/.next/server/app/api/forensics/route.js +1 -1
  66. package/dist/web/standalone/.next/server/app/api/forensics/route_client-reference-manifest.js +1 -1
  67. package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
  68. package/dist/web/standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
  69. package/dist/web/standalone/.next/server/app/api/history/route.js +1 -1
  70. package/dist/web/standalone/.next/server/app/api/history/route_client-reference-manifest.js +1 -1
  71. package/dist/web/standalone/.next/server/app/api/hooks/route.js +1 -1
  72. package/dist/web/standalone/.next/server/app/api/hooks/route_client-reference-manifest.js +1 -1
  73. package/dist/web/standalone/.next/server/app/api/inspect/route.js +1 -1
  74. package/dist/web/standalone/.next/server/app/api/inspect/route_client-reference-manifest.js +1 -1
  75. package/dist/web/standalone/.next/server/app/api/knowledge/route.js +1 -1
  76. package/dist/web/standalone/.next/server/app/api/knowledge/route_client-reference-manifest.js +1 -1
  77. package/dist/web/standalone/.next/server/app/api/live-state/route.js +1 -1
  78. package/dist/web/standalone/.next/server/app/api/live-state/route_client-reference-manifest.js +1 -1
  79. package/dist/web/standalone/.next/server/app/api/notifications/route.js +2 -2
  80. package/dist/web/standalone/.next/server/app/api/notifications/route_client-reference-manifest.js +1 -1
  81. package/dist/web/standalone/.next/server/app/api/onboarding/route.js +1 -1
  82. package/dist/web/standalone/.next/server/app/api/onboarding/route_client-reference-manifest.js +1 -1
  83. package/dist/web/standalone/.next/server/app/api/preferences/route.js +1 -1
  84. package/dist/web/standalone/.next/server/app/api/preferences/route_client-reference-manifest.js +1 -1
  85. package/dist/web/standalone/.next/server/app/api/projects/route.js +1 -1
  86. package/dist/web/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
  87. package/dist/web/standalone/.next/server/app/api/recovery/route.js +1 -1
  88. package/dist/web/standalone/.next/server/app/api/recovery/route_client-reference-manifest.js +1 -1
  89. package/dist/web/standalone/.next/server/app/api/remote-questions/route.js +2 -2
  90. package/dist/web/standalone/.next/server/app/api/remote-questions/route_client-reference-manifest.js +1 -1
  91. package/dist/web/standalone/.next/server/app/api/session/browser/route.js +1 -1
  92. package/dist/web/standalone/.next/server/app/api/session/browser/route_client-reference-manifest.js +1 -1
  93. package/dist/web/standalone/.next/server/app/api/session/command/route.js +1 -1
  94. package/dist/web/standalone/.next/server/app/api/session/command/route_client-reference-manifest.js +1 -1
  95. package/dist/web/standalone/.next/server/app/api/session/events/route.js +2 -2
  96. package/dist/web/standalone/.next/server/app/api/session/events/route_client-reference-manifest.js +1 -1
  97. package/dist/web/standalone/.next/server/app/api/session/manage/route.js +1 -1
  98. package/dist/web/standalone/.next/server/app/api/session/manage/route_client-reference-manifest.js +1 -1
  99. package/dist/web/standalone/.next/server/app/api/settings-data/route.js +1 -1
  100. package/dist/web/standalone/.next/server/app/api/settings-data/route_client-reference-manifest.js +1 -1
  101. package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
  102. package/dist/web/standalone/.next/server/app/api/shutdown/route_client-reference-manifest.js +1 -1
  103. package/dist/web/standalone/.next/server/app/api/skill-health/route.js +1 -1
  104. package/dist/web/standalone/.next/server/app/api/skill-health/route_client-reference-manifest.js +1 -1
  105. package/dist/web/standalone/.next/server/app/api/steer/route.js +1 -1
  106. package/dist/web/standalone/.next/server/app/api/steer/route_client-reference-manifest.js +1 -1
  107. package/dist/web/standalone/.next/server/app/api/switch-root/route.js +1 -1
  108. package/dist/web/standalone/.next/server/app/api/switch-root/route_client-reference-manifest.js +1 -1
  109. package/dist/web/standalone/.next/server/app/api/terminal/input/route.js +2 -2
  110. package/dist/web/standalone/.next/server/app/api/terminal/input/route_client-reference-manifest.js +1 -1
  111. package/dist/web/standalone/.next/server/app/api/terminal/resize/route.js +2 -2
  112. package/dist/web/standalone/.next/server/app/api/terminal/resize/route_client-reference-manifest.js +1 -1
  113. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js +2 -2
  114. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route_client-reference-manifest.js +1 -1
  115. package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js +4 -4
  116. package/dist/web/standalone/.next/server/app/api/terminal/stream/route_client-reference-manifest.js +1 -1
  117. package/dist/web/standalone/.next/server/app/api/terminal/upload/route.js +1 -1
  118. package/dist/web/standalone/.next/server/app/api/terminal/upload/route_client-reference-manifest.js +1 -1
  119. package/dist/web/standalone/.next/server/app/api/undo/route.js +1 -1
  120. package/dist/web/standalone/.next/server/app/api/undo/route_client-reference-manifest.js +1 -1
  121. package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
  122. package/dist/web/standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
  123. package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
  124. package/dist/web/standalone/.next/server/app/api/visualizer/route_client-reference-manifest.js +1 -1
  125. package/dist/web/standalone/.next/server/app/index.html +1 -1
  126. package/dist/web/standalone/.next/server/app/index.rsc +4 -4
  127. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  128. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +4 -4
  129. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  130. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +3 -3
  131. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  132. package/dist/web/standalone/.next/server/app/page.js +2 -2
  133. package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  134. package/dist/web/standalone/.next/server/app-paths-manifest.json +13 -13
  135. package/dist/web/standalone/.next/server/chunks/6897.js +1 -1
  136. package/dist/web/standalone/.next/server/chunks/7471.js +3 -3
  137. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  138. package/dist/web/standalone/.next/server/middleware.js +2 -2
  139. package/dist/web/standalone/.next/server/next-font-manifest.js +1 -1
  140. package/dist/web/standalone/.next/server/next-font-manifest.json +1 -1
  141. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  142. package/dist/web/standalone/.next/server/pages/500.html +2 -2
  143. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  144. package/dist/web/standalone/.next/static/chunks/app/_not-found/{page-f2a7482d42a5614b.js → page-2f24283c162b6ab3.js} +1 -1
  145. package/dist/web/standalone/.next/static/chunks/app/{layout-a16c7a7ecdf0c2cf.js → layout-9ecfd95f343793f0.js} +1 -1
  146. package/dist/web/standalone/.next/static/chunks/app/page-62be3b5fa91e4c8f.js +1 -0
  147. package/dist/web/standalone/.next/static/chunks/main-app-d3d4c336195465f9.js +1 -0
  148. package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-ab5a8926e07ec673.js +1 -0
  149. package/dist/web/standalone/node_modules/node-pty/build/Makefile +2 -2
  150. package/dist/web/standalone/node_modules/node-pty/build/Release/pty.node +0 -0
  151. package/dist/web/standalone/node_modules/node-pty/build/pty.target.mk +14 -14
  152. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api.target.mk +14 -14
  153. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_except.target.mk +14 -14
  154. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_maybe.target.mk +14 -14
  155. package/dist/web/standalone/server.js +1 -1
  156. package/package.json +1 -1
  157. package/packages/pi-coding-agent/package.json +1 -1
  158. package/packages/pi-tui/dist/tui.d.ts.map +1 -1
  159. package/packages/pi-tui/dist/tui.js +3 -1
  160. package/packages/pi-tui/dist/tui.js.map +1 -1
  161. package/packages/pi-tui/src/tui.ts +3 -1
  162. package/pkg/package.json +1 -1
  163. package/src/resources/extensions/gsd/auto/finalize-timeout.ts +3 -0
  164. package/src/resources/extensions/gsd/auto/loop.ts +2 -2
  165. package/src/resources/extensions/gsd/auto/phases.ts +68 -3
  166. package/src/resources/extensions/gsd/auto/types.ts +5 -0
  167. package/src/resources/extensions/gsd/auto-dashboard.ts +2 -1
  168. package/src/resources/extensions/gsd/auto-start.ts +143 -0
  169. package/src/resources/extensions/gsd/bootstrap/system-context.ts +3 -1
  170. package/src/resources/extensions/gsd/commands/handlers/core.ts +3 -2
  171. package/src/resources/extensions/gsd/files.ts +19 -0
  172. package/src/resources/extensions/gsd/notification-overlay.ts +1 -1
  173. package/src/resources/extensions/gsd/notification-widget.ts +2 -1
  174. package/src/resources/extensions/gsd/parallel-monitor-overlay.ts +1 -1
  175. package/src/resources/extensions/gsd/pre-execution-checks.ts +19 -2
  176. package/src/resources/extensions/gsd/prompts/system.md +2 -2
  177. package/src/resources/extensions/gsd/tests/finalize-timeout-guard.test.ts +125 -0
  178. package/src/resources/extensions/gsd/tests/format-shortcut.test.ts +69 -0
  179. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +11 -10
  180. package/src/resources/extensions/gsd/tests/orphaned-worktree-audit.test.ts +189 -0
  181. package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +66 -0
  182. package/src/resources/extensions/gsd/tests/subagent-agent-discovery.test.ts +47 -0
  183. package/src/resources/extensions/subagent/agents.ts +30 -6
  184. package/dist/web/standalone/.next/static/chunks/app/page-0c485498795110d6.js +0 -1
  185. package/dist/web/standalone/.next/static/chunks/main-app-fdab67f7802d7832.js +0 -1
  186. package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-459824ffb8c323dd.js +0 -1
  187. /package/dist/web/standalone/.next/static/{JwdBI3y1H8vtBKiYvWfEK → Bdk1mnQugYZh7ZxuXUYvc}/_buildManifest.js +0 -0
  188. /package/dist/web/standalone/.next/static/{JwdBI3y1H8vtBKiYvWfEK → Bdk1mnQugYZh7ZxuXUYvc}/_ssgManifest.js +0 -0
@@ -47,6 +47,10 @@ import {
47
47
  nativeGetCurrentBranch,
48
48
  nativeDetectMainBranch,
49
49
  nativeCheckoutBranch,
50
+ nativeBranchList,
51
+ nativeBranchListMerged,
52
+ nativeBranchDelete,
53
+ nativeWorktreeRemove,
50
54
  } from "./native-git-bridge.js";
51
55
  import { GitServiceImpl } from "./git-service.js";
52
56
  import {
@@ -56,6 +60,7 @@ import {
56
60
  } from "./worktree.js";
57
61
  import { getAutoWorktreePath, isInAutoWorktree } from "./auto-worktree.js";
58
62
  import { readResourceVersion, cleanStaleRuntimeUnits } from "./auto-worktree.js";
63
+ import { worktreePath as getWorktreeDir, isInsideWorktreesDir } from "./worktree-manager.js";
59
64
  import { initMetrics } from "./metrics.js";
60
65
  import { initRoutingHistory } from "./routing-history.js";
61
66
  import { restoreHookState, resetHookState } from "./post-unit-hooks.js";
@@ -76,6 +81,7 @@ import {
76
81
  existsSync,
77
82
  mkdirSync,
78
83
  readdirSync,
84
+ rmSync,
79
85
  statSync,
80
86
  unlinkSync,
81
87
  } from "node:fs";
@@ -117,6 +123,123 @@ export async function openProjectDbIfPresent(basePath: string): Promise<void> {
117
123
  }
118
124
  }
119
125
 
126
+ /**
127
+ * Audit for orphaned milestone branches at bootstrap.
128
+ *
129
+ * After a milestone completes, the teardown step (merge branch → main,
130
+ * delete branch, remove worktree) runs as a post-completion engine step.
131
+ * If the session ends between completion and teardown, the branch and
132
+ * worktree are orphaned — the DB says "complete" so auto-mode won't
133
+ * re-enter the milestone, and the teardown is never retried.
134
+ *
135
+ * This audit runs on every fresh bootstrap to catch that gap:
136
+ * 1. Lists all local `milestone/*` branches.
137
+ * 2. For each, checks if the milestone's DB status is "complete".
138
+ * 3. If the branch is already merged into main → deletes the branch
139
+ * and cleans up any orphaned worktree directory (safe, no data loss).
140
+ * 4. If the branch is NOT merged → preserves it and warns the user
141
+ * so they can merge manually (data safety first).
142
+ *
143
+ * Returns a summary of actions taken for the caller to surface via notify.
144
+ */
145
+ export function auditOrphanedMilestoneBranches(
146
+ basePath: string,
147
+ isolationMode: "worktree" | "branch" | "none",
148
+ ): { recovered: string[]; warnings: string[] } {
149
+ const recovered: string[] = [];
150
+ const warnings: string[] = [];
151
+
152
+ // Skip in none mode — no milestone branches are created
153
+ if (isolationMode === "none") return { recovered, warnings };
154
+
155
+ // Skip if DB not available — can't determine completion status
156
+ if (!isDbAvailable()) return { recovered, warnings };
157
+
158
+ let milestoneBranches: string[];
159
+ try {
160
+ milestoneBranches = nativeBranchList(basePath, "milestone/*");
161
+ } catch {
162
+ // git branch list failed — skip audit
163
+ return { recovered, warnings };
164
+ }
165
+
166
+ if (milestoneBranches.length === 0) return { recovered, warnings };
167
+
168
+ // Detect main branch for merge-check
169
+ let mainBranch: string;
170
+ try {
171
+ mainBranch = nativeDetectMainBranch(basePath);
172
+ } catch {
173
+ mainBranch = "main";
174
+ }
175
+
176
+ // Get branches already merged into main
177
+ let mergedBranches: Set<string>;
178
+ try {
179
+ mergedBranches = new Set(nativeBranchListMerged(basePath, mainBranch, "milestone/*"));
180
+ } catch {
181
+ mergedBranches = new Set();
182
+ }
183
+
184
+ for (const branch of milestoneBranches) {
185
+ const milestoneId = branch.replace(/^milestone\//, "");
186
+ const milestone = getMilestone(milestoneId);
187
+
188
+ // Only audit completed milestones
189
+ if (!milestone || milestone.status !== "complete") continue;
190
+
191
+ const isMerged = mergedBranches.has(branch);
192
+
193
+ if (isMerged) {
194
+ // Branch is merged — safe to delete branch and clean up worktree dir
195
+ try {
196
+ nativeBranchDelete(basePath, branch, true);
197
+ recovered.push(`Deleted merged branch ${branch} for completed milestone ${milestoneId}.`);
198
+ } catch (err) {
199
+ warnings.push(`Failed to delete merged branch ${branch}: ${err instanceof Error ? err.message : String(err)}`);
200
+ }
201
+
202
+ // Clean up orphaned worktree directory if it exists
203
+ const wtDir = getWorktreeDir(basePath, milestoneId);
204
+ if (existsSync(wtDir)) {
205
+ // Try git worktree remove first (handles registered worktrees)
206
+ try {
207
+ nativeWorktreeRemove(basePath, wtDir, true);
208
+ } catch (e) {
209
+ // Not a registered worktree — expected for orphaned dirs
210
+ logWarning("engine", `worktree remove failed (expected for orphaned dirs): ${e instanceof Error ? e.message : String(e)}`);
211
+ }
212
+
213
+ // If the directory still exists after git worktree remove (either it
214
+ // wasn't registered or the remove was a noop), fall back to direct
215
+ // filesystem removal — but only inside .gsd/worktrees/ for safety (#2365).
216
+ if (existsSync(wtDir)) {
217
+ if (isInsideWorktreesDir(basePath, wtDir)) {
218
+ try {
219
+ rmSync(wtDir, { recursive: true, force: true });
220
+ recovered.push(`Removed orphaned worktree directory for ${milestoneId}.`);
221
+ } catch (err2) {
222
+ warnings.push(`Failed to remove worktree directory for ${milestoneId}: ${err2 instanceof Error ? err2.message : String(err2)}`);
223
+ }
224
+ } else {
225
+ warnings.push(`Orphaned worktree directory for ${milestoneId} is outside .gsd/worktrees/ — skipping removal for safety.`);
226
+ }
227
+ } else {
228
+ recovered.push(`Removed orphaned worktree directory for ${milestoneId}.`);
229
+ }
230
+ }
231
+ } else {
232
+ // Branch is NOT merged — preserve for safety, warn the user
233
+ warnings.push(
234
+ `Branch ${branch} exists for completed milestone ${milestoneId} but is NOT merged into ${mainBranch}. ` +
235
+ `This may contain unmerged work. Merge manually or run \`/gsd health --fix\` to resolve.`,
236
+ );
237
+ }
238
+ }
239
+
240
+ return { recovered, warnings };
241
+ }
242
+
120
243
  export async function bootstrapAutoSession(
121
244
  s: AutoSession,
122
245
  ctx: ExtensionCommandContext,
@@ -300,6 +423,26 @@ export async function bootstrapAutoSession(
300
423
  // derivation (queue-order, task status) works on a cold start (#2841).
301
424
  await openProjectDbIfPresent(base);
302
425
 
426
+ // ── Orphaned milestone branch audit ──
427
+ // Catches completed milestones whose teardown (merge + branch delete)
428
+ // was lost due to session ending between completion and teardown.
429
+ // Must run after DB open and before worktree entry.
430
+ try {
431
+ const auditResult = auditOrphanedMilestoneBranches(base, getIsolationMode());
432
+ for (const msg of auditResult.recovered) {
433
+ ctx.ui.notify(`Orphan audit: ${msg}`, "info");
434
+ }
435
+ for (const msg of auditResult.warnings) {
436
+ ctx.ui.notify(`Orphan audit: ${msg}`, "warning");
437
+ }
438
+ if (auditResult.recovered.length > 0) {
439
+ debugLog("orphan-audit", { recovered: auditResult.recovered, warnings: auditResult.warnings });
440
+ }
441
+ } catch (err) {
442
+ // Non-fatal — the audit is defensive, never block bootstrap
443
+ logWarning("bootstrap", `orphaned milestone branch audit failed: ${err instanceof Error ? err.message : String(err)}`);
444
+ }
445
+
303
446
  let state = await deriveState(base);
304
447
 
305
448
  // Stale worktree state recovery (#654)
@@ -15,7 +15,7 @@ import { hasSkillSnapshot, detectNewSkills, formatSkillsXml } from "../skill-dis
15
15
  import { getActiveAutoWorktreeContext } from "../auto-worktree.js";
16
16
  import { getActiveWorktreeName, getWorktreeOriginalCwd } from "../worktree-command.js";
17
17
  import { deriveState } from "../state.js";
18
- import { formatOverridesSection, loadActiveOverrides, loadFile, parseContinue, parseSummary } from "../files.js";
18
+ import { formatOverridesSection, formatShortcut, loadActiveOverrides, loadFile, parseContinue, parseSummary } from "../files.js";
19
19
  import { toPosixPath } from "../../shared/mod.js";
20
20
  import { markCmuxPromptShown, shouldPromptToEnableCmux } from "../../cmux/index.js";
21
21
 
@@ -72,6 +72,8 @@ export async function buildBeforeAgentStartResult(
72
72
  const systemContent = loadPrompt("system", {
73
73
  bundledSkillsTable: buildBundledSkillsTable(),
74
74
  templatesDir: getTemplatesDir(),
75
+ shortcutDashboard: formatShortcut("Ctrl+Alt+G"),
76
+ shortcutShell: formatShortcut("Ctrl+Alt+B"),
75
77
  });
76
78
  const loadedPreferences = loadEffectiveGSDPreferences();
77
79
  if (shouldPromptToEnableCmux(loadedPreferences?.preferences)) {
@@ -8,6 +8,7 @@ import { runEnvironmentChecks } from "../../doctor-environment.js";
8
8
  import { deriveState } from "../../state.js";
9
9
  import { handleCmux } from "../../commands-cmux.js";
10
10
  import { projectRoot } from "../context.js";
11
+ import { formatShortcut } from "../../files.js";
11
12
 
12
13
  export function showHelp(ctx: ExtensionCommandContext): void {
13
14
  const lines = [
@@ -24,12 +25,12 @@ export function showHelp(ctx: ExtensionCommandContext): void {
24
25
  " /gsd new-milestone Create milestone from headless context (used by gsd headless)",
25
26
  "",
26
27
  "VISIBILITY",
27
- " /gsd status Show progress dashboard (Ctrl+Alt+G)",
28
+ ` /gsd status Show progress dashboard (${formatShortcut("Ctrl+Alt+G")})`,
28
29
  " /gsd visualize Interactive 10-tab TUI (progress, timeline, deps, metrics, health, agent, changes, knowledge, captures, export)",
29
30
  " /gsd queue Show queued/dispatched units and execution order",
30
31
  " /gsd history View execution history [--cost] [--phase] [--model] [N]",
31
32
  " /gsd changelog Show categorized release notes [version]",
32
- " /gsd notifications View persistent notification history [clear|tail|filter] (Ctrl+Alt+N)",
33
+ ` /gsd notifications View persistent notification history [clear|tail|filter] (${formatShortcut("Ctrl+Alt+N")})`,
33
34
  "",
34
35
  "COURSE CORRECTION",
35
36
  " /gsd steer <desc> Apply user override to active work",
@@ -70,6 +70,25 @@ export function clearParseCache(): void {
70
70
  for (const cb of _cacheClearCallbacks) cb();
71
71
  }
72
72
 
73
+ // ─── Platform shortcuts ───────────────────────────────────────────────────
74
+
75
+ const IS_MAC = process.platform === "darwin";
76
+
77
+ /**
78
+ * Format a keyboard shortcut for the current OS.
79
+ * Input: modifier key combo like "Ctrl+Alt+G"
80
+ * Output: "⌃⌥G" on macOS, "Ctrl+Alt+G" on Windows/Linux.
81
+ */
82
+ export function formatShortcut(combo: string): string {
83
+ if (!IS_MAC) return combo;
84
+ return combo
85
+ .replace(/Ctrl\+Alt\+/i, "⌃⌥")
86
+ .replace(/Ctrl\+/i, "⌃")
87
+ .replace(/Alt\+/i, "⌥")
88
+ .replace(/Shift\+/i, "⇧")
89
+ .replace(/Cmd\+/i, "⌘");
90
+ }
91
+
73
92
  // ─── Helpers ───────────────────────────────────────────────────────────────
74
93
 
75
94
  /** Extract the text after a heading at a given level, up to the next heading of same or higher level. */
@@ -1,6 +1,6 @@
1
1
  // GSD Extension — Notification History Overlay
2
2
  // Scrollable panel showing all persisted notifications with severity filtering.
3
- // Toggled with Ctrl+Alt+N or opened from /gsd notifications.
3
+ // Toggled with Ctrl+Alt+N (⌃⌥N on macOS) or opened from /gsd notifications.
4
4
 
5
5
  import type { Theme } from "@gsd/pi-coding-agent";
6
6
  import { truncateToWidth, visibleWidth, matchesKey, Key } from "@gsd/pi-tui";
@@ -6,6 +6,7 @@
6
6
  import type { ExtensionContext } from "@gsd/pi-coding-agent";
7
7
 
8
8
  import { getUnreadCount, readNotifications } from "./notification-store.js";
9
+ import { formatShortcut } from "./files.js";
9
10
 
10
11
  // ─── Pure rendering ──���────────────────────────���─────────────────────────
11
12
 
@@ -24,7 +25,7 @@ export function buildNotificationWidgetLines(): string[] {
24
25
  ? latest.message.slice(0, msgMax - 1) + "…"
25
26
  : latest.message;
26
27
 
27
- return [` ${icon} [${badge}] ${truncated} (Ctrl+Alt+N to view)`];
28
+ return [` ${icon} [${badge}] ${truncated} (${formatShortcut("Ctrl+Alt+N")} to view)`];
28
29
  }
29
30
 
30
31
  // ─── Widget init ────────────────────────────────────────────────────────
@@ -2,7 +2,7 @@
2
2
  * GSD Parallel Monitor Overlay
3
3
  *
4
4
  * Full-screen TUI overlay showing real-time parallel worker progress.
5
- * Opened via `/gsd parallel watch` or Ctrl+Alt+P.
5
+ * Opened via `/gsd parallel watch` or Ctrl+Alt+P (⌃⌥P on macOS).
6
6
  * Reads the same data sources as `scripts/parallel-monitor.mjs` but
7
7
  * renders as a native pi-tui overlay with theme integration.
8
8
  */
@@ -238,8 +238,7 @@ export async function checkPackageExistence(
238
238
  export function normalizeFilePath(filePath: string): string {
239
239
  if (!filePath) return filePath;
240
240
 
241
- // Strip backtick wrapping from LLM-generated paths (#3649)
242
- let normalized = filePath.replace(/`/g, "");
241
+ let normalized = extractPathFromAnnotation(filePath);
243
242
 
244
243
  // Normalize path separators to forward slashes
245
244
  normalized = normalized.replace(/\\/g, "/");
@@ -260,6 +259,24 @@ export function normalizeFilePath(filePath: string): string {
260
259
  return normalized;
261
260
  }
262
261
 
262
+ function extractPathFromAnnotation(raw: string): string {
263
+ const trimmed = raw.trim();
264
+ if (!trimmed) return trimmed;
265
+
266
+ const backtickMatch = trimmed.match(/^`([^`]+)`(?:\s+[—–-]\s+.*)?$/);
267
+ if (backtickMatch) {
268
+ return backtickMatch[1].trim();
269
+ }
270
+
271
+ const annotatedMatch = trimmed.match(/^(.+?)\s+[—–-]\s+.+$/);
272
+ if (annotatedMatch) {
273
+ return annotatedMatch[1].trim();
274
+ }
275
+
276
+ // Fall back to the original behavior for already-plain paths.
277
+ return trimmed.replace(/`/g, "");
278
+ }
279
+
263
280
  /**
264
281
  * Build a set of files that will be created by tasks up to (but not including) taskIndex.
265
282
  * All paths are normalized for consistent comparison.
@@ -131,8 +131,8 @@ Templates showing the expected format for each artifact type are in:
131
131
  - `/gsd status` - progress dashboard overlay
132
132
  - `/gsd queue` - queue future milestones (safe while auto-mode is running)
133
133
  - `/gsd quick <task>` - quick task with GSD guarantees (atomic commits, state tracking) but no milestone ceremony
134
- - `Ctrl+Alt+G` - toggle dashboard overlay
135
- - `Ctrl+Alt+B` - show shell processes
134
+ - `{{shortcutDashboard}}` - toggle dashboard overlay
135
+ - `{{shortcutShell}}` - show shell processes
136
136
 
137
137
  ## Execution Heuristics
138
138
 
@@ -19,8 +19,10 @@
19
19
  import { createTestContext } from "./test-helpers.ts";
20
20
  import {
21
21
  withTimeout,
22
+ FINALIZE_PRE_TIMEOUT_MS,
22
23
  FINALIZE_POST_TIMEOUT_MS,
23
24
  } from "../auto/finalize-timeout.ts";
25
+ import { MAX_FINALIZE_TIMEOUTS } from "../auto/types.ts";
24
26
 
25
27
  const { assertTrue, assertEq, report } = createTestContext();
26
28
 
@@ -78,6 +80,25 @@ const { assertTrue, assertEq, report } = createTestContext();
78
80
  assertTrue(caught, "rejection should propagate");
79
81
  }
80
82
 
83
+ // ═══ Test: FINALIZE_PRE_TIMEOUT_MS is defined and reasonable ═════════════════
84
+
85
+ {
86
+ console.log("\n=== #3757: pre-verification timeout constant is defined and reasonable ===");
87
+
88
+ assertTrue(
89
+ typeof FINALIZE_PRE_TIMEOUT_MS === "number",
90
+ "FINALIZE_PRE_TIMEOUT_MS should be a number",
91
+ );
92
+ assertTrue(
93
+ FINALIZE_PRE_TIMEOUT_MS >= 30_000,
94
+ `pre timeout should be >= 30s (got ${FINALIZE_PRE_TIMEOUT_MS}ms)`,
95
+ );
96
+ assertTrue(
97
+ FINALIZE_PRE_TIMEOUT_MS <= 120_000,
98
+ `pre timeout should be <= 120s (got ${FINALIZE_PRE_TIMEOUT_MS}ms)`,
99
+ );
100
+ }
101
+
81
102
  // ═══ Test: FINALIZE_POST_TIMEOUT_MS is defined and reasonable ═════════════════
82
103
 
83
104
  {
@@ -113,4 +134,108 @@ const { assertTrue, assertEq, report } = createTestContext();
113
134
  assertEq(result.timedOut, false, "should not time out");
114
135
  }
115
136
 
137
+ // ═══ Test: runFinalize wraps BOTH pre and post verification with withTimeout ═
138
+
139
+ {
140
+ console.log("\n=== #3757: runFinalize wraps preVerification with timeout guard ===");
141
+
142
+ const { readFileSync } = await import("node:fs");
143
+ const phasesSource = readFileSync(
144
+ new URL("../auto/phases.ts", import.meta.url),
145
+ "utf-8",
146
+ );
147
+
148
+ // Find the runFinalize function body
149
+ const fnIdx = phasesSource.indexOf("export async function runFinalize(");
150
+ assertTrue(fnIdx > 0, "runFinalize function should exist in phases.ts");
151
+
152
+ const fnBody = phasesSource.slice(fnIdx, fnIdx + 8000);
153
+
154
+ // postUnitPreVerification must be wrapped in withTimeout
155
+ const preTimeoutIdx = fnBody.indexOf("withTimeout(");
156
+ assertTrue(preTimeoutIdx > 0, "withTimeout should appear in runFinalize");
157
+
158
+ const preVerIdx = fnBody.indexOf("postUnitPreVerification");
159
+ assertTrue(preVerIdx > 0, "postUnitPreVerification should appear in runFinalize");
160
+
161
+ // The first withTimeout should wrap postUnitPreVerification (not postUnitPostVerification)
162
+ const firstWithTimeout = fnBody.slice(preTimeoutIdx, preTimeoutIdx + 200);
163
+ assertTrue(
164
+ firstWithTimeout.includes("postUnitPreVerification"),
165
+ "first withTimeout in runFinalize should wrap postUnitPreVerification",
166
+ );
167
+
168
+ // postUnitPostVerification must also be wrapped
169
+ const postVerIdx = fnBody.indexOf("postUnitPostVerification");
170
+ assertTrue(postVerIdx > 0, "postUnitPostVerification should appear in runFinalize");
171
+
172
+ // Count withTimeout occurrences — should be at least 2 (pre + post)
173
+ const timeoutCount = (fnBody.match(/withTimeout\(/g) || []).length;
174
+ assertTrue(
175
+ timeoutCount >= 2,
176
+ `runFinalize should have at least 2 withTimeout guards (found ${timeoutCount})`,
177
+ );
178
+ }
179
+
180
+ // ═══ Test: MAX_FINALIZE_TIMEOUTS is defined and reasonable ═══════════════════
181
+
182
+ {
183
+ console.log("\n=== #3757: MAX_FINALIZE_TIMEOUTS is defined and reasonable ===");
184
+
185
+ assertTrue(
186
+ typeof MAX_FINALIZE_TIMEOUTS === "number",
187
+ "MAX_FINALIZE_TIMEOUTS should be a number",
188
+ );
189
+ assertTrue(
190
+ MAX_FINALIZE_TIMEOUTS >= 2,
191
+ `threshold should be >= 2 (got ${MAX_FINALIZE_TIMEOUTS})`,
192
+ );
193
+ assertTrue(
194
+ MAX_FINALIZE_TIMEOUTS <= 10,
195
+ `threshold should be <= 10 (got ${MAX_FINALIZE_TIMEOUTS})`,
196
+ );
197
+ }
198
+
199
+ // ═══ Test: timeout handlers escalate after consecutive timeouts ══════════════
200
+
201
+ {
202
+ console.log("\n=== #3757: timeout handlers escalate and detach currentUnit ===");
203
+
204
+ const { readFileSync } = await import("node:fs");
205
+ const phasesSource = readFileSync(
206
+ new URL("../auto/phases.ts", import.meta.url),
207
+ "utf-8",
208
+ );
209
+
210
+ const fnIdx = phasesSource.indexOf("export async function runFinalize(");
211
+ const fnBody = phasesSource.slice(fnIdx, fnIdx + 8000);
212
+
213
+ // Both timeout handlers should increment consecutiveFinalizeTimeouts
214
+ const incrementCount = (fnBody.match(/consecutiveFinalizeTimeouts\+\+/g) || []).length;
215
+ assertTrue(
216
+ incrementCount >= 2,
217
+ `should increment consecutiveFinalizeTimeouts in both pre and post handlers (found ${incrementCount})`,
218
+ );
219
+
220
+ // Both timeout handlers should check MAX_FINALIZE_TIMEOUTS for escalation
221
+ const escalationCount = (fnBody.match(/MAX_FINALIZE_TIMEOUTS/g) || []).length;
222
+ assertTrue(
223
+ escalationCount >= 2,
224
+ `should check MAX_FINALIZE_TIMEOUTS in both handlers (found ${escalationCount})`,
225
+ );
226
+
227
+ // Both timeout handlers should null out s.currentUnit to prevent late mutations
228
+ const detachCount = (fnBody.match(/s\.currentUnit\s*=\s*null/g) || []).length;
229
+ assertTrue(
230
+ detachCount >= 2,
231
+ `should detach s.currentUnit in both timeout handlers (found ${detachCount})`,
232
+ );
233
+
234
+ // Successful finalize should reset the counter
235
+ assertTrue(
236
+ fnBody.includes("consecutiveFinalizeTimeouts = 0"),
237
+ "should reset consecutiveFinalizeTimeouts on successful finalize",
238
+ );
239
+ }
240
+
116
241
  report();
@@ -0,0 +1,69 @@
1
+ // GSD Extension — formatShortcut tests
2
+ // Verifies OS-specific keyboard shortcut rendering.
3
+
4
+ import test from 'node:test';
5
+ import assert from 'node:assert/strict';
6
+ import { formatShortcut } from '../files.ts';
7
+
8
+ // ─── formatShortcut renders per-platform shortcuts ──────────────────────
9
+
10
+ test('formatShortcut: converts Ctrl+Alt combo on macOS', () => {
11
+ // formatShortcut uses process.platform at module load time.
12
+ // We can only test the current platform's behavior.
13
+ const result = formatShortcut('Ctrl+Alt+G');
14
+ if (process.platform === 'darwin') {
15
+ assert.strictEqual(result, '⌃⌥G', 'macOS should use ⌃⌥ symbols');
16
+ } else {
17
+ assert.strictEqual(result, 'Ctrl+Alt+G', 'non-macOS should pass through unchanged');
18
+ }
19
+ });
20
+
21
+ test('formatShortcut: converts Ctrl+Alt+N', () => {
22
+ const result = formatShortcut('Ctrl+Alt+N');
23
+ if (process.platform === 'darwin') {
24
+ assert.strictEqual(result, '⌃⌥N');
25
+ } else {
26
+ assert.strictEqual(result, 'Ctrl+Alt+N');
27
+ }
28
+ });
29
+
30
+ test('formatShortcut: converts Ctrl+Alt+B', () => {
31
+ const result = formatShortcut('Ctrl+Alt+B');
32
+ if (process.platform === 'darwin') {
33
+ assert.strictEqual(result, '⌃⌥B');
34
+ } else {
35
+ assert.strictEqual(result, 'Ctrl+Alt+B');
36
+ }
37
+ });
38
+
39
+ test('formatShortcut: converts standalone Ctrl modifier', () => {
40
+ const result = formatShortcut('Ctrl+C');
41
+ if (process.platform === 'darwin') {
42
+ assert.strictEqual(result, '⌃C');
43
+ } else {
44
+ assert.strictEqual(result, 'Ctrl+C');
45
+ }
46
+ });
47
+
48
+ test('formatShortcut: converts Shift modifier', () => {
49
+ const result = formatShortcut('Shift+Tab');
50
+ if (process.platform === 'darwin') {
51
+ assert.strictEqual(result, '⇧Tab');
52
+ } else {
53
+ assert.strictEqual(result, 'Shift+Tab');
54
+ }
55
+ });
56
+
57
+ test('formatShortcut: converts Cmd modifier', () => {
58
+ const result = formatShortcut('Cmd+S');
59
+ if (process.platform === 'darwin') {
60
+ assert.strictEqual(result, '⌘S');
61
+ } else {
62
+ assert.strictEqual(result, 'Cmd+S');
63
+ }
64
+ });
65
+
66
+ test('formatShortcut: passes through plain key names', () => {
67
+ assert.strictEqual(formatShortcut('Escape'), 'Escape');
68
+ assert.strictEqual(formatShortcut('Enter'), 'Enter');
69
+ });
@@ -216,7 +216,7 @@ test("runDispatch emits dispatch-match with correct rule and flowId", async () =
216
216
  mid: "M001",
217
217
  midTitle: "Test Milestone",
218
218
  };
219
- const loopState: LoopState = { recentUnits: [], stuckRecoveryAttempts: 0 };
219
+ const loopState: LoopState = { recentUnits: [], stuckRecoveryAttempts: 0, consecutiveFinalizeTimeouts: 0 };
220
220
 
221
221
  const result = await runDispatch(ic, preData, loopState);
222
222
 
@@ -248,7 +248,7 @@ test("runDispatch emits dispatch-stop when dispatch returns stop action", async
248
248
  mid: "M001",
249
249
  midTitle: "Test",
250
250
  };
251
- const loopState: LoopState = { recentUnits: [], stuckRecoveryAttempts: 0 };
251
+ const loopState: LoopState = { recentUnits: [], stuckRecoveryAttempts: 0, consecutiveFinalizeTimeouts: 0 };
252
252
 
253
253
  const result = await runDispatch(ic, preData, loopState);
254
254
  assert.equal(result.action, "break");
@@ -303,6 +303,7 @@ test("runDispatch checks prior-slice completion against the project root in work
303
303
  const result = await runDispatch(ic, preData, {
304
304
  recentUnits: [],
305
305
  stuckRecoveryAttempts: 0,
306
+ consecutiveFinalizeTimeouts: 0,
306
307
  });
307
308
 
308
309
  assert.equal(result.action, "next");
@@ -343,7 +344,7 @@ test("runUnitPhase emits unit-start and unit-end with causedBy reference", async
343
344
  isRetry: false,
344
345
  previousTier: undefined,
345
346
  };
346
- const loopState: LoopState = { recentUnits: [{ key: "execute-task/M001/S01/T01" }], stuckRecoveryAttempts: 0 };
347
+ const loopState: LoopState = { recentUnits: [{ key: "execute-task/M001/S01/T01" }], stuckRecoveryAttempts: 0, consecutiveFinalizeTimeouts: 0 };
347
348
 
348
349
  // Start runUnitPhase (it will block on runUnit internally)
349
350
  const unitPromise = runUnitPhase(ic, iterData, loopState);
@@ -400,7 +401,7 @@ test("all events from a mock iteration have monotonically increasing seq and sam
400
401
  mid: "M001",
401
402
  midTitle: "Test",
402
403
  };
403
- const loopState: LoopState = { recentUnits: [], stuckRecoveryAttempts: 0 };
404
+ const loopState: LoopState = { recentUnits: [], stuckRecoveryAttempts: 0, consecutiveFinalizeTimeouts: 0 };
404
405
  const dispatchResult = await runDispatch(ic, preData, loopState);
405
406
  assert.equal(dispatchResult.action, "next");
406
407
 
@@ -446,7 +447,7 @@ test("dispatch-match events include matchedRule field matching the rule name", a
446
447
  midTitle: "Test",
447
448
  };
448
449
 
449
- await runDispatch(ic, preData, { recentUnits: [], stuckRecoveryAttempts: 0 });
450
+ await runDispatch(ic, preData, { recentUnits: [], stuckRecoveryAttempts: 0, consecutiveFinalizeTimeouts: 0 });
450
451
 
451
452
  const matchEvents = capture.events.filter(e => e.eventType === "dispatch-match");
452
453
  assert.equal(matchEvents.length, 1);
@@ -475,7 +476,7 @@ test("pre-dispatch-hook event is emitted when hooks fire", async () => {
475
476
  midTitle: "Test",
476
477
  };
477
478
 
478
- await runDispatch(ic, preData, { recentUnits: [], stuckRecoveryAttempts: 0 });
479
+ await runDispatch(ic, preData, { recentUnits: [], stuckRecoveryAttempts: 0, consecutiveFinalizeTimeouts: 0 });
479
480
 
480
481
  const hookEvents = capture.events.filter(e => e.eventType === "pre-dispatch-hook");
481
482
  assert.equal(hookEvents.length, 1, "should emit one pre-dispatch-hook event");
@@ -497,7 +498,7 @@ test("terminal event is emitted on milestone-complete", async () => {
497
498
  }) as any,
498
499
  });
499
500
  const ic = makeIC(deps);
500
- const loopState: LoopState = { recentUnits: [], stuckRecoveryAttempts: 0 };
501
+ const loopState: LoopState = { recentUnits: [], stuckRecoveryAttempts: 0, consecutiveFinalizeTimeouts: 0 };
501
502
 
502
503
  const result = await runPreDispatch(ic, loopState);
503
504
  assert.equal(result.action, "break");
@@ -521,7 +522,7 @@ test("terminal event is emitted on blocked state", async () => {
521
522
  }) as any,
522
523
  });
523
524
  const ic = makeIC(deps);
524
- const loopState: LoopState = { recentUnits: [], stuckRecoveryAttempts: 0 };
525
+ const loopState: LoopState = { recentUnits: [], stuckRecoveryAttempts: 0, consecutiveFinalizeTimeouts: 0 };
525
526
 
526
527
  const result = await runPreDispatch(ic, loopState);
527
528
  assert.equal(result.action, "break");
@@ -550,7 +551,7 @@ test("milestone-transition event is emitted when milestone changes", async () =>
550
551
  const ic = makeIC(deps);
551
552
  // Session says current milestone is M001, but state will return M002
552
553
  ic.s.currentMilestoneId = "M001";
553
- const loopState: LoopState = { recentUnits: [], stuckRecoveryAttempts: 0 };
554
+ const loopState: LoopState = { recentUnits: [], stuckRecoveryAttempts: 0, consecutiveFinalizeTimeouts: 0 };
554
555
 
555
556
  await runPreDispatch(ic, loopState);
556
557
 
@@ -580,7 +581,7 @@ test("unit-end event contains errorContext when unit is cancelled with structure
580
581
  isRetry: false,
581
582
  previousTier: undefined,
582
583
  };
583
- const loopState: LoopState = { recentUnits: [{ key: "execute-task/M001/S01/T01" }], stuckRecoveryAttempts: 0 };
584
+ const loopState: LoopState = { recentUnits: [{ key: "execute-task/M001/S01/T01" }], stuckRecoveryAttempts: 0, consecutiveFinalizeTimeouts: 0 };
584
585
 
585
586
  const unitPromise = runUnitPhase(ic, iterData, loopState);
586
587
  await new Promise(r => setTimeout(r, 50));