diff --git a/src/hooks/stop-continuation-guard/hook.ts b/src/hooks/stop-continuation-guard/hook.ts index 747b7a9b6..ce3ba7c0b 100644 --- a/src/hooks/stop-continuation-guard/hook.ts +++ b/src/hooks/stop-continuation-guard/hook.ts @@ -100,10 +100,16 @@ export function createStopContinuationGuardHook( }: { sessionID?: string }): Promise => { - if (sessionID && stoppedSessions.has(sessionID)) { - clear(sessionID) - log(`[${HOOK_NAME}] Cleared stop state on new user message`, { sessionID }) - } + // Intentionally no-op: stop state should persist across user messages. + // Previously this cleared the stop on any new user message, but that caused + // /stop-continuation to be ineffective — the user's very next message + // (including normal chat) would re-enable continuation. + // + // Stop state is now only cleared by: + // 1. /start-work (or /ulw-loop, /ralph-loop) via explicit clear() call + // 2. session.deleted event + // 3. Future /resume-continuation command + void sessionID } return { diff --git a/src/hooks/stop-continuation-guard/index.test.ts b/src/hooks/stop-continuation-guard/index.test.ts index a0d08f217..65d1a17b8 100644 --- a/src/hooks/stop-continuation-guard/index.test.ts +++ b/src/hooks/stop-continuation-guard/index.test.ts @@ -162,7 +162,7 @@ describe("stop-continuation-guard", () => { expect(guard.isStopped(session2)).toBe(false) }) - test("should clear stopped state on new user message (chat.message)", async () => { + test("should NOT clear stopped state on new user message (chat.message)", async () => { // given - a session that was stopped const guard = createStopContinuationGuardHook(createMockPluginInput()) const sessionID = "test-session-4" @@ -172,7 +172,38 @@ describe("stop-continuation-guard", () => { // when - user sends a new message await guard["chat.message"]({ sessionID }) - // then - stop state should be cleared (one-time only) + // then - stop state should persist (not cleared by user messages) + // Stop is only cleared by explicit work-starting commands (/start-work, /ralph-loop, /ulw-loop) + // or session deletion. This prevents /stop-continuation from being ineffective. + expect(guard.isStopped(sessionID)).toBe(true) + }) + + test("should persist stop state across multiple user messages", async () => { + // given - a session that was stopped + const guard = createStopContinuationGuardHook(createMockPluginInput()) + const sessionID = "test-session-persist" + guard.stop(sessionID) + + // when - user sends multiple messages + await guard["chat.message"]({ sessionID }) + await guard["chat.message"]({ sessionID }) + await guard["chat.message"]({ sessionID }) + + // then - stop state remains active + expect(guard.isStopped(sessionID)).toBe(true) + }) + + test("should clear stop state only via explicit clear() call", () => { + // given - a session that was stopped + const guard = createStopContinuationGuardHook(createMockPluginInput()) + const sessionID = "test-session-explicit-clear" + guard.stop(sessionID) + expect(guard.isStopped(sessionID)).toBe(true) + + // when - clear is called (simulating /start-work or /ralph-loop) + guard.clear(sessionID) + + // then - stop state is cleared expect(guard.isStopped(sessionID)).toBe(false) }) diff --git a/src/plugin/tool-execute-before.ts b/src/plugin/tool-execute-before.ts index e7585b7b3..3649720b9 100644 --- a/src/plugin/tool-execute-before.ts +++ b/src/plugin/tool-execute-before.ts @@ -184,6 +184,19 @@ export function createToolExecuteBeforeHandler(args: { sessionID, }) } + + // Clear stop state when user explicitly resumes work via work-starting commands. + // This ensures /stop-continuation persists until the user intentionally restarts. + const workStartingCommands = ["start-work", "ralph-loop", "ulw-loop"] + if (workStartingCommands.includes(command ?? "") && sessionID) { + if (hooks.stopContinuationGuard?.isStopped(sessionID)) { + hooks.stopContinuationGuard.clear(sessionID) + log("[stop-continuation] Stop state cleared by work-starting command", { + sessionID, + command, + }) + } + } } } }