@ikon85/agent-workflow-kit 0.33.0 → 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 +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-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 +79 -11
- package/agent-workflow-kit.package.json +201 -41
- 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 +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
|
@@ -55,6 +55,17 @@ ENFORCED_CLASSES = {"generic", "vendored"}
|
|
|
55
55
|
EXEMPT = "portability-lint: ok"
|
|
56
56
|
MIRROR_XFORM_START = re.compile(r"^\s*<!--\s*mirror-xform:start(?:\s+([^>]*?))?\s*-->\s*$")
|
|
57
57
|
MIRROR_XFORM_END = re.compile(r"^\s*<!--\s*mirror-xform:end\s*-->\s*$")
|
|
58
|
+
DIRECT_SPAWN_PATTERN = re.compile(
|
|
59
|
+
r"Run \*\*one read-only research subagent|"
|
|
60
|
+
r"Run both axes as \*\*parallel sub-agents|"
|
|
61
|
+
r"Spawn 3\+ sub-agents|"
|
|
62
|
+
r"spawn 3\+ sub-agents|"
|
|
63
|
+
r"make 3\+ parallel\s+`spawn_agent` calls|"
|
|
64
|
+
r"Then use the Agent tool|"
|
|
65
|
+
r"use `spawn_agent` with `agent_type: explorer`|"
|
|
66
|
+
r"use `spawn_agent` with only|"
|
|
67
|
+
r"Spin up a \*\*background agent",
|
|
68
|
+
)
|
|
58
69
|
|
|
59
70
|
# Opaque, project-coupled board constants — meaningless in a foreign repo.
|
|
60
71
|
CONSTANT_PATTERNS = [
|
|
@@ -240,6 +251,15 @@ def skill_md_set(tree: str, name: str) -> set[str]:
|
|
|
240
251
|
return {p for p in skill_file_set(tree, name) if p.endswith(".md")}
|
|
241
252
|
|
|
242
253
|
|
|
254
|
+
def direct_spawn_contracts(tree: str) -> dict[str, str]:
|
|
255
|
+
root = REPO_ROOT / tree
|
|
256
|
+
return {
|
|
257
|
+
path.relative_to(root).as_posix(): path.read_text(encoding="utf-8")
|
|
258
|
+
for path in root.rglob("*.md")
|
|
259
|
+
if DIRECT_SPAWN_PATTERN.search(path.read_text(encoding="utf-8"))
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
|
|
243
263
|
def mirror_presence_drift(name: str, claude: set[str], codex: set[str]) -> list[str]:
|
|
244
264
|
problems = [
|
|
245
265
|
f"{name}: .claude/skills/{name}/{rel} has no codex mirror"
|
|
@@ -555,6 +575,40 @@ class MirrorContentParity(unittest.TestCase):
|
|
|
555
575
|
"`<!-- mirror-xform:start <reason> -->` / `<!-- mirror-xform:end -->` "
|
|
556
576
|
"regions:\n" + "\n".join(problems))
|
|
557
577
|
|
|
578
|
+
def test_every_codex_direct_spawn_contract_uses_the_shared_guard(self):
|
|
579
|
+
claude = direct_spawn_contracts(".claude/skills")
|
|
580
|
+
codex = direct_spawn_contracts(".agents/skills")
|
|
581
|
+
self.assertTrue(claude, "direct-spawn census unexpectedly empty")
|
|
582
|
+
self.assertEqual(set(codex), set(claude))
|
|
583
|
+
|
|
584
|
+
problems = []
|
|
585
|
+
required = (
|
|
586
|
+
"provider-neutral Routing intent",
|
|
587
|
+
"routeDispatcher.mjs",
|
|
588
|
+
"shared spawn guard",
|
|
589
|
+
"Dispatch receipt",
|
|
590
|
+
)
|
|
591
|
+
for relative, body in codex.items():
|
|
592
|
+
prose = " ".join(body.split())
|
|
593
|
+
missing = [fragment for fragment in required if fragment not in prose]
|
|
594
|
+
if missing:
|
|
595
|
+
problems.append(f"{relative}: missing {', '.join(missing)}")
|
|
596
|
+
unsupported = [
|
|
597
|
+
token for token in ("Agent tool", "subagent_type", "agent_type")
|
|
598
|
+
if token in prose
|
|
599
|
+
]
|
|
600
|
+
if unsupported:
|
|
601
|
+
problems.append(
|
|
602
|
+
f"{relative}: unsupported Codex spawn syntax "
|
|
603
|
+
f"{', '.join(unsupported)}"
|
|
604
|
+
)
|
|
605
|
+
self.assertEqual(
|
|
606
|
+
problems,
|
|
607
|
+
[],
|
|
608
|
+
f"Codex direct-spawn guard coverage drift ({len(codex)} of "
|
|
609
|
+
f"{len(claude)}):\n" + "\n".join(problems),
|
|
610
|
+
)
|
|
611
|
+
|
|
558
612
|
|
|
559
613
|
class MirrorParityFixture(unittest.TestCase):
|
|
560
614
|
"""The file-set comparison catches an orphaned mirror file (regression guard)."""
|
package/src/cli.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import { diff } from './commands/diff.mjs';
|
|
|
8
8
|
import { uninstall } from './commands/uninstall.mjs';
|
|
9
9
|
import { setOwnership } from './commands/own.mjs';
|
|
10
10
|
import { CONSUMER_ORIGIN, KIT_ORIGIN } from './lib/manifest.mjs';
|
|
11
|
+
import { currentAgentSurface } from './lib/agentSurfaceRegistry.mjs';
|
|
11
12
|
import { createCommandAdapter } from '../scripts/release-state.mjs';
|
|
12
13
|
import { installedIdentityFromDir } from '../scripts/release-parity.mjs';
|
|
13
14
|
|
|
@@ -29,12 +30,18 @@ p.intro('agent-workflow-kit');
|
|
|
29
30
|
|
|
30
31
|
try {
|
|
31
32
|
if (cmd === 'init') {
|
|
32
|
-
const r = await init({
|
|
33
|
+
const r = await init({
|
|
34
|
+
kitRoot: KIT_ROOT,
|
|
35
|
+
consumerRoot,
|
|
36
|
+
force,
|
|
37
|
+
routingProfile: routingProfileOptions(),
|
|
38
|
+
});
|
|
33
39
|
p.note(
|
|
34
40
|
`copied ${r.copied.length} · seeded ${r.seeded.length} stub(s)` +
|
|
35
41
|
(r.skipped.length ? `\nskipped (pre-existing, use --force): ${r.skipped.join(', ')}` : ''),
|
|
36
42
|
'init'
|
|
37
43
|
);
|
|
44
|
+
printRoutingProfile(r.routingProfile);
|
|
38
45
|
p.outro('Next: run /setup-workflow to fill the project layer + board profile. ' +
|
|
39
46
|
'To enable the drift-guard hook, add .claude/hooks/drift-guard.py to your settings.json hooks.');
|
|
40
47
|
} else if (cmd === 'diff') {
|
|
@@ -45,9 +52,15 @@ try {
|
|
|
45
52
|
const decide = (action, path) => decideUpdate(action, path, yes);
|
|
46
53
|
const releaseIdentities = await readUpdateRelease();
|
|
47
54
|
const r = await update({
|
|
48
|
-
kitRoot: KIT_ROOT,
|
|
55
|
+
kitRoot: KIT_ROOT,
|
|
56
|
+
consumerRoot,
|
|
57
|
+
now: stamp(),
|
|
58
|
+
decide,
|
|
59
|
+
releaseIdentities,
|
|
60
|
+
routingProfile: routingProfileOptions(),
|
|
49
61
|
});
|
|
50
62
|
printPlan(r);
|
|
63
|
+
printRoutingProfile(r.routingProfile);
|
|
51
64
|
for (const c of r.conflicts) p.note(c.diff || '(binary/!text)', `conflict (not applied): ${c.path}`);
|
|
52
65
|
if (r.state === 'failed') throw new Error(renderUpdateFailure(r));
|
|
53
66
|
if (r.state === 'conflicted') {
|
|
@@ -126,6 +139,100 @@ async function decideUpdate(action, path, yes) {
|
|
|
126
139
|
throw new Error(`unknown update decision action: ${action}`);
|
|
127
140
|
}
|
|
128
141
|
|
|
142
|
+
function routingProfileOptions() {
|
|
143
|
+
return {
|
|
144
|
+
currentSurface: currentAgentSurface(),
|
|
145
|
+
prompt: yes ? undefined : promptRoutingProfile,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function printRoutingProfile(result) {
|
|
150
|
+
if (!result) return;
|
|
151
|
+
const suffix = result.reasons?.length ? ` · ${result.reasons.join(', ')}` : '';
|
|
152
|
+
p.note(`${result.status}${suffix}`, 'routing profile');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function promptRoutingProfile(question) {
|
|
156
|
+
if (question.kind === 'surfaces') {
|
|
157
|
+
return ensurePrompt(await p.multiselect({
|
|
158
|
+
message: question.message,
|
|
159
|
+
options: question.options.map(({ id, label }) => ({ value: id, label })),
|
|
160
|
+
initialValues: question.preselected,
|
|
161
|
+
required: true,
|
|
162
|
+
}), 'surface selection');
|
|
163
|
+
}
|
|
164
|
+
if (question.kind === 'autonomy') {
|
|
165
|
+
return ensurePrompt(await p.select({
|
|
166
|
+
message: question.message,
|
|
167
|
+
options: question.options,
|
|
168
|
+
}), 'switching choice');
|
|
169
|
+
}
|
|
170
|
+
if (question.kind === 'activation') {
|
|
171
|
+
const draft = question.advancedDraft ? ' · advanced draft ready' : '';
|
|
172
|
+
return ensurePrompt(await p.select({
|
|
173
|
+
message: `${question.message}${draft}`,
|
|
174
|
+
options: [
|
|
175
|
+
{ value: 'approve', label: 'Approve' },
|
|
176
|
+
{ value: 'safe-current-surface', label: 'Safe current surface' },
|
|
177
|
+
{ value: 'back', label: 'Back' },
|
|
178
|
+
{ value: 'advanced', label: 'Advanced' },
|
|
179
|
+
{ value: 'decline', label: 'Decline' },
|
|
180
|
+
],
|
|
181
|
+
}), 'activation choice');
|
|
182
|
+
}
|
|
183
|
+
if (question.kind === 'advanced') {
|
|
184
|
+
const optimization = ensurePrompt(await p.select({
|
|
185
|
+
message: question.message,
|
|
186
|
+
options: [
|
|
187
|
+
{ value: 'balanced', label: 'Balanced' },
|
|
188
|
+
{ value: 'quality', label: 'Quality' },
|
|
189
|
+
{ value: 'cost', label: 'Cost' },
|
|
190
|
+
],
|
|
191
|
+
initialValue: question.draft?.optimization ?? 'balanced',
|
|
192
|
+
}), 'advanced choice');
|
|
193
|
+
return { ...question.draft, optimization };
|
|
194
|
+
}
|
|
195
|
+
if (question.kind === 'reconcile') {
|
|
196
|
+
if (question.delta.type === 'missing-profile' || question.delta.type === 'invalid-profile') {
|
|
197
|
+
return ensurePrompt(await p.select({
|
|
198
|
+
message: question.message,
|
|
199
|
+
options: [
|
|
200
|
+
{ value: 'review', label: 'Review routing choices now' },
|
|
201
|
+
{ value: 'decline', label: 'Not now' },
|
|
202
|
+
],
|
|
203
|
+
}), 'routing migration choice');
|
|
204
|
+
}
|
|
205
|
+
const additions = question.delta.newSurfaces;
|
|
206
|
+
if (additions.length) {
|
|
207
|
+
const removed = question.delta.removedSurfaces.map(({ label }) => label);
|
|
208
|
+
const change = [
|
|
209
|
+
`new: ${additions.map(({ label }) => label).join(', ')}`,
|
|
210
|
+
...(removed.length ? [`unavailable: ${removed.join(', ')}`] : []),
|
|
211
|
+
].join(' · ');
|
|
212
|
+
const addSurfaceIds = ensurePrompt(await p.multiselect({
|
|
213
|
+
message: `Routing choices changed — ${change}`,
|
|
214
|
+
options: additions.map(({ id, label }) => ({ value: id, label })),
|
|
215
|
+
initialValues: [],
|
|
216
|
+
required: false,
|
|
217
|
+
}), 'routing reconcile choice');
|
|
218
|
+
return { action: 'apply', addSurfaceIds };
|
|
219
|
+
}
|
|
220
|
+
const removed = question.delta.removedSurfaces.map(({ label }) => label).join(', ');
|
|
221
|
+
const message = removed
|
|
222
|
+
? `Remove unavailable agent app from routing: ${removed}?`
|
|
223
|
+
: 'Refresh the routing profile registry revision?';
|
|
224
|
+
return ensurePrompt(await p.confirm({ message }), 'routing reconcile choice')
|
|
225
|
+
? { action: 'apply', addSurfaceIds: [] }
|
|
226
|
+
: { action: 'decline' };
|
|
227
|
+
}
|
|
228
|
+
throw new Error(`unknown routing profile question: ${question.kind}`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function ensurePrompt(value, label) {
|
|
232
|
+
if (p.isCancel(value)) throw new Error(`${label} cancelled`);
|
|
233
|
+
return value;
|
|
234
|
+
}
|
|
235
|
+
|
|
129
236
|
async function readUpdateRelease() {
|
|
130
237
|
const adapter = await createCommandAdapter({
|
|
131
238
|
repoRoot: KIT_ROOT,
|
package/src/commands/init.mjs
CHANGED
|
@@ -12,6 +12,9 @@ import {
|
|
|
12
12
|
CONSUMER_ORIGIN,
|
|
13
13
|
PACKAGE_MANIFEST_NAME, CONSUMER_MANIFEST_NAME,
|
|
14
14
|
} from '../lib/manifest.mjs';
|
|
15
|
+
import {
|
|
16
|
+
inspectRoutingProfile, reconcileRoutingProfile, setupRoutingProfile,
|
|
17
|
+
} from '../lib/routingProfile.mjs';
|
|
15
18
|
|
|
16
19
|
const exists = (p) => access(p).then(() => true, () => false);
|
|
17
20
|
|
|
@@ -24,7 +27,7 @@ const exists = (p) => access(p).then(() => true, () => false);
|
|
|
24
27
|
* already exist — idempotent),
|
|
25
28
|
* - never touches board-sync.md / CLAUDE.md / AGENTS.md.
|
|
26
29
|
*/
|
|
27
|
-
export async function init({ kitRoot, consumerRoot, force = false }) {
|
|
30
|
+
export async function init({ kitRoot, consumerRoot, force = false, routingProfile }) {
|
|
28
31
|
const pkg = await readManifest(join(kitRoot, PACKAGE_MANIFEST_NAME));
|
|
29
32
|
if (!pkg) throw new Error('kit package manifest not found');
|
|
30
33
|
const prior = await readManifest(join(consumerRoot, CONSUMER_MANIFEST_NAME));
|
|
@@ -80,5 +83,18 @@ export async function init({ kitRoot, consumerRoot, force = false }) {
|
|
|
80
83
|
result.seeded.push(stub);
|
|
81
84
|
}
|
|
82
85
|
|
|
86
|
+
if (routingProfile) {
|
|
87
|
+
const options = { consumerRoot, ...routingProfile };
|
|
88
|
+
const inspection = await inspectRoutingProfile(options);
|
|
89
|
+
result.routingProfile = prior
|
|
90
|
+
? await reconcileRoutingProfile(options, inspection)
|
|
91
|
+
: (inspection.status === 'still valid'
|
|
92
|
+
? { status: 'still valid' }
|
|
93
|
+
: await setupRoutingProfile({
|
|
94
|
+
...options,
|
|
95
|
+
expectedFingerprint: inspection.fingerprint,
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
|
|
83
99
|
return result;
|
|
84
100
|
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildEvidenceCatalogFromSources,
|
|
3
|
+
commitRoutingEvidenceCache,
|
|
4
|
+
routingEvidenceSourcesFromCache,
|
|
5
|
+
validateRoutingEvidenceSourceSnapshot,
|
|
6
|
+
} from '../lib/routingEvidenceCache.mjs';
|
|
7
|
+
|
|
8
|
+
function message(error) {
|
|
9
|
+
return error instanceof Error ? error.message : String(error);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function observationDiff(previous, next) {
|
|
13
|
+
const before = new Map(previous.map((entry) => [entry.id, entry]));
|
|
14
|
+
const after = new Map(next.map((entry) => [entry.id, entry]));
|
|
15
|
+
const added = [...after.keys()].filter((id) => !before.has(id)).sort();
|
|
16
|
+
const removed = [...before.keys()].filter((id) => !after.has(id)).sort();
|
|
17
|
+
const changed = [...after.keys()].filter(
|
|
18
|
+
(id) => before.has(id)
|
|
19
|
+
&& JSON.stringify(before.get(id)) !== JSON.stringify(after.get(id)),
|
|
20
|
+
).sort();
|
|
21
|
+
return { added, changed, removed };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function validateSource(source, index) {
|
|
25
|
+
if (!source || typeof source !== 'object') {
|
|
26
|
+
throw new TypeError(`sources[${index}] must be an object`);
|
|
27
|
+
}
|
|
28
|
+
const { adapter, load } = source;
|
|
29
|
+
if (!adapter || typeof adapter !== 'object') {
|
|
30
|
+
throw new TypeError(`sources[${index}].adapter must be an object`);
|
|
31
|
+
}
|
|
32
|
+
for (const field of ['sourceId', 'owner', 'artifactUrl']) {
|
|
33
|
+
if (typeof adapter[field] !== 'string' || adapter[field].trim() === '') {
|
|
34
|
+
throw new TypeError(`sources[${index}].adapter.${field} must be a non-empty string`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (typeof adapter.ingest !== 'function') {
|
|
38
|
+
throw new TypeError(`sources[${index}].adapter.ingest must be a function`);
|
|
39
|
+
}
|
|
40
|
+
if (typeof load !== 'function') {
|
|
41
|
+
throw new TypeError(`sources[${index}].load must be a function`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function modelsFor(observations, catalog) {
|
|
46
|
+
const identities = new Set(
|
|
47
|
+
observations.map(({ providerId, modelId }) => `${providerId}:${modelId}`),
|
|
48
|
+
);
|
|
49
|
+
return catalog.models.filter(
|
|
50
|
+
({ providerId, modelId }) => identities.has(`${providerId}:${modelId}`),
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function legacySourceSnapshots(cache) {
|
|
55
|
+
const groups = new Map();
|
|
56
|
+
for (const observation of cache.catalog.observations) {
|
|
57
|
+
const { source, freshness } = observation;
|
|
58
|
+
if (!source?.id || !source?.snapshotHash) continue;
|
|
59
|
+
const existing = groups.get(source.id);
|
|
60
|
+
if (existing && existing.snapshotHash !== source.snapshotHash) {
|
|
61
|
+
throw new TypeError(
|
|
62
|
+
`legacy source ${source.id} contains several snapshot hashes`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
const observations = existing?.observations ?? [];
|
|
66
|
+
observations.push(observation);
|
|
67
|
+
groups.set(source.id, {
|
|
68
|
+
sourceId: source.id,
|
|
69
|
+
owner: source.owner,
|
|
70
|
+
artifactUrl: source.url,
|
|
71
|
+
snapshotHash: source.snapshotHash,
|
|
72
|
+
observedAt: freshness.observedAt,
|
|
73
|
+
expiresAt: freshness.expiresAt,
|
|
74
|
+
models: [],
|
|
75
|
+
observations,
|
|
76
|
+
signals: [],
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return [...groups.values()].map((source, index) =>
|
|
80
|
+
validateRoutingEvidenceSourceSnapshot({
|
|
81
|
+
...source,
|
|
82
|
+
models: modelsFor(source.observations, cache.catalog),
|
|
83
|
+
}, index));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function cachedSources(cache) {
|
|
87
|
+
const explicit = routingEvidenceSourcesFromCache(cache);
|
|
88
|
+
return explicit.length > 0 ? explicit : legacySourceSnapshots(cache);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function cachedStatus(adapter, prior, reason) {
|
|
92
|
+
return {
|
|
93
|
+
sourceId: adapter.sourceId,
|
|
94
|
+
status: prior ? 'cached' : 'quarantined',
|
|
95
|
+
reason,
|
|
96
|
+
cachedAt: prior?.observedAt,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function refreshRoutingEvidence({
|
|
101
|
+
sources,
|
|
102
|
+
currentCache,
|
|
103
|
+
expectedRevision,
|
|
104
|
+
refreshedAt,
|
|
105
|
+
expiresAt,
|
|
106
|
+
onProgress = () => {},
|
|
107
|
+
}) {
|
|
108
|
+
if (!Array.isArray(sources)) throw new TypeError('sources must be an array');
|
|
109
|
+
sources.forEach(validateSource);
|
|
110
|
+
const requestedIds = new Set();
|
|
111
|
+
for (const { adapter } of sources) {
|
|
112
|
+
if (requestedIds.has(adapter.sourceId)) {
|
|
113
|
+
throw new TypeError(`duplicate routing evidence source: ${adapter.sourceId}`);
|
|
114
|
+
}
|
|
115
|
+
requestedIds.add(adapter.sourceId);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const sourceState = new Map(
|
|
119
|
+
cachedSources(currentCache).map((source) => [source.sourceId, source]),
|
|
120
|
+
);
|
|
121
|
+
const statuses = [];
|
|
122
|
+
const quarantines = [];
|
|
123
|
+
|
|
124
|
+
for (const { adapter, load } of sources) {
|
|
125
|
+
const prior = sourceState.get(adapter.sourceId);
|
|
126
|
+
onProgress({ sourceId: adapter.sourceId, status: 'loading' });
|
|
127
|
+
let loaded;
|
|
128
|
+
try {
|
|
129
|
+
loaded = await load();
|
|
130
|
+
} catch (error) {
|
|
131
|
+
const reason = `load failed: ${message(error)}`;
|
|
132
|
+
const status = cachedStatus(adapter, prior, reason);
|
|
133
|
+
if (!prior) quarantines.push({ sourceId: adapter.sourceId, reason });
|
|
134
|
+
statuses.push(status);
|
|
135
|
+
onProgress(status);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
const ingested = adapter.ingest({
|
|
141
|
+
payload: loaded.payload,
|
|
142
|
+
snapshotHash: loaded.snapshotHash,
|
|
143
|
+
observedAt: refreshedAt,
|
|
144
|
+
expiresAt,
|
|
145
|
+
});
|
|
146
|
+
if (ingested.sourceId !== adapter.sourceId) {
|
|
147
|
+
throw new TypeError(
|
|
148
|
+
`source identity changed from ${adapter.sourceId} to ${ingested.sourceId}`,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
const candidate = validateRoutingEvidenceSourceSnapshot({
|
|
152
|
+
sourceId: adapter.sourceId,
|
|
153
|
+
owner: adapter.owner,
|
|
154
|
+
artifactUrl: adapter.artifactUrl,
|
|
155
|
+
snapshotHash: loaded.snapshotHash,
|
|
156
|
+
observedAt: refreshedAt,
|
|
157
|
+
expiresAt,
|
|
158
|
+
models: ingested.models,
|
|
159
|
+
observations: ingested.observations,
|
|
160
|
+
signals: ingested.signals ?? [],
|
|
161
|
+
});
|
|
162
|
+
const prospective = new Map(sourceState);
|
|
163
|
+
prospective.set(adapter.sourceId, candidate);
|
|
164
|
+
buildEvidenceCatalogFromSources(
|
|
165
|
+
[...prospective.values()],
|
|
166
|
+
`catalog-r${expectedRevision + 1}`,
|
|
167
|
+
);
|
|
168
|
+
sourceState.set(adapter.sourceId, candidate);
|
|
169
|
+
const status = { sourceId: adapter.sourceId, status: 'live' };
|
|
170
|
+
statuses.push(status);
|
|
171
|
+
onProgress(status);
|
|
172
|
+
} catch (error) {
|
|
173
|
+
const reason = message(error);
|
|
174
|
+
const status = cachedStatus(adapter, prior, reason);
|
|
175
|
+
quarantines.push({ sourceId: adapter.sourceId, reason });
|
|
176
|
+
onProgress({ sourceId: adapter.sourceId, status: 'quarantined', reason });
|
|
177
|
+
statuses.push(status);
|
|
178
|
+
if (prior) onProgress(status);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const nextSources = [...sourceState.values()];
|
|
183
|
+
const nextCatalog = buildEvidenceCatalogFromSources(
|
|
184
|
+
nextSources,
|
|
185
|
+
`catalog-r${expectedRevision + 1}`,
|
|
186
|
+
);
|
|
187
|
+
const cache = commitRoutingEvidenceCache({
|
|
188
|
+
current: currentCache,
|
|
189
|
+
expectedRevision,
|
|
190
|
+
nextCatalog,
|
|
191
|
+
nextSources,
|
|
192
|
+
refreshedAt,
|
|
193
|
+
expiresAt,
|
|
194
|
+
});
|
|
195
|
+
return {
|
|
196
|
+
cache,
|
|
197
|
+
sources: statuses,
|
|
198
|
+
quarantines,
|
|
199
|
+
diff: observationDiff(
|
|
200
|
+
currentCache.catalog.observations,
|
|
201
|
+
nextCatalog.observations,
|
|
202
|
+
),
|
|
203
|
+
};
|
|
204
|
+
}
|
package/src/commands/update.mjs
CHANGED
|
@@ -8,6 +8,9 @@ import { reconcile } from '../lib/updateReconcile.mjs';
|
|
|
8
8
|
import {
|
|
9
9
|
CONSUMER_MANIFEST_NAME, PACKAGE_MANIFEST_NAME, readManifest,
|
|
10
10
|
} from '../lib/manifest.mjs';
|
|
11
|
+
import {
|
|
12
|
+
inspectRoutingProfile, reconcileRoutingProfile,
|
|
13
|
+
} from '../lib/routingProfile.mjs';
|
|
11
14
|
|
|
12
15
|
const RELEASE_NAME = '@ikon85/agent-workflow-kit';
|
|
13
16
|
|
|
@@ -22,6 +25,25 @@ export function renderUpdateFailure(result) {
|
|
|
22
25
|
* checking -> preview/awaiting_decision -> staging -> verifying -> terminal state.
|
|
23
26
|
*/
|
|
24
27
|
export async function update(options) {
|
|
28
|
+
const preflight = options.routingProfile
|
|
29
|
+
? await inspectRoutingProfile({ consumerRoot: options.consumerRoot, ...options.routingProfile })
|
|
30
|
+
: null;
|
|
31
|
+
const result = await updatePackage(options);
|
|
32
|
+
if (!preflight || options.dryRun || result.state !== 'applied') return result;
|
|
33
|
+
const inspection = await inspectRoutingProfile({
|
|
34
|
+
consumerRoot: options.consumerRoot,
|
|
35
|
+
...options.routingProfile,
|
|
36
|
+
});
|
|
37
|
+
return {
|
|
38
|
+
...result,
|
|
39
|
+
routingProfile: await reconcileRoutingProfile(
|
|
40
|
+
{ consumerRoot: options.consumerRoot, ...options.routingProfile },
|
|
41
|
+
inspection,
|
|
42
|
+
),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function updatePackage(options) {
|
|
25
47
|
const {
|
|
26
48
|
kitRoot, consumerRoot, decide = () => false, dryRun = false,
|
|
27
49
|
releaseIdentities, verify = verifyCandidate, activate = activateCandidate,
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { access } from 'node:fs/promises';
|
|
2
|
+
import { delimiter, join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
export const AGENT_SURFACE_REGISTRY_REVISION = 1;
|
|
5
|
+
|
|
6
|
+
export const AGENT_SURFACE_REGISTRY = Object.freeze([
|
|
7
|
+
Object.freeze({
|
|
8
|
+
id: 'claude-code',
|
|
9
|
+
label: 'Claude Code',
|
|
10
|
+
command: 'claude',
|
|
11
|
+
activeEnvironment: Object.freeze(['CLAUDE_CODE_ENTRYPOINT']),
|
|
12
|
+
adapter: Object.freeze({
|
|
13
|
+
providers: Object.freeze(['anthropic']),
|
|
14
|
+
transports: Object.freeze(['native', 'codex-cli']),
|
|
15
|
+
enforcement: Object.freeze({ model: 'named-agent', effort: 'named-agent' }),
|
|
16
|
+
}),
|
|
17
|
+
}),
|
|
18
|
+
Object.freeze({
|
|
19
|
+
id: 'codex',
|
|
20
|
+
label: 'Codex',
|
|
21
|
+
command: 'codex',
|
|
22
|
+
activeEnvironment: Object.freeze(['CODEX_THREAD_ID']),
|
|
23
|
+
adapter: Object.freeze({
|
|
24
|
+
providers: Object.freeze(['openai']),
|
|
25
|
+
transports: Object.freeze(['native', 'claude-cli']),
|
|
26
|
+
enforcement: Object.freeze({ model: 'per-spawn', effort: 'per-spawn' }),
|
|
27
|
+
}),
|
|
28
|
+
}),
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
async function commandOnPath(command, env = process.env) {
|
|
32
|
+
for (const directory of (env.PATH ?? '').split(delimiter).filter(Boolean)) {
|
|
33
|
+
if (await access(join(directory, command)).then(() => true, () => false)) return true;
|
|
34
|
+
}
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function detectAgentSurfaces({
|
|
39
|
+
registry = AGENT_SURFACE_REGISTRY,
|
|
40
|
+
commandAvailable = commandOnPath,
|
|
41
|
+
} = {}) {
|
|
42
|
+
return Promise.all(registry.map(async (surface) => Object.freeze({
|
|
43
|
+
id: surface.id,
|
|
44
|
+
label: surface.label,
|
|
45
|
+
detected: await commandAvailable(surface.command),
|
|
46
|
+
adapter: surface.adapter,
|
|
47
|
+
})));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function surfaceById(id, registry = AGENT_SURFACE_REGISTRY) {
|
|
51
|
+
return registry.find((surface) => surface.id === id);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function currentAgentSurface({
|
|
55
|
+
registry = AGENT_SURFACE_REGISTRY,
|
|
56
|
+
env = process.env,
|
|
57
|
+
} = {}) {
|
|
58
|
+
return registry.find((surface) =>
|
|
59
|
+
surface.activeEnvironment.some((name) => typeof env[name] === 'string' && env[name] !== ''))?.id;
|
|
60
|
+
}
|
package/src/lib/bundle.mjs
CHANGED
|
@@ -77,6 +77,30 @@ export const HELPER_FILES = [
|
|
|
77
77
|
{ path: 'src/lib/reportValidator.mjs', kind: 'script', mode: 0o644 },
|
|
78
78
|
// Fail-closed orchestration selector and its host-inventory adapters.
|
|
79
79
|
{ path: 'src/lib/capabilityMatrix.mjs', kind: 'script', mode: 0o644 },
|
|
80
|
+
// Provider-neutral routing runtime: one Evidence catalog, Access graph,
|
|
81
|
+
// Routing policy, resolver, spawn guard, receipt v2, and surface attestations.
|
|
82
|
+
// These modules form one consumer unit; omitting one leaves shipped dispatch
|
|
83
|
+
// prose pointing at a resolver or adapter that cannot execute.
|
|
84
|
+
{ path: 'src/commands/routing-policy-update.mjs', kind: 'script', mode: 0o644 },
|
|
85
|
+
{ path: 'src/lib/agentSurfaceRegistry.mjs', kind: 'script', mode: 0o644 },
|
|
86
|
+
{ path: 'src/lib/dispatchReceipt.mjs', kind: 'script', mode: 0o644 },
|
|
87
|
+
{ path: 'src/lib/frontendWorkloads.mjs', kind: 'script', mode: 0o644 },
|
|
88
|
+
{ path: 'src/lib/routeDispatcher.mjs', kind: 'script', mode: 0o644 },
|
|
89
|
+
{ path: 'src/lib/routingAccessGraph.mjs', kind: 'script', mode: 0o644 },
|
|
90
|
+
{ path: 'src/lib/routingAdapters/claude.mjs', kind: 'script', mode: 0o644 },
|
|
91
|
+
{ path: 'src/lib/routingAdapters/codex.mjs', kind: 'script', mode: 0o644 },
|
|
92
|
+
{ path: 'src/lib/routingCatalog.mjs', kind: 'script', mode: 0o644 },
|
|
93
|
+
{ path: 'src/lib/routingEvidenceCache.mjs', kind: 'script', mode: 0o644 },
|
|
94
|
+
{ path: 'src/lib/routingIntent.mjs', kind: 'script', mode: 0o644 },
|
|
95
|
+
{ path: 'src/lib/routingPolicy.mjs', kind: 'script', mode: 0o644 },
|
|
96
|
+
{ path: 'src/lib/routingProfile.mjs', kind: 'script', mode: 0o644 },
|
|
97
|
+
{ path: 'src/lib/routingResolver.mjs', kind: 'script', mode: 0o644 },
|
|
98
|
+
{ path: 'src/lib/routingSources/artificialAnalysis.mjs', kind: 'script', mode: 0o644 },
|
|
99
|
+
{ path: 'src/lib/routingSources/benchlm.mjs', kind: 'script', mode: 0o644 },
|
|
100
|
+
{ path: 'src/lib/routingSources/codeArena.mjs', kind: 'script', mode: 0o644 },
|
|
101
|
+
{ path: 'src/lib/routingSources/deepswe.mjs', kind: 'script', mode: 0o644 },
|
|
102
|
+
{ path: 'src/lib/routingSources/openhands.mjs', kind: 'script', mode: 0o644 },
|
|
103
|
+
{ path: 'src/lib/routingSources/openhandsFrontend.mjs', kind: 'script', mode: 0o644 },
|
|
80
104
|
// Main-thread recon boundary shared by every orchestration path.
|
|
81
105
|
{ path: 'src/lib/reconcileReconReports.mjs', kind: 'script', mode: 0o644 },
|
|
82
106
|
// Atomic compare-and-set wave claim: two sessions cannot orchestrate the same
|
|
@@ -62,6 +62,91 @@ export const capabilityAdapter = Object.freeze({
|
|
|
62
62
|
codex: adaptInventory,
|
|
63
63
|
});
|
|
64
64
|
|
|
65
|
+
const ROUTE_IDENTITY_FIELDS = [
|
|
66
|
+
'id',
|
|
67
|
+
'surfaceId',
|
|
68
|
+
'providerId',
|
|
69
|
+
'modelId',
|
|
70
|
+
'transportId',
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
const ROUTE_ENFORCEMENT_METHODS = [
|
|
74
|
+
'per-spawn',
|
|
75
|
+
'named-agent',
|
|
76
|
+
'session-default',
|
|
77
|
+
'none',
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
const ROUTE_PRECEDENCE = [
|
|
81
|
+
'explicit-argument',
|
|
82
|
+
'agent-definition-over-environment',
|
|
83
|
+
'environment-over-agent-definition',
|
|
84
|
+
'session-default',
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
function routingControl(control) {
|
|
88
|
+
const source = control && typeof control === 'object' && !Array.isArray(control)
|
|
89
|
+
? control
|
|
90
|
+
: {};
|
|
91
|
+
return {
|
|
92
|
+
method: ROUTE_ENFORCEMENT_METHODS.includes(source.method) ? source.method : UNKNOWN,
|
|
93
|
+
enforced: triState(source.enforced),
|
|
94
|
+
precedence: ROUTE_PRECEDENCE.includes(source.precedence)
|
|
95
|
+
? source.precedence
|
|
96
|
+
: UNKNOWN,
|
|
97
|
+
applied: typeof source.applied === 'string' && source.applied !== ''
|
|
98
|
+
? source.applied
|
|
99
|
+
: UNKNOWN,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function routingPath(path) {
|
|
104
|
+
const source = path && typeof path === 'object' && !Array.isArray(path) ? path : {};
|
|
105
|
+
const normalized = Object.fromEntries(ROUTE_IDENTITY_FIELDS.map((field) => [
|
|
106
|
+
field,
|
|
107
|
+
typeof source[field] === 'string' && source[field] !== '' ? source[field] : UNKNOWN,
|
|
108
|
+
]));
|
|
109
|
+
normalized.detected = triState(source.detected);
|
|
110
|
+
normalized.callable = triState(source.callable);
|
|
111
|
+
normalized.permitted = triState(source.permitted);
|
|
112
|
+
normalized.model = routingControl(source.model);
|
|
113
|
+
normalized.effort = routingControl(source.effort);
|
|
114
|
+
const failures = [];
|
|
115
|
+
if (ROUTE_IDENTITY_FIELDS.some((field) => normalized[field] === UNKNOWN)) {
|
|
116
|
+
failures.push('route identity is incomplete');
|
|
117
|
+
}
|
|
118
|
+
if (normalized.detected !== true) failures.push('transport is not detected');
|
|
119
|
+
if (normalized.callable !== true) failures.push('transport is not callable');
|
|
120
|
+
if (normalized.permitted !== true) failures.push('transport is not permitted');
|
|
121
|
+
for (const field of ['model', 'effort']) {
|
|
122
|
+
if (normalized[field].enforced !== true || normalized[field].method === 'none'
|
|
123
|
+
|| normalized[field].method === UNKNOWN) {
|
|
124
|
+
failures.push(`${field} control is not enforced`);
|
|
125
|
+
}
|
|
126
|
+
if (normalized[field].precedence === UNKNOWN) {
|
|
127
|
+
failures.push(`${field} environment precedence is unverified`);
|
|
128
|
+
}
|
|
129
|
+
if (normalized[field].applied === UNKNOWN) {
|
|
130
|
+
failures.push(`${field} applied value is unverified`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
normalized.verified = failures.length === 0;
|
|
134
|
+
normalized.verificationFailures = failures;
|
|
135
|
+
return Object.freeze(normalized);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function adaptClaudeRoutingInventory(inventory) {
|
|
139
|
+
const source = inventory && typeof inventory === 'object' && !Array.isArray(inventory)
|
|
140
|
+
? inventory
|
|
141
|
+
: {};
|
|
142
|
+
return Object.freeze({
|
|
143
|
+
contractVersion: 1,
|
|
144
|
+
paths: source.contractVersion === 1 && Array.isArray(source.paths)
|
|
145
|
+
? Object.freeze(source.paths.map(routingPath))
|
|
146
|
+
: Object.freeze([]),
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
65
150
|
function proves(tool, capability) {
|
|
66
151
|
return tool?.callable === true
|
|
67
152
|
&& tool.permitted === true
|