pi-continual-harness
Online self-improvement layer for pi. A unified, in-trajectory harness-state store (prompt notes, memory, skill descriptions, sub-agent specs) with a manual /refine that proposes evidence-backed structured CRUD deltas. Composes with pi-reflect (offline) a
Package details
Install pi-continual-harness from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:pi-continual-harness- Package
pi-continual-harness- Version
0.19.0- Published
- Oct 16, 2026
- Downloads
- 2,289/mo · 1,030/wk
- Author
- ngsoftware
- License
- MIT
- Types
- extension
- Size
- 351.8 KB
- Dependencies
- 0 dependencies · 3 peers
Pi manifest JSON
{
"extensions": [
"./dist/entry.js"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
pi-continual-harness
Online self-improvement layer for the pi coding agent.
This package owns only the online optimizer layer: a unified, in-trajectory
harness-state store (prompt notes, memory, skill descriptions, sub-agent specs)
plus a manual /refine that proposes evidence-backed structured CRUD deltas.
It deliberately does not reinvent storage or offline refinement. It composes with:
- pi-reflect — offline transcript → behavioral-file refinement (the "deep" path).
- pi-mem / pi-memory — durable memory storage.
The durable markdown layers are the composition seam — the shared
~/.pi/agent/harness-state.md plus per-project files under
~/.pi/agent/harness-state/<slug>.md — and they are two-way:
/refine --commit and /harness export write them; /harness import parses
them back and merges into the live store (offline edits win on conflict), so
refinements pi-reflect makes flow back online. /harness push-mem
pushes active items into pi-mem's semantic store (see
Composing with pi-mem).
Why
Grounded in two lines of research:
- Continual Harness (arXiv 2605.09998) — reset-free online CRUD over prompt / sub-agents / skills / memory drawn from the trajectory. Distinct from prompt-optimization methods that need episode resets.
- ACE — Agentic Context Engineering (arXiv 2510.04618) — context as an evolving playbook. Key design lesson applied here: the optimizer emits structured, itemized deltas, never prose prompt rewrites, to prevent context collapse and brevity bias.
Prime Intellect's Prime Agent ships a Continual Harness built on pi; this package is a minimal, package-shaped take on the same idea — online only, leaving offline and storage to the packages that already do them well.
Install
pi install npm:pi-continual-harness
The npm package ships precompiled (dist/): pi loads a thin entry
(dist/entry.js) that registers hooks, commands, and tool schemas in
milliseconds, then dynamic-imports each feature module only when its event
first fires. Measured marginal startup cost: ~30ms (was ~0.8s when pi
had to jiti-transpile the whole TypeScript graph every launch — issue #36).
From a source checkout instead:
npm run build
pi -e ./dist/entry.js
Usage
/refine # review last 25 turns, propose deltas
/refine 50 # review last 50 turns
/refine 25 --commit # also export durable state (global + project layers)
/refine --proposer dedupe # run the rule-based dedupe proposer instead of steering
/refine --proposer dedupe --threshold 0.75 # dedupe with a one-shot threshold
Durable I/O (round-trip with pi-reflect):
/harness status # counts + durable layer presence/mtime
/harness list [-f] [--kind <kind>] [--scope <global|project|all>] # read items — compact rows, -f = full text + evidence
/harness export [path] # layered export (or full snapshot to a path)
/harness export-corpus [path] # calibration corpus JSONL for pi-reflex (local-only; dedupe pairs + lifecycle)
/harness import [--prune] [path] # layered import (or single file from a path)
/harness prune [--decay <days>] # drop items below the importance floor
/harness keep <id> # nudge importance up (+0.1)
/harness drop <id> # nudge importance down (−0.1)
/harness move <id> global|project # move an item between durable layers
/harness split # steer the agent to classify every item's scope
/harness adopt --from <provider/id> [--all] # steer the agent to copy another model's items here
/harness push-mem [--all|--kind <kind>|--model <provider/id|active>] # persist active items to pi-mem (save_memory)
/harness setup [--preset <quiet|recommended|power>] [--yes] # guided config wizard
Subcommands, flags (-f included), --kind/--model/--from/--scope
values, move scopes, and keep/drop/move item ids all autocomplete in
the TUI as you type after /harness .
Durable layers. Every item carries its own durable scope: global
(the shared ~/.pi/agent/harness-state.md) or project
(~/.pi/agent/harness-state/<slug>.md, slug derived from the session cwd).
/harness export without a path partitions items by their scope into the
layer files; /harness import without a path merges the global file always,
plus the current project's file (the project layer wins id collisions;
--prune drops only items absent from every layer). An explicit path keeps
the classic single-file semantics. Move items between layers with
/harness move, or classify the whole store at once with /harness split
(the agent proposes scope-only harness_mutate updates — visible in the
transcript, /tree-rollback-able).
import reconciles the file(s) into the live store: items whose id matches an
existing entry are updated (offline edits win on content/evidence/importance),
new entries are created. By default nothing is deleted — --prune also drops
active items whose id is no longer in the file(s) (inactive items are always
preserved). Point pi-reflect at either layer to refine it offline:
/reflect ~/.pi/agent/harness-state.md
/reflect ~/.pi/agent/harness-state/<project-slug>.md
The model-facing tools:
harness_list({ kind?, model? })— read current state.modeldefaults to the active model's items (what gets injected this turn);"*"returns every model. Empty results are diagnostic: the message distinguishes a genuinely empty store (with zero-state guidance) from items hidden by the model or kind filter (with a tally and amodel: "*"pointer), so an empty list never reads as "broken".harness_mutate { deltas: [...] }— apply a batch ofcreate/update/deletedeltas. Everycreaterequiresevidence. New items are stamped automatically with the active model (see Model binding).create/updatedeltas also acceptscope: "global" | "project"(the project slug is stamped server-side from the session cwd — used by/harness split; normally managed via/harness move).
Active items are injected into the system prompt each turn as a structured block, appended to (never replacing) the base prompt.
Composing with pi-mem
/harness push-mem copies active harness items into
pi-mem's semantic memory store, so they
become searchable across sessions. It works by steering the agent to call
pi-mem's save_memory tool — there is no dependency on pi-mem: it is a
soft-fail composition. If pi-mem (or any memory tool) is not installed, the
agent tells you so rather than fabricating one.
pi install npm:pi-mem # optional companion
By default only memory-kind items are pushed (the clean 1:1 mapping); use
--all for every active item, --kind prompt|skill|subagent for a specific
kind, or --model <provider/id|active> to scope the push to one model's items
(--model active = the model driving the command). The harness store itself is
unchanged by a push — pi-mem gets a separate copy.
Configuration
First-run setup: instead of hand-editing the JSON below, run
/harness setup. It is a guided wizard:
- Shows the current effective config (your file merged over the shipped defaults) plus the durable-layer paths.
- Offers three presets:
quiet(pin every opt-in to the shipped defaults),recommended(autoImport+ import reminders on),power(recommended+autoRefine). Each preset is shown as a declarative diff before anything is applied. - Confirms each delta with a one-line rationale. Autonomous switches get their own explicit "this lets the agent mutate state on a cadence" confirmation regardless of preset.
- Writes via one reviewed path: backs up the existing file
(
harness.json.bak-<timestamp>), atomically writes the merged result, then reloads and prints the effective config exactly as pi will read it.
For scripted setups: /harness setup --preset recommended --yes (the --yes
flag is the scripted form of the confirmations, so it enables autonomous
features without prompts). Nothing is written on unknown input, a cancelled
preset menu, or a declined final write; declining an individual feature only
skips that feature (the accepted deltas still apply). The wizard never
touches harness state, only the config file.
Optional config at ~/.pi/agent/harness.json (missing → defaults; malformed → defaults plus a once-per-session warning, so a typo can't silently reset your settings):
{
"autoImport": false,
"importRemind": { "enabled": true, "everyTurns": 25 },
"proposer": "steering",
"dedupe": { "threshold": 0.6, "merge": true },
"injection": { "enabled": true, "maxTokens": 1500, "maxPerKind": 10, "charsPerToken": 4 },
"remindRefine": { "enabled": true, "everyTurns": 25, "detailEvery": 4 },
"autoRefine": { "enabled": false, "everyTurns": 100, "commit": false, "gate": false },
"outcomeImportance": { "enabled": false, "bump": 0.03 },
"classifier": { "model": "typesafe/jev-latest" },
"virtualBinding": "virtual"
}
autoImport— opt-in durable sync (off by default).truebundles both directions: onsession_startthe durable layers are imported automatically (global file always + the current project's file, with the same loss-free merge as/harness import— an import that changes nothing stays silent), and onturn_endthe layers are re-exported whenever the live store changed since the last export. This is what makes/refineoutput survive into new sessions without manual export/import ceremony. Both halves are visible (one notify line) and use the same persistedharness-stateentries as every other mutation (/treerollback covers them). Enabling it mid-session (e.g. via/harness setup) is safe: the firstturn_endimports the layers before any export, so a late enable can never overwrite the durable files with a never-imported store.importRemind— awareness reminders for that sync (effective only whenautoImportis on; default{ "enabled": true, "everyTurns": 25 }). The session-start import doesn't just count — it lists what arrived (item id, kind, first words of content), and everyeveryTurnsturns a one-line reminder re-surfaces the imported items so silently-injected instructions never go unnoticed (full list via the agent'sharness_listtool —/harness statusshows only counts). Bounded: at most 3 reminders per imported batch, then quiet until the next session; aborted/error turns are inert (completed-turn cadence). Kill-switch:{ "importRemind": { "enabled": false } }(the start-of-session listing still fires).injection— the selection policy for WHAT gets surfaced in the system prompt each turn (see Injection selection). ON by default: items are importance-ordered, capped atmaxPerKind(default- per kind and
maxTokens(default 1500) total. Setenabled: falseto restore the legacy "inject all items, in store order" behaviour.
- per kind and
proposer— which delta proposer/refineand auto-refine use. Defaults tosteering(the agent reasons via a steering message).dedupeapplies a rule-based merge directly. See Proposers.dedupe— the rule-based dedupe proposer's policy.threshold(default0.6, valid(0,1]) is the token-overlap level at which two items sharing the key fields (kind, owner model, durable layer) count as duplicates;merge(defaulttrue) merges each duplicate into its keeper — the keeper keeps its content verbatim, itsevidencebecomes the line-wise union of both, applied as one auditedupdate+deletepair — whilefalserestores the pre-0.10 delete-only behavior./refine --proposer dedupe --threshold 0.75overrides the threshold for one run. A fourth key,similarity, is API-only (functions cannot come fromharness.json): companion packages inject a richer comparator whose return may abstain per pair ({ score, abstain }— seeSimilarityResult); an abstaining pair is always kept, never merged or deleted.remindRefine— theturn_endactivation nudge, ON by default (informational only; it never mutates state). EveryeveryTurnscompleted turns (default 25) a one-line reminder suggests/refine, garnished with live data from the store (e.g. "12 active item(s), 4 never cited since creation"; an empty store gets first-run guidance instead). EverydetailEvery-th reminder (default 4: at the default cadence, roughly every 100 turns) escalates to a rotating educational guide: how the loop works (with alternate descriptions so it never reads as boilerplate), the/refineworkflow,keep/drop/prune --decayhygiene,push-mem,split,export-corpus, and a recommended starter config. Fresh installs used to be silent; that was the bug, not the feature. Aborted/error turns are skipped, soeveryTurnscounts completed turns of work. Silence it with{ "remindRefine": { "enabled": false } };detailEvery: 1makes every reminder detailed.autoRefine— opt-in autonomous self-improvement (off by default). Whenenabled, the agent runs/refineitself everyeveryTurnsturns (default 100). It is one of the package's opt-in autonomous paths: it reuses the exact/refineroutine (auditedREFINE_ENTRYtaggedsource: "auto", branch-local,/treerollback) and notifies before firing.commit: truealso flushes durable state on each run.gate: true(needsclassifier.model) asks the classifier first whether the recent trajectory contains a durable, reusable correction and skips the refine when it does not — cadence becomes signal, not just time (errors fall back to the plain cadence). The steering prompt is delivered via the pi ≥ 0.87 actionableturn_endboundary (a structuralharness.auto-refine-requestentry +continue: true), not a synthetic user message. Aborted/error turns (piturn_endBoundaryState.outcome) are inert: they neither seed nor advance the cadence window — a refine never fires over partial evidence.classifier— opt-in classifier model (pi ≥ 0.99) for the cheap gated paths:"provider/id"or a bare id of a classifier catalog entry, e.g."typesafe/jev-latest"or a localllama-cppclassifier (every chat model listed for llama.cpp also appears as one). With pi ≥ 1.1.0 the catalog also offers OpenAI's GPT-6 Luna through the Decisions API (e.g."openai/gpt-6-luna", needsOPENAI_API_KEY) and the native llama.cpp decision models (Julia-1, Laya, Kev, lev, OpenJev through/v1/systemone, llama.cpp ≥ 0.6.0 — listed only as classifiers, which is exactly our use: a free local gate). When set and resolvable it enables the classifier-confirmed dedupe (seededupeabove) and the auto-refinegate. When unset or unresolvable, every classifier feature degrades to its no-classifier behavior.virtualBinding— how the per-model isolation key treats pi virtual models (router extensions, pi ≥ 0.99). Default"virtual": the key is the selected model's ownprovider/id— a router starts a blank harness."physical": when the selection is virtual, the key is the physical model behind the latest successful response on the branch — notes accumulated under that physical model keep injecting through the router, and switching routed models re-scopes exactly like a manual model switch. Before the first response the key is unknown: creates stay orphans and are adopted on first contact.outcomeImportance— opt-in autonomous promotion loop (off by default). Whenenabled, aturn_endhook bumps (+bump, default 0.03) the importance of any active item the agent cited by its[h_xxxx]tag in the turn's output. Citations are read from completed turns only — an aborted turn's partial text is scanned by the next completed turn (the initial seed turn itself is never scanned, as before). Referenced items gain importance and get theirupdatedAttouched (so they survive time-based decay); ignored items keep decaying. This is the package's second opt-in autonomous path — promotion only, never deletes, persisted/branchable likeharness_mutate. Autonomous demotion from outcomes is intentionally NOT done (high false-positive); use/harness drop,prune --decay, or thededupeproposer for that.
Removed: the store-wide
durableScopeconfig key (deprecated 0.9.0, removed now). Items carry their own scope (/harness move), and durable I/O is always layered. Config files that still set the key load fine — unknown keys are ignored. Migration from adurableScope: "project"setup: run/harness importonce in the project (its file's items adopt project scope), then move any strays with/harness move(or classify the whole store with/harness split).
How it works
/refinegathers recent trajectory evidence from the current session branch.- A proposer decides what to do. The default (
steering) sends a steering user message asking the agent to propose evidence-backed CRUD deltas viaharness_mutate; rule-based proposers (e.g.dedupe) return deltas the harness applies directly. See Proposers. - The agent calls the tools (steering path); each accepted delta updates the
in-memory state, which is snapshotted to the session via
appendEntry("harness-state", ...). Direct-apply proposers snapshot the same way. New items are stamped with the active model (see Model binding). - Because pi's session tree branches at any entry,
/treenavigation gives rollback to any pre-refinement point for free — no bespoke snapshot system.
This reuses the existing agent loop (no nested/hidden model calls), is model-agnostic, and keeps every delta visible and reviewable in the transcript.
Model binding (per-model isolation)
Every item is bound to exactly one model as ownerModel ("provider/id"). An
item is only injected for the model it belongs to — so switching to a
brand-new model id starts from a blank harness, and one model's notes never
leak into another's context. Binding is at the exact model id (not family or
vendor): a new version id is a clean slate, by design.
How the binding is set and respected:
- Created items are stamped automatically with the model driving the turn.
The model-facing tools cannot read the active model, so
before_agent_start(which always fires first in a turn, with the model) caches it;harness_mutatestamps creates from that cache, and direct-apply proposers stamp from theirctx.model. You never name the model yourself. - Injection filters by owner. Only items whose
ownerModelmatches the active model are appended to the system prompt. An unknown model injects nothing. harness_listdefaults to the active model (passmodel: "*"for every model, or an explicit"provider/id").- Orphan adoption. Items with no owner — from a legacy session snapshot, an
old durable file, or created while the model was unknown — are adopted by the
active model on first contact (the next
before_agent_start). This is the migration path: existing harnesses transition cleanly with no manual steps, and it's persisted as a normalharness-stateentry (so/treerollback covers it). - Durable round-trip preserves owner.
/harness exporttags each item withmodel: provider/id;/harness importrestores it. An item whose tag pi-reflect stripped becomes an orphan and is adopted by the active model. - Carrying owned items across models:
/harness adopt. Orphan adoption only covers unowned items./harness adopt --from <provider/id>is the reviewable path for owned ones: it steers the agent to classify the source model's active items generalizes (engineering practices, user preferences, project facts) vs stays (that model's quirks, formatting preferences, failure modes), then apply the adoption as ONEharness_mutatebatch of create deltas. It is copy, not move: the source model's harness stays untouched, content is copied verbatim (importance is listed at full precision so the copy keeps its exact ranking), and evidence is annotatedadopted from <provider/id>. BecauseownerModelis never model-settable, the creates stamp to the active model via the normal create-stamping path — audited and/tree-rollback-able like every mutation.--allskips the filter; overlaps afterwards clean up with/refine --proposer dedupe. Two store-level filters keep the copy honest (review #25): items scoped to a different project are skipped (a copy would re-stamp them into this session's project — adopt them from a session in that project instead), and content the adopter already owns is skipped before the batch cap, so re-running against a >20-item source reaches new items instead of re-selecting the same top ones. The self-adopt guard compares against the resolved binding key (virtualBinding-aware — underphysicala router selection adopts as the routed model), the same key create-stamping uses. Explicit and manual only — there is no automatic cross-model adoption.
Manual commands (export, import, move, split, keep, drop, prune,
push-mem, status) operate on the whole store by design — they are
explicit human actions with full control. Isolation is enforced only where pollution would
leak automatically: injection, listing, create-stamping, and the outcome loop.
In particular, /harness push-mem pushes every model's active items into
pi-mem by default (which can yield near-duplicate memories across models); pass
--model <provider/id|active> to scope it to one model.
Injection selection (on by default)
The harness ACCUMULATES notes (create / refine / auto-refine / outcome-promotion), but the system prompt is finite. So since 0.8 the block appended each turn is the result of a selection policy, not the whole store — and it is on by default. The store itself is never changed by selection (nothing is lost); only what is surfaced changes.
The policy (pure, deterministic; src/select.ts):
- Filter — only active items bound to the active model (strict per-model isolation, unchanged).
- Order — importance desc; ties keep store/insertion order (stable). The highest-fitness notes lead each section.
- Cap —
maxPerKind(default 10: balanced sections, no single kind drowns the block), thenmaxTokens(default 1500: total budget). The budget is filled round-robin across kinds by importance rank, so one kind cannot starve the others; within that order an item that doesn't fit is skipped (not a hard stop), so a large item never blocks smaller higher-priority ones.
The defaults are deliberately generous — a no-op for small stores (nothing trimmed) and protective as the harness grows. When the policy drops items, the block ends with a one-line transparency note:
_(3 item(s) not shown — below the injection budget. Raise `injection.maxTokens`/`maxPerKind` in harness.json or run `/harness prune`.)_
Tune or disable it in harness.json:
{ "injection": { "maxTokens": 3000, "maxPerKind": 20 } } // raise the ceiling
{ "injection": { "maxPerKind": 5 } } // trim harder, per kind
{ "injection": { "enabled": false } } // opt out: legacy "all, in order"
Selection is factored into a pure function (selectForInjection) and
re-exported from the package entry, so a companion package can layer richer
policies (e.g. relevance to the current turn, via the shared tokenize /
tokenOverlap helpers) without touching inject.ts.
Proposers
/refine is split into two stages: propose (given evidence + state, decide
what deltas to pursue) and apply (send a steering message, or apply returned
deltas directly). The propose stage is pluggable via a registry
(src/proposer.ts).
| Name | What it does |
|---|---|
steering (default) |
Delegates reasoning to the agent via a steering message — reuses the agent loop, model-agnostic, fully visible. |
dedupe |
Rule-based: merges near-duplicate active items (token-overlap ≥ the configured threshold, same kind / owner model / durable layer) into the higher-importance keeper — one evidence-union update per keeper, then deletes the duplicates. "dedupe": { "merge": false } restores delete-only. No model call. |
The dedupe planner's similarity seam (DedupeOptions.similarity, re-exported
from the package entry) is how a companion package upgrades the comparison —
e.g. embedding cosine similarity, or a local decision engine such as pi-reflex
(a local-only project for now). A comparator may return
{ score, abstain } instead of a plain number: an abstaining pair is
treated as not duplicates — both items are kept — so an engine with
conformal uncertainty guarantees can safely decline to merge. Plain numeric
returns keep working (the default remains token Jaccard).
Select a proposer per run with /refine --proposer <name>, or set the default
for auto-refine via proposer in the config. Both paths are
audited: the harness-refinement entry records which proposer ran and how many
deltas it applied directly.
A dedicated-model proposer — one that makes its own (hidden) LLM call to
produce deltas directly — is the obvious next alternate. This package still
does not ship one (hidden model spend is a tradeoff kept as a separate
decision; see docs/ROADMAP.md), but it now enables one: when a model is
available, /refine injects a one-shot complete(prompt, opts?) into
ProposeInput and records any modelCall telemetry a proposer returns
(model, tokens, latency, ok/error) in the harness-refinement audit entry —
so a companion package can ship a dedicated-model proposer whose spend stays
audited, not hidden. One ships as a companion: pi-harness-model-proposer
(pi install npm:pi-harness-model-proposer). Both complete and modelCall are optional;
steering and dedupe ignore them, so the default behavior is unchanged.
Register your own proposer from another extension (the registry is re-exported from the package entry):
import { registerProposer } from "pi-continual-harness";
registerProposer({
name: "my-proposer",
async propose({ evidence, state, complete }) {
// `complete` is injected when a model is available (undefined otherwise);
// a dedicated-model proposer calls it and returns deltas + modelCall telemetry.
/* inspect evidence + state, return deltas (and/or a steering message) */
return { deltas: [/* { delta, rationale } */] };
},
});
Then "my-proposer" is selectable via /refine --proposer my-proposer or
"proposer": "my-proposer" in the config.
Cross-instance registry (0.17+). pi's extension loader isolates module
instances between extensions, so before 0.17.0 a companion's registerProposer
call wrote to a private module copy that /refine never read — the
registration was silently invisible. The registry is now backed by a
process-wide global (Symbol.for("pi-continual-harness.proposers")), so any
copy's registrations are visible to every copy. Companions that want a
zero-import startup entry can skip importing this package at runtime entirely
and push a DeltaProposer (typically a lazy delegate that dynamic-imports its
implementation on first propose()) directly into
globalThis[Symbol.for("pi-continual-harness.proposers")].
Scope and non-goals
- In scope: unified state store, online
/refine, structured deltas, prompt injection, branching rollback, durable markdown export. - Out of scope (compose instead): durable storage engines (pi-mem), offline deep refinement of behavioral files (pi-reflect), live sub-agent orchestration (pi-boss / pi-room). Sub-agent specs are stored as data only.
Status
0.18.x. Requires pi ≥ 1.2.0. Implemented:
- Unified harness-state store with branch-local snapshots (
/treerollback). - Online
/refine+harness_mutate/harness_listtools. - Two-way durable round-trip with pi-reflect (
/harness import|export|status), layered on per-item scope: every item isglobalorproject(/harness move,/harness split), and/harness export|importoperate on both layers. - Importance hygiene:
/harness prune [--decay <days>]and/harness keep|drop <id>. - pi-mem composition:
/harness push-mem [--all|--kind|--model]steers the agent to persist active items into pi-mem (soft-fail; no dependency). - Opt-in durable sync (
"autoImport": true): session_start layered auto-import + turn_end layered auto-export when the store changed —/refineoutput survives into new sessions with zero ceremony. Off by default; every action visible and/tree-rollback-able. - Optional config (
~/.pi/agent/harness.json): the durable-sync opt-in, a default-onturn_endreminder (notify-only; the activation nudge), and the opt-inturn_endauto-refine and outcome-importance loops. The package's autonomous paths stay opt-in and off by default. - Pluggable delta proposers with a registry:
steering(default) anddedupe(rule-based) shipped;registerProposer()for custom ones. - Per-model isolation: every item is bound to a
provider/idand injected only for that model; new items are stamped automatically and orphans adopted on first contact. A new model id starts from a blank harness, and the durable round-trip preserves the owner tag. With pi virtual models (routers), the"virtualBinding": "physical"policy keys items by the routed physical model instead of the router id, so a router selection does not silently reset the harness. Owned items can be carried across models explicitly with/harness adopt --from <provider/id> [--all](agent-filtered copy; see Model binding). Since pi 1.2,before_agent_startalso fires for runs started bypi.sendMessage(..., { triggerTurn: true }), so scheduled and extension-triggered runs get the same binding, orphan adoption, and injection as interactive prompts. - Bounded injection (on by default): the supplemental block is
importance-ordered and capped per kind + by a total token budget, so a growing
harness never balloons the system prompt. Tunable / opt-out via the
injectionconfig key; the store is never changed by selection. Rendered on every provider request (context_with_system), so a mid-runharness_mutateshows up on the very next request. - Classifier features (pi ≥ 0.99, opt-in):
"classifier": { "model": … }enables (1) classifier-confirmed dedupe — candidate merge pairs confirmed by one batched yes/no call (errors fall back to the rule-based plan) — and (2) the auto-refine gate"autoRefine": { "gate": true }, which skips a cadenced refine when the trajectory holds no durable correction. - Structured tool results:
harness_list/harness_mutatereturnstructuredContent(withoutputSchema), so codemode scripts andctx.executeTool()callers can consume harness state programmatically. - Boundary delivery for auto-refine: the steering prompt is persisted as a
structural entry with
{ entries, continue: true }atturn_end(no synthetic user message, no follow-up-scheduling side effects); manual/refinestill steers via a user message. Each new boundary also omits prior auto-refine request drafts from future provider context (pi ≥ 0.87 context edits;autoRefine.omitStaleDrafts: falseto keep them).
Open extension points (see docs/ROADMAP.md):
- A dedicated-model proposer (the enabler landed:
completeinjection +modelCalltelemetry; the proposer logic itself is left to a companion package — the hidden-model-spend tradeoff is resolved by making the spend audited rather than shipping it invisible). - Correction-side outcome signals (promotion is shipped; autonomous demotion
is high-false-positive, so it is served by
/harness drop,prune --decay, and thededupeproposer — a fuzzycorrectionsproposer is the natural future extension).
License
MIT