@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,308 @@
1
+ // tierSelection.js — measured reliability → tier selection (C86 BL-019,
2
+ // SPEC FR-001 §5/§7/§8/§14).
3
+ //
4
+ // modelTier stops being purely static: the route-audit ↔ exec-ledger join
5
+ // (BL-003/004/007) produces measured per-(execution-mode, tier) reliability,
6
+ // every route logs the measured pick vs the static pick as a `tier-selection`
7
+ // shadow receipt, and the measured map becomes authoritative ONLY when the
8
+ // promotion gates pass (≥50 joined routes, ≥60% agreement, disagreements skew
9
+ // cheaper, no verdict regression). Thin data → the static map stays.
10
+ //
11
+ // Boundary rules (SPEC §14, ARCH §Safety Boundary):
12
+ // * Tier names only — 'lite'|'code'|'smart'. A model ID in the artifact is
13
+ // dropped at read time so it can never reach a route field or receipt.
14
+ // * `modelRoles` config is never written — the measured map is derived state
15
+ // under .ukit/storage/cache/, not user configuration.
16
+ // * handoff-model-guard.sh enforcement is unchanged (not touched here).
17
+ //
18
+ // Mirror parity: template_project/.claude/ukit/index/tier-selection.mjs
19
+ // carries the identical logic (the mirror cannot import src/). Both twins are
20
+ // locked by tests/consistency/tierSelectionParity.test.js.
21
+
22
+ import fs from 'node:fs/promises';
23
+ import path from 'node:path';
24
+ import crypto from 'node:crypto';
25
+
26
+ // Provider-neutral cost-tier vocabulary — literal mirror of the
27
+ // lite→code→smart ladder in src/core/executionContracts.js (MODEL_TIER_ORDER).
28
+ export const MEASURED_TIER_ORDER = Object.freeze(['lite', 'code', 'smart']);
29
+
30
+ // Derived cache artifact (SPEC §7): metrics/diagnostics writes, router reads.
31
+ export const TIER_MAP_PATH_REL = path.join('.ukit', 'storage', 'cache', 'tier-map.json');
32
+
33
+ // Executor-decided defaults (task Discussion): a conservative reliability
34
+ // floor so a cheap-but-flaky tier never displaces a reliable one, and a 24h
35
+ // artifact TTL so a stale promote cannot silently own routing forever.
36
+ const DEFAULT_RELIABILITY_FLOOR = 0.8;
37
+ const TIER_MAP_MAX_AGE_MS = 24 * 60 * 60 * 1000;
38
+
39
+ const PROMOTION_REASONS = Object.freeze({
40
+ thin: 'joined<50',
41
+ agreement: 'agreement<0.6',
42
+ skew: 'disagreements-not-cheaper',
43
+ });
44
+
45
+ function tierName(value) {
46
+ return MEASURED_TIER_ORDER.includes(value) ? value : null;
47
+ }
48
+
49
+ // One joined row is either the {audit, ledger} pair collectRouteOutcomes
50
+ // emits or a flat {executionMode, modelTier, writeSucceeded} row — both are
51
+ function normalizeJoinedRow(row) {
52
+ if (!row || typeof row !== 'object' || Array.isArray(row)) return null;
53
+ const audit = row.audit && typeof row.audit === 'object' ? row.audit : row;
54
+ const ledger = row.ledger && typeof row.ledger === 'object' ? row.ledger : row;
55
+ // An empty object is not a joined row — nothing identifiable to count.
56
+ if (Object.keys(audit).length === 0) return null;
57
+ return { audit, ledger };
58
+ }
59
+
60
+ // SPEC §8: deriveTierStats(joinedRows) → { byModeTier, joinedRoutes }.
61
+ // byModeTier is "<mode>|<tier>"-keyed; `routes` counts joined rows where the
62
+ // route ran on that tier, `successes` counts writeSucceeded === true (the
63
+ // ledger outcome the existing byMode buckets already count), `reliability`
64
+ // is successes/routes. Rows without a provider-neutral tier name are counted
65
+ // in joinedRoutes (coverage) but never land in a tier bucket — a model ID
66
+ // must not create a pseudo-tier.
67
+ export function deriveTierStats(joinedRows = []) {
68
+ const byModeTier = {};
69
+ const rows = Array.isArray(joinedRows) ? joinedRows : [];
70
+ let joinedRoutes = 0;
71
+ for (const raw of rows) {
72
+ const normalized = normalizeJoinedRow(raw);
73
+ if (!normalized) continue;
74
+ joinedRoutes += 1;
75
+ const mode = typeof normalized.audit.executionMode === 'string'
76
+ ? normalized.audit.executionMode
77
+ : null;
78
+ const tier = tierName(normalized.audit.modelTier);
79
+ if (!mode || !tier) continue;
80
+ const key = `${mode}|${tier}`;
81
+ const bucket = byModeTier[key] ??= { routes: 0, successes: 0, reliability: 0 };
82
+ bucket.routes += 1;
83
+ if (normalized.ledger.writeSucceeded === true) bucket.successes += 1;
84
+ bucket.reliability = bucket.successes / bucket.routes;
85
+ }
86
+ return { byModeTier, joinedRoutes };
87
+ }
88
+
89
+ // SPEC §8: resolveMeasuredTier({executionMode, staticTier, stats,
90
+ // reliabilityFloor?}) → tier name | null. Cheapest tier whose measured
91
+ // reliability for that mode meets the floor; null when the data is thin or
92
+ // absent. `staticTier` rides the signature per SPEC but does not bias the
93
+ // pick — the measured table decides on its own.
94
+ export function resolveMeasuredTier({
95
+ executionMode = null,
96
+ staticTier = null,
97
+ stats = null,
98
+ reliabilityFloor = DEFAULT_RELIABILITY_FLOOR,
99
+ } = {}) {
100
+ if (!executionMode || !stats || typeof stats !== 'object') return null;
101
+ const floor = Number.isFinite(reliabilityFloor)
102
+ ? reliabilityFloor
103
+ : DEFAULT_RELIABILITY_FLOOR;
104
+ const byModeTier = stats.byModeTier ?? {};
105
+ for (const tier of MEASURED_TIER_ORDER) {
106
+ const bucket = byModeTier[`${executionMode}|${tier}`];
107
+ if (bucket && bucket.routes >= 1 && bucket.reliability >= floor) {
108
+ return tier;
109
+ }
110
+ }
111
+ return null;
112
+ }
113
+
114
+ // SPEC §8/§14: evaluateTierPromotion({stats, staticMap, minJoined?=50,
115
+ // minAgreement?=0.60}) → {decision, agreement, cheaperShare, regressionFree,
116
+ // reasons[], byMode, joinedRoutes}.
117
+ //
118
+ // Promote only when ALL gates pass:
119
+ // * joined ≥ minJoined (default 50 joined route↔outcome rows);
120
+ // * agreement ≥ minAgreement — the measured pick equals the static pick for
121
+ // ≥60% of modes where a pick exists;
122
+ // * disagreements skew cheaper — measured picks that differ must be a
123
+ // lower rung (cheaperShare = cheaper/disagreements > 0.5; zero
124
+ // disagreements passes as perfectly agreeing);
125
+ // * regressionFree — no mode where the measured pick's measured
126
+ // reliability is lower than the static pick's measured reliability
127
+ // (a tier-verdict regression, verdict = writeSucceeded reliability).
128
+ // `byMode` carries the measured pick per mode for the artifact; modes with
129
+ // no pick fall back to static at read time.
130
+ export function evaluateTierPromotion({
131
+ stats = null,
132
+ staticMap = {},
133
+ minJoined = 50,
134
+ minAgreement = 0.60,
135
+ reliabilityFloor = DEFAULT_RELIABILITY_FLOOR,
136
+ } = {}) {
137
+ const byModeTier = stats?.byModeTier ?? {};
138
+ const joinedRoutes = Number.isFinite(stats?.joinedRoutes) ? stats.joinedRoutes : 0;
139
+ const modes = Object.keys(staticMap && typeof staticMap === 'object' ? staticMap : {});
140
+ const byMode = {};
141
+
142
+ let compared = 0;
143
+ let agreed = 0;
144
+ let disagreements = 0;
145
+ let cheaper = 0;
146
+ const regressedModes = [];
147
+
148
+ for (const mode of modes) {
149
+ const staticTier = staticMap[mode];
150
+ const measured = resolveMeasuredTier({
151
+ executionMode: mode,
152
+ staticTier,
153
+ stats,
154
+ reliabilityFloor,
155
+ });
156
+ if (measured !== null) byMode[mode] = measured;
157
+ if (!tierName(staticTier) || measured === null) continue;
158
+ compared += 1;
159
+ if (measured === staticTier) {
160
+ agreed += 1;
161
+ continue;
162
+ }
163
+ disagreements += 1;
164
+ const measuredIdx = MEASURED_TIER_ORDER.indexOf(measured);
165
+ const staticIdx = MEASURED_TIER_ORDER.indexOf(staticTier);
166
+ if (measuredIdx !== -1 && staticIdx !== -1 && measuredIdx < staticIdx) {
167
+ cheaper += 1;
168
+ }
169
+ const measuredRel = byModeTier[`${mode}|${measured}`]?.reliability;
170
+ const staticRel = byModeTier[`${mode}|${staticTier}`]?.reliability;
171
+ if (Number.isFinite(measuredRel) && Number.isFinite(staticRel) && measuredRel < staticRel) {
172
+ regressedModes.push(mode);
173
+ }
174
+ }
175
+
176
+ const agreement = compared > 0 ? agreed / compared : 0;
177
+ const cheaperShare = disagreements > 0 ? cheaper / disagreements : 1;
178
+ const regressionFree = regressedModes.length === 0;
179
+
180
+ const reasons = [];
181
+ if (!(joinedRoutes >= minJoined)) reasons.push(PROMOTION_REASONS.thin);
182
+ if (!(agreement >= minAgreement)) reasons.push(PROMOTION_REASONS.agreement);
183
+ if (!(cheaperShare > 0.5)) reasons.push(PROMOTION_REASONS.skew);
184
+ for (const mode of regressedModes) reasons.push(`regression:${mode}`);
185
+
186
+ return {
187
+ decision: reasons.length === 0 ? 'promote' : 'hold',
188
+ agreement,
189
+ cheaperShare,
190
+ regressionFree,
191
+ reasons,
192
+ byMode,
193
+ joinedRoutes,
194
+ };
195
+ }
196
+
197
+ // SPEC §7: { version: 1, derivedAt, joinedRoutes, agreement, decision,
198
+ // byMode: {<mode>: <tier>} } — plus cheaperShare/reasons for the metrics
199
+ // rollup (additive, bounded enums/numbers only).
200
+ export function buildTierMapArtifact({
201
+ stats = null,
202
+ staticMap = {},
203
+ minJoined = 50,
204
+ minAgreement = 0.60,
205
+ now = () => new Date(),
206
+ } = {}) {
207
+ const evaluation = evaluateTierPromotion({ stats, staticMap, minJoined, minAgreement });
208
+ return {
209
+ version: 1,
210
+ derivedAt: now().toISOString(),
211
+ joinedRoutes: evaluation.joinedRoutes,
212
+ agreement: evaluation.agreement,
213
+ decision: evaluation.decision,
214
+ byMode: evaluation.byMode,
215
+ cheaperShare: evaluation.cheaperShare,
216
+ regressionFree: evaluation.regressionFree,
217
+ reasons: evaluation.reasons,
218
+ };
219
+ }
220
+
221
+ // Atomic tmp+rename write — same discipline as route-audit/state writes so a
222
+ // reader never sees a torn artifact. Advisory: a write failure returns
223
+ // {written:false}, never throws (metrics must stay read-mostly-safe).
224
+ export async function writeTierMap(projectRoot, artifact) {
225
+ try {
226
+ const filePath = path.join(projectRoot, TIER_MAP_PATH_REL);
227
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
228
+ const tempPath = `${filePath}.tmp-${process.pid}-${crypto.randomBytes(6).toString('hex')}`;
229
+ try {
230
+ await fs.writeFile(tempPath, JSON.stringify(artifact, null, 2), 'utf8');
231
+ await fs.rename(tempPath, filePath);
232
+ } catch (error) {
233
+ try { await fs.rm(tempPath, { force: true }); } catch { /* best effort */ }
234
+ throw error;
235
+ }
236
+ return { written: true, path: filePath };
237
+ } catch (error) {
238
+ return { written: false, error: error?.message ?? String(error) };
239
+ }
240
+ }
241
+
242
+ // SPEC §8: loadTierMap(projectRoot) → TierMap|null. Absent, corrupt, wrong
243
+ // version, invalid decision, or stale (derivedAt older than the 24h TTL) →
244
+ // null so the static map stays authoritative. byMode values are filtered to
245
+ // the tier vocabulary — anything else (e.g. a model ID) is dropped so it can
246
+ // never reach a route field or receipt.
247
+ export async function loadTierMap(projectRoot, { now = () => Date.now() } = {}) {
248
+ try {
249
+ const filePath = path.join(projectRoot, TIER_MAP_PATH_REL);
250
+ const doc = JSON.parse(await fs.readFile(filePath, 'utf8'));
251
+ if (!doc || typeof doc !== 'object' || doc.version !== 1) return null;
252
+ if (doc.decision !== 'promote' && doc.decision !== 'hold') return null;
253
+ const derivedAtMs = Date.parse(doc.derivedAt ?? '');
254
+ if (!Number.isFinite(derivedAtMs)) return null;
255
+ if (now() - derivedAtMs > TIER_MAP_MAX_AGE_MS) return null;
256
+ const byMode = {};
257
+ if (doc.byMode && typeof doc.byMode === 'object') {
258
+ for (const [mode, tier] of Object.entries(doc.byMode)) {
259
+ const name = tierName(tier);
260
+ if (name) byMode[mode] = name;
261
+ }
262
+ }
263
+ return {
264
+ version: 1,
265
+ derivedAt: doc.derivedAt,
266
+ joinedRoutes: Number.isFinite(doc.joinedRoutes) ? doc.joinedRoutes : 0,
267
+ agreement: Number.isFinite(doc.agreement) ? doc.agreement : 0,
268
+ decision: doc.decision,
269
+ byMode,
270
+ ...(Number.isFinite(doc.cheaperShare) ? { cheaperShare: doc.cheaperShare } : {}),
271
+ ...(Array.isArray(doc.reasons) ? { reasons: doc.reasons.filter((r) => typeof r === 'string') } : {}),
272
+ };
273
+ } catch {
274
+ return null;
275
+ }
276
+ }
277
+
278
+ // Route-side emission (SPEC §5 FR-001): folds the persisted measured map into
279
+ // a route summary. Sets the additive `measuredTier` field (nullable — null
280
+ // when data is thin or promotion is off) and, ONLY when the persisted map
281
+ // says 'promote' AND a pick exists for this mode, overrides the authoritative
282
+ // executionContract.modelTier with the measured tier. Never throws; absent or
283
+ // invalid input leaves the static map authoritative.
284
+ //
285
+ // Returns { map, staticTier, measuredPick, promoted } — `measuredPick` is the
286
+ // raw map pick (logged on the A/B receipt even on hold), `promoted` whether
287
+ // the measured pick took over modelTier.
288
+ export async function applyMeasuredTier({
289
+ projectRoot = process.cwd(),
290
+ routeSummary = null,
291
+ tierMap = undefined,
292
+ } = {}) {
293
+ const map = tierMap !== undefined ? tierMap : await loadTierMap(projectRoot);
294
+ if (!routeSummary || typeof routeSummary !== 'object') {
295
+ return { map, staticTier: null, measuredPick: null, promoted: false };
296
+ }
297
+ const executionMode = routeSummary.executionMode ?? null;
298
+ const staticTier = routeSummary.executionContract?.modelTier ?? null;
299
+ const pick = map?.byMode?.[executionMode] ?? null;
300
+ const measuredPick = tierName(pick);
301
+ const promoted = map?.decision === 'promote' && measuredPick !== null;
302
+
303
+ routeSummary.measuredTier = promoted ? measuredPick : null;
304
+ if (promoted && routeSummary.executionContract && typeof routeSummary.executionContract === 'object') {
305
+ routeSummary.executionContract.modelTier = measuredPick;
306
+ }
307
+ return { map, staticTier, measuredPick, promoted };
308
+ }