@akagilnc/pi-workflow-roles 0.1.2089 → 0.1.2096

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.
@@ -14,18 +14,25 @@ The machine entrypoint is `claude`. Run from the role project root. Non-interact
14
14
  print mode (`-p` / `--print`) is verified available on this host:
15
15
 
16
16
  ```bash
17
- claude -p "YOUR_LABOR_PROMPT"
17
+ claude -p --output-format=stream-json "YOUR_LABOR_PROMPT"
18
18
  ```
19
19
 
20
20
  Pin the Opus model explicitly (`--model opus` verified accepted on this host):
21
21
 
22
22
  ```bash
23
- claude -p --model opus "YOUR_LABOR_PROMPT"
23
+ claude -p --model opus --output-format=stream-json "YOUR_LABOR_PROMPT"
24
24
  ```
25
25
 
26
+ Use `--output-format=stream-json` (verified present on this host) so the package
27
+ idle clock can see subprocess activity while the engine works; take the labor body
28
+ from the final `result` event, not from intermediate stream rows.
29
+
26
30
  Prefer `claude --help` on the host over any remembered flag set. Do not wrap this
27
31
  engine behind `ak-role` flags.
28
32
 
29
- When the package detour tool is available, start exactly one subprocess through
30
- it with argv assembled from this material and the local CLI; return the stdout
31
- labor content to the same session for the existing typed submission path.
33
+ When the package detour tool is available, start exactly one subprocess per
34
+ labor invocation through it with argv assembled from this material and the
35
+ local CLI; return the stdout labor content to the same session for the existing
36
+ typed submission path. One labor turn = one process (not one process for the
37
+ whole role run). If the detour fails, continue labor in-session on the seat main
38
+ road and still submit via the existing typed path.
package/souls/judge.md CHANGED
@@ -9,66 +9,58 @@
9
9
  - 主张不是证据。核对引文、代码、测试与历史;只把指向当前 head 的材料当作
10
10
  当前事实。修复者的自述不能替代新 head 上的机械验证和独立复查。
11
11
  - 每条问题都必须有显式记录和处置。严重度不是入场券;不得沉默遗漏,
12
- 也不得安静降级。驳回问题时必须给出可核验的理由和证据。
12
+ 也不得安静降级。驳回须给出可核验的理由和证据。
13
13
 
14
14
  ## 案面
15
15
 
16
- **开工先立案。** 开庭先枚举本单**适用法源**——**宿主全局宪法与仓级治理文件必列**,
17
- 以及 ADR、票面既裁、陛下裁定与其它既裁法源;逐项对表——引用不等于适用;再以法源与当前一手证据独立完成双向
18
- 抽样重推(误删与该删未删同查),然后落判词。判词与驳回须能锚到具体法源;
19
- **无可依法源时不得以「无法可依」收敛,应上抛请立法**。**立案先于按派单收缩的
20
- 审理范围。**
16
+ **开工先立案。** 开庭先枚举本单适用法源——宿主全局宪法与仓级治理文件必列,
17
+ 以及 ADR、票面既裁、陛下裁定与其它既裁法源;逐项对表,引用不等于适用。
18
+ **finding 的事实与处方分开对宪:事实成立而处方违宪时,保留事实,驳回处方或
19
+ 改开合宪处方。** 再以法源与当前一手证据独立完成双向抽样重推(误删与该删未删
20
+ 同查),然后落判词。判词与驳回须锚到具体法源;无可依法源时不得以「无法可依」
21
+ 收敛,应上抛请立法。立案先于按派单收缩的审理范围。
21
22
 
22
23
  **宪法优先,已批断言不容翻。** 动过既有断言的改动须溯源到票面授权、ADR 或
23
- 先前裁定;权威仍在而相抵触即为 blocking。改动触及 `docs/adr/`、`CONTEXT.md`
24
- 或其它治理法文件而票面未授权时,实质审理并在判词中点名;确属宪法问题则上抛。
24
+ 先前裁定;权威仍在而相抵触即为 blocking。改动触及治理法文件(含 `docs/adr/`、
25
+ `CONTEXT.md`)而票面未授权时,实质审理并在判词中点名;确属宪法问题则上抛。
25
26
 
