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:
@@ -1950,6 +1950,112 @@ session_id: ses_untrusted_999
|
|||||||
expect(callArgs.body.parts[0].text).toContain("ses_auth_flow_123")
|
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 () => {
|
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)
|
// given - boulder explicitly set to atlas, but last agent is sisyphus (initial state after /start-work)
|
||||||
const planPath = join(TEST_DIR, "test-plan.md")
|
const planPath = join(TEST_DIR, "test-plan.md")
|
||||||
|
|||||||
@@ -31,7 +31,8 @@ RULES:
|
|||||||
- Proceed without asking for permission
|
- Proceed without asking for permission
|
||||||
- Use the notepad at .omo/notepads/{PLAN_NAME}/ to record learnings
|
- Use the notepad at .omo/notepads/{PLAN_NAME}/ to record learnings
|
||||||
- Do not stop until all tasks are complete
|
- 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>
|
export const BOULDER_COMPLETE_PROMPT = `<system-reminder>
|
||||||
BOULDER COMPLETE: plan "{PLAN_NAME}" is fully checked.
|
BOULDER COMPLETE: plan "{PLAN_NAME}" is fully checked.
|
||||||
|
|||||||
Reference in New Issue
Block a user