pi-continual-harness 0.15.0 → 0.16.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
@@ -62,6 +62,7 @@ Durable I/O (round-trip with pi-reflect):
62
62
 
63
63
  ```
64
64
  /harness status # counts + durable layer presence/mtime
65
+ /harness list [-f] [--kind <kind>] [--scope <global|project|all>] # read items — compact rows, -f = full text + evidence
65
66
  /harness export [path] # layered export (or full snapshot to a path)
66
67
  /harness export-corpus [path] # calibration corpus JSONL for pi-reflex (local-only; dedupe pairs + lifecycle)
67
68
  /harness import [--prune] [path] # layered import (or single file from a path)
@@ -75,9 +76,9 @@ Durable I/O (round-trip with pi-reflect):
75
76
  /harness setup [--preset <quiet|recommended|power>] [--yes] # guided config wizard
76
77
  ```
77
78
 
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
80
- `/harness `.
79
+ Subcommands, flags (`-f` included), `--kind`/`--model`/`--from`/`--scope`
80
+ values, `move` scopes, and `keep`/`drop`/`move` item ids all autocomplete in
81
+ the TUI as you type after `/harness `.
81
82
 
82
83
  **Durable layers.** Every item carries its own durable scope: `global`
83
84
  (the shared `~/.pi/agent/harness-state.md`) or `project`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-continual-harness",
3
- "version": "0.15.0",
3
+ "version": "0.16.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",
package/src/harness.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  // /harness — durable I/O. The two-way round-trip seam with pi-reflect.
2
2
  //
3
3
  // /harness status [path] counts + durable layer presence/mtime
4
+ // /harness list [-f] [--kind] [--scope] read items: compact rows, -f = full text
4
5
  // /harness export [path] layered export (or full snapshot to path)
5
6
  // /harness import [--prune] [path] layered import (or single file from path)
6
7
  // /harness move <id> <global|project> move an item between durable layers
@@ -59,6 +60,7 @@ import { buildCorpus } from "./corpus.js";
59
60
  import { buildStatusReport, renderStatusReport } from "./status.js";
60
61
  import { getInjectionStats } from "./inject.js";
61
62
  import { nextHint } from "./hints.js";
63
+ import { preview, renderListReport, type ListScope } from "./list.js";
62
64
  import { handleSetup, PRESET_COMPLETIONS } from "./setup.js";
63
65
 
64
66
  /** One row of the /harness subcommand menu (label = name, value = name). */
