@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 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, strikes, anchor reset)
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: caps, strikes, anchor reset, error outcomes
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/inject.test.ts renderer and config coercion
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
- - **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.
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 request.** No in-memory cache of the list, no
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 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
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.** Disabling takes effect on the next request, without
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 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
+ | 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 a pending strike | `tool turns clear a pending strike but never spend a round` |
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 both the continuation and the steering block` |
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`, `strikes` and `anchor` are mutated in one place and reset in three
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
- strikes, the reason `/todoloop` prints and its header. Every release-blocking defect in this repository's
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, the injected
252
- block, counter semantics, reason strings, where the list is read from, scope, cost. Technical, but
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 *Tested against* table move together in the
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 strike
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: from the first continuation of a chain it asks the model to
12
- reconcile a drifted list — update statuses, add what is missing, remove what no longer applies, in
13
- that order — so a stale list has an exit short of burning the round cap. The first request of a
14
- chain still only asks for status moves.
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 21-character body
40
- `<todo_loop_continue/>`, ~200 bytes as a session entry). Markers are kept in history: they sit
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, puts the list back in front of the model after a compaction, and stops when you
13
- clear the list.
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 three reasons, and `/todoloop` always says which:
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 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 |
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, but the model loses it.
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, nothing is injected.
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 will hold, and those refusals cost up to 12 turns.
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, strikes, open tasks, why it last stopped, config path |
104
- | `/todoloop pause` | stop continuing and stop injecting, until you resume or restart |
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 start the budget over |
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 · strikes 0/2
122
- open 2/5 tracked · inject on
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, `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 |
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, the injected block, cost:
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)