@skill-harness/cli 0.14.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
@@ -26,6 +26,18 @@ npm i -g @skill-harness/cli
26
26
  | `skill-harness add-test <skill> --skills <root> --id ID --title T ...` | scaffold a new scenario |
27
27
  | `skill-harness qualification prepare\|start\|status\|poll\|validate\|abort ...` | durable external-config qualification lifecycle |
28
28
 
29
+ ## Retained learning
30
+
31
+ `skill-harness learning` opens the guided offline workflow; `learning help` documents
32
+ named retained inputs, complete quality review, scoped trust, independent labels,
33
+ separate adoption decisions and later observations. No hand-authored manifests or model calls.
34
+ The Pi package exposes the same flow as `/skill-harness learning`; connected producer registry
35
+ activation/rollback remain separately authorized operations, never a side effect of reading.
36
+ `@skill-harness/cli/learning` exports `runLearningCommand`, `runLearningWizard` and
37
+ `reviewLearningComparison` for the existing producer dashboard bridge.
38
+
39
+ [Product guide](https://github.com/mojomanyana/skill-harness/blob/main/docs/factory/PRODUCT-GUIDE.md).
40
+
29
41
  ## More
30
42
 
31
43
  - Repo + full docs: https://github.com/mojomanyana/skill-harness
package/dist/cli.js CHANGED
@@ -9,6 +9,7 @@ import { serveReview } from "./serve.js";
9
9
  import { runCompareCommand } from "./compare.js";
10
10
  import { cmdQualification } from "./qualification.js";
11
11
  import { cmdArchive } from "./archive.js";
12
+ import { runLearningCommand } from "./learning.js";
12
13
  const DEFAULT_MODEL = "fireworks:accounts/fireworks/models/deepseek-v4-pro";
13
14
  // The judge default lives in core (`defaultJudge()`), which resolves
14
15
  // SKILL_HARNESS_JUDGE over a baked value — it was duplicated in three places
@@ -895,6 +896,8 @@ export function help() {
895
896
  ask again about untrustworthy cells (ambiguous / contradictory / non-unanimous /
896
897
  ship-deciding). OFF by default; prints the exact MAX extra call count first.
897
898
  archive ingest|inspect|watch --policy file --source id explicit external ingestion/metadata (${free("archive")})
899
+ learning [status|import|review|trust|decide|adoption|outcome|guide] guided retained learning (${free("learning")}; no models)
900
+ use learning help; quality, trust, adoption and later outcomes stay separate
898
901
  archive weekly|trust|access --state /private/dir --request file durable learning/consent lifecycle (${free("archive")})
899
902
  watch requires --max-polls N; optional --interval-ms N and --previous checkpoint
900
903
  judge-agreement <run-dir> compare two distinct persisted judge votes per scenario (${free("judge-agreement")})
@@ -928,6 +931,10 @@ defaults: model=${DEFAULT_MODEL} judge=${defaultJudge()} mode=green harness=p
928
931
  }
929
932
  export async function main(argv) {
930
933
  const cmd = argv[0];
934
+ if (cmd === "learning") {
935
+ await runLearningCommand(argv.slice(1));
936
+ return;
937
+ }
931
938
  const args = parseArgs(argv.slice(1));
932
939
  switch (cmd) {
933
940
  case "run": return cmdRun(args);
@@ -0,0 +1,26 @@
1
+ import { openLearningWorkspace } from '@skill-harness/adapters';
2
+ export interface LearningUI {
3
+ select(title: string, choices: string[]): Promise<string | undefined>;
4
+ input(title: string, initial?: string): Promise<string | undefined>;
5
+ editor(title: string, text: string): Promise<string | undefined>;
6
+ confirm(title: string, detail: string): Promise<boolean>;
7
+ notify(text: string, level?: 'info' | 'warning' | 'error'): void;
8
+ }
9
+ export declare const LEARNING_HELP = "Learning \u2014 retained evidence, no model calls\n learning open guided review (interactive terminal)\n learning init --archive DIR --author NAME [--scope-item N] --confirm\n [--scope TEXT --population TEXT only for a comparison-only archive without retained cases]\n learning status [--json] readiness; never automatic exposure\n learning import list retained batches/comparisons\n learning import --item N --name NAME --title TEXT --scope-note TEXT --confirm\n learning cases NAME [--offset N]\n learning evidence NAME --item N [--evidence N] inspect only the selected case's retained sources\n learning case NAME --item N --disposition confirmed_defect|expected_behavior|exemplar|uncertain|skip --note TEXT --confirm\n learning comparisons\n learning review NAME guided complete-output review in a terminal\n learning artifact NAME --variant A [--artifact N]\n learning choose NAME --kind one|tie|none|insufficient [--variants A,B] --full-review --note TEXT --confirm\n learning reveal NAME requires durable full-scope quality receipt\n learning decide NAME --disposition adopt|reject|defer --note TEXT --confirm\n learning hypothesis NAME --name SLUG --intervention TEXT --prediction TEXT --disproof TEXT --downside TEXT --rollback TEXT --alternative TEXT --confirm\n learning link NAME --cases BATCH --item N --hypothesis SLUG --confirm\n learning trust sample FILE --name SLUG --title TEXT [--split calibration|heldout|tuning] --confirm\n learning trust setup --cases NAME [--detector ID] [--split calibration|heldout|tuning] [--max-unflagged N]\n [--attention N --minimum-resolved N --minimum-lower-bound T --expires DAYS] --confirm\n learning trust status\n learning trust label --cases NAME --item N --reference-author NAME --confirm\n learning trust unflagged --item N --miss yes|no --evidence FILE --reference-author NAME --note TEXT --confirm\n learning adoption NAME scope/decision/readiness; activation is producer-owned\n learning outcome NAME inspect observed outcomes\n learning outcome NAME --result success|confirmed-defect|unknown --artifact FILE --original-requirement FILE --current-requirement FILE --evidence FILE --reference-author NAME --note TEXT [--accepted-artifact FILE --acceptance-evidence FILE] --confirm\n learning guide | current installed portable guide / requirement register\nAll commands accept --state DIR (default .skill-harness/learning), --json for structured output.\n--confirm authorizes only the displayed local write, never models, acceptance or registry activation.\nNo raw manifest authoring. Import selects actual retained inputs. Exit zero is command completion, not quality.\nRegistry activation/rollback use /grants learning in interactive Pi, with independent authority.";
10
+ /** Escape terminal control/format characters in untrusted retained display text. Exact archive
11
+ * bytes never change; output honestly identifies this as a display representation. */
12
+ export declare function learningDisplay(text: string): string;
13
+ export declare function formatLearningStatus(view: ReturnType<ReturnType<typeof openLearningWorkspace>['inspect']>): string;
14
+ export declare function formatLearningTrust(trust: Record<string, unknown>): string;
15
+ export interface LearningCommandOptions {
16
+ cwd?: string;
17
+ write?: (text: string) => void;
18
+ ui?: LearningUI;
19
+ }
20
+ export declare function runLearningCommand(argv: string[], options?: LearningCommandOptions): Promise<unknown>;
21
+ export declare function reviewLearningComparison(directory: string, name: string, ui: LearningUI): Promise<void>;
22
+ export declare function runLearningWizard(options: {
23
+ cwd: string;
24
+ directory: string;
25
+ ui: LearningUI;
26
+ }): Promise<void>;
@@ -0,0 +1,656 @@
1
+ import { resolve, join, dirname } from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { existsSync } from 'node:fs';
4
+ import { createInterface } from 'node:readline/promises';
5
+ import { createLearningWorkspace, openLearningWorkspace, catalogLearningArchive, learningHash, learningFile, retainArchiveSource, previewLearningTrust, configureLearningTrust, previewLearningCaseLabel, trustOutcomeDigest, } from '@skill-harness/adapters';
6
+ export const LEARNING_HELP = `Learning — retained evidence, no model calls
7
+ learning open guided review (interactive terminal)
8
+ learning init --archive DIR --author NAME [--scope-item N] --confirm
9
+ [--scope TEXT --population TEXT only for a comparison-only archive without retained cases]
10
+ learning status [--json] readiness; never automatic exposure
11
+ learning import list retained batches/comparisons
12
+ learning import --item N --name NAME --title TEXT --scope-note TEXT --confirm
13
+ learning cases NAME [--offset N]
14
+ learning evidence NAME --item N [--evidence N] inspect only the selected case's retained sources
15
+ learning case NAME --item N --disposition confirmed_defect|expected_behavior|exemplar|uncertain|skip --note TEXT --confirm
16
+ learning comparisons
17
+ learning review NAME guided complete-output review in a terminal
18
+ learning artifact NAME --variant A [--artifact N]
19
+ learning choose NAME --kind one|tie|none|insufficient [--variants A,B] --full-review --note TEXT --confirm
20
+ learning reveal NAME requires durable full-scope quality receipt
21
+ learning decide NAME --disposition adopt|reject|defer --note TEXT --confirm
22
+ learning hypothesis NAME --name SLUG --intervention TEXT --prediction TEXT --disproof TEXT --downside TEXT --rollback TEXT --alternative TEXT --confirm
23
+ learning link NAME --cases BATCH --item N --hypothesis SLUG --confirm
24
+ learning trust sample FILE --name SLUG --title TEXT [--split calibration|heldout|tuning] --confirm
25
+ learning trust setup --cases NAME [--detector ID] [--split calibration|heldout|tuning] [--max-unflagged N]
26
+ [--attention N --minimum-resolved N --minimum-lower-bound T --expires DAYS] --confirm
27
+ learning trust status
28
+ learning trust label --cases NAME --item N --reference-author NAME --confirm
29
+ learning trust unflagged --item N --miss yes|no --evidence FILE --reference-author NAME --note TEXT --confirm
30
+ learning adoption NAME scope/decision/readiness; activation is producer-owned
31
+ learning outcome NAME inspect observed outcomes
32
+ learning outcome NAME --result success|confirmed-defect|unknown --artifact FILE --original-requirement FILE --current-requirement FILE --evidence FILE --reference-author NAME --note TEXT [--accepted-artifact FILE --acceptance-evidence FILE] --confirm
33
+ learning guide | current installed portable guide / requirement register
34
+ All commands accept --state DIR (default .skill-harness/learning), --json for structured output.
35
+ --confirm authorizes only the displayed local write, never models, acceptance or registry activation.
36
+ No raw manifest authoring. Import selects actual retained inputs. Exit zero is command completion, not quality.
37
+ Registry activation/rollback use /grants learning in interactive Pi, with independent authority.`;
38
+ /** Escape terminal control/format characters in untrusted retained display text. Exact archive
39
+ * bytes never change; output honestly identifies this as a display representation. */
40
+ export function learningDisplay(text) {
41
+ return text.replace(/[\u0000-\u0008\u000b-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/g, c => `\\u${c.charCodeAt(0).toString(16).padStart(4, '0')}`);
42
+ }
43
+ function parse(argv) {
44
+ const args = [], flags = {};
45
+ for (let i = 0; i < argv.length; i++) {
46
+ const token = argv[i];
47
+ if (!token.startsWith('--')) {
48
+ args.push(token);
49
+ continue;
50
+ }
51
+ const eq = token.indexOf('='), key = token.slice(2, eq < 0 ? undefined : eq);
52
+ if (!key || Object.hasOwn(flags, key))
53
+ throw Error('duplicate or empty learning option');
54
+ if (eq >= 0)
55
+ flags[key] = token.slice(eq + 1);
56
+ else if (['confirm', 'json', 'full-review', 'help'].includes(key))
57
+ flags[key] = true;
58
+ else {
59
+ if (!argv[i + 1] || argv[i + 1].startsWith('--'))
60
+ throw Error(`--${key} needs a value`);
61
+ flags[key] = argv[++i];
62
+ }
63
+ }
64
+ return { args, flags };
65
+ }
66
+ const word = (v, name) => { if (typeof v !== 'string' || !v.trim())
67
+ throw Error(`${name} is required`); return v; };
68
+ const integer = (v, fallback) => { const n = v === undefined ? fallback : Number(v); if (!Number.isSafeInteger(n) || n < 0)
69
+ throw Error('nonnegative integer required'); return n; };
70
+ function artifactText(bytes) {
71
+ try {
72
+ return learningDisplay(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
73
+ }
74
+ catch {
75
+ throw Error('Retained source is not UTF-8 text; no complete text view is claimed');
76
+ }
77
+ }
78
+ function allCases(workspace, name) {
79
+ const page = workspace.cases(name), items = [...page.items];
80
+ for (let offset = 5; offset < page.total; offset += 5)
81
+ items.push(...workspace.cases(name, offset).items);
82
+ return { ...page, items };
83
+ }
84
+ function caseAt(workspace, name, index) {
85
+ const page = allCases(workspace, name), item = page.items[index - 1];
86
+ if (!item)
87
+ throw Error('select a listed case number');
88
+ return { page, item };
89
+ }
90
+ function trustSetup(workspace, flags) {
91
+ const caseName = word(flags.cases, '--cases'), cases = allCases(workspace, caseName).items;
92
+ const detectorId = typeof flags.detector === 'string' ? flags.detector : cases[0]?.candidate.detector.id;
93
+ if (!detectorId)
94
+ throw Error('no retained detector cases available');
95
+ const attention = integer(flags.attention, 0), days = integer(flags.expires, 7), split = String(flags.split ?? 'calibration');
96
+ if (attention && (flags['minimum-resolved'] === undefined || flags['minimum-lower-bound'] === undefined))
97
+ throw Error('automatic questions need explicit --minimum-resolved and --minimum-lower-bound');
98
+ const minimumLowerBound = Number(flags['minimum-lower-bound']);
99
+ if (attention && (!Number.isFinite(minimumLowerBound) || minimumLowerBound < 0 || minimumLowerBound > 1 || days < 1 || days > 365))
100
+ throw Error('invalid explicit exposure policy');
101
+ return { caseName, detectorId, split, maxUnflagged: integer(flags['max-unflagged'], 0), unflagged: workspace.unflagged().map(({ incidentId, manifestId, split }) => ({ incidentId, manifestId, split })),
102
+ exposure: attention ? { id: `operator-${workspace.configuration().scopeDigest.slice(0, 20)}`, attentionRemaining: attention, minimumResolved: integer(flags['minimum-resolved'], 10), minimumLowerBound, expiresAt: Date.now() + days * 86400000 } : null };
103
+ }
104
+ export function formatLearningStatus(view) {
105
+ const lines = [`Learning — ${view.configuration.population}`, 'Deliberate review; no model calls, automatic questions, or activation from reading.'];
106
+ for (const c of view.cases)
107
+ lines.push(`Cases: ${c.title} (${c.name}) — ${c.state === 'ready' ? `${c.total} retained nomination(s)` : c.reason}`);
108
+ for (const c of view.comparisons)
109
+ lines.push(`Comparison: ${c.title} (${c.name}) — ${c.reason}${'decision' in c && c.decision ? `; decision: ${c.decision.disposition}` : ''}`);
110
+ if (!view.cases.length && !view.comparisons.length)
111
+ lines.push('No inputs connected. Choose Connect retained input; missing evidence is not a failure or acceptance.');
112
+ lines.push(formatLearningTrust(view.trust));
113
+ return learningDisplay(lines.join('\n'));
114
+ }
115
+ export function formatLearningTrust(trust) {
116
+ const advice = trust.automaticExposure;
117
+ const calibration = trust.calibration;
118
+ const sample = trust.unflagged;
119
+ const lines = [`Automatic questions: ${advice?.mode ?? 'silent'} — ${advice?.reason ?? trust.reason ?? 'not configured'} (reading reserves no attention).`];
120
+ for (const r of calibration?.reports ?? [])
121
+ lines.push(`Independent labels: ${r.resolved} resolved, ${r.unresolved} unresolved, ${r.conflicted} conflicted; precision ${r.precision === null ? 'unknown' : `${Math.round(r.precision * 100)}%`}${r.interval ? ` (95% interval ${Math.round(r.interval.lower * 100)}–${Math.round(r.interval.upper * 100)}%)` : ''}.`);
122
+ if (sample)
123
+ lines.push(`Unflagged sample: ${sample.sampled} frozen, ${sample.resolved} independently labelled, ${sample.misses} observed miss(es).${sample.sampled ? '' : ' No unflagged coverage established.'}`);
124
+ if (typeof trust.attentionUsed === 'number')
125
+ lines.push(`Attention already used: ${trust.attentionUsed}. Reopening does not refill it.`);
126
+ return learningDisplay(lines.join('\n'));
127
+ }
128
+ export async function runLearningCommand(argv, options = {}) {
129
+ const { args, flags } = parse(argv), cwd = options.cwd ?? process.cwd(), directory = resolve(cwd, typeof flags.state === 'string' ? flags.state : '.skill-harness/learning');
130
+ const out = options.write ?? console.log;
131
+ const emit = (value) => { out(flags.json ? JSON.stringify(value, null, 2) : typeof value === 'string' ? value : learningDisplay(JSON.stringify(value, null, 2))); return value; };
132
+ const confirm = (preview, write) => {
133
+ if (flags.confirm !== true)
134
+ return emit({ state: 'confirmation-required', preview, next: 'Repeat with --confirm to authorize this exact local action.' });
135
+ return emit(write());
136
+ };
137
+ const global = ['state', 'json', 'confirm', 'help'];
138
+ const allowed = {
139
+ init: ['archive', 'scope', 'population', 'author', 'scope-item'], status: [], import: ['item', 'name', 'title', 'scope-note'], cases: ['offset'], evidence: ['item', 'evidence'], case: ['item', 'disposition', 'note'], comparisons: [], review: [], artifact: ['variant', 'artifact'], choose: ['kind', 'variants', 'full-review', 'note'], reveal: [], decide: ['disposition', 'note'], hypothesis: ['name', 'intervention', 'prediction', 'disproof', 'downside', 'rollback', 'alternative'], link: ['cases', 'item', 'hypothesis'], trust: ['cases', 'item', 'detector', 'split', 'max-unflagged', 'attention', 'minimum-resolved', 'minimum-lower-bound', 'expires', 'name', 'title', 'reference-author', 'miss', 'evidence', 'note'], adoption: [], outcome: ['result', 'artifact', 'original-requirement', 'current-requirement', 'evidence', 'reference-author', 'note', 'accepted-artifact', 'acceptance-evidence'], guide: [], help: []
140
+ };
141
+ const command = args[0];
142
+ if (command === 'guide' || command === 'current') {
143
+ const file = command === 'guide' ? 'PRODUCT-GUIDE.md' : 'STATUS.md', base = dirname(fileURLToPath(import.meta.url));
144
+ const selected = [resolve(base, '../docs', file), resolve(base, '../../../docs/factory', file)].find(p => existsSync(p));
145
+ if (!selected)
146
+ throw Error('packaged learning guide missing; reinstall a complete compatible package');
147
+ return emit(learningFile(selected, 65536).toString('utf8'));
148
+ }
149
+ if (flags.help || command === 'help')
150
+ return emit(LEARNING_HELP);
151
+ if (!command) {
152
+ const terminal = options.ui ? null : terminalLearningUI();
153
+ try {
154
+ return await runLearningWizard({ cwd, directory, ui: options.ui ?? terminal });
155
+ }
156
+ finally {
157
+ terminal?.close();
158
+ }
159
+ }
160
+ if (!Object.hasOwn(allowed, command))
161
+ throw Error('unknown learning command; use learning help');
162
+ const unexpected = Object.keys(flags).filter(k => !global.includes(k) && !allowed[command].includes(k));
163
+ if (unexpected.length)
164
+ throw Error(`unsupported learning option: --${unexpected[0]}`);
165
+ if (command === 'init') {
166
+ const archiveRoot = resolve(cwd, word(flags.archive, '--archive')), author = word(flags.author, '--author'), scopes = learningArchiveScopes(archiveRoot, author);
167
+ if (scopes.length > 1 && flags['scope-item'] === undefined)
168
+ return emit({ state: 'scope-selection-required', scopes: scopes.map((s, i) => ({ item: i + 1, description: s.description, population: s.population })), next: 'Repeat init with --scope-item N; scope identity is derived from retained facts, not typed hashes.' });
169
+ const selected = scopes[integer(flags['scope-item'], 1) - 1];
170
+ if (scopes.length && !selected)
171
+ throw Error('select a listed retained scope number');
172
+ if (selected && flags.population !== undefined && flags.population !== selected.population)
173
+ throw Error('population differs from retained case scope');
174
+ const input = { archiveRoot, scopeDigest: selected?.scopeDigest ?? learningHash({ declaredScope: word(flags.scope, '--scope for a comparison-only archive') }), population: selected?.population ?? word(flags.population, '--population for a comparison-only archive'), author };
175
+ return confirm({ directory, ...input, scopeMeaning: selected ? 'exact retained case snapshot' : 'operator-declared comparison-only scope; cannot bind mismatching case snapshots' }, () => createLearningWorkspace(directory, input).inspect(Date.now()));
176
+ }
177
+ if (!existsSync(directory))
178
+ throw Error('learning workspace not connected; use learning init or /grants learning with the current host');
179
+ const workspace = openLearningWorkspace(directory), config = workspace.configuration(), name = args[1];
180
+ if (command === 'status') {
181
+ const view = workspace.inspect(Date.now());
182
+ return emit(flags.json ? view : formatLearningStatus(view));
183
+ }
184
+ if (command === 'import') {
185
+ const catalog = catalogLearningArchive(config.archiveRoot, config.author);
186
+ if (flags.item === undefined)
187
+ return emit(flags.json ? catalog.map((c, i) => ({ item: i + 1, ...c })) : catalog.length ? catalog.map((c, i) => `${i + 1}. ${c.kind}: ${c.description}`).join('\n') : 'No compatible retained inputs. Ordinary work must retain actual evidence first.');
188
+ const item = catalog[integer(flags.item, 0) - 1];
189
+ if (!item)
190
+ throw Error('select a listed retained input number');
191
+ const binding = { name: word(flags.name, '--name'), title: word(flags.title, '--title') };
192
+ return confirm({ selected: item, ...binding }, () => item.kind === 'cases' ? workspace.bindCases({ ...binding, version: item.version, batchId: item.manifestId }) : workspace.bindComparison({ ...binding, scopeStatement: word(flags['scope-note'], '--scope-note'), comparisonManifestId: item.manifestId, caseReference: null, hypothesisManifestId: null, adoptionBinding: null, priorFeedbackManifestIds: [] }));
193
+ }
194
+ if (command === 'cases') {
195
+ const page = workspace.cases(word(name, 'case batch name'), integer(flags.offset, 0));
196
+ return emit(flags.json ? page : page.items.map((c, i) => `${page.offset + i + 1}. ${c.candidate.target.obligationId}: ${c.candidate.reason} — ${c.disposition}`).join('\n') + `\n${page.total} retained nomination(s). Use learning evidence for retained bytes; learning case records an independent disposition.`);
197
+ }
198
+ if (command === 'evidence') {
199
+ const batch = word(name, 'case batch name'), { item } = caseAt(workspace, batch, integer(flags.item, 1)), refs = workspace.caseEvidence(batch, item.caseManifestId);
200
+ if (flags.evidence === undefined)
201
+ return emit(flags.json ? refs : (refs.length === 1 ? 'Only nomination retained; no original observation link. Not independent proof.\n' : '') + refs.map((r, i) => `${i + 1}. ${r.role}: ${r.status === 'available' ? `${r.reference.parser.id} (${r.reference.retention})` : r.status}`).join('\n'));
202
+ const ref = refs[integer(flags.evidence, 1) - 1];
203
+ if (!ref)
204
+ throw Error('select a listed evidence number');
205
+ const result = workspace.readCaseEvidence(batch, item.caseManifestId, ref.manifestId);
206
+ if (result.status !== 'available')
207
+ return emit({ state: 'deferred', reason: result.status });
208
+ return emit(`${result.reference.retention === 'exact' ? 'Exact retained source' : 'Redacted retained source — not full evidence'}\n${artifactText(result.bytes)}`);
209
+ }
210
+ if (command === 'case') {
211
+ const batch = word(name, 'case batch name'), { item } = caseAt(workspace, batch, integer(flags.item, 0));
212
+ const request = { caseManifestId: item.caseManifestId, priorDecisionId: item.priorDecisionId, disposition: word(flags.disposition, '--disposition'), note: word(flags.note, '--note') };
213
+ return confirm({ candidate: item.candidate, request }, () => workspace.decideCase(batch, request));
214
+ }
215
+ if (command === 'comparisons') {
216
+ const view = workspace.inspect(Date.now());
217
+ return emit(flags.json ? view.comparisons : view.comparisons.map(c => `${c.name}: ${c.title} — ${c.reason}`).join('\n') || 'No retained comparisons connected.');
218
+ }
219
+ if (command === 'review') {
220
+ const terminal = options.ui ? null : terminalLearningUI();
221
+ try {
222
+ return await reviewLearningComparison(directory, word(name, 'comparison name'), options.ui ?? terminal);
223
+ }
224
+ finally {
225
+ terminal?.close();
226
+ }
227
+ }
228
+ if (command === 'artifact') {
229
+ const v = workspace.comparison(word(name, 'comparison name')), card = v.cards.find(c => c.displayLabel === flags.variant);
230
+ if (!card)
231
+ throw Error('select a listed variant letter');
232
+ const digest = card.artifactDigests[integer(flags.artifact, 1) - 1];
233
+ if (!digest)
234
+ throw Error('select a listed artifact number');
235
+ const bytes = workspace.artifact(name, card.label, digest);
236
+ const text = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
237
+ return emit(flags.json ? { variant: card.displayLabel, digest, bytes: bytes.length, scope: 'complete-artifact-display', controlCharacters: 'escaped (retained bytes unchanged)', text: learningDisplay(text) } : `Variant ${card.displayLabel} — complete artifact (${bytes.length} bytes; controls escaped, retained bytes unchanged)\n\n${learningDisplay(text)}`);
238
+ }
239
+ if (command === 'choose') {
240
+ const v = workspace.comparison(word(name, 'comparison name')), kind = word(flags.kind, '--kind');
241
+ const letters = typeof flags.variants === 'string' ? flags.variants.split(',') : [];
242
+ const labels = letters.map(letter => { const c = v.cards.find(c => c.displayLabel === letter); if (!c)
243
+ throw Error('select listed variant letters'); return c.label; });
244
+ if (kind !== 'insufficient' && flags['full-review'] !== true)
245
+ throw Error('--full-review explicitly acknowledges all complete artifacts, not excerpts');
246
+ const choice = { kind, labels }, review = { scope: 'full-artifacts', note: word(flags.note, '--note'), reviewedArtifactDigests: flags['full-review'] === true ? [...new Set(v.cards.flatMap(c => c.artifactDigests))] : [] };
247
+ return confirm({ comparison: name, choice, review }, () => workspace.choose(name, choice, review));
248
+ }
249
+ if (command === 'reveal')
250
+ return emit(workspace.reveal(word(name, 'comparison name')));
251
+ if (command === 'decide') {
252
+ const v = workspace.decisionStatus(word(name, 'comparison name')), request = { disposition: word(flags.disposition, '--disposition'), note: word(flags.note, '--note'), priorDecisionId: v.current?.id ?? null };
253
+ return confirm({ comparison: name, scope: v.scopeStatement, request, activation: 'not-performed' }, () => workspace.decide(name, request));
254
+ }
255
+ if (command === 'hypothesis') {
256
+ const batch = word(name, 'case batch name'), cases = allCases(workspace, batch).items.filter(c => c.disposition === 'confirmed_defect');
257
+ if (!cases.length)
258
+ throw Error('confirm a retained defect before proposing a repair');
259
+ const proposal = { archiveSnapshot: learningHash(cases.map(c => ({ case: c.caseManifestId, decision: c.priorDecisionId }))), caseIds: cases.map(c => c.candidate.id), population: config.population,
260
+ intervention: word(flags.intervention, '--intervention'), prediction: word(flags.prediction, '--prediction'), disproof: word(flags.disproof, '--disproof'), downside: word(flags.downside, '--downside'), rollback: word(flags.rollback, '--rollback'), alternatives: [word(flags.alternative, '--alternative')], limits: { subjectCalls: 0, judgeCalls: 0, wallMs: 0 }, effectProfile: null };
261
+ return confirm({ proposal, scope: 'proposal-only; zero calls, no authorization' }, () => workspace.propose(batch, proposal, word(flags.name, '--name')));
262
+ }
263
+ if (command === 'link') {
264
+ const batch = word(flags.cases, '--cases'), { page, item } = caseAt(workspace, batch, integer(flags.item, 0));
265
+ if (!item.priorDecisionId)
266
+ throw Error('case decision required');
267
+ const h = workspace.hypotheses().find(h => h.name === flags.hypothesis);
268
+ if (!h)
269
+ throw Error('select a hypothesis created with learning hypothesis');
270
+ const context = { ...workspace.comparisonContext(word(name, 'comparison name')), caseReference: { version: page.version, batchId: page.batchId, manifestId: item.caseManifestId, decisionId: item.priorDecisionId }, hypothesisManifestId: h.manifestId };
271
+ return confirm({ comparison: name, context }, () => workspace.linkComparisonContext(name, context));
272
+ }
273
+ if (command === 'trust') {
274
+ if (name === 'sample') {
275
+ const bytes = learningFile(resolve(cwd, word(args[2], 'sample evidence file')), 65536), sampleName = word(flags.name, '--name');
276
+ return confirm({ source: args[2], bytes: bytes.length, title: flags.title, meaning: 'operator-selected unflagged incident; NOT a correctness label' }, () => { const manifestId = retainArchiveSource(config.archiveRoot, { sourceId: `unflagged-${learningHash(bytes.toString('base64')).slice(0, 24)}`, parser: { id: 'operator-unflagged-evidence', version: '1' }, retention: 'exact', bytes }).manifestId; return workspace.addUnflagged({ name: sampleName, title: word(flags.title, '--title'), manifestId, split: String(flags.split ?? 'calibration') }); });
277
+ }
278
+ if (name === 'setup') {
279
+ const setup = trustSetup(workspace, flags), preview = previewLearningTrust(directory, setup);
280
+ return confirm({ ...preview, independentLabels: 'none created', automaticQuestions: setup.exposure ? 'only if independently calibrated policy is met' : 'disabled' }, () => configureLearningTrust(directory, setup, [preview.policyDigest]));
281
+ }
282
+ const trust = workspace.trust();
283
+ if (name === 'status' || !name) {
284
+ const view = { ...trust.inspect(Date.now()), automaticExposure: trust.previewExposure(Date.now()), cohort: trust.configuration().cohort };
285
+ return emit(flags.json ? view : formatLearningTrust(view));
286
+ }
287
+ if (name === 'label') {
288
+ const batch = word(flags.cases, '--cases'), { page, item } = caseAt(workspace, batch, integer(flags.item, 0));
289
+ if (!item.priorDecisionId)
290
+ throw Error('record an independent case decision first');
291
+ const reference = { version: page.version, batchId: page.batchId, manifestId: item.caseManifestId, decisionId: item.priorDecisionId }, preview = previewLearningCaseLabel(directory, reference), author = word(flags['reference-author'], '--reference-author');
292
+ return confirm({ author, reference, preview, meaning: 'independent correctness label, NOT writing preference or panel agreement' }, () => { trust.linkCaseOutcome(preview.predictionId, reference, [preview.digest], author); return { recorded: true, author, ...trust.inspect(Date.now()) }; });
293
+ }
294
+ if (name === 'unflagged') {
295
+ const id = trust.inspect(Date.now()).sampledIncidentIds[integer(flags.item, 0) - 1];
296
+ if (!id)
297
+ throw Error('select a frozen sampled unflagged incident number');
298
+ const author = word(flags['reference-author'], '--reference-author'), note = word(flags.note, '--note');
299
+ if (!['yes', 'no'].includes(String(flags.miss)))
300
+ throw Error('--miss yes|no required');
301
+ const bytes = learningFile(resolve(cwd, word(flags.evidence, '--evidence')), 65536);
302
+ return confirm({ author, note, incident: id, miss: flags.miss, evidenceBytes: bytes.length }, () => {
303
+ const evidence = retainArchiveSource(config.archiveRoot, { sourceId: `independent-label-${learningHash({ author, note, bytes: bytes.toString('base64') }).slice(0, 24)}`, parser: { id: 'operator-reference-evidence', version: '1' }, retention: 'exact', bytes }).manifestId;
304
+ const outcome = { kind: 'unflagged', targetId: id, value: flags.miss === 'yes', evidenceManifestId: evidence, referenceId: learningHash({ author, note, id, evidence }) };
305
+ trust.outcome(outcome, [trustOutcomeDigest(outcome)], author);
306
+ return trust.inspect(Date.now());
307
+ });
308
+ }
309
+ throw Error('unknown learning trust operation');
310
+ }
311
+ if (command === 'adoption') {
312
+ const v = workspace.comparison(word(name, 'comparison name'));
313
+ return emit({ scope: v.scopeStatement, quality: v.qualityScope, decision: v.decision, configured: v.adoptionConfigured, activation: v.activation, rollback: v.rollback,
314
+ reason: !v.adoptionConfigured ? 'independent scoped candidate/eligibility/authority not configured' : v.activation === 'recorded' ? 'activation receipt linked; existing sessions unchanged' : 'prepared/intent is not activation; use original producer registry controls', next: 'Use /grants learning in interactive Pi for authorized next-order activation/rollback.' });
315
+ }
316
+ if (command === 'outcome') {
317
+ word(name, 'comparison name');
318
+ if (flags.result === undefined)
319
+ return emit({ outcomes: workspace.comparison(name).outcomes, meaning: 'observed linkage only; no later outcome is inferred from exit zero', next: 'Record with learning outcome --result … and explicit independently reviewed evidence, or connected producer controls.' });
320
+ const status = workspace.adoptionStatus(name);
321
+ if (!status.receipt || !status.activationManifestId)
322
+ throw Error('an actual linked registry activation is required before later outcomes');
323
+ const author = word(flags['reference-author'], '--reference-author'), note = word(flags.note, '--note'), result = word(flags.result, '--result');
324
+ if (!['success', 'confirmed-defect', 'unknown'].includes(result))
325
+ throw Error('explicit success, confirmed-defect or unknown result required');
326
+ if ((flags['accepted-artifact'] === undefined) !== (flags['acceptance-evidence'] === undefined))
327
+ throw Error('accepted artifact and independent acceptance evidence must be supplied together; excerpts never imply acceptance');
328
+ const keys = ['artifact', 'original-requirement', 'current-requirement', 'evidence', ...(flags['accepted-artifact'] === undefined ? [] : ['accepted-artifact', 'acceptance-evidence'])];
329
+ const files = keys.map(key => ({ key, bytes: learningFile(resolve(cwd, word(flags[key], `--${key}`)), 1024 * 1024) }));
330
+ return confirm({ name, result, author, note, files: files.map(f => ({ kind: f.key, bytes: f.bytes.length })), meaning: 'independently supplied observation, not detector calibration or model exit status' }, () => {
331
+ const retained = new Map(files.map(f => [f.key, retainArchiveSource(config.archiveRoot, { sourceId: `outcome-${f.key}-${learningHash(f.bytes.toString('base64')).slice(0, 24)}`, parser: { id: 'operator-outcome-evidence', version: '1' }, retention: 'exact', bytes: f.bytes })]));
332
+ const audit = retainArchiveSource(config.archiveRoot, { sourceId: `outcome-reference-${learningHash({ author, note, result }).slice(0, 24)}`, parser: { id: 'operator-outcome-reference', version: '1' }, retention: 'exact', bytes: Buffer.from(JSON.stringify({ author, note, result, scopeDigest: config.scopeDigest, source: 'explicit-operator-review-not-exit-status' })) });
333
+ const observation = { id: learningHash({ author, note, result, files: [...retained.values()].map(r => r.manifestId) }), adoptionId: status.receipt.id, candidateDigest: status.receipt.candidateDigest, scopeDigest: config.scopeDigest,
334
+ originalRequirementDigest: retained.get('original-requirement').reference.sha256, currentRequirementDigest: retained.get('current-requirement').reference.sha256, outcome: result,
335
+ observedArtifactDigest: retained.get('artifact').reference.sha256, acceptedArtifactDigest: retained.get('accepted-artifact')?.reference.sha256 ?? null, acceptanceDigest: retained.get('acceptance-evidence')?.reference.sha256 ?? null, evidence: [...retained.values()].map(r => r.manifestId).concat(audit.manifestId) };
336
+ const preview = workspace.previewObservation(name, observation);
337
+ return workspace.observe(name, observation, [preview.digest], author);
338
+ });
339
+ }
340
+ }
341
+ function learningArchiveScopes(root, author) {
342
+ if (!existsSync(join(root, 'manifests')))
343
+ return [];
344
+ const selected = catalogLearningArchive(root, author).filter((c) => !!c.scopeDigest && !!c.population);
345
+ return selected.filter((c, i) => selected.findIndex(s => s.scopeDigest === c.scopeDigest && s.population === c.population) === i);
346
+ }
347
+ function terminalLearningUI() {
348
+ if (!process.stdin.isTTY || !process.stdout.isTTY)
349
+ throw Error('guided learning needs an interactive terminal; use explicit learning subcommands');
350
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
351
+ return { close: () => rl.close(), notify: text => console.log(learningDisplay(text)),
352
+ input: async (title, initial) => { const answer = await rl.question(`${title}${initial ? ` [${initial}]` : ''}: `); return answer || initial; },
353
+ select: async (title, choices) => { console.log(learningDisplay(title)); choices.forEach((c, i) => console.log(`${i + 1}. ${learningDisplay(c)}`)); const answer = await rl.question('Number (empty cancels): '); return choices[Number(answer) - 1]; },
354
+ confirm: async (title, detail) => (console.log(learningDisplay(`${title}\n${detail}`)), (await rl.question('Confirm [y/N]: ')).toLowerCase() === 'y'),
355
+ editor: async (title, text) => { console.log(learningDisplay(`${title}\n${text}`)); await rl.question('Enter to return (this is the complete display): '); return text; } };
356
+ }
357
+ export async function reviewLearningComparison(directory, name, ui) {
358
+ const workspace = openLearningWorkspace(directory);
359
+ let view = workspace.comparison(name);
360
+ ui.notify(`${view.title}\n${view.scopeStatement}\nDeliberate review, not an earned automatic question.\n${view.limitations.join('\n')}`);
361
+ const reviewed = new Set();
362
+ for (;;) {
363
+ view = workspace.comparison(name);
364
+ const choices = [...view.cards.map(c => `Read variant ${c.displayLabel} (${c.artifactDigests.length} artifact(s))`),
365
+ ...(view.revealReady ? ['Reveal model and cost'] : ['Record quality choice']), 'Adopt / reject / defer', 'Done'];
366
+ const selected = await ui.select('Comparison — quality is separate from adoption', choices);
367
+ if (!selected || selected === 'Done')
368
+ return;
369
+ const index = choices.indexOf(selected);
370
+ if (index < view.cards.length) {
371
+ const card = view.cards[index];
372
+ for (const digest of card.artifactDigests) {
373
+ const bytes = workspace.artifact(name, card.label, digest);
374
+ let text;
375
+ try {
376
+ text = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
377
+ }
378
+ catch {
379
+ ui.notify('This artifact is not UTF-8 text; use a suitable full-artifact viewer. No review acknowledgement recorded.', 'warning');
380
+ continue;
381
+ }
382
+ if (bytes.length > 1024 * 1024) {
383
+ ui.notify('Artifact exceeds interactive viewer bound. Use learning artifact in a terminal; no partial view counts as full review.', 'warning');
384
+ continue;
385
+ }
386
+ const display = learningDisplay(text), returned = await ui.editor(`Variant ${card.displayLabel} — complete artifact (${bytes.length} bytes; controls escaped)`, display);
387
+ if (returned === display && await ui.confirm('Acknowledge complete artifact review?', 'Only confirm if you reviewed the complete artifact, not an excerpt. This does not accept or adopt it.'))
388
+ reviewed.add(digest);
389
+ }
390
+ }
391
+ else if (selected === 'Record quality choice') {
392
+ const all = [...new Set(view.cards.flatMap(c => c.artifactDigests))], complete = all.length > 0 && all.every(id => reviewed.has(id));
393
+ const answers = [...(complete ? [...view.cards.map(c => `Prefer ${c.displayLabel}`), ...(view.cards.length > 1 ? ['Tie (all shown variants)'] : []), 'None acceptable'] : []), 'Insufficient evidence', 'Cancel'];
394
+ const answer = await ui.select(complete ? 'Quality — full artifacts' : 'Full artifacts not yet acknowledged; only insufficient evidence is available', answers);
395
+ if (!answer || answer === 'Cancel')
396
+ continue;
397
+ const choice = answer === 'Insufficient evidence' ? { kind: 'insufficient', labels: [] } : answer === 'None acceptable' ? { kind: 'none', labels: [] } : answer.startsWith('Tie') ? { kind: 'tie', labels: view.cards.map(c => c.label) } : { kind: 'one', labels: [view.cards.find(c => `Prefer ${c.displayLabel}` === answer).label] };
398
+ const note = await ui.input('Reason / limitations (required)');
399
+ if (!note)
400
+ continue;
401
+ if (await ui.confirm('Record immutable quality choice?', 'This locks the quality choice before reveal; it is not acceptance, calibration, or adoption.')) {
402
+ workspace.choose(name, choice, { scope: 'full-artifacts', note, reviewedArtifactDigests: [...reviewed] });
403
+ ui.notify('Quality choice durably recorded. Identity remains hidden until you choose Reveal.');
404
+ }
405
+ }
406
+ else if (selected === 'Reveal model and cost')
407
+ ui.notify(JSON.stringify(workspace.reveal(name), null, 2));
408
+ else if (selected === 'Adopt / reject / defer') {
409
+ const disposition = await ui.select('Adoption decision (not activation)', [...(view.revealReady && ['one', 'tie'].includes(view.choice.kind) ? ['adopt'] : []), 'reject', 'defer']);
410
+ if (!disposition)
411
+ continue;
412
+ const note = await ui.input('Decision reason (required)');
413
+ if (!note)
414
+ continue;
415
+ if (await ui.confirm(`Record ${disposition}?`, `${view.scopeStatement}\nNo active session or next order changes here.`)) {
416
+ workspace.decide(name, { disposition: disposition, note, priorDecisionId: view.decision?.id ?? null });
417
+ ui.notify('Decision recorded. Adoption still needs independent eligibility, authority and original registry activation.');
418
+ }
419
+ }
420
+ }
421
+ }
422
+ export async function runLearningWizard(options) {
423
+ const { directory, ui, cwd } = options;
424
+ if (!existsSync(directory)) {
425
+ ui.notify('Connect an explicitly selected retained archive. This reads no live/private Pi session and calls no model.');
426
+ const archive = await ui.input('Retained archive directory'), author = await ui.input('Review author');
427
+ if (!archive || !author)
428
+ return;
429
+ const archiveRoot = resolve(cwd, archive), scopes = learningArchiveScopes(archiveRoot, author), labels = scopes.map((s, i) => `${i + 1}. ${s.description} (${s.population})`);
430
+ const choice = scopes.length ? await ui.select('Exact retained work scope', labels) : undefined;
431
+ if (scopes.length && !choice)
432
+ return;
433
+ const selected = choice ? scopes[labels.indexOf(choice)] : null;
434
+ const population = selected?.population ?? await ui.input('Comparison-only population (no case scope available)'), scope = selected ? 'Retained case snapshot' : await ui.input('Comparison-only scope description');
435
+ if (!population || !scope)
436
+ return;
437
+ if (!await ui.confirm('Create learning workspace?', `${archiveRoot}\nPopulation: ${population}\nScope: ${scope}\nAuthor: ${author}\nFor an existing producer host use /grants learning so the exact snapshot is bound.`))
438
+ return;
439
+ createLearningWorkspace(directory, { archiveRoot, population, scopeDigest: selected?.scopeDigest ?? learningHash({ declaredScope: scope }), author });
440
+ }
441
+ const workspace = openLearningWorkspace(directory);
442
+ for (;;) {
443
+ const status = workspace.inspect(Date.now());
444
+ const selected = await ui.select('Learning — retained evidence, no model calls', ['Readiness', 'Connect retained input', 'Review cases', 'Review comparisons', 'Record adopt / reject / defer', 'Propose hypothesis', 'Link original hypothesis', 'Trust / independent labels', 'Adoption / outcomes', 'Done']);
445
+ if (!selected || selected === 'Done')
446
+ return;
447
+ try {
448
+ if (selected === 'Readiness')
449
+ ui.notify(formatLearningStatus(status));
450
+ if (selected === 'Connect retained input') {
451
+ const catalog = catalogLearningArchive(status.configuration.archiveRoot, status.configuration.author), labels = catalog.map((c, i) => `${i + 1}. ${c.kind}: ${c.description}`);
452
+ if (!labels.length) {
453
+ ui.notify('No compatible retained case batch or qualified comparison for this author. Run ordinary work first; no evidence will be fabricated.', 'warning');
454
+ continue;
455
+ }
456
+ const item = await ui.select('Retained inputs in selected archive', labels);
457
+ if (!item)
458
+ continue;
459
+ const c = catalog[labels.indexOf(item)], name = await ui.input('Short name (letters, digits, - or _)'), title = await ui.input('Display title');
460
+ if (!name || !title)
461
+ continue;
462
+ const scope = c.kind === 'comparison' ? await ui.input('What complete artifacts will this quality review assess?') : undefined;
463
+ if (c.kind === 'comparison' && !scope)
464
+ continue;
465
+ if (!await ui.confirm('Connect retained input?', `${title}\n${scope ?? 'Case nominations remain unconfirmed.'}`))
466
+ continue;
467
+ if (c.kind === 'cases')
468
+ workspace.bindCases({ name, title, version: c.version, batchId: c.manifestId });
469
+ else
470
+ workspace.bindComparison({ name, title, scopeStatement: scope, comparisonManifestId: c.manifestId, caseReference: null, hypothesisManifestId: null, priorFeedbackManifestIds: [], adoptionBinding: null });
471
+ ui.notify('Retained input connected. Reading never activates or reserves attention.');
472
+ }
473
+ if (selected === 'Review comparisons') {
474
+ const names = status.comparisons.map(c => c.name), name = await ui.select('Choose retained comparison', names);
475
+ if (name)
476
+ await reviewLearningComparison(directory, name, ui);
477
+ }
478
+ if (selected === 'Review cases') {
479
+ const name = await ui.select('Choose case batch', status.cases.map(c => c.name));
480
+ if (!name)
481
+ continue;
482
+ const p = allCases(workspace, name), labels = p.items.map((c, i) => `${i + 1}. ${c.candidate.reason} — ${c.candidate.target.obligationId} (${c.disposition})`);
483
+ const chosen = await ui.select('Retained nominations (not confirmed defects)', labels);
484
+ if (!chosen)
485
+ continue;
486
+ const item = p.items[labels.indexOf(chosen)];
487
+ await ui.editor('Case nomination — not an independent label', learningDisplay(JSON.stringify(item.candidate, null, 2)));
488
+ const refs = workspace.caseEvidence(name, item.caseManifestId), choices = refs.map((r, i) => `${i + 1}. ${r.role}: ${r.status === 'available' ? `${r.reference.parser.id} (${r.reference.retention})` : r.status}`);
489
+ let cancelled = false;
490
+ if (refs.length === 1)
491
+ ui.notify('Only nomination retained; no original observation link. This alone is not independent proof.', 'warning');
492
+ for (;;) {
493
+ const selected = await ui.select('Inspect retained case evidence (no bytes are edited)', [...choices, 'Choose disposition', 'Back']);
494
+ if (!selected || selected === 'Back') {
495
+ cancelled = true;
496
+ break;
497
+ }
498
+ if (selected === 'Choose disposition')
499
+ break;
500
+ const ref = refs[choices.indexOf(selected)], result = workspace.readCaseEvidence(name, item.caseManifestId, ref.manifestId);
501
+ if (result.status !== 'available') {
502
+ ui.notify(`Evidence unavailable: ${result.status}`, 'warning');
503
+ continue;
504
+ }
505
+ await ui.editor(`${result.reference.retention === 'exact' ? 'Exact' : 'Redacted — not full'} retained source (display only)`, artifactText(result.bytes));
506
+ }
507
+ if (cancelled)
508
+ continue;
509
+ const disposition = await ui.select('Your independent case disposition', ['confirmed_defect', 'expected_behavior', 'exemplar', 'uncertain', 'skip']);
510
+ if (!disposition)
511
+ continue;
512
+ const note = await ui.input('Evidence/reason (required)');
513
+ if (note && await ui.confirm('Record case disposition?', `${disposition}\n${note}\nThis is not a writing preference, adoption or test promotion.`)) {
514
+ workspace.decideCase(name, { caseManifestId: item.caseManifestId, priorDecisionId: item.priorDecisionId, disposition: disposition, note });
515
+ ui.notify('Case disposition durably recorded. Trust linkage is a separate action.');
516
+ }
517
+ }
518
+ if (selected === 'Record adopt / reject / defer') {
519
+ const name = await ui.select('Retained comparison (defer/reject also works when evidence is unavailable)', status.comparisons.map(c => c.name));
520
+ if (!name)
521
+ continue;
522
+ const current = workspace.decisionStatus(name);
523
+ let adopt = false;
524
+ try {
525
+ const c = workspace.comparison(name);
526
+ adopt = c.revealReady && !!c.choice && ['one', 'tie'].includes(c.choice.kind);
527
+ }
528
+ catch { /* Missing evidence cannot prevent recording a defer. */ }
529
+ const disposition = await ui.select(current.scopeStatement, [...(adopt ? ['adopt'] : []), 'reject', 'defer']), note = await ui.input('Reason (required)');
530
+ if (!disposition || !note)
531
+ continue;
532
+ if (await ui.confirm(`Record ${disposition}?`, 'Intent only. No acceptance, activation, model calls or automatic exposure.')) {
533
+ workspace.decide(name, { disposition: disposition, note, priorDecisionId: current.current?.id ?? null });
534
+ ui.notify('Decision durably recorded.');
535
+ }
536
+ }
537
+ if (selected === 'Propose hypothesis') {
538
+ const name = await ui.select('Confirmed case batch', status.cases.map(c => c.name));
539
+ if (!name)
540
+ continue;
541
+ const flags = [];
542
+ for (const [key, title] of [['name', 'Hypothesis short name'], ['intervention', 'Proposed change'], ['prediction', 'Falsifiable expected result'], ['disproof', 'What would disprove it?'], ['downside', 'Downside/risk'], ['rollback', 'Rollback plan'], ['alternative', 'Alternative explanation']]) {
543
+ const value = await ui.input(title);
544
+ if (!value)
545
+ break;
546
+ flags.push(`--${key}`, value);
547
+ }
548
+ if (flags.length !== 14)
549
+ continue;
550
+ if (await ui.confirm('Retain hypothesis?', 'Proposal only: no scenario edit, model call, adoption or causal claim.'))
551
+ await runLearningCommand(['hypothesis', name, ...flags, '--state', directory, '--confirm'], { cwd, write: t => ui.notify(t) });
552
+ }
553
+ if (selected === 'Link original hypothesis') {
554
+ const comparison = await ui.select('Comparison', status.comparisons.map(c => c.name)), batch = await ui.select('Confirmed case batch', status.cases.map(c => c.name));
555
+ if (!comparison || !batch)
556
+ continue;
557
+ const p = allCases(workspace, batch), labels = p.items.map((c, i) => `${i + 1}. ${c.candidate.target.obligationId} — ${c.disposition}`), item = await ui.select('Current independent case decision', labels), hypothesis = await ui.select('Original retained hypothesis', workspace.hypotheses().map(h => h.name));
558
+ if (!item || !hypothesis)
559
+ continue;
560
+ if (await ui.confirm('Link original evidence?', 'Only the comparison’s actual originating hypothesis is accepted. A new hypothesis cannot be retroactively presented as the experiment origin.'))
561
+ await runLearningCommand(['link', comparison, '--cases', batch, '--item', String(labels.indexOf(item) + 1), '--hypothesis', hypothesis, '--state', directory, '--confirm'], { cwd, write: t => ui.notify(t) });
562
+ }
563
+ if (selected === 'Trust / independent labels')
564
+ await trustLearningWizard(directory, cwd, ui);
565
+ if (selected === 'Adoption / outcomes') {
566
+ const name = await ui.select('Comparison', status.comparisons.map(c => c.name));
567
+ if (!name)
568
+ continue;
569
+ await runLearningCommand(['adoption', name, '--state', directory], { cwd, write: t => ui.notify(t) });
570
+ await runLearningCommand(['outcome', name, '--state', directory], { cwd, write: t => ui.notify(t) });
571
+ if (await ui.confirm('Record a later independent observation?', 'Requires an actual linked activation. Unknown is valid; exit zero is not quality. No adoption or rollback is performed.')) {
572
+ const result = await ui.select('Observed result', ['success', 'confirmed-defect', 'unknown']);
573
+ if (!result)
574
+ continue;
575
+ const flags = ['--result', result];
576
+ for (const [key, title] of [['artifact', 'Complete later artifact file'], ['original-requirement', 'Original requirement file'], ['current-requirement', 'Current requirement file'], ['evidence', 'Independent outcome evidence file'], ['reference-author', 'Independent reference author'], ['note', 'Evidence/reason']]) {
577
+ const value = await ui.input(title);
578
+ if (!value)
579
+ break;
580
+ flags.push(`--${key}`, value);
581
+ }
582
+ if (flags.length !== 14)
583
+ continue;
584
+ if (await ui.confirm('Was this exact artifact independently accepted?', 'No is normal. Never substitute an excerpt preference for full artifact acceptance.')) {
585
+ const artifact = await ui.input('Exact independently accepted artifact file'), evidence = await ui.input('Independent acceptance evidence file');
586
+ if (!artifact || !evidence)
587
+ continue;
588
+ flags.push('--accepted-artifact', artifact, '--acceptance-evidence', evidence);
589
+ }
590
+ if (await ui.confirm('Retain this independent outcome?', 'Links only to the scoped adoption. It does not become calibration, improvement proof or automatic rollback.'))
591
+ await runLearningCommand(['outcome', name, ...flags, '--state', directory, '--confirm'], { cwd, write: t => ui.notify(t) });
592
+ }
593
+ }
594
+ }
595
+ catch (e) {
596
+ ui.notify(e instanceof Error ? e.message : 'Learning action failed; no success claimed.', 'error');
597
+ }
598
+ }
599
+ }
600
+ async function trustLearningWizard(directory, cwd, ui) {
601
+ const workspace = openLearningWorkspace(directory), action = await ui.select('Trust — independent evidence, never preference', ['Readiness', 'Add unflagged evidence before freeze', 'Configure frozen policy', 'Link current case label', 'Label frozen unflagged sample', 'Back']);
602
+ if (!action || action === 'Back')
603
+ return;
604
+ if (action === 'Readiness') {
605
+ ui.notify(formatLearningTrust(workspace.inspect(Date.now()).trust));
606
+ return;
607
+ }
608
+ if (action === 'Add unflagged evidence before freeze') {
609
+ const file = await ui.input('Explicit retained/source evidence file (no private sessions)'), name = await ui.input('Incident short name'), title = await ui.input('What unflagged incident does this evidence document?'), split = await ui.select('Predeclare split', ['calibration', 'heldout', 'tuning']);
610
+ if (file && name && title && split && await ui.confirm('Retain unflagged incident?', 'This is not a correctness label. Do not include secrets or private sessions.'))
611
+ await runLearningCommand(['trust', 'sample', file, '--name', name, '--title', title, '--split', split, '--state', directory, '--confirm'], { cwd, write: t => ui.notify(t) });
612
+ return;
613
+ }
614
+ if (action === 'Configure frozen policy') {
615
+ const name = await ui.select('Case batch', workspace.inspect(0).cases.map(c => c.name));
616
+ if (!name)
617
+ return;
618
+ const detectors = [...new Set(allCases(workspace, name).items.map(c => c.candidate.detector.id))], detector = await ui.select('Detector (version and population remain exact)', detectors), split = await ui.select('Predeclare evaluation split', ['calibration', 'heldout', 'tuning']);
619
+ if (!detector || !split)
620
+ return;
621
+ const max = await ui.input('Maximum unflagged sample (0–32; missing samples stay missing)', '0');
622
+ if (max === undefined)
623
+ return;
624
+ const flags = { cases: name, detector, split, 'max-unflagged': max };
625
+ if (await ui.confirm('Configure automatic questions?', 'Default is silent. Explicit policy plus sufficient independent labels is required. This does not show a question or refill attention.')) {
626
+ const count = await ui.input('Minimum resolved independent incidents', '10'), lower = await ui.input('Minimum 95% interval lower bound', '0.8'), quota = await ui.input('Total question attention budget', '5'), days = await ui.input('Policy validity in days', '7');
627
+ if (!count || !lower || !quota || !days)
628
+ return;
629
+ Object.assign(flags, { 'minimum-resolved': count, 'minimum-lower-bound': lower, attention: quota, expires: days });
630
+ }
631
+ const setup = trustSetup(workspace, flags), preview = previewLearningTrust(directory, setup);
632
+ if (await ui.confirm('Freeze this exact scoped policy?', JSON.stringify({ ...preview, notice: 'No independent labels are created. Missing evidence keeps automatic questions silent.' }, null, 2))) {
633
+ configureLearningTrust(directory, setup, [preview.policyDigest]);
634
+ ui.notify('Policy and predictions retained; independent labels still pending.');
635
+ }
636
+ return;
637
+ }
638
+ if (action === 'Link current case label') {
639
+ const name = await ui.select('Case batch', workspace.inspect(0).cases.map(c => c.name));
640
+ if (!name)
641
+ return;
642
+ const p = allCases(workspace, name), labels = p.items.map((c, i) => `${i + 1}. ${c.candidate.reason}: ${c.disposition}`), chosen = await ui.select('Current independently reviewed case', labels), author = await ui.input('Independent reference author');
643
+ if (!chosen || !author)
644
+ return;
645
+ if (await ui.confirm('Link independent correctness label?', 'Use the actual case disposition; never treat writing preference, panel agreement or model self-report as detector accuracy.'))
646
+ await runLearningCommand(['trust', 'label', '--cases', name, '--item', String(labels.indexOf(chosen) + 1), '--reference-author', author, '--state', directory, '--confirm'], { cwd, write: t => ui.notify(t) });
647
+ return;
648
+ }
649
+ const trust = workspace.trust(), view = trust.inspect(Date.now()), labels = view.sampledIncidentIds.map((id, i) => `${i + 1}. ${id.slice(0, 12)} (frozen unflagged incident)`), chosen = await ui.select('Frozen sample', labels);
650
+ if (!chosen)
651
+ return;
652
+ const miss = await ui.select('Independent review: was a defect missed?', ['yes', 'no']), file = await ui.input('Independent evidence file'), author = await ui.input('Independent reference author'), note = await ui.input('Evidence/reason');
653
+ if (miss && file && author && note && await ui.confirm('Record independent sample outcome?', 'This label is scope-bound and separate from positive precision.'))
654
+ await runLearningCommand(['trust', 'unflagged', '--item', String(labels.indexOf(chosen) + 1), '--miss', miss, '--evidence', file, '--reference-author', author, '--note', note, '--state', directory, '--confirm'], { cwd, write: t => ui.notify(t) });
655
+ }
656
+ //# sourceMappingURL=learning.js.map
@@ -0,0 +1,133 @@
1
+ # Factory: work, review, learn
2
+
3
+ **Product guide for the 0.16.0 release candidate**, paired with pi-daddy **0.27.0**;
4
+ Principal **3.2.0** is unchanged. The prior audited baseline was harness 0.15.0 /
5
+ producer 0.26.1. Harness PR80 is merged and independently approved with green CI;
6
+ source/built cross-package checks passed with synthetic fixtures, not human evidence.
7
+ Canonical release packaging, publication and final installed-flow verification remain
8
+ separate pending gates. After installation, expect `skill-harness --version` **0.16.0**
9
+ and producer package **0.27.0**; check `skill-harness learning help` and loaded Pi resources.
10
+ Do not assume an existing session reloaded because a package was updated. Use a **fresh
11
+ Pi session** when upgrading an already-published immutable dashboard bridge: `/reload`
12
+ cannot replace the old global API. No other running session needs to be stopped.
13
+
14
+ ## Who does what?
15
+
16
+ **pi-daddy** coordinates permitted work and owns execution, steering and the future-order
17
+ registry. **Herdr** displays its sessions and side panel. **skill-harness** retains
18
+ measurement and learning evidence. The panel is not an autonomous worker. Finished
19
+ execution is not human acceptance.
20
+
21
+ Use the producer's `/grants work` guided setup for multiple obligations, dependencies,
22
+ priority and explicit agent/model/thinking selections, then its supported run/host controls.
23
+ For existing single-task use, `pi-daddy work add --id report --outcome "Review the report"`
24
+ remains supported. Only listed controls are available. Pausing new dispatch does not
25
+ cancel running work; changing a next-order policy does not mutate active sessions.
26
+ Principal's ordinary current/stale/superseded reports remain unchanged.
27
+
28
+ ## Open learning in Pi
29
+
30
+ With both matching extensions loaded, use **`/grants learning`** from the current host.
31
+ The producer binds the exact current snapshot, archive, population and local review author.
32
+ A changed scope must report a mismatch, not silently reuse an old learning workspace.
33
+
34
+ The harness also exposes **`/skill-harness learning --state "WORKSPACE"`** (use the
35
+ workspace path displayed by the host), or `skill-harness learning --state "WORKSPACE"`
36
+ in a terminal. These open the **same guided flow and durable records**, not another app.
37
+
38
+ 1. **Readiness** explains what's retained, what's missing, and why automatic questions
39
+ are silent. Opening a review is deliberate: it neither reserves attention nor
40
+ represents an earned automatic question. It starts no model or delegation.
41
+ 2. **Connect retained input** lists compatible case batches and qualified comparisons
42
+ already in the selected archive. Pick one and give it a short name and title.
43
+ No hand-written manifests or ledger JSON. No compatible evidence is an honest empty state.
44
+ 3. **Review cases** shows nominations and lets you open their actual retained evidence
45
+ sources by selection (CLI: `learning evidence BATCH --item N --evidence N`). Redacted
46
+ or unavailable sources are marked, never called complete evidence. Record confirmed defect, expected
47
+ behavior, exemplar, uncertain or skip, with a reason. Corrections are append-only.
48
+ Confirming a case is not promoting a test or establishing a cause.
49
+ 4. **Review comparisons** opens complete retained artifacts under A/B labels. Acknowledge
50
+ each complete artifact only after reviewing it. Then choose one, tie, none acceptable,
51
+ or insufficient evidence. **Reveal model/cost** is a separate action, enabled only after
52
+ the durable quality receipt. Artifact contents themselves may disclose identity.
53
+ 5. **Record adopt / reject / defer** records a separate scope-bound decision. Reject and defer
54
+ are normal outcomes; neither forces a preference or reveal. Missing evidence can be
55
+ durably deferred. Adopt is intent, not activation.
56
+ 6. **Propose hypothesis** records prediction, disproof, downside, rollback and a competing
57
+ explanation from an actual case. **Link original hypothesis** only accepts the comparison's
58
+ actual frozen origin, never a retrospective replacement.
59
+
60
+ Previously retained excerpt feedback concerned
61
+ **excerpts**. Keep that receipt unchanged and linked as prior feedback. It is not full-artifact
62
+ acceptance, detector calibration or a fresh blind experiment. An old unscoped quality choice
63
+ also does not automatically become a full-artifact acknowledgement.
64
+
65
+ ## Configure trust without writing manifests
66
+
67
+ Open **Trust / independent labels** in the same guided flow:
68
+
69
+ - Before freezing, explicitly add any real unflagged incident evidence and select its split.
70
+ Only selected evidence files are retained; do not include private sessions or secrets.
71
+ - Select a retained case batch, detector and evaluation split. The product derives the
72
+ cohort and predictions from actual nominations and groups repeated target observations
73
+ into one incident. Detector version and population remain exact.
74
+ - Automatic questions default to **silent**. To configure them, explicitly select minimum
75
+ resolved independent incidents, the confidence lower bound, attention budget and expiry.
76
+ The form previews the exact frozen policy. It creates **no independent labels**.
77
+ - Review cases independently, then choose **Link current case label**. Unflagged sample
78
+ labels have their own explicit evidence/author step. Preference, model self-report,
79
+ panel agreement and exit zero are not independent correctness labels.
80
+
81
+ Missing calibration, missing unflagged samples, held-out absence, conflicting labels,
82
+ expired policy and exhausted attention stay visible. Reopening neither resets choices nor
83
+ refills attention. Correcting a linked case withdraws its old label from confidence.
84
+ The original producer closing path still requires presence, quiescence and its exposure gate.
85
+
86
+ ## Scoped adoption and later work
87
+
88
+ Use **`/grants learning` in interactive Pi** for activation/rollback. The matching producer
89
+ implements two separate profiles: unchanged **`fixed-policy-v1`**, and
90
+ **`ordinary-work-policy-v1`** for model/effort-only changes to the same selected task IDs and
91
+ agent definitions. The latter does not change instructions, skills, grants, capabilities,
92
+ topology or assessment policy; do not disguise those changes as model/effort or fixed policy.
93
+ The producer prepares policy bytes and owns independent authority and the original registry.
94
+ Source/built integration has been checked with synthetic fixtures; final installed-release
95
+ verification remains pending. Bare legacy fixed-policy views lacking applied activation
96
+ proof defer; they must not be treated as activation receipts.
97
+
98
+ Activation needs the exact selected candidate, original confirmed case/hypothesis/comparison,
99
+ current independently verified eligible facts, explicit adoption authority, and registry
100
+ revision consent. The original registry revalidates these again. Only its actual activation
101
+ receipt advances the learning record to activated. New ordinary-work orders pin the policy
102
+ under fresh eligibility confirmation; active or history-bearing runs never migrate.
103
+ Rollback similarly needs its own authorized request and original applied registry receipt.
104
+ A stored lifecycle link, human preference or prepared receipt cannot substitute for either.
105
+
106
+ **Adoption / outcomes** can inspect linked later observations or record one using complete
107
+ artifact, original/current requirement and independent evidence files. Unknown is valid.
108
+ Acceptance requires both the exact accepted artifact and independent acceptance evidence.
109
+ Changed requirements, changed artifact, out-of-scope, success, caught defect and escape remain
110
+ distinct. No outcome triggers automatic rollback, and no single outcome proves improvement.
111
+
112
+ ## CLI and local archive use
113
+
114
+ ```sh
115
+ skill-harness learning status --state "WORKSPACE" # human readiness
116
+ skill-harness learning status --state "WORKSPACE" --json # integration/details
117
+ skill-harness learning review reports --state "WORKSPACE"
118
+ skill-harness learning help # all explicit operations
119
+ skill-harness learning guide # this guide, packaged offline
120
+ skill-harness learning current # packaged requirement register
121
+ ```
122
+
123
+ Without a producer host, `skill-harness learning` guides connection to an explicitly selected
124
+ archive and selecting an actual retained case scope. Or use
125
+ `learning init --archive DIR --author NAME --confirm`; multiple retained scopes ask you to
126
+ select `--scope-item N`. Snapshot digest and population are derived from actual cases, not
127
+ typed hashes. For a comparison-only archive with no cases, `--scope TEXT --population TEXT`
128
+ can explicitly declare a local review scope; it cannot bind mismatching case snapshots.
129
+ Use the producer flow to connect a host. Noninteractive writes require explicit `--confirm`.
130
+ All learning commands are offline/no-model. Command completion is never a quality verdict.
131
+ Local author attribution and content hashes are not authentication or hostile-owner attestation.
132
+
133
+ Current requirements and remaining evidence: [STATUS.md](STATUS.md).
package/docs/STATUS.md ADDED
@@ -0,0 +1,45 @@
1
+ # Factory requirement register — 0.16.0 release preparation
2
+
3
+ Current entry point: [PRODUCT-GUIDE.md](PRODUCT-GUIDE.md). The dated
4
+ [CURRENT.md](CURRENT.md), build reports and transport decisions are retained history,
5
+ not current instructions to supply manifests or judge engineering repairs.
6
+
7
+ ## Baseline and candidate
8
+
9
+ Release pair: **skill-harness 0.16.0 / pi-daddy 0.27.0**; Principal 3.2.0 is unchanged.
10
+ The prior audited baseline was harness 0.15.0 (`a725579`) / producer 0.26.1 (`93a9b01`).
11
+ Harness PR80 merged at `c6cd55d4db9ccf9a1ec790459c3cc5e6b1eac69f` with green CI and
12
+ Sol approval. Actual source/built cross-package learning/registry checks passed with
13
+ synthetic fixtures. This is **release preparation**, not publication, final installed
14
+ proof, human approval, calibration or measured improvement. Loaded resources, source
15
+ HEAD, canonical archives, published bytes and active sessions remain separate identities.
16
+ Canonical packing, publication and final installed verification are pending.
17
+
18
+ | Requirement | Current implementation / boundary | Remaining evidence |
19
+ |---|---|---|
20
+ | Understandable side panel | Producer owns selected-outcome-first status, observed agents, available controls and Details. | Matching producer's installed narrow-pane/host checks. No invented progress/ETA/acceptance. |
21
+ | Ordinary multi-obligation work | Producer owns work setup, dependencies, bounded parallel/sequential execution, model/effort choice and revision consent. | Installed checks of the matching producer's supported profiles. Existing Principal workflows remain unchanged. |
22
+ | Retained cases/comparisons | Harness learning workspace composes existing archive, case-review, blind-choice and CAS journal primitives. Named bindings and guided CLI/Pi entrypoints; missing/stale/mismatched inputs defer. | Final installed producer connection; source review/CI and synthetic source/built linkage passed. No new model evidence claimed. |
23
+ | Complete quality review | Complete-output acknowledgement, durable choice before reveal, immutable replay, separate adopt/reject/defer. Prior excerpt feedback stays separate. | A real full-artifact product choice remains absent where not actually recorded. Excerpt preference is not acceptance. |
24
+ | Scope-bound trust | Guided frozen cohort/policy, actual nomination predictions, incident grouping, explicit independent case/unflagged labels and pure exposure preview. Existing automatic gates unchanged. | Real independent labels, unflagged coverage and held-out evidence where absent. Implementation cannot manufacture them. |
25
+ | Hypothesis and comparison linkage | Human-authored falsifiable hypothesis form, confirmed-case linkage, monotonic optional context and retained lifecycle navigation. | Ongoing weekly model-backed retro is not demonstrated or automatically scheduled. Existing bounded weekly/model primitives remain separate authorized paths. |
26
+ | Adoption and rollback | Harness validates scope/choice/current independent authority and eligible facts. Producer source supports unchanged `fixed-policy-v1` plus separate `ordinary-work-policy-v1` (model/effort only, same task IDs/agent definitions and scope/assessment). Producer owns activation/rollback and fresh-eligibility next-order pins; profile-agnostic harness linkage retains original receipts. | Synthetic source/built original activation/rollback linkage passed. Configured production adoption/rollback and final installed integration remain unverified; bare legacy views without applied activation proof defer. No active/history-bearing run migration or instruction/skill/grant/capability/topology/assessment expansion. |
27
+ | Later outcomes | Explicit file-based CLI/Pi observation forms plus producer API linkage; independent reference attribution; changed requirements/artifacts/out-of-scope/unknown remain distinct. | Genuine later work, exact acceptance and held-out efficacy where absent. No automatic rollback or universal quality score. |
28
+ | Packaging / portability | Public adapter exports, additive frozen dashboard bridge, CLI export and bundled Pi entrypoint; guide/register shipped offline via `learning guide` / `learning current`. A pre-existing immutable bridge needs a fresh Pi session. | Release-metadata review/CI, exact merged canonical pack with retained archives, empty-prefix installed Pi/registry verification, then authorized publication. |
29
+
30
+ ## Historical claims remain bounded
31
+
32
+ - The failure-derived abort-race case and the public-weather comparison are different chains.
33
+ - Previously retained feedback covered excerpts only. It does not advance full comparison
34
+ acceptance or detector trust.
35
+ - Earlier no-tool measured SDK observations do not establish universal ordinary-child Usage,
36
+ writable-effect containment or actual provider-internal reasoning effort.
37
+ - No OS sandbox or malicious-local-owner protection has been added.
38
+ - No historical session/evidence, auth or unrelated worktree was modified in this candidate.
39
+
40
+ Feature source review/CI and source/built cross-package checks are completed evidence,
41
+ not final released-install proof. Metadata preparation runs consistency/diff validation only:
42
+ no builds, installs, suites or model calls. Canonical pack and actual installed two-extension
43
+ verification remain release gates under `PUBLISHING.md`. Prior live route smokes remain
44
+ historical and are not replayed merely for a version bump. Human acceptance, calibration and
45
+ later efficacy still require their own real evidence.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skill-harness/cli",
3
- "version": "0.14.0",
3
+ "version": "0.16.0",
4
4
  "description": "skill-harness CLI — run, grade, review, and lint agent-skill scenarios on the pi harness",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -10,7 +10,8 @@
10
10
  },
11
11
  "exports": {
12
12
  ".": "./dist/cli.js",
13
- "./serve": "./dist/serve.js"
13
+ "./serve": "./dist/serve.js",
14
+ "./learning": "./dist/learning.js"
14
15
  },
15
16
  "keywords": [
16
17
  "agent-skills",
@@ -27,6 +28,7 @@
27
28
  "dist/**/*.js",
28
29
  "dist/**/*.d.ts",
29
30
  "assets",
31
+ "docs/*.md",
30
32
  "LICENSE",
31
33
  "README.md"
32
34
  ],
@@ -44,7 +46,7 @@
44
46
  "prepack": "node ../../scripts/release-pack.mjs --guard"
45
47
  },
46
48
  "dependencies": {
47
- "@skill-harness/core": "0.14.0",
48
- "@skill-harness/adapters": "0.14.0"
49
+ "@skill-harness/core": "0.16.0",
50
+ "@skill-harness/adapters": "0.16.0"
49
51
  }
50
52
  }