@akagilnc/pi-workflow-roles 0.1.4528 → 0.1.4620

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.
Files changed (55) hide show
  1. package/README.md +5 -4
  2. package/README.zh-CN.md +6 -5
  3. package/dist/acp-host/production-host.js +1834 -637
  4. package/dist/archivist-record-topology.js +27 -35
  5. package/dist/headless-host/description.js +7 -4
  6. package/dist/headless-host/production-host.js +1652 -519
  7. package/dist/migrate-book-topology.js +8 -11
  8. package/dist/navigator-attendance.js +146 -241
  9. package/dist/navigator-public-session.js +81 -15
  10. package/dist/package-contracts/navigator-output.js +30 -12
  11. package/dist/public-cli/invocation.js +10 -4
  12. package/dist/public-cli/judge-run.js +2 -2
  13. package/dist/public-cli/main.js +879 -410
  14. package/dist/public-cli/option-definitions.js +6 -6
  15. package/dist/public-cli/post-admission.js +25 -10
  16. package/dist/public-cli/reviewer-run.js +339 -0
  17. package/dist/public-cli/run-lifecycle.js +29 -7
  18. package/dist/public-cli/settlement.js +46 -32
  19. package/dist/public-cli/terminal.js +17 -27
  20. package/dist/public-cli/turn-request.js +1 -1
  21. package/dist/public-role-summons.js +370 -20
  22. package/dist/submission-ledger.js +56 -23
  23. package/package.json +1 -1
  24. package/resources/836-deleted-machine-instruction-inventory.md +2 -0
  25. package/resources/navigator-route-playbook.md +9 -0
  26. package/scripts/build-package.mjs +6 -1
  27. package/src/acp-host/role-turn-host.ts +129 -13
  28. package/src/headless-host/description.ts +17 -7
  29. package/src/headless-host/role-turn-host.ts +42 -11
  30. package/src/host-contracts.ts +0 -1
  31. package/src/navigator-attendance.ts +290 -425
  32. package/src/navigator-public-session.ts +126 -25
  33. package/src/navigator-role.ts +2 -2
  34. package/src/package-contracts/navigator-output.ts +41 -25
  35. package/src/public-cli/coder-run.ts +2 -2
  36. package/src/public-cli/fixer-run.ts +2 -2
  37. package/src/public-cli/invocation.ts +19 -9
  38. package/src/public-cli/judge-run.ts +2 -2
  39. package/src/public-cli/merger-run.ts +2 -2
  40. package/src/public-cli/option-definitions.ts +6 -6
  41. package/src/public-cli/post-admission.ts +41 -10
  42. package/src/public-cli/reviewer-run.ts +238 -53
  43. package/src/public-cli/run-lifecycle.ts +37 -7
  44. package/src/public-cli/settlement.ts +42 -34
  45. package/src/public-cli/terminal.ts +39 -37
  46. package/src/public-cli/turn-request.ts +7 -1
  47. package/src/public-role-summons.ts +544 -22
  48. package/src/reviewer-role.ts +1 -2
  49. package/src/role-envelope.ts +89 -4
  50. package/src/role-runtime.ts +76 -16
  51. package/src/submission-ledger.ts +63 -21
  52. package/dist/public-cli/command-renderer.js +0 -5
  53. package/dist/public-command-renderer.js +0 -29
  54. package/src/public-cli/command-renderer.ts +0 -9
  55. package/src/public-command-renderer.ts +0 -55
@@ -1,11 +1,3 @@
1
- /**
2
- * Terminal result: typed semantic regions for one admitted Role run (ADR 0052 / #106).
3
- * Presentation may rearrange labels/order; only regions and typed facts are stable.
4
- *
5
- * Free-text cells are JSON-string encoded so legitimate newlines/tabs cannot forge
6
- * extra rows or shift column boundaries (note / fix.summary / decision question / reason).
7
- */
8
- import { isPublicCallableRole, renderPublicAkRoleCommand, } from "./command-renderer.js";
9
1
  /** Encode one free-text Terminal cell. JSON string form cannot embed raw tab/newline. */
