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

Packages

Package details

extension

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. model defaults 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 a model: "*" pointer), so an empty list never reads as "broken".
  • harness_mutate { deltas: [...] } — apply a batch of create / update / delete deltas. Every create requires evidence. New items are stamped automatically with the active model (see Model binding). create/update deltas also accept scope: "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:

  1. Shows the current effective config (your file merged over the shipped defaults) plus the durable-layer paths.
  2. 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.
  3. 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.
  4. 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). true bundles both directions: on session_start the 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 on turn_end the layers are re-exported whenever the live store changed since the last export. This is what makes /refine output survive into new sessions without manual export/import ceremony. Both halves are visible (one notify line) and use the same persisted harness-state entries as every other mutation (/tree rollback covers them). Enabling it mid-session (e.g. via /harness setup) is safe: the first turn_end imports 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 when autoImport is 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 every everyTurns turns a one-line reminder re-surfaces the imported items so silently-injected instructions never go unnoticed (full list via the agent's harness_list tool — /harness status shows 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 at maxPerKind (default
    1. per kind and maxTokens (default 1500) total. Set enabled: false to restore the legacy "inject all items, in store order" behaviour.
  • proposer — which delta proposer /refine and auto-refine use. Defaults to steering (the agent reasons via a steering message). dedupe applies a rule-based merge directly. See Proposers.
  • dedupe — the rule-based dedupe proposer's policy. threshold (default 0.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 (default true) merges each duplicate into its keeper — the keeper keeps its content verbatim, its evidence becomes the line-wise union of both, applied as one audited update + delete pair — while false restores the pre-0.10 delete-only behavior. /refine --proposer dedupe --threshold 0.75 overrides the threshold for one run. A fourth key, similarity, is API-only (functions cannot come from harness.json): companion packages inject a richer comparator whose return may abstain per pair ({ score, abstain } — see SimilarityResult); an abstaining pair is always kept, never merged or deleted.
  • remindRefine — the turn_end activation nudge, ON by default (informational only; it never mutates state). Every everyTurns completed 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). Every detailEvery-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 /refine workflow, keep/drop/prune --decay hygiene, 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, so everyTurns counts completed turns of work. Silence it with { "remindRefine": { "enabled": false } }; detailEvery: 1 makes every reminder detailed.
  • autoRefine — opt-in autonomous self-improvement (off by default). When enabled, the agent runs /refine itself every everyTurns turns (default 100). It is one of the package's opt-in autonomous paths: it reuses the exact /refine routine (audited REFINE_ENTRY tagged source: "auto", branch-local, /tree rollback) and notifies before firing. commit: true also flushes durable state on each run. gate: true (needs classifier.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 actionable turn_end boundary (a structural harness.auto-refine-request entry + continue: true), not a synthetic user message. Aborted/error turns (pi turn_end BoundaryState.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 local llama-cpp classifier (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", needs OPENAI_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 (see dedupe above) and the auto-refine gate. 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 own provider/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). When enabled, a turn_end hook 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 their updatedAt touched (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 like harness_mutate. Autonomous demotion from outcomes is intentionally NOT done (high false-positive); use /harness drop, prune --decay, or the dedupe proposer for that.

Removed: the store-wide durableScope config 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 a durableScope: "project" setup: run /harness import once 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

  1. /refine gathers recent trajectory evidence from the current session branch.
  2. A proposer decides what to do. The default (steering) sends a steering user message asking the agent to propose evidence-backed CRUD deltas via harness_mutate; rule-based proposers (e.g. dedupe) return deltas the harness applies directly. See Proposers.
  3. 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).
  4. Because pi's session tree branches at any entry, /tree navigation 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_mutate stamps creates from that cache, and direct-apply proposers stamp from their ctx.model. You never name the model yourself.
  • Injection filters by owner. Only items whose ownerModel matches the active model are appended to the system prompt. An unknown model injects nothing.
  • harness_list defaults to the active model (pass model: "*" 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 normal harness-state entry (so /tree rollback covers it).
  • Durable round-trip preserves owner. /harness export tags each item with model: provider/id; /harness import restores 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 ONE harness_mutate batch 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 annotated adopted from <provider/id>. Because ownerModel is never model-settable, the creates stamp to the active model via the normal create-stamping path — audited and /tree-rollback-able like every mutation. --all skips 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 — under physical a 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):

  1. Filter — only active items bound to the active model (strict per-model isolation, unchanged).
  2. Order — importance desc; ties keep store/insertion order (stable). The highest-fitness notes lead each section.
  3. Cap — maxPerKind (default 10: balanced sections, no single kind drowns the block), then maxTokens (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 (/tree rollback).
  • Online /refine + harness_mutate / harness_list tools.
  • Two-way durable round-trip with pi-reflect (/harness import|export|status), layered on per-item scope: every item is global or project (/harness move, /harness split), and /harness export|import operate 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 — /refine output 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-on turn_end reminder (notify-only; the activation nudge), and the opt-in turn_end auto-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) and dedupe (rule-based) shipped; registerProposer() for custom ones.
  • Per-model isolation: every item is bound to a provider/id and 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_start also fires for runs started by pi.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 injection config key; the store is never changed by selection. Rendered on every provider request (context_with_system), so a mid-run harness_mutate shows 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_mutate return structuredContent (with outputSchema), so codemode scripts and ctx.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 } at turn_end (no synthetic user message, no follow-up-scheduling side effects); manual /refine still 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: false to keep them).

Open extension points (see docs/ROADMAP.md):

  • A dedicated-model proposer (the enabler landed: complete injection + modelCall telemetry; 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 the dedupe proposer — a fuzzy corrections proposer is the natural future extension).

License

MIT