@akagilnc/pi-workflow-roles 0.1.2107 → 0.1.2114

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.
@@ -4,6 +4,10 @@ import type {
4
4
  } from "@earendil-works/pi-coding-agent";
5
5
  import type { Static } from "typebox";
6
6
 
7
+ import {
8
+ readActivationEngineLaborFallbackField,
9
+ withEngineLaborFallbackField,
10
+ } from "./engine-labor-fallback.ts";
7
11
  import {
8
12
  COLLECTOR_FIXED_KICKOFF,
9
13
  emptyCollectorManifest,
@@ -446,12 +450,16 @@ export function createCollectorRoleRuntime(
446
450
  );
447
451
  activation.ledger.markOutputAccepted();
448
452
  activation.ledger.completeOperational(toolCallId);
453
+ const acceptedDetails = withEngineLaborFallbackField(
454
+ receipt,
455
+ readActivationEngineLaborFallbackField(),
456
+ );
449
457
  return {
450
458
  content: [{
451
459
  type: "text" as const,
452
460
  text: "Collector receipt accepted",
453
461
  }],
454
- details: receipt,
462
+ details: acceptedDetails,
455
463
  terminate: true as const,
456
464
  };
457
465
  } catch (error) {
@@ -2,6 +2,10 @@ import type { AgentToolResult, ExtensionAPI, ExtensionContext } from "@earendil-
2
2
  import { disposeComplianceDecision } from "./audit-escalation.ts";
3
3
  import { appendActiveSessionCustomEntry, type ComplianceDecision } from "./compliance-transport.ts";
4
4
  import { DOCTOR_CANDIDATE_ENTRY_TYPE } from "./dossier-resolution.ts";
5
+ import {
6
+ readActivationEngineLaborFallbackField,
7
+ withEngineLaborFallbackField,
8
+ } from "./engine-labor-fallback.ts";
5
9
  import { DOCTOR_EVIDENCE_TOOL_NAME, DOCTOR_OUTPUT_TOOL_DESCRIPTION, DOCTOR_OUTPUT_TOOL_NAME, DoctorEvidenceStore, doctorEvidenceReadSchema, doctorSubmissionSchema, validateDoctorOutput, type DoctorCase } from "./doctor-contracts.ts";
6
10
 
7
11
  export { DOCTOR_EVIDENCE_TOOL_NAME, DOCTOR_OUTPUT_TOOL_NAME };
@@ -12,7 +16,7 @@ export function createDoctorRoleRuntime(pi: ExtensionAPI, dependencies: DoctorRo
12
16
  let activation: { soul: string; patient: DoctorCase; store: DoctorEvidenceStore } | undefined; let registered = false; pi.registerFlag(DOCTOR_CASE_FLAG.name, DOCTOR_CASE_FLAG.definition);
13
17
  return { async activate() { const path = pi.getFlag(DOCTOR_CASE_FLAG.name); if (typeof path !== "string" || !path.trim()) throw new Error("Doctor requires --ak-doctor-case"); const soul = (await dependencies.loadSoul()).trim(); if (!soul) throw new Error("Doctor soul is empty"); const patient = await dependencies.loadCase(path); activation = { soul, patient, store: new DoctorEvidenceStore(patient) };
14
18
  if (!registered) { registered = true; pi.registerTool({ name: DOCTOR_EVIDENCE_TOOL_NAME, label: "Doctor Evidence", description: "Read retained Pi session bytes with bounded pagination.", parameters: doctorEvidenceReadSchema, async execute(_id, params: { evidenceId: string; offset?: number; limit?: number }) { if (!activation) throw new Error("Doctor is not activated"); const details = activation.store.read(params.evidenceId, params.offset, params.limit); return { content: [{ type: "text" as const, text: JSON.stringify(details) }], details }; } });
15
- pi.registerTool({ name: DOCTOR_OUTPUT_TOOL_NAME, label: "Doctor Output", description: DOCTOR_OUTPUT_TOOL_DESCRIPTION, parameters: doctorSubmissionSchema, async execute(id, params, signal, _update, ctx): Promise<AgentToolResult<unknown>> { if (!activation) throw new Error("Doctor is not activated"); singleton(id, ctx); const testimony = validateDoctorOutput(params, activation.patient, activation.store); try { appendActiveSessionCustomEntry(ctx, DOCTOR_CANDIDATE_ENTRY_TYPE, { version: 1, testimony, readRecord: activation.store.readRecord(), patientIdentity: activation.patient.identity }, { unavailable: "doctor candidate retention is unavailable", failed: "doctor candidate retention failed" }); } catch (error) { host.failInfrastructure(error, ctx, id); } let audit: ComplianceDecision; try { audit = await dependencies.auditCompliance(signal === undefined ? { context: ctx } : { context: ctx, signal }); } catch (error) { host.failInfrastructure(error, ctx, id); } const details = testimony.status === "completed" ? { ...testimony, cost: activation.patient.cost } : testimony; return disposeComplianceDecision<AgentToolResult<unknown>>(audit, { pass: (usage) => ({ content: [{ type: "text" as const, text: "Doctor output accepted" }], details, terminate: true as const, ...(usage === undefined ? {} : { usage }) }), noReceipt: (auditNoReceipt, usageProjection) => ({ content: [{ type: "text" as const, text: "Doctor output accepted; compliance audit produced no receipt" }], details: { ...details, auditNoReceipt }, terminate: true as const, ...usageProjection }), revise: (violations) => { throw new Error(`Doctor output violates its soul: ${violations.join("; ")}`); }, escalate: (result) => result, auditIncomplete: (result) => result }, details); } });
19
+ pi.registerTool({ name: DOCTOR_OUTPUT_TOOL_NAME, label: "Doctor Output", description: DOCTOR_OUTPUT_TOOL_DESCRIPTION, parameters: doctorSubmissionSchema, async execute(id, params, signal, _update, ctx): Promise<AgentToolResult<unknown>> { if (!activation) throw new Error("Doctor is not activated"); singleton(id, ctx); const testimony = validateDoctorOutput(params, activation.patient, activation.store); try { appendActiveSessionCustomEntry(ctx, DOCTOR_CANDIDATE_ENTRY_TYPE, { version: 1, testimony, readRecord: activation.store.readRecord(), patientIdentity: activation.patient.identity }, { unavailable: "doctor candidate retention is unavailable", failed: "doctor candidate retention failed" }); } catch (error) { host.failInfrastructure(error, ctx, id); } let audit: ComplianceDecision; try { audit = await dependencies.auditCompliance(signal === undefined ? { context: ctx } : { context: ctx, signal }); } catch (error) { host.failInfrastructure(error, ctx, id); } const details = testimony.status === "completed" ? { ...testimony, cost: activation.patient.cost } : testimony; const acceptedDetails = withEngineLaborFallbackField(details, readActivationEngineLaborFallbackField()); return disposeComplianceDecision<AgentToolResult<unknown>>(audit, { pass: (usage) => ({ content: [{ type: "text" as const, text: "Doctor output accepted" }], details: acceptedDetails, terminate: true as const, ...(usage === undefined ? {} : { usage }) }), noReceipt: (auditNoReceipt, usageProjection) => ({ content: [{ type: "text" as const, text: "Doctor output accepted; compliance audit produced no receipt" }], details: { ...acceptedDetails, auditNoReceipt }, terminate: true as const, ...usageProjection }), revise: (violations) => { throw new Error(`Doctor output violates its soul: ${violations.join("; ")}`); }, escalate: (result) => result, auditIncomplete: (result) => result }, acceptedDetails); } });
16
20
  pi.on("before_agent_start", (event) => { if (!activation) throw new Error("Doctor is not activated"); const catalog = { version: activation.patient.version, identity: activation.patient.identity, admittedMetrics: { provenance: "runtime-derived from retained session bytes and sealed into the accepted receipt", cost: activation.patient.cost }, lawfulTargetKeys: ["case", ...activation.patient.cost.invocations.sources], evidence: activation.patient.evidence.map(({ id, kind, sha256, byteLength, contentLength }) => ({ id, kind, sha256, byteLength, contentLength })) }; return { systemPrompt: `${event.systemPrompt}\n\n<doctor_soul>\n${activation.soul}\n</doctor_soul>\n\n<doctor_case>\n${JSON.stringify(catalog)}\n</doctor_case>` }; }); }
17
21
  const required = [DOCTOR_EVIDENCE_TOOL_NAME, DOCTOR_OUTPUT_TOOL_NAME]; const names = pi.getAllTools().map((tool) => tool.name); for (const name of required) if (names.filter((item) => item === name).length !== 1) throw new Error(`Doctor required tool collision or missing: ${name}`); pi.setActiveTools(required); const active = pi.getActiveTools?.() ?? required; if (active.length !== 2 || !required.every((name) => active.includes(name))) throw new Error("Doctor active tool narrowing failed"); } };
18
22
  }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Package-owned engine detour tool (#357 T2 / #378 / #380).
3
- * Registered by shared role-runtime when Judge|Reviewer + engine activation signal is present.
3
+ * Registered by shared role-runtime when any role + engine activation signal is present.
4
4
  * Evidence-child legs install the same definition via customTools (no spawn in role modules).
5
5
  * #380: engine process failure soft-returns so the seat rejoins the main road; declaration
6
6
  * is recorded once via engine-labor-fallback (no fail-closed reject-leg).
@@ -204,7 +204,7 @@ export function createEngineDetourToolDefinition(input: {
204
204
  }
205
205
 
206
206
  /**
207
- * Register the engine-generic detour tool once for this process when Judge/Reviewer has
207
+ * Register the engine-generic detour tool once for this process when any role has
208
208
  * an engine activation signal. Returns whether registration occurred.
209
209
  * Once-latch is activation-scoped via the returned reset handle.
210
210
  */
@@ -8,9 +8,25 @@ import { spawn } from "node:child_process";
8
8
  /** Package-owned detour tool name (settlement whitelist + session principal). */
9
9
  export const ENGINE_DETOUR_TOOL_NAME = "ak_engine_detour" as const;
10
10
 
11
- /** Env presence/name signal injected by public Judge run (registration gate only). */
11
+ /** Env presence/name signal injected by public role runs (registration gate only). */
12
12
  export const AK_ROLE_ENGINE_ENV = "AK_ROLE_ENGINE" as const;
13
13
 
14
+ /**
15
+ * Sole AK_ROLE_ENGINE write seam for public role child env (#391 E2).
16
+ * Delete ambient inheritance first; own-key undefined mask survives process.env re-merge.
17
+ */
18
+ export function applyEngineChildEnv(
19
+ childEnv: NodeJS.ProcessEnv,
20
+ engine?: string,
21
+ ): void {
22
+ delete childEnv[AK_ROLE_ENGINE_ENV];
23
+ if (engine !== undefined && engine.trim() !== "") {
24
+ childEnv[AK_ROLE_ENGINE_ENV] = engine.trim();
25
+ } else {
26
+ childEnv[AK_ROLE_ENGINE_ENV] = undefined;
27
+ }
28
+ }
29
+
14
30
  export const ENGINE_DETOUR_EMPTY_STDOUT_DIAGNOSTIC =
15
31
  "engine detour produced empty stdout" as const;
16
32
 
@@ -82,7 +82,7 @@ export function withEngineLaborFallbackField<T extends object>(
82
82
  return rest as T;
83
83
  }
84
84
 
85
- /** Install the activation-scoped latch (Judge/Reviewer session_start). */
85
+ /** Install the activation-scoped latch (any role session_start with engine). */
86
86
  export function installActivationEngineLaborFallbackLatch(
87
87
  latch: EngineLaborFallbackLatch,
88
88
  ): void {
@@ -1,4 +1,8 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import {
3
+ readActivationEngineLaborFallbackField,
4
+ withEngineLaborFallbackField,
5
+ } from "./engine-labor-fallback.ts";
2
6
  import { MERGER_ACCEPTED_TEXT, MERGER_OUTPUT_TOOL_NAME, mergerOutputSchema, validateMergerInput, validateMergerOutput, type MergerInput } from "./merger-contracts.ts";
3
7
  import type { MergerGitState } from "./merger-git-state.ts";
4
8
  import { exactUtf8 } from "./exact-utf8.ts";
@@ -47,7 +51,11 @@ export function createMergerRoleRuntime(pi: ExtensionAPI, dependencies: MergerRo
47
51
  if (state.mergeCommitId !== output.mergeCommitId || !same(state.parentObjectIds, [activation.input.targetObjectId, activation.input.sourceObjectId]) || state.unmergedPaths.length !== 0 || !state.worktreeClean || state.resolutionChangedPaths.some(path => !scope.has(path))) host.failInfrastructure(new Error("Merger completed-state verification failed"), ctx, id);
48
52
  }
49
53
  accepted = true;
50
- return { content: [{ type: "text" as const, text: MERGER_ACCEPTED_TEXT }], details: output, terminate: true as const };
54
+ const acceptedDetails = withEngineLaborFallbackField(
55
+ output,
56
+ readActivationEngineLaborFallbackField(),
57
+ );
58
+ return { content: [{ type: "text" as const, text: MERGER_ACCEPTED_TEXT }], details: acceptedDetails, terminate: true as const };
51
59
  } });
52
60
  pi.on("before_agent_start", event => { if (!activation) throw new Error("Merger is not activated"); const admitted = { attemptId: activation.input.attemptId, targetObjectId: activation.input.targetObjectId, sourceObjectId: activation.input.sourceObjectId, task: materialText(activation.input, "task"), authority: materialText(activation.input, "authority"), targetIntent: materialText(activation.input, "targetIntent"), sourceIntent: materialText(activation.input, "sourceIntent"), expectedConflictPaths: activation.input.expectedConflictPaths, resolutionScope: activation.input.resolutionScope, authorizedChecks: activation.input.authorizedChecks }; return { systemPrompt: `${event.systemPrompt}\n\n<merger_soul>\n${activation.soul}\n</merger_soul>\n\n<merger_assignment>\n${JSON.stringify(admitted)}\n</merger_assignment>` }; });
53
61
  }
