@akagilnc/pi-workflow-roles 0.1.2312 → 0.1.2322

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.
@@ -7,9 +7,10 @@ import { dirname, join } from "node:path";
7
7
 
8
8
  import { assertLegalEngineName } from "../package-resources/engine-material.ts";
9
9
  import {
10
- AUTOMATIC_NAVIGATOR_SEAT,
10
+ AUTOMATIC_CONFIGURABLE_SEATS,
11
11
  PUBLIC_CALLABLE_ROLES,
12
12
  PUBLIC_CONFIGURABLE_SEATS,
13
+ isAutomaticConfigurableSeat,
13
14
  isPublicCallableRole,
14
15
  isPublicConfigurableSeat,
15
16
  type ModelRef,
@@ -19,6 +20,19 @@ import {
19
20
  publicStartupCandidates,
20
21
  } from "./registry.ts";
21
22
 
23
+ /** Province officers that may carry a persistent model override (#453). */
24
+ export const MENXIA_OFFICER_SEATS = [
25
+ "gatekeeper",
26
+ "inspector",
27
+ "notary",
28
+ ] as const;
29
+
30
+ export type MenxiaOfficerSeat = (typeof MENXIA_OFFICER_SEATS)[number];
31
+
32
+ export function isMenxiaOfficerSeat(value: string): value is MenxiaOfficerSeat {
33
+ return (MENXIA_OFFICER_SEATS as readonly string[]).includes(value);
34
+ }
35
+
22
36
  export type CredentialProviders = {
23
37
  "openai-codex": boolean;
24
38
  xai: boolean;
@@ -26,8 +40,16 @@ export type CredentialProviders = {
26
40
 
27
41
  export type SeatModelConfig = ModelRef;
28
42
 
29
- /** Persistent seat row: provider/model[:thinking] + optional engine axis (#356/#384). */
30
- export type PersistentSeatConfig = SeatModelConfig & {
43
+ /**
44
+ * Persistent seat row (#356/#384/#453): model fields and engine are independent
45
+ * axes. Engine-only residual is legal only for notary after model clear so direct
46
+ * notary activation keeps its labor engine while province inheritance resumes.
47
+ * All other seats keep the baseline provider/model required contract.
48
+ */
49
+ export type PersistentSeatConfig = {
50
+ provider?: string;
51
+ model?: string;
52
+ thinking?: PublicThinkingLevel;
31
53
  engine?: string;
32
54
  };
33
55
 
@@ -49,7 +71,7 @@ export type EngineSource = "invocation" | "persistent" | "unconfigured";
49
71
 
50
72
  export type EffectiveSeat = {
51
73
  seat: PublicConfigurableSeat;
52
- /** True when the seat is automatic Navigator attendance rather than caller-selected. */
74
+ /** True when the seat is automatic (no caller command) rather than caller-selected. */
53
75
  automatic: boolean;
54
76
  source: EffectiveSource;
55
77
  selection?: SeatModelConfig;
@@ -129,6 +151,49 @@ export function setPersistentSeatConfig(
129
151
  };
130
152
  }
131
153
 
154
+ /**
155
+ * Clear a menxia officer's persistent model override (#453).
156
+ * Scope is MenxiaOfficerSeat only — non-province seats have no destructive clear seam.
157
+ * Only notary may retain an engine-only residual so direct notary activation keeps
158
+ * its labor engine while model resolution returns to startup / province inheritance.
159
+ * gatekeeper/inspector drop the whole row. Already-absent seats are a no-op.
160
+ */
161
+ export function clearPersistentSeatConfig(
162
+ config: PublicCliConfig,
163
+ seat: MenxiaOfficerSeat,
164
+ ): PublicCliConfig {
165
+ const previous = config.seats[seat];
166
+ if (previous === undefined) return config;
167
+ // Engine-only residual ownership is notary-only — never widen to other officers.
168
+ if (seat === "notary" && previous.engine !== undefined) {
169
+ return {
170
+ ...config,
171
+ seats: {
172
+ ...config.seats,
173
+ notary: { engine: previous.engine },
174
+ },
175
+ };
176
+ }
177
+ const { [seat]: _dropped, ...seats } = config.seats;
178
+ return { ...config, seats };
179
+ }
180
+
181
+ /**
182
+ * Province-only model selection (#453): officer persistent > gatekeeper persistent
183
+ * > unset (caller inherits parent session). Never consults startup candidates —
184
+ * direct `ak-role notary` keeps resolveEffectiveSeat; only province reads this.
185
+ * Engine-only residual is not a model override.
186
+ */
187
+ export function resolveMenxiaOfficerModelSelection(
188
+ config: PublicCliConfig,
189
+ officer: MenxiaOfficerSeat,
190
+ ): SeatModelConfig | undefined {
191
+ const ownModel = seatModelOnly(config.seats[officer]);
192
+ if (ownModel !== undefined) return ownModel;
193
+ if (officer === "gatekeeper") return undefined;
194
+ return seatModelOnly(config.seats.gatekeeper);
195
+ }
196
+
132
197
  /**
133
198
  * Seats that own the labor-engine axis (#391: PUBLIC_CALLABLE_ROLES only).
134
199
  * Navigator is configurable for model but has no independent activation path.
@@ -138,9 +203,11 @@ export function isEngineAxisSeat(seat: string): seat is PublicCallableRole {
138
203
  }
139
204
 
140
205
  /**
141
- * Set or clear persistent engine on a callable role seat (#356 / #378 / #391).
142
- * Engine-only seats are rejected — provider/model[:thinking] remains required first.
143
- * Seat type is PublicCallableRole (navigator excluded at the type boundary).
206
+ * Set or clear persistent engine on a callable role seat (#356 / #378 / #391 / #453).
207
+ * First engine still requires an existing seat row (model, or notary engine residual).
208
+ * Clearing engine from a notary engine-only residual drops the empty row; clearing
209
+ * engine from a model+engine row leaves model-only. Seat type is PublicCallableRole
210
+ * (navigator excluded at the type boundary).
144
211
  */
145
212
  export function setPersistentSeatEngine(
146
213
  config: PublicCliConfig,
@@ -155,6 +222,11 @@ export function setPersistentSeatEngine(
155
222
  }
156
223
  if (engine === undefined) {
157
224
  const { engine: _dropped, ...modelOnly } = previous;
225
+ // Engine-only residual with engine cleared → drop the empty row.
226
+ if (seatModelOnly(modelOnly) === undefined) {
227
+ const { [seat]: _row, ...seats } = config.seats;
228
+ return { ...config, seats };
229
+ }
158
230
  return {
159
231
  ...config,
160
232
  seats: {
@@ -200,8 +272,14 @@ export function setAutoResumeLimit(
200
272
  return { ...config, autoResumeLimit: parseAutoResumeLimit(limit) };
201
273
  }
202
274
 
203
- /** Strip optional engine so activation model argv never sees the engine axis. */
204
- export function seatModelOnly(seat: PersistentSeatConfig): SeatModelConfig {
275
+ /**
276
+ * Strip optional engine so activation model argv never sees the engine axis.
277
+ * Engine-only residual (model cleared) yields undefined — not a model override.
278
+ */
279
+ export function seatModelOnly(
280
+ seat: PersistentSeatConfig | undefined,
281
+ ): SeatModelConfig | undefined {
282
+ if (seat?.provider === undefined || seat.model === undefined) return undefined;
205
283
  return seat.thinking === undefined
206
284
  ? { provider: seat.provider, model: seat.model }
207
285
  : { provider: seat.provider, model: seat.model, thinking: seat.thinking };
@@ -339,6 +417,27 @@ function parseSeatModelConfig(value: unknown, seat: string): PersistentSeatConfi
339
417
  throw new Error(`config seat ${seat} must be an object`);
340
418
  }
341
419
  const raw = value as Record<string, unknown>;
420
+ const hasProvider = raw.provider !== undefined;
421
+ const hasModel = raw.model !== undefined;
422
+ if (hasProvider !== hasModel) {
423
+ throw new Error(`config seat ${seat} requires both provider and model`);
424
+ }
425
+ if (raw.engine !== undefined && typeof raw.engine !== "string") {
426
+ // Shape only: engine must be a string field. Path-safety syntax is deferred to
427
+ // validatePublicCliConfigEngines → assertLegalEngineName (single authority).
428
+ throw new Error(`config seat ${seat} engine must be a string`);
429
+ }
430
+ // #453: engine-only residual is legal only for notary after model clear.
431
+ // All other seats keep the baseline provider/model required contract.
432
+ if (!hasProvider) {
433
+ if (seat === "notary" && typeof raw.engine === "string") {
434
+ if (raw.thinking !== undefined) {
435
+ throw new Error(`config seat ${seat} thinking requires provider/model`);
436
+ }
437
+ return { engine: raw.engine };
438
+ }
439
+ throw new Error(`config seat ${seat} requires provider`);
440
+ }
342
441
  if (typeof raw.provider !== "string" || raw.provider.trim() === "") {
343
442
  throw new Error(`config seat ${seat} requires provider`);
344
443
  }
@@ -355,20 +454,13 @@ function parseSeatModelConfig(value: unknown, seat: string): PersistentSeatConfi
355
454
  }
356
455
  }
357
456
  const parsed: PersistentSeatConfig = {
358
- provider: raw.provider,
359
- model: raw.model,
457
+ provider: raw.provider as string,
458
+ model: raw.model as string,
360
459
  ...(raw.thinking === undefined
361
460
  ? {}
362
461
  : { thinking: raw.thinking as PublicThinkingLevel }),
462
+ ...(raw.engine === undefined ? {} : { engine: raw.engine as string }),
363
463
  };
364
- if (raw.engine !== undefined) {
365
- // Shape only: engine must be a string field. Path-safety syntax is deferred to
366
- // validatePublicCliConfigEngines → assertLegalEngineName (single authority).
367
- if (typeof raw.engine !== "string") {
368
- throw new Error(`config seat ${seat} engine must be a string`);
369
- }
370
- parsed.engine = raw.engine;
371
- }
372
464
  return parsed;
373
465
  }
374
466
 
@@ -444,14 +536,15 @@ function resolveBaseSeat(
444
536
  seat: PublicConfigurableSeat,
445
537
  credentials: CredentialProviders,
446
538
  ): EffectiveSeat {
447
- const automatic = seat === AUTOMATIC_NAVIGATOR_SEAT;
448
- const persistent = config.seats[seat];
449
- if (persistent !== undefined) {
539
+ const automatic = isAutomaticConfigurableSeat(seat);
540
+ // Engine-only residual is not a persistent model source (#453).
541
+ const persistentModel = seatModelOnly(config.seats[seat]);
542
+ if (persistentModel !== undefined) {
450
543
  return {
451
544
  seat,
452
545
  automatic,
453
546
  source: "persistent",
454
- selection: seatModelOnly(persistent),
547
+ selection: persistentModel,
455
548
  engineSource: "unconfigured",
456
549
  };
457
550
  }
@@ -474,7 +567,7 @@ export function resolveEffectiveSeat(
474
567
  credentials: CredentialProviders,
475
568
  invocation?: InvocationModelOverride,
476
569
  ): EffectiveSeat {
477
- const automatic = seat === AUTOMATIC_NAVIGATOR_SEAT;
570
+ const automatic = isAutomaticConfigurableSeat(seat);
478
571
  const hasModelInvocation =
479
572
  invocation !== undefined &&
480
573
  (invocation.model !== undefined || invocation.thinking !== undefined);
@@ -527,7 +620,7 @@ export function effectiveSeatConfigurations(
527
620
  );
528
621
  }
529
622
 
530
- /** Callable roles first (package order), then automatic Navigator. */
623
+ /** Callable roles first (package order), then automatic configurable seats. */
531
624
  export function listRolesForDisplay(
532
625
  config: PublicCliConfig,
533
626
  credentials: CredentialProviders,
@@ -537,8 +630,10 @@ export function listRolesForDisplay(
537
630
  const callable = PUBLIC_CALLABLE_ROLES.map(
538
631
  (role) => all.find((entry) => entry.seat === role)!,
539
632
  );
540
- const navigator = all.find((entry) => entry.seat === AUTOMATIC_NAVIGATOR_SEAT)!;
541
- return [...callable, navigator];
633
+ const automatic = AUTOMATIC_CONFIGURABLE_SEATS.map(
634
+ (seat) => all.find((entry) => entry.seat === seat)!,
635
+ );
636
+ return [...callable, ...automatic];
542
637
  }
543
638
 
544
639
  /**
@@ -1125,12 +1125,14 @@ const SUPPORT_COMMAND_HELP = {
1125
1125
  summary: "Persistent seat model, labor-engine, and auto-resume defaults.",
1126
1126
  usage: [
1127
1127
  "ak-role config set <seat> <provider/model[:thinking]> [<seat> <spec> ...]",
1128
+ "ak-role config unset <gatekeeper|inspector|notary>",
1128
1129
  "ak-role config set-engine <seat> <name>",
1129
1130
  "ak-role config unset-engine <seat>",
1130
1131
  "ak-role config set-auto-resume-limit <N>",
1131
1132
  ],
1132
1133
  examples: [
1133
1134
  "ak-role config set judge openai-codex/gpt-5.6-sol:high",
1135
+ "ak-role config unset gatekeeper",
1134
1136
  "ak-role config set-engine judge opus",
1135
1137
  "ak-role config set-auto-resume-limit 3",
1136
1138
  ],
@@ -19,15 +19,35 @@ export type PublicCallableRole = (typeof PUBLIC_CALLABLE_ROLES)[number];
19
19
  /** Automatic attendance seat — configurable, never a caller-selected command. */
20
20
  export const AUTOMATIC_NAVIGATOR_SEAT = "navigator" as const;
21
21
 
22
+ /** Automatic menxia seats — configurable model only; never caller commands (#453). */
23
+ export const AUTOMATIC_GATEKEEPER_SEAT = "gatekeeper" as const;
24
+ export const AUTOMATIC_INSPECTOR_SEAT = "inspector" as const;
25
+
26
+ /** All automatic configurable seats (no independent public activation command). */
27
+ export const AUTOMATIC_CONFIGURABLE_SEATS = [
28
+ AUTOMATIC_GATEKEEPER_SEAT,
29
+ AUTOMATIC_INSPECTOR_SEAT,
30
+ AUTOMATIC_NAVIGATOR_SEAT,
31
+ ] as const;
32
+
33
+ export type AutomaticConfigurableSeat =
34
+ (typeof AUTOMATIC_CONFIGURABLE_SEATS)[number];
35
+
22
36
  export type PublicConfigurableSeat =
23
37
  | PublicCallableRole
24
- | typeof AUTOMATIC_NAVIGATOR_SEAT;
38
+ | AutomaticConfigurableSeat;
25
39
 
26
40
  export const PUBLIC_CONFIGURABLE_SEATS = [
27
41
  ...PUBLIC_CALLABLE_ROLES,
28
- AUTOMATIC_NAVIGATOR_SEAT,
42
+ ...AUTOMATIC_CONFIGURABLE_SEATS,
29
43
  ] as const satisfies readonly PublicConfigurableSeat[];
30
44
 
45
+ export function isAutomaticConfigurableSeat(
46
+ value: string,
47
+ ): value is AutomaticConfigurableSeat {
48
+ return (AUTOMATIC_CONFIGURABLE_SEATS as readonly string[]).includes(value);
49
+ }
50
+
31
51
  export const PUBLIC_CLI_SUPPORT_COMMANDS = [
32
52
  "roles",
33
53
  "config",
@@ -90,6 +110,9 @@ const STARTUP_CANDIDATES: Record<PublicConfigurableSeat, readonly ModelRef[]> =
90
110
  { provider: "openai-codex", model: "gpt-5.6-luna", thinking: "high" },
91
111
  { provider: "xai", model: "grok-4.5", thinking: "high" },
92
112
  ],
113
+ // #453: automatic menxia seats have no startup default — unset means inherit parent.
114
+ gatekeeper: [],
115
+ inspector: [],
93
116
  navigator: [
94
117
  { provider: "openai-codex", model: "gpt-5.6-luna", thinking: "medium" },
95
118
  { provider: "xai", model: "grok-4.5", thinking: "high" },