From 257eb9277ba21f372700beae22d0f8c9f0b2ad6a Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Tue, 10 Feb 2026 22:15:28 +0900 Subject: [PATCH] fix(atlas): restrict boulder continuation to sessions in boulder session_ids Main session was unconditionally allowed through the boulder session guard, causing continuation injection into sessions not part of the active boulder. Now only sessions explicitly in boulder's session_ids (or background tasks) receive boulder continuation, matching todo-continuation-enforcer behavior. --- src/hooks/atlas/event-handler.ts | 10 ++++------ src/hooks/atlas/index.test.ts | 28 ++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/hooks/atlas/event-handler.ts b/src/hooks/atlas/event-handler.ts index f9024776..8e57aea8 100644 --- a/src/hooks/atlas/event-handler.ts +++ b/src/hooks/atlas/event-handler.ts @@ -1,6 +1,6 @@ import type { PluginInput } from "@opencode-ai/plugin" import { getPlanProgress, readBoulderState } from "../../features/boulder-state" -import { getMainSessionID, subagentSessions } from "../../features/claude-code-session-state" +import { subagentSessions } from "../../features/claude-code-session-state" import { log } from "../../shared/logger" import { HOOK_NAME } from "./hook-name" import { isAbortError } from "./is-abort-error" @@ -43,13 +43,11 @@ export function createAtlasEventHandler(input: { const boulderState = readBoulderState(ctx.directory) const isBoulderSession = boulderState?.session_ids.includes(sessionID) ?? false - const mainSessionID = getMainSessionID() - const isMainSession = sessionID === mainSessionID const isBackgroundTaskSession = subagentSessions.has(sessionID) - // Allow continuation if: main session OR background task OR boulder session - if (mainSessionID && !isMainSession && !isBackgroundTaskSession && !isBoulderSession) { - log(`[${HOOK_NAME}] Skipped: not main, background task, or boulder session`, { sessionID }) + // Allow continuation only if: session is in boulder's session_ids OR is a background task + if (!isBackgroundTaskSession && !isBoulderSession) { + log(`[${HOOK_NAME}] Skipped: not boulder or background task session`, { sessionID }) return } diff --git a/src/hooks/atlas/index.test.ts b/src/hooks/atlas/index.test.ts index 88722682..1713789b 100644 --- a/src/hooks/atlas/index.test.ts +++ b/src/hooks/atlas/index.test.ts @@ -691,6 +691,34 @@ describe("atlas hook", () => { expect(mockInput._promptMock).not.toHaveBeenCalled() }) + test("should not inject when main session is not in boulder session_ids", async () => { + // given - boulder state exists but current (main) session is NOT in session_ids + const planPath = join(TEST_DIR, "test-plan.md") + writeFileSync(planPath, "# Plan\n- [ ] Task 1\n- [ ] Task 2") + + const state: BoulderState = { + active_plan: planPath, + started_at: "2026-01-02T10:00:00Z", + session_ids: ["some-other-session-id"], + plan_name: "test-plan", + } + writeBoulderState(TEST_DIR, state) + + const mockInput = createMockPluginInput() + const hook = createAtlasHook(mockInput) + + // when - main session fires idle but is NOT in boulder's session_ids + await hook.handler({ + event: { + type: "session.idle", + properties: { sessionID: MAIN_SESSION_ID }, + }, + }) + + // then - should NOT call prompt because session is not part of this boulder + expect(mockInput._promptMock).not.toHaveBeenCalled() + }) + test("should not inject when boulder plan is complete", async () => { // given - boulder state with complete plan const planPath = join(TEST_DIR, "complete-plan.md")