@ngockhoale/ukit 2.6.10 → 2.7.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.
Files changed (44) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/manifests/documentation.yaml +24 -2
  3. package/manifests/instructionRules.yaml +62 -0
  4. package/manifests/platform.full.yaml +11 -0
  5. package/package.json +1 -1
  6. package/scripts/perf/audit-perf.mjs +920 -0
  7. package/src/cli/commands/doctor.js +23 -4
  8. package/src/cli/commands/feedback.js +97 -0
  9. package/src/cli/commands/memory.js +250 -1
  10. package/src/cli/commands/metrics.js +109 -1
  11. package/src/cli/index.js +7 -0
  12. package/src/core/codeintel/retriever.js +65 -0
  13. package/src/core/diffPlan.js +8 -0
  14. package/src/core/memory/store.js +7 -2
  15. package/src/core/ompConfigMerge.js +222 -0
  16. package/src/core/runInstallPipeline.js +11 -0
  17. package/src/core/runtimeConfig.js +64 -0
  18. package/src/core/unattendedDoctor.js +227 -0
  19. package/src/diagnostics/failurePatterns.js +1 -34
  20. package/src/diagnostics/feedbackEvents.js +196 -0
  21. package/src/diagnostics/laneStats.js +111 -0
  22. package/src/diagnostics/ledgerFiles.js +47 -0
  23. package/src/diagnostics/skillAccuracy.js +158 -0
  24. package/src/learning/patternProposals.js +151 -0
  25. package/src/learning/tuning.js +213 -0
  26. package/templates/.claude/hooks/block-dangerous.sh +76 -9
  27. package/templates/.claude/hooks/context-hardcap-gate.sh +26 -8
  28. package/templates/.claude/hooks/project-important.sh +70 -9
  29. package/templates/.claude/hooks/protect-files.sh +24 -7
  30. package/templates/.claude/hooks/sensitive-data-guard.sh +57 -5
  31. package/templates/.claude/hooks/session-episode.sh +84 -0
  32. package/templates/.claude/settings.json +29 -113
  33. package/templates/.claude/ukit/index/route-task.mjs +6 -0
  34. package/templates/.claude/ukit/runtime/hook-chain-runner.mjs +217 -10
  35. package/templates/.claude/ukit/runtime/hook-input.sh +119 -0
  36. package/templates/.omp/config.yml +32 -4
  37. package/templates/.omp/hooks/pre/ukit-bridge.js +12 -1
  38. package/templates/AGENTS.md +22 -10
  39. package/templates/CLAUDE.md +22 -10
  40. package/templates/adapter-presets/opencode/opencode.template.json +1 -1
  41. package/templates/docs/UKIT_INTERNALS.md +17 -0
  42. package/templates/instructions/core.md +22 -10
  43. package/templates/instructions/layout.yaml +12 -12
  44. package/templates/ukit/storage/config.json +20 -0
@@ -23,6 +23,7 @@ import {
23
23
  inspectProjectImportantWiring,
24
24
  } from '../../core/projectImportant.js';
25
25
  import { runDocContractChecks } from '../../core/docContracts.js';
26
+ import { inspectUnattendedMode } from '../../core/unattendedDoctor.js';
26
27
 
27
28
  export const DOCTOR_HELP_FLAGS = new Set(['--help', '-h']);
28
29
  const KNOWN_FLAGS = new Set([...DOCTOR_HELP_FLAGS, '--skills', '--gateway', '--docs']);
@@ -40,7 +41,7 @@ export function printDoctorHelp() {
40
41
  console.log(' --docs Run doc-contract checks (manifests/documentation.yaml projects only)');
41
42
  }
42
43
 
