@skill-harness/cli 0.15.0 → 0.17.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 +12 -0
- package/dist/cli.js +7 -0
- package/dist/learning.d.ts +26 -0
- package/dist/learning.js +684 -0
- package/dist/session-learning.d.ts +12 -0
- package/dist/session-learning.js +148 -0
- package/docs/PRODUCT-GUIDE.md +190 -0
- package/docs/STATUS.md +56 -0
- package/package.json +6 -4
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 session help whole-session retrospective import, timeline and annotations\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>;
|
package/dist/learning.js
ADDED
|
@@ -0,0 +1,684 @@
|
|
|
1
|
+
import { runSessionLearning, reviewSessionLearning } from './session-learning.js';
|
|
2
|
+
import { resolve, join, dirname } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
5
|
+
import { createInterface } from 'node:readline/promises';
|
|
6
|
+
import { createLearningWorkspace, openLearningWorkspace, catalogLearningArchive, learningHash, learningFile, retainArchiveSource, previewLearningTrust, configureLearningTrust, previewLearningCaseLabel, trustOutcomeDigest, } from '@skill-harness/adapters';
|
|
7
|
+
export const LEARNING_HELP = `Learning — retained evidence, no model calls
|
|
8
|
+
learning open guided review (interactive terminal)
|
|
9
|
+
learning init --archive DIR --author NAME [--scope-item N] --confirm
|
|
10
|
+
[--scope TEXT --population TEXT only for a comparison-only archive without retained cases]
|
|
11
|
+
learning session help whole-session retrospective import, timeline and annotations
|
|
12
|
+
learning status [--json] readiness; never automatic exposure
|
|
13
|
+
learning import list retained batches/comparisons
|
|
14
|
+
learning import --item N --name NAME --title TEXT --scope-note TEXT --confirm
|
|
15
|
+
learning cases NAME [--offset N]
|
|
16
|
+
learning evidence NAME --item N [--evidence N] inspect only the selected case's retained sources
|
|
17
|
+
learning case NAME --item N --disposition confirmed_defect|expected_behavior|exemplar|uncertain|skip --note TEXT --confirm
|
|
18
|
+
learning comparisons
|
|
19
|
+
learning review NAME guided complete-output review in a terminal
|
|
20
|
+
learning artifact NAME --variant A [--artifact N]
|
|
21
|
+
learning choose NAME --kind one|tie|none|insufficient [--variants A,B] --full-review --note TEXT --confirm
|
|
22
|
+
learning reveal NAME requires durable full-scope quality receipt
|
|
23
|
+
learning decide NAME --disposition adopt|reject|defer --note TEXT --confirm
|
|
24
|
+
learning hypothesis NAME --name SLUG --intervention TEXT --prediction TEXT --disproof TEXT --downside TEXT --rollback TEXT --alternative TEXT --confirm
|
|
25
|
+
learning link NAME --cases BATCH --item N --hypothesis SLUG --confirm
|
|
26
|
+
learning trust sample FILE --name SLUG --title TEXT [--split calibration|heldout|tuning] --confirm
|
|
27
|
+
learning trust setup --cases NAME [--detector ID] [--split calibration|heldout|tuning] [--max-unflagged N]
|
|
28
|
+
[--attention N --minimum-resolved N --minimum-lower-bound T --expires DAYS] --confirm
|
|
29
|
+
learning trust status
|
|
30
|
+
learning trust label --cases NAME --item N --reference-author NAME --confirm
|
|
31
|
+
learning trust unflagged --item N --miss yes|no --evidence FILE --reference-author NAME --note TEXT --confirm
|
|
32
|
+
learning adoption NAME scope/decision/readiness; activation is producer-owned
|
|
33
|
+
learning outcome NAME inspect observed outcomes
|
|
34
|
+
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
|
|
35
|
+
learning guide | current installed portable guide / requirement register
|
|
36
|
+
All commands accept --state DIR (default .skill-harness/learning), --json for structured output.
|
|
37
|
+
--confirm authorizes only the displayed local write, never models, acceptance or registry activation.
|
|
38
|
+
No raw manifest authoring. Import selects actual retained inputs. Exit zero is command completion, not quality.
|
|
39
|
+
Registry activation/rollback use /grants learning in interactive Pi, with independent authority.`;
|
|
40
|
+
/** Escape terminal control/format characters in untrusted retained display text. Exact archive
|
|
41
|
+
* bytes never change; output honestly identifies this as a display representation. */
|
|
42
|
+
export function learningDisplay(text) {
|
|
43
|
+
return text.replace(/[\u0000-\u0008\u000b-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/g, c => `\\u${c.charCodeAt(0).toString(16).padStart(4, '0')}`);
|
|
44
|
+
}
|
|
45
|
+
function parse(argv) {
|
|
46
|
+
const args = [], flags = {};
|
|
47
|
+
for (let i = 0; i < argv.length; i++) {
|
|
48
|
+
const token = argv[i];
|
|
49
|
+
if (!token.startsWith('--')) {
|
|
50
|
+
args.push(token);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const eq = token.indexOf('='), key = token.slice(2, eq < 0 ? undefined : eq);
|
|
54
|
+
if (!key || Object.hasOwn(flags, key))
|
|
55
|
+
throw Error('duplicate or empty learning option');
|
|
56
|
+
if (eq >= 0)
|
|
57
|
+
flags[key] = token.slice(eq + 1);
|
|
58
|
+
else if (['confirm', 'json', 'full-review', 'help'].includes(key))
|
|
59
|
+
flags[key] = true;
|
|
60
|
+
else {
|
|
61
|
+
if (!argv[i + 1] || argv[i + 1].startsWith('--'))
|
|
62
|
+
throw Error(`--${key} needs a value`);
|
|
63
|
+
flags[key] = argv[++i];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { args, flags };
|
|
67
|
+
}
|
|
68
|
+
const word = (v, name) => { if (typeof v !== 'string' || !v.trim())
|
|
69
|
+
throw Error(`${name} is required`); return v; };
|
|
70
|
+
const integer = (v, fallback) => { const n = v === undefined ? fallback : Number(v); if (!Number.isSafeInteger(n) || n < 0)
|
|
71
|
+
throw Error('nonnegative integer required'); return n; };
|
|
72
|
+
function artifactText(bytes) {
|
|
73
|
+
try {
|
|
74
|
+
return learningDisplay(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
throw Error('Retained source is not UTF-8 text; no complete text view is claimed');
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function allCases(workspace, name) {
|
|
81
|
+
const page = workspace.cases(name), items = [...page.items];
|
|
82
|
+
for (let offset = 5; offset < page.total; offset += 5)
|
|
83
|
+
items.push(...workspace.cases(name, offset).items);
|
|
84
|
+
return { ...page, items };
|
|
85
|
+
}
|
|
86
|
+
function caseAt(workspace, name, index) {
|
|
87
|
+
const page = allCases(workspace, name), item = page.items[index - 1];
|
|
88
|
+
if (!item)
|
|
89
|
+
throw Error('select a listed case number');
|
|
90
|
+
return { page, item };
|
|
91
|
+
}
|
|
92
|
+
function trustSetup(workspace, flags) {
|
|
93
|
+
const caseName = word(flags.cases, '--cases'), cases = allCases(workspace, caseName).items;
|
|
94
|
+
const detectorId = typeof flags.detector === 'string' ? flags.detector : cases[0]?.candidate.detector.id;
|
|
95
|
+
if (!detectorId)
|
|
96
|
+
throw Error('no retained detector cases available');
|
|
97
|
+
const attention = integer(flags.attention, 0), days = integer(flags.expires, 7), split = String(flags.split ?? 'calibration');
|
|
98
|
+
if (attention && (flags['minimum-resolved'] === undefined || flags['minimum-lower-bound'] === undefined))
|
|
99
|
+
throw Error('automatic questions need explicit --minimum-resolved and --minimum-lower-bound');
|
|
100
|
+
const minimumLowerBound = Number(flags['minimum-lower-bound']);
|
|
101
|
+
if (attention && (!Number.isFinite(minimumLowerBound) || minimumLowerBound < 0 || minimumLowerBound > 1 || days < 1 || days > 365))
|
|
102
|
+
throw Error('invalid explicit exposure policy');
|
|
103
|
+
return { caseName, detectorId, split, maxUnflagged: integer(flags['max-unflagged'], 0), unflagged: workspace.unflagged().map(({ incidentId, manifestId, split }) => ({ incidentId, manifestId, split })),
|
|
104
|
+
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 };
|
|
105
|
+
}
|
|
106
|
+
export function formatLearningStatus(view) {
|
|
107
|
+
const lines = [`Learning — ${view.configuration.population}`, 'Deliberate review; no model calls, automatic questions, or activation from reading.'];
|
|
108
|
+
for (const c of view.cases)
|
|
109
|
+
lines.push(`Cases: ${c.title} (${c.name}) — ${c.state === 'ready' ? `${c.total} retained nomination(s)` : c.reason}`);
|
|
110
|
+
for (const c of view.comparisons)
|
|
111
|
+
lines.push(`Comparison: ${c.title} (${c.name}) — ${c.reason}${'decision' in c && c.decision ? `; decision: ${c.decision.disposition}` : ''}`);
|
|
112
|
+
for (const s of view.sessions)
|
|
113
|
+
lines.push('Session: ' + s.title + ' (' + s.name + ') — ' + s.reason);
|
|
114
|
+
if (!view.cases.length && !view.comparisons.length && !view.sessions.length)
|
|
115
|
+
lines.push('No inputs connected. Choose Connect retained input; missing evidence is not a failure or acceptance.');
|
|
116
|
+
lines.push(formatLearningTrust(view.trust));
|
|
117
|
+
return learningDisplay(lines.join('\n'));
|
|
118
|
+
}
|
|
119
|
+
export function formatLearningTrust(trust) {
|
|
120
|
+
const advice = trust.automaticExposure;
|
|
121
|
+
const calibration = trust.calibration;
|
|
122
|
+
const sample = trust.unflagged;
|
|
123
|
+
const lines = [`Automatic questions: ${advice?.mode ?? 'silent'} — ${advice?.reason ?? trust.reason ?? 'not configured'} (reading reserves no attention).`];
|
|
124
|
+
for (const r of calibration?.reports ?? [])
|
|
125
|
+
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)}%)` : ''}.`);
|
|
126
|
+
if (sample)
|
|
127
|
+
lines.push(`Unflagged sample: ${sample.sampled} frozen, ${sample.resolved} independently labelled, ${sample.misses} observed miss(es).${sample.sampled ? '' : ' No unflagged coverage established.'}`);
|
|
128
|
+
if (typeof trust.attentionUsed === 'number')
|
|
129
|
+
lines.push(`Attention already used: ${trust.attentionUsed}. Reopening does not refill it.`);
|
|
130
|
+
return learningDisplay(lines.join('\n'));
|
|
131
|
+
}
|
|
132
|
+
export async function runLearningCommand(argv, options = {}) {
|
|
133
|
+
const { args, flags } = parse(argv), cwd = options.cwd ?? process.cwd(), directory = resolve(cwd, typeof flags.state === 'string' ? flags.state : '.skill-harness/learning');
|
|
134
|
+
const out = options.write ?? console.log;
|
|
135
|
+
const emit = (value) => { out(flags.json ? JSON.stringify(value, null, 2) : typeof value === 'string' ? learningDisplay(value) : learningDisplay(JSON.stringify(value, null, 2))); return value; };
|
|
136
|
+
const confirm = (preview, write) => {
|
|
137
|
+
if (flags.confirm !== true)
|
|
138
|
+
return emit({ state: 'confirmation-required', preview, next: 'Repeat with --confirm to authorize this exact local action.' });
|
|
139
|
+
return emit(write());
|
|
140
|
+
};
|
|
141
|
+
const global = ['state', 'json', 'confirm', 'help'];
|
|
142
|
+
const allowed = {
|
|
143
|
+
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: []
|
|
144
|
+
};
|
|
145
|
+
const command = args[0];
|
|
146
|
+
if (command === 'session')
|
|
147
|
+
return runSessionLearning(args.slice(1), flags, { cwd, directory, emit });
|
|
148
|
+
if (command === 'guide' || command === 'current') {
|
|
149
|
+
const file = command === 'guide' ? 'PRODUCT-GUIDE.md' : 'STATUS.md', base = dirname(fileURLToPath(import.meta.url));
|
|
150
|
+
const selected = [resolve(base, '../docs', file), resolve(base, '../../../docs/factory', file)].find(p => existsSync(p));
|
|
151
|
+
if (!selected)
|
|
152
|
+
throw Error('packaged learning guide missing; reinstall a complete compatible package');
|
|
153
|
+
return emit(learningFile(selected, 65536).toString('utf8'));
|
|
154
|
+
}
|
|
155
|
+
if (flags.help || command === 'help')
|
|
156
|
+
return emit(LEARNING_HELP);
|
|
157
|
+
if (!command) {
|
|
158
|
+
const terminal = options.ui ? null : terminalLearningUI();
|
|
159
|
+
try {
|
|
160
|
+
return await runLearningWizard({ cwd, directory, ui: options.ui ?? terminal });
|
|
161
|
+
}
|
|
162
|
+
finally {
|
|
163
|
+
terminal?.close();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (!Object.hasOwn(allowed, command))
|
|
167
|
+
throw Error('unknown learning command; use learning help');
|
|
168
|
+
const unexpected = Object.keys(flags).filter(k => !global.includes(k) && !allowed[command].includes(k));
|
|
169
|
+
if (unexpected.length)
|
|
170
|
+
throw Error(`unsupported learning option: --${unexpected[0]}`);
|
|
171
|
+
if (command === 'init') {
|
|
172
|
+
const archiveRoot = resolve(cwd, word(flags.archive, '--archive')), author = word(flags.author, '--author'), scopes = learningArchiveScopes(archiveRoot, author);
|
|
173
|
+
if (scopes.length > 1 && flags['scope-item'] === undefined)
|
|
174
|
+
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.' });
|
|
175
|
+
const selected = scopes[integer(flags['scope-item'], 1) - 1];
|
|
176
|
+
if (scopes.length && !selected)
|
|
177
|
+
throw Error('select a listed retained scope number');
|
|
178
|
+
if (selected && flags.population !== undefined && flags.population !== selected.population)
|
|
179
|
+
throw Error('population differs from retained case scope');
|
|
180
|
+
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 };
|
|
181
|
+
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()));
|
|
182
|
+
}
|
|
183
|
+
if (!existsSync(directory))
|
|
184
|
+
throw Error('learning workspace not connected; use learning init or /grants learning with the current host');
|
|
185
|
+
const workspace = openLearningWorkspace(directory), config = workspace.configuration(), name = args[1];
|
|
186
|
+
if (command === 'status') {
|
|
187
|
+
const view = workspace.inspect(Date.now());
|
|
188
|
+
return emit(flags.json ? view : formatLearningStatus(view));
|
|
189
|
+
}
|
|
190
|
+
if (command === 'import') {
|
|
191
|
+
const catalog = catalogLearningArchive(config.archiveRoot, config.author);
|
|
192
|
+
if (flags.item === undefined)
|
|
193
|
+
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.');
|
|
194
|
+
const item = catalog[integer(flags.item, 0) - 1];
|
|
195
|
+
if (!item)
|
|
196
|
+
throw Error('select a listed retained input number');
|
|
197
|
+
const binding = { name: word(flags.name, '--name'), title: word(flags.title, '--title') };
|
|
198
|
+
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: [] }));
|
|
199
|
+
}
|
|
200
|
+
if (command === 'cases') {
|
|
201
|
+
const page = workspace.cases(word(name, 'case batch name'), integer(flags.offset, 0));
|
|
202
|
+
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.`);
|
|
203
|
+
}
|
|
204
|
+
if (command === 'evidence') {
|
|
205
|
+
const batch = word(name, 'case batch name'), { item } = caseAt(workspace, batch, integer(flags.item, 1)), refs = workspace.caseEvidence(batch, item.caseManifestId);
|
|
206
|
+
if (flags.evidence === undefined)
|
|
207
|
+
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'));
|
|
208
|
+
const ref = refs[integer(flags.evidence, 1) - 1];
|
|
209
|
+
if (!ref)
|
|
210
|
+
throw Error('select a listed evidence number');
|
|
211
|
+
const result = workspace.readCaseEvidence(batch, item.caseManifestId, ref.manifestId);
|
|
212
|
+
if (result.status !== 'available')
|
|
213
|
+
return emit({ state: 'deferred', reason: result.status });
|
|
214
|
+
return emit(`${result.reference.retention === 'exact' ? 'Exact retained source' : 'Redacted retained source — not full evidence'}\n${artifactText(result.bytes)}`);
|
|
215
|
+
}
|
|
216
|
+
if (command === 'case') {
|
|
217
|
+
const batch = word(name, 'case batch name'), { item } = caseAt(workspace, batch, integer(flags.item, 0));
|
|
218
|
+
const request = { caseManifestId: item.caseManifestId, priorDecisionId: item.priorDecisionId, disposition: word(flags.disposition, '--disposition'), note: word(flags.note, '--note') };
|
|
219
|
+
return confirm({ candidate: item.candidate, request }, () => workspace.decideCase(batch, request));
|
|
220
|
+
}
|
|
221
|
+
if (command === 'comparisons') {
|
|
222
|
+
const view = workspace.inspect(Date.now());
|
|
223
|
+
return emit(flags.json ? view.comparisons : view.comparisons.map(c => `${c.name}: ${c.title} — ${c.reason}`).join('\n') || 'No retained comparisons connected.');
|
|
224
|
+
}
|
|
225
|
+
if (command === 'review') {
|
|
226
|
+
const terminal = options.ui ? null : terminalLearningUI();
|
|
227
|
+
try {
|
|
228
|
+
return await reviewLearningComparison(directory, word(name, 'comparison name'), options.ui ?? terminal);
|
|
229
|
+
}
|
|
230
|
+
finally {
|
|
231
|
+
terminal?.close();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (command === 'artifact') {
|
|
235
|
+
const v = workspace.comparison(word(name, 'comparison name')), card = v.cards.find(c => c.displayLabel === flags.variant);
|
|
236
|
+
if (!card)
|
|
237
|
+
throw Error('select a listed variant letter');
|
|
238
|
+
const digest = card.artifactDigests[integer(flags.artifact, 1) - 1];
|
|
239
|
+
if (!digest)
|
|
240
|
+
throw Error('select a listed artifact number');
|
|
241
|
+
const bytes = workspace.artifact(name, card.label, digest);
|
|
242
|
+
const text = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
243
|
+
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)}`);
|
|
244
|
+
}
|
|
245
|
+
if (command === 'choose') {
|
|
246
|
+
const v = workspace.comparison(word(name, 'comparison name')), kind = word(flags.kind, '--kind');
|
|
247
|
+
const letters = typeof flags.variants === 'string' ? flags.variants.split(',') : [];
|
|
248
|
+
const labels = letters.map(letter => { const c = v.cards.find(c => c.displayLabel === letter); if (!c)
|
|
249
|
+
throw Error('select listed variant letters'); return c.label; });
|
|
250
|
+
if (kind !== 'insufficient' && flags['full-review'] !== true)
|
|
251
|
+
throw Error('--full-review explicitly acknowledges all complete artifacts, not excerpts');
|
|
252
|
+
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))] : [] };
|
|
253
|
+
return confirm({ comparison: name, choice, review }, () => workspace.choose(name, choice, review));
|
|
254
|
+
}
|
|
255
|
+
if (command === 'reveal')
|
|
256
|
+
return emit(workspace.reveal(word(name, 'comparison name')));
|
|
257
|
+
if (command === 'decide') {
|
|
258
|
+
const v = workspace.decisionStatus(word(name, 'comparison name')), request = { disposition: word(flags.disposition, '--disposition'), note: word(flags.note, '--note'), priorDecisionId: v.current?.id ?? null };
|
|
259
|
+
return confirm({ comparison: name, scope: v.scopeStatement, request, activation: 'not-performed' }, () => workspace.decide(name, request));
|
|
260
|
+
}
|
|
261
|
+
if (command === 'hypothesis') {
|
|
262
|
+
const batch = word(name, 'case batch name'), cases = allCases(workspace, batch).items.filter(c => c.disposition === 'confirmed_defect');
|
|
263
|
+
if (!cases.length)
|
|
264
|
+
throw Error('confirm a retained defect before proposing a repair');
|
|
265
|
+
const proposal = { archiveSnapshot: learningHash(cases.map(c => ({ case: c.caseManifestId, decision: c.priorDecisionId }))), caseIds: cases.map(c => c.candidate.id), population: config.population,
|
|
266
|
+
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 };
|
|
267
|
+
return confirm({ proposal, scope: 'proposal-only; zero calls, no authorization' }, () => workspace.propose(batch, proposal, word(flags.name, '--name')));
|
|
268
|
+
}
|
|
269
|
+
if (command === 'link') {
|
|
270
|
+
const batch = word(flags.cases, '--cases'), { page, item } = caseAt(workspace, batch, integer(flags.item, 0));
|
|
271
|
+
if (!item.priorDecisionId)
|
|
272
|
+
throw Error('case decision required');
|
|
273
|
+
const h = workspace.hypotheses().find(h => h.name === flags.hypothesis);
|
|
274
|
+
if (!h)
|
|
275
|
+
throw Error('select a hypothesis created with learning hypothesis');
|
|
276
|
+
const context = { ...workspace.comparisonContext(word(name, 'comparison name')), caseReference: { version: page.version, batchId: page.batchId, manifestId: item.caseManifestId, decisionId: item.priorDecisionId }, hypothesisManifestId: h.manifestId };
|
|
277
|
+
return confirm({ comparison: name, context }, () => workspace.linkComparisonContext(name, context));
|
|
278
|
+
}
|
|
279
|
+
if (command === 'trust') {
|
|
280
|
+
if (name === 'sample') {
|
|
281
|
+
const bytes = learningFile(resolve(cwd, word(args[2], 'sample evidence file')), 65536), sampleName = word(flags.name, '--name');
|
|
282
|
+
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') }); });
|
|
283
|
+
}
|
|
284
|
+
if (name === 'setup') {
|
|
285
|
+
const setup = trustSetup(workspace, flags), preview = previewLearningTrust(directory, setup);
|
|
286
|
+
return confirm({ ...preview, independentLabels: 'none created', automaticQuestions: setup.exposure ? 'only if independently calibrated policy is met' : 'disabled' }, () => configureLearningTrust(directory, setup, [preview.policyDigest]));
|
|
287
|
+
}
|
|
288
|
+
const trust = workspace.trust();
|
|
289
|
+
if (name === 'status' || !name) {
|
|
290
|
+
const view = { ...trust.inspect(Date.now()), automaticExposure: trust.previewExposure(Date.now()), cohort: trust.configuration().cohort };
|
|
291
|
+
return emit(flags.json ? view : formatLearningTrust(view));
|
|
292
|
+
}
|
|
293
|
+
if (name === 'label') {
|
|
294
|
+
const batch = word(flags.cases, '--cases'), { page, item } = caseAt(workspace, batch, integer(flags.item, 0));
|
|
295
|
+
if (!item.priorDecisionId)
|
|
296
|
+
throw Error('record an independent case decision first');
|
|
297
|
+
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');
|
|
298
|
+
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()) }; });
|
|
299
|
+
}
|
|
300
|
+
if (name === 'unflagged') {
|
|
301
|
+
const id = trust.inspect(Date.now()).sampledIncidentIds[integer(flags.item, 0) - 1];
|
|
302
|
+
if (!id)
|
|
303
|
+
throw Error('select a frozen sampled unflagged incident number');
|
|
304
|
+
const author = word(flags['reference-author'], '--reference-author'), note = word(flags.note, '--note');
|
|
305
|
+
if (!['yes', 'no'].includes(String(flags.miss)))
|
|
306
|
+
throw Error('--miss yes|no required');
|
|
307
|
+
const bytes = learningFile(resolve(cwd, word(flags.evidence, '--evidence')), 65536);
|
|
308
|
+
return confirm({ author, note, incident: id, miss: flags.miss, evidenceBytes: bytes.length }, () => {
|
|
309
|
+
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;
|
|
310
|
+
const outcome = { kind: 'unflagged', targetId: id, value: flags.miss === 'yes', evidenceManifestId: evidence, referenceId: learningHash({ author, note, id, evidence }) };
|
|
311
|
+
trust.outcome(outcome, [trustOutcomeDigest(outcome)], author);
|
|
312
|
+
return trust.inspect(Date.now());
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
throw Error('unknown learning trust operation');
|
|
316
|
+
}
|
|
317
|
+
if (command === 'adoption') {
|
|
318
|
+
const v = workspace.comparison(word(name, 'comparison name'));
|
|
319
|
+
return emit({ scope: v.scopeStatement, quality: v.qualityScope, decision: v.decision, configured: v.adoptionConfigured, activation: v.activation, rollback: v.rollback,
|
|
320
|
+
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.' });
|
|
321
|
+
}
|
|
322
|
+
if (command === 'outcome') {
|
|
323
|
+
word(name, 'comparison name');
|
|
324
|
+
if (flags.result === undefined)
|
|
325
|
+
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.' });
|
|
326
|
+
const status = workspace.adoptionStatus(name);
|
|
327
|
+
if (!status.receipt || !status.activationManifestId)
|
|
328
|
+
throw Error('an actual linked registry activation is required before later outcomes');
|
|
329
|
+
const author = word(flags['reference-author'], '--reference-author'), note = word(flags.note, '--note'), result = word(flags.result, '--result');
|
|
330
|
+
if (!['success', 'confirmed-defect', 'unknown'].includes(result))
|
|
331
|
+
throw Error('explicit success, confirmed-defect or unknown result required');
|
|
332
|
+
if ((flags['accepted-artifact'] === undefined) !== (flags['acceptance-evidence'] === undefined))
|
|
333
|
+
throw Error('accepted artifact and independent acceptance evidence must be supplied together; excerpts never imply acceptance');
|
|
334
|
+
const keys = ['artifact', 'original-requirement', 'current-requirement', 'evidence', ...(flags['accepted-artifact'] === undefined ? [] : ['accepted-artifact', 'acceptance-evidence'])];
|
|
335
|
+
const files = keys.map(key => ({ key, bytes: learningFile(resolve(cwd, word(flags[key], `--${key}`)), 1024 * 1024) }));
|
|
336
|
+
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' }, () => {
|
|
337
|
+
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 })]));
|
|
338
|
+
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' })) });
|
|
339
|
+
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,
|
|
340
|
+
originalRequirementDigest: retained.get('original-requirement').reference.sha256, currentRequirementDigest: retained.get('current-requirement').reference.sha256, outcome: result,
|
|
341
|
+
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) };
|
|
342
|
+
const preview = workspace.previewObservation(name, observation);
|
|
343
|
+
return workspace.observe(name, observation, [preview.digest], author);
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
function learningArchiveScopes(root, author) {
|
|
348
|
+
if (!existsSync(join(root, 'manifests')))
|
|
349
|
+
return [];
|
|
350
|
+
const selected = catalogLearningArchive(root, author).filter((c) => !!c.scopeDigest && !!c.population);
|
|
351
|
+
return selected.filter((c, i) => selected.findIndex(s => s.scopeDigest === c.scopeDigest && s.population === c.population) === i);
|
|
352
|
+
}
|
|
353
|
+
function terminalLearningUI() {
|
|
354
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY)
|
|
355
|
+
throw Error('guided learning needs an interactive terminal; use explicit learning subcommands');
|
|
356
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
357
|
+
return { close: () => rl.close(), notify: text => console.log(learningDisplay(text)),
|
|
358
|
+
input: async (title, initial) => { const answer = await rl.question(`${title}${initial ? ` [${initial}]` : ''}: `); return answer || initial; },
|
|
359
|
+
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]; },
|
|
360
|
+
confirm: async (title, detail) => (console.log(learningDisplay(`${title}\n${detail}`)), (await rl.question('Confirm [y/N]: ')).toLowerCase() === 'y'),
|
|
361
|
+
editor: async (title, text) => { console.log(learningDisplay(`${title}\n${text}`)); await rl.question('Enter to return (this is the complete display): '); return text; } };
|
|
362
|
+
}
|
|
363
|
+
export async function reviewLearningComparison(directory, name, ui) {
|
|
364
|
+
const workspace = openLearningWorkspace(directory);
|
|
365
|
+
let view = workspace.comparison(name);
|
|
366
|
+
ui.notify(`${view.title}\n${view.scopeStatement}\nDeliberate review, not an earned automatic question.\n${view.limitations.join('\n')}`);
|
|
367
|
+
const reviewed = new Set();
|
|
368
|
+
for (;;) {
|
|
369
|
+
view = workspace.comparison(name);
|
|
370
|
+
const choices = [...view.cards.map(c => `Read variant ${c.displayLabel} (${c.artifactDigests.length} artifact(s))`),
|
|
371
|
+
...(view.revealReady ? ['Reveal model and cost'] : ['Record quality choice']), 'Adopt / reject / defer', 'Done'];
|
|
372
|
+
const selected = await ui.select('Comparison — quality is separate from adoption', choices);
|
|
373
|
+
if (!selected || selected === 'Done')
|
|
374
|
+
return;
|
|
375
|
+
const index = choices.indexOf(selected);
|
|
376
|
+
if (index < view.cards.length) {
|
|
377
|
+
const card = view.cards[index];
|
|
378
|
+
for (const digest of card.artifactDigests) {
|
|
379
|
+
const bytes = workspace.artifact(name, card.label, digest);
|
|
380
|
+
let text;
|
|
381
|
+
try {
|
|
382
|
+
text = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
383
|
+
}
|
|
384
|
+
catch {
|
|
385
|
+
ui.notify('This artifact is not UTF-8 text; use a suitable full-artifact viewer. No review acknowledgement recorded.', 'warning');
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
if (bytes.length > 1024 * 1024) {
|
|
389
|
+
ui.notify('Artifact exceeds interactive viewer bound. Use learning artifact in a terminal; no partial view counts as full review.', 'warning');
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
const display = learningDisplay(text), returned = await ui.editor(`Variant ${card.displayLabel} — complete artifact (${bytes.length} bytes; controls escaped)`, display);
|
|
393
|
+
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.'))
|
|
394
|
+
reviewed.add(digest);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
else if (selected === 'Record quality choice') {
|
|
398
|
+
const all = [...new Set(view.cards.flatMap(c => c.artifactDigests))], complete = all.length > 0 && all.every(id => reviewed.has(id));
|
|
399
|
+
const answers = [...(complete ? [...view.cards.map(c => `Prefer ${c.displayLabel}`), ...(view.cards.length > 1 ? ['Tie (all shown variants)'] : []), 'None acceptable'] : []), 'Insufficient evidence', 'Cancel'];
|
|
400
|
+
const answer = await ui.select(complete ? 'Quality — full artifacts' : 'Full artifacts not yet acknowledged; only insufficient evidence is available', answers);
|
|
401
|
+
if (!answer || answer === 'Cancel')
|
|
402
|
+
continue;
|
|
403
|
+
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] };
|
|
404
|
+
const note = await ui.input('Reason / limitations (required)');
|
|
405
|
+
if (!note)
|
|
406
|
+
continue;
|
|
407
|
+
if (await ui.confirm('Record immutable quality choice?', 'This locks the quality choice before reveal; it is not acceptance, calibration, or adoption.')) {
|
|
408
|
+
workspace.choose(name, choice, { scope: 'full-artifacts', note, reviewedArtifactDigests: [...reviewed] });
|
|
409
|
+
ui.notify('Quality choice durably recorded. Identity remains hidden until you choose Reveal.');
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
else if (selected === 'Reveal model and cost')
|
|
413
|
+
ui.notify(JSON.stringify(workspace.reveal(name), null, 2));
|
|
414
|
+
else if (selected === 'Adopt / reject / defer') {
|
|
415
|
+
const disposition = await ui.select('Adoption decision (not activation)', [...(view.revealReady && ['one', 'tie'].includes(view.choice.kind) ? ['adopt'] : []), 'reject', 'defer']);
|
|
416
|
+
if (!disposition)
|
|
417
|
+
continue;
|
|
418
|
+
const note = await ui.input('Decision reason (required)');
|
|
419
|
+
if (!note)
|
|
420
|
+
continue;
|
|
421
|
+
if (await ui.confirm(`Record ${disposition}?`, `${view.scopeStatement}\nNo active session or next order changes here.`)) {
|
|
422
|
+
workspace.decide(name, { disposition: disposition, note, priorDecisionId: view.decision?.id ?? null });
|
|
423
|
+
ui.notify('Decision recorded. Adoption still needs independent eligibility, authority and original registry activation.');
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
export async function runLearningWizard(options) {
|
|
429
|
+
const { directory, ui, cwd } = options;
|
|
430
|
+
if (!existsSync(directory)) {
|
|
431
|
+
ui.notify('Connect an explicitly selected retained archive. This reads no live/private Pi session and calls no model.');
|
|
432
|
+
const archive = await ui.input('Retained archive directory'), author = await ui.input('Review author');
|
|
433
|
+
if (!archive || !author)
|
|
434
|
+
return;
|
|
435
|
+
const archiveRoot = resolve(cwd, archive), scopes = learningArchiveScopes(archiveRoot, author), labels = scopes.map((s, i) => `${i + 1}. ${s.description} (${s.population})`);
|
|
436
|
+
const choice = scopes.length ? await ui.select('Exact retained work scope', labels) : undefined;
|
|
437
|
+
if (scopes.length && !choice)
|
|
438
|
+
return;
|
|
439
|
+
const selected = choice ? scopes[labels.indexOf(choice)] : null;
|
|
440
|
+
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');
|
|
441
|
+
if (!population || !scope)
|
|
442
|
+
return;
|
|
443
|
+
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.`))
|
|
444
|
+
return;
|
|
445
|
+
createLearningWorkspace(directory, { archiveRoot, population, scopeDigest: selected?.scopeDigest ?? learningHash({ declaredScope: scope }), author });
|
|
446
|
+
}
|
|
447
|
+
const workspace = openLearningWorkspace(directory);
|
|
448
|
+
for (;;) {
|
|
449
|
+
const status = workspace.inspect(Date.now());
|
|
450
|
+
const selected = await ui.select('Learning — retained evidence, no model calls', ['Readiness', 'Connect retained input', 'Import session', 'Review sessions', 'Review cases', 'Review comparisons', 'Record adopt / reject / defer', 'Propose hypothesis', 'Link original hypothesis', 'Trust / independent labels', 'Adoption / outcomes', 'Done']);
|
|
451
|
+
if (!selected || selected === 'Done')
|
|
452
|
+
return;
|
|
453
|
+
try {
|
|
454
|
+
if (selected === 'Readiness')
|
|
455
|
+
ui.notify(formatLearningStatus(status));
|
|
456
|
+
if (selected === 'Review sessions')
|
|
457
|
+
await reviewSessionLearning(directory, ui, learningDisplay);
|
|
458
|
+
if (selected === 'Import session') {
|
|
459
|
+
const session = await ui.input('Explicit Pi session JSONL path (no directory scanning)'), name = await ui.input('Short session name'), title = await ui.input('Session title');
|
|
460
|
+
if (!session || !name || !title)
|
|
461
|
+
continue;
|
|
462
|
+
const ledger = await ui.input('Optional explicit pi-daddy grants.jsonl path'), feedback = await ui.input('Optional feedback file path');
|
|
463
|
+
const args = ['session', 'preview', '--session', session, '--state', directory, '--json'];
|
|
464
|
+
if (ledger)
|
|
465
|
+
args.push('--ledger', ledger);
|
|
466
|
+
if (feedback)
|
|
467
|
+
args.push('--feedback', feedback);
|
|
468
|
+
let raw = '';
|
|
469
|
+
await runLearningCommand(args, { cwd, write: t => { raw = t; } });
|
|
470
|
+
const preview = JSON.parse(raw);
|
|
471
|
+
await ui.editor('Session import preview — read only', learningDisplay(raw));
|
|
472
|
+
if (await ui.confirm('Retain these exact selected files privately?', 'May include sensitive session text. Inspect your selected files first. No automatic redaction, model calls or acceptance.')) {
|
|
473
|
+
args[1] = 'import';
|
|
474
|
+
args.push('--name', name, '--title', title, '--expected', preview.digest, '--confirm');
|
|
475
|
+
await runLearningCommand(args, { cwd, write: t => ui.notify(learningDisplay(t)) });
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
if (selected === 'Connect retained input') {
|
|
479
|
+
const catalog = catalogLearningArchive(status.configuration.archiveRoot, status.configuration.author), labels = catalog.map((c, i) => `${i + 1}. ${c.kind}: ${c.description}`);
|
|
480
|
+
if (!labels.length) {
|
|
481
|
+
ui.notify('No compatible retained case batch or qualified comparison for this author. Run ordinary work first; no evidence will be fabricated.', 'warning');
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
const item = await ui.select('Retained inputs in selected archive', labels);
|
|
485
|
+
if (!item)
|
|
486
|
+
continue;
|
|
487
|
+
const c = catalog[labels.indexOf(item)], name = await ui.input('Short name (letters, digits, - or _)'), title = await ui.input('Display title');
|
|
488
|
+
if (!name || !title)
|
|
489
|
+
continue;
|
|
490
|
+
const scope = c.kind === 'comparison' ? await ui.input('What complete artifacts will this quality review assess?') : undefined;
|
|
491
|
+
if (c.kind === 'comparison' && !scope)
|
|
492
|
+
continue;
|
|
493
|
+
if (!await ui.confirm('Connect retained input?', `${title}\n${scope ?? 'Case nominations remain unconfirmed.'}`))
|
|
494
|
+
continue;
|
|
495
|
+
if (c.kind === 'cases')
|
|
496
|
+
workspace.bindCases({ name, title, version: c.version, batchId: c.manifestId });
|
|
497
|
+
else
|
|
498
|
+
workspace.bindComparison({ name, title, scopeStatement: scope, comparisonManifestId: c.manifestId, caseReference: null, hypothesisManifestId: null, priorFeedbackManifestIds: [], adoptionBinding: null });
|
|
499
|
+
ui.notify('Retained input connected. Reading never activates or reserves attention.');
|
|
500
|
+
}
|
|
501
|
+
if (selected === 'Review comparisons') {
|
|
502
|
+
const names = status.comparisons.map(c => c.name), name = await ui.select('Choose retained comparison', names);
|
|
503
|
+
if (name)
|
|
504
|
+
await reviewLearningComparison(directory, name, ui);
|
|
505
|
+
}
|
|
506
|
+
if (selected === 'Review cases') {
|
|
507
|
+
const name = await ui.select('Choose case batch', status.cases.map(c => c.name));
|
|
508
|
+
if (!name)
|
|
509
|
+
continue;
|
|
510
|
+
const p = allCases(workspace, name), labels = p.items.map((c, i) => `${i + 1}. ${c.candidate.reason} — ${c.candidate.target.obligationId} (${c.disposition})`);
|
|
511
|
+
const chosen = await ui.select('Retained nominations (not confirmed defects)', labels);
|
|
512
|
+
if (!chosen)
|
|
513
|
+
continue;
|
|
514
|
+
const item = p.items[labels.indexOf(chosen)];
|
|
515
|
+
await ui.editor('Case nomination — not an independent label', learningDisplay(JSON.stringify(item.candidate, null, 2)));
|
|
516
|
+
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}`);
|
|
517
|
+
let cancelled = false;
|
|
518
|
+
if (refs.length === 1)
|
|
519
|
+
ui.notify('Only nomination retained; no original observation link. This alone is not independent proof.', 'warning');
|
|
520
|
+
for (;;) {
|
|
521
|
+
const selected = await ui.select('Inspect retained case evidence (no bytes are edited)', [...choices, 'Choose disposition', 'Back']);
|
|
522
|
+
if (!selected || selected === 'Back') {
|
|
523
|
+
cancelled = true;
|
|
524
|
+
break;
|
|
525
|
+
}
|
|
526
|
+
if (selected === 'Choose disposition')
|
|
527
|
+
break;
|
|
528
|
+
const ref = refs[choices.indexOf(selected)], result = workspace.readCaseEvidence(name, item.caseManifestId, ref.manifestId);
|
|
529
|
+
if (result.status !== 'available') {
|
|
530
|
+
ui.notify(`Evidence unavailable: ${result.status}`, 'warning');
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
await ui.editor(`${result.reference.retention === 'exact' ? 'Exact' : 'Redacted — not full'} retained source (display only)`, artifactText(result.bytes));
|
|
534
|
+
}
|
|
535
|
+
if (cancelled)
|
|
536
|
+
continue;
|
|
537
|
+
const disposition = await ui.select('Your independent case disposition', ['confirmed_defect', 'expected_behavior', 'exemplar', 'uncertain', 'skip']);
|
|
538
|
+
if (!disposition)
|
|
539
|
+
continue;
|
|
540
|
+
const note = await ui.input('Evidence/reason (required)');
|
|
541
|
+
if (note && await ui.confirm('Record case disposition?', `${disposition}\n${note}\nThis is not a writing preference, adoption or test promotion.`)) {
|
|
542
|
+
workspace.decideCase(name, { caseManifestId: item.caseManifestId, priorDecisionId: item.priorDecisionId, disposition: disposition, note });
|
|
543
|
+
ui.notify('Case disposition durably recorded. Trust linkage is a separate action.');
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
if (selected === 'Record adopt / reject / defer') {
|
|
547
|
+
const name = await ui.select('Retained comparison (defer/reject also works when evidence is unavailable)', status.comparisons.map(c => c.name));
|
|
548
|
+
if (!name)
|
|
549
|
+
continue;
|
|
550
|
+
const current = workspace.decisionStatus(name);
|
|
551
|
+
let adopt = false;
|
|
552
|
+
try {
|
|
553
|
+
const c = workspace.comparison(name);
|
|
554
|
+
adopt = c.revealReady && !!c.choice && ['one', 'tie'].includes(c.choice.kind);
|
|
555
|
+
}
|
|
556
|
+
catch { /* Missing evidence cannot prevent recording a defer. */ }
|
|
557
|
+
const disposition = await ui.select(current.scopeStatement, [...(adopt ? ['adopt'] : []), 'reject', 'defer']), note = await ui.input('Reason (required)');
|
|
558
|
+
if (!disposition || !note)
|
|
559
|
+
continue;
|
|
560
|
+
if (await ui.confirm(`Record ${disposition}?`, 'Intent only. No acceptance, activation, model calls or automatic exposure.')) {
|
|
561
|
+
workspace.decide(name, { disposition: disposition, note, priorDecisionId: current.current?.id ?? null });
|
|
562
|
+
ui.notify('Decision durably recorded.');
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
if (selected === 'Propose hypothesis') {
|
|
566
|
+
const name = await ui.select('Confirmed case batch', status.cases.map(c => c.name));
|
|
567
|
+
if (!name)
|
|
568
|
+
continue;
|
|
569
|
+
const flags = [];
|
|
570
|
+
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']]) {
|
|
571
|
+
const value = await ui.input(title);
|
|
572
|
+
if (!value)
|
|
573
|
+
break;
|
|
574
|
+
flags.push(`--${key}`, value);
|
|
575
|
+
}
|
|
576
|
+
if (flags.length !== 14)
|
|
577
|
+
continue;
|
|
578
|
+
if (await ui.confirm('Retain hypothesis?', 'Proposal only: no scenario edit, model call, adoption or causal claim.'))
|
|
579
|
+
await runLearningCommand(['hypothesis', name, ...flags, '--state', directory, '--confirm'], { cwd, write: t => ui.notify(t) });
|
|
580
|
+
}
|
|
581
|
+
if (selected === 'Link original hypothesis') {
|
|
582
|
+
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));
|
|
583
|
+
if (!comparison || !batch)
|
|
584
|
+
continue;
|
|
585
|
+
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));
|
|
586
|
+
if (!item || !hypothesis)
|
|
587
|
+
continue;
|
|
588
|
+
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.'))
|
|
589
|
+
await runLearningCommand(['link', comparison, '--cases', batch, '--item', String(labels.indexOf(item) + 1), '--hypothesis', hypothesis, '--state', directory, '--confirm'], { cwd, write: t => ui.notify(t) });
|
|
590
|
+
}
|
|
591
|
+
if (selected === 'Trust / independent labels')
|
|
592
|
+
await trustLearningWizard(directory, cwd, ui);
|
|
593
|
+
if (selected === 'Adoption / outcomes') {
|
|
594
|
+
const name = await ui.select('Comparison', status.comparisons.map(c => c.name));
|
|
595
|
+
if (!name)
|
|
596
|
+
continue;
|
|
597
|
+
await runLearningCommand(['adoption', name, '--state', directory], { cwd, write: t => ui.notify(t) });
|
|
598
|
+
await runLearningCommand(['outcome', name, '--state', directory], { cwd, write: t => ui.notify(t) });
|
|
599
|
+
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.')) {
|
|
600
|
+
const result = await ui.select('Observed result', ['success', 'confirmed-defect', 'unknown']);
|
|
601
|
+
if (!result)
|
|
602
|
+
continue;
|
|
603
|
+
const flags = ['--result', result];
|
|
604
|
+
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']]) {
|
|
605
|
+
const value = await ui.input(title);
|
|
606
|
+
if (!value)
|
|
607
|
+
break;
|
|
608
|
+
flags.push(`--${key}`, value);
|
|
609
|
+
}
|
|
610
|
+
if (flags.length !== 14)
|
|
611
|
+
continue;
|
|
612
|
+
if (await ui.confirm('Was this exact artifact independently accepted?', 'No is normal. Never substitute an excerpt preference for full artifact acceptance.')) {
|
|
613
|
+
const artifact = await ui.input('Exact independently accepted artifact file'), evidence = await ui.input('Independent acceptance evidence file');
|
|
614
|
+
if (!artifact || !evidence)
|
|
615
|
+
continue;
|
|
616
|
+
flags.push('--accepted-artifact', artifact, '--acceptance-evidence', evidence);
|
|
617
|
+
}
|
|
618
|
+
if (await ui.confirm('Retain this independent outcome?', 'Links only to the scoped adoption. It does not become calibration, improvement proof or automatic rollback.'))
|
|
619
|
+
await runLearningCommand(['outcome', name, ...flags, '--state', directory, '--confirm'], { cwd, write: t => ui.notify(t) });
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
catch (e) {
|
|
624
|
+
ui.notify(e instanceof Error ? e.message : 'Learning action failed; no success claimed.', 'error');
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
async function trustLearningWizard(directory, cwd, ui) {
|
|
629
|
+
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']);
|
|
630
|
+
if (!action || action === 'Back')
|
|
631
|
+
return;
|
|
632
|
+
if (action === 'Readiness') {
|
|
633
|
+
ui.notify(formatLearningTrust(workspace.inspect(Date.now()).trust));
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
if (action === 'Add unflagged evidence before freeze') {
|
|
637
|
+
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']);
|
|
638
|
+
if (file && name && title && split && await ui.confirm('Retain unflagged incident?', 'This is not a correctness label. Do not include secrets or private sessions.'))
|
|
639
|
+
await runLearningCommand(['trust', 'sample', file, '--name', name, '--title', title, '--split', split, '--state', directory, '--confirm'], { cwd, write: t => ui.notify(t) });
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
if (action === 'Configure frozen policy') {
|
|
643
|
+
const name = await ui.select('Case batch', workspace.inspect(0).cases.map(c => c.name));
|
|
644
|
+
if (!name)
|
|
645
|
+
return;
|
|
646
|
+
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']);
|
|
647
|
+
if (!detector || !split)
|
|
648
|
+
return;
|
|
649
|
+
const max = await ui.input('Maximum unflagged sample (0–32; missing samples stay missing)', '0');
|
|
650
|
+
if (max === undefined)
|
|
651
|
+
return;
|
|
652
|
+
const flags = { cases: name, detector, split, 'max-unflagged': max };
|
|
653
|
+
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.')) {
|
|
654
|
+
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');
|
|
655
|
+
if (!count || !lower || !quota || !days)
|
|
656
|
+
return;
|
|
657
|
+
Object.assign(flags, { 'minimum-resolved': count, 'minimum-lower-bound': lower, attention: quota, expires: days });
|
|
658
|
+
}
|
|
659
|
+
const setup = trustSetup(workspace, flags), preview = previewLearningTrust(directory, setup);
|
|
660
|
+
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))) {
|
|
661
|
+
configureLearningTrust(directory, setup, [preview.policyDigest]);
|
|
662
|
+
ui.notify('Policy and predictions retained; independent labels still pending.');
|
|
663
|
+
}
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
if (action === 'Link current case label') {
|
|
667
|
+
const name = await ui.select('Case batch', workspace.inspect(0).cases.map(c => c.name));
|
|
668
|
+
if (!name)
|
|
669
|
+
return;
|
|
670
|
+
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');
|
|
671
|
+
if (!chosen || !author)
|
|
672
|
+
return;
|
|
673
|
+
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.'))
|
|
674
|
+
await runLearningCommand(['trust', 'label', '--cases', name, '--item', String(labels.indexOf(chosen) + 1), '--reference-author', author, '--state', directory, '--confirm'], { cwd, write: t => ui.notify(t) });
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
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);
|
|
678
|
+
if (!chosen)
|
|
679
|
+
return;
|
|
680
|
+
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');
|
|
681
|
+
if (miss && file && author && note && await ui.confirm('Record independent sample outcome?', 'This label is scope-bound and separate from positive precision.'))
|
|
682
|
+
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) });
|
|
683
|
+
}
|
|
684
|
+
//# sourceMappingURL=learning.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { openLearningWorkspace } from '@skill-harness/adapters';
|
|
2
|
+
import type { LearningUI } from './learning.js';
|
|
3
|
+
export declare const SESSION_HELP: string;
|
|
4
|
+
export declare function formatSession(view: ReturnType<ReturnType<typeof openLearningWorkspace>['session']>): string;
|
|
5
|
+
type Flags = Record<string, string | boolean>;
|
|
6
|
+
export declare function runSessionLearning(args: string[], flags: Flags, options: {
|
|
7
|
+
cwd: string;
|
|
8
|
+
directory: string;
|
|
9
|
+
emit: (v: unknown) => unknown;
|
|
10
|
+
}): Promise<unknown>;
|
|
11
|
+
export declare function reviewSessionLearning(directory: string, ui: LearningUI, display: (s: string) => string): Promise<void>;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { resolve, relative, isAbsolute, sep } from 'node:path';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { prepareSessionImport, retainSessionImport, readRetainedSessionSource, sessionScope, createLearningWorkspace, openLearningWorkspace, learningFile, } from '@skill-harness/adapters';
|
|
4
|
+
const text = (v, label) => { if (typeof v !== 'string' || !v.trim())
|
|
5
|
+
throw Error(label + ' is required'); return v; };
|
|
6
|
+
export const SESSION_HELP = [
|
|
7
|
+
'Session learning — retrospective, no model calls',
|
|
8
|
+
' learning session preview --session FILE [--ledger FILE --feedback FILE --artifact FILE]',
|
|
9
|
+
' [--sources FILE] additional explicit {kind,path,executionId?} selections',
|
|
10
|
+
' learning session import --session FILE ... --name NAME --title TEXT',
|
|
11
|
+
' --expected DIGEST --confirm [--archive DIR --author NAME for a new workspace]',
|
|
12
|
+
' learning session list',
|
|
13
|
+
' learning session show NAME timeline excerpts, model/tool/usage evidence and gaps',
|
|
14
|
+
' learning session source NAME --item N complete selected retained source, display only',
|
|
15
|
+
' learning session note NAME --kind finding|proposal|acceptance|context --note TEXT',
|
|
16
|
+
' --item N --line N --confirm operator annotation, not calibrated quality or adoption',
|
|
17
|
+
'Use --state DIR to select a learning workspace; --json returns structured readback.',
|
|
18
|
+
'Preview writes nothing. Import retains exact selected bytes privately, including potentially sensitive',
|
|
19
|
+
'session content. Inspect selected files before confirming. No automatic discovery, redaction or export.',
|
|
20
|
+
'A changed preview needs a new digest. No running worker is contacted.',
|
|
21
|
+
].join('\n');
|
|
22
|
+
export function formatSession(view) {
|
|
23
|
+
const p = view.parent;
|
|
24
|
+
const lines = ['Session — ' + view.title, 'Session ID: ' + p.sessionId, 'Snapshot: ' + view.selectionDigest,
|
|
25
|
+
'Retrospective evidence; quality and acceptance are not inferred.',
|
|
26
|
+
p.userMessages + ' user messages · ' + p.assistantMessages + ' assistant messages · ' + view.attempts.length + ' ledger executions · ' + view.children.length + ' child transcripts',
|
|
27
|
+
'Last assistant stop: ' + (p.lastAssistantStop ?? 'unknown') + ' (not a completion or acceptance receipt)',
|
|
28
|
+
'', 'Models observed:', ...p.modelRecords.map(m => ' ' + m.provider + '/' + m.model + ': ' + m.messages + ' assistant records'),
|
|
29
|
+
'', 'Parent usage — reported sums only; not billing or combined child usage:',
|
|
30
|
+
...Object.entries(p.usage.fields).map(([k, v]) => ' ' + k + ': ' + v.sum + ' (' + v.reportedMessages + '/' + p.assistantMessages + ' messages reported)'),
|
|
31
|
+
'', 'Delegations — operator-selected ledger, association not authenticated:',
|
|
32
|
+
...view.attempts.map(a => ' ' + a.executionId + ' · ' + (a.agent ?? 'unknown') + ' · ' + a.state + ' · ' + (a.runningSeconds ?? '?') + ' s · ' + (a.pane ?? 'no pane')),
|
|
33
|
+
'', 'Child transcripts — separately reported, no combined token total:',
|
|
34
|
+
...view.children.flatMap(c => [
|
|
35
|
+
' Source ' + (c.source + 1) + ' · ' + c.session.sessionId + ' · ' + (c.executionId ?? 'unmapped execution'),
|
|
36
|
+
' Last assistant stop: ' + (c.session.lastAssistantStop ?? 'unknown') + ' (not acceptance)',
|
|
37
|
+
...c.session.modelRecords.map(m => ' ' + m.provider + '/' + m.model + ': ' + m.messages + ' assistant records'),
|
|
38
|
+
...Object.entries(c.session.usage.fields).map(([k, v]) => ' ' + k + ': ' + v.sum + ' (' + v.reportedMessages + '/' + c.session.assistantMessages + ' messages reported)'),
|
|
39
|
+
...c.session.timeline.map(e => ' line ' + e.line + ' · ' + e.kind + ': ' + e.excerpt),
|
|
40
|
+
]),
|
|
41
|
+
'', 'Sources — use session source NAME --item N for complete retained bytes:',
|
|
42
|
+
...view.sources.map((s, i) => ' ' + (i + 1) + '. ' + s.kind + ': ' + s.path + ' (' + s.bytes + ' bytes; ' + s.sha256 + ')'),
|
|
43
|
+
'', 'Chronological visible timeline — excerpts, not a complete artifact review:',
|
|
44
|
+
...p.timeline.map(e => ' ' + (e.timestamp ?? 'unknown time') + ' · line ' + e.line + ' · ' + e.kind + ': ' + e.excerpt),
|
|
45
|
+
'', 'Operator annotations — not automatically verified labels:',
|
|
46
|
+
...view.annotations.map(a => ' ' + a.kind + ' · source ' + a.item + ', line ' + a.line + ' · ' + a.author + ': ' + a.note),
|
|
47
|
+
'', 'Evidence gaps:', ...view.gaps.map(g => ' - ' + g)];
|
|
48
|
+
return lines.join('\n');
|
|
49
|
+
}
|
|
50
|
+
export async function runSessionLearning(args, flags, options) {
|
|
51
|
+
const { cwd, directory, emit } = options, [verb, name] = args;
|
|
52
|
+
const allowed = { preview: ['session', 'ledger', 'feedback', 'artifact', 'sources'], import: ['session', 'ledger', 'feedback', 'artifact', 'sources', 'name', 'title', 'expected', 'archive', 'author'], list: [], show: [], source: ['item'], note: ['kind', 'note', 'item', 'line'] };
|
|
53
|
+
if (!verb || verb === 'help')
|
|
54
|
+
return emit(SESSION_HELP);
|
|
55
|
+
if (!Object.hasOwn(allowed, verb) || args.length > (['show', 'source', 'note'].includes(verb) ? 2 : 1))
|
|
56
|
+
throw Error('unknown session command; use learning session help');
|
|
57
|
+
for (const key of Object.keys(flags))
|
|
58
|
+
if (!['state', 'json', 'confirm', 'help', ...allowed[verb]].includes(key))
|
|
59
|
+
throw Error('unsupported session option: --' + key);
|
|
60
|
+
if (verb === 'preview' || verb === 'import') {
|
|
61
|
+
const selections = [];
|
|
62
|
+
if (flags.session)
|
|
63
|
+
selections.push({ kind: 'parent', path: resolve(cwd, text(flags.session, '--session')) });
|
|
64
|
+
for (const kind of ['ledger', 'feedback', 'artifact'])
|
|
65
|
+
if (flags[kind])
|
|
66
|
+
selections.push({ kind, path: resolve(cwd, text(flags[kind], '--' + kind)) });
|
|
67
|
+
if (flags.sources) {
|
|
68
|
+
const raw = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(learningFile(resolve(cwd, text(flags.sources, '--sources')), 65536)));
|
|
69
|
+
if (!Array.isArray(raw))
|
|
70
|
+
throw Error('source selection file must be an array');
|
|
71
|
+
for (const s of raw) {
|
|
72
|
+
if (!s || typeof s !== 'object' || typeof s.path !== 'string')
|
|
73
|
+
throw Error('invalid source selection');
|
|
74
|
+
selections.push({ ...s, path: resolve(cwd, s.path) });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const preview = prepareSessionImport(selections);
|
|
78
|
+
const summary = { state: 'preview', digest: preview.digest, sources: preview.sources, sessionId: preview.parent.sessionId,
|
|
79
|
+
userMessages: preview.parent.userMessages, assistantMessages: preview.parent.assistantMessages,
|
|
80
|
+
executions: preview.attempts.length, childTranscripts: preview.children.length, gaps: preview.gaps,
|
|
81
|
+
privacy: 'Exact source bytes, potentially sensitive, will be retained privately. Inspect sources first; no automatic redaction or publication.',
|
|
82
|
+
next: 'Repeat import with --expected ' + preview.digest + ' --confirm after reviewing the selected inputs.' };
|
|
83
|
+
if (verb === 'preview' || flags.confirm !== true)
|
|
84
|
+
return emit(summary);
|
|
85
|
+
if (flags.expected !== preview.digest)
|
|
86
|
+
throw Error('exact preview digest required; re-preview changed inputs');
|
|
87
|
+
const label = text(flags.name, '--name'), title = text(flags.title, '--title');
|
|
88
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/.test(label) || title.length > 512)
|
|
89
|
+
throw Error('bounded session name/title required');
|
|
90
|
+
let workspace;
|
|
91
|
+
if (existsSync(directory)) {
|
|
92
|
+
if (flags.archive || flags.author)
|
|
93
|
+
throw Error('existing workspace already binds archive and author');
|
|
94
|
+
workspace = openLearningWorkspace(directory);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
const archiveRoot = resolve(cwd, text(flags.archive, '--archive for new workspace')), author = text(flags.author, '--author for new workspace');
|
|
98
|
+
const archiveRelative = relative(directory, archiveRoot);
|
|
99
|
+
if (!archiveRelative || (!isAbsolute(archiveRelative) && archiveRelative !== '..' && !archiveRelative.startsWith('..' + sep)))
|
|
100
|
+
throw Error('archive must be outside the workspace journal directory');
|
|
101
|
+
workspace = createLearningWorkspace(directory, { archiveRoot, author, population: 'pi-session:' + preview.parent.sessionId, scopeDigest: sessionScope(preview.parent, preview.sources.find(s => s.kind === 'parent').sha256) });
|
|
102
|
+
}
|
|
103
|
+
const previous = workspace.sessions().find(s => s.name === label);
|
|
104
|
+
if (previous) {
|
|
105
|
+
const old = workspace.session(label);
|
|
106
|
+
if (old.selectionDigest !== preview.digest || old.title !== title)
|
|
107
|
+
throw Error('session name already bound; use a new name for changed evidence');
|
|
108
|
+
return emit({ state: 'already-retained', name: label, manifestId: old.manifestId });
|
|
109
|
+
}
|
|
110
|
+
const manifestId = retainSessionImport(workspace.configuration().archiveRoot, preview, preview.digest);
|
|
111
|
+
workspace.bindSession({ name: label, title, manifestId });
|
|
112
|
+
return emit({ state: 'retained', name: label, manifestId, sources: preview.sources.length, acceptance: 'not-assessed', next: 'learning session show ' + label });
|
|
113
|
+
}
|
|
114
|
+
const workspace = openLearningWorkspace(directory);
|
|
115
|
+
if (verb === 'list')
|
|
116
|
+
return emit(workspace.sessions());
|
|
117
|
+
const view = workspace.session(text(name, 'session name'));
|
|
118
|
+
if (verb === 'show')
|
|
119
|
+
return emit(flags.json ? view : formatSession(view));
|
|
120
|
+
const item = Number(flags.item);
|
|
121
|
+
if (!Number.isSafeInteger(item) || item < 1 || item > view.sources.length)
|
|
122
|
+
throw Error('select a listed source with --item N');
|
|
123
|
+
if (verb === 'source')
|
|
124
|
+
return emit(new TextDecoder('utf-8', { fatal: true }).decode(readRetainedSessionSource(workspace.configuration().archiveRoot, view.manifestId, item - 1)));
|
|
125
|
+
const kind = text(flags.kind, '--kind'), note = text(flags.note, '--note'), line = Number(flags.line);
|
|
126
|
+
if (!['finding', 'proposal', 'acceptance', 'context'].includes(kind) || note.length > 4000 || !Number.isSafeInteger(line) || line < 1)
|
|
127
|
+
throw Error('bounded annotation and evidence line required');
|
|
128
|
+
if (flags.confirm !== true)
|
|
129
|
+
return emit({ state: 'confirmation-required', name, item, line, kind, note, meaning: 'operator annotation only; no trust/adoption authority' });
|
|
130
|
+
return emit(workspace.noteSession(name, { kind: kind, note, item, line }));
|
|
131
|
+
}
|
|
132
|
+
export async function reviewSessionLearning(directory, ui, display) {
|
|
133
|
+
const w = openLearningWorkspace(directory), name = await ui.select('Retained sessions', w.sessions().map(s => s.name));
|
|
134
|
+
if (!name)
|
|
135
|
+
return;
|
|
136
|
+
const view = w.session(name);
|
|
137
|
+
await ui.editor('Session assessment — display only', display(formatSession(view)));
|
|
138
|
+
for (;;) {
|
|
139
|
+
const choices = view.sources.map((s, i) => (i + 1) + '. ' + s.kind + ': ' + s.path), selected = await ui.select('Complete retained evidence (may contain sensitive text)', [...choices, 'Done']);
|
|
140
|
+
if (!selected || selected === 'Done')
|
|
141
|
+
return;
|
|
142
|
+
const index = choices.indexOf(selected);
|
|
143
|
+
if (index < 0)
|
|
144
|
+
throw Error('select a listed retained source');
|
|
145
|
+
await ui.editor('Exact source — changes discarded', display(new TextDecoder('utf-8', { fatal: true }).decode(readRetainedSessionSource(w.configuration().archiveRoot, view.manifestId, index))));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
//# sourceMappingURL=session-learning.js.map
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# Factory: work, review, learn
|
|
2
|
+
|
|
3
|
+
**Release candidate: skill-harness 0.17.0**, paired with **pi-daddy 0.27.3**.
|
|
4
|
+
Principal 3.2.0 is unchanged. The session retrospective feature is merged in PR82
|
|
5
|
+
with independent Sol review and green CI. This release adds explicit local session
|
|
6
|
+
import, timeline/usage coverage and evidence-linked notes. The producer adds a
|
|
7
|
+
visible count and toggle for completed ledger history.
|
|
8
|
+
|
|
9
|
+
Canonical packaging, installed verification and publication are recorded separately
|
|
10
|
+
in the release PR. Use a fresh Pi session after updating the harness: an existing
|
|
11
|
+
immutable dashboard bridge survives reload. Other running sessions need not stop.
|
|
12
|
+
|
|
13
|
+
## Who does what?
|
|
14
|
+
|
|
15
|
+
**pi-daddy** coordinates permitted work and owns execution, steering and the future-order
|
|
16
|
+
registry. **Herdr** displays its sessions and side panel. **skill-harness** retains
|
|
17
|
+
measurement and learning evidence. The panel is not an autonomous worker. Finished
|
|
18
|
+
execution is not human acceptance.
|
|
19
|
+
|
|
20
|
+
Use the producer's `/grants work` guided setup for multiple obligations, dependencies,
|
|
21
|
+
priority and explicit agent/model/thinking selections, then its supported run/host controls.
|
|
22
|
+
For existing single-task use, `pi-daddy work add --id report --outcome "Review the report"`
|
|
23
|
+
remains supported. Only listed controls are available. Pausing new dispatch does not
|
|
24
|
+
cancel running work; changing a next-order policy does not mutate active sessions.
|
|
25
|
+
Principal's ordinary current/stale/superseded reports remain unchanged.
|
|
26
|
+
|
|
27
|
+
## Open learning in Pi
|
|
28
|
+
|
|
29
|
+
With both matching extensions loaded, use **`/grants learning`** from the current host.
|
|
30
|
+
The producer binds the exact current snapshot, archive, population and local review author.
|
|
31
|
+
A changed scope must report a mismatch, not silently reuse an old learning workspace.
|
|
32
|
+
|
|
33
|
+
The harness also exposes **`/skill-harness learning --state "WORKSPACE"`** (use the
|
|
34
|
+
workspace path displayed by the host), or `skill-harness learning --state "WORKSPACE"`
|
|
35
|
+
in a terminal. These open the **same guided flow and durable records**, not another app.
|
|
36
|
+
|
|
37
|
+
1. **Readiness** explains what's retained, what's missing, and why automatic questions
|
|
38
|
+
are silent. Opening a review is deliberate: it neither reserves attention nor
|
|
39
|
+
represents an earned automatic question. It starts no model or delegation.
|
|
40
|
+
2. **Connect retained input** lists compatible case batches and qualified comparisons
|
|
41
|
+
already in the selected archive. Pick one and give it a short name and title.
|
|
42
|
+
No hand-written manifests or ledger JSON. No compatible evidence is an honest empty state.
|
|
43
|
+
3. **Review cases** shows nominations and lets you open their actual retained evidence
|
|
44
|
+
sources by selection (CLI: `learning evidence BATCH --item N --evidence N`). Redacted
|
|
45
|
+
or unavailable sources are marked, never called complete evidence. Record confirmed defect, expected
|
|
46
|
+
behavior, exemplar, uncertain or skip, with a reason. Corrections are append-only.
|
|
47
|
+
Confirming a case is not promoting a test or establishing a cause.
|
|
48
|
+
4. **Review comparisons** opens complete retained artifacts under A/B labels. Acknowledge
|
|
49
|
+
each complete artifact only after reviewing it. Then choose one, tie, none acceptable,
|
|
50
|
+
or insufficient evidence. **Reveal model/cost** is a separate action, enabled only after
|
|
51
|
+
the durable quality receipt. Artifact contents themselves may disclose identity.
|
|
52
|
+
5. **Record adopt / reject / defer** records a separate scope-bound decision. Reject and defer
|
|
53
|
+
are normal outcomes; neither forces a preference or reveal. Missing evidence can be
|
|
54
|
+
durably deferred. Adopt is intent, not activation.
|
|
55
|
+
6. **Propose hypothesis** records prediction, disproof, downside, rollback and a competing
|
|
56
|
+
explanation from an actual case. **Link original hypothesis** only accepts the comparison's
|
|
57
|
+
actual frozen origin, never a retrospective replacement.
|
|
58
|
+
|
|
59
|
+
Previously retained excerpt feedback concerned
|
|
60
|
+
**excerpts**. Keep that receipt unchanged and linked as prior feedback. It is not full-artifact
|
|
61
|
+
acceptance, detector calibration or a fresh blind experiment. An old unscoped quality choice
|
|
62
|
+
also does not automatically become a full-artifact acknowledgement.
|
|
63
|
+
|
|
64
|
+
## Configure trust without writing manifests
|
|
65
|
+
|
|
66
|
+
Open **Trust / independent labels** in the same guided flow:
|
|
67
|
+
|
|
68
|
+
- Before freezing, explicitly add any real unflagged incident evidence and select its split.
|
|
69
|
+
Only selected evidence files are retained; do not include private sessions or secrets.
|
|
70
|
+
- Select a retained case batch, detector and evaluation split. The product derives the
|
|
71
|
+
cohort and predictions from actual nominations and groups repeated target observations
|
|
72
|
+
into one incident. Detector version and population remain exact.
|
|
73
|
+
- Automatic questions default to **silent**. To configure them, explicitly select minimum
|
|
74
|
+
resolved independent incidents, the confidence lower bound, attention budget and expiry.
|
|
75
|
+
The form previews the exact frozen policy. It creates **no independent labels**.
|
|
76
|
+
- Review cases independently, then choose **Link current case label**. Unflagged sample
|
|
77
|
+
labels have their own explicit evidence/author step. Preference, model self-report,
|
|
78
|
+
panel agreement and exit zero are not independent correctness labels.
|
|
79
|
+
|
|
80
|
+
Missing calibration, missing unflagged samples, held-out absence, conflicting labels,
|
|
81
|
+
expired policy and exhausted attention stay visible. Reopening neither resets choices nor
|
|
82
|
+
refills attention. Correcting a linked case withdraws its old label from confidence.
|
|
83
|
+
The original producer closing path still requires presence, quiescence and its exposure gate.
|
|
84
|
+
|
|
85
|
+
## Scoped adoption and later work
|
|
86
|
+
|
|
87
|
+
Use **`/grants learning` in interactive Pi** for activation/rollback. The matching producer
|
|
88
|
+
implements two separate profiles: unchanged **`fixed-policy-v1`**, and
|
|
89
|
+
**`ordinary-work-policy-v1`** for model/effort-only changes to the same selected task IDs and
|
|
90
|
+
agent definitions. The latter does not change instructions, skills, grants, capabilities,
|
|
91
|
+
topology or assessment policy; do not disguise those changes as model/effort or fixed policy.
|
|
92
|
+
The producer prepares policy bytes and owns independent authority and the original registry.
|
|
93
|
+
Source/built integration has been checked with synthetic fixtures; final installed-release
|
|
94
|
+
verification remains pending. Bare legacy fixed-policy views lacking applied activation
|
|
95
|
+
proof defer; they must not be treated as activation receipts.
|
|
96
|
+
|
|
97
|
+
Activation needs the exact selected candidate, original confirmed case/hypothesis/comparison,
|
|
98
|
+
current independently verified eligible facts, explicit adoption authority, and registry
|
|
99
|
+
revision consent. The original registry revalidates these again. Only its actual activation
|
|
100
|
+
receipt advances the learning record to activated. New ordinary-work orders pin the policy
|
|
101
|
+
under fresh eligibility confirmation; active or history-bearing runs never migrate.
|
|
102
|
+
Rollback similarly needs its own authorized request and original applied registry receipt.
|
|
103
|
+
A stored lifecycle link, human preference or prepared receipt cannot substitute for either.
|
|
104
|
+
|
|
105
|
+
**Adoption / outcomes** can inspect linked later observations or record one using complete
|
|
106
|
+
artifact, original/current requirement and independent evidence files. Unknown is valid.
|
|
107
|
+
Acceptance requires both the exact accepted artifact and independent acceptance evidence.
|
|
108
|
+
Changed requirements, changed artifact, out-of-scope, success, caught defect and escape remain
|
|
109
|
+
distinct. No outcome triggers automatic rollback, and no single outcome proves improvement.
|
|
110
|
+
|
|
111
|
+
## CLI and local archive use
|
|
112
|
+
|
|
113
|
+
```sh
|
|
114
|
+
skill-harness learning status --state "WORKSPACE" # human readiness
|
|
115
|
+
skill-harness learning status --state "WORKSPACE" --json # integration/details
|
|
116
|
+
skill-harness learning review reports --state "WORKSPACE"
|
|
117
|
+
skill-harness learning help # all explicit operations
|
|
118
|
+
skill-harness learning guide # this guide, packaged offline
|
|
119
|
+
skill-harness learning current # packaged requirement register
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Without a producer host, `skill-harness learning` guides connection to an explicitly selected
|
|
123
|
+
archive and selecting an actual retained case scope. Or use
|
|
124
|
+
`learning init --archive DIR --author NAME --confirm`; multiple retained scopes ask you to
|
|
125
|
+
select `--scope-item N`. Snapshot digest and population are derived from actual cases, not
|
|
126
|
+
typed hashes. For a comparison-only archive with no cases, `--scope TEXT --population TEXT`
|
|
127
|
+
can explicitly declare a local review scope; it cannot bind mismatching case snapshots.
|
|
128
|
+
Use the producer flow to connect a host. Noninteractive writes require explicit `--confirm`.
|
|
129
|
+
All learning commands are offline/no-model. Command completion is never a quality verdict.
|
|
130
|
+
Local author attribution and content hashes are not authentication or hostile-owner attestation.
|
|
131
|
+
|
|
132
|
+
Current requirements and remaining evidence: [STATUS.md](STATUS.md).
|
|
133
|
+
|
|
134
|
+
## Whole-session retrospective (0.17.0)
|
|
135
|
+
|
|
136
|
+
Use this after a real task to inspect the conversation, model/thinking settings,
|
|
137
|
+
tool use, individual delegation attempts and provider-reported usage. The importer
|
|
138
|
+
reads only explicitly selected files and never contacts or interrupts an agent.
|
|
139
|
+
|
|
140
|
+
Start with the terminal CLI (or prefix these arguments with
|
|
141
|
+
"/skill-harness learning" in Pi instead of "skill-harness learning"):
|
|
142
|
+
|
|
143
|
+
skill-harness learning session preview --session /absolute/parent.jsonl --ledger /absolute/grants.jsonl
|
|
144
|
+
skill-harness learning session import --session /absolute/parent.jsonl --ledger /absolute/grants.jsonl --name task --title "Task retrospective" --expected DIGEST_FROM_PREVIEW --confirm --state /private/learning/workspace --archive /private/learning/archive --author operator
|
|
145
|
+
skill-harness learning session show task --state /private/learning/workspace
|
|
146
|
+
skill-harness learning session source task --item 1 --state /private/learning/workspace
|
|
147
|
+
|
|
148
|
+
The first import can create a workspace. For an existing workspace omit --archive
|
|
149
|
+
and --author. Optional --feedback FILE and --artifact FILE retain selected external
|
|
150
|
+
feedback and artifact/source evidence. Use --sources FILE for a JSON array of
|
|
151
|
+
additional selections, for example:
|
|
152
|
+
|
|
153
|
+
[{"kind":"child","path":"/absolute/child.jsonl","executionId":"exec:10000000-0000-4000-8000-000000000001"}]
|
|
154
|
+
|
|
155
|
+
Every source must be a regular, unlinked file: at most 16 files, 8 MiB each,
|
|
156
|
+
32 MiB total; Pi/ledger JSONL is bounded to 4096 records with complete final lines.
|
|
157
|
+
Pi session format v3 and the existing frozen pi-daddy v3 ledger contract are
|
|
158
|
+
supported. Re-preview if any selected bytes change. The ledger association and
|
|
159
|
+
child execution mapping are operator declarations, not authenticated parentage.
|
|
160
|
+
|
|
161
|
+
**Privacy:** import retains exact original bytes locally, potentially including
|
|
162
|
+
credentials, reasoning, tool arguments or private text present in the source.
|
|
163
|
+
Preview prints paths, hashes and coverage, not a redacted copy. Inspect selected
|
|
164
|
+
inputs before confirming; no scanning, automatic redaction or publication occurs.
|
|
165
|
+
Full-source readback deliberately displays the selected source; terminal control
|
|
166
|
+
characters are escaped for display without altering archived bytes.
|
|
167
|
+
|
|
168
|
+
Open the existing learning wizard with the same --state path to choose **Import
|
|
169
|
+
session** or **Review sessions**. Editors display evidence; edits are discarded.
|
|
170
|
+
The ordinary producer "/grants learning" bridge also reaches those menu items
|
|
171
|
+
when its current host workspace is already connected. Reading does not reserve
|
|
172
|
+
attention, run a model or change a next-order policy.
|
|
173
|
+
|
|
174
|
+
Record a finding, proposal, acceptance observation or context note against a
|
|
175
|
+
specific retained source line:
|
|
176
|
+
|
|
177
|
+
skill-harness learning session note task --kind proposal --note "Ask for the capability map before implementation; compare this on a later task." --item 1 --line 2 --confirm --state /private/learning/workspace
|
|
178
|
+
|
|
179
|
+
Notes identify the configured local author. An acceptance note is an operator
|
|
180
|
+
annotation, not an authenticated user approval or qualified comparison.
|
|
181
|
+
The timeline is chronological and may include abandoned branches; excerpts are
|
|
182
|
+
labelled. It does not reconstruct the active branch or verify a test claimed in prose.
|
|
183
|
+
Child usage and missing evidence are reported separately; totals are provider
|
|
184
|
+
observations, not billing or a complete parent-plus-child cost.
|
|
185
|
+
|
|
186
|
+
This slice makes sessions reviewable. It does **not** automatically generate or
|
|
187
|
+
measure skill/tool/instruction improvements. A later experiment still needs a
|
|
188
|
+
frozen task and candidate, appropriate independent assessment and actual retained
|
|
189
|
+
measurements before claiming faster or equally good output. Existing adoption
|
|
190
|
+
supports its documented model/effort policy scope only; annotations do not expand it.
|
package/docs/STATUS.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Factory requirement register
|
|
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
|
+
## 0.17.0 release candidate — session learning
|
|
8
|
+
|
|
9
|
+
Adds explicit Pi-session import, exact private retention, chronological evidence,
|
|
10
|
+
per-execution delegation history, parent/child usage coverage, and line-bound
|
|
11
|
+
operator annotations through the existing CLI/Pi learning workspace. No model
|
|
12
|
+
calls, automatic acceptance, calibrated improvement or expanded adoption authority.
|
|
13
|
+
See the whole-session section of PRODUCT-GUIDE.md. Feature PR82 is merged with independent review and green CI. Release metadata
|
|
14
|
+
pairs harness 0.17.0 with producer 0.27.3; canonical publication and installed user
|
|
15
|
+
testing remain separate. Entire-session optimization and measured skill/tool variants remain
|
|
16
|
+
follow-up work.
|
|
17
|
+
|
|
18
|
+
## Historical 0.16.0 release preparation
|
|
19
|
+
|
|
20
|
+
Release pair: **skill-harness 0.16.0 / pi-daddy 0.27.0**; Principal 3.2.0 is unchanged.
|
|
21
|
+
The prior audited baseline was harness 0.15.0 (`a725579`) / producer 0.26.1 (`93a9b01`).
|
|
22
|
+
Harness PR80 merged at `c6cd55d4db9ccf9a1ec790459c3cc5e6b1eac69f` with green CI and
|
|
23
|
+
Sol approval. Actual source/built cross-package learning/registry checks passed with
|
|
24
|
+
synthetic fixtures. This is **release preparation**, not publication, final installed
|
|
25
|
+
proof, human approval, calibration or measured improvement. Loaded resources, source
|
|
26
|
+
HEAD, canonical archives, published bytes and active sessions remain separate identities.
|
|
27
|
+
Canonical packing, publication and final installed verification are pending.
|
|
28
|
+
|
|
29
|
+
| Requirement | Current implementation / boundary | Remaining evidence |
|
|
30
|
+
|---|---|---|
|
|
31
|
+
| 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. |
|
|
32
|
+
| 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. |
|
|
33
|
+
| 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. |
|
|
34
|
+
| 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. |
|
|
35
|
+
| 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. |
|
|
36
|
+
| 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. |
|
|
37
|
+
| 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. |
|
|
38
|
+
| 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. |
|
|
39
|
+
| 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. |
|
|
40
|
+
|
|
41
|
+
## Historical claims remain bounded
|
|
42
|
+
|
|
43
|
+
- The failure-derived abort-race case and the public-weather comparison are different chains.
|
|
44
|
+
- Previously retained feedback covered excerpts only. It does not advance full comparison
|
|
45
|
+
acceptance or detector trust.
|
|
46
|
+
- Earlier no-tool measured SDK observations do not establish universal ordinary-child Usage,
|
|
47
|
+
writable-effect containment or actual provider-internal reasoning effort.
|
|
48
|
+
- No OS sandbox or malicious-local-owner protection has been added.
|
|
49
|
+
- No historical session/evidence, auth or unrelated worktree was modified in this candidate.
|
|
50
|
+
|
|
51
|
+
Feature source review/CI and source/built cross-package checks are completed evidence,
|
|
52
|
+
not final released-install proof. Metadata preparation runs consistency/diff validation only:
|
|
53
|
+
no builds, installs, suites or model calls. Canonical pack and actual installed two-extension
|
|
54
|
+
verification remain release gates under `PUBLISHING.md`. Prior live route smokes remain
|
|
55
|
+
historical and are not replayed merely for a version bump. Human acceptance, calibration and
|
|
56
|
+
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.
|
|
3
|
+
"version": "0.17.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.
|
|
48
|
-
"@skill-harness/adapters": "0.
|
|
49
|
+
"@skill-harness/core": "0.17.0",
|
|
50
|
+
"@skill-harness/adapters": "0.17.0"
|
|
49
51
|
}
|
|
50
52
|
}
|