@ikon85/agent-workflow-kit 0.33.0 → 0.34.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/.agents/skills/ask-matt/SKILL.md +4 -3
- package/.agents/skills/audit-skills/SKILL.md +6 -0
- package/.agents/skills/board-to-waves/SKILL.md +6 -2
- package/.agents/skills/code-review/SKILL.md +6 -0
- package/.agents/skills/codebase-design/DESIGN-IT-TWICE.md +11 -1
- package/.agents/skills/codex-adapter-sync/SKILL.md +23 -19
- package/.agents/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +6 -0
- package/.agents/skills/improve-codebase-architecture/SKILL.md +10 -1
- package/.agents/skills/kit-release/SKILL.md +29 -4
- package/.agents/skills/kit-update/SKILL.md +13 -0
- package/.agents/skills/orchestrate-wave/SKILL.md +1 -1
- package/.agents/skills/orchestrate-wave/references/dispatch-subagents.md +23 -11
- package/.agents/skills/orchestrate-wave/references/dispatch-workflow.md +8 -0
- package/.agents/skills/research/SKILL.md +6 -0
- package/.agents/skills/scale-check/SKILL.md +9 -7
- package/.agents/skills/setup-workflow/SKILL.md +47 -1
- package/.agents/skills/setup-workflow/board-sync.md +7 -2
- package/.agents/skills/setup-workflow/workflow-overview.md +1 -2
- package/.agents/skills/to-issues/SKILL.md +66 -8
- package/.agents/skills/to-waves/SKILL.md +24 -12
- package/.claude/skills/ask-matt/SKILL.md +4 -3
- package/.claude/skills/audit-skills/SKILL.md +6 -0
- package/.claude/skills/board-to-waves/SKILL.md +6 -2
- package/.claude/skills/code-review/SKILL.md +6 -0
- package/.claude/skills/codebase-design/DESIGN-IT-TWICE.md +8 -0
- package/.claude/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +6 -0
- package/.claude/skills/improve-codebase-architecture/SKILL.md +6 -0
- package/.claude/skills/kit-release/SKILL.md +29 -4
- package/.claude/skills/kit-update/SKILL.md +13 -0
- package/.claude/skills/orchestrate-wave/SKILL.md +1 -1
- package/.claude/skills/orchestrate-wave/references/dispatch-subagents.md +23 -11
- package/.claude/skills/orchestrate-wave/references/dispatch-workflow.md +8 -0
- package/.claude/skills/research/SKILL.md +6 -0
- package/.claude/skills/scale-check/SKILL.md +9 -7
- package/.claude/skills/setup-workflow/SKILL.md +47 -1
- package/.claude/skills/setup-workflow/board-sync.md +7 -2
- package/.claude/skills/setup-workflow/workflow-overview.md +1 -2
- package/.claude/skills/to-issues/SKILL.md +66 -8
- package/.claude/skills/to-waves/SKILL.md +24 -12
- package/README.md +89 -13
- package/agent-workflow-kit.package.json +204 -44
- package/docs/adr/0004-release-intent-is-a-version-tag.md +42 -0
- package/docs/adr/0005-to-issues-is-the-planning-facade.md +42 -0
- package/docs/adr/0006-routing-knowledge-access-and-policy-are-separate.md +91 -0
- package/docs/agents/skills/local-ci.md +89 -0
- package/docs/agents/wave-anchor-template.md +2 -2
- package/docs/methodology.html +1 -1
- package/docs/methodology.svg +1 -1
- package/docs/workflow.html +2 -2
- package/docs/workflow.png +0 -0
- package/package.json +1 -1
- package/scripts/codex-exec.sh +47 -8
- package/scripts/codex-exec.test.mjs +56 -0
- package/scripts/kit-release.test.mjs +178 -0
- package/scripts/release-state.mjs +35 -5
- package/scripts/release-state.test.mjs +122 -5
- package/scripts/test_codex_adapter_sync_contract.py +30 -15
- package/scripts/test_orchestrate_wave_contract.py +73 -0
- package/scripts/test_program_planning_contract.py +70 -0
- package/scripts/test_skill_portability_lint.py +54 -0
- package/src/cli.mjs +109 -2
- package/src/commands/init.mjs +17 -1
- package/src/commands/routing-policy-update.mjs +204 -0
- package/src/commands/update.mjs +22 -0
- package/src/lib/agentSurfaceRegistry.mjs +60 -0
- package/src/lib/bundle.mjs +24 -0
- package/src/lib/capabilityMatrix.mjs +85 -0
- package/src/lib/dispatchReceipt.mjs +162 -0
- package/src/lib/frontendWorkloads.mjs +170 -0
- package/src/lib/routeDispatcher.mjs +158 -0
- package/src/lib/routingAccessGraph.mjs +105 -0
- package/src/lib/routingAdapters/claude.mjs +62 -0
- package/src/lib/routingAdapters/codex.mjs +136 -0
- package/src/lib/routingCatalog.mjs +123 -0
- package/src/lib/routingEvidenceCache.mjs +222 -0
- package/src/lib/routingIntent.mjs +93 -0
- package/src/lib/routingPolicy.mjs +67 -0
- package/src/lib/routingProfile.mjs +334 -0
- package/src/lib/routingResolver.mjs +176 -0
- package/src/lib/routingSources/artificialAnalysis.mjs +129 -0
- package/src/lib/routingSources/benchlm.mjs +151 -0
- package/src/lib/routingSources/codeArena.mjs +162 -0
- package/src/lib/routingSources/deepswe.mjs +106 -0
- package/src/lib/routingSources/openhands.mjs +102 -0
- package/src/lib/routingSources/openhandsFrontend.mjs +135 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
export const DISPATCH_RECEIPT_VERSION = 2;
|
|
2
|
+
|
|
3
|
+
const ROUTE_FIELDS = [
|
|
4
|
+
'providerId',
|
|
5
|
+
'modelId',
|
|
6
|
+
'effort',
|
|
7
|
+
'surfaceId',
|
|
8
|
+
'transportId',
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
const ENFORCEMENT_METHODS = [
|
|
12
|
+
'per-spawn',
|
|
13
|
+
'named-agent',
|
|
14
|
+
'session-default',
|
|
15
|
+
'none',
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
export const DISPATCH_PRECEDENCE = Object.freeze([
|
|
19
|
+
'explicit-argument',
|
|
20
|
+
'agent-definition-over-environment',
|
|
21
|
+
'environment-over-agent-definition',
|
|
22
|
+
'session-default',
|
|
23
|
+
'uncontrolled',
|
|
24
|
+
'unreported',
|
|
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 timestamp(value, field) {
|
|
42
|
+
string(value, field);
|
|
43
|
+
if (!Number.isFinite(Date.parse(value))) throw new TypeError(`${field} must be an ISO timestamp`);
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function route(input, field) {
|
|
48
|
+
object(input, field);
|
|
49
|
+
for (const key of Object.keys(input)) {
|
|
50
|
+
if (!ROUTE_FIELDS.includes(key)) throw new TypeError(`unknown ${field} field: ${key}`);
|
|
51
|
+
}
|
|
52
|
+
return Object.freeze(Object.fromEntries(
|
|
53
|
+
ROUTE_FIELDS.map((key) => [key, string(input[key], `${field}.${key}`)]),
|
|
54
|
+
));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function enforcement(input) {
|
|
58
|
+
object(input, 'enforcement');
|
|
59
|
+
const result = {};
|
|
60
|
+
for (const field of ['model', 'effort']) {
|
|
61
|
+
if (!ENFORCEMENT_METHODS.includes(input[field])) {
|
|
62
|
+
throw new TypeError(`${field} enforcement must be one of: ${ENFORCEMENT_METHODS.join(', ')}`);
|
|
63
|
+
}
|
|
64
|
+
result[field] = input[field];
|
|
65
|
+
}
|
|
66
|
+
return Object.freeze(result);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function precedence(input) {
|
|
70
|
+
if (input == null) {
|
|
71
|
+
return Object.freeze({ model: 'unreported', effort: 'unreported' });
|
|
72
|
+
}
|
|
73
|
+
object(input, 'precedence');
|
|
74
|
+
const result = {};
|
|
75
|
+
for (const field of ['model', 'effort']) {
|
|
76
|
+
if (!DISPATCH_PRECEDENCE.includes(input[field])) {
|
|
77
|
+
throw new TypeError(
|
|
78
|
+
`${field} precedence must be one of: ${DISPATCH_PRECEDENCE.join(', ')}`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
result[field] = input[field];
|
|
82
|
+
}
|
|
83
|
+
return Object.freeze(result);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function revisions(input) {
|
|
87
|
+
object(input, 'revisions');
|
|
88
|
+
return Object.freeze({
|
|
89
|
+
catalog: string(input.catalog, 'revisions.catalog'),
|
|
90
|
+
accessGraph: string(input.accessGraph, 'revisions.accessGraph'),
|
|
91
|
+
policy: string(input.policy, 'revisions.policy'),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function blockedReceipt(input) {
|
|
96
|
+
const requestedRoute = input.requestedRoute == null
|
|
97
|
+
? null
|
|
98
|
+
: route(input.requestedRoute, 'requestedRoute');
|
|
99
|
+
const appliedRoute = input.appliedRoute == null
|
|
100
|
+
? null
|
|
101
|
+
: route(input.appliedRoute, 'appliedRoute');
|
|
102
|
+
if (appliedRoute !== null && requestedRoute === null) {
|
|
103
|
+
throw new TypeError('blocked applied route requires a requested route');
|
|
104
|
+
}
|
|
105
|
+
const appliedEnforcement = input.enforcement == null ? null : enforcement(input.enforcement);
|
|
106
|
+
const appliedPrecedence = input.precedence == null ? null : precedence(input.precedence);
|
|
107
|
+
if (appliedRoute !== null && (appliedEnforcement === null || appliedPrecedence === null)) {
|
|
108
|
+
throw new TypeError('blocked applied route requires enforcement and precedence');
|
|
109
|
+
}
|
|
110
|
+
return Object.freeze({
|
|
111
|
+
schemaVersion: DISPATCH_RECEIPT_VERSION,
|
|
112
|
+
executionId: string(input.executionId, 'executionId'),
|
|
113
|
+
status: 'blocked',
|
|
114
|
+
afk: input.afk === true,
|
|
115
|
+
requestedRoute,
|
|
116
|
+
appliedRoute,
|
|
117
|
+
enforcement: appliedEnforcement,
|
|
118
|
+
precedence: appliedPrecedence,
|
|
119
|
+
revisions: revisions(input.revisions),
|
|
120
|
+
dispatchedAt: timestamp(input.dispatchedAt, 'dispatchedAt'),
|
|
121
|
+
reason: string(input.reason, 'reason'),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function createDispatchReceipt(input) {
|
|
126
|
+
object(input, 'dispatch receipt');
|
|
127
|
+
if (input.status === 'blocked') return blockedReceipt(input);
|
|
128
|
+
if (input.status !== 'dispatched') {
|
|
129
|
+
throw new TypeError('dispatch receipt status must be dispatched or blocked');
|
|
130
|
+
}
|
|
131
|
+
const requestedRoute = route(input.requestedRoute, 'requestedRoute');
|
|
132
|
+
const appliedRoute = route(input.appliedRoute, 'appliedRoute');
|
|
133
|
+
for (const field of ROUTE_FIELDS) {
|
|
134
|
+
if (requestedRoute[field] !== appliedRoute[field]) {
|
|
135
|
+
throw new Error(`applied route differs from requested route: ${field}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const appliedEnforcement = enforcement(input.enforcement);
|
|
139
|
+
const appliedPrecedence = precedence(input.precedence);
|
|
140
|
+
if (input.afk === true
|
|
141
|
+
&& (appliedEnforcement.model === 'none' || appliedEnforcement.effort === 'none')) {
|
|
142
|
+
throw new Error('AFK dispatch requires enforced model and effort selection');
|
|
143
|
+
}
|
|
144
|
+
if (input.afk === true
|
|
145
|
+
&& Object.values(appliedPrecedence).some((value) =>
|
|
146
|
+
value === 'uncontrolled' || value === 'unreported')) {
|
|
147
|
+
throw new Error('AFK dispatch requires verified environment precedence');
|
|
148
|
+
}
|
|
149
|
+
return Object.freeze({
|
|
150
|
+
schemaVersion: DISPATCH_RECEIPT_VERSION,
|
|
151
|
+
executionId: string(input.executionId, 'executionId'),
|
|
152
|
+
status: 'dispatched',
|
|
153
|
+
afk: input.afk === true,
|
|
154
|
+
requestedRoute,
|
|
155
|
+
appliedRoute,
|
|
156
|
+
enforcement: appliedEnforcement,
|
|
157
|
+
precedence: appliedPrecedence,
|
|
158
|
+
revisions: revisions(input.revisions),
|
|
159
|
+
dispatchedAt: timestamp(input.dispatchedAt, 'dispatchedAt'),
|
|
160
|
+
reason: null,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import {
|
|
2
|
+
evidenceWorkloadIdentity,
|
|
3
|
+
validateEvidenceSelection,
|
|
4
|
+
} from './routingIntent.mjs';
|
|
5
|
+
|
|
6
|
+
const FRONTEND_DOMAINS = new Set([
|
|
7
|
+
'general',
|
|
8
|
+
'reference-design',
|
|
9
|
+
'marketing',
|
|
10
|
+
'analytics',
|
|
11
|
+
'product',
|
|
12
|
+
'game',
|
|
13
|
+
'simulation',
|
|
14
|
+
'editor',
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
const QUALITY_AXES = new Set([
|
|
18
|
+
'visual-preference',
|
|
19
|
+
'visual-fidelity',
|
|
20
|
+
'functional',
|
|
21
|
+
'accessibility',
|
|
22
|
+
'responsive',
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
export const FRONTEND_SOURCE_CLAIMS = Object.freeze({
|
|
26
|
+
'code-arena-webdev': Object.freeze({
|
|
27
|
+
workloads: Object.freeze(['frontend-greenfield']),
|
|
28
|
+
axes: Object.freeze(['visual-preference']),
|
|
29
|
+
decisive: true,
|
|
30
|
+
}),
|
|
31
|
+
'openhands-frontend': Object.freeze({
|
|
32
|
+
workloads: Object.freeze(['frontend-repository-repair']),
|
|
33
|
+
axes: Object.freeze(['functional']),
|
|
34
|
+
decisive: true,
|
|
35
|
+
}),
|
|
36
|
+
vision2web: Object.freeze({
|
|
37
|
+
workloads: Object.freeze(['frontend-greenfield']),
|
|
38
|
+
axes: Object.freeze(['visual-fidelity', 'functional', 'responsive']),
|
|
39
|
+
decisive: false,
|
|
40
|
+
}),
|
|
41
|
+
design2code: Object.freeze({
|
|
42
|
+
workloads: Object.freeze(['frontend-greenfield']),
|
|
43
|
+
axes: Object.freeze(['visual-fidelity']),
|
|
44
|
+
decisive: false,
|
|
45
|
+
}),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
function nonEmptyString(value, field) {
|
|
49
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
50
|
+
throw new TypeError(`${field} must be a non-empty string`);
|
|
51
|
+
}
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function plainObject(value, field) {
|
|
56
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
57
|
+
throw new TypeError(`${field} must be an object`);
|
|
58
|
+
}
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function classifyFrontendWorkload(input) {
|
|
63
|
+
plainObject(input, 'frontend workload');
|
|
64
|
+
const lifecycle = nonEmptyString(input.lifecycle, 'frontend workload lifecycle');
|
|
65
|
+
const repositoryContext = nonEmptyString(
|
|
66
|
+
input.repositoryContext,
|
|
67
|
+
'frontend workload repositoryContext',
|
|
68
|
+
);
|
|
69
|
+
if (!Array.isArray(input.qualityAxes) || input.qualityAxes.length === 0) {
|
|
70
|
+
throw new TypeError('frontend workload qualityAxes must be a non-empty array');
|
|
71
|
+
}
|
|
72
|
+
if (new Set(input.qualityAxes).size !== input.qualityAxes.length) {
|
|
73
|
+
throw new TypeError('frontend workload qualityAxes must be unique');
|
|
74
|
+
}
|
|
75
|
+
for (const axis of input.qualityAxes) {
|
|
76
|
+
if (!QUALITY_AXES.has(axis)) throw new TypeError(`unknown frontend quality axis: ${axis}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let workload;
|
|
80
|
+
let supportedAxes;
|
|
81
|
+
if (lifecycle === 'greenfield' && repositoryContext === 'isolated') {
|
|
82
|
+
workload = 'frontend-greenfield';
|
|
83
|
+
supportedAxes = FRONTEND_SOURCE_CLAIMS['code-arena-webdev'].axes;
|
|
84
|
+
} else if (
|
|
85
|
+
['edit', 'repair'].includes(lifecycle)
|
|
86
|
+
&& repositoryContext === 'existing-repository'
|
|
87
|
+
) {
|
|
88
|
+
workload = 'frontend-repository-repair';
|
|
89
|
+
supportedAxes = FRONTEND_SOURCE_CLAIMS['openhands-frontend'].axes;
|
|
90
|
+
} else {
|
|
91
|
+
throw new TypeError(
|
|
92
|
+
`unsupported frontend workload mapping: ${lifecycle}/${repositoryContext}`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const frontendDomain = input.frontendDomain ?? 'general';
|
|
97
|
+
if (!FRONTEND_DOMAINS.has(frontendDomain)) {
|
|
98
|
+
throw new TypeError(`unknown frontend domain: ${frontendDomain}`);
|
|
99
|
+
}
|
|
100
|
+
const evidenceAxes = input.qualityAxes.filter((axis) => supportedAxes.includes(axis));
|
|
101
|
+
const unsupportedAxes = input.qualityAxes.filter((axis) => !supportedAxes.includes(axis));
|
|
102
|
+
return Object.freeze({
|
|
103
|
+
evidenceSelection: validateEvidenceSelection({
|
|
104
|
+
workload,
|
|
105
|
+
domain: frontendDomain,
|
|
106
|
+
axes: evidenceAxes,
|
|
107
|
+
}),
|
|
108
|
+
repositoryContext,
|
|
109
|
+
unsupportedAxes: Object.freeze(unsupportedAxes),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function frontendEvidenceWorkload({ workload, frontendDomain = 'general', axis }) {
|
|
114
|
+
if (!['frontend-greenfield', 'frontend-repository-repair'].includes(workload)) {
|
|
115
|
+
throw new TypeError(`unknown frontend evidence workload: ${workload}`);
|
|
116
|
+
}
|
|
117
|
+
if (!FRONTEND_DOMAINS.has(frontendDomain)) {
|
|
118
|
+
throw new TypeError(`unknown frontend domain: ${frontendDomain}`);
|
|
119
|
+
}
|
|
120
|
+
if (!QUALITY_AXES.has(axis)) throw new TypeError(`unknown frontend quality axis: ${axis}`);
|
|
121
|
+
return evidenceWorkloadIdentity({
|
|
122
|
+
workload,
|
|
123
|
+
domain: frontendDomain,
|
|
124
|
+
axes: [axis],
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function createFrontendRouteReason(observation) {
|
|
129
|
+
plainObject(observation, 'frontend observation');
|
|
130
|
+
const [workload, frontendDomain, axis, ...rest] =
|
|
131
|
+
nonEmptyString(observation.workload, 'frontend observation workload').split(':');
|
|
132
|
+
if (rest.length > 0 || !workload || !frontendDomain || !axis) {
|
|
133
|
+
throw new TypeError('frontend observation workload must name workload, domain, and axis');
|
|
134
|
+
}
|
|
135
|
+
const sourceId = nonEmptyString(
|
|
136
|
+
observation.source?.benchmark,
|
|
137
|
+
'frontend observation source benchmark',
|
|
138
|
+
);
|
|
139
|
+
return `${workload} (${frontendDomain}) is supported on ${axis} by ${sourceId}`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function evaluateVision2WebReadiness(input) {
|
|
143
|
+
plainObject(input, 'Vision2Web leaderboard');
|
|
144
|
+
const season = nonEmptyString(input.season, 'Vision2Web season');
|
|
145
|
+
const benchmarkVersion = nonEmptyString(
|
|
146
|
+
input.benchmarkVersion,
|
|
147
|
+
'Vision2Web benchmarkVersion',
|
|
148
|
+
);
|
|
149
|
+
if (!Array.isArray(input.results)) {
|
|
150
|
+
throw new TypeError('Vision2Web results must be an array');
|
|
151
|
+
}
|
|
152
|
+
if (input.results.length === 0) {
|
|
153
|
+
return Object.freeze({
|
|
154
|
+
sourceId: 'vision2web',
|
|
155
|
+
status: 'candidate',
|
|
156
|
+
season,
|
|
157
|
+
benchmarkVersion,
|
|
158
|
+
observations: Object.freeze([]),
|
|
159
|
+
reason: 'current-season-leaderboard-empty',
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
return Object.freeze({
|
|
163
|
+
sourceId: 'vision2web',
|
|
164
|
+
status: 'awaiting-owner-adapter-validation',
|
|
165
|
+
season,
|
|
166
|
+
benchmarkVersion,
|
|
167
|
+
observations: Object.freeze([]),
|
|
168
|
+
reason: 'results-require-owner-adapter',
|
|
169
|
+
});
|
|
170
|
+
}
|
|
@@ -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
|
+
}
|