Merge pull request #3636 from lucasyounger/codex/fix-3629-worktree-plan-path
fix(boulder): resolve continuation progress from worktree plan
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import type { PluginInput } from "@opencode-ai/plugin"
|
||||
import { appendSessionId, type BoulderState, upsertTaskSessionState } from "../../features/boulder-state"
|
||||
import { appendSessionId, type BoulderState, resolveBoulderPlanPath, upsertTaskSessionState } from "../../features/boulder-state"
|
||||
import { log } from "../../shared/logger"
|
||||
import { HOOK_NAME } from "./hook-name"
|
||||
import { extractSessionIdFromOutput, validateSubagentSessionId } from "./subagent-session-id"
|
||||
@@ -40,7 +40,7 @@ export async function syncBackgroundLaunchSessionTracking(input: {
|
||||
|
||||
const { currentTask, shouldSkipTaskSessionUpdate } = resolveTaskContext(
|
||||
pendingTaskRef,
|
||||
boulderState.active_plan,
|
||||
resolveBoulderPlanPath(ctx.directory, boulderState),
|
||||
)
|
||||
|
||||
if (currentTask && !shouldSkipTaskSessionUpdate) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
getTaskSessionState,
|
||||
readBoulderState,
|
||||
readCurrentTopLevelTask,
|
||||
resolveBoulderPlanPath,
|
||||
} from "../../features/boulder-state"
|
||||
import { getSessionAgent } from "../../features/claude-code-session-state"
|
||||
import { getLastAgentFromSession } from "./session-last-agent"
|
||||
@@ -52,8 +53,12 @@ async function injectContinuation(input: {
|
||||
|
||||
try {
|
||||
const currentBoulder = readBoulderState(input.ctx.directory)
|
||||
const currentPlanPath = currentBoulder
|
||||
? resolveBoulderPlanPath(input.ctx.directory, currentBoulder)
|
||||
: null
|
||||
const currentTask = currentBoulder
|
||||
? readCurrentTopLevelTask(currentBoulder.active_plan)
|
||||
&& currentPlanPath
|
||||
? readCurrentTopLevelTask(currentPlanPath)
|
||||
: null
|
||||
const preferredTaskSession = currentTask
|
||||
? getTaskSessionState(input.ctx.directory, currentTask.key)
|
||||
@@ -163,7 +168,7 @@ function scheduleRetry(input: {
|
||||
if (!currentBoulder) return
|
||||
if (!currentBoulder.session_ids?.includes(sessionID)) return
|
||||
|
||||
const currentProgress = getPlanProgress(currentBoulder.active_plan)
|
||||
const currentProgress = getPlanProgress(resolveBoulderPlanPath(ctx.directory, currentBoulder))
|
||||
if (currentProgress.isComplete) return
|
||||
if (options?.isContinuationStopped?.(sessionID)) return
|
||||
const canContinueSession = await canContinueTrackedBoulderSession({
|
||||
|
||||
@@ -1494,6 +1494,43 @@ session_id: ses_untrusted_999
|
||||
expect(mockInput._promptMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test("should not inject when the mirrored worktree plan is complete even if the main repo plan is stale", async () => {
|
||||
// given
|
||||
const mainPlanPath = join(TEST_DIR, ".sisyphus", "plans", "worktree-complete-plan.md")
|
||||
const worktreeDir = join(tmpdir(), `atlas-worktree-${randomUUID()}`)
|
||||
const worktreePlanPath = join(worktreeDir, ".sisyphus", "plans", "worktree-complete-plan.md")
|
||||
mkdirSync(join(TEST_DIR, ".sisyphus", "plans"), { recursive: true })
|
||||
mkdirSync(join(worktreeDir, ".sisyphus", "plans"), { recursive: true })
|
||||
writeFileSync(mainPlanPath, "# Plan\n- [ ] Main repo task\n")
|
||||
writeFileSync(worktreePlanPath, "# Plan\n- [x] Worktree task\n")
|
||||
|
||||
writeBoulderState(TEST_DIR, {
|
||||
active_plan: mainPlanPath,
|
||||
started_at: "2026-01-02T10:00:00Z",
|
||||
session_ids: [MAIN_SESSION_ID],
|
||||
plan_name: "worktree-complete-plan",
|
||||
worktree_path: worktreeDir,
|
||||
})
|
||||
|
||||
const mockInput = createMockPluginInput()
|
||||
const hook = createAtlasHook(mockInput)
|
||||
|
||||
try {
|
||||
// when
|
||||
await hook.handler({
|
||||
event: {
|
||||
type: "session.idle",
|
||||
properties: { sessionID: MAIN_SESSION_ID },
|
||||
},
|
||||
})
|
||||
|
||||
// then
|
||||
expect(mockInput._promptMock).not.toHaveBeenCalled()
|
||||
} finally {
|
||||
rmSync(worktreeDir, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
test("should skip when abort error occurred before idle", async () => {
|
||||
// given - boulder state with incomplete plan
|
||||
const planPath = join(TEST_DIR, "test-plan.md")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
|
||||
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs"
|
||||
import { tmpdir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
import { dirname, join } from "node:path"
|
||||
import { randomUUID } from "node:crypto"
|
||||
import { clearBoulderState, writeBoulderState } from "../../features/boulder-state"
|
||||
import { resolveActiveBoulderSession } from "./resolve-active-boulder-session"
|
||||
@@ -96,4 +96,39 @@ describe("resolveActiveBoulderSession", () => {
|
||||
expect(result?.progress.isComplete).toBe(false)
|
||||
expect(result?.boulderState.session_ids).toContain("ses_appended")
|
||||
})
|
||||
|
||||
test("returns complete progress when a mirrored worktree plan is complete", async () => {
|
||||
// given
|
||||
const mainPlanPath = join(testDirectory, ".sisyphus", "plans", "worktree-plan.md")
|
||||
const worktreeDirectory = join(tmpdir(), `resolve-active-boulder-worktree-${randomUUID()}`)
|
||||
const worktreePlanPath = join(worktreeDirectory, ".sisyphus", "plans", "worktree-plan.md")
|
||||
mkdirSync(dirname(mainPlanPath), { recursive: true })
|
||||
mkdirSync(dirname(worktreePlanPath), { recursive: true })
|
||||
writeFileSync(mainPlanPath, "# Plan\n- [ ] Main repo task\n", "utf-8")
|
||||
writeFileSync(worktreePlanPath, "# Plan\n- [x] Worktree task\n", "utf-8")
|
||||
writeBoulderState(testDirectory, {
|
||||
active_plan: mainPlanPath,
|
||||
started_at: "2026-01-02T10:00:00Z",
|
||||
session_ids: ["ses_tracked"],
|
||||
session_origins: { ses_tracked: "direct" },
|
||||
plan_name: "worktree-plan",
|
||||
worktree_path: worktreeDirectory,
|
||||
})
|
||||
|
||||
try {
|
||||
// when
|
||||
const result = await resolveActiveBoulderSession({
|
||||
client: { session: { get: async () => ({ data: {} }) } } as never,
|
||||
directory: testDirectory,
|
||||
sessionID: "ses_tracked",
|
||||
})
|
||||
|
||||
// then
|
||||
expect(result).not.toBeNull()
|
||||
expect(result?.progress.isComplete).toBe(true)
|
||||
expect(result?.progress.completed).toBe(1)
|
||||
} finally {
|
||||
rmSync(worktreeDirectory, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { PluginInput } from "@opencode-ai/plugin"
|
||||
import { getPlanProgress, readBoulderState } from "../../features/boulder-state"
|
||||
import { getPlanProgress, readBoulderState, resolveBoulderPlanPath } from "../../features/boulder-state"
|
||||
import type { BoulderState, PlanProgress } from "../../features/boulder-state"
|
||||
|
||||
export async function resolveActiveBoulderSession(input: {
|
||||
@@ -20,7 +20,7 @@ export async function resolveActiveBoulderSession(input: {
|
||||
return null
|
||||
}
|
||||
|
||||
const progress = getPlanProgress(boulderState.active_plan)
|
||||
const progress = getPlanProgress(resolveBoulderPlanPath(input.directory, boulderState))
|
||||
if (progress.isComplete) {
|
||||
return { boulderState, progress, appendedSession: false }
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
getPlanProgress,
|
||||
getTaskSessionState,
|
||||
readBoulderState,
|
||||
resolveBoulderPlanPath,
|
||||
upsertTaskSessionState,
|
||||
} from "../../features/boulder-state"
|
||||
import { log } from "../../shared/logger"
|
||||
@@ -98,12 +99,13 @@ export function createToolExecuteAfterHandler(input: {
|
||||
const extractedSessionId = metadataSessionId ?? extractSessionIdFromOutput(toolOutput.output)
|
||||
|
||||
if (boulderState) {
|
||||
const progress = getPlanProgress(boulderState.active_plan)
|
||||
const planPath = resolveBoulderPlanPath(ctx.directory, boulderState)
|
||||
const progress = getPlanProgress(planPath)
|
||||
const {
|
||||
currentTask,
|
||||
shouldSkipTaskSessionUpdate,
|
||||
shouldIgnoreCurrentSessionId,
|
||||
} = resolveTaskContext(pendingTaskRef, boulderState.active_plan)
|
||||
} = resolveTaskContext(pendingTaskRef, planPath)
|
||||
const trackedTaskSession = currentTask
|
||||
? getTaskSessionState(ctx.directory, currentTask.key)
|
||||
: null
|
||||
@@ -136,7 +138,7 @@ export function createToolExecuteAfterHandler(input: {
|
||||
const originalResponse = toolOutput.output
|
||||
const shouldPauseForApproval = sessionState
|
||||
? shouldPauseForFinalWaveApproval({
|
||||
planPath: boulderState.active_plan,
|
||||
planPath,
|
||||
taskOutput: originalResponse,
|
||||
sessionState,
|
||||
})
|
||||
|
||||
@@ -2,7 +2,7 @@ import { log } from "../../shared/logger"
|
||||
import { SYSTEM_DIRECTIVE_PREFIX } from "../../shared/system-directive"
|
||||
import { isCallerOrchestrator } from "../../shared/session-utils"
|
||||
import type { PluginInput } from "@opencode-ai/plugin"
|
||||
import { readBoulderState, readCurrentTopLevelTask } from "../../features/boulder-state"
|
||||
import { readBoulderState, readCurrentTopLevelTask, resolveBoulderPlanPath } from "../../features/boulder-state"
|
||||
import { HOOK_NAME } from "./hook-name"
|
||||
import { ORCHESTRATOR_DELEGATION_REQUIRED, SINGLE_TASK_DIRECTIVE } from "./system-reminder-templates"
|
||||
import { isSisyphusPath } from "./sisyphus-path"
|
||||
@@ -60,7 +60,7 @@ export function createToolExecuteBeforeHandler(input: {
|
||||
} else {
|
||||
const boulderState = readBoulderState(ctx.directory)
|
||||
const currentTask = boulderState
|
||||
? readCurrentTopLevelTask(boulderState.active_plan)
|
||||
? readCurrentTopLevelTask(resolveBoulderPlanPath(ctx.directory, boulderState))
|
||||
: null
|
||||
if (currentTask) {
|
||||
const task = {
|
||||
|
||||
Reference in New Issue
Block a user