pi-continual-harness 0.12.0 → 0.15.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
@@ -70,11 +70,13 @@ Durable I/O (round-trip with pi-reflect):
70
70
  /harness drop <id> # nudge importance down (−0.1)
71
71
  /harness move <id> global|project # move an item between durable layers
72
72
  /harness split # steer the agent to classify every item's scope
73
+ /harness adopt --from <provider/id> [--all] # steer the agent to copy another model's items here
73
74
  /harness push-mem [--all|--kind <kind>|--model <provider/id|active>] # persist active items to pi-mem (save_memory)
75
+ /harness setup [--preset <quiet|recommended|power>] [--yes] # guided config wizard
74
76
  ```
75
77
 
76
- Subcommands, flags, `--kind`/`--model` values, `move` scopes, and `keep`/
77
- `drop`/`move` item ids all autocomplete in the TUI as you type after
78
+ Subcommands, flags, `--kind`/`--model`/`--from` values, `move` scopes, and
79
+ `keep`/`drop`/`move` item ids all autocomplete in the TUI as you type after
78
80
  `/harness `.
79
81
 
80
82
  **Durable layers.** Every item carries its own durable scope: `global`
@@ -104,7 +106,10 @@ The model-facing tools:
104
106
 
105
107
  - `harness_list({ kind?, model? })` — read current state. `model` defaults to
106
108
  the active model's items (what gets injected this turn); `"*"` returns every
107
- model.
109
+ model. Empty results are **diagnostic**: the message distinguishes a
110
+ genuinely empty store (with zero-state guidance) from items hidden by the
111
+ model or kind filter (with a tally and a `model: "*"` pointer), so an
112
+ empty list never reads as "broken".
108
113
  - `harness_mutate { deltas: [...] }` — apply a batch of `create` / `update` /
109
114
  `delete` deltas. Every `create` requires `evidence`. New items are stamped
110
115
  automatically with the active model (see [Model binding](#model-binding-per-model-isolation)).
@@ -136,15 +141,39 @@ unchanged by a push — pi-mem gets a separate copy.
136
141
 
137
142
  ## Configuration
138
143
 
144
+ **First-run setup:** instead of hand-editing the JSON below, run
145
+ `/harness setup`. It is a guided wizard:
146
+
147
+ 1. Shows the **current effective config** (your file merged over the shipped
148
+ defaults) plus the durable-layer paths.
149
+ 2. Offers three **presets**: `quiet` (pin every opt-in to the shipped
150
+ defaults), `recommended` (`autoImport` + import reminders on), `power`
151
+ (`recommended` + `autoRefine`). Each preset is shown as a declarative diff
152
+ before anything is applied.
153
+ 3. Confirms **each delta** with a one-line rationale. Autonomous switches
154
+ get their own explicit "this lets the agent mutate state on a cadence"
155
+ confirmation regardless of preset.
156
+ 4. Writes via one reviewed path: backs up the existing file
157
+ (`harness.json.bak-<timestamp>`), atomically writes the merged result,
158
+ then reloads and prints the effective config exactly as pi will read it.
159
+
160
+ For scripted setups: `/harness setup --preset recommended --yes` (the `--yes`
161
+ flag is the scripted form of the confirmations, so it enables autonomous
162
+ features without prompts). Nothing is written on unknown input, a cancelled
163
+ preset menu, or a declined final write; declining an individual feature only
164
+ skips that feature (the accepted deltas still apply). The wizard never
165
+ touches harness state, only the config file.
166
+
139
167
  Optional config at `~/.pi/agent/harness.json` (missing or malformed → defaults):
140
168
 
141
169
  ```json
