@akagilnc/pi-workflow-roles 0.1.2043 → 0.1.2052
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/README.md +1 -0
- package/README.zh-CN.md +1 -0
- package/dist/public-cli/main.js +489 -183
- package/extensions/role-runtime.ts +5 -0
- package/package.json +1 -1
- package/resources/engines/codex.md +32 -0
- package/resources/engines/cursor.md +32 -0
- package/resources/engines/kimi.md +31 -0
- package/src/oauth-keepalive.ts +216 -0
- package/src/package-resources/engine-material.ts +111 -0
- package/src/public-cli/cli.ts +168 -22
- package/src/public-cli/config.ts +167 -22
- package/src/public-cli/invocation.ts +7 -1
- package/src/public-cli/judge-run.ts +11 -1
- package/src/public-cli/option-definitions.ts +14 -0
- package/src/role-runtime.ts +14 -0
package/src/public-cli/cli.ts
CHANGED
|
@@ -5,6 +5,9 @@ import { realpath } from "node:fs/promises";
|
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
|
|
8
|
+
import {
|
|
9
|
+
assertLegalEngineName,
|
|
10
|
+
} from "../package-resources/engine-material.ts";
|
|
8
11
|
import {
|
|
9
12
|
effectiveSeatConfigurations,
|
|
10
13
|
formatModelSpec,
|
|
@@ -14,6 +17,8 @@ import {
|
|
|
14
17
|
resolveEffectiveSeat,
|
|
15
18
|
savePublicCliConfig,
|
|
16
19
|
setPersistentSeatConfig,
|
|
20
|
+
setPersistentSeatEngine,
|
|
21
|
+
validatePublicCliConfigEngines,
|
|
17
22
|
type CredentialProviders,
|
|
18
23
|
type EffectiveSeat,
|
|
19
24
|
type InvocationModelOverride,
|
|
@@ -94,7 +99,8 @@ export const PUBLIC_GLOBAL_OPTIONS: readonly PublicOptionDefinition[] =
|
|
|
94
99
|
type TakenPublicGlobalFlag =
|
|
95
100
|
| { flag: "help"; consume: 1 }
|
|
96
101
|
| { flag: "model"; consume: 1 | 2; value: string | undefined }
|
|
97
|
-
| { flag: "thinking"; consume: 1 | 2; raw: string | undefined }
|
|
102
|
+
| { flag: "thinking"; consume: 1 | 2; raw: string | undefined }
|
|
103
|
+
| { flag: "engine"; consume: 1 | 2; value: string | undefined };
|
|
98
104
|
|
|
99
105
|
/**
|
|
100
106
|
* If `argv[index]` is a public global flag, describe its span and payload.
|
|
@@ -127,6 +133,13 @@ function takePublicGlobalFlag(
|
|
|
127
133
|
raw: taken.value,
|
|
128
134
|
};
|
|
129
135
|
}
|
|
136
|
+
if (taken.def.id === "engine") {
|
|
137
|
+
return {
|
|
138
|
+
flag: "engine",
|
|
139
|
+
consume: consumed as 1 | 2,
|
|
140
|
+
value: taken.value,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
130
143
|
return undefined;
|
|
131
144
|
}
|
|
132
145
|
|
|
@@ -217,6 +230,7 @@ type ParsedGlobal = {
|
|
|
217
230
|
args: string[];
|
|
218
231
|
model?: string;
|
|
219
232
|
thinking?: PublicThinkingLevel;
|
|
233
|
+
engine?: string;
|
|
220
234
|
help: boolean;
|
|
221
235
|
};
|
|
222
236
|
|
|
@@ -231,6 +245,7 @@ function parseArgv(argv: readonly string[]): ParsedGlobal {
|
|
|
231
245
|
const args = [...argv];
|
|
232
246
|
let model: string | undefined;
|
|
233
247
|
let thinking: PublicThinkingLevel | undefined;
|
|
248
|
+
let engine: string | undefined;
|
|
234
249
|
let help = false;
|
|
235
250
|
const positional: string[] = [];
|
|
236
251
|
const globalOptions = createTypedOptionConsumer(PUBLIC_GLOBAL_OPTIONS);
|
|
@@ -259,12 +274,24 @@ function parseArgv(argv: readonly string[]): ParsedGlobal {
|
|
|
259
274
|
args.splice(0, taken.consume);
|
|
260
275
|
continue;
|
|
261
276
|
}
|
|
262
|
-
if (taken.
|
|
263
|
-
|
|
277
|
+
if (taken.flag === "thinking") {
|
|
278
|
+
if (taken.raw === undefined) {
|
|
279
|
+
throw new CliUsageError("--thinking requires a value");
|
|
280
|
+
}
|
|
281
|
+
thinking = parseThinking(taken.raw);
|
|
282
|
+
args.splice(0, taken.consume);
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (taken.flag === "engine") {
|
|
286
|
+
if (taken.value === undefined) {
|
|
287
|
+
throw new CliUsageError("--engine requires a value");
|
|
288
|
+
}
|
|
289
|
+
engine = taken.value;
|
|
290
|
+
args.splice(0, taken.consume);
|
|
291
|
+
continue;
|
|
264
292
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
continue;
|
|
293
|
+
// Exhaustive for known global flags; unknown id is a table bug.
|
|
294
|
+
throw new CliUsageError(`unhandled global option: ${String((taken as { flag: string }).flag)}`);
|
|
268
295
|
}
|
|
269
296
|
// Subcommands may own additional flags later; unknown dashed tokens stay
|
|
270
297
|
// positional here (same as pre-unification parseArgv).
|
|
@@ -277,18 +304,54 @@ function parseArgv(argv: readonly string[]): ParsedGlobal {
|
|
|
277
304
|
args: rest,
|
|
278
305
|
...(model === undefined ? {} : { model }),
|
|
279
306
|
...(thinking === undefined ? {} : { thinking }),
|
|
307
|
+
...(engine === undefined ? {} : { engine }),
|
|
280
308
|
help,
|
|
281
309
|
};
|
|
282
310
|
}
|
|
283
311
|
|
|
284
312
|
function invocationFromParsed(parsed: ParsedGlobal): InvocationModelOverride | undefined {
|
|
285
|
-
if (
|
|
313
|
+
if (
|
|
314
|
+
parsed.model === undefined &&
|
|
315
|
+
parsed.thinking === undefined &&
|
|
316
|
+
parsed.engine === undefined
|
|
317
|
+
) {
|
|
318
|
+
return undefined;
|
|
319
|
+
}
|
|
286
320
|
return {
|
|
287
321
|
...(parsed.model === undefined ? {} : { model: parsed.model }),
|
|
288
322
|
...(parsed.thinking === undefined ? {} : { thinking: parsed.thinking }),
|
|
323
|
+
...(parsed.engine === undefined ? {} : { engine: parsed.engine }),
|
|
289
324
|
};
|
|
290
325
|
}
|
|
291
326
|
|
|
327
|
+
function requireLegalEngineName(packageRoot: string, name: string): string {
|
|
328
|
+
try {
|
|
329
|
+
return assertLegalEngineName(packageRoot, name);
|
|
330
|
+
} catch (error) {
|
|
331
|
+
throw new CliUsageError(
|
|
332
|
+
error instanceof Error ? error.message : String(error),
|
|
333
|
+
{ cause: error },
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function loadAndValidateConfig(
|
|
339
|
+
home: string,
|
|
340
|
+
packageRoot: string,
|
|
341
|
+
): Promise<PublicCliConfig> {
|
|
342
|
+
return loadPublicCliConfig(home).then((config) => {
|
|
343
|
+
try {
|
|
344
|
+
validatePublicCliConfigEngines(config, packageRoot);
|
|
345
|
+
} catch (error) {
|
|
346
|
+
throw new CliUsageError(
|
|
347
|
+
error instanceof Error ? error.message : String(error),
|
|
348
|
+
{ cause: error },
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
return config;
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
292
355
|
/** Typed facts used by help presentation — not layout. */
|
|
293
356
|
export function helpDocument() {
|
|
294
357
|
return {
|
|
@@ -355,6 +418,7 @@ function renderHelp(): string {
|
|
|
355
418
|
"",
|
|
356
419
|
"Role options: ak-role help <command>",
|
|
357
420
|
"Persistent config: ak-role config set <seat> <provider/model:thinking>",
|
|
421
|
+
"Persistent engine (judge only): ak-role config set-engine judge <name> | unset-engine judge",
|
|
358
422
|
"Effective seats: ak-role roles",
|
|
359
423
|
);
|
|
360
424
|
return `${lines.join("\n")}\n`;
|
|
@@ -398,7 +462,7 @@ function renderRoles(seats: readonly EffectiveSeat[]): string {
|
|
|
398
462
|
}
|
|
399
463
|
|
|
400
464
|
function renderConfig(config: PublicCliConfig): string {
|
|
401
|
-
const lines: string[] = ["seat\tmodel"];
|
|
465
|
+
const lines: string[] = ["seat\tmodel\tengine"];
|
|
402
466
|
const keys = Object.keys(config.seats) as (keyof typeof config.seats)[];
|
|
403
467
|
if (keys.length === 0) {
|
|
404
468
|
lines.push("(empty)");
|
|
@@ -406,7 +470,8 @@ function renderConfig(config: PublicCliConfig): string {
|
|
|
406
470
|
for (const seat of keys.sort()) {
|
|
407
471
|
const selection = config.seats[seat];
|
|
408
472
|
if (selection === undefined) continue;
|
|
409
|
-
|
|
473
|
+
const engine = selection.engine === undefined ? "-" : selection.engine;
|
|
474
|
+
lines.push(`${seat}\t${formatModelSpec(selection)}\t${engine}`);
|
|
410
475
|
}
|
|
411
476
|
}
|
|
412
477
|
return `${lines.join("\n")}\n`;
|
|
@@ -415,10 +480,11 @@ function renderConfig(config: PublicCliConfig): string {
|
|
|
415
480
|
async function runConfigCommand(
|
|
416
481
|
args: readonly string[],
|
|
417
482
|
home: string,
|
|
483
|
+
packageRoot: string,
|
|
418
484
|
io: CliIo,
|
|
419
485
|
): Promise<number> {
|
|
420
486
|
if (args.length === 0 || args[0] === "get" || args[0] === "list" || args[0] === "show") {
|
|
421
|
-
const config = await
|
|
487
|
+
const config = await loadAndValidateConfig(home, packageRoot);
|
|
422
488
|
if (args[0] === "get" && args[1] !== undefined) {
|
|
423
489
|
if (!isPublicConfigurableSeat(args[1])) {
|
|
424
490
|
throw new CliUsageError(`unknown configurable seat: ${args[1]}`);
|
|
@@ -427,7 +493,9 @@ async function runConfigCommand(
|
|
|
427
493
|
if (selection === undefined) {
|
|
428
494
|
io.stdout(`${args[1]}\t(unconfigured)\n`);
|
|
429
495
|
} else {
|
|
430
|
-
|
|
496
|
+
const engine =
|
|
497
|
+
selection.engine === undefined ? "-" : selection.engine;
|
|
498
|
+
io.stdout(`${args[1]}\t${formatModelSpec(selection)}\t${engine}\n`);
|
|
431
499
|
}
|
|
432
500
|
return 0;
|
|
433
501
|
}
|
|
@@ -448,7 +516,7 @@ async function runConfigCommand(
|
|
|
448
516
|
"config set requires seat/spec pairs: ak-role config set <seat> <spec> [<seat> <spec> ...]",
|
|
449
517
|
);
|
|
450
518
|
}
|
|
451
|
-
let config = await
|
|
519
|
+
let config = await loadAndValidateConfig(home, packageRoot);
|
|
452
520
|
for (let i = 0; i < pairs.length; i += 2) {
|
|
453
521
|
const seat = pairs[i]!;
|
|
454
522
|
const spec = pairs[i + 1]!;
|
|
@@ -462,6 +530,66 @@ async function runConfigCommand(
|
|
|
462
530
|
return 0;
|
|
463
531
|
}
|
|
464
532
|
|
|
533
|
+
if (args[0] === "set-engine") {
|
|
534
|
+
if (args.length !== 3) {
|
|
535
|
+
throw new CliUsageError(
|
|
536
|
+
"usage: ak-role config set-engine judge <name>",
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
const seat = args[1]!;
|
|
540
|
+
const name = args[2]!;
|
|
541
|
+
if (!isPublicConfigurableSeat(seat)) {
|
|
542
|
+
throw new CliUsageError(`unknown configurable seat: ${seat}`);
|
|
543
|
+
}
|
|
544
|
+
if (seat !== "judge") {
|
|
545
|
+
throw new CliUsageError(
|
|
546
|
+
`engine axis is judge-only; refused seat ${seat}`,
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
requireLegalEngineName(packageRoot, name);
|
|
550
|
+
let config = await loadAndValidateConfig(home, packageRoot);
|
|
551
|
+
try {
|
|
552
|
+
config = setPersistentSeatEngine(config, seat, name);
|
|
553
|
+
} catch (error) {
|
|
554
|
+
throw new CliUsageError(
|
|
555
|
+
error instanceof Error ? error.message : String(error),
|
|
556
|
+
{ cause: error },
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
await savePublicCliConfig(config, home);
|
|
560
|
+
io.stdout(renderConfig(config));
|
|
561
|
+
return 0;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (args[0] === "unset-engine") {
|
|
565
|
+
if (args.length !== 2) {
|
|
566
|
+
throw new CliUsageError(
|
|
567
|
+
"usage: ak-role config unset-engine judge",
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
const seat = args[1]!;
|
|
571
|
+
if (!isPublicConfigurableSeat(seat)) {
|
|
572
|
+
throw new CliUsageError(`unknown configurable seat: ${seat}`);
|
|
573
|
+
}
|
|
574
|
+
if (seat !== "judge") {
|
|
575
|
+
throw new CliUsageError(
|
|
576
|
+
`engine axis is judge-only; refused seat ${seat}`,
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
let config = await loadAndValidateConfig(home, packageRoot);
|
|
580
|
+
try {
|
|
581
|
+
config = setPersistentSeatEngine(config, seat, undefined);
|
|
582
|
+
} catch (error) {
|
|
583
|
+
throw new CliUsageError(
|
|
584
|
+
error instanceof Error ? error.message : String(error),
|
|
585
|
+
{ cause: error },
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
await savePublicCliConfig(config, home);
|
|
589
|
+
io.stdout(renderConfig(config));
|
|
590
|
+
return 0;
|
|
591
|
+
}
|
|
592
|
+
|
|
465
593
|
throw new CliUsageError(`unknown config subcommand: ${args[0]}`);
|
|
466
594
|
}
|
|
467
595
|
|
|
@@ -477,6 +605,21 @@ export async function runAkRole(
|
|
|
477
605
|
// runtime entry, activation argv, or invocation provenance is derived.
|
|
478
606
|
env = { ...env, packageRoot: await realpath(env.packageRoot) };
|
|
479
607
|
const parsed = parseArgv(argv);
|
|
608
|
+
// Invocation --engine rejects at the call-request seam (not role submission).
|
|
609
|
+
// #356 MVP: engine axis is Judge-only (not resume / non-Judge seats).
|
|
610
|
+
if (parsed.engine !== undefined) {
|
|
611
|
+
requireLegalEngineName(env.packageRoot, parsed.engine);
|
|
612
|
+
if (
|
|
613
|
+
!parsed.help &&
|
|
614
|
+
parsed.command !== undefined &&
|
|
615
|
+
parsed.command !== "help" &&
|
|
616
|
+
parsed.command !== "judge"
|
|
617
|
+
) {
|
|
618
|
+
throw new CliUsageError(
|
|
619
|
+
`engine axis is judge-only; refused command ${parsed.command}`,
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
480
623
|
|
|
481
624
|
if (
|
|
482
625
|
parsed.help ||
|
|
@@ -501,7 +644,7 @@ export async function runAkRole(
|
|
|
501
644
|
if (parsed.args.length > 0) {
|
|
502
645
|
throw new CliUsageError("roles takes no arguments");
|
|
503
646
|
}
|
|
504
|
-
const config = await
|
|
647
|
+
const config = await loadAndValidateConfig(home, env.packageRoot);
|
|
505
648
|
const credentials =
|
|
506
649
|
env.credentials ??
|
|
507
650
|
(await loadCredentialProviders(resolveAgentDir(env, home)));
|
|
@@ -515,7 +658,9 @@ export async function runAkRole(
|
|
|
515
658
|
}
|
|
516
659
|
|
|
517
660
|
if (parsed.command === "config") {
|
|
518
|
-
return {
|
|
661
|
+
return {
|
|
662
|
+
exitCode: await runConfigCommand(parsed.args, home, env.packageRoot, io),
|
|
663
|
+
};
|
|
519
664
|
}
|
|
520
665
|
|
|
521
666
|
// Resume reopens an exact Role run after a typed HTTP 429 (#108/#109).
|
|
@@ -523,7 +668,7 @@ export async function runAkRole(
|
|
|
523
668
|
if (parsed.command === "resume") {
|
|
524
669
|
const agentDir = resolveAgentDir(env, home);
|
|
525
670
|
const cwd = env.cwd ?? process.cwd();
|
|
526
|
-
const config = await
|
|
671
|
+
const config = await loadAndValidateConfig(home, env.packageRoot);
|
|
527
672
|
const credentials =
|
|
528
673
|
env.credentials ?? (await loadCredentialProviders(agentDir));
|
|
529
674
|
const resumeRunId = parsed.args[0];
|
|
@@ -706,7 +851,7 @@ export async function runAkRole(
|
|
|
706
851
|
if (parsed.command === "judge") {
|
|
707
852
|
const agentDir = resolveAgentDir(env, home);
|
|
708
853
|
const cwd = env.cwd ?? process.cwd();
|
|
709
|
-
const config = await
|
|
854
|
+
const config = await loadAndValidateConfig(home, env.packageRoot);
|
|
710
855
|
const credentials =
|
|
711
856
|
env.credentials ?? (await loadCredentialProviders(agentDir));
|
|
712
857
|
const seat = resolveEffectiveSeat(
|
|
@@ -728,6 +873,7 @@ export async function runAkRole(
|
|
|
728
873
|
: { correlationId: env.correlationId }),
|
|
729
874
|
...(env.piRunner === undefined ? {} : { piRunner: env.piRunner }),
|
|
730
875
|
...(seat.selection === undefined ? {} : { model: seat.selection }),
|
|
876
|
+
...(seat.engine === undefined ? {} : { engine: seat.engine }),
|
|
731
877
|
...(env.judgeExtraPiArgs === undefined
|
|
732
878
|
? {}
|
|
733
879
|
: { extraPiArgs: env.judgeExtraPiArgs }),
|
|
@@ -749,7 +895,7 @@ export async function runAkRole(
|
|
|
749
895
|
if (parsed.command === "coder") {
|
|
750
896
|
const agentDir = resolveAgentDir(env, home);
|
|
751
897
|
const cwd = env.cwd ?? process.cwd();
|
|
752
|
-
const config = await
|
|
898
|
+
const config = await loadAndValidateConfig(home, env.packageRoot);
|
|
753
899
|
const credentials =
|
|
754
900
|
env.credentials ?? (await loadCredentialProviders(agentDir));
|
|
755
901
|
const seat = resolveEffectiveSeat(
|
|
@@ -792,7 +938,7 @@ export async function runAkRole(
|
|
|
792
938
|
if (parsed.command === "fixer") {
|
|
793
939
|
const agentDir = resolveAgentDir(env, home);
|
|
794
940
|
const cwd = env.cwd ?? process.cwd();
|
|
795
|
-
const config = await
|
|
941
|
+
const config = await loadAndValidateConfig(home, env.packageRoot);
|
|
796
942
|
const credentials =
|
|
797
943
|
env.credentials ?? (await loadCredentialProviders(agentDir));
|
|
798
944
|
const seat = resolveEffectiveSeat(
|
|
@@ -835,7 +981,7 @@ export async function runAkRole(
|
|
|
835
981
|
if (parsed.command === "collector") {
|
|
836
982
|
const agentDir = resolveAgentDir(env, home);
|
|
837
983
|
const cwd = env.cwd ?? process.cwd();
|
|
838
|
-
const config = await
|
|
984
|
+
const config = await loadAndValidateConfig(home, env.packageRoot);
|
|
839
985
|
const credentials =
|
|
840
986
|
env.credentials ?? (await loadCredentialProviders(agentDir));
|
|
841
987
|
const seat = resolveEffectiveSeat(
|
|
@@ -878,7 +1024,7 @@ export async function runAkRole(
|
|
|
878
1024
|
if (parsed.command === "reviewer") {
|
|
879
1025
|
const agentDir = resolveAgentDir(env, home);
|
|
880
1026
|
const cwd = env.cwd ?? process.cwd();
|
|
881
|
-
const config = await
|
|
1027
|
+
const config = await loadAndValidateConfig(home, env.packageRoot);
|
|
882
1028
|
const credentials =
|
|
883
1029
|
env.credentials ?? (await loadCredentialProviders(agentDir));
|
|
884
1030
|
const seat = resolveEffectiveSeat(
|
|
@@ -921,7 +1067,7 @@ export async function runAkRole(
|
|
|
921
1067
|
if (parsed.command === "doctor") {
|
|
922
1068
|
const agentDir = resolveAgentDir(env, home);
|
|
923
1069
|
const cwd = env.cwd ?? process.cwd();
|
|
924
|
-
const config = await
|
|
1070
|
+
const config = await loadAndValidateConfig(home, env.packageRoot);
|
|
925
1071
|
const credentials =
|
|
926
1072
|
env.credentials ?? (await loadCredentialProviders(agentDir));
|
|
927
1073
|
const seat = resolveEffectiveSeat(
|
|
@@ -964,7 +1110,7 @@ export async function runAkRole(
|
|
|
964
1110
|
if (parsed.command === "merger") {
|
|
965
1111
|
const agentDir = resolveAgentDir(env, home);
|
|
966
1112
|
const cwd = env.cwd ?? process.cwd();
|
|
967
|
-
const config = await
|
|
1113
|
+
const config = await loadAndValidateConfig(home, env.packageRoot);
|
|
968
1114
|
const credentials =
|
|
969
1115
|
env.credentials ?? (await loadCredentialProviders(agentDir));
|
|
970
1116
|
const seat = resolveEffectiveSeat(
|
package/src/public-cli/config.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
import { dirname, join } from "node:path";
|
|
7
7
|
|
|
8
|
+
import { assertLegalEngineName } from "../package-resources/engine-material.ts";
|
|
8
9
|
import {
|
|
9
10
|
AUTOMATIC_NAVIGATOR_SEAT,
|
|
10
11
|
PUBLIC_CALLABLE_ROLES,
|
|
@@ -22,23 +23,36 @@ export type CredentialProviders = {
|
|
|
22
23
|
|
|
23
24
|
export type SeatModelConfig = ModelRef;
|
|
24
25
|
|
|
26
|
+
/** Persistent seat row: required model triple + optional engine axis (#356). */
|
|
27
|
+
export type PersistentSeatConfig = SeatModelConfig & {
|
|
28
|
+
engine?: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
25
31
|
export type PublicCliConfig = {
|
|
26
|
-
seats: Partial<Record<PublicConfigurableSeat,
|
|
32
|
+
seats: Partial<Record<PublicConfigurableSeat, PersistentSeatConfig>>;
|
|
27
33
|
};
|
|
28
34
|
|
|
29
35
|
export type EffectiveSource = "persistent" | "startup" | "invocation" | "unconfigured";
|
|
30
36
|
|
|
37
|
+
/** Engine axis source is independent of model source (#356). */
|
|
38
|
+
export type EngineSource = "invocation" | "persistent" | "unconfigured";
|
|
39
|
+
|
|
31
40
|
export type EffectiveSeat = {
|
|
32
41
|
seat: PublicConfigurableSeat;
|
|
33
42
|
/** True when the seat is automatic Navigator attendance rather than caller-selected. */
|
|
34
43
|
automatic: boolean;
|
|
35
44
|
source: EffectiveSource;
|
|
36
45
|
selection?: SeatModelConfig;
|
|
46
|
+
/** Selected engine name when configured; undefined = no engine (default path). */
|
|
47
|
+
engine?: string;
|
|
48
|
+
engineSource: EngineSource;
|
|
37
49
|
};
|
|
38
50
|
|
|
39
51
|
export type InvocationModelOverride = {
|
|
40
52
|
model?: string;
|
|
41
53
|
thinking?: PublicThinkingLevel;
|
|
54
|
+
/** Optional engine override for this invocation only (#356). */
|
|
55
|
+
engine?: string;
|
|
42
56
|
};
|
|
43
57
|
|
|
44
58
|
const THINKING_LEVELS = new Set<PublicThinkingLevel>([
|
|
@@ -89,14 +103,91 @@ export function setPersistentSeatConfig(
|
|
|
89
103
|
seat: PublicConfigurableSeat,
|
|
90
104
|
selection: SeatModelConfig,
|
|
91
105
|
): PublicCliConfig {
|
|
106
|
+
const previous = config.seats[seat];
|
|
92
107
|
return {
|
|
93
108
|
seats: {
|
|
94
109
|
...config.seats,
|
|
95
|
-
[seat]: {
|
|
110
|
+
[seat]: {
|
|
111
|
+
...selection,
|
|
112
|
+
// Model rewrite preserves a previously configured engine axis.
|
|
113
|
+
...(previous?.engine === undefined ? {} : { engine: previous.engine }),
|
|
114
|
+
},
|
|
96
115
|
},
|
|
97
116
|
};
|
|
98
117
|
}
|
|
99
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Set or clear persistent engine on the Judge seat only (#356 MVP).
|
|
121
|
+
* Engine-only seats are rejected — model triple remains required.
|
|
122
|
+
*/
|
|
123
|
+
export function setPersistentSeatEngine(
|
|
124
|
+
config: PublicCliConfig,
|
|
125
|
+
seat: PublicConfigurableSeat,
|
|
126
|
+
engine: string | undefined,
|
|
127
|
+
): PublicCliConfig {
|
|
128
|
+
if (seat !== "judge") {
|
|
129
|
+
throw new Error(`engine axis is judge-only; refused seat ${seat}`);
|
|
130
|
+
}
|
|
131
|
+
const previous = config.seats[seat];
|
|
132
|
+
if (previous === undefined) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
`config seat ${seat} has no persistent model; set provider/model:thinking before engine`,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
if (engine === undefined) {
|
|
138
|
+
const { engine: _dropped, ...modelOnly } = previous;
|
|
139
|
+
return {
|
|
140
|
+
seats: {
|
|
141
|
+
...config.seats,
|
|
142
|
+
[seat]: modelOnly,
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
// Engine-name legality is owned solely by assertLegalEngineName
|
|
147
|
+
// (call-request + config-parse seams). Setter is pure seat mutation.
|
|
148
|
+
return {
|
|
149
|
+
seats: {
|
|
150
|
+
...config.seats,
|
|
151
|
+
[seat]: { ...previous, engine },
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Strip optional engine so activation model argv never sees the engine axis. */
|
|
157
|
+
export function seatModelOnly(seat: PersistentSeatConfig): SeatModelConfig {
|
|
158
|
+
return seat.thinking === undefined
|
|
159
|
+
? { provider: seat.provider, model: seat.model }
|
|
160
|
+
: { provider: seat.provider, model: seat.model, thinking: seat.thinking };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Config-parse seam: engine axis is Judge-only; Judge engine names must exist
|
|
165
|
+
* in package materials. Call with packageRoot after load / before dispatch (#356).
|
|
166
|
+
* Legality authority = assertLegalEngineName (no injected duplicate).
|
|
167
|
+
*/
|
|
168
|
+
export function validatePublicCliConfigEngines(
|
|
169
|
+
config: PublicCliConfig,
|
|
170
|
+
packageRoot: string,
|
|
171
|
+
): void {
|
|
172
|
+
for (const seat of Object.keys(config.seats) as PublicConfigurableSeat[]) {
|
|
173
|
+
const row = config.seats[seat];
|
|
174
|
+
if (row?.engine === undefined) continue;
|
|
175
|
+
if (seat !== "judge") {
|
|
176
|
+
throw new Error(
|
|
177
|
+
`config seat ${seat} engine is not allowed; engine axis is judge-only`,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
assertLegalEngineName(packageRoot, row.engine);
|
|
182
|
+
} catch (error) {
|
|
183
|
+
throw new Error(
|
|
184
|
+
`config seat ${seat} engine is unknown: ${row.engine}`,
|
|
185
|
+
{ cause: error },
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
100
191
|
export function parseModelSpec(
|
|
101
192
|
spec: string,
|
|
102
193
|
fallbackThinking?: PublicThinkingLevel,
|
|
@@ -200,7 +291,7 @@ function parsePublicCliConfig(value: unknown): PublicCliConfig {
|
|
|
200
291
|
return { seats };
|
|
201
292
|
}
|
|
202
293
|
|
|
203
|
-
function parseSeatModelConfig(value: unknown, seat: string):
|
|
294
|
+
function parseSeatModelConfig(value: unknown, seat: string): PersistentSeatConfig {
|
|
204
295
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
205
296
|
throw new Error(`config seat ${seat} must be an object`);
|
|
206
297
|
}
|
|
@@ -217,11 +308,20 @@ function parseSeatModelConfig(value: unknown, seat: string): SeatModelConfig {
|
|
|
217
308
|
) {
|
|
218
309
|
throw new Error(`config seat ${seat} requires a valid thinking level`);
|
|
219
310
|
}
|
|
220
|
-
|
|
311
|
+
const parsed: PersistentSeatConfig = {
|
|
221
312
|
provider: raw.provider,
|
|
222
313
|
model: raw.model,
|
|
223
314
|
thinking: raw.thinking as PublicThinkingLevel,
|
|
224
315
|
};
|
|
316
|
+
if (raw.engine !== undefined) {
|
|
317
|
+
// Shape only: engine must be a string field. Name legality is deferred to
|
|
318
|
+
// validatePublicCliConfigEngines → assertLegalEngineName (single authority).
|
|
319
|
+
if (typeof raw.engine !== "string") {
|
|
320
|
+
throw new Error(`config seat ${seat} engine must be a string`);
|
|
321
|
+
}
|
|
322
|
+
parsed.engine = raw.engine;
|
|
323
|
+
}
|
|
324
|
+
return parsed;
|
|
225
325
|
}
|
|
226
326
|
|
|
227
327
|
export function providerConfigured(
|
|
@@ -258,6 +358,39 @@ function pickStartupCandidate(
|
|
|
258
358
|
return undefined;
|
|
259
359
|
}
|
|
260
360
|
|
|
361
|
+
function attachEngineAxis(
|
|
362
|
+
seat: EffectiveSeat,
|
|
363
|
+
config: PublicCliConfig,
|
|
364
|
+
invocation?: InvocationModelOverride,
|
|
365
|
+
): EffectiveSeat {
|
|
366
|
+
// #356 MVP: engine axis is Judge-only. Other seats stay unconfigured.
|
|
367
|
+
if (seat.seat !== "judge") {
|
|
368
|
+
return {
|
|
369
|
+
...seat,
|
|
370
|
+
engineSource: "unconfigured",
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
const persistentEngine = config.seats.judge?.engine;
|
|
374
|
+
if (invocation?.engine !== undefined) {
|
|
375
|
+
return {
|
|
376
|
+
...seat,
|
|
377
|
+
engine: invocation.engine,
|
|
378
|
+
engineSource: "invocation",
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
if (persistentEngine !== undefined) {
|
|
382
|
+
return {
|
|
383
|
+
...seat,
|
|
384
|
+
engine: persistentEngine,
|
|
385
|
+
engineSource: "persistent",
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
return {
|
|
389
|
+
...seat,
|
|
390
|
+
engineSource: "unconfigured",
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
261
394
|
function resolveBaseSeat(
|
|
262
395
|
config: PublicCliConfig,
|
|
263
396
|
seat: PublicConfigurableSeat,
|
|
@@ -270,7 +403,8 @@ function resolveBaseSeat(
|
|
|
270
403
|
seat,
|
|
271
404
|
automatic,
|
|
272
405
|
source: "persistent",
|
|
273
|
-
selection:
|
|
406
|
+
selection: seatModelOnly(persistent),
|
|
407
|
+
engineSource: "unconfigured",
|
|
274
408
|
};
|
|
275
409
|
}
|
|
276
410
|
const startup = pickStartupCandidate(seat, credentials);
|
|
@@ -280,9 +414,10 @@ function resolveBaseSeat(
|
|
|
280
414
|
automatic,
|
|
281
415
|
source: "startup",
|
|
282
416
|
selection: startup,
|
|
417
|
+
engineSource: "unconfigured",
|
|
283
418
|
};
|
|
284
419
|
}
|
|
285
|
-
return { seat, automatic, source: "unconfigured" };
|
|
420
|
+
return { seat, automatic, source: "unconfigured", engineSource: "unconfigured" };
|
|
286
421
|
}
|
|
287
422
|
|
|
288
423
|
export function resolveEffectiveSeat(
|
|
@@ -292,36 +427,46 @@ export function resolveEffectiveSeat(
|
|
|
292
427
|
invocation?: InvocationModelOverride,
|
|
293
428
|
): EffectiveSeat {
|
|
294
429
|
const automatic = seat === AUTOMATIC_NAVIGATOR_SEAT;
|
|
295
|
-
const
|
|
430
|
+
const hasModelInvocation =
|
|
296
431
|
invocation !== undefined &&
|
|
297
432
|
(invocation.model !== undefined || invocation.thinking !== undefined);
|
|
298
|
-
if (!hasInvocation || invocation === undefined) {
|
|
299
|
-
return resolveBaseSeat(config, seat, credentials);
|
|
300
|
-
}
|
|
301
433
|
|
|
302
|
-
|
|
434
|
+
let modelSeat: EffectiveSeat;
|
|
435
|
+
if (!hasModelInvocation || invocation === undefined) {
|
|
436
|
+
modelSeat = resolveBaseSeat(config, seat, credentials);
|
|
437
|
+
} else if (invocation.model !== undefined) {
|
|
303
438
|
const spec =
|
|
304
439
|
invocation.model.includes(":") || invocation.thinking === undefined
|
|
305
440
|
? invocation.model
|
|
306
441
|
: `${invocation.model}:${invocation.thinking}`;
|
|
307
|
-
|
|
442
|
+
modelSeat = {
|
|
308
443
|
seat,
|
|
309
444
|
automatic,
|
|
310
445
|
source: "invocation",
|
|
311
446
|
selection: parseModelSpec(spec),
|
|
447
|
+
engineSource: "unconfigured",
|
|
312
448
|
};
|
|
449
|
+
} else {
|
|
450
|
+
const base = resolveBaseSeat(config, seat, credentials);
|
|
451
|
+
if (base.selection === undefined || invocation.thinking === undefined) {
|
|
452
|
+
modelSeat = {
|
|
453
|
+
seat,
|
|
454
|
+
automatic,
|
|
455
|
+
source: "unconfigured",
|
|
456
|
+
engineSource: "unconfigured",
|
|
457
|
+
};
|
|
458
|
+
} else {
|
|
459
|
+
modelSeat = {
|
|
460
|
+
seat,
|
|
461
|
+
automatic,
|
|
462
|
+
source: "invocation",
|
|
463
|
+
selection: { ...base.selection, thinking: invocation.thinking },
|
|
464
|
+
engineSource: "unconfigured",
|
|
465
|
+
};
|
|
466
|
+
}
|
|
313
467
|
}
|
|
314
468
|
|
|
315
|
-
|
|
316
|
-
if (base.selection === undefined || invocation.thinking === undefined) {
|
|
317
|
-
return { seat, automatic, source: "unconfigured" };
|
|
318
|
-
}
|
|
319
|
-
return {
|
|
320
|
-
seat,
|
|
321
|
-
automatic,
|
|
322
|
-
source: "invocation",
|
|
323
|
-
selection: { ...base.selection, thinking: invocation.thinking },
|
|
324
|
-
};
|
|
469
|
+
return attachEngineAxis(modelSeat, config, invocation);
|
|
325
470
|
}
|
|
326
471
|
|
|
327
472
|
export function effectiveSeatConfigurations(
|