@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
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
// Bounded runtime-context compiler (G5 / TASK-002)
|
|
1
|
+
// Bounded runtime-context compiler (G5 / TASK-002) + subagent context
|
|
2
|
+
// manifest builder (C89-006 / SPEC §7).
|
|
2
3
|
// Pure module: no I/O, no runtimeConfig reads, no adapter/gate imports.
|
|
3
4
|
// Compiles goal / verified state / evidence / memory records into a frozen
|
|
4
5
|
// BoundedContext under a hard byte budget that includes framing and
|
|
@@ -10,7 +11,9 @@
|
|
|
10
11
|
// ineligible records are excluded (omitted with the policy reason), and
|
|
11
12
|
// eligible-but-unverified records are labeled 'unverified' and quoted —
|
|
12
13
|
// never injected as instruction.
|
|
14
|
+
import crypto from 'node:crypto';
|
|
13
15
|
|
|
16
|
+
import { validateTaskContract } from './contract.js';
|
|
14
17
|
import { eligible } from '../memory/policy.js';
|
|
15
18
|
|
|
16
19
|
const STALE_MS = 15 * 60 * 1000; // default freshness window for evidence
|
|
@@ -147,3 +150,344 @@ export function compileRuntimeContext({
|
|
|
147
150
|
}),
|
|
148
151
|
};
|
|
149
152
|
}
|
|
153
|
+
|
|
154
|
+
// --- C89-006: subagent ContextManifest (SPEC §7, FR-05) ---------------------
|
|
155
|
+
// `buildSubagentContext` selects isolated/curated context for a delegated
|
|
156
|
+
// role from caller-supplied candidates and emits a traceable manifest:
|
|
157
|
+
// every include/exclude carries reason + provenance + a deterministic token
|
|
158
|
+
// estimate, required contract fields are never dropped by size pressure,
|
|
159
|
+
// sensitive/secret refs are redacted before any retrieval runs, exact
|
|
160
|
+
// duplicates collapse while keeping lineage (sourceRefs), and a requested
|
|
161
|
+
// `fork` is honestly labeled `fork_unavailable` until a host probe proves
|
|
162
|
+
// real state inheritance — a summary copy is never a fork.
|
|
163
|
+
//
|
|
164
|
+
// The builder stays pure: it performs no fs/config/adapter access itself.
|
|
165
|
+
// Optional original verification is injected through `resolveOriginal`
|
|
166
|
+
// (wired to eventStore.resolveRunArtifact by callers that need it);
|
|
167
|
+
// persistence is eventStore.persistContextManifestRef, not this module.
|
|
168
|
+
|
|
169
|
+
const MANIFEST_VERSION = 1;
|
|
170
|
+
export const CONTEXT_MANIFEST_POLICY_VERSION = 'context-selection.v1';
|
|
171
|
+
|
|
172
|
+
// Deterministic local estimator (chars/4, min 1) — matches the codebase
|
|
173
|
+
// convention without importing the I/O-bearing token module.
|
|
174
|
+
function estimateTokens(value) {
|
|
175
|
+
const text = String(value ?? '');
|
|
176
|
+
if (!text) return 0;
|
|
177
|
+
return Math.max(1, Math.ceil(text.length / 4));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const isNonEmptyStr = (v) => typeof v === 'string' && v.length > 0;
|
|
181
|
+
const isPlainRec = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
182
|
+
|
|
183
|
+
// Fingerprint an item's content exactly (SPEC §7: exact dedupe only first —
|
|
184
|
+
// semantic pruning is deferred). Items with no content fall back to the ref.
|
|
185
|
+
function fingerprintOf(ref, text) {
|
|
186
|
+
const payload = text != null && text !== '' ? text : `<no-content:${ref}>`;
|
|
187
|
+
return crypto.createHash('sha256').update(payload).digest('hex');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const KNOWN_MODES = Object.freeze(['isolated', 'curated', 'fork']);
|
|
191
|
+
|
|
192
|
+
function forkCellStatus(hostCapabilities) {
|
|
193
|
+
return hostCapabilities?.capabilities?.fork?.status ?? null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Assemble a subagent ContextManifest (SPEC §7).
|
|
198
|
+
*
|
|
199
|
+
* @param {object} args
|
|
200
|
+
* @param {object} args.taskContract TaskContract v1 (validateTaskContract
|
|
201
|
+
* enforced at this boundary — a malformed contract gets no context).
|
|
202
|
+
* @param {string} args.role child role; defaults to taskContract.role.
|
|
203
|
+
* @param {string} [args.mode] 'isolated'|'curated'|'fork'; defaults to
|
|
204
|
+
* taskContract.contextMode then 'curated'. 'fork' resolves
|
|
205
|
+
* 'fork_unavailable' unless hostCapabilities proves real inheritance.
|
|
206
|
+
* @param {Array<object>} [args.candidates] parent state/evidence/memory
|
|
207
|
+
* candidates. Fields: ref, content|text, required, kind, audience,
|
|
208
|
+
* sensitivity ('sensitive' → redacted), provenance, observedAt, ttlMs,
|
|
209
|
+
* memoryRecord (routed through memory/policy eligible()), artifactRef
|
|
210
|
+
* (resolved via resolveOriginal when supplied).
|
|
211
|
+
* @param {object} [args.hostCapabilities] probeSubagentCapabilities result.
|
|
212
|
+
* @param {object} [args.policyContext] {projectId, projectAliases,
|
|
213
|
+
* includeUser} for memory eligible().
|
|
214
|
+
* @param {Function} [args.resolveOriginal] async (artifactRef) →
|
|
215
|
+
* {ok:true,sha256?}|{status:'unavailable',reason} — injected resolver;
|
|
216
|
+
* availability checks run AFTER sensitivity eligibility so secrets are
|
|
217
|
+
* never fetched.
|
|
218
|
+
* @param {number} [args.budgetTokens] advisory cap: optional items that do
|
|
219
|
+
* not fit are excluded 'over_budget'; required items are never dropped —
|
|
220
|
+
* an exceedance flips manifest.overBudget instead (quality > budget).
|
|
221
|
+
* @param {string} [args.runId] parent run id; defaults taskContract.id.
|
|
222
|
+
* @param {number} [args.now] clock for staleness checks.
|
|
223
|
+
* @returns {Promise<{ok:true, mode:'isolated'|'curated'|'fork_unavailable'|'fork',
|
|
224
|
+
* manifest:object, contextRefs:string[]}|{ok:false, code:string, errors?:string[]}>}
|
|
225
|
+
*/
|
|
226
|
+
export async function buildSubagentContext({
|
|
227
|
+
taskContract,
|
|
228
|
+
role,
|
|
229
|
+
mode,
|
|
230
|
+
candidates,
|
|
231
|
+
hostCapabilities,
|
|
232
|
+
policyContext = {},
|
|
233
|
+
resolveOriginal = null,
|
|
234
|
+
budgetTokens = null,
|
|
235
|
+
runId,
|
|
236
|
+
now,
|
|
237
|
+
} = {}) {
|
|
238
|
+
if (taskContract === undefined || taskContract === null) {
|
|
239
|
+
return { ok: false, code: 'malformed_input' };
|
|
240
|
+
}
|
|
241
|
+
const contractVerdict = validateTaskContract(taskContract);
|
|
242
|
+
if (!contractVerdict.valid) {
|
|
243
|
+
return { ok: false, code: 'invalid_contract', errors: contractVerdict.errors };
|
|
244
|
+
}
|
|
245
|
+
if (candidates != null && !Array.isArray(candidates)) {
|
|
246
|
+
return { ok: false, code: 'malformed_input' };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const effectiveRole = isNonEmptyStr(role) ? role : taskContract.role;
|
|
250
|
+
const requestedMode = mode ?? taskContract.contextMode ?? 'curated';
|
|
251
|
+
if (!KNOWN_MODES.includes(requestedMode)) {
|
|
252
|
+
return { ok: false, code: 'malformed_input' };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Fork honesty (SPEC §7): a real fork exists only where a host probe proves
|
|
256
|
+
// state inheritance; everything else is the curated fallback labeled
|
|
257
|
+
// 'fork_unavailable' — copying a short summary is never labeled fork.
|
|
258
|
+
const effectiveMode = requestedMode === 'fork'
|
|
259
|
+
? (forkCellStatus(hostCapabilities) === 'supported' ? 'fork' : 'fork_unavailable')
|
|
260
|
+
: requestedMode;
|
|
261
|
+
|
|
262
|
+
const effectiveRunId = isNonEmptyStr(runId) ? runId : taskContract.id;
|
|
263
|
+
const nowMs = typeof now === 'number' ? now : Date.now();
|
|
264
|
+
const budget = Number.isInteger(budgetTokens) && budgetTokens >= 0
|
|
265
|
+
? budgetTokens
|
|
266
|
+
: null;
|
|
267
|
+
|
|
268
|
+
const items = [];
|
|
269
|
+
const pushItem = (entry) => {
|
|
270
|
+
items.push(Object.freeze(entry));
|
|
271
|
+
return entry;
|
|
272
|
+
};
|
|
273
|
+
const exclude = (ref, reason, extra = {}) =>
|
|
274
|
+
pushItem({
|
|
275
|
+
ref,
|
|
276
|
+
included: false,
|
|
277
|
+
reason,
|
|
278
|
+
tokenEstimate: estimateTokens(extra.text ?? ref),
|
|
279
|
+
provenance: extra.provenance ?? 'unknown',
|
|
280
|
+
...(extra.detail != null ? { detail: extra.detail } : {}),
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// Required contract-derived items first: objective / scope / constraints /
|
|
284
|
+
// expectedOutput / stopConditions / evidenceRequirement / knownFacts are
|
|
285
|
+
// the acceptance boundary and are never droppable by size pressure.
|
|
286
|
+
const contractItem = (ref, text, provenance = 'task-contract') =>
|
|
287
|
+
pushItem({
|
|
288
|
+
ref,
|
|
289
|
+
included: true,
|
|
290
|
+
reason: 'required_contract',
|
|
291
|
+
tokenEstimate: estimateTokens(text),
|
|
292
|
+
provenance,
|
|
293
|
+
fingerprint: fingerprintOf(ref, text),
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
contractItem('contract:objective', taskContract.objective);
|
|
297
|
+
for (const [i, p] of (taskContract.scope?.include ?? []).entries()) {
|
|
298
|
+
contractItem(`contract:scope-include:${i}`, `scope include: ${p}`);
|
|
299
|
+
}
|
|
300
|
+
for (const [i, p] of (taskContract.scope?.exclude ?? []).entries()) {
|
|
301
|
+
contractItem(`contract:scope-exclude:${i}`, `scope exclude: ${p}`);
|
|
302
|
+
}
|
|
303
|
+
(taskContract.constraints ?? []).forEach((c, i) => {
|
|
304
|
+
contractItem(`contract:constraint:${i}`, `constraint: ${c}`);
|
|
305
|
+
});
|
|
306
|
+
contractItem('contract:expected-output', taskContract.expectedOutput);
|
|
307
|
+
if (taskContract.evidenceRequirement != null) {
|
|
308
|
+
contractItem('contract:evidence-requirement', taskContract.evidenceRequirement);
|
|
309
|
+
}
|
|
310
|
+
(taskContract.stopConditions ?? []).forEach((s, i) => {
|
|
311
|
+
contractItem(`contract:stop-condition:${i}`, `stop: ${s}`);
|
|
312
|
+
});
|
|
313
|
+
(taskContract.knownFacts ?? []).forEach((f, i) => {
|
|
314
|
+
contractItem(`contract:known-fact:${i}`, f.statement, f.source);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
const memContext = {
|
|
318
|
+
projectId: isNonEmptyStr(policyContext.projectId) ? policyContext.projectId : null,
|
|
319
|
+
projectAliases: Array.isArray(policyContext.projectAliases)
|
|
320
|
+
? policyContext.projectAliases
|
|
321
|
+
: undefined,
|
|
322
|
+
includeUser: policyContext.includeUser === true,
|
|
323
|
+
now: nowMs,
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const seenFingerprints = new Map(); // fingerprint → first ref (lineage)
|
|
327
|
+
let tokensUsed = items.reduce((n, i) => n + i.tokenEstimate, 0);
|
|
328
|
+
let overBudget = budget != null && tokensUsed > budget;
|
|
329
|
+
|
|
330
|
+
for (const [idx, cand] of (candidates ?? []).entries()) {
|
|
331
|
+
const ref = isNonEmptyStr(cand?.ref) ? cand.ref : `candidate:${idx}`;
|
|
332
|
+
const provenance = isNonEmptyStr(cand?.provenance)
|
|
333
|
+
? cand.provenance
|
|
334
|
+
: isNonEmptyStr(cand?.source) ? cand.source : 'unknown';
|
|
335
|
+
const text = typeof cand?.content === 'string' ? cand.content
|
|
336
|
+
: typeof cand?.text === 'string' ? cand.text : null;
|
|
337
|
+
const tokenEstimate = estimateTokens(text ?? ref);
|
|
338
|
+
|
|
339
|
+
if (!isPlainRec(cand) || !isNonEmptyStr(cand.ref)) {
|
|
340
|
+
exclude(ref, 'malformed_candidate', { text, provenance });
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
// Secrets never reach the child — redact before any original resolution
|
|
344
|
+
// so the resolver never fetches bytes it must not see.
|
|
345
|
+
if (cand.sensitivity === 'sensitive' || cand.artifactRef?.sensitivity === 'sensitive') {
|
|
346
|
+
exclude(ref, 'redacted', { text, provenance });
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
if (cand.memoryRecord != null) {
|
|
350
|
+
const verdict = eligible(cand.memoryRecord, memContext);
|
|
351
|
+
if (!verdict.ok) {
|
|
352
|
+
exclude(ref, verdict.reason, { text, provenance });
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
// Cross-run refs are out of scope for this child — deny before resolve.
|
|
357
|
+
if (isNonEmptyStr(cand.artifactRef?.runId) && cand.artifactRef.runId !== effectiveRunId) {
|
|
358
|
+
exclude(ref, 'cross_run', { text, provenance });
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
// Verify the original when a resolver is wired: a missing or
|
|
362
|
+
// checksum-changed original is marked invalid, never presented as
|
|
363
|
+
// current, and never silently omitted.
|
|
364
|
+
if (typeof resolveOriginal === 'function' && isPlainRec(cand.artifactRef)) {
|
|
365
|
+
let resolved = null;
|
|
366
|
+
try {
|
|
367
|
+
resolved = await resolveOriginal(cand.artifactRef);
|
|
368
|
+
} catch {
|
|
369
|
+
resolved = null;
|
|
370
|
+
}
|
|
371
|
+
if (resolved == null || resolved.status === 'unavailable') {
|
|
372
|
+
const reason = resolved?.reason;
|
|
373
|
+
const marked = reason === 'checksum_mismatch' || reason === 'bytes_mismatch'
|
|
374
|
+
? 'stale_original'
|
|
375
|
+
: 'original_unavailable';
|
|
376
|
+
exclude(ref, marked, { text, provenance, detail: reason ?? 'unresolved' });
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
if (typeof cand.observedAt === 'number') {
|
|
381
|
+
const ttl = typeof cand.ttlMs === 'number' ? cand.ttlMs : STALE_MS;
|
|
382
|
+
if (nowMs - cand.observedAt > ttl) {
|
|
383
|
+
exclude(ref, 'stale', { text, provenance });
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
if (cand.audience != null) {
|
|
388
|
+
const aud = Array.isArray(cand.audience) ? cand.audience : [cand.audience];
|
|
389
|
+
if (!aud.includes(effectiveRole)) {
|
|
390
|
+
exclude(ref, 'audience_mismatch', { text, provenance });
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
// Isolated lanes (SPEC §6 reviewer default) receive requirements / diffs /
|
|
395
|
+
// tests / evidence — implementer narrative is out by construction.
|
|
396
|
+
if (effectiveMode === 'isolated' && cand.kind === 'narrative' && cand.required !== true) {
|
|
397
|
+
exclude(ref, 'isolated_excludes_narrative', { text, provenance });
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const fingerprint = fingerprintOf(ref, text);
|
|
402
|
+
|
|
403
|
+
if (cand.required === true) {
|
|
404
|
+
pushItem({
|
|
405
|
+
ref,
|
|
406
|
+
included: true,
|
|
407
|
+
reason: 'required',
|
|
408
|
+
tokenEstimate,
|
|
409
|
+
provenance,
|
|
410
|
+
fingerprint,
|
|
411
|
+
});
|
|
412
|
+
tokensUsed += tokenEstimate;
|
|
413
|
+
if (budget != null && tokensUsed > budget) overBudget = true;
|
|
414
|
+
seenFingerprints.set(fingerprint, ref);
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Unknown relevance preserves the item (SPEC §7): the default action is
|
|
419
|
+
// include, so ambiguous candidates are carried with provenance intact.
|
|
420
|
+
const prior = seenFingerprints.get(fingerprint);
|
|
421
|
+
if (prior != null) {
|
|
422
|
+
// Exact-duplicate collapse — lineage kept on both sides.
|
|
423
|
+
const kept = items.find((i) => i.ref === prior);
|
|
424
|
+
const merged = {
|
|
425
|
+
...kept,
|
|
426
|
+
sourceRefs: Object.freeze([...(kept.sourceRefs ?? []), ref]),
|
|
427
|
+
};
|
|
428
|
+
items[items.indexOf(kept)] = Object.freeze(merged);
|
|
429
|
+
exclude(ref, 'duplicate', { text, provenance, detail: `of:${prior}` });
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
if (budget != null && tokensUsed + tokenEstimate > budget) {
|
|
433
|
+
exclude(ref, 'over_budget', { text, provenance });
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
const entry = {
|
|
437
|
+
ref,
|
|
438
|
+
included: true,
|
|
439
|
+
reason: 'relevant',
|
|
440
|
+
tokenEstimate,
|
|
441
|
+
provenance,
|
|
442
|
+
fingerprint,
|
|
443
|
+
};
|
|
444
|
+
if (cand.memoryRecord != null
|
|
445
|
+
&& cand.memoryRecord.verified !== true
|
|
446
|
+
&& cand.memoryRecord.trust_tier !== 'verified') {
|
|
447
|
+
entry.label = 'unverified';
|
|
448
|
+
}
|
|
449
|
+
pushItem(entry);
|
|
450
|
+
seenFingerprints.set(fingerprint, ref);
|
|
451
|
+
tokensUsed += tokenEstimate;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const contextRefs = Object.freeze(
|
|
455
|
+
items.filter((i) => i.included).map((i) => i.ref),
|
|
456
|
+
);
|
|
457
|
+
const excludedCandidates = Object.freeze(
|
|
458
|
+
items.filter((i) => !i.included).map((i) => i.ref),
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
// Source fingerprint: deterministic over what was fed in — requested mode,
|
|
462
|
+
// run binding, and the candidate ref set — so manifests can be compared
|
|
463
|
+
// across identical inputs without trusting a regenerated build.
|
|
464
|
+
const sourceFingerprint = crypto.createHash('sha256').update(JSON.stringify({
|
|
465
|
+
runId: effectiveRunId,
|
|
466
|
+
requestedMode,
|
|
467
|
+
refs: (candidates ?? []).map((c, i) =>
|
|
468
|
+
isNonEmptyStr(c?.ref) ? c.ref : `candidate:${i}`),
|
|
469
|
+
})).digest('hex');
|
|
470
|
+
|
|
471
|
+
const manifest = {
|
|
472
|
+
version: MANIFEST_VERSION,
|
|
473
|
+
runId: effectiveRunId,
|
|
474
|
+
mode: effectiveMode,
|
|
475
|
+
policyVersion: CONTEXT_MANIFEST_POLICY_VERSION,
|
|
476
|
+
// No shipped host exposes prompt-assembly control — the manifest is an
|
|
477
|
+
// advisory selection, never a claim about the child's actual prompt.
|
|
478
|
+
enforcement: 'advisory',
|
|
479
|
+
items: Object.freeze(items),
|
|
480
|
+
excludedCandidates,
|
|
481
|
+
tokensUsed,
|
|
482
|
+
overBudget,
|
|
483
|
+
sourceFingerprint,
|
|
484
|
+
};
|
|
485
|
+
if (effectiveMode !== requestedMode) manifest.requestedMode = requestedMode;
|
|
486
|
+
|
|
487
|
+
return {
|
|
488
|
+
ok: true,
|
|
489
|
+
mode: effectiveMode,
|
|
490
|
+
manifest: Object.freeze(manifest),
|
|
491
|
+
contextRefs,
|
|
492
|
+
};
|
|
493
|
+
}
|
|
@@ -322,3 +322,299 @@ export function validateNodeQuestion(question, bounds = {}) {
|
|
|
322
322
|
}
|
|
323
323
|
return ok();
|
|
324
324
|
}
|
|
325
|
+
|
|
326
|
+
// ---------------------------------------------------------------------------
|
|
327
|
+
// C89-003 — TaskContract v1 / ResultEnvelope v1 (SPEC §4–5).
|
|
328
|
+
//
|
|
329
|
+
// Additive envelope schema: the validators below return `{valid, errors[]}`
|
|
330
|
+
// (callers need the full error list at a trust boundary) instead of the
|
|
331
|
+
// `{ok, code}` shape used by the state-machine validators above. The envelope
|
|
332
|
+
// carries its own `version: 1` — CONTRACT_VERSION stays pinned to the
|
|
333
|
+
// operation-state contract and is not repurposed.
|
|
334
|
+
|
|
335
|
+
export const TASK_CONTRACT_VERSION = 1;
|
|
336
|
+
export const RESULT_ENVELOPE_VERSION = 1;
|
|
337
|
+
|
|
338
|
+
/** ResultEnvelope v1 status vocabulary (SPEC §5). */
|
|
339
|
+
export const RESULT_ENVELOPE_STATUSES = Object.freeze([
|
|
340
|
+
'complete',
|
|
341
|
+
'partial',
|
|
342
|
+
'blocked',
|
|
343
|
+
'needs_context',
|
|
344
|
+
'needs_capability',
|
|
345
|
+
'needs_delegation',
|
|
346
|
+
'needs_review',
|
|
347
|
+
'failed',
|
|
348
|
+
]);
|
|
349
|
+
|
|
350
|
+
/** Roles a TaskContract may delegate to (SPEC §6 initial set). */
|
|
351
|
+
export const SUBAGENT_ROLES = Object.freeze(['retriever', 'reasoner', 'reviewer']);
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Context modes known to the v1 contract (SPEC §7). Real `fork` is absent:
|
|
355
|
+
* no host proves state inheritance at this contract version, so the honest
|
|
356
|
+
* v1 vocabulary stops at `fork_unavailable`.
|
|
357
|
+
*/
|
|
358
|
+
export const SUBAGENT_CONTEXT_MODES = Object.freeze(['isolated', 'curated', 'fork_unavailable']);
|
|
359
|
+
|
|
360
|
+
/** Bounded budget vocabulary — no numeric caps in v1 (SPEC §10). */
|
|
361
|
+
export const BUDGET_CLASSES = Object.freeze(['small', 'medium', 'large']);
|
|
362
|
+
|
|
363
|
+
/** Sensitivity annotation on a run-artifact ref (SPEC §5). */
|
|
364
|
+
export const REF_SENSITIVITIES = Object.freeze(['default', 'sensitive']);
|
|
365
|
+
|
|
366
|
+
/** How a caller's write scope is realized on this host (SPEC §4). */
|
|
367
|
+
export const WRITE_SCOPE_ENFORCEMENT = Object.freeze(['enforced', 'advisory']);
|
|
368
|
+
|
|
369
|
+
const SHA256_HEX_RE = /^[0-9a-f]{64}$/;
|
|
370
|
+
|
|
371
|
+
const isNonEmptyString = (v) => typeof v === 'string' && v.length > 0;
|
|
372
|
+
const isStringArray = (v) => Array.isArray(v) && v.every(isNonEmptyString);
|
|
373
|
+
const isPlainRecord = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
374
|
+
|
|
375
|
+
const TASK_CONTRACT_KEYS = Object.freeze([
|
|
376
|
+
'version', 'id', 'objective', 'scope', 'constraints', 'knownFacts',
|
|
377
|
+
'expectedOutput', 'evidenceRequirement', 'role', 'contextMode',
|
|
378
|
+
'capabilities', 'writeScope', 'budgetClass', 'stopConditions',
|
|
379
|
+
]);
|
|
380
|
+
|
|
381
|
+
const RESULT_ENVELOPE_KEYS = Object.freeze([
|
|
382
|
+
'version', 'status', 'summary', 'claims', 'evidenceRefs', 'artifactRefs',
|
|
383
|
+
'unresolved', 'provenance',
|
|
384
|
+
]);
|
|
385
|
+
|
|
386
|
+
const REF_KEYS = Object.freeze([
|
|
387
|
+
'path', 'sha256', 'bytes', 'runId', 'truncated', 'sensitivity',
|
|
388
|
+
'version', 'capturedAt', 'stream',
|
|
389
|
+
]);
|
|
390
|
+
|
|
391
|
+
const CLAIM_KEYS = Object.freeze(['statement', 'evidenceRefs', 'confidence']);
|
|
392
|
+
const SCOPE_KEYS = Object.freeze(['include', 'exclude']);
|
|
393
|
+
const WRITE_SCOPE_KEYS = Object.freeze(['include', 'exclude', 'enforcement']);
|
|
394
|
+
const KNOWN_FACT_KEYS = Object.freeze(['statement', 'source']);
|
|
395
|
+
const UNRESOLVED_KEYS = Object.freeze(['reason', 'ref']);
|
|
396
|
+
|
|
397
|
+
function pushUnknownKeys(errors, obj, allowed, label) {
|
|
398
|
+
for (const key of Object.keys(obj)) {
|
|
399
|
+
if (!allowed.includes(key)) errors.push(`${label}: unknown field '${key}'`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function validateScopeObject(value, label, allowedKeys, errors) {
|
|
404
|
+
if (!isPlainRecord(value)) {
|
|
405
|
+
errors.push(`${label} must be an object`);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
pushUnknownKeys(errors, value, allowedKeys, label);
|
|
409
|
+
if (value.include !== undefined && !isStringArray(value.include)) {
|
|
410
|
+
errors.push(`${label}.include must be an array of non-empty strings`);
|
|
411
|
+
}
|
|
412
|
+
if (value.exclude !== undefined && !isStringArray(value.exclude)) {
|
|
413
|
+
errors.push(`${label}.exclude must be an array of non-empty strings`);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Validate one run-artifact reference (SPEC §5). A ref is meaningful only
|
|
419
|
+
* when it can be checksum-verified: `path` AND `sha256` are both required.
|
|
420
|
+
*/
|
|
421
|
+
function validateResultRef(ref, label, errors) {
|
|
422
|
+
if (!isPlainRecord(ref)) {
|
|
423
|
+
errors.push(`${label} must be an object`);
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
pushUnknownKeys(errors, ref, REF_KEYS, label);
|
|
427
|
+
if (!isNonEmptyString(ref.path)) {
|
|
428
|
+
errors.push(`${label}.path must be a non-empty string`);
|
|
429
|
+
}
|
|
430
|
+
if (!SHA256_HEX_RE.test(ref.sha256)) {
|
|
431
|
+
errors.push(`${label}.sha256 must be a 64-hex digest`);
|
|
432
|
+
}
|
|
433
|
+
if (ref.bytes !== undefined && (!Number.isInteger(ref.bytes) || ref.bytes < 0)) {
|
|
434
|
+
errors.push(`${label}.bytes must be a non-negative integer`);
|
|
435
|
+
}
|
|
436
|
+
if (ref.runId !== undefined && !isNonEmptyString(ref.runId)) {
|
|
437
|
+
errors.push(`${label}.runId must be a non-empty string`);
|
|
438
|
+
}
|
|
439
|
+
if (ref.truncated !== undefined && typeof ref.truncated !== 'boolean') {
|
|
440
|
+
errors.push(`${label}.truncated must be a boolean`);
|
|
441
|
+
}
|
|
442
|
+
if (ref.sensitivity !== undefined && !REF_SENSITIVITIES.includes(ref.sensitivity)) {
|
|
443
|
+
errors.push(`${label}.sensitivity must be one of: ${REF_SENSITIVITIES.join(', ')}`);
|
|
444
|
+
}
|
|
445
|
+
if (ref.version !== undefined && ref.version !== RESULT_ENVELOPE_VERSION) {
|
|
446
|
+
errors.push(`${label}.version must be ${RESULT_ENVELOPE_VERSION}`);
|
|
447
|
+
}
|
|
448
|
+
if (ref.capturedAt !== undefined
|
|
449
|
+
&& (typeof ref.capturedAt !== 'string' || Number.isNaN(Date.parse(ref.capturedAt)))) {
|
|
450
|
+
errors.push(`${label}.capturedAt must be an ISO timestamp`);
|
|
451
|
+
}
|
|
452
|
+
if (ref.stream !== undefined && ref.stream !== 'stdout' && ref.stream !== 'stderr') {
|
|
453
|
+
errors.push(`${label}.stream must be 'stdout' or 'stderr'`);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Validate a TaskContract v1 (SPEC §4). Required fields: id, objective,
|
|
459
|
+
* scope, constraints, expectedOutput, role. The schema is closed — unknown
|
|
460
|
+
* top-level keys reject so a later contract version cannot smuggle
|
|
461
|
+
* unvalidated fields through a v1 boundary.
|
|
462
|
+
*
|
|
463
|
+
* @param {*} value
|
|
464
|
+
* @returns {{valid:boolean, errors:string[]}}
|
|
465
|
+
*/
|
|
466
|
+
export function validateTaskContract(value) {
|
|
467
|
+
const errors = [];
|
|
468
|
+
if (!isPlainRecord(value)) {
|
|
469
|
+
return { valid: false, errors: ['contract must be an object'] };
|
|
470
|
+
}
|
|
471
|
+
pushUnknownKeys(errors, value, TASK_CONTRACT_KEYS, 'contract');
|
|
472
|
+
if (value.version !== TASK_CONTRACT_VERSION) {
|
|
473
|
+
errors.push(`version must be ${TASK_CONTRACT_VERSION}`);
|
|
474
|
+
}
|
|
475
|
+
if (!isNonEmptyString(value.id)) errors.push('id must be a non-empty string');
|
|
476
|
+
if (!isNonEmptyString(value.objective)) errors.push('objective must be a non-empty string');
|
|
477
|
+
if (value.scope === undefined) {
|
|
478
|
+
errors.push('scope is required');
|
|
479
|
+
} else {
|
|
480
|
+
validateScopeObject(value.scope, 'scope', SCOPE_KEYS, errors);
|
|
481
|
+
}
|
|
482
|
+
if (!isStringArray(value.constraints)) {
|
|
483
|
+
errors.push('constraints must be an array of non-empty strings');
|
|
484
|
+
}
|
|
485
|
+
if (!isNonEmptyString(value.expectedOutput)) {
|
|
486
|
+
errors.push('expectedOutput must be a non-empty string');
|
|
487
|
+
}
|
|
488
|
+
if (!SUBAGENT_ROLES.includes(value.role)) {
|
|
489
|
+
errors.push(`role must be one of: ${SUBAGENT_ROLES.join(', ')}`);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (value.knownFacts !== undefined) {
|
|
493
|
+
if (!Array.isArray(value.knownFacts)) {
|
|
494
|
+
errors.push('knownFacts must be an array of {statement, source}');
|
|
495
|
+
} else {
|
|
496
|
+
value.knownFacts.forEach((fact, i) => {
|
|
497
|
+
if (!isPlainRecord(fact)) {
|
|
498
|
+
errors.push(`knownFacts[${i}] must be an object`);
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
pushUnknownKeys(errors, fact, KNOWN_FACT_KEYS, `knownFacts[${i}]`);
|
|
502
|
+
if (!isNonEmptyString(fact.statement)) {
|
|
503
|
+
errors.push(`knownFacts[${i}].statement must be a non-empty string`);
|
|
504
|
+
}
|
|
505
|
+
// SPEC §4: externally supplied evidence requires provenance.
|
|
506
|
+
if (!isNonEmptyString(fact.source)) {
|
|
507
|
+
errors.push(`knownFacts[${i}].source must be a non-empty provenance string`);
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
if (value.evidenceRequirement !== undefined && !isNonEmptyString(value.evidenceRequirement)) {
|
|
513
|
+
errors.push('evidenceRequirement must be a non-empty string');
|
|
514
|
+
}
|
|
515
|
+
if (value.contextMode !== undefined && !SUBAGENT_CONTEXT_MODES.includes(value.contextMode)) {
|
|
516
|
+
errors.push(`contextMode must be one of: ${SUBAGENT_CONTEXT_MODES.join(', ')}`);
|
|
517
|
+
}
|
|
518
|
+
if (value.capabilities !== undefined && !isStringArray(value.capabilities)) {
|
|
519
|
+
errors.push('capabilities must be an array of non-empty strings');
|
|
520
|
+
}
|
|
521
|
+
if (value.writeScope !== undefined) {
|
|
522
|
+
if (!isPlainRecord(value.writeScope)) {
|
|
523
|
+
errors.push('writeScope must be an object');
|
|
524
|
+
} else {
|
|
525
|
+
validateScopeObject(value.writeScope, 'writeScope', WRITE_SCOPE_KEYS, errors);
|
|
526
|
+
if (value.writeScope.enforcement !== undefined
|
|
527
|
+
&& !WRITE_SCOPE_ENFORCEMENT.includes(value.writeScope.enforcement)) {
|
|
528
|
+
errors.push(`writeScope.enforcement must be one of: ${WRITE_SCOPE_ENFORCEMENT.join(', ')}`);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
if (value.budgetClass !== undefined && !BUDGET_CLASSES.includes(value.budgetClass)) {
|
|
533
|
+
errors.push(`budgetClass must be one of: ${BUDGET_CLASSES.join(', ')}`);
|
|
534
|
+
}
|
|
535
|
+
if (value.stopConditions !== undefined && !isStringArray(value.stopConditions)) {
|
|
536
|
+
errors.push('stopConditions must be an array of non-empty strings');
|
|
537
|
+
}
|
|
538
|
+
return { valid: errors.length === 0, errors };
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Validate a ResultEnvelope v1 (SPEC §5). The compact control result a child
|
|
543
|
+
* hands the parent: every status in RESULT_ENVELOPE_STATUSES is terminal for
|
|
544
|
+
* the child, evidence is by-ref only (never raw bytes).
|
|
545
|
+
*
|
|
546
|
+
* @param {*} value
|
|
547
|
+
* @returns {{valid:boolean, errors:string[]}}
|
|
548
|
+
*/
|
|
549
|
+
export function validateResultEnvelope(value) {
|
|
550
|
+
const errors = [];
|
|
551
|
+
if (!isPlainRecord(value)) {
|
|
552
|
+
return { valid: false, errors: ['envelope must be an object'] };
|
|
553
|
+
}
|
|
554
|
+
pushUnknownKeys(errors, value, RESULT_ENVELOPE_KEYS, 'envelope');
|
|
555
|
+
if (value.version !== RESULT_ENVELOPE_VERSION) {
|
|
556
|
+
errors.push(`version must be ${RESULT_ENVELOPE_VERSION}`);
|
|
557
|
+
}
|
|
558
|
+
if (!RESULT_ENVELOPE_STATUSES.includes(value.status)) {
|
|
559
|
+
errors.push(`status must be one of: ${RESULT_ENVELOPE_STATUSES.join(', ')}`);
|
|
560
|
+
}
|
|
561
|
+
if (typeof value.summary !== 'string') {
|
|
562
|
+
errors.push('summary must be a string');
|
|
563
|
+
}
|
|
564
|
+
if (!Array.isArray(value.claims)) {
|
|
565
|
+
errors.push('claims must be an array');
|
|
566
|
+
} else {
|
|
567
|
+
value.claims.forEach((claim, i) => {
|
|
568
|
+
const label = `claims[${i}]`;
|
|
569
|
+
if (!isPlainRecord(claim)) {
|
|
570
|
+
errors.push(`${label} must be an object`);
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
pushUnknownKeys(errors, claim, CLAIM_KEYS, label);
|
|
574
|
+
if (!isNonEmptyString(claim.statement)) {
|
|
575
|
+
errors.push(`${label}.statement must be a non-empty string`);
|
|
576
|
+
}
|
|
577
|
+
if (!Array.isArray(claim.evidenceRefs)) {
|
|
578
|
+
errors.push(`${label}.evidenceRefs must be an array`);
|
|
579
|
+
} else {
|
|
580
|
+
claim.evidenceRefs.forEach((ref, j) => validateResultRef(ref, `${label}.evidenceRefs[${j}]`, errors));
|
|
581
|
+
}
|
|
582
|
+
if (claim.confidence !== undefined
|
|
583
|
+
&& (typeof claim.confidence !== 'number' || claim.confidence < 0 || claim.confidence > 1)) {
|
|
584
|
+
errors.push(`${label}.confidence must be a number in [0,1]`);
|
|
585
|
+
}
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
if (!Array.isArray(value.evidenceRefs)) {
|
|
589
|
+
errors.push('evidenceRefs must be an array');
|
|
590
|
+
} else {
|
|
591
|
+
value.evidenceRefs.forEach((ref, i) => validateResultRef(ref, `evidenceRefs[${i}]`, errors));
|
|
592
|
+
}
|
|
593
|
+
if (!Array.isArray(value.artifactRefs)) {
|
|
594
|
+
errors.push('artifactRefs must be an array');
|
|
595
|
+
} else {
|
|
596
|
+
value.artifactRefs.forEach((ref, i) => validateResultRef(ref, `artifactRefs[${i}]`, errors));
|
|
597
|
+
}
|
|
598
|
+
if (!Array.isArray(value.unresolved)) {
|
|
599
|
+
errors.push('unresolved must be an array');
|
|
600
|
+
} else {
|
|
601
|
+
value.unresolved.forEach((entry, i) => {
|
|
602
|
+
const label = `unresolved[${i}]`;
|
|
603
|
+
if (!isPlainRecord(entry)) {
|
|
604
|
+
errors.push(`${label} must be an object`);
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
pushUnknownKeys(errors, entry, UNRESOLVED_KEYS, label);
|
|
608
|
+
if (!isNonEmptyString(entry.reason)) {
|
|
609
|
+
errors.push(`${label}.reason must be a non-empty string`);
|
|
610
|
+
}
|
|
611
|
+
if (entry.ref !== undefined) validateResultRef(entry.ref, `${label}.ref`, errors);
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
if (!isPlainRecord(value.provenance)) {
|
|
615
|
+
errors.push('provenance must be an object');
|
|
616
|
+
} else if (!isNonEmptyString(value.provenance.producer)) {
|
|
617
|
+
errors.push('provenance.producer must be a non-empty string');
|
|
618
|
+
}
|
|
619
|
+
return { valid: errors.length === 0, errors };
|
|
620
|
+
}
|