@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.
- 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-update/SKILL.md +13 -0
- package/.agents/skills/orchestrate-wave/SKILL.md +54 -54
- package/.agents/skills/orchestrate-wave/references/builder-contract.md +16 -3
- package/.agents/skills/orchestrate-wave/references/dispatch-subagents.md +91 -0
- package/.agents/skills/orchestrate-wave/references/dispatch-workflow.md +66 -0
- package/.agents/skills/orchestrate-wave/references/report-contracts.md +42 -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-update/SKILL.md +13 -0
- package/.claude/skills/orchestrate-wave/SKILL.md +54 -54
- package/.claude/skills/orchestrate-wave/references/builder-contract.md +16 -3
- package/.claude/skills/orchestrate-wave/references/dispatch-subagents.md +91 -0
- package/.claude/skills/orchestrate-wave/references/dispatch-workflow.md +66 -0
- package/.claude/skills/orchestrate-wave/references/report-contracts.md +42 -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 +126 -11
- package/agent-workflow-kit.package.json +291 -39
- package/docs/adr/0002-capability-gated-orchestration.md +70 -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/test_codex_adapter_sync_contract.py +30 -15
- package/scripts/test_orchestrate_wave_contract.py +209 -0
- package/scripts/test_program_planning_contract.py +70 -0
- package/scripts/test_skill_portability_lint.py +54 -0
- package/scripts/test_worktree_setup_base_guard.py +140 -0
- package/scripts/worktree-lifecycle/setup.py +40 -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 +34 -0
- package/src/lib/capabilityMatrix.mjs +179 -0
- package/src/lib/dispatchReceipt.mjs +162 -0
- package/src/lib/frontendWorkloads.mjs +170 -0
- package/src/lib/reconcileReconReports.mjs +111 -0
- package/src/lib/reportValidator.mjs +186 -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
- package/src/lib/waveClaim.mjs +134 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import { mkdir, open, readFile, rm } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join, resolve } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { writeAtomic } from './atomicWrite.mjs';
|
|
6
|
+
import {
|
|
7
|
+
AGENT_SURFACE_REGISTRY,
|
|
8
|
+
AGENT_SURFACE_REGISTRY_REVISION,
|
|
9
|
+
detectAgentSurfaces,
|
|
10
|
+
surfaceById,
|
|
11
|
+
} from './agentSurfaceRegistry.mjs';
|
|
12
|
+
|
|
13
|
+
export const ROUTING_PROFILE_VERSION = 1;
|
|
14
|
+
export const ROUTING_PROFILE_PATH = 'routing-profile.json';
|
|
15
|
+
|
|
16
|
+
const SWITCHING = Object.freeze(['automatic', 'ask', 'current-surface-only']);
|
|
17
|
+
const ACTIVATION = Object.freeze(['approve', 'back', 'advanced', 'safe-current-surface', 'decline']);
|
|
18
|
+
const PROFILE_FIELDS = new Set([
|
|
19
|
+
'schemaVersion', 'registryRevision', 'selectedSurfaces', 'consideredSurfaces',
|
|
20
|
+
'switching', 'advanced',
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
function uniqueStrings(value, field) {
|
|
24
|
+
if (!Array.isArray(value) ||
|
|
25
|
+
!value.every((entry) => typeof entry === 'string' && entry.trim() !== '')) {
|
|
26
|
+
throw new TypeError(`${field} must be an array of non-empty strings`);
|
|
27
|
+
}
|
|
28
|
+
return [...new Set(value)];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function validateRoutingProfile(input) {
|
|
32
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
33
|
+
throw new TypeError('routing profile must be an object');
|
|
34
|
+
}
|
|
35
|
+
for (const key of Object.keys(input)) {
|
|
36
|
+
if (!PROFILE_FIELDS.has(key)) throw new TypeError(`unknown routing profile field: ${key}`);
|
|
37
|
+
}
|
|
38
|
+
if (input.schemaVersion !== ROUTING_PROFILE_VERSION) {
|
|
39
|
+
throw new TypeError(`routing profile schemaVersion must be ${ROUTING_PROFILE_VERSION}`);
|
|
40
|
+
}
|
|
41
|
+
if (!Number.isInteger(input.registryRevision) || input.registryRevision < 0) {
|
|
42
|
+
throw new TypeError('routing profile registryRevision must be a non-negative integer');
|
|
43
|
+
}
|
|
44
|
+
const selectedSurfaces = uniqueStrings(input.selectedSurfaces, 'selectedSurfaces');
|
|
45
|
+
if (!selectedSurfaces.length) throw new TypeError('selectedSurfaces must not be empty');
|
|
46
|
+
const consideredSurfaces = uniqueStrings(
|
|
47
|
+
input.consideredSurfaces ?? selectedSurfaces,
|
|
48
|
+
'consideredSurfaces',
|
|
49
|
+
);
|
|
50
|
+
if (selectedSurfaces.some((id) => !consideredSurfaces.includes(id))) {
|
|
51
|
+
throw new TypeError('consideredSurfaces must include every selected surface');
|
|
52
|
+
}
|
|
53
|
+
if (!SWITCHING.includes(input.switching)) {
|
|
54
|
+
throw new TypeError(`switching must be one of: ${SWITCHING.join(', ')}`);
|
|
55
|
+
}
|
|
56
|
+
if (input.advanced !== null &&
|
|
57
|
+
(!input.advanced || typeof input.advanced !== 'object' || Array.isArray(input.advanced))) {
|
|
58
|
+
throw new TypeError('advanced must be an object or null');
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
schemaVersion: ROUTING_PROFILE_VERSION,
|
|
62
|
+
registryRevision: input.registryRevision,
|
|
63
|
+
selectedSurfaces,
|
|
64
|
+
consideredSurfaces,
|
|
65
|
+
switching: input.switching,
|
|
66
|
+
advanced: input.advanced === null ? null : structuredClone(input.advanced),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function routingProfilePath(consumerRoot, profileRoot) {
|
|
71
|
+
const root = profileRoot ??
|
|
72
|
+
join(process.env.XDG_STATE_HOME || join(homedir(), '.local', 'state'), 'agent-workflow-kit');
|
|
73
|
+
const consumerKey = createHash('sha256').update(resolve(consumerRoot)).digest('hex').slice(0, 20);
|
|
74
|
+
return join(root, 'profiles', consumerKey, ROUTING_PROFILE_PATH);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function readProfileSnapshot(consumerRoot, profileRoot) {
|
|
78
|
+
const path = routingProfilePath(consumerRoot, profileRoot);
|
|
79
|
+
try {
|
|
80
|
+
const bytes = await readFile(path, 'utf8');
|
|
81
|
+
const fingerprint = createHash('sha256').update(bytes).digest('hex');
|
|
82
|
+
try {
|
|
83
|
+
return { profile: validateRoutingProfile(JSON.parse(bytes)), fingerprint, invalid: false };
|
|
84
|
+
} catch {
|
|
85
|
+
return { profile: null, fingerprint, invalid: true };
|
|
86
|
+
}
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (error.code === 'ENOENT') return { profile: null, fingerprint: null, invalid: false };
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function readRoutingProfile(consumerRoot, profileRoot) {
|
|
94
|
+
const snapshot = await readProfileSnapshot(consumerRoot, profileRoot);
|
|
95
|
+
if (snapshot.invalid) throw new TypeError('routing profile is invalid');
|
|
96
|
+
return snapshot.profile;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function resolvedDetectedSurfaceIds(options) {
|
|
100
|
+
if (options.detectedSurfaceIds) return uniqueStrings(options.detectedSurfaceIds, 'detectedSurfaceIds');
|
|
101
|
+
const detected = await (options.detect ?? detectAgentSurfaces)();
|
|
102
|
+
return detected.filter((surface) => surface.detected).map((surface) => surface.id);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function inspectRoutingProfile(options) {
|
|
106
|
+
const { consumerRoot, registry = AGENT_SURFACE_REGISTRY } = options;
|
|
107
|
+
const detectedSurfaceIds = await resolvedDetectedSurfaceIds(options);
|
|
108
|
+
const snapshot = await readProfileSnapshot(consumerRoot, options.profileRoot);
|
|
109
|
+
const { profile, fingerprint } = snapshot;
|
|
110
|
+
if (snapshot.invalid) {
|
|
111
|
+
return {
|
|
112
|
+
status: 'needs-reconcile',
|
|
113
|
+
reasons: ['invalid'],
|
|
114
|
+
delta: { type: 'invalid-profile' },
|
|
115
|
+
profile: null,
|
|
116
|
+
fingerprint,
|
|
117
|
+
detectedSurfaceIds,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
if (!profile) {
|
|
121
|
+
return {
|
|
122
|
+
status: 'needs-reconcile',
|
|
123
|
+
reasons: ['missing'],
|
|
124
|
+
delta: { type: 'missing-profile' },
|
|
125
|
+
profile: null,
|
|
126
|
+
fingerprint,
|
|
127
|
+
detectedSurfaceIds,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
const known = new Set(registry.map(({ id }) => id));
|
|
131
|
+
const considered = new Set(profile.consideredSurfaces);
|
|
132
|
+
const reasons = [];
|
|
133
|
+
const removedSurfaceIds = profile.selectedSurfaces.filter((id) => !known.has(id));
|
|
134
|
+
const newSurfaceIds = detectedSurfaceIds.filter((id) => known.has(id) && !considered.has(id));
|
|
135
|
+
if (profile.registryRevision < AGENT_SURFACE_REGISTRY_REVISION) reasons.push('materially-stale');
|
|
136
|
+
if (removedSurfaceIds.length) reasons.push('removed-route');
|
|
137
|
+
if (newSurfaceIds.length) reasons.push('new-meaningful-surface');
|
|
138
|
+
return {
|
|
139
|
+
status: reasons.length ? 'needs-reconcile' : 'still valid',
|
|
140
|
+
reasons,
|
|
141
|
+
delta: {
|
|
142
|
+
type: 'profile-delta',
|
|
143
|
+
registryRevision: {
|
|
144
|
+
from: profile.registryRevision,
|
|
145
|
+
to: AGENT_SURFACE_REGISTRY_REVISION,
|
|
146
|
+
},
|
|
147
|
+
removedSurfaces: removedSurfaceIds.map((id) => ({ id, label: id })),
|
|
148
|
+
newSurfaces: newSurfaceIds.map((id) => {
|
|
149
|
+
const surface = surfaceById(id, registry);
|
|
150
|
+
return { id, label: surface.label };
|
|
151
|
+
}),
|
|
152
|
+
},
|
|
153
|
+
profile,
|
|
154
|
+
fingerprint,
|
|
155
|
+
detectedSurfaceIds,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function surfaceQuestion(registry, preselected) {
|
|
160
|
+
return {
|
|
161
|
+
kind: 'surfaces',
|
|
162
|
+
message: 'Which agent apps do you use?',
|
|
163
|
+
options: registry.map(({ id, label }) => ({ id, label })),
|
|
164
|
+
preselected,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function autonomyQuestion() {
|
|
169
|
+
return {
|
|
170
|
+
kind: 'autonomy',
|
|
171
|
+
message: 'May the Kit switch agent apps for a task?',
|
|
172
|
+
options: [
|
|
173
|
+
{ value: 'automatic', label: 'Switch automatically' },
|
|
174
|
+
{ value: 'ask', label: 'Ask before switching' },
|
|
175
|
+
{ value: 'current-surface-only', label: 'Stay in the current app' },
|
|
176
|
+
],
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export async function setupRoutingProfile(options) {
|
|
181
|
+
const {
|
|
182
|
+
consumerRoot,
|
|
183
|
+
prompt,
|
|
184
|
+
registry = AGENT_SURFACE_REGISTRY,
|
|
185
|
+
} = options;
|
|
186
|
+
if (typeof prompt !== 'function') {
|
|
187
|
+
return { status: 'needs-reconcile', reasons: ['personal-choice-required'] };
|
|
188
|
+
}
|
|
189
|
+
const detectedSurfaceIds = await resolvedDetectedSurfaceIds(options);
|
|
190
|
+
const currentSurface = options.currentSurface ?? detectedSurfaceIds[0] ?? registry[0]?.id;
|
|
191
|
+
if (!surfaceById(currentSurface, registry)) throw new TypeError('currentSurface must be registered');
|
|
192
|
+
const expectedFingerprint = options.expectedFingerprint === undefined
|
|
193
|
+
? (await readProfileSnapshot(consumerRoot, options.profileRoot)).fingerprint
|
|
194
|
+
: options.expectedFingerprint;
|
|
195
|
+
const known = new Set(registry.map(({ id }) => id));
|
|
196
|
+
const preselected = detectedSurfaceIds.filter((id) => known.has(id));
|
|
197
|
+
if (!preselected.includes(currentSurface)) preselected.unshift(currentSurface);
|
|
198
|
+
|
|
199
|
+
while (true) {
|
|
200
|
+
const selectedSurfaces = uniqueStrings(
|
|
201
|
+
await prompt(surfaceQuestion(registry, preselected)),
|
|
202
|
+
'selected surfaces',
|
|
203
|
+
).filter((id) => known.has(id));
|
|
204
|
+
if (!selectedSurfaces.length) selectedSurfaces.push(currentSurface);
|
|
205
|
+
let switching = selectedSurfaces.length === 1
|
|
206
|
+
? 'current-surface-only'
|
|
207
|
+
: await prompt(autonomyQuestion());
|
|
208
|
+
if (!SWITCHING.includes(switching)) throw new TypeError('invalid autonomy choice');
|
|
209
|
+
|
|
210
|
+
let advanced = null;
|
|
211
|
+
let revisitChoices = false;
|
|
212
|
+
while (true) {
|
|
213
|
+
const summary = {
|
|
214
|
+
kind: 'activation',
|
|
215
|
+
message: 'Review routing activation',
|
|
216
|
+
selectedSurfaces,
|
|
217
|
+
switching,
|
|
218
|
+
advancedDraft: advanced,
|
|
219
|
+
actions: ACTIVATION,
|
|
220
|
+
};
|
|
221
|
+
const action = await prompt(summary);
|
|
222
|
+
if (!ACTIVATION.includes(action)) throw new TypeError('invalid activation choice');
|
|
223
|
+
if (action === 'back') {
|
|
224
|
+
revisitChoices = true;
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
if (action === 'decline') return { status: 'declined' };
|
|
228
|
+
if (action === 'advanced') {
|
|
229
|
+
advanced = await prompt({
|
|
230
|
+
kind: 'advanced',
|
|
231
|
+
message: 'Optional model and optimization preferences',
|
|
232
|
+
draft: advanced,
|
|
233
|
+
});
|
|
234
|
+
if (!advanced || typeof advanced !== 'object' || Array.isArray(advanced)) {
|
|
235
|
+
throw new TypeError('advanced choice must be an object');
|
|
236
|
+
}
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (action === 'safe-current-surface') switching = 'current-surface-only';
|
|
240
|
+
const profile = validateRoutingProfile({
|
|
241
|
+
schemaVersion: ROUTING_PROFILE_VERSION,
|
|
242
|
+
registryRevision: AGENT_SURFACE_REGISTRY_REVISION,
|
|
243
|
+
selectedSurfaces,
|
|
244
|
+
consideredSurfaces: [...new Set([...preselected, ...selectedSurfaces])],
|
|
245
|
+
switching,
|
|
246
|
+
advanced,
|
|
247
|
+
});
|
|
248
|
+
await writeProfileExpected(options, profile, expectedFingerprint);
|
|
249
|
+
return { status: 'activated', profile };
|
|
250
|
+
}
|
|
251
|
+
if (revisitChoices) continue;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export async function reconcileRoutingProfile(options, inspection) {
|
|
256
|
+
if (inspection.status === 'still valid') return { status: 'still valid' };
|
|
257
|
+
if (typeof options.prompt !== 'function') {
|
|
258
|
+
return { status: 'needs-reconcile', reasons: inspection.reasons };
|
|
259
|
+
}
|
|
260
|
+
const choice = await options.prompt({
|
|
261
|
+
kind: 'reconcile',
|
|
262
|
+
message: 'Your routing choices need review.',
|
|
263
|
+
reasons: inspection.reasons,
|
|
264
|
+
delta: inspection.delta,
|
|
265
|
+
actions: ['review', 'decline'],
|
|
266
|
+
});
|
|
267
|
+
if (choice === 'decline' || choice?.action === 'decline') {
|
|
268
|
+
return { status: 'declined', reasons: inspection.reasons };
|
|
269
|
+
}
|
|
270
|
+
if (!inspection.profile) {
|
|
271
|
+
if (choice !== 'review' && choice?.action !== 'review') {
|
|
272
|
+
return { status: 'declined', reasons: inspection.reasons };
|
|
273
|
+
}
|
|
274
|
+
return setupRoutingProfile({ ...options, expectedFingerprint: inspection.fingerprint });
|
|
275
|
+
}
|
|
276
|
+
if (choice !== 'apply' && choice?.action !== 'apply') {
|
|
277
|
+
return { status: 'declined', reasons: inspection.reasons };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const removed = new Set(inspection.delta.removedSurfaces.map(({ id }) => id));
|
|
281
|
+
const requestedAdds = new Set(choice.addSurfaceIds ?? []);
|
|
282
|
+
const knownAdds = inspection.delta.newSurfaces
|
|
283
|
+
.map(({ id }) => id)
|
|
284
|
+
.filter((id) => requestedAdds.has(id));
|
|
285
|
+
const selectedSurfaces = [
|
|
286
|
+
...inspection.profile.selectedSurfaces.filter((id) => !removed.has(id)),
|
|
287
|
+
...knownAdds,
|
|
288
|
+
];
|
|
289
|
+
let switching = inspection.profile.switching;
|
|
290
|
+
if (!selectedSurfaces.length) {
|
|
291
|
+
const safeSurface = options.currentSurface ??
|
|
292
|
+
inspection.detectedSurfaceIds[0] ??
|
|
293
|
+
AGENT_SURFACE_REGISTRY[0].id;
|
|
294
|
+
selectedSurfaces.push(safeSurface);
|
|
295
|
+
switching = 'current-surface-only';
|
|
296
|
+
}
|
|
297
|
+
const profile = validateRoutingProfile({
|
|
298
|
+
...inspection.profile,
|
|
299
|
+
registryRevision: AGENT_SURFACE_REGISTRY_REVISION,
|
|
300
|
+
selectedSurfaces,
|
|
301
|
+
consideredSurfaces: [
|
|
302
|
+
...inspection.profile.consideredSurfaces.filter((id) => !removed.has(id)),
|
|
303
|
+
...inspection.delta.newSurfaces.map(({ id }) => id),
|
|
304
|
+
],
|
|
305
|
+
switching,
|
|
306
|
+
});
|
|
307
|
+
await writeProfileExpected(options, profile, inspection.fingerprint);
|
|
308
|
+
return { status: 'reconciled', reasons: inspection.reasons, profile };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async function writeProfileExpected(options, profile, expectedFingerprint) {
|
|
312
|
+
const path = routingProfilePath(options.consumerRoot, options.profileRoot);
|
|
313
|
+
const lockPath = `${path}.lock`;
|
|
314
|
+
await mkdir(dirname(path), { recursive: true });
|
|
315
|
+
let lock;
|
|
316
|
+
try {
|
|
317
|
+
lock = await open(lockPath, 'wx');
|
|
318
|
+
} catch (error) {
|
|
319
|
+
if (error.code === 'EEXIST') {
|
|
320
|
+
throw new Error('concurrent routing profile mutation: profile lock is held');
|
|
321
|
+
}
|
|
322
|
+
throw error;
|
|
323
|
+
}
|
|
324
|
+
try {
|
|
325
|
+
const actual = (await readProfileSnapshot(options.consumerRoot, options.profileRoot)).fingerprint;
|
|
326
|
+
if (actual !== expectedFingerprint) {
|
|
327
|
+
throw new Error('concurrent routing profile mutation: profile changed during decision');
|
|
328
|
+
}
|
|
329
|
+
await writeAtomic(path, `${JSON.stringify(profile, null, 2)}\n`);
|
|
330
|
+
} finally {
|
|
331
|
+
await lock.close();
|
|
332
|
+
await rm(lockPath, { force: true });
|
|
333
|
+
}
|
|
334
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import {
|
|
2
|
+
evidenceSelectionMatchesObservation,
|
|
3
|
+
validateRoutingIntent,
|
|
4
|
+
} from './routingIntent.mjs';
|
|
5
|
+
import { validateEvidenceCatalog } from './routingCatalog.mjs';
|
|
6
|
+
import { validateAccessGraph } from './routingAccessGraph.mjs';
|
|
7
|
+
import { validateRoutingPolicy } from './routingPolicy.mjs';
|
|
8
|
+
|
|
9
|
+
export const ROUTE_DECISION_VERSION = 1;
|
|
10
|
+
|
|
11
|
+
const REQUIRED_INFRASTRUCTURE = ['catalog', 'accessGraph', 'policy'];
|
|
12
|
+
|
|
13
|
+
function missingDecision(missing, explicitFallback) {
|
|
14
|
+
return Object.freeze({
|
|
15
|
+
schemaVersion: ROUTE_DECISION_VERSION,
|
|
16
|
+
status: explicitFallback === 'inherit' ? 'inherit' : 'blocked',
|
|
17
|
+
reason: 'routing-infrastructure-missing',
|
|
18
|
+
missing: Object.freeze(missing),
|
|
19
|
+
bestOverall: null,
|
|
20
|
+
bestExecutable: null,
|
|
21
|
+
blockers: Object.freeze(missing.map((field) => `missing:${field}`)),
|
|
22
|
+
revisions: Object.freeze({ catalog: null, accessGraph: null, policy: null }),
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function compareCandidates(optimization) {
|
|
27
|
+
return (left, right) => {
|
|
28
|
+
if (optimization === 'cost') {
|
|
29
|
+
return left.cost.amount - right.cost.amount || right.score - left.score;
|
|
30
|
+
}
|
|
31
|
+
if (optimization === 'balanced') {
|
|
32
|
+
const leftValue = left.score / (1 + left.cost.amount);
|
|
33
|
+
const rightValue = right.score / (1 + right.cost.amount);
|
|
34
|
+
return rightValue - leftValue || right.score - left.score;
|
|
35
|
+
}
|
|
36
|
+
return right.score - left.score || left.cost.amount - right.cost.amount;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function observationRoute(observation) {
|
|
41
|
+
return Object.freeze({
|
|
42
|
+
observationId: observation.id,
|
|
43
|
+
providerId: observation.providerId,
|
|
44
|
+
modelId: observation.modelId,
|
|
45
|
+
effort: observation.effort,
|
|
46
|
+
workload: observation.workload,
|
|
47
|
+
harness: observation.harness,
|
|
48
|
+
score: observation.score,
|
|
49
|
+
source: observation.source,
|
|
50
|
+
uncertainty: observation.uncertainty,
|
|
51
|
+
freshness: observation.freshness,
|
|
52
|
+
cost: observation.cost,
|
|
53
|
+
reason: `${observation.workload} supported by ${observation.source.id}`,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function executableRoute(observation, path) {
|
|
58
|
+
return Object.freeze({
|
|
59
|
+
...observationRoute(observation),
|
|
60
|
+
accessPathId: path.id,
|
|
61
|
+
surfaceId: path.surfaceId,
|
|
62
|
+
transportId: path.transportId,
|
|
63
|
+
enforcement: path.enforcement,
|
|
64
|
+
capabilityEvidence: path.capabilityEvidence,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function pathAllowed(path, policy, activeSurface, knownTransports, now, blockers) {
|
|
69
|
+
if (!knownTransports.has(path.transportId)) {
|
|
70
|
+
blockers.add(`unknown-transport:${path.transportId}`);
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
if (Date.parse(path.capabilityEvidence.expiresAt) <= now) {
|
|
74
|
+
blockers.add(`stale-capability-evidence:${path.id}`);
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
if (path.availability !== 'available') {
|
|
78
|
+
blockers.add(`route-${path.availability}:${path.id}`);
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
if (!policy.allowedSurfaces.includes(path.surfaceId)) {
|
|
82
|
+
blockers.add(`surface-not-allowed:${path.surfaceId}`);
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
if (!policy.allowedTransports.includes(path.transportId)) {
|
|
86
|
+
blockers.add(`transport-not-allowed:${path.transportId}`);
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
if (path.surfaceId !== activeSurface) {
|
|
90
|
+
if (policy.switching === 'current-surface-only') {
|
|
91
|
+
blockers.add(`surface-switch-disabled:${path.surfaceId}`);
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
if (policy.switching === 'ask') {
|
|
95
|
+
blockers.add(`surface-switch-approval-required:${path.surfaceId}`);
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function noExecutableStatus(policy) {
|
|
103
|
+
if (policy.unreachable === 'inherit') return 'inherit';
|
|
104
|
+
if (policy.unreachable === 'handoff') return 'handoff';
|
|
105
|
+
return 'blocked';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function resolveRoute(input) {
|
|
109
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
110
|
+
throw new TypeError('route resolution input must be an object');
|
|
111
|
+
}
|
|
112
|
+
const missing = REQUIRED_INFRASTRUCTURE.filter((field) => input[field] == null);
|
|
113
|
+
if (missing.length > 0) return missingDecision(missing, input.missingInfrastructure);
|
|
114
|
+
|
|
115
|
+
const intent = validateRoutingIntent(input.intent);
|
|
116
|
+
const catalog = validateEvidenceCatalog(input.catalog);
|
|
117
|
+
const accessGraph = validateAccessGraph(input.accessGraph);
|
|
118
|
+
const policy = validateRoutingPolicy(input.policy);
|
|
119
|
+
if (typeof input.activeSurface !== 'string' || input.activeSurface === '') {
|
|
120
|
+
throw new TypeError('activeSurface must be a non-empty string');
|
|
121
|
+
}
|
|
122
|
+
if (!Array.isArray(input.knownTransports)
|
|
123
|
+
|| !input.knownTransports.every((entry) => typeof entry === 'string' && entry !== '')) {
|
|
124
|
+
throw new TypeError('knownTransports must be an array of non-empty strings');
|
|
125
|
+
}
|
|
126
|
+
const now = Date.parse(input.now);
|
|
127
|
+
if (!Number.isFinite(now)) throw new TypeError('now must be an ISO timestamp');
|
|
128
|
+
|
|
129
|
+
const blockers = new Set();
|
|
130
|
+
const currentEvidence = catalog.observations.filter((entry) => {
|
|
131
|
+
const matchesIntent = intent.evidenceSelection
|
|
132
|
+
? evidenceSelectionMatchesObservation(intent.evidenceSelection, entry.workload)
|
|
133
|
+
: entry.workload === intent.workload;
|
|
134
|
+
if (!matchesIntent) return false;
|
|
135
|
+
if (Date.parse(entry.freshness.expiresAt) <= now) {
|
|
136
|
+
blockers.add(`stale-catalog-evidence:${entry.id}`);
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
return true;
|
|
140
|
+
});
|
|
141
|
+
const rankedOverall = [...currentEvidence].sort(compareCandidates(policy.optimization));
|
|
142
|
+
const bestOverall = rankedOverall.length > 0 ? observationRoute(rankedOverall[0]) : null;
|
|
143
|
+
|
|
144
|
+
const knownTransports = new Set(input.knownTransports);
|
|
145
|
+
const executable = [];
|
|
146
|
+
for (const evidence of currentEvidence) {
|
|
147
|
+
for (const path of accessGraph.paths) {
|
|
148
|
+
if (path.providerId !== evidence.providerId || path.modelId !== evidence.modelId) continue;
|
|
149
|
+
if (!pathAllowed(path, policy, input.activeSurface, knownTransports, now, blockers)) continue;
|
|
150
|
+
executable.push({ evidence, path });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
executable.sort((left, right) =>
|
|
154
|
+
compareCandidates(policy.optimization)(left.evidence, right.evidence)
|
|
155
|
+
|| Number(right.path.surfaceId === input.activeSurface)
|
|
156
|
+
- Number(left.path.surfaceId === input.activeSurface)
|
|
157
|
+
|| left.path.id.localeCompare(right.path.id));
|
|
158
|
+
const bestExecutable = executable.length > 0
|
|
159
|
+
? executableRoute(executable[0].evidence, executable[0].path)
|
|
160
|
+
: null;
|
|
161
|
+
const status = bestExecutable ? 'ready' : noExecutableStatus(policy);
|
|
162
|
+
return Object.freeze({
|
|
163
|
+
schemaVersion: ROUTE_DECISION_VERSION,
|
|
164
|
+
status,
|
|
165
|
+
reason: bestExecutable ? 'route-resolved' : 'no-executable-route',
|
|
166
|
+
intent,
|
|
167
|
+
bestOverall,
|
|
168
|
+
bestExecutable,
|
|
169
|
+
blockers: Object.freeze([...blockers].sort()),
|
|
170
|
+
revisions: Object.freeze({
|
|
171
|
+
catalog: catalog.revision,
|
|
172
|
+
accessGraph: accessGraph.revision,
|
|
173
|
+
policy: policy.revision,
|
|
174
|
+
}),
|
|
175
|
+
});
|
|
176
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
const SOURCE_ID = 'artificial-analysis-coding-agents';
|
|
2
|
+
const OWNER = 'Artificial Analysis';
|
|
3
|
+
const ARTIFACT_URL = 'https://artificialanalysis.ai/agents/coding-agents';
|
|
4
|
+
|
|
5
|
+
function object(value, field) {
|
|
6
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
7
|
+
throw new TypeError(`${field} must be an object`);
|
|
8
|
+
}
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function string(value, field) {
|
|
13
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
14
|
+
throw new TypeError(`${field} must be a non-empty string`);
|
|
15
|
+
}
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function number(value, field) {
|
|
20
|
+
if (!Number.isFinite(value)) throw new TypeError(`${field} must be a finite number`);
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function ingest({ payload, snapshotHash, observedAt, expiresAt }) {
|
|
25
|
+
string(snapshotHash, 'snapshotHash');
|
|
26
|
+
if (!Number.isFinite(Date.parse(observedAt))) {
|
|
27
|
+
throw new TypeError('observedAt must be an ISO timestamp');
|
|
28
|
+
}
|
|
29
|
+
if (!Number.isFinite(Date.parse(expiresAt)) || Date.parse(expiresAt) <= Date.parse(observedAt)) {
|
|
30
|
+
throw new TypeError('expiresAt must be an ISO timestamp after observedAt');
|
|
31
|
+
}
|
|
32
|
+
object(payload, 'Artificial Analysis payload');
|
|
33
|
+
const identity = object(payload.identity, 'Artificial Analysis identity');
|
|
34
|
+
if (identity.publisher !== OWNER || identity.canonicalUrl !== ARTIFACT_URL) {
|
|
35
|
+
throw new TypeError(
|
|
36
|
+
'Artificial Analysis source identity does not match the owner artifact',
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
const benchmark = string(identity.index, 'Artificial Analysis index');
|
|
40
|
+
const benchmarkVersion = string(identity.release, 'Artificial Analysis release');
|
|
41
|
+
const harnessId = string(identity.harnessName, 'Artificial Analysis harness name');
|
|
42
|
+
const harnessVersion = string(
|
|
43
|
+
identity.harnessRelease,
|
|
44
|
+
'Artificial Analysis harness release',
|
|
45
|
+
);
|
|
46
|
+
if (!Array.isArray(payload.configurations)) {
|
|
47
|
+
throw new TypeError('Artificial Analysis configurations must be an array');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const models = new Map();
|
|
51
|
+
const observations = [];
|
|
52
|
+
const observationIds = new Set();
|
|
53
|
+
payload.configurations.forEach((row, index) => {
|
|
54
|
+
object(row, `Artificial Analysis configurations[${index}]`);
|
|
55
|
+
const providerId = string(
|
|
56
|
+
row.providerId,
|
|
57
|
+
`Artificial Analysis configurations[${index}].providerId`,
|
|
58
|
+
);
|
|
59
|
+
const modelId = string(
|
|
60
|
+
row.modelId,
|
|
61
|
+
`Artificial Analysis configurations[${index}].modelId`,
|
|
62
|
+
);
|
|
63
|
+
const effort = string(
|
|
64
|
+
row.reasoningEffort,
|
|
65
|
+
`Artificial Analysis configurations[${index}].effort`,
|
|
66
|
+
);
|
|
67
|
+
const id = `${SOURCE_ID}:${benchmarkVersion}:${providerId}:${modelId}:${effort}`;
|
|
68
|
+
if (observationIds.has(id)) {
|
|
69
|
+
throw new TypeError(`duplicate Artificial Analysis observation: ${id}`);
|
|
70
|
+
}
|
|
71
|
+
observationIds.add(id);
|
|
72
|
+
models.set(`${providerId}:${modelId}`, { providerId, modelId });
|
|
73
|
+
const uncertainty = object(
|
|
74
|
+
row.uncertainty,
|
|
75
|
+
`Artificial Analysis configurations[${index}].uncertainty`,
|
|
76
|
+
);
|
|
77
|
+
observations.push({
|
|
78
|
+
id,
|
|
79
|
+
providerId,
|
|
80
|
+
modelId,
|
|
81
|
+
effort,
|
|
82
|
+
workload: 'development',
|
|
83
|
+
harness: { id: harnessId, version: harnessVersion },
|
|
84
|
+
score: number(
|
|
85
|
+
row.indexScore,
|
|
86
|
+
`Artificial Analysis configurations[${index}].indexScore`,
|
|
87
|
+
),
|
|
88
|
+
source: {
|
|
89
|
+
id: SOURCE_ID,
|
|
90
|
+
owner: OWNER,
|
|
91
|
+
url: ARTIFACT_URL,
|
|
92
|
+
benchmark,
|
|
93
|
+
version: benchmarkVersion,
|
|
94
|
+
snapshotHash,
|
|
95
|
+
},
|
|
96
|
+
uncertainty: {
|
|
97
|
+
kind: string(
|
|
98
|
+
uncertainty.kind,
|
|
99
|
+
`Artificial Analysis configurations[${index}].uncertainty.kind`,
|
|
100
|
+
),
|
|
101
|
+
value: number(
|
|
102
|
+
uncertainty.value,
|
|
103
|
+
`Artificial Analysis configurations[${index}].uncertainty.value`,
|
|
104
|
+
),
|
|
105
|
+
},
|
|
106
|
+
freshness: { observedAt, expiresAt },
|
|
107
|
+
cost: {
|
|
108
|
+
amount: number(
|
|
109
|
+
row.costPerTaskUsd,
|
|
110
|
+
`Artificial Analysis configurations[${index}].costPerTaskUsd`,
|
|
111
|
+
),
|
|
112
|
+
currency: 'USD',
|
|
113
|
+
unit: 'task',
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
return {
|
|
118
|
+
sourceId: SOURCE_ID,
|
|
119
|
+
models: [...models.values()],
|
|
120
|
+
observations,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export const artificialAnalysisSource = Object.freeze({
|
|
125
|
+
sourceId: SOURCE_ID,
|
|
126
|
+
owner: OWNER,
|
|
127
|
+
artifactUrl: ARTIFACT_URL,
|
|
128
|
+
ingest,
|
|
129
|
+
});
|