From eb2939760fa09e4ab0ad353871e6a278336db5e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=82CHES?= Date: Thu, 19 Mar 2026 17:59:10 -0600 Subject: [PATCH] fix: prevent bare /gsd from stealing session lock from running auto-mode (#1507) (#1517) Bare /gsd and /gsd next now check for a remote auto-mode session via readSessionLockData before attempting to start step-mode. If another process holds the lock, a steering menu is shown instead of competing for the lock and killing the running session. Also fixes the guided-flow "all slices discussed" message to detect active auto-mode and direct users to /gsd status instead of bare /gsd. Closes #1507 Co-authored-by: Claude Opus 4.6 (1M context) --- src/resources/extensions/gsd/commands.ts | 37 ++++++++++++++++++++- src/resources/extensions/gsd/guided-flow.ts | 8 ++++- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/resources/extensions/gsd/commands.ts b/src/resources/extensions/gsd/commands.ts index adbb47e3d..be318cef8 100644 --- a/src/resources/extensions/gsd/commands.ts +++ b/src/resources/extensions/gsd/commands.ts @@ -48,6 +48,7 @@ import { computeProgressScore, formatProgressLine } from "./progress-score.js"; import { runEnvironmentChecks } from "./doctor-environment.js"; import { handleLogs } from "./commands-logs.js"; import { handleStart, handleTemplates, getTemplateCompletions } from "./commands-workflow-templates.js"; +import { readSessionLockData, isSessionLockProcessAlive } from "./session-lock.js"; /** Resolve the effective project root, accounting for worktree paths. */ @@ -69,6 +70,39 @@ export function projectRoot(): string { return root; } +/** + * Check if another process holds the auto-mode session lock. + * Returns the lock data if a remote session is alive, null otherwise. + */ +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 }; +} + +/** + * 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; +} + export function registerGSDCommand(pi: ExtensionAPI): void { pi.registerCommand("gsd", { description: "GSD — Get Shit Done: /gsd help|start|templates|next|auto|stop|pause|status|visualize|queue|quick|capture|triage|dispatch|history|undo|skip|export|cleanup|mode|prefs|config|keys|hooks|run-hook|skill-health|doctor|forensics|changelog|migrate|remote|steer|knowledge|new-milestone|parallel|update", @@ -512,6 +546,7 @@ 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()); @@ -906,7 +941,7 @@ Examples: } if (trimmed === "") { - // Bare /gsd defaults to step mode + if (notifyRemoteAutoActive(ctx, projectRoot())) return; await startAuto(ctx, pi, projectRoot(), false, { step: true }); return; } diff --git a/src/resources/extensions/gsd/guided-flow.ts b/src/resources/extensions/gsd/guided-flow.ts index 96faf73e4..e624540d0 100644 --- a/src/resources/extensions/gsd/guided-flow.ts +++ b/src/resources/extensions/gsd/guided-flow.ts @@ -23,6 +23,7 @@ import { } from "./paths.js"; import { join } from "node:path"; import { readFileSync, existsSync, mkdirSync, readdirSync, rmSync, unlinkSync } from "node:fs"; +import { readSessionLockData, isSessionLockProcessAlive } from "./session-lock.js"; import { nativeIsRepo, nativeInit } from "./native-git-bridge.js"; import { ensureGitignore, ensurePreferences, untrackRuntimeFiles } from "./gitignore.js"; import { loadEffectiveGSDPreferences } from "./preferences.js"; @@ -516,8 +517,13 @@ export async function showDiscuss( // If all pending slices are discussed, notify and exit instead of looping const allDiscussed = pendingSlices.every(s => discussedMap.get(s.id)); if (allDiscussed) { + const lockData = readSessionLockData(basePath); + const remoteAutoRunning = lockData && lockData.pid !== process.pid && isSessionLockProcessAlive(lockData); + const nextStep = remoteAutoRunning + ? "Auto-mode is already running — use /gsd status to check progress." + : "Run /gsd to start planning."; ctx.ui.notify( - `All ${pendingSlices.length} slices discussed. Run /gsd to start planning.`, + `All ${pendingSlices.length} slices discussed. ${nextStep}`, "info", ); return;