@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.
@@ -14473,6 +14473,9 @@ var init_packaged_role_registry = __esm({
14473
14473
  });
14474
14474
 
14475
14475
  // src/public-cli/registry.ts
14476
+ function isAutomaticConfigurableSeat(value) {
14477
+ return AUTOMATIC_CONFIGURABLE_SEATS.includes(value);
14478
+ }
14476
14479
  function publicStartupCandidates(seat) {
14477
14480
  return STARTUP_CANDIDATES[seat];
14478
14481
  }
@@ -14508,7 +14511,7 @@ function isPublicConfigurableSeat(value) {
14508
14511
  function isPublicCliSupportCommand(value) {
14509
14512
  return PUBLIC_CLI_SUPPORT_COMMANDS.includes(value);
14510
14513
  }
14511
- var INTERNAL_ROLE_ENTRYPOINT_RELATIVE, PUBLIC_CALLABLE_ROLES, AUTOMATIC_NAVIGATOR_SEAT, PUBLIC_CONFIGURABLE_SEATS, PUBLIC_CLI_SUPPORT_COMMANDS, STARTUP_CANDIDATES, PUBLIC_DETERMINISTIC_COMMANDS;
14514
+ var INTERNAL_ROLE_ENTRYPOINT_RELATIVE, PUBLIC_CALLABLE_ROLES, AUTOMATIC_NAVIGATOR_SEAT, AUTOMATIC_GATEKEEPER_SEAT, AUTOMATIC_INSPECTOR_SEAT, AUTOMATIC_CONFIGURABLE_SEATS, PUBLIC_CONFIGURABLE_SEATS, PUBLIC_CLI_SUPPORT_COMMANDS, STARTUP_CANDIDATES, PUBLIC_DETERMINISTIC_COMMANDS;
14512
14515
  var init_registry2 = __esm({
14513
14516
  "src/public-cli/registry.ts"() {
14514
14517
  "use strict";
@@ -14518,9 +14521,16 @@ var init_registry2 = __esm({
14518
14521
  (entry) => entry.role
14519
14522
  );
14520
14523
  AUTOMATIC_NAVIGATOR_SEAT = "navigator";
14524
+ AUTOMATIC_GATEKEEPER_SEAT = "gatekeeper";
14525
+ AUTOMATIC_INSPECTOR_SEAT = "inspector";
14526
+ AUTOMATIC_CONFIGURABLE_SEATS = [
14527
+ AUTOMATIC_GATEKEEPER_SEAT,
14528
+ AUTOMATIC_INSPECTOR_SEAT,
14529
+ AUTOMATIC_NAVIGATOR_SEAT
14530
+ ];
14521
14531
  PUBLIC_CONFIGURABLE_SEATS = [
14522
14532
  ...PUBLIC_CALLABLE_ROLES,
14523
- AUTOMATIC_NAVIGATOR_SEAT
14533
+ ...AUTOMATIC_CONFIGURABLE_SEATS
14524
14534
  ];
14525
14535
  PUBLIC_CLI_SUPPORT_COMMANDS = [
14526
14536
  "roles",
@@ -14561,6 +14571,9 @@ var init_registry2 = __esm({
14561
14571
  { provider: "openai-codex", model: "gpt-5.6-luna", thinking: "high" },
14562
14572
  { provider: "xai", model: "grok-4.5", thinking: "high" }
14563
14573
  ],
14574
+ // #453: automatic menxia seats have no startup default — unset means inherit parent.
14575
+ gatekeeper: [],
14576
+ inspector: [],
14564
14577
  navigator: [
14565
14578
  { provider: "openai-codex", model: "gpt-5.6-luna", thinking: "medium" },
14566
14579
  { provider: "xai", model: "grok-4.5", thinking: "high" }
@@ -14574,6 +14587,9 @@ var init_registry2 = __esm({
14574
14587
  import { mkdir, readFile, writeFile } from "node:fs/promises";
14575
14588
  import { homedir } from "node:os";
14576
14589
  import { dirname as dirname2, join as join3 } from "node:path";
14590
+ function isMenxiaOfficerSeat(value) {
14591
+ return MENXIA_OFFICER_SEATS.includes(value);
14592
+ }
14577
14593
  function publicCliConfigPath(home = homedir()) {
14578
14594
  return join3(home, ".ak-roles", "public-cli.json");
14579
14595
  }
@@ -14612,6 +14628,21 @@ function setPersistentSeatConfig(config, seat, selection) {
14612
14628
  }
14613
14629
  };
14614
14630
  }
14631
+ function clearPersistentSeatConfig(config, seat) {
14632
+ const previous = config.seats[seat];
14633
+ if (previous === void 0) return config;
14634
+ if (seat === "notary" && previous.engine !== void 0) {
14635
+ return {
14636
+ ...config,
14637
+ seats: {
14638
+ ...config.seats,
14639
+ notary: { engine: previous.engine }
14640
+ }
14641
+ };
14642
+ }
14643
+ const { [seat]: _dropped, ...seats } = config.seats;
14644
+ return { ...config, seats };
14645
+ }
14615
14646
  function isEngineAxisSeat(seat) {
14616
14647
  return isPublicCallableRole(seat);
14617
14648
  }
@@ -14624,6 +14655,10 @@ function setPersistentSeatEngine(config, seat, engine) {
14624
14655
  }
14625
14656
  if (engine === void 0) {
14626
14657
  const { engine: _dropped, ...modelOnly } = previous;
14658
+ if (seatModelOnly(modelOnly) === void 0) {
14659
+ const { [seat]: _row, ...seats } = config.seats;
14660
+ return { ...config, seats };
14661
+ }
14627
14662
  return {
14628
14663
  ...config,
14629
14664
  seats: {
@@ -14652,6 +14687,7 @@ function setAutoResumeLimit(config, limit) {
14652
14687
  return { ...config, autoResumeLimit: parseAutoResumeLimit(limit) };
14653
14688
  }
14654
14689
  function seatModelOnly(seat) {
14690
+ if (seat?.provider === void 0 || seat.model === void 0) return void 0;
14655
14691
  return seat.thinking === void 0 ? { provider: seat.provider, model: seat.model } : { provider: seat.provider, model: seat.model, thinking: seat.thinking };
14656
14692
  }
14657
14693
  function validatePublicCliConfigEngines(config, _packageRoot) {
@@ -14752,6 +14788,23 @@ function parseSeatModelConfig(value, seat) {
14752
14788
  throw new Error(`config seat ${seat} must be an object`);
14753
14789
  }
14754
14790
  const raw = value;
14791
+ const hasProvider = raw.provider !== void 0;
14792
+ const hasModel = raw.model !== void 0;
14793
+ if (hasProvider !== hasModel) {
14794
+ throw new Error(`config seat ${seat} requires both provider and model`);
14795
+ }
14796
+ if (raw.engine !== void 0 && typeof raw.engine !== "string") {
14797
+ throw new Error(`config seat ${seat} engine must be a string`);
14798
+ }
14799
+ if (!hasProvider) {
14800
+ if (seat === "notary" && typeof raw.engine === "string") {
14801
+ if (raw.thinking !== void 0) {
14802
+ throw new Error(`config seat ${seat} thinking requires provider/model`);
14803
+ }
14804
+ return { engine: raw.engine };
14805
+ }
14806
+ throw new Error(`config seat ${seat} requires provider`);
14807
+ }
14755
14808
  if (typeof raw.provider !== "string" || raw.provider.trim() === "") {
14756
14809
  throw new Error(`config seat ${seat} requires provider`);
14757
14810
  }
@@ -14766,14 +14819,9 @@ function parseSeatModelConfig(value, seat) {
14766
14819
  const parsed = {
14767
14820
  provider: raw.provider,
14768
14821
  model: raw.model,
14769
- ...raw.thinking === void 0 ? {} : { thinking: raw.thinking }
14822
+ ...raw.thinking === void 0 ? {} : { thinking: raw.thinking },
14823
+ ...raw.engine === void 0 ? {} : { engine: raw.engine }
14770
14824
  };
14771
- if (raw.engine !== void 0) {
14772
- if (typeof raw.engine !== "string") {
14773
- throw new Error(`config seat ${seat} engine must be a string`);
14774
- }
14775
- parsed.engine = raw.engine;
14776
- }
14777
14825
  return parsed;
14778
14826
  }
14779
14827
  function providerConfigured(credentials, provider) {
@@ -14821,14 +14869,14 @@ function attachEngineAxis(seat, config, invocation) {
14821
14869
  };
14822
14870
  }
14823
14871
  function resolveBaseSeat(config, seat, credentials) {
14824
- const automatic = seat === AUTOMATIC_NAVIGATOR_SEAT;
14825
- const persistent = config.seats[seat];
14826
- if (persistent !== void 0) {
14872
+ const automatic = isAutomaticConfigurableSeat(seat);
14873
+ const persistentModel = seatModelOnly(config.seats[seat]);
14874
+ if (persistentModel !== void 0) {
14827
14875
  return {
14828
14876
  seat,
14829
14877
  automatic,
14830
14878
  source: "persistent",
14831
- selection: seatModelOnly(persistent),
14879
+ selection: persistentModel,
14832
14880
  engineSource: "unconfigured"
14833
14881
  };
14834
14882
  }
@@ -14845,7 +14893,7 @@ function resolveBaseSeat(config, seat, credentials) {
14845
14893
  return { seat, automatic, source: "unconfigured", engineSource: "unconfigured" };
14846
14894
  }
14847
14895
  function resolveEffectiveSeat(config, seat, credentials, invocation) {
14848
- const automatic = seat === AUTOMATIC_NAVIGATOR_SEAT;
14896
+ const automatic = isAutomaticConfigurableSeat(seat);
14849
14897
  const hasModelInvocation = invocation !== void 0 && (invocation.model !== void 0 || invocation.thinking !== void 0);
14850
14898
  let modelSeat;
14851
14899
  if (!hasModelInvocation || invocation === void 0) {
@@ -14906,12 +14954,17 @@ async function loadCredentialProviders(agentDir) {
14906
14954
  throw error;
14907
14955
  }
14908
14956
  }
14909
- var THINKING_LEVELS;
14957
+ var MENXIA_OFFICER_SEATS, THINKING_LEVELS;
14910
14958
  var init_config2 = __esm({
14911
14959
  "src/public-cli/config.ts"() {
14912
14960
  "use strict";
14913
14961
  init_engine_material();
14914
14962
  init_registry2();
14963
+ MENXIA_OFFICER_SEATS = [
14964
+ "gatekeeper",
14965
+ "inspector",
14966
+ "notary"
14967
+ ];
14915
14968
  THINKING_LEVELS = /* @__PURE__ */ new Set([
14916
14969
  "off",
14917
14970
  "minimal",
@@ -17411,12 +17464,14 @@ var init_option_definitions = __esm({
17411
17464
  summary: "Persistent seat model, labor-engine, and auto-resume defaults.",
17412
17465
  usage: [
17413
17466
  "ak-role config set <seat> <provider/model[:thinking]> [<seat> <spec> ...]",
17467
+ "ak-role config unset <gatekeeper|inspector|notary>",
17414
17468
  "ak-role config set-engine <seat> <name>",
17415
17469
  "ak-role config unset-engine <seat>",
17416
17470
  "ak-role config set-auto-resume-limit <N>"
17417
17471
  ],
17418
17472
  examples: [
17419
17473
  "ak-role config set judge openai-codex/gpt-5.6-sol:high",
17474
+ "ak-role config unset gatekeeper",
17420
17475
  "ak-role config set-engine judge opus",
17421
17476
  "ak-role config set-auto-resume-limit 3"
17422
17477
  ]
@@ -20062,6 +20117,7 @@ var init_evidence_child_executor = __esm({
20062
20117
  init_engine_detour_tool();
20063
20118
  init_engine_detour();
20064
20119
  init_engine_material();
20120
+ init_config2();
20065
20121
  init_receipt_delivery_policy();
20066
20122
  init_reviewer_construction();
20067
20123
  init_stream_idle_guard();
@@ -28740,9 +28796,9 @@ function requireLegalEngineName(name) {
28740
28796
  }
28741
28797
  }
28742
28798
  function requireEngineAxisSeat(seat, verb) {
28743
- if (seat === AUTOMATIC_NAVIGATOR_SEAT) {
28799
+ if (isAutomaticConfigurableSeat(seat)) {
28744
28800
  throw new CliUsageError(
28745
- `config ${verb} refuses navigator: no independent activation path; storing would be silently ineffective`
28801
+ `config ${verb} refuses ${seat}: no independent activation path; storing would be silently ineffective`
28746
28802
  );
28747
28803
  }
28748
28804
  if (!isEngineAxisSeat(seat)) {
@@ -28838,7 +28894,7 @@ function renderHelp() {
28838
28894
  lines.push(
28839
28895
  "",
28840
28896
  "Role options: ak-role help <command>",
28841
- "Persistent config: ak-role config set <seat> <provider/model[:thinking]>",
28897
+ "Persistent config: ak-role config set <seat> <provider/model[:thinking]> | unset <gatekeeper|inspector|notary>",
28842
28898
  "Persistent engine (callable roles): ak-role config set-engine <seat> <name> | unset-engine <seat>",
28843
28899
  "Effective seats: ak-role roles"
28844
28900
  );
@@ -28879,6 +28935,10 @@ function renderRoles(seats) {
28879
28935
  return `${lines.join("\n")}
28880
28936
  `;
28881
28937
  }
28938
+ function renderPersistentSeatModel(selection) {
28939
+ const model = seatModelOnly(selection);
28940
+ return model === void 0 ? "-" : formatModelSpec(model);
28941
+ }
28882
28942
  function renderConfig(config) {
28883
28943
  const lines = ["seat model engine"];
28884
28944
  const keys = Object.keys(config.seats);
@@ -28889,7 +28949,7 @@ function renderConfig(config) {
28889
28949
  const selection = config.seats[seat];
28890
28950
  if (selection === void 0) continue;
28891
28951
  const engine = selection.engine === void 0 ? "-" : selection.engine;
28892
- lines.push(`${seat} ${formatModelSpec(selection)} ${engine}`);
28952
+ lines.push(`${seat} ${renderPersistentSeatModel(selection)} ${engine}`);
28893
28953
  }
28894
28954
  }
28895
28955
  lines.push(`autoResumeLimit ${config.autoResumeLimit ?? AUTO_RESUME_LIMIT}`);
@@ -28909,8 +28969,10 @@ async function runConfigCommand(args, home, packageRoot2, io) {
28909
28969
  `);
28910
28970
  } else {
28911
28971
  const engine = selection.engine === void 0 ? "-" : selection.engine;
28912
- io.stdout(`${args[1]} ${formatModelSpec(selection)} ${engine}
28913
- `);
28972
+ io.stdout(
28973
+ `${args[1]} ${renderPersistentSeatModel(selection)} ${engine}
28974
+ `
28975
+ );
28914
28976
  }
28915
28977
  return 0;
28916
28978
  }
@@ -28942,6 +29004,26 @@ async function runConfigCommand(args, home, packageRoot2, io) {
28942
29004
  io.stdout(renderConfig(config));
28943
29005
  return 0;
28944
29006
  }
29007
+ if (args[0] === "unset") {
29008
+ if (args.length !== 2) {
29009
+ throw new CliUsageError(
29010
+ "usage: ak-role config unset <gatekeeper|inspector|notary>"
29011
+ );
29012
+ }
29013
+ const seat = args[1];
29014
+ if (!isMenxiaOfficerSeat(seat)) {
29015
+ throw new CliUsageError(
29016
+ `config unset serves menxia officer overrides only (gatekeeper|inspector|notary); got ${seat}`
29017
+ );
29018
+ }
29019
+ const config = clearPersistentSeatConfig(
29020
+ await loadAndValidateConfig(home, packageRoot2),
29021
+ seat
29022
+ );
29023
+ await savePublicCliConfig(config, home);
29024
+ io.stdout(renderConfig(config));
29025
+ return 0;
29026
+ }
28945
29027
  if (args[0] === "set-engine") {
28946
29028
  if (args.length !== 3) {
28947
29029
  throw new CliUsageError(
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Typed public CLI registry — sole source for discoverable commands, seats,
3
+ * and startup model candidates (ADR 0052 / #105).
4
+ */
5
+ import { PACKAGED_ROLE_REGISTRY, } from "../packaged-role-registry.js";
6
+ /** Package-relative path of the Internal role entrypoint (explicit load only). */
7
+ export const INTERNAL_ROLE_ENTRYPOINT_RELATIVE = "extensions/role-runtime.ts";
8
+ export const PUBLIC_CALLABLE_ROLES = PACKAGED_ROLE_REGISTRY.map((entry) => entry.role);
9
+ /** Automatic attendance seat — configurable, never a caller-selected command. */
10
+ export const AUTOMATIC_NAVIGATOR_SEAT = "navigator";
11
+ /** Automatic menxia seats — configurable model only; never caller commands (#453). */
12
+ export const AUTOMATIC_GATEKEEPER_SEAT = "gatekeeper";
13
+ export const AUTOMATIC_INSPECTOR_SEAT = "inspector";
14
+ /** All automatic configurable seats (no independent public activation command). */
15
+ export const AUTOMATIC_CONFIGURABLE_SEATS = [
16
+ AUTOMATIC_GATEKEEPER_SEAT,
17
+ AUTOMATIC_INSPECTOR_SEAT,
18
+ AUTOMATIC_NAVIGATOR_SEAT,
19
+ ];
20
+ export const PUBLIC_CONFIGURABLE_SEATS = [
21
+ ...PUBLIC_CALLABLE_ROLES,
22
+ ...AUTOMATIC_CONFIGURABLE_SEATS,
23
+ ];
24
+ export function isAutomaticConfigurableSeat(value) {
25
+ return AUTOMATIC_CONFIGURABLE_SEATS.includes(value);
26
+ }
27
+ export const PUBLIC_CLI_SUPPORT_COMMANDS = [
28
+ "roles",
29
+ "config",
30
+ "help",
31
+ "resume",
32
+ ];
33
+ /**
34
+ * Package startup candidates (#11): Codex family first, then Grok 4.5.
35
+ * Selection among candidates is credential-driven at resolve time.
36
+ */
37
+ const STARTUP_CANDIDATES = {
38
+ judge: [
39
+ { provider: "openai-codex", model: "gpt-5.6-sol", thinking: "high" },
40
+ { provider: "xai", model: "grok-4.5", thinking: "high" },
41
+ ],
42
+ reviewer: [
43
+ { provider: "openai-codex", model: "gpt-5.6-sol", thinking: "medium" },
44
+ { provider: "xai", model: "grok-4.5", thinking: "high" },
45
+ ],
46
+ coder: [
47
+ { provider: "openai-codex", model: "gpt-5.6-luna", thinking: "high" },
48
+ { provider: "xai", model: "grok-4.5", thinking: "high" },
49
+ ],
50
+ fixer: [
51
+ { provider: "openai-codex", model: "gpt-5.6-luna", thinking: "high" },
52
+ { provider: "xai", model: "grok-4.5", thinking: "high" },
53
+ ],
54
+ collector: [
55
+ { provider: "openai-codex", model: "gpt-5.6-luna", thinking: "high" },
56
+ { provider: "xai", model: "grok-4.5", thinking: "high" },
57
+ ],
58
+ doctor: [
59
+ { provider: "openai-codex", model: "gpt-5.6-luna", thinking: "high" },
60
+ { provider: "xai", model: "grok-4.5", thinking: "high" },
61
+ ],
62
+ merger: [
63
+ { provider: "openai-codex", model: "gpt-5.6-luna", thinking: "high" },
64
+ { provider: "xai", model: "grok-4.5", thinking: "high" },
65
+ ],
66
+ notary: [
67
+ { provider: "openai-codex", model: "gpt-5.6-luna", thinking: "high" },
68
+ { provider: "xai", model: "grok-4.5", thinking: "high" },
69
+ ],
70
+ // #453: automatic menxia seats have no startup default — unset means inherit parent.
71
+ gatekeeper: [],
72
+ inspector: [],
73
+ navigator: [
74
+ { provider: "openai-codex", model: "gpt-5.6-luna", thinking: "medium" },
75
+ { provider: "xai", model: "grok-4.5", thinking: "high" },
76
+ ],
77
+ };
78
+ export function publicStartupCandidates(seat) {
79
+ return STARTUP_CANDIDATES[seat];
80
+ }
81
+ /** Deterministic public commands — discoverable, never LLM-configurable seats. */
82
+ export const PUBLIC_DETERMINISTIC_COMMANDS = ["analyst"];
83
+ /**
84
+ * Typed help surface. Presentation formats these facts; tests must not assert
85
+ * exact help prose or layout (锚定宪法 / ADR 0016 / #105 AC).
86
+ * analyst is a deterministic analysis command on the public CLI — not an LLM seat.
87
+ */
88
+ export function listHelpCapabilities() {
89
+ const support = PUBLIC_CLI_SUPPORT_COMMANDS.map((name) => ({
90
+ kind: "support",
91
+ name,
92
+ }));
93
+ const roles = PACKAGED_ROLE_REGISTRY.map((entry) => {
94
+ const phases = entry.phases;
95
+ const defaultPhase = phases.length === 1 && phases[0] === null
96
+ ? null
97
+ : phases.includes("apply")
98
+ ? "apply"
99
+ : (phases[0] ?? null);
100
+ return {
101
+ kind: "role",
102
+ name: entry.role,
103
+ phases,
104
+ defaultPhase,
105
+ };
106
+ });
107
+ const deterministic = PUBLIC_DETERMINISTIC_COMMANDS.map((name) => ({
108
+ kind: "deterministic",
109
+ name,
110
+ }));
111
+ return [...support, ...roles, ...deterministic];
112
+ }
113
+ export function isPublicCallableRole(value) {
114
+ return PUBLIC_CALLABLE_ROLES.includes(value);
115
+ }
116
+ export function isPublicConfigurableSeat(value) {
117
+ return PUBLIC_CONFIGURABLE_SEATS.includes(value);
118
+ }
119
+ export function isPublicCliSupportCommand(value) {
120
+ return PUBLIC_CLI_SUPPORT_COMMANDS.includes(value);
121
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akagilnc/pi-workflow-roles",
3
- "version": "0.1.2312",
3
+ "version": "0.1.2322",
4
4
  "description": "Soul-bound workflow roles for Pi",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -36,6 +36,13 @@ import {
36
36
  engineSessionMaterialFromOptions,
37
37
  type EngineSessionMaterial,
38
38
  } from "./package-resources/engine-material.ts";
39
+ import {
40
+ formatModelSpec,
41
+ loadPublicCliConfig,
42
+ resolveMenxiaOfficerModelSelection,
43
+ type MenxiaOfficerSeat,
44
+ } from "./public-cli/config.ts";
45
+ import type { PublicThinkingLevel } from "./public-cli/registry.ts";
39
46
  import { createReceiptDeliveryPolicy, NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, RECEIPT_DELIVERY_PROMPT, type NoReceiptLifecycleFacts } from "./receipt-delivery-policy.ts";
40
47
  import { REVIEWER_VERIFICATION_BOUNDARY } from "./reviewer-construction.ts";
41
48
  import type { ReviewerPromptText } from "./reviewer-prompt-identity.ts";
@@ -129,6 +136,11 @@ export type InheritedRuntimeOptions = {
129
136
  readonly signal?: AbortSignal;
130
137
  /** When true, wrap provider streams with ADR 0059 idle-only retry. */
131
138
  readonly idleRetry?: boolean;
139
+ /**
140
+ * Explicit child model. When set, auth/availability failures do not fall back
141
+ * to the parent session model (#453 menxia overrides).
142
+ */
143
+ readonly model?: Model<Api>;
132
144
  };
133
145
 
134
146
  export type InheritedRuntime = {
@@ -145,7 +157,8 @@ export type InheritedRuntime = {
145
157
  */
146
158
  export async function createInheritedRuntime(options: InheritedRuntimeOptions): Promise<InheritedRuntime> {
147
159
  const { ModelRuntime } = await import("@earendil-works/pi-coding-agent");
148
- const activeModel = options.context.model;
160
+ // Explicit override wins; never silent-fallback to parent when override is set (#453).
161
+ const activeModel = options.model ?? options.context.model;
149
162
  if (activeModel === undefined) throw new Error(`${options.label} requires an active model`);
150
163
  const dispatch = await prepareComplianceDispatch(activeModel, options.context, options.label);
151
164
  const parentProvider = options.runCompletion === undefined
@@ -333,9 +346,11 @@ export async function createInheritedRuntime(options: InheritedRuntimeOptions):
333
346
  return wrapped as ReturnType<Provider["stream"]>;
334
347
  };
335
348
 
349
+ // Provider id must match activeModel.provider so ModelRuntime auth lookup
350
+ // finds this registration when a menxia override changes provider (#453).
336
351
  const provider: Provider = options.idleRetry === true || options.runCompletion !== undefined
337
352
  ? {
338
- id: parentProvider?.id ?? activeModel.provider,
353
+ id: activeModel.provider,
339
354
  name: parentProvider?.name ?? options.label,
340
355
  auth: {
341
356
  apiKey: {
@@ -716,8 +731,39 @@ export type AuditorRoleOptions = {
716
731
  signal?: AbortSignal;
717
732
  runCompletion?: AuditorCompletion;
718
733
  retainResponse?(response: AssistantMessage): void;
734
+ /**
735
+ * Province seat identity only — shared executor owns config load, registry
736
+ * resolve, and loud auth/availability failure (#453 / ADR 0018).
737
+ */
738
+ menxiaSeat?: MenxiaOfficerSeat;
719
739
  };
720
740
 
741
+ /**
742
+ * Resolve province child model from public-cli persistent config (#453).
743
+ * Own override > gatekeeper override > unset (inherit parent). Availability
744
+ * failures throw — createInheritedRuntime must not silent-fallback to parent.
745
+ */
746
+ async function resolveMenxiaSeatModelOptions(
747
+ context: ExtensionContext,
748
+ seat: MenxiaOfficerSeat,
749
+ roleLabel: string,
750
+ ): Promise<{ model?: Model<Api>; thinkingLevel?: PublicThinkingLevel }> {
751
+ const selection = resolveMenxiaOfficerModelSelection(await loadPublicCliConfig(), seat);
752
+ if (selection === undefined) return {};
753
+ const find = context.modelRegistry.find?.bind(context.modelRegistry);
754
+ if (typeof find !== "function") {
755
+ throw new Error(`${roleLabel} model registry cannot resolve models`);
756
+ }
757
+ const model = find(selection.provider, selection.model);
758
+ if (model === undefined) {
759
+ throw new Error(`${roleLabel} model is unavailable: ${formatModelSpec(selection)}`);
760
+ }
761
+ return {
762
+ model,
763
+ ...(selection.thinking === undefined ? {} : { thinkingLevel: selection.thinking }),
764
+ };
765
+ }
766
+
721
767
  /**
722
768
  * Auditor lifecycle via the shared in-process helper.
723
769
  * Adapter keeps role label / soul / decision tool / result projection only.
@@ -730,10 +776,19 @@ export async function executeAuditorChild(
730
776
  const { createRecordSession } = await import("./archivist-record-entry.ts");
731
777
 
732
778
  return withInProcessScratch({ prefix: "ak-auditor-role-" }, async (scratch) => {
779
+ const menxia =
780
+ options.menxiaSeat === undefined
781
+ ? {}
782
+ : await resolveMenxiaSeatModelOptions(
783
+ options.context,
784
+ options.menxiaSeat,
785
+ options.roleLabel,
786
+ );
733
787
  const inherited = await createInheritedRuntime({
734
788
  context: options.context,
735
789
  label: options.roleLabel,
736
790
  idleRetry: true,
791
+ ...(menxia.model === undefined ? {} : { model: menxia.model }),
737
792
  ...(options.runCompletion === undefined
738
793
  ? {}
739
794
  : { runCompletion: options.runCompletion, injectedSystemPrompt: options.systemPrompt }),
@@ -792,7 +847,7 @@ export async function executeAuditorChild(
792
847
  cwd,
793
848
  agentDir: scratch,
794
849
  model: inherited.model,
795
- thinkingLevel: options.context.thinkingLevel ?? "off",
850
+ thinkingLevel: menxia.thinkingLevel ?? options.context.thinkingLevel ?? "off",
796
851
  modelRuntime: inherited.runtime,
797
852
  systemPrompt: options.systemPrompt,
798
853
  customTools: [{ ...options.dossierTool, label: options.roleLabel }, tool],
@@ -215,9 +215,11 @@ export async function runGatekeeper(options: RunGatekeeperOptions): Promise<Gate
215
215
  const loadSoul = options.loadSoul ?? defaultLoadSoul;
216
216
  let provinceRun: Awaited<ReturnType<typeof executeAuditorChild>>;
217
217
  try {
218
+ // Seat identity only — shared executor owns model config/registry/auth (#453 / ADR 0018).
218
219
  provinceRun = await executeAuditorChild({
219
220
  context: options.context,
220
221
  roleLabel: "Gatekeeper",
222
+ menxiaSeat: "gatekeeper",
221
223
  systemPrompt: `${await loadSoul("gatekeeper")}\n\n${INVOCATION_OVERLAY}`,
222
224
  prompt: "Read the admitted subject with ak_gatekeeper_subject, then dispatch it or submit typed incomplete.",
223
225
  tool: createGatekeeperOutputTool(),
@@ -236,9 +238,11 @@ export async function runGatekeeper(options: RunGatekeeperOptions): Promise<Gate
236
238
 
237
239
  const officer = province.officer;
238
240
  try {
241
+ const roleLabel = officer === "inspector" ? "Inspector" : "Notary";
239
242
  const officerRun = await executeAuditorChild({
240
243
  context: options.context,
241
- roleLabel: officer === "inspector" ? "Inspector" : "Notary",
244
+ roleLabel,
245
+ menxiaSeat: officer,
242
246
  systemPrompt: `${await loadSoul(officer)}\n\n${INVOCATION_OVERLAY}`,
243
247
  prompt: "Read the admitted subject with ak_gatekeeper_subject, then submit one typed decision on only your assigned axes.",
244
248
  tool: createOfficerDecisionTool(officer === "inspector" ? INSPECTOR_OUTPUT_TOOL : NOTARY_OUTPUT_TOOL),
@@ -9,14 +9,17 @@ import {
9
9
  assertLegalEngineName,
10
10
  } from "../package-resources/engine-material.ts";
11
11
  import {
12
+ clearPersistentSeatConfig,
12
13
  effectiveSeatConfigurations,
13
14
  formatModelSpec,
14
15
  isEngineAxisSeat,
16
+ isMenxiaOfficerSeat,
15
17
  loadCredentialProviders,
16
18
  loadPublicCliConfig,
17
19
  parseModelSpec,
18
20
  resolveEffectiveSeat,
19
21
  savePublicCliConfig,
22
+ seatModelOnly,
20
23
  setAutoResumeLimit,
21
24
  setPersistentSeatConfig,
22
25
  setPersistentSeatEngine,
@@ -63,8 +66,8 @@ import { runPublicReviewer, runPublicReviewerResume } from "./reviewer-run.ts";
63
66
  import { runPublicAnalyst } from "./analyst-run.ts";
64
67
  import { AUTO_RESUME_LIMIT, peekRoleRunRole } from "./run-lifecycle.ts";
65
68
  import {
66
- AUTOMATIC_NAVIGATOR_SEAT,
67
69
  INTERNAL_ROLE_ENTRYPOINT_RELATIVE,
70
+ isAutomaticConfigurableSeat,
68
71
  isPublicCallableRole,
69
72
  isPublicCliSupportCommand,
70
73
  isPublicConfigurableSeat,
@@ -351,15 +354,15 @@ function requireLegalEngineName(name: string): string {
351
354
 
352
355
  /**
353
356
  * Persistent engine axis gate (#391 E1): PUBLIC_CALLABLE_ROLES only.
354
- * Navigator is a configurable model seat but has no independent activation path.
357
+ * Automatic seats are configurable for model but have no independent activation path.
355
358
  */
356
359
  function requireEngineAxisSeat(
357
360
  seat: string,
358
361
  verb: "set-engine" | "unset-engine",
359
362
  ): asserts seat is PublicCallableRole {
360
- if (seat === AUTOMATIC_NAVIGATOR_SEAT) {
363
+ if (isAutomaticConfigurableSeat(seat)) {
361
364
  throw new CliUsageError(
362
- `config ${verb} refuses navigator: no independent activation path; storing would be silently ineffective`,
365
+ `config ${verb} refuses ${seat}: no independent activation path; storing would be silently ineffective`,
363
366
  );
364
367
  }
365
368
  if (!isEngineAxisSeat(seat)) {
@@ -478,7 +481,7 @@ function renderHelp(): string {
478
481
  lines.push(
479
482
  "",
480
483
  "Role options: ak-role help <command>",
481
- "Persistent config: ak-role config set <seat> <provider/model[:thinking]>",
484
+ "Persistent config: ak-role config set <seat> <provider/model[:thinking]> | unset <gatekeeper|inspector|notary>",
482
485
  "Persistent engine (callable roles): ak-role config set-engine <seat> <name> | unset-engine <seat>",
483
486
  "Effective seats: ak-role roles",
484
487
  );
@@ -521,6 +524,15 @@ function renderRoles(seats: readonly EffectiveSeat[]): string {
521
524
  return `${lines.join("\n")}\n`;
522
525
  }
523
526
 
527
+ function renderPersistentSeatModel(selection: {
528
+ provider?: string;
529
+ model?: string;
530
+ thinking?: PublicThinkingLevel;
531
+ }): string {
532
+ const model = seatModelOnly(selection);
533
+ return model === undefined ? "-" : formatModelSpec(model);
534
+ }
535
+
524
536
  function renderConfig(config: PublicCliConfig): string {
525
537
  const lines: string[] = ["seat\tmodel\tengine"];
526
538
  const keys = Object.keys(config.seats) as (keyof typeof config.seats)[];
@@ -531,7 +543,7 @@ function renderConfig(config: PublicCliConfig): string {
531
543
  const selection = config.seats[seat];
532
544
  if (selection === undefined) continue;
533
545
  const engine = selection.engine === undefined ? "-" : selection.engine;
534
- lines.push(`${seat}\t${formatModelSpec(selection)}\t${engine}`);
546
+ lines.push(`${seat}\t${renderPersistentSeatModel(selection)}\t${engine}`);
535
547
  }
536
548
  }
537
549
  // #422: show the effective auto-resume ceiling (configured value or default).
@@ -557,7 +569,9 @@ async function runConfigCommand(
557
569
  } else {
558
570
  const engine =
559
571
  selection.engine === undefined ? "-" : selection.engine;
560
- io.stdout(`${args[1]}\t${formatModelSpec(selection)}\t${engine}\n`);
572
+ io.stdout(
573
+ `${args[1]}\t${renderPersistentSeatModel(selection)}\t${engine}\n`,
574
+ );
561
575
  }
562
576
  return 0;
563
577
  }
@@ -594,6 +608,28 @@ async function runConfigCommand(
594
608
  return 0;
595
609
  }
596
610
 
611
+ // #453: clear menxia officer model override only (engine axis preserved).
612
+ if (args[0] === "unset") {
613
+ if (args.length !== 2) {
614
+ throw new CliUsageError(
615
+ "usage: ak-role config unset <gatekeeper|inspector|notary>",
616
+ );
617
+ }
618
+ const seat = args[1]!;
619
+ if (!isMenxiaOfficerSeat(seat)) {
620
+ throw new CliUsageError(
621
+ `config unset serves menxia officer overrides only (gatekeeper|inspector|notary); got ${seat}`,
622
+ );
623
+ }
624
+ const config = clearPersistentSeatConfig(
625
+ await loadAndValidateConfig(home, packageRoot),
626
+ seat,
627
+ );
628
+ await savePublicCliConfig(config, home);
629
+ io.stdout(renderConfig(config));
630
+ return 0;
631
+ }
632
+
597
633
  if (args[0] === "set-engine") {
598
634
  if (args.length !== 3) {
599
635
  throw new CliUsageError(