@akagilnc/pi-workflow-roles 0.1.4528 → 0.1.4586

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.
@@ -436,12 +436,12 @@ const REVIEWER_OPTIONS = [
436
436
  canonical: "--lens",
437
437
  aliases: [],
438
438
  valueMetavar: "completeness|correctness",
439
- required: true,
439
+ required: false,
440
440
  repeatable: false,
441
441
  form: "option",
442
442
  description: {
443
- en: "Required single review lens: completeness or correctness (no default).",
444
- zh: "必填单 lens:completeness 或 correctness(无默认值)。",
443
+ en: "Optional single-lens override: completeness or correctness; omitted runs both in parallel.",
444
+ zh: "可选单 lens 覆盖:completeness 或 correctness;省略时并行运行两轴。",
445
445
  },
446
446
  },
447
447
  {
@@ -997,9 +997,9 @@ const ROLE_COMMAND_HELP = {
997
997
  },
998
998
  reviewer: {
999
999
  command: "reviewer",
1000
- summary: "Fixed-target single-lens review (completeness or correctness).",
1000
+ summary: "Fixed-target parallel two-lens review, with an optional single-lens override.",
1001
1001
  usage: [
1002
- "ak-role reviewer --base <revision> --lens completeness|correctness --authority-ref <ref> [options] <instruction>",
1002
+ "ak-role reviewer --base <revision> [--lens completeness|correctness] --authority-ref <ref> [options] <instruction>",
1003
1003
  ],
1004
1004
  examples: [
1005
1005
  'ak-role reviewer --base main --lens completeness --authority-ref docs/adr/0001.md "Review the branch."',
@@ -878,6 +878,9 @@ export async function runPostAdmissionSeatResume(input) {
878
878
  }
879
879
  adapters = prepared.adapters;
880
880
  }
881
+ // Call-local execution env; afterAdmittedPrepare may replace it (Reviewer fresh-copy).
882
+ let env = input.env;
883
+ let preparedCleanup;
881
884
  const buildRequestAfterLease = async () => {
882
885
  let openCourtAttemptId;
883
886
  // Build uses the admitted judged under this lease (rehydrated when open
@@ -945,8 +948,15 @@ export async function runPostAdmissionSeatResume(input) {
945
948
  // Court recovery / open under lease, then dispatch.
946
949
  // Station-child same-ticket/same-parent resume is call-local auto-resume
947
950
  // (#840 / #416). Public `ak-role resume` stays one-shot (ADR 0080).
951
+ // afterAdmittedPrepare runs inside this try so mint failure and cleanup share one finally.
948
952
  try {
949
- if (input.env.stationChild === true) {
953
+ if (input.afterAdmittedPrepare !== undefined) {
954
+ const prepared = await input.afterAdmittedPrepare(loaded.admitted);
955
+ if (prepared.env !== undefined)
956
+ env = prepared.env;
957
+ preparedCleanup = prepared.cleanup;
958
+ }
959
+ if (env.stationChild === true) {
950
960
  let firstTurn;
951
961
  const stationAdapters = withOnceSuccessfulBeforeDispatch(adapters);
952
962
  // One public call → one detour scope across in-place auto-resume dispatches.
@@ -957,11 +967,11 @@ export async function runPostAdmissionSeatResume(input) {
957
967
  });
958
968
  return await runWithAutoResumeLoop({
959
969
  admitted: loaded.admitted,
960
- principalAuthority: input.env.principalAuthority,
961
- isPrincipalAvailable: resolveHostAwareSessionAvailability(input.env.host, input.env.principalAuthority),
970
+ principalAuthority: env.principalAuthority,
971
+ isPrincipalAvailable: resolveHostAwareSessionAvailability(env.host, env.principalAuthority),
962
972
  io: input.io,
963
- sessionAppender: input.env.sessionAppender,
964
- autoResumeLimit: input.env.autoResumeLimit,
973
+ sessionAppender: env.sessionAppender,
974
+ autoResumeLimit: env.autoResumeLimit,
965
975
  buildInitialPayload: () => ({ resumeTurn: false }),
966
976
  buildResumePayload: () => ({ resumeTurn: true }),
967
977
  // Same as public manual resume: prior-court sealed acceptance is not a
@@ -995,7 +1005,7 @@ export async function runPostAdmissionSeatResume(input) {
995
1005
  dispatch: async (turnRequest) => {
996
1006
  const result = await dispatchPostAdmissionTurn({
997
1007
  admitted: loaded.admitted,
998
- env: input.env,
1008
+ env,
999
1009
  io: attemptIo,
1000
1010
  request: turnRequest,
1001
1011
  lease,
@@ -1005,14 +1015,14 @@ export async function runPostAdmissionSeatResume(input) {
1005
1015
  ? {}
1006
1016
  : { effectiveEngine: input.effectiveEngine }),
1007
1017
  });
1008
- return settleDeferredPersist(loaded.admitted, input.env.principalAuthority, stationAdapters, attemptIo, result);
1018
+ return settleDeferredPersist(loaded.admitted, env.principalAuthority, stationAdapters, attemptIo, result);
1009
1019
  },
1010
1020
  }),
1011
1021
  });
1012
1022
  }
1013
1023
  return await runPostAdmissionManualResume({
1014
1024
  admitted: loaded.admitted,
1015
- env: input.env,
1025
+ env,
1016
1026
  io: input.io,
1017
1027
  adapters,
1018
1028
  ...(input.effectiveEngine === undefined
@@ -1030,6 +1040,11 @@ export async function runPostAdmissionSeatResume(input) {
1030
1040
  }
1031
1041
  throw error;
1032
1042
  }
1043
+ finally {
1044
+ if (preparedCleanup !== undefined) {
1045
+ await preparedCleanup();
1046
+ }
1047
+ }
1033
1048
  }
1034
1049
  /**
1035
1050
  * Shared post-admission one-shot path: folds into runPostAdmissionResumable (#840 / #416).
@@ -0,0 +1,339 @@
1
+ /**
2
+ * Public Reviewer Role run: admit → post-admission coordinator → settle Terminal result (#917 / #517).
3
+ * Package-owned ak-cross-m-review method is forced; users never submit
4
+ * extra packets. Controlled-failure settlement reuses #107.
5
+ * #526: execution via RoleTurnHost; argv is Pi adapter internal.
6
+ */
7
+ import { resolve } from "node:path";
8
+ import { appendEngineSessionMaterial, engineSessionMaterialFromOptions, pickEngineAxis, } from "../package-resources/engine-material.js";
9
+ import { loadPackagedMethodSkillMaterial, resolvePackagedMethodSkillPath, } from "../package-resources/method-skill.js";
10
+ import { CliUsageError } from "./cli-errors.js";
11
+ import { admitReviewerInvocation, buildReviewerTransportPrompt, } from "./invocation.js";
12
+ import { loadResumableReviewerRun, markRunAdmitted, RESUME_TRANSPORT_ENVELOPE, } from "./run-lifecycle.js";
13
+ import { presentStructuralRejection, readEngineDetourInfrastructureFailure, trySettleReviewerTerminalResult, } from "./settlement.js";
14
+ import { formatTerminalResult, isLawfulTypedTerminalOutcome, } from "./terminal.js";
15
+ import { projectRoleTurnRequest, } from "./turn-request.js";
16
+ import { presentControlledFailure, resolveResumeMethodMaterialAdapters, runPostAdmissionResumable, runPostAdmissionSeatResume, resumeTurnRequestProjectionOptions, } from "./post-admission.js";
17
+ function reviewerMethods(packageRoot) {
18
+ return [{ kind: "skill", path: resolvePackagedMethodSkillPath(packageRoot, "ak-cross-m-review") }];
19
+ }
20
+ /**
21
+ * All Reviewer turns (explicit --lens, dual-lens child, resume) execute in a
22
+ * fresh worktree of the source tree's current HEAD (#946 统一新副本). Dual-lens
23
+ * summon already injects executionCwd; only mint when absent. Cleanup failure
24
+ * is diagnostic only and does not flip the exit code. Mint failure propagates
25
+ * so the caller can settle a controlled failure against the admitted run.
26
+ */
27
+ async function runReviewerTurnInFreshCopy(env, projectRoot, io, body) {
28
+ if (env.executionCwd !== undefined) {
29
+ return body(env);
30
+ }
31
+ const { withEphemeralReviewerWorktree } = await import("../public-role-summons.js");
32
+ return await withEphemeralReviewerWorktree({
33
+ projectRoot,
34
+ onCleanupDiagnostic: (diagnostic) => {
35
+ io.stderr(`${diagnostic}\n`);
36
+ },
37
+ run: (executionCwd) => body({ ...env, executionCwd }),
38
+ });
39
+ }
40
+ /** Project admitted Reviewer invocation onto the host-neutral turn request. */
41
+ export function buildReviewerTurnRequest(admitted, options) {
42
+ return projectRoleTurnRequest(admitted, {
43
+ activation: {
44
+ role: "reviewer",
45
+ baseRevision: admitted.baseRevision,
46
+ lens: admitted.lens,
47
+ authorityRefs: admitted.authorityRefs,
48
+ ...(admitted.ticketNumber === undefined ? {} : { ticketNumber: admitted.ticketNumber }),
49
+ },
50
+ methods: reviewerMethods(options.packageRoot),
51
+ }, options);
52
+ }
53
+ function reviewerAdapters(packageRoot, methodMaterial) {
54
+ return {
55
+ trySettle: (admitted, authority, scope) => methodMaterial === undefined
56
+ ? Promise.resolve(undefined)
57
+ : trySettleReviewerTerminalResult(admitted, authority, {
58
+ methodProvenance: methodMaterial.provenance,
59
+ methodSkillPath: methodMaterial.skillPath,
60
+ methodSkillConfiguredPath: resolvePackagedMethodSkillPath(packageRoot, "ak-cross-m-review"),
61
+ }, scope),
62
+ resolveRunnerKnownFailure: async ({ result, sessionFile }) => {
63
+ const infrastructureFailure = await readEngineDetourInfrastructureFailure(sessionFile);
64
+ return infrastructureFailure === undefined
65
+ ? result.knownFailure
66
+ : {
67
+ ...(infrastructureFailure.cause === undefined
68
+ ? {}
69
+ : { cause: infrastructureFailure.cause }),
70
+ diagnostic: infrastructureFailure.diagnostic,
71
+ ...(infrastructureFailure.identity === undefined
72
+ ? {}
73
+ : { identity: infrastructureFailure.identity }),
74
+ };
75
+ },
76
+ };
77
+ }
78
+ async function loadReviewerMethodMaterial(packageRoot) {
79
+ return await loadPackagedMethodSkillMaterial(packageRoot, "ak-cross-m-review");
80
+ }
81
+ /** Continue the existing method turn; frozen axes remain on typed activation fields. */
82
+ function reviewerResumePrompt(env, message) {
83
+ const lines = [RESUME_TRANSPORT_ENVELOPE];
84
+ if (message !== undefined)
85
+ lines.push("", message);
86
+ return appendEngineSessionMaterial(lines, engineSessionMaterialFromOptions({
87
+ ...pickEngineAxis(env),
88
+ packageRoot: env.packageRoot,
89
+ })).join("\n");
90
+ }
91
+ export async function runPublicReviewer(argv, env, io, parseReviewerArgv) {
92
+ let parsed;
93
+ try {
94
+ parsed = parseReviewerArgv(argv);
95
+ }
96
+ catch (error) {
97
+ if (error instanceof CliUsageError) {
98
+ presentStructuralRejection(error, io);
99
+ return { exitCode: 2 };
100
+ }
101
+ throw error;
102
+ }
103
+ // Omitted public lens is the parallel two-axis branch mark; never admitted as a parent run.
104
+ // Each leg reuses this call's public argv and only adds --lens (#946 / 10a).
105
+ if (parsed.lens === undefined) {
106
+ const { summonParallelReviewerLenses } = await import("../public-role-summons.js");
107
+ const children = await summonParallelReviewerLenses({
108
+ argv,
109
+ // Replay argv under the same cwd the single-axis entry would see (10a).
110
+ // Do not substitute the resolved project path — relative --project must
111
+ // not be re-resolved against a shifted cwd.
112
+ cwd: env.cwd,
113
+ projectRoot: resolve(parsed.project ?? env.cwd),
114
+ // Typed base from the public parse — precheck only; child argv stays verbatim.
115
+ baseRevision: parsed.baseRevision,
116
+ home: env.home,
117
+ agentDir: env.agentDir,
118
+ ...(env.credentials === undefined ? {} : { credentials: env.credentials }),
119
+ ...(env.model === undefined ? {} : { model: env.model }),
120
+ ...(env.host === undefined ? {} : { host: env.host }),
121
+ ...(env.engine === undefined ? {} : { engine: env.engine }),
122
+ ...(env.engineModel === undefined ? {} : { engineModel: env.engineModel }),
123
+ packageRoot: env.packageRoot,
124
+ ...(env.signal === undefined ? {} : { signal: env.signal }),
125
+ ...(env.correlationId === undefined ? {} : { correlationId: env.correlationId }),
126
+ roleTurnHost: env.roleTurnHost,
127
+ ...(env.hostAdapters === undefined ? {} : { hostAdapters: env.hostAdapters }),
128
+ principalAuthority: env.principalAuthority,
129
+ ...(env.timeoutMs === undefined ? {} : { timeoutMs: env.timeoutMs }),
130
+ });
131
+ const childResults = [children.completeness, children.correctness];
132
+ const terminals = childResults
133
+ .map((child) => child.terminal)
134
+ .filter((terminal) => terminal !== undefined);
135
+ // ADR 0052 / terminal.ts: lawful typed child terminals (accepted / no_receipt /
136
+ // audit_escalation) are not batch failures. Child exitCode remains the live
137
+ // surface for seal/infrastructure overlays that keep the original Terminal.
138
+ // failedChildren counts only non-lawful/missing child Terminals; seal overlays
139
+ // that flip exitCode while keeping lawful Terminals are a separate identity.
140
+ const failedChildren = childResults.filter((child) => child.terminal === undefined
141
+ || !isLawfulTypedTerminalOutcome(child.terminal.roleOutcome)).length;
142
+ const overlayExit = childResults.some((child) => child.exitCode !== 0);
143
+ const failed = failedChildren > 0 || overlayExit;
144
+ const overlayDiagnostics = [...new Set(childResults
145
+ .map((child) => child.stderr)
146
+ .filter((text) => typeof text === "string" && text !== ""))];
147
+ const terminal = {
148
+ batch: "reviewer",
149
+ roleOutcome: failed
150
+ ? {
151
+ kind: "failure",
152
+ role: "reviewer",
153
+ diagnostic: failedChildren > 0
154
+ ? "Reviewer batch child failure"
155
+ : (overlayDiagnostics[0] ?? "Reviewer batch infrastructure failure"),
156
+ decisiveFacts: { failedChildren },
157
+ payloads: terminals,
158
+ }
159
+ : { kind: "accepted", role: "reviewer", payloads: terminals },
160
+ reviewerChildren: {
161
+ ...(children.completeness.terminal === undefined ? {} : { completeness: children.completeness.terminal }),
162
+ ...(children.correctness.terminal === undefined ? {} : { correctness: children.correctness.terminal }),
163
+ },
164
+ reviewerChildOutcomes: {
165
+ completeness: { exitCode: children.completeness.exitCode, ...(children.completeness.stderr === undefined ? {} : { stderr: children.completeness.stderr }) },
166
+ correctness: { exitCode: children.correctness.exitCode, ...(children.correctness.stderr === undefined ? {} : { stderr: children.correctness.stderr }) },
167
+ },
168
+ // Batch has no parent run and no own attendance; no-advice is affirmative
169
+ // only (navigator-attendance.ts). Children carry their own navigator facts.
170
+ navigator: {
171
+ disposition: "unavailable",
172
+ source: "unknown",
173
+ reason: "Reviewer batch has no parent-run Navigator attendance",
174
+ },
175
+ artifacts: terminals.flatMap((item) => item.artifacts),
176
+ };
177
+ io.stdout(formatTerminalResult(terminal));
178
+ return { exitCode: failed ? 1 : 0, terminal };
179
+ }
180
+ let admitted;
181
+ try {
182
+ admitted = await admitReviewerInvocation({
183
+ home: env.home,
184
+ principalAuthority: env.principalAuthority,
185
+ cwd: env.cwd,
186
+ instruction: parsed.instruction,
187
+ attachmentPaths: parsed.attachmentPaths,
188
+ baseRevision: parsed.baseRevision,
189
+ lens: parsed.lens,
190
+ authorityRefs: parsed.authorityRefs,
191
+ ...(parsed.project === undefined ? {} : { project: parsed.project }),
192
+ ...(env.createRunId === undefined ? {} : { createRunId: env.createRunId }),
193
+ ...(env.correlationId === undefined ? {} : { correlationId: env.correlationId }),
194
+ ...(env.model === undefined ? {} : { model: env.model }),
195
+ });
196
+ }
197
+ catch (error) {
198
+ if (error instanceof CliUsageError) {
199
+ presentStructuralRejection(error, io);
200
+ return { exitCode: 2 };
201
+ }
202
+ throw error;
203
+ }
204
+ await markRunAdmitted(admitted, env.principalAuthority);
205
+ let methodMaterial;
206
+ try {
207
+ methodMaterial = await loadReviewerMethodMaterial(env.packageRoot);
208
+ }
209
+ catch (error) {
210
+ return (await presentControlledFailure(admitted, {
211
+ timedOut: false,
212
+ code: null,
213
+ stderr: "",
214
+ thrown: error,
215
+ }, reviewerAdapters(env.packageRoot), env.principalAuthority, io));
216
+ }
217
+ // Fresh copy for this call (explicit --lens or dual-lens child with pre-set cwd).
218
+ // Durable projectRoot stays the caller project; only the host turn uses the sandbox.
219
+ try {
220
+ return await runReviewerTurnInFreshCopy(env, admitted.projectRoot, io, async (sandboxedEnv) => await runPostAdmissionResumable({
221
+ admitted,
222
+ env: sandboxedEnv,
223
+ io,
224
+ buildInitialRequest: () => buildReviewerTurnRequest(admitted, {
225
+ packageRoot: sandboxedEnv.packageRoot,
226
+ home: sandboxedEnv.home,
227
+ agentDir: sandboxedEnv.agentDir,
228
+ ...(sandboxedEnv.model === undefined ? {} : { model: sandboxedEnv.model }),
229
+ ...pickEngineAxis(sandboxedEnv),
230
+ ...(sandboxedEnv.timeoutMs === undefined ? {} : { timeoutMs: sandboxedEnv.timeoutMs }),
231
+ ...(admitted.correlationId === undefined && sandboxedEnv.correlationId === undefined
232
+ ? {}
233
+ : { correlationId: admitted.correlationId ?? sandboxedEnv.correlationId }),
234
+ // Ephemeral worktree cwd; durable projectRoot stays on admitted caller project.
235
+ ...(sandboxedEnv.executionCwd === undefined ? {} : { cwd: sandboxedEnv.executionCwd }),
236
+ continuation: {
237
+ kind: "initial",
238
+ prompt: buildReviewerTransportPrompt(admitted, engineSessionMaterialFromOptions({
239
+ ...pickEngineAxis(sandboxedEnv),
240
+ packageRoot: sandboxedEnv.packageRoot,
241
+ })),
242
+ },
243
+ }),
244
+ buildResumeRequest: () => buildReviewerTurnRequest(admitted, {
245
+ packageRoot: sandboxedEnv.packageRoot,
246
+ home: sandboxedEnv.home,
247
+ agentDir: sandboxedEnv.agentDir,
248
+ ...(sandboxedEnv.model === undefined ? {} : { model: sandboxedEnv.model }),
249
+ ...pickEngineAxis(sandboxedEnv),
250
+ ...(sandboxedEnv.timeoutMs === undefined ? {} : { timeoutMs: sandboxedEnv.timeoutMs }),
251
+ ...(admitted.correlationId === undefined && sandboxedEnv.correlationId === undefined
252
+ ? {}
253
+ : { correlationId: admitted.correlationId ?? sandboxedEnv.correlationId }),
254
+ // In-batch auto-resume keeps the same call's sandbox (dual-lens or single).
255
+ ...(sandboxedEnv.executionCwd === undefined ? {} : { cwd: sandboxedEnv.executionCwd }),
256
+ continuation: {
257
+ kind: "resume",
258
+ prompt: reviewerResumePrompt(sandboxedEnv),
259
+ },
260
+ }),
261
+ adapters: reviewerAdapters(sandboxedEnv.packageRoot, methodMaterial),
262
+ ...(sandboxedEnv.engine === undefined ? {} : { effectiveEngine: sandboxedEnv.engine }),
263
+ }));
264
+ }
265
+ catch (error) {
266
+ return (await presentControlledFailure(admitted, {
267
+ timedOut: false,
268
+ code: null,
269
+ stderr: "",
270
+ thrown: error,
271
+ }, reviewerAdapters(env.packageRoot, methodMaterial), env.principalAuthority, io));
272
+ }
273
+ }
274
+ /**
275
+ * Resume a previously admitted Reviewer Role run after a typed HTTP 429.
276
+ * Restores task/base/session identity; model override is temporary.
277
+ * Execution always uses a fresh worktree of the source tree at resume time
278
+ * (#946 统一新副本 / 10a) — no old worktree, no ownership record.
279
+ * Fresh-copy mint reuses the coordinator's single pre-lease load via
280
+ * afterAdmittedPrepare; does not pre-load outside the coordinator.
281
+ */
282
+ export async function runPublicReviewerResume(request, env, io) {
283
+ // Call-local cell: afterAdmittedPrepare writes executionCwd; buildTurnRequest reads it.
284
+ // Keeps the single-load coordinator contract — no preliminary load outside.
285
+ const sandbox = {
286
+ ...(env.executionCwd === undefined ? {} : { executionCwd: env.executionCwd }),
287
+ };
288
+ return await runPostAdmissionSeatResume({
289
+ request,
290
+ env,
291
+ io,
292
+ load: (effective) => loadResumableReviewerRun(env.home, effective.runId, env.principalAuthority),
293
+ buildTurnRequest: (admitted, effective) => {
294
+ const activeEnv = sandbox.executionCwd === undefined
295
+ ? env
296
+ : { ...env, executionCwd: sandbox.executionCwd };
297
+ const base = resumeTurnRequestProjectionOptions(admitted, effective, activeEnv);
298
+ return buildReviewerTurnRequest(admitted, {
299
+ ...base,
300
+ // Fresh copy at resume time; durable projectRoot unchanged.
301
+ ...(sandbox.executionCwd === undefined ? {} : { cwd: sandbox.executionCwd }),
302
+ continuation: {
303
+ kind: "resume",
304
+ prompt: reviewerResumePrompt(activeEnv, effective.message),
305
+ },
306
+ });
307
+ },
308
+ adapters: reviewerAdapters(env.packageRoot),
309
+ afterAdmittedLoad: async (admitted) => {
310
+ return resolveResumeMethodMaterialAdapters({
311
+ admitted,
312
+ authority: env.principalAuthority,
313
+ io,
314
+ loadMaterial: () => loadReviewerMethodMaterial(env.packageRoot),
315
+ adaptersWith: (material) => reviewerAdapters(env.packageRoot, material),
316
+ emptyAdapters: reviewerAdapters(env.packageRoot),
317
+ });
318
+ },
319
+ afterAdmittedPrepare: async (admitted) => {
320
+ // Already sandboxed (in-batch auto-resume path) — nothing to mint.
321
+ if (sandbox.executionCwd !== undefined) {
322
+ return { env: { ...env, executionCwd: sandbox.executionCwd } };
323
+ }
324
+ const { openEphemeralReviewerWorktree } = await import("../public-role-summons.js");
325
+ const opened = await openEphemeralReviewerWorktree({
326
+ projectRoot: admitted.projectRoot,
327
+ onCleanupDiagnostic: (diagnostic) => {
328
+ io.stderr(`${diagnostic}\n`);
329
+ },
330
+ });
331
+ sandbox.executionCwd = opened.executionCwd;
332
+ return {
333
+ env: { ...env, executionCwd: opened.executionCwd },
334
+ cleanup: opened.close,
335
+ };
336
+ },
337
+ ...(env.engine === undefined ? {} : { effectiveEngine: env.engine }),
338
+ });
339
+ }
@@ -107,6 +107,15 @@ export function formatTerminalResult(result) {
107
107
  lines.push(`gate-round\t${round.roundIndex}\t${round.dispatch.kind}\t${round.dispatch.officer}\t${reason}\t${round.officer.seat}\t${encodeTerminalField(round.officer.status)}\t${encodeTerminalField(JSON.stringify(round.officer.findings))}`);
108
108
  }
109
109
  }
110
+ if (result.reviewerChildOutcomes !== undefined) {
111
+ for (const axis of ["completeness", "correctness"]) {
112
+ const child = result.reviewerChildOutcomes[axis];
113
+ lines.push(`reviewer-child\t${axis}\t${child.exitCode}`);
114
+ if (child.stderr !== undefined && child.stderr !== "") {
115
+ lines.push(`reviewer-child-diagnostic\t${axis}\t${encodeTerminalField(child.stderr)}`);
116
+ }
117
+ }
118
+ }
110
119
  if (result.resume !== undefined) {
111
120
  // Resumable failure: run ID is revealed only inside the complete resume command.
112
121
  lines.push(`resume\t${encodeTerminalField(result.resume.command)}`);
@@ -1,6 +1,6 @@
1
1
  import { pickEngineAxis } from "../package-resources/engine-material.js";
2
2
  export function projectRoleTurnRequest(admitted, roleDetails, options) {
3
- const cwd = admitted.projectRoot ?? admitted.repoRoot ?? admitted.cwd;
3
+ const cwd = options.cwd ?? admitted.projectRoot ?? admitted.repoRoot ?? admitted.cwd;
4
4
  if (cwd === undefined)
5
5
  throw new Error("admitted invocation missing working directory");
6
6
  if (admitted.principal === undefined)