agentic-workflow-manager 9.8.0 → 9.10.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.
Files changed (47) hide show
  1. package/dist/src/commands/job/gate.js +18 -0
  2. package/dist/src/commands/job/index.js +61 -0
  3. package/dist/src/commands/model-policy/index.js +94 -0
  4. package/dist/src/commands/plan/index.js +59 -2
  5. package/dist/src/commands/watch/apply.js +32 -0
  6. package/dist/src/core/admission/index.js +52 -2
  7. package/dist/src/core/admission/registry-contracts.js +1 -1
  8. package/dist/src/core/journal/requests.js +1 -1
  9. package/dist/src/core/journal/types.js +22 -1
  10. package/dist/src/core/model-policy/canonical.js +18 -0
  11. package/dist/src/core/model-policy/capabilities.js +197 -0
  12. package/dist/src/core/model-policy/capability-types.js +2 -0
  13. package/dist/src/core/model-policy/digest.js +35 -0
  14. package/dist/src/core/model-policy/journal.js +124 -0
  15. package/dist/src/core/model-policy/paths.js +12 -0
  16. package/dist/src/core/model-policy/resolve.js +155 -0
  17. package/dist/src/core/model-policy/store.js +189 -0
  18. package/dist/src/core/model-policy/types.js +2 -0
  19. package/dist/src/core/model-policy/validate.js +112 -0
  20. package/dist/src/core/plan/identity.js +4 -2
  21. package/dist/src/core/plan/validate.js +15 -16
  22. package/dist/src/index.js +2 -0
  23. package/dist/tests/commands/job/gate-reconcile.test.js +16 -0
  24. package/dist/tests/commands/job/gate.test.js +9 -0
  25. package/dist/tests/commands/job/routing-report-cli.test.js +46 -0
  26. package/dist/tests/commands/job/routing.test.js +11 -0
  27. package/dist/tests/commands/model-policy/index.test.js +117 -0
  28. package/dist/tests/commands/plan/index.test.js +52 -0
  29. package/dist/tests/commands/watch/apply.test.js +12 -0
  30. package/dist/tests/core/admission/index.test.js +53 -0
  31. package/dist/tests/core/model-policy/capabilities.test.js +365 -0
  32. package/dist/tests/core/model-policy/digest.test.js +117 -0
  33. package/dist/tests/core/model-policy/journal.test.js +46 -0
  34. package/dist/tests/core/model-policy/policy.test.js +47 -0
  35. package/dist/tests/core/model-policy/resolve.test.js +185 -0
  36. package/dist/tests/core/model-policy/store.test.js +177 -0
  37. package/dist/tests/core/plan/validate.test.js +40 -2
  38. package/dist/tests/core/secure-fs/native-bridge.test.js +81 -4
  39. package/dist/tests/structural/directory-fsync-is-single-source.test.js +97 -0
  40. package/dist/tests/structural/native-release-artifacts.test.js +14 -0
  41. package/package.json +3 -2
  42. package/prebuilds/darwin-arm64/secure_fs.node +0 -0
  43. package/prebuilds/darwin-x64/secure_fs.node +0 -0
  44. package/prebuilds/linux-arm64/secure_fs.node +0 -0
  45. package/prebuilds/linux-x64/secure_fs.node +0 -0
  46. package/prebuilds/win32-arm64/secure_fs.node +0 -0
  47. package/prebuilds/win32-x64/secure_fs.node +0 -0
@@ -3,6 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.computeGate = computeGate;
4
4
  exports.computeTrackGate = computeTrackGate;
5
5
  const LIVE = ['received', 'spawn-intent', 'claimed', 'running', 'cancel-requested'];
