@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.
@@ -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) {
@@ -290,6 +319,16 @@ async function summonPublicRole(options) {
290
319
  result = stepped.ok;
291
320
  break;
292
321
  }
322
+ case "reviewer": {
323
+ const [{ runPublicReviewer }, { parseReviewerArgv }] = await Promise.all([
324
+ import("./public-cli/reviewer-run.js"),
325
+ import("./public-cli/invocation.js")
326
+ ]);
327
+ const stepped = await runPrepared(parseReviewerArgv, (env, once) => runPublicReviewer(options.argv, env, io, once));
328
+ if ("fail" in stepped) return stepped.fail;
329
+ result = stepped.ok;
330
+ break;
331
+ }
293
332
  }
294
333
  const stderr = captured?.stderrText();
295
334
  const runDirectory = result.admitted?.runDirectory;
@@ -301,6 +340,296 @@ async function summonPublicRole(options) {
301
340
  ...stderr === void 0 || stderr === "" ? {} : { stderr }
302
341
  };
303
342
  }
343
+ function withReviewerLens(argv, lens) {
344
+ const separator = argv.indexOf("--");
345
+ if (separator === -1) return [...argv, "--lens", lens];
346
+ return [...argv.slice(0, separator), "--lens", lens, ...argv.slice(separator)];
347
+ }
348
+ async function resolveReviewerWorktreeRoots(projectRoot) {
349
+ const callerProjectRoot = await realpath(projectRoot);
350
+ const sourceProjectRoot = await realpath((await execFileAsync(
351
+ "git",
352
+ ["rev-parse", "--show-toplevel"],
353
+ { cwd: callerProjectRoot }
354
+ )).stdout.trim());
355
+ const callerRelative = relative(sourceProjectRoot, callerProjectRoot);
356
+ const projectRelative = callerRelative === "" || callerRelative === "." || callerRelative.startsWith(`..${sep}`) || callerRelative === ".." ? "" : callerRelative;
357
+ return { callerProjectRoot, sourceProjectRoot, projectRelative };
358
+ }
359
+ function reviewerSandboxPath(worktreeAxis, projectRelative) {
360
+ return projectRelative === "" ? worktreeAxis : join(worktreeAxis, projectRelative);
361
+ }
362
+ async function openEphemeralReviewerWorktree(options) {
363
+ const { sourceProjectRoot, projectRelative } = await resolveReviewerWorktreeRoots(
364
+ options.projectRoot
365
+ );
366
+ const { stdout: headStdout } = await execFileAsync(
367
+ "git",
368
+ ["rev-parse", "--verify", "HEAD^{commit}"],
369
+ { cwd: sourceProjectRoot }
370
+ );
371
+ const targetCommit = headStdout.trim();
372
+ const root = await mkdtemp(join(tmpdir(), "ak-reviewer-sandbox-"));
373
+ const worktreeRoot = join(root, "work");
374
+ let registered = false;
375
+ const rollback = async (cause) => {
376
+ const cleanupErrors = [];
377
+ if (registered) {
378
+ try {
379
+ await execFileAsync("git", ["worktree", "remove", "--force", worktreeRoot], {
380
+ cwd: sourceProjectRoot
381
+ });
382
+ } catch (error) {
383
+ cleanupErrors.push(error);
384
+ }
385
+ }
386
+ try {
387
+ await rm(root, { recursive: true, force: true });
388
+ } catch (error) {
389
+ cleanupErrors.push(error);
390
+ }
391
+ if (cleanupErrors.length > 0) {
392
+ options.onCleanupDiagnostic?.([
393
+ "reviewer worktree cleanup failed",
394
+ ...cleanupErrors.map((error) => error instanceof Error ? error.message : String(error))
395
+ ].join("\n"));
396
+ }
397
+ throw cause;
398
+ };
399
+ try {
400
+ await execFileAsync("git", ["worktree", "add", "--detach", worktreeRoot, targetCommit], {
401
+ cwd: sourceProjectRoot
402
+ });
403
+ registered = true;
404
+ if (projectRelative !== "") {
405
+ await mkdir(reviewerSandboxPath(worktreeRoot, projectRelative), { recursive: true });
406
+ }
407
+ } catch (error) {
408
+ await rollback(error);
409
+ }
410
+ let closed = false;
411
+ return {
412
+ executionCwd: reviewerSandboxPath(worktreeRoot, projectRelative),
413
+ close: async () => {
414
+ if (closed) return;
415
+ closed = true;
416
+ const cleanupErrors = [];
417
+ try {
418
+ await execFileAsync("git", ["worktree", "remove", worktreeRoot], {
419
+ cwd: sourceProjectRoot
420
+ });
421
+ } catch (error) {
422
+ cleanupErrors.push(error);
423
+ }
424
+ try {
425
+ await rm(root, { recursive: true, force: true });
426
+ } catch (error) {
427
+ cleanupErrors.push(error);
428
+ }
429
+ if (cleanupErrors.length > 0) {
430
+ options.onCleanupDiagnostic?.([
431
+ "reviewer worktree cleanup failed",
432
+ ...cleanupErrors.map((error) => error instanceof Error ? error.message : String(error))
433
+ ].join("\n"));
434
+ }
435
+ }
436
+ };
437
+ }
438
+ async function withEphemeralReviewerWorktree(options) {
439
+ const sandbox = await openEphemeralReviewerWorktree({
440
+ projectRoot: options.projectRoot,
441
+ ...options.onCleanupDiagnostic === void 0 ? {} : { onCleanupDiagnostic: options.onCleanupDiagnostic }
442
+ });
443
+ try {
444
+ return await options.run(sandbox.executionCwd);
445
+ } finally {
446
+ await sandbox.close();
447
+ }
448
+ }
449
+ async function summonParallelReviewerLenses(options) {
450
+ const describeFailure = (error) => {
451
+ if (error instanceof AggregateError) {
452
+ return [error.message, ...error.errors.map(describeFailure)].join("\n");
453
+ }
454
+ return error instanceof Error ? error.message : String(error);
455
+ };
456
+ const failedResult = (error) => ({
457
+ exitCode: 1,
458
+ stderr: describeFailure(error)
459
+ });
460
+ const dualFailure = (error) => {
461
+ const failure = failedResult(error);
462
+ return { completeness: failure, correctness: failure };
463
+ };
464
+ let sourceProjectRoot;
465
+ let projectRelative;
466
+ let targetCommit;
467
+ const statusArgs = [
468
+ "status",
469
+ "--porcelain=v1",
470
+ "--untracked-files=all",
471
+ "--",
472
+ ":/",
473
+ ":(top,exclude).claude/worktrees/**"
474
+ ];
475
+ try {
476
+ const roots = await resolveReviewerWorktreeRoots(options.projectRoot);
477
+ sourceProjectRoot = roots.sourceProjectRoot;
478
+ projectRelative = roots.projectRelative;
479
+ const { stdout: statusStdout } = await execFileAsync("git", statusArgs, {
480
+ cwd: sourceProjectRoot
481
+ });
482
+ if (statusStdout !== "") {
483
+ const diagnostic = [
484
+ "Reviewer target status gate failed:",
485
+ "git status --porcelain=v1 --untracked-files=all -- :/ ':(top,exclude).claude/worktrees/**'",
486
+ statusStdout
487
+ ].join("\n");
488
+ return dualFailure(new Error(diagnostic));
489
+ }
490
+ const { stdout: targetStdout } = await execFileAsync(
491
+ "git",
492
+ ["rev-parse", "--verify", "HEAD^{commit}"],
493
+ { cwd: sourceProjectRoot }
494
+ );
495
+ targetCommit = targetStdout.trim();
496
+ await execFileAsync(
497
+ "git",
498
+ ["rev-parse", "--verify", `${options.baseRevision}^{commit}`],
499
+ { cwd: sourceProjectRoot }
500
+ );
501
+ } catch (error) {
502
+ return dualFailure(error);
503
+ }
504
+ const root = await mkdtemp(join(tmpdir(), "ak-reviewer-lenses-"));
505
+ const completenessRoot = join(root, "completeness");
506
+ const correctnessRoot = join(root, "correctness");
507
+ const worktrees = [completenessRoot, correctnessRoot];
508
+ const created = /* @__PURE__ */ new Set();
509
+ let results;
510
+ const creation = await Promise.allSettled(
511
+ worktrees.map(async (path) => {
512
+ await execFileAsync("git", ["worktree", "add", "--detach", path, targetCommit], {
513
+ cwd: sourceProjectRoot
514
+ });
515
+ created.add(path);
516
+ if (projectRelative !== "") {
517
+ await mkdir(reviewerSandboxPath(path, projectRelative), { recursive: true });
518
+ }
519
+ })
520
+ );
521
+ const creationFailures = creation.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
522
+ if (creationFailures.length > 0) {
523
+ const failure = failedResult(new AggregateError(
524
+ creationFailures,
525
+ "parallel reviewer worktree creation failed"
526
+ ));
527
+ results = { completeness: failure, correctness: failure };
528
+ } else {
529
+ const summon = (lens, worktreeAxis) => {
530
+ const sandbox = reviewerSandboxPath(worktreeAxis, projectRelative);
531
+ return summonPublicRole({
532
+ role: "reviewer",
533
+ argv: withReviewerLens(options.argv, lens),
534
+ cwd: options.cwd,
535
+ executionCwd: sandbox,
536
+ // Ordinary single-axis public semantics — not a nested court station.
537
+ stationChild: false,
538
+ home: options.home,
539
+ ...options.agentDir === void 0 ? {} : { agentDir: options.agentDir },
540
+ ...options.credentials === void 0 ? {} : { credentials: options.credentials },
541
+ ...options.model === void 0 ? {} : { model: options.model },
542
+ ...options.host === void 0 ? {} : { host: options.host },
543
+ ...options.engine === void 0 ? {} : { engine: options.engine },
544
+ ...options.engineModel === void 0 ? {} : { engineModel: options.engineModel },
545
+ ...options.signal === void 0 ? {} : { signal: options.signal },
546
+ ...options.correlationId === void 0 ? {} : { correlationId: options.correlationId },
547
+ ...options.packageRoot === void 0 ? {} : { packageRoot: options.packageRoot },
548
+ ...options.roleTurnHost === void 0 ? {} : { roleTurnHost: options.roleTurnHost },
549
+ ...options.hostAdapters === void 0 ? {} : { hostAdapters: options.hostAdapters },
550
+ ...options.principalAuthority === void 0 ? {} : { principalAuthority: options.principalAuthority },
551
+ ...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs }
552
+ });
553
+ };
554
+ const settled = await Promise.allSettled([
555
+ summon("completeness", completenessRoot),
556
+ summon("correctness", correctnessRoot)
557
+ ]);
558
+ results = {
559
+ completeness: settled[0].status === "fulfilled" ? settled[0].value : failedResult(settled[0].reason),
560
+ correctness: settled[1].status === "fulfilled" ? settled[1].value : failedResult(settled[1].reason)
561
+ };
562
+ }
563
+ let sealDiagnostic;
564
+ try {
565
+ const { stdout: headBeforeStdout } = await execFileAsync(
566
+ "git",
567
+ ["rev-parse", "--verify", "HEAD^{commit}"],
568
+ { cwd: sourceProjectRoot }
569
+ );
570
+ const { stdout: sealedStatusStdout } = await execFileAsync("git", statusArgs, {
571
+ cwd: sourceProjectRoot
572
+ });
573
+ const { stdout: headAfterStdout } = await execFileAsync(
574
+ "git",
575
+ ["rev-parse", "--verify", "HEAD^{commit}"],
576
+ { cwd: sourceProjectRoot }
577
+ );
578
+ const headBefore = headBeforeStdout.trim();
579
+ const headAfter = headAfterStdout.trim();
580
+ if (headBefore !== targetCommit || headAfter !== targetCommit || sealedStatusStdout !== "") {
581
+ sealDiagnostic = [
582
+ "Reviewer target final seal failed:",
583
+ `HEAD before status => ${headBefore}`,
584
+ `HEAD after status => ${headAfter}`,
585
+ `expected PRE_HEAD ${targetCommit}`,
586
+ "git status --porcelain=v1 --untracked-files=all -- :/ ':(top,exclude).claude/worktrees/**'",
587
+ sealedStatusStdout
588
+ ].join("\n");
589
+ }
590
+ } catch (error) {
591
+ sealDiagnostic = `Reviewer target final seal failed:
592
+ ${describeFailure(error)}`;
593
+ }
594
+ if (sealDiagnostic !== void 0) {
595
+ const withSealFailure = (result) => ({
596
+ ...result,
597
+ exitCode: 1,
598
+ stderr: [result.stderr, sealDiagnostic].filter(
599
+ (text) => typeof text === "string" && text !== ""
600
+ ).join("\n")
601
+ });
602
+ results = {
603
+ completeness: withSealFailure(results.completeness),
604
+ correctness: withSealFailure(results.correctness)
605
+ };
606
+ }
607
+ const cleanup = await Promise.allSettled(
608
+ [...created].map((path) => execFileAsync("git", ["worktree", "remove", path], { cwd: sourceProjectRoot }))
609
+ );
610
+ const cleanupFailures = cleanup.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
611
+ if (cleanupFailures.length === 0) {
612
+ const rootCleanup = await Promise.allSettled([rm(root, { recursive: true, force: true })]);
613
+ cleanupFailures.push(...rootCleanup.flatMap((result) => result.status === "rejected" ? [result.reason] : []));
614
+ }
615
+ if (cleanupFailures.length > 0) {
616
+ const diagnostic = [
617
+ "parallel reviewer worktree cleanup failed",
618
+ ...cleanupFailures.map((failure) => failure instanceof Error ? failure.message : String(failure))
619
+ ].join("\n");
620
+ const withCleanupDiagnostic = (result) => ({
621
+ ...result,
622
+ stderr: [result.stderr, diagnostic].filter(
623
+ (text) => typeof text === "string" && text !== ""
624
+ ).join("\n")
625
+ });
626
+ results = {
627
+ completeness: withCleanupDiagnostic(results.completeness),
628
+ correctness: withCleanupDiagnostic(results.correctness)
629
+ };
630
+ }
631
+ return results;
632
+ }
304
633
  async function summonGateOfficer(options) {
305
634
  let home = options.home;
306
635
  if (home === void 0) {
@@ -352,7 +681,10 @@ async function summonGateOfficer(options) {
352
681
  }
353
682
  export {
354
683
  AK_ROLE_PACKAGE_ROOT_ENV,
684
+ openEphemeralReviewerWorktree,
355
685
  resolveSummonsPackageRoot,
356
686
  summonGateOfficer,
357
- summonPublicRole
687
+ summonParallelReviewerLenses,
688
+ summonPublicRole,
689
+ withEphemeralReviewerWorktree
358
690
  };
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.4586",
4
4
  "description": "Soul-bound workflow roles for Pi",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -100,7 +100,6 @@ export type RoleTurnActivation =
100
100
  | {
101
101
  readonly role: "reviewer";
102
102
  readonly baseRevision: string;
103
- /** Caller-selected single lens; no default (ADR 0082 reviewer-method-lens). */
104
103
  readonly lens: "completeness" | "correctness";
105
104
  readonly authorityRefs: readonly string[];
106
105
  readonly ticketNumber?: number;
@@ -239,14 +239,14 @@ export type AdmittedNotaryInvocation = AdmittedRoleInvocationBase & {
239
239
  readonly sourceRun: NotarySourceRunLocator;
240
240
  };
241
241
 
242
- /** Public Reviewer single-lens token; no default and no `all` (caller-selected parent run). */
242
+ /** Durable single-axis Reviewer lens. Parallel default is parse-only omission, never admitted. */
243
243
  export type ReviewerLens = "completeness" | "correctness";
244
244
 
245
245
  export type AdmittedReviewerInvocation = AdmittedRoleInvocationBase & {
246
246
  readonly role: "reviewer";
247
247
  /** Required fixed base revision for the pinned review target (ADR 0037). */
248
248
  readonly baseRevision: string;
249
- /** Required single lens; frozen at admission and reused on resume. */
249
+ /** Frozen single-axis shape for an ordinary Reviewer run; reused on resume. */
250
250
  readonly lens: ReviewerLens;
251
251
  /**
252
252
  * Required durable authority references/URLs frozen at admission.
@@ -929,8 +929,11 @@ export type ParseReviewerArgvResult = {
929
929
  attachmentPaths: string[];
930
930
  /** Required fixed base revision for the pinned review target. */
931
931
  baseRevision: string;
932
- /** Required single lens; no default. */
933
- lens: ReviewerLens;
932
+ /**
933
+ * Explicit single-axis override. Omitted public `--lens` is the internal
934
+ * parallel two-axis branch mark only — never persisted on an admitted run.
935
+ */
936
+ lens?: ReviewerLens;
934
937
  /** Repeatable durable authority references/URLs (exact order preserved; at least one). */
935
938
  authorityRefs: string[];
936
939
  project?: string;
@@ -1048,7 +1051,7 @@ export function isReviewerLens(value: unknown): value is ReviewerLens {
1048
1051
  return value === "completeness" || value === "correctness";
1049
1052
  }
1050
1053
 
1051
- /** Public --lens enum; sole owner for parse + fresh admission. */
1054
+ /** Admitted single-axis lens enum; public `--lens all` and bare omission are not admitted values. */
1052
1055
  export function requireReviewerLens(value: string | undefined): ReviewerLens {
1053
1056
  const trimmed = (value ?? "").trim();
1054
1057
  if (!isReviewerLens(trimmed)) {
@@ -3073,7 +3076,7 @@ export function parseReviewerArgv(
3073
3076
  continue;
3074
3077
  }
3075
3078
  if (taken.def.id === "lens") {
3076
- // Empty and other non-enum values share one message — do not borrow path helper.
3079
+ // Public override is single-axis only; omission stays undefined for the parallel branch.
3077
3080
  lens = requireReviewerLens(taken.value);
3078
3081
  continue;
3079
3082
  }
@@ -3090,13 +3093,13 @@ export function parseReviewerArgv(
3090
3093
  positional.push(token);
3091
3094
  }
3092
3095
 
3093
- // Unconditional required (--base/--lens/--authority-ref) from typed table (#342).
3096
+ // Base and authority are required; omitted lens selects the parallel two-axis mode.
3094
3097
  options.assertRequired();
3095
3098
  return {
3096
3099
  instruction: positional.join(" "),
3097
3100
  attachmentPaths,
3098
3101
  baseRevision: baseRevision!,
3099
- lens: lens!,
3102
+ ...(lens === undefined ? {} : { lens }),
3100
3103
  authorityRefs,
3101
3104
  ...(project === undefined ? {} : { project }),
3102
3105
  };
@@ -3110,12 +3113,13 @@ export type AdmitReviewerInvocationOptions = {
3110
3113
  instruction: string;
3111
3114
  attachmentPaths: readonly string[];
3112
3115
  baseRevision: string;
3113
- /** Required single lens; frozen unchanged at admission. */
3116
+ /** Explicit single-axis shape, frozen unchanged at admission and resume. */
3114
3117
  lens: ReviewerLens;
3115
3118
  /** Required durable authority references/URLs; frozen unchanged at admission. */
3116
3119
  authorityRefs: readonly string[];
3117
3120
  project?: string;
3118
3121
  createRunId?: () => string;
3122
+ correlationId?: string;
3119
3123
  /** Effective model for this invocation — written onto invocation.json. */
3120
3124
  model?: InvocationEffectiveModel;
3121
3125
  };
@@ -3175,6 +3179,9 @@ export async function admitReviewerInvocation(
3175
3179
  baseRevision,
3176
3180
  lens,
3177
3181
  authorityRefs: [...authorityRefs],
3182
+ ...(options.correlationId === undefined
3183
+ ? {}
3184
+ : { correlationId: options.correlationId }),
3178
3185
  attachments: attachments.map((a) => ({
3179
3186
  provenancePath: a.provenancePath,
3180
3187
  frozenPath: a.frozenPath,
@@ -3204,6 +3211,9 @@ export async function admitReviewerInvocation(
3204
3211
  baseRevision,
3205
3212
  lens,
3206
3213
  authorityRefs,
3214
+ ...(options.correlationId === undefined
3215
+ ? {}
3216
+ : { correlationId: options.correlationId }),
3207
3217
  };
3208
3218
  }
3209
3219
 
@@ -552,12 +552,12 @@ const REVIEWER_OPTIONS = [
552
552
  canonical: "--lens",
553
553
  aliases: [],
554
554
  valueMetavar: "completeness|correctness",
555
- required: true,
555
+ required: false,
556
556
  repeatable: false,
557
557
  form: "option",
558
558
  description: {
559
- en: "Required single review lens: completeness or correctness (no default).",
560
- zh: "必填单 lens:completeness 或 correctness(无默认值)。",
559
+ en: "Optional single-lens override: completeness or correctness; omitted runs both in parallel.",
560
+ zh: "可选单 lens 覆盖:completeness 或 correctness;省略时并行运行两轴。",
561
561
  },
562
562
  },
563
563
  {
@@ -1227,9 +1227,9 @@ const ROLE_COMMAND_HELP = {
1227
1227
  },
1228
1228
  reviewer: {
1229
1229
  command: "reviewer",
1230
- summary: "Fixed-target single-lens review (completeness or correctness).",
1230
+ summary: "Fixed-target parallel two-lens review, with an optional single-lens override.",
1231
1231
  usage: [
1232
- "ak-role reviewer --base <revision> --lens completeness|correctness --authority-ref <ref> [options] <instruction>",
1232
+ "ak-role reviewer --base <revision> [--lens completeness|correctness] --authority-ref <ref> [options] <instruction>",
1233
1233
  ],
1234
1234
  examples: [
1235
1235
  'ak-role reviewer --base main --lens completeness --authority-ref docs/adr/0001.md "Review the branch."',