todo-enforcer 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +216 -0
- package/package.json +53 -0
- package/src/conditions.ts +79 -0
- package/src/config.ts +592 -0
- package/src/external-caller.ts +219 -0
- package/src/index.ts +1022 -0
- package/src/lib/hooks-manager.ts +207 -0
- package/src/lib/plugin-logger.ts +155 -0
- package/src/lib/types.ts +59 -0
- package/src/message-stall.ts +188 -0
- package/src/session-state.ts +395 -0
- package/src/todo-snapshot.ts +288 -0
- package/src/type-guards.ts +105 -0
- package/todo-enforcer.example.json +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 buihongduc132
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# todo-enforcer
|
|
2
|
+
|
|
3
|
+
> Todo continuation enforcer for [pi-coding-agent](https://github.com/mariozechner/pi-coding-agent) — monitors todo state and injects prompts to keep agents working until all tasks complete.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/todo-enforcer)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
[](https://www.npmjs.com/search?q=pi-package)
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **Automatic task continuation** — injects prompts when the agent goes idle with incomplete tasks
|
|
12
|
+
- **Configurable rule engine** — first-match-wins rule evaluation with built-in and custom conditions
|
|
13
|
+
- **Multiple delivery modes** — `userMessage` (default) or `customMessage` via pi's messaging API
|
|
14
|
+
- **External command support** — call external scripts/HTTP endpoints for dynamic continuation logic
|
|
15
|
+
- **Spawn action** — run `pi -p` in background to generate continuation guidance
|
|
16
|
+
- **Stagnation detection** — stops injecting when the agent is stuck on the same incomplete count
|
|
17
|
+
- **Exponential backoff** — rate-limits retries on LLM errors (429, rate limits, etc.)
|
|
18
|
+
- **Message stall guard** — prevents infinite loops from repeated identical messages
|
|
19
|
+
- **Polling timer** — re-evaluates after cooldown even without agent_end events
|
|
20
|
+
- **Completion summary control** — configurable `completionSummary` to suppress or enable the "all done" message (default: suppressed)
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
### For Humans
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install -g todo-enforcer
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### For AI Agents (pi / OpenCode / Claude Code / Codex)
|
|
31
|
+
|
|
32
|
+
Add to your `settings.json`:
|
|
33
|
+
|
|
34
|
+
```jsonc
|
|
35
|
+
{
|
|
36
|
+
"packages": ["todo-enforcer"]
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Or tell your agent:
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
Install and configure todo-enforcer by following:
|
|
44
|
+
https://raw.githubusercontent.com/buihongduc132/todo-enforcer/refs/heads/main/README.md
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### For pi (git-sourced)
|
|
48
|
+
|
|
49
|
+
In `settings.json`:
|
|
50
|
+
|
|
51
|
+
```jsonc
|
|
52
|
+
{
|
|
53
|
+
"packages": ["https://github.com/buihongduc132/todo-enforcer"]
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### For pi (local path)
|
|
58
|
+
|
|
59
|
+
In `settings.json`:
|
|
60
|
+
|
|
61
|
+
```jsonc
|
|
62
|
+
{
|
|
63
|
+
"packages": ["/path/to/todo-enforcer"]
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Usage
|
|
68
|
+
|
|
69
|
+
todo-enforcer auto-activates via pi's `agent_end` lifecycle hook. No manual invocation needed.
|
|
70
|
+
|
|
71
|
+
### How It Works
|
|
72
|
+
|
|
73
|
+
1. On each `agent_end` event, the enforcer reads the current todo state
|
|
74
|
+
2. It evaluates rules in order — the first matching rule wins
|
|
75
|
+
3. If a rule matches, it generates a prompt and delivers it to the agent
|
|
76
|
+
4. A polling timer re-checks after the cooldown expires
|
|
77
|
+
|
|
78
|
+
### Default Rules
|
|
79
|
+
|
|
80
|
+
| Rule | Condition | Action |
|
|
81
|
+
|------|-----------|--------|
|
|
82
|
+
| `incomplete-tasks-remain` | Any pending/in_progress tasks | Inject continuation prompt |
|
|
83
|
+
| `all-complete-celebration` | All tasks completed | Inject summary (disabled by default) |
|
|
84
|
+
|
|
85
|
+
### Slash Commands
|
|
86
|
+
|
|
87
|
+
| Command | Description |
|
|
88
|
+
|---------|-------------|
|
|
89
|
+
| `/enforcer-status` | Show current enforcer state, rules, and injection count |
|
|
90
|
+
| `/enforcer-switch <rule1,rule2,...>` | Switch active rules for this session |
|
|
91
|
+
| `/enforcer-switch reset` | Reset to config defaults |
|
|
92
|
+
| `/enforcer-reset` | Reset all enforcer state for this session |
|
|
93
|
+
|
|
94
|
+
### Keyboard Shortcut
|
|
95
|
+
|
|
96
|
+
| Shortcut | Action |
|
|
97
|
+
|----------|--------|
|
|
98
|
+
| `Ctrl+Shift+T` | Toggle enforcer enabled/disabled |
|
|
99
|
+
|
|
100
|
+
## Configuration
|
|
101
|
+
|
|
102
|
+
Create `~/.todo-enforcer.json` (global) or `.todo-enforcer.json` (project-level):
|
|
103
|
+
|
|
104
|
+
```jsonc
|
|
105
|
+
{
|
|
106
|
+
"enabled": true,
|
|
107
|
+
"maxInjections": 5, // Max injections per session
|
|
108
|
+
"cooldownMs": 5000, // Cooldown between injections (ms)
|
|
109
|
+
"completionSummary": false, // Suppress "all done" message (default)
|
|
110
|
+
"detectStagnation": true, // Stop injecting when stuck
|
|
111
|
+
"stagnationThreshold": 3, // Consecutive idle events before stagnation
|
|
112
|
+
"backoff": {
|
|
113
|
+
"enabled": true,
|
|
114
|
+
"factor": 2,
|
|
115
|
+
"maxDelayMs": 3600000,
|
|
116
|
+
"errorPatterns": ["429", "rate limit", "No deployments available"]
|
|
117
|
+
},
|
|
118
|
+
"messageDelivery": {
|
|
119
|
+
"mode": "userMessage", // "userMessage" or "customMessage"
|
|
120
|
+
"display": true,
|
|
121
|
+
"triggerTurn": true
|
|
122
|
+
},
|
|
123
|
+
"rules": [
|
|
124
|
+
{
|
|
125
|
+
"name": "incomplete-tasks-remain",
|
|
126
|
+
"condition": "has_incomplete",
|
|
127
|
+
"action": "prompt",
|
|
128
|
+
"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."
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"name": "all-complete-celebration",
|
|
132
|
+
"condition": "all_complete",
|
|
133
|
+
"action": "prompt",
|
|
134
|
+
"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."
|
|
135
|
+
}
|
|
136
|
+
]
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Built-in Conditions
|
|
141
|
+
|
|
142
|
+
| Condition | Description |
|
|
143
|
+
|-----------|-------------|
|
|
144
|
+
| `has_incomplete` | Any pending/in_progress tasks remain |
|
|
145
|
+
| `all_complete` | Every non-deleted task is completed |
|
|
146
|
+
| `has_in_progress` | At least one task is in_progress |
|
|
147
|
+
| `none` | Never matches (disabled rule) |
|
|
148
|
+
| `always` | Always matches |
|
|
149
|
+
|
|
150
|
+
### Template Variables
|
|
151
|
+
|
|
152
|
+
| Variable | Description |
|
|
153
|
+
|----------|-------------|
|
|
154
|
+
| `{{incomplete_count}}` | Number of incomplete tasks |
|
|
155
|
+
| `{{completed_count}}` | Number of completed tasks |
|
|
156
|
+
| `{{total_count}}` | Total non-deleted tasks |
|
|
157
|
+
| `{{incomplete_list}}` | Formatted list of incomplete tasks |
|
|
158
|
+
| `{{completed_list}}` | Formatted list of completed tasks |
|
|
159
|
+
| `{{latest_user_message}}` | Latest user message content |
|
|
160
|
+
| `{{assistant_messages}}` | Recent assistant messages |
|
|
161
|
+
|
|
162
|
+
### Actions
|
|
163
|
+
|
|
164
|
+
| Action | Description |
|
|
165
|
+
|--------|-------------|
|
|
166
|
+
| `prompt` | Inject a static template string |
|
|
167
|
+
| `external` | Call an external command or HTTP endpoint |
|
|
168
|
+
| `spawn` | Run `pi -p` in background with a template |
|
|
169
|
+
| `noop` | Do nothing (for logging/future use) |
|
|
170
|
+
|
|
171
|
+
## Architecture
|
|
172
|
+
|
|
173
|
+
```
|
|
174
|
+
todo-enforcer/
|
|
175
|
+
├── src/
|
|
176
|
+
│ ├── index.ts ← Entry point — hooks, commands, shortcuts
|
|
177
|
+
│ ├── config.ts ← Config loader + types + template interpolation
|
|
178
|
+
│ ├── conditions.ts ← Built-in + custom condition evaluator
|
|
179
|
+
│ ├── external-caller.ts ← External command/HTTP executor
|
|
180
|
+
│ ├── session-state.ts ← Per-session state tracking
|
|
181
|
+
│ ├── todo-snapshot.ts ← Reads todo state from session branch
|
|
182
|
+
│ ├── message-stall.ts ← Repeated message + rate limit guard
|
|
183
|
+
│ ├── type-guards.ts ← Runtime type guards
|
|
184
|
+
│ └── lib/
|
|
185
|
+
│ ├── plugin-logger.ts ← File-based structured logger
|
|
186
|
+
│ ├── hooks-manager.ts ← Hook registration and state
|
|
187
|
+
│ └── types.ts ← Shared types
|
|
188
|
+
├── tests/ ← Test suite (134 tests)
|
|
189
|
+
├── package.json
|
|
190
|
+
├── tsconfig.json
|
|
191
|
+
└── vitest.config.ts
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Development
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
# Install dependencies
|
|
198
|
+
npm install
|
|
199
|
+
|
|
200
|
+
# Run tests
|
|
201
|
+
npm test
|
|
202
|
+
|
|
203
|
+
# Run tests with coverage
|
|
204
|
+
npm run test:ci
|
|
205
|
+
|
|
206
|
+
# Type check
|
|
207
|
+
npm run typecheck
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## License
|
|
211
|
+
|
|
212
|
+
MIT
|
|
213
|
+
|
|
214
|
+
## Repository
|
|
215
|
+
|
|
216
|
+
**GitHub**: [buihongduc132/todo-enforcer](https://github.com/buihongduc132/todo-enforcer)
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "todo-enforcer",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Todo continuation enforcer for pi-coding-agent — monitors todo state and injects prompts to keep agents working until all tasks complete",
|
|
5
|
+
"main": "src/index.ts",
|
|
6
|
+
"types": "src/index.ts",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"test": "vitest run",
|
|
10
|
+
"test:watch": "vitest",
|
|
11
|
+
"test:ci": "vitest run --coverage",
|
|
12
|
+
"typecheck": "tsc --noEmit",
|
|
13
|
+
"prepublishOnly": "tsc --noEmit && vitest run"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"pi-package",
|
|
17
|
+
"pi-extension",
|
|
18
|
+
"pi",
|
|
19
|
+
"pi-coding-agent",
|
|
20
|
+
"todo",
|
|
21
|
+
"enforcer",
|
|
22
|
+
"task-continuation",
|
|
23
|
+
"agent-productivity"
|
|
24
|
+
],
|
|
25
|
+
"author": "buihongduc132",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/buihongduc132/todo-enforcer.git"
|
|
30
|
+
},
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/buihongduc132/todo-enforcer/issues"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://github.com/buihongduc132/todo-enforcer#readme",
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"typescript": "^5.8.0",
|
|
37
|
+
"vitest": "^3.2.0"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"@mariozechner/pi-coding-agent": ">=0.1.0"
|
|
41
|
+
},
|
|
42
|
+
"peerDependenciesMeta": {
|
|
43
|
+
"@mariozechner/pi-coding-agent": {
|
|
44
|
+
"optional": false
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"files": [
|
|
48
|
+
"src/",
|
|
49
|
+
"todo-enforcer.example.json",
|
|
50
|
+
"README.md",
|
|
51
|
+
"LICENSE"
|
|
52
|
+
]
|
|
53
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* conditions — Built-in and extensible condition evaluator
|
|
3
|
+
*
|
|
4
|
+
* Evaluates rule conditions against a TodoSnapshot.
|
|
5
|
+
* Custom conditions can be registered via registerCondition().
|
|
6
|
+
*/
|
|
7
|
+
// @ts-nocheck
|
|
8
|
+
|
|
9
|
+
//
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
import { createPluginLogger } from "./lib/plugin-logger";
|
|
13
|
+
import type { TodoSnapshot } from "./config";
|
|
14
|
+
|
|
15
|
+
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
export type ConditionFn = (snapshot: TodoSnapshot) => boolean;
|
|
18
|
+
|
|
19
|
+
// ─── Built-in conditions ────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
const logger = createPluginLogger("todo-enforcer");
|
|
22
|
+
|
|
23
|
+
const builtinConditions: Record<string, ConditionFn> = {
|
|
24
|
+
has_incomplete: (s) => s.incompleteCount > 0,
|
|
25
|
+
all_complete: (s) => s.totalCount > 0 && s.incompleteCount === 0,
|
|
26
|
+
has_in_progress: (s) => s.inProgressCount > 0,
|
|
27
|
+
none: () => false,
|
|
28
|
+
always: () => true,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// ─── Registry ────────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
const customConditions: Map<string, ConditionFn> = new Map();
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Register a custom condition function.
|
|
37
|
+
* Custom conditions override built-in conditions of the same name.
|
|
38
|
+
*/
|
|
39
|
+
export function registerCondition(name: string, fn: ConditionFn): void {
|
|
40
|
+
customConditions.set(name, fn);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Evaluate a condition against a todo snapshot.
|
|
45
|
+
* Returns true if the condition matches.
|
|
46
|
+
*/
|
|
47
|
+
export function evaluateCondition(
|
|
48
|
+
conditionName: string,
|
|
49
|
+
snapshot: TodoSnapshot,
|
|
50
|
+
): boolean {
|
|
51
|
+
// Custom conditions take priority
|
|
52
|
+
const custom = customConditions.get(conditionName);
|
|
53
|
+
if (custom) {
|
|
54
|
+
try {
|
|
55
|
+
return custom(snapshot);
|
|
56
|
+
} catch (err) {
|
|
57
|
+
logger.error(`Custom condition "${conditionName}" threw`, err);
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const builtin = builtinConditions[conditionName];
|
|
63
|
+
if (builtin) {
|
|
64
|
+
return builtin(snapshot);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
logger.warn(`Unknown condition: "${conditionName}" — treating as no-match`);
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Get all registered condition names (built-in + custom).
|
|
73
|
+
*/
|
|
74
|
+
export function getRegisteredConditions(): string[] {
|
|
75
|
+
return [
|
|
76
|
+
...Object.keys(builtinConditions),
|
|
77
|
+
...Array.from(customConditions.keys()),
|
|
78
|
+
];
|
|
79
|
+
}
|