fix(atlas): require blocked plan checkbox edits

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
YeonGyu-Kim
2026-05-18 12:41:47 +09:00
parent 3b50b7c8ba
commit 0994c107d2
2 changed files with 108 additions and 1 deletions
+106
View File
@@ -1950,6 +1950,112 @@ session_id: ses_untrusted_999
expect(callArgs.body.parts[0].text).toContain("ses_auth_flow_123")
})
test("#given blocked-task continuation #when prompt is built #then it requires a plan edit to mark the task blocked", async () => {
// given - boulder state with an incomplete externally blockable task
const planPath = join(TEST_DIR, "blocked-plan.md")
writeFileSync(planPath, "# Plan\n- [ ] Wait for external credentials")
writeBoulderState(TEST_DIR, {
active_plan: planPath,
started_at: "2026-01-02T10:00:00Z",
session_ids: [MAIN_SESSION_ID],
plan_name: "blocked-plan",
})
const mockInput = createMockPluginInput()
const hook = createTestAtlasHook(mockInput)
// when
await hook.handler({
event: {
type: "session.idle",
properties: { sessionID: MAIN_SESSION_ID },
},
})
// then - prompt enforces the behavioral invariant instead of allowing text-only blocker reports
const callArgs = mockInput._promptMock.mock.calls[0][0]
const promptText = callArgs.body.parts[0].text
expect(promptText).toContain("- [~]")
expect(promptText).toMatch(/edit the plan file/i)
expect(promptText).toMatch(/text-only explanation.*not progress/i)
})
test("#given continuation emits text without tool progress #when three continuation iterations repeat #then Atlas stalls instead of looping", async () => {
// given - boulder state with one externally blocked task that never receives a tool edit
const planPath = join(TEST_DIR, "blocked-loop-plan.md")
writeFileSync(planPath, "# Plan\n- [ ] Wait for external approval")
writeBoulderState(TEST_DIR, {
active_plan: planPath,
started_at: "2026-01-02T10:00:00Z",
session_ids: [MAIN_SESSION_ID],
plan_name: "blocked-loop-plan",
})
const mockInput = createMockPluginInput()
const hook = createTestAtlasHook(mockInput)
const originalDateNow = Date.now
let now = 0
Date.now = () => now
try {
// when - each idle represents a completed text-only continuation turn with no bash/edit/write progress
for (let iteration = 0; iteration < 4; iteration += 1) {
await hook.handler({ event: { type: "session.idle", properties: { sessionID: MAIN_SESSION_ID } } })
await flushMicrotasks()
now += 6000
}
// then - K=3 continuations are attempted, and the fourth idle aborts cleanly instead of injecting again
expect(mockInput._promptMock).toHaveBeenCalledTimes(3)
} finally {
Date.now = originalDateNow
}
})
test("#given continuation makes tangible tool progress #when idle repeats #then no-progress stall counter resets", async () => {
// given - boulder state with incomplete work and a successful edit between continuation turns
const planPath = join(TEST_DIR, "progress-plan.md")
writeFileSync(planPath, "# Plan\n- [ ] Task 1\n- [ ] Task 2")
writeBoulderState(TEST_DIR, {
active_plan: planPath,
started_at: "2026-01-02T10:00:00Z",
session_ids: [MAIN_SESSION_ID],
plan_name: "progress-plan",
})
const mockInput = createMockPluginInput()
const hook = createTestAtlasHook(mockInput)
const originalDateNow = Date.now
let now = 0
Date.now = () => now
try {
// when - the first continuation is followed by a successful edit tool result
await hook.handler({ event: { type: "session.idle", properties: { sessionID: MAIN_SESSION_ID } } })
await hook["tool.execute.after"](
{ tool: "edit", sessionID: MAIN_SESSION_ID, callID: "progress-edit" },
{ title: "Edited file", output: "Updated plan", metadata: { filePath: planPath } },
)
now += 6000
for (let iteration = 0; iteration < 3; iteration += 1) {
await hook.handler({ event: { type: "session.idle", properties: { sessionID: MAIN_SESSION_ID } } })
await flushMicrotasks()
now += 6000
}
// then - one progress reset plus three no-progress continuation attempts are allowed
expect(mockInput._promptMock).toHaveBeenCalledTimes(4)
} finally {
Date.now = originalDateNow
}
})
test("should inject when last agent is sisyphus and boulder targets atlas explicitly", async () => {
// given - boulder explicitly set to atlas, but last agent is sisyphus (initial state after /start-work)
const planPath = join(TEST_DIR, "test-plan.md")
+2 -1
View File
@@ -31,7 +31,8 @@ RULES:
- Proceed without asking for permission
- Use the notepad at .omo/notepads/{PLAN_NAME}/ to record learnings
- Do not stop until all tasks are complete
- If blocked, document the blocker and move to the next task`
- If a task is blocked by missing external input, unavailable credentials, access limits, or a decision only the user can make, you MUST edit the plan file in this turn and change that task's checkbox from \`- [ ]\` to \`- [~]\` before moving on
- A text-only explanation of a blocker is NOT progress. The \`- [~]\` checkbox edit is mandatory and must happen via a real file-editing tool call`
export const BOULDER_COMPLETE_PROMPT = `<system-reminder>
BOULDER COMPLETE: plan "{PLAN_NAME}" is fully checked.