@ikon85/agent-workflow-kit 0.32.1 → 0.34.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 (89) hide show
  1. package/.agents/skills/ask-matt/SKILL.md +4 -3
  2. package/.agents/skills/audit-skills/SKILL.md +6 -0
  3. package/.agents/skills/board-to-waves/SKILL.md +6 -2
  4. package/.agents/skills/code-review/SKILL.md +6 -0
  5. package/.agents/skills/codebase-design/DESIGN-IT-TWICE.md +11 -1
  6. package/.agents/skills/codex-adapter-sync/SKILL.md +23 -19
  7. package/.agents/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +6 -0
  8. package/.agents/skills/improve-codebase-architecture/SKILL.md +10 -1
  9. package/.agents/skills/kit-update/SKILL.md +13 -0
  10. package/.agents/skills/orchestrate-wave/SKILL.md +54 -54
  11. package/.agents/skills/orchestrate-wave/references/builder-contract.md +16 -3
  12. package/.agents/skills/orchestrate-wave/references/dispatch-subagents.md +91 -0
  13. package/.agents/skills/orchestrate-wave/references/dispatch-workflow.md +66 -0
  14. package/.agents/skills/orchestrate-wave/references/report-contracts.md +42 -0
  15. package/.agents/skills/research/SKILL.md +6 -0
  16. package/.agents/skills/scale-check/SKILL.md +9 -7
  17. package/.agents/skills/setup-workflow/SKILL.md +47 -1
  18. package/.agents/skills/setup-workflow/board-sync.md +7 -2
  19. package/.agents/skills/setup-workflow/workflow-overview.md +1 -2
  20. package/.agents/skills/to-issues/SKILL.md +66 -8
  21. package/.agents/skills/to-waves/SKILL.md +24 -12
  22. package/.claude/skills/ask-matt/SKILL.md +4 -3
  23. package/.claude/skills/audit-skills/SKILL.md +6 -0
  24. package/.claude/skills/board-to-waves/SKILL.md +6 -2
  25. package/.claude/skills/code-review/SKILL.md +6 -0
  26. package/.claude/skills/codebase-design/DESIGN-IT-TWICE.md +8 -0
  27. package/.claude/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +6 -0
  28. package/.claude/skills/improve-codebase-architecture/SKILL.md +6 -0
  29. package/.claude/skills/kit-update/SKILL.md +13 -0
  30. package/.claude/skills/orchestrate-wave/SKILL.md +54 -54
  31. package/.claude/skills/orchestrate-wave/references/builder-contract.md +16 -3
  32. package/.claude/skills/orchestrate-wave/references/dispatch-subagents.md +91 -0
  33. package/.claude/skills/orchestrate-wave/references/dispatch-workflow.md +66 -0
  34. package/.claude/skills/orchestrate-wave/references/report-contracts.md +42 -0
  35. package/.claude/skills/research/SKILL.md +6 -0
  36. package/.claude/skills/scale-check/SKILL.md +9 -7
  37. package/.claude/skills/setup-workflow/SKILL.md +47 -1
  38. package/.claude/skills/setup-workflow/board-sync.md +7 -2
  39. package/.claude/skills/setup-workflow/workflow-overview.md +1 -2
  40. package/.claude/skills/to-issues/SKILL.md +66 -8
  41. package/.claude/skills/to-waves/SKILL.md +24 -12
  42. package/README.md +126 -11
  43. package/agent-workflow-kit.package.json +291 -39
  44. package/docs/adr/0002-capability-gated-orchestration.md +70 -0
  45. package/docs/adr/0005-to-issues-is-the-planning-facade.md +42 -0
  46. package/docs/adr/0006-routing-knowledge-access-and-policy-are-separate.md +91 -0
  47. package/docs/agents/skills/local-ci.md +89 -0
  48. package/docs/agents/wave-anchor-template.md +2 -2
  49. package/docs/methodology.html +1 -1
  50. package/docs/methodology.svg +1 -1
  51. package/docs/workflow.html +2 -2
  52. package/docs/workflow.png +0 -0
  53. package/package.json +1 -1
  54. package/scripts/codex-exec.sh +47 -8
  55. package/scripts/codex-exec.test.mjs +56 -0
  56. package/scripts/test_codex_adapter_sync_contract.py +30 -15
  57. package/scripts/test_orchestrate_wave_contract.py +209 -0
  58. package/scripts/test_program_planning_contract.py +70 -0
  59. package/scripts/test_skill_portability_lint.py +54 -0
  60. package/scripts/test_worktree_setup_base_guard.py +140 -0
  61. package/scripts/worktree-lifecycle/setup.py +40 -0
  62. package/src/cli.mjs +109 -2
  63. package/src/commands/init.mjs +17 -1
  64. package/src/commands/routing-policy-update.mjs +204 -0
  65. package/src/commands/update.mjs +22 -0
  66. package/src/lib/agentSurfaceRegistry.mjs +60 -0
  67. package/src/lib/bundle.mjs +34 -0
  68. package/src/lib/capabilityMatrix.mjs +179 -0
  69. package/src/lib/dispatchReceipt.mjs +162 -0
  70. package/src/lib/frontendWorkloads.mjs +170 -0
  71. package/src/lib/reconcileReconReports.mjs +111 -0
  72. package/src/lib/reportValidator.mjs +186 -0
  73. package/src/lib/routeDispatcher.mjs +158 -0
  74. package/src/lib/routingAccessGraph.mjs +105 -0
  75. package/src/lib/routingAdapters/claude.mjs +62 -0
  76. package/src/lib/routingAdapters/codex.mjs +136 -0
  77. package/src/lib/routingCatalog.mjs +123 -0
  78. package/src/lib/routingEvidenceCache.mjs +222 -0
  79. package/src/lib/routingIntent.mjs +93 -0
  80. package/src/lib/routingPolicy.mjs +67 -0
  81. package/src/lib/routingProfile.mjs +334 -0
  82. package/src/lib/routingResolver.mjs +176 -0
  83. package/src/lib/routingSources/artificialAnalysis.mjs +129 -0
  84. package/src/lib/routingSources/benchlm.mjs +151 -0
  85. package/src/lib/routingSources/codeArena.mjs +162 -0
  86. package/src/lib/routingSources/deepswe.mjs +106 -0
  87. package/src/lib/routingSources/openhands.mjs +102 -0
  88. package/src/lib/routingSources/openhandsFrontend.mjs +135 -0
  89. package/src/lib/waveClaim.mjs +134 -0
