@skill-harness/cli 0.16.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/dist/learning.d.ts +1 -1
- package/dist/learning.js +31 -3
- package/dist/session-learning.d.ts +12 -0
- package/dist/session-learning.js +148 -0
- package/docs/PRODUCT-GUIDE.md +67 -10
- package/docs/STATUS.md +13 -2
- package/package.json +3 -3
package/dist/learning.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export interface LearningUI {
|
|
|
6
6
|
confirm(title: string, detail: string): Promise<boolean>;
|
|
7
7
|
notify(text: string, level?: 'info' | 'warning' | 'error'): void;
|
|
8
8
|
}
|
|
9
|
-
export declare const LEARNING_HELP = "Learning \u2014 retained evidence, no model calls\n learning open guided review (interactive terminal)\n learning init --archive DIR --author NAME [--scope-item N] --confirm\n [--scope TEXT --population TEXT only for a comparison-only archive without retained cases]\n learning status [--json] readiness; never automatic exposure\n learning import list retained batches/comparisons\n learning import --item N --name NAME --title TEXT --scope-note TEXT --confirm\n learning cases NAME [--offset N]\n learning evidence NAME --item N [--evidence N] inspect only the selected case's retained sources\n learning case NAME --item N --disposition confirmed_defect|expected_behavior|exemplar|uncertain|skip --note TEXT --confirm\n learning comparisons\n learning review NAME guided complete-output review in a terminal\n learning artifact NAME --variant A [--artifact N]\n learning choose NAME --kind one|tie|none|insufficient [--variants A,B] --full-review --note TEXT --confirm\n learning reveal NAME requires durable full-scope quality receipt\n learning decide NAME --disposition adopt|reject|defer --note TEXT --confirm\n learning hypothesis NAME --name SLUG --intervention TEXT --prediction TEXT --disproof TEXT --downside TEXT --rollback TEXT --alternative TEXT --confirm\n learning link NAME --cases BATCH --item N --hypothesis SLUG --confirm\n learning trust sample FILE --name SLUG --title TEXT [--split calibration|heldout|tuning] --confirm\n learning trust setup --cases NAME [--detector ID] [--split calibration|heldout|tuning] [--max-unflagged N]\n [--attention N --minimum-resolved N --minimum-lower-bound T --expires DAYS] --confirm\n learning trust status\n learning trust label --cases NAME --item N --reference-author NAME --confirm\n learning trust unflagged --item N --miss yes|no --evidence FILE --reference-author NAME --note TEXT --confirm\n learning adoption NAME scope/decision/readiness; activation is producer-owned\n learning outcome NAME inspect observed outcomes\n learning outcome NAME --result success|confirmed-defect|unknown --artifact FILE --original-requirement FILE --current-requirement FILE --evidence FILE --reference-author NAME --note TEXT [--accepted-artifact FILE --acceptance-evidence FILE] --confirm\n learning guide | current installed portable guide / requirement register\nAll commands accept --state DIR (default .skill-harness/learning), --json for structured output.\n--confirm authorizes only the displayed local write, never models, acceptance or registry activation.\nNo raw manifest authoring. Import selects actual retained inputs. Exit zero is command completion, not quality.\nRegistry activation/rollback use /grants learning in interactive Pi, with independent authority.";
|
|
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
10
|
/** Escape terminal control/format characters in untrusted retained display text. Exact archive
|
|
11
11
|
* bytes never change; output honestly identifies this as a display representation. */
|
|
12
12
|
export declare function learningDisplay(text: string): string;
|
package/dist/learning.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { runSessionLearning, reviewSessionLearning } from './session-learning.js';
|
|
1
2
|
import { resolve, join, dirname } from 'node:path';
|
|
2
3
|
import { fileURLToPath } from 'node:url';
|
|
3
4
|
import { existsSync } from 'node:fs';
|
|
@@ -7,6 +8,7 @@ export const LEARNING_HELP = `Learning — retained evidence, no model calls
|
|
|
7
8
|
learning open guided review (interactive terminal)
|
|
8
9
|
learning init --archive DIR --author NAME [--scope-item N] --confirm
|
|
9
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
|
|
10
12
|
learning status [--json] readiness; never automatic exposure
|
|
11
13
|
learning import list retained batches/comparisons
|
|
12
14
|
learning import --item N --name NAME --title TEXT --scope-note TEXT --confirm
|
|
@@ -107,7 +109,9 @@ export function formatLearningStatus(view) {
|
|
|
107
109
|
lines.push(`Cases: ${c.title} (${c.name}) — ${c.state === 'ready' ? `${c.total} retained nomination(s)` : c.reason}`);
|
|
108
110
|
for (const c of view.comparisons)
|
|
109
111
|
lines.push(`Comparison: ${c.title} (${c.name}) — ${c.reason}${'decision' in c && c.decision ? `; decision: ${c.decision.disposition}` : ''}`);
|
|
110
|
-
|
|
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)
|
|
111
115
|
lines.push('No inputs connected. Choose Connect retained input; missing evidence is not a failure or acceptance.');
|
|
112
116
|
lines.push(formatLearningTrust(view.trust));
|
|
113
117
|
return learningDisplay(lines.join('\n'));
|
|
@@ -128,7 +132,7 @@ export function formatLearningTrust(trust) {
|
|
|
128
132
|
export async function runLearningCommand(argv, options = {}) {
|
|
129
133
|
const { args, flags } = parse(argv), cwd = options.cwd ?? process.cwd(), directory = resolve(cwd, typeof flags.state === 'string' ? flags.state : '.skill-harness/learning');
|
|
130
134
|
const out = options.write ?? console.log;
|
|
131
|
-
const emit = (value) => { out(flags.json ? JSON.stringify(value, null, 2) : typeof value === 'string' ? value : learningDisplay(JSON.stringify(value, null, 2))); return value; };
|
|
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; };
|
|
132
136
|
const confirm = (preview, write) => {
|
|
133
137
|
if (flags.confirm !== true)
|
|
134
138
|
return emit({ state: 'confirmation-required', preview, next: 'Repeat with --confirm to authorize this exact local action.' });
|
|
@@ -139,6 +143,8 @@ export async function runLearningCommand(argv, options = {}) {
|
|
|
139
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: []
|
|
140
144
|
};
|
|
141
145
|
const command = args[0];
|
|
146
|
+
if (command === 'session')
|
|
147
|
+
return runSessionLearning(args.slice(1), flags, { cwd, directory, emit });
|
|
142
148
|
if (command === 'guide' || command === 'current') {
|
|
143
149
|
const file = command === 'guide' ? 'PRODUCT-GUIDE.md' : 'STATUS.md', base = dirname(fileURLToPath(import.meta.url));
|
|
144
150
|
const selected = [resolve(base, '../docs', file), resolve(base, '../../../docs/factory', file)].find(p => existsSync(p));
|
|
@@ -441,12 +447,34 @@ export async function runLearningWizard(options) {
|
|
|
441
447
|
const workspace = openLearningWorkspace(directory);
|
|
442
448
|
for (;;) {
|
|
443
449
|
const status = workspace.inspect(Date.now());
|
|
444
|
-
const selected = await ui.select('Learning — retained evidence, no model calls', ['Readiness', 'Connect retained input', 'Review cases', 'Review comparisons', 'Record adopt / reject / defer', 'Propose hypothesis', 'Link original hypothesis', 'Trust / independent labels', 'Adoption / outcomes', 'Done']);
|
|
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']);
|
|
445
451
|
if (!selected || selected === 'Done')
|
|
446
452
|
return;
|
|
447
453
|
try {
|
|
448
454
|
if (selected === 'Readiness')
|
|
449
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
|
+
}
|
|
450
478
|
if (selected === 'Connect retained input') {
|
|
451
479
|
const catalog = catalogLearningArchive(status.configuration.archiveRoot, status.configuration.author), labels = catalog.map((c, i) => `${i + 1}. ${c.kind}: ${c.description}`);
|
|
452
480
|
if (!labels.length) {
|
|
@@ -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
|
package/docs/PRODUCT-GUIDE.md
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
# Factory: work, review, learn
|
|
2
2
|
|
|
3
|
-
**
|
|
4
|
-
Principal
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
cannot replace the old global API. No other running session needs to be stopped.
|
|
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.
|
|
13
12
|
|
|
14
13
|
## Who does what?
|
|
15
14
|
|
|
@@ -131,3 +130,61 @@ All learning commands are offline/no-model. Command completion is never a qualit
|
|
|
131
130
|
Local author attribution and content hashes are not authentication or hostile-owner attestation.
|
|
132
131
|
|
|
133
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
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
|
-
# Factory requirement register
|
|
1
|
+
# Factory requirement register
|
|
2
2
|
|
|
3
3
|
Current entry point: [PRODUCT-GUIDE.md](PRODUCT-GUIDE.md). The dated
|
|
4
4
|
[CURRENT.md](CURRENT.md), build reports and transport decisions are retained history,
|
|
5
5
|
not current instructions to supply manifests or judge engineering repairs.
|
|
6
6
|
|
|
7
|
-
##
|
|
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
|
|
8
19
|
|
|
9
20
|
Release pair: **skill-harness 0.16.0 / pi-daddy 0.27.0**; Principal 3.2.0 is unchanged.
|
|
10
21
|
The prior audited baseline was harness 0.15.0 (`a725579`) / producer 0.26.1 (`93a9b01`).
|
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",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"prepack": "node ../../scripts/release-pack.mjs --guard"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@skill-harness/core": "0.
|
|
50
|
-
"@skill-harness/adapters": "0.
|
|
49
|
+
"@skill-harness/core": "0.17.0",
|
|
50
|
+
"@skill-harness/adapters": "0.17.0"
|
|
51
51
|
}
|
|
52
52
|
}
|