10
2
  export function encodeTerminalField(value) {
11
3
  return JSON.stringify(value);
@@ -34,21 +26,13 @@ export function roleResultPayloads(outcome) {
34
26
  return [];
35
27
  }
36
28
  /**
37
- * Build a recommendation navigator fact. Model command is kept when present;
38
- * registry render is fallback only. Unknown seats stay recommendations (#836 B4.1).
29
+ * Build an advice navigator fact (#959). Prose is presented as submitted;
30
+ * code does not parse route/next or judge usability.
39
31
  */
40
- export function recommendationNavigatorFact(input) {
41
- const command = typeof input.modelCommand === "string" && input.modelCommand.trim() !== ""
42
- ? input.modelCommand
43
- : isPublicCallableRole(input.next.role)
44
- ? renderPublicAkRoleCommand(input.next)
45
- : undefined;
32
+ export function adviceNavigatorFact(input) {
46
33
  return {
47
- disposition: "recommendation",
48
- next: input.next,
49
- reason: input.reason,
50
- ...(command === undefined ? {} : { command }),
51
- ...(input.route === undefined ? {} : { route: input.route }),
34
+ disposition: "advice",
35
+ prose: input.prose,
52
36
  ...(input.advisoryDiagnostic === undefined ? {} : { advisoryDiagnostic: input.advisoryDiagnostic }),
53
37
  };
54
38
  }
@@ -82,12 +66,9 @@ export function formatTerminalResult(result) {
82
66
  if (result.navigator.advisoryDiagnostic !== undefined) {
83
67
  lines.push(`navigator-advisory\t${encodeTerminalField(result.navigator.advisoryDiagnostic)}`);
84
68
  }
85
- if (result.navigator.disposition === "recommendation") {
86
- lines.push(`next\t${result.navigator.next.role}\t${result.navigator.next.phase ?? "none"}`);
87
- lines.push(`reason\t${encodeTerminalField(result.navigator.reason)}`);
88
- if (result.navigator.command !== undefined) {
89
- lines.push(`command\t${encodeTerminalField(result.navigator.command)}`);
90
- }
69
+ if (result.navigator.disposition === "advice") {
70
+ // #959: present navigator prose as-is — no next/reason/command parsing.
71
+ lines.push(`prose\t${encodeTerminalField(result.navigator.prose)}`);
91
72
  }
92
73
  else if (result.navigator.disposition === "unavailable") {
93
74
  lines.push(`unavailable\t${result.navigator.source}\t${encodeTerminalField(result.navigator.reason)}`);
@@ -107,6 +88,15 @@ export function formatTerminalResult(result) {
107
88
  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
89
  }
109
90
  }
91
+ if (result.reviewerChildOutcomes !== undefined) {
92
+ for (const axis of ["completeness", "correctness"]) {
93
+ const child = result.reviewerChildOutcomes[axis];
94
+ lines.push(`reviewer-child\t${axis}\t${child.exitCode}`);
95
+ if (child.stderr !== undefined && child.stderr !== "") {
96
+ lines.push(`reviewer-child-diagnostic\t${axis}\t${encodeTerminalField(child.stderr)}`);
97
+ }
98
+ }
99
+ }
110
100
  if (result.resume !== undefined) {
111
101
  // Resumable failure: run ID is revealed only inside the complete resume command.
112
102
  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)
@@ -1,7 +1,12 @@
1
+ import { execFile } from "node:child_process";
1
2
  import { existsSync } from "node:fs";
2
- import { join } from "node:path";
3
+ import { mkdir, mkdtemp, realpath, rm } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { join, relative, sep } from "node:path";
6
+ import { promisify } from "node:util";
3
7
  import { pickEngineAxis } from "./package-resources/engine-material.js";
4
8
  const AK_ROLE_PACKAGE_ROOT_ENV = "AK_ROLE_PACKAGE_ROOT";
9
+ const execFileAsync = promisify(execFile);
5
10
  function createCapturingIo() {
6
11
  const chunks = [];
7
12
  return {
@@ -89,13 +94,14 @@ async function createSummonEnv(options, afterHost) {
89
94
  import("./public-cli/role-turn-host-resolution.js"),
90
95
  import("./public-cli/config.js")
91
96
  ]);
92
- const principalAuthority = piDurablePrincipalAuthority;
97
+ const principalAuthority = options.principalAuthority ?? piDurablePrincipalAuthority;
93
98
  const roleTurnHost = resolveRoleTurnHost(
94
99
  {
95
100
  packageRoot: options.packageRoot,
96
101
  ...options.roleTurnHost === void 0 ? {} : { roleTurnHost: options.roleTurnHost },
97
102
  ...options.hostAdapters === void 0 ? {} : { hostAdapters: options.hostAdapters },
98
- ...options.extraPiArgs === void 0 || options.extraPiArgs.length === 0 ? {} : { extraPiArgs: options.extraPiArgs }
103
+ ...options.extraPiArgs === void 0 || options.extraPiArgs.length === 0 ? {} : { extraPiArgs: options.extraPiArgs },
104
+ ...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs }
99
105
  },
100
106
  { role: options.role, seat: options.seat, principalAuthority }
101
107
  );
@@ -108,13 +114,16 @@ async function createSummonEnv(options, afterHost) {
108
114
  };
109
115
  }
110
116
  const hostName = seatWithModel.host ?? "pi";
111
- const { loadHostProvidersTable, projectHostFacingProvider } = await import("./public-cli/host-providers.js");
112
- const hostFacingSelection = projectHostFacingProvider(
113
- seatWithModel.selection,
114
- hostName,
115
- loadHostProvidersTable(options.home),
116
- options.home
117
- );
117
+ let hostFacingSelection = options.hostFacingModel;
118
+ if (hostFacingSelection === void 0) {
119
+ const { loadHostProvidersTable, projectHostFacingProvider } = await import("./public-cli/host-providers.js");
120
+ hostFacingSelection = projectHostFacingProvider(
121
+ seatWithModel.selection,
122
+ hostName,
123
+ loadHostProvidersTable(options.home),
124
+ options.home
125
+ );
126
+ }
118
127
  return {
119
128
  ok: true,
120
129
  env: {
@@ -128,7 +137,8 @@ async function createSummonEnv(options, afterHost) {
128
137
  credentials: options.credentials,
129
138
  ...hostFacingSelection === void 0 ? {} : { model: hostFacingSelection },
130
139
  ...projectSeatEngine(seatWithModel),
131
- ...projectSeatHost(seatWithModel)
140
+ ...projectSeatHost(seatWithModel),
141
+ ...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs }
132
142
  }
133
143
  };
134
144
  }
@@ -143,7 +153,19 @@ async function summonPublicRole(options) {
143
153
  } = await import("./public-cli/config.js");
144
154
  const credentials = options.credentials ?? await loadCredentialProviders(agentDir);
145
155
  const config = await loadPublicCliConfig(home);
146
- const seat = resolveEffectiveSeat(config, options.role, credentials);
156
+ const resolvedSeat = resolveEffectiveSeat(config, options.role, credentials);
157
+ const inheritedSelection = options.model === void 0 ? void 0 : {
158
+ provider: options.model.provider,
159
+ model: options.model.model,
160
+ ...options.model.thinking === void 0 ? {} : { thinking: options.model.thinking }
161
+ };
162
+ const seat = {
163
+ ...resolvedSeat,
164
+ ...inheritedSelection === void 0 ? {} : { selection: inheritedSelection, source: "invocation" },
165
+ ...options.host === void 0 ? {} : { host: options.host, hostSource: "invocation" },
166
+ ...options.engine === void 0 ? {} : { engine: options.engine, engineSource: "invocation" },
167
+ ...options.engineModel === void 0 ? {} : { engineModel: options.engineModel }
168
+ };
147
169
  const captured = options.io === void 0 ? createCapturingIo() : void 0;
148
170
  const io = options.io ?? captured.io;
149
171
  const summonEnvInput = {
@@ -156,7 +178,11 @@ async function summonPublicRole(options) {
156
178
  seat,
157
179
  ...options.extraPiArgs === void 0 ? {} : { extraPiArgs: options.extraPiArgs },
158
180
  ...options.roleTurnHost === void 0 ? {} : { roleTurnHost: options.roleTurnHost },
159
- ...options.hostAdapters === void 0 ? {} : { hostAdapters: options.hostAdapters }
181
+ ...options.hostAdapters === void 0 ? {} : { hostAdapters: options.hostAdapters },
182
+ ...options.principalAuthority === void 0 ? {} : { principalAuthority: options.principalAuthority },
183
+ ...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs },
184
+ // Parent model is already host-facing; child must not project again.
185
+ ...options.model === void 0 ? {} : { hostFacingModel: options.model }
160
186
  };
161
187
  async function prepareSummonEnv(afterHost) {
162
188
  try {
@@ -168,7 +194,9 @@ async function summonPublicRole(options) {
168
194
  ok: true,
169
195
  env: {
170
196
  ...built.env,
171
- stationChild: true,
197
+ // Nested court stations keep station-child semantics; dual-lens
198
+ // ordinary Reviewer axes opt out (#946 / ADR 0082).
199
+ ...options.stationChild === false ? {} : { stationChild: true },
172
200
  // Forward composition-root adapters so nested court stations (e.g.
173
201
  // countersign → diarist) select the same faux/production table (#924).
174
202
  ...options.hostAdapters === void 0 ? {} : { hostAdapters: options.hostAdapters },
@@ -178,7 +206,8 @@ async function summonPublicRole(options) {
178
206
  ...options.gateReviewInstruction === void 0 ? {} : { gateReviewInstruction: options.gateReviewInstruction },
179
207
  ...options.boundTicketNumber === void 0 ? {} : { boundTicketNumber: options.boundTicketNumber },
180
208
  ...options.correlationId === void 0 || options.correlationId.trim() === "" ? {} : { correlationId: options.correlationId },
181
- ...options.createRunId === void 0 ? {} : { createRunId: options.createRunId }
209
+ ...options.createRunId === void 0 ? {} : { createRunId: options.createRunId },
210
+ ...options.executionCwd === void 0 ? {} : { executionCwd: options.executionCwd }
182
211
  }
183
212
  };
184
213
  } catch (error) {
@@ -240,14 +269,32 @@ async function summonPublicRole(options) {
240
269
  case "navigator":
241
270
  case "gatekeeper": {
242
271
  const seat2 = options.role;
243
- const [{ runPublicInstructionSeat }, invocation] = await Promise.all([
272
+ const [{ runPublicInstructionSeat, runPublicInstructionSeatResume }, invocation] = await Promise.all([
244
273
  import("./public-cli/instruction-seat-run.js"),
245
274
  import("./public-cli/invocation.js")
246
275
  ]);
247
276
  const parse = seat2 === "auditor" ? invocation.parseAuditorArgv : seat2 === "navigator" ? invocation.parseNavigatorArgv : invocation.parseGatekeeperArgv;
248
- const stepped = await runPrepared(parse, (env, once) => runPublicInstructionSeat(options.argv, env, io, seat2, once));
249
- if ("fail" in stepped) return stepped.fail;
250
- result = stepped.ok;
277
+ const resumeRunId = typeof options.resumeRunId === "string" && options.resumeRunId.trim() !== "" ? options.resumeRunId.trim() : void 0;
278
+ if (resumeRunId !== void 0) {
279
+ const instruction = options.argv[0] ?? "";
280
+ const stepped = await runPrepared(parse, (env) => runPublicInstructionSeatResume(
281
+ {
282
+ runId: resumeRunId,
283
+ summons: {
284
+ instruction,
285
+ instructionEmpty: instruction.trim() === ""
286
+ }
287
+ },
288
+ env,
289
+ io
290
+ ));
291
+ if ("fail" in stepped) return stepped.fail;
292
+ result = stepped.ok;
293
+ } else {
294
+ const stepped = await runPrepared(parse, (env, once) => runPublicInstructionSeat(options.argv, env, io, seat2, once));
295
+ if ("fail" in stepped) return stepped.fail;
296
+ result = stepped.ok;
297
+ }
251
298
  break;
252
299
  }
253
300
  case "judge": {
@@ -290,6 +337,16 @@ async function summonPublicRole(options) {
290
337
  result = stepped.ok;
291
338
  break;
292
339
  }
340
+ case "reviewer": {
341
+ const [{ runPublicReviewer }, { parseReviewerArgv }] = await Promise.all([
342
+ import("./public-cli/reviewer-run.js"),
343
+ import("./public-cli/invocation.js")
344
+ ]);
345
+ const stepped = await runPrepared(parseReviewerArgv, (env, once) => runPublicReviewer(options.argv, env, io, once));
346
+ if ("fail" in stepped) return stepped.fail;
347
+ result = stepped.ok;
348
+ break;
349
+ }
293
350
  }
294
351
  const stderr = captured?.stderrText();
295
352
  const runDirectory = result.admitted?.runDirectory;
@@ -301,6 +358,296 @@ async function summonPublicRole(options) {
301
358
  ...stderr === void 0 || stderr === "" ? {} : { stderr }
302
359
  };
303
360
  }
361
+ function withReviewerLens(argv, lens) {
362
+ const separator = argv.indexOf("--");
363
+ if (separator === -1) return [...argv, "--lens", lens];
364
+ return [...argv.slice(0, separator), "--lens", lens, ...argv.slice(separator)];
365
+ }
366
+ async function resolveReviewerWorktreeRoots(projectRoot) {
367
+ const callerProjectRoot = await realpath(projectRoot);
368
+ const sourceProjectRoot = await realpath((await execFileAsync(
369
+ "git",
370
+ ["rev-parse", "--show-toplevel"],
371
+ { cwd: callerProjectRoot }
372
+ )).stdout.trim());
373
+ const callerRelative = relative(sourceProjectRoot, callerProjectRoot);
374
+ const projectRelative = callerRelative === "" || callerRelative === "." || callerRelative.startsWith(`..${sep}`) || callerRelative === ".." ? "" : callerRelative;
375
+ return { callerProjectRoot, sourceProjectRoot, projectRelative };
376
+ }
377
+ function reviewerSandboxPath(worktreeAxis, projectRelative) {
378
+ return projectRelative === "" ? worktreeAxis : join(worktreeAxis, projectRelative);
379
+ }
380
+ async function openEphemeralReviewerWorktree(options) {
381
+ const { sourceProjectRoot, projectRelative } = await resolveReviewerWorktreeRoots(
382
+ options.projectRoot
383
+ );
384
+ const { stdout: headStdout } = await execFileAsync(
385
+ "git",
386
+ ["rev-parse", "--verify", "HEAD^{commit}"],
387
+ { cwd: sourceProjectRoot }
388
+ );
389
+ const targetCommit = headStdout.trim();
390
+ const root = await mkdtemp(join(tmpdir(), "ak-reviewer-sandbox-"));
391
+ const worktreeRoot = join(root, "work");
392
+ let registered = false;
393
+ const rollback = async (cause) => {
394
+ const cleanupErrors = [];
395
+ if (registered) {
396
+ try {
397
+ await execFileAsync("git", ["worktree", "remove", "--force", worktreeRoot], {
398
+ cwd: sourceProjectRoot
399
+ });
400
+ } catch (error) {
401
+ cleanupErrors.push(error);
402
+ }
403
+ }
404
+ try {
405
+ await rm(root, { recursive: true, force: true });
406
+ } catch (error) {
407
+ cleanupErrors.push(error);
408
+ }
409
+ if (cleanupErrors.length > 0) {
410
+ options.onCleanupDiagnostic?.([
411
+ "reviewer worktree cleanup failed",
412
+ ...cleanupErrors.map((error) => error instanceof Error ? error.message : String(error))
413
+ ].join("\n"));
414
+ }
415
+ throw cause;
416
+ };
417
+ try {
418
+ await execFileAsync("git", ["worktree", "add", "--detach", worktreeRoot, targetCommit], {
419
+ cwd: sourceProjectRoot
420
+ });
421
+ registered = true;
422
+ if (projectRelative !== "") {
423
+ await mkdir(reviewerSandboxPath(worktreeRoot, projectRelative), { recursive: true });
424
+ }
425
+ } catch (error) {
426
+ await rollback(error);
427
+ }
428
+ let closed = false;
429
+ return {
430
+ executionCwd: reviewerSandboxPath(worktreeRoot, projectRelative),
431
+ close: async () => {
432
+ if (closed) return;
433
+ closed = true;
434
+ const cleanupErrors = [];
435
+ try {
436
+ await execFileAsync("git", ["worktree", "remove", worktreeRoot], {
437
+ cwd: sourceProjectRoot
438
+ });
439
+ } catch (error) {
440
+ cleanupErrors.push(error);
441
+ }
442
+ try {
443
+ await rm(root, { recursive: true, force: true });
444
+ } catch (error) {
445
+ cleanupErrors.push(error);
446
+ }
447
+ if (cleanupErrors.length > 0) {
448
+ options.onCleanupDiagnostic?.([
449
+ "reviewer worktree cleanup failed",
450
+ ...cleanupErrors.map((error) => error instanceof Error ? error.message : String(error))
451
+ ].join("\n"));
452
+ }
453
+ }
454
+ };
455
+ }
456
+ async function withEphemeralReviewerWorktree(options) {
457
+ const sandbox = await openEphemeralReviewerWorktree({
458
+ projectRoot: options.projectRoot,
459
+ ...options.onCleanupDiagnostic === void 0 ? {} : { onCleanupDiagnostic: options.onCleanupDiagnostic }
460
+ });
461
+ try {
462
+ return await options.run(sandbox.executionCwd);
463
+ } finally {
464
+ await sandbox.close();
465
+ }
466
+ }
467
+ async function summonParallelReviewerLenses(options) {
468
+ const describeFailure = (error) => {
469
+ if (error instanceof AggregateError) {
470
+ return [error.message, ...error.errors.map(describeFailure)].join("\n");
471
+ }
472
+ return error instanceof Error ? error.message : String(error);
473
+ };
474
+ const failedResult = (error) => ({
475
+ exitCode: 1,
476
+ stderr: describeFailure(error)
477
+ });
478
+ const dualFailure = (error) => {
479
+ const failure = failedResult(error);
480
+ return { completeness: failure, correctness: failure };
481
+ };
482
+ let sourceProjectRoot;
483
+ let projectRelative;
484
+ let targetCommit;
485
+ const statusArgs = [
486
+ "status",
487
+ "--porcelain=v1",
488
+ "--untracked-files=all",
489
+ "--",
490
+ ":/",
491
+ ":(top,exclude).claude/worktrees/**"
492
+ ];
493
+ try {
494
+ const roots = await resolveReviewerWorktreeRoots(options.projectRoot);
495
+ sourceProjectRoot = roots.sourceProjectRoot;
496
+ projectRelative = roots.projectRelative;
497
+ const { stdout: statusStdout } = await execFileAsync("git", statusArgs, {
498
+ cwd: sourceProjectRoot
499
+ });
500
+ if (statusStdout !== "") {
501
+ const diagnostic = [
502
+ "Reviewer target status gate failed:",
503
+ "git status --porcelain=v1 --untracked-files=all -- :/ ':(top,exclude).claude/worktrees/**'",
504
+ statusStdout
505
+ ].join("\n");
506
+ return dualFailure(new Error(diagnostic));
507
+ }
508
+ const { stdout: targetStdout } = await execFileAsync(
509
+ "git",
510
+ ["rev-parse", "--verify", "HEAD^{commit}"],
511
+ { cwd: sourceProjectRoot }
512
+ );
513
+ targetCommit = targetStdout.trim();
514
+ await execFileAsync(
515
+ "git",
516
+ ["rev-parse", "--verify", `${options.baseRevision}^{commit}`],
517
+ { cwd: sourceProjectRoot }
518
+ );
519
+ } catch (error) {
520
+ return dualFailure(error);
521
+ }
522
+ const root = await mkdtemp(join(tmpdir(), "ak-reviewer-lenses-"));
523
+ const completenessRoot = join(root, "completeness");
524
+ const correctnessRoot = join(root, "correctness");
525
+ const worktrees = [completenessRoot, correctnessRoot];
526
+ const created = /* @__PURE__ */ new Set();
527
+ let results;
528
+ const creation = await Promise.allSettled(
529
+ worktrees.map(async (path) => {
530
+ await execFileAsync("git", ["worktree", "add", "--detach", path, targetCommit], {
531
+ cwd: sourceProjectRoot
532
+ });
533
+ created.add(path);
534
+ if (projectRelative !== "") {
535
+ await mkdir(reviewerSandboxPath(path, projectRelative), { recursive: true });
536
+ }
537
+ })
538
+ );
539
+ const creationFailures = creation.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
540
+ if (creationFailures.length > 0) {
541
+ const failure = failedResult(new AggregateError(
542
+ creationFailures,
543
+ "parallel reviewer worktree creation failed"
544
+ ));
545
+ results = { completeness: failure, correctness: failure };
546
+ } else {
547
+ const summon = (lens, worktreeAxis) => {
548
+ const sandbox = reviewerSandboxPath(worktreeAxis, projectRelative);
549
+ return summonPublicRole({
550
+ role: "reviewer",
551
+ argv: withReviewerLens(options.argv, lens),
552
+ cwd: options.cwd,
553
+ executionCwd: sandbox,
554
+ // Ordinary single-axis public semantics — not a nested court station.
555
+ stationChild: false,
556
+ home: options.home,
557
+ ...options.agentDir === void 0 ? {} : { agentDir: options.agentDir },
558
+ ...options.credentials === void 0 ? {} : { credentials: options.credentials },
559
+ ...options.model === void 0 ? {} : { model: options.model },
560
+ ...options.host === void 0 ? {} : { host: options.host },
561
+ ...options.engine === void 0 ? {} : { engine: options.engine },
562
+ ...options.engineModel === void 0 ? {} : { engineModel: options.engineModel },
563
+ ...options.signal === void 0 ? {} : { signal: options.signal },
564
+ ...options.correlationId === void 0 ? {} : { correlationId: options.correlationId },
565
+ ...options.packageRoot === void 0 ? {} : { packageRoot: options.packageRoot },
566
+ ...options.roleTurnHost === void 0 ? {} : { roleTurnHost: options.roleTurnHost },
567
+ ...options.hostAdapters === void 0 ? {} : { hostAdapters: options.hostAdapters },
568
+ ...options.principalAuthority === void 0 ? {} : { principalAuthority: options.principalAuthority },
569
+ ...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs }
570
+ });
571
+ };
572
+ const settled = await Promise.allSettled([
573
+ summon("completeness", completenessRoot),
574
+ summon("correctness", correctnessRoot)
575
+ ]);
576
+ results = {
577
+ completeness: settled[0].status === "fulfilled" ? settled[0].value : failedResult(settled[0].reason),
578
+ correctness: settled[1].status === "fulfilled" ? settled[1].value : failedResult(settled[1].reason)
579
+ };
580
+ }
581
+ let sealDiagnostic;
582
+ try {
583
+ const { stdout: headBeforeStdout } = await execFileAsync(
584
+ "git",
585
+ ["rev-parse", "--verify", "HEAD^{commit}"],
586
+ { cwd: sourceProjectRoot }
587
+ );
588
+ const { stdout: sealedStatusStdout } = await execFileAsync("git", statusArgs, {
589
+ cwd: sourceProjectRoot
590
+ });
591
+ const { stdout: headAfterStdout } = await execFileAsync(
592
+ "git",
593
+ ["rev-parse", "--verify", "HEAD^{commit}"],
594
+ { cwd: sourceProjectRoot }
595
+ );
596
+ const headBefore = headBeforeStdout.trim();
597
+ const headAfter = headAfterStdout.trim();
598
+ if (headBefore !== targetCommit || headAfter !== targetCommit || sealedStatusStdout !== "") {
599
+ sealDiagnostic = [
600
+ "Reviewer target final seal failed:",
601
+ `HEAD before status => ${headBefore}`,
602
+ `HEAD after status => ${headAfter}`,
603
+ `expected PRE_HEAD ${targetCommit}`,
604
+ "git status --porcelain=v1 --untracked-files=all -- :/ ':(top,exclude).claude/worktrees/**'",
605
+ sealedStatusStdout
606
+ ].join("\n");
607
+ }
608
+ } catch (error) {
609
+ sealDiagnostic = `Reviewer target final seal failed:
610
+ ${describeFailure(error)}`;
611
+ }
612
+ if (sealDiagnostic !== void 0) {
613
+ const withSealFailure = (result) => ({
614
+ ...result,
615
+ exitCode: 1,
616
+ stderr: [result.stderr, sealDiagnostic].filter(
617
+ (text) => typeof text === "string" && text !== ""
618
+ ).join("\n")
619
+ });
620
+ results = {
621
+ completeness: withSealFailure(results.completeness),
622
+ correctness: withSealFailure(results.correctness)
623
+ };
624
+ }
625
+ const cleanup = await Promise.allSettled(
626
+ [...created].map((path) => execFileAsync("git", ["worktree", "remove", path], { cwd: sourceProjectRoot }))
627
+ );
628
+ const cleanupFailures = cleanup.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
629
+ if (cleanupFailures.length === 0) {
630
+ const rootCleanup = await Promise.allSettled([rm(root, { recursive: true, force: true })]);
631
+ cleanupFailures.push(...rootCleanup.flatMap((result) => result.status === "rejected" ? [result.reason] : []));
632
+ }
633
+ if (cleanupFailures.length > 0) {
634
+ const diagnostic = [
635
+ "parallel reviewer worktree cleanup failed",
636
+ ...cleanupFailures.map((failure) => failure instanceof Error ? failure.message : String(failure))
637
+ ].join("\n");
638
+ const withCleanupDiagnostic = (result) => ({
639
+ ...result,
640
+ stderr: [result.stderr, diagnostic].filter(
641
+ (text) => typeof text === "string" && text !== ""
642
+ ).join("\n")
643
+ });
644
+ results = {
645
+ completeness: withCleanupDiagnostic(results.completeness),
646
+ correctness: withCleanupDiagnostic(results.correctness)
647
+ };
648
+ }
649
+ return results;
650
+ }
304
651
  async function summonGateOfficer(options) {
305
652
  let home = options.home;
306
653
  if (home === void 0) {
@@ -352,7 +699,10 @@ async function summonGateOfficer(options) {
352
699
  }
353
700
  export {
354
701
  AK_ROLE_PACKAGE_ROOT_ENV,
702
+ openEphemeralReviewerWorktree,
355
703
  resolveSummonsPackageRoot,
356
704
  summonGateOfficer,
357
- summonPublicRole
705
+ summonParallelReviewerLenses,
706
+ summonPublicRole,
707
+ withEphemeralReviewerWorktree
358
708
  };
@@ -338,6 +338,59 @@ async function restoreState(cwd, runId, scope) {
338
338
  }, 0),
339
339
  };
340
340
  }
341
+ /** Sole HostContext-derived session parent for ledger restore/append (never process.env). */
342
+ function sessionParentFromHostContext(context) {
343
+ const runDirectory = runDirectoryFromHostContext(context);
344
+ if (runDirectory !== undefined)
345
+ return join(runDirectory, "session", "session.jsonl");
346
+ const sessionFile = context.sessionManager.getSessionFile?.();
347
+ if (typeof sessionFile === "string" && sessionFile.length > 0)
348
+ return sessionFile;
349
+ const sessionDir = context.sessionManager.getSessionDir?.();
350
+ if (typeof sessionDir === "string" && sessionDir.length > 0) {
351
+ return join(sessionDir, "session.jsonl");
352
+ }
353
+ return undefined;
354
+ }
355
+ function homeFromHostContext(context, home) {
356
+ if (home !== undefined)
357
+ return home;
358
+ const sessionParent = sessionParentFromHostContext(context);
359
+ return sessionParent !== undefined ? tryHomeFromAkRolesPath(sessionParent) : undefined;
360
+ }
361
+ /**
362
+ * #959: seal one accepted submission without a model tool call.
363
+ * Same ledger row shape and priorEventId chain as the terminating-tool wrap.
364
+ * Used when a prose-exit seat (navigator) harvests the final assistant text.
365
+ */
366
+ export async function sealAcceptedSubmission(options) {
367
+ const runId = runIdentity(options.context);
368
+ const attemptId = attemptIdentity(options.context, runId);
369
+ const sessionParent = sessionParentFromHostContext(options.context);
370
+ const home = homeFromHostContext(options.context, options.home);
371
+ const state = await restoreState(options.context.cwd, runId, {
372
+ ...(home === undefined ? {} : { home }),
373
+ ...(sessionParent === undefined ? {} : { sessionParent }),
374
+ });
375
+ const pointer = sitianReport({
376
+ level: "event",
377
+ kind: "sealed",
378
+ subject: { runId, attemptId },
379
+ ...(state.prior === undefined ? {} : { priorEventId: state.prior.identity }),
380
+ payload: {
381
+ type: "sealed",
382
+ attemptId,
383
+ toolCallId: options.toolCallId,
384
+ role: options.role,
385
+ accepted: options.accepted,
386
+ },
387
+ source: "role-runtime",
388
+ cwd: options.context.cwd,
389
+ ...(home !== undefined ? { home } : {}),
390
+ ...(sessionParent === undefined ? {} : { sessionParent }),
391
+ });
392
+ state.prior = pointer;
393
+ }
341
394
  /**
342
395
  * Submission ledger host — record only (#836).
343
396
  * Each terminating submission is appended with the role's original payload.
@@ -346,30 +399,10 @@ async function restoreState(cwd, runId, scope) {
346
399
  */
347
400
  export function createSubmissionLedgerHost(host, outputTools, failInfrastructure = (error) => { throw error; }, projectClosure = () => undefined, options) {
348
401
  const states = new Map();
349
- /** Sole HostContext-derived run coordinate for restore and append (never process.env). */
350
- const sessionParentFromContext = (context) => {
351
- const runDirectory = runDirectoryFromHostContext(context);
352
- if (runDirectory !== undefined) {
353
- return join(runDirectory, "session", "session.jsonl");
354
- }
355
- const sessionFile = context.sessionManager.getSessionFile?.();
356
- if (typeof sessionFile === "string" && sessionFile.length > 0)
357
- return sessionFile;
358
- const sessionDir = context.sessionManager.getSessionDir?.();
359
- if (typeof sessionDir === "string" && sessionDir.length > 0) {
360
- return join(sessionDir, "session.jsonl");
361
- }
362
- return undefined;
363
- };
364
- const resolveHomeFromContext = (context) => {
365
- if (options?.home !== undefined)
366
- return options.home;
367
- const sessionParent = sessionParentFromContext(context);
368
- return sessionParent !== undefined ? tryHomeFromAkRolesPath(sessionParent) : undefined;
369
- };
402
+ const resolveHomeFromContext = (context) => homeFromHostContext(context, options?.home);
370
403
  const stateFor = (context, runId) => states.get(runId) ?? (() => {
371
404
  const home = resolveHomeFromContext(context);
372
- const sessionParent = sessionParentFromContext(context);
405
+ const sessionParent = sessionParentFromHostContext(context);
373
406
  const pending = restoreState(context.cwd, runId, {
374
407
  ...(home === undefined ? {} : { home }),
375
408
  ...(sessionParent === undefined ? {} : { sessionParent }),
@@ -379,7 +412,7 @@ export function createSubmissionLedgerHost(host, outputTools, failInfrastructure
379
412
  })();
380
413
  const appendFor = (state, context, runId, attemptId, event) => {
381
414
  const home = resolveHomeFromContext(context);
382
- const sessionParent = sessionParentFromContext(context);
415
+ const sessionParent = sessionParentFromHostContext(context);
383
416
  const pointer = sitianReport({
384
417
  level: "event",
385
418
  kind: event.type,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akagilnc/pi-workflow-roles",
3
- "version": "0.1.4528",
3
+ "version": "0.1.4620",
4
4
  "description": "Soul-bound workflow roles for Pi",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -14,5 +14,7 @@
14
14
  | `run-lifecycle.ts` A4.2 | `重新读 <path>` | 前轮已删改写。 |
15
15
  | `submission-correctable-error.ts` A4.5 | `终局交卷并非本轮唯一工具调用。` | 删。Pi/ACP `deliverSubmissionRejection` 不再注入该句。 |
16
16
  | `role-runtime.ts` 催交 | `本会话尚无已接受的 typed 回执。` | **留**(轮次/预算,Q2)。 |
17
+ | `navigator-attendance.ts` early prepare 开场 | `父衙门进行中。请在本宿主会话熟悉下列材料并待命;结算结果送达后,再给出下一步建议。本轮不要提交最终路线建议。` | #959 迁入 `resources/navigator-route-playbook.md`「到场阶段」;代码只留中立指针句「本轮为待命轮。」 |
18
+ | `navigator-attendance.ts` settlement feed 开场 | `父衙门结算已送达。请根据下列材料给出下一步建议。` | #959 迁入同手册;代码只留中立指针句「本轮为结算投喂轮。」 |
17
19
 
18
20
  催交句与 2.1–2.3 打回文、重问三句不在删除列。