@dumbmice/pi-todo-loop 0.0.0-stage → 0.1.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 +365 -0
- package/CHANGELOG.md +66 -0
- package/CONTRIBUTING.md +4 -0
- package/LICENSE +21 -0
- package/README.md +182 -2
- package/docs/behaviour.md +162 -0
- package/docs/pi-api-alignment.md +134 -0
- package/package.json +61 -5
- package/scripts/check-pack.mjs +62 -0
- package/scripts/pack-json.mjs +33 -0
- package/src/config.ts +48 -0
- package/src/index.ts +323 -0
- package/src/inject.ts +77 -0
- package/src/loop.ts +131 -0
- package/src/state.ts +216 -0
- package/tsconfig.json +17 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
# Working on this repository
|
|
2
|
+
|
|
3
|
+
Instructions for anyone — human or coding agent — changing `@dumbmice/pi-todo-loop`. For what the extension
|
|
4
|
+
does and how to install it, read [README.md](README.md).
|
|
5
|
+
|
|
6
|
+
## Layout
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
src/index.ts pi wiring: the context hook, the turn_end hook, /todoloop
|
|
10
|
+
src/loop.ts the pure decision policy (rounds, strikes, anchor reset)
|
|
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
|
+
src/config.ts config load and coercion, defaults on any failure
|
|
14
|
+
test/loop.test.ts decision policy: caps, strikes, anchor reset, error outcomes
|
|
15
|
+
test/state.test.ts branch reader: both shapes, recency preference, malformed input
|
|
16
|
+
test/inject.test.ts renderer and config coercion
|
|
17
|
+
test/index.test.ts the pi-facing half through a stub `pi`: tail handling, kill switch, caps
|
|
18
|
+
test/state-machine.test.ts the counters as a table of event sequences (the suite to extend)
|
|
19
|
+
test/helpers/harness.ts the stub `pi`, mutable branch and scripted context those two share
|
|
20
|
+
test/fixture.test.ts the same hooks over real captured lines (test/fixtures/branch.jsonl)
|
|
21
|
+
test/docs.test.ts pins the version table, invariant map, reason table, config table, links
|
|
22
|
+
scripts/check-pack.mjs asserts the published tarball contains what package.json promises
|
|
23
|
+
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
|
+
README.md user-facing documentation (what it is, install, config, troubleshooting)
|
|
26
|
+
AGENTS.md this file
|
|
27
|
+
CONTRIBUTING.md four-line pointer to this file (GitHub convention)
|
|
28
|
+
CHANGELOG.md Keep a Changelog format, released and unreleased entries
|
|
29
|
+
package.json scripts, engines, the files allow-list, pi.extensions entry point
|
|
30
|
+
tsconfig.json the strict / noEmit settings used by typecheck
|
|
31
|
+
.github/workflows/ test.yml (Node 22.6, 22 and 24: ci, test, typecheck, pack)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Requirements and commands
|
|
35
|
+
|
|
36
|
+
Node >= 22.19 (`engines`, the floor pi itself requires; the entry point is TypeScript and the test
|
|
37
|
+
runner uses `--experimental-strip-types`, which exists from 22.6). **If `npm test` fails with
|
|
38
|
+
`node: bad option: --experimental-strip-types`, a pre-22 node is first in `PATH`** — nvm's v20 shadows
|
|
39
|
+
pi's own node in a typical shell. `pretest` now says so instead of leaking the raw node error. Run the
|
|
40
|
+
publish (and the tests) with pi's node first:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
export PATH="$HOME/.local/share/pi-node/current/bin:$PATH"
|
|
44
|
+
npm publish --access public
|
|
45
|
+
``` devDependencies stay limited to `typescript` and `@types/node`; the test
|
|
46
|
+
workflow installs with `npm ci`.
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npm install # devDependencies only: typescript, @types/node
|
|
50
|
+
npm test # node:test, no model, no key, no network
|
|
51
|
+
npm run typecheck # tsc -p tsconfig.json, strict
|
|
52
|
+
npm run pack:check # asserts src, docs, tsconfig, README, AGENTS, CONTRIBUTING, LICENSE, CHANGELOG ship
|
|
53
|
+
|
|
54
|
+
# one file, while iterating
|
|
55
|
+
node --experimental-strip-types --no-warnings --test test/loop.test.ts
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
There is no pi install and no build step: the source is TypeScript that pi loads directly, and
|
|
59
|
+
`npm run typecheck` resolves no pi types because the extension imports none.
|
|
60
|
+
|
|
61
|
+
## Design decisions worth preserving
|
|
62
|
+
|
|
63
|
+
These are the invariants the suite protects. Changing one means changing its test, any affected
|
|
64
|
+
README text, the matching `docs/pi-api-alignment.md` item (or marking that item inapplicable), and
|
|
65
|
+
adding a `CHANGELOG.md` line — all in the same commit.
|
|
66
|
+
|
|
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
|
+
- **A `todo`-only clear is announced, not policed.** The loop's success and failure states are the same
|
|
71
|
+
observable; a warning is the most it may do about it.
|
|
72
|
+
- **Task text is model-authored, so it is sanitised, not interpolated.** `sanitizeLabel` collapses
|
|
73
|
+
flattens every Unicode line terminator, escapes `<`, drops controls/bidi/unpaired surrogates and
|
|
74
|
+
truncates by code point, because a subject can come from a fetched page and is rendered at the end of
|
|
75
|
+
a request inside a tag. **`status` is task-authored too** and gets the same treatment. Never render a
|
|
76
|
+
task field raw.
|
|
77
|
+
- **A marker is kept only where it separates two assistant turns.** There it is the alternation glue;
|
|
78
|
+
removing it exposes consecutive assistant messages that alternation-requiring providers reject. Any
|
|
79
|
+
other marker — at the tail, or followed by a human message — is dropped, because it would duplicate a
|
|
80
|
+
real user turn. The test `no returned request ever contains two adjacent user-role messages` is the
|
|
81
|
+
general statement; if you change the filter, run it first.
|
|
82
|
+
- **The marker body is constant** (`<todo_loop_continue/>`). It carries no round counter, because a
|
|
83
|
+
persisted marker must not claim the loop is on round `n` long after that chain ended. Counters live in
|
|
84
|
+
memory and are reported by `/todoloop`.
|
|
85
|
+
- **The injection is conditional; the marker is not.** The block is appended only when `injectState` is
|
|
86
|
+
on and tasks are open. The non-assistant constraint is pi's `turn_end` projection check, not the
|
|
87
|
+
request (register item 3) — do not re-derive a coupling between the two.
|
|
88
|
+
- **One entry per continuation is a floor, not a choice.** pi has no `resume()` that appends nothing.
|
|
89
|
+
Do not try to reach zero entries; keep the single `custom_message` with `display: false`.
|
|
90
|
+
- **The marker `customType` is a stable literal.** The tail check must keep recognising markers written
|
|
91
|
+
by earlier builds; do not derive it from the package name or version.
|
|
92
|
+
- **Task state is read from the branch on every request.** No in-memory cache of the list, no
|
|
93
|
+
`session_compact` resync handler: memory would drift across forks and compaction. Loop state
|
|
94
|
+
(counters, anchor, pause) *is* in memory, and a restart necessarily starts a new budget.
|
|
95
|
+
- **The decision is a pure function.** `decide()` in `src/loop.ts` takes a value and returns a value.
|
|
96
|
+
It must stay free of pi, filesystem and time so the caps can be tested exhaustively.
|
|
97
|
+
- **Only a settle spends a round.** `turn_end` fires for every assistant response, including one that
|
|
98
|
+
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 strike are therefore always about a turn that would have ended the run.
|
|
100
|
+
- **A tool turn clears a strike and spends no round** — except a tool turn that ends `error`/`aborted`,
|
|
101
|
+
which records the stop and leaves the strike budget alone. Tool turns already cause pi's next request, so
|
|
102
|
+
they must not consume the budget; but they are work, so they reset `strikes`. Without that, a model
|
|
103
|
+
that runs tools and never updates the list collects two strikes across its settles and stops claiming
|
|
104
|
+
"no progress" while it was working.
|
|
105
|
+
- **A stop keeps the counters; only a new human message resets them.** Clearing `rounds` / `strikes` on
|
|
106
|
+
stop re-arms a full budget on the next `turn_end` under the same anchor (another extension's
|
|
107
|
+
continuation, a provider retry). The anchor comparison is the only *automatic* reset;
|
|
108
|
+
`/todoloop reset` and process start clear the counters explicitly. A queued human message is a
|
|
109
|
+
new anchor, so it resets the budget the ordinary way.
|
|
110
|
+
- **Open means "not finished".** `CLOSED_STATUSES` is an exclusion list (`completed`, `deleted`), not an
|
|
111
|
+
allow-list: a status the todo tool adds later must not make a task vanish and the loop report the
|
|
112
|
+
list clear while work remains.
|
|
113
|
+
- **The newer of the two shapes wins.** Custom entry and toolResult fallback are compared by branch
|
|
114
|
+
position, not by type preference; preference by type would serve a stale list forever if the tool
|
|
115
|
+
stopped writing custom entries partway through a session.
|
|
116
|
+
- **No runtime dependencies.** The only imports are `node:` builtins. Argument validation, config
|
|
117
|
+
parsing and rendering are hand-written for that reason.
|
|
118
|
+
- **Fail soft at the boundary.** Parsing a branch, a config file or an entry never throws: malformed
|
|
119
|
+
input degrades to "no list" or to defaults. A thrown boundary handler is reported and the turn
|
|
120
|
+
continues, but the loop would then stop silently — inert is better than loud.
|
|
121
|
+
- **Nothing is written.** No files, no network, no process execution. The only reads are the optional
|
|
122
|
+
config, the two kill-switch paths and the session branch.
|
|
123
|
+
- **The kill switch is checked in both hooks.** Disabling takes effect on the next request, without
|
|
124
|
+
`/reload`.
|
|
125
|
+
- **No pi type import; `pi: any` at the registration boundary.** Contexts the file reads are typed
|
|
126
|
+
structurally (`SessionContext`). Adding the pi types would make a dependency out of a package that
|
|
127
|
+
deliberately has none.
|
|
128
|
+
|
|
129
|
+
## Invariant → test map
|
|
130
|
+
|
|
131
|
+
Find the invariant you are touching, run the case that pins it, and add a neighbour if your change
|
|
132
|
+
introduces an edge.
|
|
133
|
+
|
|
134
|
+
| Invariant | Pinned by |
|
|
135
|
+
|---|---|
|
|
136
|
+
| 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
|
+
| error and aborted outcomes never continue | `never continues on an error or aborted turn` |
|
|
138
|
+
| a no-progress turn spends a strike, the second stops | `a text-only turn that changes nothing spends a strike, then stops` |
|
|
139
|
+
| the first sighting of a list is progress, not a strike | `the first sighting of a list counts as progress, not a strike` |
|
|
140
|
+
| a list change resets strikes even with no tool call | `a list change counts as progress even with no tool results` |
|
|
141
|
+
| the round cap stops the loop | `round cap stops the loop`, `config caps are honoured` |
|
|
142
|
+
| a new human message resets the budget | `a new human message resets the budget` |
|
|
143
|
+
| 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` |
|
|
144
|
+
| the newest custom entry wins over older ones | `reads the newest rpiv-todo custom entry` |
|
|
145
|
+
| the toolResult fallback works and loses to the custom entry | `falls back to todo toolResult details`, `prefers the custom entry over the toolResult fallback` |
|
|
146
|
+
| 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
|
+
| `deleted` tasks leave list, open set and signature | `deleted tasks are excluded from tasks, open and signature` |
|
|
148
|
+
| 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
|
+
| model-authored task text is sanitised before it enters a request | `a hostile task subject cannot break out of the tag` |
|
|
151
|
+
| 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` |
|
|
152
|
+
| a status change moves the signature | `signature changes when a status changes` |
|
|
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` |
|
|
154
|
+
| only a `todo` tool's own shapes are read | `a foreign custom entry or toolResult carrying tasks is ignored` |
|
|
155
|
+
| an unknown status counts as open, and status text is normalised | `an unknown status counts as open, not as done`, `status is normalised, so a differently-cased value still closes` |
|
|
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` |
|
|
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` |
|
|
158
|
+
| the newer shape wins, in either order | `the newer of a custom entry and a toolResult wins, in either order` |
|
|
159
|
+
| 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` |
|
|
160
|
+
| 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` |
|
|
161
|
+
| a malformed context payload is not rewritten | `context leaves a malformed payload alone instead of rewriting it` |
|
|
162
|
+
| a task subject cannot break out of the injection tag | `a hostile task subject cannot break out of the tag` |
|
|
163
|
+
| the block ends on the concession, not on a call to act | `the block ends on the concession, not on a call to act` |
|
|
164
|
+
| pause stops the block as well as the continuation | `pause stops both the continuation and the steering block` |
|
|
165
|
+
| either kill switch disables both hooks | `the global kill switch disables both hooks`, `the project-local kill switch disables both hooks` |
|
|
166
|
+
| only a settle spends a round: tool turns cost nothing | `a turn that ran tools spends no round and appends no marker` |
|
|
167
|
+
| a tool turn clears a pending strike | `tool turns clear a pending strike but never spend a round` |
|
|
168
|
+
| 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` |
|
|
169
|
+
| 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` |
|
|
170
|
+
| a malformed event payload does not throw | `turn_end tolerates a malformed event payload instead of throwing` |
|
|
171
|
+
| 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` |
|
|
172
|
+
| an errored or aborted turn is never continued | `turn_end stops on an errored or aborted turn and does not spend a round` |
|
|
173
|
+
| the cap and pause gate the loop at the hook level | `the round cap is sticky for one human message`, `pause stops both the continuation and the steering block` |
|
|
174
|
+
| 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` |
|
|
175
|
+
|
|
176
|
+
## Adding or changing a continuation rule
|
|
177
|
+
|
|
178
|
+
1. Put the logic in `decide()` (`src/loop.ts`), not in the hook. The hook only adapts events and
|
|
179
|
+
applies the returned state. Add a row to `SCENARIOS` in `test/state-machine.test.ts` for the new
|
|
180
|
+
behaviour before touching prose.
|
|
181
|
+
2. Add the field to `LoopState` or `DecisionInput` if new information is needed, and make it
|
|
182
|
+
default-safe: a session recorded by an older build must still decide sensibly.
|
|
183
|
+
3. Add cases to `test/loop.test.ts`: the new rule, its boundary, and the ordering against the reset
|
|
184
|
+
and cap rules. Order matters — the anchor reset runs before every other check.
|
|
185
|
+
4. Add a user-visible line to the `/todoloop` status if an operator needs to see it.
|
|
186
|
+
5. Update the README *Behaviour to expect* list and `CHANGELOG.md` under `[Unreleased]`.
|
|
187
|
+
|
|
188
|
+
## Fragile areas
|
|
189
|
+
|
|
190
|
+
Ranked by how easily a small change breaks them.
|
|
191
|
+
|
|
192
|
+
1. **Two different tails.** A continuation whose *persisted projection* ends on an assistant message is
|
|
193
|
+
rejected by pi (`canContinue`); the *request* has no such constraint but must keep role alternation.
|
|
194
|
+
Conflating the two produced the round-2 marker leak. `test/index.test.ts` pins both shapes.
|
|
195
|
+
2. **Counter lifecycle.** `rounds`, `strikes` and `anchor` are mutated in one place and reset in three
|
|
196
|
+
(anchor change, `/todoloop reset`, process start). A missing reset either loops forever or refuses to
|
|
197
|
+
continue after one bad turn; a reset in the wrong place (inside `stop()`) silently re-arms the cap.
|
|
198
|
+
3. **The anchor.** It must be human-authored and never empty. `#<index>` is the fallback when an entry
|
|
199
|
+
carries no id; returning `""` would make "no anchor yet" and "this entry had no id"
|
|
200
|
+
indistinguishable, freezing the per-message reset. A branch switch that lands on a sibling branch
|
|
201
|
+
with the same newest user entry keeps the spent budget — deliberate, so a fork cannot refill it.
|
|
202
|
+
4. **State shape drift in the todo tool.** Both shapes are read defensively; keep `normalizeTask`
|
|
203
|
+
tolerant of renamed fields (`content` vs `subject`) and of unusable ids, and keep `withUniqueIds` in
|
|
204
|
+
`snapshotFrom` — it moves only synthetic or *colliding* ids, never a unique one the payload supplied,
|
|
205
|
+
because that number is what the model must use to update the task.
|
|
206
|
+
5. **Branch walking.** The reader stops once it holds a list in *either* shape and the anchor. It walks
|
|
207
|
+
newest-first, so a toolResult newer than the custom entry is already in hand; the extra walking is to
|
|
208
|
+
reach the anchor, which is older than the newest list entry. Breaking out on the list alone leaves
|
|
209
|
+
`anchor` empty, which makes every turn look like a new human message and re-arms the full budget each
|
|
210
|
+
time.
|
|
211
|
+
|
|
212
|
+
## Tests
|
|
213
|
+
|
|
214
|
+
`test/*.test.ts` uses `node:test` and imports the source directly; nothing needs a model, a key or a
|
|
215
|
+
network. `test/state-machine.test.ts` is the one to extend when you touch the counters: it replays sequences in
|
|
216
|
+
the vocabulary pi actually produces — `settle` (a response with no tool calls), `tool` (a response that
|
|
217
|
+
ended in tool calls), `edit` (the list changed), `human` (a new message), plus pause/resume/reset/
|
|
218
|
+
error/abort/kill-switch — and asserts the whole outcome at once: continuations, markers, rounds,
|
|
219
|
+
strikes, the reason `/todoloop` prints and its header. Every release-blocking defect in this repository's
|
|
220
|
+
history lived in that interaction, so a new rule belongs there as a row in `SCENARIOS` before it belongs
|
|
221
|
+
in prose.
|
|
222
|
+
|
|
223
|
+
`test/index.test.ts` fakes pi's surface (an object with `on` and `registerCommand`, plus a
|
|
224
|
+
`SessionContext` over a hand-built branch) and asserts the hook *return values*; it is the only place
|
|
225
|
+
the tail coupling is checked. `test/docs.test.ts` reads the repository's own Markdown, which is how the
|
|
226
|
+
tested-version table is enforced. When fixing a bug, add the failing case first. Run `npm test` before every commit; CI repeats
|
|
227
|
+
it on Node 22 and 24 together with `npm run typecheck` and `npm run pack:check`.
|
|
228
|
+
|
|
229
|
+
### Try a change locally
|
|
230
|
+
|
|
231
|
+
There is no build step: pi loads `src/index.ts` through `package.json` → `pi.extensions`.
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
pi -e /absolute/path/to/pi-todo-loop # one session, straight from the working copy
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
A live check needs `rpiv-todo` installed and an agent that creates a task list. Ask for two tasks and
|
|
238
|
+
then tell it to stop; with the loop active you should see it continue, and `/todoloop` should report
|
|
239
|
+
`round 1/12`. To watch the hidden machinery:
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
grep -c 'todo-loop:continue' ~/.pi/agent/sessions/<project>/*.jsonl # one per continuation
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## Documentation conventions
|
|
246
|
+
|
|
247
|
+
- `README.md` is for users of the extension, read top-down on npm: what it does, quick start, one
|
|
248
|
+
example, when it helps and when it does not, configuration, commands, "is it working", troubleshooting,
|
|
249
|
+
uninstall, requirements. No implementation inventories, no counter mechanics, no CI or release steps.
|
|
250
|
+
- `docs/behaviour.md` is the depth the README links to: marker lifetime, role alternation, the injected
|
|
251
|
+
block, counter semantics, reason strings, where the list is read from, scope, cost. Technical, but
|
|
252
|
+
written for a user debugging behaviour rather than a contributor.
|
|
253
|
+
- `docs/pi-api-alignment.md` is the register of deliberate workarounds over pi's documented extension
|
|
254
|
+
API, and the record of which versions those were established against. Add an item when behaviour
|
|
255
|
+
deviates; items with no output signal are marked _(silent)_. The tested-version table at the top,
|
|
256
|
+
`PI_API_SNAPSHOT` in `src/index.ts`, and the README *Tested against* table move together in the
|
|
257
|
+
same commit as the change that depends on them, and `test/docs.test.ts` fails if the constant, the
|
|
258
|
+
date or the tested rpiv-todo version goes missing from either file. Note that `@juicesharp/rpiv-todo`
|
|
259
|
+
is a *shape* dependency only (register item 12): it is never imported, so it has no `package.json`
|
|
260
|
+
entry, and its tested version lives in that table rather than in a constant.
|
|
261
|
+
- `CHANGELOG.md` follows Keep a Changelog. Put unreleased work under `[Unreleased]` with
|
|
262
|
+
Added/Changed/Fixed headings; add the version heading when the release is tagged. `test/docs.test.ts`
|
|
263
|
+
fails if the published version has no entry, so this one is enforced.
|
|
264
|
+
- `AGENTS.md` (this file) holds contributor, architecture and release material.
|
|
265
|
+
|
|
266
|
+
## Release canary
|
|
267
|
+
|
|
268
|
+
The one failure this suite cannot see is pi changing how a `display: false` `custom_message` projects,
|
|
269
|
+
or tightening `canContinue`. Drafts are committed before the continuation is validated, so that change
|
|
270
|
+
would persist a marker, drop the continuation, and leave `/todoloop` reporting `continuing: round n/12`
|
|
271
|
+
until it reports `round cap` with zero continuations — while every test here stays green, because the
|
|
272
|
+
harness drives a stub `pi`.
|
|
273
|
+
|
|
274
|
+
Before every version bump, on the *new* pi, run the documented live check once:
|
|
275
|
+
|
|
276
|
+
```bash
|
|
277
|
+
pi -e . -p "create two todos 'a' and 'b', then reply ok" --tools todo
|
|
278
|
+
grep -c 'todo-loop:continue' ~/.pi/agent/sessions/--<project>--/*.jsonl # 0 is a failure
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
and read the pi changelog for `custom_message`, `display`, `canContinue`, plus rpiv-todo's for
|
|
282
|
+
`reminder` (upstream PR #190 would put a second task block in the request).
|
|
283
|
+
|
|
284
|
+
## Known limitations to revisit
|
|
285
|
+
|
|
286
|
+
- **The decision is at `turn_end`, one boundary early.** pi 1.5.0 documents `agent_before_settle` as
|
|
287
|
+
the final actionable boundary that can append entries and request one continuation, and at
|
|
288
|
+
`turn_end` the hook has to reconstruct "the run would otherwise end" from `toolResults.length === 0`.
|
|
289
|
+
The consequence is a bounded wasted round when a human message is queued mid-turn, and the strike
|
|
290
|
+
reset lives in the adapter rather than in `decide()`. Moving the decision is the 0.2.0 change; it must
|
|
291
|
+
ship with a CHANGELOG line because it changes which turns spend a round.
|
|
292
|
+
- **`getBranch()` is not the model-visible context** (register item 16): after compaction the panel can
|
|
293
|
+
show work the model has no list for.
|
|
294
|
+
|
|
295
|
+
## Release process
|
|
296
|
+
|
|
297
|
+
Publishing runs in `.github/workflows/publish.yml` on a pushed `v*` tag, using npm
|
|
298
|
+
[trusted publishing](https://docs.npmjs.com/trusted-publishers): the job exchanges its OIDC token for a
|
|
299
|
+
short-lived publish credential, so the repository holds no `NPM_TOKEN`, and npm attaches a provenance
|
|
300
|
+
attestation automatically because the repository is public.
|
|
301
|
+
|
|
302
|
+
**The first version is published from the CLI**, because a package that does not exist yet cannot have
|
|
303
|
+
a trusted publisher configured from a workflow (npm also offers a *pending publisher* for that case):
|
|
304
|
+
|
|
305
|
+
```bash
|
|
306
|
+
npm login # or refresh the token in ~/.npmrc
|
|
307
|
+
npm publish --access public # scoped: --access public is required
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Then **configure the publisher once** at <https://www.npmjs.com/package/@dumbmice/pi-todo-loop/access>:
|
|
311
|
+
|
|
312
|
+
| Field | Value |
|
|
313
|
+
|---|---|
|
|
314
|
+
| Provider | GitHub Actions |
|
|
315
|
+
| Organization or user | `DumbMice` |
|
|
316
|
+
| Repository | `pi-todo-loop` |
|
|
317
|
+
| Workflow filename | `publish.yml` |
|
|
318
|
+
| Environment | leave empty |
|
|
319
|
+
|
|
320
|
+
Or from the CLI (npm >= 11.15):
|
|
321
|
+
|
|
322
|
+
```bash
|
|
323
|
+
npx npm@latest trust github @dumbmice/pi-todo-loop \
|
|
324
|
+
--file publish.yml --repo DumbMice/pi-todo-loop --allow-publish
|
|
325
|
+
npx npm@latest trust list @dumbmice/pi-todo-loop
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
The workflow must keep that filename: npm pins the publisher to it. It skips the publish when the tag's
|
|
329
|
+
version already exists on npm, so the manual first release and a re-run of the same tag both work; the
|
|
330
|
+
GitHub release is still created. It runs `npm ci`, the suite, the
|
|
331
|
+
typecheck and `npm run pack:check`, refuses to publish when the tag and `package.json` disagree, packs
|
|
332
|
+
the tarball and asserts the packed manifest still declares `pi.extensions`, then publishes and creates
|
|
333
|
+
the GitHub release from the matching `CHANGELOG.md` section. It has no inputs: a manual `workflow_dispatch` is a
|
|
334
|
+
`dry_run` — it verifies everything and publishes nothing.
|
|
335
|
+
|
|
336
|
+
Cutting a release:
|
|
337
|
+
|
|
338
|
+
```bash
|
|
339
|
+
npm version patch --no-git-tag-version # or minor/major; the tree must be clean
|
|
340
|
+
VERSION="$(node -p "require('./package.json').version")"
|
|
341
|
+
# move the CHANGELOG entry from [Unreleased] to "## [$VERSION] — YYYY-MM-DD"
|
|
342
|
+
git add -A
|
|
343
|
+
git commit -m "chore: release v$VERSION"
|
|
344
|
+
git tag -a "v$VERSION" -m "v$VERSION"
|
|
345
|
+
git push origin main --follow-tags
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
For the **first** release only: `npm publish --access public` before pushing the tag. Push the tag
|
|
349
|
+
anyway — the workflow notices the version already exists, skips the upload, and creates the GitHub
|
|
350
|
+
release.
|
|
351
|
+
|
|
352
|
+
`--follow-tags` pushes only **annotated** tags, which is why the recipe uses `git tag -a`. A pushed tag
|
|
353
|
+
starts the publish; nothing else does.
|
|
354
|
+
|
|
355
|
+
Until the trusted publisher is configured, `publish.yml` fails at the publish step by design (the OIDC
|
|
356
|
+
exchange is rejected) — and that is the safe failure: the checks still run, nothing is uploaded.
|
|
357
|
+
## Commits and security
|
|
358
|
+
|
|
359
|
+
- Commits follow Conventional Commits (`feat:`, `fix:`, `docs:`, `ci:`, `chore:`); release commits are
|
|
360
|
+
`chore: release vX.Y.Z`.
|
|
361
|
+
- Style: tabs and double quotes, no formatter or linter in the repository. Do not reformat untouched
|
|
362
|
+
lines.
|
|
363
|
+
- Report vulnerabilities through GitHub's private advisory form (Security → Report a vulnerability),
|
|
364
|
+
not a public issue.
|
|
365
|
+
- Contributions are accepted under the MIT license (see [LICENSE](LICENSE)).
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project adheres to
|
|
5
|
+
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [0.1.0] - 2026-11-04
|
|
8
|
+
|
|
9
|
+
Initial release.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `turn_end` continuation while a todo list has open tasks — anything not `completed` or
|
|
14
|
+
`deleted` — with a per-human-message round cap (`maxRounds`, default 12) and a
|
|
15
|
+
no-progress strike budget (`maxStrikes`, default 2). Only a settle spends a round: a turn
|
|
16
|
+
that ended in a tool call already causes pi's next request and costs nothing — but it clears a
|
|
17
|
+
pending strike, because it is work. A stop keeps the counters: only a new human message starts a
|
|
18
|
+
new budget.
|
|
19
|
+
- Per-request injection of the authoritative open-task list through the `context` hook,
|
|
20
|
+
never persisted, closing the post-compaction context hole
|
|
21
|
+
([rpiv-mono#222](https://github.com/juicesharp/rpiv-mono/issues/222)) and the "model
|
|
22
|
+
never calls the todo tool's `list` action" failure ([rpiv-mono#159](https://github.com/juicesharp/rpiv-mono/issues/159)).
|
|
23
|
+
- Hidden continuation marker (`custom_message`, `display: false`, constant 21-character body
|
|
24
|
+
`<todo_loop_continue/>`, ~200 bytes as a session entry). Markers are kept in history: they sit
|
|
25
|
+
between assistant turns and are what keeps the conversation alternating. A marker at the tail is
|
|
26
|
+
replaced by the state block, so a request never ends on two user-role messages.
|
|
27
|
+
- Task state read from `rpiv-todo-state` custom entries with a `todo` toolResult
|
|
28
|
+
`details.tasks` fallback, the newer of the two winning; never throws inside a turn
|
|
29
|
+
boundary, and a task with an unusable id falls back to its position instead of vanishing.
|
|
30
|
+
- `/todoloop` command: `status`, `pause`, `resume`, `reset`.
|
|
31
|
+
- Kill switch: `~/.pi/agent/todo-loop-off` or `.pi/todo-loop-off`.
|
|
32
|
+
- Config at `~/.pi/agent/todo-loop.json`; invalid values fall back to defaults, and numbers are
|
|
33
|
+
clamped (`maxRounds` ≤ 1000, `maxStrikes` ≤ 100, `maxInjectedTasks` ≤ 50).
|
|
34
|
+
- The documentation is split: `README.md` is a user-facing page (what it does, quick start, one example,
|
|
35
|
+
configuration, commands, troubleshooting, uninstall), and `docs/behaviour.md` holds the mechanics it
|
|
36
|
+
used to inline — marker lifetime, role alternation, counter semantics, reason strings, scope and cost.
|
|
37
|
+
- `scripts/pack-json.mjs` normalises `npm pack --json`, whose shape differs between npm 10
|
|
38
|
+
(array) and npm 11 (object keyed by package name); the publish workflow upgrades npm, so
|
|
39
|
+
both are exercised.
|
|
40
|
+
- Tests, runnable with no dependencies: decision policy, state reader, injection renderer, config
|
|
41
|
+
loader, the pi-facing hooks through a stub `pi`, a captured real branch, and documentation tests
|
|
42
|
+
that pin the tested-version table, the invariant map, the reason table, the configuration table and
|
|
43
|
+
README link targets.
|
|
44
|
+
- `test/state-machine.test.ts`: the counters as a table of event sequences in pi's own vocabulary
|
|
45
|
+
(`settle`, `tool`, `edit`, `human`, pause/resume/reset, error/abort, kill switch), asserting
|
|
46
|
+
continuations, markers, rounds, strikes, the `/todoloop` reason and header at once. Re-introducing any
|
|
47
|
+
of the three historical budget defects fails it.
|
|
48
|
+
- The continuation marker's constant body states the instruction ("Continue with the open todo
|
|
49
|
+
tasks…") instead of a self-closing tag, so `injectState: false` no longer continues with no
|
|
50
|
+
instruction at all, and a marker left in history is self-explanatory.
|
|
51
|
+
- A list that goes open → closed in a turn whose only tool results were `todo` is announced once: the
|
|
52
|
+
loop's success state and its failure state are the same observable.
|
|
53
|
+
- A stale-list burn now says so: the round-cap reason appends `, list unchanged for N settles`.
|
|
54
|
+
- Task text is sanitised before it enters a request — subject, `activeForm` **and `status`**: every
|
|
55
|
+
Unicode line terminator flattens, `<` is escaped, controls/bidi/zero-width and unpaired surrogates are
|
|
56
|
+
dropped, and labels truncate by code point, so a task subject cannot close the injection tag or forge a directive. Verified against a
|
|
57
|
+
crafted subject that closed the tag and inserted a `SYSTEM:` line — the structure broke before the
|
|
58
|
+
fix and the model refused both times.
|
|
59
|
+
- `.github/workflows/publish.yml`: tag-triggered publish through npm Trusted Publishing (OIDC, no
|
|
60
|
+
`NPM_TOKEN`, automatic provenance), gated on the suite, the typecheck, `pack:check`, a tag/version
|
|
61
|
+
match and a packed-manifest assertion; creates the GitHub release from the CHANGELOG section.
|
|
62
|
+
- `scripts/check-pack.mjs` as `npm run pack:check`: asserts the published tarball really contains the
|
|
63
|
+
entry point, sources, docs, licence and `tsconfig.json`, and that no dev-only path (`test/`,
|
|
64
|
+
`.github/`) leaks into it (`npm pack --dry-run` exits 0 otherwise).
|
|
65
|
+
|
|
66
|
+
[0.1.0]: https://github.com/DumbMice/pi-todo-loop/releases/tag/v0.1.0
|
package/CONTRIBUTING.md
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DumbMice
|
|
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
CHANGED
|
@@ -1,3 +1,183 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @dumbmice/pi-todo-loop
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@dumbmice/pi-todo-loop)
|
|
4
|
+
[](https://github.com/DumbMice/pi-todo-loop/blob/main/LICENSE)
|
|
5
|
+
[](https://github.com/DumbMice/pi-todo-loop/blob/main/package.json)
|
|
6
|
+
[](https://github.com/DumbMice/pi-todo-loop/actions/workflows/test.yml)
|
|
7
|
+
|
|
8
|
+
Keeps the [pi coding agent](https://pi.dev) working while its todo list still has open tasks.
|
|
9
|
+
|
|
10
|
+
`rpiv-todo` gives the agent a task list and shows it to you. It never asks pi for another turn, so the
|
|
11
|
+
agent still stops halfway and hands control back. This extension adds the turn: while tasks are open it
|
|
12
|
+
keeps the agent going, puts the list back in front of the model after a compaction, and stops when you
|
|
13
|
+
clear the list.
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pi install npm:@juicesharp/rpiv-todo # the list this reads (skip if installed)
|
|
19
|
+
pi install npm:@dumbmice/pi-todo-loop
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Then start a session and ask for something with several steps:
|
|
23
|
+
|
|
24
|
+
```text
|
|
25
|
+
Use the todo tool to plan this, then work through it: make the reports page
|
|
26
|
+
export CSV, add tests, and update the docs.
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
There is nothing else to configure. `/todoloop` shows what it is doing; `touch
|
|
30
|
+
~/.pi/agent/todo-loop-off` turns it off.
|
|
31
|
+
|
|
32
|
+
## What it looks like
|
|
33
|
+
|
|
34
|
+
```text
|
|
35
|
+
you: make the whole sequence web-editable, then expose it in the browser
|
|
36
|
+
|
|
37
|
+
pi: [todo: 12 tasks created, first marked in_progress]
|
|
38
|
+
pi: T5 backend is in and verified. Committing a report of the state …
|
|
39
|
+
← without this extension the run ends here, with 11 tasks open.
|
|
40
|
+
|
|
41
|
+
pi: [todo: T5 completed, T6 in_progress]
|
|
42
|
+
pi: T6 audio/face lanes wired. T7 per-camera PNG export next …
|
|
43
|
+
|
|
44
|
+
pi: [todo: all 12 completed]
|
|
45
|
+
pi: All 12 tasks are complete; here is what was verified …
|
|
46
|
+
← the list is clear, so the loop stops and stays stopped.
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
It stops for three reasons, and `/todoloop` always says which:
|
|
50
|
+
|
|
51
|
+
| Why it stopped | What you see |
|
|
52
|
+
|---|---|
|
|
53
|
+
| the list is clear | `stopped: list is clear` — the normal ending |
|
|
54
|
+
| the model stopped working the list | `stopped: 2 consecutive turns with no progress, 3 open` — two quiet turns in a row |
|
|
55
|
+
| the budget is spent | `stopped: round cap 12 reached, 3 open` — 12 continuations for your one message |
|
|
56
|
+
|
|
57
|
+
## When it helps
|
|
58
|
+
|
|
59
|
+
- Long, multi-step work where the agent summarises and hands back halfway.
|
|
60
|
+
- Sessions long enough to be compacted — the list survives on disk, but the model loses it.
|
|
61
|
+
- A model that runs tools but rarely updates its list. It will still be nudged; it may keep working to
|
|
62
|
+
the round cap.
|
|
63
|
+
|
|
64
|
+
## When it does not
|
|
65
|
+
|
|
66
|
+
- **No todo tool installed.** The extension is inert: nothing continues, nothing is injected.
|
|
67
|
+
- **You want an autonomous agent that invents its own goals.** This drives the list you have; it stops
|
|
68
|
+
after 12 continuations per message by default.
|
|
69
|
+
- **You want the continuations visible.** They are silent by design.
|
|
70
|
+
- **You want the agent to keep going after you said stop.** Your instruction wins; a model that reads
|
|
71
|
+
"stop" as final will hold, and those refusals cost up to 12 turns.
|
|
72
|
+
|
|
73
|
+
It buys *liveness*, not work. A model can satisfy the loop by closing tasks, including deleting
|
|
74
|
+
placeholders, and a list that is never updated looks the same as one that is finished. Clearing the list
|
|
75
|
+
is the only thing that stops it, so the list has to mean something to you.
|
|
76
|
+
|
|
77
|
+
## Configuration
|
|
78
|
+
|
|
79
|
+
Optional, at `~/.pi/agent/todo-loop.json`:
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
{
|
|
83
|
+
"maxRounds": 12,
|
|
84
|
+
"maxStrikes": 2,
|
|
85
|
+
"injectState": true,
|
|
86
|
+
"maxInjectedTasks": 6
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
| Key | Default | What it does |
|
|
91
|
+
|---|---|---|
|
|
92
|
+
| `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
|
+
A missing or broken file falls back to these values; a bad config never fails a turn.
|
|
98
|
+
|
|
99
|
+
## Commands
|
|
100
|
+
|
|
101
|
+
| Command | What it does |
|
|
102
|
+
|---|---|
|
|
103
|
+
| `/todoloop` | status: mode, rounds, strikes, open tasks, why it last stopped, config path |
|
|
104
|
+
| `/todoloop pause` | stop continuing and stop injecting, until you resume or restart |
|
|
105
|
+
| `/todoloop resume` | allow the next turn to continue again |
|
|
106
|
+
| `/todoloop reset` | clear the counters and start the budget over |
|
|
107
|
+
|
|
108
|
+
**Kill switch** — checked before every decision:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
touch ~/.pi/agent/todo-loop-off # everywhere
|
|
112
|
+
touch .pi/todo-loop-off # this project only
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Is it working?
|
|
116
|
+
|
|
117
|
+
After a response that made no tool calls while tasks are open, `/todoloop` should read something like:
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
todo-loop active
|
|
121
|
+
rounds 1/12 · strikes 0/2
|
|
122
|
+
open 2/5 tracked · inject on
|
|
123
|
+
anchor 4f3c1a2e
|
|
124
|
+
last: continuing: round 1/12, 2 open
|
|
125
|
+
config ~/.pi/agent/todo-loop.json
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
To count continuations in a session file:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
grep -c 'todo-loop:continue' ~/.pi/agent/sessions/<project>/*.jsonl
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Troubleshooting
|
|
135
|
+
|
|
136
|
+
| Symptom | What to do |
|
|
137
|
+
|---|---|
|
|
138
|
+
| Nothing continues | Check `/todoloop`: `DISABLED` means a kill-switch file exists, `PAUSED` means you paused it, `last: stopped: no list` means no todo tool is writing a list |
|
|
139
|
+
| It stopped with `no progress` | The model answered twice without running a tool or changing the list. Raise `maxStrikes` if you want more attempts |
|
|
140
|
+
| It stopped at the round cap | The budget for that message is spent; send another message. If the reason ends with `list unchanged for N settles`, the model was working without updating the list |
|
|
141
|
+
| Rounds jump by two, or continuations appear twice | Two continuation extensions are installed; `pi list` shows them, remove one, then `/reload` |
|
|
142
|
+
| The list is stale | The extension nudges, it does not write statuses. Nothing here fabricates progress |
|
|
143
|
+
| 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 |
|
|
144
|
+
|
|
145
|
+
## Uninstall
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
pi remove npm:@dumbmice/pi-todo-loop
|
|
149
|
+
rm -f ~/.pi/agent/todo-loop.json
|
|
150
|
+
rm -f ~/.pi/agent/todo-loop-off .pi/todo-loop-off
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Then `/reload`. Nothing was ever written to your working tree, and old session files keep only the
|
|
154
|
+
hidden continuation entries.
|
|
155
|
+
|
|
156
|
+
## Requirements
|
|
157
|
+
|
|
158
|
+
- **pi 1.5.0** (tested; needs the `turn_end` boundary).
|
|
159
|
+
- **A todo tool** named `todo` that stores its list in the session — tested with
|
|
160
|
+
`@juicesharp/rpiv-todo` 2.15.0.
|
|
161
|
+
- **Node 22.19+**, the floor pi itself requires. The extension has no dependencies.
|
|
162
|
+
|
|
163
|
+
Last verified 2026-11-04; the exact table and what was checked is in
|
|
164
|
+
[docs/pi-api-alignment.md](docs/pi-api-alignment.md).
|
|
165
|
+
|
|
166
|
+
Install only one continuation extension: a second one that also requests turns makes the counters and
|
|
167
|
+
the budget meaningless.
|
|
168
|
+
|
|
169
|
+
## Further reading
|
|
170
|
+
|
|
171
|
+
- **How it behaves, precisely** — counters, markers, the injected block, cost:
|
|
172
|
+
[docs/behaviour.md](docs/behaviour.md)
|
|
173
|
+
- **pi API alignment** — the deliberate workarounds: [docs/pi-api-alignment.md](docs/pi-api-alignment.md)
|
|
174
|
+
- **Working on the repository**: [AGENTS.md](AGENTS.md)
|
|
175
|
+
- **Changes by version**: [CHANGELOG.md](CHANGELOG.md)
|
|
176
|
+
- **Why it exists** — the upstream issues:
|
|
177
|
+
[rpiv-mono#159](https://github.com/juicesharp/rpiv-mono/issues/159),
|
|
178
|
+
[#222](https://github.com/juicesharp/rpiv-mono/issues/222),
|
|
179
|
+
[#259](https://github.com/juicesharp/rpiv-mono/issues/259)
|
|
180
|
+
|
|
181
|
+
## License
|
|
182
|
+
|
|
183
|
+
MIT. Source and issues: <https://github.com/DumbMice/pi-todo-loop>.
|