@@ -0,0 +1,186 @@
1
+ const HASH_PATTERN = '^[0-9a-f]{40}$|^[0-9a-f]{64}$';
2
+
3
+ const stringArray = { type: 'array', items: { type: 'string' } };
4
+
5
+ export const RECON_REPORT_SCHEMA = {
6
+ type: 'object',
7
+ required: ['sliceId', 'plannedFiles', 'dependencyEdges'],
8
+ additionalProperties: false,
9
+ properties: {
10
+ sliceId: { type: 'string', pattern: '^[0-9]+$' },
11
+ plannedFiles: {
12
+ type: 'array',
13
+ items: {
14
+ type: 'object',
15
+ required: ['path', 'role'],
16
+ additionalProperties: false,
17
+ properties: {
18
+ path: { type: 'string' },
19
+ role: { type: 'string', enum: ['edit', 'consume', 'sharedMutable'] },
20
+ },
21
+ },
22
+ },
23
+ dependencyEdges: {
24
+ type: 'array',
25
+ items: {
26
+ type: 'object',
27
+ required: ['from', 'to'],
28
+ additionalProperties: false,
29
+ properties: {
30
+ from: { type: 'string', pattern: '^[0-9]+$' },
31
+ to: { type: 'string', pattern: '^[0-9]+$' },
32
+ },
33
+ },
34
+ },
35
+ },
36
+ };
37
+
38
+ export const BUILDER_REPORT_SCHEMA = {
39
+ type: 'object',
40
+ required: [
41
+ 'status', 'filesTouched', 'testDecisions', 'commands',
42
+ 'commitSha', 'stopItems', 'visualVerify',
43
+ ],
44
+ additionalProperties: false,
45
+ properties: {
46
+ status: { type: 'string', enum: ['pass', 'stop'] },
47
+ filesTouched: stringArray,
48
+ testDecisions: stringArray,
49
+ commands: {
50
+ type: 'array',
51
+ items: {
52
+ type: 'object',
53
+ required: ['command', 'exitCode', 'summary'],
54
+ additionalProperties: false,
55
+ properties: {
56
+ command: { type: 'string' },
57
+ exitCode: { type: 'integer' },
58
+ summary: { type: 'string' },
59
+ },
60
+ },
61
+ },
62
+ commitSha: { anyOf: [{ type: 'string', pattern: HASH_PATTERN }, { type: 'null' }] },
63
+ stopItems: stringArray,
64
+ visualVerify: { type: 'string' },
65
+ },
66
+ oneOf: [
67
+ {
68
+ required: ['status', 'commitSha', 'stopItems'],
69
+ properties: {
70
+ status: { const: 'pass' },
71
+ commitSha: { type: 'string', pattern: HASH_PATTERN },
72
+ stopItems: { type: 'array', maxItems: 0 },
73
+ },
74
+ },
75
+ {
76
+ required: ['status', 'commitSha', 'stopItems'],
77
+ properties: {
78
+ status: { const: 'stop' },
79
+ commitSha: { anyOf: [{ type: 'string', pattern: HASH_PATTERN }, { type: 'null' }] },
80
+ stopItems: { type: 'array', minItems: 1 },
81
+ },
82
+ },
83
+ ],
84
+ };
85
+
86
+ function valueHasType(value, type) {
87
+ if (type === 'null') return value === null;
88
+ if (type === 'array') return Array.isArray(value);
89
+ if (type === 'object') return value !== null && typeof value === 'object' && !Array.isArray(value);
90
+ if (type === 'integer') return Number.isInteger(value);
91
+ return typeof value === type;
92
+ }
93
+
94
+ function validateCombinators(value, schema, path) {
95
+ const errors = [];
96
+ if (schema.anyOf && !schema.anyOf.some((branch) => collectErrors(value, branch, path).length === 0)) {
97
+ errors.push(`${path} must match at least one allowed schema`);
98
+ }
99
+ if (schema.oneOf) {
100
+ const matches = schema.oneOf.filter((branch) => collectErrors(value, branch, path).length === 0).length;
101
+ if (matches !== 1) errors.push(`${path} must match exactly one allowed schema`);
102
+ }
103
+ return errors;
104
+ }
105
+
106
+ function validateObject(value, schema, path) {
107
+ const errors = [];
108
+ for (const key of schema.required ?? []) {
109
+ if (!Object.hasOwn(value, key)) errors.push(`${path}.${key} is required`);
110
+ }
111
+ if (schema.additionalProperties === false) {
112
+ for (const key of Object.keys(value)) {
113
+ if (!Object.hasOwn(schema.properties ?? {}, key)) errors.push(`${path}.${key} is not allowed`);
114
+ }
115
+ }
116
+ for (const [key, childSchema] of Object.entries(schema.properties ?? {})) {
117
+ if (Object.hasOwn(value, key)) errors.push(...collectErrors(value[key], childSchema, `${path}.${key}`));
118
+ }
119
+ return errors;
120
+ }
121
+
122
+ function collectErrors(value, schema, path = '$') {
123
+ const errors = validateCombinators(value, schema, path);
124
+ if (schema.const !== undefined && value !== schema.const) errors.push(`${path} must equal ${schema.const}`);
125
+ if (schema.enum && !schema.enum.includes(value)) errors.push(`${path} has an unsupported value`);
126
+ if (schema.type && !valueHasType(value, schema.type)) return [...errors, `${path} must be ${schema.type}`];
127
+ if (schema.pattern && !new RegExp(schema.pattern).test(value)) errors.push(`${path} has an invalid format`);
128
+ if (Array.isArray(value)) {
129
+ if (schema.minItems !== undefined && value.length < schema.minItems) errors.push(`${path} has too few items`);
130
+ if (schema.maxItems !== undefined && value.length > schema.maxItems) errors.push(`${path} has too many items`);
131
+ if (schema.items) value.forEach((item, index) => errors.push(...collectErrors(item, schema.items, `${path}[${index}]`)));
132
+ }
133
+ if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
134
+ errors.push(...validateObject(value, schema, path));
135
+ }
136
+ return errors;
137
+ }
138
+
139
+ function validateSchema(value, schema) {
140
+ const errors = collectErrors(value, schema);
141
+ return { ok: errors.length === 0, errors };
142
+ }
143
+
144
+ export const validateReconReport = (report) => validateSchema(report, RECON_REPORT_SCHEMA);
145
+ export const validateBuilderReport = (report) => validateSchema(report, BUILDER_REPORT_SCHEMA);
146
+
147
+ function verifyCommit(report, gitFacts) {
148
+ if (!['sha1', 'sha256'].includes(gitFacts?.objectFormat)) return ['gitFacts.objectFormat is unsupported'];
149
+ if (report.commitSha === null) return [];
150
+ const errors = [];
151
+ const expectedLength = gitFacts.objectFormat === 'sha1' ? 40 : 64;
152
+ if (report.commitSha.length !== expectedLength) {
153
+ errors.push(`commitSha does not match ${gitFacts.objectFormat} object format`);
154
+ }
155
+ if (gitFacts.commitSha !== report.commitSha) errors.push('commit SHA does not match independently observed git facts');
156
+ if (gitFacts.baseIsAncestorOfCommit !== true) {
157
+ errors.push('integration base is not an ancestor of the builder commit');
158
+ }
159
+ return errors;
160
+ }
161
+
162
+ function verifyFilesAndCommands(report, gitFacts, allowlist, requiredCommands) {
163
+ const errors = [];
164
+ if (!Array.isArray(gitFacts?.changedFiles)) errors.push('gitFacts.changedFiles must be an array');
165
+ else if (gitFacts.changedFiles.some((path) => !allowlist.includes(path))) {
166
+ errors.push('git diff contains a path outside the allowlist');
167
+ }
168
+ const observedCommands = new Set(report.commands.map(({ command }) => command));
169
+ for (const command of requiredCommands) {
170
+ if (!observedCommands.has(command)) errors.push(`required command was not reported: ${command}`);
171
+ }
172
+ return errors;
173
+ }
174
+
175
+ export function semanticVerify(report, { gitFacts, allowlist = [], requiredCommands = [] } = {}) {
176
+ const structural = validateBuilderReport(report);
177
+ const errors = [...structural.errors];
178
+ if (structural.ok) {
179
+ if (report.status === 'pass' && report.commands.some(({ exitCode }) => exitCode !== 0)) {
180
+ errors.push('PASS report contains a command with a nonzero exit');
181
+ }
182
+ errors.push(...verifyCommit(report, gitFacts));
183
+ errors.push(...verifyFilesAndCommands(report, gitFacts, allowlist, requiredCommands));
184
+ }
185
+ return { ok: errors.length === 0, errors };
186
+ }
@@ -0,0 +1,158 @@
1
+ import { createDispatchReceipt } from './dispatchReceipt.mjs';
2
+ import { resolveRoute } from './routingResolver.mjs';
3
+ import {
4
+ assertRoutingProfileUnchanged,
5
+ captureRoutingProfileSnapshot,
6
+ } from './routingEvidenceCache.mjs';
7
+
8
+ const RECEIPT_ROUTE_FIELDS = [
9
+ 'providerId',
10
+ 'modelId',
11
+ 'effort',
12
+ 'surfaceId',
13
+ 'transportId',
14
+ ];
15
+
16
+ function receiptRoute(route) {
17
+ return Object.fromEntries(RECEIPT_ROUTE_FIELDS.map((field) => [field, route[field]]));
18
+ }
19
+
20
+ function receiptRevisions(decision) {
21
+ return Object.fromEntries(
22
+ ['catalog', 'accessGraph', 'policy'].map((field) => [
23
+ field,
24
+ decision.revisions[field] ?? 'missing',
25
+ ]),
26
+ );
27
+ }
28
+
29
+ function blockedReceipt(input, decision, reason, details = {}) {
30
+ return createDispatchReceipt({
31
+ executionId: input.executionId,
32
+ status: 'blocked',
33
+ afk: input.afk,
34
+ requestedRoute: details.requestedRoute ?? null,
35
+ appliedRoute: details.appliedRoute ?? null,
36
+ enforcement: details.enforcement ?? null,
37
+ precedence: details.precedence ?? null,
38
+ revisions: receiptRevisions(decision),
39
+ dispatchedAt: input.dispatchedAt,
40
+ reason,
41
+ });
42
+ }
43
+
44
+ function decisionReason(decision) {
45
+ const blockers = decision.blockers.length > 0 ? `:${decision.blockers.join(',')}` : '';
46
+ return `${decision.status}:${decision.reason}${blockers}`;
47
+ }
48
+
49
+ function safeFailureReason(error) {
50
+ const message = error instanceof Error ? error.message : '';
51
+ const safeReasons = [
52
+ ['Claude route capability is not attested', 'Claude route capability is not attested'],
53
+ ['route identity is incomplete', 'route identity is incomplete'],
54
+ ['transport is not detected', 'transport is not detected'],
55
+ ['transport is not callable', 'transport is not callable'],
56
+ ['transport is not permitted', 'transport is not permitted'],
57
+ ['model control is not enforced', 'model control is not enforced'],
58
+ ['effort control is not enforced', 'effort control is not enforced'],
59
+ ['model environment precedence is unverified', 'model environment precedence is unverified'],
60
+ ['effort environment precedence is unverified', 'effort environment precedence is unverified'],
61
+ ['model applied value is unverified', 'model applied value is unverified'],
62
+ ['effort applied value is unverified', 'effort applied value is unverified'],
63
+ ['transport has no approved dispatcher', 'transport has no approved dispatcher'],
64
+ ['environment precedence mismatch: model', 'environment precedence mismatch: model'],
65
+ ['environment precedence mismatch: effort', 'environment precedence mismatch: effort'],
66
+ ['applied route mismatch', 'applied route mismatch'],
67
+ ['spawn guard received no callable dispatcher', 'spawn guard received no callable dispatcher'],
68
+ ['applied route differs from requested route', 'applied route differs from requested route'],
69
+ ['applied enforcement differs from attested access path', 'applied enforcement differs from attested access path'],
70
+ ['AFK dispatch requires enforced model and effort selection', 'AFK dispatch requires enforced model and effort selection'],
71
+ ['AFK dispatch requires verified environment precedence', 'AFK dispatch requires verified environment precedence'],
72
+ ['concurrent routing profile mutation', 'concurrent routing profile mutation'],
73
+ ['concurrent evidence catalog mutation', 'concurrent evidence catalog mutation'],
74
+ ];
75
+ return safeReasons.find(([prefix]) => message.startsWith(prefix))?.[1]
76
+ ?? 'dispatch adapter rejected route';
77
+ }
78
+
79
+ function safeDispatchResult(result) {
80
+ if (!result || typeof result !== 'object' || Array.isArray(result)) return null;
81
+ const taskId = typeof result.taskId === 'string' && result.taskId !== ''
82
+ ? result.taskId
83
+ : null;
84
+ return taskId === null ? null : Object.freeze({ taskId });
85
+ }
86
+
87
+ function assertAppliedEnforcement(decision, applied) {
88
+ for (const field of ['model', 'effort']) {
89
+ if (decision.bestExecutable.enforcement[field] !== applied[field]) {
90
+ throw new Error(`applied enforcement differs from attested access path: ${field}`);
91
+ }
92
+ }
93
+ }
94
+
95
+ export async function dispatchResolvedRoute(input) {
96
+ if (!input || typeof input !== 'object' || Array.isArray(input)) {
97
+ throw new TypeError('dispatch input must be an object');
98
+ }
99
+ if (!input.adapter || typeof input.adapter.prepare !== 'function') {
100
+ throw new TypeError('dispatch adapter must expose prepare');
101
+ }
102
+ const decision = resolveRoute(input.resolverInput);
103
+ if (decision.status !== 'ready') {
104
+ return Object.freeze({
105
+ decision,
106
+ receipt: blockedReceipt(input, decision, decisionReason(decision)),
107
+ dispatchResult: null,
108
+ });
109
+ }
110
+ const profileSnapshot = captureRoutingProfileSnapshot(input.resolverInput);
111
+ const catalogRevision = input.resolverInput.catalog.revision;
112
+
113
+ const requestedRoute = receiptRoute(decision.bestExecutable);
114
+ try {
115
+ const prepared = await input.adapter.prepare(requestedRoute);
116
+ assertRoutingProfileUnchanged(profileSnapshot, input.resolverInput);
117
+ if (catalogRevision !== input.resolverInput.catalog?.revision) {
118
+ throw new Error('concurrent evidence catalog mutation');
119
+ }
120
+ assertAppliedEnforcement(decision, prepared.enforcement);
121
+ if (prepared.mismatchReason) {
122
+ return Object.freeze({
123
+ decision,
124
+ receipt: blockedReceipt(input, decision, safeFailureReason(
125
+ new Error(prepared.mismatchReason),
126
+ ), {
127
+ requestedRoute,
128
+ appliedRoute: receiptRoute(prepared.appliedRoute),
129
+ enforcement: prepared.enforcement,
130
+ precedence: prepared.precedence,
131
+ }),
132
+ dispatchResult: null,
133
+ });
134
+ }
135
+ const receipt = createDispatchReceipt({
136
+ executionId: input.executionId,
137
+ status: 'dispatched',
138
+ afk: input.afk,
139
+ requestedRoute,
140
+ appliedRoute: receiptRoute(prepared.appliedRoute),
141
+ enforcement: prepared.enforcement,
142
+ precedence: prepared.precedence,
143
+ revisions: receiptRevisions(decision),
144
+ dispatchedAt: input.dispatchedAt,
145
+ });
146
+ if (typeof prepared.dispatch !== 'function') {
147
+ throw new Error('spawn guard received no callable dispatcher');
148
+ }
149
+ const dispatchResult = safeDispatchResult(await prepared.dispatch());
150
+ return Object.freeze({ decision, receipt, dispatchResult });
151
+ } catch (error) {
152
+ return Object.freeze({
153
+ decision,
154
+ receipt: blockedReceipt(input, decision, safeFailureReason(error), { requestedRoute }),
155
+ dispatchResult: null,
156
+ });
157
+ }
158
+ }
@@ -0,0 +1,105 @@
1
+ export const ACCESS_GRAPH_VERSION = 1;
2
+
3
+ export const ROUTE_AVAILABILITY = Object.freeze([
4
+ 'available',
5
+ 'unavailable',
6
+ 'unknown',
7
+ ]);
8
+
9
+ export const ENFORCEMENT_METHODS = Object.freeze([
10
+ 'per-spawn',
11
+ 'named-agent',
12
+ 'session-default',
13
+ 'none',
14
+ ]);
15
+
16
+ const PATH_FIELDS = new Set([
17
+ 'id',
18
+ 'surfaceId',
19
+ 'providerId',
20
+ 'modelId',
21
+ 'transportId',
22
+ 'availability',
23
+ 'enforcement',
24
+ 'capabilityEvidence',
25
+ ]);
26
+
27
+ function object(value, field) {
28
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
29
+ throw new TypeError(`${field} must be an object`);
30
+ }
31
+ return value;
32
+ }
33
+
34
+ function string(value, field) {
35
+ if (typeof value !== 'string' || value.trim() === '') {
36
+ throw new TypeError(`${field} must be a non-empty string`);
37
+ }
38
+ return value;
39
+ }
40
+
41
+ function oneOf(value, values, field) {
42
+ if (!values.includes(value)) throw new TypeError(`${field} must be one of: ${values.join(', ')}`);
43
+ return value;
44
+ }
45
+
46
+ function timestamp(value, field) {
47
+ string(value, field);
48
+ if (!Number.isFinite(Date.parse(value))) throw new TypeError(`${field} must be an ISO timestamp`);
49
+ return value;
50
+ }
51
+
52
+ function deepFreeze(value) {
53
+ if (!value || typeof value !== 'object' || Object.isFrozen(value)) return value;
54
+ Object.freeze(value);
55
+ for (const child of Object.values(value)) deepFreeze(child);
56
+ return value;
57
+ }
58
+
59
+ function validatePath(path, index) {
60
+ const field = `paths[${index}]`;
61
+ object(path, field);
62
+ for (const key of Object.keys(path)) {
63
+ if (!PATH_FIELDS.has(key)) throw new TypeError(`unknown access path field: ${key}`);
64
+ }
65
+ const enforcement = object(path.enforcement, `${field}.enforcement`);
66
+ const evidence = object(path.capabilityEvidence, `${field}.capabilityEvidence`);
67
+ const observedAt = timestamp(evidence.observedAt, `${field}.capabilityEvidence.observedAt`);
68
+ const expiresAt = timestamp(evidence.expiresAt, `${field}.capabilityEvidence.expiresAt`);
69
+ if (Date.parse(expiresAt) <= Date.parse(observedAt)) {
70
+ throw new TypeError(`${field}.capabilityEvidence.expiresAt must follow observedAt`);
71
+ }
72
+ return {
73
+ id: string(path.id, `${field}.id`),
74
+ surfaceId: string(path.surfaceId, `${field}.surfaceId`),
75
+ providerId: string(path.providerId, `${field}.providerId`),
76
+ modelId: string(path.modelId, `${field}.modelId`),
77
+ transportId: string(path.transportId, `${field}.transportId`),
78
+ availability: oneOf(path.availability, ROUTE_AVAILABILITY, `${field}.availability`),
79
+ enforcement: {
80
+ model: oneOf(enforcement.model, ENFORCEMENT_METHODS, `${field}.enforcement.model`),
81
+ effort: oneOf(enforcement.effort, ENFORCEMENT_METHODS, `${field}.enforcement.effort`),
82
+ },
83
+ capabilityEvidence: {
84
+ revision: string(evidence.revision, `${field}.capabilityEvidence.revision`),
85
+ observedAt,
86
+ expiresAt,
87
+ },
88
+ };
89
+ }
90
+
91
+ export function validateAccessGraph(input) {
92
+ object(input, 'access graph');
93
+ if (input.schemaVersion !== ACCESS_GRAPH_VERSION) {
94
+ throw new TypeError(`access graph schemaVersion must be ${ACCESS_GRAPH_VERSION}`);
95
+ }
96
+ const revision = string(input.revision, 'access graph revision');
97
+ if (!Array.isArray(input.paths)) throw new TypeError('access graph paths must be an array');
98
+ const paths = input.paths.map(validatePath);
99
+ const ids = new Set();
100
+ for (const path of paths) {
101
+ if (ids.has(path.id)) throw new TypeError(`duplicate access path: ${path.id}`);
102
+ ids.add(path.id);
103
+ }
104
+ return deepFreeze({ schemaVersion: ACCESS_GRAPH_VERSION, revision, paths });
105
+ }
@@ -0,0 +1,62 @@
1
+ import { adaptClaudeRoutingInventory } from '../capabilityMatrix.mjs';
2
+
3
+ function matchesRoute(path, route) {
4
+ return ['surfaceId', 'providerId', 'modelId', 'transportId']
5
+ .every((field) => path[field] === route[field]);
6
+ }
7
+
8
+ function appliedRoute(path, requestedRoute) {
9
+ return Object.freeze({
10
+ ...requestedRoute,
11
+ modelId: path.model.applied,
12
+ effort: path.effort.applied,
13
+ });
14
+ }
15
+
16
+ function mismatchReason(path, requested, applied) {
17
+ for (const field of ['modelId', 'effort']) {
18
+ if (requested[field] === applied[field]) continue;
19
+ const control = field === 'modelId' ? path.model : path.effort;
20
+ if (control.precedence === 'environment-over-agent-definition') {
21
+ return `environment precedence mismatch: ${field === 'modelId' ? 'model' : field}`;
22
+ }
23
+ return `applied route mismatch: ${field}`;
24
+ }
25
+ return null;
26
+ }
27
+
28
+ export function createClaudeRoutingAdapter({ inventory, dispatchers = {} }) {
29
+ const capabilities = adaptClaudeRoutingInventory(inventory);
30
+ return Object.freeze({
31
+ async prepare(requestedRoute) {
32
+ const path = capabilities.paths.find((candidate) => matchesRoute(candidate, requestedRoute));
33
+ if (!path) throw new Error('Claude route capability is not attested');
34
+ if (!path.verified) throw new Error(path.verificationFailures.join('; '));
35
+ const invoke = dispatchers[path.transportId];
36
+ if (typeof invoke !== 'function') {
37
+ throw new Error(`transport has no approved dispatcher: ${path.transportId}`);
38
+ }
39
+ const applied = appliedRoute(path, requestedRoute);
40
+ const mismatch = mismatchReason(path, requestedRoute, applied);
41
+ return Object.freeze({
42
+ appliedRoute: applied,
43
+ enforcement: Object.freeze({
44
+ model: path.model.method,
45
+ effort: path.effort.method,
46
+ }),
47
+ precedence: Object.freeze({
48
+ model: path.model.precedence,
49
+ effort: path.effort.precedence,
50
+ }),
51
+ mismatchReason: mismatch,
52
+ dispatch: () => invoke(Object.freeze({
53
+ route: applied,
54
+ enforcement: Object.freeze({
55
+ model: path.model.method,
56
+ effort: path.effort.method,
57
+ }),
58
+ })),
59
+ });
60
+ },
61
+ });
62
+ }
@@ -0,0 +1,136 @@
1
+ import { adaptClaudeRoutingInventory } from '../capabilityMatrix.mjs';
2
+
3
+ const MODEL_SELECTORS = ['model'];
4
+ const EFFORT_SELECTORS = ['effort', 'reasoning_effort', 'model_reasoning_effort'];
5
+
6
+ function object(value, field) {
7
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
8
+ throw new TypeError(`${field} must be an object`);
9
+ }
10
+ return value;
11
+ }
12
+
13
+ function string(value, field) {
14
+ if (typeof value !== 'string' || value.trim() === '') {
15
+ throw new TypeError(`${field} must be a non-empty string`);
16
+ }
17
+ return value;
18
+ }
19
+
20
+ function timestamp(value, field) {
21
+ string(value, field);
22
+ if (!Number.isFinite(Date.parse(value))) {
23
+ throw new TypeError(`${field} must be an ISO timestamp`);
24
+ }
25
+ return value;
26
+ }
27
+
28
+ function schemaProperties(schema) {
29
+ if (!schema || typeof schema !== 'object' || Array.isArray(schema)) return {};
30
+ if (!schema.properties || typeof schema.properties !== 'object'
31
+ || Array.isArray(schema.properties)) return {};
32
+ return schema.properties;
33
+ }
34
+
35
+ function hasSelector(properties, selectors) {
36
+ return selectors.some((selector) =>
37
+ Object.prototype.hasOwnProperty.call(properties, selector));
38
+ }
39
+
40
+ function uncontrolled(control) {
41
+ return {
42
+ ...control,
43
+ method: 'none',
44
+ enforced: false,
45
+ precedence: 'uncontrolled',
46
+ applied: undefined,
47
+ };
48
+ }
49
+
50
+ function applySpawnSchemaEvidence(path, properties) {
51
+ const candidate = { ...path };
52
+ if (path?.model?.method === 'per-spawn'
53
+ && !hasSelector(properties, MODEL_SELECTORS)) {
54
+ candidate.model = uncontrolled(path.model);
55
+ }
56
+ if (path?.effort?.method === 'per-spawn'
57
+ && !hasSelector(properties, EFFORT_SELECTORS)) {
58
+ candidate.effort = uncontrolled(path.effort);
59
+ }
60
+ return candidate;
61
+ }
62
+
63
+ function matchesRoute(path, route) {
64
+ return ['surfaceId', 'providerId', 'modelId', 'transportId']
65
+ .every((field) => path[field] === route[field]);
66
+ }
67
+
68
+ function appliedRoute(path, requestedRoute) {
69
+ return Object.freeze({
70
+ ...requestedRoute,
71
+ modelId: path.model.applied,
72
+ effort: path.effort.applied,
73
+ });
74
+ }
75
+
76
+ function mismatchReason(path, requested, applied) {
77
+ for (const field of ['modelId', 'effort']) {
78
+ if (requested[field] === applied[field]) continue;
79
+ const control = field === 'modelId' ? path.model : path.effort;
80
+ if (control.precedence === 'environment-over-agent-definition') {
81
+ return `environment precedence mismatch: ${field === 'modelId' ? 'model' : field}`;
82
+ }
83
+ return `applied route mismatch: ${field}`;
84
+ }
85
+ return null;
86
+ }
87
+
88
+ export function adaptCodexRoutingInventory(inventory) {
89
+ const source = object(inventory, 'Codex host attestation');
90
+ timestamp(source.observedAt, 'Codex host attestation observedAt');
91
+ const host = object(source.host, 'Codex host attestation host');
92
+ string(host.id, 'Codex host attestation host.id');
93
+ string(host.version, 'Codex host attestation host.version');
94
+ const properties = schemaProperties(source.spawnSchema);
95
+ const paths = Array.isArray(source.paths)
96
+ ? source.paths
97
+ .filter((path) => path?.surfaceId === 'codex')
98
+ .map((path) => applySpawnSchemaEvidence(path, properties))
99
+ : [];
100
+ return adaptClaudeRoutingInventory({
101
+ contractVersion: source.contractVersion,
102
+ paths,
103
+ });
104
+ }
105
+
106
+ export function createCodexRoutingAdapter({ inventory, dispatchers = {} }) {
107
+ const capabilities = adaptCodexRoutingInventory(inventory);
108
+ return Object.freeze({
109
+ async prepare(requestedRoute) {
110
+ const path = capabilities.paths.find((candidate) =>
111
+ matchesRoute(candidate, requestedRoute));
112
+ if (!path) throw new Error('Codex route capability is not attested');
113
+ if (!path.verified) throw new Error(path.verificationFailures.join('; '));
114
+ const invoke = dispatchers[path.transportId];
115
+ if (typeof invoke !== 'function') {
116
+ throw new Error(`transport has no approved dispatcher: ${path.transportId}`);
117
+ }
118
+ const applied = appliedRoute(path, requestedRoute);
119
+ const mismatch = mismatchReason(path, requestedRoute, applied);
120
+ const enforcement = Object.freeze({
121
+ model: path.model.method,
122
+ effort: path.effort.method,
123
+ });
124
+ return Object.freeze({
125
+ appliedRoute: applied,
126
+ enforcement,
127
+ precedence: Object.freeze({
128
+ model: path.model.precedence,
129
+ effort: path.effort.precedence,
130
+ }),
131
+ mismatchReason: mismatch,
132
+ dispatch: () => invoke(Object.freeze({ route: applied, enforcement })),
133
+ });
134
+ },
135
+ });
136
+ }