@akagilnc/pi-workflow-roles 0.1.5068 → 0.1.5096

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 (80) hide show
  1. package/dist/acp-host/production-host.js +429 -383
  2. package/dist/atomic-write.js +13 -19
  3. package/dist/auditor-soul.js +2 -1
  4. package/dist/compliance-transport.js +3 -2
  5. package/dist/countersign-contracts.js +2 -2
  6. package/dist/doctor-contracts.js +1 -1
  7. package/dist/gatekeeper-role.js +14 -35
  8. package/dist/headless-host/production-host.js +461 -415
  9. package/dist/inspector-contracts.js +5 -2
  10. package/dist/merger-contracts.js +1 -1
  11. package/dist/migrate-book-topology.js +49 -33
  12. package/dist/navigator-attendance.js +28 -143
  13. package/dist/navigator-invocation-identity.js +2 -0
  14. package/dist/navigator-public-session.js +13 -1
  15. package/dist/navigator-work-base.js +113 -0
  16. package/dist/notary-contracts.js +0 -2
  17. package/dist/open-tool-schema.js +4 -1
  18. package/dist/package-contracts/fixer-output.js +1 -1
  19. package/dist/package-contracts/gatekeeper-output.js +1 -1
  20. package/dist/package-contracts/judge-output.js +2 -2
  21. package/dist/package-contracts/navigator-output.js +1 -1
  22. package/dist/package-contracts/terminating-infrastructure.js +7 -4
  23. package/dist/packaged-role-registry.js +9 -5
  24. package/dist/public-cli/countersign-run.js +1 -1
  25. package/dist/public-cli/invocation.js +6 -3
  26. package/dist/public-cli/main.js +78 -41
  27. package/dist/public-cli/settlement.js +20 -1
  28. package/dist/receipt-delivery-policy.js +9 -2
  29. package/dist/review-submission.js +19 -8
  30. package/dist/submission-errors.js +17 -0
  31. package/dist/submission-ledger.js +18 -6
  32. package/docs/adr/0057-schema-narrowing-cuts-the-required-set-not-the-declared-set.md +9 -0
  33. package/extensions/role-runtime.ts +0 -27
  34. package/package.json +2 -1
  35. package/resources/836-deleted-machine-instruction-inventory.md +3 -3
  36. package/resources/collector-bot-handbook.md +4 -0
  37. package/resources/navigator-route-playbook.md +4 -6
  38. package/scripts/build-package.mjs +4 -0
  39. package/souls/coder-output-guide.md +1 -0
  40. package/src/auditor-soul.ts +2 -1
  41. package/src/collector-role.ts +6 -7
  42. package/src/collector-tool-schemas.ts +5 -5
  43. package/src/compliance-transport.ts +3 -2
  44. package/src/countersign-contracts.ts +2 -2
  45. package/src/countersign-role.ts +3 -7
  46. package/src/diarist-role.ts +2 -2
  47. package/src/doctor-contracts.ts +1 -1
  48. package/src/gatekeeper-role.ts +15 -36
  49. package/src/gleaner-left-role.ts +2 -2
  50. package/src/inspector-contracts.ts +5 -2
  51. package/src/inspector-role.ts +1 -1
  52. package/src/judge-role.ts +14 -22
  53. package/src/merger-contracts.ts +1 -1
  54. package/src/navigator-attendance.ts +38 -164
  55. package/src/navigator-invocation-identity.ts +2 -0
  56. package/src/navigator-public-session.ts +17 -1
  57. package/src/navigator-role.ts +4 -0
  58. package/src/navigator-session-contracts.ts +2 -0
  59. package/src/navigator-work-base.ts +123 -0
  60. package/src/notary-contracts.ts +0 -4
  61. package/src/open-tool-schema.ts +9 -4
  62. package/src/package-contracts/fixer-output.ts +1 -1
  63. package/src/package-contracts/gatekeeper-output.ts +3 -3
  64. package/src/package-contracts/judge-output.ts +2 -4
  65. package/src/package-contracts/navigator-output.ts +1 -1
  66. package/src/package-contracts/terminating-infrastructure.ts +12 -10
  67. package/src/packaged-role-registry.ts +5 -1
  68. package/src/public-cli/countersign-run.ts +1 -1
  69. package/src/public-cli/invocation.ts +6 -3
  70. package/src/public-cli/settlement.ts +21 -0
  71. package/src/receipt-delivery-policy.ts +9 -1
  72. package/src/review-submission.ts +24 -10
  73. package/src/reviewer-role.ts +3 -3
  74. package/src/role-runtime-dependencies.ts +1 -5
  75. package/src/role-runtime.ts +61 -63
  76. package/src/secretariat-role.ts +3 -3
  77. package/src/submission-errors.ts +16 -0
  78. package/src/submission-gate.ts +2 -1
  79. package/src/submission-ledger.ts +23 -11
  80. package/src/worker-role.ts +7 -9
