@tduyng/pi-harness-loop 0.0.2 → 0.0.4

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.
Files changed (2) hide show
  1. package/README.md +106 -346
  2. package/package.json +2 -2
package/README.md CHANGED
@@ -1,381 +1,141 @@
1
1
  # @tduyng/pi-harness-loop
2
2
 
3
- Reads what the sessions actually did, then proposes the harness part that would change it, and
4
- keeps the lesson that transfers.
5
-
6
- ## What it adds
7
-
8
- The loop runs when the session goes idle. The extension reads the sessions of the project, and
9
- if a failure recurred across them, it writes a brief with the evidence, one model call turns
10
- that brief into the part, and the part is installed. Nothing has to be typed, and the loop
11
- never takes the session over.
12
-
13
- A part that does not lower the rate it was written for is taken back out by the next idle, so
14
- the always-on context does not keep a rule the sessions did not need. `PI_HARNESS_LOOP=off`
15
- stops all of it.
16
-
17
- `/harness-loop` reports the setup, `/harness-loop propose [id]` runs a proposal now, and
18
- `/harness-loop lint` reports only the problems. `/harness-loop clear` takes the report off the
19
- screen, and the next prompt does it as well, so a report read once does not sit above the editor
20
- for the rest of the session.
21
-
22
- The harness tree: the skills and agent profiles pi loads, the problems in them, and what the
23
- skill block costs on every request. The rules come from pi's own loader, so they do not drift
24
- from it:
25
-
26
- - a skill with no description is dropped at load, so it never reaches the model
27
- - a skill whose frontmatter name differs from its directory is advertised under the frontmatter
28
- name, and a prompt that names the folder does not name the skill
29
- - a profile with no description is listed as "no description", so the model cannot choose it
30
- - a skill that tells the agent to run `scripts/something.py` without shipping that file sends the
31
- agent after a file that is not there
32
-
33
- The findings: what recurred in this session's transcript. The same failure three times, the same
34
- command three times, the same file read three times, a run that hit a failure and changed
35
- nothing, a tool call with no result.
36
-
37
- The recurrences: what recurred across the sessions of this project. The same failure in two
38
- sessions, the same command in three, the same file read in five. A defect that shows up in one
39
- run is noise. A defect that shows up in every run is a property of the harness, and the session
40
- files already on disk are the runs. A signature that reduces to `<path>` or a bare `Script
41
- failed` is not a finding, because it names nothing a part can change.
42
-
43
- A failure also needs independent evidence. When every session that hit it edited a file and each
44
- pair of those sessions shares one, they are one piece of work, and a rule written from it would
45
- encode that task rather than the harness. The recurrence is listed and held back. A session that
46
- edited nothing met the failure outside the edit loop, so it counts as independent.
47
-
48
- The skills nobody opened: pi advertises every skill by name and description in the system prompt,
49
- so a skill that was never opened is a tax on every request of every session. The report lists
50
- the advertised ones with the number of characters they cost, and it says how many skills are not
51
- advertised at all, because a skill the model cannot open cannot be opened by accident.
52
-
53
- The lessons: the ones whose cue matches the request in the editor, read from
54
- `.pi/harness/lessons.jsonl`.
55
-
56
- ## The loop on its own
57
-
58
- The session goes idle, and the loop looks at what the sessions of this project did. The scan is
59
- cached in `.pi/harness/scan.json` with a fingerprint of the session files, so a session that
60
- changed nothing does not pay for the scan again.
61
-
62
- The parsed form of each session file is cached beside it in `.pi/harness/corpus-cache.json`. A
63
- scan reads the files that changed, and the rest come back from the cache. At most 8 MB of new
64
- session files are parsed per idle, and every parsed session is kept. The report says how many
65
- it sees of the sessions on disk. The session being written is left out, so the file the loop is
66
- in does not force a rescan.
67
-
68
- The cache holds 400 sessions, so the oldest leave only when that ceiling is reached, not when
69
- the newest few exceed a byte budget.
70
-
71
- A failure that shows up in two sessions or more is a property of the harness, and the loop acts
72
- on it. A command or a file that shows up in every session is a habit, and a habit is a
73
- judgement call the operator makes, so the loop leaves those to `/harness-loop propose`.
74
-
75
- A new error string that is a smaller variant of an edit matching failure the loop already tried
76
- is not a new finding. The edit tool has one matching mechanic, so the 2-session "oldText must be
77
- unique" is the 9-session "oldText must match" with a different error string, and a second
78
- proposal for it is the same rule said twice.
79
-
80
- The failure's own command is replayed before a proposal. When it passes, the failure no longer
81
- reproduces, and the loop writes nothing. The same command runs again after an install, and its
82
- result stays with the install.
83
-
84
- A failure whose message the tree no longer carries is not a property of the harness any more.
85
- Before it proposes, the loop cuts the ids, the paths and the numbers out of the message, and
86
- searches the tree for what is left, then the dependencies. When neither has it and git shows a
87
- commit that touched that text after the last session that hit it, the producer was removed and
88
- the finding is skipped. The verdict is cached in `.pi/harness/producers.json`, keyed by the
89
- commit and the last session, because it is a search of the tree.
90
-
91
- The loop stands out of the way when the session has something else driving it. A goal that is
92
- active and the day's budget of five proposals each stop it. The writer is one completion, not a
93
- turn: it carries the brief, it has no tools, and it returns the files of the part as text. The
94
- files are parsed, checked and installed by the loop, so nothing the writer says is executed. The
95
- session is notified with what landed, and it keeps its own turn.
96
-
97
- The lesson tool arms itself for the run whose request names a trigger of a lesson that is still
98
- in force, and it is off otherwise, so the request with nothing to do with a lesson pays
99
- nothing. The cost on the runs that do arm it is about 259 tokens: the schema, the snippet and
100
- the guidelines.
101
-
102
- ## The proposal
103
-
104
- `/harness-loop propose [id]` runs the same path for one finding, one recurrence or one skill:
105
- brief, part, install. The brief asks for a skill, an agent profile or a rule, and the writer
106
- puts the files in `.pi/harness/<id>/`. The part is read back, checked against the same rules the
107
- tree is checked against, and installed. A part that breaks a rule is not installed, and the rule
108
- it broke is recorded with it. A reply with no file block leaves `REPLY.txt` beside the brief,
109
- which is what the writer said instead.
110
-
111
- A part under `global/` holds in every project. A skill under `global/` goes to the skills every
112
- project loads, and a global rule goes to the context file every project reads. It is not
113
- installed unless `global` is set in `.pi/harness.json`, because one project's failure does not
114
- get to become every project's rule. Without it the part lands in the project: `.agents/skills/<name>/`,
115
- `.pi/agents/<name>.md`, and the project's context file for a rule.
116
-
117
- The prompt says what each form costs. A skill is paid for by its name and description, about 300
118
- characters on every request, and its body costs nothing until the model opens it. A rule is paid
119
- for by its length and always applies. The rule wins when the behaviour has to hold every time,
120
- and the skill wins when the situation is specific enough to route on.
121
-
122
- The part may also carry `CONFIRM.md`, one shell command that exits 0 only when the finding can
123
- no longer happen. It must not write files or use the network. The loop runs it after the project
124
- check, and a confirmation that fails puts the part back. The brief asks for the command, and a
125
- part without one installs on the project check alone, recorded as unconfirmed.
126
-
127
- When the part passes, the session records the lesson that transfers: the cue (the shape of
128
- situation), the lesson (the operation on the harness), the way to confirm it worked, the
129
- evidence it rests on, and a confidence. The model may decline with `NO_ENTRY`, and then nothing
130
- is written. A block with no confirm line is dropped, because a rule nobody can check is a rule
131
- of thumb. The lesson is appended to `.pi/harness/lessons.jsonl` and matched against later
132
- requests by its trigger words. A lesson about a situation an earlier lesson already covers
133
- replaces it: the old one keeps its record, stops matching, and the report names the install
134
- that still holds the old rule.
135
-
136
- The trigger words are the ones the prompts of this project actually used, because a word no
137
- request ever carried cannot route to anything. A trigger seen once has to be at least eight
138
- characters and pass a consonant check. A typo is the rarest token in the corpus, and it routes
139
- nowhere.
140
-
141
- A request matches when it carries two triggers, or one trigger of six characters or more that
142
- the project rarely uses: one short word is a coincidence, and `key` is in half the requests of a
143
- project that does not talk about credentials. Two processes that record the same cue leave two
144
- lines in the file, and the read keeps the newest active one, so the same situation does not arm
145
- twice. A match records a use, so a lesson nobody ever recalls is visible in the record.
146
-
147
- ## The lint
148
-
149
- `/harness-loop lint` is the harness check on its own: the skills and agent profiles pi loads,
150
- against the rules pi's own loader applies, and the extension layer on top.
151
-
152
- The skill rules are the ones that bite silently. A skill with no `description` is dropped at
153
- load and never reaches the model. One whose frontmatter name does not match its directory is
154
- advertised under one name and looked for under another. Another that calls a helper script it
155
- does not ship points at a file that is not there. Two skills with the same name means the second
156
- one is dropped. An agent profile with no `description` is listed as "no description".
157
-
158
- The extension rules are the ones the repo's own scripts do not cover. A package under
159
- `extensions/` that declares an entry the root manifest never loads is a package nobody gets.
160
- An entry file that is not on disk is a manifest that points at nothing.
161
-
162
- The rule rules matter because the project's check cannot fail for a rule. A rule is markdown in
163
- a file no test reads, so the lint is the only gate it passes. A rule without a heading cannot
164
- be replaced later, a rule longer than thirty lines pays for itself on every request of every
165
- project, and a rule that carries a credential lands in a file every session reads.
166
-
167
- ## The check decides
168
-
169
- A part that passes the rules can still be wrong, and pi has no grader to say so. So the part is
170
- installed and the project's own check runs against it. A rule part installs without a check,
171
- because no test reads a markdown file, and the rate verdict below is what holds it. A skill or a
172
- profile waits for the check. The check is the first of:
173
-
174
- - `check` in `.pi/harness.json`
175
- - the `test` script in `package.json`, run with `bun run test` or `npm test`
176
- - the `test` recipe in `justfile`
177
-
178
- The check runs before the change and after it. The part is installed first, so the common case
179
- costs one run of the suite: when the check passes, the part stays. When it fails, the part goes
180
- back the way it was and the check runs once more on the tree without it, so the verdict is about
181
- the difference. A suite that was already failing the same way is not read as a verdict against
182
- the change. A part that adds a failure is put back, and no lesson is recorded, because a part the check turned
183
- down taught nothing.
184
-
185
- When the part carries a `CONFIRM.md` command, that command runs after the project check. A
186
- confirmation that fails puts the part back, because the part did not prove the finding gone. The
187
- command and its output stay with the install.
188
-
189
- When the finding carried a command, it is replayed after the install. Its result is recorded but
190
- does not decide the install, because the project check and the confirm command are the gates.
191
- Three installs a day is the ceiling for the loop, and the operator's own hand is not counted
192
- against it. The count is in `.pi/harness/installs.jsonl`. The ceiling is a budget that frees
193
- after a day, not a verdict: a proposal that lost to the budget stays open and the loop tries it
194
- again, and the spend is counted once.
195
-
196
- A second ceiling is the prompt itself. `maxStandingTokens`, 1,500 by default, is how many tokens
197
- the active installs may add together to the front of every request. An install that would cross
198
- it stays on disk with its brief, and the message names the current total, so the way through is
199
- to revert a part that did not carry rather than to raise the cap. A rule is paid in full on every
200
- request, which is why this ceiling counts the rule text and only the name and description of a
201
- skill. The report prints the standing total in tokens. `0` turns the ceiling off.
202
-
203
- A rule is identified by its heading, so a rule that is revised replaces the rule it revises
204
- rather than saying it twice.
205
-
206
- ## The sessions decide whether it held
207
-
208
- The check says the part did not break the project. It cannot say the failure stopped, and for a
209
- rule or a skill it cannot see the part at all, because no test reads a markdown file. So the
210
- install is read against the sessions that come after it.
211
-
212
- The signature of the finding is counted per session, before the install and after it. The rate
213
- is failures per session, and every session counts whether it hit the failure or not, because a
214
- session that did not fail is the evidence. The verdict:
215
-
216
- - `held` when the rate fell to three quarters of what it was, including to zero, and the
217
- sessions after it still call tools at more than half their old rate
218
- - `flat` when it stayed within a quarter either way
219
- - `worse` when it rose by a quarter or more
220
- - `avoided` when the rate fell but the work did too, so the failure stopped because the agent
221
- did less rather than because the part reached it
222
- - `confounded` when a commit touched the failure in the window, so the rate is not the part's
223
- - `early` until three sessions have run after the install
224
- - `out-of-window` when the corpus no longer holds a session from before it
225
-
226
- The work rate is the calls of every tool per session, read from the same parse as the
227
- signature, and a cache written before it counted tool calls reads as unknown and skips the
228
- check. The count is across tools rather than per tool, because a session that switched from
229
- `edit` to `write` did the work and fixed the failure.
230
-
231
- The verdict is computed on idle as well as in the report, so a lesson whose part did not carry
232
- stops matching without anyone typing a command.
233
-
234
- A `flat`, `worse` or `avoided` install is not the end of the finding. The loop tries it again,
235
- the brief says the earlier form did not carry and shows the two rates and the form itself, and
236
- the lesson that came with it stops matching, because a rule nobody can act on is worse than no
237
- rule. A finding whose part was all markdown and still did not carry is marked `needs code`. The
238
- loop stops proposing prose for it, because the failure lives in tool behavior and a rule cannot
239
- reach it. The report shows the verdicts, and a `held` finding is the one thing the loop can call
240
- done.
241
-
242
- A finding comes back as soon as the failure grows past the count it had at the last proposal,
243
- and a finding stops after three proposals, so one failure cannot spend the week's budget
244
- rephrasing the same rule.
245
-
246
- `/harness-loop revert <install>` puts the files back by hand, and the newest install goes first
247
- when two of them touched the same file. A file whose content changed after the install is left
248
- alone and named, because the operator's edit is not the loop's to overwrite. `/harness-loop
249
- install <id>` runs the same path over a part that is already on disk. One install runs at a time,
250
- through a lock beside the record.
251
-
252
- `.pi/harness.json` turns the loop off or changes the ceiling:
3
+ The harness fixes itself. It reads what a project's sessions repeated, writes the skill or rule that would have prevented it, installs it, and removes it when the failure rate does not fall.
4
+
5
+ A correction typed in every session lives in a chat message that dies with the session. A rule written by hand is usually worse, because nothing says which rule earns its place and the front of every request grows either way.
6
+
7
+ A session pays nothing until the loop acts. The report is a slash command rather than a tool. A tool would put its schema in the system prompt of every session, and that schema is the tax this package exists to reduce. `lesson` is the one tool, 252 tokens, and it arms only on a request naming a trigger of a lesson still in force.
8
+
9
+ ## Measure the rate, not the story
10
+
11
+ The loop installs a part, runs the project's check, and counts the sessions after the install against the sessions before it. Every session counts, including the ones that did not hit the failure, because a session that did not fail is the evidence.
12
+
13
+ The loop takes a part back out at the next idle if its failure rate did not fall, without anyone typing a command.
14
+
15
+ | Verdict | Meaning |
16
+ | --- | --- |
17
+ | `held` | The rate fell to three quarters of what it was, and sessions still call tools at more than half their old rate |
18
+ | `flat` | The rate stayed within a quarter either way |
19
+ | `worse` | The rate rose by a quarter or more |
20
+ | `avoided` | The rate fell but the work did too, so the failure stopped because the agent did less |
21
+ | `confounded` | A commit touched the failure in the window, so the rate is not the part's |
22
+ | `early` | Fewer than three sessions have run since the install |
23
+ | `out-of-window` | The corpus holds no session from before the install |
24
+
25
+ A `flat`, `worse` or `avoided` install is not the end of the finding. The next brief says the earlier form did not carry and shows both rates, and the part's lesson stops matching. A finding whose part was all markdown and still did not carry is marked `needs code`, and the loop stops proposing prose for it, because the failure lives in tool behaviour and a rule cannot reach it. A finding comes back when the failure grows past the count at the last proposal, and it stops after three.
26
+
27
+ ## The loop
28
+
29
+ The session goes idle. The loop reads the sessions of this project, and a failure that recurred gets a brief with the evidence. One model call turns that brief into the part, the loop checks it against the rules pi's loader applies, and installs it. The loop never takes the session over.
30
+
31
+ The writer has no tools. It carries the brief, returns files as text, and the loop parses, checks and installs them, so nothing the writer says is executed. A proposal costs one completion, and a part adds to the front of every later request, so both have a ceiling. The scan is cached in `.pi/harness/scan.json` behind a fingerprint of the session files.
32
+
33
+ `PI_HARNESS_LOOP=off` stops all of it.
253
34
 
