pi-continual-harness 0.8.0 → 0.8.1
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 +3 -0
- package/package.json +1 -1
- package/src/harness.ts +146 -4
package/README.md
CHANGED
|
@@ -67,6 +67,9 @@ Durable I/O (round-trip with pi-reflect):
|
|
|
67
67
|
/harness push-mem [--all|--kind <kind>|--model <provider/id|active>] # persist active items to pi-mem (save_memory)
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
+
Subcommands, flags, `--kind`/`--model` values, and `keep`/`drop` item ids all
|
|
71
|
+
autocomplete in the TUI as you type after `/harness `.
|
|
72
|
+
|
|
70
73
|
`import` reconciles the file into the live store: items whose id matches an
|
|
71
74
|
existing entry are updated (offline edits win on content/evidence/importance),
|
|
72
75
|
new entries are created. By default nothing is deleted — `--prune` also drops
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-continual-harness",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.1",
|
|
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",
|
package/src/harness.ts
CHANGED
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
// /harness export [path] write active items to a markdown file
|
|
5
5
|
// /harness import [--prune] [path] parse it back and merge (durable wins)
|
|
6
6
|
//
|
|
7
|
+
// The command registers getArgumentCompletions so the TUI offers a filtered
|
|
8
|
+
// menu of subcommands (and, one level deeper, flags / item ids / kinds) as
|
|
9
|
+
// you type — see completions() below. The handler itself stays parsing-only.
|
|
10
|
+
//
|
|
7
11
|
// export writes the active items to ~/.pi/agent/harness-state.md (best-effort);
|
|
8
12
|
// import parses that file and merges into the live store. Because pi-reflect
|
|
9
13
|
// edits markdown files and git-commits, pointing it at the same file closes the
|
|
@@ -23,15 +27,152 @@ import {
|
|
|
23
27
|
reconstructFromDurable,
|
|
24
28
|
} from "./store.js";
|
|
25
29
|
import { loadConfig, resolveDurablePath } from "./config.js";
|
|
30
|
+
import { KIND_LABEL } from "./types.js";
|
|
26
31
|
import type { ComponentKind, HarnessItem } from "./types.js";
|
|
27
32
|
|
|
33
|
+
/** One row of the /harness subcommand menu (label = name, value = name). */
|
|
34
|
+
interface CompletionEntry {
|
|
35
|
+
name: string;
|
|
36
|
+
description: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const SUBCOMMANDS: CompletionEntry[] = [
|
|
40
|
+
{ name: "import", description: "Import durable state (--prune to prune stale items)" },
|
|
41
|
+
{ name: "export", description: "Export active items to durable file" },
|
|
42
|
+
{ name: "status", description: "Show harness status (active/total, per-kind counts, durable file)" },
|
|
43
|
+
{ name: "prune", description: "Decay & prune inactive items (--decay <days>)" },
|
|
44
|
+
{ name: "keep", description: "Bump item importance (+0.1)" },
|
|
45
|
+
{ name: "drop", description: "Lower item importance (−0.1)" },
|
|
46
|
+
{ name: "push-mem", description: "Persist active items to pi-mem (--all, --kind, --model)" },
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
const KINDS: ComponentKind[] = ["prompt", "memory", "skill", "subagent"];
|
|
50
|
+
|
|
51
|
+
/** Flags each subcommand accepts, in menu order. */
|
|
52
|
+
const FLAGS: Record<string, CompletionEntry[]> = {
|
|
53
|
+
import: [{ name: "--prune", description: "Drop live items missing from the durable file" }],
|
|
54
|
+
prune: [{ name: "--decay", description: "Decay items not updated for <days> before pruning" }],
|
|
55
|
+
"push-mem": [
|
|
56
|
+
{ name: "--all", description: "Push every active item, not just memories" },
|
|
57
|
+
{ name: "--kind", description: `Limit to one kind (${KINDS.join("|")})` },
|
|
58
|
+
{ name: "--model", description: "Limit to one owner model (provider/id or \"active\")" },
|
|
59
|
+
],
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
function preview(text: string, max = 60): string {
|
|
63
|
+
// Single line + truncated so the menu row stays readable.
|
|
64
|
+
const flat = text.replace(/\s+/g, " ").trim();
|
|
65
|
+
return flat.length > max ? `${flat.slice(0, max)}…` : flat;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Argument autocomplete for /harness (pi calls this with the FULL text after
|
|
70
|
+
* the command name; a selected item's `value` REPLACES that whole text, so
|
|
71
|
+
* multi-token rows re-emit the tokens already typed). Two levels:
|
|
72
|
+
*
|
|
73
|
+
* /harness <partial-subcommand> → matching subcommands
|
|
74
|
+
* /harness <sub> …<partial-token> → flags / ids / kind+model values for sub
|
|
75
|
+
*
|
|
76
|
+
* Returns null where a menu would only get in the way (path arguments,
|
|
77
|
+
* numeric flag values, unknown subcommands) so the editor falls back to its
|
|
78
|
+
* own behavior.
|
|
79
|
+
*/
|
|
80
|
+
function completions(argumentPrefix: string) {
|
|
81
|
+
// Level 1 — still typing the first token: the subcommand menu.
|
|
82
|
+
if (!/\s/.test(argumentPrefix)) {
|
|
83
|
+
const q = argumentPrefix.toLowerCase();
|
|
84
|
+
return filterSubcommands(q);
|
|
85
|
+
}
|
|
86
|
+
const tokens = argumentPrefix.trim().split(/\s+/).filter(Boolean);
|
|
87
|
+
// Fresh token (trailing whitespace) → the token being completed is "";
|
|
88
|
+
// otherwise it is the last typed token. `before` are the committed ones.
|
|
89
|
+
const fresh = argumentPrefix !== argumentPrefix.trimEnd();
|
|
90
|
+
const last = fresh ? "" : (tokens.at(-1) ?? "");
|
|
91
|
+
const before = fresh ? tokens : tokens.slice(0, -1);
|
|
92
|
+
|
|
93
|
+
// A stray leading space ("/harness imp") still means level 1, matching the
|
|
94
|
+
// handler's tolerant split.
|
|
95
|
+
if (before.length === 0) return filterSubcommands(last.toLowerCase());
|
|
96
|
+
|
|
97
|
+
const sub = before[0]!.toLowerCase();
|
|
98
|
+
// Flag-value completion: the token right before the one being typed.
|
|
99
|
+
const prev = before.at(-1)!;
|
|
100
|
+
if (prev === "--kind" && sub === "push-mem") {
|
|
101
|
+
return filterValues(
|
|
102
|
+
KINDS.map((k) => ({ value: k, description: KIND_LABEL[k] })),
|
|
103
|
+
last,
|
|
104
|
+
before,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
if (prev === "--model" && sub === "push-mem") {
|
|
108
|
+
const owners = [...new Set(getState().items.filter((i) => i.active && i.ownerModel).map((i) => i.ownerModel))];
|
|
109
|
+
return filterValues(
|
|
110
|
+
[
|
|
111
|
+
{ value: "active", description: "The model driving this command" },
|
|
112
|
+
...owners.map((m) => ({ value: m, description: "Items owned by this model" })),
|
|
113
|
+
],
|
|
114
|
+
last,
|
|
115
|
+
before,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Flag completion ("--…" prefix, or a fresh token for flag-taking subs).
|
|
120
|
+
// A fresh --kind/--model value was already completed above (early return),
|
|
121
|
+
// so a fresh token here means "offer the remaining flags".
|
|
122
|
+
if (last.startsWith("--") || (fresh && sub in FLAGS)) {
|
|
123
|
+
const used = new Set(before.filter((t) => t.startsWith("--")));
|
|
124
|
+
const rows = (FLAGS[sub] ?? []).filter((f) => !used.has(f.name));
|
|
125
|
+
const items = rows
|
|
126
|
+
.filter((f) => f.name.startsWith(last))
|
|
127
|
+
.map((f) => ({ value: [...before, f.name].join(" "), label: f.name, description: f.description }));
|
|
128
|
+
return items.length > 0 ? items : null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Positional id completion for keep/drop: the one argument they take.
|
|
132
|
+
if (sub === "keep" || sub === "drop") {
|
|
133
|
+
const idCommitted = before.slice(1).some((t) => !t.startsWith("--"));
|
|
134
|
+
if (idCommitted) return null; // id already chosen; nothing left to complete
|
|
135
|
+
const items = getState()
|
|
136
|
+
.items.filter((i) => i.active && i.id.startsWith(last))
|
|
137
|
+
.map((i: HarnessItem) => ({
|
|
138
|
+
value: [...before, i.id].join(" "),
|
|
139
|
+
label: i.id,
|
|
140
|
+
description: `${i.kind} · ${preview(i.content)}`,
|
|
141
|
+
}));
|
|
142
|
+
return items.length > 0 ? items : null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Everything else is a path/number/free-text argument — no menu.
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function filterSubcommands(prefix: string) {
|
|
150
|
+
const items = SUBCOMMANDS.filter((s) => s.name.startsWith(prefix)).map((s) => ({
|
|
151
|
+
value: s.name,
|
|
152
|
+
label: s.name,
|
|
153
|
+
description: s.description,
|
|
154
|
+
}));
|
|
155
|
+
return items.length > 0 ? items : null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Flag-value completion: `value` = committed tokens + the chosen value. */
|
|
159
|
+
function filterValues(
|
|
160
|
+
rows: Array<{ value: string; description: string }>,
|
|
161
|
+
prefix: string,
|
|
162
|
+
before: string[],
|
|
163
|
+
) {
|
|
164
|
+
const items = rows
|
|
165
|
+
.filter((r) => r.value.startsWith(prefix))
|
|
166
|
+
.map((r) => ({ value: [...before, r.value].join(" "), label: r.value, description: r.description }));
|
|
167
|
+
return items.length > 0 ? items : null;
|
|
168
|
+
}
|
|
169
|
+
|
|
28
170
|
export function registerHarness(pi: ExtensionAPI): void {
|
|
29
171
|
pi.registerCommand("harness", {
|
|
30
172
|
description:
|
|
31
|
-
"Durable harness-state I/O + importance hygiene
|
|
32
|
-
"import
|
|
33
|
-
"
|
|
34
|
-
"push-mem [--all|--kind <kind>|--model <provider/id|active>] (persist active items to pi-mem).",
|
|
173
|
+
"Durable harness-state I/O + importance hygiene " +
|
|
174
|
+
"(import · export · status · prune · keep · drop · push-mem — " +
|
|
175
|
+
"subcommands autocomplete as you type)",
|
|
35
176
|
handler: async (args, ctx) => {
|
|
36
177
|
const parts = args.trim().split(/\s+/).filter(Boolean);
|
|
37
178
|
const sub = (parts[0] ?? "status").toLowerCase();
|
|
@@ -61,6 +202,7 @@ export function registerHarness(pi: ExtensionAPI): void {
|
|
|
61
202
|
return;
|
|
62
203
|
}
|
|
63
204
|
},
|
|
205
|
+
getArgumentCompletions: (argumentPrefix) => completions(argumentPrefix),
|
|
64
206
|
});
|
|
65
207
|
}
|
|
66
208
|
|