142
170
  {
143
171
  "autoImport": false,
172
+ "importRemind": { "enabled": true, "everyTurns": 25 },
144
173
  "proposer": "steering",
145
174
  "dedupe": { "threshold": 0.6, "merge": true },
146
175
  "injection": { "enabled": true, "maxTokens": 1500, "maxPerKind": 10, "charsPerToken": 4 },
147
- "remindRefine": { "enabled": false, "everyTurns": 50 },
176
+ "remindRefine": { "enabled": true, "everyTurns": 25, "detailEvery": 4 },
148
177
  "autoRefine": { "enabled": false, "everyTurns": 100, "commit": false, "gate": false },
149
178
  "outcomeImportance": { "enabled": false, "bump": 0.03 },
150
179
  "classifier": { "model": "typesafe/jev-latest" },
@@ -160,7 +189,21 @@ Optional config at `~/.pi/agent/harness.json` (missing or malformed → defaults
160
189
  changed since the last export. This is what makes `/refine` output survive
161
190
  into **new** sessions without manual export/import ceremony. Both halves are
162
191
  visible (one notify line) and use the same persisted `harness-state` entries
163
- as every other mutation (`/tree` rollback covers them).
192
+ as every other mutation (`/tree` rollback covers them). Enabling it
193
+ mid-session (e.g. via `/harness setup`) is safe: the first `turn_end`
194
+ imports the layers *before* any export, so a late enable can never
195
+ overwrite the durable files with a never-imported store.
196
+ - **`importRemind`** — **awareness reminders** for that sync (effective only
197
+ when `autoImport` is on; default `{ "enabled": true, "everyTurns": 25 }`).
198
+ The session-start import doesn't just count — it **lists what arrived**
199
+ (item id, kind, first words of content), and every `everyTurns` turns a
200
+ one-line reminder re-surfaces the imported items so silently-injected
201
+ instructions never go unnoticed (full list via the agent's `harness_list`
202
+ tool — `/harness status` shows only counts). Bounded: at most 3 reminders
203
+ per imported batch, then quiet until the next session; aborted/error turns
204
+ are inert (completed-turn cadence). Kill-switch:
205
+ `{ "importRemind": { "enabled": false } }` (the start-of-session listing
206
+ still fires).
164
207
  - **`injection`** — the selection policy for WHAT gets surfaced in the system
165
208
  prompt each turn (see [Injection selection](#injection-selection-on-by-default)).
166
209
  ON by default: items are importance-ordered, capped at `maxPerKind` (default
@@ -181,9 +224,20 @@ Optional config at `~/.pi/agent/harness.json` (missing or malformed → defaults
181
224
  companion packages inject a richer comparator whose return may **abstain**
182
225
  per pair (`{ score, abstain }` — see [`SimilarityResult`](#proposers)); an
183
226
  abstaining pair is always kept, never merged or deleted.
184
- - **`remindRefine`** — opt-in `turn_end` nudge. `{ "enabled": true,
185
- "everyTurns": 50 }` notifies you to run `/refine` on a cadence. It is
186
- informational only — it never mutates state.
227
+ - **`remindRefine`** — the `turn_end` **activation nudge**, ON by default
228
+ (informational only; it never mutates state). Every `everyTurns` completed
229
+ turns (default 25) a one-line reminder suggests `/refine`, garnished with
230
+ live data from the store (e.g. "12 active item(s), 4 never cited since
231
+ creation"; an empty store gets first-run guidance instead). Every
232
+ `detailEvery`-th reminder (default 4: at the default cadence, roughly
233
+ every 100 turns) escalates to a rotating educational guide: how the loop
234
+ works (with alternate descriptions so it never reads as boilerplate), the
235
+ `/refine` workflow, `keep`/`drop`/`prune --decay` hygiene, `push-mem`,
236
+ `split`, `export-corpus`, and a recommended starter config. Fresh installs
237
+ used to be silent; that was the bug, not the feature. Aborted/error turns
238
+ are skipped, so `everyTurns` counts completed turns of work. Silence it
239
+ with `{ "remindRefine": { "enabled": false } }`; `detailEvery: 1` makes
240
+ every reminder detailed.
187
241
  - **`autoRefine`** — opt-in autonomous self-improvement (**off by default**).
188
242
  When `enabled`, the agent runs `/refine` itself every `everyTurns` turns
189
243
  (default 100). It is one of the package's opt-in autonomous paths: it reuses
@@ -195,14 +249,21 @@ Optional config at `~/.pi/agent/harness.json` (missing or malformed → defaults
195
249
  signal, not just time (errors fall back to the plain cadence). The steering
196
250
  prompt is delivered via the pi ≥ 0.87 actionable `turn_end` boundary (a
197
251
  structural `harness.auto-refine-request` entry + `continue: true`), not a
198
- synthetic user message.
252
+ synthetic user message. Aborted/error turns (pi `turn_end`
253
+ `BoundaryState.outcome`) are inert: they neither seed nor advance the
254
+ cadence window — a refine never fires over partial evidence.
199
255
  - **`classifier`** — opt-in classifier model (pi ≥ 0.99) for the cheap gated
200
256
  paths: `"provider/id"` or a bare id of a **classifier** catalog entry, e.g.
201
257
  `"typesafe/jev-latest"` or a local `llama-cpp` classifier (every chat model
202
- listed for llama.cpp also appears as a classifier). When set and resolvable
203
- it enables the classifier-confirmed dedupe (see `dedupe` above) and the
204
- auto-refine `gate`. When unset or unresolvable, every classifier feature
205
- degrades to its no-classifier behavior.
258
+ listed for llama.cpp also appears as one). With pi ≥ 1.1.0 the catalog also
259
+ offers OpenAI's GPT-6 Luna through the Decisions API (e.g.
260
+ `"openai/gpt-6-luna"`, needs `OPENAI_API_KEY`) and the native llama.cpp
261
+ decision models (Julia-1, Laya, Kev, lev, OpenJev through `/v1/systemone`,
262
+ llama.cpp ≥ 0.6.0 — listed *only* as classifiers, which is exactly our use:
263
+ a free local gate). When set and resolvable it enables the
264
+ classifier-confirmed dedupe (see `dedupe` above) and the auto-refine `gate`.
265
+ When unset or unresolvable, every classifier feature degrades to its
266
+ no-classifier behavior.
206
267
  - **`virtualBinding`** — how the per-model isolation key treats pi **virtual
207
268
  models** (router extensions, pi ≥ 0.99). Default `"virtual"`: the key is
208
269
  the selected model's own `provider/id` — a router starts a blank harness.
@@ -215,19 +276,23 @@ Optional config at `~/.pi/agent/harness.json` (missing or malformed → defaults
215
276
  - **`outcomeImportance`** — opt-in autonomous **promotion** loop (**off by
216
277
  default**). When `enabled`, a `turn_end` hook bumps (+`bump`, default 0.03)
217
278
  the importance of any active item the agent cited by its `[h_xxxx]` tag in the
218
- turn's output. Referenced items gain importance and get their `updatedAt`
279
+ turn's output. Citations are read from completed turns only — an aborted
280
+ turn's partial text is scanned by the next completed turn (the initial seed
281
+ turn itself is never scanned, as before). Referenced items
282
+ gain importance and get their `updatedAt`
219
283
  touched (so they survive time-based decay); ignored items keep decaying. This
220
284
  is the package's second opt-in autonomous path — promotion only, never
221
285
  deletes, persisted/branchable like `harness_mutate`. Autonomous demotion from
222
286
  outcomes is intentionally NOT done (high false-positive); use `/harness drop`,
223
287
  `prune --decay`, or the `dedupe` proposer for that.
224
288
 
225
- > **Deprecated:** `durableScope` (0.9.0). Items now carry their own scope
226
- > (`/harness move`), and durable I/O is always layered — the key no longer
227
- > switches anything but is still parsed so existing configs keep loading.
228
- > Migration from a `durableScope: "project"` setup: run `/harness import` once
229
- > in the project (its file's items adopt project scope), then move any strays
230
- > with `/harness move` (or classify the whole store with `/harness split`).
289
+ > **Removed:** the store-wide `durableScope` config key (deprecated 0.9.0,
290
+ > removed now). Items carry their own scope (`/harness move`), and durable I/O
291
+ > is always layered. Config files that still set the key load fine — unknown
292
+ > keys are ignored. Migration from a `durableScope: "project"` setup: run
293
+ > `/harness import` once in the project (its file's items adopt project
294
+ > scope), then move any strays with `/harness move` (or classify the whole
295
+ > store with `/harness split`).
231
296
 
232
297
  ## How it works
233
298
 
@@ -276,6 +341,29 @@ How the binding is set and respected:
276
341
  - **Durable round-trip preserves owner.** `/harness export` tags each item with
277
342
  `model: provider/id`; `/harness import` restores it. An item whose tag
278
343
  pi-reflect stripped becomes an orphan and is adopted by the active model.
344
+ - **Carrying owned items across models: `/harness adopt`.** Orphan adoption
345
+ only covers *unowned* items. `/harness adopt --from <provider/id>` is the
346
+ reviewable path for owned ones: it steers the agent to classify the source
347
+ model's active items **generalizes** (engineering practices, user
348
+ preferences, project facts) vs **stays** (that model's quirks, formatting
349
+ preferences, failure modes), then apply the adoption as ONE `harness_mutate`
350
+ batch of create deltas. It is **copy, not move**: the source model's harness
351
+ stays untouched, content is copied verbatim (importance is listed at full
352
+ precision so the copy keeps its exact ranking), and evidence is annotated
353
+ `adopted from <provider/id>`. Because `ownerModel` is never model-settable,
354
+ the creates stamp to the **active** model via the normal create-stamping
355
+ path — audited and `/tree`-rollback-able like every mutation. `--all` skips
356
+ the filter; overlaps afterwards clean up with `/refine --proposer dedupe`.
357
+ Two store-level filters keep the copy honest (review #25): items scoped to
358
+ a **different project** are skipped (a copy would re-stamp them into this
359
+ session's project — adopt them from a session in that project instead),
360
+ and content the adopter already owns is skipped **before** the batch cap,
361
+ so re-running against a >20-item source reaches new items instead of
362
+ re-selecting the same top ones. The self-adopt guard compares against the
363
+ **resolved binding key** (`virtualBinding`-aware — under `physical` a
364
+ router selection adopts as the routed model), the same key
365
+ create-stamping uses. Explicit and manual only — there is no automatic
366
+ cross-model adoption.
279
367
 
280
368
  Manual commands (`export`, `import`, `move`, `split`, `keep`, `drop`, `prune`,
281
369
  `push-mem`, `status`) operate on the **whole store** by design — they are
@@ -410,10 +498,10 @@ Then `"my-proposer"` is selectable via `/refine --proposer my-proposer` or
410
498
  auto-import + turn_end layered auto-export when the store changed —
411
499
  `/refine` output survives into new sessions with zero ceremony. Off by
412
500
  default; every action visible and `/tree`-rollback-able.
413
- - Optional config (`~/.pi/agent/harness.json`): the durable-sync opt-in, an
414
- opt-in `turn_end` reminder, opt-in `turn_end` auto-refine, and an opt-in
415
- `turn_end` outcome-importance loop — the package's opt-in autonomous
416
- paths (all off by default).
501
+ - Optional config (`~/.pi/agent/harness.json`): the durable-sync opt-in, a
502
+ default-on `turn_end` reminder (notify-only; the activation nudge), and the
503
+ opt-in `turn_end` auto-refine and outcome-importance loops. The package's
504
+ autonomous paths stay opt-in and off by default.
417
505
  - **Pluggable delta proposers** with a registry: `steering` (default) and
418
506
  `dedupe` (rule-based) shipped; `registerProposer()` for custom ones.
419
507
  - **Per-model isolation**: every item is bound to a `provider/id` and injected
@@ -422,7 +510,9 @@ Then `"my-proposer"` is selectable via `/refine --proposer my-proposer` or
422
510
  round-trip preserves the owner tag. With pi virtual models (routers), the
423
511
  `"virtualBinding": "physical"` policy keys items by the routed physical
424
512
  model instead of the router id, so a router selection does not silently
425
- reset the harness.
513
+ reset the harness. Owned items can be carried across models explicitly with
514
+ `/harness adopt --from <provider/id> [--all]` (agent-filtered copy; see
515
+ [Model binding](#model-binding-per-model-isolation)).
426
516
  - **Bounded injection (on by default)**: the supplemental block is
427
517
  importance-ordered and capped per kind + by a total token budget, so a growing
428
518
  harness never balloons the system prompt. Tunable / opt-out via the `injection`
@@ -440,7 +530,9 @@ Then `"my-proposer"` is selectable via `/refine --proposer my-proposer` or
440
530
  - **Boundary delivery for auto-refine**: the steering prompt is persisted as a
441
531
  structural entry with `{ entries, continue: true }` at `turn_end` (no
442
532
  synthetic user message, no follow-up-scheduling side effects); manual
443
- `/refine` still steers via a user message.
533
+ `/refine` still steers via a user message. Each new boundary also omits
534
+ prior auto-refine request drafts from future provider context (pi ≥ 0.87
535
+ context edits; `autoRefine.omitStaleDrafts: false` to keep them).
444
536
 
445
537
  Open extension points (see `docs/ROADMAP.md`):
446
538
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-continual-harness",
3
- "version": "0.12.0",
3
+ "version": "0.15.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",
@@ -25,6 +25,12 @@
25
25
  // sendUserMessage when the boundary cannot continue (event.context.
26
26
  // canContinue false, e.g. after an error turn).
27
27
  //
28
+ // Aborted/error turns are inert here: a turn_end whose BoundaryState.outcome
29
+ // is "aborted" or "error" neither seeds nor advances the cadence window, so
30
+ // "every N turns" counts completed turns of work and a refine never fires
31
+ // over partial evidence. (Durable auto-export deliberately still exports on
32
+ // such turns — committed entries must not be lost.)
33
+ //
28
34
  // It stays visible: it notifies before firing, and the boundary draft (and the
29
35
  // gate decision) appear in the transcript. The decision logic is factored into
30
36
  // evaluateAutoRefine() so it is unit-testable without a live pi runtime.
@@ -34,29 +40,47 @@ import { DEFAULT_AUTO_EVERY_TURNS, type HarnessConfig, loadConfig } from "./conf
34
40
  import { runRefine, gatherEvidence, DEFAULT_LOOKBACK_TURNS } from "./refine.js";
35
41
  import { buildClassify, buildGateRequest, gateDecision } from "./classify.js";
36
42
 
37
- // turnIndex of the last auto-refine (or the seeded baseline). -1 = unseen.
38
- let lastTurn = -1;
43
+ // Completed turns observed since the last fire (or the seeded baseline).
44
+ // -1 = unseen. Aborted/error turns never touch this count — the window is
45
+ // completed turns of work, not raw turn indices.
46
+ let completedTurns = -1;
39
47
 
40
48
  /** Test hook: reset internal counter. */
41
49
  export function resetAutoRefine(): void {
42
- lastTurn = -1;
50
+ completedTurns = -1;
51
+ }
52
+
53
+ /**
54
+ * True when a turn_end's outcome marks a turn that ended badly (pi's
55
+ * BoundaryState.outcome: "aborted" | "error"). Undefined is treated as
56
+ * completed — current pi always reports the field, but synthetic events and
57
+ * the earliest 0.99.x may omit it.
58
+ */
59
+ export function turnEndedBadly(outcome?: string): boolean {
60
+ return outcome === "aborted" || outcome === "error";
43
61
  }
44
62
 
45
63
  /**
46
64
  * Decide whether to auto-refine this turn. Side-effectful: the first observed
47
- * turn seeds the baseline (no refine then); each fired refine resets it, which
48
- * also prevents the refine turn itself from immediately re-triggering.
65
+ * completed turn seeds the baseline (no refine then); every completed turn
66
+ * after it advances the count, and each fired refine resets it (which also
67
+ * prevents the refine turn itself from immediately re-triggering).
68
+ * Aborted/error turns are inert: they neither seed nor advance the count —
69
+ * the cadence counts completed turns of work, never raw turn indices, so an
70
+ * aborted turn in between cannot shorten the window.
49
71
  */
50
- export function evaluateAutoRefine(config: HarnessConfig, turnIndex: number): boolean {
72
+ export function evaluateAutoRefine(config: HarnessConfig, outcome?: string): boolean {
51
73
  if (!config.autoRefine?.enabled) return false;
74
+ if (turnEndedBadly(outcome)) return false;
52
75
  const every = config.autoRefine.everyTurns ?? DEFAULT_AUTO_EVERY_TURNS;
53
76
  if (every <= 0) return false;
54
- if (lastTurn < 0) {
55
- lastTurn = turnIndex;
77
+ if (completedTurns < 0) {
78
+ completedTurns = 0; // seed: the seed turn itself doesn't count
56
79
  return false;
57
80
  }
58
- if (turnIndex - lastTurn >= every) {
59
- lastTurn = turnIndex;
81
+ completedTurns += 1;
82
+ if (completedTurns >= every) {
83
+ completedTurns = 0;
60
84
  return true;
61
85
  }
62
86
  return false;
@@ -75,11 +99,44 @@ export function buildRefineDraft(steeringMessage: string): {
75
99
  return { type: "custom_message", customType: AUTO_REFINE_ENTRY, content: steeringMessage, display: true };
76
100
  }
77
101
 
102
+ /** Structural slice of pi's ProjectedSessionEntry (sourceEntry + messages). */
103
+ export interface ProjectedLike {
104
+ sourceEntry?: { type?: string; customType?: string; id?: string };
105
+ messages?: unknown[];
106
+ }
107
+
108
+ /** A context_edit boundary draft (pi ≥ 0.87): omit one entry from future
109
+ * provider context — raw history, usage, and UI history stay untouched. */
110
+ export interface OmissionDraft {
111
+ type: "context_edit";
112
+ targetId: string;
113
+ replacement: null;
114
+ }
115
+
116
+ /**
117
+ * Context-edit drafts omitting every LIVE prior auto-refine request from
118
+ * future provider context (pi ≥ 0.87 ContextEditEntry). Invariant: at most
119
+ * ONE auto-refine request is ever model-visible — the newest. Already-omitted
120
+ * drafts surface in the projection with empty `messages` and are skipped, so
121
+ * the operation is idempotent and never stacks redundant edits. Raw history
122
+ * (and the HTML export) keeps every draft: /tree rollback still applies.
123
+ */
124
+ export function buildOmissionDrafts(projected: Iterable<ProjectedLike>): OmissionDraft[] {
125
+ const drafts: OmissionDraft[] = [];
126
+ for (const p of projected) {
127
+ const src = p.sourceEntry;
128
+ if (src?.type !== "custom_message" || src.customType !== AUTO_REFINE_ENTRY) continue;
129
+ if ((p.messages?.length ?? 0) === 0) continue; // already omitted → skip
130
+ if (typeof src.id === "string") drafts.push({ type: "context_edit", targetId: src.id, replacement: null });
131
+ }
132
+ return drafts;
133
+ }
134
+
78
135
  /** Subscribe to turn_end and run /refine on the configured cadence. */
79
136
  export function registerAutoRefine(pi: ExtensionAPI): void {
80
137
  pi.on("turn_end", async (event, ctx) => {
81
138
  const config = await loadConfig();
82
- if (!evaluateAutoRefine(config, event.turnIndex)) return;
139
+ if (!evaluateAutoRefine(config, event.outcome)) return;
83
140
  const every = config.autoRefine?.everyTurns ?? DEFAULT_AUTO_EVERY_TURNS;
84
141
 
85
142
  // Opt-in classifier gate: cheap yes/no before spending the refine.
@@ -118,7 +175,19 @@ export function registerAutoRefine(pi: ExtensionAPI): void {
118
175
  pi.sendUserMessage(result.steeringMessage);
119
176
  return;
120
177
  }
121
- return { entries: [...event.entries, buildRefineDraft(result.steeringMessage)], continue: true };
178
+ // Hygiene (pi ≥ 0.87 context edits): omit every LIVE prior auto-refine
179
+ // request from future provider context, so at most one draft (the new
180
+ // one) is ever model-visible. Already-omitted drafts are skipped by
181
+ // buildOmissionDrafts; raw history and /tree rollback are untouched.
182
+ // Kill-switch: autoRefine.omitStaleDrafts: false.
183
+ const omissions =
184
+ config.autoRefine?.omitStaleDrafts === false
185
+ ? []
186
+ : buildOmissionDrafts(event.context?.contextEntries ?? []);
187
+ return {
188
+ entries: [...event.entries, ...omissions, buildRefineDraft(result.steeringMessage)],
189
+ continue: true,
190
+ };
122
191
  }
123
192
  } catch (err) {
124
193
  ctx.ui.notify(`Auto-refine failed: ${(err as Error).message}`, "error");