@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.
@@ -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
- if (!view.cases.length && !view.comparisons.length)
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
@@ -1,15 +1,14 @@
1
1
  # Factory: work, review, learn
2
2
 
3
- **Product guide for the 0.16.0 release candidate**, paired with pi-daddy **0.27.0**;
4
- Principal **3.2.0** is unchanged. The prior audited baseline was harness 0.15.0 /
5
- producer 0.26.1. Harness PR80 is merged and independently approved with green CI;
6
- source/built cross-package checks passed with synthetic fixtures, not human evidence.
7
- Canonical release packaging, publication and final installed-flow verification remain
8
- separate pending gates. After installation, expect `skill-harness --version` **0.16.0**
9
- and producer package **0.27.0**; check `skill-harness learning help` and loaded Pi resources.
10
- Do not assume an existing session reloaded because a package was updated. Use a **fresh
11
- Pi session** when upgrading an already-published immutable dashboard bridge: `/reload`
12
- cannot replace the old global API. No other running session needs to be stopped.
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 — 0.16.0 release preparation
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
- ## Baseline and candidate
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.16.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.16.0",
50
- "@skill-harness/adapters": "0.16.0"
49
+ "@skill-harness/core": "0.17.0",
50
+ "@skill-harness/adapters": "0.17.0"
51
51
  }
52
52
  }