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 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
+ [![npm version](https://img.shields.io/npm/v/todo-enforcer?style=flat-square)](https://www.npmjs.com/package/todo-enforcer)
6
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue?style=flat-square)](LICENSE)
7
+ [![pi-package](https://img.shields.io/badge/pi-package-blue?style=flat-square)](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
+ }