@ngockhoale/ukit 3.3.3 → 3.4.1

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 (89) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/manifests/engineConformance.yaml +17 -1
  3. package/manifests/hostCapabilities.yaml +68 -1
  4. package/manifests/platform.full.yaml +138 -0
  5. package/manifests/platform.user.yaml +255 -3
  6. package/package.json +1 -1
  7. package/scripts/bench/subagent-orchestrator-corpus.mjs +275 -0
  8. package/scripts/bench/subagent-orchestrator-eval.mjs +565 -0
  9. package/scripts/probe/codex-capability-probe.mjs +169 -0
  10. package/src/cli/commands/doctor.js +168 -0
  11. package/src/cli/commands/indexTools.js +7 -0
  12. package/src/cli/commands/metrics.js +66 -2
  13. package/src/cli/commands/playbook.js +4 -4
  14. package/src/cli/commands/vm.js +49 -8
  15. package/src/core/agentRuntime/adapters.js +328 -27
  16. package/src/core/agentRuntime/artifacts.js +89 -0
  17. package/src/core/agentRuntime/context.js +345 -1
  18. package/src/core/agentRuntime/contract.js +296 -0
  19. package/src/core/agentRuntime/eventStore.js +176 -0
  20. package/src/core/agentRuntime/shadowRun.js +481 -5
  21. package/src/core/agentRuntime/telemetry.js +121 -0
  22. package/src/core/observability/emit/lifecycle.js +68 -1
  23. package/src/core/observability/emit/sessionBoot.js +393 -0
  24. package/src/core/observability/privacy/allowlist.js +10 -1
  25. package/src/core/observability/schema/registry.js +10 -0
  26. package/src/core/runtimeConfig.js +133 -0
  27. package/src/core/userPlaybooks.js +18 -3
  28. package/src/decision/registry.js +19 -0
  29. package/src/diagnostics/feedbackEvents.js +7 -4
  30. package/src/diagnostics/routeOutcomes.js +51 -6
  31. package/src/diagnostics/skillAccuracy.js +43 -3
  32. package/src/index/crossCheckMatrix.js +412 -0
  33. package/src/index/fixLoopEscalation.js +453 -0
  34. package/src/index/playbookRegistry.js +691 -0
  35. package/src/index/reviewPolicy.js +368 -0
  36. package/src/index/routeResolver.js +915 -0
  37. package/src/index/sessionHistoryExtractor.js +359 -0
  38. package/src/index/taskRouting.js +764 -581
  39. package/src/index/tierSelection.js +308 -0
  40. package/src/index/verificationMap.js +404 -0
  41. package/template_project/.claude/hooks/observability-emit.mjs +14 -0
  42. package/template_project/.claude/hooks/record-execution.mjs +19 -1
  43. package/template_project/.claude/hooks/skill-router.sh +691 -25
  44. package/template_project/.claude/hooks/verification-guard.sh +230 -1
  45. package/template_project/.claude/settings.json +2 -2
  46. package/template_project/.claude/ukit/index/cross-check-matrix.mjs +415 -0
  47. package/template_project/.claude/ukit/index/fix-loop-escalation.mjs +456 -0
  48. package/template_project/.claude/ukit/index/playbook-registry.mjs +690 -0
  49. package/template_project/.claude/ukit/index/review-panel-aggregate.mjs +20 -2
  50. package/template_project/.claude/ukit/index/review-policy.mjs +376 -0
  51. package/template_project/.claude/ukit/index/route-resolver.mjs +1059 -0
  52. package/template_project/.claude/ukit/index/route-task.mjs +1253 -846
  53. package/template_project/.claude/ukit/index/session-history-extractor.mjs +362 -0
  54. package/template_project/.claude/ukit/index/tier-selection.mjs +309 -0
  55. package/template_project/.claude/ukit/index/verification-map.mjs +403 -0
  56. package/template_project/.claude/ukit/index/worktree-sweep.mjs +195 -0
  57. package/template_project/.claude/ukit/runtime/execution-ledger.mjs +789 -11
  58. package/template_project/.claude/ukit/runtime/observability-emit.mjs +1102 -0
  59. package/template_project/.claude/ukit/runtime/reinject-context.mjs +9 -1
  60. package/template_project/.claude/ukit/runtime/resumable-run.mjs +149 -5
  61. package/template_project/.claude/ukit/runtime/stop-coordinator.mjs +323 -6
  62. package/template_project/.codex/README.md +8 -0
  63. package/template_project/.omp/hooks/pre/ukit-bridge.js +8 -1
  64. package/template_project/ukit/README.md +1 -1
  65. package/template_project/ukit/storage/config.json +20 -0
  66. package/template_user/playbooks/architecture-decision.md +28 -0
  67. package/template_user/playbooks/autonomous-run.md +43 -0
  68. package/template_user/playbooks/autopilot-full.md +59 -0
  69. package/template_user/playbooks/autopilot-stack.md +54 -0
  70. package/template_user/playbooks/babysit.md +39 -0
  71. package/template_user/playbooks/bug-fix.md +3 -1
  72. package/template_user/playbooks/{issue-implementation.md → feature-implementation.md} +4 -2
  73. package/template_user/playbooks/hillclimb.md +44 -0
  74. package/template_user/playbooks/investigation.md +21 -0
  75. package/template_user/playbooks/migration.md +21 -0
  76. package/template_user/playbooks/open-pr.md +48 -0
  77. package/template_user/playbooks/orchestrate.md +45 -0
  78. package/template_user/playbooks/performance.md +33 -0
  79. package/template_user/playbooks/prototype.md +28 -0
  80. package/template_user/playbooks/refactor.md +19 -0
  81. package/template_user/playbooks/release.md +28 -0
  82. package/template_user/playbooks/runtime-forensics.md +23 -0
  83. package/template_user/playbooks/session-pickup.md +31 -0
  84. package/template_user/playbooks/shipping.md +53 -0
  85. package/template_user/playbooks/skill-evaluation.md +48 -0
  86. package/template_user/playbooks/small-feature.md +20 -0
  87. package/template_user/playbooks/verification-map.json +153 -0
  88. package/template_user/playbooks/verification.md +22 -0
  89. package/template_user/playbooks/worktree-cleanup.md +37 -0