@@ -4,7 +4,7 @@
4
4
  * Dual path: gate-dispatched and independently callable (#568 / ADR 0074).
5
5
  */
6
6
 
7
- import { REVIEW_SUBMISSION_OUTPUT_TOOL_NAME } from "./review-submission.ts";
7
+ import { REVIEW_QUEUE_STATUSES, REVIEW_SUBMISSION_OUTPUT_TOOL_NAME } from "./review-submission.ts";
8
8
 
9
9
  export const INSPECTOR_OUTPUT_TOOL_NAME: string = REVIEW_SUBMISSION_OUTPUT_TOOL_NAME;
10
10
  export const INSPECTOR_SOURCE_RUN_FLAG = {
@@ -30,7 +30,10 @@ export function validateRecordedInspectorOutput(value: unknown): InspectorOutput
30
30
  } catch {
31
31
  throw new Error("Inspector output has no execution discriminator");
32
32
  }
33
- if (status === "converged" || status === "continue" || status === "escalate") {
33
+ if (typeof status !== "string") {
34
+ throw new Error("Inspector output has no execution discriminator");
35
+ }
36
+ if (REVIEW_QUEUE_STATUSES.has(status)) {
34
37
  return value as InspectorOutput;
35
38
  }
36
39
  throw new Error("Inspector output has no execution discriminator");
@@ -13,7 +13,7 @@ export { INSPECTOR_OUTPUT_TOOL_NAME as INSPECTOR_OUTPUT_TOOL };
13
13
  export type { InspectorOutput };
14
14
  export { validateRecordedInspectorOutput };
15
15
 
16
- /** 台院事后察举交卷形状;形状指引,非 schema 闸。 */
16
+ /** 台院事后察举交卷形状。 */
17
17
  export const inspectorOutputSchema = reviewSubmissionSchema;
18
18
 
19
19
  export type InspectorOutputParameters = Static<typeof inspectorOutputSchema>;
package/src/judge-role.ts CHANGED
@@ -1,19 +1,15 @@
1
1
  import type { RoleHost, HostContext, HostToolResult, HostGatekeeperActions } from "./host-contracts.ts";
2
2
  import type { Static } from "typebox";
3
- import { reviewSubmissionSchema } from "./review-submission.ts";
4
- import { GatekeeperDecisionError, ParentQueueReaskError } from "./submission-errors.ts";
3
+ import { REVIEW_QUEUE_STATUSES, reviewSubmissionSchema } from "./review-submission.ts";
4
+ import { GatekeeperDecisionError, ParentQueueReaskError, unreadableDiscriminatorNotice } from "./submission-errors.ts";
5
5
  import { projectGatekeeperEscalation } from "./audit-escalation.ts";
6
6
 
7
7
  import { readableGateItem } from "./readable-gate-item.ts";
8
8
  import {
9
9
  JUDGE_OUTPUT_TOOL_NAME,
10
- validateAcceptedJudgeDetails,
11
10
  type JudgeVerdict,
12
11
  } from "./package-contracts/judge-output.ts";
13
12
 
14
- const JUDGE_QUEUE_STATUSES = new Set(["converged", "continue", "escalate"]);
15
- const JUDGE_STATUS_REASK =
16
- "status 不是 converged、continue、escalate 三态之一。请重新交卷,status 写明其一。" as const;
17
13
 
18
14
  export { JUDGE_OUTPUT_TOOL_NAME };
19
15
  export type { JudgeVerdict };
@@ -25,8 +21,6 @@ export type { JudgeVerdict };
25
21
  // The shared open schema preserves role-specific prose without adding another contract.
26
22
  export const judgeVerdictSchema = reviewSubmissionSchema;
27
23
 
28
- type JudgeVerdictParameters = Static<typeof judgeVerdictSchema>;
29
-
30
24
  export type JudgeRoleDependencies = {
31
25
  loadSoul(): Promise<string>;
32
26
  };
@@ -36,10 +30,6 @@ export type JudgeRoleHostActions = HostGatekeeperActions & {
36
30
  bindPriorGatePass(toolCallId: string, receipt: unknown): void;
37
31
  };
38
32
 
39
- export function validateVerdict(verdict: JudgeVerdictParameters): JudgeVerdict {
40
- return validateAcceptedJudgeDetails(verdict);
41
- }
42
-
43
33
 
44
34
  export function createJudgeRoleRuntime(
45
35
  pi: RoleHost,
@@ -58,7 +48,7 @@ export function createJudgeRoleRuntime(
58
48
  pi.registerTool({
59
49
  name: JUDGE_OUTPUT_TOOL_NAME,
60
50
  label: "大理寺输出",
61
- description: "提交大理寺终局判词;受理前经符宝郎内闸与审刑院合规审核。",
51
+ description: "提交大理寺终局判词。",
62
52
  promptSnippet: "提交大理寺终局判词",
63
53
  parameters: judgeVerdictSchema,
64
54
  async execute(toolCallId: string, parameters: Static<typeof judgeVerdictSchema>, signal: AbortSignal | undefined, _onUpdate: unknown, ctx: HostContext): Promise<HostToolResult<unknown>> {
@@ -70,19 +60,21 @@ export function createJudgeRoleRuntime(
70
60
  && typeof (parameters as Record<string, unknown>).status === "string"
71
61
  ? (parameters as Record<string, unknown>).status as string
72
62
  : undefined;
73
- if (rawStatus === undefined || !JUDGE_QUEUE_STATUSES.has(rawStatus)) {
74
- throw new ParentQueueReaskError(JUDGE_STATUS_REASK);
63
+ if (rawStatus === undefined || !REVIEW_QUEUE_STATUSES.has(rawStatus)) {
64
+ const received = parameters !== null && typeof parameters === "object" && !Array.isArray(parameters)
65
+ ? (parameters as Record<string, unknown>).status
66
+ : undefined;
67
+ throw new ParentQueueReaskError(unreadableDiscriminatorNotice("status", received));
75
68
  }
76
69
  if (rawStatus === "escalate") {
77
70
  // Parent escalate → throw to caller as-is; officers do not attend (#753 / #756).
78
- const verdict = validateVerdict(parameters);
79
71
  return {
80
72
  content: [],
81
- details: verdict,
73
+ details: parameters,
82
74
  terminate: true as const,
83
75
  };
84
76
  }
85
- const verdict = validateVerdict(parameters);
77
+ // Queue membership above is the only three-state check. The receipt is not re-judged.
86
78
  // Candidate verdict is already on the parent session books as this
87
79
  // tool-call leaf (first-record-then-audit; run 019fea05 L61/L62).
88
80
  // #753: 符宝郎内闸 — continue returns the raw receipt as a nonterminal tool result.
@@ -101,7 +93,7 @@ export function createJudgeRoleRuntime(
101
93
  if (draftPass?.status === "continue") {
102
94
  return {
103
95
  content: [{ type: "text" as const, text: readableGateItem(draftPass.receipt) }],
104
- details: verdict,
96
+ details: parameters,
105
97
  terminate: false,
106
98
  };
107
99
  }
@@ -121,18 +113,18 @@ export function createJudgeRoleRuntime(
121
113
  if (compliancePass?.status === "continue") {
122
114
  return {
123
115
  content: [draftPass, compliancePass].filter((pass) => pass !== undefined).map((pass) => ({ type: "text" as const, text: readableGateItem(pass.receipt) })),
124
- details: verdict,
116
+ details: parameters,
125
117
  terminate: false,
126
118
  };
127
119
  }
128
120
  return {
129
121
  content: [draftPass, compliancePass].filter((pass) => pass !== undefined).map((pass) => ({ type: "text" as const, text: readableGateItem(pass.receipt) })),
130
- details: verdict,
122
+ details: parameters,
131
123
  terminate: true as const,
132
124
  };
133
125
  } catch (error) {
134
126
  if (error instanceof GatekeeperDecisionError && error.result.status === "escalate") {
135
- return projectGatekeeperEscalation(error.result, verdict);
127
+ return projectGatekeeperEscalation(error.result, parameters);
136
128
  }
137
129
  throw error;
138
130
  }
@@ -20,7 +20,7 @@ export const mergerInputSchema = Type.Object({
20
20
  // #836 (ADR 0003 Amendment): status kept open like countersignStatus
21
21
  // (src/countersign-role.ts) — one shared description across both variants
22
22
  // so openToolObjectFromUnion's identical-declaration collapse drops none of it.
23
- const MERGER_STATUS_DESCRIPTION = "completed | escalate — 形状指引,非 schema 闸" as const;
23
+ const MERGER_STATUS_DESCRIPTION = "completed | escalate" as const;
24
24
  const mergerOutputVariants = Type.Union([
25
25
  Type.Object({ status: Type.Unknown({ description: MERGER_STATUS_DESCRIPTION }), attemptId: Type.String({ description: "已受理合并 attempt 身份" }), report: Type.String({ description: "如实结果报告" }), mergeCommitId: Type.String({ description: "完成合并 commit object ID" }) }, { additionalProperties: false }),
26
26
  Type.Object({ status: Type.Unknown({ description: MERGER_STATUS_DESCRIPTION }), attemptId: Type.String({ description: "已受理合并 attempt 身份" }), diagnosis: Type.String({ description: "合并无法或不应由本席完成的原因(含无进行中合并、无活可干、需新的产品/权力决定)" }), report: Type.String({ description: "如实结果报告" }) }, { additionalProperties: false }),
@@ -8,6 +8,7 @@ import { Type, type Static } from "typebox";
8
8
 
9
9
  import {
10
10
  NAVIGATOR_INVOCATION_ENTRY,
11
+ NAVIGATOR_ROUTE_PLAYBOOK_FAILURE_ENTRY,
11
12
  mintNavigatorInvocationId,
12
13
  } from "./navigator-invocation-identity.ts";
13
14
  import { PACKAGED_ROLE_REGISTRY, type PackagedRole } from "./packaged-role-registry.ts";
@@ -59,11 +60,12 @@ export {
59
60
  export { createNativeNavigatorSessionFactory };
60
61
  export { resolveNavigatorSeatSelection };
61
62
  import { issueRoot, subjectPath } from "./work-subject-identity.ts";
62
- import { createReceiptDeliveryPolicy, NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, RECEIPT_DELIVERY_PROMPT } from "./receipt-delivery-policy.ts";
63
+ import { createReceiptDeliveryPolicy, NO_RECEIPT_LIFECYCLE_ENTRY_TYPE } from "./receipt-delivery-policy.ts";
63
64
  import { navigatorProseFromUnknown } from "./package-contracts/navigator-output.ts";
65
+ import { persistNavigatorWorkBase } from "./navigator-work-base.ts";
64
66
 
65
67
  export const NAVIGATOR_EVENT_TYPE = "ak-navigator-attendance" as const;
66
- export const NAVIGATOR_PREPARE_ACCEPTED_TEXT = "游奕使准备已接受";
68
+ export { NAVIGATOR_ROUTE_PLAYBOOK_FAILURE_ENTRY };
67
69
 
68
70
  /**
69
71
  * Role-input document bytes win verbatim over work-root file authority when non-empty.
@@ -131,23 +133,11 @@ export type NavigatorEvent = {
131
133
  arrivalMessage?: string;
132
134
  };
133
135
 
134
- export type NavigatorSettlementFact = NavigatorSettlement & { invocationId: string; subjectKey: string };
135
- export type NavigatorContextProjection = {
136
- subjectKey: string;
137
- subject: string;
138
- authority: string;
139
- currentRole: { role: string; phase: NavigatorPhase };
140
- /** Present on the unique settlement-bound advice prompt. */
141
- currentSettlement?: NavigatorSettlement;
142
- publicSettlementHistory: NavigatorSettlementFact[];
143
- liveRoleHelp: Array<{ role: NavigatorTargetRole; help: string }>;
144
- };
145
-
146
136
  // #959: prepare is a prose vehicle. Object root only (ADR 0060); nested shape is
147
137
  // never a gate — every object root reaches execute exactly once (Rule 0).
148
138
  const prepareSchema = Type.Object({
149
139
  prose: Type.Optional(Type.Unknown({
150
- description: "游奕使散文建议,原样呈现。不要求 candidates/next 结构。非受理闸",
140
+ description: "游奕使散文建议,原样呈现。不要求 candidates/next 结构。",
151
141
  })),
152
142
  }, { additionalProperties: true });
153
143
  type PrepareOutput = Static<typeof prepareSchema>;
@@ -157,9 +147,6 @@ export type NavigatorAttendanceOptions = {
157
147
  role: string;
158
148
  phase: NavigatorPhase;
159
149
  subjectKey: string;
160
- loadSoul: () => Promise<string>;
161
- loadRoutePlaybook?: () => Promise<string>;
162
- loadRoleHelp: (role: NavigatorTargetRole) => Promise<string>;
163
150
  createSession: NavigatorSessionFactory;
164
151
  modelSettingPath?: string;
165
152
  subject: string;
@@ -170,7 +157,6 @@ export type NavigatorAttendanceOptions = {
170
157
  onEvent: (event: NavigatorEvent, report: NavigatorReport) => void | Promise<void>;
171
158
  };
172
159
 
173
- const CONTEXT_ENTRY = "ak-navigator-context";
174
160
  const INVOCATION_ENTRY = NAVIGATOR_INVOCATION_ENTRY;
175
161
  const SETTLEMENT_ENTRY = "ak-navigator-settlement";
176
162
  const unavailableKeys = new Set<NavigatorUnavailableKey>(["context", "session", "model", "thinking", "auth", "quota", "transport", "unknown"]);
@@ -263,7 +249,7 @@ export function createNavigatorPrepareTool(onOutput: (value: PrepareOutput) => v
263
249
  async execute(_id, value) {
264
250
  // Rule 0: the unique prepare submission is accepted once. Shape is never a gate.
265
251
  onOutput(value as PrepareOutput);
266
- return { content: [{ type: "text" as const, text: NAVIGATOR_PREPARE_ACCEPTED_TEXT }], details: value, terminate: true as const };
252
+ return { content: [], details: value, terminate: true as const };
267
253
  },
268
254
  };
269
255
  }
@@ -271,7 +257,7 @@ export function createNavigatorPrepareTool(onOutput: (value: PrepareOutput) => v
271
257
  export function formatNavigatorReport(report: NavigatorReport): string {
272
258
  const playbookFailure = report.routePlaybookReadFailure === undefined
273
259
  ? []
274
- : [`路书读取失败:${report.routePlaybookReadFailure}`];
260
+ : [report.routePlaybookReadFailure];
275
261
  if (report.disposition === "no-advice") return playbookFailure.join("\n");
276
262
  if (report.disposition === "unavailable") {
277
263
  return [...playbookFailure, ...(report.unavailableReason ? [report.unavailableReason] : [])].join("\n");
@@ -300,16 +286,13 @@ export function createNavigatorAttendance(options: NavigatorAttendanceOptions) {
300
286
  let settlementTail: Promise<void> = Promise.resolve();
301
287
  let settlementFailure: unknown;
302
288
  let preparationFailure: unknown;
303
- let routePlaybookReadFailure: string | undefined;
289
+ let observedRoutePlaybookFailure: string | undefined;
304
290
  let disposed = false;
305
291
  /** In-flight session teardown; repeat dispose returns the same promise (#959 mutation proof). */
306
292
  let closing: Promise<void> | undefined;
307
293
  // Shared attendance seam owns nested summon cancel (ADR 0018 / #959).
308
294
  // Role session factory only forwards HostContext.signal — never its own controller.
309
295
  const nestCancel = new AbortController();
310
- /** One-shot live-help warm; consumed by the next prepare so later prepares reread live help. */
311
- let warmedHelp: Promise<Array<{ role: NavigatorTargetRole; help: string }>> | undefined;
312
-
313
296
  const sessionHostContext = (): HostContext => {
314
297
  const parentSignal = options.context.signal;
315
298
  const signal = parentSignal === undefined
@@ -318,19 +301,6 @@ export function createNavigatorAttendance(options: NavigatorAttendanceOptions) {
318
301
  return { ...options.context, signal };
319
302
  };
320
303
 
321
- const loadLiveHelp = async (): Promise<Array<{ role: NavigatorTargetRole; help: string }>> => {
322
- try {
323
- return await Promise.all(
324
- NAVIGATOR_TARGETS.map(async ({ role }) => ({
325
- role,
326
- help: await options.loadRoleHelp(role),
327
- })),
328
- );
329
- } catch (error) {
330
- throw navigatorUnavailableError("transport", error);
331
- }
332
- };
333
-
334
304
  const unavailable = (invocationId: string, reason: unknown): NavigatorReport => {
335
305
  const failure = reason instanceof NavigatorUnavailableError
336
306
  ? reason
@@ -342,24 +312,13 @@ export function createNavigatorAttendance(options: NavigatorAttendanceOptions) {
342
312
  unavailableCause: failure.unavailableCause,
343
313
  };
344
314
  };
345
- let routePlaybookSettlement: Promise<void> | undefined;
346
315
  /**
347
- * Settlement fed for the output prompt. Early prepare() runs the host round
348
- * from parent start (ready and wait); settleOnce feeds currentSettlement and
349
- * lets navigator speak. No candidates/next parse (owner 2026-09-17 #959).
316
+ * Settlement is the only model round. Standby records attendance and does not prompt.
317
+ * Soul and route playbook stay on the navigator system prompt, not this user turn.
350
318
  */
351
319
  let prepareBoundSettlement: NavigatorSettlement | undefined;
352
- type MaterialsBundle = {
353
- soul: string;
354
- routePlaybook: string;
355
- helpContext: string;
356
- modelSetting: string;
357
- model: ReturnType<typeof parseNavigatorModelSetting>;
358
- help: Array<{ role: NavigatorTargetRole; help: string }>;
359
- activeSession: NavigatorPreparationSession;
360
- };
361
- /** Materials + session only — no host prompt (cold settle book-before-feed). */
362
- const loadMaterialsAndSession = async (invocationId: string): Promise<MaterialsBundle> => {
320
+ /** Session only — no host prompt (standby attendance, or cold settle book-before-feed). */
321
+ const loadMaterialsAndSession = async (invocationId: string): Promise<NavigatorPreparationSession> => {
363
322
  if (contextError !== undefined) throw navigatorUnavailableError("context", contextError);
364
323
  if (typeof authority !== "string" || authority.trim() === "") {
365
324
  throw navigatorUnavailableError(
@@ -367,30 +326,6 @@ export function createNavigatorAttendance(options: NavigatorAttendanceOptions) {
367
326
  new Error("controlling authority content was not supplied as typed work context"),
368
327
  );
369
328
  }
370
- let soul: string;
371
- let modelSetting: string;
372
- let help: Array<{ role: NavigatorTargetRole; help: string }>;
373
- let routePlaybook = "";
374
- routePlaybookReadFailure = undefined;
375
- const soulPromise = (async () => {
376
- try {
377
- const text = (await options.loadSoul()).trim();
378
- if (!text) throw new Error("Navigator soul is empty");
379
- return text;
380
- } catch (error) {
381
- throw navigatorUnavailableError("context", error);
382
- }
383
- })();
384
- const routePlaybookPromise = (async () => {
385
- if (options.loadRoutePlaybook === undefined) return "";
386
- try {
387
- return await options.loadRoutePlaybook();
388
- } catch (error) {
389
- routePlaybookReadFailure = error instanceof Error ? error.message : String(error);
390
- return "";
391
- }
392
- })();
393
- routePlaybookSettlement = routePlaybookPromise.then(() => undefined);
394
329
  const modelPromise = (async () => {
395
330
  try {
396
331
  const resolved = await resolveNavigatorSeatSelection(options.context);
@@ -400,21 +335,13 @@ export function createNavigatorAttendance(options: NavigatorAttendanceOptions) {
400
335
  throw navigatorUnavailableError("model", error);
401
336
  }
402
337
  })();
403
- const helpPromise = warmedHelp ?? loadLiveHelp();
404
- warmedHelp = undefined;
405
- [soul, routePlaybook, modelSetting, help] = await Promise.all([
406
- soulPromise,
407
- routePlaybookPromise,
408
- modelPromise,
409
- helpPromise,
410
- ]);
338
+ const modelSetting = await modelPromise;
411
339
  let model: ReturnType<typeof parseNavigatorModelSetting>;
412
340
  try {
413
341
  model = parseNavigatorModelSetting(modelSetting);
414
342
  } catch (error) {
415
343
  throw navigatorUnavailableError("model", error);
416
344
  }
417
- const helpContext = help.map(({ role, help: text }) => `<role_help role="${role}">\n${text}\n</role_help>`).join("\n");
418
345
  const tool = createNavigatorPrepareTool((value) => { outputSink?.(value); });
419
346
  if (session === undefined) {
420
347
  sessionReady = (async () => {
@@ -457,9 +384,9 @@ export function createNavigatorAttendance(options: NavigatorAttendanceOptions) {
457
384
  }
458
385
  if (disposed) throw navigatorUnavailableError("session", new Error("Navigator attendance was disposed"));
459
386
  if (session === undefined) throw new Error("Navigator session was not created");
460
- return { soul, routePlaybook, helpContext, modelSetting, model, help, activeSession: session };
387
+ return session;
461
388
  };
462
- const prepare = async (materialsBundle?: MaterialsBundle): Promise<string | undefined> => {
389
+ const prepare = async (existingSession?: NavigatorPreparationSession): Promise<string | undefined> => {
463
390
  // Exact principal is owned by shared lifecycle (or one mint per attendance).
464
391
  // Model/tool/advice paths cannot override it; role-session persistence is
465
392
  // pi.appendEntry at lifecycle start — not optional sessionManager probing.
@@ -480,53 +407,14 @@ export function createNavigatorAttendance(options: NavigatorAttendanceOptions) {
480
407
  const merged = [prior, next].filter((part) => part.trim() !== "").join("\n\n");
481
408
  output = { prose: merged };
482
409
  };
483
- // Cold settle passes its first-load bundle; early/warm prepare loads here.
484
- const { soul, routePlaybook, helpContext, help, activeSession } =
485
- materialsBundle ?? await loadMaterialsAndSession(invocationId);
486
- const publicSettlementHistory = activeSession.entries()
487
- .filter((entry): entry is { type: "custom"; customType: string; data?: unknown } => exactRecord(entry) && entry.type === "custom" && entry.customType === SETTLEMENT_ENTRY && exactRecord(entry.data))
488
- .map((entry) => entry.data as NavigatorSettlementFact);
489
- // Prior advice is the host session's own history (CLI resume), not a package ledger.
490
- const projection: NavigatorContextProjection = {
410
+ const activeSession = existingSession ?? await loadMaterialsAndSession(invocationId);
411
+ // Standby records the invocation entry and does not call the model.
412
+ if (boundSettlement === undefined) return undefined;
413
+ const request = JSON.stringify({
414
+ ...boundSettlement,
415
+ invocationId,
491
416
  subjectKey,
492
- subject,
493
- authority,
494
- currentRole: { role: options.role, phase: options.phase },
495
- ...(boundSettlement === undefined ? {} : { currentSettlement: boundSettlement }),
496
- publicSettlementHistory,
497
- liveRoleHelp: help,
498
- };
499
- activeSession.appendEntry(CONTEXT_ENTRY, projection);
500
- // Early (parent start): full host round — ready and wait. Settlement-bound:
501
- // feed the result and let navigator speak (owner 2026-09-17 #959).
502
- // ADR 0073: phase behavior text lives in resources/navigator-route-playbook.md;
503
- // code only binds a neutral phase pointer + typed materials.
504
- const request = boundSettlement === undefined
505
- ? [
506
- "本轮为待命轮。",
507
- `<navigator_soul>\n${soul}\n</navigator_soul>`,
508
- ...(routePlaybookReadFailure === undefined
509
- ? [`<route_playbook>\n${routePlaybook}\n</route_playbook>`]
510
- : ["可选路线手册未能读取。"]),
511
- `<work_subject>\n${subject}\n</work_subject>`,
512
- `<controlling_authority>\n${authority}\n</controlling_authority>`,
513
- `<current_role>\n${JSON.stringify({ role: options.role, phase: options.phase })}\n</current_role>`,
514
- `<public_settlement_history>\n${JSON.stringify(projection.publicSettlementHistory)}\n</public_settlement_history>`,
515
- `<live_role_help>\n${helpContext}\n</live_role_help>`,
516
- ].join("\n\n")
517
- : [
518
- "本轮为结算投喂轮。",
519
- `<navigator_soul>\n${soul}\n</navigator_soul>`,
520
- ...(routePlaybookReadFailure === undefined
521
- ? [`<route_playbook>\n${routePlaybook}\n</route_playbook>`]
522
- : ["可选路线手册未能读取。"]),
523
- `<work_subject>\n${subject}\n</work_subject>`,
524
- `<controlling_authority>\n${authority}\n</controlling_authority>`,
525
- `<current_role>\n${JSON.stringify({ role: options.role, phase: options.phase })}\n</current_role>`,
526
- `<current_settlement>\n${JSON.stringify(boundSettlement)}\n</current_settlement>`,
527
- `<public_settlement_history>\n${JSON.stringify(projection.publicSettlementHistory)}\n</public_settlement_history>`,
528
- `<live_role_help>\n${helpContext}\n</live_role_help>`,
529
- ].join("\n\n");
417
+ });
530
418
  try {
531
419
  try {
532
420
  if (disposed) throw navigatorUnavailableError("session", new Error("Navigator attendance was disposed"));
@@ -535,6 +423,7 @@ export function createNavigatorAttendance(options: NavigatorAttendanceOptions) {
535
423
  // (navigator-public-session). No assistant-entry harvest — entries() is
536
424
  // archivist custom-only on the wired factory (#959).
537
425
  const promptAllowingRejectedPrepare = async (text: string, deliveryRequest: boolean) => {
426
+ await persistNavigatorWorkBase(activeSession.recordPointer(), { subject, authority });
538
427
  const entryStart = activeSession.entries().length;
539
428
  prepareBatchRejected = false;
540
429
  let promptFailure: unknown;
@@ -572,7 +461,7 @@ export function createNavigatorAttendance(options: NavigatorAttendanceOptions) {
572
461
  // does not 催交 final advice (owner: prepare then wait for settlement feed).
573
462
  if (boundSettlement !== undefined) {
574
463
  while (output === undefined && prepareBatchRejected && delivery.nextAction() === "request-delivery") {
575
- await promptAllowingRejectedPrepare(RECEIPT_DELIVERY_PROMPT, true);
464
+ await promptAllowingRejectedPrepare(JSON.stringify(delivery.deliveryState()), true);
576
465
  }
577
466
  if (output === undefined && delivery.nextAction() === "request-delivery") {
578
467
  while (delivery.nextAction() === "request-delivery") {
@@ -618,6 +507,10 @@ export function createNavigatorAttendance(options: NavigatorAttendanceOptions) {
618
507
  preparedProse = normalizePrepareProse(output);
619
508
  return preparedProse;
620
509
  } finally {
510
+ const playbookFailure = activeSession.routePlaybookReadFailure?.();
511
+ observedRoutePlaybookFailure = typeof playbookFailure === "string" && playbookFailure.trim() !== ""
512
+ ? playbookFailure
513
+ : undefined;
621
514
  outputSink = undefined;
622
515
  }
623
516
  };
@@ -636,17 +529,6 @@ export function createNavigatorAttendance(options: NavigatorAttendanceOptions) {
636
529
  contextError = next.contextError;
637
530
  return closing;
638
531
  },
639
- /**
640
- * Start live-help subprocesses during activation without beginning full
641
- * preparation. Next prepare() consumes the warm result; a later prepare
642
- * reloads so live help edits remain visible.
643
- */
644
- warmHelp(): void {
645
- if (disposed || warmedHelp !== undefined || preparation !== undefined) return;
646
- warmedHelp = loadLiveHelp();
647
- // Prevent unhandled rejection if attendance is disposed before prepare.
648
- void warmedHelp.catch(() => undefined);
649
- },
650
532
  prepare(): void {
651
533
  if (disposed || preparation !== undefined) return;
652
534
  preparationFailure = undefined;
@@ -662,9 +544,6 @@ export function createNavigatorAttendance(options: NavigatorAttendanceOptions) {
662
544
  isPreparing(): boolean {
663
545
  return preparationInFlight;
664
546
  },
665
- knownRoutePlaybookReadFailure(): string | undefined {
666
- return routePlaybookReadFailure;
667
- },
668
547
  settle(settlement: NavigatorSettlement): Promise<void> {
669
548
  const next = settlementTail.then(() => settleOnce(settlement));
670
549
  // Contract: README.md#Navigator-attendance — rejected attendance settlements are drained only to serialize later attendance; retain the exact rejection for the caller/audit path.
@@ -698,10 +577,11 @@ export function createNavigatorAttendance(options: NavigatorAttendanceOptions) {
698
577
  async function settleOnce(settlement: NavigatorSettlement): Promise<void> {
699
578
  // Dispose during post-role grace must ignore late completion entirely (#675).
700
579
  if (disposed) return;
580
+ observedRoutePlaybookFailure = undefined;
701
581
  const invocationId = activeInvocationId ?? invocationPrincipal;
702
582
  let report: NavigatorReport;
703
- // Drain in-flight early prepare (parent-start host round: ready and wait).
704
- // Then feed settlement for the output prompt on the same host session.
583
+ // Drain in-flight standby attendance (record only). Then feed the typed
584
+ // settlement as the only model round on the same host session.
705
585
  if (sessionReady !== undefined) {
706
586
  try { await sessionReady; } catch (error) { preparationFailure ??= error; }
707
587
  }
@@ -740,12 +620,12 @@ export function createNavigatorAttendance(options: NavigatorAttendanceOptions) {
740
620
  // session already exists so the output prompt can run (owner: 准备好了就等着,
741
621
  // 结果出来喂给它让它自己输出).
742
622
  if (session !== undefined) preparationFailure = undefined;
743
- // Cold settle: materials+session only (no unbound host prompt), then book + feed.
744
- // Pass the bundle into prepare so loadLiveHelp/setModel/INVOCATION run once.
745
- let coldMaterials: MaterialsBundle | undefined;
623
+ // Cold settle: session only (no unbound host prompt), then book + feed.
624
+ // Pass the session into prepare so setModel/INVOCATION run once.
625
+ let coldSession: NavigatorPreparationSession | undefined;
746
626
  if (session === undefined && preparationFailure === undefined) {
747
627
  try {
748
- coldMaterials = await loadMaterialsAndSession(invocationId);
628
+ coldSession = await loadMaterialsAndSession(invocationId);
749
629
  } catch (error) {
750
630
  preparationFailure = error;
751
631
  }
@@ -753,7 +633,7 @@ export function createNavigatorAttendance(options: NavigatorAttendanceOptions) {
753
633
  session?.appendEntry(SETTLEMENT_ENTRY, settlementFact);
754
634
  if (preparationFailure === undefined) {
755
635
  prepareBoundSettlement = settlement;
756
- preparation = prepare(coldMaterials);
636
+ preparation = prepare(coldSession);
757
637
  void preparation.catch((error) => { preparationFailure = error; });
758
638
  try {
759
639
  drainedProse = await preparation;
@@ -777,12 +657,8 @@ export function createNavigatorAttendance(options: NavigatorAttendanceOptions) {
777
657
  report = { disposition: "no-advice" };
778
658
  }
779
659
  }
780
- // A primary preparation failure may reject Promise.all before the optional
781
- // routebook read finishes. Preserve that primary unavailable cause while
782
- // waiting for, and independently attaching, the routebook diagnostic.
783
- await routePlaybookSettlement;
784
- if (routePlaybookReadFailure !== undefined) {
785
- report = { ...report, routePlaybookReadFailure };
660
+ if (observedRoutePlaybookFailure !== undefined) {
661
+ report = { ...report, routePlaybookReadFailure: observedRoutePlaybookFailure };
786
662
  }
787
663
  const event: NavigatorEvent = {
788
664
  version: 1,
@@ -813,8 +689,6 @@ export function createNavigatorAttendance(options: NavigatorAttendanceOptions) {
813
689
  sessionReady = undefined;
814
690
  preparedProse = undefined;
815
691
  preparationFailure = undefined;
816
- routePlaybookSettlement = undefined;
817
- routePlaybookReadFailure = undefined;
818
692
  }
819
693
  }
820
694
 
@@ -29,6 +29,8 @@ import { isUuidV7, uuidv7 } from "./uuidv7.ts";
29
29
  import { workSubjectKeysEqual } from "./work-subject-identity.ts";
30
30
 
31
31
  export const NAVIGATOR_INVOCATION_ENTRY = "ak-navigator-invocation" as const;
32
+ /** Native playbook read failure recorded by the navigator process that read the file. */
33
+ export const NAVIGATOR_ROUTE_PLAYBOOK_FAILURE_ENTRY = "ak-navigator-route-playbook-failure" as const;
32
34
 
33
35
  /** Typed durable infrastructure-failure fact on a packaged role output toolResult. */
34
36
  export const NAVIGATOR_INFRASTRUCTURE_FAILURE_KIND = "role_infrastructure_failure" as const;
@@ -6,6 +6,7 @@
6
6
  * host conversation continuity uses CLI resume (`resumeRunId`), not a package
7
7
  * advice ledger (development-closure: do not invent package-level memory).
8
8
  */
9
+ import { existsSync } from "node:fs";
9
10
  import { basename } from "node:path";
10
11
 
11
12
  import { sitianReport } from "./sitian-facade.ts";
@@ -23,6 +24,10 @@ import { runDirectoryFromHostContext, type HostContext } from "./host-contracts.
23
24
  import type { PublicSummonResult } from "./public-role-summons.ts";
24
25
  import { CliUsageError } from "./public-cli/cli-errors.ts";
25
26
  import { isNavigatorSeat } from "./packaged-role-registry.ts";
27
+ import {
28
+ attachWorkContextPointer,
29
+ navigatorWorkContextFile,
30
+ } from "./navigator-work-base.ts";
26
31
 
27
32
  /**
28
33
  * Ledger process home for navigator attendance: admitted HostContext.runDirectory
@@ -130,6 +135,7 @@ export function createNativeNavigatorSessionFactory(deps?: {
130
135
 
131
136
  let providerFailure: NavigatorProviderFailureFact | undefined;
132
137
  let noReceipt: NoReceiptLifecycleFacts | undefined;
138
+ let routePlaybookReadFailure: string | undefined;
133
139
  let disposed = false;
134
140
  /** In-factory host run id for CLI resume; durable pointer also lives on the nest. */
135
141
  let hostRunId = readNavigatorHostRunPointer(sessionManager.getEntries() as readonly unknown[]);
@@ -147,18 +153,23 @@ export function createNativeNavigatorSessionFactory(deps?: {
147
153
  }
148
154
  providerFailure = undefined;
149
155
  noReceipt = undefined;
156
+ routePlaybookReadFailure = undefined;
150
157
  try {
151
158
  const summonHome = await resolveNavigatorLedgerHome(context);
152
159
  // Admission after every await: dispose during preflight must not start summon
153
160
  // (ADR 0018 / #959 — legal HostContext may omit signal).
154
161
  if (disposed) return;
155
162
  const resumeRunId = hostRunId;
163
+ const workContextPath = navigatorWorkContextFile(sessionManager.getSessionDir());
164
+ const argvText = existsSync(workContextPath)
165
+ ? attachWorkContextPointer(text, workContextPath)
166
+ : text;
156
167
 
157
168
  // Call contract only: forward shared-lifecycle signal; do not own AbortController here
158
169
  // (ADR 0018 / #959 — cancel ownership stays on the attendance/envelope seam).
159
170
  const baseSummon = {
160
171
  role: "navigator" as const,
161
- argv: [text] as const,
172
+ argv: [argvText] as const,
162
173
  cwd: context.cwd,
163
174
  ...(summonHome === undefined ? {} : { home: summonHome }),
164
175
  ...(context.signal === undefined ? {} : { signal: context.signal }),
@@ -186,6 +197,10 @@ export function createNativeNavigatorSessionFactory(deps?: {
186
197
  // Cancel ownership stays on attendance/envelope; this only closes side effects (#959).
187
198
  if (disposed) return;
188
199
 
200
+ const playbookFailure = summoned.terminal?.navigator?.advisoryDiagnostic;
201
+ routePlaybookReadFailure = typeof playbookFailure === "string" && playbookFailure.trim() !== ""
202
+ ? playbookFailure
203
+ : undefined;
189
204
  const outcome = summoned.terminal?.roleOutcome;
190
205
  if (outcome === undefined) {
191
206
  const detail = summoned.stderr?.trim() || `exit ${summoned.exitCode}`;
@@ -258,6 +273,7 @@ export function createNativeNavigatorSessionFactory(deps?: {
258
273
  },
259
274
  providerFailure: () => providerFailure,
260
275
  noReceipt: () => noReceipt,
276
+ routePlaybookReadFailure: () => routePlaybookReadFailure,
261
277
  appendEntry: (customType, data) => {
262
278
  sessionManager.appendCustomEntry(customType, data);
263
279
  try {
@@ -14,4 +14,8 @@ export const NAVIGATOR_TOOL_SPEC = {
14
14
 
15
15
  export type NavigatorRuntimeDependencies = {
16
16
  loadSoul(): Promise<string>;
17
+ /** Standing route playbook. Production omits this and reads the package file once. */
18
+ loadRoutePlaybook?(): Promise<string>;
19
+ /** Same read's native failure, for the existing routePlaybookReadFailure outlet. */
20
+ recordRoutePlaybookReadFailure?(message: string | undefined): void;
17
21
  };
@@ -203,6 +203,8 @@ export type NavigatorPreparationSession = {
203
203
  * independent summon rather than press the session that owes the receipt.
204
204
  */
205
205
  noReceipt?(): NoReceiptLifecycleFacts | undefined;
206
+ /** Native message from the nested playbook read, when that read failed. */
207
+ routePlaybookReadFailure?(): string | undefined;
206
208
  setModel?(model: string, thinkingLevel?: string): Promise<void>;
207
209
  getThinkingLevel?(): string | undefined;
208
210
  recordPointer(): string;