gsd-pi 2.65.0-dev.d0517ff → 2.66.0-dev.1b4e601
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 +18 -18
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- 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.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- 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 +1 -1
- 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/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- 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 +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +18 -18
- 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/package.json +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-tui/dist/tui.d.ts +1 -0
- package/packages/pi-tui/dist/tui.d.ts.map +1 -1
- package/packages/pi-tui/dist/tui.js +8 -2
- package/packages/pi-tui/dist/tui.js.map +1 -1
- package/packages/pi-tui/src/tui.ts +8 -2
- 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/{JwdBI3y1H8vtBKiYvWfEK → fcV2z87tmOazTEreFWNdG}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{JwdBI3y1H8vtBKiYvWfEK → fcV2z87tmOazTEreFWNdG}/_ssgManifest.js +0 -0
|
@@ -239,6 +239,7 @@ export class TUI extends Container {
|
|
|
239
239
|
public onDebug?: () => void;
|
|
240
240
|
private renderRequested = false;
|
|
241
241
|
private cursorRow = 0; // Logical cursor row (end of rendered content)
|
|
242
|
+
private contentCursorRow = 0; // Cursor row after content rendering, before IME repositioning
|
|
242
243
|
private hardwareCursorRow = 0; // Actual terminal cursor row (may differ due to IME positioning)
|
|
243
244
|
private inputBuffer = ""; // Buffer for parsing terminal responses
|
|
244
245
|
private cellSizeQueryPending = false;
|
|
@@ -498,6 +499,7 @@ export class TUI extends Container {
|
|
|
498
499
|
this.previousWidth = -1; // -1 triggers widthChanged, forcing a full clear
|
|
499
500
|
this.previousHeight = -1; // -1 triggers heightChanged, forcing a full clear
|
|
500
501
|
this.cursorRow = 0;
|
|
502
|
+
this.contentCursorRow = 0;
|
|
501
503
|
this.hardwareCursorRow = 0;
|
|
502
504
|
this.maxLinesRendered = 0;
|
|
503
505
|
this.previousViewportTop = 0;
|
|
@@ -616,7 +618,7 @@ export class TUI extends Container {
|
|
|
616
618
|
const height = this.terminal.rows;
|
|
617
619
|
let viewportTop = Math.max(0, this.maxLinesRendered - height);
|
|
618
620
|
let prevViewportTop = this.previousViewportTop;
|
|
619
|
-
let hardwareCursorRow = this.
|
|
621
|
+
let hardwareCursorRow = this.contentCursorRow;
|
|
620
622
|
const computeLineDiff = (targetRow: number): number => {
|
|
621
623
|
const currentScreenRow = hardwareCursorRow - prevViewportTop;
|
|
622
624
|
const targetScreenRow = targetRow - viewportTop;
|
|
@@ -663,6 +665,7 @@ export class TUI extends Container {
|
|
|
663
665
|
buffer += "\x1b[?2026l"; // End synchronized output
|
|
664
666
|
this.terminal.write(buffer);
|
|
665
667
|
this.cursorRow = Math.max(0, newLines.length - 1);
|
|
668
|
+
this.contentCursorRow = this.cursorRow;
|
|
666
669
|
this.hardwareCursorRow = this.cursorRow;
|
|
667
670
|
// Reset max lines when clearing, otherwise track growth
|
|
668
671
|
if (clear) {
|
|
@@ -770,6 +773,7 @@ export class TUI extends Container {
|
|
|
770
773
|
buffer += "\x1b[?2026l";
|
|
771
774
|
this.terminal.write(buffer);
|
|
772
775
|
this.cursorRow = targetRow;
|
|
776
|
+
this.contentCursorRow = targetRow;
|
|
773
777
|
this.hardwareCursorRow = targetRow;
|
|
774
778
|
}
|
|
775
779
|
this.positionHardwareCursor(cursorPos, newLines.length);
|
|
@@ -887,8 +891,10 @@ export class TUI extends Container {
|
|
|
887
891
|
|
|
888
892
|
// Track cursor position for next render
|
|
889
893
|
// cursorRow tracks end of content (for viewport calculation)
|
|
890
|
-
//
|
|
894
|
+
// contentCursorRow tracks cursor after content rendering (before IME repositioning)
|
|
895
|
+
// hardwareCursorRow tracks actual terminal cursor position (may differ due to IME)
|
|
891
896
|
this.cursorRow = Math.max(0, newLines.length - 1);
|
|
897
|
+
this.contentCursorRow = finalCursorRow;
|
|
892
898
|
this.hardwareCursorRow = finalCursorRow;
|
|
893
899
|
// Track terminal's working area (grows but doesn't shrink unless cleared)
|
|
894
900
|
this.maxLinesRendered = Math.max(this.maxLinesRendered, newLines.length);
|
package/pkg/package.json
CHANGED
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
* Leaf module — no imports from auto/ to avoid circular dependencies.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
/** Timeout for postUnitPreVerification in runFinalize (ms). */
|
|
11
|
+
export const FINALIZE_PRE_TIMEOUT_MS = 60_000;
|
|
12
|
+
|
|
10
13
|
/** Timeout for postUnitPostVerification in runFinalize (ms). */
|
|
11
14
|
export const FINALIZE_POST_TIMEOUT_MS = 60_000;
|
|
12
15
|
|
|
@@ -46,7 +46,7 @@ export async function autoLoop(
|
|
|
46
46
|
): Promise<void> {
|
|
47
47
|
debugLog("autoLoop", { phase: "enter" });
|
|
48
48
|
let iteration = 0;
|
|
49
|
-
const loopState: LoopState = { recentUnits: [], stuckRecoveryAttempts: 0 };
|
|
49
|
+
const loopState: LoopState = { recentUnits: [], stuckRecoveryAttempts: 0, consecutiveFinalizeTimeouts: 0 };
|
|
50
50
|
let consecutiveErrors = 0;
|
|
51
51
|
const recentErrorMessages: string[] = [];
|
|
52
52
|
|
|
@@ -247,7 +247,7 @@ export async function autoLoop(
|
|
|
247
247
|
|
|
248
248
|
// ── Phase 5: Finalize ───────────────────────────────────────────────
|
|
249
249
|
|
|
250
|
-
const finalizeResult = await runFinalize(ic, iterData, sidecarItem);
|
|
250
|
+
const finalizeResult = await runFinalize(ic, iterData, loopState, sidecarItem);
|
|
251
251
|
if (finalizeResult.action === "break") break;
|
|
252
252
|
if (finalizeResult.action === "continue") continue;
|
|
253
253
|
|
|
@@ -15,6 +15,7 @@ import type { PostUnitContext, PreVerificationOpts } from "../auto-post-unit.js"
|
|
|
15
15
|
import {
|
|
16
16
|
MAX_RECOVERY_CHARS,
|
|
17
17
|
BUDGET_THRESHOLDS,
|
|
18
|
+
MAX_FINALIZE_TIMEOUTS,
|
|
18
19
|
type PhaseResult,
|
|
19
20
|
type IterationContext,
|
|
20
21
|
type LoopState,
|
|
@@ -33,7 +34,7 @@ import { gsdRoot } from "../paths.js";
|
|
|
33
34
|
import { atomicWriteSync } from "../atomic-write.js";
|
|
34
35
|
import { verifyExpectedArtifact, diagnoseExpectedArtifact, buildLoopRemediationSteps } from "../auto-recovery.js";
|
|
35
36
|
import { writeUnitRuntimeRecord } from "../unit-runtime.js";
|
|
36
|
-
import { withTimeout, FINALIZE_POST_TIMEOUT_MS } from "./finalize-timeout.js";
|
|
37
|
+
import { withTimeout, FINALIZE_PRE_TIMEOUT_MS, FINALIZE_POST_TIMEOUT_MS } from "./finalize-timeout.js";
|
|
37
38
|
import { getEligibleSlices } from "../slice-parallel-eligibility.js";
|
|
38
39
|
import { startSliceParallel } from "../slice-parallel-orchestrator.js";
|
|
39
40
|
import { isDbAvailable, getMilestoneSlices } from "../gsd-db.js";
|
|
@@ -1427,6 +1428,7 @@ export async function runUnitPhase(
|
|
|
1427
1428
|
export async function runFinalize(
|
|
1428
1429
|
ic: IterationContext,
|
|
1429
1430
|
iterData: IterationData,
|
|
1431
|
+
loopState: LoopState,
|
|
1430
1432
|
sidecarItem?: SidecarItem,
|
|
1431
1433
|
): Promise<PhaseResult> {
|
|
1432
1434
|
const { ctx, pi, s, deps } = ic;
|
|
@@ -1450,13 +1452,58 @@ export async function runFinalize(
|
|
|
1450
1452
|
};
|
|
1451
1453
|
|
|
1452
1454
|
// Pre-verification processing (commit, doctor, state rebuild, etc.)
|
|
1455
|
+
// Timeout guard: if postUnitPreVerification hangs (e.g., safety harness
|
|
1456
|
+
// deadlock, browser teardown hang, worktree sync stall), force-continue
|
|
1457
|
+
// after timeout so the auto-loop is not permanently frozen (#3757).
|
|
1458
|
+
//
|
|
1459
|
+
// On timeout, null out s.currentUnit so the timed-out task's late async
|
|
1460
|
+
// mutations are harmless — postUnitPreVerification guards all side effects
|
|
1461
|
+
// behind `if (s.currentUnit)`. The next iteration sets a fresh currentUnit.
|
|
1453
1462
|
// Sidecar items use lightweight pre-verification opts
|
|
1454
1463
|
const preVerificationOpts: PreVerificationOpts | undefined = sidecarItem
|
|
1455
1464
|
? sidecarItem.kind === "hook"
|
|
1456
1465
|
? { skipSettleDelay: true, skipWorktreeSync: true }
|
|
1457
1466
|
: { skipSettleDelay: true }
|
|
1458
1467
|
: undefined;
|
|
1459
|
-
const
|
|
1468
|
+
const preUnitSnapshot = s.currentUnit
|
|
1469
|
+
? { type: s.currentUnit.type, id: s.currentUnit.id, startedAt: s.currentUnit.startedAt }
|
|
1470
|
+
: null;
|
|
1471
|
+
const preResultGuard = await withTimeout(
|
|
1472
|
+
deps.postUnitPreVerification(postUnitCtx, preVerificationOpts),
|
|
1473
|
+
FINALIZE_PRE_TIMEOUT_MS,
|
|
1474
|
+
"postUnitPreVerification",
|
|
1475
|
+
);
|
|
1476
|
+
|
|
1477
|
+
if (preResultGuard.timedOut) {
|
|
1478
|
+
// Detach session from the timed-out unit so late async completions
|
|
1479
|
+
// cannot mutate state for the next unit (#3757).
|
|
1480
|
+
s.currentUnit = null;
|
|
1481
|
+
loopState.consecutiveFinalizeTimeouts++;
|
|
1482
|
+
debugLog("autoLoop", {
|
|
1483
|
+
phase: "pre-verification-timeout",
|
|
1484
|
+
iteration: ic.iteration,
|
|
1485
|
+
unitType: iterData.unitType,
|
|
1486
|
+
unitId: iterData.unitId,
|
|
1487
|
+
consecutiveTimeouts: loopState.consecutiveFinalizeTimeouts,
|
|
1488
|
+
});
|
|
1489
|
+
|
|
1490
|
+
if (loopState.consecutiveFinalizeTimeouts >= MAX_FINALIZE_TIMEOUTS) {
|
|
1491
|
+
ctx.ui.notify(
|
|
1492
|
+
`postUnitPreVerification timed out ${loopState.consecutiveFinalizeTimeouts} consecutive times — stopping auto-mode to prevent budget waste`,
|
|
1493
|
+
"error",
|
|
1494
|
+
);
|
|
1495
|
+
await deps.stopAuto(ctx, pi, `${loopState.consecutiveFinalizeTimeouts} consecutive finalize timeouts`);
|
|
1496
|
+
return { action: "break", reason: "finalize-timeout-escalation" };
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
ctx.ui.notify(
|
|
1500
|
+
`postUnitPreVerification timed out after ${FINALIZE_PRE_TIMEOUT_MS / 1000}s for ${iterData.unitType} ${iterData.unitId} (${loopState.consecutiveFinalizeTimeouts}/${MAX_FINALIZE_TIMEOUTS}) — continuing to next iteration`,
|
|
1501
|
+
"warning",
|
|
1502
|
+
);
|
|
1503
|
+
return { action: "next", data: undefined as void };
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
const preResult = preResultGuard.value;
|
|
1460
1507
|
if (preResult === "dispatched") {
|
|
1461
1508
|
debugLog("autoLoop", {
|
|
1462
1509
|
phase: "exit",
|
|
@@ -1525,14 +1572,29 @@ export async function runFinalize(
|
|
|
1525
1572
|
);
|
|
1526
1573
|
|
|
1527
1574
|
if (postResultGuard.timedOut) {
|
|
1575
|
+
// Detach session from the timed-out unit so late async completions
|
|
1576
|
+
// cannot mutate state for the next unit (#3757).
|
|
1577
|
+
s.currentUnit = null;
|
|
1578
|
+
loopState.consecutiveFinalizeTimeouts++;
|
|
1528
1579
|
debugLog("autoLoop", {
|
|
1529
1580
|
phase: "post-verification-timeout",
|
|
1530
1581
|
iteration: ic.iteration,
|
|
1531
1582
|
unitType: iterData.unitType,
|
|
1532
1583
|
unitId: iterData.unitId,
|
|
1584
|
+
consecutiveTimeouts: loopState.consecutiveFinalizeTimeouts,
|
|
1533
1585
|
});
|
|
1586
|
+
|
|
1587
|
+
if (loopState.consecutiveFinalizeTimeouts >= MAX_FINALIZE_TIMEOUTS) {
|
|
1588
|
+
ctx.ui.notify(
|
|
1589
|
+
`postUnitPostVerification timed out ${loopState.consecutiveFinalizeTimeouts} consecutive times — stopping auto-mode to prevent budget waste`,
|
|
1590
|
+
"error",
|
|
1591
|
+
);
|
|
1592
|
+
await deps.stopAuto(ctx, pi, `${loopState.consecutiveFinalizeTimeouts} consecutive finalize timeouts`);
|
|
1593
|
+
return { action: "break", reason: "finalize-timeout-escalation" };
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1534
1596
|
ctx.ui.notify(
|
|
1535
|
-
`postUnitPostVerification timed out after ${FINALIZE_POST_TIMEOUT_MS / 1000}s for ${iterData.unitType} ${iterData.unitId} — continuing to next iteration`,
|
|
1597
|
+
`postUnitPostVerification timed out after ${FINALIZE_POST_TIMEOUT_MS / 1000}s for ${iterData.unitType} ${iterData.unitId} (${loopState.consecutiveFinalizeTimeouts}/${MAX_FINALIZE_TIMEOUTS}) — continuing to next iteration`,
|
|
1536
1598
|
"warning",
|
|
1537
1599
|
);
|
|
1538
1600
|
return { action: "next", data: undefined as void };
|
|
@@ -1554,6 +1616,9 @@ export async function runFinalize(
|
|
|
1554
1616
|
return { action: "break", reason: "step-wizard" };
|
|
1555
1617
|
}
|
|
1556
1618
|
|
|
1619
|
+
// Both pre and post verification completed without timeout — reset counter
|
|
1620
|
+
loopState.consecutiveFinalizeTimeouts = 0;
|
|
1621
|
+
|
|
1557
1622
|
return { action: "next", data: undefined as void };
|
|
1558
1623
|
}
|
|
1559
1624
|
|
|
@@ -91,8 +91,13 @@ export interface IterationContext {
|
|
|
91
91
|
export interface LoopState {
|
|
92
92
|
recentUnits: Array<{ key: string; error?: string }>;
|
|
93
93
|
stuckRecoveryAttempts: number;
|
|
94
|
+
/** Consecutive finalize timeout count — stops auto-mode after threshold. */
|
|
95
|
+
consecutiveFinalizeTimeouts: number;
|
|
94
96
|
}
|
|
95
97
|
|
|
98
|
+
/** Max consecutive finalize timeouts before hard-stopping auto-mode. */
|
|
99
|
+
export const MAX_FINALIZE_TIMEOUTS = 3;
|
|
100
|
+
|
|
96
101
|
export interface PreDispatchData {
|
|
97
102
|
state: GSDState;
|
|
98
103
|
mid: string;
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
resolveSliceFile,
|
|
17
17
|
} from "./paths.js";
|
|
18
18
|
import { isDbAvailable, getMilestoneSlices, getSliceTasks } from "./gsd-db.js";
|
|
19
|
+
import { formatShortcut } from "./files.js";
|
|
19
20
|
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
20
21
|
import { execFileSync } from "node:child_process";
|
|
21
22
|
import { truncateToWidth, visibleWidth } from "@gsd/pi-tui";
|
|
@@ -855,7 +856,7 @@ export function updateProgressWidget(
|
|
|
855
856
|
// Hints line
|
|
856
857
|
const hintParts: string[] = [];
|
|
857
858
|
hintParts.push("esc pause");
|
|
858
|
-
hintParts.push(
|
|
859
|
+
hintParts.push(`${formatShortcut("Ctrl+Alt+G")} dashboard`);
|
|
859
860
|
const hintStr = theme.fg("dim", hintParts.join(" | "));
|
|
860
861
|
const commitStr = lastCommit
|
|
861
862
|
? theme.fg("dim", `${lastCommit.timeAgo} ago: ${commitMsg}`)
|
|
@@ -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
|
|