6
+ function sameSelection(left, right) {
7
+ return left.selector.kind === right.selector.kind && left.selector.id === right.selector.id && left.effort.kind === right.effort.kind && left.effort.value === right.effort.value;
8
+ }
6
9
  function evaluateEvidence(state, fingerprintNow, scope) {
7
10
  const reasons = [];
8
11
  if (state.cycle.status === 'BLOCKED') {
@@ -123,6 +126,21 @@ function evaluateEvidence(state, fingerprintNow, scope) {
123
126
  }
124
127
  }
125
128
  for (const v of state.verdicts) {
129
+ if (v.routingAttemptId !== undefined) {
130
+ const attempt = state.routingAttempts?.find((candidate) => candidate.id === v.routingAttemptId);
131
+ const binding = state.planBinding;
132
+ const bound = binding !== undefined && binding.digest === attempt?.envelope.planDigest && binding.executionDigest === attempt?.envelope.executionDigest;
133
+ const observed = attempt?.observed;
134
+ const selectionMatches = attempt !== undefined && observed !== undefined && sameSelection(observed, attempt.envelope.resolved);
135
+ // A reported actual selection is authoritative: degradation may
136
+ // explain absent observability, never excuse an observed mismatch.
137
+ const nativeObservationValid = observed !== undefined
138
+ ? selectionMatches
139
+ : attempt?.envelope.outcome === 'degraded' && (attempt.envelope.unavailableEvidence.length ?? 0) > 0 && Boolean(attempt.reasonCode);
140
+ if (attempt === undefined || !['active', 'complete'].includes(attempt.state) || !attempt.nativeAgentId || attempt.obligationId !== v.obligationId || attempt.fingerprint !== v.fingerprint || !bound || !nativeObservationValid) {
141
+ reasons.push({ category: 'routing-evidence', detail: `verdict routed ${v.id} no tiene evidencia de intento observado, vigente y ligado` });
142
+ }
143
+ }
126
144
  if (v.result !== 'pass') {
127
145
  const fix = state.fixes.find((f) => f.verdictId === v.id);
128
146
  if (fix === undefined || !fix.closed)
@@ -23,6 +23,9 @@ const paths_1 = require("../../core/journal/paths");
23
23
  const lock_1 = require("../watch/lock");
24
24
  const atomic_file_1 = require("../../core/atomic-file");
25
25
  const context_1 = require("../../core/tracks/context");
26
+ const journal_1 = require("../../core/model-policy/journal");
27
+ const types_1 = require("../../core/journal/types");
28
+ const json_1 = require("../../core/plan/json");
26
29
  const fs_1 = __importDefault(require("fs"));
27
30
  function branchOf(cwd) {
28
31
  // stdio explicito (ver EXEC_STDIO en journal/process.ts): evita el relay
@@ -43,6 +46,19 @@ function realFingerprintNow(repo) {
43
46
  }
44
47
  };
45
48
  }
49
+ function readBoundedJson(file) {
50
+ if (typeof file !== 'string' || file.length === 0 || file.length > 4096)
51
+ throw new Error('routing file path is invalid');
52
+ const stat = fs_1.default.lstatSync(file);
53
+ if (stat.isSymbolicLink() || !stat.isFile() || stat.size > 256 * 1024)
54
+ throw new Error('routing file must be a bounded non-symlink regular file');
55
+ try {
56
+ return (0, json_1.parseJsonNoDuplicate)(fs_1.default.readFileSync(file, 'utf8'));
57
+ }
58
+ catch {
59
+ throw new Error('routing file must contain JSON without duplicate keys');
60
+ }
61
+ }
46
62
  /** Guard de entrada (R9.4): sin descriptor de track, es un no-op — el caso
47
63
  * comun de siempre no paga costo ni riesgo nuevo. Con descriptor presente
48
64
  * que no autentica (fencing/realpath/journalId no coinciden), rechaza ANTES
@@ -143,6 +159,36 @@ function registerJobCommand(program) {
143
159
  assertAuthenticatedCwd(repo, branch);
144
160
  (0, heartbeat_1.emitHeartbeat)(repo, branch, opts.generation);
145
161
  });
162
+ job.command('routing-reserve')
163
+ .requiredOption('--generation <token>').requiredOption('--obligation <id>').requiredOption('--lineage <id>')
164
+ .requiredOption('--envelope-file <file>').requiredOption('--fingerprint <sha>').option('--cwd <root>', 'repository root', '.').option('--json')
165
+ .action((opts) => {
166
+ const envelope = readBoundedJson(opts.envelopeFile);
167
+ if (!(0, types_1.isRoutingEnvelope)(envelope) || !/^[a-f0-9]{64}$/.test(opts.fingerprint))
168
+ throw new Error('routing-reserve requires a valid envelope and fingerprint');
169
+ const repo = path_1.default.resolve(opts.cwd);
170
+ const branch = branchOf(repo);
171
+ assertAuthenticatedCwd(repo, branch);
172
+ const emitted = (0, requests_1.emitRequest)(repo, branch, { kind: 'routing-reserve', generationToken: opts.generation, idempotencyKey: crypto_1.default.createHash('sha256').update(`routing-reserve:${opts.generation}:${opts.obligation}:${opts.lineage}:${opts.fingerprint}:${JSON.stringify(envelope)}`).digest('hex'), payload: { obligationId: opts.obligation, lineageId: opts.lineage, envelope, fingerprint: opts.fingerprint } });
173
+ process.stdout.write(JSON.stringify({ requestId: emitted.requestId }) + '\n');
174
+ });
175
+ job.command('routing-observe')
176
+ .requiredOption('--generation <token>').requiredOption('--attempt <id>').requiredOption('--native-agent-id <id>')
177
+ .requiredOption('--observation-file <file>').option('--cwd <root>', 'repository root', '.').option('--json')
178
+ .action((opts) => {
179
+ const observation = readBoundedJson(opts.observationFile);
180
+ if (typeof observation !== 'object' || observation === null || Array.isArray(observation))
181
+ throw new Error('routing-observe requires an observation object');
182
+ const observed = observation.observed;
183
+ const unavailableReason = observation.unavailableReason;
184
+ if ((observed !== undefined && !(0, types_1.isRoutingSelection)(observed)) || (unavailableReason !== undefined && typeof unavailableReason !== 'string'))
185
+ throw new Error('routing-observe requires a valid observation');
186
+ const repo = path_1.default.resolve(opts.cwd);
187
+ const branch = branchOf(repo);
188
+ assertAuthenticatedCwd(repo, branch);
189
+ const emitted = (0, requests_1.emitRequest)(repo, branch, { kind: 'routing-observe', generationToken: opts.generation, idempotencyKey: crypto_1.default.createHash('sha256').update(`routing-observe:${opts.generation}:${opts.attempt}:${opts.nativeAgentId}:${JSON.stringify(observation)}`).digest('hex'), payload: { attemptId: opts.attempt, nativeAgentId: opts.nativeAgentId, ...(observed === undefined ? {} : { observed }), ...(unavailableReason === undefined ? {} : { unavailableReason }) } });
190
+ process.stdout.write(JSON.stringify({ requestId: emitted.requestId }) + '\n');
191
+ });
146
192
  job.command('ps').action(() => {
147
193
  const repo = process.cwd();
148
194
  const branch = branchOf(repo);
@@ -216,6 +262,21 @@ function registerJobCommand(program) {
216
262
  if (!g.pass)
217
263
  process.exit(1); // falla cerrado (R3.2)
218
264
  });
265
+ job.command('routing-report')
266
+ .description('informe de routing read-only; nunca expone envelopes ni identidad nativa')
267
+ .option('--json', 'emit the routing report as JSON')
268
+ .action(() => {
269
+ const repo = process.cwd();
270
+ const branch = branchOf(repo);
271
+ assertAuthenticatedCwd(repo, branch);
272
+ const r = (0, store_1.readJournal)(repo, branch);
273
+ if (r.corrupt || r.state === null) {
274
+ process.stdout.write(JSON.stringify({ corruptState: true }) + '\n');
275
+ process.exit(1);
276
+ return;
277
+ }
278
+ process.stdout.write(JSON.stringify((0, journal_1.routingReport)(r.state), null, 2) + '\n');
279
+ });
219
280
  job.command('reap')
220
281
  .description('lista procesos de jobs (identidad completa); --execute --jobs <ids...> para terminar confirmando')
221
282
  .option('--execute', 'ejecutar la terminacion de los jobs listados en --jobs')
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerModelPolicyCommand = registerModelPolicyCommand;
4
+ const crypto_1 = require("crypto");
5
+ const providers_1 = require("../../providers");
6
+ const store_1 = require("../../core/model-policy/store");
7
+ const validate_1 = require("../../core/model-policy/validate");
8
+ const capabilities_1 = require("../../core/model-policy/capabilities");
9
+ const digest_1 = require("../../core/model-policy/digest");
10
+ const roles = ['implementer', 'specification-reviewer', 'code-quality-reviewer', 'final-reviewer', 'architecture', 'track-a-qa', 'track-b-qa', 'controller', 'documentation', 'retro', 'finishing'];
11
+ const implementerProfiles = ['mechanical', 'integration', 'judgment'];
12
+ const contract = { schema: 'routing-protocol/v1', supportedPlanSchemas: ['compact-slices/v1', 'compact-slices/v2'], roles, implementerProfiles, limits: { maxFileBytes: validate_1.MAX_POLICY_BYTES, maxMappings: validate_1.MAX_MAPPINGS, maxIdentifierChars: 128, maxDiagnostics: 20, maxDiagnosticChars: 4096 } };
13
+ const protocolDigest = (0, crypto_1.createHash)('sha256').update(JSON.stringify(contract), 'utf8').digest('hex');
14
+ function text(value, label) { if (typeof value !== 'string' || value.length === 0 || value.startsWith('--') || value.length > 4096 || /[\u0000-\u001f\u007f-\u009f]/.test(value))
15
+ throw new Error(`${label} requires a non-empty value without control characters`); }
16
+ function output(value, json) { process.stdout.write(json ? `${JSON.stringify(value)}\n` : `${JSON.stringify(value, null, 2)}\n`); }
17
+ function registerModelPolicyCommand(program, deps = {}) {
18
+ if (!program || typeof program.command !== 'function')
19
+ throw new Error('program must be a Commander command');
20
+ const approve = deps.approvePolicy ?? store_1.approvePolicy;
21
+ const read = deps.readEffectivePolicy ?? store_1.readEffectivePolicy;
22
+ const approveReceipt = deps.approveCapabilities ?? capabilities_1.approveCapabilities;
23
+ const readReceipt = deps.readCapabilities ?? capabilities_1.readCapabilities;
24
+ const digest = deps.candidateDigest ?? digest_1.candidateDigest;
25
+ const command = program.command('model-policy').description('inspect and explicitly approve portable routing policy');
26
+ command.command('contract').option('--cwd <path>').option('--json').action((options) => { if (options.cwd !== undefined)
27
+ text(options.cwd, '--cwd'); output({ ...contract, protocolDigest }, options.json === true); });
28
+ // Approval demands --expected-digest so that it stays an explicit operator
29
+ // act. Without a way to observe the digest first, that safeguard blocked the
30
+ // legitimate path instead of gating it, and the only way through was to
31
+ // reimplement canonicalisation outside the CLI — the second parser the
32
+ // admission contract forbids. This discloses the digest and approves nothing.
33
+ command.command('digest').description('compute the canonical digest of a candidate policy or capability receipt without approving it').requiredOption('--file <path>').option('--cwd <path>').option('--json').action((options) => {
34
+ text(options.file, '--file');
35
+ if (options.cwd !== undefined)
36
+ text(options.cwd, '--cwd');
37
+ output(digest({ file: options.file, cwd: options.cwd ?? process.cwd() }), options.json === true);
38
+ });
39
+ command.command('approve').requiredOption('--file <path>').requiredOption('--scope <scope>').requiredOption('--expected-digest <sha>').option('--replace-digest <sha>').option('--cwd <path>').option('--json').action((options) => {
40
+ text(options.file, '--file');
41
+ text(options.scope, '--scope');
42
+ text(options.expectedDigest, '--expected-digest');
43
+ if (options.cwd !== undefined)
44
+ text(options.cwd, '--cwd');
45
+ if (options.replaceDigest !== undefined)
46
+ text(options.replaceDigest, '--replace-digest');
47
+ (0, validate_1.assertDigest)(options.expectedDigest, 'expectedDigest');
48
+ if (options.replaceDigest !== undefined)
49
+ (0, validate_1.assertDigest)(options.replaceDigest, 'replaceDigest');
50
+ const policy = approve({ file: options.file, scope: options.scope, cwd: options.cwd ?? process.cwd(), expectedDigest: options.expectedDigest, replaceDigest: options.replaceDigest });
51
+ output({ state: 'approved', policy }, options.json === true);
52
+ });
53
+ // `command('capabilities approve')` registered ONE command named
54
+ // `capabilities` with a required positional `<approve>`, so Commander called
55
+ // the action as (positional, options, command) and the handler's first
56
+ // parameter — read as `options` — was the string 'approve'. Every invocation
57
+ // therefore died on `--file requires a non-empty value`, and the path could
58
+ // never approve anything. A real subcommand group keeps the invocation
59
+ // spelling identical and gives the action the options object it expects.
60
+ const capabilities = command.command('capabilities').description('inspect and explicitly approve runtime capability attestations');
61
+ capabilities.command('approve').requiredOption('--file <path>').requiredOption('--expected-digest <sha>').option('--replace-digest <sha>').option('--cwd <path>').option('--json').action((options) => {
62
+ text(options.file, '--file');
63
+ text(options.expectedDigest, '--expected-digest');
64
+ if (options.replaceDigest !== undefined)
65
+ text(options.replaceDigest, '--replace-digest');
66
+ if (options.cwd !== undefined)
67
+ text(options.cwd, '--cwd');
68
+ (0, validate_1.assertDigest)(options.expectedDigest, 'expectedDigest');
69
+ if (options.replaceDigest)
70
+ (0, validate_1.assertDigest)(options.replaceDigest, 'replaceDigest');
71
+ output({ state: 'approved', receipt: approveReceipt({ file: options.file, cwd: options.cwd ?? process.cwd(), expectedDigest: options.expectedDigest, replaceDigest: options.replaceDigest }) }, options.json === true);
72
+ });
73
+ command.command('status').requiredOption('--provider <target>').requiredOption('--runtime-kind <kind>').requiredOption('--runtime-version <version>').requiredOption('--account-scope-digest <sha>').option('--cwd <path>').option('--json').action((options) => {
74
+ text(options.provider, '--provider');
75
+ text(options.runtimeKind, '--runtime-kind');
76
+ text(options.runtimeVersion, '--runtime-version');
77
+ text(options.accountScopeDigest, '--account-scope-digest');
78
+ if (options.cwd !== undefined)
79
+ text(options.cwd, '--cwd');
80
+ if (!providers_1.AGENT_TARGETS.includes(options.provider))
81
+ throw new Error('--provider is invalid');
82
+ if (!/^[a-z0-9][a-z0-9_-]{0,127}$/.test(options.runtimeKind))
83
+ throw new Error('--runtime-kind is invalid');
84
+ if (!/^\d+\.\d+\.\d+$/.test(options.runtimeVersion))
85
+ throw new Error('--runtime-version must be numeric semver');
86
+ (0, validate_1.assertDigest)(options.accountScopeDigest, 'accountScopeDigest');
87
+ const cwd = options.cwd ?? process.cwd();
88
+ const effective = read(cwd);
89
+ const capability = readReceipt({ target: options.provider, kind: options.runtimeKind, version: options.runtimeVersion, accountScopeDigest: options.accountScopeDigest }, new Date());
90
+ output({ policy: effective, capability }, options.json === true);
91
+ if (effective.state !== 'approved' || capability.state !== 'current')
92
+ process.exitCode = 2;
93
+ });
94
+ }
@@ -18,7 +18,11 @@ const path_1 = __importDefault(require("path"));
18
18
  const child_process_1 = require("child_process");
19
19
  const store_1 = require("../../core/journal/store");
20
20
  const migration_1 = require("../../core/migration");
21
- const SUPPORTED_SCHEMA = 'compact-slices/v1';
21
+ const store_2 = require("../../core/model-policy/store");
22
+ const capabilities_1 = require("../../core/model-policy/capabilities");
23
+ const resolve_1 = require("../../core/model-policy/resolve");
24
+ const journal_1 = require("../../core/model-policy/journal");
25
+ const SUPPORTED_SCHEMA = 'compact-slices/v1, compact-slices/v2';
22
26
  const MAX_PATH_LENGTH = 4096;
23
27
  const MAX_DIAGNOSTICS = 20;
24
28
  const MAX_DIAGNOSTIC_LENGTH = 4096;
@@ -140,6 +144,53 @@ function registerPlanCommand(program, deps = { validatePlanFile: validate_1.vali
140
144
  throw new Error('program must be a Commander command');
141
145
  assertDependencies(deps);
142
146
  const plan = program.command('plan').description('inspect plan contracts');
147
+ plan.command('resolve <plan-path>').requiredOption('--provider <target>').requiredOption('--runtime-kind <kind>').requiredOption('--runtime-version <version>').requiredOption('--account-scope-digest <sha>').requiredOption('--role <role>').option('--opt-in-v1').option('--slice <id>').option('--lineage <id>').option('--cwd <path>').option('--json').action((planPath, options) => {
148
+ assertText(planPath, 'plan path');
149
+ for (const [value, label] of [[options.provider, '--provider'], [options.runtimeKind, '--runtime-kind'], [options.runtimeVersion, '--runtime-version'], [options.accountScopeDigest, '--account-scope-digest'], [options.role, '--role']])
150
+ assertText(value, label);
151
+ if (options.slice)
152
+ assertText(options.slice, '--slice');
153
+ if (options.lineage)
154
+ assertText(options.lineage, '--lineage');
155
+ const cwd = options.cwd ?? process.cwd();
156
+ assertText(cwd, '--cwd');
157
+ const report = deps.validatePlanFile(planPath, cwd);
158
+ assertReport(report);
159
+ if (report.state !== 'valid') {
160
+ process.stdout.write(options.json ? `${JSON.stringify({ state: 'blocked', diagnostics: report.state === 'invalid' ? report.diagnostics : [{ code: 'ROUTING_PLAN_INVALID', message: 'Plan must validate before resolution.' }] })}\n` : 'Plan routing: blocked\n');
161
+ process.exitCode = 2;
162
+ return;
163
+ }
164
+ if (options.lineage) {
165
+ if (report.schema !== 'compact-slices/v2') {
166
+ process.stdout.write(options.json ? `${JSON.stringify({ state: 'blocked', diagnostics: [{ code: 'ROUTING_LINEAGE_V1', message: 'Lineage routing is only available for compact v2 plans.' }] })}\n` : 'Plan routing: blocked\n');
167
+ process.exitCode = 2;
168
+ return;
169
+ }
170
+ const observed = journalObservation(cwd);
171
+ const binding = observed.journalState?.planBinding;
172
+ const attempts = observed.journalState?.routingAttempts?.filter((attempt) => attempt.lineageId === options.lineage) ?? [];
173
+ const lineage = observed.journalState?.implementationLineages?.find((candidate) => candidate.id === options.lineage);
174
+ const bound = !observed.journalCorrupt && binding?.digest === report.planDigest && binding.executionDigest === report.executionDigest;
175
+ const lineageMatches = lineage !== undefined && lineage.sliceId === options.slice && lineage.planDigest === report.planDigest && lineage.executionDigest === report.executionDigest && attempts.every((attempt) => attempt.obligationId === lineage.obligationId && attempt.envelope.sliceId === options.slice);
176
+ if (!bound || !lineageMatches || attempts.length > 3 || attempts.some((attempt) => attempt.envelope.planDigest !== report.planDigest || attempt.envelope.executionDigest !== report.executionDigest || ['unknown', 'reserved', 'active'].includes(attempt.state))) {
177
+ process.stdout.write(options.json ? `${JSON.stringify({ state: 'blocked', diagnostics: [{ code: 'ROUTING_LINEAGE_UNBOUND', message: 'Lineage routing requires a current journal binding and non-unknown matching attempts.' }] })}\n` : 'Plan routing: blocked\n');
178
+ process.exitCode = 2;
179
+ return;
180
+ }
181
+ }
182
+ const roles = ['implementer', 'specification-reviewer', 'code-quality-reviewer', 'final-reviewer', 'architecture', 'track-a-qa', 'track-b-qa', 'controller', 'documentation', 'retro', 'finishing'];
183
+ if (!roles.includes(options.role))
184
+ throw new Error('--role is invalid');
185
+ const runtime = (0, capabilities_1.validateRuntimeKey)({ target: options.provider, kind: options.runtimeKind, version: options.runtimeVersion, accountScopeDigest: options.accountScopeDigest });
186
+ const result = report.schema === 'compact-slices/v1' && !options.optInV1 ? (0, resolve_1.resolveDispatch)({ plan: report, role: options.role, policy: undefined, capabilities: undefined, runtime, now: new Date(), optInV1: false }) : (() => { const policy = (deps.readEffectivePolicy ?? store_2.readEffectivePolicy)(cwd); const capabilities = (deps.readCapabilities ?? capabilities_1.readCapabilities)(runtime, new Date()); if (!options.lineage)
187
+ return (0, resolve_1.resolveDispatch)({ plan: report, role: options.role, sliceId: options.slice, policy: policy.state === 'approved' ? policy.policy : undefined, capabilities: capabilities.state === 'current' ? capabilities.receipt : undefined, runtime, now: new Date(), optInV1: options.optInV1 === true }); const state = journalObservation(cwd).journalState; const lineage = state.implementationLineages.find(candidate => candidate.id === options.lineage); const next = (0, journal_1.resolveLineageEscalation)(state, options.lineage, lineage.initialProfile); const resolved = (0, resolve_1.resolveSelection)({ role: 'implementer', requestedProfile: next.profile, policy: policy.state === 'approved' ? policy.policy : undefined, capabilities: capabilities.state === 'current' ? capabilities.receipt : undefined, runtime, now: new Date() }); if (resolved.state === 'blocked')
188
+ return resolved; const highRequiresFull = next.effort !== 'high' || (policy.state === 'approved' && (() => { const full = policy.policy.content.mappings.find(row => row.target === runtime.target && row.runtimeKind === runtime.kind)?.fullCapability; return full?.selector.kind === resolved.selection.selector.kind && full.selector.id === resolved.selection.selector.id && full.effort.kind === 'explicit' && full.effort.value === 'high'; })()); if (resolved.selection.effort.kind !== 'explicit' || resolved.selection.effort.value !== next.effort || !highRequiresFull)
189
+ return { state: 'blocked', diagnostics: [{ code: 'ROUTING_ESCALATION_EFFORT_UNAVAILABLE', message: 'The approved selected effort does not match the next lineage escalation.' }] }; return { ...resolved, envelope: { schema: 'routing-envelope/v1', runtime, role: 'implementer', planDigest: report.planDigest, executionDigest: report.executionDigest, sliceId: lineage.sliceId, requestedProfile: lineage.initialProfile, effectiveProfile: next.profile, resolved: resolved.selection, policyDigest: resolved.policyDigest, capabilityDigest: resolved.capabilityDigest, outcome: resolved.outcome, unavailableEvidence: resolved.unavailableEvidence }, custodyHandoff: { kind: 'routing-reserve', obligationId: lineage.obligationId, lineageId: lineage.id } }; })();
190
+ process.stdout.write(options.json ? `${JSON.stringify(result)}\n` : `Plan routing: ${result.state}\n`);
191
+ if (result.state === 'blocked')
192
+ process.exitCode = 2;
193
+ });
143
194
  plan.command('migration-facts <plan-path>')
144
195
  .description('collect read-only durable migration facts')
145
196
  .option('--cwd <path>')
@@ -179,6 +230,9 @@ function registerPlanCommand(program, deps = { validatePlanFile: validate_1.vali
179
230
  .option('--execution-mode <mode>', 'explicit override: interactivo or desatendido')
180
231
  .option('--require-current', 'require authoritative consumed-contract currentness')
181
232
  .option('--verify-sensors', 'require an empirical sensor pass')
233
+ .option('--runtime-kind <kind>', 'routing runtime kind for compact v2')
234
+ .option('--runtime-version <version>', 'routing runtime version for compact v2')
235
+ .option('--account-scope-digest <sha>', 'routing account scope digest for compact v2')
182
236
  .option('--json', 'emit one stable JSON report')
183
237
  .action(async (planPath, options) => {
184
238
  assertText(planPath, 'plan path');
@@ -208,7 +262,10 @@ function registerPlanCommand(program, deps = { validatePlanFile: validate_1.vali
208
262
  process.exitCode = 2;
209
263
  return;
210
264
  }
211
- const report = await (0, registry_contracts_1.admitRegistryPlan)({ plan: planReport, provider: options.provider, cwd: options.cwd, enabledAgents, executionMode, planPath: normalizedPlanPath, ...journal, requireCurrent: options.requireCurrent === true, verifySensors: options.verifySensors === true }, { admitPlan: admission, listRegistries: registryInventory, checkCurrentness: currentnessCheck, runSensors: sensorRun });
265
+ const readRouting = () => { if (!options.runtimeKind || !options.runtimeVersion || !options.accountScopeDigest)
266
+ return undefined; const runtime = (0, capabilities_1.validateRuntimeKey)({ target: options.provider, kind: options.runtimeKind, version: options.runtimeVersion, accountScopeDigest: options.accountScopeDigest }); const policy = (deps.readEffectivePolicy ?? store_2.readEffectivePolicy)(options.cwd); const at = deps.routingNow?.() ?? new Date(); if (!(at instanceof Date) || !Number.isFinite(at.getTime()))
267
+ throw new Error('routingNow must return a finite Date'); const capabilities = (deps.readCapabilities ?? capabilities_1.readCapabilities)(runtime, at); return { runtime, policy: policy.state === 'approved' ? policy.policy : undefined, capabilities: capabilities.state === 'current' ? capabilities.receipt : undefined, now: at }; };
268
+ const report = await (0, registry_contracts_1.admitRegistryPlan)({ plan: planReport, provider: options.provider, cwd: options.cwd, enabledAgents, executionMode, planPath: normalizedPlanPath, ...journal, requireCurrent: options.requireCurrent === true, verifySensors: options.verifySensors === true }, { admitPlan: admission, listRegistries: registryInventory, checkCurrentness: currentnessCheck, runSensors: sensorRun, readRouting });
212
269
  process.stdout.write(admissionOutput(report, options.json === true));
213
270
  if (report.state !== 'admitted')
214
271
  process.exitCode = 2;
@@ -18,6 +18,8 @@ const paths_1 = require("../../core/journal/paths");
18
18
  const atomic_file_1 = require("../../core/atomic-file");
19
19
  const redact_1 = require("../../core/journal/redact");
20
20
  const git_1 = require("../../core/tracks/git");
21
+ const types_1 = require("../../core/journal/types");
22
+ const journal_1 = require("../../core/model-policy/journal");
21
23
  function now() { return new Date().toISOString(); }
22
24
  const VERIFICATION_KINDS = ['test', 'lint', 'sensors', 'review', 'qa', 'interlock', 'track-integration'];
23
25
  function verificationItems(value, field) {
@@ -110,6 +112,24 @@ function applyRequestToState(s, env, digest, repoRoot) {
110
112
  (0, requests_1.applyOutcome)(s, { ...base, outcome: 'applied' });
111
113
  return;
112
114
  }
115
+ if (env.kind === 'routing-reserve') {
116
+ const p = env.payload;
117
+ if (typeof p.obligationId !== 'string' || typeof p.lineageId !== 'string' || !(0, types_1.isRoutingEnvelope)(p.envelope) || typeof p.fingerprint !== 'string' || (p.at !== undefined && typeof p.at !== 'string'))
118
+ throw new Error('routing-reserve requiere payload estricto');
119
+ const reserved = (0, journal_1.reserveRoutingAttempt)(s, { obligationId: p.obligationId, lineageId: p.lineageId, envelope: p.envelope, fingerprint: p.fingerprint }, p.at ?? now());
120
+ Object.assign(s, reserved.state);
121
+ (0, requests_1.applyOutcome)(s, { ...base, outcome: 'applied', resultRef: reserved.attemptId });
122
+ return;
123
+ }
124
+ if (env.kind === 'routing-observe') {
125
+ const p = env.payload;
126
+ if (typeof p.attemptId !== 'string' || typeof p.nativeAgentId !== 'string' || (p.observed !== undefined && !(0, types_1.isRoutingSelection)(p.observed)) || (p.unavailableReason !== undefined && typeof p.unavailableReason !== 'string') || (p.at !== undefined && typeof p.at !== 'string'))
127
+ throw new Error('routing-observe requiere payload estricto');
128
+ const observed = (0, journal_1.observeRoutingAttempt)(s, { attemptId: p.attemptId, nativeAgentId: p.nativeAgentId, ...(p.observed === undefined ? {} : { observed: p.observed }), ...(p.unavailableReason === undefined ? {} : { unavailableReason: p.unavailableReason }) }, p.at ?? now());
129
+ Object.assign(s, observed);
130
+ (0, requests_1.applyOutcome)(s, { ...base, outcome: 'applied', resultRef: p.attemptId });
131
+ return;
132
+ }
113
133
  if (env.kind === 'job-request') {
114
134
  // get-or-create por idempotencyKey (RNF-T.7); duplicado => applyOutcome
115
135
  // registra el ALIAS con el mismo resultRef (Task 8).
@@ -449,11 +469,23 @@ function applyRequestToState(s, env, digest, repoRoot) {
449
469
  }
450
470
  if (!s.verdicts.some((v) => v.id === verdictId)) {
451
471
  const result = p.result;
472
+ if (p.routingAttemptId !== undefined) {
473
+ if (typeof p.routingAttemptId !== 'string')
474
+ throw new Error('verdict routed requiere routingAttemptId valido');
475
+ const attempt = s.routingAttempts?.find(candidate => candidate.id === p.routingAttemptId);
476
+ if (!attempt || attempt.obligationId !== obligationId || attempt.state !== 'active')
477
+ throw new Error('verdict routed requiere intento activo ligado a la obligacion');
478
+ attempt.state = result === 'pass' ? 'complete' : 'blocked';
479
+ attempt.verdict = result;
480
+ if (typeof p.reasonCode === 'string' && p.reasonCode.length > 0 && p.reasonCode.length <= 128)
481
+ attempt.reasonCode = p.reasonCode;
482
+ }
452
483
  // R2.3: redaccion tambien en el `detail` de texto libre humano, no
453
484
  // solo en argv — antes de cualquier escritura durable.
454
485
  s.verdicts.push({
455
486
  id: verdictId, obligationId, result, detail: (0, redact_1.redactText)(String(p.detail ?? '')), receivedAt: now(),
456
487
  fingerprint: p.fingerprint, argv: verdictArgv, paths: verdictPaths, cwd: p.cwd,
488
+ ...(typeof p.routingAttemptId === 'string' ? { routingAttemptId: p.routingAttemptId } : {}),
457
489
  });
458
490
  for (const t of s.tasks) {
459
491
  const o = t.reviewObligations.find((x) => x.id === obligationId);
@@ -5,6 +5,8 @@ exports.sanitizeAdmissionReport = sanitizeAdmissionReport;
5
5
  exports.admitPlan = admitPlan;
6
6
  const providers_1 = require("../../providers");
7
7
  const paths_1 = require("../journal/paths");
8
+ const resolve_1 = require("../model-policy/resolve");
9
+ const capabilities_1 = require("../model-policy/capabilities");
8
10
  const UNKNOWN = {
9
11
  artifactDelivery: 'unverified', interactiveExecution: 'unverified', unattendedController: 'unverified', nativeSubagents: 'unverified',
10
12
  modelOverride: 'unverified', effortOverride: 'unverified', observedModelEvidence: 'unverified', durableResume: 'unverified',
@@ -31,6 +33,17 @@ function forecast(slices) {
31
33
  const roles = { implementer: slices, 'specification-reviewer': slices, 'code-quality-reviewer': slices, 'final-reviewer': 1, 'track-a-qa': 1, 'track-b-qa': 1, documentation: 1, retro: 1, finishing: 1 };
32
34
  return { kind: 'topology', slices, roles, total: Object.values(roles).reduce((sum, count) => sum + count, 0) };
33
35
  }
36
+ function routingForecast(plan, routing) {
37
+ const profiles = { mechanical: 0, integration: 0, judgment: 0 };
38
+ for (const slice of plan.manifest.slices)
39
+ profiles[slice.implementerProfile] += 1;
40
+ const qa = routing.qaLens;
41
+ if (qa !== undefined && (!Array.isArray(qa) || qa.length > 64 || new Set(qa).size !== qa.length || qa.some(id => typeof id !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/.test(id))))
42
+ throw new Error('routing qaLens must be unique validated lens ids');
43
+ if (routing.controllerCount !== undefined && (!Number.isSafeInteger(routing.controllerCount) || routing.controllerCount < 0 || routing.controllerCount > 1))
44
+ throw new Error('routing controllerCount must be 0 or 1');
45
+ return { kind: 'routing-v1', slices: plan.manifest.slices.length, implementerProfiles: profiles, roles: { 'specification-reviewer': plan.manifest.slices.length, 'code-quality-reviewer': plan.manifest.slices.length, 'final-reviewer': 1, 'track-a-qa': 1, documentation: 1, retro: 1, finishing: 1 }, trackB: qa === undefined ? { state: 'unavailable', lowerBound: 1 } : { state: 'known', count: qa.length }, controller: routing.controllerCount === undefined ? { state: 'unavailable', lowerBound: 0 } : { state: 'known', count: routing.controllerCount } };
46
+ }
34
47
  function currentness(report, consumedRegistryComponents) {
35
48
  const consumed = new Set(['cli', ...consumedRegistryComponents]);
36
49
  const present = new Set(report.components.map(component => component.component));
@@ -89,6 +102,18 @@ function validForecast(value) {
89
102
  && roles.every(role => record.roles[role] === topology[role])
90
103
  && record.total === Object.values(topology).reduce((sum, count) => sum + count, 0);
91
104
  }
105
+ function validRoutingForecast(value) {
106
+ if (!value || typeof value !== 'object' || Array.isArray(value))
107
+ return false;
108
+ const record = value;
109
+ const profiles = record.implementerProfiles;
110
+ const roles = record.roles;
111
+ const count = (item) => Number.isSafeInteger(item) && item >= 0;
112
+ const roleKeys = ['specification-reviewer', 'code-quality-reviewer', 'final-reviewer', 'track-a-qa', 'documentation', 'retro', 'finishing'];
113
+ return record.kind === 'routing-v1' && Object.keys(record).length === 6 && count(record.slices) && !!profiles && Object.keys(profiles).length === 3 && ['mechanical', 'integration', 'judgment'].every(key => count(profiles[key])) && Object.values(profiles).reduce((sum, profile) => sum + profile, 0) === record.slices && !!roles && typeof roles === 'object' && Object.keys(roles).length === roleKeys.length && roleKeys.every(key => key in roles) && roles['specification-reviewer'] === record.slices && roles['code-quality-reviewer'] === record.slices && ['final-reviewer', 'track-a-qa', 'documentation', 'retro', 'finishing'].every(key => roles[key] === 1)
114
+ && !!record.trackB && typeof record.trackB === 'object' && ((record.trackB.state === 'known' && count(record.trackB.count)) || (record.trackB.state === 'unavailable' && record.trackB.lowerBound === 1))
115
+ && !!record.controller && typeof record.controller === 'object' && ((record.controller.state === 'known' && count(record.controller.count)) || (record.controller.state === 'unavailable' && record.controller.lowerBound === 0));
116
+ }
92
117
  function validDiagnostics(value) {
93
118
  return Array.isArray(value) && value.every(item => item && typeof item === 'object' && typeof item.code === 'string' && typeof item.message === 'string' && (item.field === undefined || typeof item.field === 'string'));
94
119
  }
@@ -112,6 +137,8 @@ function sanitizeAdmissionReport(report) {
112
137
  }
113
138
  if (value.forecast !== undefined && !validForecast(value.forecast))
114
139
  invalidAdmission();
140
+ if (value.routingForecast !== undefined && !validRoutingForecast(value.routingForecast))
141
+ invalidAdmission();
115
142
  const capabilityResolution = value.capabilityResolution === undefined ? undefined : { ...value.capabilityResolution, diagnostics: sanitizeDiagnostics(value.capabilityResolution.diagnostics) };
116
143
  return { ...value, ...(capabilityResolution ? { capabilityResolution } : {}), diagnostics: sanitizeDiagnostics(value.diagnostics) };
117
144
  }
@@ -167,13 +194,36 @@ async function admitPlan(input) {
167
194
  const resolution = { outcome: capabilities.unattendedController === 'supported' ? 'native' : 'blocked', provider, capabilities, evidenceVersion: 'r1-v1', diagnostics: [] };
168
195
  if (resolution.outcome === 'blocked')
169
196
  return blocked(input, [diagnostic('ADMISSION_CAPABILITY_UNVERIFIED', `Provider ${provider} has no verified unattended execution capability.`)], { planDigest: plan.planDigest, provider, executionMode: mode, journal, currentness: input.requireCurrent ? 'current' : 'not-checked', sensors: input.verifySensors ? 'pass' : 'not-required', capabilityResolution: resolution });
170
- return { state: 'admitted', planState: 'valid', planDigest: plan.planDigest, provider, executionMode: mode, journal, currentness: input.requireCurrent ? 'current' : 'not-checked', sensors: input.verifySensors ? 'pass' : 'not-required', capabilityResolution: resolution, forecast: forecast(plan.manifest.slices.length), diagnostics: [] };
197
+ return completeAdmission(input, plan, provider, mode, journal, input.requireCurrent ? 'current' : 'not-checked', input.verifySensors ? 'pass' : 'not-required', resolution);
171
198
  }
172
199
  const capabilities = exports.PROVIDER_EXECUTION_CAPABILITIES[provider];
173
200
  const resolution = { outcome: capabilities.interactiveExecution === 'supported' ? 'native' : 'blocked', provider, capabilities, evidenceVersion: 'r1-v1', diagnostics: [] };
174
201
  if (resolution.outcome === 'blocked')
175
202
  return blocked(input, [diagnostic('ADMISSION_CAPABILITY_UNVERIFIED', `Provider ${provider} has no verified interactive execution capability.`)], { planDigest: plan.planDigest, provider, executionMode: mode, capabilityResolution: resolution });
176
- return { state: 'admitted', planState: 'valid', planDigest: plan.planDigest, provider, executionMode: mode, journal: 'not-required', currentness: input.requireCurrent ? 'current' : 'not-checked', sensors: input.verifySensors ? 'pass' : 'not-required', capabilityResolution: resolution, forecast: forecast(plan.manifest.slices.length), diagnostics: [] };
203
+ return completeAdmission(input, plan, provider, mode, 'not-required', input.requireCurrent ? 'current' : 'not-checked', input.verifySensors ? 'pass' : 'not-required', resolution);
204
+ }
205
+ function completeAdmission(input, plan, provider, executionMode, journal, currentness, sensors, capabilityResolution) {
206
+ if (plan.schema === 'compact-slices/v1')
207
+ return { state: 'admitted', planState: 'valid', planDigest: plan.planDigest, provider, executionMode, journal, currentness, sensors, capabilityResolution, forecast: forecast(plan.manifest.slices.length), diagnostics: [] };
208
+ const routing = input.routing ?? input.routingReader?.();
209
+ if (!routing?.runtime || !routing.policy || !routing.capabilities)
210
+ return blocked(input, [diagnostic('ADMISSION_ROUTING_FACTS_REQUIRED', 'Compact v2 requires an approved policy, current capability receipt, and runtime identity.')], { planDigest: plan.planDigest, provider, executionMode, journal, currentness, sensors, capabilityResolution });
211
+ let runtime;
212
+ try {
213
+ runtime = (0, capabilities_1.validateRuntimeKey)(routing.runtime);
214
+ }
215
+ catch {
216
+ return blocked(input, [diagnostic('ADMISSION_ROUTING_RUNTIME_INVALID', 'Compact v2 routing runtime identity is invalid.')], { planDigest: plan.planDigest, provider, executionMode, journal, currentness, sensors, capabilityResolution });
217
+ }
218
+ if (runtime.target !== provider)
219
+ return blocked(input, [diagnostic('ADMISSION_ROUTING_PROVIDER_MISMATCH', 'Compact v2 routing runtime target must exactly match the admitted provider.')], { planDigest: plan.planDigest, provider, executionMode, journal, currentness, sensors, capabilityResolution });
220
+ const now = routing.now ?? new Date();
221
+ for (const slice of plan.manifest.slices) {
222
+ const resolved = (0, resolve_1.resolveSelection)({ role: 'implementer', requestedProfile: slice.implementerProfile, policy: routing.policy, capabilities: routing.capabilities, runtime, now });
223
+ if (resolved.state === 'blocked')
224
+ return blocked(input, resolved.diagnostics, { planDigest: plan.planDigest, provider, executionMode, journal, currentness, sensors, capabilityResolution });
225
+ }
226
+ return { state: 'admitted', planState: 'valid', planDigest: plan.planDigest, provider, executionMode, journal, currentness, sensors, capabilityResolution, forecast: forecast(plan.manifest.slices.length), routingForecast: routingForecast(plan, routing), diagnostics: [] };
177
227
  }
178
228
  // Structural exhaustiveness guard: adding an AgentTarget requires an explicit entry above.
179
229
  const capabilityTargets = providers_1.AGENT_TARGETS;
@@ -286,5 +286,5 @@ async function admitRegistryPlan(input, dependencies = {}) {
286
286
  return currentnessGate;
287
287
  compatibilityDiagnostics = registryCompatibility(registries, scope.consumedRegistryComponents);
288
288
  }
289
- return compose({ currentness, compatibilityDiagnostics, sensors });
289
+ return compose({ currentness, compatibilityDiagnostics, sensors, ...(dependencies.readRouting ? { routingReader: dependencies.readRouting } : {}) });
290
290
  }
@@ -68,7 +68,7 @@ function emitRequest(repoRoot, branch, env) {
68
68
  const KNOWN_KINDS = [
69
69
  'job-request', 'register-entity', 'controller-heartbeat', 'verdict',
70
70
  'track-prepare-request', 'track-freeze-request', 'track-join-request',
71
- 'track-teardown-request', 'track-finalize-request',
71
+ 'track-teardown-request', 'track-finalize-request', 'routing-reserve', 'routing-observe',
72
72
  ];
73
73
  function isRecord(x) {
74
74
  return typeof x === 'object' && x !== null && !Array.isArray(x);
@@ -9,6 +9,8 @@ exports.GENERATION_STATES = exports.EXECUTION_STATES = void 0;
9
9
  exports.isControllerRecoveryAction = isControllerRecoveryAction;
10
10
  exports.emptyState = emptyState;
11
11
  exports.isWellFormedState = isWellFormedState;
12
+ exports.isRoutingSelection = isRoutingSelection;
13
+ exports.isRoutingEnvelope = isRoutingEnvelope;
12
14
  exports.isWellFormedProcessRef = isWellFormedProcessRef;
13
15
  exports.isWellFormedJob = isWellFormedJob;
14
16
  const crypto_1 = __importDefault(require("crypto"));
@@ -39,6 +41,8 @@ function isControllerRecoveryAction(x) {
39
41
  && typeof x.kind === 'string'
40
42
  && ['resume-cycle', 'resume-next-action', 'reconcile-active-jobs'].includes(x.kind);
41
43
  }
44
+ const ROUTING_ROLES = ['implementer', 'specification-reviewer', 'code-quality-reviewer', 'final-reviewer', 'architecture', 'track-a-qa', 'track-b-qa', 'controller', 'documentation', 'retro', 'finishing'];
45
+ const UNAVAILABLE_EVIDENCE = ['modelOverride', 'effortOverride', 'observedModelEvidence'];
42
46
  function emptyState(branch) {
43
47
  return {
44
48
  schema: 1, revision: 0, journalId: `j-${crypto_1.default.randomUUID()}`, branch,
@@ -88,6 +92,10 @@ function isWellFormedState(x) {
88
92
  return false;
89
93
  if (!isObj(x.appliedRequests) || !Object.values(x.appliedRequests).every(isWellFormedAppliedRequest))
90
94
  return false;
95
+ if (x.routingAttempts !== undefined && (!Array.isArray(x.routingAttempts) || x.routingAttempts.length > 4096 || !x.routingAttempts.every(isWellFormedRoutingAttempt)))
96
+ return false;
97
+ if (x.implementationLineages !== undefined && (!Array.isArray(x.implementationLineages) || x.implementationLineages.length > 4096 || !x.implementationLineages.every(isWellFormedImplementationLineage)))
98
+ return false;
91
99
  if (!Array.isArray(x.requestProblems) || !x.requestProblems.every(isWellFormedRequestProblem))
92
100
  return false;
93
101
  if (x.custodyDecisions !== undefined && (!Array.isArray(x.custodyDecisions) || !x.custodyDecisions.every(isWellFormedCustodyDecision)))
@@ -190,7 +198,8 @@ function isWellFormedVerdict(x) {
190
198
  return isObj(x) && typeof x.id === 'string' && typeof x.obligationId === 'string'
191
199
  && ['pass', 'fail', 'inconclusive'].includes(String(x.result))
192
200
  && typeof x.detail === 'string' && typeof x.receivedAt === 'string'
193
- && typeof x.fingerprint === 'string' && strings(x.argv) && strings(x.paths) && typeof x.cwd === 'string';
201
+ && typeof x.fingerprint === 'string' && strings(x.argv) && strings(x.paths) && typeof x.cwd === 'string'
202
+ && (x.routingAttemptId === undefined || typeof x.routingAttemptId === 'string');
194
203
  }
195
204
  function isWellFormedFix(x) {
196
205
  return isObj(x) && typeof x.id === 'string' && typeof x.verdictId === 'string' && typeof x.closed === 'boolean';
@@ -201,6 +210,18 @@ function isWellFormedAppliedRequest(x) {
201
210
  && ['applied', 'rejected-stale-generation', 'rejected-digest-mismatch', 'rejected-secret'].includes(String(x.outcome))
202
211
  && (x.resultRef === undefined || typeof x.resultRef === 'string');
203
212
  }
213
+ function isWellFormedRoutingAttempt(x) {
214
+ return isObj(x) && Object.keys(x).every(key => ['schema', 'id', 'obligationId', 'lineageId', 'attempt', 'envelope', 'envelopeDigest', 'fingerprint', 'state', 'nativeAgentId', 'observed', 'verdict', 'reasonCode', 'administrativeRepair'].includes(key))
215
+ && x.schema === 'routing-attempt/v1'
216
+ && typeof x.id === 'string' && typeof x.obligationId === 'string' && typeof x.lineageId === 'string' && Number.isSafeInteger(x.attempt) && x.attempt > 0
217
+ && typeof x.envelopeDigest === 'string' && /^[a-f0-9]{64}$/.test(x.envelopeDigest) && typeof x.fingerprint === 'string' && /^[a-f0-9]{64}$/.test(x.fingerprint)
218
+ && ['reserved', 'active', 'complete', 'blocked', 'unknown'].includes(String(x.state)) && (x.nativeAgentId === undefined || typeof x.nativeAgentId === 'string') && (x.observed === undefined || isRoutingSelection(x.observed)) && (x.verdict === undefined || ['pass', 'fail', 'inconclusive'].includes(String(x.verdict))) && (x.reasonCode === undefined || typeof x.reasonCode === 'string') && (x.administrativeRepair === undefined || x.administrativeRepair === true) && isRoutingEnvelope(x.envelope);
219
+ }
220
+ function isDigest(value) { return typeof value === 'string' && /^[a-f0-9]{64}$/.test(value); }
221
+ function isRoutingSelection(x) { return isObj(x) && Object.keys(x).every(key => key === 'selector' || key === 'effort') && isObj(x.selector) && Object.keys(x.selector).every(key => key === 'kind' || key === 'id') && ['model', 'tier'].includes(String(x.selector.kind)) && typeof x.selector.id === 'string' && x.selector.id.length > 0 && x.selector.id.length <= 128 && isObj(x.effort) && ((x.effort.kind === 'runtime-default' && Object.keys(x.effort).length === 1) || (x.effort.kind === 'explicit' && Object.keys(x.effort).length === 2 && typeof x.effort.value === 'string' && x.effort.value.length > 0 && x.effort.value.length <= 128)); }
222
+ function isRoutingEnvelope(x) { if (!isObj(x) || !Object.keys(x).every(key => ['schema', 'runtime', 'role', 'planDigest', 'executionDigest', 'sliceId', 'requestedProfile', 'effectiveProfile', 'resolved', 'policyDigest', 'capabilityDigest', 'outcome', 'unavailableEvidence'].includes(key)))
223
+ return false; const runtime = x.runtime; return x.schema === 'routing-envelope/v1' && isObj(runtime) && Object.keys(runtime).every(key => ['target', 'kind', 'version', 'accountScopeDigest'].includes(key)) && ['codex', 'claude-code', 'cursor', 'copilot', 'opencode', 'antigravity'].includes(String(runtime.target)) && typeof runtime.kind === 'string' && runtime.kind.length > 0 && runtime.kind.length <= 128 && typeof runtime.version === 'string' && runtime.version.length > 0 && runtime.version.length <= 128 && isDigest(runtime.accountScopeDigest) && typeof x.role === 'string' && ROUTING_ROLES.includes(x.role) && isDigest(x.planDigest) && isDigest(x.executionDigest) && (x.sliceId === undefined || (typeof x.sliceId === 'string' && x.sliceId.length > 0 && x.sliceId.length <= 128)) && ['mechanical', 'integration', 'judgment', 'full'].includes(String(x.requestedProfile)) && ['mechanical', 'integration', 'judgment', 'full'].includes(String(x.effectiveProfile)) && isRoutingSelection(x.resolved) && isDigest(x.policyDigest) && isDigest(x.capabilityDigest) && ['native', 'degraded'].includes(String(x.outcome)) && Array.isArray(x.unavailableEvidence) && x.unavailableEvidence.length <= UNAVAILABLE_EVIDENCE.length && x.unavailableEvidence.every(value => typeof value === 'string' && UNAVAILABLE_EVIDENCE.includes(value)); }
224
+ function isWellFormedImplementationLineage(x) { return isObj(x) && Object.keys(x).every(key => ['id', 'obligationId', 'sliceId', 'planDigest', 'executionDigest', 'initialProfile', 'initialEffort', 'attempts'].includes(key)) && typeof x.id === 'string' && x.id.length > 0 && typeof x.obligationId === 'string' && x.obligationId.length > 0 && typeof x.sliceId === 'string' && x.sliceId.length > 0 && isDigest(x.planDigest) && isDigest(x.executionDigest) && ['mechanical', 'integration', 'judgment'].includes(String(x.initialProfile)) && ['medium', 'high'].includes(String(x.initialEffort)) && Number.isSafeInteger(x.attempts) && x.attempts >= 0 && x.attempts <= 3; }
204
225
  function isWellFormedRequestProblem(x) {
205
226
  return isObj(x) && typeof x.file === 'string' && (x.kind === 'corrupt' || x.kind === 'rejected')
206
227
  && typeof x.detail === 'string' && typeof x.at === 'string';
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.canonicalPolicyDigest = canonicalPolicyDigest;
4
+ const node_crypto_1 = require("node:crypto");
5
+ const validate_1 = require("./validate");
6
+ function canonical(value) {
7
+ if (value === null || typeof value !== 'object') {
8
+ const serialized = JSON.stringify(value);
9
+ if (serialized === undefined)
10
+ throw new Error('Invalid canonical value');
11
+ return serialized;
12
+ }
13
+ if (Array.isArray(value))
14
+ return `[${value.map(canonical).join(',')}]`;
15
+ const object = value;
16
+ return `{${Object.keys(object).sort().map(key => `${JSON.stringify(key)}:${canonical(object[key])}`).join(',')}}`;
17
+ }
18
+ function canonicalPolicyDigest(content) { const validated = (0, validate_1.validatePolicyContent)(content); return (0, node_crypto_1.createHash)('sha256').update(canonical(validated), 'utf8').digest('hex'); }