@akagilnc/pi-workflow-roles 0.1.2091 → 0.1.2101
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.
- package/README.md +8 -1
- package/README.zh-CN.md +8 -1
- package/dist/engine-detour-tool.js +57 -4
- package/dist/engine-detour.js +53 -4
- package/dist/engine-labor-fallback.js +121 -0
- package/dist/evidence-child-executor.js +3 -15
- package/dist/package-owned-tool-idle.js +64 -7
- package/dist/public-cli/main.js +56 -22
- package/package.json +1 -1
- package/resources/engines/cursor.md +7 -0
- package/resources/engines/opus.md +12 -5
- package/src/engine-detour-tool.ts +83 -13
- package/src/engine-detour.ts +55 -4
- package/src/engine-labor-fallback.ts +172 -0
- package/src/evidence-child-executor.ts +3 -15
- package/src/judge-role.ts +11 -3
- package/src/package-owned-tool-idle.ts +73 -7
- package/src/public-cli/cli.ts +6 -4
- package/src/public-cli/config.ts +15 -28
- package/src/public-cli/settlement.ts +9 -1
- package/src/reviewer-role.ts +13 -5
- package/src/role-runtime.ts +22 -2
package/dist/public-cli/main.js
CHANGED
|
@@ -14449,7 +14449,7 @@ function setPersistentSeatEngine(config, seat, engine) {
|
|
|
14449
14449
|
const previous = config.seats[seat];
|
|
14450
14450
|
if (previous === void 0) {
|
|
14451
14451
|
throw new Error(
|
|
14452
|
-
`config seat ${seat} has no persistent model; set provider/model:thinking before engine`
|
|
14452
|
+
`config seat ${seat} has no persistent model; set provider/model[:thinking] before engine`
|
|
14453
14453
|
);
|
|
14454
14454
|
}
|
|
14455
14455
|
if (engine === void 0) {
|
|
@@ -14518,19 +14518,6 @@ function parseModelSpec(spec, fallbackThinking) {
|
|
|
14518
14518
|
const model = modelPart.slice(slash + 1);
|
|
14519
14519
|
return thinking === void 0 ? { provider, model } : { provider, model, thinking };
|
|
14520
14520
|
}
|
|
14521
|
-
function parsePersistentModelSpec(spec) {
|
|
14522
|
-
const parsed = parseModelSpec(spec);
|
|
14523
|
-
if (parsed.thinking === void 0) {
|
|
14524
|
-
throw new Error(
|
|
14525
|
-
`model specification requires a thinking level (provider/model:thinking), got ${spec}`
|
|
14526
|
-
);
|
|
14527
|
-
}
|
|
14528
|
-
return {
|
|
14529
|
-
provider: parsed.provider,
|
|
14530
|
-
model: parsed.model,
|
|
14531
|
-
thinking: parsed.thinking
|
|
14532
|
-
};
|
|
14533
|
-
}
|
|
14534
14521
|
function formatModelSpec(selection) {
|
|
14535
14522
|
const base = `${selection.provider}/${selection.model}`;
|
|
14536
14523
|
return selection.thinking === void 0 ? base : `${base}:${selection.thinking}`;
|
|
@@ -14578,13 +14565,15 @@ function parseSeatModelConfig(value, seat) {
|
|
|
14578
14565
|
if (typeof raw.model !== "string" || raw.model.trim() === "") {
|
|
14579
14566
|
throw new Error(`config seat ${seat} requires model`);
|
|
14580
14567
|
}
|
|
14581
|
-
if (
|
|
14582
|
-
|
|
14568
|
+
if (raw.thinking !== void 0) {
|
|
14569
|
+
if (typeof raw.thinking !== "string" || !THINKING_LEVELS.has(raw.thinking)) {
|
|
14570
|
+
throw new Error(`config seat ${seat} requires a valid thinking level`);
|
|
14571
|
+
}
|
|
14583
14572
|
}
|
|
14584
14573
|
const parsed = {
|
|
14585
14574
|
provider: raw.provider,
|
|
14586
14575
|
model: raw.model,
|
|
14587
|
-
thinking: raw.thinking
|
|
14576
|
+
...raw.thinking === void 0 ? {} : { thinking: raw.thinking }
|
|
14588
14577
|
};
|
|
14589
14578
|
if (raw.engine !== void 0) {
|
|
14590
14579
|
if (typeof raw.engine !== "string") {
|
|
@@ -19170,6 +19159,42 @@ var init_engine_detour = __esm({
|
|
|
19170
19159
|
}
|
|
19171
19160
|
});
|
|
19172
19161
|
|
|
19162
|
+
// src/engine-labor-fallback.ts
|
|
19163
|
+
function buildEngineLaborFallbackField(input) {
|
|
19164
|
+
return Object.freeze({
|
|
19165
|
+
engineLaborFallback: Object.freeze({
|
|
19166
|
+
engine: input.engine,
|
|
19167
|
+
failure: input.failure,
|
|
19168
|
+
laborBy: "seat"
|
|
19169
|
+
})
|
|
19170
|
+
});
|
|
19171
|
+
}
|
|
19172
|
+
function readEngineLaborFallbackFieldFrom(source) {
|
|
19173
|
+
if (typeof source !== "object" || source === null || Array.isArray(source)) {
|
|
19174
|
+
return void 0;
|
|
19175
|
+
}
|
|
19176
|
+
let raw;
|
|
19177
|
+
try {
|
|
19178
|
+
raw = source.engineLaborFallback;
|
|
19179
|
+
} catch {
|
|
19180
|
+
return void 0;
|
|
19181
|
+
}
|
|
19182
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return void 0;
|
|
19183
|
+
const rec = raw;
|
|
19184
|
+
if (typeof rec.engine !== "string" || typeof rec.failure !== "string" || rec.laborBy !== "seat") {
|
|
19185
|
+
return void 0;
|
|
19186
|
+
}
|
|
19187
|
+
return buildEngineLaborFallbackField({
|
|
19188
|
+
engine: rec.engine,
|
|
19189
|
+
failure: rec.failure
|
|
19190
|
+
});
|
|
19191
|
+
}
|
|
19192
|
+
var init_engine_labor_fallback = __esm({
|
|
19193
|
+
"src/engine-labor-fallback.ts"() {
|
|
19194
|
+
"use strict";
|
|
19195
|
+
}
|
|
19196
|
+
});
|
|
19197
|
+
|
|
19173
19198
|
// src/stream-idle-guard.ts
|
|
19174
19199
|
var init_stream_idle_guard = __esm({
|
|
19175
19200
|
"src/stream-idle-guard.ts"() {
|
|
@@ -19235,6 +19260,7 @@ var init_engine_detour_tool = __esm({
|
|
|
19235
19260
|
"use strict";
|
|
19236
19261
|
init_build();
|
|
19237
19262
|
init_engine_detour();
|
|
19263
|
+
init_engine_labor_fallback();
|
|
19238
19264
|
init_package_owned_tool_idle();
|
|
19239
19265
|
engineDetourArgsSchema = typebox_exports.Object(
|
|
19240
19266
|
{
|
|
@@ -20318,7 +20344,12 @@ function auditNoReceiptDecisiveFact(candidate) {
|
|
|
20318
20344
|
}
|
|
20319
20345
|
}
|
|
20320
20346
|
function judgeDecisiveFacts(verdict, judgeStatus) {
|
|
20321
|
-
const facts = {
|
|
20347
|
+
const facts = {
|
|
20348
|
+
judgeStatus,
|
|
20349
|
+
...auditNoReceiptDecisiveFact(verdict),
|
|
20350
|
+
// #380: spread sole-built field; do not re-key here (S1).
|
|
20351
|
+
...readEngineLaborFallbackFieldFrom(verdict)
|
|
20352
|
+
};
|
|
20322
20353
|
if (judgeStatus === "continue") {
|
|
20323
20354
|
const fix = safelyRead(verdict, "fix");
|
|
20324
20355
|
if (fix.readable && isRecord5(fix.value)) {
|
|
@@ -20504,7 +20535,9 @@ function reviewerDecisiveFacts(output) {
|
|
|
20504
20535
|
axes,
|
|
20505
20536
|
reportAxes,
|
|
20506
20537
|
acceptedBatchPresent: acceptedBatch.readable && acceptedBatch.value !== void 0,
|
|
20507
|
-
...auditNoReceiptDecisiveFact(candidate)
|
|
20538
|
+
...auditNoReceiptDecisiveFact(candidate),
|
|
20539
|
+
// #380: spread sole-built field; do not re-key here (S1).
|
|
20540
|
+
...readEngineLaborFallbackFieldFrom(candidate)
|
|
20508
20541
|
};
|
|
20509
20542
|
if (status.readable && typeof status.value === "string") facts.reviewerStatus = status.value;
|
|
20510
20543
|
if (specDisposition.readable && (specDisposition.value === "launched" || specDisposition.value === "skipped-missing")) {
|
|
@@ -22312,6 +22345,7 @@ var init_settlement = __esm({
|
|
|
22312
22345
|
init_compliance_transport();
|
|
22313
22346
|
init_collector_ledger();
|
|
22314
22347
|
init_engine_detour();
|
|
22348
|
+
init_engine_labor_fallback();
|
|
22315
22349
|
init_judge_output();
|
|
22316
22350
|
init_collector_output();
|
|
22317
22351
|
init_worker_output();
|
|
@@ -27072,7 +27106,7 @@ function renderHelp() {
|
|
|
27072
27106
|
lines.push(
|
|
27073
27107
|
"",
|
|
27074
27108
|
"Role options: ak-role help <command>",
|
|
27075
|
-
"Persistent config: ak-role config set <seat> <provider/model:thinking>",
|
|
27109
|
+
"Persistent config: ak-role config set <seat> <provider/model[:thinking]>",
|
|
27076
27110
|
"Persistent engine (judge|reviewer): ak-role config set-engine <seat> <name> | unset-engine <seat>",
|
|
27077
27111
|
"Effective seats: ak-role roles"
|
|
27078
27112
|
);
|
|
@@ -27153,7 +27187,7 @@ async function runConfigCommand(args, home, packageRoot2, io) {
|
|
|
27153
27187
|
if (args[0] === "set") {
|
|
27154
27188
|
if (args.length < 3) {
|
|
27155
27189
|
throw new CliUsageError(
|
|
27156
|
-
"usage: ak-role config set <seat> <provider/model:thinking>"
|
|
27190
|
+
"usage: ak-role config set <seat> <provider/model[:thinking]>"
|
|
27157
27191
|
);
|
|
27158
27192
|
}
|
|
27159
27193
|
const pairs = args.slice(1);
|
|
@@ -27169,7 +27203,7 @@ async function runConfigCommand(args, home, packageRoot2, io) {
|
|
|
27169
27203
|
if (!isPublicConfigurableSeat(seat)) {
|
|
27170
27204
|
throw new CliUsageError(`unknown configurable seat: ${seat}`);
|
|
27171
27205
|
}
|
|
27172
|
-
config = setPersistentSeatConfig(config, seat,
|
|
27206
|
+
config = setPersistentSeatConfig(config, seat, parseModelSpec(spec));
|
|
27173
27207
|
}
|
|
27174
27208
|
await savePublicCliConfig(config, home);
|
|
27175
27209
|
io.stdout(renderConfig(config));
|
package/package.json
CHANGED
|
@@ -30,3 +30,10 @@ agent -p --force --output-format stream-json "YOUR_LABOR_PROMPT"
|
|
|
30
30
|
|
|
31
31
|
Prefer `agent --help` on the host over any remembered flag set. Do not wrap this
|
|
32
32
|
engine behind `ak-role` flags.
|
|
33
|
+
|
|
34
|
+
When the package detour tool is available, start exactly one subprocess per
|
|
35
|
+
labor invocation through it with argv assembled from this material and the
|
|
36
|
+
local CLI; return the stdout labor content to the same session for the existing
|
|
37
|
+
typed submission path. One labor turn = one process (not one process for the
|
|
38
|
+
whole role run). If the detour fails, continue labor in-session on the seat main
|
|
39
|
+
road and still submit via the existing typed path.
|
|
@@ -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
|
|
30
|
-
it with argv assembled from this material and the
|
|
31
|
-
labor content to the same session for the existing
|
|
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.
|
|
@@ -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 {
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
119
|
-
new Error(engineDetourFailureDiagnostic(result)),
|
|
120
|
-
toolCallId,
|
|
121
|
-
ctx,
|
|
122
|
-
);
|
|
192
|
+
return softFail(engineDetourFailureDiagnostic(result));
|
|
123
193
|
}
|
|
124
194
|
|
|
125
195
|
return {
|
package/src/engine-detour.ts
CHANGED
|
@@ -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
|
-
|
|
67
|
-
child.on("close", (code) => {
|
|
94
|
+
const succeed = (result: EngineDetourResult): void => {
|
|
68
95
|
if (settled) return;
|
|
69
96
|
settled = true;
|
|
70
|
-
|
|
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
|
|
600
|
-
|
|
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
|
-
|
|
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()
|