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