@@ -72,6 +74,7 @@ const SUBCOMMANDS: CompletionEntry[] = [
72
74
  { name: "export", description: "Export active items to durable layers (or one file with a path)" },
73
75
  { name: "export-corpus", description: "Export calibration corpus JSONL for pi-reflex (dedupe pairs + lifecycle)" },
74
76
  { name: "status", description: "Show harness status (counts, audit-trail impact, injection stats, durable layers)" },
77
+ { name: "list", description: "Read items — compact rows, -f for full text (--kind/--scope filter)" },
75
78
  { name: "prune", description: "Decay & prune inactive items (--decay <days>)" },
76
79
  { name: "keep", description: "Bump item importance (+0.1)" },
77
80
  { name: "drop", description: "Lower item importance (−0.1)" },
@@ -84,6 +87,13 @@ const SUBCOMMANDS: CompletionEntry[] = [
84
87
 
85
88
  const KINDS: ComponentKind[] = ["prompt", "memory", "skill", "subagent"];
86
89
 
90
+ /** `--scope` values /harness list offers (semantics in src/list.ts). */
91
+ const LIST_SCOPES: Array<{ value: ListScope; description: string }> = [
92
+ { value: "global", description: "Global-scope items only (~/.pi/agent/harness-state.md)" },
93
+ { value: "project", description: "Project-scoped items (any project; rows carry the slug)" },
94
+ { value: "all", description: "Every item, foreign-project included" },
95
+ ];
96
+
87
97
  /** Flags each subcommand accepts, in menu order. */
88
98
  const FLAGS: Record<string, CompletionEntry[]> = {
89
99
  import: [{ name: "--prune", description: "Drop live items missing from the durable file" }],
@@ -97,17 +107,18 @@ const FLAGS: Record<string, CompletionEntry[]> = {
97
107
  { name: "--from", description: "Source model (provider/id) whose active items are up for adoption" },
98
108
  { name: "--all", description: "Adopt everything; skip the generalizes-vs-stays filter" },
99
109
  ],
110
+ list: [
111
+ { name: "-f", description: "Full item text + evidence instead of the compact preview rows" },
112
+ { name: "--kind", description: `Limit to one kind (${KINDS.join("|")})` },
113
+ { name: "--scope", description: "global | project | all (default: global + this project)" },
114
+ ],
100
115
  setup: [
101
116
  { name: "--preset", description: "Apply one preset: quiet | recommended | power" },
102
117
  { name: "--yes", description: "Scripted consent — skip every confirm prompt" },
103
118
  ],
104
119
  };
105
120
 
106
- function preview(text: string, max = 60): string {
107
- // Single line + truncated so the menu row stays readable.
108
- const flat = text.replace(/\s+/g, " ").trim();
109
- return flat.length > max ? `${flat.slice(0, max)}…` : flat;
110
- }
121
+ // preview() lives in src/list.ts (shared with /harness list rows).
111
122
 
112
123
  /**
113
124
  * Argument autocomplete for /harness (pi calls this with the FULL text after
@@ -141,7 +152,7 @@ function completions(argumentPrefix: string) {
141
152
  const sub = before[0]!.toLowerCase();
142
153
  // Flag-value completion: the token right before the one being typed.
143
154
  const prev = before.at(-1)!;
144
- if (prev === "--kind" && sub === "push-mem") {
155
+ if (prev === "--kind" && (sub === "push-mem" || sub === "list")) {
145
156
  return filterValues(
146
157
  KINDS.map((k) => ({ value: k, description: KIND_LABEL[k] })),
147
158
  last,
@@ -162,6 +173,9 @@ function completions(argumentPrefix: string) {
162
173
  if (prev === "--preset" && sub === "setup") {
163
174
  return filterValues(PRESET_COMPLETIONS, last, before);
164
175
  }
176
+ if (prev === "--scope" && sub === "list") {
177
+ return filterValues(LIST_SCOPES, last, before);
178
+ }
165
179
  // --from completes from the model keys actually present in the store (the
166
180
  // adoption source pool), with each owner's active-item count as the help
167
181
  // line — the same store-backed completion style as --model above.
@@ -175,11 +189,12 @@ function completions(argumentPrefix: string) {
175
189
  );
176
190
  }
177
191
 
178
- // Flag completion ("--…" prefix, or a fresh token for flag-taking subs).
179
- // A fresh --kind/--model value was already completed above (early return),
180
- // so a fresh token here means "offer the remaining flags".
181
- if (last.startsWith("--") || (fresh && sub in FLAGS)) {
182
- const used = new Set(before.filter((t) => t.startsWith("--")));
192
+ // Flag completion ("-…" prefix, or a fresh token for flag-taking subs —
193
+ // the "-" prefix also matches short flags like list's -f). A fresh
194
+ // --kind/--scope/--model value was already completed above (early
195
+ // return), so a fresh token here means "offer the remaining flags".
196
+ if (last.startsWith("-") || (fresh && sub in FLAGS)) {
197
+ const used = new Set(before.filter((t) => t.startsWith("-")));
183
198
  const rows = (FLAGS[sub] ?? []).filter((f) => !used.has(f.name));
184
199
  const items = rows
185
200
  .filter((f) => f.name.startsWith(last))
@@ -285,6 +300,9 @@ export function registerHarness(pi: ExtensionAPI): void {
285
300
  case "setup":
286
301
  await handleSetup(ctx, rest);
287
302
  return;
303
+ case "list":
304
+ await handleList(ctx, rest);
305
+ return;
288
306
  case "status":
289
307
  default:
290
308
  await handleStatus(ctx);
@@ -300,6 +318,59 @@ function explicitPath(rest: string[]): string | undefined {
300
318
  return rest.find((a) => !a.startsWith("-"));
301
319
  }
302
320
 
321
+ /** `/harness list [-f] [--kind <kind>] [--scope <global|project|all>]`
322
+ * (issue #31) — the read view: thin glue (flag parsing + one notify); every
323
+ * selection/rendering rule lives in the pure src/list.ts. Read-only: no
324
+ * store mutation, no durable writes. Invalid flag values AND unexpected
325
+ * arguments warn instead of silently falling back — a filter that quietly
326
+ * ignores its own argument would make the listing lie. */
327
+ async function handleList(ctx: ExtensionCommandContext, rest: string[]): Promise<void> {
328
+ const full = rest.includes("-f");
329
+ let kind: ComponentKind | undefined;
330
+ const kindIdx = rest.indexOf("--kind");
331
+ if (kindIdx >= 0) {
332
+ const k = kindIdx + 1 < rest.length ? rest[kindIdx + 1]! : undefined;
333
+ if (!k || k.startsWith("-") || !KINDS.includes(k as ComponentKind)) {
334
+ ctx.ui.notify(`/harness list: --kind expects one of ${KINDS.join("|")}.`, "warning");
335
+ return;
336
+ }
337
+ kind = k as ComponentKind;
338
+ }
339
+ let scope: ListScope | undefined;
340
+ const scopeIdx = rest.indexOf("--scope");
341
+ if (scopeIdx >= 0) {
342
+ const s = scopeIdx + 1 < rest.length ? rest[scopeIdx + 1]! : undefined;
343
+ if (s !== "global" && s !== "project" && s !== "all") {
344
+ ctx.ui.notify("/harness list: --scope expects global|project|all.", "warning");
345
+ return;
346
+ }
347
+ scope = s;
348
+ }
349
+ // Unexpected arguments (unknown flags like --full, stray positionals,
350
+ // repeated flags) warn too — the same no-silent-fallback rule as invalid
351
+ // values. Anything but -f, --kind <value>, --scope <value> is refused.
352
+ const consumed = new Set<number>();
353
+ if (kindIdx >= 0) { consumed.add(kindIdx); consumed.add(kindIdx + 1); }
354
+ if (scopeIdx >= 0) { consumed.add(scopeIdx); consumed.add(scopeIdx + 1); }
355
+ const unexpected = rest.filter((t, idx) => t !== "-f" && !consumed.has(idx));
356
+ if (unexpected.length > 0) {
357
+ ctx.ui.notify(
358
+ `/harness list: unexpected argument(s): ${unexpected.join(" ")} — expected -f, --kind <kind>, --scope <global|project|all>.`,
359
+ "warning",
360
+ );
361
+ return;
362
+ }
363
+ ctx.ui.notify(
364
+ renderListReport(getState().items, {
365
+ ...(kind ? { kind } : {}),
366
+ ...(scope ? { scope } : {}),
367
+ ...(full ? { full } : {}),
368
+ slug: projectSlug(ctx.cwd),
369
+ }),
370
+ "info",
371
+ );
372
+ }
373
+
303
374
  async function handleImport(
304
375
  pi: ExtensionAPI,
305
376
  ctx: ExtensionCommandContext,
package/src/index.ts CHANGED
@@ -132,6 +132,17 @@ export {
132
132
  } from "./hints.js";
133
133
  export { getInjectionStats, resetInjectionStats, type InjectionStats } from "./inject.js";
134
134
 
135
+ // /harness list (issue #31): pure selection + rendering for the read view of
136
+ // the store (compact rows / full text + evidence). Re-exported for tests and
137
+ // companions without reaching into internal paths.
138
+ export {
139
+ preview,
140
+ renderListReport,
141
+ selectListItems,
142
+ type ListOptions,
143
+ type ListScope,
144
+ } from "./list.js";
145
+
135
146
  // Injection selection policy (on by default). Re-exported so companion packages
136
147
  // and tests can reuse the pure selection/normalization without reaching into
137
148
  // internal paths.
@@ -190,6 +201,11 @@ export {
190
201
  type ProjectedLike,
191
202
  } from "./auto-refine.js";
192
203
 
204
+ // Message-content flattening (0.15.1): pi message content is a plain string OR
205
+ // a block array; every trajectory reader must go through this instead of
206
+ // assuming blocks. Re-exported for tests and companion packages.
207
+ export { messageText } from "./refine.js";
208
+
193
209
  // Setup wizard (issue #22): presets as pure data + expandPreset, and the one
194
210
  // reviewed write path for harness.json (backup + atomic write + reload).
195
211
  // Re-exported so tests and companion packages never reach into internal
package/src/list.ts ADDED
@@ -0,0 +1,157 @@
1
+ // `/harness list` (issue #31) — the read view of the store. One compact row
2
+ // per item (id, kind, importance, scope, preview) or, with `-f`, a
3
+ // harness_list-style full text + evidence rendering (extended with the
4
+ // scope). Read-only: selection
5
+ // and rendering are pure; harness.ts's handleList stays thin glue (flag
6
+ // parsing + one ctx.ui.notify).
7
+ //
8
+ // Scope semantics mirror the durable layers (issue #7): the default view is
9
+ // the session's own layer set — global + the ACTIVE project — the same set
10
+ // layered /harness import READS for this cwd (layered export writes more:
11
+ // every project layer with active items, foreign included — store.ts).
12
+ // Items scoped to a DIFFERENT project are hidden (and counted in a note,
13
+ // listed or not) unless you ask for them:
14
+ // `--scope project` lists every project-scoped item (rows carry the slug) and
15
+ // `--scope all` lists everything. An item's undefined scope counts as global
16
+ // (legacy snapshots / durable files — see types.ts).
17
+
18
+ import type { ComponentKind, HarnessItem } from "./types.js";
19
+
20
+ /** `--scope` values for /harness list. `undefined` (no flag) = the session's
21
+ * layer set: global + the active project. */
22
+ export type ListScope = "global" | "project" | "all";
23
+
24
+ /** Options for selectListItems / renderListReport. */
25
+ export interface ListOptions {
26
+ /** Filter by component kind (mirrors harness_list's kind parameter). */
27
+ kind?: ComponentKind;
28
+ /** Durable-scope filter; undefined = global + the active project. */
29
+ scope?: ListScope;
30
+ /** `-f`: full text + evidence per item (default: one compact row). */
31
+ full?: boolean;
32
+ /** The active project's slug (from the session cwd); selects the default
33
+ * view and labels the header. */
34
+ slug: string;
35
+ }
36
+
37
+ /** Single-line, whitespace-flattened preview truncated to `max` chars — the
38
+ * shared truncation style for one-line rows (menu entries, notifications). */
39
+ export function preview(text: string, max = 60): string {
40
+ const flat = text.replace(/\s+/g, " ").trim();
41
+ return flat.length > max ? `${flat.slice(0, max)}…` : flat;
42
+ }
43
+
44
+ /** True when an item belongs to the durable layer(s) the options select. */
45
+ function inScope(i: HarnessItem, opts: ListOptions): boolean {
46
+ switch (opts.scope) {
47
+ case "global":
48
+ return i.scope !== "project";
49
+ case "project":
50
+ return i.scope === "project";
51
+ case "all":
52
+ return true;
53
+ default:
54
+ // Session layer set: global + the active project (undefined scope is
55
+ // global; a project item only shows when its slug is the active one).
56
+ return i.scope !== "project" || i.project === opts.slug;
57
+ }
58
+ }
59
+
60
+ /** Select the listable items: kind filter, scope filter, then importance
61
+ * desc with ties keeping store order — the same stable ranking policy as
62
+ * injection selection and /harness adopt. Pure. */
63
+ export function selectListItems(items: HarnessItem[], opts: ListOptions): HarnessItem[] {
64
+ return items
65
+ .filter((i) => (opts.kind ? i.kind === opts.kind : true) && inScope(i, opts))
66
+ .map((i, idx) => [i, idx] as const)
67
+ .sort((a, b) => b[0].importance - a[0].importance || a[1] - b[1])
68
+ .map(([i]) => i);
69
+ }
70
+
71
+ /** Compact row: `[id] kind importance scope preview` (issue #31's table);
72
+ * inactive items carry an `(inactive)` marker so the row stays one line. */
73
+ function row(i: HarnessItem): string {
74
+ const scope = i.scope === "project" ? `project:${i.project ?? "?"}` : "global";
75
+ return `[${i.id}] ${i.kind.padEnd(8)} ${i.importance.toFixed(2)} ${scope} ${
76
+ i.active ? "" : "(inactive) "
77
+ }${preview(i.content)}`;
78
+ }
79
+
80
+ /** Full row (`-f`): the harness_list rendering, scope-aware — the same
81
+ * parenthetical the split/adopt prompts use, plus the evidence line. */
82
+ function fullRow(i: HarnessItem): string {
83
+ const bits = [
84
+ i.kind,
85
+ `importance ${i.importance.toFixed(2)}`,
86
+ i.active ? "active" : "inactive",
87
+ `model ${i.ownerModel || "(orphan)"}`,
88
+ i.scope === "project" ? `project "${i.project ?? ""}"` : "global",
89
+ ];
90
+ return `[${i.id}] (${bits.join(", ")}) ${i.content}\n evidence: ${i.evidence}`;
91
+ }
92
+
93
+ /** Tally `items` by a string key, rendered as "key (n), key (n)". */
94
+ function tally<T>(items: T[], key: (i: T) => string): string {
95
+ const counts = new Map<string, number>();
96
+ for (const i of items) counts.set(key(i), (counts.get(key(i)) ?? 0) + 1);
97
+ return [...counts.entries()].map(([k, n]) => `${k} (${n})`).join(", ");
98
+ }
99
+
100
+ function scopeDescription(opts: ListOptions): string {
101
+ switch (opts.scope) {
102
+ case "global":
103
+ return "scope global";
104
+ case "project":
105
+ return 'scope project (any project\'s items; rows carry the slug)';
106
+ case "all":
107
+ return "scope all";
108
+ default:
109
+ return `scope global + project "${opts.slug}"`;
110
+ }
111
+ }
112
+
113
+ /** Render the whole `/harness list` notify string: header (count + active
114
+ * filters), rows (compact by default, full text + evidence with `full`),
115
+ * and distinct empty-state guidance for an empty store vs an empty filter
116
+ * result (the emptyListMessage lesson: never collapse the two). Pure. */
117
+ export function renderListReport(items: HarnessItem[], opts: ListOptions): string {
118
+ const shown = selectListItems(items, opts);
119
+ const header =
120
+ `Harness list: ${shown.length} item(s) — ${scopeDescription(opts)}` +
121
+ (opts.kind ? `, kind ${opts.kind}` : "") +
122
+ ` (${opts.full ? "full text" : "compact; -f for full text"}).`;
123
+ // The default view hides foreign-project items on purpose (they are not
124
+ // this session's layers) — say so instead of letting them look missing.
125
+ // Computed ONCE so the note rides along on every non-empty-store result,
126
+ // empty filter results included (a foreign-only store still gets it).
127
+ let foreignNote: string | undefined;
128
+ if (opts.scope === undefined) {
129
+ const foreign = items.filter(
130
+ (i) => i.scope === "project" && i.project !== opts.slug && (opts.kind ? i.kind === opts.kind : true),
131
+ ).length;
132
+ if (foreign > 0) {
133
+ foreignNote = `+ ${foreign} foreign-project item(s) hidden — re-run with --scope all to see them.`;
134
+ }
135
+ }
136
+ if (shown.length === 0) {
137
+ if (items.length === 0) {
138
+ return [
139
+ header,
140
+ "No harness items yet. Run `/refine` after a working session (or `/harness import`) — items appear here as they are created.",
141
+ ].join("\n");
142
+ }
143
+ // A store whose matching items are ALL foreign must not collapse into
144
+ // generic filter guidance: the hidden note explains where they went
145
+ // (review #32) — the default view hides them, no filter removed them.
146
+ // (No flag echo here: the header already names scope+kind, and a
147
+ // pseudo-flag like `--scope global+project` would not even parse.)
148
+ return [
149
+ header,
150
+ `Nothing matches the current filters. The store holds ${items.length} item(s): ${tally(items, (i) => i.kind)}. Widen --scope or drop --kind.`,
151
+ ...(foreignNote ? [foreignNote] : []),
152
+ ].join("\n");
153
+ }
154
+ const lines = [header, ...shown.map(opts.full ? fullRow : row)];
155
+ if (foreignNote) lines.push(foreignNote);
156
+ return lines.join("\n");
157
+ }
package/src/outcome.ts CHANGED
@@ -26,13 +26,15 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
26
26
  import { DEFAULT_REF_BUMP, type HarnessConfig, loadConfig } from "./config.js";
27
27
  import { turnEndedBadly } from "./auto-refine.js";
28
28
  import { bumpImportance, getState, modelKey } from "./store.js";
29
+ import { messageText } from "./refine.js";
29
30
 
30
31
  // Minimal entry shape; kept loose to avoid coupling to pi's internal types.
32
+ // Message content is a plain string OR a block array depending on the source.
31
33
  type AnyEntry = {
32
34
  type?: string;
33
35
  message?: {
34
36
  role?: string;
35
- content?: Array<{ type?: string; text?: string }>;
37
+ content?: string | Array<{ type?: string; text?: string }>;
36
38
  };
37
39
  };
38
40
 
@@ -88,9 +90,7 @@ export function registerOutcome(pi: ExtensionAPI): void {
88
90
  const fresh = asst.slice(scannedMessages);
89
91
  scannedMessages = asst.length;
90
92
 
91
- const text = fresh
92
- .map((e) => (e.message?.content ?? []).map((c) => c.text ?? "").join(" "))
93
- .join("\n");
93
+ const text = fresh.map((e) => messageText(e.message?.content)).join("\n");
94
94
  if (!text.trim()) return;
95
95
 
96
96
  // Only the active model's items are injected, so only those can be cited.
package/src/refine.ts CHANGED
@@ -31,16 +31,28 @@ const DEFAULT_EVIDENCE_BYTES = 16000;
31
31
  export const DEFAULT_LOOKBACK_TURNS = 25;
32
32
 
33
33
  // Minimal entry shape for trajectory walking; kept loose to avoid coupling to
34
- // pi's internal session entry types.
34
+ // pi's internal session entry types. NOTE: message content is a plain string
35
+ // OR a block array depending on the message source — never assume either.
35
36
  type AnyEntry = {
36
37
  type?: string;
37
38
  customType?: string;
38
39
  message?: {
39
40
  role?: string;
40
- content?: Array<{ type?: string; text?: string }>;
41
+ content?: string | Array<{ type?: string; text?: string }>;
41
42
  };
42
43
  };
43
44
 
45
+ /**
46
+ * Flatten a pi message content to plain text. Pi messages carry content as a
47
+ * plain string or as an array of content blocks (text blocks carry `.text`);
48
+ * anything else degrades to "" instead of throwing. Pure.
49
+ */
50
+ export function messageText(content: string | Array<{ type?: string; text?: string }> | undefined): string {
51
+ if (typeof content === "string") return content;
52
+ if (!Array.isArray(content)) return "";
53
+ return content.map((c) => c?.text ?? "").join(" ");
54
+ }
55
+
44
56
  export function registerRefine(pi: ExtensionAPI): void {
45
57
  pi.registerCommand("refine", {
46
58
  description:
@@ -268,9 +280,7 @@ export function gatherEvidence(ctx: ExtensionContext, lookback: number): string
268
280
  let bytes = 0;
269
281
  for (const e of recent) {
270
282
  const role = e.message?.role ?? "?";
271
- const text = (e.message?.content ?? [])
272
- .map((c) => c.text ?? "")
273
- .join(" ")
283
+ const text = messageText(e.message?.content)
274
284
  .replace(/\s+/g, " ")
275
285
  .trim();
276
286
  if (!text) continue;
package/src/tools.ts CHANGED
@@ -88,7 +88,7 @@ export function registerTools(pi: ExtensionAPI): void {
88
88
  name: "harness_list",
89
89
  label: "List harness state",
90
90
  description:
91
- "List items in the Continual Harness state (self-improved prompt notes, memory, skill descriptions, sub-agent specs). Optionally filter by kind.",
91
+ "List items in the Continual Harness state (self-improved prompt notes, memory, skill descriptions, sub-agent specs). Optionally filter by kind. (/harness list is the human-facing TUI view: compact rows + durable-scope filters.)",
92
92
  promptSnippet: "Read Continual Harness self-improvement state",
93
93
  promptGuidelines: [
94
94
  "Call harness_list before proposing changes so you operate on current state.",