Replace the simple notifyRemoteAutoActive notification with an interactive guardRemoteSession menu that shows session details and offers actionable choices (view status, steer, stop, or force start). Guards all auto-mode entry points: bare /gsd, /gsd next, and /gsd auto. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2a87687483
commit
2822a2954f
2 changed files with 117 additions and 31 deletions
|
|
@ -418,6 +418,38 @@ export function stopAutoRemote(projectRoot: string): {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a remote auto-mode session is running (from a different process).
|
||||
* Reads the crash lock, checks PID liveness, and returns session details.
|
||||
* Used by the guard in commands.ts to prevent bare /gsd, /gsd next, and
|
||||
* /gsd auto from stealing the session lock.
|
||||
*/
|
||||
export function checkRemoteAutoSession(projectRoot: string): {
|
||||
running: boolean;
|
||||
pid?: number;
|
||||
unitType?: string;
|
||||
unitId?: string;
|
||||
startedAt?: string;
|
||||
completedUnits?: number;
|
||||
} {
|
||||
const lock = readCrashLock(projectRoot);
|
||||
if (!lock) return { running: false };
|
||||
|
||||
if (!isLockProcessAlive(lock)) {
|
||||
// Stale lock from a dead process — not a live remote session
|
||||
return { running: false };
|
||||
}
|
||||
|
||||
return {
|
||||
running: true,
|
||||
pid: lock.pid,
|
||||
unitType: lock.unitType,
|
||||
unitId: lock.unitId,
|
||||
startedAt: lock.startedAt,
|
||||
completedUnits: lock.completedUnits,
|
||||
};
|
||||
}
|
||||
|
||||
export function isStepMode(): boolean {
|
||||
return s.stepMode;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import { deriveState } from "./state.js";
|
|||
import { GSDDashboardOverlay } from "./dashboard-overlay.js";
|
||||
import { GSDVisualizerOverlay } from "./visualizer-overlay.js";
|
||||
import { showQueue, showDiscuss, showHeadlessMilestoneCreation } from "./guided-flow.js";
|
||||
import { startAuto, stopAuto, pauseAuto, isAutoActive, isAutoPaused, isStepMode, stopAutoRemote } from "./auto.js";
|
||||
import { startAuto, stopAuto, pauseAuto, isAutoActive, isAutoPaused, isStepMode, stopAutoRemote, checkRemoteAutoSession } from "./auto.js";
|
||||
import { dispatchDirectPhase } from "./auto-direct-dispatch.js";
|
||||
import { resolveProjectRoot } from "./worktree.js";
|
||||
import { assertSafeDirectory } from "./validate-directory.js";
|
||||
|
|
@ -50,6 +50,7 @@ import { handleLogs } from "./commands-logs.js";
|
|||
import { handleStart, handleTemplates, getTemplateCompletions } from "./commands-workflow-templates.js";
|
||||
import { readSessionLockData, isSessionLockProcessAlive } from "./session-lock.js";
|
||||
import { handleCmux } from "./commands-cmux.js";
|
||||
import { showNextAction } from "../shared/mod.js";
|
||||
|
||||
|
||||
/** Resolve the effective project root, accounting for worktree paths. */
|
||||
|
|
@ -72,36 +73,88 @@ export function projectRoot(): string {
|
|||
}
|
||||
|
||||
/**
|
||||
* Check if another process holds the auto-mode session lock.
|
||||
* Returns the lock data if a remote session is alive, null otherwise.
|
||||
* Guard against starting auto-mode when a remote session is already running.
|
||||
* Returns true if the caller should proceed with startAuto, false if handled.
|
||||
*/
|
||||
function getRemoteAutoSession(basePath: string): { pid: number } | null {
|
||||
const lockData = readSessionLockData(basePath);
|
||||
if (!lockData) return null;
|
||||
if (lockData.pid === process.pid) return null;
|
||||
if (!isSessionLockProcessAlive(lockData)) return null;
|
||||
return { pid: lockData.pid };
|
||||
}
|
||||
async function guardRemoteSession(
|
||||
ctx: ExtensionCommandContext,
|
||||
pi: ExtensionAPI,
|
||||
): Promise<boolean> {
|
||||
// Local session already active — proceed (startAuto handles re-entrant calls)
|
||||
if (isAutoActive() || isAutoPaused()) return true;
|
||||
|
||||
/**
|
||||
* Show a steering menu when auto-mode is running in another process.
|
||||
* Returns true if a remote session was detected (caller should return early).
|
||||
*/
|
||||
function notifyRemoteAutoActive(ctx: ExtensionCommandContext, basePath: string): boolean {
|
||||
const remote = getRemoteAutoSession(basePath);
|
||||
if (!remote) return false;
|
||||
ctx.ui.notify(
|
||||
`Auto-mode is running in another process (PID ${remote.pid}).\n` +
|
||||
`Use these commands to interact with it:\n` +
|
||||
` /gsd status — check progress\n` +
|
||||
` /gsd discuss — discuss architecture decisions\n` +
|
||||
` /gsd queue — queue the next milestone\n` +
|
||||
` /gsd steer — apply an override to active work\n` +
|
||||
` /gsd capture — fire-and-forget thought\n` +
|
||||
` /gsd stop — stop auto-mode`,
|
||||
"warning",
|
||||
);
|
||||
return true;
|
||||
const remote = checkRemoteAutoSession(projectRoot());
|
||||
if (!remote.running || !remote.pid) return true;
|
||||
|
||||
const unitLabel = remote.unitType && remote.unitId
|
||||
? `${remote.unitType} (${remote.unitId})`
|
||||
: "unknown unit";
|
||||
const unitsMsg = remote.completedUnits != null
|
||||
? `${remote.completedUnits} units completed`
|
||||
: "";
|
||||
|
||||
const choice = await showNextAction(ctx, {
|
||||
title: `Auto-mode is running in another terminal (PID ${remote.pid})`,
|
||||
summary: [
|
||||
`Currently executing: ${unitLabel}`,
|
||||
...(unitsMsg ? [unitsMsg] : []),
|
||||
...(remote.startedAt ? [`Started: ${remote.startedAt}`] : []),
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
id: "status",
|
||||
label: "View status",
|
||||
description: "Show the current GSD progress dashboard.",
|
||||
recommended: true,
|
||||
},
|
||||
{
|
||||
id: "steer",
|
||||
label: "Steer the session",
|
||||
description: "Use /gsd steer <instruction> to redirect the running session.",
|
||||
},
|
||||
{
|
||||
id: "stop",
|
||||
label: "Stop remote session",
|
||||
description: `Send SIGTERM to PID ${remote.pid} to stop it gracefully.`,
|
||||
},
|
||||
{
|
||||
id: "force",
|
||||
label: "Force start (steal lock)",
|
||||
description: "Start a new session, terminating the existing one.",
|
||||
},
|
||||
],
|
||||
notYetMessage: "Run /gsd when ready.",
|
||||
});
|
||||
|
||||
if (choice === "status") {
|
||||
await handleStatus(ctx);
|
||||
return false;
|
||||
}
|
||||
if (choice === "steer") {
|
||||
ctx.ui.notify(
|
||||
"Use /gsd steer <instruction> to redirect the running auto-mode session.\n" +
|
||||
"Example: /gsd steer Use Postgres instead of SQLite",
|
||||
"info",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (choice === "stop") {
|
||||
const result = stopAutoRemote(projectRoot());
|
||||
if (result.found) {
|
||||
ctx.ui.notify(`Sent stop signal to auto-mode session (PID ${result.pid}). It will shut down gracefully.`, "info");
|
||||
} else if (result.error) {
|
||||
ctx.ui.notify(`Failed to stop remote auto-mode: ${result.error}`, "error");
|
||||
} else {
|
||||
ctx.ui.notify("Remote session is no longer running.", "info");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (choice === "force") {
|
||||
return true; // Proceed — startAuto will steal the lock
|
||||
}
|
||||
|
||||
// "not_yet" or escape
|
||||
return false;
|
||||
}
|
||||
|
||||
export function registerGSDCommand(pi: ExtensionAPI): void {
|
||||
|
|
@ -598,10 +651,10 @@ export async function handleGSDCommand(
|
|||
await handleDryRun(ctx, projectRoot());
|
||||
return;
|
||||
}
|
||||
if (notifyRemoteAutoActive(ctx, projectRoot())) return;
|
||||
const verboseMode = trimmed.includes("--verbose");
|
||||
const debugMode = trimmed.includes("--debug");
|
||||
if (debugMode) enableDebug(projectRoot());
|
||||
if (!(await guardRemoteSession(ctx, pi))) return;
|
||||
await startAuto(ctx, pi, projectRoot(), verboseMode, { step: true });
|
||||
return;
|
||||
}
|
||||
|
|
@ -610,6 +663,7 @@ export async function handleGSDCommand(
|
|||
const verboseMode = trimmed.includes("--verbose");
|
||||
const debugMode = trimmed.includes("--debug");
|
||||
if (debugMode) enableDebug(projectRoot());
|
||||
if (!(await guardRemoteSession(ctx, pi))) return;
|
||||
await startAuto(ctx, pi, projectRoot(), verboseMode);
|
||||
return;
|
||||
}
|
||||
|
|
@ -993,7 +1047,7 @@ Examples:
|
|||
}
|
||||
|
||||
if (trimmed === "") {
|
||||
if (notifyRemoteAutoActive(ctx, projectRoot())) return;
|
||||
if (!(await guardRemoteSession(ctx, pi))) return;
|
||||
await startAuto(ctx, pi, projectRoot(), false, { step: true });
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue