@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.
Files changed (67) hide show
  1. package/.agents/skills/audit-skills/SKILL.md +7 -4
  2. package/.agents/skills/code-review/SKILL.md +7 -4
  3. package/.agents/skills/codebase-design/DESIGN-IT-TWICE.md +7 -4
  4. package/.agents/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +7 -4
  5. package/.agents/skills/improve-codebase-architecture/SKILL.md +7 -4
  6. package/.agents/skills/orchestrate-wave/SKILL.md +1 -1
  7. package/.agents/skills/orchestrate-wave/references/dispatch-subagents.md +9 -0
  8. package/.agents/skills/orchestrate-wave/references/dispatch-workflow.md +9 -0
  9. package/.agents/skills/research/SKILL.md +7 -4
  10. package/.agents/skills/to-issues/SKILL.md +25 -4
  11. package/.claude/skills/audit-skills/SKILL.md +7 -4
  12. package/.claude/skills/code-review/SKILL.md +7 -4
  13. package/.claude/skills/codebase-design/DESIGN-IT-TWICE.md +7 -4
  14. package/.claude/skills/codex-build/SKILL.md +13 -0
  15. package/.claude/skills/codex-review/SKILL.md +13 -0
  16. package/.claude/skills/grill-me-codex/SKILL.md +14 -0
  17. package/.claude/skills/grill-with-docs-codex/SKILL.md +14 -0
  18. package/.claude/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +7 -4
  19. package/.claude/skills/improve-codebase-architecture/SKILL.md +7 -4
  20. package/.claude/skills/orchestrate-wave/SKILL.md +1 -1
  21. package/.claude/skills/orchestrate-wave/references/dispatch-subagents.md +9 -0
  22. package/.claude/skills/orchestrate-wave/references/dispatch-workflow.md +9 -0
  23. package/.claude/skills/research/SKILL.md +7 -4
  24. package/.claude/skills/skill-manifest.json +34 -23
  25. package/.claude/skills/to-issues/SKILL.md +25 -4
  26. package/README.md +71 -0
  27. package/agent-workflow-kit.package.json +149 -45
  28. package/package.json +1 -1
  29. package/scripts/doctrine-migration/index.mjs +296 -0
  30. package/scripts/kit-release.mjs +41 -9
  31. package/src/cli.mjs +521 -80
  32. package/src/commands/routing-status.mjs +288 -0
  33. package/src/commands/update.mjs +9 -1
  34. package/src/consumer-migrations.json +23 -1
  35. package/src/lib/bundle.mjs +158 -2
  36. package/src/lib/consumerMigrations.mjs +55 -0
  37. package/src/lib/dispatchJournal.mjs +300 -0
  38. package/src/lib/dispatchPlan.mjs +286 -0
  39. package/src/lib/dispatchReceipt.mjs +226 -89
  40. package/src/lib/frontendWorkloads.mjs +35 -33
  41. package/src/lib/routeDispatcher.mjs +367 -70
  42. package/src/lib/routingAccessGraph.mjs +265 -20
  43. package/src/lib/routingAccessGraphStore.mjs +300 -0
  44. package/src/lib/routingAdapters/claude.mjs +104 -4
  45. package/src/lib/routingAdapters/codex.mjs +132 -7
  46. package/src/lib/routingAdapters/hostBridge.mjs +291 -0
  47. package/src/lib/routingCatalog.mjs +201 -24
  48. package/src/lib/routingDispatchLease.mjs +253 -0
  49. package/src/lib/routingEvidenceCache.mjs +78 -0
  50. package/src/lib/routingIntent.mjs +176 -10
  51. package/src/lib/routingIntentClassifier.mjs +137 -0
  52. package/src/lib/routingInventory/snapshots/claude.json +49 -0
  53. package/src/lib/routingInventory/snapshots/codex.json +109 -0
  54. package/src/lib/routingInventory.mjs +182 -0
  55. package/src/lib/routingPolicy.mjs +193 -6
  56. package/src/lib/routingProfile.mjs +1251 -123
  57. package/src/lib/routingProfilePolicy.mjs +156 -0
  58. package/src/lib/routingProfileStorage.mjs +299 -0
  59. package/src/lib/routingResolver.mjs +369 -86
  60. package/src/lib/routingSources/artificialAnalysis.mjs +19 -7
  61. package/src/lib/routingSources/benchlm.mjs +5 -0
  62. package/src/lib/routingSources/codeArena.mjs +13 -3
  63. package/src/lib/routingSources/deepswe.mjs +19 -5
  64. package/src/lib/routingSources/openhands.mjs +17 -4
  65. package/src/lib/routingSources/openhandsFrontend.mjs +13 -3
  66. package/src/lib/safeText.mjs +26 -0
  67. package/src/lib/updateCandidate.mjs +36 -3
