@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.
- package/CHANGELOG.md +44 -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
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
// verificationMap.js — verification-map table + recipe resolution
|
|
2
|
+
// (C85 TASK-014, BL-014 / SPEC FR-002 / ARCH §Verification Map).
|
|
3
|
+
//
|
|
4
|
+
// The map is DATA: template_user/playbooks/verification-map.json carries one
|
|
5
|
+
// launch/doctor/drive/expected/evidence/cleanup row per playbook work group
|
|
6
|
+
// (verbatim from ARCH §Verification Map) plus a per-artifact-class recipe table
|
|
7
|
+
// (ui|cli|docs|config). This module is the single query surface for both shell
|
|
8
|
+
// consumers (verification-guard.sh imports the .mjs twin and resolves recipes
|
|
9
|
+
// without parsing prose) and node consumers (stop-coordinator.mjs completion
|
|
10
|
+
// policy, ukit doctor's recipe-vs-package.json check).
|
|
11
|
+
//
|
|
12
|
+
// Resolution order mirrors the playbook registry:
|
|
13
|
+
// <projectRoot>/.ukit/playbooks/verification-map.json
|
|
14
|
+
// <homeDir>/.ukit/playbooks/verification-map.json
|
|
15
|
+
// packaged template_user/playbooks/verification-map.json (builtin)
|
|
16
|
+
// The first file that parses AND validates wins; a malformed higher tier cannot
|
|
17
|
+
// shadow a valid lower tier. Every miss/malformed case fails closed to
|
|
18
|
+
// map=null — consumers fall back to pre-map behavior, never fabricated checks.
|
|
19
|
+
//
|
|
20
|
+
// Mirror parity: template_project/.claude/ukit/index/verification-map.mjs
|
|
21
|
+
// carries the identical logic (the mirror cannot import src/). Both twins are
|
|
22
|
+
// locked by tests/consistency/verificationMapParity.test.js.
|
|
23
|
+
|
|
24
|
+
import fs from 'node:fs/promises';
|
|
25
|
+
import fsSync from 'node:fs';
|
|
26
|
+
import os from 'node:os';
|
|
27
|
+
import path from 'node:path';
|
|
28
|
+
import { fileURLToPath } from 'node:url';
|
|
29
|
+
|
|
30
|
+
const MAP_FILENAME = 'verification-map.json';
|
|
31
|
+
// src/index/verificationMap.js is two dirs below the package root — '../..'
|
|
32
|
+
// lands on it. The installed mirror (.claude/ukit/index/verification-map.mjs)
|
|
33
|
+
// walks four dirs instead so the repo checkout resolves the same
|
|
34
|
+
// template_user tree; on a real installed project the mirror's walk lands
|
|
35
|
+
// outside the project (no template_user dir), ENOENT skips the tier, and the
|
|
36
|
+
// ~/.ukit seed is the effective builtin. Callers may pass `builtinPath`.
|
|
37
|
+
const PACKAGE_BUILTIN_MAP_PATH = path.resolve(
|
|
38
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
39
|
+
'..', '..',
|
|
40
|
+
'template_user', 'playbooks', MAP_FILENAME,
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
export const ARTIFACT_CLASSES = Object.freeze(['ui', 'cli', 'docs', 'config']);
|
|
44
|
+
const ARTIFACT_CLASS_SET = new Set(ARTIFACT_CLASSES);
|
|
45
|
+
|
|
46
|
+
const PLAYBOOK_ROW_KEYS = ['launch', 'doctor', 'drive', 'expected', 'evidence', 'cleanup'];
|
|
47
|
+
|
|
48
|
+
// ─── Artifact classification ────────────────────────────────────────────────
|
|
49
|
+
// The artifact class comes from the files a change actually touched — the same
|
|
50
|
+
// git --porcelain path list verification-guard.sh already computes. Priority is
|
|
51
|
+
// deliberate: any UI file makes the diff `ui` (the ARCH falsifying bar blocks an
|
|
52
|
+
// unverified UI change), explicit CLI surfaces next, and docs/config only when
|
|
53
|
+
// the whole diff is docs/config. A diff with no signal classifies `null` —
|
|
54
|
+
// `recipeForArtifact` then returns null and every consumer keeps the pre-map
|
|
55
|
+
// behavior (SPEC §10: unknown class → no recipe → unchanged).
|
|
56
|
+
|
|
57
|
+
const UI_PATH_RE = /(^|\/)(components?|pages?|views?|screens?|layouts?|widgets?)\//i;
|
|
58
|
+
const UI_EXT_RE = /\.(jsx|tsx|vue|svelte|css|scss|sass|less|html|styl)$/i;
|
|
59
|
+
const DOCS_PATH_RE = /(^|\/)(docs?|wiki)\//i;
|
|
60
|
+
const DOCS_EXT_RE = /\.(md|mdx|rst|adoc|txt)$/i;
|
|
61
|
+
const CONFIG_BASENAME_RE = /^(?:[^/]*\.(json|ya?ml|toml|ini|env|cfg|conf)|\.?[A-Za-z][\w.-]*(rc|file|config|ignore|rc\.js|rc\.cjs|rc\.mjs|rc\.json|rc\.ya?ml)|dockerfile|compose[^/]*\.ya?ml|makefile|justfile)$/i;
|
|
62
|
+
const CLI_PATH_RE = /(^|\/)(bin|cli|cmd|commands)\//i;
|
|
63
|
+
const CLI_EXT_RE = /\.(sh|bash|zsh|ps1|bat|cmd)$/i;
|
|
64
|
+
const LOCKFILE_RE = /(^|\/)(?:package-lock\.json|yarn\.lock|pnpm-lock\.yaml|bun\.lockb?|composer\.lock|gemfile\.lock|poetry\.lock|cargo\.lock|go\.sum)$/i;
|
|
65
|
+
// Runtime-produced files are never the artifact being verified: UKit internal
|
|
66
|
+
// storage (.ukit/**) and the router's own state files appear in every real
|
|
67
|
+
// diff, and counting them would poison the whole-set docs/config rules.
|
|
68
|
+
const INTERNAL_STATE_RE = /(^|\/)\.ukit\/|(^|\/)(?:skill-router-state|route-cache|verification-progress|decisions)\.json$|(^|\/)decisions\.tsv$/i;
|
|
69
|
+
/**
|
|
70
|
+
* Classify a change's artifact class from the set of paths it touched.
|
|
71
|
+
* `changedPaths` are repo-relative paths (git --porcelain entries already have
|
|
72
|
+
* their status prefix stripped by the caller). Returns one of ARTIFACT_CLASSES
|
|
73
|
+
* or null for "no classifiable signal" (pre-map behavior everywhere downstream).
|
|
74
|
+
*/
|
|
75
|
+
|
|
76
|
+
// `git status --porcelain` C-quotes paths containing spaces/non-ASCII
|
|
77
|
+
// ("src/My Widget.jsx") — the trailing quote must go before the class regexes
|
|
78
|
+
// run or the entry silently misclassifies to null and the recipe lane skips
|
|
79
|
+
// (fail-open). `-z` callers never produce quotes; this is a defensive net for
|
|
80
|
+
// the porcelain form. Octal escapes (\303\244) are not valid JSON — the
|
|
81
|
+
// fallback keeps the inner text, still classified by extension/dir, never
|
|
82
|
+
// dropped.
|
|
83
|
+
function unquotePorcelainPath(entry) {
|
|
84
|
+
if (entry.length >= 2 && entry.startsWith('"') && entry.endsWith('"')) {
|
|
85
|
+
const inner = entry.slice(1, -1);
|
|
86
|
+
try {
|
|
87
|
+
const parsed = JSON.parse(entry);
|
|
88
|
+
if (typeof parsed === 'string') return parsed;
|
|
89
|
+
} catch { /* octal escapes are not valid JSON — keep the inner text */ }
|
|
90
|
+
return inner;
|
|
91
|
+
}
|
|
92
|
+
return entry;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function classifyArtifactClass(changedPaths = []) {
|
|
96
|
+
const paths = (Array.isArray(changedPaths) ? changedPaths : [])
|
|
97
|
+
.map((entry) => String(entry || '').trim())
|
|
98
|
+
.filter(Boolean)
|
|
99
|
+
// Rename entries carry "old -> new"; the destination is the changed artifact.
|
|
100
|
+
.map((entry) => entry.split(' -> ').pop())
|
|
101
|
+
.map(unquotePorcelainPath)
|
|
102
|
+
.filter((entry) => !INTERNAL_STATE_RE.test(entry));
|
|
103
|
+
if (paths.length === 0) return null;
|
|
104
|
+
// Lockfile-only diffs carry no verification recipe — they are dependency
|
|
105
|
+
// bookkeeping, verified by the next real change that runs the project.
|
|
106
|
+
if (paths.every((entry) => LOCKFILE_RE.test(entry))) return null;
|
|
107
|
+
if (paths.some((entry) => UI_PATH_RE.test(entry) || UI_EXT_RE.test(entry))) return 'ui';
|
|
108
|
+
if (paths.some((entry) => CLI_PATH_RE.test(entry) || CLI_EXT_RE.test(entry))) return 'cli';
|
|
109
|
+
if (paths.every((entry) => DOCS_PATH_RE.test(entry) || DOCS_EXT_RE.test(entry))) return 'docs';
|
|
110
|
+
if (paths.every((entry) => CONFIG_BASENAME_RE.test(path.basename(entry)))) return 'config';
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ─── Map loading ────────────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
export function verificationMapCandidatePaths({ projectRoot = null, homeDir = null, builtinPath = null } = {}) {
|
|
117
|
+
const candidates = [];
|
|
118
|
+
if (projectRoot) {
|
|
119
|
+
candidates.push({ path: path.join(projectRoot, '.ukit', 'playbooks', MAP_FILENAME), source: 'project' });
|
|
120
|
+
}
|
|
121
|
+
if (homeDir !== null) {
|
|
122
|
+
const base = homeDir || os.homedir();
|
|
123
|
+
candidates.push({ path: path.join(base, '.ukit', 'playbooks', MAP_FILENAME), source: 'user' });
|
|
124
|
+
}
|
|
125
|
+
candidates.push({ path: builtinPath ?? PACKAGE_BUILTIN_MAP_PATH, source: 'builtin' });
|
|
126
|
+
return candidates;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function isPlainObject(value) {
|
|
130
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Shape check. A valid map needs `version: 1`, at least one playbook row
|
|
135
|
+
* carrying every required recipe column, and an `artifactClasses` table whose
|
|
136
|
+
* rows are objects. Returns { ok, reason } — `reason` is a bounded code the
|
|
137
|
+
// doctor check prints verbatim (never the raw parse text on the report line).
|
|
138
|
+
*/
|
|
139
|
+
export function validateVerificationMap(map) {
|
|
140
|
+
if (!isPlainObject(map)) return { ok: false, reason: 'not-an-object' };
|
|
141
|
+
if (map.version !== 1) return { ok: false, reason: 'unsupported-version' };
|
|
142
|
+
if (!isPlainObject(map.playbooks) || Object.keys(map.playbooks).length === 0) {
|
|
143
|
+
return { ok: false, reason: 'missing-playbooks' };
|
|
144
|
+
}
|
|
145
|
+
for (const [id, row] of Object.entries(map.playbooks)) {
|
|
146
|
+
if (!isPlainObject(row)) return { ok: false, reason: `playbook-row-not-object:${id}` };
|
|
147
|
+
for (const key of PLAYBOOK_ROW_KEYS) {
|
|
148
|
+
if (key === 'evidence') {
|
|
149
|
+
if (!Array.isArray(row.evidence) || row.evidence.length === 0
|
|
150
|
+
|| !row.evidence.every((entry) => typeof entry === 'string' && entry.trim())) {
|
|
151
|
+
return { ok: false, reason: `playbook-row-missing-evidence:${id}` };
|
|
152
|
+
}
|
|
153
|
+
} else if (typeof row[key] !== 'string' || !row[key].trim()) {
|
|
154
|
+
return { ok: false, reason: `playbook-row-missing-key:${id}.${key}` };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (!isPlainObject(map.artifactClasses)) return { ok: false, reason: 'missing-artifact-classes' };
|
|
159
|
+
for (const [name, row] of Object.entries(map.artifactClasses)) {
|
|
160
|
+
if (!ARTIFACT_CLASS_SET.has(name)) return { ok: false, reason: `unknown-artifact-class:${name}` };
|
|
161
|
+
if (!isPlainObject(row)) return { ok: false, reason: `artifact-row-not-object:${name}` };
|
|
162
|
+
if (row.evidenceRequired !== undefined
|
|
163
|
+
&& (!Array.isArray(row.evidenceRequired)
|
|
164
|
+
|| !row.evidenceRequired.every((entry) => typeof entry === 'string' && entry.trim()))) {
|
|
165
|
+
return { ok: false, reason: `artifact-row-bad-evidence:${name}` };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return { ok: true, reason: null };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Inspect (never throws): walk the candidate tiers, return the first valid map
|
|
173
|
+
* plus its provenance. On total failure `map` is null and `reason` says why —
|
|
174
|
+
* `no-map` (nothing readable at any tier) or a validation reason from the
|
|
175
|
+
* last readable-but-invalid candidate. Consumers treat map===null as
|
|
176
|
+
// "pre-map behavior", the fail-closed contract of ARCH §Failure Modes.
|
|
177
|
+
*/
|
|
178
|
+
export async function inspectVerificationMap({ projectRoot = null, homeDir = null, builtinPath = null } = {}) {
|
|
179
|
+
let lastInvalidReason = 'no-map';
|
|
180
|
+
for (const candidate of verificationMapCandidatePaths({ projectRoot, homeDir, builtinPath })) {
|
|
181
|
+
let raw;
|
|
182
|
+
try {
|
|
183
|
+
raw = await fs.readFile(candidate.path, 'utf8');
|
|
184
|
+
} catch {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
let parsed;
|
|
188
|
+
try {
|
|
189
|
+
parsed = JSON.parse(raw);
|
|
190
|
+
} catch {
|
|
191
|
+
lastInvalidReason = `invalid-json:${candidate.source}`;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
const verdict = validateVerificationMap(parsed);
|
|
195
|
+
if (!verdict.ok) {
|
|
196
|
+
lastInvalidReason = `${verdict.reason}@${candidate.source}`;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
return { map: parsed, path: candidate.path, source: candidate.source, reason: null };
|
|
200
|
+
}
|
|
201
|
+
return { map: null, path: null, source: null, reason: lastInvalidReason };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* loadVerificationMap(root?) → VerificationMap|null.
|
|
206
|
+
* `root` is the project root (project-tier map lookup); pass an options object
|
|
207
|
+
* `{projectRoot, homeDir, builtinPath}` to control every tier. Fails closed:
|
|
208
|
+
* missing/malformed map → null.
|
|
209
|
+
*/
|
|
210
|
+
export async function loadVerificationMap(rootOrOptions = null) {
|
|
211
|
+
const options = isPlainObject(rootOrOptions)
|
|
212
|
+
? rootOrOptions
|
|
213
|
+
: { projectRoot: rootOrOptions || null };
|
|
214
|
+
const { map } = await inspectVerificationMap(options);
|
|
215
|
+
return map;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ─── Recipe resolution ──────────────────────────────────────────────────────
|
|
219
|
+
|
|
220
|
+
function normalizeArtifactClass(artifactClass) {
|
|
221
|
+
const normalized = String(artifactClass || '').trim().toLowerCase();
|
|
222
|
+
return ARTIFACT_CLASS_SET.has(normalized) ? normalized : null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* recipeForArtifact({playbookId, artifactClass, map?}) → recipe | null.
|
|
227
|
+
*
|
|
228
|
+
* The resolved recipe is the artifact-class row merged with the routed
|
|
229
|
+
* playbook row (for observability — the class row is what gates):
|
|
230
|
+
* { playbookId, artifactClass, check, harness?, fallback?, evidenceRequired[],
|
|
231
|
+
* playbook: {launch,doctor,drive,expected,evidence[],cleanup}|null }
|
|
232
|
+
*
|
|
233
|
+
* - unknown/missing `artifactClass` → null (pre-map behavior, SPEC §10);
|
|
234
|
+
* - `map` omitted → loaded on demand (project tier under `projectRoot`);
|
|
235
|
+
* - map===null / missing class row → null;
|
|
236
|
+
* - unknown/absent `playbookId` → `playbook: null`, the class row still resolves
|
|
237
|
+
* (recipes key on the artifact, not the route).
|
|
238
|
+
*/
|
|
239
|
+
export async function recipeForArtifact({
|
|
240
|
+
playbookId = null,
|
|
241
|
+
artifactClass = null,
|
|
242
|
+
map: mapOption,
|
|
243
|
+
projectRoot = null,
|
|
244
|
+
} = {}) {
|
|
245
|
+
const cls = normalizeArtifactClass(artifactClass);
|
|
246
|
+
if (!cls) return null;
|
|
247
|
+
const map = mapOption !== undefined ? mapOption : await loadVerificationMap(projectRoot);
|
|
248
|
+
if (!isPlainObject(map)) return null;
|
|
249
|
+
const classRow = map.artifactClasses?.[cls];
|
|
250
|
+
if (!isPlainObject(classRow)) return null;
|
|
251
|
+
|
|
252
|
+
const playbookRow = (() => {
|
|
253
|
+
const id = String(playbookId || '').trim();
|
|
254
|
+
if (!id) return null;
|
|
255
|
+
const row = map.playbooks?.[id];
|
|
256
|
+
if (!isPlainObject(row)) return null;
|
|
257
|
+
const shaped = {};
|
|
258
|
+
for (const key of PLAYBOOK_ROW_KEYS) {
|
|
259
|
+
shaped[key] = key === 'evidence'
|
|
260
|
+
? (Array.isArray(row.evidence) ? [...row.evidence] : [])
|
|
261
|
+
: (typeof row[key] === 'string' ? row[key] : '');
|
|
262
|
+
}
|
|
263
|
+
return shaped;
|
|
264
|
+
})();
|
|
265
|
+
|
|
266
|
+
const harness = isPlainObject(classRow.harness) && typeof classRow.harness.packageScript === 'string'
|
|
267
|
+
? { packageScript: classRow.harness.packageScript }
|
|
268
|
+
: null;
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
playbookId: playbookRow ? String(playbookId).trim() : null,
|
|
272
|
+
artifactClass: cls,
|
|
273
|
+
name: typeof classRow.name === 'string' ? classRow.name : cls,
|
|
274
|
+
check: typeof classRow.check === 'string' && classRow.check.trim() ? classRow.check : null,
|
|
275
|
+
harness,
|
|
276
|
+
fallback: typeof classRow.fallback === 'string' && classRow.fallback.trim() ? classRow.fallback.trim() : null,
|
|
277
|
+
evidenceRequired: Array.isArray(classRow.evidenceRequired)
|
|
278
|
+
? classRow.evidenceRequired.filter((entry) => typeof entry === 'string' && entry.trim())
|
|
279
|
+
: [],
|
|
280
|
+
playbook: playbookRow,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ─── Receipt satisfaction ───────────────────────────────────────────────────
|
|
285
|
+
// Shared by the stop gate (execution-ledger.mjs requires evidence) and the
|
|
286
|
+
// guard's advisory findings — the ledger carries the same semantics pinned by
|
|
287
|
+
// tests/consistency/receiptSatisfactionParity.test.js (runtime/ cannot import
|
|
288
|
+
// index/ — partial installs break — so parity is enforced by test, not a
|
|
289
|
+
// shared import). A required class token may carry `|` alternatives
|
|
290
|
+
// (`render-observation|io-case` — the ARCH evidence cell verbatim): any ONE
|
|
291
|
+
// alternative satisfies it. A class is satisfied when the ledger carries a
|
|
292
|
+
// receipt whose attested `evidence.class` matches an alternative
|
|
293
|
+
// (UKIT_EVIDENCE=class=…;surface=… — direct proof the guard may not have
|
|
294
|
+
// observed), or when the playbook-finding bank records the class — whole
|
|
295
|
+
// token or one alternative — as status 'satisfied'. Findings are latest-wins:
|
|
296
|
+
// ledger.playbookFindings (latest per class, banked outside the evictable
|
|
297
|
+
// receipt window) governs — a later 'missing' finding revokes an earlier
|
|
298
|
+
// 'satisfied' one, and a satisfied class survives receipt eviction via the
|
|
299
|
+
// bank. A failed attestation never satisfies — a failed run that happened to
|
|
300
|
+
// attest a class is not proof of the class.
|
|
301
|
+
|
|
302
|
+
export function receiptEvidenceSatisfied(requiredClass, ledger = {}) {
|
|
303
|
+
const token = String(requiredClass || '').trim();
|
|
304
|
+
const alternatives = token
|
|
305
|
+
.split('|')
|
|
306
|
+
.map((entry) => entry.trim())
|
|
307
|
+
.filter(Boolean);
|
|
308
|
+
if (alternatives.length === 0) return false;
|
|
309
|
+
const matchesClass = (value) => {
|
|
310
|
+
const cls = typeof value === 'string' ? value.trim() : '';
|
|
311
|
+
return cls === token || alternatives.includes(cls);
|
|
312
|
+
};
|
|
313
|
+
// The banked latest-per-class status governs the receipt window; several
|
|
314
|
+
// banked keys can match one anyOf token, so the newest entry wins. A
|
|
315
|
+
// playbook-finding receipt folds into the bank regardless of its success
|
|
316
|
+
// flag — the receipt scan below mirrors that.
|
|
317
|
+
let bankedTs = -1;
|
|
318
|
+
let bankedStatus = null;
|
|
319
|
+
const bank = ledger?.playbookFindings;
|
|
320
|
+
if (bank && typeof bank === 'object') {
|
|
321
|
+
for (const [key, entry] of Object.entries(bank)) {
|
|
322
|
+
if (!matchesClass(key)) continue;
|
|
323
|
+
const ts = typeof entry?.ts === 'number' ? entry.ts : 0;
|
|
324
|
+
if (ts >= bankedTs) {
|
|
325
|
+
bankedTs = ts;
|
|
326
|
+
bankedStatus = typeof entry?.status === 'string' ? entry.status : null;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
const receipts = Array.isArray(ledger?.receipts) ? ledger.receipts : [];
|
|
331
|
+
let attested = false;
|
|
332
|
+
let scannedStatus = null;
|
|
333
|
+
for (const receipt of receipts) {
|
|
334
|
+
if (!receipt || typeof receipt !== 'object') continue;
|
|
335
|
+
if (receipt.kind === 'playbook-finding') {
|
|
336
|
+
if (matchesClass(receipt.class) && typeof receipt.status === 'string') {
|
|
337
|
+
scannedStatus = receipt.status; // chronological — last write wins
|
|
338
|
+
}
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
if (receipt.success === false) continue;
|
|
342
|
+
const cls = receipt?.evidence?.class;
|
|
343
|
+
if (typeof cls === 'string' && alternatives.includes(cls.trim())) attested = true;
|
|
344
|
+
}
|
|
345
|
+
if (attested) return true;
|
|
346
|
+
const findingStatus = bankedTs >= 0 ? bankedStatus : scannedStatus;
|
|
347
|
+
return findingStatus === 'satisfied';
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ─── Doctor recipe check ────────────────────────────────────────────────────
|
|
351
|
+
/**
|
|
352
|
+
* doctorRecipeCheck({map, packageJsonScripts}) → { checked, missing[], mapAbsent }.
|
|
353
|
+
*
|
|
354
|
+
* ARCH §Failure Modes "verification recipe demands absent harness": a recipe
|
|
355
|
+
* whose named package.json script is absent degrades to a capability-negotiated
|
|
356
|
+
* FALLBACK RECEIPT — the returned `fallback` text is the receipt spec to print,
|
|
357
|
+
* never a fabricated command. `map===null` (absent/malformed) reports
|
|
358
|
+
* mapAbsent with zero missing entries — a broken map is itself a doctor warning
|
|
359
|
+
// (the caller composes the row), not silent data.
|
|
360
|
+
*/
|
|
361
|
+
export function doctorRecipeCheck({ map = null, packageJsonScripts = {} } = {}) {
|
|
362
|
+
if (!isPlainObject(map)) {
|
|
363
|
+
return { checked: 0, missing: [], mapAbsent: true };
|
|
364
|
+
}
|
|
365
|
+
const scripts = isPlainObject(packageJsonScripts) ? packageJsonScripts : {};
|
|
366
|
+
const missing = [];
|
|
367
|
+
let checked = 0;
|
|
368
|
+
for (const [className, row] of Object.entries(map.artifactClasses || {})) {
|
|
369
|
+
const script = row?.harness?.packageScript;
|
|
370
|
+
if (typeof script !== 'string' || !script.trim()) continue;
|
|
371
|
+
checked += 1;
|
|
372
|
+
if (!Object.hasOwn(scripts, script)) {
|
|
373
|
+
missing.push({
|
|
374
|
+
artifactClass: className,
|
|
375
|
+
script,
|
|
376
|
+
check: typeof row.check === 'string' ? row.check : null,
|
|
377
|
+
fallback: typeof row.fallback === 'string' ? row.fallback : null,
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return { checked, missing, mapAbsent: false };
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Sync layer for hook runtimes that evaluate inside a deadline (the guard's
|
|
385
|
+
// git-porcelain path is already synchronous there). Same fail-closed contract.
|
|
386
|
+
export function loadVerificationMapSync(rootOrOptions = null) {
|
|
387
|
+
const options = isPlainObject(rootOrOptions)
|
|
388
|
+
? rootOrOptions
|
|
389
|
+
: { projectRoot: rootOrOptions || null };
|
|
390
|
+
const { projectRoot = null, homeDir = null, builtinPath = null } = options;
|
|
391
|
+
for (const candidate of verificationMapCandidatePaths({ projectRoot, homeDir, builtinPath })) {
|
|
392
|
+
let raw;
|
|
393
|
+
try {
|
|
394
|
+
raw = fsSync.readFileSync(candidate.path, 'utf8');
|
|
395
|
+
} catch {
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
try {
|
|
399
|
+
const parsed = JSON.parse(raw);
|
|
400
|
+
if (validateVerificationMap(parsed).ok) return parsed;
|
|
401
|
+
} catch {}
|
|
402
|
+
}
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// observability-emit.mjs — TASK-003 (FR-003, BL-005) in-proc chain step.
|
|
2
|
+
//
|
|
3
|
+
// Thin entry the hook-chain-runner loads for the SessionStart /
|
|
4
|
+
// session_start chains. All behavior lives in the parity-locked emit
|
|
5
|
+
// module under ukit/runtime/ so the same record writer is importable by
|
|
6
|
+
// TASK-005's terminal emitters and this step stays a one-line delegate.
|
|
7
|
+
//
|
|
8
|
+
// Advisory end-to-end: runHook always returns { code: 0 } — a telemetry
|
|
9
|
+
// fault can never block or delay session start. Listed FIRST in the chain
|
|
10
|
+
// so the `execution.started` record lands before any slow .sh member can
|
|
11
|
+
// exhaust the chain budget (a timeout further down would silently skip a
|
|
12
|
+
// trailing emit step).
|
|
13
|
+
|
|
14
|
+
export { runHook } from '../ukit/runtime/observability-emit.mjs';
|
|
@@ -64,8 +64,9 @@ async function record({ rawInput, projectRoot, env, signal, deadlineMs }) {
|
|
|
64
64
|
if (typeof ledger.setLedgerMessageEmitter === 'function') {
|
|
65
65
|
ledger.setLedgerMessageEmitter(emit);
|
|
66
66
|
}
|
|
67
|
+
let receiptResult = null;
|
|
67
68
|
try {
|
|
68
|
-
await ledger.recordExecutionReceipt({
|
|
69
|
+
receiptResult = await ledger.recordExecutionReceipt({
|
|
69
70
|
projectRoot,
|
|
70
71
|
payload,
|
|
71
72
|
toolName: payload.tool_name,
|
|
@@ -80,6 +81,23 @@ async function record({ rawInput, projectRoot, env, signal, deadlineMs }) {
|
|
|
80
81
|
ledger.setLedgerMessageEmitter(null);
|
|
81
82
|
}
|
|
82
83
|
}
|
|
84
|
+
// TASK-005 (FR-005): a committed completion receipt (write/verification
|
|
85
|
+
// terminal fields) emits `outcome.observed` — the ledger minted the
|
|
86
|
+
// descriptor inside its lock, this hook emits it here so the telemetry
|
|
87
|
+
// write never delays the ledger lock. Fire-and-forget: a failed emit
|
|
88
|
+
// returns null and must not touch the advisory exit.
|
|
89
|
+
if (receiptResult?.value?.outcome && typeof ledger.emitTerminalOutcome === 'function') {
|
|
90
|
+
try {
|
|
91
|
+
await ledger.emitTerminalOutcome({
|
|
92
|
+
projectRoot,
|
|
93
|
+
outcome: receiptResult.value.outcome,
|
|
94
|
+
payload,
|
|
95
|
+
harness: env.UKIT_HARNESS || 'claude-code',
|
|
96
|
+
env,
|
|
97
|
+
deadlineMs: Number.isFinite(deadlineMs) ? Math.min(250, deadlineMs) : 250,
|
|
98
|
+
});
|
|
99
|
+
} catch { /* telemetry never blocks the receipt */ }
|
|
100
|
+
}
|
|
83
101
|
return { code: 0, stdout: emitted.join(''), stderr: '' };
|
|
84
102
|
}
|
|
85
103
|
|