@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
@@ -2,9 +2,12 @@
2
2
  * Navigator attendance session factory (#675 r3).
3
3
  * Each prepare turn uses the public navigator activation path (summonPublicRole) —
4
4
  * same seat table and shared envelope as `ak-role navigator`.
5
- * Archivist createRecordSession only books route-memory nest under the parent;
6
- * no openPiInProcessSession second lifecycle / no agentDir patch on the old path.
5
+ * Archivist createRecordSession books the attendance nest under the parent;
6
+ * host conversation continuity uses CLI resume (`resumeRunId`), not a package
7
+ * advice ledger (development-closure: do not invent package-level memory).
7
8
  */
9
+ import { basename } from "node:path";
10
+
8
11
  import { sitianReport } from "./sitian-facade.ts";
9
12
  import {
10
13
  NavigatorUnavailableError,
@@ -17,6 +20,8 @@ import {
17
20
  } from "./navigator-session-contracts.ts";
18
21
  import type { NoReceiptLifecycleFacts } from "./receipt-delivery-policy.ts";
19
22
  import { runDirectoryFromHostContext, type HostContext } from "./host-contracts.ts";
23
+ import type { PublicSummonResult } from "./public-role-summons.ts";
24
+ import { CliUsageError } from "./public-cli/cli-errors.ts";
20
25
 
21
26
  /**
22
27
  * Ledger process home for navigator attendance: admitted HostContext.runDirectory
@@ -40,13 +45,73 @@ async function resolveNavigatorLedgerHome(context: HostContext): Promise<string
40
45
  return undefined;
41
46
  }
42
47
 
43
- export function createNativeNavigatorSessionFactory(): NavigatorSessionFactory {
48
+ /** Resume key only — points at a host run principal, never stores advice prose. */
49
+ export const NAVIGATOR_HOST_RUN_POINTER_ENTRY = "ak-navigator-host-run";
50
+
51
+ function exactRecord(value: unknown): value is Record<string, unknown> {
52
+ return typeof value === "object" && value !== null && !Array.isArray(value);
53
+ }
54
+
55
+ export function runIdFromNavigatorDirectory(runDirectory: string): string | undefined {
56
+ const entry = basename(runDirectory);
57
+ const suffix = "@navigator";
58
+ if (!entry.endsWith(suffix)) return undefined;
59
+ const runId = entry.slice(0, entry.length - suffix.length);
60
+ return runId.length > 0 ? runId : undefined;
61
+ }
62
+
63
+ export function readNavigatorHostRunPointer(entries: readonly unknown[]): string | undefined {
64
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
65
+ const entry = entries[index];
66
+ if (!exactRecord(entry)) continue;
67
+ if (entry.type === "custom" && entry.customType === NAVIGATOR_HOST_RUN_POINTER_ENTRY) {
68
+ const data = entry.data;
69
+ if (exactRecord(data) && typeof data.runId === "string" && data.runId.trim() !== "") {
70
+ return data.runId.trim();
71
+ }
72
+ }
73
+ }
74
+ return undefined;
75
+ }
76
+
77
+ /**
78
+ * Typed preflight via public CLI load path: can this run reopen as navigator?
79
+ * CliUsageError (code AK_ROLE_USAGE) from loadResumableInstructionSeatRun means
80
+ * unknown id / principal unavailable / non-instruction seat — not resumable.
81
+ * Other throws propagate. Never reads stderr prose.
82
+ */
83
+ export async function navigatorHostRunResumable(home: string, runId: string): Promise<boolean> {
84
+ const { piDurablePrincipalAuthority } = await import("./pi/durable-principal.ts");
85
+ const { loadResumableInstructionSeatRun } = await import("./public-cli/run-lifecycle.ts");
86
+ try {
87
+ const loaded = await loadResumableInstructionSeatRun(home, runId, piDurablePrincipalAuthority);
88
+ return loaded.admitted.role === "navigator";
89
+ } catch (error) {
90
+ if (error instanceof CliUsageError) return false;
91
+ throw error;
92
+ }
93
+ }
94
+
95
+ export type NavigatorPublicSummon = (options: {
96
+ readonly role: "navigator";
97
+ readonly argv: readonly string[];
98
+ readonly cwd: string;
99
+ readonly home?: string;
100
+ readonly resumeRunId?: string;
101
+ }) => Promise<PublicSummonResult>;
102
+
103
+ export function createNativeNavigatorSessionFactory(deps?: {
104
+ /** Test/composition inject — production leaves unset and uses public-role-summons. */
105
+ readonly summonPublicRole?: NavigatorPublicSummon;
106
+ /** Test inject — production uses navigatorHostRunResumable (typed CLI load). */
107
+ readonly hostRunResumable?: (home: string, runId: string) => Promise<boolean>;
108
+ }): NavigatorSessionFactory {
44
109
  return async ({ context, subject, tool }) => {
45
110
  // Model is enforced at prepare (attendance seat resolve) and at prompt (summon).
46
111
  // Factory open only books the archivist nest — no package default, no eager seat read.
47
112
  let thinkingLevel: string | undefined;
48
113
 
49
- // Archivist nest for attendance route memory only (ADR 0018 / 0065) — not a session open.
114
+ // Archivist nest for attendance bookkeeping (ADR 0018 / 0065) — not a host session open.
50
115
  // #852: navigator/<work-subject> is the sole book-top exception; always pass subject so
51
116
  // unmaterialized/missing parent still gets a durable nest instead of silent in-memory.
52
117
  // Home comes from HostContext.runDirectory (or ledger parent path) — never context.home.
@@ -64,6 +129,14 @@ export function createNativeNavigatorSessionFactory(): NavigatorSessionFactory {
64
129
  let noReceipt: NoReceiptLifecycleFacts | undefined;
65
130
  let disposed = false;
66
131
  let inFlightPrompt: Promise<unknown> | undefined;
132
+ /** In-factory host run id for CLI resume; durable pointer also lives on the nest. */
133
+ let hostRunId = readNavigatorHostRunPointer(sessionManager.getEntries() as readonly unknown[]);
134
+
135
+ const summon: NavigatorPublicSummon = deps?.summonPublicRole
136
+ ?? (async (options) => {
137
+ const { summonPublicRole } = await import("./public-role-summons.ts");
138
+ return summonPublicRole(options);
139
+ });
67
140
 
68
141
  return {
69
142
  prompt: async (text) => {
@@ -74,15 +147,28 @@ export function createNativeNavigatorSessionFactory(): NavigatorSessionFactory {
74
147
  noReceipt = undefined;
75
148
  const run = (async () => {
76
149
  try {
77
- const { summonPublicRole } = await import("./public-role-summons.ts");
78
150
  const summonHome = await resolveNavigatorLedgerHome(context);
79
- // Public activation — same face as `ak-role navigator <instruction>` (#675).
80
- const summoned = await summonPublicRole({
81
- role: "navigator",
82
- argv: [text],
151
+ const resumeRunId = hostRunId;
152
+
153
+ const baseSummon = {
154
+ role: "navigator" as const,
155
+ argv: [text] as const,
83
156
  cwd: context.cwd,
84
157
  ...(summonHome === undefined ? {} : { home: summonHome }),
85
- });
158
+ };
159
+
160
+ // Prefer host CLI resume so prior advice stays on the host session.
161
+ // Fresh mint only when typed load says the principal cannot reopen.
162
+ const resumable = deps?.hostRunResumable ?? navigatorHostRunResumable;
163
+ let summoned: PublicSummonResult;
164
+ if (resumeRunId === undefined || summonHome === undefined) {
165
+ summoned = await summon(baseSummon);
166
+ } else if (await resumable(summonHome, resumeRunId)) {
167
+ summoned = await summon({ ...baseSummon, resumeRunId });
168
+ } else {
169
+ hostRunId = undefined;
170
+ summoned = await summon(baseSummon);
171
+ }
86
172
 
87
173
  const outcome = summoned.terminal?.roleOutcome;
88
174
  if (outcome === undefined) {
@@ -109,23 +195,40 @@ export function createNativeNavigatorSessionFactory(): NavigatorSessionFactory {
109
195
  noReceipt = outcome;
110
196
  return;
111
197
  }
198
+
199
+ // Pin host run for the next attendance prompt on this subject (CLI resume key).
200
+ const runDirectory = summoned.runDirectory
201
+ ?? (typeof summoned.admitted?.runDirectory === "string"
202
+ ? summoned.admitted.runDirectory
203
+ : undefined);
204
+ if (typeof runDirectory === "string" && runDirectory.trim() !== "") {
205
+ const nextRunId = runIdFromNavigatorDirectory(runDirectory);
206
+ if (nextRunId !== undefined) {
207
+ hostRunId = nextRunId;
208
+ sessionManager.appendCustomEntry(NAVIGATOR_HOST_RUN_POINTER_ENTRY, { runId: nextRunId });
209
+ }
210
+ }
211
+
112
212
  if (outcome.kind !== "accepted") {
113
- // Non-accepted kinds (e.g. audit_escalation): no route advice this turn.
213
+ // Non-accepted kinds (e.g. audit_escalation): no prose advice this turn.
114
214
  // Not a shape-unusable judgment on the navigator reply (#757).
115
215
  return;
116
216
  }
217
+ // #959: present navigator words as prose. No candidates/next parsing.
218
+ const { navigatorProseFromUnknown } = await import("./package-contracts/navigator-output.ts");
219
+ const proseParts: string[] = [];
117
220
  for (const payload of outcome.payloads ?? []) {
118
- if (typeof payload !== "object" || payload === null || Array.isArray(payload)) continue;
119
- const candidates = (payload as { candidates?: unknown }).candidates;
120
- if (!Array.isArray(candidates)) continue;
121
- await tool.execute(
122
- "navigator-public-prepare",
123
- { candidates },
124
- undefined,
125
- undefined,
126
- context as never,
127
- );
221
+ const prose = navigatorProseFromUnknown(payload);
222
+ if (prose !== undefined) proseParts.push(prose);
128
223
  }
224
+ if (proseParts.length === 0) return;
225
+ await tool.execute(
226
+ "navigator-public-prepare",
227
+ { prose: proseParts.join("\n\n") },
228
+ undefined,
229
+ undefined,
230
+ context as never,
231
+ );
129
232
  } catch (error) {
130
233
  if (error instanceof NavigatorUnavailableError) throw error;
131
234
  const fact = navigatorProviderFailureFromError(error);
@@ -166,10 +269,8 @@ export function createNativeNavigatorSessionFactory(): NavigatorSessionFactory {
166
269
  } catch (error) {
167
270
  throw navigatorUnavailableError("model", error);
168
271
  }
169
- // No live provider session is pinned here: every prompt is an independent
170
- // public summon whose nested CLI reads the current seat table (#675 验收②
171
- // / #617 DK-3). A seat edit between prepares therefore applies on the next
172
- // summon — it is never an unavailable session.
272
+ // Resume and fresh mint both read the live seat table (#675 / #617 DK-3).
273
+ // A seat edit between prepares applies on the next host turn.
173
274
  thinkingLevel = nextThinking ?? nextParsed.thinkingLevel;
174
275
  },
175
276
  getThinkingLevel: () => thinkingLevel,
@@ -2,12 +2,12 @@ import { NAVIGATOR_OUTPUT_TOOL_NAME, navigatorOutputSchema } from "./package-con
2
2
 
3
3
  /**
4
4
  * 决定工具规格。生命周期装配归注册信封 owner——src/role-runtime.ts(ADR 0018)。
5
- * Candidates share the route-advice shape the attendance prepare tool owns.
5
+ * #959: navigator speaks free-form prose; tool is a lifecycle vehicle only.
6
6
  */
7
7
  export const NAVIGATOR_TOOL_SPEC = {
8
8
  name: NAVIGATOR_OUTPUT_TOOL_NAME,
9
9
  label: "游奕使建议",
10
- description: "游奕使终局回执:排好序的下一步角色路线建议。",
10
+ description: "游奕使终局回执:散文建议,原样呈现。",
11
11
  promptSnippet: "游奕使建议",
12
12
  parameters: navigatorOutputSchema,
13
13
  } as const;
@@ -1,7 +1,8 @@
1
1
  /**
2
- * Public Navigator (游奕使) terminating receipt contracts (#639).
3
- * Direct public seat submits route advice with the same candidate shape the
4
- * attendance prepare tool owns — one shape authority for navigator advice.
2
+ * Public Navigator (游奕使) terminating receipt contracts (#639 / #959).
3
+ * Navigator speaks free-form prose. The tool is only a lifecycle vehicle so the
4
+ * host can seal a turn; the model-facing content is prose, not structured route
5
+ * candidates. Code does not parse, rank, or judge the advice.
5
6
  * No usable result is infrastructure failure via public settlement, not a judgment status (#475).
6
7
  */
7
8
  import { Type } from "typebox";
@@ -15,42 +16,57 @@ export const NAVIGATOR_ACCEPTED_TEXT = "游奕使建议已受理";
15
16
  export const navigatorOutputSchema = withTerminatingOutputDeclarations(
16
17
  openToolObject(
17
18
  Type.Object({
18
- status: Type.Unknown({
19
- description: "advice — 形状指引,非 schema 闸",
20
- }),
21
- candidates: Type.Unknown({
22
- description: "排好序的路线建议数组,元素含 next/phase/reason — 形状指引,非 schema 闸",
19
+ prose: Type.Unknown({
20
+ description: "游奕使散文建议,原样呈现。不要求 candidates/next 等结构化字段。形状指引,非 schema 闸",
23
21
  }),
24
22
  }),
25
23
  ),
26
24
  );
27
25
 
28
- function isRecord(value: unknown): value is Record<string, unknown> {
29
- return typeof value === "object" && value !== null && !Array.isArray(value);
30
- }
31
-
32
- export type NavigatorAdvice = { readonly status: "advice"; readonly candidates: readonly unknown[] };
26
+ export type NavigatorAdvice = { readonly prose: string };
33
27
 
34
28
  /**
35
- * Project one lawful explicit Navigator advice receipt: candidates array,
36
- * each candidate a record. Broken ancillary fields are preserved as submitted —
37
- * shape is not an admission gate (ADR 0055 / 第 0 条).
29
+ * Project one Navigator advice receipt as prose.
30
+ * Accepts { prose }, a bare string, or any object — objects stringify as the
31
+ * prose body so historical / free-form submissions still present. Shape is not
32
+ * an admission gate (ADR 0055 / 第 0 条 / #959).
38
33
  */
39
34
  export function projectLawfulNavigatorOutput(value: unknown): NavigatorAdvice | undefined {
40
- // #836: no field drop
41
- return (typeof value === "object" && value !== null && !Array.isArray(value))
42
- ? (value as NavigatorAdvice)
43
- : undefined;
35
+ if (value === undefined || value === null) return undefined;
36
+ if (typeof value === "string") return { prose: value };
37
+ if (typeof value === "object" && !Array.isArray(value)) {
38
+ const record = value as Record<string, unknown>;
39
+ if (typeof record.prose === "string") return { prose: record.prose };
40
+ if (record.prose !== undefined && record.prose !== null) {
41
+ return { prose: typeof record.prose === "string" ? record.prose : String(record.prose) };
42
+ }
43
+ // Free-form object submission: present the whole body as prose when it has content.
44
+ const keys = Object.keys(record);
45
+ if (keys.length === 0) return { prose: "" };
46
+ try {
47
+ return { prose: JSON.stringify(record) };
48
+ } catch {
49
+ return { prose: String(record) };
50
+ }
51
+ }
52
+ return { prose: String(value) };
44
53
  }
45
54
 
46
55
  /**
47
- * Settlement/recording path: only lawful recorded candidate arrays.
48
- * Does not gate role admission — callers must not use this to reject a submission.
56
+ * Settlement/recording path: project prose. Does not gate role admission.
49
57
  */
50
58
  export function validateRecordedNavigatorOutput(value: unknown): NavigatorAdvice {
51
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
52
- throw new Error("Navigator output is not an object");
59
+ const projected = projectLawfulNavigatorOutput(value);
60
+ if (projected === undefined) {
61
+ throw new Error("Navigator output is empty");
53
62
  }
54
- return value as NavigatorAdvice;
63
+ return projected;
55
64
  }
56
65
 
66
+ /** Extract display prose from any navigator payload / attendance body. */
67
+ export function navigatorProseFromUnknown(value: unknown): string | undefined {
68
+ const projected = projectLawfulNavigatorOutput(value);
69
+ if (projected === undefined) return undefined;
70
+ const trimmed = projected.prose.trim();
71
+ return trimmed === "" ? undefined : projected.prose;
72
+ }
@@ -22,7 +22,7 @@ import {
22
22
  type AdmittedCoderInvocation,
23
23
  } from "./invocation.ts";
24
24
  import {
25
- buildResumeContinuationPrompt,
25
+ buildAutoResumeContinuationPrompt,
26
26
  loadResumableCoderRun,
27
27
  markRunAdmitted,
28
28
  type PublicResumeRequest,
@@ -198,7 +198,7 @@ export async function runPublicCoder(
198
198
  : { correlationId: admitted.correlationId ?? env.correlationId }),
199
199
  continuation: {
200
200
  kind: "resume",
201
- prompt: buildResumeContinuationPrompt({
201
+ prompt: buildAutoResumeContinuationPrompt({
202
202
  packageRoot: env.packageRoot,
203
203
  ...pickEngineAxis(env),
204
204
  }),
@@ -25,7 +25,7 @@ import {
25
25
  type AdmittedFixerInvocation,
26
26
  } from "./invocation.ts";
27
27
  import {
28
- buildResumeContinuationPrompt,
28
+ buildAutoResumeContinuationPrompt,
29
29
  loadResumableFixerRun,
30
30
  markRunAdmitted,
31
31
  type PublicResumeRequest,
@@ -213,7 +213,7 @@ export async function runPublicFixer(
213
213
  : { correlationId: admitted.correlationId ?? env.correlationId }),
214
214
  continuation: {
215
215
  kind: "resume",
216
- prompt: buildResumeContinuationPrompt({
216
+ prompt: buildAutoResumeContinuationPrompt({
217
217
  packageRoot: env.packageRoot,
218
218
  ...pickEngineAxis(env),
219
219
  }),
@@ -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
 
@@ -18,7 +18,7 @@ import {
18
18
  import {
19
19
  loadResumableJudgeRun,
20
20
  markRunAdmitted,
21
- buildResumeContinuationPrompt,
21
+ buildAutoResumeContinuationPrompt,
22
22
  type PublicResumeRequest,
23
23
  } from "./run-lifecycle.ts";
24
24
  import {
@@ -152,7 +152,7 @@ export async function runPublicJudge(
152
152
  : { correlationId: admitted.correlationId ?? env.correlationId }),
153
153
  continuation: {
154
154
  kind: "resume",
155
- prompt: buildResumeContinuationPrompt({
155
+ prompt: buildAutoResumeContinuationPrompt({
156
156
  packageRoot: env.packageRoot,
157
157
  ...pickEngineAxis(env),
158
158
  }),
@@ -24,7 +24,7 @@ import {
24
24
  import {
25
25
  loadResumableMergerRun,
26
26
  markRunAdmitted,
27
- buildResumeContinuationPrompt,
27
+ buildAutoResumeContinuationPrompt,
28
28
  type PublicResumeRequest,
29
29
  } from "./run-lifecycle.ts";
30
30
  import {
@@ -241,7 +241,7 @@ export async function runPublicMerger(
241
241
  : { correlationId: admitted.correlationId ?? env.correlationId }),
242
242
  continuation: {
243
243
  kind: "resume",
244
- prompt: buildResumeContinuationPrompt({
244
+ prompt: buildAutoResumeContinuationPrompt({
245
245
  packageRoot: env.packageRoot,
246
246
  ...pickEngineAxis(env),
247
247
  }),
@@ -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."',
@@ -1280,7 +1280,7 @@ const ROLE_COMMAND_HELP = {
1280
1280
  },
1281
1281
  navigator: {
1282
1282
  command: "navigator",
1283
- summary: "Direct Navigator (游奕使) route advice: ordered next-role candidates.",
1283
+ summary: "Direct Navigator (游奕使) free-form prose route advice.",
1284
1284
  usage: ["ak-role navigator [options] [instruction]"],
1285
1285
  examples: [
1286
1286
  'ak-role navigator "刚完成 coder apply 收敛,下一步?"',
@@ -10,6 +10,7 @@ import { writeFile } from "node:fs/promises";
10
10
  import { isAbsolute, join, resolve } from "node:path";
11
11
 
12
12
  import {
13
+ buildAutoResumeContinuationPrompt,
13
14
  buildResumeContinuationPrompt,
14
15
  RESUME_TRANSPORT_ENVELOPE,
15
16
  type PublicResumeRequest,
@@ -236,6 +237,12 @@ export type PostAdmissionEnv = {
236
237
  freshSummons?: true;
237
238
  /** Station child role run (#840): omit automatic navigator attendance. */
238
239
  stationChild?: boolean;
240
+ /**
241
+ * Ephemeral host-turn cwd for Reviewer fresh-copy execution (#946 统一新副本).
242
+ * Admission and durable projectRoot stay on the caller project; the host turn
243
+ * (initial and resume) runs in this sandbox when present.
244
+ */
245
+ executionCwd?: string;
239
246
  };
240
247
 
241
248
  /**
@@ -1352,6 +1359,16 @@ export async function runPostAdmissionSeatResume<
1352
1359
  afterAdmittedLoad?: (
1353
1360
  admitted: A,
1354
1361
  ) => Promise<AfterAdmittedLoadResult<A, T>>;
1362
+ /**
1363
+ * After the single pre-lease load (and optional afterAdmittedLoad), prepare
1364
+ * call-local execution context. Reviewer fresh-copy (#946) mints an ephemeral
1365
+ * worktree from the admitted projectRoot here — reuses this load, does not
1366
+ * pre-load outside the coordinator. cleanup runs after dispatch settles.
1367
+ */
1368
+ afterAdmittedPrepare?: (admitted: A) => Promise<{
1369
+ readonly env?: PostAdmissionEnv;
1370
+ readonly cleanup?: () => Promise<void>;
1371
+ }>;
1355
1372
  effectiveEngine?: string;
1356
1373
  }): Promise<{ exitCode: number; admitted?: A; terminal?: T }> {
1357
1374
  let request = input.request;
@@ -1382,6 +1399,10 @@ export async function runPostAdmissionSeatResume<
1382
1399
  adapters = prepared.adapters;
1383
1400
  }
1384
1401
 
1402
+ // Call-local execution env; afterAdmittedPrepare may replace it (Reviewer fresh-copy).
1403
+ let env = input.env;
1404
+ let preparedCleanup: (() => Promise<void>) | undefined;
1405
+
1385
1406
  const buildRequestAfterLease = async (): Promise<RoleTurnRequest> => {
1386
1407
  let openCourtAttemptId: string | undefined;
1387
1408
  // Build uses the admitted judged under this lease (rehydrated when open
@@ -1464,8 +1485,14 @@ export async function runPostAdmissionSeatResume<
1464
1485
  // Court recovery / open under lease, then dispatch.
1465
1486
  // Station-child same-ticket/same-parent resume is call-local auto-resume
1466
1487
  // (#840 / #416). Public `ak-role resume` stays one-shot (ADR 0080).
1488
+ // afterAdmittedPrepare runs inside this try so mint failure and cleanup share one finally.
1467
1489
  try {
1468
- if (input.env.stationChild === true) {
1490
+ if (input.afterAdmittedPrepare !== undefined) {
1491
+ const prepared = await input.afterAdmittedPrepare(loaded.admitted);
1492
+ if (prepared.env !== undefined) env = prepared.env;
1493
+ preparedCleanup = prepared.cleanup;
1494
+ }
1495
+ if (env.stationChild === true) {
1469
1496
  let firstTurn: RoleTurnRequest | undefined;
1470
1497
  const stationAdapters = withOnceSuccessfulBeforeDispatch(adapters);
1471
1498
  type StationChildAttempt = { readonly resumeTurn: boolean };
@@ -1477,14 +1504,14 @@ export async function runPostAdmissionSeatResume<
1477
1504
  });
1478
1505
  return await runWithAutoResumeLoop({
1479
1506
  admitted: loaded.admitted,
1480
- principalAuthority: input.env.principalAuthority,
1507
+ principalAuthority: env.principalAuthority,
1481
1508
  isPrincipalAvailable: resolveHostAwareSessionAvailability(
1482
- input.env.host,
1483
- input.env.principalAuthority,
1509
+ env.host,
1510
+ env.principalAuthority,
1484
1511
  ),
1485
1512
  io: input.io,
1486
- sessionAppender: input.env.sessionAppender,
1487
- autoResumeLimit: input.env.autoResumeLimit,
1513
+ sessionAppender: env.sessionAppender,
1514
+ autoResumeLimit: env.autoResumeLimit,
1488
1515
  buildInitialPayload: (): StationChildAttempt => ({ resumeTurn: false }),
1489
1516
  buildResumePayload: (): StationChildAttempt => ({ resumeTurn: true }),
1490
1517
  // Same as public manual resume: prior-court sealed acceptance is not a
@@ -1522,7 +1549,7 @@ export async function runPostAdmissionSeatResume<
1522
1549
  dispatch: async (turnRequest) => {
1523
1550
  const result = await dispatchPostAdmissionTurn({
1524
1551
  admitted: loaded.admitted,
1525
- env: input.env,
1552
+ env,
1526
1553
  io: attemptIo,
1527
1554
  request: turnRequest,
1528
1555
  lease,
@@ -1534,7 +1561,7 @@ export async function runPostAdmissionSeatResume<
1534
1561
  });
1535
1562
  return settleDeferredPersist(
1536
1563
  loaded.admitted,
1537
- input.env.principalAuthority,
1564
+ env.principalAuthority,
1538
1565
  stationAdapters,
1539
1566
  attemptIo,
1540
1567
  result,
@@ -1545,7 +1572,7 @@ export async function runPostAdmissionSeatResume<
1545
1572
  }
1546
1573
  return await runPostAdmissionManualResume({
1547
1574
  admitted: loaded.admitted,
1548
- env: input.env,
1575
+ env,
1549
1576
  io: input.io,
1550
1577
  adapters,
1551
1578
  ...(input.effectiveEngine === undefined
@@ -1561,6 +1588,10 @@ export async function runPostAdmissionSeatResume<
1561
1588
  return { exitCode: 2 };
1562
1589
  }
1563
1590
  throw error;
1591
+ } finally {
1592
+ if (preparedCleanup !== undefined) {
1593
+ await preparedCleanup();
1594
+ }
1564
1595
  }
1565
1596
  }
1566
1597
 
@@ -1594,7 +1625,7 @@ export async function runPostAdmissionOneShot<
1594
1625
  ...input.request,
1595
1626
  continuation: {
1596
1627
  kind: "resume",
1597
- prompt: buildResumeContinuationPrompt({
1628
+ prompt: buildAutoResumeContinuationPrompt({
1598
1629
  packageRoot: input.env.packageRoot,
1599
1630
  ...pickEngineAxis({
1600
1631
  engine: input.effectiveEngine ?? input.env.engine,