@akagilnc/pi-workflow-roles 0.1.4586 → 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 (47) hide show
  1. package/README.md +2 -2
  2. package/README.zh-CN.md +2 -2
  3. package/dist/acp-host/production-host.js +951 -497
  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 +783 -393
  7. package/dist/migrate-book-topology.js +3 -6
  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/judge-run.js +2 -2
  12. package/dist/public-cli/main.js +69 -94
  13. package/dist/public-cli/option-definitions.js +1 -1
  14. package/dist/public-cli/post-admission.js +2 -2
  15. package/dist/public-cli/run-lifecycle.js +29 -7
  16. package/dist/public-cli/settlement.js +46 -32
  17. package/dist/public-cli/terminal.js +8 -27
  18. package/dist/public-role-summons.js +22 -4
  19. package/dist/submission-ledger.js +56 -23
  20. package/package.json +1 -1
  21. package/resources/836-deleted-machine-instruction-inventory.md +2 -0
  22. package/resources/navigator-route-playbook.md +9 -0
  23. package/scripts/build-package.mjs +6 -1
  24. package/src/acp-host/role-turn-host.ts +129 -13
  25. package/src/headless-host/description.ts +17 -7
  26. package/src/headless-host/role-turn-host.ts +42 -11
  27. package/src/navigator-attendance.ts +290 -425
  28. package/src/navigator-public-session.ts +126 -25
  29. package/src/navigator-role.ts +2 -2
  30. package/src/package-contracts/navigator-output.ts +41 -25
  31. package/src/public-cli/coder-run.ts +2 -2
  32. package/src/public-cli/fixer-run.ts +2 -2
  33. package/src/public-cli/judge-run.ts +2 -2
  34. package/src/public-cli/merger-run.ts +2 -2
  35. package/src/public-cli/option-definitions.ts +1 -1
  36. package/src/public-cli/post-admission.ts +2 -1
  37. package/src/public-cli/run-lifecycle.ts +37 -7
  38. package/src/public-cli/settlement.ts +42 -33
  39. package/src/public-cli/terminal.ts +12 -37
  40. package/src/public-role-summons.ts +34 -5
  41. package/src/role-envelope.ts +89 -4
  42. package/src/role-runtime.ts +74 -14
  43. package/src/submission-ledger.ts +63 -21
  44. package/dist/public-cli/command-renderer.js +0 -5
  45. package/dist/public-command-renderer.js +0 -29
  46. package/src/public-cli/command-renderer.ts +0 -9
  47. 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
  }),
@@ -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
  }),
@@ -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,
@@ -1624,7 +1625,7 @@ export async function runPostAdmissionOneShot<
1624
1625
  ...input.request,
