feat(todo-enforcer): add skipAgents option and improve permission check

- Add skipAgents option to skip continuation for specified agents
- Default skip: Prometheus (Planner)
- Improve tool permission check to handle 'allow'/'deny' string values
- Add agent name detection from session messages

🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)
This commit is contained in:
YeonGyu-Kim
2026-01-05 13:51:21 +09:00
parent 30e5760e2f
commit f49d92852a
2 changed files with 55 additions and 9 deletions

View File

@@ -349,6 +349,25 @@ describe("todo-continuation-enforcer", () => {
expect(promptCalls).toHaveLength(0)
})
test("should accept skipAgents option without error", async () => {
// #given - session with skipAgents configured for Prometheus
const sessionID = "main-prometheus-option"
setMainSession(sessionID)
// #when - create hook with skipAgents option (should not throw)
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {
skipAgents: ["Prometheus (Planner)", "custom-agent"],
})
// #then - handler works without error
await hook.handler({
event: { type: "session.idle", properties: { sessionID } },
})
await new Promise(r => setTimeout(r, 100))
expect(toastCalls.length).toBeGreaterThanOrEqual(1)
})
test("should show countdown toast updates", async () => {
// #given - session with incomplete todos
const sessionID = "main-toast"

View File

@@ -11,8 +11,11 @@ import { log } from "../shared/logger"
const HOOK_NAME = "todo-continuation-enforcer"
const DEFAULT_SKIP_AGENTS = ["Prometheus (Planner)"]
export interface TodoContinuationEnforcerOptions {
backgroundManager?: BackgroundManager
skipAgents?: string[]
}
export interface TodoContinuationEnforcer {
@@ -89,7 +92,7 @@ export function createTodoContinuationEnforcer(
ctx: PluginInput,
options: TodoContinuationEnforcerOptions = {}
): TodoContinuationEnforcer {
const { backgroundManager } = options
const { backgroundManager, skipAgents = DEFAULT_SKIP_AGENTS } = options
const sessions = new Map<string, SessionState>()
function getState(sessionID: string): SessionState {
@@ -184,17 +187,19 @@ export function createTodoContinuationEnforcer(
const messageDir = getMessageDir(sessionID)
const prevMessage = messageDir ? findNearestMessageWithFields(messageDir) : null
const hasWritePermission = !prevMessage?.tools ||
(prevMessage.tools.write !== false && prevMessage.tools.edit !== false)
if (!hasWritePermission) {
log(`[${HOOK_NAME}] Skipped: agent lacks write permission`, { sessionID, agent: prevMessage?.agent })
const agentName = prevMessage?.agent
if (agentName && skipAgents.includes(agentName)) {
log(`[${HOOK_NAME}] Skipped: agent in skipAgents list`, { sessionID, agent: agentName })
return
}
const agentName = prevMessage?.agent?.toLowerCase() ?? ""
if (agentName === "plan" || agentName === "planner-sisyphus") {
log(`[${HOOK_NAME}] Skipped: plan mode agent`, { sessionID, agent: prevMessage?.agent })
const editPermission = prevMessage?.tools?.edit
const writePermission = prevMessage?.tools?.write
const hasWritePermission = !prevMessage?.tools ||
((editPermission !== false && editPermission !== "deny") &&
(writePermission !== false && writePermission !== "deny"))
if (!hasWritePermission) {
log(`[${HOOK_NAME}] Skipped: agent lacks write permission`, { sessionID, agent: prevMessage?.agent })
return
}
@@ -324,6 +329,28 @@ export function createTodoContinuationEnforcer(
return
}
let agentName: string | undefined
try {
const messagesResp = await ctx.client.session.messages({
path: { id: sessionID },
})
const messages = (messagesResp.data ?? []) as Array<{ info?: { agent?: string } }>
for (let i = messages.length - 1; i >= 0; i--) {
if (messages[i].info?.agent) {
agentName = messages[i].info?.agent
break
}
}
} catch (err) {
log(`[${HOOK_NAME}] Failed to fetch messages for agent check`, { sessionID, error: String(err) })
}
log(`[${HOOK_NAME}] Agent check`, { sessionID, agentName, skipAgents })
if (agentName && skipAgents.includes(agentName)) {
log(`[${HOOK_NAME}] Skipped: agent in skipAgents list`, { sessionID, agent: agentName })
return
}
startCountdown(sessionID, incompleteCount, todos.length)
return
}