pi-continual-harness 0.20.0 → 0.22.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 ADDED
@@ -0,0 +1,1035 @@
1
+ # Changelog
2
+
3
+ All notable changes to **pi-continual-harness** are documented here. The format
4
+ follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the
5
+ project adheres to [Semantic Versioning](https://semver.org/).
6
+
7
+ Releases are tag-driven (`vX.Y.Z`) and published by GitHub Actions via npm
8
+ Trusted Publishing. This file begins at 0.7.0; earlier releases are recorded in
9
+ the git tags (`git tag -l`) and the [GitHub release history](https://github.com/pungggi/pi-continual-harness/releases).
10
+
11
+ ## [0.22.0] — 2026-11-11
12
+
13
+ ### Added
14
+
15
+ - **Classifier auto-discovery** (pi ≥ 1.5.1 interplay): when `classifier.model`
16
+ is unset, the classifier seam now falls back to a single known-good hosted
17
+ classifier — `github-copilot/microsoft-decision-1`, shipped by pi 1.5.1+
18
+ with GitHub Copilot auth — so classifier-confirmed dedupe and the auto-refine
19
+ gate work with zero setup for Copilot users. Discovery deliberately never
20
+ picks a local llama.cpp entry (the catalog can list one while no server
21
+ runs). An explicit `classifier.model` always wins; `"classifier": { "auto":
22
+ false }` restores explicit-only. `/harness status` and the setup/reminder
23
+ copy now treat the gate as the only required key when discovery is on.
24
+ - **Determinism contract for the injected block** (pinned by a test): for an
25
+ unchanged store + config, `renderHarnessBlock` is byte-identical. This is
26
+ what lets pi 1.5.1+'s Anthropic message threads `continue` a conversation —
27
+ the thread-prefix hash covers the system prompt, so unchanged harness state
28
+ rides the server-side thread and the warmed prompt cache, while any harness
29
+ change mismatches the hash and pi correctly falls back to a full resend.
30
+ Idle prompt-cache warming (pi 1.5.1) similarly keeps the injected system
31
+ prefix warm between runs — no package change needed, both verified against
32
+ the pi-ai 1.5.2 source.
33
+ - Dev dependencies bumped to `@earendil-works/pi-*` `^1.5.2` (runtime peers
34
+ stay `>=1.2.0`; nothing 1.5-only is imported at runtime).
35
+
36
+ ## [0.21.0] — 2026-11-07
37
+
38
+ ### Added
39
+
40
+ - **CI on every push/PR** (`.github/workflows/ci.yml`): typecheck + lint +
41
+ tests with coverage + build, so breakage is caught before a release tag —
42
+ previously verification only ran at publish time in `release.yml`.
43
+ - **Biome** for lint + format (`biome.json`, `npm run lint` / `npm run
44
+ format`), with `noExplicitAny` and unused-code checks on; the codebase is
45
+ clean against it (the one `Model<any>` in `refine.ts` became `Model<Api>`).
46
+ - **Coverage measurement** (`npm run test:coverage`, `vitest.config.ts`):
47
+ v8 provider with thresholds — 95% statements package-wide, and 100% for the
48
+ pure policy modules (`select.ts`, `lessons.ts`, `cadence.ts`, `vmodel.ts`)
49
+ whose "pure, deterministic, unit-tested" property the README advertises.
50
+ - **Provenance trust boundary**: every item now records who last wrote its
51
+ content. `harness_mutate` creates and content rewrites (plus all `/refine`
52
+ proposer writes) are stamped `provenance: "agent"` server-side; the model
53
+ can never set the field (smuggled values are stripped at the tool boundary,
54
+ and agent writes can only ever lose trust, never gain it). Absent provenance
55
+ means trusted, so owner-authored items, AGENTS.md imports, and existing
56
+ stores need no migration. Two on-by-default gates under
57
+ `harness.json { "provenance": { "label": true, "priority": true } }`:
58
+ **label** renders agent items as `(unverified: agent-authored)` in the
59
+ injected block (plus one defining intro line, paid only when such an item is
60
+ surfaced) and `(unverified)` chips in `/harness list` (both hidden by
61
+ `label: false`); **priority** fills each kind's item slots and the injection
62
+ budget with trusted items first, so an agent note can never crowd
63
+ an owner note out of the block (stores without agent items queue identically
64
+ to before). The durable file carries a `- provenance: agent` sub-line
65
+ (durable wins on import: hand-deleting the line trusts the item); legacy
66
+ files stay byte-identical. Stamping always happens — the gates only affect
67
+ presentation. Inspired by Protect-Context's forced agent trust level.
68
+ New tests: `test/provenance.test.ts` (22).
69
+ - **`makePersist(pi)`** (store.ts): the one factory for the
70
+ `appendEntry("harness-state", …)` snapshot callback every mutation path
71
+ uses. The entry-type literal and persist closure were previously
72
+ copy-pasted at ~10 call sites — a typo in any one silently broke `/tree`
73
+ restore for that path.
74
+ - **`validateDeltas`** boundary guard (tools.ts) plus `maxLength` caps on
75
+ `content`/`evidence`/`reason` in the tool schemas: a model could previously
76
+ create unbounded-size items that flowed verbatim into the durable files.
77
+ - `test/entry.test.ts` pins the **turn_end registration order** (durable
78
+ sync's export before outcome/auto-refine) via the new exported
79
+ `TURN_END_CHAIN`; the invariant used to live only in a comment.
80
+ - `test/hardening.test.ts` covers the new hardening behaviors (corrupt
81
+ snapshot coercion, ENOENT vs real read errors, flag validation, the
82
+ boundary guard).
83
+ - **pi 1.5 conformance tests** (`test/schemas.test.ts`): pi 1.5 moved
84
+ tool-argument validation to `@earendil-works/pi-schema` (breaking change —
85
+ new coercion rules for JSON-Schema tools, new failure-message format). The
86
+ shipped hand-written schemas are now run through the same
87
+ `compile({ coerce: true })` path pi applies to tool arguments: well-formed
88
+ batches pass, loosely typed args coerce (`importance: "0.8"` → `0.8`,
89
+ `active: "true"` → `true`, a bare delta wraps into a one-item array), and
90
+ schema violations still reject after coercion (range/required/enum/
91
+ minItems/maxItems asserted by path+keyword, not message text).
92
+ - **`CHANGELOG.md` is published** (added to npm `files`): pi ≥ 1.5 shows
93
+ users what changed after an extension update, reading the installed
94
+ package's changelog — which was previously never shipped. Timely, because
95
+ 1.5 also fixed `-e npm:` extensions to actually update on start.
96
+ - **`.gitattributes` pins LF** (`* text=auto eol=lf`): with
97
+ `core.autocrlf=true` on Windows, a fresh clone checks out CRLF while
98
+ biome.json enforces `lf`, so `npm run check` failed locally (50 format
99
+ errors) while Linux CI stayed green. New clones now check out LF.
100
+
101
+ ### Fixed
102
+
103
+ - **Explicit-path `/harness import` now honors the path's layer scope**: the
104
+ `defaultScope` from `defaultScopeForPath` was computed and then never
105
+ threaded through `reconstructFromDurable`, so importing a project-dir file
106
+ by path silently defaulted untagged items to the global scope (contradicting
107
+ the documented behavior; surfaced by the new unused-variable lint).
108
+ - Only `ENOENT` counts as "no durable file": a real read error (EACCES,
109
+ EISDIR, …) now surfaces as the import failure it is instead of the
110
+ misleading "No durable files — run /refine --commit" advice.
111
+ - **Corrupt-snapshot field types are coerced at `reconstruct`** instead of
112
+ crashing far-away renders later (`importance: "high"` died at `toFixed(2)`,
113
+ an unknown `kind` at per-kind tallies).
114
+ - The `session_start` durable auto-import is wrapped in the same soft-fail
115
+ try/catch as its `turn_end` counterpart — a throw can no longer escape the
116
+ session_start handler.
117
+ - `/harness push-mem`, `prune`, `adopt`, `keep|drop`, `move`, `split`,
118
+ `lessons`, `import|export|export-corpus`, and `setup` now **warn on
119
+ invalid/extra arguments** instead of silently falling back (the rule
120
+ `/harness list` already documented): an invalid `--kind` used to quietly
121
+ push memories, a bad `--decay` to prune without decay — and, per the PR
122
+ review, a *misspelled* flag (`prune --decya 30`, `push-mem --knd skill`)
123
+ was still silently ignored. A grammar gate (`ARG_SPECS`/`checkRest`) now
124
+ rejects unknown flags, valueless value-flags, and extra positionals for
125
+ every subcommand before dispatch.
126
+ - **Durable sync survives an unreadable layer** (PR review): a layer that
127
+ fails to read (EACCES, EISDIR, …) no longer crashes `syncDurableOnStart`
128
+ or blocks the healthy ones — it is skipped and reported **once per
129
+ session**, the readable layers import, and auto-export stays paused until
130
+ every layer reads again (an unread layer must never be overwritten by a
131
+ store that never saw it). The turn_end catch-up retries quietly and the
132
+ sync self-heals; previously every turn re-warned and even the healthy
133
+ global layer never exported. Manual `/harness import` still surfaces the
134
+ real error (unchanged).
135
+ - `validateDeltas`' create branch now checks `importance` (number in [0,1])
136
+ and `scope` like the update branch (PR review: a non-pi host could send
137
+ `importance: "high"` past the guard); `clamp` in the store is NaN-safe as
138
+ defense in depth for direct-apply proposers; and a smuggled `ownerModel`
139
+ on a model-facing `harness_mutate` delta is stripped at the boundary
140
+ (same rule as `support` — creates always stamp to the active model).
141
+ - Under `virtualBinding: "physical"`, a mid-run route change re-caches the
142
+ binding key at `context_with_system`, so `harness_mutate` creates stamp
143
+ with the same key injection renders (previously they could diverge until
144
+ the next agent start).
145
+ - `loadConfig` is single-flight (concurrent cold callers share one read)
146
+ with an epoch guard, so a read that raced a cache reset can no longer
147
+ repopulate the cache with a stale result.
148
+
149
+ ### Changed
150
+
151
+ - Internal consolidation, no behavior change: shared constants in
152
+ `types.ts` (`KIND_VALUES`/`KIND_ORDER`/`KIND_LABEL`, `KEEP_DROP_BUMP`) and
153
+ `schemas.ts` (`MAX_DELTAS`, which `ADOPT_BATCH_CAP` now derives from instead
154
+ of a hand-synced copy); `tally`/`AnyEntry`/`preview` deduplicated across
155
+ modules; the audit-trail walk extracted into one `walkAuditTrail` shared by
156
+ `status.ts` and `corpus.ts`; `layerPaths` exported as `defaultLayerPaths`
157
+ and reused; `runRefine`, `mergeParsed`, `handleSetup`, and the `/harness`
158
+ completions split into focused helpers; dead exports `registerHarness` /
159
+ `registerAutoRefine` removed.
160
+ - README: the long per-key Configuration reference moved to
161
+ **[docs/CONFIG.md](docs/CONFIG.md)**; the README keeps the shape, a key
162
+ table, and the link. Status section updated to 0.20.x. ROADMAP phases
163
+ reordered chronologically (8 → 13).
164
+ - `ProposeInput`'s optional fields are now explicitly `| undefined`-tolerant
165
+ (matches the package's `exactOptionalPropertyTypes` convention; type-level
166
+ only).
167
+ - **Adopted pi 1.5 in the toolchain**: devDependencies
168
+ `@earendil-works/pi-ai` / `pi-coding-agent` `^1.3.0` → `^1.5.0` (+ explicit
169
+ `@earendil-works/pi-schema` devDependency for the conformance tests), so CI
170
+ runs against the validator pi 1.5 actually applies to tool arguments. No
171
+ runtime code changes were needed — the tool schemas have been plain JSON
172
+ Schema since the issue-#36 startup refactor, which is exactly what 1.5's
173
+ pi-schema migration validates — and `peerDependencies` stay `>=1.2.0`
174
+ because nothing 1.5-only is imported at runtime. README Status notes
175
+ tested-against-pi-1.5.
176
+ - **Typed tool factories** (schemas.ts): `harnessListTool` /
177
+ `harnessMutateTool` now type their `execute` argument with the hand-written
178
+ `Static<>` params (a `ToolExecute<P>` derived from pi's own `execute`
179
+ signature, so rest/return types cannot drift), and the one cast back to
180
+ pi's `Static<TSchema>` slot lives inside the factories. The four
181
+ `params as Harness*Params` casts at the registration sites (entry.ts,
182
+ tools.ts) are gone. Type-level only — the emitted dist is byte-identical
183
+ (verified: the #36 startup graph and timings are unchanged).
184
+
185
+ ## [0.20.0] — 2026-10-21
186
+
187
+ ### Added
188
+
189
+ - **Harness²-inspired harnessing-level recursion** (arXiv
190
+ [2610.14224](https://arxiv.org/abs/2610.14224)): the improvement process now
191
+ learns from what past improvement yielded, using only signals already on the
192
+ branch — no extra model calls.
193
+ - **Support counters** on every item: `citations` (the outcome loop saw the
194
+ agent cite `[h_xxxx]`), `keeps` / `drops` (`/harness keep|drop`). Dedupe
195
+ merges carry them onto the keeper; durable export/import round-trips them
196
+ via a `support:` sub-line; `/harness list -f` and the `harness_list` tool
197
+ show them.
198
+ - **Lessons digest** injected into the `/refine` steering prompt:
199
+ evidence-gated sections for items proven useful (cited/kept), rejected
200
+ signals (drops strictly outweighing later use — a drop followed by
201
+ re-citations is contested, not rejected), and recent refine-run
202
+ productivity. Never-cited items are never named (no signal ≠ negative
203
+ signal); the digest is scoped to the **active model's items** (per-model
204
+ isolation applies to what the improver is told). Visible
205
+ any time via the new **`/harness lessons`** command.
206
+ - **Scoped refine proposals**: the steering prompt classifies every create
207
+ as invariant (cross-domain agentic behavior → `scope: "global"`) or
208
+ domain-specific (project knowledge → `scope: "project"`).
209
+ - **History gate** for auto-refine: `autoRefine.refineNoOpSkip` (default 3,
210
+ `0` disables) skips a cadenced refine when the last N refine runs all
211
+ produced nothing durable. Runs before the classifier gate and costs
212
+ nothing; **bounded to one consecutive skip** so the gate can never latch
213
+ shut against later trajectory evidence; manual `/refine` bypasses it.
214
+ - New exports for companion packages: `buildLessonsDigest`, `classifyRuns`,
215
+ `isEffectiveRun`, `recentIneffectiveRuns`, `mergeSupport`, `renderSupport`,
216
+ `parseSupport`, `buildSteeringPrompt`, `DEFAULT_REFINE_NOOP_SKIP`,
217
+ `HISTORY_GATE_MAX_CONSECUTIVE_SKIPS`, and the
218
+ `SupportInfo` type.
219
+ - **`harness.schema.json`** — published JSON Schema for `harness.json`
220
+ (same pattern as pi ≥ 1.2's published config schemas). Both doc examples
221
+ now start with a `$schema` line pointing at the raw GitHub URL; the schema
222
+ also ships inside the npm package (`files`). The loader already ignores
223
+ unknown keys — `$schema` included — and `/harness setup` rewrites preserve
224
+ it; a new test pins both behaviors.
225
+
226
+ ### Changed
227
+
228
+ - README: noted that codemode (pi ≥ 1.2) renders the harness tools' input
229
+ constraints as `@` tags on property comments (`@minimum 0 @maximum 1` on
230
+ `importance`, `@maxItems 20` on the batch cap) — already declared by the
231
+ tool schemas, so scripts see the limits for free.
232
+
233
+ ## [0.19.0] — 2026-10-16
234
+
235
+ ### Changed
236
+
237
+ - **pi 1.2 adoption** — the peer floor moves from `>=0.99.0` to `>=1.2.0`.
238
+ - Every `pi.sendUserMessage` steering send now passes an explicit
239
+ `deliverAs`: `"steer"` for `/refine` (mid-run injection when busy) and
240
+ `"followUp"` for the auto-refine boundary fallback and the `/harness`
241
+ split/adopt/push-mem steering prompts (queued instead of throwing). Pi
242
+ 1.2 counts the session as busy from run start, and a bare send throws
243
+ "Agent is already processing" in that window.
244
+ - Extension-triggered runs (`pi.sendMessage(..., { triggerTurn: true })`,
245
+ e.g. scheduled tasks) now fire `before_agent_start` with
246
+ `messageType: "custom"`, so the model-binding bridge — binding key,
247
+ orphan adoption, injection — covers them. Before pi 1.2 those runs ran
248
+ with an empty binding cache.
249
+
250
+ ## [0.18.0] — 2026-10-11
251
+
252
+ ### Changed
253
+
254
+ - **Near-zero startup cost: thin entry + precompiled dist** (issue #36). Pi
255
+ loads extension entries sequentially and awaits every factory before
256
+ startup continues, so the old all-static TypeScript entry paid its whole
257
+ import graph (~230KB, jiti-transpiled) on every launch: **~0.8s marginal**
258
+ in the 2026-10-11 measurement (9.4s total with all 16 packages installed).
259
+ Measured now: **~30ms marginal** (bogus-provider fast-fail, issue's
260
+ acceptance target was ≤150ms).
261
+ - **`pi.extensions` now points at `./dist/entry.js`** — a thin factory that
262
+ only registers hooks, commands, and tool schemas. Every feature module is
263
+ dynamic-imported inside its own handler (`session_start`, `turn_end`,
264
+ `before_agent_start`, `context_with_system`, command handlers, tool
265
+ `execute`), so nothing heavy evaluates until the relevant event fires.
266
+ - **The package ships precompiled `dist/`** (`main: dist/index.js`, types
267
+ included): no per-startup jiti transpile, and companion packages get real
268
+ JavaScript + `.d.ts` instead of raw TS via `main`. `src/` is no longer part
269
+ of the npm tarball; `prepack` builds `dist/` and CI verifies it exists.
270
+ - **Tool schemas are hand-written plain JSON Schema objects** (module
271
+ `src/schemas.ts`) instead of TypeBox builders: pi does not preload `typebox`
272
+ or `pi-ai` before extensions in the one-shot path, and importing either
273
+ costs ~0.4–1.0s cold. `test/schemas.test.ts` guards that the emitted JSON
274
+ is identical to the historical TypeBox output.
275
+ - **No behavior change**: commands, tools, hooks, registration order, and the
276
+ public API are identical. The feature modules' `register*` functions remain
277
+ (tests use them); they now delegate to the same extracted handlers the thin
278
+ entry calls.
279
+
280
+ ### Notes for companion authors
281
+
282
+ - `getArgumentCompletions` for `/harness` may now return a Promise (pi's API
283
+ allows `AutocompleteItem[] | null | Promise<AutocompleteItem[] | null>`);
284
+ sync values still work. Only relevant if you wrap the command.
285
+ - Importing `pi-continual-harness` from a companion is now cheaper too: the
286
+ package root is precompiled JS, not jiti-transpiled TS.
287
+
288
+ ## [0.17.0] — 2026-10-11
289
+
290
+ ### Fixed
291
+
292
+ - **Companion proposer registrations were silently invisible under pi's
293
+ extension loader** — pi gives every extension a fresh, module-cache-disabled
294
+ jiti instance, so a companion package's
295
+ `import { registerProposer } from "pi-continual-harness"` resolved to a
296
+ DIFFERENT copy of the module than the one whose `/refine` reads the
297
+ registry. `registerProposer` wrote to a private map, `getProposer` never saw
298
+ it, and `/refine --proposer <name>` silently fell back to `steering`.
299
+ Verified against the real loader (pi 1.1.0): after running the companion's
300
+ factory, `listProposers()` on the harness copy still showed only the
301
+ built-ins.
302
+ - The registry is now backed by a **process-wide global**
303
+ (`Symbol.for("pi-continual-harness.proposers")`), which is shared across all
304
+ of pi's isolated module instances. `registerProposer` writes to it;
305
+ `getProposer` reads it first and falls back to the module-local built-ins
306
+ (`steering`, `dedupe`), so two harness copies never hand each other's
307
+ closures to `/refine`. Foreign entries without a `propose` function are
308
+ ignored defensively.
309
+ - **No API change**: same `registerProposer` / `getProposer` / `listProposers`
310
+ signatures. Upgrading the harness alone also fixes companion packages that
311
+ were built against older versions (their private copy of
312
+ `registerProposer` now writes to the same shared registry).
313
+
314
+ ### Notes for companion authors
315
+
316
+ - Companions that want a zero-import startup entry can skip importing this
317
+ package entirely and push a `DeltaProposer` (typically a lazy delegate that
318
+ dynamic-imports its implementation on first `propose()`) straight into
319
+ `globalThis[Symbol.for("pi-continual-harness.proposers")]`. The string key
320
+ `"pi-continual-harness.proposers"` is the stable public contract.
321
+
322
+ ## [0.16.1] — 2026-10-11
323
+
324
+ Deep-review release: six correctness fixes (each found by a full code review,
325
+ `review findings #1–#7` below) plus the simplifications that remove the drift
326
+ surfaces the bugs grew on.
327
+
328
+ ### Fixed
329
+
330
+ - **`/harness export-corpus` dropped the first snapshot's `created` events**
331
+ (review finding #1) — `buildCorpus`'s snapshot walk skipped the branch's
332
+ FIRST `harness-state` entry, while `/harness status` had the implicit
333
+ empty-baseline fix since review #27. A fresh session's entire first create
334
+ batch (often the auto-import) never landed in `lifecycle.jsonl`, and the
335
+ two reports disagreed on `created` counts for the same branch. The corpus
336
+ walk now diffs against `{ items: [] }` exactly like the status aggregator.
337
+ - **Completions crashed on prototype-collision subcommands** (review finding
338
+ #2) — typing `/harness constructor --` (or `toString`, `valueOf`, …) hit
339
+ `sub in FLAGS` / `FLAGS[sub] ?? []`, which resolve `Function` through the
340
+ prototype chain: not nullish, so `.filter` threw. Both the condition and
341
+ the lookup now go through `Object.hasOwn` (same class as the review #26
342
+ `expandPreset` fix).
343
+ - **No-op `/harness prune` appended a `harness-state` entry** (review finding
344
+ #3) — `decayAndPrune` persisted unconditionally. A prune that decayed and
345
+ removed nothing now persists nothing, matching the `mergeParsed` /
346
+ `adoptOrphans` idempotency contract (no `/tree` noise).
347
+ - **Re-importing a durable file without an export in between duplicated
348
+ items** (review finding #4) — id-less bullets and foreign (non-`h_`) ids
349
+ never hit the id map, so every import created a fresh copy. Import now
350
+ falls back to a kind+content match: the re-import folds into the item the
351
+ first import created (and under `--prune`, content-matched items count as
352
+ "present in the file" — no delete+recreate id churn).
353
+ - **One corrupt `harness-state` entry could break `session_start`** (review
354
+ finding #5) — `reconstruct` called `.map` on whatever `state.items`
355
+ happened to be. Non-array `items` and non-object entries are now filtered
356
+ out instead of throwing; a fully-corrupt snapshot degrades to an empty
357
+ store.
358
+ - **Malformed `harness.json` silently reset every setting** (review finding
359
+ #6) — the loader fell back to defaults with zero feedback. `loadConfig`
360
+ now flags the parse failure (`configMalformed()`) and `session_start`
361
+ warns once per session: "not valid JSON — every setting is on its DEFAULT
362
+ until the file parses."
363
+ - **Typo'd `/refine` flags and proposer names vanished silently** (review
364
+ finding #7) — `--comit` ran a plain no-commit refine; `--proposer dedupee`
365
+ silently fell back to steering. Unknown `--flags` and unknown proposer
366
+ names now produce a warning (the refine still runs).
367
+
368
+ ### Changed
369
+
370
+ - **One shared turn-cadence counter** (new `src/cadence.ts`) backs
371
+ auto-refine, the `/refine` reminder, and the import-awareness reminder —
372
+ three hand-rolled copies of seed-on-first-completed-turn / count-completed
373
+ / reset-on-fire had already drifted once (review #26's turnIndex fix, and
374
+ issue #29 grew on that drift). `turnEndedBadly` moved to `cadence.ts`
375
+ (still re-exported from `auto-refine.ts`). Behavior is identical for every
376
+ consumer except one normalization: the import reminder now also treats
377
+ `everyTurns <= 0` as disabled (previously it fired every completed turn
378
+ until the reminder cap).
379
+ - `durableLayer` is exported from `proposer.ts` and shared with `corpus.ts`
380
+ (the dedupe-pair layer key had to stay in sync by hand).
381
+ - The split/adopt steering prompts render their item listings through one
382
+ shared `formatItemLines()` (adopt keeps full-precision importance — the
383
+ agent copies it verbatim; split rounds). The two formats had already
384
+ drifted once.
385
+ - Status aggregation counts lifecycle events with an exhaustive `switch` —
386
+ a future `LifecycleEvent` falls to an explicit `default` instead of being
387
+ silently counted as `deleted`.
388
+ ## [0.16.0] — 2026-10-11
389
+
390
+ ### Added
391
+
392
+ - **`/harness list [-f] [--kind <kind>] [--scope <global|project|all>]`**
393
+ (issue #31): the read view of the store in the TUI. What `/harness status`
394
+ counts, but with the actual item text: no agent turn and no trip to the
395
+ layer files required.
396
+ - **Compact rows by default**: id, kind, importance, scope, ~60-char
397
+ preview, one line per item, ranked importance-first (ties keep store
398
+ order, the same stable policy as injection selection). Inactive items
399
+ carry an `(inactive)` marker.
400
+ - **`-f`**: full text plus the evidence line, harness_list-style, extended
401
+ with owner model and scope in the parenthetical.
402
+ - **Scope filters mirror the durable layers**: default = global + the
403
+ active project; foreign-project items are hidden and counted in a
404
+ trailing note (empty results included) so they look hidden, never
405
+ missing. `--scope project` lists every project's items with the slug on
406
+ each row; `--scope all` lists everything.
407
+ - **Guidance over bare zeros**: an empty store gets zero-state first steps,
408
+ an empty filter result gets kind tallies of what the store holds, and
409
+ invalid `--kind`/`--scope` values or unexpected arguments warn instead of
410
+ silently listing unfiltered.
411
+ - **Autocomplete**: `list` in the subcommand menu, `-f`/`--kind`/`--scope`
412
+ flag rows (flag matching now also catches short `-` prefixes), and value
413
+ menus for both filters.
414
+ - **Read-only**: no store mutation, no durable writes, one notify. The
415
+ selection/rendering core is pure in `src/list.ts`
416
+ (`selectListItems`/`renderListReport`) and re-exported from the package
417
+ entry; `preview()` consolidated there.
418
+ ## [0.15.1] — 2026-10-11
419
+
420
+ ### Fixed
421
+
422
+ - **`/refine` crash on plain-string message content** — pi messages carry
423
+ `content` as a plain string OR an array of content blocks depending on the
424
+ message source; both trajectory readers (`gatherEvidence` behind `/refine`,
425
+ and the opt-in `outcomeImportance` loop's `turn_end` scan) assumed blocks and
426
+ threw `((intermediate value) ?? []).map is not a function` the moment a
427
+ branch contained a string-content message. Both now flatten through one
428
+ shared pure helper, `messageText()` (re-exported from the package entry):
429
+ strings pass through, block arrays join on spaces, and any other shape
430
+ degrades to `""` instead of throwing. No behavior change otherwise —
431
+ citations, evidence lines, and cadences are untouched.
432
+
433
+ ## [0.15.0] — 2026-10-11
434
+
435
+ The activation, visibility, and onboarding release: issues **#19–#22**
436
+ (activation nudges, rich status, cross-model adoption, setup wizard) plus
437
+ the pending 0.14.0 outcome-guard work.
438
+
439
+ > **Fold note:** releases 0.11.0–0.14.0 shipped from this same accumulated
440
+ > section without header cuts (see git tags / npm history), so their entries
441
+ > are retained below verbatim. Per-release headers resume here.
442
+
443
+ ### Changed
444
+
445
+ - **`remindRefine` defaults flipped: on, every 25 turns** (issue #19). Fresh
446
+ installs were silent, so the cheapest activation lever (a notify-only
447
+ nudge) never fired. A reminder mutates nothing, which keeps the "manual by
448
+ default" safety posture intact while the `/refine` habit gets built for
449
+ you. **Migration note for existing silent-by-default users:** if your
450
+ `harness.json` has no `remindRefine` key you will start seeing nudges
451
+ (first one at 25 completed turns). Restore the old silence with
452
+ `{ "remindRefine": { "enabled": false } }`. Explicit config keeps winning
453
+ in both directions, and the cadence still counts completed turns per the
454
+ aborted/error-inert semantics (seed on the first observed completed
455
+ `turn_end`, reset per fire and per session).
456
+
457
+ ### Added
458
+
459
+ - **Tiered, rotating reminder content (`remindRefine.detailEvery`)** — every
460
+ 4th fired reminder (default) escalates from the one-line nudge to a
461
+ detailed educational guide, rotating through 7 slots so repeat viewers
462
+ never see the same wall of text: how the improvement loop works (3
463
+ alternating descriptions), the `/refine` workflow, `keep`/`drop`/
464
+ `prune --decay <days>` hygiene, `push-mem` (pi-mem composition), `split`
465
+ (global vs project scopes), `export-corpus` (pi-reflex contract), and a
466
+ recommended starter config whose guidance is honest about autonomy (review
467
+ #28): with `autoImport` on, durable layers are already written on every
468
+ store change — `commit` only adds a refine-time flush when `autoImport` is
469
+ off — so the conservative start is `autoImport` off + manual
470
+ `/harness export`. Config: `"remindRefine": { "everyTurns": 25,
471
+ "detailEvery": 4 }` with `1` = every reminder detailed; non-integer/sub-1
472
+ values coerce to the default (spam guard). The short nudge is now
473
+ data-aware ("12 active item(s), 4 never cited since creation"; an empty
474
+ store gets first-run guidance instead), derived from `getState()` with no
475
+ new persisted state. Copy builders are pure and exported (`buildNudge`,
476
+ `buildDetailGuide`, `reminderState`, `ReminderState`) so the richer
477
+ `/harness status` (companion issue) can share the same copy source.
478
+
479
+ - **Awareness reminders for durable sync (`importRemind`)** — with
480
+ `"autoImport": true`, silently-imported instructions now announce
481
+ themselves. The session-start import lists **what arrived** (item id, kind,
482
+ first words of content — bounded to 5 lines; an id present in both layers
483
+ is listed once, with its post-merge content), and a one-line `turn_end`
484
+ reminder re-surfaces the imported batch every `importRemind.everyTurns`
485
+ **completed** turns (default 25; aborted/error `turn_end`s are inert — they
486
+ neither seed nor fire, so they never consume one of the bounded notices,
487
+ and the count cannot be defeated by pi resetting `turnIndex` on every
488
+ `agent_start`). Both pointers are truthful (review #28): the overflow line
489
+ and the reminder point at the agent's `harness_list` tool (which actually
490
+ lists ids/content), not `/harness status` (counts only), and the advertised
491
+ kill-switch is valid, paste-able JSON. Bounded by design: at most 3
492
+ reminders per imported batch, then quiet until the next session — awareness
493
+ without nagging.
494
+ Config: `"importRemind": { "enabled": true, "everyTurns": 25 }`
495
+ (effective only with `autoImport`; kill-switch `enabled: false` keeps the
496
+ start-of-session listing). `DurableImportResult` now carries
497
+ `createdIds`/`updatedIds` (additive) so the sync knows exactly which items
498
+ to re-surface, and `imported` counts DISTINCT items — an id folded across
499
+ layers is one item, not one per copy (so the "imported N item(s)" notify
500
+ no longer double-counts collisions);
501
+ `evaluateImportReminder`/`registerImportReminder`/
502
+ `getImportedThisSession`/`summarizeImported` exported for tests and
503
+ companions.
504
+
505
+ ### Removed
506
+
507
+ - **`durableScope` config key removed** (deprecated since 0.9.0 as a no-op):
508
+ items carry their own scope and durable I/O is always layered, so the key
509
+ switched nothing. The dead `resolveDurablePath()` helper is gone too (no
510
+ callers). Config files that still set `durableScope` load unchanged —
511
+ unknown keys are ignored; migration is the documented one-time
512
+ `/harness import` + `/harness move`/`split`.
513
+
514
+ ### Fixed
515
+
516
+ - **Review #27 fixes (all unreleased features, amended in place)** — impact's
517
+ implicit empty baseline (first-snapshot `created 0`), per-model
518
+ update/merge attribution, status keying under `virtualBinding: "physical"`,
519
+ honest classifier-gate hint (names `classifier.model` AND `autoRefine.gate`),
520
+ and imported refs reconciled against the live store every `turn_end` (an
521
+ item deleted or deactivated after the import drops out of the reminders;
522
+ review pointers now target `harness_list`, which lists ids/content — status
523
+ intentionally stays aggregate-only).
524
+
525
+ - **`harness_list` empty results are now diagnostic** — the bare
526
+ `"Harness state is empty."` collapsed three very different situations into
527
+ one opaque line, which users read as "the feature is broken": (1) a
528
+ genuinely empty store (a new session starts empty — durable sync is
529
+ opt-in), (2) everything owned by another model (per-model isolation hides
530
+ it from the default view), (3) a `kind` filter that matched nothing. Each
531
+ case now gets its own actionable text: zero-state guidance (`/refine`,
532
+ `harness_mutate`, `"autoImport": true`), an owner tally with a
533
+ `model:"*"` pointer, and a kind tally respectively. The diagnosis is
534
+ attributed from the store's actual contents (review #23): a kind miss for a
535
+ model that owns items is never reported as model isolation, and the
536
+ `model:"*"` pointer only appears when the requested kind actually exists
537
+ for another model. `structuredContent` is unchanged (stable schema); only
538
+ the model-facing text changed.
539
+
540
+ ### Added
541
+
542
+ - **Rich `/harness status` (issue #20)** — the status report is now a
543
+ three-band, still read-only notify. **Impact**: derived from the session
544
+ branch's audit trail by reusing the corpus lifecycle classification
545
+ (`classifyTransition` now exported with item attribution): refine runs
546
+ (manual vs auto), items created / updated (direct-apply, a lower bound) /
547
+ kept / cited (config-aware bump) / dropped / pruned / deleted / merged
548
+ (dedupe merges), whole-store plus a per-active-model line (lifecycle events
549
+ AND direct-apply update/merge ops are owner-attributed; the key follows the
550
+ configured `virtualBinding` policy). The branch's first snapshot diffs
551
+ against an implicit empty baseline, so a fresh session's first create batch
552
+ counts its items. **Injection
553
+ stats**: renders that served items, items surfaced, last block's estimated
554
+ tokens vs `injection.maxTokens`, and selection-omitted counts
555
+ (`InjectionStats`, recorded at render time in `inject.ts`, reset on
556
+ `session_start`). **Zero-state guidance**: 0 active items → actionable
557
+ first steps (`/refine`, MANUAL Quick start) instead of bare zeros. **Hint**:
558
+ one rotating contextual one-liner per call (classifier gate, outcome loop,
559
+ `push-mem`, `remindRefine`, `split`), never repeating the previous call's
560
+ hint (`src/hints.ts`, shared copy source for #19's content library). The
561
+ report builder is pure and exported — `buildStatusReport` /
562
+ `renderStatusReport` — and `handleStatus` stays thin glue. No new persisted
563
+ state; no mutation.
564
+
565
+ - **Cross-model adoption: `/harness adopt --from <provider/id> [--all]`**
566
+ (issue #21) — the audited, agent-filtered path for carrying **owned** items
567
+ across models (per-model isolation starts every new model id on a blank
568
+ harness; orphan adoption only covered *unowned* items). Steers the agent to
569
+ classify the source model's active items **generalizes** (model-agnostic:
570
+ engineering practices, user preferences, project facts) vs **stays** (that
571
+ model's quirks, formatting preferences, failure modes), then apply ONE
572
+ `harness_mutate` batch of create deltas — **copy, not move**: the source
573
+ harness is never touched, content is copied verbatim, evidence is annotated
574
+ `adopted from <provider/id>`, and because `ownerModel` is never
575
+ model-settable the creates stamp to the **active** model via the existing
576
+ create-stamping path (audited, `/tree`-rollback-able). `--all` adopts
577
+ everything without the filter. The batch is capped at 20 items (matching
578
+ `harness_mutate`'s per-call delta limit so the whole adoption fits one
579
+ batch), keeping the highest-importance items first and announcing any
580
+ truncation. Guards: missing `--from`, empty store, unknown model (warning
581
+ lists the owners present, with counts), self-adopt. `--from` values
582
+ complete from the model keys in the store. Overlaps afterwards are the
583
+ dedupe proposer's job: `/refine --proposer dedupe`. No automatic adoption
584
+ exists — explicit, manual, reviewable only. Review #25 hardening: the
585
+ self-adopt guard compares against the **resolved binding key**
586
+ (`virtualBinding`-aware, the same key create-stamping uses — a router
587
+ selection under `physical` adopts as the routed model); foreign-project
588
+ source items are skipped rather than re-stamped into the current
589
+ session's project layer (adopt them from a session in that project);
590
+ content the adopter already owns is skipped **before** the cap so repeated
591
+ adoption of a >20-item source progresses instead of re-selecting the same
592
+ top items; and importance is listed at **full precision** so an adopted
593
+ copy keeps its exact ranking. `buildAdoptPrompt`,
594
+ `selectAdoptSource`, and `ADOPT_BATCH_CAP` re-exported from the package
595
+ entry.
596
+
597
+ - **`/harness setup` — guided configuration wizard (presets, reviewed
598
+ harness.json write)** — every opt-in feature used to require hand-editing
599
+ `~/.pi/agent/harness.json` with exact key spellings; the wizard replaces
600
+ that with presets, per-feature confirms, and one safe write. Flow: show
601
+ the current effective config (file merged over defaults) + durable-layer
602
+ paths; choose `quiet` (pin the shipped defaults), `recommended`
603
+ (`autoImport` + `importRemind` on, with a gate-readiness note), or `power`
604
+ (`recommended` + `autoRefine`); confirm each implied delta (autonomous
605
+ switches get their own explicit "mutates state on a cadence" confirm
606
+ regardless of preset); then write via the new `saveConfig` — timestamped
607
+ backup (`harness.json.bak-<timestamp>`), atomic same-dir write, unknown
608
+ keys preserved, and a reload that round-trips through the loader's
609
+ coercion path so the printed effective config is exactly what pi reads.
610
+ Scripted form: `/harness setup --preset recommended [--yes]` (`--yes` is
611
+ the scripted consent and skips every prompt). Nothing is written on
612
+ unknown input, a cancelled preset menu, or a declined final write;
613
+ declining an individual feature only skips that feature. The wizard
614
+ touches config files only — never harness state. Preset values derive
615
+ from `DEFAULT_CONFIG` (not hardcoded), so wizard and loader cannot drift.
616
+ Presets are pure data: `PRESETS` / `expandPreset` / `planWrite` / `setPath`
617
+ / `saveConfig` exported for tests and companions; subcommand +
618
+ `--preset`/`--yes` flags autocomplete in the TUI.
619
+
620
+ - **Awareness reminders for durable sync (`importRemind`)** — with
621
+ `"autoImport": true`, silently-imported instructions now announce
622
+ themselves. The session-start import lists **what arrived** (item id, kind,
623
+ first words of content — bounded to 5 lines), and a one-line `turn_end`
624
+ reminder re-surfaces the imported batch every `importRemind.everyTurns`
625
+ turns (default 25). Bounded by design: at most 3 reminders per imported
626
+ batch, then quiet until the next session — awareness without nagging.
627
+ Config: `"importRemind": { "enabled": true, "everyTurns": 25 }`
628
+ (effective only with `autoImport`; kill-switch `enabled: false` keeps the
629
+ start-of-session listing). `DurableImportResult` now carries
630
+ `createdIds`/`updatedIds` (additive) so the sync knows exactly which items
631
+ to re-surface; `evaluateImportReminder`/`registerImportReminder`/
632
+ `getImportedThisSession`/`summarizeImported` exported for tests and
633
+ companions.
634
+
635
+ - **Aborted/error turns are inert (pi `turn_end` `BoundaryState.outcome`)** —
636
+ a `turn_end` whose outcome is `"aborted"` or `"error"` no longer counts
637
+ toward the auto-refine or reminder cadence: both windows now count
638
+ *completed* turns (never raw turn indices, so an aborted turn in between
639
+ cannot shorten a window). The outcome loop seeds its scan cursor on the
640
+ first observed `turn_end` regardless of outcome, then defers an aborted
641
+ turn's partial assistant text to the next completed turn — a refine can no
642
+ longer fire, and the classifier gate no longer judges, a trajectory that
643
+ was cut off. A missing outcome (synthetic events, earliest 0.99.x) is
644
+ treated as completed. Durable auto-export deliberately still exports on
645
+ aborted turns (committed entries must not be lost). Pure `turnEndedBadly()`
646
+ re-exported from the package entry; `evaluateAutoRefine`/`evaluateReminder`
647
+ accept an optional `outcome` argument (the dead `turnIndex` parameter was
648
+ dropped — the count replaced the index arithmetic).
649
+ - **pi 1.1.0 classifier catalog documented** — `classifier.model` accepts the
650
+ new entries with no code change: OpenAI's GPT-6 Luna (Decisions API, e.g.
651
+ `openai/gpt-6-luna`, needs `OPENAI_API_KEY`) and the native llama.cpp
652
+ decision models (Julia-1, Laya, Kev, lev, OpenJev through `/v1/systemone`,
653
+ llama.cpp ≥ 0.6.0 — classifier-only listings, a free local gate). README +
654
+ MANUAL updated; no minimum-pi change (the APIs used still ship by 0.99.0).
655
+
656
+ ### Removed
657
+
658
+ - **`durableScope` config key removed** (deprecated since 0.9.0 as a no-op):
659
+ items carry their own scope and durable I/O is always layered, so the key
660
+ switched nothing. The dead `resolveDurablePath()` helper is gone too (no
661
+ callers). Config files that still set `durableScope` load unchanged —
662
+ unknown keys are ignored; migration is the documented one-time
663
+ `/harness import` + `/harness move`/`split`.
664
+
665
+ ### Fixed
666
+
667
+ - **`harness_list` empty results are now diagnostic** — the bare
668
+ `"Harness state is empty."` collapsed three very different situations into
669
+ one opaque line, which users read as "the feature is broken": (1) a
670
+ genuinely empty store (a new session starts empty — durable sync is
671
+ opt-in), (2) everything owned by another model (per-model isolation hides
672
+ it from the default view), (3) a `kind` filter that matched nothing. Each
673
+ case now gets its own actionable text: zero-state guidance (`/refine`,
674
+ `harness_mutate`, `"autoImport": true`), an owner tally with a
675
+ `model:"*"` pointer, and a kind tally respectively. The diagnosis is
676
+ attributed from the store's actual contents (review #23): a kind miss for a
677
+ model that owns items is never reported as model isolation, and the
678
+ `model:"*"` pointer only appears when the requested kind actually exists
679
+ for another model. `structuredContent` is unchanged (stable schema); only
680
+ the model-facing text changed.
681
+ - **`harness_list`'s `model:"*"` pointer also holds when the model owns
682
+ nothing (review #26)** — requesting a kind that exists for NO model used
683
+ to still show the pointer (which returns nothing with the same kind
684
+ filter); that case now says "no `<kind>` items for this model or any other
685
+ model" with an honest kind tally, and the pointer only appears when the
686
+ kind exists elsewhere.
687
+ - **Late `autoImport` enable can no longer clobber the durable layers
688
+ (review #26)** — enabling `autoImport` mid-session (wizard or hand-edit)
689
+ used to make the very next `turn_end` auto-export overwrite the
690
+ global/project durable files with the never-imported live store (the
691
+ session-start import had been skipped, so saved refinements were lost).
692
+ The export hook now runs the layered import FIRST when the session's
693
+ start-sync never happened; a failing catch-up import defers the export
694
+ to the next turn.
695
+ - **Cadences count `turn_end` events, not pi's `turnIndex` (review #26)** —
696
+ pi resets `turnIndex` to 0 on every `agent_start` (each user prompt), so
697
+ the import-reminder (and the older `/refine` reminder) cadences could
698
+ never accumulate across prompts: 25 separate prompts never fired a
699
+ reminder. Both reminders now count completed turn_end events
700
+ (session-stable ordinal; resets on session_start as before).
701
+ - **`saveConfig` is safe under concurrency (review #26)** — the temp file is
702
+ unique per call (UUID + exclusive `wx` create), so two concurrent writes
703
+ (two pi sessions) can never truncate or rename each other's temp file;
704
+ the final rename retries briefly on Windows' transient `EPERM` (target
705
+ just replaced).
706
+ - **`planWrite` merges like `saveConfig` (review #26)** — the wizard's
707
+ reviewed JSON preview previously dropped nested unknown keys (e.g. a
708
+ future `importRemind.<key>`), which the write then re-added from disk, so
709
+ the written file could differ from the confirmed preview. Deltas now
710
+ apply with the same two-level deep-merge: the preview equals the file.
711
+ - **`expandPreset` no longer leaks `Object.prototype` (review #26)** —
712
+ `--preset constructor` (or `toString`/`__proto__`) passed validation and
713
+ crashed; inherited names now report `Unknown preset` and nothing is
714
+ written.
715
+ - **The import-reminder's disable hint is now valid JSON (review #26)** —
716
+ the suggested snippet used single quotes; pasting it broke the whole
717
+ config file (all settings fell back to defaults). It now reads
718
+ `"importRemind": { "enabled": false }`.
719
+ - **Global/project id collisions list once (review #26)** — an id created
720
+ by the global layer then updated by the project layer appeared twice in
721
+ the "what arrived" listing and inflated the reminder counts; the batch
722
+ is now de-duplicated.
723
+
724
+ ### Added
725
+
726
+ - **Stale-draft hygiene for auto-refine (pi 0.87 context edits)** — every
727
+ boundary delivery now also appends `context_edit` drafts omitting each LIVE
728
+ prior `harness.auto-refine-request` from future provider context, so at most
729
+ ONE auto-refine request (the newest) is ever model-visible in long sessions.
730
+ Already-omitted drafts are skipped (idempotent — never stacks edits); raw
731
+ history, usage, and UI history are untouched, so `/tree` rollback and the
732
+ HTML export still show every draft. Kill-switch:
733
+ `"autoRefine": { "omitStaleDrafts": false }` (default `true`).
734
+ `buildOmissionDrafts` exported for reuse.
735
+
736
+ ### Added (0.12.0 — pi 0.87/0.99 adoption)
737
+
738
+ - **Live re-injection via `context_with_system` (pi 0.87)** — the harness block
739
+ is now rendered on EVERY provider request (full-transcript system-message
740
+ transformation), not baked into the base prompt once per agent run. A
741
+ `harness_mutate` that fixes a stale note mid-run shows up on the very next
742
+ request. `before_agent_start` remains the model-binding bridge (key cache +
743
+ orphan adoption) but no longer rewrites the prompt; when there is nothing to
744
+ inject the extension does not own the transcript at all.
745
+ - **Virtual-model binding policy (pi 0.99)** — `ctx.model` names the *virtual*
746
+ model when a router extension is selected, which silently split the harness
747
+ per router. New config `"virtualBinding": "virtual" | "physical"` (default
748
+ `virtual`, the previous behavior): `physical` keys items by the *physical*
749
+ model behind the latest successful response on the branch, so notes
750
+ accumulated under `anthropic/claude-x` keep injecting through a router that
751
+ routes there; before the first response the binding is unknown (creates stay
752
+ orphans, adopted on first contact). Pure helpers (`isVirtualModel`,
753
+ `physicalKeyFromBranch`, `resolveBindingKey`) re-exported from the package
754
+ entry.
755
+ - **Classifier integration (pi 0.99)** — `ctx.modelRegistry.classify()` behind
756
+ a minimal structural `ClassifyFn` seam (`ProposeInput.classify`, re-exported):
757
+ - `"classifier": { "model": "typesafe/jev-latest" }` (or any classifier
758
+ catalog entry, e.g. a local llama.cpp classifier) enables both paths.
759
+ - **Classifier-confirmed dedupe**: the `dedupe` proposer recall-widens its
760
+ candidate pass (threshold − 0.15, floor 0.35) and confirms each pair with
761
+ ONE batched yes/no call (≤ 20 pairs). Only confirmed pairs merge; a
762
+ classifier error falls back to the plain rule-based plan — the classifier
763
+ can narrow merges, never block them. Telemetry (`modelCall`) recorded in
764
+ the audit entry. `planDedupePairs` / `deltasFromGroups` / `confirmGroups` /
765
+ `buildPairQuestions` exported for reuse.
766
+ - **Auto-refine gate**: `"autoRefine": { "gate": true }` asks one yes/no
767
+ question over the recent trajectory before spending a refine pass;
768
+ "no durable correction" skips it. Errors fall back to the plain cadence.
769
+ - **Structured tool results (pi 0.99)** — `harness_list` and `harness_mutate`
770
+ declare `outputSchema` and return `structuredContent` (items array / applied
771
+ counts), so codemode scripts and `ctx.executeTool()` callers consume the
772
+ harness programmatically; the model still sees the text content.
773
+ - **Boundary delivery for auto-refine (pi 0.87)** — the steering prompt is no
774
+ longer sent as a synthetic user message: `runRefine` (with the new
775
+ `delivery: "boundary"`) returns it undelivered and the `turn_end` handler
776
+ persists it as a structural `harness.auto-refine-request` custom-message
777
+ draft with `{ entries, continue: true }` — one guaranteed next provider
778
+ request with no steering-queue or follow-up-scheduling side effects. Falls
779
+ back to `sendUserMessage` when the boundary cannot continue. Manual `/refine`
780
+ still steers (it may run while idle).
781
+
782
+ ### Changed
783
+
784
+ - **Minimum pi version is now 0.99.0** (peer + dev dependencies). All adopted
785
+ APIs ship by 0.99.0 (`context_with_system`, actionable `turn_end`
786
+ boundaries, classifier `classify()`, `outputSchema`/`structuredContent`,
787
+ virtual models).
788
+
789
+ ### Added (earlier in this release)
790
+
791
+ - **`/harness export-corpus [path]`** — the calibration corpus exporter for
792
+ the pi-reflex consumer contract (§4, issue #12). Reconstructs two JSONL corpora
793
+ from the session branch's own audit trail: `dedupe-pairs.jsonl` (merged
794
+ pairs as ground-truth `dup` records parsed from delete reasons; same
795
+ key-field non-merged pairs with informative overlap as `needs_review`
796
+ `not_dup` candidates) and `lifecycle.jsonl` (`created` / `cited` / `kept` /
797
+ `dropped` / `pruned` / `deleted`, classified from consecutive snapshot
798
+ diffs). Pure `buildCorpus()` core in `src/corpus.ts` (+
799
+ `parseDedupeDelete`), re-exported from the package entry. Default output
800
+ `./harness-corpus/<yyyy-mm-dd>/`; local file writes only — nothing leaves
801
+ the machine.
802
+
803
+ - **Review fixes (PR #14 feedback)** — three correctness issues in the first
804
+ cut: (1) the dedupe audit entry now records the full `appliedDeltas` list
805
+ (incl. delete reasons) alongside the legacy `applied` count — without it,
806
+ dup pairs were unreconstructable and the exporter crashed on the count-only
807
+ legacy shape; (2) `not_dup` candidates now emit from the CURRENT state — a
808
+ no-merge dedupe run writes no snapshot, so the previous two-snapshot walk
809
+ read a stale state; (3) citation classification is config-aware
810
+ (`buildCorpus(entries, { citeBump })`, wired to `outcomeImportance.bump`,
811
+ default 0.03) instead of a hardcoded `(0, 0.06]` band; keep/drop (±0.1) are
812
+ checked first, so a configured bump of exactly 0.1 classifies as keep.
813
+
814
+ ## [0.10.0] — 2026-09-20
815
+
816
+ The merge-capable dedupe proposer: fuzzy-corrections take 1. Spec and research
817
+ grounding in [docs/PLAN-dedupe-merge.md](docs/PLAN-dedupe-merge.md).
818
+
819
+ ### Added
820
+
821
+ - **`dedupe` proposer merges instead of deleting** — two active items sharing
822
+ the key fields (kind, owner model, durable layer: scope + project slug) with
823
+ token overlap ≥ the threshold merge into the higher-importance keeper: one
824
+ audited `update` (evidence = line-wise union, capped at 2000 chars) + one
825
+ `delete` per duplicate (`merged into h_x (overlap …)`). The keeper's content
826
+ is never prose-merged (ACE); injection is content-only, so the prompt block
827
+ is unchanged. Identical evidence degenerates to a plain delete.
828
+ - **Config keys** `dedupe: { "threshold": 0.6, "merge": true }` in
829
+ `harness.json` — threshold valid in `(0,1]` (bad values degrade to `0.6`),
830
+ `merge: false` restores the pre-0.10 delete-only behavior.
831
+ - **`/refine --threshold <t>`** — one-shot threshold override for the run
832
+ (affects the `dedupe` proposer; invalid values are ignored with a warning).
833
+ - **`ProposeInput.config`** — `runRefine` now threads the loaded `harness.json`
834
+ into every proposer (optional, additive), so proposers read tuned knobs
835
+ without file I/O.
836
+ - **Public API**: `planDedupe(state, opts)` (the pure planner), `DedupeOptions`
837
+ (with a `similarity?` seam for the future semantic upgrade), `DEFAULT_DEDUPE`,
838
+ `EVIDENCE_MERGE_CAP`, `unionEvidence`, `DEDUPE_THRESHOLD` re-exported from
839
+ the package entry.
840
+ - **`SimilarityResult` abstain seam** — `DedupeOptions.similarity` may return
841
+ `{ score, abstain }` instead of a plain number: an abstaining pair is treated
842
+ as NOT duplicates (keep both), so a semantic engine with conformal-style
843
+ uncertainty (e.g. pi-reflex, formerly pi-jev — local-only for now) can decline to
844
+ merge on uncertainty. `SimilarityResult` is re-exported from the package
845
+ entry; plain numeric comparators keep working unchanged.
846
+
847
+ ### Changed
848
+
849
+ - The `dedupe` proposer's default behavior (was: delete-only, hardcoded 0.6).
850
+ Rollback path: set `"dedupe": { "merge": false }`.
851
+
852
+ ## [0.9.0] — 2026-09-17
853
+
854
+ The project-scope split (issue #7). Scope becomes a property of each item, the
855
+ durable file becomes durable **layers**, and one opt-in flag makes `/refine`
856
+ output survive into new sessions without export/import ceremony.
857
+
858
+ ### Added
859
+
860
+ - **Per-item durable scope** — `HarnessItem.scope: "global" | "project"`
861
+ (+ `project` slug). Scope decides which durable layer an item is exported to
862
+ / imported from; it never filters per-turn injection.
863
+ - **`/harness move <id> <global|project>`** — flip an item's layer
864
+ (audited update delta, `/tree`-rollback-able; `project` stamps the current
865
+ session's slug). Autocompletes ids then scope values.
866
+ - **`/harness split`** — the interactive migration helper: steers the agent
867
+ to classify every active item global-vs-project and apply the result as one
868
+ `harness_mutate` batch of scope-only updates (visible, audited,
869
+ rollback-able — the push-mem pattern).
870
+ - **Layered durable I/O** — `/harness export` (no path) partitions items
871
+ into `~/.pi/agent/harness-state.md` + `harness-state/<slug>.md`;
872
+ `/harness import` (no path) merges global first then the current project's
873
+ file (project wins id collisions; `--prune` is union-scoped across
874
+ layers). Explicit paths keep classic single-file semantics. Project items
875
+ carry a `scope: project (<slug>)` sub-line so any copy round-trips.
876
+ `/refine --commit` now exports the layers too. `/harness status` shows
877
+ both layers + the scope split.
878
+ - **`scope` on `harness_mutate` create/update deltas** — server-side slug
879
+ stamping from the session cwd (the tool never sees raw slugs).
880
+ - **Opt-in durable sync: `"autoImport": true`** (`src/durable.ts`) — bundles
881
+ both directions: `session_start` layered auto-import (loss-free merge,
882
+ silent when nothing changes — imports are now idempotent and skip the
883
+ persist when the live store already matches the files) + `turn_end` layered
884
+ auto-export whenever the store version changed since the last export. Every
885
+ action is visible and uses the same `harness-state` entries (`/tree`
886
+ rollback covers them).
887
+
888
+ ### Changed
889
+
890
+ - **`durableScope` is deprecated** (no-op): durable I/O is always layered on
891
+ per-item scope now. The key still parses so existing configs keep loading;
892
+ migration is one layered `/harness import` + `/harness move`/`split`.
893
+ - Import merge is **idempotent**: an import that changes nothing persists
894
+ nothing (no session-tree noise from repeated or auto imports).
895
+ - Layered import handles the same id in **both** layers (project copy wins,
896
+ no duplicate items).
897
+
898
+ [0.15.1]: https://github.com/pungggi/pi-continual-harness/compare/v0.15.0...v0.15.1
899
+ [0.15.0]: https://github.com/pungggi/pi-continual-harness/compare/v0.13.0...v0.15.0
900
+ [0.9.0]: https://github.com/pungggi/pi-continual-harness/compare/v0.8.1...v0.9.0
901
+
902
+ ## [0.8.1] — 2026-09-13
903
+
904
+ The completions release. `/harness` now registers `getArgumentCompletions`, so
905
+ subcommands stop being one long crammed command description and become a
906
+ filtered autocomplete menu instead. Handler logic is unchanged — purely UX.
907
+
908
+ ### Added
909
+
910
+ - **`/harness` argument autocomplete** (`src/harness.ts`) — two levels:
911
+ - `/harness <partial>` lists the seven subcommands (import, export, status,
912
+ prune, keep, drop, push-mem), each with a one-line description, filtered
913
+ case-insensitively as you type (matching the handler's `toLowerCase()`
914
+ tolerance).
915
+ - `/harness <sub> …` completes what comes next per subcommand: flags
916
+ (`--prune`, `--decay`, `--all`, `--kind`, `--model` — already-used flags
917
+ are not re-offered), `--kind` values (`prompt|memory|skill|subagent`),
918
+ `--model` values (`active` plus the distinct owner models in the store),
919
+ and `keep`/`drop` item ids straight from the live store with a
920
+ `kind · content-preview` description row.
921
+ - Path/number/free-text arguments return no menu, leaving the editor's own
922
+ completion alone.
923
+ - Completion rows carry the full replacement text (pi replaces the whole
924
+ argument string on selection), so multi-token completion round-trips exactly.
925
+ - The `/harness` command description is now a compact one-liner instead of the
926
+ full subcommand + flag grammar.
927
+
928
+ ## [0.8.0] — 2026-08-10
929
+
930
+ The bounded-injection release. The harness ACCUMULATES notes, but the system
931
+ prompt is finite — so selection is now ON BY DEFAULT: injected items are
932
+ importance-ordered, capped per kind, and bounded by a total token budget. The
933
+ store itself is unchanged (nothing is ever lost — only what is *surfaced*
934
+ changes), and the whole policy is opt-out via `injection.enabled: false`.
935
+
936
+ ### Added
937
+
938
+ - **Injection selection policy (on by default)** — `src/select.ts` decides
939
+ WHICH active items for the active model get surfaced each turn, and in what
940
+ order. Pure and fully unit-tested; `inject.ts` is now thin glue over it.
941
+ Policy: (1) filter to active + owner-model items (strict per-model isolation,
942
+ unchanged); (2) order by importance desc, ties stable on store index; (3) cap
943
+ via `maxPerKind` (balanced sections — no single kind drowns the block) then
944
+ `maxTokens` (total budget, filled round-robin across kinds by importance rank
945
+ so one kind can't starve the others; an item that doesn't fit is *skipped*, not
946
+ a hard stop, so a large item never blocks smaller higher-priority ones).
947
+ - **Config: `injection`** in `harness.json` — `{ enabled, maxTokens, maxPerKind,
948
+ charsPerToken }`, always resolved by `loadConfig`. Shipped defaults: `enabled:
949
+ true`, `maxTokens: 1500`, `maxPerKind: 10`, `charsPerToken: 4` — generous
950
+ enough to be a NO-OP for small stores (nothing trimmed) and protective as the
951
+ harness accumulates. Opt out with `injection.enabled: false` → legacy "all
952
+ items, in store order".
953
+ - **Transparency footer** — when the policy drops items, the injected block ends
954
+ with a one-line `_(N item(s) not shown — below the injection budget…)_` note,
955
+ so a bounded block is never silently truncated.
956
+ - **Public API** — `selectForInjection`, `normalizeInjection`, `estimateTokens`,
957
+ `DEFAULT_INJECTION`, and the `InjectionConfig` / `NormalizedInjection` types
958
+ are re-exported from the package entry for companion packages and tests.
959
+
960
+ ### Changed
961
+
962
+ - `renderHarnessBlock(ownerKey?, cfg?)` now renders the selected subset (defaults
963
+ apply when `cfg` is omitted, so direct callers — including existing tests —
964
+ get the new policy). `before_agent_start` reads `injection` from config and
965
+ passes it through.
966
+ - Default injection is now importance-ordered. This reorders (and, for large
967
+ stores, trims) the supplemental block, but changes no data: items remain in the
968
+ store, `/tree` rollback is unaffected, and the durable round-trip is
969
+ unchanged. Set `injection.enabled: false` to restore the pre-0.8 block exactly.
970
+
971
+ ### Docs
972
+
973
+ - README: new "Injection selection (on by default)" section + `injection` in the
974
+ config block; Status updated. MANUAL: injection-selection section. ROADMAP:
975
+ Phase 7. 25 new tests (`test/select.test.ts` + injection/config/integration
976
+ coverage); 137 total, typecheck clean.
977
+
978
+ ## [0.7.0] — 2026-08-09
979
+
980
+ The per-model isolation release. Every item is bound to an exact `provider/id`
981
+ and injected only for the model it belongs to — a brand-new model id starts from
982
+ a blank harness, and one model's notes never leak into another's context.
983
+
984
+ ### Added
985
+
986
+ - **Per-model isolation (`ownerModel`).** Every item now carries an
987
+ `ownerModel` (`"provider/id"`, or `""` for an orphan) and is injected only for
988
+ the model it belongs to. Binding is at the exact id by design — a new version
989
+ is a clean slate.
990
+ - Creates are stamped automatically: `before_agent_start` caches the active
991
+ model (the model-facing tools receive no `ctx`), then `harness_mutate` and
992
+ direct-apply proposers stamp creates from it.
993
+ - **Orphan adoption** is the migration path: items with no owner (legacy
994
+ session snapshots, old durable files, or created while the model was
995
+ unknown) are adopted by the active model on first contact — persisted as a
996
+ normal `harness-state` entry, so `/tree` rollback covers it.
997
+ - `harness_list({ model? })` — defaults to the active model's items; `"*"`
998
+ returns every model; an explicit `"provider/id"` filters.
999
+ - **`/harness push-mem --model <provider/id|active>`** — scope a pi-mem push to
1000
+ one model's items (`active` resolves to the model driving the command).
1001
+
1002
+ ### Changed
1003
+
1004
+ - **Durable round-trip preserves owner** via a per-item `model:` line; an
1005
+ untagged item degrades to an orphan and is adopted on import/first contact.
1006
+ - Injection, `harness_list`, and the outcome loop are model-scoped; the `dedupe`
1007
+ proposer no longer compares items across models.
1008
+ - `/harness status` shows whole-store kind counts, annotated with the active
1009
+ model's share.
1010
+
1011
+ ### Fixed
1012
+
1013
+ - **`harness_mutate` isolation** (PR review): `update`/`delete` are now scoped
1014
+ to the active model too — an agent on model A can no longer mutate model B's
1015
+ item by id (atomic rollback with a clear error). Cross-model maintenance paths
1016
+ (`dedupe`, `/harness keep|drop|prune`) pass no actor and are unaffected.
1017
+ - **Durable owner semantics** (PR review): an absent `model:` tag on an existing
1018
+ item now orphans it (→ adopted by the active model) instead of silently
1019
+ keeping the old owner — "durable wins" now covers owner uniformly.
1020
+ - `session_start` resets the cached active-model key across fork/resume.
1021
+
1022
+ ### Docs
1023
+
1024
+ - README + MANUAL model-binding sections; MANUAL updated for the review fixes
1025
+ (`harness_mutate` actor scoping, model-aware `dedupe`, model-scoped outcome,
1026
+ whole-store `status`); ROADMAP Phase 6; stale `0.5.x` version strings
1027
+ corrected; this CHANGELOG introduced.
1028
+ - Cross-linked the [pi-harness-model-proposer](https://github.com/pungggi/pi-harness-model-proposer)
1029
+ companion.
1030
+
1031
+ ### Internal
1032
+
1033
+ - 19 files changed (+876/−52 over 0.6.2). **112 tests** (+25), typecheck clean;
1034
+ new `test/model-binding.test.ts`. Durable format and tool schemas extended
1035
+ backward-compatibly (missing `ownerModel` → orphan → adopted).