@ngockhoale/ukit 3.3.3 → 3.4.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.
- package/CHANGELOG.md +40 -0
- package/manifests/engineConformance.yaml +17 -1
- package/manifests/hostCapabilities.yaml +68 -1
- package/manifests/platform.full.yaml +138 -0
- package/manifests/platform.user.yaml +255 -3
- package/package.json +1 -1
- package/scripts/bench/subagent-orchestrator-corpus.mjs +275 -0
- package/scripts/bench/subagent-orchestrator-eval.mjs +565 -0
- package/scripts/probe/codex-capability-probe.mjs +169 -0
- package/src/cli/commands/doctor.js +168 -0
- package/src/cli/commands/indexTools.js +7 -0
- package/src/cli/commands/metrics.js +66 -2
- package/src/cli/commands/playbook.js +4 -4
- package/src/cli/commands/vm.js +49 -8
- package/src/core/agentRuntime/adapters.js +328 -27
- package/src/core/agentRuntime/artifacts.js +89 -0
- package/src/core/agentRuntime/context.js +345 -1
- package/src/core/agentRuntime/contract.js +296 -0
- package/src/core/agentRuntime/eventStore.js +176 -0
- package/src/core/agentRuntime/shadowRun.js +481 -5
- package/src/core/agentRuntime/telemetry.js +121 -0
- package/src/core/observability/emit/lifecycle.js +68 -1
- package/src/core/observability/emit/sessionBoot.js +393 -0
- package/src/core/observability/privacy/allowlist.js +10 -1
- package/src/core/observability/schema/registry.js +10 -0
- package/src/core/runtimeConfig.js +133 -0
- package/src/core/userPlaybooks.js +18 -3
- package/src/decision/registry.js +19 -0
- package/src/diagnostics/feedbackEvents.js +7 -4
- package/src/diagnostics/routeOutcomes.js +51 -6
- package/src/diagnostics/skillAccuracy.js +43 -3
- package/src/index/crossCheckMatrix.js +412 -0
- package/src/index/fixLoopEscalation.js +453 -0
- package/src/index/playbookRegistry.js +691 -0
- package/src/index/reviewPolicy.js +368 -0
- package/src/index/routeResolver.js +915 -0
- package/src/index/sessionHistoryExtractor.js +359 -0
- package/src/index/taskRouting.js +764 -581
- package/src/index/tierSelection.js +308 -0
- package/src/index/verificationMap.js +404 -0
- package/template_project/.claude/hooks/observability-emit.mjs +14 -0
- package/template_project/.claude/hooks/record-execution.mjs +19 -1
- package/template_project/.claude/hooks/skill-router.sh +691 -25
- package/template_project/.claude/hooks/verification-guard.sh +230 -1
- package/template_project/.claude/settings.json +2 -2
- package/template_project/.claude/ukit/index/cross-check-matrix.mjs +415 -0
- package/template_project/.claude/ukit/index/fix-loop-escalation.mjs +456 -0
- package/template_project/.claude/ukit/index/playbook-registry.mjs +690 -0
- package/template_project/.claude/ukit/index/review-panel-aggregate.mjs +20 -2
- package/template_project/.claude/ukit/index/review-policy.mjs +376 -0
- package/template_project/.claude/ukit/index/route-resolver.mjs +1059 -0
- package/template_project/.claude/ukit/index/route-task.mjs +1253 -846
- package/template_project/.claude/ukit/index/session-history-extractor.mjs +362 -0
- package/template_project/.claude/ukit/index/tier-selection.mjs +309 -0
- package/template_project/.claude/ukit/index/verification-map.mjs +403 -0
- package/template_project/.claude/ukit/index/worktree-sweep.mjs +195 -0
- package/template_project/.claude/ukit/runtime/execution-ledger.mjs +789 -11
- package/template_project/.claude/ukit/runtime/observability-emit.mjs +1102 -0
- package/template_project/.claude/ukit/runtime/reinject-context.mjs +9 -1
- package/template_project/.claude/ukit/runtime/resumable-run.mjs +149 -5
- package/template_project/.claude/ukit/runtime/stop-coordinator.mjs +323 -6
- package/template_project/.codex/README.md +8 -0
- package/template_project/.omp/hooks/pre/ukit-bridge.js +8 -1
- package/template_project/ukit/README.md +1 -1
- package/template_project/ukit/storage/config.json +20 -0
- package/template_user/playbooks/architecture-decision.md +28 -0
- package/template_user/playbooks/autonomous-run.md +43 -0
- package/template_user/playbooks/autopilot-full.md +59 -0
- package/template_user/playbooks/autopilot-stack.md +54 -0
- package/template_user/playbooks/babysit.md +39 -0
- package/template_user/playbooks/bug-fix.md +3 -1
- package/template_user/playbooks/{issue-implementation.md → feature-implementation.md} +4 -2
- package/template_user/playbooks/hillclimb.md +44 -0
- package/template_user/playbooks/investigation.md +21 -0
- package/template_user/playbooks/migration.md +21 -0
- package/template_user/playbooks/open-pr.md +48 -0
- package/template_user/playbooks/orchestrate.md +45 -0
- package/template_user/playbooks/performance.md +33 -0
- package/template_user/playbooks/prototype.md +28 -0
- package/template_user/playbooks/refactor.md +19 -0
- package/template_user/playbooks/release.md +28 -0
- package/template_user/playbooks/runtime-forensics.md +23 -0
- package/template_user/playbooks/session-pickup.md +31 -0
- package/template_user/playbooks/shipping.md +53 -0
- package/template_user/playbooks/skill-evaluation.md +48 -0
- package/template_user/playbooks/small-feature.md +20 -0
- package/template_user/playbooks/verification-map.json +153 -0
- package/template_user/playbooks/verification.md +22 -0
- package/template_user/playbooks/worktree-cleanup.md +37 -0
|
@@ -60,8 +60,8 @@
|
|
|
60
60
|
* No LLM, no daemon, no sync fs on the hot path. The module never throws.
|
|
61
61
|
*/
|
|
62
62
|
|
|
63
|
+
import crypto from 'node:crypto';
|
|
63
64
|
import path from 'node:path';
|
|
64
|
-
|
|
65
65
|
import { createRecorder } from './recorder.js';
|
|
66
66
|
import { registerCrashCapture } from './crash.js';
|
|
67
67
|
import { resolveStage } from './config.js';
|
|
@@ -101,6 +101,73 @@ function isNonEmptyString(value) {
|
|
|
101
101
|
return typeof value === 'string' && value.length > 0;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
// --- FR-003 (BL-005): shared semantic names + record builder --------------
|
|
105
|
+
//
|
|
106
|
+
// The parity-locked pair src/core/observability/emit/sessionBoot.js ↔
|
|
107
|
+
// template_project/.claude/ukit/runtime/observability-emit.mjs emits the
|
|
108
|
+
// same record shape the recorder's startSpan() produces (recorder.js) —
|
|
109
|
+
// these names are the single canonical vocabulary for both emit paths.
|
|
110
|
+
|
|
111
|
+
export const SEMANTIC_EXECUTION_STARTED = 'execution.started';
|
|
112
|
+
export const SEMANTIC_EXECUTION_COMPLETED = 'execution.completed';
|
|
113
|
+
export const SEMANTIC_EXECUTION_FAILED = 'execution.failed';
|
|
114
|
+
export const SEMANTIC_EXECUTION_BLOCKED = 'execution.blocked';
|
|
115
|
+
export const SEMANTIC_OUTCOME_OBSERVED = 'outcome.observed';
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Deterministic session-boot join key. `payload.request_key` on the boot
|
|
119
|
+
* record is derived (not random) so terminal emitters can recompute the
|
|
120
|
+
* same key for the session: sha256('<projectRoot>\n<sessionId>'), 24 hex.
|
|
121
|
+
*/
|
|
122
|
+
export function sessionBootRequestKey({ projectRoot, sessionId } = {}) {
|
|
123
|
+
const digest = crypto
|
|
124
|
+
.createHash('sha256')
|
|
125
|
+
.update(`${String(projectRoot ?? '')}\n${String(sessionId ?? '')}`)
|
|
126
|
+
.digest('hex')
|
|
127
|
+
.slice(0, 24);
|
|
128
|
+
return `session-boot-${digest}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The ONE session-boot `execution.started` record shape. Same field
|
|
133
|
+
* ordering contract as the recorder's own span-root emit: fresh
|
|
134
|
+
* trace/span/execution ids, caller's session/engine stamped in both the
|
|
135
|
+
* envelope and the payload, wall-clock `ts` echoed inside payload for the
|
|
136
|
+
* join tooling that never parses envelope timestamps.
|
|
137
|
+
* Returns null for a missing projectRoot (callers must never fabricate
|
|
138
|
+
* a storage root) — the emit layer maps that to a typed drop.
|
|
139
|
+
*/
|
|
140
|
+
export function buildSessionBootRecord({
|
|
141
|
+
projectRoot,
|
|
142
|
+
engine,
|
|
143
|
+
sessionId,
|
|
144
|
+
nowMs = Date.now(),
|
|
145
|
+
} = {}) {
|
|
146
|
+
if (!isNonEmptyString(projectRoot)) return null;
|
|
147
|
+
const trace_id = `trace-${crypto.randomUUID()}`;
|
|
148
|
+
return {
|
|
149
|
+
semantic_name: SEMANTIC_EXECUTION_STARTED,
|
|
150
|
+
trace_id,
|
|
151
|
+
span_id: `span-${crypto.randomUUID()}`,
|
|
152
|
+
parent_span_id: null,
|
|
153
|
+
execution_id: `exec-${crypto.randomUUID()}`,
|
|
154
|
+
session_id: isNonEmptyString(sessionId) ? sessionId : undefined,
|
|
155
|
+
project_ref: `proj-${crypto
|
|
156
|
+
.createHash('sha256')
|
|
157
|
+
.update(String(path.resolve(projectRoot)))
|
|
158
|
+
.digest('hex')
|
|
159
|
+
.slice(0, 12)}`,
|
|
160
|
+
payload: {
|
|
161
|
+
operation: 'session-boot',
|
|
162
|
+
duration_ms: null,
|
|
163
|
+
engine: isNonEmptyString(engine) ? engine : 'unknown',
|
|
164
|
+
sessionId: isNonEmptyString(sessionId) ? sessionId : 'unknown',
|
|
165
|
+
ts: new Date(nowMs).toISOString(),
|
|
166
|
+
request_key: sessionBootRequestKey({ projectRoot, sessionId }),
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
104
171
|
// --- disabled recorder (SPEC §8: missing root → emit→dropped/recorder-disabled)
|
|
105
172
|
|
|
106
173
|
function makeDisabledRecorder() {
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sessionBoot.js (TASK-003, SPEC §5 FR-003, §6, §14) — the live session-boot
|
|
3
|
+
* emit surface: the production caller the recorder layer never had.
|
|
4
|
+
*
|
|
5
|
+
* emitSessionBoot({ projectRoot, engine, sessionId, config?, deps? })
|
|
6
|
+
* → { status: 'written', written, elapsed_ms }
|
|
7
|
+
* | { status: 'dropped'|'partial'|'error', reason }
|
|
8
|
+
* emitOutcomeObserved({ projectRoot, record, config?, deps? }) → same result
|
|
9
|
+
* resolveEmitStage(config) → 'off' | 'shadow' | 'canary' | 'default'
|
|
10
|
+
* loadEmitConfig(projectRoot) → merged emit config (async, never throws)
|
|
11
|
+
*
|
|
12
|
+
* A hook child process lives for milliseconds — `startLifecycle`'s interval
|
|
13
|
+
* timers and beforeExit flush cannot survive it (SPEC §14 chose the bounded
|
|
14
|
+
* one-shot emit). One call = one recorder = one record = one flush, each
|
|
15
|
+
* step racing a deadline, so the emit can never delay session start: it is
|
|
16
|
+
* fire-and-forget, bounded, and never throws.
|
|
17
|
+
*
|
|
18
|
+
* Stage contract: this surface emits by default — the shipped
|
|
19
|
+
* `.ukit/storage/config.json` sets observability.stage='default' and the
|
|
20
|
+
* runtime default config agrees — so 'off' is an explicit kill switch the
|
|
21
|
+
* owner sets; a missing config file or missing stage emits (the same
|
|
22
|
+
* "default" the rest of the runtime resolves to). A caller-supplied
|
|
23
|
+
* `config` object is used as the complete merged config, so callers that
|
|
24
|
+
* want the live stage pass the runtime config themselves; absent config
|
|
25
|
+
* resolves the 'default' effective stage.
|
|
26
|
+
*
|
|
27
|
+
* The parity-locked mirror
|
|
28
|
+
* template_project/.claude/ukit/runtime/observability-emit.mjs cannot
|
|
29
|
+
* import package source — it replicates this pipeline (envelope fill →
|
|
30
|
+
* validate → sanitize → bounded append) with identical semantics. Keep the
|
|
31
|
+
* record shape changes coordinated with `tests/consistency/
|
|
32
|
+
* sessionBootEmitParity.test.js`.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import crypto from 'node:crypto';
|
|
36
|
+
import fs from 'node:fs';
|
|
37
|
+
import path from 'node:path';
|
|
38
|
+
|
|
39
|
+
import { createRecorder } from './recorder.js';
|
|
40
|
+
import {
|
|
41
|
+
buildSessionBootRecord,
|
|
42
|
+
segmentsRoot,
|
|
43
|
+
sessionBootRequestKey,
|
|
44
|
+
SEMANTIC_OUTCOME_OBSERVED,
|
|
45
|
+
} from './lifecycle.js';
|
|
46
|
+
import { resolveStage } from './config.js';
|
|
47
|
+
|
|
48
|
+
/** Hard upper bound for the whole one-shot emit (ms). */
|
|
49
|
+
export const SESSION_BOOT_EMIT_DEADLINE_MS = 250;
|
|
50
|
+
|
|
51
|
+
const VALID_STAGES = new Set(['off', 'shadow', 'canary', 'default']);
|
|
52
|
+
|
|
53
|
+
function isPlainObject(value) {
|
|
54
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isNonEmptyString(value) {
|
|
58
|
+
return typeof value === 'string' && value.length > 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The emit surface's effective stage: the explicit literal 'off' is the
|
|
63
|
+
* kill switch; any other explicit stage value resolves through
|
|
64
|
+
* resolveStage verbatim; absent/malformed resolves 'default' (the shipped
|
|
65
|
+
* + code default), never the recorder's fail-safe 'off'.
|
|
66
|
+
*/
|
|
67
|
+
export function resolveEmitStage(config = null) {
|
|
68
|
+
const node = isPlainObject(config) ? config.observability : undefined;
|
|
69
|
+
if (isPlainObject(node) && node.stage === 'off') return 'off';
|
|
70
|
+
const stage = resolveStage(config);
|
|
71
|
+
return stage === 'off' ? 'default' : stage;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* `.ukit/storage/config.json` on disk → emit config. A missing/corrupt
|
|
76
|
+
* file means a pre-install or bare project: effective stage 'default'
|
|
77
|
+
* (the shipped value), mirroring the runtime's own code default.
|
|
78
|
+
*/
|
|
79
|
+
export async function loadEmitConfig(projectRoot) {
|
|
80
|
+
if (!isNonEmptyString(projectRoot)) return null;
|
|
81
|
+
try {
|
|
82
|
+
const raw = await fs.promises.readFile(
|
|
83
|
+
path.join(projectRoot, '.ukit', 'storage', 'config.json'),
|
|
84
|
+
'utf8',
|
|
85
|
+
);
|
|
86
|
+
const parsed = JSON.parse(raw);
|
|
87
|
+
return isPlainObject(parsed) ? parsed : null;
|
|
88
|
+
} catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Race a promise against a deadline; the loser keeps running detached
|
|
95
|
+
* (bounded telemetry accepts a late write over blocking the caller).
|
|
96
|
+
* Returns { value } | { timedOut: true }. The timer is unref'd so it can
|
|
97
|
+
* never pin a dying hook process.
|
|
98
|
+
*/
|
|
99
|
+
async function bounded(promise, deadlineMs) {
|
|
100
|
+
if (!Number.isFinite(deadlineMs) || deadlineMs <= 0) {
|
|
101
|
+
return { timedOut: true };
|
|
102
|
+
}
|
|
103
|
+
let timer;
|
|
104
|
+
try {
|
|
105
|
+
const result = await Promise.race([
|
|
106
|
+
Promise.resolve(promise).then((value) => ({ value })),
|
|
107
|
+
new Promise((resolve) => {
|
|
108
|
+
timer = setTimeout(() => resolve({ timedOut: true }), deadlineMs);
|
|
109
|
+
if (timer.unref) timer.unref();
|
|
110
|
+
}),
|
|
111
|
+
]);
|
|
112
|
+
return result;
|
|
113
|
+
} finally {
|
|
114
|
+
clearTimeout(timer);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function toEmitResult(result, elapsed_ms) {
|
|
119
|
+
const out = { ...result };
|
|
120
|
+
out.elapsed_ms = Math.max(0, elapsed_ms);
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The shared one-shot emit pipeline. `record` is a semantic-record
|
|
126
|
+
* candidate; the recorder fills envelope fields, validates, sanitizes,
|
|
127
|
+
* applies the sampling decider, and flush() drains through the segment
|
|
128
|
+
* writer — the twin never reimplements any of those stages.
|
|
129
|
+
*/
|
|
130
|
+
async function emitOne({ projectRoot, config, record, deps, deadlineMs }) {
|
|
131
|
+
const started = Date.now();
|
|
132
|
+
const fail = (reason) => toEmitResult({ status: 'dropped', reason }, Date.now() - started);
|
|
133
|
+
|
|
134
|
+
if (!isNonEmptyString(projectRoot)) return fail('missing-project-root');
|
|
135
|
+
if (!isPlainObject(record)) return fail('invalid-record');
|
|
136
|
+
if (resolveEmitStage(config) === 'off') return fail('stage-off');
|
|
137
|
+
|
|
138
|
+
const injected = isPlainObject(deps) ? deps : {};
|
|
139
|
+
const clock = {
|
|
140
|
+
nowMs: typeof injected.nowMs === 'function' ? injected.nowMs : () => injected.nowMs ?? Date.now(),
|
|
141
|
+
nowNs:
|
|
142
|
+
typeof injected.nowNs === 'function'
|
|
143
|
+
? injected.nowNs
|
|
144
|
+
: () => injected.nowNs ?? process.hrtime.bigint(),
|
|
145
|
+
iso:
|
|
146
|
+
typeof injected.iso === 'function'
|
|
147
|
+
? injected.iso
|
|
148
|
+
: () => injected.iso ?? new Date().toISOString(),
|
|
149
|
+
};
|
|
150
|
+
const writer =
|
|
151
|
+
typeof injected.writer === 'function' ? injected.writer : undefined;
|
|
152
|
+
const samplingSeed = Number.isFinite(injected.samplingSeed) ? injected.samplingSeed : undefined;
|
|
153
|
+
|
|
154
|
+
// resolveEmitStage owns the 'default'-absent asymmetry: the effective
|
|
155
|
+
// stage written back here is 'off' only when the owner set the literal
|
|
156
|
+
// 'off' (that case already returned above); an absent or malformed stage
|
|
157
|
+
// resolves 'default' — the shipped + code default — while a supplied
|
|
158
|
+
// 'shadow'/'canary' passes through verbatim. Other observability keys
|
|
159
|
+
// (sampling, seams) ride along untouched for the recorder's own gates.
|
|
160
|
+
const effectiveConfig = isPlainObject(config)
|
|
161
|
+
? {
|
|
162
|
+
...config,
|
|
163
|
+
observability: {
|
|
164
|
+
...(isPlainObject(config.observability) ? config.observability : {}),
|
|
165
|
+
stage: resolveEmitStage(config),
|
|
166
|
+
},
|
|
167
|
+
}
|
|
168
|
+
: { observability: { stage: 'default' } };
|
|
169
|
+
|
|
170
|
+
const budget = Number.isFinite(deadlineMs) && deadlineMs > 0
|
|
171
|
+
? Math.min(deadlineMs, SESSION_BOOT_EMIT_DEADLINE_MS)
|
|
172
|
+
: SESSION_BOOT_EMIT_DEADLINE_MS;
|
|
173
|
+
|
|
174
|
+
// appendRecord's resolveRoot requires the segments root's PARENT to
|
|
175
|
+
// already exist (realpath containment), so a fresh project needs the
|
|
176
|
+
// .ukit/storage/observability tree created here — bounded by the same
|
|
177
|
+
// deadline so a stalled mount can never pin the hook.
|
|
178
|
+
const targetRoot = segmentsRoot(path.resolve(projectRoot));
|
|
179
|
+
const seeded = await bounded(
|
|
180
|
+
fs.promises.mkdir(targetRoot, { recursive: true }).catch((err) => err),
|
|
181
|
+
budget,
|
|
182
|
+
);
|
|
183
|
+
if (seeded.timedOut) {
|
|
184
|
+
return toEmitResult({ status: 'partial', reason: 'deadline' }, Date.now() - started);
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
if (seeded.value instanceof Error) throw seeded.value;
|
|
188
|
+
} catch {
|
|
189
|
+
// mkdir raced with another process or failed — fall through and let
|
|
190
|
+
// the write path report the typed io reason.
|
|
191
|
+
}
|
|
192
|
+
try {
|
|
193
|
+
const recorder = createRecorder({
|
|
194
|
+
root: segmentsRoot(path.resolve(projectRoot)),
|
|
195
|
+
config: effectiveConfig,
|
|
196
|
+
clock,
|
|
197
|
+
...(writer ? { writer } : {}),
|
|
198
|
+
...(samplingSeed !== undefined ? { samplingSeed } : {}),
|
|
199
|
+
});
|
|
200
|
+
const emitted = recorder.emit(record);
|
|
201
|
+
if (emitted.status !== 'accepted') {
|
|
202
|
+
return fail(emitted.reason || 'dropped');
|
|
203
|
+
}
|
|
204
|
+
const flushed = await bounded(recorder.flush({ deadlineMs: budget }), budget);
|
|
205
|
+
if (flushed.timedOut) {
|
|
206
|
+
return toEmitResult({ status: 'partial', reason: 'deadline' }, Date.now() - started);
|
|
207
|
+
}
|
|
208
|
+
const result = flushed.value;
|
|
209
|
+
if (result.status === 'ok' && result.written > 0) {
|
|
210
|
+
return toEmitResult(
|
|
211
|
+
{ status: 'written', written: result.written },
|
|
212
|
+
Date.now() - started,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
return toEmitResult(
|
|
216
|
+
{ status: result.status === 'partial' ? 'partial' : 'dropped', reason: result.reason || 'write-failed' },
|
|
217
|
+
Date.now() - started,
|
|
218
|
+
);
|
|
219
|
+
} catch {
|
|
220
|
+
return fail('emit-error');
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* `emitSessionBoot({ projectRoot, engine, sessionId, config?, deps? })` —
|
|
226
|
+
* append ONE `execution.started` record to the project's active segment.
|
|
227
|
+
* `deps` is the test-injection seam: { nowMs?, nowNs?, iso?, writer?,
|
|
228
|
+
* samplingSeed? } (values or zero-arg functions; the recorder's clock
|
|
229
|
+
* contract). Never throws, never exceeds the deadline.
|
|
230
|
+
*/
|
|
231
|
+
export async function emitSessionBoot({
|
|
232
|
+
projectRoot,
|
|
233
|
+
engine,
|
|
234
|
+
sessionId,
|
|
235
|
+
config,
|
|
236
|
+
deps,
|
|
237
|
+
deadlineMs,
|
|
238
|
+
} = {}) {
|
|
239
|
+
const injected = isPlainObject(deps) ? deps : {};
|
|
240
|
+
const nowMs = typeof injected.nowMs === 'function' ? injected.nowMs() : injected.nowMs;
|
|
241
|
+
const record = buildSessionBootRecord({
|
|
242
|
+
projectRoot,
|
|
243
|
+
engine: isNonEmptyString(engine) ? engine : 'unknown',
|
|
244
|
+
sessionId,
|
|
245
|
+
nowMs: Number.isFinite(nowMs) ? nowMs : Date.now(),
|
|
246
|
+
});
|
|
247
|
+
return emitOne({ projectRoot, config, record, deps: injected, deadlineMs });
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* `emitOutcomeObserved({ projectRoot, record, config?, deps? })` — the
|
|
252
|
+
* generic terminal-record seam TASK-005's emitters consume. `record` is a
|
|
253
|
+
* full semantic-record candidate; a missing `semantic_name` defaults to
|
|
254
|
+
* 'outcome.observed' (the seam's own event).
|
|
255
|
+
*/
|
|
256
|
+
export async function emitOutcomeObserved({
|
|
257
|
+
projectRoot,
|
|
258
|
+
record,
|
|
259
|
+
config,
|
|
260
|
+
deps,
|
|
261
|
+
deadlineMs,
|
|
262
|
+
} = {}) {
|
|
263
|
+
const candidate = isPlainObject(record) ? { ...record } : record;
|
|
264
|
+
if (isPlainObject(candidate) && candidate.semantic_name === undefined) {
|
|
265
|
+
candidate.semantic_name = SEMANTIC_OUTCOME_OBSERVED;
|
|
266
|
+
}
|
|
267
|
+
return emitOne({
|
|
268
|
+
projectRoot,
|
|
269
|
+
config,
|
|
270
|
+
record: candidate,
|
|
271
|
+
deps: isPlainObject(deps) ? deps : {},
|
|
272
|
+
deadlineMs,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
// --- TASK-005 (FR-005, BL-007): terminal OutcomeRecord construction ---------
|
|
278
|
+
// The three terminal emitters (record-execution.mjs receipts,
|
|
279
|
+
// stop-coordinator.mjs verdicts, execution-ledger.mjs finalizers) share ONE
|
|
280
|
+
// record builder so every `outcome.observed` record carries the ARCH
|
|
281
|
+
// §Data Contracts OutcomeRecord fields plus the TASK-003 session-boot
|
|
282
|
+
// `request_key` join id — the missing 1-of-3934 emitter surface (TM §2).
|
|
283
|
+
|
|
284
|
+
export const OUTCOME_VERDICTS = Object.freeze([
|
|
285
|
+
'VERIFIED', 'FAILED', 'BLOCKED', 'INCONCLUSIVE', 'INTERRUPTED',
|
|
286
|
+
]);
|
|
287
|
+
|
|
288
|
+
const OUTCOME_ENFORCEMENTS = new Set(['enforced', 'advisory']);
|
|
289
|
+
|
|
290
|
+
// FR-005/FR-006: `advisory` is the Codex marker — the engine stamp may arrive
|
|
291
|
+
// as an explicit `engine` argument or as `env.UKIT_HARNESS` (the stamp the
|
|
292
|
+
// .sh wrappers and the omp bridge export). Anything else is `enforced`.
|
|
293
|
+
export function deriveEnforcement({ engine = null, env = null } = {}) {
|
|
294
|
+
const fromArg = isNonEmptyString(engine) ? engine : null;
|
|
295
|
+
const fromEnv = isPlainObject(env) && isNonEmptyString(env.UKIT_HARNESS) ? env.UKIT_HARNESS : null;
|
|
296
|
+
return (fromArg === 'codex' || fromEnv === 'codex') ? 'advisory' : 'enforced';
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// The run id is the ledger's session identity: `run-<safe session id>` when a
|
|
300
|
+
// session is known, `run-transcript-<sha256[:12]>` for transcript-only
|
|
301
|
+
// payloads (never the raw path — it would be path-redacted anyway), else null.
|
|
302
|
+
export function outcomeRunId(payload = {}) {
|
|
303
|
+
const sessionId = isNonEmptyString(payload.session_id)
|
|
304
|
+
? payload.session_id
|
|
305
|
+
: (isNonEmptyString(payload.sessionId) ? payload.sessionId : null);
|
|
306
|
+
if (sessionId) {
|
|
307
|
+
const safe = sessionId.replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 96);
|
|
308
|
+
return `run-${safe}`;
|
|
309
|
+
}
|
|
310
|
+
const transcript = isNonEmptyString(payload.transcript_path)
|
|
311
|
+
? payload.transcript_path
|
|
312
|
+
: (isNonEmptyString(payload.transcriptPath) ? payload.transcriptPath : null);
|
|
313
|
+
if (transcript) {
|
|
314
|
+
const digest = crypto.createHash('sha256').update(String(transcript)).digest('hex').slice(0, 12);
|
|
315
|
+
return `run-transcript-${digest}`;
|
|
316
|
+
}
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function outcomeStringList(value, cap) {
|
|
321
|
+
if (!Array.isArray(value)) return [];
|
|
322
|
+
return value
|
|
323
|
+
.filter((item) => typeof item === 'string' && item.length > 0)
|
|
324
|
+
.map((item) => item.slice(0, 160))
|
|
325
|
+
.slice(0, cap);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function outcomeCost(value) {
|
|
329
|
+
if (!isPlainObject(value)) return { latencyMs: null, rounds: null };
|
|
330
|
+
const cost = value;
|
|
331
|
+
return {
|
|
332
|
+
latencyMs: Number.isFinite(cost.latencyMs) && cost.latencyMs >= 0 ? cost.latencyMs : null,
|
|
333
|
+
rounds: Number.isFinite(cost.rounds) && cost.rounds >= 0 ? cost.rounds : null,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* buildOutcomeObservedRecord({ outcome, projectRoot, payload, sessionId,
|
|
339
|
+
* engine, env }) → semantic-record candidate for `outcome.observed`.
|
|
340
|
+
*
|
|
341
|
+
* `outcome` is the ARCH §Data Contracts OutcomeRecord subset the emitter knows:
|
|
342
|
+
* { outcomeId?, runId?, routeId?, routeFingerprint?, ledgerKey?, verdict,
|
|
343
|
+
* evidenceIds?, cost?, reviewActions?, enforcement?, engine?, source?, ts? }
|
|
344
|
+
* Every field defaults sanely — a terminal event without runId/route data
|
|
345
|
+
* still emits with nulls (task case 4) and never blocks the caller's path.
|
|
346
|
+
* `payload` is the hook payload: it supplies session identity for runId
|
|
347
|
+
* derivation, the session-boot request_key join, and the envelope session_id.
|
|
348
|
+
* `enforcement` derives per FR-006 unless the emitter overrode it.
|
|
349
|
+
*/
|
|
350
|
+
export function buildOutcomeObservedRecord({
|
|
351
|
+
outcome = null,
|
|
352
|
+
projectRoot = null,
|
|
353
|
+
payload = {},
|
|
354
|
+
sessionId = null,
|
|
355
|
+
engine = null,
|
|
356
|
+
env = null,
|
|
357
|
+
} = {}) {
|
|
358
|
+
const safe = isPlainObject(outcome) ? outcome : {};
|
|
359
|
+
const resolvedSessionId = isNonEmptyString(sessionId)
|
|
360
|
+
? sessionId
|
|
361
|
+
: (isNonEmptyString(payload?.session_id)
|
|
362
|
+
? payload.session_id
|
|
363
|
+
: (isNonEmptyString(payload?.sessionId) ? payload.sessionId : null));
|
|
364
|
+
const engineName = isNonEmptyString(engine)
|
|
365
|
+
? engine
|
|
366
|
+
: (isNonEmptyString(safe.engine) ? safe.engine : null);
|
|
367
|
+
const verdict = OUTCOME_VERDICTS.includes(safe.verdict) ? safe.verdict : 'INCONCLUSIVE';
|
|
368
|
+
const recordPayload = {
|
|
369
|
+
outcomeId: isNonEmptyString(safe.outcomeId) ? safe.outcomeId : `oc-${crypto.randomUUID()}`,
|
|
370
|
+
runId: safe.runId !== undefined ? safe.runId : outcomeRunId(payload),
|
|
371
|
+
routeId: safe.routeId !== undefined ? safe.routeId : null,
|
|
372
|
+
routeFingerprint: safe.routeFingerprint !== undefined ? safe.routeFingerprint : null,
|
|
373
|
+
ledgerKey: safe.ledgerKey !== undefined ? safe.ledgerKey : null,
|
|
374
|
+
verdict,
|
|
375
|
+
evidenceIds: outcomeStringList(safe.evidenceIds, 32),
|
|
376
|
+
cost: outcomeCost(safe.cost),
|
|
377
|
+
reviewActions: outcomeStringList(safe.reviewActions, 32),
|
|
378
|
+
enforcement: OUTCOME_ENFORCEMENTS.has(safe.enforcement)
|
|
379
|
+
? safe.enforcement
|
|
380
|
+
: deriveEnforcement({ engine: engineName, env }),
|
|
381
|
+
engine: engineName,
|
|
382
|
+
source: isNonEmptyString(safe.source) ? safe.source.slice(0, 96) : null,
|
|
383
|
+
request_key: sessionBootRequestKey({ projectRoot, sessionId: resolvedSessionId }),
|
|
384
|
+
ts: isNonEmptyString(safe.ts) ? safe.ts : new Date().toISOString(),
|
|
385
|
+
};
|
|
386
|
+
return {
|
|
387
|
+
semantic_name: SEMANTIC_OUTCOME_OBSERVED,
|
|
388
|
+
session_id: resolvedSessionId ?? undefined,
|
|
389
|
+
payload: recordPayload,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export { sessionBootRequestKey };
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* redaction rules change so downstream readers can detect stale records.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
export const REDACTION_VERSION = 'df-redact-
|
|
14
|
+
export const REDACTION_VERSION = 'df-redact-6';
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Envelope fields permitted on a persisted record (DF-FR01) plus the
|
|
@@ -179,6 +179,15 @@ export const ALLOWED_SUPPORT_PAYLOAD_FIELDS = new Set([
|
|
|
179
179
|
'digest',
|
|
180
180
|
'policy_version',
|
|
181
181
|
'rate',
|
|
182
|
+
// TASK-C89-002 (SPEC §3 FR-01): subagent.usage provenance payload — the
|
|
183
|
+
// fingerprint-deduped component rows, measured byte counts, typed token
|
|
184
|
+
// estimates, the observed parent run ref and the telemetry-completeness
|
|
185
|
+
// marker. Metadata only; component text itself is never emitted.
|
|
186
|
+
'components',
|
|
187
|
+
'bytes',
|
|
188
|
+
'tokens',
|
|
189
|
+
'parent_run_ref',
|
|
190
|
+
'telemetry_complete',
|
|
182
191
|
]);
|
|
183
192
|
|
|
184
193
|
// --- size caps (prototype defaults per SPEC §14; freeze after measurement) ---
|
|
@@ -15,6 +15,7 @@ import { PRIVACY_CLASSES } from './constants.js';
|
|
|
15
15
|
|
|
16
16
|
const V1 = '1';
|
|
17
17
|
const V1_1 = '1.1';
|
|
18
|
+
const V1_2 = '1.2';
|
|
18
19
|
|
|
19
20
|
export const SEMANTIC_REGISTRY = Object.freeze({
|
|
20
21
|
'execution.started': {
|
|
@@ -210,6 +211,15 @@ export const SEMANTIC_REGISTRY = Object.freeze({
|
|
|
210
211
|
introduced_version: V1_1,
|
|
211
212
|
deprecated: null,
|
|
212
213
|
},
|
|
214
|
+
// --- schema v1.2 additions (TASK-C89-002, SPEC §3 FR-01, append-only) ---
|
|
215
|
+
'subagent.usage': {
|
|
216
|
+
definition: 'One child/subagent run emitted typed usage provenance. Payload carries fingerprint-deduped prompt components with measured bytes and ESTIMATED tokens, provider usage as a typed {value,source} resource, and the observed parent run ref — unknown stays UNKNOWN/null, never 0, never prompt text.',
|
|
217
|
+
unit: 'event',
|
|
218
|
+
privacy_class: 'internal',
|
|
219
|
+
retention_hint: 'retain',
|
|
220
|
+
introduced_version: V1_2,
|
|
221
|
+
deprecated: null,
|
|
222
|
+
},
|
|
213
223
|
});
|
|
214
224
|
|
|
215
225
|
// Error/wait/reason vocabulary: codes + definitions, no free-text grouping.
|