@@ -11,12 +11,12 @@ import {
11
11
  import {
12
12
  effectiveSeatConfigurations,
13
13
  formatModelSpec,
14
+ isEngineAxisSeat,
14
15
  loadCredentialProviders,
15
16
  loadPublicCliConfig,
16
17
  parseModelSpec,
17
18
  resolveEffectiveSeat,
18
19
  savePublicCliConfig,
19
- isEngineAxisSeat,
20
20
  setPersistentSeatConfig,
21
21
  setPersistentSeatEngine,
22
22
  validatePublicCliConfigEngines,
@@ -58,10 +58,13 @@ import { runPublicReviewer, runPublicReviewerResume } from "./reviewer-run.ts";
58
58
  import { runPublicTaishi } from "./taishi-run.ts";
59
59
  import { peekRoleRunRole } from "./run-lifecycle.ts";
60
60
  import {
61
+ AUTOMATIC_NAVIGATOR_SEAT,
61
62
  INTERNAL_ROLE_ENTRYPOINT_RELATIVE,
63
+ isPublicCallableRole,
62
64
  isPublicCliSupportCommand,
63
65
  isPublicConfigurableSeat,
64
66
  listHelpCapabilities,
67
+ type PublicCallableRole,
65
68
  type PublicThinkingLevel,
66
69
  } from "./registry.ts";
67
70
  import {
@@ -336,6 +339,31 @@ function requireLegalEngineName(name: string): string {
336
339
  }
337
340
  }
338
341
 
342
+ /**
343
+ * Persistent engine axis gate (#391 E1): PUBLIC_CALLABLE_ROLES only.
344
+ * Navigator is a configurable model seat but has no independent activation path.
345
+ */
346
+ function requireEngineAxisSeat(
347
+ seat: string,
348
+ verb: "set-engine" | "unset-engine",
349
+ ): asserts seat is PublicCallableRole {
350
+ if (seat === AUTOMATIC_NAVIGATOR_SEAT) {
351
+ throw new CliUsageError(
352
+ `config ${verb} refuses navigator: no independent activation path; storing would be silently ineffective`,
353
+ );
354
+ }
355
+ if (!isEngineAxisSeat(seat)) {
356
+ throw new CliUsageError(`unknown engine-axis seat: ${seat}`);
357
+ }
358
+ }
359
+
360
+ /** Single seat.engine → run-options projection (#391 E2). */
361
+ function projectSeatEngine(
362
+ seat: Readonly<{ engine?: string }>,
363
+ ): { engine: string } | Record<PropertyKey, never> {
364
+ return seat.engine === undefined ? {} : { engine: seat.engine };
365
+ }
366
+
339
367
  function loadAndValidateConfig(
340
368
  home: string,
341
369
  packageRoot: string,
@@ -419,7 +447,7 @@ function renderHelp(): string {
419
447
  "",
420
448
  "Role options: ak-role help <command>",
421
449
  "Persistent config: ak-role config set <seat> <provider/model[:thinking]>",
422
- "Persistent engine (judge|reviewer): ak-role config set-engine <seat> <name> | unset-engine <seat>",
450
+ "Persistent engine (callable roles): ak-role config set-engine <seat> <name> | unset-engine <seat>",
423
451
  "Effective seats: ak-role roles",
424
452
  );
425
453
  return `${lines.join("\n")}\n`;
@@ -536,19 +564,12 @@ async function runConfigCommand(
536
564
  if (args[0] === "set-engine") {
537
565
  if (args.length !== 3) {
538
566
  throw new CliUsageError(
539
- "usage: ak-role config set-engine <judge|reviewer> <name>",
567
+ "usage: ak-role config set-engine <seat> <name>",
540
568
  );
541
569
  }
542
570
  const seat = args[1]!;
543
571
  const name = args[2]!;
544
- if (!isPublicConfigurableSeat(seat)) {
545
- throw new CliUsageError(`unknown configurable seat: ${seat}`);
546
- }
547
- if (!isEngineAxisSeat(seat)) {
548
- throw new CliUsageError(
549
- `engine axis is judge+reviewer only; refused seat ${seat}`,
550
- );
551
- }
572
+ requireEngineAxisSeat(seat, "set-engine");
552
573
  requireLegalEngineName(name);
553
574
  let config = await loadAndValidateConfig(home, packageRoot);
554
575
  try {
@@ -567,18 +588,11 @@ async function runConfigCommand(
567
588
  if (args[0] === "unset-engine") {
568
589
  if (args.length !== 2) {
569
590
  throw new CliUsageError(
570
- "usage: ak-role config unset-engine <judge|reviewer>",
591
+ "usage: ak-role config unset-engine <seat>",
571
592
  );
572
593
  }
573
594
  const seat = args[1]!;
574
- if (!isPublicConfigurableSeat(seat)) {
575
- throw new CliUsageError(`unknown configurable seat: ${seat}`);
576
- }
577
- if (!isEngineAxisSeat(seat)) {
578
- throw new CliUsageError(
579
- `engine axis is judge+reviewer only; refused seat ${seat}`,
580
- );
581
- }
595
+ requireEngineAxisSeat(seat, "unset-engine");
582
596
  let config = await loadAndValidateConfig(home, packageRoot);
583
597
  try {
584
598
  config = setPersistentSeatEngine(config, seat, undefined);
@@ -609,17 +623,17 @@ export async function runAkRole(
609
623
  env = { ...env, packageRoot: await realpath(env.packageRoot) };
610
624
  const parsed = parseArgv(argv);
611
625
  // Invocation --engine rejects at the call-request seam (not role submission).
612
- // #356 / #378: engine axis is Judge+Reviewer (not resume / other seats).
626
+ // #356 / #378 / #391: engine axis is every callable role (not resume / support).
613
627
  if (parsed.engine !== undefined) {
614
628
  requireLegalEngineName(parsed.engine);
615
629
  if (
616
630
  !parsed.help &&
617
631
  parsed.command !== undefined &&
618
632
  parsed.command !== "help" &&
619
- !isEngineAxisSeat(parsed.command)
633
+ !isPublicCallableRole(parsed.command)
620
634
  ) {
621
635
  throw new CliUsageError(
622
- `engine axis is judge+reviewer only; refused command ${parsed.command}`,
636
+ `engine axis is role commands only; refused command ${parsed.command}`,
623
637
  );
624
638
  }
625
639
  }
@@ -876,7 +890,7 @@ export async function runAkRole(
876
890
  : { correlationId: env.correlationId }),
877
891
  ...(env.piRunner === undefined ? {} : { piRunner: env.piRunner }),
878
892
  ...(seat.selection === undefined ? {} : { model: seat.selection }),
879
- ...(seat.engine === undefined ? {} : { engine: seat.engine }),
893
+ ...projectSeatEngine(seat),
880
894
  ...(env.judgeExtraPiArgs === undefined
881
895
  ? {}
882
896
  : { extraPiArgs: env.judgeExtraPiArgs }),
@@ -920,6 +934,7 @@ export async function runAkRole(
920
934
  : { correlationId: env.correlationId }),
921
935
  ...(env.piRunner === undefined ? {} : { piRunner: env.piRunner }),
922
936
  ...(seat.selection === undefined ? {} : { model: seat.selection }),
937
+ ...projectSeatEngine(seat),
923
938
  ...(env.coderExtraPiArgs === undefined
924
939
  ? {}
925
940
  : { extraPiArgs: env.coderExtraPiArgs }),
@@ -963,6 +978,7 @@ export async function runAkRole(
963
978
  : { correlationId: env.correlationId }),
964
979
  ...(env.piRunner === undefined ? {} : { piRunner: env.piRunner }),
965
980
  ...(seat.selection === undefined ? {} : { model: seat.selection }),
981
+ ...projectSeatEngine(seat),
966
982
  ...(env.fixerExtraPiArgs === undefined
967
983
  ? {}
968
984
  : { extraPiArgs: env.fixerExtraPiArgs }),
@@ -1006,6 +1022,7 @@ export async function runAkRole(
1006
1022
  : { correlationId: env.correlationId }),
1007
1023
  ...(env.piRunner === undefined ? {} : { piRunner: env.piRunner }),
1008
1024
  ...(seat.selection === undefined ? {} : { model: seat.selection }),
1025
+ ...projectSeatEngine(seat),
1009
1026
  ...(env.collectorExtraPiArgs === undefined
1010
1027
  ? {}
1011
1028
  : { extraPiArgs: env.collectorExtraPiArgs }),
@@ -1049,7 +1066,7 @@ export async function runAkRole(
1049
1066
  : { correlationId: env.correlationId }),
1050
1067
  ...(env.piRunner === undefined ? {} : { piRunner: env.piRunner }),
1051
1068
  ...(seat.selection === undefined ? {} : { model: seat.selection }),
1052
- ...(seat.engine === undefined ? {} : { engine: seat.engine }),
1069
+ ...projectSeatEngine(seat),
1053
1070
  ...(env.reviewerExtraPiArgs === undefined
1054
1071
  ? {}
1055
1072
  : { extraPiArgs: env.reviewerExtraPiArgs }),
@@ -1093,6 +1110,7 @@ export async function runAkRole(
1093
1110
  : { correlationId: env.correlationId }),
1094
1111
  ...(env.piRunner === undefined ? {} : { piRunner: env.piRunner }),
1095
1112
  ...(seat.selection === undefined ? {} : { model: seat.selection }),
1113
+ ...projectSeatEngine(seat),
1096
1114
  ...(env.doctorExtraPiArgs === undefined
1097
1115
  ? {}
1098
1116
  : { extraPiArgs: env.doctorExtraPiArgs }),
@@ -1136,6 +1154,7 @@ export async function runAkRole(
1136
1154
  : { correlationId: env.correlationId }),
1137
1155
  ...(env.piRunner === undefined ? {} : { piRunner: env.piRunner }),
1138
1156
  ...(seat.selection === undefined ? {} : { model: seat.selection }),
1157
+ ...projectSeatEngine(seat),
1139
1158
  ...(env.mergerExtraPiArgs === undefined
1140
1159
  ? {}
1141
1160
  : { extraPiArgs: env.mergerExtraPiArgs }),
@@ -6,6 +6,8 @@
6
6
  import { writeFile } from "node:fs/promises";
7
7
  import { join } from "node:path";
8
8
 
9
+ import { applyEngineChildEnv } from "../engine-detour.ts";
10
+ import { engineSessionMaterialFromOptions } from "../package-resources/engine-material.ts";
9
11
  import {
10
12
  loadPackagedMethodSkillMaterial,
11
13
  resolvePackagedMethodSkillPath,
@@ -79,6 +81,8 @@ export type CoderRunEnv = {
79
81
  correlationId?: string;
80
82
  piRunner?: ExplicitInternalPiRunner;
81
83
  model?: SeatModelConfig;
84
+ /** Optional labor engine name (config→activation; session material + env signal). */
85
+ engine?: string;
82
86
  credentials?: CredentialProviders;
83
87
  createRunId?: () => string;
84
88
  extraPiArgs?: readonly string[];
@@ -96,10 +100,14 @@ export function buildCoderActivationExtraArgs(
96
100
  options: {
97
101
  packageRoot: string;
98
102
  model?: SeatModelConfig;
103
+ engine?: string;
99
104
  extraPiArgs?: readonly string[];
100
105
  },
101
106
  ): string[] {
102
- const prompt = buildCoderTransportPrompt(admitted);
107
+ const prompt = buildCoderTransportPrompt(
108
+ admitted,
109
+ engineSessionMaterialFromOptions(options),
110
+ );
103
111
  const skillArgs =
104
112
  admitted.phase === "apply"
105
113
  ? [
@@ -276,12 +284,14 @@ async function dispatchAdmittedCoder(input: {
276
284
  extraArgs: string[];
277
285
  lease: RunWriterLease;
278
286
  methodProvenance?: PackagedMethodSkillProvenance;
287
+ /** Mechanical engine provenance for initial Coder dispatch only. */
288
+ effectiveEngine?: string;
279
289
  }): Promise<{
280
290
  exitCode: number;
281
291
  admitted: AdmittedCoderInvocation;
282
292
  terminal?: TerminalResult;
283
293
  }> {
284
- const { admitted, env, io, extraArgs, lease, methodProvenance } = input;
294
+ const { admitted, env, io, extraArgs, lease, methodProvenance, effectiveEngine } = input;
285
295
  try {
286
296
  const missingCredential = missingCredentialPreDispatchFailure(
287
297
  env.model,
@@ -294,7 +304,7 @@ async function dispatchAdmittedCoder(input: {
294
304
  io,
295
305
  );
296
306
  }
297
- await markRunRunning(admitted.runDirectory, env.model);
307
+ await markRunRunning(admitted.runDirectory, env.model, effectiveEngine);
298
308
  await clearTypedProviderHttpObservation(admitted.runDirectory);
299
309
 
300
310
  const childEnv: NodeJS.ProcessEnv = {
@@ -303,6 +313,7 @@ async function dispatchAdmittedCoder(input: {
303
313
  PI_CODING_AGENT_DIR: env.agentDir,
304
314
  AK_ROLE_RUN_DIR: admitted.runDirectory,
305
315
  };
316
+ applyEngineChildEnv(childEnv, env.engine);
306
317
  const correlationId = admitted.correlationId ?? env.correlationId;
307
318
  if (correlationId !== undefined && correlationId.trim() !== "") {
308
319
  childEnv.AK_CORRELATION_ID = correlationId;
@@ -474,6 +485,7 @@ export async function runPublicCoder(
474
485
  const extraArgs = buildCoderActivationExtraArgs(admitted, {
475
486
  packageRoot: env.packageRoot,
476
487
  ...(env.model === undefined ? {} : { model: env.model }),
488
+ ...(env.engine === undefined ? {} : { engine: env.engine }),
477
489
  ...(env.extraPiArgs === undefined ? {} : { extraPiArgs: env.extraPiArgs }),
478
490
  });
479
491
 
@@ -487,6 +499,7 @@ export async function runPublicCoder(
487
499
  extraArgs,
488
500
  lease,
489
501
  ...(methodProvenance === undefined ? {} : { methodProvenance }),
502
+ ...(env.engine === undefined ? {} : { effectiveEngine: env.engine }),
490
503
  });
491
504
  }
492
505
 
@@ -6,6 +6,8 @@
6
6
  import { writeFile } from "node:fs/promises";
7
7
  import { join } from "node:path";
8
8
 
9
+ import { applyEngineChildEnv } from "../engine-detour.ts";
10
+ import { engineSessionMaterialFromOptions } from "../package-resources/engine-material.ts";
9
11
  import {
10
12
  runExplicitInternalActivation,
11
13
  type ExplicitInternalKnownFailure,
@@ -64,6 +66,8 @@ export type CollectorRunEnv = {
64
66
  correlationId?: string;
65
67
  piRunner?: ExplicitInternalPiRunner;
66
68
  model?: SeatModelConfig;
69
+ /** Optional labor engine name (config→activation; session material + env signal). */
70
+ engine?: string;
67
71
  credentials?: CredentialProviders;
68
72
  createRunId?: () => string;
69
73
  extraPiArgs?: readonly string[];
@@ -79,10 +83,15 @@ export function buildCollectorActivationExtraArgs(
79
83
  admitted: AdmittedCollectorInvocation,
80
84
  options: {
81
85
  model?: SeatModelConfig;
86
+ engine?: string;
87
+ packageRoot?: string;
82
88
  extraPiArgs?: readonly string[];
83
89
  } = {},
84
90
  ): string[] {
85
- const prompt = buildCollectorTransportPrompt(admitted);
91
+ const prompt = buildCollectorTransportPrompt(
92
+ admitted,
93
+ engineSessionMaterialFromOptions(options),
94
+ );
86
95
  return [
87
96
  "--no-skills",
88
97
  "--no-prompt-templates",
@@ -174,12 +183,13 @@ async function dispatchAdmittedCollector(input: {
174
183
  io: CliIo;
175
184
  extraArgs: string[];
176
185
  lease: RunWriterLease;
186
+ effectiveEngine?: string;
177
187
  }): Promise<{
178
188
  exitCode: number;
179
189
  admitted: AdmittedCollectorInvocation;
180
190
  terminal?: TerminalResult;
181
191
  }> {
182
- const { admitted, env, io, extraArgs, lease } = input;
192
+ const { admitted, env, io, extraArgs, lease, effectiveEngine } = input;
183
193
  try {
184
194
  const missingCredential = missingCredentialPreDispatchFailure(
185
195
  env.model,
@@ -192,7 +202,7 @@ async function dispatchAdmittedCollector(input: {
192
202
  io,
193
203
  );
194
204
  }
195
- await markRunRunning(admitted.runDirectory, env.model);
205
+ await markRunRunning(admitted.runDirectory, env.model, effectiveEngine);
196
206
  await clearTypedProviderHttpObservation(admitted.runDirectory);
197
207
 
198
208
  const childEnv: NodeJS.ProcessEnv = {
@@ -201,6 +211,7 @@ async function dispatchAdmittedCollector(input: {
201
211
  PI_CODING_AGENT_DIR: env.agentDir,
202
212
  AK_ROLE_RUN_DIR: admitted.runDirectory,
203
213
  };
214
+ applyEngineChildEnv(childEnv, env.engine);
204
215
  const correlationId = admitted.correlationId ?? env.correlationId;
205
216
  if (correlationId !== undefined && correlationId.trim() !== "") {
206
217
  childEnv.AK_CORRELATION_ID = correlationId;
@@ -354,7 +365,9 @@ export async function runPublicCollector(
354
365
  }
355
366
 
356
367
  const extraArgs = buildCollectorActivationExtraArgs(admitted, {
368
+ packageRoot: env.packageRoot,
357
369
  ...(env.model === undefined ? {} : { model: env.model }),
370
+ ...(env.engine === undefined ? {} : { engine: env.engine }),
358
371
  ...(env.extraPiArgs === undefined ? {} : { extraPiArgs: env.extraPiArgs }),
359
372
  });
360
373
 
@@ -367,5 +380,6 @@ export async function runPublicCollector(
367
380
  io,
368
381
  extraArgs,
369
382
  lease,
383
+ ...(env.engine === undefined ? {} : { effectiveEngine: env.engine }),
370
384
  });
371
385
  }
@@ -10,7 +10,10 @@ import {
10
10
  AUTOMATIC_NAVIGATOR_SEAT,
11
11
  PUBLIC_CALLABLE_ROLES,
12
12
  PUBLIC_CONFIGURABLE_SEATS,
13
+ isPublicCallableRole,
14
+ isPublicConfigurableSeat,
13
15
  type ModelRef,
16
+ type PublicCallableRole,
14
17
  type PublicConfigurableSeat,
15
18
  type PublicThinkingLevel,
16
19
  publicStartupCandidates,
@@ -116,23 +119,24 @@ export function setPersistentSeatConfig(
116
119
  };
117
120
  }
118
121
 
119
- /** Seats that own the labor-engine axis (#356 Judge; #378 Reviewer). */
120
- export function isEngineAxisSeat(seat: string): seat is "judge" | "reviewer" {
121
- return seat === "judge" || seat === "reviewer";
122
+ /**
123
+ * Seats that own the labor-engine axis (#391: PUBLIC_CALLABLE_ROLES only).
124
+ * Navigator is configurable for model but has no independent activation path.
125
+ */
126
+ export function isEngineAxisSeat(seat: string): seat is PublicCallableRole {
127
+ return isPublicCallableRole(seat);
122
128
  }
123
129
 
124
130
  /**
125
- * Set or clear persistent engine on Judge or Reviewer (#356 / #378).
131
+ * Set or clear persistent engine on a callable role seat (#356 / #378 / #391).
126
132
  * Engine-only seats are rejected — provider/model[:thinking] remains required first.
133
+ * Seat type is PublicCallableRole (navigator excluded at the type boundary).
127
134
  */
128
135
  export function setPersistentSeatEngine(
129
136
  config: PublicCliConfig,
130
- seat: PublicConfigurableSeat,
137
+ seat: PublicCallableRole,
131
138
  engine: string | undefined,
132
139
  ): PublicCliConfig {
133
- if (!isEngineAxisSeat(seat)) {
134
- throw new Error(`engine axis is judge+reviewer only; refused seat ${seat}`);
135
- }
136
140
  const previous = config.seats[seat];
137
141
  if (previous === undefined) {
138
142
  throw new Error(
@@ -166,9 +170,10 @@ export function seatModelOnly(seat: PersistentSeatConfig): SeatModelConfig {
166
170
  }
167
171
 
168
172
  /**
169
- * Config-parse seam: engine axis is Judge+Reviewer; engine names need only
170
- * path-safety syntax (no closed material catalog; #376 / #378 / ADR 0069).
171
- * Call with packageRoot after load / before dispatch (#356).
173
+ * Config-parse seam: engine axis is PUBLIC_CALLABLE_ROLES; engine names need only
174
+ * path-safety syntax (no closed material catalog; #376 / #378 / #391 / ADR 0069).
175
+ * Disk-handwritten navigator.engine is rejected (no independent activation → silent
176
+ * ineffective would violate failure honesty). Call after load / before dispatch (#356).
172
177
  * Syntax authority = assertLegalEngineName (no injected duplicate).
173
178
  */
174
179
  export function validatePublicCliConfigEngines(
@@ -180,7 +185,7 @@ export function validatePublicCliConfigEngines(
180
185
  if (row?.engine === undefined) continue;
181
186
  if (!isEngineAxisSeat(seat)) {
182
187
  throw new Error(
183
- `config seat ${seat} engine is not allowed; engine axis is judge+reviewer only`,
188
+ `config seat ${seat} cannot persist engine: no independent activation path; storing would be silently ineffective`,
184
189
  );
185
190
  }
186
191
  try {
@@ -356,7 +361,7 @@ function attachEngineAxis(
356
361
  config: PublicCliConfig,
357
362
  invocation?: InvocationModelOverride,
358
363
  ): EffectiveSeat {
359
- // #356 / #378: engine axis is Judge+Reviewer. Other seats stay unconfigured.
364
+ // #391: engine axis is PUBLIC_CALLABLE_ROLES only (single isEngineAxisSeat predicate).
360
365
  if (!isEngineAxisSeat(seat.seat)) {
361
366
  return {
362
367
  ...seat,
@@ -6,6 +6,8 @@
6
6
  import { writeFile } from "node:fs/promises";
7
7
  import { join } from "node:path";
8
8
 
9
+ import { applyEngineChildEnv } from "../engine-detour.ts";
10
+ import { engineSessionMaterialFromOptions } from "../package-resources/engine-material.ts";
9
11
  import {
10
12
  runExplicitInternalActivation,
11
13
  type ExplicitInternalKnownFailure,
@@ -65,6 +67,8 @@ export type DoctorRunEnv = {
65
67
  correlationId?: string;
66
68
  piRunner?: ExplicitInternalPiRunner;
67
69
  model?: SeatModelConfig;
70
+ /** Optional labor engine name (config→activation; session material + env signal). */
71
+ engine?: string;
68
72
  credentials?: CredentialProviders;
69
73
  createRunId?: () => string;
70
74
  extraPiArgs?: readonly string[];
@@ -81,10 +85,15 @@ export function buildDoctorActivationExtraArgs(
81
85
  admitted: AdmittedDoctorInvocation,
82
86
  options: {
83
87
  model?: SeatModelConfig;
88
+ engine?: string;
89
+ packageRoot?: string;
84
90
  extraPiArgs?: readonly string[];
85
91
  } = {},
86
92
  ): string[] {
87
- const prompt = buildDoctorTransportPrompt(admitted);
93
+ const prompt = buildDoctorTransportPrompt(
94
+ admitted,
95
+ engineSessionMaterialFromOptions(options),
96
+ );
88
97
  return [
89
98
  "--no-skills",
90
99
  "--no-prompt-templates",
@@ -155,12 +164,13 @@ async function dispatchAdmittedDoctor(input: {
155
164
  io: CliIo;
156
165
  extraArgs: string[];
157
166
  lease: RunWriterLease;
167
+ effectiveEngine?: string;
158
168
  }): Promise<{
159
169
  exitCode: number;
160
170
  admitted: AdmittedDoctorInvocation;
161
171
  terminal?: TerminalResult;
162
172
  }> {
163
- const { admitted, env, io, extraArgs, lease } = input;
173
+ const { admitted, env, io, extraArgs, lease, effectiveEngine } = input;
164
174
  try {
165
175
  const missingCredential = missingCredentialPreDispatchFailure(
166
176
  env.model,
@@ -173,7 +183,7 @@ async function dispatchAdmittedDoctor(input: {
173
183
  io,
174
184
  );
175
185
  }
176
- await markRunRunning(admitted.runDirectory, env.model);
186
+ await markRunRunning(admitted.runDirectory, env.model, effectiveEngine);
177
187
  await clearTypedProviderHttpObservation(admitted.runDirectory);
178
188
 
179
189
  const childEnv: NodeJS.ProcessEnv = {
@@ -182,6 +192,7 @@ async function dispatchAdmittedDoctor(input: {
182
192
  PI_CODING_AGENT_DIR: env.agentDir,
183
193
  AK_ROLE_RUN_DIR: admitted.runDirectory,
184
194
  };
195
+ applyEngineChildEnv(childEnv, env.engine);
185
196
  if (env.correlationId !== undefined && env.correlationId.trim() !== "") {
186
197
  childEnv.AK_CORRELATION_ID = env.correlationId;
187
198
  }
@@ -333,7 +344,9 @@ export async function runPublicDoctor(
333
344
  }
334
345
 
335
346
  const extraArgs = buildDoctorActivationExtraArgs(admitted, {
347
+ packageRoot: env.packageRoot,
336
348
  ...(env.model === undefined ? {} : { model: env.model }),
349
+ ...(env.engine === undefined ? {} : { engine: env.engine }),
337
350
  ...(env.extraPiArgs === undefined ? {} : { extraPiArgs: env.extraPiArgs }),
338
351
  });
339
352
 
@@ -343,5 +356,6 @@ export async function runPublicDoctor(
343
356
  io,
344
357
  extraArgs,
345
358
  lease,
359
+ ...(env.engine === undefined ? {} : { effectiveEngine: env.engine }),
346
360
  });
347
361
  }