@@ -1,6 +1,51 @@
1
- export const ROUTING_POLICY_VERSION = 1;
1
+ /**
2
+ * The Routing policy schema: the constraint object derived from a Routing
3
+ * profile for one dispatch — allowed surfaces, allowed transports, the
4
+ * authorized Model roster, the effective Standard routes, and switching
5
+ * autonomy. It carries a revision so a Dispatch receipt can prove which
6
+ * constraints applied, and it never changes Evidence catalog facts.
7
+ *
8
+ * v2 removes the optimization dial. What a user is willing to spend
9
+ * is a Routing profile choice, not a per-dispatch constraint, so the roster
10
+ * pairs and the effective Standard routes move in instead: the resolver keeps
11
+ * its four inputs and one receipt names one revision. `unreachable` and
12
+ * `missingInfrastructure` stay — they are the resolver's fallback semantics and
13
+ * have nothing to do with the removed dial.
14
+ *
15
+ * Fail-closed by construction. A policy that authorizes no roster pair and no
16
+ * Standard route is invalid rather than an empty default that would read as a
17
+ * policy; dispatch blocks with that named reason.
18
+ *
19
+ * Authorization only. The policy states what a user authorized; whether a pair
20
+ * is reachable is decided at decision time against the Access graph, never
21
+ * here. The roster vocabulary and the Standard-route classes come from the
22
+ * Routing profile, which owns them — this module never restates them.
23
+ */
24
+ import {
25
+ STANDARD_ROUTE_CLASSES,
26
+ STANDARD_ROUTE_STATES,
27
+ normalizeRosterModelId,
28
+ } from './routingProfile.mjs';
29
+
30
+ export const ROUTING_POLICY_VERSION = 2;
31
+ export const ROUTING_POLICY_LEGACY_VERSION = 1;
32
+
33
+ /** The reason an authorization-free policy gives, so a dispatch can name it. */
34
+ export const ROUTING_POLICY_NOT_DERIVABLE = 'routing-policy-not-derivable';
2
35
 
