gsd-pi 2.65.0-dev.6cc5110 → 2.65.0-dev.800ece0
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/register-hooks.js +7 -2
- package/dist/resources/extensions/gsd/bootstrap/system-context.js +3 -1
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +31 -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/gsd-db.js +36 -2
- package/dist/resources/extensions/gsd/index.js +1 -1
- 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/discuss.md +2 -0
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +2 -0
- package/dist/resources/extensions/gsd/prompts/queue.md +2 -0
- package/dist/resources/extensions/gsd/prompts/system.md +2 -2
- package/dist/resources/extensions/gsd/state.js +3 -6
- package/dist/resources/extensions/gsd/workflow-events.js +1 -0
- package/dist/resources/extensions/gsd/workflow-projections.js +3 -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 +20 -20
- 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 +20 -20
- 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-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/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/register-hooks.ts +7 -2
- package/src/resources/extensions/gsd/bootstrap/system-context.ts +3 -1
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +36 -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/gsd-db.ts +33 -2
- package/src/resources/extensions/gsd/index.ts +1 -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/discuss.md +2 -0
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +2 -0
- package/src/resources/extensions/gsd/prompts/queue.md +2 -0
- package/src/resources/extensions/gsd/prompts/system.md +2 -2
- package/src/resources/extensions/gsd/state.ts +3 -6
- 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/gsd/tests/wave5-consistency-regressions.test.ts +165 -0
- package/src/resources/extensions/gsd/tests/write-gate.test.ts +127 -2
- package/src/resources/extensions/gsd/workflow-events.ts +5 -3
- package/src/resources/extensions/gsd/workflow-projections.ts +3 -2
- package/src/resources/extensions/subagent/agents.ts +30 -6
- /package/dist/web/standalone/.next/static/{iueakR5x5bQbax2sGz8Yr → E0hBt4ifuG7QBbhUR5-6U}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{iueakR5x5bQbax2sGz8Yr → E0hBt4ifuG7QBbhUR5-6U}/_ssgManifest.js +0 -0
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Leaf module — no imports from auto/ to avoid circular dependencies.
|
|
8
8
|
*/
|
|
9
|
+
/** Timeout for postUnitPreVerification in runFinalize (ms). */
|
|
10
|
+
export const FINALIZE_PRE_TIMEOUT_MS = 60_000;
|
|
9
11
|
/** Timeout for postUnitPostVerification in runFinalize (ms). */
|
|
10
12
|
export const FINALIZE_POST_TIMEOUT_MS = 60_000;
|
|
11
13
|
/**
|
|
@@ -24,7 +24,7 @@ import { resolveEngine } from "../engine-resolver.js";
|
|
|
24
24
|
export async function autoLoop(ctx, pi, s, deps) {
|
|
25
25
|
debugLog("autoLoop", { phase: "enter" });
|
|
26
26
|
let iteration = 0;
|
|
27
|
-
const loopState = { recentUnits: [], stuckRecoveryAttempts: 0 };
|
|
27
|
+
const loopState = { recentUnits: [], stuckRecoveryAttempts: 0, consecutiveFinalizeTimeouts: 0 };
|
|
28
28
|
let consecutiveErrors = 0;
|
|
29
29
|
const recentErrorMessages = [];
|
|
30
30
|
while (s.active) {
|
|
@@ -202,7 +202,7 @@ export async function autoLoop(ctx, pi, s, deps) {
|
|
|
202
202
|
if (unitPhaseResult.action === "break")
|
|
203
203
|
break;
|
|
204
204
|
// ── Phase 5: Finalize ───────────────────────────────────────────────
|
|
205
|
-
const finalizeResult = await runFinalize(ic, iterData, sidecarItem);
|
|
205
|
+
const finalizeResult = await runFinalize(ic, iterData, loopState, sidecarItem);
|
|
206
206
|
if (finalizeResult.action === "break")
|
|
207
207
|
break;
|
|
208
208
|
if (finalizeResult.action === "continue")
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* Imports from: auto/types, auto/detect-stuck, auto/run-unit, auto/loop-deps
|
|
8
8
|
*/
|
|
9
9
|
import { importExtensionModule } from "@gsd/pi-coding-agent";
|
|
10
|
-
import { MAX_RECOVERY_CHARS, BUDGET_THRESHOLDS, } from "./types.js";
|
|
10
|
+
import { MAX_RECOVERY_CHARS, BUDGET_THRESHOLDS, MAX_FINALIZE_TIMEOUTS, } from "./types.js";
|
|
11
11
|
import { detectStuck } from "./detect-stuck.js";
|
|
12
12
|
import { runUnit } from "./run-unit.js";
|
|
13
13
|
import { debugLog } from "../debug-logger.js";
|
|
@@ -20,7 +20,7 @@ import { gsdRoot } from "../paths.js";
|
|
|
20
20
|
import { atomicWriteSync } from "../atomic-write.js";
|
|
21
21
|
import { verifyExpectedArtifact, diagnoseExpectedArtifact, buildLoopRemediationSteps } from "../auto-recovery.js";
|
|
22
22
|
import { writeUnitRuntimeRecord } from "../unit-runtime.js";
|
|
23
|
-
import { withTimeout, FINALIZE_POST_TIMEOUT_MS } from "./finalize-timeout.js";
|
|
23
|
+
import { withTimeout, FINALIZE_PRE_TIMEOUT_MS, FINALIZE_POST_TIMEOUT_MS } from "./finalize-timeout.js";
|
|
24
24
|
import { getEligibleSlices } from "../slice-parallel-eligibility.js";
|
|
25
25
|
import { startSliceParallel } from "../slice-parallel-orchestrator.js";
|
|
26
26
|
import { isDbAvailable, getMilestoneSlices } from "../gsd-db.js";
|
|
@@ -1040,7 +1040,7 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
|
|
|
1040
1040
|
* Phase 5: Post-unit finalize — pre/post verification, UAT pause, step-wizard.
|
|
1041
1041
|
* Returns break/continue/next to control the outer loop.
|
|
1042
1042
|
*/
|
|
1043
|
-
export async function runFinalize(ic, iterData, sidecarItem) {
|
|
1043
|
+
export async function runFinalize(ic, iterData, loopState, sidecarItem) {
|
|
1044
1044
|
const { ctx, pi, s, deps } = ic;
|
|
1045
1045
|
const { pauseAfterUatDispatch } = iterData;
|
|
1046
1046
|
debugLog("autoLoop", { phase: "finalize", iteration: ic.iteration });
|
|
@@ -1058,13 +1058,44 @@ export async function runFinalize(ic, iterData, sidecarItem) {
|
|
|
1058
1058
|
updateProgressWidget: deps.updateProgressWidget,
|
|
1059
1059
|
};
|
|
1060
1060
|
// Pre-verification processing (commit, doctor, state rebuild, etc.)
|
|
1061
|
+
// Timeout guard: if postUnitPreVerification hangs (e.g., safety harness
|
|
1062
|
+
// deadlock, browser teardown hang, worktree sync stall), force-continue
|
|
1063
|
+
// after timeout so the auto-loop is not permanently frozen (#3757).
|
|
1064
|
+
//
|
|
1065
|
+
// On timeout, null out s.currentUnit so the timed-out task's late async
|
|
1066
|
+
// mutations are harmless — postUnitPreVerification guards all side effects
|
|
1067
|
+
// behind `if (s.currentUnit)`. The next iteration sets a fresh currentUnit.
|
|
1061
1068
|
// Sidecar items use lightweight pre-verification opts
|
|
1062
1069
|
const preVerificationOpts = sidecarItem
|
|
1063
1070
|
? sidecarItem.kind === "hook"
|
|
1064
1071
|
? { skipSettleDelay: true, skipWorktreeSync: true }
|
|
1065
1072
|
: { skipSettleDelay: true }
|
|
1066
1073
|
: undefined;
|
|
1067
|
-
const
|
|
1074
|
+
const preUnitSnapshot = s.currentUnit
|
|
1075
|
+
? { type: s.currentUnit.type, id: s.currentUnit.id, startedAt: s.currentUnit.startedAt }
|
|
1076
|
+
: null;
|
|
1077
|
+
const preResultGuard = await withTimeout(deps.postUnitPreVerification(postUnitCtx, preVerificationOpts), FINALIZE_PRE_TIMEOUT_MS, "postUnitPreVerification");
|
|
1078
|
+
if (preResultGuard.timedOut) {
|
|
1079
|
+
// Detach session from the timed-out unit so late async completions
|
|
1080
|
+
// cannot mutate state for the next unit (#3757).
|
|
1081
|
+
s.currentUnit = null;
|
|
1082
|
+
loopState.consecutiveFinalizeTimeouts++;
|
|
1083
|
+
debugLog("autoLoop", {
|
|
1084
|
+
phase: "pre-verification-timeout",
|
|
1085
|
+
iteration: ic.iteration,
|
|
1086
|
+
unitType: iterData.unitType,
|
|
1087
|
+
unitId: iterData.unitId,
|
|
1088
|
+
consecutiveTimeouts: loopState.consecutiveFinalizeTimeouts,
|
|
1089
|
+
});
|
|
1090
|
+
if (loopState.consecutiveFinalizeTimeouts >= MAX_FINALIZE_TIMEOUTS) {
|
|
1091
|
+
ctx.ui.notify(`postUnitPreVerification timed out ${loopState.consecutiveFinalizeTimeouts} consecutive times — stopping auto-mode to prevent budget waste`, "error");
|
|
1092
|
+
await deps.stopAuto(ctx, pi, `${loopState.consecutiveFinalizeTimeouts} consecutive finalize timeouts`);
|
|
1093
|
+
return { action: "break", reason: "finalize-timeout-escalation" };
|
|
1094
|
+
}
|
|
1095
|
+
ctx.ui.notify(`postUnitPreVerification timed out after ${FINALIZE_PRE_TIMEOUT_MS / 1000}s for ${iterData.unitType} ${iterData.unitId} (${loopState.consecutiveFinalizeTimeouts}/${MAX_FINALIZE_TIMEOUTS}) — continuing to next iteration`, "warning");
|
|
1096
|
+
return { action: "next", data: undefined };
|
|
1097
|
+
}
|
|
1098
|
+
const preResult = preResultGuard.value;
|
|
1068
1099
|
if (preResult === "dispatched") {
|
|
1069
1100
|
debugLog("autoLoop", {
|
|
1070
1101
|
phase: "exit",
|
|
@@ -1119,13 +1150,23 @@ export async function runFinalize(ic, iterData, sidecarItem) {
|
|
|
1119
1150
|
// auto-loop is not permanently frozen (#2344).
|
|
1120
1151
|
const postResultGuard = await withTimeout(deps.postUnitPostVerification(postUnitCtx), FINALIZE_POST_TIMEOUT_MS, "postUnitPostVerification");
|
|
1121
1152
|
if (postResultGuard.timedOut) {
|
|
1153
|
+
// Detach session from the timed-out unit so late async completions
|
|
1154
|
+
// cannot mutate state for the next unit (#3757).
|
|
1155
|
+
s.currentUnit = null;
|
|
1156
|
+
loopState.consecutiveFinalizeTimeouts++;
|
|
1122
1157
|
debugLog("autoLoop", {
|
|
1123
1158
|
phase: "post-verification-timeout",
|
|
1124
1159
|
iteration: ic.iteration,
|
|
1125
1160
|
unitType: iterData.unitType,
|
|
1126
1161
|
unitId: iterData.unitId,
|
|
1162
|
+
consecutiveTimeouts: loopState.consecutiveFinalizeTimeouts,
|
|
1127
1163
|
});
|
|
1128
|
-
|
|
1164
|
+
if (loopState.consecutiveFinalizeTimeouts >= MAX_FINALIZE_TIMEOUTS) {
|
|
1165
|
+
ctx.ui.notify(`postUnitPostVerification timed out ${loopState.consecutiveFinalizeTimeouts} consecutive times — stopping auto-mode to prevent budget waste`, "error");
|
|
1166
|
+
await deps.stopAuto(ctx, pi, `${loopState.consecutiveFinalizeTimeouts} consecutive finalize timeouts`);
|
|
1167
|
+
return { action: "break", reason: "finalize-timeout-escalation" };
|
|
1168
|
+
}
|
|
1169
|
+
ctx.ui.notify(`postUnitPostVerification timed out after ${FINALIZE_POST_TIMEOUT_MS / 1000}s for ${iterData.unitType} ${iterData.unitId} (${loopState.consecutiveFinalizeTimeouts}/${MAX_FINALIZE_TIMEOUTS}) — continuing to next iteration`, "warning");
|
|
1129
1170
|
return { action: "next", data: undefined };
|
|
1130
1171
|
}
|
|
1131
1172
|
const postResult = postResultGuard.value;
|
|
@@ -1141,5 +1182,7 @@ export async function runFinalize(ic, iterData, sidecarItem) {
|
|
|
1141
1182
|
debugLog("autoLoop", { phase: "exit", reason: "step-wizard" });
|
|
1142
1183
|
return { action: "break", reason: "step-wizard" };
|
|
1143
1184
|
}
|
|
1185
|
+
// Both pre and post verification completed without timeout — reset counter
|
|
1186
|
+
loopState.consecutiveFinalizeTimeouts = 0;
|
|
1144
1187
|
return { action: "next", data: undefined };
|
|
1145
1188
|
}
|
|
@@ -21,3 +21,5 @@ export const BUDGET_THRESHOLDS = [
|
|
|
21
21
|
{ pct: 80, label: "Approaching budget ceiling — 80%", notifyLevel: "warning", cmuxLevel: "warning" },
|
|
22
22
|
{ pct: 75, label: "Budget 75%", notifyLevel: "info", cmuxLevel: "progress" },
|
|
23
23
|
];
|
|
24
|
+
/** Max consecutive finalize timeouts before hard-stopping auto-mode. */
|
|
25
|
+
export const MAX_FINALIZE_TIMEOUTS = 3;
|
|
@@ -9,6 +9,7 @@ import { getCurrentBranch } from "./worktree.js";
|
|
|
9
9
|
import { getActiveHook } from "./post-unit-hooks.js";
|
|
10
10
|
import { getLedger, getProjectTotals } from "./metrics.js";
|
|
11
11
|
import { isDbAvailable, getMilestoneSlices, getSliceTasks } from "./gsd-db.js";
|
|
12
|
+
import { formatShortcut } from "./files.js";
|
|
12
13
|
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
13
14
|
import { execFileSync } from "node:child_process";
|
|
14
15
|
import { truncateToWidth, visibleWidth } from "@gsd/pi-tui";
|
|
@@ -725,7 +726,7 @@ export function updateProgressWidget(ctx, unitType, unitId, state, accessors, ti
|
|
|
725
726
|
// Hints line
|
|
726
727
|
const hintParts = [];
|
|
727
728
|
hintParts.push("esc pause");
|
|
728
|
-
hintParts.push(
|
|
729
|
+
hintParts.push(`${formatShortcut("Ctrl+Alt+G")} dashboard`);
|
|
729
730
|
const hintStr = theme.fg("dim", hintParts.join(" | "));
|
|
730
731
|
const commitStr = lastCommit
|
|
731
732
|
? theme.fg("dim", `${lastCommit.timeAgo} ago: ${commitMsg}`)
|
|
@@ -20,11 +20,12 @@ import { synthesizeCrashRecovery } from "./session-forensics.js";
|
|
|
20
20
|
import { writeLock, clearLock, readCrashLock, formatCrashInfo, isLockProcessAlive, } from "./crash-recovery.js";
|
|
21
21
|
import { acquireSessionLock, releaseSessionLock, updateSessionLock, } from "./session-lock.js";
|
|
22
22
|
import { ensureGitignore, untrackRuntimeFiles } from "./gitignore.js";
|
|
23
|
-
import { nativeIsRepo, nativeInit, nativeAddAll, nativeCommit, nativeGetCurrentBranch, nativeDetectMainBranch, nativeCheckoutBranch, } from "./native-git-bridge.js";
|
|
23
|
+
import { nativeIsRepo, nativeInit, nativeAddAll, nativeCommit, nativeGetCurrentBranch, nativeDetectMainBranch, nativeCheckoutBranch, nativeBranchList, nativeBranchListMerged, nativeBranchDelete, nativeWorktreeRemove, } from "./native-git-bridge.js";
|
|
24
24
|
import { GitServiceImpl } from "./git-service.js";
|
|
25
25
|
import { captureIntegrationBranch, detectWorktreeName, setActiveMilestoneId, } from "./worktree.js";
|
|
26
26
|
import { getAutoWorktreePath } from "./auto-worktree.js";
|
|
27
27
|
import { readResourceVersion, cleanStaleRuntimeUnits } from "./auto-worktree.js";
|
|
28
|
+
import { worktreePath as getWorktreeDir, isInsideWorktreesDir } from "./worktree-manager.js";
|
|
28
29
|
import { initMetrics } from "./metrics.js";
|
|
29
30
|
import { initRoutingHistory } from "./routing-history.js";
|
|
30
31
|
import { restoreHookState, resetHookState } from "./post-unit-hooks.js";
|
|
@@ -35,7 +36,7 @@ import { hideFooter } from "./auto-dashboard.js";
|
|
|
35
36
|
import { debugLog, enableDebug, isDebugEnabled, getDebugLogPath, } from "./debug-logger.js";
|
|
36
37
|
import { logWarning, logError } from "./workflow-logger.js";
|
|
37
38
|
import { parseUnitId } from "./unit-id.js";
|
|
38
|
-
import { existsSync, mkdirSync, readdirSync, statSync, unlinkSync, } from "node:fs";
|
|
39
|
+
import { existsSync, mkdirSync, readdirSync, rmSync, statSync, unlinkSync, } from "node:fs";
|
|
39
40
|
import { join } from "node:path";
|
|
40
41
|
import { sep as pathSep } from "node:path";
|
|
41
42
|
import { resolveProjectRootDbPath } from "./bootstrap/dynamic-tools.js";
|
|
@@ -62,6 +63,117 @@ export async function openProjectDbIfPresent(basePath) {
|
|
|
62
63
|
logWarning("engine", `gsd-db: failed to open existing database: ${err instanceof Error ? err.message : String(err)}`);
|
|
63
64
|
}
|
|
64
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Audit for orphaned milestone branches at bootstrap.
|
|
68
|
+
*
|
|
69
|
+
* After a milestone completes, the teardown step (merge branch → main,
|
|
70
|
+
* delete branch, remove worktree) runs as a post-completion engine step.
|
|
71
|
+
* If the session ends between completion and teardown, the branch and
|
|
72
|
+
* worktree are orphaned — the DB says "complete" so auto-mode won't
|
|
73
|
+
* re-enter the milestone, and the teardown is never retried.
|
|
74
|
+
*
|
|
75
|
+
* This audit runs on every fresh bootstrap to catch that gap:
|
|
76
|
+
* 1. Lists all local `milestone/*` branches.
|
|
77
|
+
* 2. For each, checks if the milestone's DB status is "complete".
|
|
78
|
+
* 3. If the branch is already merged into main → deletes the branch
|
|
79
|
+
* and cleans up any orphaned worktree directory (safe, no data loss).
|
|
80
|
+
* 4. If the branch is NOT merged → preserves it and warns the user
|
|
81
|
+
* so they can merge manually (data safety first).
|
|
82
|
+
*
|
|
83
|
+
* Returns a summary of actions taken for the caller to surface via notify.
|
|
84
|
+
*/
|
|
85
|
+
export function auditOrphanedMilestoneBranches(basePath, isolationMode) {
|
|
86
|
+
const recovered = [];
|
|
87
|
+
const warnings = [];
|
|
88
|
+
// Skip in none mode — no milestone branches are created
|
|
89
|
+
if (isolationMode === "none")
|
|
90
|
+
return { recovered, warnings };
|
|
91
|
+
// Skip if DB not available — can't determine completion status
|
|
92
|
+
if (!isDbAvailable())
|
|
93
|
+
return { recovered, warnings };
|
|
94
|
+
let milestoneBranches;
|
|
95
|
+
try {
|
|
96
|
+
milestoneBranches = nativeBranchList(basePath, "milestone/*");
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// git branch list failed — skip audit
|
|
100
|
+
return { recovered, warnings };
|
|
101
|
+
}
|
|
102
|
+
if (milestoneBranches.length === 0)
|
|
103
|
+
return { recovered, warnings };
|
|
104
|
+
// Detect main branch for merge-check
|
|
105
|
+
let mainBranch;
|
|
106
|
+
try {
|
|
107
|
+
mainBranch = nativeDetectMainBranch(basePath);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
mainBranch = "main";
|
|
111
|
+
}
|
|
112
|
+
// Get branches already merged into main
|
|
113
|
+
let mergedBranches;
|
|
114
|
+
try {
|
|
115
|
+
mergedBranches = new Set(nativeBranchListMerged(basePath, mainBranch, "milestone/*"));
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
mergedBranches = new Set();
|
|
119
|
+
}
|
|
120
|
+
for (const branch of milestoneBranches) {
|
|
121
|
+
const milestoneId = branch.replace(/^milestone\//, "");
|
|
122
|
+
const milestone = getMilestone(milestoneId);
|
|
123
|
+
// Only audit completed milestones
|
|
124
|
+
if (!milestone || milestone.status !== "complete")
|
|
125
|
+
continue;
|
|
126
|
+
const isMerged = mergedBranches.has(branch);
|
|
127
|
+
if (isMerged) {
|
|
128
|
+
// Branch is merged — safe to delete branch and clean up worktree dir
|
|
129
|
+
try {
|
|
130
|
+
nativeBranchDelete(basePath, branch, true);
|
|
131
|
+
recovered.push(`Deleted merged branch ${branch} for completed milestone ${milestoneId}.`);
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
warnings.push(`Failed to delete merged branch ${branch}: ${err instanceof Error ? err.message : String(err)}`);
|
|
135
|
+
}
|
|
136
|
+
// Clean up orphaned worktree directory if it exists
|
|
137
|
+
const wtDir = getWorktreeDir(basePath, milestoneId);
|
|
138
|
+
if (existsSync(wtDir)) {
|
|
139
|
+
// Try git worktree remove first (handles registered worktrees)
|
|
140
|
+
try {
|
|
141
|
+
nativeWorktreeRemove(basePath, wtDir, true);
|
|
142
|
+
}
|
|
143
|
+
catch (e) {
|
|
144
|
+
// Not a registered worktree — expected for orphaned dirs
|
|
145
|
+
logWarning("engine", `worktree remove failed (expected for orphaned dirs): ${e instanceof Error ? e.message : String(e)}`);
|
|
146
|
+
}
|
|
147
|
+
// If the directory still exists after git worktree remove (either it
|
|
148
|
+
// wasn't registered or the remove was a noop), fall back to direct
|
|
149
|
+
// filesystem removal — but only inside .gsd/worktrees/ for safety (#2365).
|
|
150
|
+
if (existsSync(wtDir)) {
|
|
151
|
+
if (isInsideWorktreesDir(basePath, wtDir)) {
|
|
152
|
+
try {
|
|
153
|
+
rmSync(wtDir, { recursive: true, force: true });
|
|
154
|
+
recovered.push(`Removed orphaned worktree directory for ${milestoneId}.`);
|
|
155
|
+
}
|
|
156
|
+
catch (err2) {
|
|
157
|
+
warnings.push(`Failed to remove worktree directory for ${milestoneId}: ${err2 instanceof Error ? err2.message : String(err2)}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
warnings.push(`Orphaned worktree directory for ${milestoneId} is outside .gsd/worktrees/ — skipping removal for safety.`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
recovered.push(`Removed orphaned worktree directory for ${milestoneId}.`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
// Branch is NOT merged — preserve for safety, warn the user
|
|
171
|
+
warnings.push(`Branch ${branch} exists for completed milestone ${milestoneId} but is NOT merged into ${mainBranch}. ` +
|
|
172
|
+
`This may contain unmerged work. Merge manually or run \`/gsd health --fix\` to resolve.`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return { recovered, warnings };
|
|
176
|
+
}
|
|
65
177
|
export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, requestedStepMode, deps) {
|
|
66
178
|
const { shouldUseWorktreeIsolation, registerSigtermHandler, lockBase, buildResolver, } = deps;
|
|
67
179
|
const lockResult = acquireSessionLock(base);
|
|
@@ -191,6 +303,26 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
|
|
|
191
303
|
// Open the project-root DB before deriveState so DB-backed state
|
|
192
304
|
// derivation (queue-order, task status) works on a cold start (#2841).
|
|
193
305
|
await openProjectDbIfPresent(base);
|
|
306
|
+
// ── Orphaned milestone branch audit ──
|
|
307
|
+
// Catches completed milestones whose teardown (merge + branch delete)
|
|
308
|
+
// was lost due to session ending between completion and teardown.
|
|
309
|
+
// Must run after DB open and before worktree entry.
|
|
310
|
+
try {
|
|
311
|
+
const auditResult = auditOrphanedMilestoneBranches(base, getIsolationMode());
|
|
312
|
+
for (const msg of auditResult.recovered) {
|
|
313
|
+
ctx.ui.notify(`Orphan audit: ${msg}`, "info");
|
|
314
|
+
}
|
|
315
|
+
for (const msg of auditResult.warnings) {
|
|
316
|
+
ctx.ui.notify(`Orphan audit: ${msg}`, "warning");
|
|
317
|
+
}
|
|
318
|
+
if (auditResult.recovered.length > 0) {
|
|
319
|
+
debugLog("orphan-audit", { recovered: auditResult.recovered, warnings: auditResult.warnings });
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
catch (err) {
|
|
323
|
+
// Non-fatal — the audit is defensive, never block bootstrap
|
|
324
|
+
logWarning("bootstrap", `orphaned milestone branch audit failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
325
|
+
}
|
|
194
326
|
let state = await deriveState(base);
|
|
195
327
|
// Stale worktree state recovery (#654)
|
|
196
328
|
if (state.activeMilestone &&
|
|
@@ -3,7 +3,7 @@ import { isToolCallEventType } from "@gsd/pi-coding-agent";
|
|
|
3
3
|
import { buildMilestoneFileName, resolveMilestonePath, resolveSliceFile, resolveSlicePath } from "../paths.js";
|
|
4
4
|
import { buildBeforeAgentStartResult } from "./system-context.js";
|
|
5
5
|
import { handleAgentEnd } from "./agent-end-recovery.js";
|
|
6
|
-
import { clearDiscussionFlowState, isDepthVerified, isQueuePhaseActive, markDepthVerified, resetWriteGateState, shouldBlockContextWrite, shouldBlockQueueExecution } from "./write-gate.js";
|
|
6
|
+
import { clearDiscussionFlowState, isDepthVerified, isDepthConfirmationAnswer, isQueuePhaseActive, markDepthVerified, resetWriteGateState, shouldBlockContextWrite, shouldBlockQueueExecution } from "./write-gate.js";
|
|
7
7
|
import { isBlockedStateFile, isBashWriteToStateFile, BLOCKED_WRITE_ERROR } from "../write-intercept.js";
|
|
8
8
|
import { cleanupQuickBranch } from "../quick.js";
|
|
9
9
|
import { getDiscussionMilestoneId } from "../guided-flow.js";
|
|
@@ -230,7 +230,12 @@ export function registerHooks(pi) {
|
|
|
230
230
|
const questions = event.input?.questions ?? [];
|
|
231
231
|
for (const question of questions) {
|
|
232
232
|
if (typeof question.id === "string" && question.id.includes("depth_verification")) {
|
|
233
|
-
|
|
233
|
+
// Only unlock the gate if the user selected the first option (confirmation).
|
|
234
|
+
// Cross-references against the question's defined options to reject free-form "Other" text.
|
|
235
|
+
const answer = details.response?.answers?.[question.id];
|
|
236
|
+
if (isDepthConfirmationAnswer(answer?.selected, question.options)) {
|
|
237
|
+
markDepthVerified();
|
|
238
|
+
}
|
|
234
239
|
break;
|
|
235
240
|
}
|
|
236
241
|
}
|
|
@@ -12,7 +12,7 @@ import { hasSkillSnapshot, detectNewSkills, formatSkillsXml } from "../skill-dis
|
|
|
12
12
|
import { getActiveAutoWorktreeContext } from "../auto-worktree.js";
|
|
13
13
|
import { getActiveWorktreeName, getWorktreeOriginalCwd } from "../worktree-command.js";
|
|
14
14
|
import { deriveState } from "../state.js";
|
|
15
|
-
import { formatOverridesSection, loadActiveOverrides, loadFile, parseContinue, parseSummary } from "../files.js";
|
|
15
|
+
import { formatOverridesSection, formatShortcut, loadActiveOverrides, loadFile, parseContinue, parseSummary } from "../files.js";
|
|
16
16
|
import { toPosixPath } from "../../shared/mod.js";
|
|
17
17
|
import { markCmuxPromptShown, shouldPromptToEnableCmux } from "../../cmux/index.js";
|
|
18
18
|
const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
|
|
@@ -60,6 +60,8 @@ export async function buildBeforeAgentStartResult(event, ctx) {
|
|
|
60
60
|
const systemContent = loadPrompt("system", {
|
|
61
61
|
bundledSkillsTable: buildBundledSkillsTable(),
|
|
62
62
|
templatesDir: getTemplatesDir(),
|
|
63
|
+
shortcutDashboard: formatShortcut("Ctrl+Alt+G"),
|
|
64
|
+
shortcutShell: formatShortcut("Ctrl+Alt+B"),
|
|
63
65
|
});
|
|
64
66
|
const loadedPreferences = loadEffectiveGSDPreferences();
|
|
65
67
|
if (shouldPromptToEnableCmux(loadedPreferences?.preferences)) {
|
|
@@ -43,6 +43,30 @@ export function clearDiscussionFlowState() {
|
|
|
43
43
|
export function markDepthVerified() {
|
|
44
44
|
depthVerificationDone = true;
|
|
45
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Check whether a depth_verification answer confirms the discussion is complete.
|
|
48
|
+
* Uses structural validation: the selected answer must exactly match the first
|
|
49
|
+
* option label from the question definition (the confirmation option by convention).
|
|
50
|
+
* This rejects free-form "Other" text, decline options, and garbage input without
|
|
51
|
+
* coupling to any specific label substring.
|
|
52
|
+
*
|
|
53
|
+
* @param selected The answer's selected value from details.response.answers[id].selected
|
|
54
|
+
* @param options The question's options array from event.input.questions[n].options
|
|
55
|
+
*/
|
|
56
|
+
export function isDepthConfirmationAnswer(selected, options) {
|
|
57
|
+
const value = Array.isArray(selected) ? selected[0] : selected;
|
|
58
|
+
if (typeof value !== "string" || !value)
|
|
59
|
+
return false;
|
|
60
|
+
// If options are available, structurally validate: selected must exactly match
|
|
61
|
+
// the first option (confirmation) label. Rejects free-form "Other" and decline options.
|
|
62
|
+
if (Array.isArray(options) && options.length > 0) {
|
|
63
|
+
const confirmLabel = options[0]?.label;
|
|
64
|
+
return typeof confirmLabel === "string" && value === confirmLabel;
|
|
65
|
+
}
|
|
66
|
+
// Fallback when options aren't available (e.g., older call sites):
|
|
67
|
+
// accept only if it contains "(Recommended)" — the prompt convention suffix.
|
|
68
|
+
return value.includes("(Recommended)");
|
|
69
|
+
}
|
|
46
70
|
export function shouldBlockContextWrite(toolName, inputPath, milestoneId, depthVerified, queuePhaseActive) {
|
|
47
71
|
if (toolName !== "write")
|
|
48
72
|
return { block: false };
|
|
@@ -56,7 +80,13 @@ export function shouldBlockContextWrite(toolName, inputPath, milestoneId, depthV
|
|
|
56
80
|
return { block: false };
|
|
57
81
|
return {
|
|
58
82
|
block: true,
|
|
59
|
-
reason:
|
|
83
|
+
reason: [
|
|
84
|
+
`HARD BLOCK: Cannot write to milestone CONTEXT.md without depth verification.`,
|
|
85
|
+
`This is a mechanical gate — you MUST NOT proceed, retry, or rationalize past this block.`,
|
|
86
|
+
`Required action: call ask_user_questions with question id containing "depth_verification".`,
|
|
87
|
+
`The user MUST select the "(Recommended)" confirmation option to unlock this gate.`,
|
|
88
|
+
`If the user declines, cancels, or the tool fails, you must re-ask — not bypass.`,
|
|
89
|
+
].join(" "),
|
|
60
90
|
};
|
|
61
91
|
}
|
|
62
92
|
/**
|
|
@@ -5,6 +5,7 @@ import { runEnvironmentChecks } from "../../doctor-environment.js";
|
|
|
5
5
|
import { deriveState } from "../../state.js";
|
|
6
6
|
import { handleCmux } from "../../commands-cmux.js";
|
|
7
7
|
import { projectRoot } from "../context.js";
|
|
8
|
+
import { formatShortcut } from "../../files.js";
|
|
8
9
|
export function showHelp(ctx) {
|
|
9
10
|
const lines = [
|
|
10
11
|
"GSD — Get Shit Done\n",
|
|
@@ -20,12 +21,12 @@ export function showHelp(ctx) {
|
|
|
20
21
|
" /gsd new-milestone Create milestone from headless context (used by gsd headless)",
|
|
21
22
|
"",
|
|
22
23
|
"VISIBILITY",
|
|
23
|
-
|
|
24
|
+
` /gsd status Show progress dashboard (${formatShortcut("Ctrl+Alt+G")})`,
|
|
24
25
|
" /gsd visualize Interactive 10-tab TUI (progress, timeline, deps, metrics, health, agent, changes, knowledge, captures, export)",
|
|
25
26
|
" /gsd queue Show queued/dispatched units and execution order",
|
|
26
27
|
" /gsd history View execution history [--cost] [--phase] [--model] [N]",
|
|
27
28
|
" /gsd changelog Show categorized release notes [version]",
|
|
28
|
-
|
|
29
|
+
` /gsd notifications View persistent notification history [clear|tail|filter] (${formatShortcut("Ctrl+Alt+N")})`,
|
|
29
30
|
"",
|
|
30
31
|
"COURSE CORRECTION",
|
|
31
32
|
" /gsd steer <desc> Apply user override to active work",
|
|
@@ -52,6 +52,23 @@ export function clearParseCache() {
|
|
|
52
52
|
for (const cb of _cacheClearCallbacks)
|
|
53
53
|
cb();
|
|
54
54
|
}
|
|
55
|
+
// ─── Platform shortcuts ───────────────────────────────────────────────────
|
|
56
|
+
const IS_MAC = process.platform === "darwin";
|
|
57
|
+
/**
|
|
58
|
+
* Format a keyboard shortcut for the current OS.
|
|
59
|
+
* Input: modifier key combo like "Ctrl+Alt+G"
|
|
60
|
+
* Output: "⌃⌥G" on macOS, "Ctrl+Alt+G" on Windows/Linux.
|
|
61
|
+
*/
|
|
62
|
+
export function formatShortcut(combo) {
|
|
63
|
+
if (!IS_MAC)
|
|
64
|
+
return combo;
|
|
65
|
+
return combo
|
|
66
|
+
.replace(/Ctrl\+Alt\+/i, "⌃⌥")
|
|
67
|
+
.replace(/Ctrl\+/i, "⌃")
|
|
68
|
+
.replace(/Alt\+/i, "⌥")
|
|
69
|
+
.replace(/Shift\+/i, "⇧")
|
|
70
|
+
.replace(/Cmd\+/i, "⌘");
|
|
71
|
+
}
|
|
55
72
|
// ─── Helpers ───────────────────────────────────────────────────────────────
|
|
56
73
|
/** Extract the text after a heading at a given level, up to the next heading of same or higher level. */
|
|
57
74
|
export function extractSection(body, heading, level = 2) {
|
|
@@ -391,6 +391,28 @@ function migrateSchema(db) {
|
|
|
391
391
|
const currentVersion = row ? row["v"] : 0;
|
|
392
392
|
if (currentVersion >= SCHEMA_VERSION)
|
|
393
393
|
return;
|
|
394
|
+
// Backup database before migration so a mid-migration crash doesn't
|
|
395
|
+
// leave a partially-migrated DB with no recovery path.
|
|
396
|
+
// WAL-safe: checkpoint first to flush WAL into the main DB file, then copy.
|
|
397
|
+
if (currentPath && currentPath !== ":memory:" && existsSync(currentPath)) {
|
|
398
|
+
try {
|
|
399
|
+
const backupPath = `${currentPath}.backup-v${currentVersion}`;
|
|
400
|
+
if (!existsSync(backupPath)) {
|
|
401
|
+
// Flush WAL to main DB file before copying — without this, the backup
|
|
402
|
+
// may be missing committed data that only exists in the -wal file.
|
|
403
|
+
try {
|
|
404
|
+
db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
|
|
405
|
+
}
|
|
406
|
+
catch { /* checkpoint is best-effort */ }
|
|
407
|
+
copyFileSync(currentPath, backupPath);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
catch (backupErr) {
|
|
411
|
+
// Log but proceed — blocking migration leaves the DB stuck at an old
|
|
412
|
+
// schema version permanently on read-only or full filesystems.
|
|
413
|
+
logWarning("db", `Pre-migration backup failed: ${backupErr instanceof Error ? backupErr.message : String(backupErr)}`);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
394
416
|
db.exec("BEGIN");
|
|
395
417
|
try {
|
|
396
418
|
if (currentVersion < 2) {
|
|
@@ -937,8 +959,20 @@ export function _resetProvider() {
|
|
|
937
959
|
export function upsertDecision(d) {
|
|
938
960
|
if (!currentDb)
|
|
939
961
|
throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
|
|
940
|
-
|
|
941
|
-
|
|
962
|
+
// Use ON CONFLICT DO UPDATE instead of INSERT OR REPLACE to preserve the
|
|
963
|
+
// seq column. INSERT OR REPLACE deletes then reinserts, resetting seq and
|
|
964
|
+
// corrupting decision ordering in DECISIONS.md after reconcile replay.
|
|
965
|
+
currentDb.prepare(`INSERT INTO decisions (id, when_context, scope, decision, choice, rationale, revisable, made_by, superseded_by)
|
|
966
|
+
VALUES (:id, :when_context, :scope, :decision, :choice, :rationale, :revisable, :made_by, :superseded_by)
|
|
967
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
968
|
+
when_context = excluded.when_context,
|
|
969
|
+
scope = excluded.scope,
|
|
970
|
+
decision = excluded.decision,
|
|
971
|
+
choice = excluded.choice,
|
|
972
|
+
rationale = excluded.rationale,
|
|
973
|
+
revisable = excluded.revisable,
|
|
974
|
+
made_by = excluded.made_by,
|
|
975
|
+
superseded_by = excluded.superseded_by`).run({
|
|
942
976
|
":id": d.id,
|
|
943
977
|
":when_context": d.when_context,
|
|
944
978
|
":scope": d.scope,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { isDepthVerified, isQueuePhaseActive, setQueuePhaseActive, shouldBlockContextWrite, shouldBlockQueueExecution, } from "./bootstrap/write-gate.js";
|
|
1
|
+
export { isDepthConfirmationAnswer, isDepthVerified, isQueuePhaseActive, setQueuePhaseActive, shouldBlockContextWrite, shouldBlockQueueExecution, } from "./bootstrap/write-gate.js";
|
|
2
2
|
export default async function registerExtension(pi) {
|
|
3
3
|
const { registerGsdExtension } = await import("./bootstrap/register-extension.js");
|
|
4
4
|
registerGsdExtension(pi);
|
|
@@ -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
|
import { truncateToWidth, visibleWidth, matchesKey, Key } from "@gsd/pi-tui";
|
|
5
5
|
import { readNotifications, markAllRead, clearNotifications, } from "./notification-store.js";
|
|
6
6
|
import { padRight, joinColumns } from "../shared/mod.js";
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// the most recent notification message. Refreshes every 5 seconds.
|
|
4
4
|
// Widget key: "gsd-notifications", placement: "belowEditor"
|
|
5
5
|
import { getUnreadCount, readNotifications } from "./notification-store.js";
|
|
6
|
+
import { formatShortcut } from "./files.js";
|
|
6
7
|
// ─── Pure rendering ──���────────────────────────���─────────────────────────
|
|
7
8
|
export function buildNotificationWidgetLines() {
|
|
8
9
|
const unread = getUnreadCount();
|
|
@@ -18,7 +19,7 @@ export function buildNotificationWidgetLines() {
|
|
|
18
19
|
const truncated = latest.message.length > msgMax
|
|
19
20
|
? latest.message.slice(0, msgMax - 1) + "…"
|
|
20
21
|
: latest.message;
|
|
21
|
-
return [` ${icon} [${badge}] ${truncated} (Ctrl+Alt+N to view)`];
|
|
22
|
+
return [` ${icon} [${badge}] ${truncated} (${formatShortcut("Ctrl+Alt+N")} to view)`];
|
|
22
23
|
}
|
|
23
24
|
// ─── Widget init ────────────────────────────────────────────────────────
|
|
24
25
|
const REFRESH_INTERVAL_MS = 5_000;
|
|
@@ -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
|
*/
|
|
@@ -197,8 +197,7 @@ export async function checkPackageExistence(tasks, _basePath) {
|
|
|
197
197
|
export function normalizeFilePath(filePath) {
|
|
198
198
|
if (!filePath)
|
|
199
199
|
return filePath;
|
|
200
|
-
|
|
201
|
-
let normalized = filePath.replace(/`/g, "");
|
|
200
|
+
let normalized = extractPathFromAnnotation(filePath);
|
|
202
201
|
// Normalize path separators to forward slashes
|
|
203
202
|
normalized = normalized.replace(/\\/g, "/");
|
|
204
203
|
// Remove leading ./
|
|
@@ -213,6 +212,21 @@ export function normalizeFilePath(filePath) {
|
|
|
213
212
|
}
|
|
214
213
|
return normalized;
|
|
215
214
|
}
|
|
215
|
+
function extractPathFromAnnotation(raw) {
|
|
216
|
+
const trimmed = raw.trim();
|
|
217
|
+
if (!trimmed)
|
|
218
|
+
return trimmed;
|
|
219
|
+
const backtickMatch = trimmed.match(/^`([^`]+)`(?:\s+[—–-]\s+.*)?$/);
|
|
220
|
+
if (backtickMatch) {
|
|
221
|
+
return backtickMatch[1].trim();
|
|
222
|
+
}
|
|
223
|
+
const annotatedMatch = trimmed.match(/^(.+?)\s+[—–-]\s+.+$/);
|
|
224
|
+
if (annotatedMatch) {
|
|
225
|
+
return annotatedMatch[1].trim();
|
|
226
|
+
}
|
|
227
|
+
// Fall back to the original behavior for already-plain paths.
|
|
228
|
+
return trimmed.replace(/`/g, "");
|
|
229
|
+
}
|
|
216
230
|
/**
|
|
217
231
|
* Build a set of files that will be created by tasks up to (but not including) taskIndex.
|
|
218
232
|
* All paths are normalized for consistent comparison.
|
|
@@ -114,6 +114,8 @@ If they clarify, absorb the correction and re-verify.
|
|
|
114
114
|
|
|
115
115
|
The depth verification is the required write-gate. Do **not** add another meta "ready to proceed?" checkpoint immediately after it unless there is still material ambiguity.
|
|
116
116
|
|
|
117
|
+
**CRITICAL — Non-bypassable gate:** The system mechanically blocks CONTEXT.md writes until the user selects the "(Recommended)" option. If the user declines, cancels, or the tool fails, you MUST re-ask — never rationalize past the block ("tool not responding, I'll proceed" is forbidden). The gate exists to protect the user's work; treat a block as an instruction, not an obstacle to work around.
|
|
118
|
+
|
|
117
119
|
## Wrap-up Gate
|
|
118
120
|
|
|
119
121
|
Once the depth checklist is fully satisfied, move directly into requirements and roadmap preview. Do not insert a separate "are you ready to continue?" gate unless the user explicitly wants to keep brainstorming or you still see material ambiguity.
|
|
@@ -100,6 +100,8 @@ If they clarify, absorb the correction and re-verify.
|
|
|
100
100
|
|
|
101
101
|
The depth verification is the only required confirmation gate. Do not add a second "ready to proceed?" gate after it.
|
|
102
102
|
|
|
103
|
+
**CRITICAL — Non-bypassable gate:** The system mechanically blocks CONTEXT.md writes until the user selects the "(Recommended)" option. If the user declines, cancels, or the tool fails, you MUST re-ask — never rationalize past the block ("tool not responding, I'll proceed" is forbidden). The gate exists to protect the user's work; treat a block as an instruction, not an obstacle to work around.
|
|
104
|
+
|
|
103
105
|
---
|
|
104
106
|
|
|
105
107
|
## Output
|
|
@@ -103,6 +103,8 @@ The user confirms or corrects before you write. One depth verification per miles
|
|
|
103
103
|
|
|
104
104
|
**If you skip this step, the system will block the CONTEXT.md write and return an error telling you to complete verification first.**
|
|
105
105
|
|
|
106
|
+
**CRITICAL — Non-bypassable gate:** The system mechanically blocks CONTEXT.md writes until the user selects the "(Recommended)" option. If the user declines, cancels, or the tool fails, you MUST re-ask — never rationalize past the block ("tool not responding, I'll proceed" is forbidden). The gate exists to protect the user's work; treat a block as an instruction, not an obstacle to work around.
|
|
107
|
+
|
|
106
108
|
## Output Phase
|
|
107
109
|
|
|
108
110
|
Once the user is satisfied, in a single pass for **each** new milestone:
|
|
@@ -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
|
|