26
- **全部提交物都是案面**:票面、计划、处置清单、packet、**派单措辞与输入 prompt**、
27
- 代码、测试。
27
+ **全部提交物都是案面**:票面、计划、处置清单、packet、派单措辞与输入 prompt、
28
+ 代码、测试。举证责任由所审材料决定,不由派单指定;计划只审是否足以开工,
29
+ 实现细节归 Apply。
28
30
 
29
- **审查范围绑定本次授权的 target/range,并覆盖该范围内的完整变更与未声明的
30
- 生产默认行为**;不得因派单或票面清单缩减适用的举证责任。派单非法缩 scope 时:
31
- 可施工纠正的判 `continue`;属陛下取舍、法源冲突或无法安全裁定的判 `escalate`。
31
+ **审查范围绑定本次授权的 target/range**,覆盖范围内完整变更与未声明的生产
32
+ 默认行为;不得因派单或票面清单缩减举证责任。派单非法缩 scope:可施工纠正判
33
+ `continue`;属陛下取舍、法源冲突或无法安全裁定判 `escalate`。
32
34
 
33
- **落判前逐项过改动清单。** 授权 range 内每项改动都要标明由票面哪条驱动:**标不出
34
- 来的即为夹带**(默认值、护栏、平行机制、仅测试用的生产钩子同论);**票面有要求而
35
- 清单中没有的即为缺口**。二者同为缺陷,须逐条处置;清单未列全不得收敛。
36
-
37
- ## 举证责任
38
-
39
- 举证责任由**所审材料**决定,不由派单指定。
40
- 计划只审是否足以开工,实现细节归 Apply。
41
-
42
- **实现须与需求相符——多与少都是缺陷。** 票面要的没做到是缺陷;票面没要的
43
- 自作主张同样是缺陷(夹带的默认值、护栏、平行机制、仅测试用的生产钩子同论)。
35
+ **落判前逐项过改动清单,与票面双向对表。** 每项改动标明由票面哪条驱动,
36
+ 标不出来的即为夹带;票面有要求而清单没有的即为缺口(夹带的默认值、护栏、
37
+ 平行机制、仅测试用的生产钩子同论)。多与少同为缺陷,逐条处置;清单未列全
38
+ 不得收敛。
44
39
 
45
40
  ## 修理原则
46
41
 
47
- 删除或简化根因优先于增加平行机制。提出或批准护栏前必须回答三问:
48
-
49
- 1. 哪个真实、可复现的失败证明需要这道护栏?
50
- 2. 哪个接缝拥有它要维护的不变式?
51
- 3. 为什么删除或简化根因不足以解决这一类失败?
52
-
53
- 同一接缝反复失败、根因不明、范围持续发散或出现 flake 时,应要求先做最小化、
54
- 证据驱动的诊断。**一轮轮只加不减的修复流,本身就是该上报的病。**
42
+ 删除或简化根因优先于增加平行机制。提出或批准护栏前必答三问:①哪个真实、
43
+ 可复现的失败证明需要它?②哪个接缝拥有它要维护的不变式?③为什么删除或简化
44
+ 根因不足以解决这类失败?
55
45
 
56
46
  审查修复波及面,区分原始缺陷所需改动、修复引入后再修的改动和无依据的新机制;
57
- 后两类主导时,优先删除/简化。超出**本次授权**的取舍必须上抛。
47
+ 后两类主导时优先删除/简化。超出本次授权的取舍必须上抛。**一轮轮只加不减的
48
+ 修复流,本身就是该上报的病。**
58
49
 
59
50
  ## 判词
60
51
 
61
52
  合法判词只有三种,含义相对所审材料的举证责任:
62
53
 
63
54
  - `converged` — 适用举证责任下的事项均已明确处置。修内司/将作监的提交另需:
64
- 本轮御史台无 p2 及以上 finding(p 级由你自行判定;被你驳回的 finding 不计数)。
55
+ 本轮御史台无 p2 及以上 finding(p 级由你自行判定;被你驳回的不计数)。
65
56
  - `continue` — 仍有可施工的问题;给出清晰、可验证的修理正文。
66
57
  - `escalate` — 存在必须由陛下决定的门槛、相互冲突的法源,或依据失败走势
67
- 与专业证据已无法形成安全裁定;写明问题**和可选项**。
58
+ 与专业证据已无法形成安全裁定;写明问题和可选项。
68
59
 
69
60
  **送修正文由你执笔——判词即包。** 首轮可薄(finding + 法源锚点 + 边界);有
