pi-continual-harness 0.18.0 → 0.20.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 +57 -4
- package/dist/auto-refine.d.ts +9 -1
- package/dist/auto-refine.js +36 -3
- package/dist/config.d.ts +11 -0
- package/dist/config.js +8 -1
- package/dist/harness.js +24 -4
- package/dist/index.d.ts +5 -2
- package/dist/index.js +7 -1
- package/dist/lessons.d.ts +65 -0
- package/dist/lessons.js +182 -0
- package/dist/list.d.ts +4 -1
- package/dist/list.js +16 -1
- package/dist/outcome.js +3 -1
- package/dist/proposer.d.ts +13 -0
- package/dist/proposer.js +31 -7
- package/dist/refine.js +20 -4
- package/dist/schemas.js +9 -0
- package/dist/store.d.ts +30 -6
- package/dist/store.js +128 -7
- package/dist/tools.js +23 -2
- package/dist/types.d.ts +23 -0
- package/harness.schema.json +139 -0
- package/package.json +6 -5
package/README.md
CHANGED
|
@@ -129,6 +129,11 @@ The model-facing tools:
|
|
|
129
129
|
project slug is stamped server-side from the session cwd — used by
|
|
130
130
|
`/harness split`; normally managed via `/harness move`).
|
|
131
131
|
|
|
132
|
+
In **codemode** (pi ≥ 1.2), these tools' input constraints render as `@`
|
|
133
|
+
tags on property comments — `importance` shows `@minimum 0 @maximum 1`, the
|
|
134
|
+
batch cap shows `@maxItems 20` — because the tool schemas already declare
|
|
135
|
+
them. Scripts see the limits without the descriptions repeating them.
|
|
136
|
+
|
|
132
137
|
Active items are injected into the system prompt each turn as a structured
|
|
133
138
|
block, appended to (never replacing) the base prompt.
|
|
134
139
|
|
|
@@ -180,19 +185,27 @@ Optional config at `~/.pi/agent/harness.json` (missing → defaults; malformed
|
|
|
180
185
|
|
|
181
186
|
```json
|
|
182
187
|
{
|
|
188
|
+
"$schema": "https://raw.githubusercontent.com/pungggi/pi-continual-harness/main/harness.schema.json",
|
|
183
189
|
"autoImport": false,
|
|
184
190
|
"importRemind": { "enabled": true, "everyTurns": 25 },
|
|
185
191
|
"proposer": "steering",
|
|
186
192
|
"dedupe": { "threshold": 0.6, "merge": true },
|
|
187
193
|
"injection": { "enabled": true, "maxTokens": 1500, "maxPerKind": 10, "charsPerToken": 4 },
|
|
188
194
|
"remindRefine": { "enabled": true, "everyTurns": 25, "detailEvery": 4 },
|
|
189
|
-
"autoRefine": { "enabled": false, "everyTurns": 100, "commit": false, "gate": false },
|
|
195
|
+
"autoRefine": { "enabled": false, "everyTurns": 100, "commit": false, "gate": false, "refineNoOpSkip": 3 },
|
|
190
196
|
"outcomeImportance": { "enabled": false, "bump": 0.03 },
|
|
191
197
|
"classifier": { "model": "typesafe/jev-latest" },
|
|
192
198
|
"virtualBinding": "virtual"
|
|
193
199
|
}
|
|
194
200
|
```
|
|
195
201
|
|
|
202
|
+
**Editor validation**: add `"$schema":
|
|
203
|
+
"https://raw.githubusercontent.com/pungggi/pi-continual-harness/main/harness.schema.json"`
|
|
204
|
+
to the file for completion and validation in editors that support JSON
|
|
205
|
+
schemas (the same pattern pi ≥ 1.2 ships for its own `settings.json` /
|
|
206
|
+
`models.json`). The loader ignores unknown keys — `$schema` included — and a
|
|
207
|
+
`/harness setup` rewrite preserves it.
|
|
208
|
+
|
|
196
209
|
- **`autoImport`** — opt-in **durable sync** (off by default). `true` bundles
|
|
197
210
|
both directions: on `session_start` the durable layers are imported
|
|
198
211
|
automatically (global file always + the current project's file, with the same
|
|
@@ -258,7 +271,14 @@ Optional config at `~/.pi/agent/harness.json` (missing → defaults; malformed
|
|
|
258
271
|
also flushes durable state on each run. `gate: true` (needs `classifier.model`)
|
|
259
272
|
asks the classifier first whether the recent trajectory contains a durable,
|
|
260
273
|
reusable correction and skips the refine when it does not — cadence becomes
|
|
261
|
-
signal, not just time (errors fall back to the plain cadence).
|
|
274
|
+
signal, not just time (errors fall back to the plain cadence).
|
|
275
|
+
`refineNoOpSkip` (default 3, `0` disables) is a free **history gate** that
|
|
276
|
+
runs first: when the last N refine runs (any source) all produced nothing
|
|
277
|
+
durable, the cadenced refine is skipped as a likely repeat no-op. It is
|
|
278
|
+
**bounded**: at most one consecutive cadence is skipped on history grounds,
|
|
279
|
+
so the gate can never latch shut — later trajectory evidence always gets a
|
|
280
|
+
refine within one cadence (and manual `/refine` never consults the gate).
|
|
281
|
+
The steering
|
|
262
282
|
prompt is delivered via the pi ≥ 0.87 actionable `turn_end` boundary (a
|
|
263
283
|
structural `harness.auto-refine-request` entry + `continue: true`), not a
|
|
264
284
|
synthetic user message. Aborted/error turns (pi `turn_end`
|
|
@@ -306,6 +326,36 @@ Optional config at `~/.pi/agent/harness.json` (missing → defaults; malformed
|
|
|
306
326
|
> scope), then move any strays with `/harness move` (or classify the whole
|
|
307
327
|
> store with `/harness split`).
|
|
308
328
|
|
|
329
|
+
## Lessons and support signals (harnessing-level recursion)
|
|
330
|
+
|
|
331
|
+
Harness²'s second loop: the improvement process should learn from what past
|
|
332
|
+
improvement actually yielded, and keep a lesson only when execution evidence
|
|
333
|
+
supports it. Four pieces implement this without any extra model calls:
|
|
334
|
+
|
|
335
|
+
- **Support counters.** Every item accumulates `support` signals: `citations`
|
|
336
|
+
(the outcome loop saw the agent cite the item's `[h_xxxx]` tag), `keeps` and
|
|
337
|
+
`drops` (`/harness keep|drop`). Dedupe merges carry the counters onto the
|
|
338
|
+
keeper, and durable export/import round-trips them (`support:` sub-line), so
|
|
339
|
+
feedback survives restarts. See them with `/harness list -f`.
|
|
340
|
+
- **Lessons digest.** `/refine` steers with a compact, evidence-gated digest:
|
|
341
|
+
items proven useful (cited or kept), rejected signals (drops that strictly
|
|
342
|
+
outweigh any later use — a drop followed by re-citations is contested, not
|
|
343
|
+
rejected), and how productive recent refine runs were. Never-cited items are
|
|
344
|
+
NOT named: no signal is not a negative signal. The digest is **scoped to the
|
|
345
|
+
active model's items** — per-model isolation applies to what the improver is
|
|
346
|
+
told, not just what it is shown. `/harness lessons` prints the exact digest
|
|
347
|
+
the next `/refine` will use. Why: the improver starts from
|
|
348
|
+
what worked instead of re-deriving it, and stops re-creating rejected notes.
|
|
349
|
+
- **Scoped proposals.** The `/refine` steering prompt classifies every create
|
|
350
|
+
as **invariant** (cross-domain agentic behavior → `scope: "global"`) or
|
|
351
|
+
**domain-specific** (this project's knowledge → `scope: "project"`).
|
|
352
|
+
Harness²'s ablation shows this scoping is load-bearing; here it also keeps
|
|
353
|
+
the global layer free of project noise.
|
|
354
|
+
- **History gate.** Auto-refine skips a cadenced run when the last
|
|
355
|
+
`refineNoOpSkip` refine runs all produced nothing durable, **bounded to one
|
|
356
|
+
consecutive skip** so later evidence always gets a chance (see
|
|
357
|
+
`autoRefine` in [Configuration](#configuration)).
|
|
358
|
+
|
|
309
359
|
## How it works
|
|
310
360
|
|
|
311
361
|
1. `/refine` gathers recent trajectory evidence from the current session branch.
|
|
@@ -505,7 +555,7 @@ implementation on first `propose()`) directly into
|
|
|
505
555
|
|
|
506
556
|
## Status
|
|
507
557
|
|
|
508
|
-
0.
|
|
558
|
+
0.18.x. Requires **pi ≥ 1.2.0**. Implemented:
|
|
509
559
|
|
|
510
560
|
- Unified harness-state store with branch-local snapshots (`/tree` rollback).
|
|
511
561
|
- Online `/refine` + `harness_mutate` / `harness_list` tools.
|
|
@@ -535,7 +585,10 @@ implementation on first `propose()`) directly into
|
|
|
535
585
|
model instead of the router id, so a router selection does not silently
|
|
536
586
|
reset the harness. Owned items can be carried across models explicitly with
|
|
537
587
|
`/harness adopt --from <provider/id> [--all]` (agent-filtered copy; see
|
|
538
|
-
[Model binding](#model-binding-per-model-isolation)).
|
|
588
|
+
[Model binding](#model-binding-per-model-isolation)). Since pi 1.2,
|
|
589
|
+
`before_agent_start` also fires for runs started by `pi.sendMessage(...,
|
|
590
|
+
{ triggerTurn: true })`, so scheduled and extension-triggered runs get the
|
|
591
|
+
same binding, orphan adoption, and injection as interactive prompts.
|
|
539
592
|
- **Bounded injection (on by default)**: the supplemental block is
|
|
540
593
|
importance-ordered and capped per kind + by a total token budget, so a growing
|
|
541
594
|
harness never balloons the system prompt. Tunable / opt-out via the `injection`
|
package/dist/auto-refine.d.ts
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext, TurnEndEvent, TurnEndEventResult } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { type HarnessConfig } from "./config.js";
|
|
3
3
|
export { turnEndedBadly } from "./cadence.js";
|
|
4
|
-
/**
|
|
4
|
+
/** History gate bound (review r4341960942): the gate's predicate only reads
|
|
5
|
+
* REFINE entries on the branch, and a SKIPPED cadence adds none — without a
|
|
6
|
+
* bound, three consecutive no-op runs would latch the gate shut until a
|
|
7
|
+
* manual /refine, no matter what later turns contain. So at most ONE
|
|
8
|
+
* consecutive cadence is skipped on history grounds: the next cadence runs
|
|
9
|
+
* the refine regardless (its result re-evaluates the window afterwards),
|
|
10
|
+
* giving later trajectory evidence a bounded path in. */
|
|
11
|
+
export declare const HISTORY_GATE_MAX_CONSECUTIVE_SKIPS = 1;
|
|
12
|
+
/** Test hook: reset internal counters. */
|
|
5
13
|
export declare function resetAutoRefine(): void;
|
|
6
14
|
/**
|
|
7
15
|
* Decide whether to auto-refine this turn. Side-effectful: the first observed
|
package/dist/auto-refine.js
CHANGED
|
@@ -34,18 +34,31 @@
|
|
|
34
34
|
// It stays visible: it notifies before firing, and the boundary draft (and the
|
|
35
35
|
// gate decision) appear in the transcript. The decision logic is factored into
|
|
36
36
|
// evaluateAutoRefine() so it is unit-testable without a live pi runtime.
|
|
37
|
-
import { DEFAULT_AUTO_EVERY_TURNS, loadConfig } from "./config.js";
|
|
37
|
+
import { DEFAULT_AUTO_EVERY_TURNS, DEFAULT_REFINE_NOOP_SKIP, loadConfig } from "./config.js";
|
|
38
38
|
import { runRefine, gatherEvidence, DEFAULT_LOOKBACK_TURNS } from "./refine.js";
|
|
39
39
|
import { buildClassify, buildGateRequest, gateDecision } from "./classify.js";
|
|
40
|
+
import { recentIneffectiveRuns } from "./lessons.js";
|
|
40
41
|
import { makeTurnCadence } from "./cadence.js";
|
|
41
42
|
// turnEndedBadly's canonical home is cadence.ts (shared completed-turn
|
|
42
43
|
// semantics); re-exported here so existing imports keep working.
|
|
43
44
|
export { turnEndedBadly } from "./cadence.js";
|
|
44
45
|
// The auto-refine cadence counter (shared implementation — see cadence.ts).
|
|
45
46
|
const cadence = makeTurnCadence();
|
|
46
|
-
/**
|
|
47
|
+
/** History gate bound (review r4341960942): the gate's predicate only reads
|
|
48
|
+
* REFINE entries on the branch, and a SKIPPED cadence adds none — without a
|
|
49
|
+
* bound, three consecutive no-op runs would latch the gate shut until a
|
|
50
|
+
* manual /refine, no matter what later turns contain. So at most ONE
|
|
51
|
+
* consecutive cadence is skipped on history grounds: the next cadence runs
|
|
52
|
+
* the refine regardless (its result re-evaluates the window afterwards),
|
|
53
|
+
* giving later trajectory evidence a bounded path in. */
|
|
54
|
+
export const HISTORY_GATE_MAX_CONSECUTIVE_SKIPS = 1;
|
|
55
|
+
/** Consecutive cadences the history gate has skipped this session (reset when
|
|
56
|
+
* a refine actually runs, by resetAutoRefine, and at session_start). */
|
|
57
|
+
let historyGateSkips = 0;
|
|
58
|
+
/** Test hook: reset internal counters. */
|
|
47
59
|
export function resetAutoRefine() {
|
|
48
60
|
cadence.reset();
|
|
61
|
+
historyGateSkips = 0;
|
|
49
62
|
}
|
|
50
63
|
/**
|
|
51
64
|
* Decide whether to auto-refine this turn. Side-effectful: the first observed
|
|
@@ -97,6 +110,19 @@ export async function handleAutoRefineTurnEnd(pi, event, ctx) {
|
|
|
97
110
|
if (!evaluateAutoRefine(config, event.outcome))
|
|
98
111
|
return;
|
|
99
112
|
const every = config.autoRefine?.everyTurns ?? DEFAULT_AUTO_EVERY_TURNS;
|
|
113
|
+
// History gate (harnessing-level recursion): free, runs before the classifier
|
|
114
|
+
// gate — when the last N refine runs were ALL ineffective, a cadenced refine
|
|
115
|
+
// over the same quiet trajectory would most likely be another no-op. Manual
|
|
116
|
+
// /refine never consults this gate. BOUNDED: at most
|
|
117
|
+
// HISTORY_GATE_MAX_CONSECUTIVE_SKIPS consecutive cadence is skipped, so the
|
|
118
|
+
// gate can never latch shut against later, richer trajectory evidence
|
|
119
|
+
// (review r4341960942) — the run after a skip always happens.
|
|
120
|
+
const noOpSkip = config.autoRefine?.refineNoOpSkip ?? DEFAULT_REFINE_NOOP_SKIP;
|
|
121
|
+
if (historyGateSkips < HISTORY_GATE_MAX_CONSECUTIVE_SKIPS && recentIneffectiveRuns(ctx.sessionManager.getBranch(), noOpSkip)) {
|
|
122
|
+
historyGateSkips += 1;
|
|
123
|
+
ctx.ui.notify(`Auto-refine: skipped — the last ${noOpSkip} refine run(s) produced nothing durable (history gate, ${historyGateSkips}/${HISTORY_GATE_MAX_CONSECUTIVE_SKIPS} consecutive skip; the next cadence runs regardless; refineNoOpSkip: 0 disables).`, "info");
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
100
126
|
// Opt-in classifier gate: cheap yes/no before spending the refine.
|
|
101
127
|
if (config.autoRefine?.gate) {
|
|
102
128
|
const classify = buildClassify(ctx, config.classifier?.model);
|
|
@@ -113,6 +139,9 @@ export async function handleAutoRefineTurnEnd(pi, event, ctx) {
|
|
|
113
139
|
// No resolvable classifier → plain cadence behavior (documented soft-fail).
|
|
114
140
|
}
|
|
115
141
|
ctx.ui.notify(`Auto-refine: running /refine (every ${every} turns, opt-in).`, "info");
|
|
142
|
+
// A refine runs this cadence → the history gate's consecutive-skip budget
|
|
143
|
+
// is spent again (window re-evaluated from scratch next cadence).
|
|
144
|
+
historyGateSkips = 0;
|
|
116
145
|
try {
|
|
117
146
|
const result = await runRefine(pi, ctx, {
|
|
118
147
|
commit: config.autoRefine?.commit ?? false,
|
|
@@ -124,7 +153,11 @@ export async function handleAutoRefineTurnEnd(pi, event, ctx) {
|
|
|
124
153
|
// continue — otherwise fall back to the legacy steering send.
|
|
125
154
|
if (result.steeringMessage) {
|
|
126
155
|
if (event.context?.canContinue === false) {
|
|
127
|
-
|
|
156
|
+
// deliverAs "followUp": queue after the in-flight run instead of
|
|
157
|
+
// throwing — pi 1.2 counts the session as busy from run start, so a
|
|
158
|
+
// bare sendUserMessage can fail here when another extension is
|
|
159
|
+
// starting a run at this turn_end.
|
|
160
|
+
pi.sendUserMessage(result.steeringMessage, { deliverAs: "followUp" });
|
|
128
161
|
return;
|
|
129
162
|
}
|
|
130
163
|
// Hygiene (pi ≥ 0.87 context edits): omit every LIVE prior auto-refine
|
package/dist/config.d.ts
CHANGED
|
@@ -4,6 +4,10 @@ export declare const DEFAULT_EVERY_TURNS = 25;
|
|
|
4
4
|
export declare const DEFAULT_AUTO_EVERY_TURNS = 100;
|
|
5
5
|
/** Default per-reference importance bump for the opt-in outcome loop. */
|
|
6
6
|
export declare const DEFAULT_REF_BUMP = 0.03;
|
|
7
|
+
/** History-gate default: a cadenced auto-refine is skipped when the last N
|
|
8
|
+
* refine runs (any source) were ALL ineffective — no deltas applied and no
|
|
9
|
+
* state mutation followed. 0 disables the gate (autoRefine.refineNoOpSkip). */
|
|
10
|
+
export declare const DEFAULT_REFINE_NOOP_SKIP = 3;
|
|
7
11
|
/** Default cadence (turns) for the auto-import awareness reminder. */
|
|
8
12
|
export declare const DEFAULT_IMPORT_REMIND_TURNS = 25;
|
|
9
13
|
/** Default escalation cadence for the /refine reminder: every Nth fired
|
|
@@ -59,6 +63,13 @@ export interface HarnessConfig {
|
|
|
59
63
|
* one (the newest) is ever model-visible. Raw history and /tree rollback
|
|
60
64
|
* are untouched; set false to keep every draft in context. */
|
|
61
65
|
omitStaleDrafts?: boolean;
|
|
66
|
+
/** History gate (harnessing-level recursion): skip a cadenced refine when
|
|
67
|
+
* the last N refine runs (any source) were ALL ineffective — no deltas
|
|
68
|
+
* applied and no state mutation followed. Refines over an unchanged quiet
|
|
69
|
+
* trajectory would most likely be no-ops again; the classifier gate (if
|
|
70
|
+
* enabled) then never even runs. Manual /refine bypasses this. Default
|
|
71
|
+
* DEFAULT_REFINE_NOOP_SKIP (3); 0 disables the history gate. */
|
|
72
|
+
refineNoOpSkip?: number;
|
|
62
73
|
};
|
|
63
74
|
/** Classifier model for the cheap gated paths (pi ≥ 0.99): "provider/id" or
|
|
64
75
|
* a bare id of a classifier catalog entry (e.g. "typesafe/jev-latest" or a
|
package/dist/config.js
CHANGED
|
@@ -24,6 +24,10 @@ export const DEFAULT_EVERY_TURNS = 25;
|
|
|
24
24
|
export const DEFAULT_AUTO_EVERY_TURNS = 100;
|
|
25
25
|
/** Default per-reference importance bump for the opt-in outcome loop. */
|
|
26
26
|
export const DEFAULT_REF_BUMP = 0.03;
|
|
27
|
+
/** History-gate default: a cadenced auto-refine is skipped when the last N
|
|
28
|
+
* refine runs (any source) were ALL ineffective — no deltas applied and no
|
|
29
|
+
* state mutation followed. 0 disables the gate (autoRefine.refineNoOpSkip). */
|
|
30
|
+
export const DEFAULT_REFINE_NOOP_SKIP = 3;
|
|
27
31
|
/** Default cadence (turns) for the auto-import awareness reminder. */
|
|
28
32
|
export const DEFAULT_IMPORT_REMIND_TURNS = 25;
|
|
29
33
|
/** Default escalation cadence for the /refine reminder: every Nth fired
|
|
@@ -33,7 +37,7 @@ export const DEFAULT_CONFIG = {
|
|
|
33
37
|
autoImport: false,
|
|
34
38
|
importRemind: { enabled: true, everyTurns: DEFAULT_IMPORT_REMIND_TURNS },
|
|
35
39
|
remindRefine: { enabled: true, everyTurns: DEFAULT_EVERY_TURNS, detailEvery: DEFAULT_DETAIL_EVERY },
|
|
36
|
-
autoRefine: { enabled: false, everyTurns: DEFAULT_AUTO_EVERY_TURNS, commit: false, gate: false, omitStaleDrafts: true },
|
|
40
|
+
autoRefine: { enabled: false, everyTurns: DEFAULT_AUTO_EVERY_TURNS, commit: false, gate: false, omitStaleDrafts: true, refineNoOpSkip: DEFAULT_REFINE_NOOP_SKIP },
|
|
37
41
|
classifier: { model: undefined },
|
|
38
42
|
virtualBinding: "virtual",
|
|
39
43
|
proposer: "steering",
|
|
@@ -75,6 +79,9 @@ function mergeConfig(over) {
|
|
|
75
79
|
commit: over.autoRefine?.commit ?? false,
|
|
76
80
|
gate: over.autoRefine?.gate === true,
|
|
77
81
|
omitStaleDrafts: over.autoRefine?.omitStaleDrafts !== false,
|
|
82
|
+
refineNoOpSkip: typeof over.autoRefine?.refineNoOpSkip === "number" && Number.isFinite(over.autoRefine.refineNoOpSkip)
|
|
83
|
+
? Math.max(0, Math.floor(over.autoRefine.refineNoOpSkip))
|
|
84
|
+
: DEFAULT_REFINE_NOOP_SKIP,
|
|
78
85
|
},
|
|
79
86
|
classifier: {
|
|
80
87
|
model: typeof over.classifier?.model === "string" && over.classifier.model ? over.classifier.model : undefined,
|
package/dist/harness.js
CHANGED
|
@@ -40,6 +40,7 @@ import { buildStatusReport, renderStatusReport } from "./status.js";
|
|
|
40
40
|
import { getInjectionStats } from "./inject.js";
|
|
41
41
|
import { nextHint } from "./hints.js";
|
|
42
42
|
import { preview, renderListReport } from "./list.js";
|
|
43
|
+
import { buildLessonsDigest } from "./lessons.js";
|
|
43
44
|
import { handleSetup, PRESET_COMPLETIONS } from "./setup.js";
|
|
44
45
|
const SUBCOMMANDS = [
|
|
45
46
|
{ name: "import", description: "Import durable state, layered (--prune to prune stale items)" },
|
|
@@ -55,6 +56,7 @@ const SUBCOMMANDS = [
|
|
|
55
56
|
{ name: "adopt", description: "Steer the agent to adopt another model's items (--from <provider/id> [--all])" },
|
|
56
57
|
{ name: "push-mem", description: "Persist active items to pi-mem (--all, --kind, --model)" },
|
|
57
58
|
{ name: "setup", description: "Guided config wizard — presets: quiet | recommended | power (--yes for scripted)" },
|
|
59
|
+
{ name: "lessons", description: "Evidence-gated lessons from past refines (the digest /refine steers with)" },
|
|
58
60
|
];
|
|
59
61
|
const KINDS = ["prompt", "memory", "skill", "subagent"];
|
|
60
62
|
/** `--scope` values /harness list offers (semantics in src/list.ts). */
|
|
@@ -243,6 +245,9 @@ export async function runHarnessCommand(pi, args, ctx) {
|
|
|
243
245
|
case "setup":
|
|
244
246
|
await handleSetup(ctx, rest);
|
|
245
247
|
return;
|
|
248
|
+
case "lessons":
|
|
249
|
+
handleLessons(ctx);
|
|
250
|
+
return;
|
|
246
251
|
case "list":
|
|
247
252
|
await handleList(ctx, rest);
|
|
248
253
|
return;
|
|
@@ -442,7 +447,9 @@ async function handleBump(pi, ctx, rest, delta, label) {
|
|
|
442
447
|
}
|
|
443
448
|
const item = bumpImportance(id, delta, (snapshot, ver) => {
|
|
444
449
|
pi.appendEntry("harness-state", { state: snapshot, version: ver });
|
|
445
|
-
}
|
|
450
|
+
},
|
|
451
|
+
// Record the human signal in the support counters (keep/drop).
|
|
452
|
+
{ support: label === "keep" ? "keep" : "drop" });
|
|
446
453
|
if (!item) {
|
|
447
454
|
ctx.ui.notify(`No harness item with id ${id}.`, "warning");
|
|
448
455
|
return;
|
|
@@ -519,7 +526,9 @@ async function handleSplit(pi, ctx) {
|
|
|
519
526
|
ctx.ui.notify("No active items to split (run /refine first).", "warning");
|
|
520
527
|
return;
|
|
521
528
|
}
|
|
522
|
-
pi
|
|
529
|
+
// deliverAs "followUp": queue behind any in-flight run (pi ≥ 1.2 busy window
|
|
530
|
+
// starts at run start; a bare send would throw) rather than steering mid-task.
|
|
531
|
+
pi.sendUserMessage(buildSplitPrompt(items, projectSlug(ctx.cwd), ctx.cwd), { deliverAs: "followUp" });
|
|
523
532
|
ctx.ui.notify(`Steering agent to classify ${items.length} item(s) global vs project "${projectSlug(ctx.cwd)}".`, "info");
|
|
524
533
|
}
|
|
525
534
|
/** Active items grouped by owning model key ("provider/id" → count). The
|
|
@@ -640,7 +649,7 @@ async function handleAdopt(pi, ctx, rest) {
|
|
|
640
649
|
if (foreign > 0)
|
|
641
650
|
bits.push(`${foreign} foreign-project skipped`);
|
|
642
651
|
const suffix = bits.length > 0 ? ` (${bits.join("; ")})` : "";
|
|
643
|
-
pi.sendUserMessage(buildAdoptPrompt(items, from, { all: rest.includes("--all") }));
|
|
652
|
+
pi.sendUserMessage(buildAdoptPrompt(items, from, { all: rest.includes("--all") }), { deliverAs: "followUp" });
|
|
644
653
|
ctx.ui.notify(`Steering agent to ${rest.includes("--all") ? "adopt" : "review for adoption"} ${items.length} item(s) from ${from}${suffix}.`, "info");
|
|
645
654
|
}
|
|
646
655
|
/** Compose a steering message that asks the agent to persist the given active
|
|
@@ -691,9 +700,20 @@ async function handlePushMem(pi, ctx, rest) {
|
|
|
691
700
|
const scope = all ? "item(s)" : `${kind ?? "memory"} item(s)`;
|
|
692
701
|
const modelNote = modelFilter !== undefined ? ` [model ${modelFilter || "(orphan)"}]` : "";
|
|
693
702
|
const msg = buildPushMemPrompt(items, scope);
|
|
694
|
-
pi.sendUserMessage(msg);
|
|
703
|
+
pi.sendUserMessage(msg, { deliverAs: "followUp" });
|
|
695
704
|
ctx.ui.notify(`Steering agent to persist ${items.length} ${scope}${modelNote} to pi-mem.`, "info");
|
|
696
705
|
}
|
|
706
|
+
/** `/harness lessons` — print the exact evidence-gated digest the next /refine
|
|
707
|
+
* steers with (harnessing-level recursion, Harness² arXiv 2610.14224).
|
|
708
|
+
* Read-only; distinct empty state when nothing supportable exists yet. */
|
|
709
|
+
function handleLessons(ctx) {
|
|
710
|
+
const digest = buildLessonsDigest(getState(), ctx.sessionManager.getBranch());
|
|
711
|
+
if (!digest) {
|
|
712
|
+
ctx.ui.notify("No lessons yet — items need citations (outcome loop) or /harness keep|drop feedback, or past refine runs, before anything is evidence-gated.", "info");
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
ctx.ui.notify(digest, "info");
|
|
716
|
+
}
|
|
697
717
|
async function handleStatus(ctx) {
|
|
698
718
|
// Thin glue (issue #20): config + durable-layer stat() I/O here; every
|
|
699
719
|
// derived band lives in the pure buildStatusReport (src/status.ts).
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
export { default } from "./entry.js";
|
|
2
2
|
export { STATE_ENTRY } from "./store.js";
|
|
3
|
-
export { type AppliedDelta, type ComponentKind, type Delta, type HarnessItem, type HarnessState, } from "./types.js";
|
|
4
|
-
export { DEFAULT_DEDUPE, DEDUPE_THRESHOLD, EVIDENCE_MERGE_CAP, listProposers, planDedupe, registerProposer, unionEvidence, type CompleteOptions, type CompleteResult, type DedupeOptions, type DeltaProposer, type ModelCallTelemetry, type ProposeInput, type ProposedDelta, type ProposeResult, type SimilarityResult, } from "./proposer.js";
|
|
3
|
+
export { type AppliedDelta, type ComponentKind, type Delta, type HarnessItem, type HarnessState, type SupportInfo, } from "./types.js";
|
|
4
|
+
export { DEFAULT_DEDUPE, DEDUPE_THRESHOLD, EVIDENCE_MERGE_CAP, buildSteeringPrompt, listProposers, planDedupe, registerProposer, unionEvidence, type CompleteOptions, type CompleteResult, type DedupeOptions, type DeltaProposer, type ModelCallTelemetry, type ProposeInput, type ProposedDelta, type ProposeResult, type SimilarityResult, } from "./proposer.js";
|
|
5
|
+
export { buildLessonsDigest, classifyRuns, isEffectiveRun, recentIneffectiveRuns, type DigestOptions, type RefineRun, } from "./lessons.js";
|
|
6
|
+
export { mergeSupport, renderSupport, parseSupport } from "./store.js";
|
|
7
|
+
export { DEFAULT_REFINE_NOOP_SKIP } from "./config.js";
|
|
5
8
|
export { buildCorpus, classifyTransition, NOT_DUP_FLOOR, parseDedupeDelete, type CorpusResult, type DedupePairRecord, type LifecycleEvent, type LifecycleRecord, type LifecycleTransition, } from "./corpus.js";
|
|
6
9
|
export { buildStatusReport, renderStatusReport, type ImpactCounts, type StatusOptions, type StatusReport, } from "./status.js";
|
|
7
10
|
export { nextHint, pickHint, resetHints, type StatusHint, } from "./hints.js";
|
package/dist/index.js
CHANGED
|
@@ -20,7 +20,13 @@ export {} from "./types.js";
|
|
|
20
20
|
// Public extension API: other extensions can register their own delta proposer
|
|
21
21
|
// (see src/proposer.ts) and it becomes selectable via /refine --proposer <name>
|
|
22
22
|
// or the `proposer` config key.
|
|
23
|
-
export { DEFAULT_DEDUPE, DEDUPE_THRESHOLD, EVIDENCE_MERGE_CAP, listProposers, planDedupe, registerProposer, unionEvidence, } from "./proposer.js";
|
|
23
|
+
export { DEFAULT_DEDUPE, DEDUPE_THRESHOLD, EVIDENCE_MERGE_CAP, buildSteeringPrompt, listProposers, planDedupe, registerProposer, unionEvidence, } from "./proposer.js";
|
|
24
|
+
// Harnessing-level recursion (Harness², arXiv 2610.14224): evidence-gated
|
|
25
|
+
// lessons from past refines + the auto-refine history gate. Re-exported for
|
|
26
|
+
// tests and companion packages.
|
|
27
|
+
export { buildLessonsDigest, classifyRuns, isEffectiveRun, recentIneffectiveRuns, } from "./lessons.js";
|
|
28
|
+
export { mergeSupport, renderSupport, parseSupport } from "./store.js";
|
|
29
|
+
export { DEFAULT_REFINE_NOOP_SKIP } from "./config.js";
|
|
24
30
|
// Calibration corpus exporter (pi-reflex contract §4, formerly pi-jev). Re-exported so companion
|
|
25
31
|
// packages can build corpora from exported session branches without reaching
|
|
26
32
|
// into internal paths.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { HarnessState } from "./types.js";
|
|
2
|
+
/** What a harness-refinement entry records (subset we consume). */
|
|
3
|
+
export interface RefineRun {
|
|
4
|
+
/** Manual /refine vs. cadenced auto-refine. */
|
|
5
|
+
source?: string | undefined;
|
|
6
|
+
/** Which proposer ran. */
|
|
7
|
+
proposer?: string | undefined;
|
|
8
|
+
/** Deltas applied DIRECTLY by the proposer (0 for the steering path at
|
|
9
|
+
* entry-creation time — effectiveness is then inferred from a following
|
|
10
|
+
* harness-state entry, see classifyRuns). */
|
|
11
|
+
applied?: number | undefined;
|
|
12
|
+
/** ms epoch (entry data startedAt). */
|
|
13
|
+
at?: number | undefined;
|
|
14
|
+
/** True when a harness-state mutation landed AFTER this refine entry and
|
|
15
|
+
* before the next one — the steering agent actually mutated. */
|
|
16
|
+
followedByStateChange?: boolean | undefined;
|
|
17
|
+
}
|
|
18
|
+
/** Annotate runs with followedByStateChange by co-walking state entries.
|
|
19
|
+
*
|
|
20
|
+
* Attribution subtlety (review r4341960980): runRefine writes a direct-apply
|
|
21
|
+
* run's harness-state snapshot BEFORE that run's own audit entry, so a state
|
|
22
|
+
* entry sitting between run k and run k+1 may belong to EITHER. Disambiguate
|
|
23
|
+
* by ownership: if run k+1 direct-applied (applied > 0), exactly one of those
|
|
24
|
+
* state entries is its own snapshot and must NOT be credited to run k.
|
|
25
|
+
* Leftover entries credit run k (its steering mutation — or a human keep/drop
|
|
26
|
+
* in between, an acceptable false credit: it only ever LIFTS the gate, never
|
|
27
|
+
* makes it skip). */
|
|
28
|
+
export declare function classifyRuns(entries: Iterable<unknown>): RefineRun[];
|
|
29
|
+
/** A run is EFFECTIVE when the proposer applied deltas directly, or the
|
|
30
|
+
* steering agent mutated state right after. No-ops are everything else. */
|
|
31
|
+
export declare function isEffectiveRun(r: RefineRun): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* History gate (auto-refine): true when the LAST `n` refine runs (fewer if
|
|
34
|
+
* the branch holds fewer) were ALL ineffective — a cadenced refine over the
|
|
35
|
+
* same quiet trajectory would most likely be another no-op. n <= 0 always
|
|
36
|
+
* returns false (gate off).
|
|
37
|
+
*/
|
|
38
|
+
export declare function recentIneffectiveRuns(entries: Iterable<unknown>, n: number): boolean;
|
|
39
|
+
export interface DigestOptions {
|
|
40
|
+
/** Max supported items listed (default 5). */
|
|
41
|
+
maxSupported?: number;
|
|
42
|
+
/** Max struggling items listed (default 3). */
|
|
43
|
+
maxStruggling?: number;
|
|
44
|
+
/** Refine runs summarized (default 6). */
|
|
45
|
+
runsWindow?: number;
|
|
46
|
+
/** Hard character cap on the whole digest (default 1200). */
|
|
47
|
+
maxChars?: number;
|
|
48
|
+
/** Model-binding key (see vmodel.resolveBindingKey): only items OWNED by
|
|
49
|
+
* this model are named (review r4341960955) — the digest must respect the
|
|
50
|
+
* same per-model isolation as injection, otherwise a fresh model is fed
|
|
51
|
+
* foreign notes and told to delete ids it cannot mutate
|
|
52
|
+
* (assertOwnsItem rejects them). Undefined = no owner filter (callers that
|
|
53
|
+
* know the actor — runRefine — always pass it, `""` for an unknown model). */
|
|
54
|
+
ownerKey?: string | undefined;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Build the lessons digest injected into the refine steering prompt. Every
|
|
58
|
+
* line is evidence-gated (Harness²'s retention rule): supported items must
|
|
59
|
+
* have organic citations or keeps, struggling items must show explicit
|
|
60
|
+
* negative signal (drops) — never-cited-but-never-dropped items are NOT
|
|
61
|
+
* named (no signal is not a negative signal). Returns "" when there is
|
|
62
|
+
* nothing supportable to say, so the prompt stays unchanged for fresh stores.
|
|
63
|
+
* Pure.
|
|
64
|
+
*/
|
|
65
|
+
export declare function buildLessonsDigest(state: HarnessState, entries: Iterable<unknown>, opts?: DigestOptions): string;
|
package/dist/lessons.js
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// lessons.ts — harnessing-level recursion (Harness², arXiv 2610.14224 §2.3).
|
|
2
|
+
//
|
|
3
|
+
// Harness² separates two loops: task-level recursion improves the harness for
|
|
4
|
+
// the CURRENT task, while harnessing-level recursion lets the IMPROVEMENT
|
|
5
|
+
// PROCESS ITSELF learn from what past refinements revealed. Its library rule:
|
|
6
|
+
// "lessons are retained only when supported by execution contrasts" — never
|
|
7
|
+
// from the mere fact that an edit was proposed.
|
|
8
|
+
//
|
|
9
|
+
// This module is our online-sized take on that rule, built entirely from
|
|
10
|
+
// signals we already persist:
|
|
11
|
+
// - item support counters (citations from the outcome loop, keeps/drops from
|
|
12
|
+
// /harness keep|drop) — the "execution contrast" proxy: adoption followed
|
|
13
|
+
// by observed use (or by the human explicitly rejecting it);
|
|
14
|
+
// - harness-refinement audit entries on the branch — which refine runs
|
|
15
|
+
// produced durable deltas vs. no-ops.
|
|
16
|
+
//
|
|
17
|
+
// Two consumers:
|
|
18
|
+
// - buildLessonsDigest() → injected into the /refine steering prompt (and any
|
|
19
|
+
// other proposer via ProposeInput.lessons), so the improver starts from
|
|
20
|
+
// what past improvement actually yielded.
|
|
21
|
+
// - recentIneffectiveRuns() → the history gate for auto-refine (skip a
|
|
22
|
+
// cadenced refine when the last N runs were all no-ops).
|
|
23
|
+
//
|
|
24
|
+
// Everything here is PURE over (state, branch entries): no I/O, no config,
|
|
25
|
+
// fully unit-testable, and silent when there is nothing supportable to say.
|
|
26
|
+
import { preview } from "./list.js";
|
|
27
|
+
import { REFINE_ENTRY, STATE_ENTRY } from "./store.js";
|
|
28
|
+
/** One-line, whitespace-flattened preview (shared style with list rows). */
|
|
29
|
+
function flat(text, max) {
|
|
30
|
+
return preview(text, max);
|
|
31
|
+
}
|
|
32
|
+
/** Annotate runs with followedByStateChange by co-walking state entries.
|
|
33
|
+
*
|
|
34
|
+
* Attribution subtlety (review r4341960980): runRefine writes a direct-apply
|
|
35
|
+
* run's harness-state snapshot BEFORE that run's own audit entry, so a state
|
|
36
|
+
* entry sitting between run k and run k+1 may belong to EITHER. Disambiguate
|
|
37
|
+
* by ownership: if run k+1 direct-applied (applied > 0), exactly one of those
|
|
38
|
+
* state entries is its own snapshot and must NOT be credited to run k.
|
|
39
|
+
* Leftover entries credit run k (its steering mutation — or a human keep/drop
|
|
40
|
+
* in between, an acceptable false credit: it only ever LIFTS the gate, never
|
|
41
|
+
* makes it skip). */
|
|
42
|
+
export function classifyRuns(entries) {
|
|
43
|
+
const runs = [];
|
|
44
|
+
let pending;
|
|
45
|
+
// harness-state entries seen since `pending`'s audit entry.
|
|
46
|
+
let stateAfter = 0;
|
|
47
|
+
const flush = (nextApplied) => {
|
|
48
|
+
if (pending) {
|
|
49
|
+
const ownedByNext = (nextApplied ?? 0) > 0 ? 1 : 0;
|
|
50
|
+
pending.followedByStateChange = stateAfter - ownedByNext > 0;
|
|
51
|
+
runs.push(pending);
|
|
52
|
+
}
|
|
53
|
+
pending = undefined;
|
|
54
|
+
stateAfter = 0;
|
|
55
|
+
};
|
|
56
|
+
for (const raw of entries) {
|
|
57
|
+
if (!raw || typeof raw !== "object")
|
|
58
|
+
continue;
|
|
59
|
+
const e = raw;
|
|
60
|
+
if (e.type === "custom" && e.customType === STATE_ENTRY) {
|
|
61
|
+
if (pending)
|
|
62
|
+
stateAfter += 1;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (e.type === "custom" && e.customType === REFINE_ENTRY) {
|
|
66
|
+
const next = {
|
|
67
|
+
source: e.data?.source,
|
|
68
|
+
proposer: e.data?.proposer,
|
|
69
|
+
applied: e.data?.applied,
|
|
70
|
+
at: e.data?.startedAt,
|
|
71
|
+
};
|
|
72
|
+
// Flush the previous run knowing whether the new one direct-applied.
|
|
73
|
+
flush(next.applied);
|
|
74
|
+
pending = next;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
flush(); // last run: any state entry after its audit is attributable to it
|
|
78
|
+
return runs;
|
|
79
|
+
}
|
|
80
|
+
/** A run is EFFECTIVE when the proposer applied deltas directly, or the
|
|
81
|
+
* steering agent mutated state right after. No-ops are everything else. */
|
|
82
|
+
export function isEffectiveRun(r) {
|
|
83
|
+
return (r.applied ?? 0) > 0 || r.followedByStateChange === true;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* History gate (auto-refine): true when the LAST `n` refine runs (fewer if
|
|
87
|
+
* the branch holds fewer) were ALL ineffective — a cadenced refine over the
|
|
88
|
+
* same quiet trajectory would most likely be another no-op. n <= 0 always
|
|
89
|
+
* returns false (gate off).
|
|
90
|
+
*/
|
|
91
|
+
export function recentIneffectiveRuns(entries, n) {
|
|
92
|
+
if (n <= 0)
|
|
93
|
+
return false;
|
|
94
|
+
const runs = classifyRuns(entries);
|
|
95
|
+
if (runs.length === 0)
|
|
96
|
+
return false;
|
|
97
|
+
const last = runs.slice(-n);
|
|
98
|
+
return last.length === n && last.every((r) => !isEffectiveRun(r));
|
|
99
|
+
}
|
|
100
|
+
// ---- the digest -----------------------------------------------------------
|
|
101
|
+
/** Positive support score: citations weigh double keeps (organic use is a
|
|
102
|
+
* stronger contrast than a human nudge), drops are negative. */
|
|
103
|
+
function supportScore(i) {
|
|
104
|
+
const s = i.support;
|
|
105
|
+
if (!s)
|
|
106
|
+
return 0;
|
|
107
|
+
return 2 * (s.citations ?? 0) + (s.keeps ?? 0) - 2 * (s.drops ?? 0);
|
|
108
|
+
}
|
|
109
|
+
/** Cap an assembled digest: drop trailing sections until it fits. */
|
|
110
|
+
function cap(sections, maxChars) {
|
|
111
|
+
const out = [];
|
|
112
|
+
let total = 0;
|
|
113
|
+
for (const s of sections) {
|
|
114
|
+
if (total + s.length > maxChars && out.length > 0)
|
|
115
|
+
break;
|
|
116
|
+
out.push(s);
|
|
117
|
+
total += s.length;
|
|
118
|
+
}
|
|
119
|
+
return out.join("\n");
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Build the lessons digest injected into the refine steering prompt. Every
|
|
123
|
+
* line is evidence-gated (Harness²'s retention rule): supported items must
|
|
124
|
+
* have organic citations or keeps, struggling items must show explicit
|
|
125
|
+
* negative signal (drops) — never-cited-but-never-dropped items are NOT
|
|
126
|
+
* named (no signal is not a negative signal). Returns "" when there is
|
|
127
|
+
* nothing supportable to say, so the prompt stays unchanged for fresh stores.
|
|
128
|
+
* Pure.
|
|
129
|
+
*/
|
|
130
|
+
export function buildLessonsDigest(state, entries, opts = {}) {
|
|
131
|
+
const sections = [];
|
|
132
|
+
const active = state.items.filter((i) => i.active && (opts.ownerKey === undefined || i.ownerModel === opts.ownerKey));
|
|
133
|
+
// 1. Supported — proven useful since adoption.
|
|
134
|
+
const supported = active
|
|
135
|
+
.filter((i) => supportScore(i) > 0)
|
|
136
|
+
.sort((a, b) => supportScore(b) - supportScore(a))
|
|
137
|
+
.slice(0, opts.maxSupported ?? 5);
|
|
138
|
+
if (supported.length > 0) {
|
|
139
|
+
sections.push([
|
|
140
|
+
`Proven useful (do not duplicate; strengthen only with new evidence):`,
|
|
141
|
+
...supported.map((i) => {
|
|
142
|
+
const s = i.support;
|
|
143
|
+
const bits = [
|
|
144
|
+
s.citations ? `cited ${s.citations}×` : "",
|
|
145
|
+
s.keeps ? `kept ${s.keeps}×` : "",
|
|
146
|
+
].filter(Boolean);
|
|
147
|
+
return `- [${i.id}] (${bits.join(", ")}): ${flat(i.content, 90)}`;
|
|
148
|
+
}),
|
|
149
|
+
].join("\n"));
|
|
150
|
+
}
|
|
151
|
+
// 2. Struggling — explicit NEGATIVE net signal only (drops strictly
|
|
152
|
+
// outweigh any later use). An item dropped once and then cited again
|
|
153
|
+
// (score 0) or cited more than dropped (score > 0) is contested, not
|
|
154
|
+
// rejected — later execution evidence outranks the drop (review
|
|
155
|
+
// r4341960962). Net-neutral items are never named (no signal ≠ negative).
|
|
156
|
+
const struggling = active
|
|
157
|
+
.filter((i) => (i.support?.drops ?? 0) > 0 && supportScore(i) < 0)
|
|
158
|
+
.sort((a, b) => (b.support.drops ?? 0) - (a.support.drops ?? 0))
|
|
159
|
+
.slice(0, opts.maxStruggling ?? 3);
|
|
160
|
+
if (struggling.length > 0) {
|
|
161
|
+
sections.push([
|
|
162
|
+
`Rejected signals (candidates to delete or rewrite — past drops, no use since):`,
|
|
163
|
+
...struggling.map((i) => `- [${i.id}] (dropped ${i.support.drops}×): ${flat(i.content, 90)}`),
|
|
164
|
+
].join("\n"));
|
|
165
|
+
}
|
|
166
|
+
// 3. Refine-run history — how productive past improvement was.
|
|
167
|
+
const runs = classifyRuns(entries);
|
|
168
|
+
const window = opts.runsWindow ?? 6;
|
|
169
|
+
const recent = runs.slice(-window);
|
|
170
|
+
if (recent.length > 0) {
|
|
171
|
+
const effective = recent.filter(isEffectiveRun).length;
|
|
172
|
+
const noOps = recent.length - effective;
|
|
173
|
+
const line = noOps === 0
|
|
174
|
+
? `Refine history: all ${recent.length} recent run(s) produced durable deltas — keep proposing surgical, evidence-backed edits.`
|
|
175
|
+
: `Refine history: ${noOps} of the last ${recent.length} run(s) proposed nothing durable — only record a correction when the trajectory clearly supports it; say so and do nothing otherwise.`;
|
|
176
|
+
sections.push(line);
|
|
177
|
+
}
|
|
178
|
+
if (sections.length === 0)
|
|
179
|
+
return "";
|
|
180
|
+
const header = "## Lessons from past refines (evidence-gated — apply when relevant)";
|
|
181
|
+
return cap([header, ...sections], opts.maxChars ?? 1200);
|
|
182
|
+
}
|
package/dist/list.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ComponentKind, HarnessItem } from "./types.js";
|
|
1
|
+
import type { ComponentKind, HarnessItem, SupportInfo } from "./types.js";
|
|
2
2
|
/** `--scope` values for /harness list. `undefined` (no flag) = the session's
|
|
3
3
|
* layer set: global + the active project. */
|
|
4
4
|
export type ListScope = "global" | "project" | "all";
|
|
@@ -21,6 +21,9 @@ export declare function preview(text: string, max?: number): string;
|
|
|
21
21
|
* desc with ties keeping store order — the same stable ranking policy as
|
|
22
22
|
* injection selection and /harness adopt. Pure. */
|
|
23
23
|
export declare function selectListItems(items: HarnessItem[], opts: ListOptions): HarnessItem[];
|
|
24
|
+
/** Compact support summary for one line: "cited 3, kept 1, dropped 0" —
|
|
25
|
+
* only non-zero signals, so clean items add nothing. */
|
|
26
|
+
export declare function supportSummary(s?: SupportInfo): string | undefined;
|
|
24
27
|
/** Render the whole `/harness list` notify string: header (count + active
|
|
25
28
|
* filters), rows (compact by default, full text + evidence with `full`),
|
|
26
29
|
* and distinct empty-state guidance for an empty store vs an empty filter
|