@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,359 @@
1
+ // sessionHistoryExtractor.js — bounded session-history signal extractor (BL-013).
2
+ //
3
+ // Reads the transcript tail (<=16KiB by default) carried on hook/bridge payloads
4
+ // (`transcriptPath`, ukit-bridge.js:217,228) and emits bounded counters/enums:
5
+ //
6
+ // { priorAttemptCount, sameSymptomReask, correctionEvents, fixLoopCount,
7
+ // degraded, reason? }
8
+ //
9
+ // Detection is deliberately heuristic + bounded (SPEC §14):
10
+ // * Route markers — any tail entry carrying `requestKey`/`routeFingerprint`
11
+ // (top-level or nested under route/ukit/metadata/message) is a prior route
12
+ // record; markers matching the current requestKey OR routeFingerprint count
13
+ // as prior attempts, and a failure-valued outcome counts a fix-loop round.
14
+ // * Re-ask — a user entry after the last matched marker (or >=2 matched
15
+ // markers) means the same route was asked again.
16
+ // * Corrections — explicit correction markers (`ukit-correction`/`correction`
17
+ // kinds) plus bounded correction phrasing in user text.
18
+ //
19
+ // Contracts (SPEC FR-001 + §10):
20
+ // * NEVER THROWS. Missing/unreadable transcript → zeroed struct with
21
+ // degraded:true + reason; malformed lines are skipped, never fatal.
22
+ // * BOUNDED: tail-only read (<=maxBytes, hard ceiling 4MiB like
23
+ // transcript-tail.mjs), counters clamp at 99, ≤50ms wall budget.
24
+ // * PRIVACY: output is counters/enums only — no transcript text may ever
25
+ // reach route state, decisions.tsv, or any receipt through this module.
26
+ //
27
+ // Two entry points share one classifier: extractHistorySignals (sync) for
28
+ // callers outside deadline-guarded hook blocks, extractHistorySignalsAsync for
29
+ // the hook path where every fs call must be async (BUG-C21-04 posture — a
30
+ // stalled mount must not park the event loop under an armed deadline).
31
+
32
+ import fs from 'node:fs';
33
+ import fsp from 'node:fs/promises';
34
+
35
+ // SPEC §14: the history extraction bound is a 16KiB tail. The ceiling mirrors
36
+ // transcript-tail.mjs's hard cap so one number describes a UKit tail scan.
37
+ export const DEFAULT_HISTORY_MAX_BYTES = 16 * 1024;
38
+ const HISTORY_MAX_BYTES_CEILING = 4 * 1024 * 1024;
39
+
40
+ const COUNTER_MAX = 99;
41
+
42
+ // Outcome fields probed on markers, in priority order. A marker without an
43
+ // outcome field counts as an attempt but never as a failed round.
44
+ const OUTCOME_FIELDS = ['outcome', 'verdict', 'status', 'result'];
45
+ const FAILURE_PATTERN = /fail|error|timeout|abort|crash|block/i;
46
+
47
+ // Object containers probed for marker keys — top-level plus the bounded nests
48
+ // writers may use. `message` is included so stamped user-prompt entries
49
+ // ({type:'user', message:{content, ukit:{requestKey}}}) still match.
50
+ const MARKER_PROBE_KEYS = ['ukit', 'route', 'routeRecord', 'routeMeta', 'metadata', 'meta', 'message'];
51
+
52
+ const USER_ENTRY_TYPES = new Set(['user', 'human']);
53
+ const CORRECTION_TYPES = new Set(['ukit-correction', 'correction', 'user-correction']);
54
+
55
+ // Bounded correction phrasing — explicit pushback patterns only; frustration
56
+ // noise stays out of the counter by design (GAP M12 failure-mode note).
57
+ const CORRECTION_PATTERN = /\b(?:no[,.!]?\s+that'?s|that'?s not|not what i|wrong|you (?:said|did|were|meant)|still (?:fail|failing|broken|wrong|crash|not working)|didn'?t work|incorrect|try again)\b/i;
58
+
59
+ // User message text is scanned only this far — long pasted diffs/logs in a
60
+ // user entry never extend the classification surface.
61
+ const USER_TEXT_SCAN_CHARS = 4096;
62
+
63
+ function isPlainObject(value) {
64
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
65
+ }
66
+
67
+ function nonEmptyString(value) {
68
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
69
+ }
70
+
71
+ function boundedCap(maxBytes) {
72
+ const requested = Number.isFinite(maxBytes) && maxBytes > 0
73
+ ? Math.floor(maxBytes)
74
+ : DEFAULT_HISTORY_MAX_BYTES;
75
+ return Math.min(requested, HISTORY_MAX_BYTES_CEILING);
76
+ }
77
+
78
+ function degradedResult(reason) {
79
+ return {
80
+ priorAttemptCount: 0,
81
+ sameSymptomReask: false,
82
+ correctionEvents: 0,
83
+ fixLoopCount: 0,
84
+ degraded: true,
85
+ reason,
86
+ };
87
+ }
88
+
89
+ function zeroedResult() {
90
+ return {
91
+ priorAttemptCount: 0,
92
+ sameSymptomReask: false,
93
+ correctionEvents: 0,
94
+ fixLoopCount: 0,
95
+ degraded: false,
96
+ };
97
+ }
98
+
99
+ function clampCounter(value) {
100
+ const count = Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
101
+ return Math.min(count, COUNTER_MAX);
102
+ }
103
+
104
+ // --- bounded tail read -------------------------------------------------------
105
+
106
+ // A tail read almost certainly starts mid-record: drop everything before the
107
+ // first complete line. Malformed or non-object lines are skipped — consumers
108
+ // count only well-formed bounded events.
109
+ function tailTextToEntries(text, truncated) {
110
+ let payload = text;
111
+ if (truncated) {
112
+ const firstBreak = payload.indexOf('\n');
113
+ payload = firstBreak >= 0 ? payload.slice(firstBreak + 1) : '';
114
+ }
115
+ const entries = [];
116
+ for (const line of payload.split('\n')) {
117
+ const trimmed = line.trim();
118
+ if (!trimmed) continue;
119
+ try {
120
+ const parsed = JSON.parse(trimmed);
121
+ if (isPlainObject(parsed)) entries.push(parsed);
122
+ } catch {
123
+ // partial/corrupt line — skipped, never fatal
124
+ }
125
+ }
126
+ return entries;
127
+ }
128
+
129
+ async function readTailEntriesAsync(filePath, cap) {
130
+ const stat = await fsp.stat(filePath);
131
+ if (!stat.isFile()) {
132
+ throw new Error('not-a-file');
133
+ }
134
+ if (stat.size <= 0) {
135
+ return { entries: [], bytesRead: 0 };
136
+ }
137
+ const start = Math.max(0, stat.size - cap);
138
+ const length = Math.min(cap, stat.size);
139
+ const buffer = Buffer.allocUnsafe(length);
140
+ const handle = await fsp.open(filePath, 'r');
141
+ let bytesRead = 0;
142
+ try {
143
+ const result = await handle.read(buffer, 0, length, start);
144
+ bytesRead = result.bytesRead;
145
+ } finally {
146
+ try { await handle.close(); } catch {}
147
+ }
148
+ return {
149
+ entries: tailTextToEntries(buffer.toString('utf8', 0, bytesRead), start > 0),
150
+ bytesRead,
151
+ };
152
+ }
153
+
154
+ function readTailEntriesSync(filePath, cap) {
155
+ const stat = fs.statSync(filePath);
156
+ if (!stat.isFile()) {
157
+ throw new Error('not-a-file');
158
+ }
159
+ if (stat.size <= 0) {
160
+ return { entries: [], bytesRead: 0 };
161
+ }
162
+ const start = Math.max(0, stat.size - cap);
163
+ const length = Math.min(cap, stat.size);
164
+ const buffer = Buffer.allocUnsafe(length);
165
+ const fd = fs.openSync(filePath, 'r');
166
+ let bytesRead = 0;
167
+ try {
168
+ bytesRead = fs.readSync(fd, buffer, 0, length, start);
169
+ } finally {
170
+ try { fs.closeSync(fd); } catch {}
171
+ }
172
+ return {
173
+ entries: tailTextToEntries(buffer.toString('utf8', 0, bytesRead), start > 0),
174
+ bytesRead,
175
+ };
176
+ }
177
+
178
+ // --- bounded classification --------------------------------------------------
179
+
180
+ // Probe order for marker keys + outcomes: the entry itself first, then the
181
+ // bounded nests. Returns the first non-empty string found.
182
+ function probeField(entry, names) {
183
+ if (!isPlainObject(entry)) return null;
184
+ for (const name of names) {
185
+ const direct = nonEmptyString(entry[name]);
186
+ if (direct) return direct;
187
+ }
188
+ for (const nestKey of MARKER_PROBE_KEYS) {
189
+ const nested = entry[nestKey];
190
+ if (!isPlainObject(nested)) continue;
191
+ for (const name of names) {
192
+ const value = nonEmptyString(nested[name]);
193
+ if (value) return value;
194
+ }
195
+ }
196
+ return null;
197
+ }
198
+
199
+ function markerKeysOf(entry) {
200
+ const requestKey = probeField(entry, ['requestKey']);
201
+ const routeFingerprint = probeField(entry, ['routeFingerprint', 'fingerprint']);
202
+ return requestKey || routeFingerprint ? { requestKey, routeFingerprint } : null;
203
+ }
204
+
205
+ function markerOutcome(entry) {
206
+ return probeField(entry, OUTCOME_FIELDS);
207
+ }
208
+
209
+ function isFailedOutcome(outcome) {
210
+ return typeof outcome === 'string' && FAILURE_PATTERN.test(outcome);
211
+ }
212
+
213
+ function isCorrectionMarker(entry) {
214
+ if (!isPlainObject(entry)) return false;
215
+ if (CORRECTION_TYPES.has(entry.type)) return true;
216
+ if (entry.event === 'correction' || entry.kind === 'correction') return true;
217
+ if (isPlainObject(entry.event) && entry.event.kind === 'correction') return true;
218
+ if (isPlainObject(entry.ukit) && entry.ukit.kind === 'correction') return true;
219
+ return false;
220
+ }
221
+
222
+ function isUserEntry(entry) {
223
+ if (!isPlainObject(entry)) return false;
224
+ if (USER_ENTRY_TYPES.has(entry.type)) return true;
225
+ if (isPlainObject(entry.message) && entry.message.role === 'user') return true;
226
+ return false;
227
+ }
228
+
229
+ // Bounded user text: string content, or the text blocks of a content array.
230
+ // Tool-result/thinking blocks are ignored — only user-authored text feeds the
231
+ // correction heuristic.
232
+ function userTextOf(entry) {
233
+ if (!isUserEntry(entry)) return null;
234
+ const content = entry.message?.content ?? entry.text ?? entry.content;
235
+ let text = '';
236
+ if (typeof content === 'string') {
237
+ text = content;
238
+ } else if (Array.isArray(content)) {
239
+ text = content
240
+ .map((block) => (isPlainObject(block) && block.type === 'text' ? String(block.text ?? '') : ''))
241
+ .filter(Boolean)
242
+ .join('\n');
243
+ }
244
+ return text ? text.slice(0, USER_TEXT_SCAN_CHARS) : null;
245
+ }
246
+
247
+ function computeHistorySignals(entries, { routeFingerprint = null, requestKey = null } = {}) {
248
+ const result = zeroedResult();
249
+ let attempts = 0;
250
+ let failedAttempts = 0;
251
+ let corrections = 0;
252
+ let lastMatchedIndex = -1;
253
+ let userAfterLastMatch = false;
254
+
255
+ entries.forEach((entry, index) => {
256
+ const marker = markerKeysOf(entry);
257
+ if (marker) {
258
+ const matches = Boolean(
259
+ (marker.requestKey && requestKey && marker.requestKey === requestKey)
260
+ || (marker.routeFingerprint && routeFingerprint && marker.routeFingerprint === routeFingerprint),
261
+ );
262
+ if (matches) {
263
+ attempts += 1;
264
+ lastMatchedIndex = index;
265
+ if (isFailedOutcome(markerOutcome(entry))) {
266
+ failedAttempts += 1;
267
+ }
268
+ }
269
+ }
270
+
271
+ const userText = userTextOf(entry);
272
+ if (isCorrectionMarker(entry) || (userText !== null && CORRECTION_PATTERN.test(userText))) {
273
+ corrections += 1;
274
+ }
275
+
276
+ if (lastMatchedIndex >= 0 && index > lastMatchedIndex && userText !== null) {
277
+ userAfterLastMatch = true;
278
+ }
279
+ });
280
+
281
+ result.priorAttemptCount = clampCounter(attempts);
282
+ result.fixLoopCount = clampCounter(failedAttempts);
283
+ result.correctionEvents = clampCounter(corrections);
284
+ // A re-ask on the same route shape: either the route resolved more than once
285
+ // in the tail, or a user prompt followed the last matched attempt (the
286
+ // current prompt is appended before the hook runs).
287
+ result.sameSymptomReask = attempts > 0 && (attempts >= 2 || userAfterLastMatch);
288
+ return result;
289
+ }
290
+
291
+ // --- public contract ---------------------------------------------------------
292
+
293
+ // Coerce arbitrary input into the historySignals route-field shape. Non-object
294
+ // input yields null so callers can emit the additive nullable field honestly.
295
+ export function normalizeHistorySignals(value) {
296
+ if (!isPlainObject(value)) return null;
297
+ const normalized = {
298
+ priorAttemptCount: clampCounter(value.priorAttemptCount),
299
+ sameSymptomReask: Boolean(value.sameSymptomReask),
300
+ correctionEvents: clampCounter(value.correctionEvents),
301
+ fixLoopCount: clampCounter(value.fixLoopCount),
302
+ degraded: Boolean(value.degraded),
303
+ };
304
+ const reason = nonEmptyString(value.reason);
305
+ if (reason) normalized.reason = reason.slice(0, 120);
306
+ return normalized;
307
+ }
308
+
309
+ // SPEC §8: extractHistorySignals({transcriptPath, routeFingerprint, requestKey,
310
+ // maxBytes?}) → bounded counters, never throws. Sync reader — the ≤50ms wall
311
+ // budget assumes a healthy filesystem; deadline-guarded hook code must use the
312
+ // async twin so a stalled mount cannot park the event loop.
313
+ export function extractHistorySignals({
314
+ transcriptPath,
315
+ routeFingerprint = null,
316
+ requestKey = null,
317
+ maxBytes,
318
+ } = {}) {
319
+ try {
320
+ const normalizedPath = nonEmptyString(transcriptPath);
321
+ if (!normalizedPath) {
322
+ return degradedResult('missing-transcript-path');
323
+ }
324
+ let tail;
325
+ try {
326
+ tail = readTailEntriesSync(normalizedPath, boundedCap(maxBytes));
327
+ } catch {
328
+ return degradedResult('transcript-unreadable');
329
+ }
330
+ return computeHistorySignals(tail.entries, { routeFingerprint, requestKey });
331
+ } catch {
332
+ return degradedResult('extract-failed');
333
+ }
334
+ }
335
+
336
+ // Async twin of extractHistorySignals — identical classification, fsp-based
337
+ // bounded read for deadline-guarded hook code (skill-router.sh).
338
+ export async function extractHistorySignalsAsync({
339
+ transcriptPath,
340
+ routeFingerprint = null,
341
+ requestKey = null,
342
+ maxBytes,
343
+ } = {}) {
344
+ try {
345
+ const normalizedPath = nonEmptyString(transcriptPath);
346
+ if (!normalizedPath) {
347
+ return degradedResult('missing-transcript-path');
348
+ }
349
+ let tail;
350
+ try {
351
+ tail = await readTailEntriesAsync(normalizedPath, boundedCap(maxBytes));
352
+ } catch {
353
+ return degradedResult('transcript-unreadable');
354
+ }
355
+ return computeHistorySignals(tail.entries, { routeFingerprint, requestKey });
356
+ } catch {
357
+ return degradedResult('extract-failed');
358
+ }
359
+ }