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.
- package/dist/resources/extensions/gsd/auto/finalize-timeout.js +2 -0
- package/dist/resources/extensions/gsd/auto/loop.js +2 -2
- package/dist/resources/extensions/gsd/auto/phases.js +48 -5
- package/dist/resources/extensions/gsd/auto/types.js +2 -0
- package/dist/resources/extensions/gsd/auto-dashboard.js +2 -1
- package/dist/resources/extensions/gsd/auto-start.js +134 -2
- package/dist/resources/extensions/gsd/bootstrap/system-context.js +3 -1
- package/dist/resources/extensions/gsd/commands/handlers/core.js +3 -2
- package/dist/resources/extensions/gsd/files.js +17 -0
- package/dist/resources/extensions/gsd/notification-overlay.js +1 -1
- package/dist/resources/extensions/gsd/notification-widget.js +2 -1
- package/dist/resources/extensions/gsd/parallel-monitor-overlay.js +1 -1
- package/dist/resources/extensions/gsd/pre-execution-checks.js +16 -2
- package/dist/resources/extensions/gsd/prompts/system.md +2 -2
- package/dist/resources/extensions/subagent/agents.js +19 -5
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +13 -13
- package/dist/web/standalone/.next/build-manifest.json +3 -3
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/required-server-files.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error/page.js +3 -3
- package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found/page.js +2 -2
- package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +3 -3
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +3 -3
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/api/boot/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/boot/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/browse-directories/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/browse-directories/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/captures/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/captures/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/cleanup/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/cleanup/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/dev-mode/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/dev-mode/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/doctor/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/doctor/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/experimental/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/experimental/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/export-data/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/export-data/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/files/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/forensics/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/forensics/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/history/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/history/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/hooks/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/hooks/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/inspect/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/inspect/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/knowledge/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/knowledge/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/live-state/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/live-state/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/notifications/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/notifications/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/onboarding/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/onboarding/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/preferences/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/preferences/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/projects/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/recovery/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/recovery/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/remote-questions/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/remote-questions/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/browser/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/browser/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/command/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/command/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/events/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/session/events/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/manage/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/manage/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/settings-data/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/settings-data/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/shutdown/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/skill-health/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/skill-health/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/steer/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/steer/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/switch-root/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/switch-root/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/input/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/terminal/input/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/resize/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/terminal/resize/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/terminal/sessions/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js +4 -4
- package/dist/web/standalone/.next/server/app/api/terminal/stream/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/upload/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/upload/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/undo/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/undo/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/visualizer/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +4 -4
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +4 -4
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +3 -3
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/page.js +2 -2
- package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +13 -13
- package/dist/web/standalone/.next/server/chunks/6897.js +1 -1
- package/dist/web/standalone/.next/server/chunks/7471.js +3 -3
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware.js +2 -2
- package/dist/web/standalone/.next/server/next-font-manifest.js +1 -1
- package/dist/web/standalone/.next/server/next-font-manifest.json +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +2 -2
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/.next/static/chunks/app/_not-found/{page-f2a7482d42a5614b.js → page-2f24283c162b6ab3.js} +1 -1
- package/dist/web/standalone/.next/static/chunks/app/{layout-a16c7a7ecdf0c2cf.js → layout-9ecfd95f343793f0.js} +1 -1
- package/dist/web/standalone/.next/static/chunks/app/page-62be3b5fa91e4c8f.js +1 -0
- package/dist/web/standalone/.next/static/chunks/main-app-d3d4c336195465f9.js +1 -0
- package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-ab5a8926e07ec673.js +1 -0
- package/dist/web/standalone/node_modules/node-pty/build/Makefile +2 -2
- package/dist/web/standalone/node_modules/node-pty/build/Release/pty.node +0 -0
- package/dist/web/standalone/node_modules/node-pty/build/pty.target.mk +14 -14
- package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api.target.mk +14 -14
- package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_except.target.mk +14 -14
- package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_maybe.target.mk +14 -14
- package/dist/web/standalone/server.js +1 -1
- package/package.json +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-tui/dist/tui.d.ts.map +1 -1
- package/packages/pi-tui/dist/tui.js +3 -1
- package/packages/pi-tui/dist/tui.js.map +1 -1
- package/packages/pi-tui/src/tui.ts +3 -1
- package/pkg/package.json +1 -1
- package/src/resources/extensions/gsd/auto/finalize-timeout.ts +3 -0
- package/src/resources/extensions/gsd/auto/loop.ts +2 -2
- package/src/resources/extensions/gsd/auto/phases.ts +68 -3
- package/src/resources/extensions/gsd/auto/types.ts +5 -0
- package/src/resources/extensions/gsd/auto-dashboard.ts +2 -1
- package/src/resources/extensions/gsd/auto-start.ts +143 -0
- package/src/resources/extensions/gsd/bootstrap/system-context.ts +3 -1
- package/src/resources/extensions/gsd/commands/handlers/core.ts +3 -2
- package/src/resources/extensions/gsd/files.ts +19 -0
- package/src/resources/extensions/gsd/notification-overlay.ts +1 -1
- package/src/resources/extensions/gsd/notification-widget.ts +2 -1
- package/src/resources/extensions/gsd/parallel-monitor-overlay.ts +1 -1
- package/src/resources/extensions/gsd/pre-execution-checks.ts +19 -2
- package/src/resources/extensions/gsd/prompts/system.md +2 -2
- package/src/resources/extensions/gsd/tests/finalize-timeout-guard.test.ts +125 -0
- package/src/resources/extensions/gsd/tests/format-shortcut.test.ts +69 -0
- package/src/resources/extensions/gsd/tests/journal-integration.test.ts +11 -10
- package/src/resources/extensions/gsd/tests/orphaned-worktree-audit.test.ts +189 -0
- package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +66 -0
- package/src/resources/extensions/gsd/tests/subagent-agent-discovery.test.ts +47 -0
- package/src/resources/extensions/subagent/agents.ts +30 -6
- package/dist/web/standalone/.next/static/chunks/app/page-0c485498795110d6.js +0 -1
- package/dist/web/standalone/.next/static/chunks/main-app-fdab67f7802d7832.js +0 -1
- package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-459824ffb8c323dd.js +0 -1
- /package/dist/web/standalone/.next/static/{JwdBI3y1H8vtBKiYvWfEK → Bdk1mnQugYZh7ZxuXUYvc}/_buildManifest.js +0 -0
- /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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
- `
|
|
135
|
-
- `
|
|
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));
|