@akagilnc/pi-workflow-roles 0.1.2086 → 0.1.2089

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.
@@ -14439,9 +14439,12 @@ function setPersistentSeatConfig(config, seat, selection) {
14439
14439
  }
14440
14440
  };
14441
14441
  }
14442
+ function isEngineAxisSeat(seat) {
14443
+ return seat === "judge" || seat === "reviewer";
14444
+ }
14442
14445
  function setPersistentSeatEngine(config, seat, engine) {
14443
- if (seat !== "judge") {
14444
- throw new Error(`engine axis is judge-only; refused seat ${seat}`);
14446
+ if (!isEngineAxisSeat(seat)) {
14447
+ throw new Error(`engine axis is judge+reviewer only; refused seat ${seat}`);
14445
14448
  }
14446
14449
  const previous = config.seats[seat];
14447
14450
  if (previous === void 0) {
@@ -14472,9 +14475,9 @@ function validatePublicCliConfigEngines(config, _packageRoot) {
14472
14475
  for (const seat of Object.keys(config.seats)) {
14473
14476
  const row = config.seats[seat];
14474
14477
  if (row?.engine === void 0) continue;
14475
- if (seat !== "judge") {
14478
+ if (!isEngineAxisSeat(seat)) {
14476
14479
  throw new Error(
14477
- `config seat ${seat} engine is not allowed; engine axis is judge-only`
14480
+ `config seat ${seat} engine is not allowed; engine axis is judge+reviewer only`
14478
14481
  );
14479
14482
  }
14480
14483
  try {
@@ -14609,13 +14612,13 @@ function pickStartupCandidate(seat, credentials) {
14609
14612
  return void 0;
14610
14613
  }
14611
14614
  function attachEngineAxis(seat, config, invocation) {
14612
- if (seat.seat !== "judge") {
14615
+ if (!isEngineAxisSeat(seat.seat)) {
14613
14616
  return {
14614
14617
  ...seat,
14615
14618
  engineSource: "unconfigured"
14616
14619
  };
14617
14620
  }
14618
- const persistentEngine = config.seats.judge?.engine;
14621
+ const persistentEngine = config.seats[seat.seat]?.engine;
14619
14622
  if (invocation?.engine !== void 0) {
14620
14623
  return {
14621
14624
  ...seat,
@@ -15917,8 +15920,8 @@ var init_option_definitions = __esm({
15917
15920
  repeatable: false,
15918
15921
  form: "option",
15919
15922
  description: {
15920
- en: "Optional Judge labor engine for this invocation (owner pool-directive name; packaged notes attached when present; judge-only).",
15921
- zh: "\u672C\u8C03\u7528\u53EF\u9009 Judge \u52B3\u52A8\u5F15\u64CE\uFF08\u6C60\u4EE4\u540D\u5B57\uFF1B\u6709\u5305\u5185\u8C03\u6CD5\u7B14\u8BB0\u5219\u9644\u5377\uFF1B\u4EC5 Judge\uFF09\u3002"
15923
+ en: "Optional Judge/Reviewer labor engine for this invocation (owner pool-directive name; packaged notes attached when present; judge+reviewer only).",
15924
+ zh: "\u672C\u8C03\u7528\u53EF\u9009 Judge/Reviewer \u52B3\u52A8\u5F15\u64CE\uFF08\u6C60\u4EE4\u540D\u5B57\uFF1B\u6709\u5305\u5185\u8C03\u6CD5\u7B14\u8BB0\u5219\u9644\u5377\uFF1B\u4EC5 Judge+Reviewer\uFF09\u3002"
15922
15925
  }
15923
15926
  },
15924
15927
  {
@@ -17494,11 +17497,12 @@ async function admitReviewerInvocation(options) {
17494
17497
  ...ticketFields
17495
17498
  };
17496
17499
  }
17497
- function buildReviewerTransportPrompt(admitted) {
17498
- return [
17500
+ function buildReviewerTransportPrompt(admitted, engineMaterial) {
17501
+ const lines = [
17499
17502
  `Base revision for the fixed review target: ${admitted.baseRevision}`,
17500
17503
  "Use this exact revision as the fixed review point."
17501
- ].join("\n");
17504
+ ];
17505
+ return appendEngineSessionMaterial(lines, engineMaterial).join("\n");
17502
17506
  }
17503
17507
  function parseMergerArgv(args) {
17504
17508
  const attachmentPaths = [];
@@ -19156,6 +19160,16 @@ var init_auditor_dossier_tool = __esm({
19156
19160
  }
19157
19161
  });
19158
19162
 
19163
+ // src/engine-detour.ts
19164
+ var ENGINE_DETOUR_TOOL_NAME, AK_ROLE_ENGINE_ENV;
19165
+ var init_engine_detour = __esm({
19166
+ "src/engine-detour.ts"() {
19167
+ "use strict";
19168
+ ENGINE_DETOUR_TOOL_NAME = "ak_engine_detour";
19169
+ AK_ROLE_ENGINE_ENV = "AK_ROLE_ENGINE";
19170
+ }
19171
+ });
19172
+
19159
19173
  // src/stream-idle-guard.ts
19160
19174
  var init_stream_idle_guard = __esm({
19161
19175
  "src/stream-idle-guard.ts"() {
@@ -19214,6 +19228,26 @@ var init_package_owned_tool_idle = __esm({
19214
19228
  }
19215
19229
  });
19216
19230
 
19231
+ // src/engine-detour-tool.ts
19232
+ var engineDetourArgsSchema;
19233
+ var init_engine_detour_tool = __esm({
19234
+ "src/engine-detour-tool.ts"() {
19235
+ "use strict";
19236
+ init_build();
19237
+ init_engine_detour();
19238
+ init_package_owned_tool_idle();
19239
+ engineDetourArgsSchema = typebox_exports.Object(
19240
+ {
19241
+ argv: typebox_exports.Array(typebox_exports.String({ minLength: 1 }), {
19242
+ minItems: 1,
19243
+ description: "Executable argv for one engine subprocess. First element is the command (PATH lookup); remaining elements are arguments. Build argv from the host CLI actual interface for the configured engine name; when optional packaged notes are present in the session prompt, follow those bytes. Do not invent package flags."
19244
+ })
19245
+ },
19246
+ { additionalProperties: false }
19247
+ );
19248
+ }
19249
+ });
19250
+
19217
19251
  // src/receipt-delivery-policy.ts
19218
19252
  function isRecord4(value) {
19219
19253
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -19249,6 +19283,9 @@ var init_evidence_child_executor = __esm({
19249
19283
  "src/evidence-child-executor.ts"() {
19250
19284
  "use strict";
19251
19285
  init_compliance_transport();
19286
+ init_engine_detour_tool();
19287
+ init_engine_detour();
19288
+ init_engine_material();
19252
19289
  init_package_owned_tool_idle();
19253
19290
  init_receipt_delivery_policy();
19254
19291
  init_reviewer_construction();
@@ -19403,16 +19440,6 @@ var init_collector_ledger = __esm({
19403
19440
  }
19404
19441
  });
19405
19442
 
19406
- // src/engine-detour.ts
19407
- var ENGINE_DETOUR_TOOL_NAME, AK_ROLE_ENGINE_ENV;
19408
- var init_engine_detour = __esm({
19409
- "src/engine-detour.ts"() {
19410
- "use strict";
19411
- ENGINE_DETOUR_TOOL_NAME = "ak_engine_detour";
19412
- AK_ROLE_ENGINE_ENV = "AK_ROLE_ENGINE";
19413
- }
19414
- });
19415
-
19416
19443
  // src/work-subject-identity.ts
19417
19444
  var init_work_subject_identity = __esm({
19418
19445
  "src/work-subject-identity.ts"() {
@@ -24360,7 +24387,10 @@ function buildReviewerTicketNumberArgs(ticketNumber) {
24360
24387
  return ticketNumber === void 0 ? [] : ["--ak-review-ticket-number", String(ticketNumber)];
24361
24388
  }
24362
24389
  function buildReviewerActivationExtraArgs(admitted, options) {
24363
- const prompt = buildReviewerTransportPrompt(admitted);
24390
+ const prompt = buildReviewerTransportPrompt(
24391
+ admitted,
24392
+ engineSessionMaterialFromOptions(options)
24393
+ );
24364
24394
  const skillPath = resolvePackagedMethodSkillPath(
24365
24395
  options.packageRoot,
24366
24396
  "code-review"
@@ -24468,7 +24498,7 @@ async function presentControlledFailure8(admitted, failureInput, io) {
24468
24498
  };
24469
24499
  }
24470
24500
  async function dispatchAdmittedReviewer(input) {
24471
- const { admitted, env, io, extraArgs, lease, methodMaterial } = input;
24501
+ const { admitted, env, io, extraArgs, lease, methodMaterial, effectiveEngine } = input;
24472
24502
  try {
24473
24503
  const missingCredential = missingCredentialPreDispatchFailure(
24474
24504
  env.model,
@@ -24481,7 +24511,7 @@ async function dispatchAdmittedReviewer(input) {
24481
24511
  io
24482
24512
  );
24483
24513
  }
24484
- await markRunRunning(admitted.runDirectory, env.model);
24514
+ await markRunRunning(admitted.runDirectory, env.model, effectiveEngine);
24485
24515
  await clearTypedProviderHttpObservation(admitted.runDirectory);
24486
24516
  await clearReviewerDispatchRejection(admitted.runDirectory);
24487
24517
  const childEnv = {
@@ -24490,6 +24520,12 @@ async function dispatchAdmittedReviewer(input) {
24490
24520
  PI_CODING_AGENT_DIR: env.agentDir,
24491
24521
  AK_ROLE_RUN_DIR: admitted.runDirectory
24492
24522
  };
24523
+ delete childEnv[AK_ROLE_ENGINE_ENV];
24524
+ if (env.engine !== void 0 && env.engine.trim() !== "") {
24525
+ childEnv[AK_ROLE_ENGINE_ENV] = env.engine.trim();
24526
+ } else {
24527
+ childEnv[AK_ROLE_ENGINE_ENV] = void 0;
24528
+ }
24493
24529
  const correlationId = admitted.correlationId ?? env.correlationId;
24494
24530
  if (correlationId !== void 0 && correlationId.trim() !== "") {
24495
24531
  childEnv.AK_CORRELATION_ID = correlationId;
@@ -24571,13 +24607,20 @@ async function dispatchAdmittedReviewer(input) {
24571
24607
  terminal: auditIncomplete
24572
24608
  };
24573
24609
  }
24610
+ const infrastructureFailure = await readEngineDetourInfrastructureFailure(
24611
+ admitted.sessionFile
24612
+ );
24574
24613
  const credentialFailure = postRunMissingCredentialFailure(
24575
24614
  result2,
24576
24615
  env.model,
24577
24616
  env.credentials
24578
24617
  );
24579
24618
  const resolution = await resolveAuditedRunnerFailureResolution({
24580
- runner: result2.knownFailure,
24619
+ runner: infrastructureFailure === void 0 ? result2.knownFailure : {
24620
+ cause: infrastructureFailure.cause,
24621
+ diagnostic: infrastructureFailure.diagnostic,
24622
+ ...infrastructureFailure.identity === void 0 ? {} : { identity: infrastructureFailure.identity }
24623
+ },
24581
24624
  sessionFile: admitted.sessionFile,
24582
24625
  credential: credentialFailure,
24583
24626
  runDirectory: admitted.runDirectory
@@ -24651,6 +24694,7 @@ async function runPublicReviewer(argv, env, io, parseReviewerArgv2) {
24651
24694
  const extraArgs = buildReviewerActivationExtraArgs(admitted, {
24652
24695
  packageRoot: env.packageRoot,
24653
24696
  ...env.model === void 0 ? {} : { model: env.model },
24697
+ ...env.engine === void 0 ? {} : { engine: env.engine },
24654
24698
  ...env.extraPiArgs === void 0 ? {} : { extraPiArgs: env.extraPiArgs }
24655
24699
  });
24656
24700
  return await dispatchAdmittedReviewer({
@@ -24662,7 +24706,9 @@ async function runPublicReviewer(argv, env, io, parseReviewerArgv2) {
24662
24706
  io,
24663
24707
  extraArgs,
24664
24708
  lease,
24665
- methodMaterial
24709
+ methodMaterial,
24710
+ // #378: only initial Reviewer dispatch records mechanical engine provenance.
24711
+ ...env.engine === void 0 ? {} : { effectiveEngine: env.engine }
24666
24712
  });
24667
24713
  }
24668
24714
  async function runPublicReviewerResume(argv, env, io) {
@@ -24738,6 +24784,8 @@ async function runPublicReviewerResume(argv, env, io) {
24738
24784
  var init_reviewer_run = __esm({
24739
24785
  "src/public-cli/reviewer-run.ts"() {
24740
24786
  "use strict";
24787
+ init_engine_detour();
24788
+ init_engine_material();
24741
24789
  init_method_skill();
24742
24790
  init_explicit_internal();
24743
24791
  init_cli_errors();
@@ -27025,7 +27073,7 @@ function renderHelp() {
27025
27073
  "",
27026
27074
  "Role options: ak-role help <command>",
27027
27075
  "Persistent config: ak-role config set <seat> <provider/model:thinking>",
27028
- "Persistent engine (judge only): ak-role config set-engine judge <name> | unset-engine judge",
27076
+ "Persistent engine (judge|reviewer): ak-role config set-engine <seat> <name> | unset-engine <seat>",
27029
27077
  "Effective seats: ak-role roles"
27030
27078
  );
27031
27079
  return `${lines.join("\n")}
@@ -27130,7 +27178,7 @@ async function runConfigCommand(args, home, packageRoot2, io) {
27130
27178
  if (args[0] === "set-engine") {
27131
27179
  if (args.length !== 3) {
27132
27180
  throw new CliUsageError(
27133
- "usage: ak-role config set-engine judge <name>"
27181
+ "usage: ak-role config set-engine <judge|reviewer> <name>"
27134
27182
  );
27135
27183
  }
27136
27184
  const seat = args[1];
@@ -27138,9 +27186,9 @@ async function runConfigCommand(args, home, packageRoot2, io) {
27138
27186
  if (!isPublicConfigurableSeat(seat)) {
27139
27187
  throw new CliUsageError(`unknown configurable seat: ${seat}`);
27140
27188
  }
27141
- if (seat !== "judge") {
27189
+ if (!isEngineAxisSeat(seat)) {
27142
27190
  throw new CliUsageError(
27143
- `engine axis is judge-only; refused seat ${seat}`
27191
+ `engine axis is judge+reviewer only; refused seat ${seat}`
27144
27192
  );
27145
27193
  }
27146
27194
  requireLegalEngineName(name);
@@ -27160,16 +27208,16 @@ async function runConfigCommand(args, home, packageRoot2, io) {
27160
27208
  if (args[0] === "unset-engine") {
27161
27209
  if (args.length !== 2) {
27162
27210
  throw new CliUsageError(
27163
- "usage: ak-role config unset-engine judge"
27211
+ "usage: ak-role config unset-engine <judge|reviewer>"
27164
27212
  );
27165
27213
  }
27166
27214
  const seat = args[1];
27167
27215
  if (!isPublicConfigurableSeat(seat)) {
27168
27216
  throw new CliUsageError(`unknown configurable seat: ${seat}`);
27169
27217
  }
27170
- if (seat !== "judge") {
27218
+ if (!isEngineAxisSeat(seat)) {
27171
27219
  throw new CliUsageError(
27172
- `engine axis is judge-only; refused seat ${seat}`
27220
+ `engine axis is judge+reviewer only; refused seat ${seat}`
27173
27221
  );
27174
27222
  }
27175
27223
  let config = await loadAndValidateConfig(home, packageRoot2);
@@ -27195,9 +27243,9 @@ async function runAkRole(argv, env) {
27195
27243
  const parsed = parseArgv(argv);
27196
27244
  if (parsed.engine !== void 0) {
27197
27245
  requireLegalEngineName(parsed.engine);
27198
- if (!parsed.help && parsed.command !== void 0 && parsed.command !== "help" && parsed.command !== "judge") {
27246
+ if (!parsed.help && parsed.command !== void 0 && parsed.command !== "help" && !isEngineAxisSeat(parsed.command)) {
27199
27247
  throw new CliUsageError(
27200
- `engine axis is judge-only; refused command ${parsed.command}`
27248
+ `engine axis is judge+reviewer only; refused command ${parsed.command}`
27201
27249
  );
27202
27250
  }
27203
27251
  }
@@ -27528,6 +27576,7 @@ async function runAkRole(argv, env) {
27528
27576
  ...env.correlationId === void 0 ? {} : { correlationId: env.correlationId },
27529
27577
  ...env.piRunner === void 0 ? {} : { piRunner: env.piRunner },
27530
27578
  ...seat.selection === void 0 ? {} : { model: seat.selection },
27579
+ ...seat.engine === void 0 ? {} : { engine: seat.engine },
27531
27580
  ...env.reviewerExtraPiArgs === void 0 ? {} : { extraPiArgs: env.reviewerExtraPiArgs },
27532
27581
  ...env.reviewerTimeoutMs === void 0 ? {} : { timeoutMs: env.reviewerTimeoutMs },
27533
27582
  ...env.createRunId === void 0 ? {} : { createRunId: env.createRunId }
@@ -73,6 +73,9 @@ export function createReviewerAgentRunner(dependencies = {}) {
73
73
  ...(dependencies.credentialScratchParent === undefined
74
74
  ? {}
75
75
  : { credentialScratchParent: dependencies.credentialScratchParent }),
76
+ ...(dependencies.packageRoot === undefined
77
+ ? {}
78
+ : { packageRoot: dependencies.packageRoot }),
76
79
  });
77
80
  const disposition = await workspaceOwner.dispose(workspace);
78
81
  return [leg.axis, Object.freeze({ status: "successful", report: child.report, usage: child.usage, target: batch.target, prompt: child.prompt, workspaceDisposition: disposition })];
@@ -242,7 +242,7 @@ export async function loadNavigatorWorkContext(
242
242
  }
243
243
 
244
244
  export default function roleRuntime(pi: ExtensionAPI): void {
245
- const reviewerAgent = createReviewerAgentRunner();
245
+ const reviewerAgent = createReviewerAgentRunner({ packageRoot });
246
246
  registerNavigatorModelCommand(pi);
247
247
  const navigatorSessionFactory = createNativeNavigatorSessionFactory();
248
248
  // #351: static provider list from extension setting (default ["kimi-coding"]).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akagilnc/pi-workflow-roles",
3
- "version": "0.1.2086",
3
+ "version": "0.1.2089",
4
4
  "description": "Soul-bound workflow roles for Pi",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -1,12 +1,13 @@
1
1
  /**
2
- * Package-owned engine detour tool (#357 T2).
3
- * Registered by shared role-runtime when Judge + engine activation signal is present.
4
- * Forbidden in judge-role.ts (lifecycle ban — no spawn in role modules).
2
+ * Package-owned engine detour tool (#357 T2 / #378).
3
+ * Registered by shared role-runtime when Judge|Reviewer + engine activation signal is present.
4
+ * Evidence-child legs install the same definition via customTools (no spawn in role modules).
5
5
  */
6
6
  import type {
7
7
  AgentToolResult,
8
8
  ExtensionAPI,
9
9
  ExtensionContext,
10
+ ToolDefinition,
10
11
  } from "@earendil-works/pi-coding-agent";
11
12
  import { Type, type Static } from "typebox";
12
13
 
@@ -18,6 +19,7 @@ import {
18
19
  isEngineDetourFailure,
19
20
  runEngineDetourOnce,
20
21
  } from "./engine-detour.ts";
22
+ import { wrapPackageOwnedToolDefinition } from "./package-owned-tool-idle.ts";
21
23
 
22
24
  const engineDetourArgsSchema = Type.Object(
23
25
  {
@@ -45,28 +47,21 @@ export type EngineDetourToolRegistration = {
45
47
  readonly registered: boolean;
46
48
  };
47
49
 
50
+ type EngineDetourLatch = { used: boolean };
51
+
48
52
  /**
49
- * Register the engine-generic detour tool once for this process when Judge has
50
- * an engine activation signal. Returns whether registration occurred.
51
- * Once-latch is activation-scoped via the returned reset handle.
53
+ * Build one once-latch detour tool definition for a configured engine name.
54
+ * `latch` is shared so parent registration can reset between activations.
55
+ * `fail` owns host abort (parent) vs throw (evidence child).
52
56
  */
53
- export function registerEngineDetourTool(
54
- pi: ExtensionAPI,
55
- hostActions: EngineDetourHostActions,
56
- ): EngineDetourToolRegistration & { resetLatch(): void } {
57
- const engineName = engineNameFromEnv();
58
- if (engineName === undefined) {
59
- return {
60
- registered: false,
61
- resetLatch() {
62
- /* no-op when unregistered */
63
- },
64
- };
65
- }
66
-
67
- let used = false;
68
-
69
- pi.registerTool({
57
+ export function createEngineDetourToolDefinition(input: {
58
+ engineName: string;
59
+ latch?: EngineDetourLatch;
60
+ fail: (error: Error, toolCallId: string, ctx: ExtensionContext) => never;
61
+ }): ToolDefinition {
62
+ const latch = input.latch ?? { used: false };
63
+ const engineName = input.engineName;
64
+ return wrapPackageOwnedToolDefinition({
70
65
  name: ENGINE_DETOUR_TOOL_NAME,
71
66
  label: "Engine Detour",
72
67
  description:
@@ -75,7 +70,7 @@ export function registerEngineDetourTool(
75
70
  promptGuidelines: [
76
71
  `Use ${ENGINE_DETOUR_TOOL_NAME} exactly once for the configured engine (${engineName}). Optional packaged notes are guidance when present; a bare engine name alone is also a valid call path.`,
77
72
  "Pass argv for the host CLI of this engine name — first element is the executable name on PATH. Follow optional packaged notes when delivered; otherwise act from the engine name and the host CLI actual interface. Do not invent package flags.",
78
- "On success, use the returned stdout as labor content for the existing typed submission tool.",
73
+ "On success, use the returned stdout as labor content for the existing typed submission / report path.",
79
74
  ],
80
75
  parameters: engineDetourArgsSchema,
81
76
  async execute(
@@ -85,22 +80,22 @@ export function registerEngineDetourTool(
85
80
  _onUpdate,
86
81
  ctx,
87
82
  ): Promise<AgentToolResult<unknown>> {
88
- if (used) {
89
- hostActions.failInfrastructure(
83
+ if (latch.used) {
84
+ input.fail(
90
85
  new Error(ENGINE_DETOUR_ALREADY_USED_DIAGNOSTIC),
91
- ctx,
92
86
  toolCallId,
87
+ ctx,
93
88
  );
94
89
  }
95
- used = true;
90
+ latch.used = true;
96
91
 
97
92
  const args = params as EngineDetourArgs;
98
93
  const argv = Array.isArray(args.argv) ? args.argv : [];
99
94
  if (argv.length === 0 || argv.some((part) => typeof part !== "string" || part.length === 0)) {
100
- hostActions.failInfrastructure(
95
+ input.fail(
101
96
  new Error("engine detour argv must be a non-empty string array"),
102
- ctx,
103
97
  toolCallId,
98
+ ctx,
104
99
  );
105
100
  }
106
101
 
@@ -112,14 +107,18 @@ export function registerEngineDetourTool(
112
107
  ...(signal === undefined ? {} : { signal }),
113
108
  });
114
109
  } catch (error) {
115
- hostActions.failInfrastructure(error, ctx, toolCallId);
110
+ input.fail(
111
+ error instanceof Error ? error : new Error(String(error)),
112
+ toolCallId,
113
+ ctx,
114
+ );
116
115
  }
117
116
 
118
117
  if (isEngineDetourFailure(result)) {
119
- hostActions.failInfrastructure(
118
+ input.fail(
120
119
  new Error(engineDetourFailureDiagnostic(result)),
121
- ctx,
122
120
  toolCallId,
121
+ ctx,
123
122
  );
124
123
  }
125
124
 
@@ -131,12 +130,42 @@ export function registerEngineDetourTool(
131
130
  },
132
131
  };
133
132
  },
133
+ }) as ToolDefinition;
134
+ }
135
+
136
+ /**
137
+ * Register the engine-generic detour tool once for this process when Judge/Reviewer has
138
+ * an engine activation signal. Returns whether registration occurred.
139
+ * Once-latch is activation-scoped via the returned reset handle.
140
+ */
141
+ export function registerEngineDetourTool(
142
+ pi: ExtensionAPI,
143
+ hostActions: EngineDetourHostActions,
144
+ ): EngineDetourToolRegistration & { resetLatch(): void } {
145
+ const engineName = engineNameFromEnv();
146
+ if (engineName === undefined) {
147
+ return {
148
+ registered: false,
149
+ resetLatch() {
150
+ /* no-op when unregistered */
151
+ },
152
+ };
153
+ }
154
+
155
+ const latch: EngineDetourLatch = { used: false };
156
+ const definition = createEngineDetourToolDefinition({
157
+ engineName,
158
+ latch,
159
+ fail(error, toolCallId, ctx) {
160
+ hostActions.failInfrastructure(error, ctx, toolCallId);
161
+ },
134
162
  });
163
+ pi.registerTool(definition);
135
164
 
136
165
  return {
137
166
  registered: true,
138
167
  resetLatch() {
139
- used = false;
168
+ latch.used = false;
140
169
  },
141
170
  };
142
171
  }
@@ -29,6 +29,13 @@ import {
29
29
  prepareComplianceDispatch,
30
30
  type AuditorParentAttemptBinding,
31
31
  } from "./compliance-transport.ts";
32
+ import { createEngineDetourToolDefinition } from "./engine-detour-tool.ts";
33
+ import { engineNameFromEnv } from "./engine-detour.ts";
34
+ import {
35
+ appendEngineSessionMaterial,
36
+ engineSessionMaterialFromOptions,
37
+ type EngineSessionMaterial,
38
+ } from "./package-resources/engine-material.ts";
32
39
  import { wrapPackageOwnedToolDefinition } from "./package-owned-tool-idle.ts";
33
40
  import { createReceiptDeliveryPolicy, NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, RECEIPT_DELIVERY_PROMPT, type NoReceiptLifecycleFacts } from "./receipt-delivery-policy.ts";
34
41
  import { REVIEWER_VERIFICATION_BOUNDARY } from "./reviewer-construction.ts";
@@ -41,13 +48,16 @@ import {
41
48
  } from "./upstream-error-testimony.ts";
42
49
 
43
50
  /** Package-owned system prompt for Reviewer Standards/Spec evidence children (private carrier). */
44
- function buildEvidenceChildSystemPrompt(): string {
45
- return [
51
+ function buildEvidenceChildSystemPrompt(
52
+ engineMaterial?: EngineSessionMaterial,
53
+ ): string {
54
+ const lines = [
46
55
  "Work only in the supplied workspace.",
47
56
  "Use the available evidence tools to investigate. Do not commit, push, or mutate remotes.",
48
57
  REVIEWER_VERIFICATION_BOUNDARY,
49
58
  "Return one substantive non-blank report.",
50
- ].join("\n");
59
+ ];
60
+ return appendEngineSessionMaterial(lines, engineMaterial).join("\n");
51
61
  }
52
62
 
53
63
  // ── shared constants / types ──────────────────────────────────────────────
@@ -540,6 +550,11 @@ export type EvidenceChildExecuteOptions = Readonly<{
540
550
  signal?: AbortSignal;
541
551
  /** Parent directory for credential/config scratch. Defaults to os.tmpdir(). */
542
552
  credentialScratchParent?: string;
553
+ /**
554
+ * Package root for optional engine method-material resolution (#378).
555
+ * Required only when AK_ROLE_ENGINE is set and packaged notes should attach.
556
+ */
557
+ packageRoot?: string;
543
558
  }>;
544
559
 
545
560
  export async function executeEvidenceChild(
@@ -568,6 +583,32 @@ export async function executeEvidenceChild(
568
583
  } catch (error) {
569
584
  throw classifiedError(error, "provider");
570
585
  }
586
+ // #378: when labor engine is configured, legs get the same detour tool + material
587
+ // dual-path as the parent seat (ADR 0069 detour-rejoins-main-road).
588
+ const engineName = engineNameFromEnv();
589
+ const engineMaterial =
590
+ engineName === undefined
591
+ ? undefined
592
+ : options.packageRoot === undefined || options.packageRoot.trim() === ""
593
+ // Name-only when package root is unavailable (still a valid #376 path).
594
+ ? Object.freeze({ name: engineName })
595
+ : engineSessionMaterialFromOptions({
596
+ engine: engineName,
597
+ packageRoot: options.packageRoot,
598
+ });
599
+ // #378: detour failure is durable for this leg — later assistant reports cannot wash it.
600
+ let engineDetourFailure: Error | undefined;
601
+ const engineDetourTool =
602
+ engineName === undefined
603
+ ? undefined
604
+ : createEngineDetourToolDefinition({
605
+ engineName,
606
+ fail(error) {
607
+ const failure = error instanceof Error ? error : new Error(String(error));
608
+ engineDetourFailure = failure;
609
+ throw failure;
610
+ },
611
+ });
571
612
  // No tools allowlist — Pi defaults + unrestricted evidence surface (ADR 0064).
572
613
  // Single createAgentSession owner: in-process-session.ts.
573
614
  const { session, dispose } = await openInProcessAgentSession({
@@ -576,7 +617,10 @@ export async function executeEvidenceChild(
576
617
  model: inherited.model,
577
618
  thinkingLevel: context.thinkingLevel ?? "off",
578
619
  modelRuntime: inherited.runtime,
579
- systemPrompt: buildEvidenceChildSystemPrompt(),
620
+ systemPrompt: buildEvidenceChildSystemPrompt(engineMaterial),
621
+ ...(engineDetourTool === undefined
622
+ ? {}
623
+ : { customTools: [engineDetourTool] }),
580
624
  sessionManager: createRecordSession({
581
625
  cwd: workspace,
582
626
  kind: "evidence-children",
@@ -598,8 +642,18 @@ export async function executeEvidenceChild(
598
642
  try {
599
643
  await session.prompt(delivered);
600
644
  } catch (error) {
645
+ // Engine detour fail becomes isError toolResult and must outrank a later
646
+ // provider-shaped throw from the same prompt turn (#378).
647
+ if (engineDetourFailure !== undefined) {
648
+ throw classifiedError(engineDetourFailure, "child");
649
+ }
601
650
  throw classifiedError(error, "provider");
602
651
  }
652
+ // Launched-leg detour non-zero / empty stdout / spawn failure is infrastructure:
653
+ // reject the leg even when the model still emits a non-blank report afterward.
654
+ if (engineDetourFailure !== undefined) {
655
+ throw classifiedError(engineDetourFailure, "child");
656
+ }
603
657
  if (signal?.aborted) throw new Error("Evidence child was cancelled");
604
658
  const lastAssistant = [...session.messages]
605
659
  .reverse()