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.21.0
Published
Nov 7, 2026
Downloads
1,638/mo · 64/wk
Author
ngsoftware
License
MIT
Types
extension
Size
491.1 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).

In codemode (pi ≥ 1.2), these tools' input constraints render as @ tags on property comments — importance shows @minimum 0 @maximum 1, the batch cap shows @maxItems 20 — because the tool schemas already declare them. Scripts see the limits without the descriptions repeating them.

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). The full key-by-key reference lives in docs/CONFIG.md; the shape:

{
  "$schema": "https://raw.githubusercontent.com/pungggi/pi-continual-harness/main/harness.schema.json",
  "autoImport": false,
  "importRemind": { "enabled": true, "everyTurns": 25 },
  "proposer": "steering",
  "dedupe": { "threshold": 0.6, "merge": true },
  "injection": { "enabled": true, "maxTokens": 1500, "maxPerKind": 10, "charsPerToken": 4 },
  "provenance": { "label": true, "priority": true },
  "remindRefine": { "enabled": true, "everyTurns": 25, "detailEvery": 4 },
  "autoRefine": { "enabled": false, "everyTurns": 100, "commit": false, "gate": false, "refineNoOpSkip": 3 },
  "outcomeImportance": { "enabled": false, "bump": 0.03 },
  "classifier": { "model": "typesafe/jev-latest" },
  "virtualBinding": "virtual"
}

Editor validation: add "$schema": "https://raw.githubusercontent.com/pungggi/pi-continual-harness/main/harness.schema.json" to the file for completion and validation in editors that support JSON schemas (the same pattern pi ≥ 1.2 ships for its own settings.json / models.json). The loader ignores unknown keys — $schema included — and a /harness setup rewrite preserves it.

Key Default What it controls
autoImport false Opt-in durable sync: session_start layered auto-import + turn_end auto-export — /refine output survives into new sessions.
importRemind on, every 25 turns Awareness reminders for the sync (what arrived; bounded re-surfacings). Effective only with autoImport.
injection on, 1500 tokens / 10 per kind The per-turn prompt selection policy (below).
provenance on, both gates Trust boundary: label agent-authored items unverified + let trusted items win budget contention (below). Stamping always happens; the gates only affect presentation.
proposer "steering" Which delta proposer /refine uses (below).
dedupe threshold 0.6, merge on The rule-based dedupe proposer's policy.
remindRefine on, every 25 turns The notify-only /refine activation nudge; every 4th one is a detailed educational guide.
autoRefine off, every 100 turns Opt-in autonomous /refine on a cadence; gate adds the classifier question, refineNoOpSkip the free history gate.
outcomeImportance off, bump 0.03 Opt-in promotion of items you cite by [h_xxxx] tag. Promotion only — never deletes.
classifier unset Classifier model (pi ≥ 0.99) enabling classifier-confirmed dedupe + the auto-refine gate.
virtualBinding "virtual" Per-model isolation key for router (virtual) models; "physical" keys by the routed model.

The package's autonomous paths (autoImport, autoRefine, outcomeImportance) are all opt-in and off by default; everything else is notify-only or human-triggered. Removed keys: the store-wide durableScope (0.9.0) — items carry their own scope (/harness move, /harness split); see docs/CONFIG.md for the migration note.

Lessons and support signals (harnessing-level recursion)

Harness²'s second loop: the improvement process should learn from what past improvement actually yielded, and keep a lesson only when execution evidence supports it. Four pieces implement this without any extra model calls:

  • Support counters. Every item accumulates support signals: citations (the outcome loop saw the agent cite the item's [h_xxxx] tag), keeps and drops (/harness keep|drop). Dedupe merges carry the counters onto the keeper, and durable export/import round-trips them (support: sub-line), so feedback survives restarts. See them with /harness list -f.
  • Lessons digest. /refine steers with a compact, evidence-gated digest: items proven useful (cited or kept), rejected signals (drops that strictly outweigh any later use — a drop followed by re-citations is contested, not rejected), and how productive recent refine runs were. Never-cited items are NOT named: no signal is not a negative signal. The digest is scoped to the active model's items — per-model isolation applies to what the improver is told, not just what it is shown. /harness lessons prints the exact digest the next /refine will use. Why: the improver starts from what worked instead of re-deriving it, and stops re-creating rejected notes.
  • Scoped proposals. The /refine steering prompt classifies every create as invariant (cross-domain agentic behavior → scope: "global") or domain-specific (this project's knowledge → scope: "project"). Harness²'s ablation shows this scoping is load-bearing; here it also keeps the global layer free of project noise.
  • History gate. Auto-refine skips a cadenced run when the last refineNoOpSkip refine runs all produced nothing durable, bounded to one consecutive skip so later evidence always gets a chance (see autoRefine in Configuration).

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.

Provenance and trust boundary

Almost every item in a running store was written by the model itself (via harness_mutate or a /refine proposer). Without a trust boundary, a stale, wrong, or injected note steers future sessions with the same authority as your own instructions. Since 0.21 each item records who last wrote its content:

  • provenance: "agent" — the content was last written by the model (harness_mutate create, or any content rewrite through the tool or a proposer). Stamped server-side: the model can never set it, and the tool strips attempts (a smuggled provenance: "owner" cannot upgrade trust).
  • absent — trusted. Owner-authored items, AGENTS.md imports, legacy snapshots, and durable files without the marker. No migration needed: existing stores keep today's rendering until new agent items appear.
  • Flip rule — provenance follows the last content write. An agent rewrite of a trusted item flips it to agent (losing trust is the safe direction; nothing the model does can ever flip it back). Importance/keep/drop/scope updates never flip it.

Two presentation effects, both on by default (harness.json):

{ "provenance": { "label": true, "priority": true } }
  • Label — agent items render in the block as - [h_ab12] (unverified: agent-authored) …, with one intro line defining the marker (paid only when such an item is actually surfaced). /harness list rows carry an (unverified) chip; -f spells it out.
  • Priority — trusted items fill each kind's item slots and the injection budget first; agent items only compete for what remains. An agent note can never crowd an owner note out of the block — neither at the per-kind cap nor at the token budget. Stores without agent items queue identically to before.

Stamping always happens regardless of the knobs; they gate only presentation. The durable file carries a - provenance: agent sub-line for agent items (legacy files stay byte-identical), and durable wins on import: hand- deleting the line is the documented way to trust an item again.

This is a visibility convention, not authentication. It makes agent authority explicit so you can review it; it cannot stop a model that already runs commands from writing files or persuading you to import its text.

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.20.x. Requires pi ≥ 1.2.0 (tool schemas are plain JSON Schema, so pi 1.5's move to @earendil-works/pi-schema argument validation needs no migration; developed and tested against pi 1.5). 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