pi-continual-harness 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -51,6 +51,7 @@ Or drop `src/index.ts` into `~/.pi/agent/extensions/`.
51
51
  /refine # review last 25 turns, propose deltas
52
52
  /refine 50 # review last 50 turns
53
53
  /refine 25 --commit # also export durable state to ~/.pi/agent/harness-state.md
54
+ /refine --proposer dedupe # run the rule-based dedupe proposer instead of steering
54
55
  ```
55
56
 
56
57
  Durable I/O (round-trip with pi-reflect):
@@ -90,6 +91,7 @@ Optional config at `~/.pi/agent/harness.json` (missing or malformed → defaults
90
91
  ```json
91
92
  {
92
93
  "durableScope": "global",
94
+ "proposer": "steering",
93
95
  "remindRefine": { "enabled": false, "everyTurns": 50 },
94
96
  "autoRefine": { "enabled": false, "everyTurns": 100, "commit": false }
95
97
  }
@@ -99,6 +101,9 @@ Optional config at `~/.pi/agent/harness.json` (missing or malformed → defaults
99
101
  `~/.pi/agent/harness-state.md`; `"project"` writes to
100
102
  `~/.pi/agent/harness-state/<slug>.md` (slug derived from `cwd`) so each
101
103
  project keeps separate state for pi-reflect.
104
+ - **`proposer`** — which delta proposer `/refine` and auto-refine use. Defaults
105
+ to `steering` (the agent reasons via a steering message). `dedupe` applies a
106
+ rule-based dedupe directly. See [Proposers](#proposers).
102
107
  - **`remindRefine`** — opt-in `turn_end` nudge. `{ "enabled": true,
103
108
  "everyTurns": 50 }` notifies you to run `/refine` on a cadence. It is
104
109
  informational only — it never mutates state.
@@ -112,16 +117,59 @@ Optional config at `~/.pi/agent/harness.json` (missing or malformed → defaults
112
117
  ## How it works
113
118
 
114
119
  1. `/refine` gathers recent trajectory evidence from the current session branch.
115
- 2. It sends a steering user message asking the agent to propose evidence-backed
116
- CRUD deltas via `harness_mutate`.
117
- 3. The agent calls the tools; each accepted delta updates the in-memory state,
118
- which is snapshotted to the session via `appendEntry("harness-state", ...)`.
120
+ 2. A **proposer** decides what to do. The default (`steering`) sends a steering
121
+ user message asking the agent to propose evidence-backed CRUD deltas via
122
+ `harness_mutate`; rule-based proposers (e.g. `dedupe`) return deltas the
123
+ harness applies directly. See [Proposers](#proposers).
124
+ 3. The agent calls the tools (steering path); each accepted delta updates the
125
+ in-memory state, which is snapshotted to the session via
126
+ `appendEntry("harness-state", ...)`. Direct-apply proposers snapshot the
127
+ same way.
119
128
  4. Because pi's session tree branches at any entry, `/tree` navigation gives
120
129
  rollback to any pre-refinement point for free — no bespoke snapshot system.
121
130
 
122
131
  This reuses the existing agent loop (no nested/hidden model calls), is
123
132
  model-agnostic, and keeps every delta visible and reviewable in the transcript.
124
133
 
134
+ ## Proposers
135
+
136
+ `/refine` is split into two stages: **propose** (given evidence + state, decide
137
+ what deltas to pursue) and **apply** (send a steering message, or apply returned
138
+ deltas directly). The propose stage is pluggable via a registry
139
+ (`src/proposer.ts`).
140
+
141
+ | Name | What it does |
142
+ |---|---|
143
+ | `steering` (default) | Delegates reasoning to the agent via a steering message — reuses the agent loop, model-agnostic, fully visible. |
144
+ | `dedupe` | Rule-based: drops near-duplicate active items (token-overlap ≥ 0.6), keeping the higher-importance one. No model call. |
145
+
146
+ Select a proposer per run with `/refine --proposer <name>`, or set the default
147
+ for auto-refine via `proposer` in the [config](#configuration). Both paths are
148
+ audited: the `harness-refinement` entry records which proposer ran and how many
149
+ deltas it applied directly.
150
+
151
+ A dedicated-model proposer (a hidden LLM call) is the obvious next alternate
152
+ the interface supports, but is intentionally not shipped — non-visible model
153
+ spend is a tradeoff kept as a separate decision (see `docs/ROADMAP.md`).
154
+
155
+ Register your own proposer from another extension (the registry is re-exported
156
+ from the package entry):
157
+
158
+ ```ts
159
+ import { registerProposer } from "pi-continual-harness";
160
+
161
+ registerProposer({
162
+ name: "my-proposer",
163
+ async propose({ evidence, state }) {
164
+ /* inspect evidence + state, return deltas (and/or a steering message) */
165
+ return { deltas: [/* { delta, rationale } */] };
166
+ },
167
+ });
168
+ ```
169
+
170
+ Then `"my-proposer"` is selectable via `/refine --proposer my-proposer` or
171
+ `"proposer": "my-proposer"` in the config.
172
+
125
173
  ## Scope and non-goals
126
174
 
127
175
  - **In scope:** unified state store, online `/refine`, structured deltas,
@@ -132,7 +180,7 @@ model-agnostic, and keeps every delta visible and reviewable in the transcript.
132
180
 
133
181
  ## Status
134
182
 
135
- 0.2.x. Implemented:
183
+ 0.3.x. Implemented:
136
184
 
137
185
  - Unified harness-state store with branch-local snapshots (`/tree` rollback).
138
186
  - Online `/refine` + `harness_mutate` / `harness_list` tools.
@@ -142,10 +190,13 @@ model-agnostic, and keeps every delta visible and reviewable in the transcript.
142
190
  - Optional config (`~/.pi/agent/harness.json`): project-local durable scope, an
143
191
  opt-in `turn_end` reminder, and opt-in `turn_end` auto-refine (the one
144
192
  autonomous-self-mutation path; off by default).
193
+ - **Pluggable delta proposers** with a registry: `steering` (default) and
194
+ `dedupe` (rule-based) shipped; `registerProposer()` for custom ones.
145
195
 
146
196
  Open extension points (see `docs/ROADMAP.md`):
147
197
 
148
- - A pluggable delta proposer (currently the agent itself, via steering).
198
+ - A dedicated-model proposer (interface ready; the hidden-model-spend tradeoff
199
+ is a separate decision).
149
200
  - pi-mem ingestion of the durable export.
150
201
  - Outcome-driven importance (currently model-set + manual nudges).
151
202
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-continual-harness",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "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) and pi-mem (storage); owns only the online optimizer layer. Inspired by Continual Harness (arXiv 2605.09998) and ACE (arXiv 2510.04618).",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -53,7 +53,12 @@ export function registerAutoRefine(pi: ExtensionAPI): void {
53
53
  const every = config.autoRefine?.everyTurns ?? DEFAULT_AUTO_EVERY_TURNS;
54
54
  ctx.ui.notify(`Auto-refine: running /refine (every ${every} turns, opt-in).`, "info");
55
55
  try {
56
- await runRefine(pi, ctx, { commit: config.autoRefine?.commit ?? false }, "auto");
56
+ await runRefine(
57
+ pi,
58
+ ctx,
59
+ { commit: config.autoRefine?.commit ?? false, ...(config.proposer ? { proposer: config.proposer } : {}) },
60
+ "auto",
61
+ );
57
62
  } catch (err) {
58
63
  ctx.ui.notify(`Auto-refine failed: ${(err as Error).message}`, "error");
59
64
  }
package/src/config.ts CHANGED
@@ -29,12 +29,15 @@ export interface HarnessConfig {
29
29
  everyTurns?: number;
30
30
  commit?: boolean;
31
31
  };
32
+ /** Delta proposer name (see proposer.ts registry). Defaults to "steering". */
33
+ proposer?: string;
32
34
  }
33
35
 
34
36
  export const DEFAULT_CONFIG: HarnessConfig = {
35
37
  durableScope: "global",
36
38
  remindRefine: { enabled: false, everyTurns: DEFAULT_EVERY_TURNS },
37
39
  autoRefine: { enabled: false, everyTurns: DEFAULT_AUTO_EVERY_TURNS, commit: false },
40
+ proposer: "steering",
38
41
  };
39
42
 
40
43
  export const CONFIG_PATH = join(homedir(), ".pi", "agent", "harness.json");
@@ -54,6 +57,7 @@ function mergeConfig(over: Partial<HarnessConfig>): HarnessConfig {
54
57
  everyTurns: over.autoRefine?.everyTurns ?? DEFAULT_AUTO_EVERY_TURNS,
55
58
  commit: over.autoRefine?.commit ?? false,
56
59
  },
60
+ proposer: over.proposer ?? "steering",
57
61
  };
58
62
  }
59
63
 
package/src/index.ts CHANGED
@@ -36,3 +36,14 @@ export default function continualHarness(pi: ExtensionAPI): void {
36
36
  }
37
37
 
38
38
  export { STATE_ENTRY };
39
+
40
+ // Public extension API: other extensions can register their own delta proposer
41
+ // (see src/proposer.ts) and it becomes selectable via /refine --proposer <name>
42
+ // or the `proposer` config key.
43
+ export {
44
+ listProposers,
45
+ registerProposer,
46
+ type DeltaProposer,
47
+ type ProposeInput,
48
+ type ProposeResult,
49
+ } from "./proposer.js";
@@ -0,0 +1,174 @@
1
+ // Pluggable delta proposers (Phase 4 of the roadmap).
2
+ //
3
+ // /refine is split into two stages:
4
+ // 1. PROPOSE — given trajectory evidence + current state, decide what deltas
5
+ // to apply (or what to ask the agent to do).
6
+ // 2. APPLY — either send a steering message (the agent reasons + calls
7
+ // harness_mutate) or apply returned deltas directly.
8
+ //
9
+ // This file owns stage 1. The default `steeringProposer` preserves the
10
+ // original behavior exactly (delegates to the agent via a steering message,
11
+ // reusing the agent loop — model-agnostic, fully visible). Alternate proposers
12
+ // produce deltas directly:
13
+ // - `dedupeProposer` (rule-based): drops near-duplicate active items by token
14
+ // overlap, keeping the higher-importance one. Pure function of state —
15
+ // deterministic, no model call, fully testable.
16
+ //
17
+ // A dedicated-model proposer (a hidden LLM call) is the obvious next alternate
18
+ // the interface supports, but is intentionally NOT shipped here: it is the one
19
+ // tradeoff (non-visible model spend) the roadmap flags as a separate decision.
20
+ //
21
+ // Extension is open: call registerProposer() from your own extension to add a
22
+ // named proposer, then select it via /refine --proposer <name> or config.
23
+
24
+ import type { Delta, HarnessItem, HarnessState } from "./types.js";
25
+
26
+ /** Inputs to a proposer. */
27
+ export interface ProposeInput {
28
+ /** Recent trajectory evidence (the same string /refine gathers). */
29
+ evidence: string;
30
+ /** Current harness state as a defensive copy; mutations are ignored — use
31
+ * the returned deltas to change state. */
32
+ state: HarnessState;
33
+ /** Lookback window in turns. */
34
+ lookback: number;
35
+ }
36
+
37
+ /** A single delta plus a human-readable reason for the audit trail. */
38
+ export interface ProposedDelta {
39
+ delta: Delta;
40
+ rationale: string;
41
+ }
42
+
43
+ export interface ProposeResult {
44
+ /** Deltas the proposer produced directly (rule-based / model proposers). */
45
+ deltas?: ProposedDelta[];
46
+ /**
47
+ * A steering message for the agent, when the proposer delegates reasoning to
48
+ * the agent loop (the default). runRefine sends this via sendUserMessage.
49
+ * A proposer MAY return both — deltas are applied first, then steering sent.
50
+ */
51
+ steeringMessage?: string;
52
+ }
53
+
54
+ /** A strategy for turning evidence + state into (or toward) harness deltas. */
55
+ export interface DeltaProposer {
56
+ readonly name: string;
57
+ propose(input: ProposeInput): Promise<ProposeResult>;
58
+ }
59
+
60
+ // ---- default: steering (delegates reasoning to the agent) ------------------
61
+
62
+ function buildSteeringPrompt(evidence: string, lookback: number): string {
63
+ return [
64
+ `/refine (online self-improvement, last ${lookback} turns as evidence)`,
65
+ "",
66
+ "Review the trajectory evidence below. Identify DURABLE, REUSABLE corrections — things that will help in future turns, not one-off fixes. Then update the Continual Harness state.",
67
+ "",
68
+ "Steps:",
69
+ "1. Call harness_list to see the current state.",
70
+ "2. Call harness_mutate with small, surgical CRUD deltas. Every create MUST include concrete evidence from the trajectory. Prefer updating existing items over creating duplicates; delete items that are wrong, stale, or contradicted.",
71
+ "3. Keep prompt notes terse and behavioral. Memory facts should be specific. Skill/sub-agent entries should describe reusable patterns, not one task.",
72
+ "",
73
+ "Constraints: never rewrite the whole store; ground every change in evidence; if nothing durable is worth recording, say so and do nothing.",
74
+ "",
75
+ "## Trajectory evidence",
76
+ evidence,
77
+ ].join("\n");
78
+ }
79
+
80
+ /** Default proposer: delegates reasoning to the agent via a steering message. */
81
+ export const steeringProposer: DeltaProposer = {
82
+ name: "steering",
83
+ async propose({ evidence, lookback }): Promise<ProposeResult> {
84
+ return { steeringMessage: buildSteeringPrompt(evidence, lookback) };
85
+ },
86
+ };
87
+
88
+ // ---- rule-based alternate: dedupe -----------------------------------------
89
+
90
+ export const DEDUPE_THRESHOLD = 0.6;
91
+
92
+ /** Tokenize for overlap comparison: lowercase alnum tokens. */
93
+ export function tokenize(text: string): string[] {
94
+ return text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter(Boolean);
95
+ }
96
+
97
+ /** Jaccard token overlap in [0,1]. 0 if either side has no tokens. */
98
+ export function tokenOverlap(a: string, b: string): number {
99
+ const sa = new Set(tokenize(a));
100
+ const sb = new Set(tokenize(b));
101
+ if (sa.size === 0 || sb.size === 0) return 0;
102
+ let inter = 0;
103
+ for (const t of sa) if (sb.has(t)) inter++;
104
+ return inter / (sa.size + sb.size - inter);
105
+ }
106
+
107
+ function truncate(s: string, n = 48): string {
108
+ return s.length > n ? `${s.slice(0, n - 1)}…` : s;
109
+ }
110
+
111
+ /**
112
+ * Rule-based proposer: drops near-duplicate ACTIVE items (same kind, Jaccard
113
+ * token overlap >= DEDUPE_THRESHOLD), keeping the higher-importance one.
114
+ *
115
+ * Greedy and contradiction-free: items are considered in importance-descending
116
+ * order; the first item is always a keeper, and each later item is compared
117
+ * only against keepers, so a keeper is never subsequently dropped.
118
+ */
119
+ export const dedupeProposer: DeltaProposer = {
120
+ name: "dedupe",
121
+ async propose({ state }): Promise<ProposeResult> {
122
+ const ordered = state.items
123
+ .filter((i) => i.active)
124
+ .sort((a, b) => b.importance - a.importance);
125
+ const keepers: HarnessItem[] = [];
126
+ const proposals: ProposedDelta[] = [];
127
+ for (const cand of ordered) {
128
+ let dup: HarnessItem | undefined;
129
+ let best = 0;
130
+ for (const k of keepers) {
131
+ if (k.kind !== cand.kind) continue;
132
+ const sim = tokenOverlap(k.content, cand.content);
133
+ if (sim >= DEDUPE_THRESHOLD && sim > best) {
134
+ best = sim;
135
+ dup = k;
136
+ }
137
+ }
138
+ if (dup) {
139
+ proposals.push({
140
+ delta: {
141
+ op: "delete",
142
+ id: cand.id,
143
+ reason: `near-duplicate of ${dup.id} (overlap ${best.toFixed(2)})`,
144
+ },
145
+ rationale: `dedupe: "${truncate(cand.content)}" ≈ keeper "${truncate(dup.content)}" (Jaccard ${best.toFixed(2)}); kept higher-importance ${dup.id}.`,
146
+ });
147
+ } else {
148
+ keepers.push(cand);
149
+ }
150
+ }
151
+ return proposals.length ? { deltas: proposals } : {};
152
+ },
153
+ };
154
+
155
+ // ---- registry -------------------------------------------------------------
156
+
157
+ const registry = new Map<string, DeltaProposer>([
158
+ ["steering", steeringProposer],
159
+ ["dedupe", dedupeProposer],
160
+ ]);
161
+
162
+ /** Register (or replace) a named proposer. For external extensions. */
163
+ export function registerProposer(p: DeltaProposer): void {
164
+ registry.set(p.name, p);
165
+ }
166
+
167
+ /** Look up a proposer by name; falls back to the steering default. */
168
+ export function getProposer(name: string | undefined): DeltaProposer {
169
+ return registry.get(name ?? "steering") ?? steeringProposer;
170
+ }
171
+
172
+ export function listProposers(): string[] {
173
+ return [...registry.keys()];
174
+ }
package/src/refine.ts CHANGED
@@ -1,19 +1,25 @@
1
- // /refine — the online optimizer. Manual only (no turn_end auto-trigger).
1
+ // /refine — the online optimizer. Manual by default; opt-in turn_end
2
+ // auto-refine lives in auto-refine.ts and reuses runRefine().
2
3
  //
3
4
  // Flow:
4
5
  // 1. Gather recent trajectory evidence from the current branch.
5
- // 2. Send a steering user message that asks the agent to propose evidence-
6
- // backed CRUD deltas via harness_mutate (and to inspect state with
7
- // harness_list first).
8
- // 3. The agent does the reasoning and calls the tools. Each accepted delta is
9
- // recorded as a session entry, so /tree gives rollback for free.
6
+ // 2. Hand evidence + current state to a pluggable DeltaProposer (proposer.ts).
7
+ // The default `steering` proposer returns a steering user message; the
8
+ // agent then reasons and calls harness_mutate. Rule-based/model proposers
9
+ // return deltas directly, which runRefine applies itself.
10
+ // 3. Every mutation — whether from the agent (harness_mutate) or a direct-apply
11
+ // proposer — is recorded as a session entry, so /tree gives rollback for
12
+ // free. Which proposer ran is captured in the audit entry.
10
13
  //
11
- // Why a steering message instead of a nested LLM call: it reuses the existing
12
- // agent loop, is model-agnostic, and keeps every delta visible and reviewable
13
- // in the transcript. No hidden model calls.
14
+ // Why a steering message as the DEFAULT instead of a nested LLM call: it reuses
15
+ // the existing agent loop, is model-agnostic, and keeps every delta visible and
16
+ // reviewable in the transcript. Alternate proposers can opt out via the
17
+ // registry; the dedicated-model variant is intentionally not shipped (hidden
18
+ // model spend is a tradeoff the roadmap keeps as a separate decision).
14
19
 
15
20
  import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
16
- import { exportDurable, REFINE_ENTRY } from "./store.js";
21
+ import { applyDeltas, exportDurable, REFINE_ENTRY, snapshotState } from "./store.js";
22
+ import { getProposer } from "./proposer.js";
17
23
 
18
24
  const DEFAULT_EVIDENCE_BYTES = 16000;
19
25
  export const DEFAULT_LOOKBACK_TURNS = 25;
@@ -32,10 +38,10 @@ type AnyEntry = {
32
38
  export function registerRefine(pi: ExtensionAPI): void {
33
39
  pi.registerCommand("refine", {
34
40
  description:
35
- "Online self-improvement: review recent trajectory and propose evidence-backed CRUD deltas to the harness state. Usage: /refine [lookback-turns] [--commit]",
41
+ "Online self-improvement: review recent trajectory and propose evidence-backed CRUD deltas to the harness state. Usage: /refine [lookback-turns] [--commit] [--proposer steering|dedupe]",
36
42
  handler: async (args, ctx) => {
37
- const { lookback, commit } = parseArgs(args);
38
- await runRefine(pi, ctx, { lookback, commit });
43
+ const { lookback, commit, proposer } = parseArgs(args);
44
+ await runRefine(pi, ctx, { lookback, commit, ...(proposer ? { proposer } : {}) });
39
45
  },
40
46
  });
41
47
  }
@@ -43,6 +49,8 @@ export function registerRefine(pi: ExtensionAPI): void {
43
49
  export interface RefineOptions {
44
50
  lookback?: number;
45
51
  commit?: boolean;
52
+ /** Proposer name (see proposer.ts registry). Defaults to "steering". */
53
+ proposer?: string;
46
54
  }
47
55
 
48
56
  export interface RefineResult {
@@ -50,13 +58,18 @@ export interface RefineResult {
50
58
  lookback: number;
51
59
  commit: boolean;
52
60
  source: "manual" | "auto";
61
+ /** Which proposer ran. */
62
+ proposer: string;
63
+ /** Deltas the proposer applied directly (0 for the steering path). */
64
+ applied: number;
53
65
  }
54
66
 
55
- /** Core refine routine shared by /refine (manual) and turn_end auto-refine.
56
- * Gathers trajectory evidence, records an audit entry, optionally flushes the
57
- * durable state, then sends the steering user message that drives the agent to
58
- * propose evidence-backed CRUD deltas. `source` tags the audit entry so
59
- * autonomous runs are distinguishable in the transcript. */
67
+ /**
68
+ * Core refine routine shared by /refine (manual) and turn_end auto-refine.
69
+ * Resolves a proposer, gathers evidence, lets the proposer decide what to do,
70
+ * then either applies returned deltas directly or sends a steering message.
71
+ * `source` tags the audit entry so autonomous runs are distinguishable.
72
+ */
60
73
  export async function runRefine(
61
74
  pi: ExtensionAPI,
62
75
  ctx: ExtensionContext,
@@ -65,12 +78,40 @@ export async function runRefine(
65
78
  ): Promise<RefineResult> {
66
79
  const lookback = options.lookback ?? DEFAULT_LOOKBACK_TURNS;
67
80
  const commit = options.commit ?? false;
68
- ctx.ui.setStatus("harness", `Refining (last ${lookback} turns)…`);
81
+ const proposer = getProposer(options.proposer);
82
+ ctx.ui.setStatus("harness", `Refining (last ${lookback} turns)… [${proposer.name}]`);
83
+
69
84
  const evidence = gatherEvidence(ctx, lookback);
70
- const prompt = buildSteeringPrompt(evidence, lookback);
85
+ // Pass a defensive copy: proposers are a public extension point and must not
86
+ // be able to mutate the live store outside applyDeltas (no audit / no rollback).
87
+ const result = await proposer.propose({ evidence, state: snapshotState(), lookback });
88
+ const proposedDeltas = result.deltas ?? [];
89
+
90
+ let applied = 0;
91
+ if (proposedDeltas.length > 0) {
92
+ // Direct-apply path (rule-based / model proposers): persist each batch the
93
+ // same way harness_mutate does, so /tree rollback covers these too.
94
+ const appliedDeltas = applyDeltas(
95
+ proposedDeltas.map((d) => d.delta),
96
+ (snapshot, ver) => pi.appendEntry("harness-state", { state: snapshot, version: ver }),
97
+ );
98
+ applied = appliedDeltas.length;
99
+ ctx.ui.notify(`Proposer "${proposer.name}" applied ${applied} delta(s) directly.`, "info");
100
+ }
101
+
71
102
  // Audit trail (branchable via /tree). `source` distinguishes manual /refine
72
- // from opt-in auto-refine so autonomous runs are visible in the transcript.
73
- pi.appendEntry(REFINE_ENTRY, { lookback, commit, startedAt: Date.now(), source });
103
+ // from opt-in auto-refine; `proposer` records WHICH strategy ran; the
104
+ // rationales make rule-based decisions visible in the transcript.
105
+ pi.appendEntry(REFINE_ENTRY, {
106
+ lookback,
107
+ commit,
108
+ startedAt: Date.now(),
109
+ source,
110
+ proposer: proposer.name,
111
+ applied,
112
+ rationales: proposedDeltas.map((d) => d.rationale),
113
+ });
114
+
74
115
  if (commit) {
75
116
  // Flush current state to the durable file first so the agent refines
76
117
  // against the same view pi-reflect / pi-mem would see.
@@ -81,20 +122,38 @@ export async function runRefine(
81
122
  ctx.ui.notify(`Durable export failed: ${(err as Error).message}`, "warning");
82
123
  }
83
124
  }
84
- pi.sendUserMessage(prompt);
125
+
126
+ if (result.steeringMessage) {
127
+ pi.sendUserMessage(result.steeringMessage);
128
+ }
85
129
  ctx.ui.setStatus("harness", undefined);
86
- return { evidenceBytes: evidence.length, lookback, commit, source };
130
+ return {
131
+ evidenceBytes: evidence.length,
132
+ lookback,
133
+ commit,
134
+ source,
135
+ proposer: proposer.name,
136
+ applied,
137
+ };
87
138
  }
88
139
 
89
- function parseArgs(args: string): { lookback: number; commit: boolean } {
140
+ function parseArgs(args: string): {
141
+ lookback: number;
142
+ commit: boolean;
143
+ proposer: string | undefined;
144
+ } {
90
145
  const parts = args.trim().split(/\s+/).filter(Boolean);
91
146
  let lookback = DEFAULT_LOOKBACK_TURNS;
92
147
  let commit = false;
93
- for (const p of parts) {
148
+ let proposer: string | undefined;
149
+ for (let i = 0; i < parts.length; i++) {
150
+ const p = parts[i]!;
94
151
  if (p === "--commit") commit = true;
152
+ else if (p === "--proposer" && i + 1 < parts.length) proposer = parts[++i];
153
+ else if (p.startsWith("--proposer=")) proposer = p.slice("--proposer=".length);
95
154
  else if (/^\d+$/.test(p)) lookback = Math.max(1, Math.min(200, Number(p)));
96
155
  }
97
- return { lookback, commit };
156
+ return { lookback, commit, proposer };
98
157
  }
99
158
 
100
159
  function gatherEvidence(ctx: ExtensionContext, lookback: number): string {
@@ -121,21 +180,3 @@ function gatherEvidence(ctx: ExtensionContext, lookback: number): string {
121
180
  }
122
181
  return lines.join("\n") || "(no recent trajectory evidence found)";
123
182
  }
124
-
125
- function buildSteeringPrompt(evidence: string, lookback: number): string {
126
- return [
127
- `/refine (online self-improvement, last ${lookback} turns as evidence)`,
128
- "",
129
- "Review the trajectory evidence below. Identify DURABLE, REUSABLE corrections — things that will help in future turns, not one-off fixes. Then update the Continual Harness state.",
130
- "",
131
- "Steps:",
132
- "1. Call harness_list to see the current state.",
133
- "2. Call harness_mutate with small, surgical CRUD deltas. Every create MUST include concrete evidence from the trajectory. Prefer updating existing items over creating duplicates; delete items that are wrong, stale, or contradicted.",
134
- "3. Keep prompt notes terse and behavioral. Memory facts should be specific. Skill/sub-agent entries should describe reusable patterns, not one task.",
135
- "",
136
- "Constraints: never rewrite the whole store; ground every change in evidence; if nothing durable is worth recording, say so and do nothing.",
137
- "",
138
- "## Trajectory evidence",
139
- evidence,
140
- ].join("\n");
141
- }
package/src/store.ts CHANGED
@@ -32,6 +32,13 @@ export function getState(): HarnessState {
32
32
  return state;
33
33
  }
34
34
 
35
+ /** A defensive deep-enough copy of current state (items are flat). Use when
36
+ * handing state to untrusted/external code (e.g. a DeltaProposer) so it cannot
37
+ * mutate the live store outside applyDeltas. */
38
+ export function snapshotState(): HarnessState {
39
+ return { items: state.items.map((i) => ({ ...i })) };
40
+ }
41
+
35
42
  export function listItems(kind?: ComponentKind): HarnessItem[] {
36
43
  return kind ? state.items.filter((i) => i.kind === kind) : state.items;
37
44
  }