@hank-warren/pi-loop 0.9.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @hank-warren/pi-loop
2
2
 
3
+ ## 1.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - 449f989: `/loop` is menu-first, and planning is the only way to start a loop. Bare `/loop` opens a tui-kit menu (launch, planning, approval card, or manager, depending on state) and `/loop <text>` opens planning with that text as the first drafting message, mirroring `/plan <prompt>`. **Removed:** the typed start grammar (`/loop 30m fix CI`), its flags (`--max`, `--compact-at`, `--expires`), the typed subcommands (`status|pause|resume|stop|settings`), argument completions, mid-prompt inline `/loop` invocation, and the `loop_start` tool it pointed at. Each authored a loop's acceptance gate in one unreviewed line; the approval card now shows the objective, the exact derived criteria, the ground rules and the caps while they can still be changed. Pause, resume, stop, status, focus and cadence all live in the manager menu, and creating a loop now requires the interactive menu (a restored loop still runs headless).
8
+
9
+ `loop_propose` gains `ground_rules`: up to ten hard constraints, approved with the objective, shown on the card, persisted in loop state so a fresh-session handoff carries them, and injected into every turn's system prompt as constraints rather than criteria — they never enter `criteria.json` and never gate completion.
10
+
11
+ `maxTurns` now defaults to `null` (unlimited). A turn budget is a proxy for cost, not progress, and stops a loop mid-work for a reason nobody can act on; the expiry and the no-progress breaker remain the real bounds. The removed `inlineInvocation` setting is tolerated in existing settings files and dropped on the next save.
12
+
13
+ The companion `pi-loop` skill is rewritten around the planning flow, cadence, ground rules and evidence.
14
+
3
15
  ## 0.9.0
4
16
 
5
17
  ### Minor Changes
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Inspired by Claude Code's `/loop`, adapted to Pi: keep work moving across many turns, and keep long loops coherent across context compaction.
4
4
 
5
- A loop is a **pacemaker** that owns its own work: it carries an objective and completion criteria, and ends when the model calls `loop_complete` with cited evidence, a cap is reached, its expiry arrives, or you stop it. **No other extension is required.**
5
+ A loop is a **pacemaker** that owns its own work: it carries an objective, completion criteria and ground rules, and ends when the model calls `loop_complete` with cited evidence, its expiry arrives, a cap you set is reached, or you stop it. Every loop is planned with you and started from an approval card. **No other extension is required.**
6
6
 
