diff --git a/src/hooks/atlas/idle-event-lineage.test.ts b/src/hooks/atlas/idle-event-lineage.test.ts index e31e670d6..061195a97 100644 --- a/src/hooks/atlas/idle-event-lineage.test.ts +++ b/src/hooks/atlas/idle-event-lineage.test.ts @@ -6,7 +6,7 @@ import { tmpdir } from "node:os" import { join } from "node:path" import { clearBoulderState, readBoulderState, writeBoulderState } from "../../features/boulder-state" import type { BoulderState } from "../../features/boulder-state" -import { _resetForTesting, subagentSessions } from "../../features/claude-code-session-state" +import { _resetForTesting, setSessionAgent, subagentSessions } from "../../features/claude-code-session-state" const { createAtlasHook } = await import("./index") @@ -16,7 +16,7 @@ describe("atlas hook idle-event session lineage", () => { let testDirectory = "" let promptCalls: Array = [] - function writeIncompleteBoulder(): void { + function writeIncompleteBoulder(overrides: Partial = {}): void { const planPath = join(testDirectory, "test-plan.md") writeFileSync(planPath, "# Plan\n- [ ] Task 1\n- [ ] Task 2") @@ -25,6 +25,7 @@ describe("atlas hook idle-event session lineage", () => { started_at: "2026-01-02T10:00:00Z", session_ids: [MAIN_SESSION_ID], plan_name: "test-plan", + ...overrides, } writeBoulderState(testDirectory, state) @@ -103,6 +104,7 @@ describe("atlas hook idle-event session lineage", () => { writeIncompleteBoulder() subagentSessions.add(subagentSessionID) + setSessionAgent(subagentSessionID, "atlas") const hook = createHook({ [subagentSessionID]: intermediateParentSessionID, @@ -119,4 +121,63 @@ describe("atlas hook idle-event session lineage", () => { assert.equal(readBoulderState(testDirectory)?.session_ids.includes(subagentSessionID), true) assert.equal(promptCalls.length, 1) }) + + it("does not inject continuation for boulder-lineage subagent with non-matching agent", async () => { + const subagentSessionID = "subagent-session-agent-mismatch" + + writeIncompleteBoulder({ agent: "atlas" }) + subagentSessions.add(subagentSessionID) + setSessionAgent(subagentSessionID, "sisyphus-junior") + + const hook = createHook({ + [subagentSessionID]: MAIN_SESSION_ID, + }) + + await hook.handler({ + event: { + type: "session.idle", + properties: { sessionID: subagentSessionID }, + }, + }) + + assert.equal(readBoulderState(testDirectory)?.session_ids.includes(subagentSessionID), true) + assert.equal(promptCalls.length, 0) + }) + + it("injects continuation for boulder-lineage subagent with matching agent", async () => { + const subagentSessionID = "subagent-session-agent-match" + + writeIncompleteBoulder({ agent: "atlas" }) + subagentSessions.add(subagentSessionID) + setSessionAgent(subagentSessionID, "atlas") + + const hook = createHook({ + [subagentSessionID]: MAIN_SESSION_ID, + }) + + await hook.handler({ + event: { + type: "session.idle", + properties: { sessionID: subagentSessionID }, + }, + }) + + assert.equal(promptCalls.length, 1) + }) + + it("injects continuation for explicitly tracked boulder session regardless of agent", async () => { + writeIncompleteBoulder({ agent: "atlas" }) + setSessionAgent(MAIN_SESSION_ID, "hephaestus") + + const hook = createHook() + + await hook.handler({ + event: { + type: "session.idle", + properties: { sessionID: MAIN_SESSION_ID }, + }, + }) + + assert.equal(promptCalls.length, 1) + }) }) diff --git a/src/hooks/atlas/idle-event.ts b/src/hooks/atlas/idle-event.ts index 26714e328..9923d7d0e 100644 --- a/src/hooks/atlas/idle-event.ts +++ b/src/hooks/atlas/idle-event.ts @@ -5,6 +5,8 @@ import { readBoulderState, readCurrentTopLevelTask, } from "../../features/boulder-state" +import { getSessionAgent, subagentSessions } from "../../features/claude-code-session-state" +import { getAgentConfigKey } from "../../shared/agent-display-names" import { log } from "../../shared/logger" import { injectBoulderContinuation } from "./boulder-continuation-injector" import { HOOK_NAME } from "./hook-name" @@ -136,6 +138,23 @@ export async function handleAtlasSessionIdle(input: { }) } + if (subagentSessions.has(sessionID)) { + const sessionAgent = getSessionAgent(sessionID) + const agentKey = getAgentConfigKey(sessionAgent ?? "") + const requiredAgentKey = getAgentConfigKey(boulderState.agent ?? "atlas") + const agentMatches = + agentKey === requiredAgentKey || + (requiredAgentKey === getAgentConfigKey("atlas") && agentKey === getAgentConfigKey("sisyphus")) + if (!agentMatches) { + log(`[${HOOK_NAME}] Skipped: subagent agent does not match boulder agent`, { + sessionID, + agent: sessionAgent ?? "unknown", + requiredAgent: boulderState.agent ?? "atlas", + }) + return + } + } + const sessionState = getState(sessionID) const now = Date.now() diff --git a/src/hooks/atlas/index.test.ts b/src/hooks/atlas/index.test.ts index c3a16a90b..4f01547cd 100644 --- a/src/hooks/atlas/index.test.ts +++ b/src/hooks/atlas/index.test.ts @@ -1282,6 +1282,7 @@ session_id: ses_untrusted_999 } writeBoulderState(TEST_DIR, state) subagentSessions.add(subagentSessionID) + updateSessionAgent(subagentSessionID, "atlas") const mockInput = createMockPluginInput() const hook = createAtlasHook(mockInput)