@@ -0,0 +1,169 @@
1
+ #!/usr/bin/env node
2
+ // codex-capability-probe.mjs — TASK-001 / BL-001 + BL-002 scripted probe.
3
+ //
4
+ // Probes the installed `.codex/` surface of a UKit workspace and records what
5
+ // can be shown WITHOUT a live Codex CLI session:
6
+ // 1. codex CLI presence on PATH (live-session gate).
7
+ // 2. `.codex/` surface inventory (symlinks + files shipped by install).
8
+ // 3. `.codex/agents` resolution into real agent definitions.
9
+ // 4. Review-lane script reachability: `review-verdict.mjs` and
10
+ // `review-panel-aggregate.mjs` invoked THROUGH the `.codex/ukit` symlink
11
+ // (the exact path instruction-mediated Codex Bash would use).
12
+ // 5. Instruction mediation: `.codex/README.md` / `settings.json` point the
13
+ // model at `.codex/ukit/index/*` helpers.
14
+ //
15
+ // Honesty contract (SPEC §14): output states are PROVEN / ADVISORY-REACHABLE /
16
+ // INCONCLUSIVE / UNSUPPORTED — a cell can never be promoted beyond what the
17
+ // printed evidence shows. `codex-cli: absent` ⇒ subagent consumption is
18
+ // INCONCLUSIVE by construction; no claim of tested capability is emitted.
19
+ //
20
+ // Usage: node scripts/probe/codex-capability-probe.mjs [workspace-root]
21
+
22
+ import fs from 'node:fs';
23
+ import os from 'node:os';
24
+ import path from 'node:path';
25
+ import process from 'node:process';
26
+ import { spawnSync } from 'node:child_process';
27
+
28
+ // The probe targets an INSTALLED workspace (.codex/ exists after `ukit
29
+ // install`); pass the workspace root explicitly when it differs from cwd.
30
+ const ROOT = path.resolve(process.argv[2] ?? process.cwd());
31
+ const CODEX_DIR = path.join(ROOT, '.codex');
32
+
33
+ const out = [];
34
+ const say = (line = '') => { out.push(line); console.log(line); };
35
+
36
+ function commandOnPath(cmd) {
37
+ const r = spawnSync('sh', ['-c', `command -v ${cmd}`], { encoding: 'utf8' });
38
+ return r.status === 0 ? r.stdout.trim() : null;
39
+ }
40
+
41
+ function lstatKind(p) {
42
+ try {
43
+ const st = fs.lstatSync(p);
44
+ if (st.isSymbolicLink()) return `symlink -> ${fs.readlinkSync(p)}`;
45
+ if (st.isDirectory()) return 'dir';
46
+ return `file (${st.size}B)`;
47
+ } catch {
48
+ return 'ABSENT';
49
+ }
50
+ }
51
+
52
+ say(`probe-root: ${ROOT}`);
53
+ say(`probe-at: ${new Date().toISOString()}`);
54
+
55
+ // --- 1. codex CLI presence -------------------------------------------------
56
+ const codexBin = commandOnPath('codex');
57
+ say(`codex-cli: ${codexBin ? `present (${codexBin})` : 'absent'}`);
58
+
59
+ // --- 2. .codex surface inventory ------------------------------------------
60
+ say('--- .codex surface inventory ---');
61
+ let codexEntries = [];
62
+ try {
63
+ codexEntries = fs.readdirSync(CODEX_DIR).sort();
64
+ } catch {
65
+ say('.codex/: ABSENT (run `node bin/ukit install` first)');
66
+ }
67
+ for (const name of codexEntries) {
68
+ say(` .codex/${name}: ${lstatKind(path.join(CODEX_DIR, name))}`);
69
+ }
70
+
71
+ // --- 3. .codex/agents resolution -------------------------------------------
72
+ say('--- .codex/agents resolution ---');
73
+ const agentsDir = path.join(CODEX_DIR, 'agents');
74
+ let agentFiles = [];
75
+ try {
76
+ agentFiles = fs.readdirSync(agentsDir).filter((f) => f.endsWith('.md')).sort();
77
+ } catch {
78
+ // absent
79
+ }
80
+ say(` resolved-agent-files: ${agentFiles.length} [${agentFiles.join(', ')}]`);
81
+ for (const wanted of ['code-reviewer.md', 'handoff-planner.md', 'handoff-review.md']) {
82
+ say(` ${wanted}: ${agentFiles.includes(wanted) ? 'present' : 'absent'}`);
83
+ }
84
+
85
+ // --- 4. review-lane reachability via .codex/ukit ---------------------------
86
+ say('--- review-lane reachability (via .codex/ukit symlink) ---');
87
+ const verdictCli = path.join(CODEX_DIR, 'ukit', 'index', 'review-verdict.mjs');
88
+ const panelCli = path.join(CODEX_DIR, 'ukit', 'index', 'review-panel-aggregate.mjs');
89
+ say(` .codex/ukit/index/review-verdict.mjs: ${fs.existsSync(verdictCli) ? 'resolves' : 'missing'}`);
90
+ say(` .codex/ukit/index/review-panel-aggregate.mjs: ${fs.existsSync(panelCli) ? 'resolves' : 'missing'}`);
91
+
92
+ let verdictReachable = false;
93
+ let panelReachable = false;
94
+ if (fs.existsSync(verdictCli) || fs.existsSync(panelCli)) {
95
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ukit-codex-probe-'));
96
+ try {
97
+ if (fs.existsSync(verdictCli)) {
98
+ const findings = path.join(tmp, 'findings.json');
99
+ fs.writeFileSync(findings, JSON.stringify({
100
+ findings: [{ id: 'P1', severity: 'minor', file: 'probe.js', line: 1, claim: 'probe finding' }],
101
+ }));
102
+ // Isolate config: empty root/home => decisionPlane stage resolves 'off',
103
+ // so no gateway call — deterministic fallback is the demonstrated path.
104
+ const r = spawnSync('node', [verdictCli, '--file', findings], {
105
+ encoding: 'utf8',
106
+ env: { ...process.env, UKIT_TEST_ROOT: tmp, UKIT_TEST_HOME: tmp },
107
+ timeout: 15000,
108
+ });
109
+ const parsed = JSON.parse(r.stdout.trim() || '{}');
110
+ say(` review-verdict run: exit=${r.status} verdict=${parsed.verdict ?? 'UNPARSED'} `
111
+ + `stage=${parsed.stage ?? '-'} verdictSource=${parsed.verdictSource ?? '-'} `
112
+ + `outcomeClass=${parsed.outcomeClass ?? '-'}`);
113
+ verdictReachable = r.status === 0 && typeof parsed.verdict === 'string';
114
+ }
115
+ if (fs.existsSync(panelCli)) {
116
+ const task = path.join(tmp, 'PROBE-TASK.md');
117
+ fs.writeFileSync(task, [
118
+ '# probe task',
119
+ '## Reviewer Verdict',
120
+ 'MEMBER: probe-reviewer-a',
121
+ 'VERDICT: APPROVED',
122
+ '## Reviewer Verdict',
123
+ 'MEMBER: probe-reviewer-b',
124
+ 'VERDICT: APPROVED',
125
+ '## Other',
126
+ 'tail',
127
+ '',
128
+ ].join('\n'));
129
+ const r = spawnSync('node', [panelCli, task], { encoding: 'utf8', timeout: 15000 });
130
+ const verdictLine = (r.stdout || '').split('\n').find((l) => /^VERDICT:/.test(l)) ?? '(no VERDICT line)';
131
+ say(` review-panel-aggregate run: exit=${r.status} ${verdictLine.trim()}`);
132
+ panelReachable = r.status === 0 && /^VERDICT: approved/.test(verdictLine.trim());
133
+ }
134
+ } finally {
135
+ fs.rmSync(tmp, { recursive: true, force: true });
136
+ }
137
+ }
138
+
139
+ // --- 5. instruction mediation ----------------------------------------------
140
+ say('--- instruction mediation ---');
141
+ const readmePath = path.join(CODEX_DIR, 'README.md');
142
+ const settingsPath = path.join(CODEX_DIR, 'settings.json');
143
+ try {
144
+ const readme = fs.readFileSync(readmePath, 'utf8');
145
+ const hits = (readme.match(/\.codex\/ukit\/index\/[\w-]+\.mjs/g) ?? []);
146
+ say(` .codex/README.md helper references: ${[...new Set(hits)].join(', ') || 'none'}`);
147
+ } catch {
148
+ say(' .codex/README.md: ABSENT');
149
+ }
150
+ try {
151
+ const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
152
+ const helper = settings?.skillRouter?.helperCommand ?? '(none)';
153
+ say(` .codex/settings.json skillRouter.helperCommand: ${helper}`);
154
+ } catch {
155
+ say(' .codex/settings.json: ABSENT/unparseable');
156
+ }
157
+
158
+ // --- verdicts ---------------------------------------------------------------
159
+ say('--- verdicts ---');
160
+ const agentsSurface = agentFiles.length > 0;
161
+ say(`codex.subagents: ${codexBin ? 'PENDING-LIVE' : 'INCONCLUSIVE'}`
162
+ + ` (.codex/agents surface ${agentsSurface ? 'shipped+resolved' : 'not resolvable'};`
163
+ + ` host consumption ${codexBin ? 'requires live-session drive' : 'unprovable — codex CLI absent'})`);
164
+ say(`codex.review-lane: ${verdictReachable && panelReachable ? 'ADVISORY-REACHABLE' : 'INCONCLUSIVE'}`
165
+ + ` (instruction-mediated Bash via .codex/ukit symlink;`
166
+ + ` review-verdict=${verdictReachable ? 'ran' : 'failed'},`
167
+ + ` panel-aggregate=${panelReachable ? 'ran' : 'failed'};`
168
+ + ' enforcement: advisory — Codex exposes no hook surface)');
169
+ say('probe-end');
@@ -31,6 +31,10 @@ import { findOpencodeArtifacts, opencodeSteerMessage } from '../../core/opencode
31
31
  import { buildUserPaths } from '../../core/userPaths.js';