70
- 病史则综合(病史全列、方向可钉、拆除清单明文)。判其发散(同缝反复、根因
71
- 不明、疑跨接缝、flake)时,在送修单中提示先做诊断定根因。
61
+ 病史则综合(病史全列、方向可钉、拆除清单明文)。同缝反复、根因不明、范围
62
+ 持续发散、疑跨接缝或 flake 即为发散,送修单中要求先做最小化、证据驱动的
63
+ 诊断定根因。
72
64
 
73
65
  `continue` 的 `classes` 只含尚未结清、须派施工的根因类;已结清项、过程账、
74
66
  双向抽样不得进 classes。
@@ -1,7 +1,10 @@
1
1
  /**
2
- * Package-owned engine detour tool (#357 T2 / #378).
2
+ * Package-owned engine detour tool (#357 T2 / #378 / #380).
3
3
  * Registered by shared role-runtime when Judge|Reviewer + engine activation signal is present.
4
4
  * Evidence-child legs install the same definition via customTools (no spawn in role modules).
5
+ * #380: engine process failure soft-returns so the seat rejoins the main road; declaration
6
+ * is recorded once via engine-labor-fallback (no fail-closed reject-leg).
7
+ * Caller AbortSignal cancel propagates without fallback; package-owned idle abort soft-fails.
5
8
  */
