@akagilnc/pi-workflow-roles 0.1.2054 → 0.1.2058

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.
@@ -19417,6 +19417,16 @@ var init_collector_ledger = __esm({
19417
19417
  }
19418
19418
  });
19419
19419
 
19420
+ // src/engine-detour.ts
19421
+ var ENGINE_DETOUR_TOOL_NAME, AK_ROLE_ENGINE_ENV;
19422
+ var init_engine_detour = __esm({
19423
+ "src/engine-detour.ts"() {
19424
+ "use strict";
19425
+ ENGINE_DETOUR_TOOL_NAME = "ak_engine_detour";
19426
+ AK_ROLE_ENGINE_ENV = "AK_ROLE_ENGINE";
19427
+ }
19428
+ });
19429
+
19420
19430
  // src/work-subject-identity.ts
19421
19431
  import { resolve as resolve6 } from "node:path";
19422
19432
  function issueRoot(value) {
@@ -20570,34 +20580,46 @@ function boundErroredToolCandidate(entries, resultIndex, message, toolName) {
20570
20580
  const diagnostic = toolResultText(message);
20571
20581
  return bound === void 0 || diagnostic === "" ? void 0 : { candidate: bound.candidate, diagnostic, callIndex: bound.callIndex };
20572
20582
  }
20573
- function extractCollectorInfrastructureFailure(entries) {
20583
+ function extractInfrastructureToolFailure(entries, spec) {
20574
20584
  for (let i = entries.length - 1; i >= 0; i -= 1) {
20575
20585
  const entry = entries[i];
20576
20586
  if (entry?.type !== "message") continue;
20577
20587
  const message = entry.message;
20578
20588
  if (message?.role !== "toolResult") continue;
20579
20589
  if (message.isError !== true) continue;
20580
- if (typeof message.toolName !== "string" || !COLLECTOR_INFRASTRUCTURE_TOOLS.has(message.toolName)) {
20590
+ if (typeof message.toolName !== "string" || !spec.matchTool(message.toolName)) {
20581
20591
  continue;
20582
20592
  }
20583
20593
  const diagnostic = toolResultText(message);
20584
20594
  if (diagnostic.length === 0) continue;
20585
20595
  return {
20586
- cause: "activation",
20596
+ cause: spec.cause,
20587
20597
  diagnostic,
20588
- identity: { name: "CollectorInfrastructureError" }
20598
+ identity: { name: spec.identityName }
20589
20599
  };
20590
20600
  }
20591
20601
  return void 0;
20592
20602
  }
20593
- async function readCollectorInfrastructureFailure(sessionFile) {
20603
+ async function readInfrastructureToolFailure(sessionFile, spec) {
20594
20604
  try {
20595
20605
  const entries = await readBoundSessionEntries(sessionFile);
20596
- return extractCollectorInfrastructureFailure(entries);
20606
+ return extractInfrastructureToolFailure(entries, spec);
20597
20607
  } catch {
20598
20608
  return void 0;
20599
20609
  }
20600
20610
  }
20611
+ async function readCollectorInfrastructureFailure(sessionFile) {
20612
+ return readInfrastructureToolFailure(
20613
+ sessionFile,
20614
+ COLLECTOR_INFRASTRUCTURE_FAILURE_SPEC
20615
+ );
20616
+ }
20617
+ async function readEngineDetourInfrastructureFailure(sessionFile) {
20618
+ return readInfrastructureToolFailure(
20619
+ sessionFile,
20620
+ ENGINE_DETOUR_INFRASTRUCTURE_FAILURE_SPEC
20621
+ );
20622
+ }
20601
20623
  function assertCollectorReceiptMatchesAdmitted(receipt, admitted) {
20602
20624
  if (receipt.repository !== admitted.repository.canonical) {
20603
20625
  throw collectorReceiptBindingFailure(
@@ -22393,7 +22415,7 @@ function presentFailureTerminal(terminal, io) {
22393
22415
  }));
22394
22416
  }
22395
22417
  }
22396
- var CONCISE_DIAGNOSTIC_MAX_CHARS, COLLECTOR_INFRASTRUCTURE_TOOLS;
22418
+ var CONCISE_DIAGNOSTIC_MAX_CHARS, COLLECTOR_INFRASTRUCTURE_TOOLS, COLLECTOR_INFRASTRUCTURE_FAILURE_SPEC, ENGINE_DETOUR_INFRASTRUCTURE_FAILURE_SPEC;
22397
22419
  var init_settlement = __esm({
22398
22420
  "src/public-cli/settlement.ts"() {
22399
22421
  "use strict";
@@ -22406,6 +22428,7 @@ var init_settlement = __esm({
22406
22428
  init_run_lifecycle();
22407
22429
  init_compliance_transport();
22408
22430
  init_collector_ledger();
22431
+ init_engine_detour();
22409
22432
  init_judge_output();
22410
22433
  init_collector_output();
22411
22434
  init_worker_output();
@@ -22426,6 +22449,16 @@ var init_settlement = __esm({
22426
22449
  COLLECTOR_REQUEST_TOOL,
22427
22450
  COLLECTOR_WAIT_TOOL
22428
22451
  ]);
22452
+ COLLECTOR_INFRASTRUCTURE_FAILURE_SPEC = {
22453
+ matchTool: (toolName) => COLLECTOR_INFRASTRUCTURE_TOOLS.has(toolName),
22454
+ cause: "activation",
22455
+ identityName: "CollectorInfrastructureError"
22456
+ };
22457
+ ENGINE_DETOUR_INFRASTRUCTURE_FAILURE_SPEC = {
22458
+ matchTool: (toolName) => toolName === ENGINE_DETOUR_TOOL_NAME,
22459
+ cause: "output",
22460
+ identityName: "EngineDetourInfrastructureError"
22461
+ };
22429
22462
  }
22430
22463
  });
22431
22464
 
@@ -23776,6 +23809,12 @@ async function dispatchAdmittedJudge(input) {
23776
23809
  // and role-runtime can record typed provider HTTP observations.
23777
23810
  AK_ROLE_RUN_DIR: admitted.runDirectory
23778
23811
  };
23812
+ delete childEnv[AK_ROLE_ENGINE_ENV];
23813
+ if (env.engine !== void 0 && env.engine.trim() !== "") {
23814
+ childEnv[AK_ROLE_ENGINE_ENV] = env.engine.trim();
23815
+ } else {
23816
+ childEnv[AK_ROLE_ENGINE_ENV] = void 0;
23817
+ }
23779
23818
  const correlationId = admitted.correlationId ?? env.correlationId;
23780
23819
  if (correlationId !== void 0 && correlationId.trim() !== "") {
23781
23820
  childEnv.AK_CORRELATION_ID = correlationId;
@@ -23850,13 +23889,20 @@ async function dispatchAdmittedJudge(input) {
23850
23889
  terminal: auditIncomplete
23851
23890
  };
23852
23891
  }
23892
+ const infrastructureFailure = await readEngineDetourInfrastructureFailure(
23893
+ admitted.sessionFile
23894
+ );
23853
23895
  const credentialFailure = postRunMissingCredentialFailure(
23854
23896
  result2,
23855
23897
  env.model,
23856
23898
  env.credentials
23857
23899
  );
23858
23900
  const resolution = await resolveAuditedRunnerFailureResolution({
23859
- runner: result2.knownFailure,
23901
+ runner: result2.knownFailure ?? (infrastructureFailure === void 0 ? void 0 : {
23902
+ cause: infrastructureFailure.cause,
23903
+ diagnostic: infrastructureFailure.diagnostic,
23904
+ ...infrastructureFailure.identity === void 0 ? {} : { identity: infrastructureFailure.identity }
23905
+ }),
23860
23906
  sessionFile: admitted.sessionFile,
23861
23907
  credential: credentialFailure,
23862
23908
  runDirectory: admitted.runDirectory
@@ -23980,6 +24026,7 @@ async function runPublicResume(argv, env, io) {
23980
24026
  var init_judge_run = __esm({
23981
24027
  "src/public-cli/judge-run.ts"() {
23982
24028
  "use strict";
24029
+ init_engine_detour();
23983
24030
  init_engine_material();
23984
24031
  init_explicit_internal();
23985
24032
  init_cli_errors();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akagilnc/pi-workflow-roles",
3
- "version": "0.1.2054",
3
+ "version": "0.1.2058",
4
4
  "description": "Soul-bound workflow roles for Pi",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -29,3 +29,7 @@ kimi --yolo --output-format text -p "YOUR_LABOR_PROMPT"
29
29
 
30
30
  Prefer `kimi --help` on the host over any remembered flag set. Do not wrap this
31
31
  engine behind `ak-role` flags.
32
+
33
+ When the package detour tool is available, start exactly one subprocess through
34
+ it with argv assembled from this material and the local CLI; return the stdout
35
+ labor content to the same session for the existing typed submission path.
@@ -0,0 +1,142 @@
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).
5
+ */
6
+ import type {
7
+ AgentToolResult,
8
+ ExtensionAPI,
9
+ ExtensionContext,
10
+ } from "@earendil-works/pi-coding-agent";
11
+ import { Type, type Static } from "typebox";
12
+
13
+ import {
14
+ ENGINE_DETOUR_ALREADY_USED_DIAGNOSTIC,
15
+ ENGINE_DETOUR_TOOL_NAME,
16
+ engineDetourFailureDiagnostic,
17
+ engineNameFromEnv,
18
+ isEngineDetourFailure,
19
+ runEngineDetourOnce,
20
+ } from "./engine-detour.ts";
21
+
22
+ const engineDetourArgsSchema = Type.Object(
23
+ {
24
+ argv: Type.Array(Type.String({ minLength: 1 }), {
25
+ minItems: 1,
26
+ description:
27
+ "Executable argv for one engine subprocess. First element is the command (PATH lookup); remaining elements are arguments. Assemble from engine method material — do not invent package flags.",
28
+ }),
29
+ },
30
+ { additionalProperties: false },
31
+ );
32
+
33
+ type EngineDetourArgs = Static<typeof engineDetourArgsSchema>;
34
+
35
+ export type EngineDetourHostActions = {
36
+ failInfrastructure(
37
+ error: unknown,
38
+ ctx: ExtensionContext,
39
+ toolCallId?: string,
40
+ ): never;
41
+ };
42
+
43
+ export type EngineDetourToolRegistration = {
44
+ /** True when the tool definition was installed on this ExtensionAPI. */
45
+ readonly registered: boolean;
46
+ };
47
+
48
+ /**
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.
52
+ */
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({
70
+ name: ENGINE_DETOUR_TOOL_NAME,
71
+ label: "Engine Detour",
72
+ description:
73
+ `Run one labor-engine subprocess (engine=${engineName}) and return its stdout to this session. Call at most once per activation. Assemble argv from the engine method material path delivered in the session prompt.`,
74
+ promptSnippet: "Run the configured labor engine once and return its stdout",
75
+ promptGuidelines: [
76
+ `Use ${ENGINE_DETOUR_TOOL_NAME} exactly once when engine method material is present.`,
77
+ "Pass argv assembled from the material and the host CLI — first element is the executable name on PATH.",
78
+ "On success, use the returned stdout as labor content for the existing typed submission tool.",
79
+ ],
80
+ parameters: engineDetourArgsSchema,
81
+ async execute(
82
+ toolCallId,
83
+ params,
84
+ signal,
85
+ _onUpdate,
86
+ ctx,
87
+ ): Promise<AgentToolResult<unknown>> {
88
+ if (used) {
89
+ hostActions.failInfrastructure(
90
+ new Error(ENGINE_DETOUR_ALREADY_USED_DIAGNOSTIC),
91
+ ctx,
92
+ toolCallId,
93
+ );
94
+ }
95
+ used = true;
96
+
97
+ const args = params as EngineDetourArgs;
98
+ const argv = Array.isArray(args.argv) ? args.argv : [];
99
+ if (argv.length === 0 || argv.some((part) => typeof part !== "string" || part.length === 0)) {
100
+ hostActions.failInfrastructure(
101
+ new Error("engine detour argv must be a non-empty string array"),
102
+ ctx,
103
+ toolCallId,
104
+ );
105
+ }
106
+
107
+ let result: Awaited<ReturnType<typeof runEngineDetourOnce>>;
108
+ try {
109
+ result = await runEngineDetourOnce({
110
+ argv,
111
+ cwd: ctx.cwd,
112
+ ...(signal === undefined ? {} : { signal }),
113
+ });
114
+ } catch (error) {
115
+ hostActions.failInfrastructure(error, ctx, toolCallId);
116
+ }
117
+
118
+ if (isEngineDetourFailure(result)) {
119
+ hostActions.failInfrastructure(
120
+ new Error(engineDetourFailureDiagnostic(result)),
121
+ ctx,
122
+ toolCallId,
123
+ );
124
+ }
125
+
126
+ return {
127
+ content: [{ type: "text" as const, text: result.stdout }],
128
+ details: {
129
+ tool: ENGINE_DETOUR_TOOL_NAME,
130
+ code: result.code,
131
+ },
132
+ };
133
+ },
134
+ });
135
+
136
+ return {
137
+ registered: true,
138
+ resetLatch() {
139
+ used = false;
140
+ },
141
+ };
142
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Engine-generic one-shot subprocess detour (#357 T2 / ADR 0069).
3
+ * Spawn once; no retry, hang surface, or per-engine branch.
4
+ * Material body is LLM data — this module only executes argv the model assembled.
5
+ */
6
+ import { spawn } from "node:child_process";
7
+
8
+ /** Package-owned detour tool name (settlement whitelist + session principal). */
9
+ export const ENGINE_DETOUR_TOOL_NAME = "ak_engine_detour" as const;
10
+
11
+ /** Env presence/name signal injected by public Judge run (registration gate only). */
12
+ export const AK_ROLE_ENGINE_ENV = "AK_ROLE_ENGINE" as const;
13
+
14
+ export const ENGINE_DETOUR_EMPTY_STDOUT_DIAGNOSTIC =
15
+ "engine detour produced empty stdout" as const;
16
+
17
+ export const ENGINE_DETOUR_ALREADY_USED_DIAGNOSTIC =
18
+ "engine detour already used in this activation" as const;
19
+
20
+ export type EngineDetourResult = Readonly<{
21
+ code: number;
22
+ stdout: string;
23
+ stderr: string;
24
+ }>;
25
+
26
+ export type EngineDetourRunInput = Readonly<{
27
+ argv: readonly string[];
28
+ cwd: string;
29
+ env?: NodeJS.ProcessEnv;
30
+ signal?: AbortSignal;
31
+ }>;
32
+
33
+ /**
34
+ * Run one engine subprocess. First argv element is the executable (PATH lookup).
35
+ * stdio: ignore stdin, pipe stdout+stderr. No shell, no retry, no hang timer.
36
+ */
37
+ export async function runEngineDetourOnce(
38
+ input: EngineDetourRunInput,
39
+ ): Promise<EngineDetourResult> {
40
+ if (input.argv.length === 0) {
41
+ throw new Error("engine detour argv must be non-empty");
42
+ }
43
+ const command = input.argv[0]!;
44
+ const args = input.argv.slice(1);
45
+ return await new Promise<EngineDetourResult>((resolve, reject) => {
46
+ let settled = false;
47
+ const child = spawn(command, args, {
48
+ cwd: input.cwd,
49
+ env: input.env ?? process.env,
50
+ stdio: ["ignore", "pipe", "pipe"],
51
+ ...(input.signal === undefined ? {} : { signal: input.signal }),
52
+ });
53
+ let stdout = "";
54
+ let stderr = "";
55
+ child.stdout.setEncoding("utf8").on("data", (chunk: string) => {
56
+ stdout += chunk;
57
+ });
58
+ child.stderr.setEncoding("utf8").on("data", (chunk: string) => {
59
+ stderr += chunk;
60
+ });
61
+ const fail = (error: unknown): void => {
62
+ if (settled) return;
63
+ settled = true;
64
+ reject(error instanceof Error ? error : new Error(String(error)));
65
+ };
66
+ child.on("error", (error) => fail(error));
67
+ child.on("close", (code) => {
68
+ if (settled) return;
69
+ settled = true;
70
+ resolve({ code: code ?? 1, stdout, stderr });
71
+ });
72
+ });
73
+ }
74
+
75
+ /** Failure predicate: nonzero exit OR stdout trim-empty (including whitespace-only). */
76
+ export function isEngineDetourFailure(result: {
77
+ code: number;
78
+ stdout: string;
79
+ }): boolean {
80
+ return result.code !== 0 || result.stdout.trim() === "";
81
+ }
82
+
83
+ /**
84
+ * Diagnostic string for shared settlement / Terminal Error Artifact.
85
+ * Prefer engine stderr 原样; whitespace-only/empty stderr is absent → stable fallback.
86
+ */
87
+ export function engineDetourFailureDiagnostic(result: {
88
+ stderr: string;
89
+ code: number;
90
+ stdout: string;
91
+ }): string {
92
+ if (result.stderr.trim().length > 0) return result.stderr;
93
+ if (result.stdout.trim() === "") return ENGINE_DETOUR_EMPTY_STDOUT_DIAGNOSTIC;
94
+ return `engine detour exited with code ${result.code}`;
95
+ }
96
+
97
+ /** Non-empty trimmed engine name from process.env, else undefined. */
98
+ export function engineNameFromEnv(): string | undefined {
99
+ const raw = process.env[AK_ROLE_ENGINE_ENV];
100
+ if (typeof raw !== "string") return undefined;
101
+ const trimmed = raw.trim();
102
+ return trimmed === "" ? undefined : trimmed;
103
+ }
@@ -6,6 +6,7 @@
6
6
  import { writeFile } from "node:fs/promises";
7
7
  import { join } from "node:path";
8
8
 
9
+ import { AK_ROLE_ENGINE_ENV } from "../engine-detour.ts";
9
10
  import { engineSessionMaterialFromOptions } from "../package-resources/engine-material.ts";
10
11
  import {
11
12
  runExplicitInternalActivation,
@@ -58,6 +59,7 @@ import {
58
59
  resolveControlledFailureResumeObservation,
59
60
  controlledFailureInputFromResolution,
60
61
  explicitInternalKnownFailureClassificationInput,
62
+ readEngineDetourInfrastructureFailure,
61
63
  settleJudgeFailureTerminalResult,
62
64
  trySettleJudgeTerminalResult,
63
65
  trySettleComplianceAuditIncompleteTerminalResult,
@@ -296,6 +298,14 @@ async function dispatchAdmittedJudge(input: {
296
298
  // and role-runtime can record typed provider HTTP observations.
297
299
  AK_ROLE_RUN_DIR: admitted.runDirectory,
298
300
  };
301
+ // Engine presence/name signal: registration gate + label only (no per-engine branch).
302
+ // Delete ambient inheritance first; own-key undefined mask survives process.env re-merge.
303
+ delete childEnv[AK_ROLE_ENGINE_ENV];
304
+ if (env.engine !== undefined && env.engine.trim() !== "") {
305
+ childEnv[AK_ROLE_ENGINE_ENV] = env.engine.trim();
306
+ } else {
307
+ childEnv[AK_ROLE_ENGINE_ENV] = undefined;
308
+ }
299
309
  const correlationId = admitted.correlationId ?? env.correlationId;
300
310
  if (correlationId !== undefined && correlationId.trim() !== "") {
301
311
  childEnv.AK_CORRELATION_ID = correlationId;
@@ -377,6 +387,11 @@ async function dispatchAdmittedJudge(input: {
377
387
  };
378
388
  }
379
389
 
390
+ // Prefer engine-detour infrastructure failure already on the session principal
391
+ // over a later secondary provider-stop after abort (#357 T2 / collector-isomorphic).
392
+ const infrastructureFailure = await readEngineDetourInfrastructureFailure(
393
+ admitted.sessionFile,
394
+ );
380
395
  // Production-owned typed cause channel — never inferred from stderr wording.
381
396
  const credentialFailure = postRunMissingCredentialFailure(
382
397
  result,
@@ -384,7 +399,17 @@ async function dispatchAdmittedJudge(input: {
384
399
  env.credentials,
385
400
  );
386
401
  const resolution = await resolveAuditedRunnerFailureResolution({
387
- runner: result.knownFailure,
402
+ runner:
403
+ result.knownFailure ??
404
+ (infrastructureFailure === undefined
405
+ ? undefined
406
+ : {
407
+ cause: infrastructureFailure.cause,
408
+ diagnostic: infrastructureFailure.diagnostic,
409
+ ...(infrastructureFailure.identity === undefined
410
+ ? {}
411
+ : { identity: infrastructureFailure.identity }),
412
+ }),
388
413
  sessionFile: admitted.sessionFile,
389
414
  credential: credentialFailure,
390
415
  runDirectory: admitted.runDirectory,
@@ -33,6 +33,7 @@ import {
33
33
  COLLECTOR_REQUEST_TOOL,
34
34
  COLLECTOR_WAIT_TOOL,
35
35
  } from "../collector-ledger.ts";
36
+ import { ENGINE_DETOUR_TOOL_NAME } from "../engine-detour.ts";
36
37
  import {
37
38
  JUDGE_OUTPUT_TOOL_NAME,
38
39
  type JudgeVerdict,
@@ -1348,13 +1349,33 @@ const COLLECTOR_INFRASTRUCTURE_TOOLS = new Set<string>([
1348
1349
  COLLECTOR_WAIT_TOOL,
1349
1350
  ]);
1350
1351
 
1352
+ /** Shared match/cause/identity knobs for session-principal infrastructure failures. */
1353
+ type InfrastructureFailureSpec = Readonly<{
1354
+ matchTool: (toolName: string) => boolean;
1355
+ cause: ControlledFailureCause;
1356
+ identityName: string;
1357
+ }>;
1358
+
1359
+ const COLLECTOR_INFRASTRUCTURE_FAILURE_SPEC: InfrastructureFailureSpec = {
1360
+ matchTool: (toolName) => COLLECTOR_INFRASTRUCTURE_TOOLS.has(toolName),
1361
+ cause: "activation",
1362
+ identityName: "CollectorInfrastructureError",
1363
+ };
1364
+
1365
+ const ENGINE_DETOUR_INFRASTRUCTURE_FAILURE_SPEC: InfrastructureFailureSpec = {
1366
+ matchTool: (toolName) => toolName === ENGINE_DETOUR_TOOL_NAME,
1367
+ cause: "output",
1368
+ identityName: "EngineDetourInfrastructureError",
1369
+ };
1370
+
1351
1371
  /**
1352
- * Prefer a real Collector infrastructure tool failure already on the session
1353
- * principal over a later secondary provider-stop (failure-honesty).
1354
- * Observe/request/wait host failures keep their diagnostic identity (e.g. HTTP 404).
1372
+ * Prefer a real infrastructure tool failure already on the session principal
1373
+ * over a later secondary provider-stop (failure-honesty).
1374
+ * Tool match + cause + identity are call-site parameters — one extraction body.
1355
1375
  */
1356
- export function extractCollectorInfrastructureFailure(
1376
+ function extractInfrastructureToolFailure(
1357
1377
  entries: readonly SessionEntry[],
1378
+ spec: InfrastructureFailureSpec,
1358
1379
  ): ControlledFailure | undefined {
1359
1380
  for (let i = entries.length - 1; i >= 0; i -= 1) {
1360
1381
  const entry = entries[i];
@@ -1364,33 +1385,82 @@ export function extractCollectorInfrastructureFailure(
1364
1385
  if (message.isError !== true) continue;
1365
1386
  if (
1366
1387
  typeof message.toolName !== "string" ||
1367
- !COLLECTOR_INFRASTRUCTURE_TOOLS.has(message.toolName)
1388
+ !spec.matchTool(message.toolName)
1368
1389
  ) {
1369
1390
  continue;
1370
1391
  }
1371
1392
  const diagnostic = toolResultText(message);
1372
1393
  if (diagnostic.length === 0) continue;
1373
1394
  return {
1374
- cause: "activation",
1395
+ cause: spec.cause,
1375
1396
  diagnostic,
1376
- identity: { name: "CollectorInfrastructureError" },
1397
+ identity: { name: spec.identityName },
1377
1398
  };
1378
1399
  }
1379
1400
  return undefined;
1380
1401
  }
1381
1402
 
1382
- /** Read the bound session principal for a Collector infrastructure tool failure. */
1383
- export async function readCollectorInfrastructureFailure(
1403
+ /** Read the bound session principal for a parameterized infrastructure tool failure. */
1404
+ async function readInfrastructureToolFailure(
1384
1405
  sessionFile: string,
1406
+ spec: InfrastructureFailureSpec,
1385
1407
  ): Promise<ControlledFailure | undefined> {
1386
1408
  try {
1387
1409
  const entries = await readBoundSessionEntries(sessionFile);
1388
- return extractCollectorInfrastructureFailure(entries);
1410
+ return extractInfrastructureToolFailure(entries, spec);
1389
1411
  } catch {
1390
1412
  return undefined;
1391
1413
  }
1392
1414
  }
1393
1415
 
1416
+ /**
1417
+ * Prefer a real Collector infrastructure tool failure already on the session
1418
+ * principal over a later secondary provider-stop (failure-honesty).
1419
+ * Observe/request/wait host failures keep their diagnostic identity (e.g. HTTP 404).
1420
+ */
1421
+ export function extractCollectorInfrastructureFailure(
1422
+ entries: readonly SessionEntry[],
1423
+ ): ControlledFailure | undefined {
1424
+ return extractInfrastructureToolFailure(
1425
+ entries,
1426
+ COLLECTOR_INFRASTRUCTURE_FAILURE_SPEC,
1427
+ );
1428
+ }
1429
+
1430
+ /** Read the bound session principal for a Collector infrastructure tool failure. */
1431
+ export async function readCollectorInfrastructureFailure(
1432
+ sessionFile: string,
1433
+ ): Promise<ControlledFailure | undefined> {
1434
+ return readInfrastructureToolFailure(
1435
+ sessionFile,
1436
+ COLLECTOR_INFRASTRUCTURE_FAILURE_SPEC,
1437
+ );
1438
+ }
1439
+
1440
+ /**
1441
+ * Prefer a real engine-detour infrastructure tool failure already on the session
1442
+ * principal over a later secondary provider-stop (failure-honesty / #357 T2).
1443
+ * Cause stays `output` — labor leg failed before accepted typed Receipt.
1444
+ */
1445
+ export function extractEngineDetourInfrastructureFailure(
1446
+ entries: readonly SessionEntry[],
1447
+ ): ControlledFailure | undefined {
1448
+ return extractInfrastructureToolFailure(
1449
+ entries,
1450
+ ENGINE_DETOUR_INFRASTRUCTURE_FAILURE_SPEC,
1451
+ );
1452
+ }
1453
+
1454
+ /** Read the bound session principal for an engine-detour infrastructure failure. */
1455
+ export async function readEngineDetourInfrastructureFailure(
1456
+ sessionFile: string,
1457
+ ): Promise<ControlledFailure | undefined> {
1458
+ return readInfrastructureToolFailure(
1459
+ sessionFile,
1460
+ ENGINE_DETOUR_INFRASTRUCTURE_FAILURE_SPEC,
1461
+ );
1462
+ }
1463
+
1394
1464
  /**
1395
1465
  * Compare a validated receipt with the admitted Collector invocation identity.
1396
1466
  * Throws a typed output failure when any identity field mismatches.
@@ -21,6 +21,7 @@ import {
21
21
  writeToolExecutionObservationRecord,
22
22
  type ToolExecutionObservationWriter,
23
23
  } from "./tool-execution-observation.ts";
24
+ import { registerEngineDetourTool } from "./engine-detour-tool.ts";
24
25
  import { installPackageOwnedToolRegistration } from "./package-owned-tool-idle.ts";
25
26
  import { installWorkerGitHooks } from "./worker-submission-gates.ts";
26
27
  import { createReceiptDeliveryPolicy, NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, RECEIPT_DELIVERY_PROMPT } from "./receipt-delivery-policy.ts";
@@ -284,6 +285,7 @@ export {
284
285
  type JudgeVerdict,
285
286
  type SoulAuditResult,
286
287
  } from "./judge-role.ts";
288
+ export { ENGINE_DETOUR_TOOL_NAME, AK_ROLE_ENGINE_ENV } from "./engine-detour.ts";
287
289
  export {
288
290
  AGENT_TOOL_NAME,
289
291
  REVIEWER_OUTPUT_TOOL_NAME,
@@ -590,6 +592,8 @@ export function createRoleRuntimeExtension(
590
592
  let pendingNavigatorSettlement: Promise<void> | undefined;
591
593
  let navigatorWorkContext: NavigatorWorkContext | undefined;
592
594
  const pendingInfrastructureToolCallIds = new Set<string>();
595
+ // #357 T2: engine detour once-latch + registration (Judge+engine only).
596
+ let engineDetourRegistration: ReturnType<typeof registerEngineDetourTool> | undefined;
593
597
  // #288 primary-session thin adapter. The policy is the sole budget owner;
594
598
  // terminating-tool rejections and mechanical delivery requests share two turns.
595
599
  let receiptDelivery = createReceiptDeliveryPolicy();
@@ -1053,6 +1057,7 @@ export function createRoleRuntimeExtension(
1053
1057
  pendingNavigatorPresentation = undefined;
1054
1058
  pendingNavigatorSettlement = undefined;
1055
1059
  pendingInfrastructureToolCallIds.clear();
1060
+ engineDetourRegistration?.resetLatch();
1056
1061
  navigatorWorkContext = undefined;
1057
1062
  // #351: OAuth keepalive is orthogonal to --ak-role; start before role early-return
1058
1063
  // so role-less sessions (and reload after shutdown stop) still keep tokens alive.
@@ -1164,6 +1169,14 @@ export function createRoleRuntimeExtension(
1164
1169
  }
1165
1170
 
1166
1171
  await executeActivationStage(entry.role, activationStage(entry.role, runtime), { clock, writeTrace });
1172
+ // #357 T2: Judge+engine activation registers the package detour tool once.
1173
+ // Gate is env presence only — no per-engine execute branch; no judge-role spawn.
1174
+ if (entry.role === "judge" && engineDetourRegistration === undefined) {
1175
+ engineDetourRegistration = registerEngineDetourTool(pi, hostActions);
1176
+ if (!engineDetourRegistration.registered) {
1177
+ engineDetourRegistration = undefined;
1178
+ }
1179
+ }
1167
1180
  // Worker gates ②④ + ① baseline: envelope arms the worktree after role install.
1168
1181
  // Parent session feeds #216 createRecordSession so baseline/bounce survive resume.
1169
1182
  if (entry.role === "coder" || entry.role === "fixer") {