32
32
  import { loadPlaybooks } from '../../core/userPlaybooks.js';
33
33
  import { userMemoryStats } from '../../core/memory/userMemory.js';
34
+ import {
35
+ inspectVerificationMap,
36
+ doctorRecipeCheck,
37
+ } from '../../index/verificationMap.js';
34
38
 
35
39
  import { resolveSeamStages } from '../../core/observability/rollout.js';
36
40
  import {
@@ -248,6 +252,97 @@ function printFlightRecorderSection(flightRecorder) {
248
252
  }
249
253
  }
250
254
 
255
+ // TASK-006 (BL-008) — Codex surface report. Advisory only: the section prints
256
+ // what was measured, never an assumption — hook-surface absence, the
257
+ // .codex/agents + .codex/ukit symlink state, the subagents capability cell read
258
+ // from manifests/hostCapabilities.yaml (TASK-001 probe values), and the
259
+ // advisory enforcement ceiling. It never enters `checks`/`projectChecks`, so
260
+ // it can never move the exit code.
261
+
262
+ // '.codex/agents' ships as a symlink to '../.claude/agents'. readdir follows
263
+ // the symlink, so a broken link and a missing real directory collapse into
264
+ // the same 'broken' state — readdir failing while the path itself exists
265
+ // means the entry is present but unreadable (dangling link, or a file).
266
+ async function inspectCodexDirEntry(projectRoot, name) {
267
+ const entryPath = path.join(projectRoot, '.codex', name);
268
+ // lstat (not pathExists/stat): the entry ships as a symlink, and a dangling
269
+ // symlink stats ENOENT — indistinguishable from absent. lstat sees the link
270
+ // itself, so 'broken' reports the dangling-link truth.
271
+ let stat = null;
272
+ try {
273
+ stat = await fs.lstat(entryPath);
274
+ } catch {
275
+ return { state: 'absent', count: null };
276
+ }
277
+ if (!stat.isDirectory() && !stat.isSymbolicLink()) return { state: 'broken', count: null };
278
+ try {
279
+ const entries = await fs.readdir(entryPath);
280
+ return {
281
+ state: 'resolves',
282
+ count: entries.filter((e) => e.endsWith('.md')).length,
283
+ };
284
+ } catch {
285
+ return { state: 'broken', count: null };
286
+ }
287
+ }
288
+
289
+ async function inspectCodexSurface({ projectRoot, packageRoot }) {
290
+ const codexDir = path.join(projectRoot, '.codex');
291
+ if (!(await pathExists(codexDir))) return { installed: false };
292
+
293
+ const subagentsCell = { state: null, note: null };
294
+ try {
295
+ const caps = parse(await fs.readFile(
296
+ path.join(packageRoot, 'manifests', 'hostCapabilities.yaml'), 'utf8',
297
+ ));
298
+ const cell = caps?.hosts?.codex?.subagents;
299
+ subagentsCell.state = typeof cell?.state === 'string' ? cell.state : null;
300
+ subagentsCell.note = typeof cell?.note === 'string' ? cell.note
301
+ : typeof cell?.evidence === 'string' ? cell.evidence : null;
302
+ } catch {
303
+ // manifest absent/unreadable — state stays unknown, never assumed.
304
+ }
305
+
306
+ return {
307
+ installed: true,
308
+ readme: await pathExists(path.join(codexDir, 'README.md')),
309
+ settings: await pathExists(path.join(codexDir, 'settings.json')),
310
+ settingsLocal: await pathExists(path.join(codexDir, 'settings.local.json')),
311
+ agents: await inspectCodexDirEntry(projectRoot, 'agents'),
312
+ ukit: await inspectCodexDirEntry(projectRoot, 'ukit'),
313
+ subagents: subagentsCell,
314
+ };
315
+ }
316
+
317
+ function printCodexSurfaceSection(surface) {
318
+ const mark = (v) => (v ? '✓' : '✗');
319
+ console.log('[UKit] Codex surface checks (advisory — never affects exit code):');
320
+ if (!surface.installed) {
321
+ console.log('[UKit] - codex surface not installed (no .codex/ directory — run ukit install to add it)');
322
+ return;
323
+ }
324
+ const entryLine = (label, entry) => {
325
+ if (entry.state === 'resolves') {
326
+ console.log(`[UKit] ✓ ${label}: resolves${entry.count !== null ? ` (${entry.count} .md)` : ''}`);
327
+ } else if (entry.state === 'broken') {
328
+ console.log(`[UKit] ✗ ${label}: broken (present but unreadable — rerun ukit install)`);
329
+ } else {
330
+ console.log(`[UKit] - ${label}: absent`);
331
+ }
332
+ };
333
+ console.log(`[UKit] ${mark(surface.readme)} .codex/README.md`);
334
+ console.log(`[UKit] ${mark(surface.settings)} .codex/settings.json`);
335
+ console.log(`[UKit] ${mark(surface.settingsLocal)} .codex/settings.local.json`);
336
+ entryLine('.codex/agents', surface.agents);
337
+ entryLine('.codex/ukit', surface.ukit);
338
+ const subagents = surface.subagents;
339
+ console.log(
340
+ `[UKit] - codex.subagents capability: ${subagents.state ?? 'unknown'}${subagents.note ? ` — ${subagents.note}` : ''}`,
341
+ );
342
+ console.log('[UKit] - hook surface: absent — Codex exposes no hook surface; routing is instruction-mediated via route-task.mjs');
343
+ console.log('[UKit] - enforcement ceiling: advisory — codex outcome records carry enforcement: advisory');
344
+ }
345
+
251
346
  // --json machine shape: null = unknown (mirrors `ukit telemetry status`).