43
- export async function runDoctor({ packageRoot, projectRoot, argv = [], homeDir = os.homedir() }) {
44
+ export async function runDoctor({ packageRoot, projectRoot, argv = [], homeDir = os.homedir(), ompPath = 'omp' }) {
44
45
  const unknownFlags = argv.filter((flag) => !KNOWN_FLAGS.has(flag));
45
46
  if (unknownFlags.length > 0) {
46
47
  throw new Error(`Unknown option: ${unknownFlags[0]}. Supported: ${SUPPORTED_FLAGS_LIST}`);
@@ -254,6 +255,20 @@ export async function runDoctor({ packageRoot, projectRoot, argv = [], homeDir =
254
255
  console.log(`[UKit] ${ok(check.passed)} ${check.label}`);
255
256
  }
256
257
 
258
+ // TASK-011 / SPEC §8 (FR-007): unattended-mode checks — same remediation-class
259
+ // shape as projectChecks; install-repairable/owner-action failures block the
260
+ // exit code, advisory never does.
261
+ const unattended = await inspectUnattendedMode({ projectRoot, ompPath });
262
+ console.log('[UKit] Unattended-mode checks:');
263
+ for (const check of unattended.checks) {
264
+ if (check.applicable === false) continue;
265
+ console.log(`[UKit] ${ok(check.passed)} ${check.label}`);
266
+ if (check.detail) console.log(`[UKit] detail: ${check.detail}`);
267
+ if (!check.passed && check.remedy) {
268
+ console.log(`[UKit] remedy: ${check.remedy}`);
269
+ }
270
+ }
271
+
257
272
  if (runtimeConfigInspection.errors.length > 0) {
258
273
  console.log(`[UKit] Runtime config issues: ${runtimeConfigInspection.errors.join(' | ')}`);
259
274
  }
@@ -402,13 +417,17 @@ export async function runDoctor({ packageRoot, projectRoot, argv = [], homeDir =
402
417
  const failedProjectChecks = projectChecks.filter(
403
418
  (check) => check.applicable !== false && !check.passed,
404
419
  );
405
- const blockingFailures = failedProjectChecks.filter(
420
+ const failedUnattendedChecks = unattended.checks.filter(
421
+ (check) => check.applicable !== false && !check.passed,
422
+ );
423
+ const allFailedChecks = [...failedProjectChecks, ...failedUnattendedChecks];
424
+ const blockingFailures = allFailedChecks.filter(
406
425
  (check) => check.remediationClass === 'install-repairable' || check.remediationClass === 'owner-action',
407
426
  );
408
427
 
409
- if (failedProjectChecks.length > 0) {
428
+ if (allFailedChecks.length > 0) {
410
429
  console.log('[UKit] Remedies:');
411
- for (const check of failedProjectChecks) {
430
+ for (const check of allFailedChecks) {
412
431
  console.log(`[UKit] - [${check.remediationClass}] ${check.remedy}`);
413
432
  }
414
433
  }
@@ -0,0 +1,97 @@
1
+ // `ukit feedback` — manual wrong-route label capture (SPEC C32 §3c).
2
+ //
3
+ // `ukit feedback "<text>" [--target <file>]` appends
4
+ // `{ts, text, targetFile?, sessionId?}` to
5
+ // `.ukit/storage/cache/feedback-manual.jsonl` and prints `feedback recorded`.
6
+ // `sessionId` = UKIT_SESSION_ID env only (SessionEnd hook exports it) — omitted
7
+ // when unset. `--list` prints a byKind roll-up via collectFeedbackEvents and
8
+ // never writes. Missing/empty text or unknown flag → usage on stderr, exit 1.
9
+
10
+ import fs from 'node:fs/promises';
11
+ import path from 'node:path';
12
+ import { collectFeedbackEvents } from '../../diagnostics/feedbackEvents.js';
13
+
14
+ const HELP_FLAGS = new Set(['--help', '-h', 'help']);
15
+
16
+ function printUsage(stream = console.log) {
17
+ stream('Usage: ukit feedback "<text>" [--target <file>]');
18
+ stream(' ukit feedback --list');
19
+ stream('');
20
+ stream('Record a manual wrong-route correction label, or inspect collected');
21
+ stream('feedback events (rescue / re-route / repeat-stall / user-correction).');
22
+ }
23
+
24
+ async function appendManualLine(projectRoot, record) {
25
+ const filePath = path.join(projectRoot, '.ukit', 'storage', 'cache', 'feedback-manual.jsonl');
26
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
27
+ await fs.appendFile(filePath, JSON.stringify(record) + '\n');
28
+ }
29
+
30
+ export async function runFeedback({ projectRoot, argv = [] }) {
31
+ const args = argv ?? [];
32
+
33
+ if (args.some((a) => HELP_FLAGS.has(a))) {
34
+ printUsage();
35
+ return;
36
+ }
37
+
38
+ if (args.includes('--list')) {
39
+ const result = await collectFeedbackEvents(projectRoot);
40
+ console.log('feedback-events');
41
+ console.log(` audit rows: ${result.auditRowsScanned} ledgers: ${result.ledgersScanned}`);
42
+ for (const [kind, count] of Object.entries(result.byKind)) {
43
+ console.log(` ${kind}: ${count}`);
44
+ }
45
+ for (const event of result.events.slice(0, 5)) {
46
+ console.log(` - ${event.kind} · ${event.detail}`);
47
+ }
48
+ return;
49
+ }
50
+
51
+ let text = null;
52
+ let targetFile;
53
+ for (let i = 0; i < args.length; i += 1) {
54
+ const arg = args[i];
55
+ if (arg === '--target') {
56
+ targetFile = args[i + 1];
57
+ i += 1;
58
+ if (typeof targetFile !== 'string' || targetFile.startsWith('--')) {
59
+ console.error('Missing value for --target.');
60
+ printUsage(console.error);
61
+ process.exitCode = 1;
62
+ return;
63
+ }
64
+ continue;
65
+ }
66
+ if (arg.startsWith('--')) {
67
+ console.error(`Unknown flag: ${arg}`);
68
+ printUsage(console.error);
69
+ process.exitCode = 1;
70
+ return;
71
+ }
72
+ if (text == null) {
73
+ text = arg;
74
+ }
75
+ }
76
+
77
+ if (!text || !text.trim()) {
78
+ console.error('Missing feedback text.');
79
+ printUsage(console.error);
80
+ process.exitCode = 1;
81
+ return;
82
+ }
83
+
84
+ const record = { ts: Date.now(), text: text.trim() };
85
+ if (typeof targetFile === 'string' && targetFile) record.targetFile = targetFile;
86
+ const sessionId = process.env.UKIT_SESSION_ID;
87
+ if (sessionId) record.sessionId = sessionId;
88
+
89
+ try {
90
+ await appendManualLine(projectRoot, record);
91
+ } catch (error) {
92
+ console.error(`Failed to record feedback: ${error?.message ?? error}`);
93
+ process.exitCode = 1;
94
+ return;
95
+ }
96
+ console.log('feedback recorded');
97
+ }
@@ -8,6 +8,7 @@ import {
8
8
  runProjectHygiene,
9
9
  } from '../../core/memory/store.js';
10
10
  import {
11
+ addRecord,
11
12
  getRecord,
12
13
  loadRecords,
13
14
  queryRecords,
@@ -20,7 +21,9 @@ import { inspectRuntimeConfig } from '../../core/runtimeConfig.js';
20
21
  import { buildRuntimePaths } from '../../core/runtimePaths.js';
21
22
  import { pathExists } from '../../core/fileOps.js';
22
23
  import { detectProjectContext } from '../../context/detectProjectContext.js';
24
+ import { listLedgerFiles, LEDGER_DIR_REL } from '../../diagnostics/ledgerFiles.js';
23
25
  import fs from 'node:fs/promises';
26
+ import path from 'node:path';
24
27
 
25
28
  const HELP_FLAGS = new Set(['--help', '-h', 'help']);
26
29
 
@@ -177,6 +180,192 @@ async function runMemoryV2(projectRoot, args) {
177
180
  throw new Error(`Unknown memory v2 op: ${op}. Expected list|show|promote|stale|migrate.`);
178
181
  }
179
182
 
183
+ // ---- `ukit memory promote` — MEMORY.md `## ukit-learned` lane (SPEC §7a) ----
184
+
185
+ const LEARNED_START = '<!-- ukit-learned:start -->';
186
+ const LEARNED_END = '<!-- ukit-learned:end -->';
187
+ const MEM_REF_RE = /<!--\s*mem:(mem_[0-9a-f]{12})\s*-->/g;
188
+
189
+ // Approved promotable records: active project_rule/procedure belonging to the
190
+ // project, not bulk-migrated v1 leftovers (created_by 'migration'), and not
191
+ // still-pending candidates (meta.legacyStatus 'pending').
192
+ async function collectPromotableRecords(projectRoot, projectId) {
193
+ const records = await loadRecords(projectRoot);
194
+ return records.filter((r) =>
195
+ (r.type === 'project_rule' || r.type === 'procedure')
196
+ && r.status === 'active'
197
+ && r.created_by !== 'migration'
198
+ && r.meta?.legacyStatus !== 'pending'
199
+ && (r.project_id == null || r.project_id === projectId));
200
+ }
201
+
202
+ function renderLearnedBlock(records) {
203
+ const lines = [LEARNED_START, '## ukit-learned'];
204
+ for (const r of records) {
205
+ lines.push(`- [${r.type}] ${r.text} <!-- mem:${r.id} -->`);
206
+ }
207
+ lines.push(LEARNED_END);
208
+ return lines.join('\n');
209
+ }
210
+
211
+ // Ids already referenced in mem:<id> comments OUTSIDE the managed block are
212
+ // user-promoted manually — promote must not duplicate them inside the block.
213
+ function externallyReferencedIds(content) {
214
+ const withoutBlock = content.replace(
215
+ new RegExp(`${escapeRe(LEARNED_START)}[\\s\\S]*?${escapeRe(LEARNED_END)}`, 'g'),
216
+ '',
217
+ );
218
+ const ids = new Set();
219
+ for (const m of withoutBlock.matchAll(MEM_REF_RE)) ids.add(m[1]);
220
+ return ids;
221
+ }
222
+
223
+ function escapeRe(s) {
224
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
225
+ }
226
+
227
+ async function writeTextAtomic(filePath, text) {
228
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
229
+ const tmp = `${filePath}.tmp-${process.pid}-${Math.random().toString(16).slice(2)}`;
230
+ await fs.writeFile(tmp, text);
231
+ await fs.rename(tmp, filePath);
232
+ }
233
+
234
+ async function runMemoryPromote(projectRoot, args) {
235
+ const dryRun = args.includes('--dry-run');
236
+ const projectId = (await detectProjectContext(projectRoot)).project.name;
237
+ const memoryMdPath = path.join(projectRoot, 'docs', 'MEMORY.md');
238
+
239
+ let existing = '';
240
+ try {
241
+ existing = await fs.readFile(memoryMdPath, 'utf8');
242
+ } catch {
243
+ existing = '';
244
+ }
245
+
246
+ const outsideIds = externallyReferencedIds(existing);
247
+ const promotable = (await collectPromotableRecords(projectRoot, projectId))
248
+ .filter((r) => !outsideIds.has(r.id));
249
+
250
+ const block = renderLearnedBlock(promotable);
251
+
252
+ if (dryRun) {
253
+ console.log('[UKit] memory promote (dry-run) — rendered block:');
254
+ console.log(block);
255
+ console.log(`[UKit] promoted: ${promotable.length} records → docs/MEMORY.md (dry-run)`);
256
+ return;
257
+ }
258
+
259
+ let next;
260
+ const startIdx = existing.indexOf(LEARNED_START);
261
+ const endIdx = existing.indexOf(LEARNED_END);
262
+ if (startIdx >= 0 && endIdx > startIdx) {
263
+ next = `${existing.slice(0, startIdx)}${block}${existing.slice(endIdx + LEARNED_END.length)}`;
264
+ } else if (existing.length > 0) {
265
+ next = `${existing.replace(/\s*$/, '')}\n\n${block}\n`;
266
+ } else {
267
+ next = `${block}\n`;
268
+ }
269
+
270
+ await writeTextAtomic(memoryMdPath, next);
271
+ console.log(`[UKit] promoted: ${promotable.length} records → docs/MEMORY.md`);
272
+ }
273
+
274
+ // ---- `ukit memory episode` — session episode from exec-ledger (SPEC §7b) ----
275
+
276
+ // Mirrors execution-ledger.mjs safeSegment — src/ cannot import the runtime
277
+ // module, so the segment rule is duplicated deliberately.
278
+ function safeSegment(value) {
279
+ return String(value || 'default')
280
+ .trim()
281
+ .replace(/[^a-zA-Z0-9._-]/g, '_')
282
+ .slice(0, 96) || 'default';
283
+ }
284
+
285
+ async function readJsonIfExists(filePath) {
286
+ try {
287
+ return JSON.parse(await fs.readFile(filePath, 'utf8'));
288
+ } catch {
289
+ return null;
290
+ }
291
+ }
292
+
293
+ // Resolution order: --session <id> → UKIT_SESSION_ID env → most-recent ledger.
294
+ async function resolveLedger(projectRoot, sessionFlag) {
295
+ const dir = path.join(projectRoot, LEDGER_DIR_REL);
296
+ const candidate = sessionFlag ?? process.env.UKIT_SESSION_ID;
297
+ if (candidate) {
298
+ const file = `${safeSegment(candidate)}.json`;
299
+ const ledger = await readJsonIfExists(path.join(dir, file));
300
+ return ledger ? { ledger, ledgerKey: file } : { ledger: null, ledgerKey: file };
301
+ }
302
+ const [name] = await listLedgerFiles(dir, 1);
303
+ if (!name) return { ledger: null, ledgerKey: null };
304
+ const ledger = await readJsonIfExists(path.join(dir, name));
305
+ return ledger ? { ledger, ledgerKey: name } : { ledger: null, ledgerKey: name };
306
+ }
307
+
308
+ function episodeText(ledger, sessionId) {
309
+ const write = ledger.writeSucceeded === true ? 'writeOk' : 'writeFail';
310
+ const verify = ledger.verificationAttempted !== true
311
+ ? 'not-run'
312
+ : ledger.verificationSucceeded === true ? 'ok' : 'fail';
313
+ const receipts = Array.isArray(ledger.receipts) ? ledger.receipts : [];
314
+ const lastCommand = receipts.length > 0
315
+ ? String(receipts[receipts.length - 1].command ?? '').split('\n')[0].trim() || 'n/a'
316
+ : 'n/a';
317
+ const text = `Session ${sessionId}: ${write}, verify=${verify}, `
318
+ + `${receipts.length} receipts, last command ${lastCommand}`;
319
+ return text.slice(0, 300);
320
+ }
321
+
322
+ async function runMemoryEpisode(projectRoot, args) {
323
+ const dryRun = args.includes('--dry-run');
324
+ const sessionFlag = extractFlag(args, '--session').value;
325
+
326
+ const { config } = await inspectRuntimeConfig(projectRoot);
327
+ if (config?.memoryV2?.enabled === false) {
328
+ console.log('[UKit] episode: skipped (memoryV2 disabled)');
329
+ return;
330
+ }
331
+
332
+ const { ledger, ledgerKey } = await resolveLedger(projectRoot, sessionFlag);
333
+ if (!ledger) {
334
+ console.log('[UKit] episode: nothing to record');
335
+ return;
336
+ }
337
+
338
+ const sessionId = ledger.sessionId ?? ledger.sessionKey
339
+ ?? (ledgerKey ? ledgerKey.replace(/\.json$/, '') : 'unknown');
340
+ const text = episodeText(ledger, sessionId);
341
+
342
+ if (dryRun) {
343
+ console.log(`[UKit] episode (dry-run): ${text}`);
344
+ return;
345
+ }
346
+
347
+ const existing = await loadRecords(projectRoot);
348
+ if (existing.some((r) => r.meta?.ledgerKey === ledgerKey)) {
349
+ console.log('[UKit] episode: already recorded');
350
+ return;
351
+ }
352
+
353
+ const ttlDays = Number(config?.memoryV2?.episodeTtlDays) || 90;
354
+ const projectId = (await detectProjectContext(projectRoot)).project.name;
355
+ const record = await addRecord(projectRoot, {
356
+ type: 'episode',
357
+ scope: 'session',
358
+ text,
359
+ provenance: 'exec-ledger',
360
+ confidence: 0.6,
361
+ createdBy: 'episode-hook',
362
+ projectId,
363
+ validUntil: Date.now() + ttlDays * 24 * 60 * 60 * 1000,
364
+ meta: { sessionId, ledgerKey },
365
+ });
366
+ console.log(`[UKit] episode: recorded ${record.id} for session ${sessionId}`);
367
+ }
368
+
180
369
  export async function runMemory({ projectRoot, argv = [] }) {
181
370
  const runtimePaths = buildRuntimePaths(projectRoot);
182
371
  if (!(await pathExists(runtimePaths.runtimeRoot))) {
@@ -242,6 +431,53 @@ export async function runMemory({ projectRoot, argv = [] }) {
242
431
  return;
243
432
  }
244
433
 
434
+ if (subcommand === 'learn') {
435
+ const applyFlag = rest.includes('--apply');
436
+ const knownFlags = new Set(['--apply']);
437
+ const minFlag = extractFlag(rest, '--min');
438
+ const projectFlag = extractFlag(minFlag.rest, '--project');
439
+ for (const arg of projectFlag.rest) {
440
+ if (arg.startsWith('--') && !knownFlags.has(arg)) {
441
+ throw new Error(`Unknown flag for memory learn: ${arg}`);
442
+ }
443
+ }
444
+ const minCount = minFlag.value != null ? Number(minFlag.value) : undefined;
445
+ if (minFlag.value != null && (!Number.isFinite(minCount) || minCount < 1)) {
446
+ throw new Error(`Invalid --min value: ${minFlag.value}`);
447
+ }
448
+ const projectId = projectFlag.value ?? (await detectProjectContext(projectRoot)).project.name;
449
+
450
+ const { proposeFromPatterns } = await import('../../learning/patternProposals.js');
451
+ const result = await proposeFromPatterns(projectRoot, projectId, {
452
+ minCount,
453
+ dryRun: !applyFlag,
454
+ });
455
+ if (result.error) {
456
+ console.log(`[UKit] memory learn: ${result.error}`);
457
+ }
458
+
459
+ if (!applyFlag) {
460
+ console.log(`[UKit] memory learn (dry-run) — ${result.patternsScanned} pattern(s) scanned, ${result.eligible} eligible.`);
461
+ for (const entry of result.proposed) {
462
+ console.log(` [${entry.status}] ${entry.signature} — ${entry.text}`);
463
+ }
464
+ console.log('[UKit] Run `ukit memory learn --apply` to write pending candidates.');
465
+ return;
466
+ }
467
+
468
+ const proposed = result.proposed.filter((p) => p.status === 'proposed').length;
469
+ const duplicates = result.proposed.filter((p) => p.status === 'duplicate').length;
470
+ const skipped = result.proposed.filter((p) => p.status === 'skipped').length;
471
+ for (const entry of result.proposed) {
472
+ console.log(` [${entry.status}] ${entry.signature} — ${entry.text}`);
473
+ }
474
+ console.log(`[UKit] memory learn: proposed ${proposed}, duplicate ${duplicates}, skipped ${skipped} for project ${projectId}.`);
475
+ if (proposed > 0) {
476
+ console.log('[UKit] Review with `ukit memory list --pending`, then `ukit memory approve <id>`.');
477
+ }
478
+ return;
479
+ }
480
+
245
481
  if (subcommand === 'approve' || subcommand === 'reject') {
246
482
  const projectFlagIndex = rest.indexOf('--project');
247
483
  const explicitProjectId = projectFlagIndex >= 0 ? rest[projectFlagIndex + 1] : null;
@@ -327,6 +563,16 @@ export async function runMemory({ projectRoot, argv = [] }) {
327
563
  return;
328
564
  }
329
565
 
566
+ if (subcommand === 'promote') {
567
+ await runMemoryPromote(projectRoot, rest);
568
+ return;
569
+ }
570
+
571
+ if (subcommand === 'episode') {
572
+ await runMemoryEpisode(projectRoot, rest);
573
+ return;
574
+ }
575
+
330
576
  if (subcommand === 'forget') {
331
577
  const memoryId = rest.join(' ').trim();
332
578
  if (!memoryId) {
@@ -353,7 +599,7 @@ export async function runMemory({ projectRoot, argv = [] }) {
353
599
 
354
600
  export function printMemoryHelp() {
355
601
  console.log('UKit Memory Commands');
356
- console.log('Usage: ukit memory <list|search|recall|forget|export|propose|approve|reject|hygiene> [args]');
602
+ console.log('Usage: ukit memory <list|search|recall|forget|export|propose|approve|reject|learn|promote|episode|hygiene> [args]');
357
603
  console.log('');
358
604
  console.log('Subcommands:');
359
605
  console.log(' list List memory items in shared .ukit/storage/memory');
@@ -365,5 +611,8 @@ export function printMemoryHelp() {
365
611
  console.log(' propose "<text>" [--category <name>] [--project <id>] Propose a project convention for human approval');
366
612
  console.log(' approve <id> [--project <id>] Approve a pending pattern candidate into project conventions');
367
613
  console.log(' reject <id> [--project <id>] Reject a pending pattern candidate');
614
+ console.log(' learn [--apply] [--min <n>] [--project <id>] Propose pending candidates from mined failure patterns (dry-run by default)');
615
+ console.log(' promote [--dry-run] Render approved rules/procedures into the ## ukit-learned block in docs/MEMORY.md');
616
+ console.log(' episode [--dry-run] [--session <id>] Write a session episode record from the exec-ledger');
368
617
  console.log(' hygiene [--project <id>] Run decision-conflict resolution + session archiving now');
369
618
  }
@@ -51,6 +51,16 @@ async function collectFailurePatterns(projectRoot) {
51
51
  }
52
52
  }
53
53
 
54
+ async function lazyCollect(modulePath, exportName, projectRoot) {
55
+ try {
56
+ const mod = await import(modulePath);
57
+ if (typeof mod?.[exportName] !== 'function') return null;
58
+ return await mod[exportName](projectRoot);
59
+ } catch {
60
+ return null;
61
+ }
62
+ }
63
+
54
64
  function printRouteOutcomes(routeOutcomes) {
55
65
  console.log('route-outcomes');
56
66
  console.log(` ledgers: ${routeOutcomes.ledgersScanned} audit rows: ${routeOutcomes.auditRowsScanned} joined: ${routeOutcomes.joined} coverage: ${(routeOutcomes.joinCoverage * 100).toFixed(1)}% unmatched audit: ${routeOutcomes.unmatchedAudit} unmatched ledger: ${routeOutcomes.unmatchedLedger}`);
@@ -85,6 +95,92 @@ function printMemory(memory) {
85
95
  console.log(`memory: v2 records=${memory.total}${statusPairs ? ` (${statusPairs})` : ''}`);
86
96
  }
87
97
 
98
+ function printFeedback(feedback) {
99
+ console.log('feedback');
100
+ if (!feedback) {
101
+ console.log(' n/a');
102
+ return;
103
+ }
104
+ const kinds = Object.entries(feedback.byKind ?? {})
105
+ .map(([kind, count]) => `${kind}=${count}`).join(' ');
106
+ console.log(` ${kinds || '(none)'}`);
107
+ const events = Array.isArray(feedback.events) ? feedback.events.slice(0, 5) : [];
108
+ for (const event of events) {
109
+ console.log(` ${event.kind}: ${event.detail ?? ''}`);
110
+ }
111
+ }
112
+
113
+ function printSkills(skillAccuracy) {
114
+ console.log('skills');
115
+ if (!skillAccuracy) {
116
+ console.log(' n/a');
117
+ return;
118
+ }
119
+ const skills = Object.entries(skillAccuracy.skills ?? {});
120
+ if (skills.length === 0) {
121
+ console.log(' (none)');
122
+ } else {
123
+ console.log(' skill\ttriggers\tjoined\taccuracy');
124
+ for (const [id, bucket] of skills) {
125
+ const accuracy = bucket.accuracy === null ? 'n/a' : bucket.accuracy.toFixed(2);
126
+ console.log(` ${id}\t${bucket.triggers}\t${bucket.joined}\t${accuracy}`);
127
+ }
128
+ }
129
+ const low = Array.isArray(skillAccuracy.lowAccuracy) ? skillAccuracy.lowAccuracy : [];
130
+ if (low.length > 0) {
131
+ console.log(` lowAccuracy: ${low.map((e) => `${e.id}=${e.accuracy.toFixed(2)}`).join(' ')}`);
132
+ }
133
+ }
134
+
135
+ async function collectLearningSuggestions(projectRoot) {
136
+ try {
137
+ const raw = await fs.readFile(
138
+ path.join(projectRoot, '.ukit', 'storage', 'learning', 'suggestions.json'), 'utf8');
139
+ const doc = JSON.parse(raw);
140
+ const suggestions = Array.isArray(doc?.suggestions) ? doc.suggestions : [];
141
+ return {
142
+ pending: suggestions.length,
143
+ targets: suggestions.map((s) => s?.target).filter(Boolean),
144
+ generatedAt: doc?.generatedAt ?? null,
145
+ };
146
+ } catch {
147
+ return null;
148
+ }
149
+ }
150
+
151
+ function printLearning(learning) {
152
+ console.log('learning');
153
+ if (!learning) {
154
+ console.log(' n/a');
155
+ return;
156
+ }
157
+ console.log(` pending=${learning.pending}${learning.generatedAt ? ` generatedAt=${learning.generatedAt}` : ''}`);
158
+ for (const target of learning.targets.slice(0, 10)) {
159
+ console.log(` - ${target}`);
160
+ }
161
+ }
162
+
163
+ function printLaneStats(laneStats) {
164
+ console.log('retriever-lanes');
165
+ if (!laneStats) {
166
+ console.log(' n/a');
167
+ return;
168
+ }
169
+ if (laneStats.empty) {
170
+ console.log(' (no retriever-lanes.jsonl data)');
171
+ return;
172
+ }
173
+ const lanes = Object.entries(laneStats.lanes ?? {});
174
+ if (lanes.length === 0) {
175
+ console.log(' (none)');
176
+ return;
177
+ }
178
+ console.log(' lane\tretrievals\ttotalHits\texclusive\tavgWeight\tcontribution');
179
+ for (const [lane, bucket] of lanes) {
180
+ console.log(` ${lane}\t${bucket.retrievals}\t${bucket.totalHits}\t${bucket.exclusive}\t${bucket.avgWeight.toFixed(2)}\t${bucket.contribution.toFixed(2)}`);
181
+ }
182
+ }
183
+
88
184
  export async function runMetrics({ projectRoot, argv = [] }) {
89
185
  if (argv.some((flag) => HELP_FLAGS.has(flag))) {
90
186
  printUsage();
@@ -111,8 +207,12 @@ export async function runMetrics({ projectRoot, argv = [] }) {
111
207
  }));
112
208
  const failurePatterns = await collectFailurePatterns(projectRoot);
113
209
  const memory = await collectMemorySummary(projectRoot);
210
+ const feedback = await lazyCollect('../../diagnostics/feedbackEvents.js', 'collectFeedbackEvents', projectRoot);
211
+ const skillAccuracy = await lazyCollect('../../diagnostics/skillAccuracy.js', 'collectSkillAccuracy', projectRoot);
212
+ const laneStats = await lazyCollect('../../diagnostics/laneStats.js', 'collectLaneStats', projectRoot);
213
+ const learning = await collectLearningSuggestions(projectRoot);
114
214
 
115
- const rollup = { routeOutcomes, failurePatterns, memory };
215
+ const rollup = { routeOutcomes, failurePatterns, memory, feedback, skillAccuracy, laneStats, learning };
116
216
 
117
217
  if (argv.includes('--json')) {
118
218
  console.log(JSON.stringify(rollup, null, 2));
@@ -123,5 +223,13 @@ export async function runMetrics({ projectRoot, argv = [] }) {
123
223
  console.log('');
124
224
  printFailurePatterns(failurePatterns);
125
225
  console.log('');
226
+ printFeedback(feedback);
227
+ console.log('');
228
+ printSkills(skillAccuracy);
229
+ console.log('');
230
+ printLaneStats(laneStats);
231
+ console.log('');
232
+ printLearning(learning);
233
+ console.log('');
126
234
  printMemory(memory);
127
235
  }
package/src/cli/index.js CHANGED
@@ -8,6 +8,7 @@ import { runMemory } from './commands/memory.js';
8
8
  import { runUpdate } from './commands/update.js';
9
9
  import { runCode } from './commands/code.js';
10
10
  import { runMetrics } from './commands/metrics.js';
11
+ import { runFeedback } from './commands/feedback.js';
11
12
 
12
13
  const GLOBAL_FLAGS = new Set(['--help', '-h', '--version', '-v']);
13
14
 
@@ -71,6 +72,11 @@ export async function runCli({ argv, packageRoot, projectRoot, packageVersion })
71
72
  return;
72
73
  }
73
74
 
75
+ if (command === 'feedback') {
76
+ await runFeedback({ projectRoot, argv: commandArgv });
77
+ return;
78
+ }
79
+
74
80
  if (command === 'update') {
75
81
  await runUpdate({ packageVersion, argv: commandArgv });
76
82
  return;
@@ -105,6 +111,7 @@ export async function runCli({ argv, packageRoot, projectRoot, packageVersion })
105
111
  console.log(' status Show UKit runtime status');
106
112
  console.log(' memory Inspect shared UKit memory');
107
113
  console.log(' metrics Telemetry roll-up (route outcomes, failure patterns, memory)');
114
+ console.log(' feedback Record or list wrong-route feedback labels');
108
115
  console.log(' update Upgrade the global UKit CLI to the latest version');
109
116
  console.log(' version Show UKit version');
110
117
  console.log('');