From 24ca704821328805698da3ffc6827ab51325b9b9 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sun, 5 Apr 2026 15:34:02 +0900 Subject: [PATCH] feat(run): implement boulder lineage detection in continuation state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make getContinuationState async to support client-side lineage checks. Add hasActiveBoulderContinuation with agent eligibility verification and subagent session tracking for proper boulder session inheritance. 🤖 Generated with assistance of OhMyOpenCode --- src/cli/run/completion.ts | 2 +- src/cli/run/continuation-state-marker.test.ts | 8 +-- src/cli/run/continuation-state.ts | 50 +++++++++++++++++-- 3 files changed, 50 insertions(+), 10 deletions(-) diff --git a/src/cli/run/completion.ts b/src/cli/run/completion.ts index 77f00ebdd..f28927f12 100644 --- a/src/cli/run/completion.ts +++ b/src/cli/run/completion.ts @@ -8,7 +8,7 @@ import { export async function checkCompletionConditions(ctx: RunContext): Promise { try { - const continuationState = getContinuationState(ctx.directory, ctx.sessionID) + const continuationState = await getContinuationState(ctx.directory, ctx.sessionID, ctx.client) if (continuationState.hasActiveHookMarker) { const reason = continuationState.activeHookMarkerReason ?? "continuation hook is active" diff --git a/src/cli/run/continuation-state-marker.test.ts b/src/cli/run/continuation-state-marker.test.ts index 0d2a1581e..2b5f8cce0 100644 --- a/src/cli/run/continuation-state-marker.test.ts +++ b/src/cli/run/continuation-state-marker.test.ts @@ -23,21 +23,21 @@ afterEach(() => { }) describe("getContinuationState marker integration", () => { - it("reports active marker state from continuation hooks", () => { + it("reports active marker state from continuation hooks", async () => { // given const directory = createTempDir() const sessionID = "ses_marker_active" setContinuationMarkerSource(directory, sessionID, "todo", "active", "todos remaining") // when - const state = getContinuationState(directory, sessionID) + const state = await getContinuationState(directory, sessionID) // then expect(state.hasActiveHookMarker).toBe(true) expect(state.activeHookMarkerReason).toContain("todos") }) - it("does not report active marker when all sources are idle/stopped", () => { + it("does not report active marker when all sources are idle/stopped", async () => { // given const directory = createTempDir() const sessionID = "ses_marker_idle" @@ -45,7 +45,7 @@ describe("getContinuationState marker integration", () => { setContinuationMarkerSource(directory, sessionID, "stop", "stopped") // when - const state = getContinuationState(directory, sessionID) + const state = await getContinuationState(directory, sessionID) // then expect(state.hasActiveHookMarker).toBe(false) diff --git a/src/cli/run/continuation-state.ts b/src/cli/run/continuation-state.ts index eaa268a66..a9645b6ee 100644 --- a/src/cli/run/continuation-state.ts +++ b/src/cli/run/continuation-state.ts @@ -1,10 +1,18 @@ import { getPlanProgress, readBoulderState } from "../../features/boulder-state" +import { + getSessionAgent, + isAgentRegistered, + subagentSessions, +} from "../../features/claude-code-session-state" import { getActiveContinuationMarkerReason, isContinuationMarkerActive, readContinuationMarker, } from "../../features/run-continuation-state" +import { isSessionInBoulderLineage } from "../../hooks/atlas/boulder-session-lineage" +import { getAgentConfigKey } from "../../shared/agent-display-names" import { readState as readRalphLoopState } from "../../hooks/ralph-loop/storage" +import type { RunContext } from "./types" export interface ContinuationState { hasActiveBoulder: boolean @@ -15,11 +23,15 @@ export interface ContinuationState { activeHookMarkerReason: string | null } -export function getContinuationState(directory: string, sessionID: string): ContinuationState { +export async function getContinuationState( + directory: string, + sessionID: string, + client?: RunContext["client"], +): Promise { const marker = readContinuationMarker(directory, sessionID) return { - hasActiveBoulder: hasActiveBoulderContinuation(directory, sessionID), + hasActiveBoulder: await hasActiveBoulderContinuation(directory, sessionID, client), hasActiveRalphLoop: hasActiveRalphLoopContinuation(directory, sessionID), hasHookMarker: marker !== null, hasTodoHookMarker: marker?.sources.todo !== undefined, @@ -28,13 +40,41 @@ export function getContinuationState(directory: string, sessionID: string): Cont } } -function hasActiveBoulderContinuation(directory: string, sessionID: string): boolean { +async function hasActiveBoulderContinuation( + directory: string, + sessionID: string, + client?: RunContext["client"], +): Promise { const boulder = readBoulderState(directory) if (!boulder) return false - if (!boulder.session_ids.includes(sessionID)) return false const progress = getPlanProgress(boulder.active_plan) - return !progress.isComplete + if (progress.isComplete) return false + if (boulder.session_ids.includes(sessionID)) return true + if (!client) return false + if (!subagentSessions.has(sessionID)) return false + + const requiredAgentName = boulder.agent ?? (isAgentRegistered("atlas") ? "atlas" : undefined) + if (!requiredAgentName || !isAgentRegistered(requiredAgentName)) { + return false + } + + const sessionAgent = getSessionAgent(sessionID) + const agentKey = getAgentConfigKey(sessionAgent ?? "") + const requiredAgentKey = getAgentConfigKey(requiredAgentName) + const isEligibleSubagent = + agentKey === requiredAgentKey + || (requiredAgentKey === getAgentConfigKey("atlas") && agentKey === getAgentConfigKey("sisyphus")) + + if (!isEligibleSubagent) { + return false + } + + return isSessionInBoulderLineage({ + client, + sessionID, + boulderSessionIDs: boulder.session_ids, + }) } function hasActiveRalphLoopContinuation(directory: string, sessionID: string): boolean {