7
7
  Earlier versions could instead bind a loop to a [pi-goal](https://www.npmjs.com/package/@hank-warren/pi-goal) goal and let that extension own *whether the work is done*. That mode is gone as of 0.6.0, along with every `goal-state` reader: a loop now does everything the pairing did — objective, durable ledger, post-compaction re-anchor, evidence-gated completion — so the coupling bought nothing. A loop persisted before 0.6.0 that carries no objective adopts its focus text as one on restore, and pauses with an explanation when there is nothing to adopt.
8
8
 
@@ -11,35 +11,35 @@ pi-plan-mode's `plan-mode-state` is read fail-open — the only sibling state le
11
11
  ## Usage
12
12
 
13
13
  ```
14
- /loop 30m until all tests pass and CI is green # the trailing text is the objective
15
- /loop # manager TUI (status, pause/resume, edit, settings, stop)
16
- /loop status | pause | resume | stop | settings
17
- /loop --max 20 --compact-at 60% --expires 3d 10m # per-loop overrides
18
- /loop 10m --max 20 <objective> # ...or after the interval; both work
14
+ /loop # the menu: plan a loop, or manage the one that is running
15
+ /loop get CI green # skip the menu: open planning and say that first
19
16
  ```
20
17
 
21
- - **Intervals** are `<number><unit>` with unit `s`/`m`/`h`/`d`, parsed by the extension (never the model), minimum 1 minute (smaller values clamp, and the effective value is echoed).
22
- - **`--max`** caps the turns the loop causes — continuations and pokes alike (see [One cap](#one-cap)); `--max unlimited` opts out.
23
- - Loops are **user-driven only**: the model may start one *you* invoked inline (below), and never on its own initiative.
18
+ That is the entire command surface. Everything else status, pause, resume, stop, focus, cadence, settings is in the menu, and every loop is created the same way: a drafting conversation, an approval card, and a start that only you can press.
24
19
 
25
- ### Inline invocation
26
-
27
- Pi dispatches `/loop` only when it is the first thing in the message, so `quick check /loop 10m get CI green` arrives as ordinary prose and nothing happens. That is the common way to lose an invocation, so pi-loop detects it:
20
+ ### Planning is the only way in
28
21
 
29
22
  ```
30
- quick check /loop 10m get CI green on main, verified by a passing run
31
- loop: get CI green on main, verified by a passing run
23
+ /loop → launch menu → planning → loop_propose → approval card → you start it
32
24
  ```
33
25
 
34
- Both make the model call the `loop_start` tool with the objective and then begin working, in the same turn.
26
+ Bare `/loop` with nothing running opens a menu whose first item starts planning; `/loop <text>` opens planning and sends `<text>` as the first drafting message, exactly as `/plan <prompt>` does. You describe what the loop should achieve and how you will know it is done, the agent drafts it, and `loop_propose` renders an **approval card**: the objective, the exact completion criteria the split will produce, the ground rules, the cadence and the caps. Nothing is running at that point. From the card you can start the loop here, start it in a **fresh session** that carries only the objective, change the cadence, keep editing, or cancel.
27
+
28
+ Earlier versions also accepted a typed start (`/loop 30m fix CI`), flags (`--max`, `--expires`, `--compact-at`), typed subcommands, and a mid-prompt `/loop` token that pointed the model at a `loop_start` tool. **All of it is gone as of 1.0.0.** Each of them authored an acceptance gate in one unreviewed line: the criteria a loop is frozen to were invisible until after it had started, the flags were invisible full stop, and the mandatory interval implied a pacemaker the loop does not have. The card shows all of it while it can still be changed, which made every one of those surfaces a worse version of the same thing.
29
+
30
+ The consequence worth knowing: `/loop status` is no longer a subcommand, so it seeds planning with the word "status". Open the menu instead — it is one keystroke, and it shows more.
31
+
32
+ `loop_propose` is the only loop tool the model can reach before a loop exists, and it **starts nothing**. There is no tool that starts, pauses, resumes, or stops a loop; those are yours.
35
33
 
36
- **Your message is never touched** — not cut, split, re-sent, or annotated. An `input` handler only *records* the text; `before_agent_start` appends a one-turn `<system-reminder>` when the starting prompt is that same message. The hint is a per-turn system-prompt append, never a stored message, so "call loop_start now" cannot linger and fire on a later turn.
34
+ ### Ground rules
37
35
 
38
- Detection is deliberately narrow. A token at position 0 is ignored (that is Pi's own dispatch), as is one inside backticks or a quoted span `` `/loop` `` and `"/loop 5m ship it"` are how people *discuss* the commandand a bare trailing `/loop` with no remainder. The `loop:` form needs to start a line, so "my loop: is unclear" is prose. Extension-sent prompts never arm, and neither does steered or queued input, whose delivery order would leave a stale arm behind.
36
+ A loop runs with nobody watching, so the useful thing to fix in advance is not only what "done" looks like but what the loop must never do on the way there. `loop_propose` takes up to ten **ground rules**"never touch production", "never force-push", "never edit a test to make it pass", "stay inside this worktree" which appear on the card, cross into the loop's state when you approve it, and are injected into every turn's system prompt as hard constraints.
39
37
 
40
- **`loop_start` refuses unless the turn was armed.** This is enforcement, not guidance. A loop is *self-continuing*: a spurious start does not produce one unwanted answer, it produces turns until a cap. So the tool checks a flag the inline hooks set for exactly one turn, and refuses outright otherwise, along with a loop already being active, a missing `loop_complete`, or an unparsable interval or expiry.
38
+ They are **constraints, not criteria**: they never enter `criteria.json` and never gate completion. "Never touch production" has no evidence that proves it; folding it into the objective would turn it into a criterion nobody can satisfy. If the only way forward violates one, the loop is told to stop and call `loop_wait`.
41
39
 
42
- An invocation that names no interval uses the `defaultInterval` setting (`10m`). In a settle-paced loop the interval is only a fallback heartbeat, so the value matters far less than it looks. Turn the whole feature off with `"inlineInvocation": false`.
40
+ ### Headless
41
+
42
+ Creating a loop needs the interactive menu, so print and JSON modes cannot start one — by design, since the approval card is what authorises a self-continuing loop. A loop **restored** into a headless session runs normally, which is what the fresh-session handoff relies on.
43
43
 
44
44
  ## What paces a loop
45
45
 
@@ -61,11 +61,13 @@ A reply that starts or ends with `LOOP_OK` and carries at most **300 characters*
61
61
 
62
62
  The chip is **display-only**. The stored message keeps its exact bytes, because rewriting them would break the prompt cache this whole design is built around.
63
63
 
64
- ### One cap
64
+ ### One cap, unlimited by default
65
+
66
+ `maxTurns` counts **every turn the loop caused** — settle continuations plus fallback pokes — and stops the loop when it is reached. It defaults to `null`: **unlimited**.
65
67
 
66
- `maxTurns` (default 25, `--max`, settings) counts **every turn the loop caused** settle continuations plus fallback pokes and stops the loop when it is reached. `null` means unlimited.
68
+ That default changed in 1.0.0, from 25. A turn budget is a proxy for cost, not for progress: a loop that hits one stops in the middle of the work, with nothing decided and no reason a user can act on, and the number that stopped it was never chosen against the work. The bounds that remain are the ones that stop a loop *for a reason* — its expiry (7 days), and the no-progress breaker that pauses a loop repeating itself. Set a number in Settings to opt back into a budget.
67
69
 
68
- There used to be two caps: this one, and a `maxIterations` counter of **delivered wakes**. The wake cap bounded nothing the turn cap did not, because a settle-paced loop can run its whole life without delivering a single wake — so in practice the turn cap was always the one that tripped, and the second cap cost a settings field, a decision branch, a state field, and a paragraph of documentation to say so. Wakes are still counted and shown (`/loop status`); they just cap nothing.
70
+ There used to be two caps: this one, and a `maxIterations` counter of **delivered wakes**. The wake cap bounded nothing the turn cap did not, because a settle-paced loop can run its whole life without delivering a single wake — so in practice the turn cap was always the one that tripped, and the second cap cost a settings field, a decision branch, a state field, and a paragraph of documentation to say so. Wakes are still counted and shown on the menu's Status screen; they just cap nothing.
69
71
 
70
72
  A settings file or an in-flight loop still carrying the old pair keeps the **tighter** of the two: that is the bound it was already running under, and nobody has to rewrite settings to keep a cap they already chose.
71
73
 
@@ -73,7 +75,7 @@ A settings file or an in-flight loop still carrying the old pair keeps the **tig
73
75
 
74
76
  Each tick — fallback heartbeat or settled boundary — evaluates, in order:
75
77
 
76
- 1. **Expired?** Loops hard-expire after `maxLoopDuration` (default 7 days, or per loop with `--expires 3d`, echoed at start) — a forgotten loop is bounded. The loop gets **one final turn** first: "write the current state into the ledger, start no new work, claim no completion", and the settle after it stops the loop. A loop that simply vanished at its deadline would leave its most recent state only in a conversation about to be closed. If that final wake cannot be delivered, the loop stops immediately rather than living past its deadline.
78
+ 1. **Expired?** Loops hard-expire after `maxLoopDuration` (default 7 days, or per loop from the proposal's `expires`, shown on the card and echoed at start) — a forgotten loop is bounded. The loop gets **one final turn** first: "write the current state into the ledger, start no new work, claim no completion", and the settle after it stops the loop. A loop that simply vanished at its deadline would leave its most recent state only in a conversation about to be closed. If that final wake cannot be delivered, the loop stops immediately rather than living past its deadline.
77
79
  2. **Plan mode active?** Skip quietly; never inject prompts into a planning conversation.
78
80
  3. **Agent busy?** Never interrupt: coalesce into a single pending wake delivered at the next fully-settled idle boundary. N missed ticks collapse into one poke.
79
81
  4. **A declared wait?** A `loop_wait` whose deadline has not passed holds both drivers: the loop is not stalled, it is waiting on the world.
@@ -87,7 +89,7 @@ In the transcript, a poke renders as a one-line chip (`⏰ loop wake 4 · stalle
87
89
 
88
90
  Expiry and the cap are evaluated whenever the session settles, so a loop ends as soon as the work does rather than at the next scheduled tick. Only the fallback heartbeat pokes; a settle continues.
89
91
 
90
- The footer status shows `loop 5m · 5/25 · next 14:32` (loop turns against the cap), and a widget above the editor shows the same state with the loop focus beneath it; `/loop status` shows the full card including the last tick's decision and reason.
92
+ The footer status and the widget above the editor render from **one formatter**, so they cannot drift: `⟳ loop 1/3 done · turn 4/∞ · 12m · next 14:32`, leading with criteria progress rather than budget burn, and with the loop focus dimmed beneath it in the widget. Attention states come first (`⏸ loop paused`, `⏳ loop waiting`, `⚠ loop blocked`), and the planning states use the same `◆` the sibling pi-plan-mode uses for a plan awaiting a decision. The Status screen in the `/loop` menu shows the full card, including the last tick's decision and reason.
91
93
 
92
94
  ## Objective, injection, and `loop_complete`
93
95
 
@@ -118,7 +120,7 @@ There is no judge model: a second model grading the first is a bigger change tha
118
120
 
119
121
  Without it a loop has exactly one answer to "progress depends on something outside this session": keep continuing, and burn turns re-checking. `loop_wait` lets the model say what it is waiting for and roughly how long:
120
122
 
121
- - **`reason`** (required, one sentence) is shown in the widget and `/loop status`, and is the only record of what the loop was waiting for.
123
+ - **`reason`** (required, one sentence) is shown in the widget and on the menu's Status screen, and is the only record of what the loop was waiting for.
122
124
  - **`resume_after_ms`** is optional and clamped to **[60s, 1h]**, with the clamped value echoed back. Below a minute a "wait" is polling, which is what the tool replaces; above an hour it stops being a wait and the fallback heartbeat covers it better. Omitting it keeps the loop quiet until something else wakes the session.
123
125
  - The tool description carries the **cache-window guidance**: never poll for work Pi already notifies about, avoid ~300s (the prompt-cache dead zone, where the cache has just expired and the next turn re-reads the conversation at full price), use ≤270s only when actively polling external state, otherwise commit to 1200s+.
124
126
 
@@ -147,7 +149,7 @@ The `⚠ loop blocked` widget state remains, unchanged, as the backstop: it catc
147
149
 
148
150
  ## Breakers
149
151
 
150
- - **No progress.** The characteristic failure of an autonomous loop is not crashing, it is *restating*: the same paragraph of "here is what I would do next", turn after turn, calling no tools. pi-loop fingerprints the visible assistant text (SHA-256 over NFKC-normalised, case- and whitespace-folded text) of every tool-free loop-caused turn; `noProgressTurns` consecutive repeats (default 3, settings-tunable, `null` disables) **pause** the loop rather than stopping it — it stays configured, the widget says why, and `/loop resume` or your next message continues it with a fresh safety epoch. A turn that called **any** tool, including `loop_wait`, is progress by definition and resets the counter; counting a declared wait is the false positive that made this class of breaker infamous.
152
+ - **No progress.** The characteristic failure of an autonomous loop is not crashing, it is *restating*: the same paragraph of "here is what I would do next", turn after turn, calling no tools. pi-loop fingerprints the visible assistant text (SHA-256 over NFKC-normalised, case- and whitespace-folded text) of every tool-free loop-caused turn; `noProgressTurns` consecutive repeats (default 3, settings-tunable, `null` disables) **pause** the loop rather than stopping it — it stays configured, the widget says why, and Resume in the `/loop` menu (or your next message) continues it with a fresh safety epoch. A turn that called **any** tool, including `loop_wait`, is progress by definition and resets the counter; counting a declared wait is the false positive that made this class of breaker infamous.
151
153
  - **Interruption classification.** A loop that answers every provider failure with "continue" retries into exhausted quotas and re-sends requests too large to succeed. So each class gets its own answer: usage/billing exhaustion **pauses** (retrying a quota window that has not reset just burns the cap), an unrecoverable auth error **pauses**, an aborted loop turn (`Esc`) **pauses**, a context overflow **compacts and then continues** regardless of what the usage gauge says — the failed request just disproved that reading — and a transient error simply continues, because the next continuation *is* the retry.
152
154
 
153
155
  ## The loop ledger
@@ -156,7 +158,7 @@ A multi-day loop cannot keep its state in the conversation: compaction is lossy
156
158
 
157
159
  - **`criteria.json`** — this loop's completion criteria, derived from the objective when the loop starts (bullets if you wrote a list, otherwise sentences, otherwise one implicit criterion) and echoed back to you so you can see what `loop_complete` will answer for. JSON deliberately, not Markdown: models rewrite prose they are asked to maintain far more readily than they rewrite a structured file. The model may change **only** the `passes` field, only with cited evidence, and may never add, remove, or reword an entry — a model allowed to rewrite its own acceptance criteria eventually rewrites them into something it has already achieved. A restore keeps the file it finds: re-deriving it on every session start would reset the flips the loop had earned.
158
160
 
159
- **The one exception is at start.** The derivation is grammar, not comprehension, so `/loop fix CI. it has been red since Tuesday.` yields a `c2` demanding cited evidence for a piece of background. So `loop_start` takes an optional `criteria` array (≤ 12 entries, ≤ 500 characters each, no blank entries a malformed list is refused rather than quietly ignored), and when the model passes one it replaces the split. That is the *only* place a model may write criteria: at start there is no work yet to grade, the criteria are echoed to you immediately, and the incentive to write an easy gate is at its weakest — which is exactly untrue at completion time, where they stay immutable. The typed `/loop` command never takes them; there is no model in that path.
161
+ The derivation is grammar, not comprehension "fix CI. it has been red since Tuesday." yields a `c2` demanding cited evidence for a piece of background — which is precisely why the card shows the result **before** the loop starts. Fixing the objective in the drafting conversation costs a sentence; discovering the same problem at completion costs the loop.
160
162
  - **`PROGRESS.md`** — the agent-maintained ledger, created with a fixed four-section schema (current status / completed / **failed approaches and why** / next actions) so "update the ledger" means the same thing on every turn. Failed approaches matter most: nothing else remembers them once the conversation is compacted.
161
163
 
162
164
  Both are **best-effort**. An unwritable home directory, a full disk, or a file hand-edited into invalid JSON degrades the loop to "no ledger" with a single warning; it never breaks the loop. `PROGRESS.md` is created and then never overwritten, so a session restart cannot erase days of ledger.
@@ -169,7 +171,7 @@ The system append carries the objective only while the loop is *active*, and con
169
171
 
170
172
  Long loops die by context exhaustion, not by failing. pi-loop owns the compaction path:
171
173
 
172
- - **Proactive compact at a threshold** (default 70% of the context window, `--compact-at` / settings): at an idle boundary, pi-loop triggers `/compact` itself with loop-specific instructions — preserve the objective and acceptance criteria verbatim, **every failed approach and the reason it failed**, decisions and rationale, files modified, commands and unresolved errors, and the next 1-3 actions. The instructions explicitly **stop carrying prior summaries forward wholesale** and tell the next turn to re-derive status from the ledger and authoritative state instead: cumulative carry-forward grows the text while the information in it decays. Pending pokes are held until the compaction completes. Pi's reserve-token auto-compaction remains as the fault handler.
174
+ - **Proactive compact at a threshold** (default 70% of the context window, settings): at an idle boundary, pi-loop triggers `/compact` itself with loop-specific instructions — preserve the objective and acceptance criteria verbatim, **every failed approach and the reason it failed**, decisions and rationale, files modified, commands and unresolved errors, and the next 1-3 actions. The instructions explicitly **stop carrying prior summaries forward wholesale** and tell the next turn to re-derive status from the ledger and authoritative state instead: cumulative carry-forward grows the text while the information in it decays. Pending pokes are held until the compaction completes. Pi's reserve-token auto-compaction remains as the fault handler.
173
175
  - **Loop-owned re-anchor**: when a compaction completes mid-loop, pi-loop dispatches one pointer-sized continuation at the next settle — re-read `PROGRESS.md` and `criteria.json`, continue from authoritative state, plus the next 1-3 actions lifted out of the summary that just replaced the conversation. The loop no longer goes quiet until the next wake. A re-anchor supersedes an ordinary continuation already queued: after a compaction, "re-read the ledger" is strictly the better instruction.
174
176
  - Loop state itself lives in custom session entries, which compaction never touches, and survives session restarts (the timer re-arms on resume; expired loops are dropped with a notice).
175
177
 
@@ -179,10 +181,9 @@ Long loops die by context exhaustion, not by failing. pi-loop owns the compactio
179
181
 
180
182
  ```json
181
183
  {
182
- "maxTurns": 25,
184
+ "maxTurns": null,
183
185
  "noProgressTurns": 3,
184
186
  "maxLoopDuration": "7d",
185
- "inlineInvocation": true,
186
187
  "defaultInterval": "10m",
187
188
  "compaction": {
188
189
  "enabled": true,
@@ -192,7 +193,9 @@ Long loops die by context exhaustion, not by failing. pi-loop owns the compactio
192
193
  }
193
194
  ```
194
195
 
195
- `maxTurns: null` means unlimited; `noProgressTurns: null` disables the breaker. A file still carrying the superseded `maxIterations`/`automaticTurns` pair loads, keeping the tighter of them, and the next save rewrites it as `maxTurns`. `inlineInvocation: false` turns off mid-prompt `/loop` detection; `defaultInterval` is the fallback heartbeat an inline invocation gets when it names none.
196
+ `maxTurns: null` means unlimited (the default); `noProgressTurns: null` disables the breaker. A file still carrying the superseded `maxIterations`/`automaticTurns` pair loads, keeping the tighter of them, and the next save rewrites it as `maxTurns`. `inlineInvocation` is gone with the inline token it controlled: a file still carrying it loads — an unknown field never fails a settings file — and the next save drops it. `defaultInterval` is the fallback heartbeat a proposal gets when it names none.
197
+
198
+ Settings are reachable from every `/loop` menu.
196
199
 
197
200
  ## Deliberate omissions
198
201
 
@@ -206,7 +209,7 @@ These were considered and cut, and the reasoning is recorded so they are not sil
206
209
 
207
210
  The package is a hybrid: it ships the extension **and** a `pi-loop` skill (`skills/pi-loop/SKILL.md`), which carries the judgment the engine cannot encode — how an objective becomes falsifiable criteria, what the evidence gate accepts as a citation, when to declare a `loop_wait` instead of polling, what `PROGRESS.md` is worth, and when the work belongs in no loop at all.
208
211
 
209
- It is **loaded on demand**: one line in each of `loop_start` and `loop_complete`'s prompt guidelines points at it by name, exactly as `pi-processes` does, and the model reads the body when it judges it needs it. Nothing about the guidance enters a stored loop message or the system append — those bytes are the cache prefix, and `test/bytes.test.ts` fails if any of them so much as mentions a skill.
212
+ It is **loaded on demand**: the planning hint and `loop_complete`'s prompt guidelines point at it by name, exactly as `pi-processes` does, and the model reads the body when it judges it needs it. Nothing about the guidance enters a stored loop message or the system append — those bytes are the cache prefix, and `test/bytes.test.ts` fails if any of them so much as mentions a skill.
210
213
 
211
214
  Skill and extension version as one artifact on purpose: a skill describing an engine the installed extension does not have is the coupling failure this repository already learned once.
212
215
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hank-warren/pi-loop",
3
- "version": "0.9.0",
3
+ "version": "1.0.0",
4
4
  "description": "Long-running work for Pi: settle-paced loops with a durable ledger, adaptive waits, no-progress breakers, evidence-gated completion, and a task scheduler.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -1,55 +1,81 @@
1
1
  ---
2
2
  name: pi-loop
3
- description: Write loop objectives that become falsifiable criteria, and complete a loop with evidence the gate accepts. Use before the first loop_start of a session, before assembling loop_complete evidence, and when deciding whether work belongs in a loop at all.
3
+ description: Draft loop objectives that become falsifiable criteria, set cadence and ground rules, and complete a loop with evidence the gate accepts. Use while drafting a loop in /loop planning, before calling loop_propose, before assembling loop_complete evidence, and when deciding whether work belongs in a loop at all.
4
4
  license: MIT
5
5
  ---
6
6
 
7
7
  # pi-loop
8
8
 
9
- The engine handles pacing, wakes, the cap, compaction, and the ledger. It cannot handle the two things that decide whether a loop is worth running: **what end state the objective names**, and **what evidence proves it**. That is this file.
9
+ The engine handles pacing, wakes, compaction, and the ledger. It cannot handle the three things that decide whether a loop is worth running: **what end state the objective names**, **what constraints bound the work**, and **what evidence proves it is done**. That is this file.
10
10
 
11
- Nothing here changes how you invoke a loop. `loop_start` still refuses unless the user's message carried an inline `/loop` or `loop:` token — that gate is enforcement, not guidance.
11
+ ## How a loop comes into being
12
+
13
+ There is exactly one path, and it is a conversation:
14
+
15
+ ```
16
+ /loop → launch menu → planning → loop_propose → approval card → the user starts it
17
+ ```
18
+
19
+ `/loop` with nothing running opens a menu; its first item starts planning. `/loop <text>` skips the menu and makes `<text>` the first drafting message. From there **you draft with the user and call `loop_propose`**, which renders an approval card and starts nothing. The user starts the loop from that card — here, or in a fresh session that carries only the objective.
20
+
21
+ There is no typed start, no flags, and no inline token. Every one of them authored an acceptance gate in a single line that nobody reviewed, which is exactly the failure this flow exists to prevent. While planning is open, a conversational request for a loop is the signal to draft one and propose it — never to refuse and tell the user to type a command.
22
+
23
+ Everything after the start — pause, resume, stop, status, focus, cadence, settings — lives in the `/loop` menu. It belongs to the user, not to you: there is no tool for any of it.
12
24
 
13
25
  ## The objective is not a prompt, it is an acceptance test
14
26
 
15
- At `loop_start` the extension splits the objective into `criteria.json` by default, deterministically and without a model:
27
+ The objective you pass to `loop_propose` is split into `criteria.json`, deterministically and without a model:
16
28
 
17
29
  - **Bullets or a numbered list** (2+) → one criterion per item.
18
30
  - Otherwise **sentences** → one criterion per sentence.
19
31
  - Otherwise **the whole objective** → one implicit criterion.
20
32
 
21
- Up to 12 criteria; each gets an id (`c1`, `c2`, …) and `passes: false`. `loop_complete` later demands **one cited piece of evidence per id**. So the objective's grammar decides the shape of the gate — and a conjunction *inside a sentence* does **not** split. "Fix the flaky test and update the docs" is a single criterion whose evidence must cover both halves; nothing will remind you of the second half. When the work has several requirements, write them as separate sentences or bullets on purpose — or pass the criteria yourself (below).
33
+ Up to 12 criteria; each gets an id (`c1`, `c2`, …) and `passes: false`. `loop_complete` later demands **one cited piece of evidence per id**. So the objective's grammar decides the shape of the gate — and a conjunction *inside a sentence* does **not** split. "Fix the flaky test and update the docs" is a single criterion whose evidence must cover both halves; nothing will remind you of the second half. When the work has several requirements, write them as separate bullets on purpose.
34
+
35
+ The card shows the exact criteria the split produced **before anything starts**, which is the whole reason planning exists: they are frozen the moment the loop begins, and until the card existed nobody saw them until after that point.
36
+
37
+ Two rules follow:
22
38
 
23
- **You pass the user's objective text verbatim** never a cleaned-up version of it. So the criteria are only as falsifiable as what was typed, and the evidence gate cannot rescue them later: it checks that you said something specific about every criterion, not that the criterion was worth checking.
39
+ 1. **Name the evidence in the requirement itself.** "…, verified by `npm test` passing" pre-commits the citation, so completion is a lookup instead of an argument. Where a requirement names no check, its `check` field is empty and completion becomes an audit against authoritative current state.
40
+ 2. **Keep each criterion checkable by one observation.** A criterion that needs three different commands to prove is three bullets.
24
41
 
25
- What you can do is say so **at the start**, in the same turn as `loop_start`. When the objective names no observable end state, start the loop as invoked and then, in one or two lines, name the end state you will hold yourself to and the check that will prove it — and tell the user that `/loop` can edit the objective if that is not what they meant. Record the same statement in `PROGRESS.md`. Do not silently invent an easier target and discover the disagreement at completion.
42
+ **Draft the objective from what the user said, never a tidier version of it.** If they decline to name checks, say plainly what the gate will and will not catch, and let them decide.
26
43
 
27
- | Vague as typed | The end state to name back |
44
+ | Vague as said | The end state to draft back |
28
45
  | --- | --- |
29
46
  | `make the tests better` | raise `packages/foo` line coverage above 80%, proven by `npm run coverage` |
30
47
  | `fix CI` | the CI workflow green on this branch, proven by `gh pr checks` reporting all checks passed |
31
48
  | `clean up the parser` | the duplicated token table gone from `src/parse.ts`, with `npm test` still passing |
32
- | `investigate the memory leak` | no end state exists yet — see *When not to start a loop* |
49
+ | `investigate the memory leak` | no end state exists yet — see *When not to draft a loop* |
33
50
 
34
- Two rules follow from the split:
51
+ Two shapes need care because the split is grammar, not comprehension:
35
52
 
36
- 1. **The evidence is best named in the objective itself.** "…, verified by `npm test` passing" pre-commits the citation, so completion is a lookup instead of an argument. Where the objective names no check, the criterion's `check` field is empty and you must audit against authoritative current state. When you suggest a `/loop` invocation to the user, write it that way.
37
- 2. **Keep each criterion checkable by one observation.** A criterion that needs three different commands to prove is three criteria — that is a reason to suggest bullets.
53
+ - **Requirements mixed with context** "fix CI, it has been red since Tuesday" yields a criterion demanding cited evidence that CI has been red since Tuesday. Drop the background from the objective; it belongs in the conversation.
54
+ - **Several requirements in one sentence** split them into bullets so neither half can be forgotten.
38
55
 
39
- The criteria are echoed back at start. If they came out wrong, that is visible *then*: say so, rather than discovering it at completion.
56
+ ## Cadence: what actually bounds a loop
40
57
 
41
- ### When the split would misfire: propose the criteria
58
+ Cover this explicitly while drafting; the card shows all three.
42
59
 
43
- The split is grammar, not comprehension. `/loop fix CI. it has been red since Tuesday.` gives you a `c2` demanding cited evidence that CI has been red since Tuesday — background, not a requirement. So `loop_start` takes an optional **`criteria`** array (at most 12 entries, at most 500 characters each, no blank entries; a malformed list is refused, not ignored). When you pass one it *replaces* the split, with ids `c1`…`cN` exactly as if it had been derived.
60
+ - **Expiry** (`expires`, default 7 days) is the real bound. It grants one final turn to write state into `PROGRESS.md` no new work, no completion claim.
61
+ - **The fallback heartbeat** (`interval`, default 10m) is *not* the pacemaker. The loop advances whenever the session settles, so a busy loop may never deliver a single wake. The heartbeat matters only for a session that has gone quiet. Pick it to match how long the loop might legitimately sit waiting on the world, not how often you want it to work.
62
+ - **The turn cap** (`max_turns`) is **unlimited by default**, deliberately. A turn budget is a proxy for cost, not for progress, and a loop that hits one stops mid-work with nothing decided and no reason a user can act on. Propose a number only when the user asks for a spend bound.
63
+ - **The no-progress breaker** pauses the loop after 3 consecutive tool-free turns with the same visible text. Restating a plan is the characteristic failure of an autonomous loop; calling any tool, including `loop_wait`, resets it.
64
+
65
+ ## Ground rules: constraints, not criteria
44
66
 
45
- Use it only when the split would misfire:
67
+ `loop_propose` takes **`ground_rules`** up to 10 short lines, each a hard constraint the loop must never violate. They are shown on the card, approved with the objective, and injected into the loop's system prompt on every turn as `Ground rules (hard constraints, never violate):`.
46
68
 
47
- - the objective **mixes requirements with context** ("fix CI. it has been red since Tuesday." one criterion: get CI green on main);
48
- - the objective **packs several requirements into one sentence** ("fix the flaky test and update the docs" → two criteria, so neither half can be forgotten).
69
+ They never enter `criteria.json` and never gate completion. A criterion is something to reach; a ground rule is something never to do on the way there. Folding a constraint into the objective turns it into a criterion nobody can satisfy — "never touch production" has no evidence that proves it.
49
70
 
50
- Every criterion you write must be a **faithful restatement of something the user asked for** never fewer, never weaker, never easier than the objective as typed, and never a requirement they did not state. **When in doubt, omit the parameter** and let the deterministic split stand: an awkward criterion you must answer for is safer than a tidy one you wrote for yourself.
71
+ **Ask for them.** A loop runs with nobody watching, so an unstated constraint is one nobody enforces. The ones worth asking about:
51
72
 
52
- This is the only point where you may write criteria at all. Once the loop starts they are frozen you may flip `passes`, nothing else — and the user has already seen them echoed.
73
+ - systems that are off limits (production, a customer's cluster, a shared branch);
74
+ - operations that must never happen unattended (force-push, `git reset --hard`, dropping a table, a release);
75
+ - the shape of the fix (never edit a test or fixture to make a check pass, never widen a permission, never add a dependency);
76
+ - blast radius (stay inside this worktree, one pull request, no merges).
77
+
78
+ Keep each one short and checkable by reading it. "Be careful" is not a ground rule.
53
79
 
54
80
  ## Completing: cite, do not assert
55
81
 
@@ -68,7 +94,7 @@ c1: "npm test → 214 passing, 0 failing (packages/pi-loop/test, full run at 14:
68
94
  c2: "gh pr checks 171 → all 3 checks passed (ci/gate, ci/typecheck, ci/scan-secrets)"
69
95
  ```
70
96
 
71
- Audit requirement by requirement before calling it, and match the verification scope to the requirement scope: a criterion about the whole suite is not proven by one test file. **Effort exhaustion is not completion** — running long, running out of ideas, or nearing a cap is never a reason to call the tool. If the criteria are not met, keep working; another continuation is coming.
97
+ Audit requirement by requirement before calling it, and match the verification scope to the requirement scope: a criterion about the whole suite is not proven by one test file. **Effort exhaustion is not completion** — running long or running out of ideas is never a reason to call the tool. With no turn cap by default, there is no budget to "use up" as an excuse: if the criteria are not met, keep working.
72
98
 
73
99
  ## A loop runs unattended, so a question is a deadlock
74
100
 
@@ -83,9 +109,9 @@ loop liveness -> expiry -> plan mode -> compaction -> busy -> wait -> the turn c
83
109
  ```
84
110
 
85
111
  So `agent_settled` never fires and no continuation is queued; every fallback tick returns
86
- `skip: agent-busy`; `automaticTurns` never increments, so the turn cap never trips; and the
87
- no-progress breaker needs tool-free *turns*, of which there are none. **Expiry is the only thing
88
- left, up to seven days later** — and the widget shows the next wake time the whole time.
112
+ `skip: agent-busy`; `automaticTurns` never increments, so a turn cap (if one was set) never trips;
113
+ and the no-progress breaker needs tool-free *turns*, of which there are none. **Expiry is the only
114
+ thing left, up to seven days later** — and the widget shows the next wake time the whole time.
89
115
 
90
116
  This is why the guidance is a posture rather than a list of forbidden tools. Your other
91
117
  extensions are unknowable and a blacklist goes stale the moment one of them ships a new prompt.
@@ -95,14 +121,14 @@ The rule follows from the mechanics instead:
95
121
  into `PROGRESS.md`, and keep going. The user reads it when they return and the loop never
96
122
  stopped. A decision recorded beats a question unanswered.
97
123
  2. **`loop_wait` is the loop-safe form of asking.** It is the one way to say "I need a human"
98
- that does not deadlock: continuations stop, the reason shows in the widget and `/loop status`,
99
- and any wake resumes it. Use it exactly where you would otherwise open a modal, and put the
100
- options in the ledger first so the answer can be one word.
124
+ that does not deadlock: continuations stop, the reason shows in the widget and the `/loop`
125
+ status screen, and any wake resumes it. Use it exactly where you would otherwise open a modal,
126
+ and put the options in the ledger first so the answer can be one word.
101
127
  3. **Never reshape a command to get *around* a permission gate.** This is the dangerous one. An
102
- agent under a turn cap has real incentive to rewrite a blocked command into something the
103
- guardian waves through, and that failure *looks like progress*. Splitting the command up,
104
- obfuscating it, routing it through a different tool, or simply retrying variations until one
105
- is allowed are all the same move, whatever the loop's state.
128
+ autonomous agent has real incentive to rewrite a blocked command into something the guardian
129
+ waves through, and that failure *looks like progress*. Splitting the command up, obfuscating
130
+ it, routing it through a different tool, or simply retrying variations until one is allowed
131
+ are all the same move, whatever the loop's state.
106
132
 
107
133
  Addressing a stated concern is not that move. A guardian that blocks with a specific
108
134
  objection — pi-auto-permissions does exactly this while `PI_LOOP_ACTIVE=1`, because a modal
@@ -117,6 +143,8 @@ The rule follows from the mechanics instead:
117
143
  5. **Ask only when proceeding is irreversible *and* the choice is load-bearing.** Then use
118
144
  `loop_wait`, not a prompt.
119
145
 
146
+ A ground rule outranks all of this: if the only way forward violates one, stop and call `loop_wait`.
147
+
120
148
  Autonomy is not permission to be reckless. It is the opposite: the absence of a human in the loop
121
149
  is exactly why the reversible path is the right default.
122
150
 
@@ -126,7 +154,6 @@ When progress depends on something outside the session — a CI run, a deploy, a
126
154
 
127
155
  - `resume_after_ms` is clamped to **[60s, 1h]** and the clamped value is echoed back. Omit it to stay quiet until something else wakes the session.
128
156
  - Avoid ~300s: that is the prompt-cache dead zone, where the cache has just expired and the next turn re-reads the whole conversation at full price. Use ≤270s only when actively polling external state nothing else reports; otherwise commit to 1200s or more.
129
- - The turn a wake delivers for an elapsed wait counts against the cap like any other, so re-arming a wait forever is not free.
130
157
  - Never wait for what Pi already notifies you about: background processes, subagents, and tool completions wake the session on their own.
131
158
 
132
159
  `loop_wait` is for a genuine external event, never a way to end a turn early with work outstanding.
@@ -143,22 +170,14 @@ Write both files with the **`loop_progress`** tool, never with the file or shell
143
170
 
144
171
  Both files are best-effort. If the ledger could not be created the loop still runs; it just has no memory outside the conversation.
145
172
 
146
- ## What actually ends a loop
147
-
148
- - **The turn cap** (`maxTurns`, default 25, `--max`) counts every turn the loop caused — continuations *and* pokes. It is the only cap: a settle-paced loop can run its whole life without delivering a single fallback wake, so counting wakes bounded nothing. Wakes are still counted and shown in `/loop status`; they cap nothing.
149
- - **Expiry** (default 7 days, `--expires`) grants one final turn to write state into `PROGRESS.md` — no new work, no completion claim.
150
- - **The no-progress breaker** pauses the loop after 3 consecutive tool-free turns with the same visible text. Restating a plan is the characteristic failure of an autonomous loop; calling any tool, including `loop_wait`, resets it.
151
-
152
- The interval is a fallback heartbeat, not the pacemaker — the loop advances when the session settles. So an interval argument is worth much less thought than the objective.
153
-
154
- ## When not to start a loop
173
+ ## When not to draft a loop
155
174
 
156
- An inline invocation *permits* a loop; it does not oblige you to start one. When the work is a bad fit, say so in one line and offer the alternative instead:
175
+ Planning being open *permits* a loop; it does not oblige you to propose one. When the work is a bad fit, say so in one line and offer the alternative instead:
157
176
 
158
- - **Recurring cadence** ("check the release queue every morning") → not a loop. A loop pursues one objective until it is done and then stops; it is not a timer, and an objective that is never "done" only produces turns until a cap. Use whatever scheduling your setup provides.
177
+ - **Recurring cadence** ("check the release queue every morning") → not a loop. A loop pursues one objective until it is done and then stops; it is not a timer, and an objective that is never "done" only produces turns until it expires. Use whatever scheduling your setup provides.
159
178
  - **Genuine open exploration** ("figure out why memory grows") → do the investigation in the conversation. A loop's value is the gate at the end; an investigation has no end state to gate on, so the loop only supplies unwanted turns. Once the investigation names a fix, *that* is a loop objective.
160
179
  - **Work that finishes this turn** → just do it, and say why no loop was needed.
161
180
 
162
- A vague-but-real objective is not in this list: start it, and name the end state back (above).
181
+ A vague-but-real objective is not in this list: draft the end state back and let the user correct it on the card.
163
182
 
164
- A loop is self-continuing: starting one wrongly does not produce one bad answer, it produces turns until a cap.
183
+ A loop is self-continuing: starting one wrongly does not produce one bad answer, it produces turns until it expires.