@dumbmice/pi-todo-loop 0.1.1 → 0.2.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/AGENTS.md +67 -55
- package/CHANGELOG.md +63 -12
- package/README.md +25 -24
- package/docs/behaviour.md +68 -63
- package/docs/pi-api-alignment.md +17 -20
- package/package.json +2 -2
- package/scripts/check-pack.mjs +0 -1
- package/src/config.ts +0 -8
- package/src/index.ts +156 -83
- package/src/loop.ts +36 -26
- package/src/inject.ts +0 -97
package/AGENTS.md
CHANGED
|
@@ -7,21 +7,22 @@ does and how to install it, read [README.md](README.md).
|
|
|
7
7
|
|
|
8
8
|
```
|
|
9
9
|
src/index.ts pi wiring: the context hook, the turn_end hook, /todoloop
|
|
10
|
-
src/loop.ts the pure decision policy (rounds,
|
|
10
|
+
src/loop.ts the pure decision policy (rounds, stalls, hint, anchor reset)
|
|
11
11
|
src/state.ts reads rpiv-todo state off the session branch
|
|
12
|
-
src/inject.ts renders the per-request <todo_loop_state> block
|
|
13
12
|
src/config.ts config load and coercion, defaults on any failure
|
|
14
|
-
test/loop.test.ts decision policy:
|
|
13
|
+
test/loop.test.ts decision policy: cap, stalls, hint, anchor reset, error outcomes
|
|
15
14
|
test/state.test.ts branch reader: both shapes, recency preference, malformed input
|
|
16
|
-
test/
|
|
15
|
+
test/config.test.ts config coercion: clamps, garbage, below-1 values
|
|
17
16
|
test/index.test.ts the pi-facing half through a stub `pi`: tail handling, kill switch, caps
|
|
18
17
|
test/state-machine.test.ts the counters as a table of event sequences (the suite to extend)
|
|
19
18
|
test/helpers/harness.ts the stub `pi`, mutable branch and scripted context those two share
|
|
20
19
|
test/fixture.test.ts the same hooks over real captured lines (test/fixtures/branch.jsonl)
|
|
21
20
|
test/docs.test.ts pins the version table, invariant map, reason table, config table, links
|
|
22
21
|
scripts/check-pack.mjs asserts the published tarball contains what package.json promises
|
|
22
|
+
scripts/pack-json.mjs reads `npm pack --json` in both shapes (array, object)
|
|
23
|
+
test/pack-json.test.ts pins both shapes
|
|
23
24
|
docs/pi-api-alignment.md every deliberate workaround over pi's documented extension API
|
|
24
|
-
docs/behaviour.md how it behaves, precisely: markers, counters, the block, cost
|
|
25
|
+
docs/behaviour.md how it behaves, precisely: markers, counters, stalls, the block tool, cost
|
|
25
26
|
README.md user-facing documentation (what it is, install, config, troubleshooting)
|
|
26
27
|
AGENTS.md this file
|
|
27
28
|
CONTRIBUTING.md four-line pointer to this file (GitHub convention)
|
|
@@ -64,45 +65,47 @@ These are the invariants the suite protects. Changing one means changing its tes
|
|
|
64
65
|
README text, the matching `docs/pi-api-alignment.md` item (or marking that item inapplicable), and
|
|
65
66
|
adding a `CHANGELOG.md` line — all in the same commit.
|
|
66
67
|
|
|
67
|
-
- **Merge only into a `user` or `custom` tail.** pi rebuilds `bashExecution`/`branchSummary`/
|
|
68
|
-
`compactionSummary` content from their own fields and ignores `content`, so merging into those would
|
|
69
|
-
delete the block; they get a separate message and a documented adjacency (register item 15).
|
|
70
68
|
- **A `todo`-only clear is announced, not policed.** The loop's success and failure states are the same
|
|
71
69
|
observable; a warning is the most it may do about it.
|
|
72
|
-
- **
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
- **The marker body is
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
70
|
+
- **The extension never writes task text into a request.** The list reaches the model only through
|
|
71
|
+
the `todo` tool's own results. Nothing here renders a subject, a status or an id, so there is no
|
|
72
|
+
escaping to get wrong and no way for a task subject to forge an instruction. Keep it that way: a
|
|
73
|
+
future "show the list in the prompt" feature reintroduces the whole sanitiser.
|
|
74
|
+
- **The `context` hook may remove a marker and must never add anything.** A marker is dropped when
|
|
75
|
+
either non-marker neighbour is user-role (next to a human message, or in a run), because there it
|
|
76
|
+
would project to `user → user`. A marker at the tail is the nudge and always stays: dropping it
|
|
77
|
+
would leave the continuation ending on an assistant turn, which pi refuses to continue from. The
|
|
78
|
+
test `no returned request ever contains two adjacent user-role messages` is the general statement;
|
|
79
|
+
if you change the filter, run it first.
|
|
80
|
+
- **The marker body is one of two constants** — `MARKER_BODY` (plain) and `MARKER_BODY_HINT` (plain
|
|
81
|
+
plus the `todo_loop_block` clause), both in `src/index.ts`. A third variant is a design decision, not
|
|
82
|
+
a convenience. Neither carries a round counter, because a persisted marker must not claim the loop is
|
|
83
|
+
on round `n` long after that chain ended. Counters live in memory and are reported by `/todoloop`.
|
|
84
|
+
- **The only *variable* agent-facing text is the nudge.** One of two fixed sentences, appended once
|
|
85
|
+
per continuation. No state block, no reminder, no per-request rewrite: the list is the agent's to
|
|
86
|
+
read with the todo tool. The tool declaration (`todo_loop_block`) is constant and carries no task
|
|
87
|
+
state, but it is in every request — count it in cost claims. The non-assistant constraint is pi's `turn_end` projection check, not the request (register
|
|
88
|
+
item 3) — do not re-derive a coupling between the two.
|
|
88
89
|
- **One entry per continuation is a floor, not a choice.** pi has no `resume()` that appends nothing.
|
|
89
90
|
Do not try to reach zero entries; keep the single `custom_message` with `display: false`.
|
|
90
91
|
- **The marker `customType` is a stable literal.** The tail check must keep recognising markers written
|
|
91
92
|
by earlier builds; do not derive it from the package name or version.
|
|
92
|
-
- **Task state is read from the branch on every
|
|
93
|
+
- **Task state is read from the branch on every turn.** No in-memory cache of the list, no
|
|
93
94
|
`session_compact` resync handler: memory would drift across forks and compaction. Loop state
|
|
94
95
|
(counters, anchor, pause) *is* in memory, and a restart necessarily starts a new budget.
|
|
95
96
|
- **The decision is a pure function.** `decide()` in `src/loop.ts` takes a value and returns a value.
|
|
96
97
|
It must stay free of pi, filesystem and time so the caps can be tested exhaustively.
|
|
97
98
|
- **Only a settle spends a round.** `turn_end` fires for every assistant response, including one that
|
|
98
99
|
ended in a tool call; those already cause pi's next request, so the hook returns before deciding. A
|
|
99
|
-
round, a marker and a
|
|
100
|
-
- **A tool turn clears
|
|
101
|
-
which records the stop and leaves the
|
|
102
|
-
they must not consume
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
100
|
+
round, a marker and a stall are therefore always about a turn that would have ended the run.
|
|
101
|
+
- **A tool turn clears the stall and spends no round** — except a tool turn that ends `error`/`aborted`,
|
|
102
|
+
which records the stop and leaves the counter alone. Tool turns already cause pi's next request, so
|
|
103
|
+
they must not consume a round; but they are work, so they reset `stalled` and re-arm the block hint.
|
|
104
|
+
- **Nothing stops the loop for being quiet.** There is no strike limit: `stalled` decides only when the
|
|
105
|
+
nudge offers `todo_loop_block`, once per stall, and tool work re-arms it. A blocked agent and a lazy
|
|
106
|
+
one are indistinguishable from here, so the agent is asked rather than judged; the only stops are a
|
|
107
|
+
clear list, the round cap, an error, and the human's own `todo_loop_block` call.
|
|
108
|
+
- **A stop keeps the counters; only a new human message resets them.** Clearing `rounds` / `stalled` on
|
|
106
109
|
stop re-arms a full budget on the next `turn_end` under the same anchor (another extension's
|
|
107
110
|
continuation, a provider retry). The anchor comparison is the only *automatic* reset;
|
|
108
111
|
`/todoloop reset` and process start clear the counters explicitly. A queued human message is a
|
|
@@ -120,7 +123,7 @@ adding a `CHANGELOG.md` line — all in the same commit.
|
|
|
120
123
|
continues, but the loop would then stop silently — inert is better than loud.
|
|
121
124
|
- **Nothing is written.** No files, no network, no process execution. The only reads are the optional
|
|
122
125
|
config, the two kill-switch paths and the session branch.
|
|
123
|
-
- **The kill switch is checked in both hooks
|
|
126
|
+
- **The kill switch is checked in both hooks and in `todo_loop_block`'s `execute`.** Disabling takes effect on the next request, without
|
|
124
127
|
`/reload`.
|
|
125
128
|
- **No pi type import; `pi: any` at the registration boundary.** Contexts the file reads are typed
|
|
126
129
|
structurally (`SessionContext`). Adding the pi types would make a dependency out of a package that
|
|
@@ -135,9 +138,9 @@ introduces an edge.
|
|
|
135
138
|
|---|---|
|
|
136
139
|
| continues only while items are open | `continues while work is open and the turn did work`, `stops when the list is clear`, `stops when there is no list at all` |
|
|
137
140
|
| error and aborted outcomes never continue | `never continues on an error or aborted turn` |
|
|
138
|
-
| a no-progress
|
|
139
|
-
| the first sighting of a list is progress, not a
|
|
140
|
-
| a list change
|
|
141
|
+
| a no-progress settle never stops the loop; the block hint is offered once per stall | `a no-progress settle continues, and offers the block hint once` |
|
|
142
|
+
| the first sighting of a list is progress, not a stall | `the first sighting of a list counts as progress, not a stall` |
|
|
143
|
+
| a list change clears the stall and re-arms the hint | `a list change clears the stall and re-arms the hint` |
|
|
141
144
|
| the round cap stops the loop | `round cap stops the loop`, `config caps are honoured` |
|
|
142
145
|
| a new human message resets the budget | `a new human message resets the budget` |
|
|
143
146
|
| a stop keeps the counters for the anchor, and a new message clears them | `a stop keeps the counters, so a cap stays shut for the same anchor`, `a new human message after a stop starts a fresh budget`, `the round cap is sticky for one human message` |
|
|
@@ -146,10 +149,6 @@ introduces an edge.
|
|
|
146
149
|
| the anchor is the newest `role:"user"` message without a `customType` — not assistant, toolResult, our own marker, or another extension's custom user message | `anchor is the newest human message id, ignoring assistant and toolResult`, `our own projected marker never becomes the anchor`, `a user message without an id anchors on its position` |
|
|
147
150
|
| `deleted` tasks leave list, open set and signature | `deleted tasks are excluded from tasks, open and signature` |
|
|
148
151
|
| malformed entries degrade to the empty snapshot | `malformed or missing state degrades to the empty snapshot` |
|
|
149
|
-
| config defaults survive garbage; fields are coerced | `missing or malformed config falls back to defaults`, `valid fields override, invalid ones are ignored` |
|
|
150
|
-
| the reconciliation clause appears only after one continuation | `the reconciliation clause is off on the first nudge`, `the reconciliation clause appears after one continuation, update before remove`, `the block asks for reconciliation only from the second request of a chain` |
|
|
151
|
-
| model-authored task text is sanitised before it enters a request | `a hostile task subject cannot break out of the tag` |
|
|
152
|
-
| the injection shows open tasks only, bounded, `activeForm` for in_progress | `injection lists open tasks with their status`, `injection is bounded and reports the overflow`, `injection uses activeForm only for in_progress`, `injection tolerates an empty subject` |
|
|
153
152
|
| a status change moves the signature | `signature changes when a status changes` |
|
|
154
153
|
| a record with neither a label nor a status is not a task, and arrays are not tasks | `a record with neither a label nor a status is not a task`, `an array element is not a task` |
|
|
155
154
|
| only a `todo` tool's own shapes are read | `a foreign custom entry or toolResult carrying tasks is ignored` |
|
|
@@ -157,23 +156,36 @@ introduces an edge.
|
|
|
157
156
|
| ids are made distinct: a synthetic or colliding id moves to a free number, a unique real id is never renumbered | `a positional id never shadows a real one`, `duplicate ids in the source are made distinct`, `a synthetic id never renumbers a real one, even when it comes first` |
|
|
158
157
|
| a task without a usable id falls back to its position | `a task without a usable id falls back to its position, not to being dropped`, `a null or empty id falls back to the position instead of colliding on 0` |
|
|
159
158
|
| the newer shape wins, in either order | `the newer of a custom entry and a toolResult wins, in either order` |
|
|
160
|
-
| the block is appended only when injecting, replacing a tail marker | `context strips the marker and appends the state block when tasks are open`, `a tail marker is replaced by the block, never followed by it`, `the block is appended when the tail is an assistant turn`, `the request never ends with two user-role messages` |
|
|
161
|
-
| history markers are left in place; they are the alternation glue | `history markers are left alone when nothing is injected`, `history markers are left alone when injection is disabled` |
|
|
162
159
|
| a malformed context payload is not rewritten | `context leaves a malformed payload alone instead of rewriting it` |
|
|
163
|
-
| a task subject cannot break out of the injection tag | `a hostile task subject cannot break out of the tag` |
|
|
164
|
-
| the block ends on the concession, not on a call to act | `the block ends on the concession, not on a call to act` |
|
|
165
|
-
| pause stops the block as well as the continuation | `pause stops both the continuation and the steering block` |
|
|
166
160
|
| either kill switch disables both hooks | `the global kill switch disables both hooks`, `the project-local kill switch disables both hooks` |
|
|
167
161
|
| only a settle spends a round: tool turns cost nothing | `a turn that ran tools spends no round and appends no marker` |
|
|
168
|
-
| a tool turn clears
|
|
162
|
+
| a tool turn clears the stall and re-arms the hint | `tool turns clear the stall but never spend a round` |
|
|
163
|
+
| the block hint is offered once per stall, on the repeat stop | `the nudge offers todo_loop_block once, after a repeat stop` |
|
|
164
|
+
| `todo_loop_block` stops the loop and reports the reason | `todo_loop_block stops the loop for this message and reports the reason` |
|
|
165
|
+
| a block without a reason is refused | `a block without a reason is refused and does not stop the loop` |
|
|
166
|
+
| the human's next message clears the block | `the human's next message clears the block` |
|
|
167
|
+
| the block stops the loop and the next human message resumes it, end to end | `state machine: the agent asks for a person: the loop stops until the human answers` |
|
|
168
|
+
| a block survives pause and resume, and reset clears it | `state machine: a block survives pause and resume, and reset clears it`, `/todoloop reset clears a pending block` |
|
|
169
|
+
| the block's tool turn costs no round, and no marker is written after it | `state machine: the block call's tool turn costs no round, and the settle after it stops` |
|
|
170
|
+
| a model-authored reason is flattened and bounded | `a reason is flattened to one line and bounded` |
|
|
171
|
+
| the kill switch silences the block tool too | `the kill switch also silences todo_loop_block` |
|
|
172
|
+
| a paused loop refuses the block tool and stays silent | `a paused loop refuses todo_loop_block and stays silent` |
|
|
173
|
+
| a stall clause that lands on the cap round is reported, not dropped | `state machine: cap stays shut across a repeated settle under the same message`, `a stale list stops on the cap with a reason that says so` |
|
|
174
|
+
| the next human message clears the block and the stale `last:` | `the human's next message clears the block` |
|
|
169
175
|
| a continuation appends one hidden `custom_message` with a constant body and preserves prior proposals | `turn_end requests one turn with a hidden marker when work is open` |
|
|
170
|
-
| a user-role tail receives the block instead of a second message (marker first dropped) | `a human-role tail receives the block instead of a second user message`, `a marker after a human message still yields one user-role tail` |
|
|
171
176
|
| a malformed event payload does not throw | `turn_end tolerates a malformed event payload instead of throwing` |
|
|
172
|
-
| clamped and invalid config values fall back | `numbers are clamped so a typo cannot defeat the cap it configures`, `a value below 1 is treated as invalid, not as zero` |
|
|
177
|
+
| clamped and invalid config values fall back | `numbers are clamped so a typo cannot defeat the cap it configures`, `a value below 1 is treated as invalid, not as zero`, `valid fields override, invalid ones are ignored` |
|
|
173
178
|
| an errored or aborted turn is never continued | `turn_end stops on an errored or aborted turn and does not spend a round` |
|
|
174
|
-
| the cap and pause gate the loop at the hook level | `the round cap is sticky for one human message`, `pause stops
|
|
179
|
+
| the cap and pause gate the loop at the hook level | `the round cap is sticky for one human message`, `pause stops the continuation and the marker cleanup` |
|
|
180
|
+
| the prompt is never rewritten with task state | `the prompt is never rewritten with task state`, `context removes a marker next to a human message and adds nothing` |
|
|
181
|
+
| an ordinary request comes back untouched | `an ordinary request is not touched at all`, `a request with no markers and no continuation is never rewritten` |
|
|
182
|
+
| a human's own message is never edited | `the human's own message is left exactly as sent` |
|
|
183
|
+
| a marker next to a human message is removed; markers between assistant turns stay | `a marker after a human message is dropped, not merged`, `a marker between an assistant turn and a human message is dropped`, `a marker between two assistant turns is left alone` |
|
|
184
|
+
| the tail marker survives as the nudge | `the tail marker is the nudge and is left alone`, `a run of markers at the tail keeps one, so the nudge survives` |
|
|
185
|
+
| a real continuation request ends on a marker and gains nothing | `the context hook keeps real markers and adds nothing to the request`, `the captured markers are the shape the context hook looks for` |
|
|
175
186
|
| the docs match the code | `the pi snapshot constant appears in the README and the register`, `the verification date appears in the README and the register`, `the tested rpiv-todo version is recorded in both places`, `the CHANGELOG documents the version in package.json`, `every README link target inside the repository exists`, `every test name the invariant map cites actually exists`, `every stop reason the code can report appears in the README table`, `the README configuration table states every default, not just the numbers` |
|
|
176
187
|
|
|
188
|
+
|
|
177
189
|
## Adding or changing a continuation rule
|
|
178
190
|
|
|
179
191
|
1. Put the logic in `decide()` (`src/loop.ts`), not in the hook. The hook only adapts events and
|
|
@@ -193,7 +205,7 @@ Ranked by how easily a small change breaks them.
|
|
|
193
205
|
1. **Two different tails.** A continuation whose *persisted projection* ends on an assistant message is
|
|
194
206
|
rejected by pi (`canContinue`); the *request* has no such constraint but must keep role alternation.
|
|
195
207
|
Conflating the two produced the round-2 marker leak. `test/index.test.ts` pins both shapes.
|
|
196
|
-
2. **Counter lifecycle.** `rounds`, `
|
|
208
|
+
2. **Counter lifecycle.** `rounds`, `stalled` and `anchor` are mutated in one place and reset in three
|
|
197
209
|
(anchor change, `/todoloop reset`, process start). A missing reset either loops forever or refuses to
|
|
198
210
|
continue after one bad turn; a reset in the wrong place (inside `stop()`) silently re-arms the cap.
|
|
199
211
|
3. **The anchor.** It must be human-authored and never empty. `#<index>` is the fallback when an entry
|
|
@@ -217,7 +229,7 @@ network. `test/state-machine.test.ts` is the one to extend when you touch the co
|
|
|
217
229
|
the vocabulary pi actually produces — `settle` (a response with no tool calls), `tool` (a response that
|
|
218
230
|
ended in tool calls), `edit` (the list changed), `human` (a new message), plus pause/resume/reset/
|
|
219
231
|
error/abort/kill-switch — and asserts the whole outcome at once: continuations, markers, rounds,
|
|
220
|
-
|
|
232
|
+
stalls, the block tool, the reason `/todoloop` prints and its header. Every release-blocking defect in this repository's
|
|
221
233
|
history lived in that interaction, so a new rule belongs there as a row in `SCENARIOS` before it belongs
|
|
222
234
|
in prose.
|
|
223
235
|
|
|
@@ -248,13 +260,13 @@ grep -c 'todo-loop:continue' ~/.pi/agent/sessions/<project>/*.jsonl # one per
|
|
|
248
260
|
- `README.md` is for users of the extension, read top-down on npm: what it does, quick start, one
|
|
249
261
|
example, when it helps and when it does not, configuration, commands, "is it working", troubleshooting,
|
|
250
262
|
uninstall, requirements. No implementation inventories, no counter mechanics, no CI or release steps.
|
|
251
|
-
- `docs/behaviour.md` is the depth the README links to: marker lifetime, role alternation,
|
|
252
|
-
|
|
263
|
+
- `docs/behaviour.md` is the depth the README links to: marker lifetime, role alternation, stalls,
|
|
264
|
+
counter semantics, reason strings, where the list is read from, scope, cost. Technical, but
|
|
253
265
|
written for a user debugging behaviour rather than a contributor.
|
|
254
266
|
- `docs/pi-api-alignment.md` is the register of deliberate workarounds over pi's documented extension
|
|
255
267
|
API, and the record of which versions those were established against. Add an item when behaviour
|
|
256
268
|
deviates; items with no output signal are marked _(silent)_. The tested-version table at the top,
|
|
257
|
-
`PI_API_SNAPSHOT` in `src/index.ts`, and the README *
|
|
269
|
+
`PI_API_SNAPSHOT` in `src/index.ts`, and the README *Requirements* section move together in the
|
|
258
270
|
same commit as the change that depends on them, and `test/docs.test.ts` fails if the constant, the
|
|
259
271
|
date or the tested rpiv-todo version goes missing from either file. Note that `@juicesharp/rpiv-todo`
|
|
260
272
|
is a *shape* dependency only (register item 12): it is never imported, so it has no `package.json`
|
|
@@ -287,7 +299,7 @@ and read the pi changelog for `custom_message`, `display`, `canContinue`, plus r
|
|
|
287
299
|
- **The decision is at `turn_end`, one boundary early.** pi 1.5.0 documents `agent_before_settle` as
|
|
288
300
|
the final actionable boundary that can append entries and request one continuation, and at
|
|
289
301
|
`turn_end` the hook has to reconstruct "the run would otherwise end" from `toolResults.length === 0`.
|
|
290
|
-
The consequence is a bounded wasted round when a human message is queued mid-turn, and the
|
|
302
|
+
The consequence is a bounded wasted round when a human message is queued mid-turn, and the stall
|
|
291
303
|
reset lives in the adapter rather than in `decide()`. Moving the decision is the 0.2.0 change; it must
|
|
292
304
|
ship with a CHANGELOG line because it changes which turns spend a round.
|
|
293
305
|
- **`getBranch()` is not the model-visible context** (register item 16): after compaction the panel can
|
package/CHANGELOG.md
CHANGED
|
@@ -4,14 +4,69 @@ All notable changes to this project are documented here. The format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project adheres to
|
|
5
5
|
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.2.0] - 2026-11-05
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `todo_loop_block`, a tool the agent can call when it cannot proceed without a person. It takes a
|
|
12
|
+
reason, stops continuations for the rest of that message, notifies the human, and shows
|
|
13
|
+
`waiting for you: <reason>` in `/todoloop`. The human's next message resumes the loop; there is no
|
|
14
|
+
unblock call. The nudge offers this tool once per stall — on the first settle since progress that
|
|
15
|
+
did not move the list — which is the shape a blocked agent has. If that settle is the one the round
|
|
16
|
+
cap ends, the stop reason says `the agent stalled and may need a person`, so the hint is not lost.
|
|
17
|
+
- `MARKER_BODY_HINT`: the same nudge plus that one clause. Both variants end on the concession that the
|
|
18
|
+
human outranks the loop.
|
|
19
|
+
|
|
20
|
+
### Removed
|
|
21
|
+
|
|
22
|
+
- **The injected state block is gone.** The extension no longer writes task state into a request: no
|
|
23
|
+
`<todo_loop_state>` list, no reconciliation clause, no truncation variant, no merge into a `user` or
|
|
24
|
+
`custom` tail. The only variable text it gives the model is the one-sentence nudge appended with each
|
|
25
|
+
continuation, and that nudge now points at the todo tool instead of restating the list. Reading and
|
|
26
|
+
updating the list is the agent's job; the prompt stays the human's.
|
|
27
|
+
- `injectState` and `maxInjectedTasks` are gone with it. A config file that still carries them loads
|
|
28
|
+
fine — unknown keys are ignored — so upgrading needs no edit.
|
|
29
|
+
- `maxStrikes`. The loop no longer stops for being quiet: two no-progress settles used to end a run, and
|
|
30
|
+
a stalled agent that was waiting for a person was abandoned with "no progress". The counter is now
|
|
31
|
+
`stalled`, uncapped, and it only decides when to offer `todo_loop_block`. A `maxStrikes` key in an
|
|
32
|
+
existing config is ignored.
|
|
33
|
+
- The renderer and its sanitiser (`src/inject.ts`, `test/inject.test.ts`). Config coercion moved to
|
|
34
|
+
`test/config.test.ts`, because it outlived the renderer.
|
|
35
|
+
|
|
36
|
+
### Upgrade notes
|
|
37
|
+
|
|
38
|
+
- `maxRounds` is now the only cost knob: a model that keeps stopping without moving the list runs to
|
|
39
|
+
the cap instead of stopping at two, and each continuation re-sends the conversation.
|
|
40
|
+
- A running session keeps the old build until `/reload` or a restart, and that starts a fresh budget.
|
|
41
|
+
- 0.1.1 re-injected the open-task list into every request after a compaction. That is gone: the model
|
|
42
|
+
now sees the list only when it calls `todo` or is nudged, so a long session right after a compaction
|
|
43
|
+
may take one extra turn before the agent knows what is left.
|
|
44
|
+
|
|
45
|
+
### Changed
|
|
46
|
+
|
|
47
|
+
- `context` may only *remove* a marker, never add one. It drops a marker whose neighbour is user-role
|
|
48
|
+
(next to a human message, or in a run of markers) and returns `undefined` for every other request, so
|
|
49
|
+
an ordinary turn is passed through as the object pi already built. A marker at the tail always
|
|
50
|
+
survives: it is the nudge, and dropping it would leave the continuation ending on an assistant turn.
|
|
51
|
+
- `staleSettles` no longer drives injected wording. It still counts settles since the list last
|
|
52
|
+
changed and still decorates the cap reason (`, list unchanged for N settles`).
|
|
53
|
+
|
|
54
|
+
### Fixed
|
|
55
|
+
|
|
56
|
+
- A run of adjacent continuation markers keeps its first marker. Testing only the literal neighbours
|
|
57
|
+
dropped the whole run, leaving two assistant turns adjacent — the alternation the filter exists to
|
|
58
|
+
prevent.
|
|
59
|
+
- The publish workflow's release step is idempotent: re-running a tag edits the existing release
|
|
60
|
+
instead of failing on "release already exists".
|
|
61
|
+
|
|
7
62
|
## [0.1.1] - 2026-11-04
|
|
8
63
|
|
|
9
64
|
### Added
|
|
10
65
|
|
|
11
|
-
- The injected block escalates once
|
|
12
|
-
reconcile a drifted list — update statuses, add what is missing, remove what no longer applies
|
|
13
|
-
|
|
14
|
-
|
|
66
|
+
- The injected block escalates once, from the first continuation of a chain: it asks the model to
|
|
67
|
+
reconcile a drifted list — update statuses, add what is missing, remove what no longer applies — so a
|
|
68
|
+
stale list has an exit short of burning the round cap. (Known issue, fixed in the entry above: the
|
|
69
|
+
clause was armed by the round counter, so it also reached the first request of a new human message.)
|
|
15
70
|
|
|
16
71
|
### Fixed
|
|
17
72
|
|
|
@@ -36,8 +91,8 @@ Initial release.
|
|
|
36
91
|
never persisted, closing the post-compaction context hole
|
|
37
92
|
([rpiv-mono#222](https://github.com/juicesharp/rpiv-mono/issues/222)) and the "model
|
|
38
93
|
never calls the todo tool's `list` action" failure ([rpiv-mono#159](https://github.com/juicesharp/rpiv-mono/issues/159)).
|
|
39
|
-
- Hidden continuation marker (`custom_message`, `display: false`, constant
|
|
40
|
-
|
|
94
|
+
- Hidden continuation marker (`custom_message`, `display: false`, a constant instruction body,
|
|
95
|
+
~220 bytes as a session entry). Markers are kept in history: they sit
|
|
41
96
|
between assistant turns and are what keeps the conversation alternating. A marker at the tail is
|
|
42
97
|
replaced by the state block, so a request never ends on two user-role messages.
|
|
43
98
|
- Task state read from `rpiv-todo-state` custom entries with a `todo` toolResult
|
|
@@ -61,12 +116,6 @@ Initial release.
|
|
|
61
116
|
(`settle`, `tool`, `edit`, `human`, pause/resume/reset, error/abort, kill switch), asserting
|
|
62
117
|
continuations, markers, rounds, strikes, the `/todoloop` reason and header at once. Re-introducing any
|
|
63
118
|
of the three historical budget defects fails it.
|
|
64
|
-
- The continuation marker's constant body states the instruction ("Continue with the open todo
|
|
65
|
-
tasks…") instead of a self-closing tag, so `injectState: false` no longer continues with no
|
|
66
|
-
instruction at all, and a marker left in history is self-explanatory.
|
|
67
|
-
- The injected block escalates once: from the first continuation onwards it asks the model to
|
|
68
|
-
reconcile a drifted list (update statuses, add what is missing, remove what no longer applies —
|
|
69
|
-
in that order), which gives a stale list a deterministic exit short of burning the round cap.
|
|
70
119
|
- A list that goes open → closed in a turn whose only tool results were `todo` is announced once: the
|
|
71
120
|
loop's success state and its failure state are the same observable.
|
|
72
121
|
- A stale-list burn now says so: the round-cap reason appends `, list unchanged for N settles`.
|
|
@@ -83,3 +132,5 @@ Initial release.
|
|
|
83
132
|
`.github/`) leaks into it (`npm pack --dry-run` exits 0 otherwise).
|
|
84
133
|
|
|
85
134
|
[0.1.0]: https://github.com/DumbMice/pi-todo-loop/releases/tag/v0.1.0
|
|
135
|
+
[0.1.1]: https://github.com/DumbMice/pi-todo-loop/releases/tag/v0.1.1
|
|
136
|
+
[Unreleased]: https://github.com/DumbMice/pi-todo-loop/compare/v0.1.1...HEAD
|
package/README.md
CHANGED
|
@@ -9,8 +9,8 @@ Keeps the [pi coding agent](https://pi.dev) working while its todo list still ha
|
|
|
9
9
|
|
|
10
10
|
`rpiv-todo` gives the agent a task list and shows it to you. It never asks pi for another turn, so the
|
|
11
11
|
agent still stops halfway and hands control back. This extension adds the turn: while tasks are open it
|
|
12
|
-
keeps the agent going,
|
|
13
|
-
|
|
12
|
+
keeps the agent going, and stops when you clear the list — or when the agent tells you it needs a
|
|
13
|
+
person. It never writes the list into your prompt: the agent reads it with its own todo tool.
|
|
14
14
|
|
|
15
15
|
## Quick start
|
|
16
16
|
|
|
@@ -46,53 +46,54 @@ pi: All 12 tasks are complete; here is what was verified …
|
|
|
46
46
|
← the list is clear, so the loop stops and stays stopped.
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
It stops for
|
|
49
|
+
It stops for one of these reasons, and `/todoloop` always says which:
|
|
50
50
|
|
|
51
51
|
| Why it stopped | What you see |
|
|
52
52
|
|---|---|
|
|
53
53
|
| the list is clear | `stopped: list is clear` — the normal ending |
|
|
54
|
-
| the model
|
|
55
|
-
| the budget is spent | `stopped: round cap 12 reached, 3 open` — 12 continuations for your one message |
|
|
54
|
+
| the model said it needs a person | `waiting for you: waiting on Dana's review` — it called `todo_loop_block`; your next message resumes |
|
|
55
|
+
| the budget is spent | `stopped: round cap 12 reached, 3 open` — 12 continuations for your one message; if it ends with `the agent stalled and may need a person`, ask it what it is waiting on |
|
|
56
|
+
| the turn failed | `stopped: turn outcome was error` — the provider failed, or you aborted it |
|
|
56
57
|
|
|
57
58
|
## When it helps
|
|
58
59
|
|
|
59
60
|
- Long, multi-step work where the agent summarises and hands back halfway.
|
|
60
|
-
- Sessions long enough to be compacted — the list survives on disk
|
|
61
|
+
- Sessions long enough to be compacted — the list survives on disk; the nudge is what gets the model to
|
|
62
|
+
read it again.
|
|
61
63
|
- A model that runs tools but rarely updates its list. It will still be nudged; it may keep working to
|
|
62
64
|
the round cap.
|
|
63
65
|
|
|
64
66
|
## When it does not
|
|
65
67
|
|
|
66
|
-
- **No todo tool installed.** The extension is inert: nothing continues
|
|
68
|
+
- **No todo tool installed.** The extension is inert: nothing continues.
|
|
67
69
|
- **You want an autonomous agent that invents its own goals.** This drives the list you have; it stops
|
|
68
70
|
after 12 continuations per message by default.
|
|
69
71
|
- **You want the continuations visible.** They are silent by design.
|
|
70
72
|
- **You want the agent to keep going after you said stop.** Your instruction wins; a model that reads
|
|
71
|
-
"stop" as final
|
|
73
|
+
"stop" as final can say so with `todo_loop_block`, and that stops the loop immediately.
|
|
72
74
|
|
|
73
75
|
It buys *liveness*, not work. A model can satisfy the loop by closing tasks, including deleting
|
|
74
76
|
placeholders, and a list that is never updated looks the same as one that is finished. Clearing the list
|
|
75
77
|
is the only thing that stops it, so the list has to mean something to you.
|
|
76
78
|
|
|
79
|
+
The extension never edits your prompt. The only *variable* text it gives the model is a one-line nudge —
|
|
80
|
+
*continue, and read or update the list with the todo tool* — plus one constant tool declaration
|
|
81
|
+
(`todo_loop_block`, which carries no task state), so the list stays the agent's to maintain and the
|
|
82
|
+
conversation stays yours.
|
|
83
|
+
|
|
77
84
|
## Configuration
|
|
78
85
|
|
|
79
86
|
Optional, at `~/.pi/agent/todo-loop.json`:
|
|
80
87
|
|
|
81
88
|
```json
|
|
82
89
|
{
|
|
83
|
-
"maxRounds": 12
|
|
84
|
-
"maxStrikes": 2,
|
|
85
|
-
"injectState": true,
|
|
86
|
-
"maxInjectedTasks": 6
|
|
90
|
+
"maxRounds": 12
|
|
87
91
|
}
|
|
88
92
|
```
|
|
89
93
|
|
|
90
94
|
| Key | Default | What it does |
|
|
91
95
|
|---|---|---|
|
|
92
96
|
| `maxRounds` | `12` | continuations allowed per message to the agent |
|
|
93
|
-
| `maxStrikes` | `2` | turns in a row that leave the list unchanged before stopping |
|
|
94
|
-
| `injectState` | `true` | put the open-task list into each request; `false` leaves the model to read the list itself |
|
|
95
|
-
| `maxInjectedTasks` | `6` | rows shown before `… and N more` |
|
|
96
97
|
|
|
97
98
|
A missing or broken file falls back to these values; a bad config never fails a turn.
|
|
98
99
|
|
|
@@ -100,10 +101,10 @@ A missing or broken file falls back to these values; a bad config never fails a
|
|
|
100
101
|
|
|
101
102
|
| Command | What it does |
|
|
102
103
|
|---|---|
|
|
103
|
-
| `/todoloop` | status: mode, rounds,
|
|
104
|
-
| `/todoloop pause` | stop continuing
|
|
104
|
+
| `/todoloop` | status: mode, rounds, stalls, open tasks, whether it waits for you, why it last stopped, config path |
|
|
105
|
+
| `/todoloop pause` | stop continuing until you resume or restart |
|
|
105
106
|
| `/todoloop resume` | allow the next turn to continue again |
|
|
106
|
-
| `/todoloop reset` | clear the counters and
|
|
107
|
+
| `/todoloop reset` | clear the counters, the list signature and any pending `todo_loop_block` |
|
|
107
108
|
|
|
108
109
|
**Kill switch** — checked before every decision:
|
|
109
110
|
|
|
@@ -118,8 +119,8 @@ After a response that made no tool calls while tasks are open, `/todoloop` shoul
|
|
|
118
119
|
|
|
119
120
|
```
|
|
120
121
|
todo-loop active
|
|
121
|
-
rounds 1/12 ·
|
|
122
|
-
open 2/5 tracked
|
|
122
|
+
rounds 1/12 · stalled 0
|
|
123
|
+
open 2/5 tracked
|
|
123
124
|
anchor 4f3c1a2e
|
|
124
125
|
last: continuing: round 1/12, 2 open
|
|
125
126
|
config ~/.pi/agent/todo-loop.json
|
|
@@ -135,9 +136,9 @@ grep -c 'todo-loop:continue' ~/.pi/agent/sessions/<project>/*.jsonl
|
|
|
135
136
|
|
|
136
137
|
| Symptom | What to do |
|
|
137
138
|
|---|---|
|
|
138
|
-
| Nothing continues | Check `/todoloop`: `DISABLED` means a kill-switch file exists, `PAUSED` means you paused it
|
|
139
|
-
|
|
|
140
|
-
| It stopped at the round cap | The budget for that message is spent; send another message.
|
|
139
|
+
| Nothing continues | Check `/todoloop`: `DISABLED` means a kill-switch file exists, `PAUSED` means you paused it. `last: stopped: no list` with `open 0/0 tracked` means the model has not created a list in this session; if the open count is non-zero the tool is writing a shape this extension does not read |
|
|
140
|
+
| The model asked for a person | It called `todo_loop_block` with a reason, so the loop is stopped until your next message. The reason appears in `/todoloop` as `waiting for you: …` |
|
|
141
|
+
| It stopped at the round cap | The budget for that message is spent; send another message. `list unchanged for N settles` means the model was working without updating the list; `the agent stalled and may need a person` means it stopped without moving the list on the last round — ask what it is waiting on |
|
|
141
142
|
| Rounds jump by two, or continuations appear twice | Two continuation extensions are installed; `pi list` shows them, remove one, then `/reload` |
|
|
142
143
|
| The list is stale | The extension nudges, it does not write statuses. Nothing here fabricates progress |
|
|
143
144
|
| A warning says the list was cleared by todo alone | Every task closed in a turn with no other tool work. Check the work before trusting it |
|
|
@@ -168,7 +169,7 @@ the budget meaningless.
|
|
|
168
169
|
|
|
169
170
|
## Further reading
|
|
170
171
|
|
|
171
|
-
- **How it behaves, precisely** — counters, markers,
|
|
172
|
+
- **How it behaves, precisely** — counters, markers, stalls, cost:
|
|
172
173
|
[docs/behaviour.md](docs/behaviour.md)
|
|
173
174
|
- **pi API alignment** — the deliberate workarounds: [docs/pi-api-alignment.md](docs/pi-api-alignment.md)
|
|
174
175
|
- **Working on the repository**: [AGENTS.md](AGENTS.md)
|