252
347
  function flightRecorderJson(flightRecorder) {
253
348
  const { segments, queue, lastProjection, crashes } = flightRecorder;
@@ -276,6 +371,53 @@ function flightRecorderJson(flightRecorder) {
276
371
  };
277
372
  }
278
373
 
374
+
375
+ // TASK-C85-014 (BL-014): recipe-vs-package.json check. A verification recipe
376
+ // whose named harness script is absent degrades to a capability-negotiated
377
+ // fallback RECEIPT — the report returns the receipt spec verbatim and never
378
+ // fabricates a runnable command. Advisory row only: a missing/malformed map
379
+ // or an absent harness warns, it never fails the doctor run.
380
+ export async function inspectVerificationRecipe({ projectRoot, homeDir = null, builtinPath = null } = {}) {
381
+ const inspection = await inspectVerificationMap({ projectRoot, homeDir, builtinPath });
382
+ if (!inspection.map) {
383
+ return {
384
+ status: 'warn',
385
+ label: `verification-map recipes: unavailable (${inspection.reason || 'no-map'})`,
386
+ detail: 'guard and stop gate fall back to pre-map verification behavior',
387
+ remedy: 'Run ukit install to seed ~/.ukit/playbooks/verification-map.json',
388
+ missing: [],
389
+ checked: 0,
390
+ source: null,
391
+ reason: inspection.reason || 'no-map',
392
+ };
393
+ }
394
+ let scripts = {};
395
+ try {
396
+ const pkg = JSON.parse(await fs.readFile(path.join(projectRoot, 'package.json'), 'utf8'));
397
+ scripts = pkg?.scripts && typeof pkg.scripts === 'object' ? pkg.scripts : {};
398
+ } catch {
399
+ scripts = {};
400
+ }
401
+ const check = doctorRecipeCheck({ map: inspection.map, packageJsonScripts: scripts });
402
+ if (check.missing.length === 0) {
403
+ return {
404
+ status: 'ok',
405
+ label: `verification-map recipes: ${check.checked} harness script(s) present`,
406
+ missing: [],
407
+ checked: check.checked,
408
+ source: inspection.source,
409
+ reason: null,
410
+ };
411
+ }
412
+ return {
413
+ status: 'warn',
414
+ label: `verification-map recipes: ${check.missing.length} harness script(s) absent from package.json`,
415
+ missing: check.missing,
416
+ checked: check.checked,
417
+ source: inspection.source,
418
+ reason: 'absent-harness',
419
+ };
420
+ }
279
421
  export async function runDoctor({ packageRoot, projectRoot, argv = [], homeDir = os.homedir(), ompPath = 'omp' }) {
280
422
  const unknownFlags = argv.filter((flag) => !KNOWN_FLAGS.has(flag));
281
423
  if (unknownFlags.length > 0) {
@@ -616,6 +758,32 @@ export async function runDoctor({ packageRoot, projectRoot, argv = [], homeDir =
616
758
  }
617
759
  }
618
760
 
761
+ // TASK-006 (BL-008) — codex surface report. Statements, never assumptions:
762
+ // what is on disk and what the manifests record. Advisory only — it never
763
+ // enters `checks` or `projectChecks` and cannot move the exit code.
764
+ printCodexSurfaceSection(await inspectCodexSurface({ projectRoot, packageRoot }));
765
+
766
+
767
+ // TASK-C85-014 (BL-014) — verification-map recipe check. Advisory only:
768
+ // warn rows never enter the blocking set and never fabricate a command —
769
+ // a recipe naming an absent harness prints its fallback RECEIPT spec.
770
+ {
771
+ const recipe = await inspectVerificationRecipe({ projectRoot, homeDir });
772
+ console.log('[UKit] Verification-map checks (advisory — does not affect exit code):');
773
+ const sev = recipe.status === 'warn' ? ' (warning)' : '';
774
+ console.log(`[UKit] ${ok(recipe.status === 'ok')} ${recipe.label}${sev}`);
775
+ if (recipe.detail) console.log(`[UKit] detail: ${recipe.detail}`);
776
+ for (const entry of recipe.missing ?? []) {
777
+ console.log(`[UKit] missing: ${entry.artifactClass} → package.json script '${entry.script}'`);
778
+ if (entry.fallback) console.log(`[UKit] fallback: ${entry.fallback}`);
779
+ }
780
+ if (recipe.remedy && recipe.status === 'warn') {
781
+ console.log(`[UKit] remedy: ${recipe.remedy}`);
782
+ }
783
+ if (recipe.status === 'warn' && recipe.reason === 'absent-harness') {
784
+ console.log('[UKit] remedy: add the named script(s) to package.json, or rely on the fallback receipt attestation — no substitute command is fabricated.');
785
+ }
786
+ }
619
787
  if (runtimeConfigInspection.errors.length > 0) {
620
788
  console.log(`[UKit] Runtime config issues: ${runtimeConfigInspection.errors.join(' | ')}`);
621
789
  }
@@ -226,6 +226,13 @@ export async function runIndexTools({ projectRoot, argv = [] }) {
226
226
  if (nextDisplay) {
227
227
  console.log(`next: ${nextDisplay}`);
228
228
  }
229
+ // BL-018: the fix-loop escalation lane — 'blocked' on exhaustion.
230
+ if (result.routeSummary?.escalatedLane) {
231
+ console.log(`escalatedLane: ${result.routeSummary.escalatedLane}`);
232
+ if (result.routeSummary.escalateReason) {
233
+ console.log(`escalateReason: ${result.routeSummary.escalateReason}`);
234
+ }
235
+ }
229
236
  if (result.routeSummary?.delegateHint) {
230
237
  console.log(`delegate: ${result.routeSummary.delegateHint}`);
231
238
  }
@@ -1,10 +1,19 @@
1
- // `ukit metrics` — read-only telemetry roll-up (SPEC §6, SI-102).
1
+ // `ukit metrics` — telemetry roll-up (SPEC §6, SI-102).
2
2
  // Route outcomes + failure patterns (lazy, degrade to n/a) + v2 memory count.
3
3
  // Never exits non-zero on missing telemetry; unknown flag → exit 1.
4
+ // BL-019 (SPEC FR-001): the one deliberate write — the joined corpus derives
5
+ // tier-map.json, the cache artifact the router reads for measured tier picks.
4
6
 
5
7
  import fs from 'node:fs/promises';
6
8
  import path from 'node:path';
7
9
  import { collectRouteOutcomes } from '../../diagnostics/routeOutcomes.js';
10
+ import { MODEL_TIER_BY_CONTRACT } from '../../core/executionContracts.js';
11
+ import {
12
+ deriveTierStats,
13
+ evaluateTierPromotion,
14
+ buildTierMapArtifact,
15
+ writeTierMap,
16
+ } from '../../index/tierSelection.js';
8
17
  import { buildRuntimePaths } from '../../core/runtimePaths.js';
9
18
 
10
19
  const HELP_FLAGS = new Set(['--help', '-h', 'help']);
@@ -74,6 +83,57 @@ function printRouteOutcomes(routeOutcomes) {
74
83
  }
75
84
  }
76
85
 
86
+ // BL-019 (SPEC FR-001 §7): derive the measured tier map from the joined
87
+ // corpus and persist the cache artifact the router reads. Returns the rollup
88
+ // the print/JSON surfaces consume; null when there is no joined data at all
89
+ // (no artifact written — an empty corpus must not clobber a live promote map).
90
+ async function deriveAndWriteTierMap(projectRoot, routeOutcomes) {
91
+ try {
92
+ const joinedRows = Array.isArray(routeOutcomes?.joinedRows) ? routeOutcomes.joinedRows : [];
93
+ if (joinedRows.length === 0) return null;
94
+ const stats = deriveTierStats(joinedRows);
95
+ const evaluation = evaluateTierPromotion({ stats, staticMap: MODEL_TIER_BY_CONTRACT });
96
+ const artifact = buildTierMapArtifact({ stats, staticMap: MODEL_TIER_BY_CONTRACT });
97
+ await writeTierMap(projectRoot, artifact);
98
+ return {
99
+ joinedRoutes: stats.joinedRoutes,
100
+ agreement: evaluation.agreement,
101
+ cheaperShare: evaluation.cheaperShare,
102
+ regressionFree: evaluation.regressionFree,
103
+ decision: evaluation.decision,
104
+ reasons: evaluation.reasons,
105
+ byMode: evaluation.byMode,
106
+ };
107
+ } catch (error) {
108
+ return { error: error?.message ?? String(error) };
109
+ }
110
+ }
111
+
112
+ function printTierSelection(tierSelection) {
113
+ console.log('tier-selection');
114
+ if (!tierSelection) {
115
+ console.log(' n/a (no joined route↔outcome data)');
116
+ return;
117
+ }
118
+ if (tierSelection.error) {
119
+ console.log(` n/a (${tierSelection.error})`);
120
+ return;
121
+ }
122
+ const reasons = Array.isArray(tierSelection.reasons) && tierSelection.reasons.length > 0
123
+ ? ` reasons: ${tierSelection.reasons.join(', ')}`
124
+ : '';
125
+ console.log(
126
+ ` joined: ${tierSelection.joinedRoutes} agreement: ${(tierSelection.agreement * 100).toFixed(1)}%`
127
+ + ` cheaper-share: ${(tierSelection.cheaperShare * 100).toFixed(1)}%`
128
+ + ` regression-free: ${tierSelection.regressionFree}`
129
+ + ` decision: ${tierSelection.decision}${reasons}`,
130
+ );
131
+ const picks = Object.entries(tierSelection.byMode ?? {});
132
+ if (picks.length > 0) {
133
+ console.log(` measured: ${picks.map(([mode, tier]) => `${mode}→${tier}`).join(' ')}`);
134
+ }
135
+ }
136
+
77
137
  function printFailurePatterns(failurePatterns) {
78
138
  console.log('failure-patterns');
79
139
  if (!failurePatterns) {
@@ -211,8 +271,10 @@ export async function runMetrics({ projectRoot, argv = [] }) {
211
271
  const skillAccuracy = await lazyCollect('../../diagnostics/skillAccuracy.js', 'collectSkillAccuracy', projectRoot);
212
272
  const laneStats = await lazyCollect('../../diagnostics/laneStats.js', 'collectLaneStats', projectRoot);
213
273
  const learning = await collectLearningSuggestions(projectRoot);
274
+ // BL-019: derive + persist the measured tier map from the joined corpus.
275
+ const tierSelection = await deriveAndWriteTierMap(projectRoot, routeOutcomes);
214
276
 
215
- const rollup = { routeOutcomes, failurePatterns, memory, feedback, skillAccuracy, laneStats, learning };
277
+ const rollup = { routeOutcomes, failurePatterns, memory, feedback, skillAccuracy, laneStats, learning, tierSelection };
216
278
 
217
279
  if (argv.includes('--json')) {
218
280
  console.log(JSON.stringify(rollup, null, 2));
@@ -229,6 +291,8 @@ export async function runMetrics({ projectRoot, argv = [] }) {
229
291
  console.log('');
230
292
  printLaneStats(laneStats);
231
293
  console.log('');
294
+ printTierSelection(tierSelection);
295
+ console.log('');
232
296
  printLearning(learning);
233
297
  console.log('');
234
298
  printMemory(memory);
@@ -13,7 +13,7 @@ export function printPlaybookHelp() {
13
13
  console.log('Resolution precedence: project (.ukit/playbooks) > user (~/.ukit/playbooks) > builtin.');
14
14
  }
15
15
 
16
- export async function runPlaybook({ projectRoot, homeDir, argv = [], builtins } = {}) {
16
+ export async function runPlaybook({ projectRoot, homeDir, argv = [], builtins, builtinDir } = {}) {
17
17
  const [subcommandRaw, ...rest] = argv;
18
18
  const subcommand = (subcommandRaw ?? 'list').toLowerCase();
19
19
 
@@ -23,9 +23,9 @@ export async function runPlaybook({ projectRoot, homeDir, argv = [], builtins }
23
23
  }
24
24
 
25
25
  if (subcommand === 'list') {
26
- const rows = await listPlaybooks({ projectRoot, homeDir, builtins });
26
+ const rows = await listPlaybooks({ projectRoot, homeDir, builtins, builtinDir });
27
27
  if (rows.length === 0) {
28
- console.log('No playbooks found (built-ins: bug-fix, issue-implementation)');
28
+ console.log('No playbooks found (built-ins: bug-fix, feature-implementation)');
29
29
  return;
30
30
  }
31
31
  console.log('id | source | lanes');
@@ -40,7 +40,7 @@ export async function runPlaybook({ projectRoot, homeDir, argv = [], builtins }
40
40
  if (!id) {
41
41
  throw new Error('Missing playbook id. Usage: ukit playbook show <id>');
42
42
  }
43
- const playbook = await resolvePlaybook(id, { projectRoot, homeDir, builtins });
43
+ const playbook = await resolvePlaybook(id, { projectRoot, homeDir, builtins, builtinDir });
44
44
  if (playbook === null) {
45
45
  throw new Error(`Unknown playbook: ${id}`);
46
46
  }
@@ -1,19 +1,25 @@
1
1
  // `ukit vm` — agent-VM live-path driver surface (AGENT_VM_DRIVER_PLAN D-01,
2
- // D-02, D-03). Discovery + shadow-evidence commands over the
3
- // language-compiled runtime; nothing here executes real operations
4
- // (shadow startFn is a stub) and nothing mutates config.
2
+ // D-02, D-03; BL-031 live `run` entry). Discovery + shadow-evidence
3
+ // commands over the language-compiled runtime; shadow runs never execute
4
+ // real operations (stub startFn), the `run` lane launches RUN-node specs
5
+ // through the real supervisor — canary/default only. Nothing mutates
6
+ // config.
5
7
  //
6
8
  // list Reference plans (planId, planVersion, irHash)
7
9
  // show <planId> One plan's compiled node ledger
8
10
  // status Stage + evidence counts + shadow dir
9
11
  // shadow-run <planId> Deterministic replay through a real VmEngine;
10
12
  // appends one evidence record per run (D-03)
13
+ // run <planId> Live execution through a real VmEngine +
14
+ // supervisor launches (canary/default only);
15
+ // appends one evidence record per run (BL-031)
11
16
  // evidence <key> Read .ukit/storage/agent-runtime/evidence/<key>.jsonl
12
17
  // and print the promote() verdict verbatim (G-D3)
13
18
  //
14
19
  // Every subcommand is stage-gated on `decisionRuntime.vm`: 'off' → typed
15
20
  // refuse `stage_off`, exit 2, zero writes under
16
- // .ukit/storage/agent-runtime/ (G-D1). Exit codes: 0 success,
21
+ // .ukit/storage/agent-runtime/ (G-D1); 'run' additionally refuses any
22
+ // stage below canary with `live_stage_required`. Exit codes: 0 success,
17
23
  // 2 typed failure, 1 usage error.
18
24
 
19
25
  import fs from 'node:fs/promises';
@@ -28,12 +34,13 @@ import { loadPlan, loadPlanLibrary } from '../../core/agentRuntime/planLibrary.j
28
34
  import { promote, promotionCriteriaFor } from '../../core/agentRuntime/promotion.js';
29
35
  import {
30
36
  runShadowPlan,
37
+ runLivePlan,
31
38
  readEvidence,
32
39
  } from '../../core/agentRuntime/shadowRun.js';
33
40
 
34
41
  const HELP_FLAGS = new Set(['--help', '-h', 'help']);
35
- const SUBCOMMANDS = new Set(['list', 'show', 'status', 'shadow-run', 'evidence']);
36
- const OPERAND_SUBCOMMANDS = new Set(['show', 'shadow-run', 'evidence']);
42
+ const SUBCOMMANDS = new Set(['list', 'show', 'status', 'shadow-run', 'run', 'evidence']);
43
+ const OPERAND_SUBCOMMANDS = new Set(['show', 'shadow-run', 'run', 'evidence']);
37
44
 
38
45
  const RUNTIME_REL = path.join('.ukit', 'storage', 'agent-runtime');
39
46
 
@@ -45,6 +52,8 @@ function printUsage() {
45
52
  console.log(' show <planId> Show one plan\'s compiled node ledger');
46
53
  console.log(' status Show decisionRuntime.vm stage + evidence summary');
47
54
  console.log(' shadow-run <planId> Run one deterministic shadow pass (stub startFn)');
55
+ console.log(' run <planId> Execute live via the real VmEngine+supervisor path');
56
+ console.log(' (requires decisionRuntime.vm stage canary or default)');
48
57
  console.log(' evidence <key> Print the promote() verdict over stored runs');
49
58
  console.log('');
50
59
  console.log('Options:');
@@ -168,6 +177,27 @@ async function shadowRunCommand({ projectRoot, config, planId }) {
168
177
  }
169
178
  }
170
179
 
180
+ // --- run (BL-031 live lane) -------------------------------------------------
181
+
182
+ async function runCommand({ projectRoot, config, planId }) {
183
+ // Stage narrowing lives inside runLivePlan: 'shadow' → live_stage_required
184
+ // typed refuse, exit 2, zero writes; canary/default execute + append
185
+ // exactly one evidence record through the same sink as shadow-run.
186
+ const res = await runLivePlan(planId, { projectRoot, config });
187
+ if (!res.ok) {
188
+ console.error(`[UKit] vm run: ${res.code} — ${res.reason}`);
189
+ process.exitCode = 2;
190
+ return;
191
+ }
192
+ console.log(`run ${res.planId}: ${res.status} (${res.code})`);
193
+ console.log(` planInstance ${res.planInstanceId}`);
194
+ console.log(` planVersion ${res.planVersion} irHash ${res.irHash}`);
195
+ console.log(` transitions ${res.transitions.length} wallMs ${res.wallMs}`);
196
+ for (const t of res.transitions) {
197
+ console.log(` ${t.node} -> ${t.to}${t.branch !== undefined ? ` (${t.branch})` : ''}`);
198
+ }
199
+ }
200
+
171
201
  // --- evidence -------------------------------------------------------------
172
202
 
173
203
  async function evidenceCommand({ projectRoot, config, key, json }) {
@@ -177,15 +207,25 @@ async function evidenceCommand({ projectRoot, config, key, json }) {
177
207
  process.exitCode = 2;
178
208
  return;
179
209
  }
180
- const verdict = promote(config, { key, runs: res.runs });
210
+ // Comparison artifacts are recorded evidence, not sampled runs — they
211
+ // stay out of the promote() run set but their verdict prints verbatim.
212
+ const replay = [...res.runs].reverse().find((r) => r?.kind === 'replay-comparison');
213
+ const sampled = res.runs.filter((r) => r?.kind !== 'replay-comparison');
214
+ const verdict = promote(config, { key, runs: sampled });
181
215
  const criteria = promotionCriteriaFor(key);
182
216
  if (json) {
183
- console.log(JSON.stringify({ key, runs: res.runs.length, dropped: res.dropped, criteria, verdict }));
217
+ console.log(JSON.stringify({
218
+ key, runs: res.runs.length, dropped: res.dropped, criteria, verdict,
219
+ ...(replay ? { replay } : {}),
220
+ }));
184
221
  return;
185
222
  }
186
223
  console.log(`evidence ${key}: ${res.runs.length} run(s)${res.dropped > 0 ? ` (${res.dropped} malformed dropped)` : ''}`);
187
224
  // G-D3: verdict is advisory — printed verbatim, never written back.
188
225
  console.log(`promote ${key}: stage=${verdict.stage} promoted=${verdict.promoted} reason=${verdict.reason}`);
226
+ if (replay) {
227
+ console.log(`replay ${key}: verdict=${replay.verdict} reason=${replay.reason} defectsCaught=${replay.defectsCaught} rows=${replay.rows?.length ?? 0}`);
228
+ }
189
229
  }
190
230
 
191
231
  // --- dispatch -------------------------------------------------------------
@@ -238,5 +278,6 @@ export async function runVm({ projectRoot, argv = [] }) {
238
278
  if (sub === 'show') return showCommand({ config, planId: operand });
239
279
  if (sub === 'status') return statusCommand({ projectRoot, config, json: flags.includes('--json') });
240
280
  if (sub === 'shadow-run') return shadowRunCommand({ projectRoot, config, planId: operand });
281
+ if (sub === 'run') return runCommand({ projectRoot, config, planId: operand });
241
282
  return evidenceCommand({ projectRoot, config, key: operand, json: flags.includes('--json') });
242
283
  }