fix(atlas): restore agent mismatch guard for subagent boulder continuation (#18681)
This commit is contained in:
@@ -6,7 +6,7 @@ import { tmpdir } from "node:os"
|
|||||||
import { join } from "node:path"
|
import { join } from "node:path"
|
||||||
import { clearBoulderState, readBoulderState, writeBoulderState } from "../../features/boulder-state"
|
import { clearBoulderState, readBoulderState, writeBoulderState } from "../../features/boulder-state"
|
||||||
import type { BoulderState } 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")
|
const { createAtlasHook } = await import("./index")
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ describe("atlas hook idle-event session lineage", () => {
|
|||||||
let testDirectory = ""
|
let testDirectory = ""
|
||||||
let promptCalls: Array<unknown> = []
|
let promptCalls: Array<unknown> = []
|
||||||
|
|
||||||
function writeIncompleteBoulder(): void {
|
function writeIncompleteBoulder(overrides: Partial<BoulderState> = {}): void {
|
||||||
const planPath = join(testDirectory, "test-plan.md")
|
const planPath = join(testDirectory, "test-plan.md")
|
||||||
writeFileSync(planPath, "# Plan\n- [ ] Task 1\n- [ ] Task 2")
|
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",
|
started_at: "2026-01-02T10:00:00Z",
|
||||||
session_ids: [MAIN_SESSION_ID],
|
session_ids: [MAIN_SESSION_ID],
|
||||||
plan_name: "test-plan",
|
plan_name: "test-plan",
|
||||||
|
...overrides,
|
||||||
}
|
}
|
||||||
|
|
||||||
writeBoulderState(testDirectory, state)
|
writeBoulderState(testDirectory, state)
|
||||||
@@ -103,6 +104,7 @@ describe("atlas hook idle-event session lineage", () => {
|
|||||||
|
|
||||||
writeIncompleteBoulder()
|
writeIncompleteBoulder()
|
||||||
subagentSessions.add(subagentSessionID)
|
subagentSessions.add(subagentSessionID)
|
||||||
|
setSessionAgent(subagentSessionID, "atlas")
|
||||||
|
|
||||||
const hook = createHook({
|
const hook = createHook({
|
||||||
[subagentSessionID]: intermediateParentSessionID,
|
[subagentSessionID]: intermediateParentSessionID,
|
||||||
@@ -119,4 +121,63 @@ describe("atlas hook idle-event session lineage", () => {
|
|||||||
assert.equal(readBoulderState(testDirectory)?.session_ids.includes(subagentSessionID), true)
|
assert.equal(readBoulderState(testDirectory)?.session_ids.includes(subagentSessionID), true)
|
||||||
assert.equal(promptCalls.length, 1)
|
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)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import {
|
|||||||
readBoulderState,
|
readBoulderState,
|
||||||
readCurrentTopLevelTask,
|
readCurrentTopLevelTask,
|
||||||
} from "../../features/boulder-state"
|
} 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 { log } from "../../shared/logger"
|
||||||
import { injectBoulderContinuation } from "./boulder-continuation-injector"
|
import { injectBoulderContinuation } from "./boulder-continuation-injector"
|
||||||
import { HOOK_NAME } from "./hook-name"
|
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 sessionState = getState(sessionID)
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
|
|
||||||
|
|||||||
@@ -1282,6 +1282,7 @@ session_id: ses_untrusted_999
|
|||||||
}
|
}
|
||||||
writeBoulderState(TEST_DIR, state)
|
writeBoulderState(TEST_DIR, state)
|
||||||
subagentSessions.add(subagentSessionID)
|
subagentSessions.add(subagentSessionID)
|
||||||
|
updateSessionAgent(subagentSessionID, "atlas")
|
||||||
|
|
||||||
const mockInput = createMockPluginInput()
|
const mockInput = createMockPluginInput()
|
||||||
const hook = createAtlasHook(mockInput)
|
const hook = createAtlasHook(mockInput)
|
||||||
|
|||||||
Reference in New Issue
Block a user