254
- ```json
255
- {
256
- "check": "bun run test",
257
- "auto": false,
258
- "autoInstall": false,
259
- "global": false,
260
- "maxInstallsPerDay": 3,
261
- "maxStandingTokens": 1500,
262
- "writerModel": "opencode-go/mimo-v2.6-flash"
263
- }
264
35
  ```
36
+ /harness-loop the setup, the findings, the cost, the lessons
37
+ /harness-loop propose [id] run a proposal now
38
+ /harness-loop lint only the problems
39
+ /harness-loop revert <id> put the files back by hand
40
+ /harness-loop install <id> install a part already on disk
41
+ /harness-loop clear take the report off the screen
42
+ ```
43
+
44
+ The next prompt clears a report too, so one already read does not sit above the editor.
265
45
 
266
- `writerModel` is the model that writes the part. The loop reads three answers in this order:
267
- `writerModel` in the project's `.pi/harness.json`, `PI_HARNESS_WRITER_MODEL` in the environment,
268
- then the `model` in `.pi/subagent.json` or `~/.pi/agent/subagent.json`, which is the model pi
269
- already runs chores on. With none of them it uses the model of the session that is running, and
270
- a session on an expensive model pays that model for a short chore. The writer sees the brief
271
- and nothing else, so another model loses no context by writing it.
46
+ ## What the report covers
272
47
 
273
- `auto` is read when the session goes idle, and it stops the loop from writing a part on its
274
- own. The manual command still works. `autoInstall` is read when the part is ready, so a project
275
- that wants every change by hand sets it to `false` and the part stays on disk.
276
- `PI_HARNESS_LOOP=off` in the environment turns the automatic run off for every project without
277
- changing a file, and the command still works.
48
+ The report checks the harness tree against pi's own loader rules: a skill with no `description` is dropped at load, a frontmatter name that differs from its directory is advertised under one name and looked for under another, a profile with no description cannot be chosen by the model, a skill that runs a script it does not ship sends the agent after a missing file, and an `extensions/` package that declares an entry the root manifest never loads is a package nobody gets.
278
49
 
279
- A rule part installs without a project check, because no test reads a markdown file. A skill or
280
- an agent profile needs the check, because it changes what the loader sees. What a rule is held
281
- to instead is the rate of the failure it was written for: flat, worse or avoided after three
282
- sessions and the loop reverts it, records `auto-reverted`, and stops proposing prose for that
283
- finding.
50
+ Pi advertises every skill by name and description in the system prompt, so a skill nobody opens is a tax on every request. The report lists the advertised ones with the characters they cost and how many are not advertised at all.
284
51
 
285
- ## Taking it back out
52
+ The findings are what recurred in this session: the same failure three times, the same command three times, the same file read three times, a run that hit a failure and changed nothing, a tool call with no result. The recurrences are the same shapes across the sessions of this project.
286
53
 
287
- The loop reverts its own part when the sessions after it show the same rate or a worse one. The
288
- files go back byte for byte, the install is recorded as reverted with the two rates, and the
289
- finding is marked needs code, so the next attempt has to live in the tool rather than in a rule.
290
- A file the operator changed after the install is left alone and named, because that edit is not
291
- the loop's to overwrite.
54
+ The lessons are the ones whose cue matches the request in the editor, read from `.pi/harness/lessons.jsonl`.
292
55
 
293
- ## What the loop spends
56
+ ## Three checks before a part lands
294
57
 
295
- The loop is not free. A proposal costs one completion, and the completion carries the brief
296
- with the evidence, which is the whole input. `writerModel` names the model that runs it, and
297
- the output is the part. The tokens the provider reported are written to
298
- `.pi/harness/spend.jsonl` with the outcome, so a budget of five proposals a day is five
299
- completions and not five sessions. The report sums them.
58
+ Each one is a false positive that would otherwise get installed.
300
59
 
301
- A part also costs the prompt after it is installed, and that cost is not a completion. Every
302
- install records the characters it adds to the front of the prompt, which is the rule as it is
303
- appended or the name and description a skill is routed on, and the report sums the active ones.
304
- The brief carries that sum, so the writer knows what this project already pays on every request
305
- before it chooses a rule over a skill.
60
+ - **Replay**: the failure's own command runs again. When it passes, the failure no longer reproduces and nothing is written. The same command runs after an install and its result stays with the install.
306
61
 
307
- ## What it works with
62
+ - **Producer**: a failure whose message the tree no longer carries is not a property of the harness. The loop cuts ids, paths and numbers out of the message, searches the tree then the dependencies, and when neither has it and git shows a commit that touched that text after the last session that hit it, the finding is skipped.
308
63
 
309
- The other extensions drive the same session, so the loop reads their state instead of guessing at it.
64
+ - **Variant**: a new error string that is a smaller variant of an edit failure already tried is not a new finding. The edit tool has one matching mechanic, so the 2-session "oldText must be unique" is the 9-session "oldText must match" with a different error string.
310
65
 
311
- `pi-goal` owns the turns of a session with a goal. The loop does not propose while a goal is
312
- active, and it waits to install while one is, because both of them would be driving the same
313
- session at the same time.
66
+ A defect in one run is noise, a defect in every run is the harness. A signature that reduces to `<path>` or a bare `Script failed` is not a finding, because it names nothing a part can change. A failure also needs independent evidence: when every session that hit it edited a file and each pair shares one, they are one piece of work, and a rule written from it would encode that task rather than the harness.
314
67
 
315
- `pi-goal` owns the check runner in `src/check.ts`. The loop uses the same runner, so
316
- the two do not disagree about what a check said.
68
+ The loop stands out of the way when something else drives the session: an active goal, or the day's budget of five proposals, stops it.
317
69
 
318
- `pi-context` prunes the context the model sees, not the session, so the findings still see the
319
- whole branch. The recurrences read the session files, which no compaction touches.
70
+ ## What a part may be
320
71
 
321
- `pi-guard` blocks the shell from reading credentials. The rule it needs is in the global
322
- context, and it says what to do instead of what is blocked.
72
+ The brief asks for a skill, an agent profile or a rule, and the writer puts the files in `.pi/harness/<id>/`. The part is read back, checked against the same rules the tree is checked against, and installed.
323
73
 
324
- ## What it found here
74
+ A part under `global/` holds in every project, and it is installed only when `global` is set, because one project's failure does not get to become every project's rule. Without it the part lands in the project: `.agents/skills/<name>/`, `.pi/agents/<name>.md`, or the project's context file for a rule.
325
75
 
326
- The measurements come from the sessions on this machine, so they are about this setup rather than
327
- about pi.
76
+ The prompt says what each form costs. A skill is paid for by its name and description, about 300 characters on every request, and its body costs nothing until opened. A rule is paid for by its length and applies always. The rule wins when the behaviour has to hold every time, and the skill wins when the situation is specific enough to route on.
328
77
 
329
- - 12 of the 19 skills are not advertised at all, because their frontmatter says
330
- `disable-model-invocation`, so they cost nothing. The other 7 are advertised, and the block
331
- they make is 2,652 characters, about 663 tokens on every request of every project.
78
+ A part may carry `CONFIRM.md`: one shell command that exits 0 only when the finding can no longer happen, with no file writes and no network. It runs after the project check, and a failing confirmation puts the part back. Without one the part installs on the project check alone, recorded as unconfirmed.
332
79
 
333
- - 5 of those 7 were opened in none of the 26 sessions scanned last. They cost 1,447 of the 2,652
334
- characters, about 362 tokens per request. `tdd` was the sixth, and the loop's finding for it
335
- is the one change that came from a proposal and nothing else: 39 sessions, no open, and the
336
- skill stopped being advertised on 2026-10-26.
80
+ ## The check behind an install
337
81
 
338
- An open is a read of a `SKILL.md` or a `/skill:<name>` command in a prompt, so a skill reached
339
- by hand is not called dead. The report names the rest and the number, and the operator
340
- decides, because a skill that was never needed is not the same as a skill that cannot be
341
- found.
82
+ The check is the first of `check` in `.pi/harness.json`, the `test` script in `package.json`, or the `test` recipe in `justfile`. The loop installs the part first, so the common case costs one run: when the check passes, the part stays. When it fails, the loop puts the part back and runs the check once more without it, so the verdict is about the difference rather than about a suite that was already red. A rule installs without a check, since no test reads a markdown file.
342
83
 
343
- - The shell blocked 14 reads of credential files across 2 sessions, and every retry with another
344
- shell reached the same block.
84
+ `maxStandingTokens` is the second ceiling, 1,500 tokens by default. An install that would cross it stays on disk with its brief, and the message names the current total, so the way through is to revert a part that did not carry rather than to raise the cap. A rule is charged in full, a skill only for its name and description. `0` turns the ceiling off.
345
85
 
346
- The rule that came out of it is in the global context, and it says what to do instead of what
347
- is blocked.
86
+ `/harness-loop revert <install>` puts the files back by hand, newest install first when two touched the same file. A file whose content changed after the install is left alone and named, because an edit is not the loop's to overwrite.
348
87
 
349
- - The edit tool rejected 9 old texts across 4 sessions, and each rejection reported a closest
350
- match the model had paraphrased.
88
+ ## The lesson that transfers
351
89
 
352
- The rule that came out of it says to copy the old text out of the output of the read that
353
- preceded the edit, and to read the file again when an edit is rejected.
90
+ When a part passes, the session records the cue, the lesson, how to confirm it worked, the evidence, and a confidence. The model may decline with `NO_ENTRY`, and a block with no confirm line is dropped, because a rule nobody can check is a rule of thumb.
354
91
 
355
- - 4 reads were refused for being over 350 lines across 3 sessions, and each one was a file a
356
- grep would have narrowed.
92
+ A request matches a lesson when it carries two triggers, or one trigger of six characters or more that the project rarely uses. One short word is a coincidence, and `key` is in half the requests of a project that never talks about credentials. A match records a use, so a lesson nobody recalls is visible in the record.
357
93
 
358
- The rule that came out of it says to grep first.
94
+ ## With the rest of the suite
359
95
 
360
- ## Why the report is a command and not a tool
96
+ `pi-goal` owns the turns of a session with a goal, so the loop does not propose while one is active and waits to install while one is, and it uses the same check runner so the two never disagree about what a check said. `pi-context` prunes what the model sees, not the session, so findings still see the whole branch. `pi-guard` blocks the shell from reading credentials, and the rule it needs is in the global context, saying what to do instead of what is blocked.
361
97
 
362
- A tool would put its schema in the system prompt of every session, which is the tax this package
363
- exists to reduce. The command costs nothing until it is typed.
98
+ ## Findings from this machine
364
99
 
365
- The lesson tool is the one tool, and it arms itself for the run whose request names a trigger
366
- of a lesson in force, so a session can record a lesson the moment it hits the failure that
367
- taught it. It is 259 tokens on the runs that arm it and nothing on the others. The words that
368
- count are the ones that say the situation: a word in more than a fiftieth of the prompts is
369
- noise, and a word no prompt ever used cannot route. The tool is fixed for the run, so a failure
370
- that surfaces in a later turn of the same run still finds it.
100
+ - 12 of 19 skills are not advertised at all, because their frontmatter says `disable-model-invocation`, so they cost nothing. The other 7 make a block of 2,652 characters, about 663 tokens on every request.
101
+
102
+ - 5 of those 7 were opened in none of 26 sessions scanned, costing 362 tokens per request. `tdd` was the sixth until the skill stopped being advertised on 2026-10-26, from a proposal and nothing else.
103
+
104
+ - The shell blocked 14 reads of credential files across 2 sessions, and every retry with another shell reached the same block. The rule from it says what to do instead.
105
+
106
+ - The edit tool rejected 9 old texts across 4 sessions, and each rejection reported a closest match the model had paraphrased. The rule from it says to copy old text out of the read that preceded the edit.
107
+
108
+ - 4 reads were refused for being over 350 lines across 3 sessions, each a file a grep would have narrowed. The rule from it says to grep first.
109
+
110
+ ## Install
111
+
112
+ ```bash
113
+ pi install npm:@tduyng/pi-harness-loop
114
+ ```
115
+
116
+ Local checkout: `pi install /path/to/dotfiles/pi/agent/extensions/pi-harness-loop`
117
+
118
+ The scans use `rg`, so it has to be on PATH. Sessions of the project must exist on disk, which they do after one run.
119
+
120
+ `.pi/harness.json`, every key optional:
121
+
122
+ ```json
123
+ {
124
+ "check": "bun run test",
125
+ "auto": false,
126
+ "autoInstall": false,
127
+ "global": false,
128
+ "maxInstallsPerDay": 3,
129
+ "maxStandingTokens": 1500,
130
+ "writerModel": "opencode-go/mimo-v2.6-flash"
131
+ }
132
+ ```
371
133
 
372
- ## What it is not
134
+ `writerModel` resolves from the project file, then `PI_HARNESS_WRITER_MODEL`, then the `model` in `subagent.json`, which is the model pi already runs chores on. With none of them the session's own model writes it, and a session on an expensive model pays that model for a short chore. `auto` is read on idle and stops the loop writing a part on its own while the manual command still works. `autoInstall` is read when the part is ready, so a project that wants every change by hand sets it to `false` and the part stays on disk.
373
135
 
374
- It does not score the harness. It measures the rate of the failure a part was written for,
375
- which is not the same as the work being better: a session ends, and nothing says whether the
376
- work was good. The findings are the part that can be read without a grader, and the rate is the
377
- part that says whether the part held.
136
+ ## Limits
378
137
 
379
- It does not compare two runs of the same task. The comparison it makes is across the sessions
380
- of this project, which are different tasks. The same task run twice is what a benchmark does,
381
- and what a working session does not.
138
+ - Five proposals a day, three installs a day, six turns per proposal, three proposals per finding.
139
+ - It does not score the harness. It measures the rate of the failure a part was written for. Whether the work got better is a different question.
140
+ - It does not compare two runs of the same task. The comparison is across the sessions of this project, which are different tasks.
141
+ - A rule is markdown in a file no test reads, so `/harness-loop lint` is the only gate it passes.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tduyng/pi-harness-loop",
3
- "version": "0.0.2",
4
- "description": "Reads what the session repeated, proposes the harness part that would have prevented it, and records the lesson that transfers.",
3
+ "version": "0.0.4",
4
+ "description": "Reads what the sessions repeated, writes the skill or rule that would have prevented it, and takes it back out when the rate does not drop.",
5
5
  "keywords": [
6
6
  "pi",
7
7
  "pi-package",