6
9
  import type {
7
10
  AgentToolResult,
@@ -19,7 +22,16 @@ import {
19
22
  isEngineDetourFailure,
20
23
  runEngineDetourOnce,
21
24
  } from "./engine-detour.ts";
22
- import { wrapPackageOwnedToolDefinition } from "./package-owned-tool-idle.ts";
25
+ import {
26
+ activationEngineLaborFallbackLatch,
27
+ recordEngineLaborFallback,
28
+ type EngineLaborFallbackField,
29
+ } from "./engine-labor-fallback.ts";
30
+ import {
31
+ isPackageOwnedToolIdleTimeoutError,
32
+ pokePackageOwnedToolIdle,
33
+ wrapPackageOwnedToolDefinition,
34
+ } from "./package-owned-tool-idle.ts";
23
35
 
24
36
  const engineDetourArgsSchema = Type.Object(
25
37
  {
@@ -49,10 +61,55 @@ export type EngineDetourToolRegistration = {
49
61
 
50
62
  type EngineDetourLatch = { used: boolean };
51
63
 
64
+ function seatFallbackToolResult(
65
+ field: EngineLaborFallbackField,
66
+ failure: string,
67
+ ): AgentToolResult<unknown> {
68
+ return {
69
+ content: [
70
+ {
71
+ type: "text" as const,
72
+ text:
73
+ `Engine detour failed: ${failure}. Perform the labor in this session (seat main road) and submit via the existing typed path. A mechanical fallback declaration will attach to the typed receipt.`,
74
+ },
75
+ ],
76
+ details: {
77
+ tool: ENGINE_DETOUR_TOOL_NAME,
78
+ detourFailed: true,
79
+ ...field,
80
+ },
81
+ };
82
+ }
83
+
84
+ /** Caller/upper-layer cancel must propagate; idle backstop is seat-fallback, not cancel. */
85
+ function isCallerCancellation(
86
+ error: unknown,
87
+ signal: AbortSignal | undefined,
88
+ ): boolean {
89
+ if (isPackageOwnedToolIdleTimeoutError(error)) return false;
90
+ if (
91
+ signal !== undefined &&
92
+ isPackageOwnedToolIdleTimeoutError(signal.reason)
93
+ ) {
94
+ return false;
95
+ }
96
+ if (signal?.aborted === true) return true;
97
+ if (
98
+ typeof error === "object" &&
99
+ error !== null &&
100
+ (error as { name?: unknown }).name === "AbortError"
101
+ ) {
102
+ return true;
103
+ }
104
+ return false;
105
+ }
106
+
52
107
  /**
53
108
  * Build one once-latch detour tool definition for a configured engine name.
54
109
  * `latch` is shared so parent registration can reset between activations.
55
- * `fail` owns host abort (parent) vs throw (evidence child).
110
+ * `fail` owns host abort (parent) vs throw (evidence child) for tool misuse only.
111
+ * Engine process failure (nonzero/empty/spawn/idle-timeout) soft-returns seat fallback (#380).
112
+ * Caller AbortSignal cancel propagates without writing fallback.
56
113
  */
57
114
  export function createEngineDetourToolDefinition(input: {
58
115
  engineName: string;
@@ -71,6 +128,7 @@ export function createEngineDetourToolDefinition(input: {
71
128
  `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.`,
72
129
  "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.",
73
130
  "On success, use the returned stdout as labor content for the existing typed submission / report path.",
131
+ "On engine failure the tool returns a soft failure: continue labor in this session and submit via the existing typed path. Do not treat engine failure as a reason to withhold the typed receipt.",
74
132
  ],
75
133
  parameters: engineDetourArgsSchema,
76
134
  async execute(
@@ -99,27 +157,39 @@ export function createEngineDetourToolDefinition(input: {
99
157
  );
100
158
  }
101
159
 
160
+ const softFail = (failure: string): AgentToolResult<unknown> => {
161
+ // Activation-scoped latch is the sole shared recorder (parent seat + legs).
162
+ const fallbackLatch =
163
+ activationEngineLaborFallbackLatch() ?? { field: undefined };
164
+ const field = recordEngineLaborFallback(fallbackLatch, {
165
+ engine: engineName,
166
+ failure,
167
+ });
168
+ return seatFallbackToolResult(field, failure);
169
+ };
170
+
102
171
  let result: Awaited<ReturnType<typeof runEngineDetourOnce>>;
103
172
  try {
173
+ // Byte activity on stdout/stderr touches the outer package-owned idle clock
174
+ // (183s silence law unchanged). True hangs still die; slow streaming engines live.
104
175
  result = await runEngineDetourOnce({
105
176
  argv,
106
177
  cwd: ctx.cwd,
107
178
  ...(signal === undefined ? {} : { signal }),
179
+ onOutputActivity: pokePackageOwnedToolIdle,
108
180
  });
109
181
  } catch (error) {
110
- input.fail(
111
- error instanceof Error ? error : new Error(String(error)),
112
- toolCallId,
113
- ctx,
114
- );
182
+ // Caller cancel: propagate. Idle backstop + spawn/engine failure: seat fallback.
183
+ if (isCallerCancellation(error, signal)) {
184
+ throw error;
185
+ }
186
+ const failure =
187
+ error instanceof Error ? error.message : String(error);
188
+ return softFail(failure.trim() === "" ? "engine detour spawn failed" : failure);
115
189
  }
116
190
 
117
191
  if (isEngineDetourFailure(result)) {
118
- input.fail(
119
- new Error(engineDetourFailureDiagnostic(result)),
120
- toolCallId,
121
- ctx,
122
- );
192
+ return softFail(engineDetourFailureDiagnostic(result));
123
193
  }
124
194
 
125
195
  return {
@@ -28,11 +28,28 @@ export type EngineDetourRunInput = Readonly<{
28
28
  cwd: string;
29
29
  env?: NodeJS.ProcessEnv;
30
30
  signal?: AbortSignal;
31
+ /**
32
+ * Called when the child emits at least one byte on stdout or stderr.
33
+ * Host wires this to the package-owned idle clock (activity = touch); no timer here.
34
+ */
35
+ onOutputActivity?: () => void;
31
36
  }>;
32
37
 
38
+ function abortReasonError(signal: AbortSignal): Error {
39
+ const reason = signal.reason;
40
+ if (reason instanceof Error) return reason;
41
+ if (typeof reason === "string" && reason.trim() !== "") {
42
+ return new Error(reason);
43
+ }
44
+ const error = new Error("aborted");
45
+ error.name = "AbortError";
46
+ return error;
47
+ }
48
+
33
49
  /**
34
50
  * Run one engine subprocess. First argv element is the executable (PATH lookup).
35
51
  * stdio: ignore stdin, pipe stdout+stderr. No shell, no retry, no hang timer.
52
+ * AbortSignal cancels the child immediately via an explicit listener (reason preserved).
36
53
  */
37
54
  export async function runEngineDetourOnce(
38
55
  input: EngineDetourRunInput,
@@ -44,30 +61,64 @@ export async function runEngineDetourOnce(
44
61
  const args = input.argv.slice(1);
45
62
  return await new Promise<EngineDetourResult>((resolve, reject) => {
46
63
  let settled = false;
64
+ const signal = input.signal;
65
+ // Own abort→kill explicitly so rejection preserves signal.reason (caller cancel
66
+ // vs package-owned idle). Do not pass `signal` to spawn (Node replaces reason).
47
67
  const child = spawn(command, args, {
48
68
  cwd: input.cwd,
49
69
  env: input.env ?? process.env,
50
70
  stdio: ["ignore", "pipe", "pipe"],
51
- ...(input.signal === undefined ? {} : { signal: input.signal }),
52
71
  });
53
72
  let stdout = "";
54
73
  let stderr = "";
74
+ const noteActivity = (chunk: string): void => {
75
+ if (chunk.length === 0) return;
76
+ input.onOutputActivity?.();
77
+ };
55
78
  child.stdout.setEncoding("utf8").on("data", (chunk: string) => {
56
79
  stdout += chunk;
80
+ noteActivity(chunk);
57
81
  });
58
82
  child.stderr.setEncoding("utf8").on("data", (chunk: string) => {
59
83
  stderr += chunk;
84
+ noteActivity(chunk);
60
85
  });
61
86
  const fail = (error: unknown): void => {
62
87
  if (settled) return;
63
88
  settled = true;
89
+ if (signal !== undefined) {
90
+ signal.removeEventListener("abort", onAbort);
91
+ }
64
92
  reject(error instanceof Error ? error : new Error(String(error)));
65
93
  };
66
- child.on("error", (error) => fail(error));
67
- child.on("close", (code) => {
94
+ const succeed = (result: EngineDetourResult): void => {
68
95
  if (settled) return;
69
96
  settled = true;
70
- resolve({ code: code ?? 1, stdout, stderr });
97
+ if (signal !== undefined) {
98
+ signal.removeEventListener("abort", onAbort);
99
+ }
100
+ resolve(result);
101
+ };
102
+ const onAbort = (): void => {
103
+ // Fail synchronously so cooperative idle/cancel paths can soft-settle
104
+ // before the outer package-owned idle hard-reject drain.
105
+ fail(signal !== undefined ? abortReasonError(signal) : new Error("aborted"));
106
+ try {
107
+ child.kill("SIGTERM");
108
+ } catch {
109
+ // already exited
110
+ }
111
+ };
112
+ if (signal !== undefined) {
113
+ if (signal.aborted) {
114
+ onAbort();
115
+ } else {
116
+ signal.addEventListener("abort", onAbort, { once: true });
117
+ }
118
+ }
119
+ child.on("error", (error) => fail(error));
120
+ child.on("close", (code) => {
121
+ succeed({ code: code ?? 1, stdout, stderr });
71
122
  });
72
123
  });
73
124
  }
@@ -0,0 +1,172 @@
1
+ /**
2
+ * #380 — sole shared seat-fallback declaration after engine detour failure.
3
+ * Detour rejoins the main road (ADR 0069); silence is the only crime.
4
+ * Construction of the typed receipt field lives in exactly one site (S1).
5
+ */
6
+
7
+ export type EngineLaborFallback = Readonly<{
8
+ engine: string;
9
+ failure: string;
10
+ laborBy: "seat";
11
+ }>;
12
+
13
+ /** Activation-scoped latch holder (one parent seat activation at a time). */
14
+ let activationLatch: EngineLaborFallbackLatch | undefined;
15
+
16
+ /** Sole construction site for typed receipt field `engineLaborFallback` (#380 S1). */
17
+ export function buildEngineLaborFallbackField(input: {
18
+ readonly engine: string;
19
+ readonly failure: string;
20
+ }) {
21
+ return Object.freeze({
22
+ engineLaborFallback: Object.freeze({
23
+ engine: input.engine,
24
+ failure: input.failure,
25
+ laborBy: "seat" as const,
26
+ }),
27
+ });
28
+ }
29
+
30
+ /** Frozen field bag produced by the sole construction site. */
31
+ export type EngineLaborFallbackField = ReturnType<
32
+ typeof buildEngineLaborFallbackField
33
+ >;
34
+
35
+ export type EngineLaborFallbackLatch = {
36
+ field: EngineLaborFallbackField | undefined;
37
+ };
38
+
39
+ export function createEngineLaborFallbackLatch(): EngineLaborFallbackLatch {
40
+ return { field: undefined };
41
+ }
42
+
43
+ /**
44
+ * Record first detour failure for this latch (first wins; parallel legs share one field).
45
+ * Always returns the latched first-wins value so tool details and receipt projection match.
46
+ */
47
+ export function recordEngineLaborFallback(
48
+ latch: EngineLaborFallbackLatch,
49
+ input: { readonly engine: string; readonly failure: string },
50
+ ): EngineLaborFallbackField {
51
+ const field = buildEngineLaborFallbackField(input);
52
+ if (latch.field === undefined) latch.field = field;
53
+ return latch.field;
54
+ }
55
+
56
+ export function readEngineLaborFallbackField(
57
+ latch: EngineLaborFallbackLatch | undefined,
58
+ ): EngineLaborFallbackField | undefined {
59
+ return latch?.field;
60
+ }
61
+
62
+ /**
63
+ * Merge sole-built field into typed receipt details.
64
+ * Spread only — must not construct the field key again (S1).
65
+ * Without a mechanical latch, strip any model-injected reserved key (no forged declaration).
66
+ */
67
+ export function withEngineLaborFallbackField<T extends object>(
68
+ receipt: T,
69
+ field: EngineLaborFallbackField | undefined,
70
+ ): T {
71
+ if (field !== undefined) {
72
+ return { ...receipt, ...field };
73
+ }
74
+ if (
75
+ !Object.prototype.hasOwnProperty.call(receipt, "engineLaborFallback")
76
+ ) {
77
+ return receipt;
78
+ }
79
+ const { engineLaborFallback: _forged, ...rest } = receipt as T & {
80
+ engineLaborFallback?: unknown;
81
+ };
82
+ return rest as T;
83
+ }
84
+
85
+ /** Install the activation-scoped latch (Judge/Reviewer session_start). */
86
+ export function installActivationEngineLaborFallbackLatch(
87
+ latch: EngineLaborFallbackLatch,
88
+ ): void {
89
+ activationLatch = latch;
90
+ }
91
+
92
+ /** Clear activation latch (session end / next activation). */
93
+ export function clearActivationEngineLaborFallbackLatch(): void {
94
+ activationLatch = undefined;
95
+ }
96
+
97
+ /** Active activation latch, if any (legs inherit parent seat activation). */
98
+ export function activationEngineLaborFallbackLatch():
99
+ | EngineLaborFallbackLatch
100
+ | undefined {
101
+ return activationLatch;
102
+ }
103
+
104
+ /** Read fallback field from the active activation latch. */
105
+ export function readActivationEngineLaborFallbackField():
106
+ | EngineLaborFallbackField
107
+ | undefined {
108
+ return readEngineLaborFallbackField(activationLatch);
109
+ }
110
+
111
+ /**
112
+ * Read a previously attached declaration from a typed receipt / details object.
113
+ * Rebuilds via the sole construction site — callers must spread, never re-key.
114
+ */
115
+ export function readEngineLaborFallbackFieldFrom(
116
+ source: unknown,
117
+ ): EngineLaborFallbackField | undefined {
118
+ if (typeof source !== "object" || source === null || Array.isArray(source)) {
119
+ return undefined;
120
+ }
121
+ let raw: unknown;
122
+ try {
123
+ raw = (source as Record<string, unknown>).engineLaborFallback;
124
+ } catch {
125
+ return undefined;
126
+ }
127
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return undefined;
128
+ const rec = raw as Record<string, unknown>;
129
+ if (
130
+ typeof rec.engine !== "string" ||
131
+ typeof rec.failure !== "string" ||
132
+ rec.laborBy !== "seat"
133
+ ) {
134
+ return undefined;
135
+ }
136
+ return buildEngineLaborFallbackField({
137
+ engine: rec.engine,
138
+ failure: rec.failure,
139
+ });
140
+ }
141
+
142
+ /**
143
+ * Restore activation latch from durable session tool results (#380 resume).
144
+ * Scans existing same-session detour tool results only — no sidecar / new entry type.
145
+ * Replays through recordEngineLaborFallback so first-wins + sole producer stay intact.
146
+ */
147
+ export function restoreEngineLaborFallbackFromSessionEntries(
148
+ latch: EngineLaborFallbackLatch,
149
+ entries: readonly unknown[],
150
+ toolName: string,
151
+ ): void {
152
+ for (const entry of entries) {
153
+ if (typeof entry !== "object" || entry === null) continue;
154
+ const row = entry as { type?: unknown; message?: unknown };
155
+ if (row.type !== "message") continue;
156
+ const message = row.message;
157
+ if (typeof message !== "object" || message === null) continue;
158
+ const msg = message as {
159
+ role?: unknown;
160
+ toolName?: unknown;
161
+ details?: unknown;
162
+ };
163
+ if (msg.role !== "toolResult") continue;
164
+ if (msg.toolName !== toolName) continue;
165
+ const field = readEngineLaborFallbackFieldFrom(msg.details);
166
+ if (field === undefined) continue;
167
+ recordEngineLaborFallback(latch, {
168
+ engine: field.engineLaborFallback.engine,
169
+ failure: field.engineLaborFallback.failure,
170
+ });
171
+ }
172
+ }
@@ -596,17 +596,15 @@ export async function executeEvidenceChild(
596
596
  engine: engineName,
597
597
  packageRoot: options.packageRoot,
598
598
  });
599
- // #378: detour failure is durable for this leg — later assistant reports cannot wash it.
600
- let engineDetourFailure: Error | undefined;
599
+ // #378/#380: same detour tool as parent seat. Engine process failure soft-returns
600
+ // seat main-road fallback (ADR 0069 detour-rejoins-main-road); tool misuse still throws.
601
601
  const engineDetourTool =
602
602
  engineName === undefined
603
603
  ? undefined
604
604
  : createEngineDetourToolDefinition({
605
605
  engineName,
606
606
  fail(error) {
607
- const failure = error instanceof Error ? error : new Error(String(error));
608
- engineDetourFailure = failure;
609
- throw failure;
607
+ throw error instanceof Error ? error : new Error(String(error));
610
608
  },
611
609
  });
612
610
  // No tools allowlist — Pi defaults + unrestricted evidence surface (ADR 0064).
@@ -642,18 +640,8 @@ export async function executeEvidenceChild(
642
640
  try {
643
641
  await session.prompt(delivered);
644
642
  } 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
- }
650
643
  throw classifiedError(error, "provider");
651
644
  }
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
- }
657
645
  if (signal?.aborted) throw new Error("Evidence child was cancelled");
658
646
  const lastAssistant = [...session.messages]
659
647
  .reverse()
package/src/judge-role.ts CHANGED
@@ -8,6 +8,10 @@ import { Type, type Static } from "typebox";
8
8
 
9
9
  import { disposeComplianceDecision } from "./audit-escalation.ts";
10
10
  import type { ComplianceDecision } from "./compliance-transport.ts";
11
+ import {
12
+ readActivationEngineLaborFallbackField,
13
+ withEngineLaborFallbackField,
14
+ } from "./engine-labor-fallback.ts";
11
15
 
12
16
  import {
13
17
  JUDGE_OUTPUT_TOOL_NAME,
@@ -126,18 +130,21 @@ export function createJudgeRoleRuntime(
126
130
  } catch (error) {
127
131
  hostActions.failInfrastructure(error, ctx, toolCallId);
128
132
  }
133
+ // #380: attach sole-built engineLaborFallback when detour fell back to seat labor.
134
+ const laborFallback = readActivationEngineLaborFallbackField();
135
+ const acceptedDetails = withEngineLaborFallbackField(verdict, laborFallback);
129
136
  return disposeComplianceDecision<AgentToolResult<unknown>>(
130
137
  audit,
131
138
  {
132
139
  pass: (usage) => ({
133
140
  content: [{ type: "text" as const, text: "Judge verdict accepted" }],
134
- details: verdict,
141
+ details: acceptedDetails,
135
142
  terminate: true as const,
136
143
  ...(usage === undefined ? {} : { usage }),
137
144
  }),
138
145
  noReceipt: (auditNoReceipt, usageProjection) => ({
139
146
  content: [{ type: "text" as const, text: "Judge verdict accepted; compliance audit produced no receipt" }],
140
- details: { ...verdict, auditNoReceipt },
147
+ details: { ...acceptedDetails, auditNoReceipt },
141
148
  terminate: true as const,
142
149
  ...usageProjection,
143
150
  }),
@@ -149,7 +156,8 @@ export function createJudgeRoleRuntime(
149
156
  escalate: (result) => result,
150
157
  auditIncomplete: (result) => result,
151
158
  },
152
- verdict,
159
+ // #380: escalate deliveredOutput must carry the same mechanical projection.
160
+ acceptedDetails,
153
161
  );
154
162
  },
155
163
  });