@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
package/src/cli.mjs
CHANGED
|
@@ -8,6 +8,9 @@ import { renderUpdateFailure, update } from './commands/update.mjs';
|
|
|
8
8
|
import { diff } from './commands/diff.mjs';
|
|
9
9
|
import { uninstall } from './commands/uninstall.mjs';
|
|
10
10
|
import { setOwnership } from './commands/own.mjs';
|
|
11
|
+
import {
|
|
12
|
+
routeOrigin, routingStatus, routingStatusFailure,
|
|
13
|
+
} from './commands/routing-status.mjs';
|
|
11
14
|
import {
|
|
12
15
|
beginContributionBridge, prepareContributionArtifact,
|
|
13
16
|
} from './lib/contributionBridge.mjs';
|
|
@@ -16,13 +19,84 @@ import {
|
|
|
16
19
|
} from './lib/contributionRouting.mjs';
|
|
17
20
|
import { CONSUMER_ORIGIN, KIT_ORIGIN } from './lib/manifest.mjs';
|
|
18
21
|
import { nonInteractiveUpdateDecision } from './lib/updateDecisions.mjs';
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
22
|
+
import { sanitizeReadinessText } from './lib/updateCandidate.mjs';
|
|
23
|
+
import {
|
|
24
|
+
renderConsumerAdvisory, renderRequiredMigration,
|
|
25
|
+
} from './lib/consumerMigrations.mjs';
|
|
26
|
+
import { currentAgentSurface, surfaceById } from './lib/agentSurfaceRegistry.mjs';
|
|
27
|
+
import { routingProfilePath } from './lib/routingProfile.mjs';
|
|
21
28
|
import { createCommandAdapter } from '../scripts/release-state.mjs';
|
|
22
29
|
import { installedIdentityFromDir } from '../scripts/release-parity.mjs';
|
|
23
30
|
|
|
24
31
|
const KIT_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
25
32
|
|
|
33
|
+
// Declared before the self-invocation below: `runCli` renders the plan during
|
|
34
|
+
// module evaluation, so a later `const` would be in its temporal dead zone.
|
|
35
|
+
const READINESS_STATE_PHRASE = {
|
|
36
|
+
missing: 'not configured yet',
|
|
37
|
+
pending: 'deferred as pending',
|
|
38
|
+
invalid: 'present but not valid',
|
|
39
|
+
'not-applicable': 'recorded as not applicable',
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Declared here for the same reason: `promptRoutingProfile` runs inside
|
|
43
|
+
// `runCli`, i.e. during module evaluation, so these tables must be initialized
|
|
44
|
+
// before the self-invocation below.
|
|
45
|
+
|
|
46
|
+
/** What choosing an option means, per routing prompt. */
|
|
47
|
+
const ROUTING_HINTS = {
|
|
48
|
+
autonomy: {
|
|
49
|
+
automatic: 'the Kit may move a task to another selected app on its own',
|
|
50
|
+
ask: 'the Kit proposes a switch and waits for your confirmation',
|
|
51
|
+
'current-surface-only': 'every task stays in the app it started in',
|
|
52
|
+
},
|
|
53
|
+
activation: {
|
|
54
|
+
approve: 'store this routing profile exactly as summarized above',
|
|
55
|
+
'safe-current-surface': 'store it, but keep every task in the current app',
|
|
56
|
+
back: 'answer the app and switching questions again',
|
|
57
|
+
advanced: 'add optional preferences to the draft before anything is stored',
|
|
58
|
+
decline: 'store nothing; the Kit asks again on the next update',
|
|
59
|
+
},
|
|
60
|
+
advanced: {
|
|
61
|
+
balanced: 'note a preference for a balance of quality and cost',
|
|
62
|
+
quality: 'note a preference for the strongest model even when it costs more',
|
|
63
|
+
cost: 'note a preference for the cheaper model when it can do the job',
|
|
64
|
+
},
|
|
65
|
+
reconcile: {
|
|
66
|
+
review: 'answer the routing questions now and store an updated profile',
|
|
67
|
+
decline: 'change nothing now; the Kit asks again on the next update',
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/** Option labels the routing question does not carry itself. */
|
|
72
|
+
const ROUTING_LABELS = {
|
|
73
|
+
activation: {
|
|
74
|
+
approve: 'Approve',
|
|
75
|
+
'safe-current-surface': 'Safe current surface',
|
|
76
|
+
back: 'Back',
|
|
77
|
+
advanced: 'Advanced',
|
|
78
|
+
decline: 'Decline',
|
|
79
|
+
},
|
|
80
|
+
advanced: { balanced: 'Balanced', quality: 'Quality', cost: 'Cost' },
|
|
81
|
+
reconcile: { review: 'Review routing choices now', decline: 'Not now' },
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const ADVANCED_OPTIMIZATIONS = ['balanced', 'quality', 'cost'];
|
|
85
|
+
const RECONCILE_MIGRATION_ACTIONS = ['review', 'decline'];
|
|
86
|
+
/** The answer that nominates no Standard route for a workload class. */
|
|
87
|
+
const NO_STANDARD_ROUTE = 'none';
|
|
88
|
+
|
|
89
|
+
const ROUTING_PAYLOADS = {
|
|
90
|
+
surfaces: surfacesPayload,
|
|
91
|
+
transports: transportsPayload,
|
|
92
|
+
autonomy: autonomyPayload,
|
|
93
|
+
roster: rosterPayload,
|
|
94
|
+
'standard-route': standardRoutePayload,
|
|
95
|
+
activation: activationPayload,
|
|
96
|
+
advanced: advancedPayload,
|
|
97
|
+
reconcile: reconcilePayload,
|
|
98
|
+
};
|
|
99
|
+
|
|
26
100
|
function stamp() {
|
|
27
101
|
// backup suffix: YYYYMMDDTHHMMSS (no separators that collide with shells)
|
|
28
102
|
return new Date().toISOString().replace(/[-:]/g, '').replace(/\..+/, '');
|
|
@@ -46,6 +120,8 @@ export async function runCli({
|
|
|
46
120
|
const ownershipState = args.find((arg) => arg.startsWith('--as='))?.slice('--as='.length);
|
|
47
121
|
// Machine-readable rendering of the same update record the interactive run prints.
|
|
48
122
|
const jsonReport = cmd === 'update' && args.includes('--json');
|
|
123
|
+
// The routing status machine contract: the document is the only thing on stdout.
|
|
124
|
+
const routingJson = cmd === 'routing' && args.includes('--json');
|
|
49
125
|
let exitCode = 0;
|
|
50
126
|
|
|
51
127
|
if (cmd === 'update' && keepDeleted && restoreDeleted) {
|
|
@@ -60,7 +136,7 @@ export async function runCli({
|
|
|
60
136
|
return 1;
|
|
61
137
|
}
|
|
62
138
|
|
|
63
|
-
if (!jsonReport) p.intro('agent-workflow-kit');
|
|
139
|
+
if (!jsonReport && !routingJson) p.intro('agent-workflow-kit');
|
|
64
140
|
|
|
65
141
|
try {
|
|
66
142
|
if (cmd === 'init') {
|
|
@@ -75,7 +151,7 @@ export async function runCli({
|
|
|
75
151
|
(r.skipped.length ? `\nskipped (pre-existing, use --force): ${r.skipped.join(', ')}` : ''),
|
|
76
152
|
'init'
|
|
77
153
|
);
|
|
78
|
-
printRoutingProfile(r.routingProfile);
|
|
154
|
+
printRoutingProfile(r.routingProfile, consumerRoot);
|
|
79
155
|
p.outro('Next: run /setup-workflow to fill the project layer + board profile. ' +
|
|
80
156
|
'To enable the drift-guard hook, add .claude/hooks/drift-guard.py to your settings.json hooks.');
|
|
81
157
|
} else if (cmd === 'diff') {
|
|
@@ -103,7 +179,7 @@ export async function runCli({
|
|
|
103
179
|
return r.state === 'conflicted' ? 2 : 0;
|
|
104
180
|
}
|
|
105
181
|
printPlan(r);
|
|
106
|
-
printRoutingProfile(r.routingProfile);
|
|
182
|
+
printRoutingProfile(r.routingProfile, consumerRoot);
|
|
107
183
|
for (const c of r.conflicts) p.note(c.diff || '(binary/!text)', `conflict (not applied): ${c.path}`);
|
|
108
184
|
if (r.state === 'failed') throw new Error(renderUpdateFailure(r));
|
|
109
185
|
if (r.state === 'conflicted') {
|
|
@@ -169,6 +245,19 @@ export async function runCli({
|
|
|
169
245
|
});
|
|
170
246
|
p.outro(`prepared local contribution artifact ${output}; no remote was changed`);
|
|
171
247
|
}
|
|
248
|
+
} else if (cmd === 'routing') {
|
|
249
|
+
if (args[1] !== 'status') {
|
|
250
|
+
throw new Error('Usage: agent-workflow-kit routing status --intent-file=<file> ' +
|
|
251
|
+
'[--surface=<id>] [--json]');
|
|
252
|
+
}
|
|
253
|
+
const status = await routingStatus({ argv: args.slice(2), consumerRoot });
|
|
254
|
+
if (routingJson) {
|
|
255
|
+
process.stdout.write(`${JSON.stringify(status.document, null, 2)}\n`);
|
|
256
|
+
return status.exitCode;
|
|
257
|
+
}
|
|
258
|
+
p.note(renderRoutingStatus(status.document), 'routing status');
|
|
259
|
+
p.outro(`routing status: ${status.document.outcome}`);
|
|
260
|
+
exitCode = status.exitCode;
|
|
172
261
|
} else if (cmd === 'own' || cmd === 'disown') {
|
|
173
262
|
if (!args[1]) {
|
|
174
263
|
throw new Error(
|
|
@@ -185,9 +274,11 @@ export async function runCli({
|
|
|
185
274
|
p.outro(`${args[1]} is now ${origin}-owned` +
|
|
186
275
|
(origin === CONSUMER_ORIGIN ? ` (${ownershipState ?? 'explicit-fork'})` : ''));
|
|
187
276
|
} else {
|
|
188
|
-
p.note('Usage: agent-workflow-kit
|
|
277
|
+
p.note('Usage: agent-workflow-kit ' +
|
|
278
|
+
'<init|update|diff|uninstall|own|disown|contribute|routing status> ' +
|
|
189
279
|
'[<path>] [--force] [--yes] [--keep-deleted|--restore-deleted] [--owned] ' +
|
|
190
|
-
'[--as=contribution-bridge|explicit-fork]'
|
|
280
|
+
'[--as=contribution-bridge|explicit-fork] ' +
|
|
281
|
+
'[--intent-file=<file>] [--surface=<id>] [--json]');
|
|
191
282
|
p.outro('');
|
|
192
283
|
}
|
|
193
284
|
} catch (err) {
|
|
@@ -197,6 +288,11 @@ export async function runCli({
|
|
|
197
288
|
}, null, 2)}\n`);
|
|
198
289
|
return 1;
|
|
199
290
|
}
|
|
291
|
+
if (routingJson) {
|
|
292
|
+
const document = routingStatusFailure(err.message);
|
|
293
|
+
process.stdout.write(`${JSON.stringify(document, null, 2)}\n`);
|
|
294
|
+
return document.exitCode;
|
|
295
|
+
}
|
|
200
296
|
p.cancel(`Error: ${err.message}`);
|
|
201
297
|
return 1;
|
|
202
298
|
}
|
|
@@ -233,15 +329,19 @@ function printPlan(r) {
|
|
|
233
329
|
if (r.availability) {
|
|
234
330
|
for (const [key, label] of [
|
|
235
331
|
['newlyAvailable', 'newly available'], ['newlyDegraded', 'newly degraded'],
|
|
236
|
-
['newlyBlocked', 'newly blocked'],
|
|
332
|
+
['newlyBlocked', 'newly blocked'],
|
|
237
333
|
]) {
|
|
238
334
|
lines.push(`${label}: ${r.availability[key].join(', ') || 'none'}`);
|
|
239
335
|
}
|
|
336
|
+
lines.push(...renderReadinessAvailability(r.availability));
|
|
240
337
|
}
|
|
241
338
|
for (const action of r.requiredMigrations ?? []) {
|
|
242
339
|
lines.push(renderRequiredMigration(action));
|
|
243
340
|
lines.push(` ${action.consequence} ${action.remediation}`);
|
|
244
341
|
}
|
|
342
|
+
for (const advisory of r.advisories ?? []) {
|
|
343
|
+
lines.push(renderConsumerAdvisory(advisory));
|
|
344
|
+
}
|
|
245
345
|
for (const owned of r.ownedDiffs ?? []) {
|
|
246
346
|
lines.push(`${owned.state} ${owned.path}`);
|
|
247
347
|
if (owned.binary) {
|
|
@@ -254,6 +354,77 @@ function printPlan(r) {
|
|
|
254
354
|
p.note(lines.join('\n') || 'no changes', 'plan');
|
|
255
355
|
}
|
|
256
356
|
|
|
357
|
+
/**
|
|
358
|
+
* Render unresolved readiness in plain terms — one sentence naming the
|
|
359
|
+
* capability and its state, one next step. Every line comes from the readiness
|
|
360
|
+
* catalog, so a new capability reads correctly without a code change; the
|
|
361
|
+
* legacy `capability:state` list stays the fallback for an older record.
|
|
362
|
+
*/
|
|
363
|
+
export function renderReadinessAvailability(availability) {
|
|
364
|
+
const entries = availability?.unresolved;
|
|
365
|
+
if (!Array.isArray(entries) || !entries.length) {
|
|
366
|
+
return [`still unresolved: ${availability?.stillUnresolved?.join(', ') || 'none'}`];
|
|
367
|
+
}
|
|
368
|
+
const lines = ['still unresolved:'];
|
|
369
|
+
for (const entry of entries) {
|
|
370
|
+
const subject = sanitizeReadinessText(entry.title) ?? entry.capability;
|
|
371
|
+
const phrase = READINESS_STATE_PHRASE[entry.state] ?? entry.state;
|
|
372
|
+
const evidence = (entry.evidencePaths ?? [])
|
|
373
|
+
.map(sanitizeReadinessText).filter(Boolean).join(', ');
|
|
374
|
+
lines.push(` ${subject} is ${phrase}${evidence ? ` (evidence: ${evidence})` : ''}.`);
|
|
375
|
+
const remedy = sanitizeReadinessText(entry.remedy);
|
|
376
|
+
if (remedy) lines.push(` next step: ${remedy}`);
|
|
377
|
+
}
|
|
378
|
+
return lines;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** A model-and-effort pair as a route reads out loud. */
|
|
382
|
+
function describeRoutePair(route) {
|
|
383
|
+
if (!route) return 'none';
|
|
384
|
+
return `${route.modelId} · ${route.effort ?? 'no effort axis'}`;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** Where the route would run — the Access path, not the evidence. */
|
|
388
|
+
function describeRouteAccess(route) {
|
|
389
|
+
if (!route?.surfaceId) return '';
|
|
390
|
+
const app = sanitizeReadinessText(surfaceById(route.surfaceId)?.label ?? route.surfaceId);
|
|
391
|
+
return ` on ${app} · ${route.transportId}`;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* The Route decision in the terms the question was asked in: what the evidence
|
|
396
|
+
* ranks first, what this machine could actually run, what it costs, what blocks
|
|
397
|
+
* it, and which revisions the answer stands on.
|
|
398
|
+
*/
|
|
399
|
+
export function renderRoutingStatus(document) {
|
|
400
|
+
const lines = [`outcome: ${document.outcome} (exit ${document.exitCode})`];
|
|
401
|
+
if (document.surface) {
|
|
402
|
+
lines.push(`surface: ${document.surface.id} (${document.surface.source})`);
|
|
403
|
+
}
|
|
404
|
+
if (document.intent) {
|
|
405
|
+
lines.push(`intent: ${document.intent.workload} · ${document.intent.reasoning} reasoning · `
|
|
406
|
+
+ `${document.intent.risk} risk · ${document.intent.autonomyRequirement}`);
|
|
407
|
+
}
|
|
408
|
+
if (document.state) {
|
|
409
|
+
lines.push(`pick: ${routeOrigin(document.origin).label} · ${document.state}`
|
|
410
|
+
+ `${document.reason ? ` (${document.reason})` : ''}`);
|
|
411
|
+
lines.push(`best overall: ${describeRoutePair(document.bestOverall?.route)}`
|
|
412
|
+
+ ` (${document.bestOverall?.status ?? 'unavailable'})`);
|
|
413
|
+
lines.push(`best executable: ${describeRoutePair(document.bestExecutable)}`
|
|
414
|
+
+ describeRouteAccess(document.bestExecutable));
|
|
415
|
+
lines.push(`cost per task: ${document.costPerTask
|
|
416
|
+
? `${document.costPerTask.amount} ${document.costPerTask.currency} `
|
|
417
|
+
+ `per ${document.costPerTask.unit}`
|
|
418
|
+
: 'none published'}`);
|
|
419
|
+
lines.push(`blockers: ${document.blockers.join(', ') || 'none'}`);
|
|
420
|
+
lines.push(`revisions: policy ${document.revisions?.policy ?? 'none'} · `
|
|
421
|
+
+ `catalog ${document.revisions?.catalog ?? 'none'} · `
|
|
422
|
+
+ `access graph ${document.revisions?.accessGraph ?? 'none'}`);
|
|
423
|
+
}
|
|
424
|
+
for (const { code, detail } of document.diagnostics) lines.push(`${code}: ${detail}`);
|
|
425
|
+
return lines.join('\n');
|
|
426
|
+
}
|
|
427
|
+
|
|
257
428
|
async function decideUpdate(action, path, yes, classification, choices = {}) {
|
|
258
429
|
if (yes) return nonInteractiveUpdateDecision(action, choices);
|
|
259
430
|
if (action === 'delete') {
|
|
@@ -284,86 +455,356 @@ function routingProfileOptions(yes) {
|
|
|
284
455
|
};
|
|
285
456
|
}
|
|
286
457
|
|
|
287
|
-
function printRoutingProfile(result) {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
p.note(`${result.status}${suffix}`, 'routing profile');
|
|
458
|
+
function printRoutingProfile(result, consumerRoot) {
|
|
459
|
+
const note = routingResultNote(result, consumerRoot);
|
|
460
|
+
if (note) p.note(note, 'routing profile');
|
|
291
461
|
}
|
|
292
462
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
463
|
+
/**
|
|
464
|
+
* The routing outcome in the terms a user can act on: what was decided, why,
|
|
465
|
+
* which profile it produced, and the user-local file that now holds it.
|
|
466
|
+
*/
|
|
467
|
+
export function routingResultNote(result, consumerRoot, profileRoot) {
|
|
468
|
+
if (!result) return null;
|
|
469
|
+
const lines = [`status: ${result.status}`];
|
|
470
|
+
if (result.reasons?.length) lines.push(`reasons: ${result.reasons.join(', ')}`);
|
|
471
|
+
if (result.profile) {
|
|
472
|
+
lines.push(`agent apps: ${surfaceLabels(result.profile.selectedSurfaces)}`);
|
|
473
|
+
lines.push(`transports: ${describeTransports(result.profile.authorizedTransports)}`);
|
|
474
|
+
lines.push(`switching: ${describeSwitching(result.profile.switching)}`);
|
|
475
|
+
lines.push(`model roster: ${describeRoster(result.profile.roster)}`);
|
|
476
|
+
lines.push(`standard routes: ${describeStandardRoutes(result.profile.standardRoutes)}`);
|
|
307
477
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
478
|
+
lines.push(`profile file: ${routingProfilePath(consumerRoot, profileRoot)}`);
|
|
479
|
+
return lines.join('\n');
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* The exact payload a routing question renders as — every option carries a
|
|
484
|
+
* hint saying what choosing it means, so no prompt asks a user to guess.
|
|
485
|
+
*/
|
|
486
|
+
export function routingPromptPayload(question) {
|
|
487
|
+
const build = ROUTING_PAYLOADS[question?.kind];
|
|
488
|
+
if (!build) throw new Error(`unknown routing profile question: ${question?.kind}`);
|
|
489
|
+
return build(question);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/** An option the Kit cannot explain is a bug, not a silently bare label. */
|
|
493
|
+
function hintedOptions(kind, options) {
|
|
494
|
+
return options.map(({ value, label }) => {
|
|
495
|
+
const hint = ROUTING_HINTS[kind]?.[value];
|
|
496
|
+
if (!hint) throw new Error(`missing routing prompt hint: ${kind}.${value}`);
|
|
497
|
+
return { value, label: label ?? value, hint };
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function labelledOptions(kind, values) {
|
|
502
|
+
return values.map((value) => ({ value, label: ROUTING_LABELS[kind]?.[value] ?? value }));
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function surfacesPayload(question) {
|
|
506
|
+
const preselected = new Set(question.preselected ?? []);
|
|
507
|
+
return {
|
|
508
|
+
control: 'multiselect',
|
|
509
|
+
label: 'surface selection',
|
|
510
|
+
message: question.message,
|
|
511
|
+
options: question.options.map(({ id, label }) => ({
|
|
512
|
+
value: id,
|
|
513
|
+
label,
|
|
514
|
+
hint: preselected.has(id)
|
|
515
|
+
? `preselected — selecting it lets the Kit route work to ${label}`
|
|
516
|
+
: `not preselected — select it only if you actually use ${label}`,
|
|
517
|
+
})),
|
|
518
|
+
initialValues: question.preselected,
|
|
519
|
+
required: true,
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function autonomyPayload(question) {
|
|
524
|
+
return {
|
|
525
|
+
control: 'select',
|
|
526
|
+
label: 'switching choice',
|
|
527
|
+
message: question.message,
|
|
528
|
+
options: hintedOptions('autonomy', question.options ?? []),
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/** Prompt-side identity for a `(surface, transport)` authorization. */
|
|
533
|
+
const transportValue = ({ surface, transport }) => `${surface}/${transport}`;
|
|
534
|
+
/** Prompt-side identity for a model-and-effort pair; the label is the readable half. */
|
|
535
|
+
const pairValue = ({ model, effort }) => `${model}/${effort ?? ''}`;
|
|
536
|
+
const pairText = ({ model, effort }) => sanitizeReadinessText(
|
|
537
|
+
`${model} · ${effort ?? 'no effort axis'}`,
|
|
538
|
+
) ?? model;
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Authorizing an app is not authorizing it to drive another app's command line,
|
|
542
|
+
* so every transport says which of the two it is.
|
|
543
|
+
*/
|
|
544
|
+
function transportsPayload(question) {
|
|
545
|
+
const preselected = new Set((question.preselected ?? []).map(transportValue));
|
|
546
|
+
const options = (question.options ?? []).map((option) => {
|
|
547
|
+
const app = sanitizeReadinessText(option.surfaceLabel) ?? option.surface;
|
|
548
|
+
return {
|
|
549
|
+
value: transportValue(option),
|
|
550
|
+
label: `${app} · ${option.transport}`,
|
|
551
|
+
hint: option.native
|
|
552
|
+
? `${app} may drive its own runtime`
|
|
553
|
+
: `lets ${app} drive the ${option.transport} command line — selecting the app does not`,
|
|
554
|
+
};
|
|
555
|
+
});
|
|
556
|
+
return {
|
|
557
|
+
control: 'multiselect',
|
|
558
|
+
label: 'transport authorization',
|
|
559
|
+
message: question.message,
|
|
560
|
+
options,
|
|
561
|
+
initialValues: options.filter(({ value }) => preselected.has(value)).map(({ value }) => value),
|
|
562
|
+
required: false,
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* The inventory can list dozens of pairs, so the roster question is grouped per
|
|
568
|
+
* agent app — detected apps first — and says how much it is showing. It also
|
|
569
|
+
* says what leaving a pair out means, because that answer is durable.
|
|
570
|
+
*/
|
|
571
|
+
function rosterPayload(question) {
|
|
572
|
+
const groups = question.groups ?? [];
|
|
573
|
+
const options = {};
|
|
574
|
+
for (const group of groups) {
|
|
575
|
+
const app = sanitizeReadinessText(group.label) ?? group.surface;
|
|
576
|
+
options[app] = group.pairs.map((pair) => ({
|
|
577
|
+
value: pairValue(pair),
|
|
578
|
+
label: pairText(pair),
|
|
579
|
+
hint: group.detected
|
|
580
|
+
? `${app} is installed here — selecting the pair authorizes it`
|
|
581
|
+
: `${app} is not installed here — authorizing it is still your choice`,
|
|
582
|
+
}));
|
|
320
583
|
}
|
|
321
|
-
|
|
322
|
-
|
|
584
|
+
return {
|
|
585
|
+
control: 'groupmultiselect',
|
|
586
|
+
label: 'roster choice',
|
|
587
|
+
message: `${question.message} (${question.total} pairs across ${groups.length} agent apps; `
|
|
588
|
+
+ 'a pair you leave out is recorded as declined and is not asked again)',
|
|
589
|
+
options,
|
|
590
|
+
initialValues: (question.preselected ?? []).map(pairValue),
|
|
591
|
+
required: false,
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function standardRoutePayload(question) {
|
|
596
|
+
const options = (question.options ?? []).map((pair) => ({
|
|
597
|
+
value: pairValue(pair),
|
|
598
|
+
label: pairText(pair),
|
|
599
|
+
hint: `${question.workload} work uses this pair when no evidence covers the intent`,
|
|
600
|
+
}));
|
|
601
|
+
options.push({
|
|
602
|
+
value: NO_STANDARD_ROUTE,
|
|
603
|
+
label: 'Leave unset',
|
|
604
|
+
hint: `no Standard route for ${question.workload} work — the class blocks instead of guessing`,
|
|
605
|
+
});
|
|
606
|
+
return {
|
|
607
|
+
control: 'select',
|
|
608
|
+
label: 'standard route choice',
|
|
609
|
+
message: question.message,
|
|
610
|
+
options,
|
|
611
|
+
initialValue: question.current ? pairValue(question.current) : NO_STANDARD_ROUTE,
|
|
612
|
+
maxItems: question.pageSize,
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function activationPayload(question) {
|
|
617
|
+
return {
|
|
618
|
+
control: 'select',
|
|
619
|
+
label: 'activation choice',
|
|
620
|
+
message: `${question.message}\n${activationSummary(question)}`,
|
|
621
|
+
options: hintedOptions('activation', labelledOptions('activation', question.actions ?? [])),
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/** What the activation review is actually reviewing. */
|
|
626
|
+
function activationSummary(question) {
|
|
627
|
+
return [
|
|
628
|
+
`agent apps: ${surfaceLabels(question.selectedSurfaces)}`,
|
|
629
|
+
`transports: ${describeTransports(question.authorizedTransports)}`,
|
|
630
|
+
`switching: ${describeSwitching(question.switching)}`,
|
|
631
|
+
`model roster: ${describeRoster(question.roster)}`,
|
|
632
|
+
`standard routes: ${describeStandardRoutes(question.standardRoutes)}`,
|
|
633
|
+
`advanced draft: ${describeDraft(question.advancedDraft)}`,
|
|
634
|
+
].join('\n');
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function describeTransports(transports) {
|
|
638
|
+
const rendered = (transports ?? []).map(({ surface, transport }) =>
|
|
639
|
+
`${sanitizeReadinessText(surfaceById(surface)?.label ?? surface) ?? surface} · ${transport}`);
|
|
640
|
+
return sanitizeReadinessText(rendered.join(', ')) || 'none';
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function describeRoster(roster) {
|
|
644
|
+
if (!Array.isArray(roster) || !roster.length) return 'none';
|
|
645
|
+
const counted = ['admitted', 'declined', 'withdrawn']
|
|
646
|
+
.map((state) => [state, roster.filter((entry) => entry.state === state).length])
|
|
647
|
+
.filter(([, count]) => count > 0)
|
|
648
|
+
.map(([state, count]) => `${count} ${state}`);
|
|
649
|
+
return counted.join(' · ') || 'none';
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function describeStandardRoutes(routes) {
|
|
653
|
+
if (!routes || typeof routes !== 'object') return 'none';
|
|
654
|
+
const rendered = Object.entries(routes).map(([workload, entry]) => {
|
|
655
|
+
if (!entry) return `${workload}: unset`;
|
|
656
|
+
return `${workload}: ${pairText(entry)}${entry.state === 'unresolved' ? ' (unresolved)' : ''}`;
|
|
657
|
+
});
|
|
658
|
+
return sanitizeReadinessText(rendered.join(' · ')) || 'none';
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function surfaceLabels(ids) {
|
|
662
|
+
const labels = (ids ?? [])
|
|
663
|
+
.map((id) => sanitizeReadinessText(surfaceById(id)?.label ?? id))
|
|
664
|
+
.filter(Boolean);
|
|
665
|
+
return labels.join(', ') || 'none';
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function describeSwitching(value) {
|
|
669
|
+
const name = sanitizeReadinessText(value) ?? 'unknown';
|
|
670
|
+
const hint = ROUTING_HINTS.autonomy[value];
|
|
671
|
+
return hint ? `${name} — ${hint}` : name;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function describeDraft(draft) {
|
|
675
|
+
if (!draft || typeof draft !== 'object') return 'none';
|
|
676
|
+
const rendered = Object.entries(draft)
|
|
677
|
+
.map(([key, value]) => `${key}=${typeof value === 'string' ? value : JSON.stringify(value)}`)
|
|
678
|
+
.join(' · ');
|
|
679
|
+
return sanitizeReadinessText(rendered) ?? 'none';
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function advancedPayload(question) {
|
|
683
|
+
return {
|
|
684
|
+
control: 'select',
|
|
685
|
+
label: 'advanced choice',
|
|
686
|
+
message: `${question.message} — kept as an optional note`,
|
|
687
|
+
options: hintedOptions('advanced', labelledOptions('advanced', ADVANCED_OPTIMIZATIONS)),
|
|
688
|
+
initialValue: question.draft?.optimization ?? 'balanced',
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/** What the roster and the Standard routes still owe an answer on, if anything. */
|
|
693
|
+
function rosterWork(delta) {
|
|
694
|
+
const roster = delta.roster ?? {};
|
|
695
|
+
return ['pending', 'withdrawn', 'reopenable', 'unresolvedRoutes']
|
|
696
|
+
.some((key) => (roster[key] ?? []).length > 0);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function reconcilePayload(question) {
|
|
700
|
+
const delta = question.delta;
|
|
701
|
+
if (delta.type === 'missing-profile' || delta.type === 'invalid-profile') {
|
|
702
|
+
return {
|
|
703
|
+
control: 'select',
|
|
704
|
+
label: 'routing migration choice',
|
|
323
705
|
message: question.message,
|
|
324
|
-
options:
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
],
|
|
329
|
-
initialValue: question.draft?.optimization ?? 'balanced',
|
|
330
|
-
}), 'advanced choice');
|
|
331
|
-
return { ...question.draft, optimization };
|
|
706
|
+
options: hintedOptions(
|
|
707
|
+
'reconcile', labelledOptions('reconcile', RECONCILE_MIGRATION_ACTIONS),
|
|
708
|
+
),
|
|
709
|
+
};
|
|
332
710
|
}
|
|
333
|
-
if (
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
],
|
|
341
|
-
}), 'routing migration choice');
|
|
342
|
-
}
|
|
343
|
-
const additions = question.delta.newSurfaces;
|
|
344
|
-
if (additions.length) {
|
|
345
|
-
const removed = question.delta.removedSurfaces.map(({ label }) => label);
|
|
346
|
-
const change = [
|
|
347
|
-
`new: ${additions.map(({ label }) => label).join(', ')}`,
|
|
348
|
-
...(removed.length ? [`unavailable: ${removed.join(', ')}`] : []),
|
|
349
|
-
].join(' · ');
|
|
350
|
-
const addSurfaceIds = ensurePrompt(await p.multiselect({
|
|
351
|
-
message: `Routing choices changed — ${change}`,
|
|
352
|
-
options: additions.map(({ id, label }) => ({ value: id, label })),
|
|
353
|
-
initialValues: [],
|
|
354
|
-
required: false,
|
|
355
|
-
}), 'routing reconcile choice');
|
|
356
|
-
return { action: 'apply', addSurfaceIds };
|
|
357
|
-
}
|
|
358
|
-
const removed = question.delta.removedSurfaces.map(({ label }) => label).join(', ');
|
|
359
|
-
const message = removed
|
|
711
|
+
if (delta.newSurfaces.length) return reconcileAdditionsPayload(delta);
|
|
712
|
+
const removed = delta.removedSurfaces.map(({ label }) => label).join(', ');
|
|
713
|
+
if (!removed && rosterWork(delta)) return reconcileRosterPayload(question, delta);
|
|
714
|
+
return {
|
|
715
|
+
control: 'confirm',
|
|
716
|
+
label: 'routing reconcile choice',
|
|
717
|
+
message: removed
|
|
360
718
|
? `Remove unavailable agent app from routing: ${removed}?`
|
|
361
|
-
: 'Refresh the routing profile registry revision?'
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
719
|
+
: 'Refresh the routing profile registry revision?',
|
|
720
|
+
active: 'Apply the change to the stored routing profile',
|
|
721
|
+
inactive: 'Leave the stored routing profile as it is',
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/** A roster-only change names what moved before it asks whether to review it. */
|
|
726
|
+
function reconcileRosterPayload(question, delta) {
|
|
727
|
+
const roster = delta.roster ?? {};
|
|
728
|
+
const change = [
|
|
729
|
+
['new pairs', roster.pending], ['pairs no longer offered', roster.withdrawn],
|
|
730
|
+
['pairs available again', roster.reopenable],
|
|
731
|
+
].filter(([, pairs]) => (pairs ?? []).length)
|
|
732
|
+
.map(([label, pairs]) => `${label}: ${pairs.map(pairText).join(', ')}`);
|
|
733
|
+
for (const { workload } of roster.unresolvedRoutes ?? []) {
|
|
734
|
+
change.push(`${workload} has no authorized Standard route any more`);
|
|
735
|
+
}
|
|
736
|
+
return {
|
|
737
|
+
control: 'select',
|
|
738
|
+
label: 'routing roster choice',
|
|
739
|
+
// Sanitized per line: collapsing the whole block would flatten the list.
|
|
740
|
+
message: [question.message, ...change.map(sanitizeReadinessText).filter(Boolean)].join('\n'),
|
|
741
|
+
options: hintedOptions('reconcile', labelledOptions('reconcile', RECONCILE_MIGRATION_ACTIONS)),
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function reconcileAdditionsPayload(delta) {
|
|
746
|
+
const removed = delta.removedSurfaces.map(({ label }) => label);
|
|
747
|
+
const change = [
|
|
748
|
+
`new: ${delta.newSurfaces.map(({ label }) => label).join(', ')}`,
|
|
749
|
+
...(removed.length ? [`unavailable: ${removed.join(', ')}`] : []),
|
|
750
|
+
].join(' · ');
|
|
751
|
+
return {
|
|
752
|
+
control: 'multiselect',
|
|
753
|
+
label: 'routing reconcile choice',
|
|
754
|
+
message: `Routing choices changed — ${change}`,
|
|
755
|
+
options: delta.newSurfaces.map(({ id, label }) => ({
|
|
756
|
+
value: id,
|
|
757
|
+
label,
|
|
758
|
+
hint: `newly available — select it to let the Kit route work to ${label}`,
|
|
759
|
+
})),
|
|
760
|
+
initialValues: [],
|
|
761
|
+
required: false,
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
async function promptRoutingProfile(question) {
|
|
766
|
+
const { control, label, ...payload } = routingPromptPayload(question);
|
|
767
|
+
const answer = ensurePrompt(await askRouting(control, payload), label);
|
|
768
|
+
return decodeRoutingAnswer(question, answer);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function askRouting(control, payload) {
|
|
772
|
+
if (control === 'multiselect') return p.multiselect(payload);
|
|
773
|
+
if (control === 'groupmultiselect') return p.groupMultiselect(payload);
|
|
774
|
+
if (control === 'confirm') return p.confirm(payload);
|
|
775
|
+
return p.select(payload);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/** Map prompt values back onto the very objects the question offered. */
|
|
779
|
+
function decodeSelected(offered, identity, answer) {
|
|
780
|
+
const known = new Map(offered.map((option) => [identity(option), option]));
|
|
781
|
+
return (Array.isArray(answer) ? answer : [answer])
|
|
782
|
+
.map((value) => known.get(value))
|
|
783
|
+
.filter(Boolean);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/** The answer shape `routingProfile` expects back, per question. */
|
|
787
|
+
function decodeRoutingAnswer(question, answer) {
|
|
788
|
+
if (question.kind === 'advanced') return { ...question.draft, optimization: answer };
|
|
789
|
+
if (question.kind === 'transports') {
|
|
790
|
+
return decodeSelected(question.options ?? [], transportValue, answer)
|
|
791
|
+
.map(({ surface, transport }) => ({ surface, transport }));
|
|
792
|
+
}
|
|
793
|
+
if (question.kind === 'roster') {
|
|
794
|
+
const pairs = (question.groups ?? []).flatMap(({ pairs: group }) => group);
|
|
795
|
+
return decodeSelected(pairs, pairValue, answer);
|
|
796
|
+
}
|
|
797
|
+
if (question.kind === 'standard-route') {
|
|
798
|
+
return decodeSelected(question.options ?? [], pairValue, answer)[0] ?? null;
|
|
799
|
+
}
|
|
800
|
+
if (question.kind !== 'reconcile') return answer;
|
|
801
|
+
const delta = question.delta;
|
|
802
|
+
if (delta.type === 'missing-profile' || delta.type === 'invalid-profile') return answer;
|
|
803
|
+
if (delta.newSurfaces.length) return { action: 'apply', addSurfaceIds: answer };
|
|
804
|
+
if (!delta.removedSurfaces.length && rosterWork(delta)) {
|
|
805
|
+
return answer === 'review' ? { action: 'apply', addSurfaceIds: [] } : { action: 'decline' };
|
|
365
806
|
}
|
|
366
|
-
|
|
807
|
+
return answer === true ? { action: 'apply', addSurfaceIds: [] } : { action: 'decline' };
|
|
367
808
|
}
|
|
368
809
|
|
|
369
810
|
function ensurePrompt(value, label) {
|