todo-enforcer
v1.0.0
Published
Todo continuation enforcer for pi-coding-agent — monitors todo state and injects prompts to keep agents working until all tasks complete
Maintainers
Readme
todo-enforcer
Todo continuation enforcer for pi-coding-agent — monitors todo state and injects prompts to keep agents working until all tasks complete.
Features
- Automatic task continuation — injects prompts when the agent goes idle with incomplete tasks
- Configurable rule engine — first-match-wins rule evaluation with built-in and custom conditions
- Multiple delivery modes —
userMessage(default) orcustomMessagevia pi's messaging API - External command support — call external scripts/HTTP endpoints for dynamic continuation logic
- Spawn action — run
pi -pin background to generate continuation guidance - Stagnation detection — stops injecting when the agent is stuck on the same incomplete count
- Exponential backoff — rate-limits retries on LLM errors (429, rate limits, etc.)
- Message stall guard — prevents infinite loops from repeated identical messages
- Polling timer — re-evaluates after cooldown even without agent_end events
- Completion summary control — configurable
completionSummaryto suppress or enable the "all done" message (default: suppressed)
Installation
For Humans
npm install -g todo-enforcerFor AI Agents (pi / OpenCode / Claude Code / Codex)
Add to your settings.json:
{
"packages": ["todo-enforcer"]
}Or tell your agent:
Install and configure todo-enforcer by following:
https://raw.githubusercontent.com/buihongduc132/todo-enforcer/refs/heads/main/README.mdFor pi (git-sourced)
In settings.json:
{
"packages": ["https://github.com/buihongduc132/todo-enforcer"]
}For pi (local path)
In settings.json:
{
"packages": ["/path/to/todo-enforcer"]
}Usage
todo-enforcer auto-activates via pi's agent_end lifecycle hook. No manual invocation needed.
How It Works
- On each
agent_endevent, the enforcer reads the current todo state - It evaluates rules in order — the first matching rule wins
- If a rule matches, it generates a prompt and delivers it to the agent
- A polling timer re-checks after the cooldown expires
Default Rules
| Rule | Condition | Action |
|------|-----------|--------|
| incomplete-tasks-remain | Any pending/in_progress tasks | Inject continuation prompt |
| all-complete-celebration | All tasks completed | Inject summary (disabled by default) |
Slash Commands
| Command | Description |
|---------|-------------|
| /enforcer-status | Show current enforcer state, rules, and injection count |
| /enforcer-switch <rule1,rule2,...> | Switch active rules for this session |
| /enforcer-switch reset | Reset to config defaults |
| /enforcer-reset | Reset all enforcer state for this session |
Keyboard Shortcut
| Shortcut | Action |
|----------|--------|
| Ctrl+Shift+T | Toggle enforcer enabled/disabled |
Configuration
Create ~/.todo-enforcer.json (global) or .todo-enforcer.json (project-level):
{
"enabled": true,
"maxInjections": 5, // Max injections per session
"cooldownMs": 5000, // Cooldown between injections (ms)
"completionSummary": false, // Suppress "all done" message (default)
"detectStagnation": true, // Stop injecting when stuck
"stagnationThreshold": 3, // Consecutive idle events before stagnation
"backoff": {
"enabled": true,
"factor": 2,
"maxDelayMs": 3600000,
"errorPatterns": ["429", "rate limit", "No deployments available"]
},
"messageDelivery": {
"mode": "userMessage", // "userMessage" or "customMessage"
"display": true,
"triggerTurn": true
},
"rules": [
{
"name": "incomplete-tasks-remain",
"condition": "has_incomplete",
"action": "prompt",
"prompt": "You have incomplete tasks. Continue working on them.\n\n[Status: {{completed_count}}/{{total_count}} completed, {{incomplete_count}} remaining]\n\nRemaining tasks:\n{{incomplete_list}}\n\nPick up where you left off."
},
{
"name": "all-complete-celebration",
"condition": "all_complete",
"action": "prompt",
"prompt": "All {{total_count}} tasks are complete. Great work.\n\nCompleted tasks:\n{{completed_list}}\n\nYou may now summarize the results or ask the user for next steps."
}
]
}Built-in Conditions
| Condition | Description |
|-----------|-------------|
| has_incomplete | Any pending/in_progress tasks remain |
| all_complete | Every non-deleted task is completed |
| has_in_progress | At least one task is in_progress |
| none | Never matches (disabled rule) |
| always | Always matches |
Template Variables
| Variable | Description |
|----------|-------------|
| {{incomplete_count}} | Number of incomplete tasks |
| {{completed_count}} | Number of completed tasks |
| {{total_count}} | Total non-deleted tasks |
| {{incomplete_list}} | Formatted list of incomplete tasks |
| {{completed_list}} | Formatted list of completed tasks |
| {{latest_user_message}} | Latest user message content |
| {{assistant_messages}} | Recent assistant messages |
Actions
| Action | Description |
|--------|-------------|
| prompt | Inject a static template string |
| external | Call an external command or HTTP endpoint |
| spawn | Run pi -p in background with a template |
| noop | Do nothing (for logging/future use) |
Architecture
todo-enforcer/
├── src/
│ ├── index.ts ← Entry point — hooks, commands, shortcuts
│ ├── config.ts ← Config loader + types + template interpolation
│ ├── conditions.ts ← Built-in + custom condition evaluator
│ ├── external-caller.ts ← External command/HTTP executor
│ ├── session-state.ts ← Per-session state tracking
│ ├── todo-snapshot.ts ← Reads todo state from session branch
│ ├── message-stall.ts ← Repeated message + rate limit guard
│ ├── type-guards.ts ← Runtime type guards
│ └── lib/
│ ├── plugin-logger.ts ← File-based structured logger
│ ├── hooks-manager.ts ← Hook registration and state
│ └── types.ts ← Shared types
├── tests/ ← Test suite (134 tests)
├── package.json
├── tsconfig.json
└── vitest.config.tsDevelopment
# Install dependencies
npm install
# Run tests
npm test
# Run tests with coverage
npm run test:ci
# Type check
npm run typecheckLicense
MIT
Repository
GitHub: buihongduc132/todo-enforcer