1625
1626
  continuation: {
1626
1627
  kind: "resume",
1627
- prompt: buildResumeContinuationPrompt({
1628
+ prompt: buildAutoResumeContinuationPrompt({
1628
1629
  packageRoot: input.env.packageRoot,
1629
1630
  ...pickEngineAxis({
1630
1631
  engine: input.effectiveEngine ?? input.env.engine,
@@ -127,8 +127,17 @@ export type RoleRunRecord = {
127
127
  readonly resumable?: TypedHttp429Observation;
128
128
  };
129
129
 
130
- /** Package-owned turn trigger for resume. Not caller instruction and not semantic task content. */
131
- export const RESUME_TRANSPORT_ENVELOPE = "[ak-role:resume-continue]" as const;
130
+ /**
131
+ * Package-owned non-empty Chinese neutral resume transport (#959 / ADR 0073).
132
+ * Used by:
133
+ * - auto-resume (all seats via buildAutoResumeContinuationPrompt) — required so
134
+ * hosts that reject empty stdin (codex) still receive a prompt;
135
+ * - reviewer seat manual resume (reviewerResumePrompt) — same non-empty need on
136
+ * that seat's own manual entry.
137
+ * Generic bare `ak-role resume` stays empty-capable via selectResumeContinuationPrompt
138
+ * / buildResumeContinuationPrompt — ADR 0080 keeps auto and generic-manual entries separate.
139
+ */
140
+ export const RESUME_TRANSPORT_ENVELOPE = "继续。" as const;
132
141
 
133
142
  /** Public manual resume request after the unique CLI parser owns runId + optional message. */
134
143
  export type PublicResumeRequest = {
@@ -169,9 +178,11 @@ export type SameTicketSummonsMaterials = {
169
178
  };
170
179
 
171
180
  /**
172
- * Unique continuation-prompt selector for manual/auto engine-axis resume
173
- * (#471 / #600 / #736). Message present → those bytes; absent → engine pointers only.
174
- * #836: no transport-token prompt, no line-by-line 重新读 rewrite.
181
+ * Manual resume continuation selector (#471 / #600 / #736 / ADR 0080).
182
+ * Message present → those bytes; absent → engine pointers only (may be empty).
183
+ * Caller message (including blank) still wins verbatim when supplied.
184
+ * Auto-resume must use buildAutoResumeContinuationPrompt — do not fold the
185
+ * non-empty Chinese envelope into this shared manual selector (#959).
175
186
  */
176
187
  export function selectResumeContinuationPrompt(
177
188
  message?: string,
@@ -182,8 +193,8 @@ export function selectResumeContinuationPrompt(
182
193
  }
183
194
 
184
195
  /**
185
- * Resume continuation with engine material resolved from the seat env (#600).
186
- * Seat table / invocation engine axis rides the same prompt seam as initial runs.
196
+ * Manual resume continuation with engine material from the seat env (#600).
197
+ * Bare manual resume stays empty-prompt-capable; auto-resume is a separate entry.
187
198
  */
188
199
  export function buildResumeContinuationPrompt(options: {
189
200
  packageRoot: string;
@@ -200,6 +211,25 @@ export function buildResumeContinuationPrompt(options: {
200
211
  );
201
212
  }
202
213
 
214
+ /**
215
+ * Auto-resume continuation only (#959 / ADR 0080).
216
+ * Always non-empty: Chinese neutral envelope plus optional engine pointers.
217
+ * Never call this from manual `ak-role resume`.
218
+ */
219
+ export function buildAutoResumeContinuationPrompt(options: {
220
+ packageRoot: string;
221
+ engine?: string;
222
+ engineModel?: string;
223
+ }): string {
224
+ return selectResumeContinuationPrompt(
225
+ RESUME_TRANSPORT_ENVELOPE,
226
+ engineSessionMaterialFromOptions({
227
+ packageRoot: options.packageRoot,
228
+ ...pickEngineAxis(options),
229
+ }),
230
+ );
231
+ }
232
+
203
233
  const RUN_STATE_FILE = "run-state.json";
204
234
  const WRITER_LOCK_FILE = "writer.lock";
205
235
 
@@ -123,7 +123,6 @@ import {
123
123
  parseInvocationMarkerIdentity,
124
124
  type InvocationMarkerIdentity,
125
125
  } from "../navigator-invocation-identity.ts";
126
- import type { NavigatorPhase } from "../navigator-attendance.ts";
127
126
  import {
128
127
  NO_RECEIPT_LIFECYCLE_ENTRY_TYPE,
129
128
  RECEIPT_DELIVERY_TURN_LIMIT,
@@ -360,7 +359,7 @@ import {
360
359
  exitCodeForTerminalOutcome,
361
360
  formatTerminalResult,
362
361
  isLawfulTypedTerminalOutcome,
363
- recommendationNavigatorFact,
362
+ adviceNavigatorFact,
364
363
  type ControlledFailureCause,
365
364
  type TerminalArtifactRef,
366
365
  type TerminalGateFact,
@@ -782,7 +781,13 @@ export function explicitInternalKnownFailureClassificationInput(
782
781
  };
783
782
  }
784
783
 
785
- /** Post-role Navigator delivery grace (Issue #11 / #101 / #106 / #159). */
784
+ /**
785
+ * Post-role Navigator delivery grace (Issue #11 / #101 / #106 / #159).
786
+ * After the parent finishes: wait at most this long for navigator output, then
787
+ * stop waiting. Navigator must already be running from parent start (prepare
788
+ * host round in parallel); this window is only the tail after parent end — not
789
+ * the budget to start a cold full host turn (owner 2026-09-17 #959).
790
+ */
786
791
  export const NAVIGATOR_POST_ROLE_GRACE_MS = 10_000;
787
792
 
788
793
  type SessionMessage = {
@@ -1931,11 +1936,6 @@ export type LawfulJudgeRoleOutcome = Extract<
1931
1936
  TerminalRoleOutcome,
1932
1937
  { kind: "accepted" } | { kind: "audit_escalation" }
1933
1938
  >;
1934
- function navigatorPhaseValue(value: unknown): NavigatorPhase {
1935
- if (value === "plan" || value === "apply") return value;
1936
- return null;
1937
- }
1938
-
1939
1939
  /**
1940
1940
  * Minimal attendance provenance against the bound marker (ADR 0043).
1941
1941
  * Keep only invocationId + post-terminal ordering. Runtime-self-produced
@@ -1960,35 +1960,44 @@ function parseNavigatorAttendanceDetails(
1960
1960
  const advisoryDiagnostic = typeof details.routePlaybookReadFailure === "string"
1961
1961
  ? { advisoryDiagnostic: details.routePlaybookReadFailure }
1962
1962
  : {};
1963
- if (disposition === "recommendation") {
1964
- const next = details.next;
1965
- if (!isRecord(next) || typeof next.role !== "string") {
1963
+ // #959: advice prose is presented as-is. Legacy "recommendation" with next/reason/
1964
+ // command is projected into prose so historical sessions still render — never wash
1965
+ // a real recommendation into no-advice when any advice body is recoverable.
1966
+ if (disposition === "advice" || disposition === "recommendation") {
1967
+ let prose: string | undefined;
1968
+ if (typeof details.prose === "string" && details.prose.trim() !== "") {
1969
+ prose = details.prose;
1970
+ } else {
1971
+ const next = isRecord(details.next) && typeof details.next.role === "string"
1972
+ ? details.next.role
1973
+ : undefined;
1974
+ const reason = typeof details.reason === "string" && details.reason.trim() !== ""
1975
+ ? details.reason
1976
+ : undefined;
1977
+ const command = typeof details.command === "string" && details.command.trim() !== ""
1978
+ ? details.command
1979
+ : undefined;
1980
+ if (reason !== undefined && next !== undefined) {
1981
+ prose = `${reason}(下一步:${next})`;
1982
+ } else if (reason !== undefined) {
1983
+ prose = reason;
1984
+ } else if (next !== undefined) {
1985
+ // Historical recommendation with only typed next — still real advice.
1986
+ prose = `下一步:${next}`;
1987
+ } else if (command !== undefined) {
1988
+ prose = command;
1989
+ }
1990
+ }
1991
+ if (prose === undefined || prose.trim() === "") {
1992
+ // Attended but empty body is affirmative no-advice, not unavailable (#959).
1966
1993
  return {
1967
- disposition: "unavailable",
1968
- source: "unknown",
1969
- reason: "navigator recommendation missing typed next role",
1994
+ disposition: "no-advice",
1995
+ ...advisoryDiagnostic,
1970
1996
  };
1971
1997
  }
1972
- const reason = typeof details.reason === "string" ? details.reason : "";
1973
- const route = Array.isArray(details.route)
1974
- ? details.route
1975
- .filter(isRecord)
1976
- .map((target) => ({
1977
- role: String(target.role),
1978
- phase: navigatorPhaseValue(target.phase),
1979
- }))
1980
- : undefined;
1981
- return recommendationNavigatorFact({
1998
+ return adviceNavigatorFact({
1999
+ prose,
1982
2000
  ...advisoryDiagnostic,
1983
- next: {
1984
- role: next.role,
1985
- phase: navigatorPhaseValue(next.phase),
1986
- },
1987
- reason,
1988
- ...(route === undefined ? {} : { route }),
1989
- ...(typeof details.command === "string"
1990
- ? { modelCommand: details.command }
1991
- : {}),
1992
2001
  });
1993
2002
  }
1994
2003
  if (disposition === "unavailable") {