3
36
  const POLICY_FIELDS = new Set([
37
+ 'schemaVersion',
38
+ 'revision',
39
+ 'allowedSurfaces',
40
+ 'allowedTransports',
41
+ 'switching',
42
+ 'roster',
43
+ 'standardRoutes',
44
+ 'unreachable',
45
+ 'missingInfrastructure',
46
+ ]);
47
+ /** The v1 shape, kept only so the decoder can read one and say what it dropped. */
48
+ const LEGACY_POLICY_FIELDS = new Set([
4
49
  'schemaVersion',
5
50
  'revision',
6
51
  'allowedSurfaces',
@@ -10,9 +55,12 @@ const POLICY_FIELDS = new Set([
10
55
  'unreachable',
11
56
  'missingInfrastructure',
12
57
  ]);
58
+ /** What a v1 document cannot carry and a caller must therefore supply. */
59
+ const DERIVATION_FIELDS = new Set(['roster', 'standardRoutes']);
60
+ const PAIR_FIELDS = new Set(['model', 'effort']);
61
+ const STANDARD_ROUTE_FIELDS = new Set(['model', 'effort', 'state']);
13
62
 
14
63
  const SWITCHING = ['automatic', 'ask', 'current-surface-only'];
15
- const OPTIMIZATION = ['quality', 'balanced', 'cost'];
16
64
  const UNREACHABLE = ['handoff', 'inherit', 'block'];
17
65
  const MISSING_INFRASTRUCTURE = ['inherit', 'block'];
18
66
 
@@ -23,6 +71,12 @@ function object(value, field) {
23
71
  return value;
24
72
  }
25
73
 
74
+ function assertFields(input, allowed, label) {
75
+ for (const key of Object.keys(input)) {
76
+ if (!allowed.has(key)) throw new TypeError(`unknown ${label} field: ${key}`);
77
+ }
78
+ }
79
+
26
80
  function string(value, field) {
27
81
  if (typeof value !== 'string' || value.trim() === '') {
28
82
  throw new TypeError(`${field} must be a non-empty string`);
@@ -42,21 +96,106 @@ function oneOf(value, values, field) {
42
96
  return value;
43
97
  }
44
98
 
99
+ /**
100
+ * A model-and-effort pair, under the roster's own identity rule: effort is
101
+ * always explicit, `null` meaning a model with no effort axis.
102
+ */
103
+ function policyPair(input, field) {
104
+ object(input, field);
105
+ assertFields(input, PAIR_FIELDS, field);
106
+ const model = normalizeRosterModelId(input.model, `${field}.model`);
107
+ if (!('effort' in input) || (input.effort !== null
108
+ && (typeof input.effort !== 'string' || input.effort.trim() === ''))) {
109
+ throw new TypeError(`${field}.effort must be a non-empty string or null (no effort axis)`);
110
+ }
111
+ return { model, effort: input.effort === null ? null : input.effort.trim() };
112
+ }
113
+
114
+ const pairKey = ({ model, effort }) => `${model}\u0000${effort ?? ''}`;
115
+
116
+ /** The positive list this policy authorizes: pairs, deduped, order preserved. */
117
+ function validateRoster(value) {
118
+ if (!Array.isArray(value)) {
119
+ throw new TypeError('roster must be an array of {model, effort} pairs');
120
+ }
121
+ const pairs = new Map();
122
+ value.forEach((input, index) => {
123
+ const pair = policyPair(input, `roster[${index}]`);
124
+ pairs.set(pairKey(pair), Object.freeze(pair));
125
+ });
126
+ return [...pairs.values()];
127
+ }
128
+
129
+ /**
130
+ * The effective Standard routes, one per workload class. A `configured` route
131
+ * must name a pair the roster authorizes — the policy is the authorization
132
+ * surface, so an authorized route over an unauthorized pair is a contradiction,
133
+ * not a fallback. An `unresolved` route keeps a knowingly broken nomination
134
+ * instead of invalidating the whole policy; it authorizes nothing.
135
+ */
136
+ function validateStandardRoutes(value, roster) {
137
+ object(value, 'standardRoutes');
138
+ assertFields(value, new Set(STANDARD_ROUTE_CLASSES), 'standardRoutes');
139
+ const authorized = new Set(roster.map(pairKey));
140
+ const routes = {};
141
+ for (const workload of STANDARD_ROUTE_CLASSES) {
142
+ if (!(workload in value)) {
143
+ throw new TypeError(
144
+ `standardRoutes must name every workload class: ${STANDARD_ROUTE_CLASSES.join(', ')}`,
145
+ );
146
+ }
147
+ if (value[workload] === null) {
148
+ routes[workload] = null;
149
+ continue;
150
+ }
151
+ const field = `standardRoutes.${workload}`;
152
+ object(value[workload], field);
153
+ assertFields(value[workload], STANDARD_ROUTE_FIELDS, field);
154
+ const pair = policyPair({ model: value[workload].model, effort: value[workload].effort }, field);
155
+ const state = oneOf(value[workload].state, STANDARD_ROUTE_STATES, `${field}.state`);
156
+ if (state === 'configured' && !authorized.has(pairKey(pair))) {
157
+ throw new TypeError(`${field} must name an authorized roster pair`);
158
+ }
159
+ routes[workload] = Object.freeze({ ...pair, state });
160
+ }
161
+ return routes;
162
+ }
163
+
164
+ /** Never a silent default: a policy that authorizes nothing is invalid. */
165
+ function assertDerivable(roster, routes) {
166
+ const configured = STANDARD_ROUTE_CLASSES.some((cls) => routes[cls]?.state === 'configured');
167
+ if (roster.length === 0 && !configured) {
168
+ throw new TypeError('routing policy derives neither a Model roster nor a Standard route: '
169
+ + ROUTING_POLICY_NOT_DERIVABLE);
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Validate a Routing policy. A v1 document is accepted and decoded, so a caller
175
+ * that still holds one gets the named migration reason rather than an opaque
176
+ * schema error.
177
+ */
45
178
  export function validateRoutingPolicy(input) {
46
179
  object(input, 'routing policy');
47
- for (const key of Object.keys(input)) {
48
- if (!POLICY_FIELDS.has(key)) throw new TypeError(`unknown routing policy field: ${key}`);
180
+ if (input.schemaVersion === ROUTING_POLICY_LEGACY_VERSION) {
181
+ return decodeRoutingPolicy(input).policy;
49
182
  }
183
+ assertFields(input, POLICY_FIELDS, 'routing policy');
50
184
  if (input.schemaVersion !== ROUTING_POLICY_VERSION) {
51
- throw new TypeError(`routing policy schemaVersion must be ${ROUTING_POLICY_VERSION}`);
185
+ throw new TypeError('routing policy schemaVersion must be '
186
+ + `${ROUTING_POLICY_LEGACY_VERSION} or ${ROUTING_POLICY_VERSION}`);
52
187
  }
188
+ const roster = validateRoster(input.roster);
189
+ const standardRoutes = validateStandardRoutes(input.standardRoutes, roster);
190
+ assertDerivable(roster, standardRoutes);
53
191
  return Object.freeze({
54
192
  schemaVersion: ROUTING_POLICY_VERSION,
55
193
  revision: string(input.revision, 'routing policy revision'),
56
194
  allowedSurfaces: Object.freeze(stringList(input.allowedSurfaces, 'allowedSurfaces')),
57
195
  allowedTransports: Object.freeze(stringList(input.allowedTransports, 'allowedTransports')),
58
196
  switching: oneOf(input.switching, SWITCHING, 'switching'),
59
- optimization: oneOf(input.optimization, OPTIMIZATION, 'optimization'),
197
+ roster: Object.freeze(roster),
198
+ standardRoutes: Object.freeze(standardRoutes),
60
199
  unreachable: oneOf(input.unreachable, UNREACHABLE, 'unreachable'),
61
200
  missingInfrastructure: oneOf(
62
201
  input.missingInfrastructure,
@@ -65,3 +204,51 @@ export function validateRoutingPolicy(input) {
65
204
  ),
66
205
  });
67
206
  }
207
+
208
+ function droppedOptimization(document) {
209
+ if (!('optimization' in document)) return { dropped: [], notes: [] };
210
+ return {
211
+ dropped: ['optimization'],
212
+ notes: [Object.freeze({ code: 'optimization-removed', value: document.optimization ?? null })],
213
+ };
214
+ }
215
+
216
+ /**
217
+ * The deterministic v1 → v2 decode. The optimization dial is dropped and
218
+ * *recorded*, never silently ignored, and the roster and Standard routes a v1
219
+ * document cannot carry come from the derivation its caller already holds. A
220
+ * decode that can derive neither fails closed with that reason instead of
221
+ * producing an empty authorization that would read as a policy.
222
+ */
223
+ export function decodeRoutingPolicy(document, derivation = {}) {
224
+ object(document, 'routing policy');
225
+ object(derivation, 'routing policy derivation');
226
+ assertFields(derivation, DERIVATION_FIELDS, 'routing policy derivation');
227
+ if (document.schemaVersion === ROUTING_POLICY_VERSION) {
228
+ return Object.freeze({
229
+ policy: validateRoutingPolicy(document),
230
+ fromVersion: ROUTING_POLICY_VERSION,
231
+ dropped: Object.freeze([]),
232
+ notes: Object.freeze([]),
233
+ });
234
+ }
235
+ if (document.schemaVersion !== ROUTING_POLICY_LEGACY_VERSION) {
236
+ throw new TypeError('routing policy schemaVersion must be '
237
+ + `${ROUTING_POLICY_LEGACY_VERSION} or ${ROUTING_POLICY_VERSION}`);
238
+ }
239
+ assertFields(document, LEGACY_POLICY_FIELDS, 'routing policy');
240
+ const { dropped, notes } = droppedOptimization(document);
241
+ const { schemaVersion, optimization, ...carried } = document;
242
+ return Object.freeze({
243
+ policy: validateRoutingPolicy({
244
+ ...carried,
245
+ schemaVersion: ROUTING_POLICY_VERSION,
246
+ roster: derivation.roster ?? [],
247
+ standardRoutes: derivation.standardRoutes
248
+ ?? Object.fromEntries(STANDARD_ROUTE_CLASSES.map((cls) => [cls, null])),
249
+ }),
250
+ fromVersion: ROUTING_POLICY_LEGACY_VERSION,
251
+ dropped: Object.freeze(dropped),
252
+ notes: Object.freeze(notes),
253
+ });
254
+ }