@ikon85/agent-workflow-kit 0.44.1 → 0.45.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.
- package/.agents/skills/audit-skills/SKILL.md +7 -4
- package/.agents/skills/code-review/SKILL.md +7 -4
- package/.agents/skills/codebase-design/DESIGN-IT-TWICE.md +7 -4
- package/.agents/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +7 -4
- package/.agents/skills/improve-codebase-architecture/SKILL.md +7 -4
- package/.agents/skills/orchestrate-wave/SKILL.md +1 -1
- package/.agents/skills/orchestrate-wave/references/dispatch-subagents.md +9 -0
- package/.agents/skills/orchestrate-wave/references/dispatch-workflow.md +9 -0
- package/.agents/skills/research/SKILL.md +7 -4
- package/.agents/skills/to-issues/SKILL.md +25 -4
- package/.claude/skills/audit-skills/SKILL.md +7 -4
- package/.claude/skills/code-review/SKILL.md +7 -4
- package/.claude/skills/codebase-design/DESIGN-IT-TWICE.md +7 -4
- package/.claude/skills/codex-build/SKILL.md +13 -0
- package/.claude/skills/codex-review/SKILL.md +13 -0
- package/.claude/skills/grill-me-codex/SKILL.md +14 -0
- package/.claude/skills/grill-with-docs-codex/SKILL.md +14 -0
- package/.claude/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +7 -4
- package/.claude/skills/improve-codebase-architecture/SKILL.md +7 -4
- package/.claude/skills/orchestrate-wave/SKILL.md +1 -1
- package/.claude/skills/orchestrate-wave/references/dispatch-subagents.md +9 -0
- package/.claude/skills/orchestrate-wave/references/dispatch-workflow.md +9 -0
- package/.claude/skills/research/SKILL.md +7 -4
- package/.claude/skills/skill-manifest.json +34 -23
- package/.claude/skills/to-issues/SKILL.md +25 -4
- package/README.md +71 -0
- package/agent-workflow-kit.package.json +149 -45
- package/package.json +1 -1
- package/scripts/doctrine-migration/index.mjs +296 -0
- package/scripts/kit-release.mjs +41 -9
- package/src/cli.mjs +521 -80
- package/src/commands/routing-status.mjs +288 -0
- package/src/commands/update.mjs +9 -1
- package/src/consumer-migrations.json +23 -1
- package/src/lib/bundle.mjs +158 -2
- package/src/lib/consumerMigrations.mjs +55 -0
- package/src/lib/dispatchJournal.mjs +300 -0
- package/src/lib/dispatchPlan.mjs +286 -0
- package/src/lib/dispatchReceipt.mjs +226 -89
- package/src/lib/frontendWorkloads.mjs +35 -33
- package/src/lib/routeDispatcher.mjs +367 -70
- package/src/lib/routingAccessGraph.mjs +265 -20
- package/src/lib/routingAccessGraphStore.mjs +300 -0
- package/src/lib/routingAdapters/claude.mjs +104 -4
- package/src/lib/routingAdapters/codex.mjs +132 -7
- package/src/lib/routingAdapters/hostBridge.mjs +291 -0
- package/src/lib/routingCatalog.mjs +201 -24
- package/src/lib/routingDispatchLease.mjs +253 -0
- package/src/lib/routingEvidenceCache.mjs +78 -0
- package/src/lib/routingIntent.mjs +176 -10
- package/src/lib/routingIntentClassifier.mjs +137 -0
- package/src/lib/routingInventory/snapshots/claude.json +49 -0
- package/src/lib/routingInventory/snapshots/codex.json +109 -0
- package/src/lib/routingInventory.mjs +182 -0
- package/src/lib/routingPolicy.mjs +193 -6
- package/src/lib/routingProfile.mjs +1251 -123
- package/src/lib/routingProfilePolicy.mjs +156 -0
- package/src/lib/routingProfileStorage.mjs +299 -0
- package/src/lib/routingResolver.mjs +369 -86
- package/src/lib/routingSources/artificialAnalysis.mjs +19 -7
- package/src/lib/routingSources/benchlm.mjs +5 -0
- package/src/lib/routingSources/codeArena.mjs +13 -3
- package/src/lib/routingSources/deepswe.mjs +19 -5
- package/src/lib/routingSources/openhands.mjs +17 -4
- package/src/lib/routingSources/openhandsFrontend.mjs +13 -3
- package/src/lib/safeText.mjs +26 -0
- package/src/lib/updateCandidate.mjs +36 -3
|
@@ -1,29 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Dispatch receipt — the runtime record proving which requested Route
|
|
3
|
+
* decision was actually applied, by which enforcement mechanism, under which
|
|
4
|
+
* policy and evidence revisions.
|
|
5
|
+
*
|
|
6
|
+
* A receipt has one of four kinds, and each validates its own field set rather
|
|
7
|
+
* than a union of everything any dispatch could ever carry. `routed-dispatch`
|
|
8
|
+
* ran the resolved route, applied matching requested field for field, and
|
|
9
|
+
* `blocked` did not run at all. `inherited-dispatch` ran the session default: it
|
|
10
|
+
* is a receipt only when it carries the *attested* applied pair and the
|
|
11
|
+
* session-default enforcement method, because the inherited pair may
|
|
12
|
+
* legitimately differ from the route that was requested. `handoff` dispatched
|
|
13
|
+
* nothing and handed the work on, so it records no applied route and no
|
|
14
|
+
* enforcement and can never read as proof that something ran.
|
|
15
|
+
*
|
|
16
|
+
* Every kind names the revisions it decided under (`policyRevision`,
|
|
17
|
+
* `catalogRevision`, `decisionAccessRevision`) plus the plan-authorization id
|
|
18
|
+
* it dispatched against; a blocked receipt additionally names
|
|
19
|
+
* `resultingAccessRevision` when the failure itself mutated the Access graph.
|
|
20
|
+
*/
|
|
1
21
|
export const DISPATCH_RECEIPT_VERSION = 2;
|
|
2
22
|
|
|
3
|
-
const
|
|
4
|
-
'
|
|
5
|
-
|
|
6
|
-
'effort',
|
|
7
|
-
'surfaceId',
|
|
8
|
-
'transportId',
|
|
9
|
-
];
|
|
23
|
+
export const DISPATCH_RECEIPT_KINDS = Object.freeze([
|
|
24
|
+
'routed-dispatch', 'inherited-dispatch', 'handoff', 'blocked',
|
|
25
|
+
]);
|
|
10
26
|
|
|
11
|
-
const
|
|
12
|
-
'
|
|
13
|
-
'
|
|
14
|
-
'
|
|
15
|
-
'
|
|
16
|
-
|
|
27
|
+
const KIND_STATUS = Object.freeze({
|
|
28
|
+
'routed-dispatch': 'dispatched',
|
|
29
|
+
'inherited-dispatch': 'dispatched',
|
|
30
|
+
handoff: 'handoff',
|
|
31
|
+
blocked: 'blocked',
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
/** What a caller that only knows the legacy outcome axis means by its status. */
|
|
35
|
+
const STATUS_KIND = Object.freeze({
|
|
36
|
+
dispatched: 'routed-dispatch', handoff: 'handoff', blocked: 'blocked',
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const ROUTE_FIELDS = ['providerId', 'modelId', 'effort', 'surfaceId', 'transportId'];
|
|
40
|
+
|
|
41
|
+
const ENFORCEMENT_METHODS = ['per-spawn', 'named-agent', 'session-default', 'none'];
|
|
17
42
|
|
|
18
43
|
export const DISPATCH_PRECEDENCE = Object.freeze([
|
|
19
|
-
'explicit-argument',
|
|
20
|
-
'agent-definition-
|
|
21
|
-
'environment-over-agent-definition',
|
|
22
|
-
'session-default',
|
|
23
|
-
'uncontrolled',
|
|
24
|
-
'unreported',
|
|
44
|
+
'explicit-argument', 'agent-definition-over-environment',
|
|
45
|
+
'environment-over-agent-definition', 'session-default', 'uncontrolled', 'unreported',
|
|
25
46
|
]);
|
|
26
47
|
|
|
48
|
+
/**
|
|
49
|
+
* The readback channels that may attest an applied pair, and what each proves
|
|
50
|
+
* per axis. A channel absent from this table is not an attestation:
|
|
51
|
+
* `~/.claude/settings.json` reported `medium` for a session that ran at `xhigh`,
|
|
52
|
+
* so a configured value never attests an applied one. Claude's transcript
|
|
53
|
+
* returns `message.model` from the server while its top-level `effort` is the
|
|
54
|
+
* CLI's own record; Codex's `turn_context` is client-written throughout, so a
|
|
55
|
+
* Codex receipt cannot claim provider attestation.
|
|
56
|
+
*/
|
|
57
|
+
export const APPLIED_PAIR_ATTESTATION_SOURCES = Object.freeze({
|
|
58
|
+
'session-transcript': Object.freeze({ model: true, effort: false }),
|
|
59
|
+
'rollout-turn-context': Object.freeze({ model: false, effort: false }),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const COMMON_FIELDS = [
|
|
63
|
+
'kind', 'status', 'executionId', 'afk', 'revisions', 'authorizationId', 'dispatchedAt',
|
|
64
|
+
];
|
|
65
|
+
const APPLIED_FIELDS = ['requestedRoute', 'appliedRoute', 'enforcement', 'precedence'];
|
|
66
|
+
|
|
67
|
+
/** The closed field set of each kind — what it may carry, and nothing else. */
|
|
68
|
+
const KIND_FIELDS = Object.freeze({
|
|
69
|
+
'routed-dispatch': new Set([...COMMON_FIELDS, ...APPLIED_FIELDS]),
|
|
70
|
+
'inherited-dispatch': new Set([...COMMON_FIELDS, ...APPLIED_FIELDS, 'attestation']),
|
|
71
|
+
handoff: new Set([...COMMON_FIELDS, 'requestedRoute', 'handoff', 'reason']),
|
|
72
|
+
blocked: new Set([...COMMON_FIELDS, ...APPLIED_FIELDS, 'reason', 'resultingAccessRevision']),
|
|
73
|
+
});
|
|
74
|
+
|
|
27
75
|
function object(value, field) {
|
|
28
76
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
29
77
|
throw new TypeError(`${field} must be an object`);
|
|
@@ -31,6 +79,13 @@ function object(value, field) {
|
|
|
31
79
|
return value;
|
|
32
80
|
}
|
|
33
81
|
|
|
82
|
+
function closedFields(value, allowed, message) {
|
|
83
|
+
for (const key of Object.keys(value)) {
|
|
84
|
+
if (!allowed.includes(key)) throw new TypeError(`${message}: ${key}`);
|
|
85
|
+
}
|
|
86
|
+
return value;
|
|
87
|
+
}
|
|
88
|
+
|
|
34
89
|
function string(value, field) {
|
|
35
90
|
if (typeof value !== 'string' || value.trim() === '') {
|
|
36
91
|
throw new TypeError(`${field} must be a non-empty string`);
|
|
@@ -38,6 +93,8 @@ function string(value, field) {
|
|
|
38
93
|
return value;
|
|
39
94
|
}
|
|
40
95
|
|
|
96
|
+
const optionalString = (value, field) => (value == null ? null : string(value, field));
|
|
97
|
+
|
|
41
98
|
function timestamp(value, field) {
|
|
42
99
|
string(value, field);
|
|
43
100
|
if (!Number.isFinite(Date.parse(value))) throw new TypeError(`${field} must be an ISO timestamp`);
|
|
@@ -45,42 +102,31 @@ function timestamp(value, field) {
|
|
|
45
102
|
}
|
|
46
103
|
|
|
47
104
|
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
|
-
}
|
|
105
|
+
closedFields(object(input, field), ROUTE_FIELDS, `unknown ${field} field`);
|
|
52
106
|
return Object.freeze(Object.fromEntries(
|
|
53
107
|
ROUTE_FIELDS.map((key) => [key, string(input[key], `${field}.${key}`)]),
|
|
54
108
|
));
|
|
55
109
|
}
|
|
56
110
|
|
|
57
|
-
|
|
58
|
-
|
|
111
|
+
const optionalRoute = (input, field) => (input == null ? null : route(input, field));
|
|
112
|
+
|
|
113
|
+
function pair(input, field, allowed) {
|
|
114
|
+
object(input, field);
|
|
59
115
|
const result = {};
|
|
60
|
-
for (const
|
|
61
|
-
if (!
|
|
62
|
-
throw new TypeError(`${field}
|
|
116
|
+
for (const axis of ['model', 'effort']) {
|
|
117
|
+
if (!allowed.includes(input[axis])) {
|
|
118
|
+
throw new TypeError(`${axis} ${field} must be one of: ${allowed.join(', ')}`);
|
|
63
119
|
}
|
|
64
|
-
result[
|
|
120
|
+
result[axis] = input[axis];
|
|
65
121
|
}
|
|
66
122
|
return Object.freeze(result);
|
|
67
123
|
}
|
|
68
124
|
|
|
125
|
+
const enforcement = (input) => pair(input, 'enforcement', ENFORCEMENT_METHODS);
|
|
126
|
+
|
|
69
127
|
function precedence(input) {
|
|
70
|
-
if (input == null) {
|
|
71
|
-
|
|
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);
|
|
128
|
+
if (input == null) return Object.freeze({ model: 'unreported', effort: 'unreported' });
|
|
129
|
+
return pair(input, 'precedence', DISPATCH_PRECEDENCE);
|
|
84
130
|
}
|
|
85
131
|
|
|
86
132
|
function revisions(input) {
|
|
@@ -92,42 +138,84 @@ function revisions(input) {
|
|
|
92
138
|
});
|
|
93
139
|
}
|
|
94
140
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
throw new TypeError('blocked applied route requires a requested route');
|
|
141
|
+
/** The kind a caller asked for, or the one its legacy status still implies. */
|
|
142
|
+
function receiptKind(input) {
|
|
143
|
+
if (input.kind == null) {
|
|
144
|
+
const derived = STATUS_KIND[input.status];
|
|
145
|
+
if (derived) return derived;
|
|
146
|
+
throw new TypeError(
|
|
147
|
+
`dispatch receipt needs a kind, or a status of: ${Object.keys(STATUS_KIND).join(', ')}`,
|
|
148
|
+
);
|
|
104
149
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
150
|
+
if (!DISPATCH_RECEIPT_KINDS.includes(input.kind)) {
|
|
151
|
+
throw new TypeError(
|
|
152
|
+
`dispatch receipt kind must be one of: ${DISPATCH_RECEIPT_KINDS.join(', ')}`,
|
|
153
|
+
);
|
|
109
154
|
}
|
|
110
|
-
|
|
155
|
+
if (input.status != null && input.status !== KIND_STATUS[input.kind]) {
|
|
156
|
+
throw new TypeError(`${input.kind} receipt status must be ${KIND_STATUS[input.kind]}`);
|
|
157
|
+
}
|
|
158
|
+
return input.kind;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* `resultingAccessRevision` is the Access-graph revision *after* a failure
|
|
163
|
+
* mutated the graph, so it belongs to a blocked receipt and must differ from the
|
|
164
|
+
* decision's own — repeating that one names a mutation that never happened.
|
|
165
|
+
*/
|
|
166
|
+
function namedRevisions(input, kind, decided) {
|
|
167
|
+
const resulting = optionalString(input.resultingAccessRevision, 'resultingAccessRevision');
|
|
168
|
+
if (resulting !== null && resulting === decided.accessGraph) {
|
|
169
|
+
throw new TypeError('resultingAccessRevision names no access graph mutation');
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
catalogRevision: decided.catalog,
|
|
173
|
+
decisionAccessRevision: decided.accessGraph,
|
|
174
|
+
policyRevision: decided.policy,
|
|
175
|
+
resultingAccessRevision: kind === 'blocked' ? resulting : null,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function baseReceipt(input, kind) {
|
|
180
|
+
const decided = revisions(input.revisions);
|
|
181
|
+
return {
|
|
111
182
|
schemaVersion: DISPATCH_RECEIPT_VERSION,
|
|
183
|
+
kind,
|
|
112
184
|
executionId: string(input.executionId, 'executionId'),
|
|
113
|
-
status:
|
|
185
|
+
status: KIND_STATUS[kind],
|
|
114
186
|
afk: input.afk === true,
|
|
115
|
-
requestedRoute,
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
187
|
+
requestedRoute: null, appliedRoute: null, enforcement: null, precedence: null,
|
|
188
|
+
attestation: null, handoff: null,
|
|
189
|
+
revisions: decided,
|
|
190
|
+
...namedRevisions(input, kind, decided),
|
|
191
|
+
authorizationId: optionalString(input.authorizationId, 'authorizationId'),
|
|
120
192
|
dispatchedAt: timestamp(input.dispatchedAt, 'dispatchedAt'),
|
|
121
|
-
reason:
|
|
122
|
-
}
|
|
193
|
+
reason: null,
|
|
194
|
+
};
|
|
123
195
|
}
|
|
124
196
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if (input
|
|
128
|
-
|
|
129
|
-
|
|
197
|
+
/** The attested applied pair: which channel read it back, and what that proves. */
|
|
198
|
+
function appliedPairAttestation(input, appliedRoute) {
|
|
199
|
+
if (input == null) throw new TypeError('inherited dispatch requires an attested applied pair');
|
|
200
|
+
const sources = Object.keys(APPLIED_PAIR_ATTESTATION_SOURCES);
|
|
201
|
+
closedFields(object(input, 'attestation'),
|
|
202
|
+
['source', 'model', 'effort', 'observedAt'], 'unknown attestation field');
|
|
203
|
+
if (!sources.includes(input.source)) {
|
|
204
|
+
throw new TypeError(`attestation source must be one of: ${sources.join(', ')}`);
|
|
205
|
+
}
|
|
206
|
+
const model = string(input.model, 'attestation.model');
|
|
207
|
+
const effort = string(input.effort, 'attestation.effort');
|
|
208
|
+
if (model !== appliedRoute.modelId || effort !== appliedRoute.effort) {
|
|
209
|
+
throw new Error('attested applied pair differs from the applied route');
|
|
130
210
|
}
|
|
211
|
+
return Object.freeze({
|
|
212
|
+
source: input.source, model, effort,
|
|
213
|
+
observedAt: timestamp(input.observedAt, 'attestation.observedAt'),
|
|
214
|
+
providerAttested: APPLIED_PAIR_ATTESTATION_SOURCES[input.source],
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function routedDispatch(input, base) {
|
|
131
219
|
const requestedRoute = route(input.requestedRoute, 'requestedRoute');
|
|
132
220
|
const appliedRoute = route(input.appliedRoute, 'appliedRoute');
|
|
133
221
|
for (const field of ROUTE_FIELDS) {
|
|
@@ -135,28 +223,77 @@ export function createDispatchReceipt(input) {
|
|
|
135
223
|
throw new Error(`applied route differs from requested route: ${field}`);
|
|
136
224
|
}
|
|
137
225
|
}
|
|
138
|
-
const
|
|
139
|
-
const
|
|
140
|
-
if (
|
|
141
|
-
&& (appliedEnforcement.model === 'none' || appliedEnforcement.effort === 'none')) {
|
|
226
|
+
const applied = enforcement(input.enforcement);
|
|
227
|
+
const order = precedence(input.precedence);
|
|
228
|
+
if (base.afk && (applied.model === 'none' || applied.effort === 'none')) {
|
|
142
229
|
throw new Error('AFK dispatch requires enforced model and effort selection');
|
|
143
230
|
}
|
|
144
|
-
if (
|
|
145
|
-
|
|
146
|
-
value === 'uncontrolled' || value === 'unreported')) {
|
|
231
|
+
if (base.afk && Object.values(order).some((value) =>
|
|
232
|
+
value === 'uncontrolled' || value === 'unreported')) {
|
|
147
233
|
throw new Error('AFK dispatch requires verified environment precedence');
|
|
148
234
|
}
|
|
149
|
-
return
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
235
|
+
return { ...base, requestedRoute, appliedRoute, enforcement: applied, precedence: order };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Inheritance is the constrained non-AFK path: the applied pair is whatever the
|
|
240
|
+
* session already runs, so it proves nothing without its attestation.
|
|
241
|
+
*/
|
|
242
|
+
function inheritedDispatch(input, base) {
|
|
243
|
+
const appliedRoute = route(input.appliedRoute, 'appliedRoute');
|
|
244
|
+
const applied = enforcement(input.enforcement);
|
|
245
|
+
if (applied.model !== 'session-default' || applied.effort !== 'session-default') {
|
|
246
|
+
throw new TypeError('inherited dispatch requires the session-default enforcement method');
|
|
247
|
+
}
|
|
248
|
+
if (base.afk) throw new Error('AFK dispatch cannot inherit a session default');
|
|
249
|
+
return {
|
|
250
|
+
...base,
|
|
251
|
+
requestedRoute: optionalRoute(input.requestedRoute, 'requestedRoute'),
|
|
155
252
|
appliedRoute,
|
|
156
|
-
enforcement:
|
|
157
|
-
precedence:
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
253
|
+
enforcement: applied,
|
|
254
|
+
precedence: precedence(input.precedence),
|
|
255
|
+
attestation: appliedPairAttestation(input.attestation, appliedRoute),
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function handoffReceipt(input, base) {
|
|
260
|
+
const target = closedFields(object(input.handoff, 'handoff'), ['to'], 'unknown handoff field');
|
|
261
|
+
return {
|
|
262
|
+
...base,
|
|
263
|
+
requestedRoute: optionalRoute(input.requestedRoute, 'requestedRoute'),
|
|
264
|
+
handoff: Object.freeze({ to: string(target.to, 'handoff.to') }),
|
|
265
|
+
reason: string(input.reason, 'reason'),
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function blockedReceipt(input, base) {
|
|
270
|
+
const requestedRoute = optionalRoute(input.requestedRoute, 'requestedRoute');
|
|
271
|
+
const appliedRoute = optionalRoute(input.appliedRoute, 'appliedRoute');
|
|
272
|
+
if (appliedRoute !== null && requestedRoute === null) {
|
|
273
|
+
throw new TypeError('blocked applied route requires a requested route');
|
|
274
|
+
}
|
|
275
|
+
const applied = input.enforcement == null ? null : enforcement(input.enforcement);
|
|
276
|
+
const order = input.precedence == null ? null : precedence(input.precedence);
|
|
277
|
+
if (appliedRoute !== null && (applied === null || order === null)) {
|
|
278
|
+
throw new TypeError('blocked applied route requires enforcement and precedence');
|
|
279
|
+
}
|
|
280
|
+
return {
|
|
281
|
+
...base,
|
|
282
|
+
requestedRoute, appliedRoute, enforcement: applied, precedence: order,
|
|
283
|
+
reason: string(input.reason, 'reason'),
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const BUILDERS = Object.freeze({
|
|
288
|
+
'routed-dispatch': routedDispatch,
|
|
289
|
+
'inherited-dispatch': inheritedDispatch,
|
|
290
|
+
handoff: handoffReceipt,
|
|
291
|
+
blocked: blockedReceipt,
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
export function createDispatchReceipt(input) {
|
|
295
|
+
object(input, 'dispatch receipt');
|
|
296
|
+
const kind = receiptKind(input);
|
|
297
|
+
closedFields(input, [...KIND_FIELDS[kind]], `unknown ${kind} receipt field`);
|
|
298
|
+
return Object.freeze(BUILDERS[kind](input, baseReceipt(input, kind)));
|
|
162
299
|
}
|
|
@@ -2,46 +2,47 @@ import {
|
|
|
2
2
|
evidenceWorkloadIdentity,
|
|
3
3
|
validateEvidenceSelection,
|
|
4
4
|
} from './routingIntent.mjs';
|
|
5
|
+
import {
|
|
6
|
+
FRONTEND_QUALITY_AXES,
|
|
7
|
+
evidenceDomainsFor,
|
|
8
|
+
evidenceIdentity,
|
|
9
|
+
evidenceSourceClaim,
|
|
10
|
+
} from './routingCatalog.mjs';
|
|
5
11
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
'
|
|
11
|
-
'
|
|
12
|
-
'game',
|
|
13
|
-
'simulation',
|
|
14
|
-
'editor',
|
|
12
|
+
// The frontend vocabulary is a strict subset of the researched taxonomy
|
|
13
|
+
// (docs/research/agent-task-taxonomy-benchmark-coverage.md §1.1): it reads its
|
|
14
|
+
// domains and quality axes from the catalog rather than restating them.
|
|
15
|
+
const FRONTEND_WORKLOADS = Object.freeze([
|
|
16
|
+
'frontend-greenfield',
|
|
17
|
+
'frontend-repository-repair',
|
|
15
18
|
]);
|
|
19
|
+
const FRONTEND_DOMAINS = new Set(evidenceDomainsFor(FRONTEND_WORKLOADS[0]));
|
|
20
|
+
const QUALITY_AXES = new Set(FRONTEND_QUALITY_AXES);
|
|
16
21
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
// Each source carries the three-part decisiveness test instead of a boolean, so
|
|
23
|
+
// a collapsed dimension is named rather than hidden (§4).
|
|
24
|
+
const frontendClaim = (sourceId, { workloads, axes }) => Object.freeze({
|
|
25
|
+
workloads: Object.freeze(workloads),
|
|
26
|
+
axes: Object.freeze(axes),
|
|
27
|
+
...evidenceSourceClaim(sourceId),
|
|
28
|
+
});
|
|
24
29
|
|
|
25
30
|
export const FRONTEND_SOURCE_CLAIMS = Object.freeze({
|
|
26
|
-
'code-arena-webdev':
|
|
27
|
-
workloads:
|
|
28
|
-
axes:
|
|
29
|
-
decisive: true,
|
|
31
|
+
'code-arena-webdev': frontendClaim('code-arena-webdev', {
|
|
32
|
+
workloads: ['frontend-greenfield'],
|
|
33
|
+
axes: ['visual-preference'],
|
|
30
34
|
}),
|
|
31
|
-
'openhands-frontend':
|
|
32
|
-
workloads:
|
|
33
|
-
axes:
|
|
34
|
-
decisive: true,
|
|
35
|
+
'openhands-frontend': frontendClaim('openhands-frontend', {
|
|
36
|
+
workloads: ['frontend-repository-repair'],
|
|
37
|
+
axes: ['functional'],
|
|
35
38
|
}),
|
|
36
|
-
vision2web:
|
|
37
|
-
workloads:
|
|
38
|
-
axes:
|
|
39
|
-
decisive: false,
|
|
39
|
+
vision2web: frontendClaim('vision2web', {
|
|
40
|
+
workloads: ['frontend-greenfield'],
|
|
41
|
+
axes: ['visual-fidelity', 'functional', 'responsive'],
|
|
40
42
|
}),
|
|
41
|
-
design2code:
|
|
42
|
-
workloads:
|
|
43
|
-
axes:
|
|
44
|
-
decisive: false,
|
|
43
|
+
design2code: frontendClaim('design2code', {
|
|
44
|
+
workloads: ['frontend-greenfield'],
|
|
45
|
+
axes: ['visual-fidelity'],
|
|
45
46
|
}),
|
|
46
47
|
});
|
|
47
48
|
|
|
@@ -111,13 +112,14 @@ export function classifyFrontendWorkload(input) {
|
|
|
111
112
|
}
|
|
112
113
|
|
|
113
114
|
export function frontendEvidenceWorkload({ workload, frontendDomain = 'general', axis }) {
|
|
114
|
-
if (!
|
|
115
|
+
if (!FRONTEND_WORKLOADS.includes(workload)) {
|
|
115
116
|
throw new TypeError(`unknown frontend evidence workload: ${workload}`);
|
|
116
117
|
}
|
|
117
118
|
if (!FRONTEND_DOMAINS.has(frontendDomain)) {
|
|
118
119
|
throw new TypeError(`unknown frontend domain: ${frontendDomain}`);
|
|
119
120
|
}
|
|
120
121
|
if (!QUALITY_AXES.has(axis)) throw new TypeError(`unknown frontend quality axis: ${axis}`);
|
|
122
|
+
evidenceIdentity({ workload, domain: frontendDomain, axis });
|
|
121
123
|
return evidenceWorkloadIdentity({
|
|
122
124
|
workload,
|
|
123
125
|
domain: frontendDomain,
|