From 29c42485a8551c473a36a5851c300bf8ff9c3734 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Mon, 11 May 2026 14:48:49 +0900 Subject: [PATCH] fix(hooks/atlas): capture plan snapshot for .sisyphus paths Oracle review of PR #3943 surfaced that endTaskTimer never fires for real Prometheus plans because their canonical path is .sisyphus/plans/ and the snapshot capture was nested inside the !isSisyphusPath branch intended for direct-work warning suppression. Move the snapshot/path tracking out of the warning gate so all plan-file edits are snapshotted regardless of .sisyphus prefix. Keep the warning branch isSisyphus-gated so Atlas does not yell at legitimate plan edits. Regression test now uses a real .sisyphus/plans/ path and fails against HEAD before the fix. --- .../tool-execute-after-task-timers.test.ts | 4 +- src/hooks/atlas/tool-execute-before.ts | 47 ++++++++++--------- 2 files changed, 28 insertions(+), 23 deletions(-) diff --git a/src/hooks/atlas/tool-execute-after-task-timers.test.ts b/src/hooks/atlas/tool-execute-after-task-timers.test.ts index c565e5c2d..095d9f4c3 100644 --- a/src/hooks/atlas/tool-execute-after-task-timers.test.ts +++ b/src/hooks/atlas/tool-execute-after-task-timers.test.ts @@ -230,7 +230,9 @@ describe("createToolExecuteAfterHandler task timers", () => { it("ends task timer when plan checkbox flips to checked via edit tool", async () => { // given const parentSessionID = "ses_parent_3" - const planPath = join(testDirectory, "task-timer-edit-plan.md") + const planDirectory = join(testDirectory, ".sisyphus", "plans") + mkdirSync(planDirectory, { recursive: true }) + const planPath = join(planDirectory, "task-timer-edit-plan.md") writeFileSync(planPath, "# Plan\n\n## TODOs\n- [ ] 1. Implement auth flow\n", "utf-8") writeBoulderState(testDirectory, { schema_version: 2, diff --git a/src/hooks/atlas/tool-execute-before.ts b/src/hooks/atlas/tool-execute-before.ts index d6e4789ee..5dfc24a7d 100644 --- a/src/hooks/atlas/tool-execute-before.ts +++ b/src/hooks/atlas/tool-execute-before.ts @@ -80,32 +80,35 @@ export function createToolExecuteBeforeHandler(input: { // Warn-only policy: Atlas guides orchestrators toward delegation but doesn't block, allowing flexibility for urgent fixes if (isWriteOrEditToolName(toolInput.tool)) { const filePath = (toolOutput.args.filePath ?? toolOutput.args.path ?? toolOutput.args.file) as string | undefined - if (filePath && !isSisyphusPath(filePath)) { - // Store filePath for use in tool.execute.after - if (toolInput.callID) { - pendingFilePaths.set(toolInput.callID, filePath) + if (!filePath || !toolInput.callID) { + return + } - const sessionID = toolInput.sessionID - const sessionWork = sessionID - ? getWorkForSession(ctx.directory, sessionID) - : null - const state = sessionWork ? null : readBoulderState(ctx.directory) - const planPath = sessionWork - ? resolveBoulderPlanPathForWork(ctx.directory, sessionWork) - : state - ? resolveBoulderPlanPath(ctx.directory, state) - : null + // Store filePath for use in tool.execute.after + pendingFilePaths.set(toolInput.callID, filePath) - if (planPath && resolve(filePath) === resolve(planPath) && pendingPlanSnapshots) { - try { - if (existsSync(planPath)) { - pendingPlanSnapshots.set(toolInput.callID, readFileSync(planPath, "utf-8")) - } - } catch { - pendingPlanSnapshots.delete(toolInput.callID) - } + const sessionID = toolInput.sessionID + const sessionWork = sessionID + ? getWorkForSession(ctx.directory, sessionID) + : null + const state = sessionWork ? null : readBoulderState(ctx.directory) + const planPath = sessionWork + ? resolveBoulderPlanPathForWork(ctx.directory, sessionWork) + : state + ? resolveBoulderPlanPath(ctx.directory, state) + : null + + if (planPath && resolve(filePath) === resolve(planPath) && pendingPlanSnapshots) { + try { + if (existsSync(planPath)) { + pendingPlanSnapshots.set(toolInput.callID, readFileSync(planPath, "utf-8")) } + } catch { + pendingPlanSnapshots.delete(toolInput.callID) } + } + + if (!isSisyphusPath(filePath)) { const warning = ORCHESTRATOR_DELEGATION_REQUIRED.replace("$FILE_PATH", filePath) toolOutput.message = (toolOutput.message || "") + warning log(`[${HOOK_NAME}] Injected delegation warning for direct file modification`, {