@akagilnc/pi-workflow-roles 0.1.5845 → 0.1.5853

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 (66) hide show
  1. package/README.md +4 -4
  2. package/README.zh-CN.md +4 -4
  3. package/dist/acp-host/production-host.js +348 -1254
  4. package/dist/acp-host/role-turn-host.js +4 -98
  5. package/dist/headless-host/production-host.js +15731 -16582
  6. package/dist/headless-host/role-turn-host.js +6 -31
  7. package/dist/migrate-book-topology.js +31 -16
  8. package/dist/navigator-attendance.js +54 -404
  9. package/dist/navigator-invocation-identity.js +0 -2
  10. package/dist/navigator-lookup-host.js +41 -0
  11. package/dist/navigator-lookup.js +68 -0
  12. package/dist/navigator-route-table.js +42 -0
  13. package/dist/package-contracts/navigator-output.js +3 -11
  14. package/dist/packaged-role-registry.js +3 -27
  15. package/dist/public-cli/instruction-seat-run.js +4 -1
  16. package/dist/public-cli/invocation.js +13 -26
  17. package/dist/public-cli/main.js +286 -114
  18. package/dist/public-cli/option-definitions.js +24 -4
  19. package/dist/public-cli/post-admission.js +0 -9
  20. package/dist/public-cli/role-seat-runtime.js +14 -0
  21. package/dist/public-cli/settlement.js +14 -40
  22. package/dist/public-cli/terminal.js +1 -8
  23. package/dist/public-cli/turn-request.js +0 -3
  24. package/dist/public-role-summons.js +0 -1
  25. package/dist/receipt-delivery-policy.js +0 -23
  26. package/dist/role-runtime-dependencies.js +4 -11
  27. package/dist/role-runtime.js +18 -194
  28. package/dist/submission-ledger.js +19 -15
  29. package/dist/ticket-progress.js +23 -5
  30. package/extensions/role-runtime.ts +0 -16
  31. package/package.json +1 -1
  32. package/scripts/build-package.mjs +3 -7
  33. package/src/acp-host/role-turn-host.ts +8 -97
  34. package/src/headless-host/description.ts +0 -2
  35. package/src/headless-host/role-turn-host.ts +7 -32
  36. package/src/host-contracts.ts +5 -7
  37. package/src/navigator-attendance.ts +86 -549
  38. package/src/navigator-invocation-identity.ts +0 -2
  39. package/src/navigator-lookup-host.ts +45 -0
  40. package/src/navigator-lookup.ts +98 -0
  41. package/src/navigator-route-table.ts +92 -0
  42. package/src/package-contracts/navigator-output.ts +4 -15
  43. package/src/packaged-role-registry.ts +3 -32
  44. package/src/public-cli/instruction-seat-run.ts +3 -0
  45. package/src/public-cli/invocation.ts +18 -30
  46. package/src/public-cli/option-definitions.ts +25 -4
  47. package/src/public-cli/post-admission.ts +0 -11
  48. package/src/public-cli/role-seat-runtime.ts +14 -0
  49. package/src/public-cli/settlement.ts +23 -51
  50. package/src/public-cli/terminal.ts +3 -18
  51. package/src/public-cli/turn-request.ts +0 -5
  52. package/src/public-role-summons.ts +0 -8
  53. package/src/receipt-delivery-policy.ts +0 -21
  54. package/src/role-runtime-dependencies.ts +4 -11
  55. package/src/role-runtime.ts +19 -210
  56. package/src/submission-ledger.ts +23 -16
  57. package/src/ticket-progress.ts +32 -5
  58. package/dist/navigator-public-session.js +0 -205
  59. package/dist/navigator-role.js +0 -6
  60. package/dist/navigator-session-contracts.js +0 -227
  61. package/dist/navigator-work-context.js +0 -72
  62. package/resources/navigator-route-playbook.md +0 -33
  63. package/src/navigator-public-session.ts +0 -305
  64. package/src/navigator-role.ts +0 -15
  65. package/src/navigator-session-contracts.ts +0 -320
  66. package/src/navigator-work-context.ts +0 -97
@@ -23,8 +23,6 @@ import { isRecord } from "./unknown-value.ts";
23
23
  import { workSubjectKeysEqual } from "./work-subject-identity.ts";
24
24
 
25
25
  export const NAVIGATOR_INVOCATION_ENTRY = "ak-navigator-invocation" as const;
26
- /** Native playbook read failure recorded by the navigator process that read the file. */
27
- export const NAVIGATOR_ROUTE_PLAYBOOK_FAILURE_ENTRY = "ak-navigator-route-playbook-failure" as const;
28
26
 
29
27
  /** Typed durable infrastructure-failure fact on a packaged role output toolResult. */
30
28
  export const NAVIGATOR_INFRASTRUCTURE_FAILURE_KIND = "role_infrastructure_failure" as const;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * 游奕使的非模型回合宿主(#1192 / ADR 0082「不适配者按类型开新管线」)。
3
+ * 不起宿主会话、不调模型:按结构化的目标角色查 navigator-route-table,
4
+ * 经提交账封一行,结算照常读账本。
5
+ */
6
+ import { randomUUID } from "node:crypto";
7
+
8
+ import type { RoleTurnHost } from "./host-contracts.ts";
9
+ import { navigatorFullTable, navigatorRowsForRole } from "./navigator-route-table.ts";
10
+ import { PACKAGED_ROLE_REGISTRY, type PackagedRole } from "./packaged-role-registry.ts";
11
+ import { sealAcceptedSubmission } from "./submission-ledger.ts";
12
+
13
+ /** 记入 invocation 的宿主名:如实表明这一腿是查表,不冒用真实宿主。 */
14
+ export const NAVIGATOR_LOOKUP_HOST = "lookup" as const;
15
+
16
+ function targetRole(forRole: string): PackagedRole {
17
+ const known = PACKAGED_ROLE_REGISTRY.find(({ role }) => role === forRole);
18
+ if (known === undefined) throw new Error(`navigator lookup: unknown role ${forRole}`);
19
+ return known.role;
20
+ }
21
+
22
+ export function createNavigatorLookupHost(): RoleTurnHost {
23
+ return {
24
+ async executeTurn(request) {
25
+ const { activation } = request;
26
+ if (activation.role !== "navigator") {
27
+ throw new Error(`navigator lookup host cannot run ${activation.role}`);
28
+ }
29
+ const accepted = activation.forRole === undefined
30
+ ? { byRole: navigatorFullTable() }
31
+ : { byStatus: navigatorRowsForRole(targetRole(activation.forRole)) };
32
+ sealAcceptedSubmission({
33
+ runDirectory: request.runDirectory,
34
+ cwd: request.cwd,
35
+ home: request.home,
36
+ role: "navigator",
37
+ accepted,
38
+ instruction: request.continuation.prompt,
39
+ toolCallId: `navigator-lookup:${randomUUID()}`,
40
+ ...(request.courtAttemptId === undefined ? {} : { courtAttemptId: request.courtAttemptId }),
41
+ });
42
+ return { code: 0, stderr: "", timedOut: false };
43
+ },
44
+ };
45
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Automatic navigator attendance lookup (#1192, builds on #1160).
3
+ * The parent role's start summons the navigator public activation — an ordinary
4
+ * `@navigator` run that looks its rows up in navigator-route-table with no model
5
+ * and no host session. The parent role arrives as the structured `--role` input,
6
+ * never inferred from prose. A failed lookup is reported with its real cause.
7
+ */
8
+ import {
9
+ mergePreparedAdvice,
10
+ preparedAdviceFromUnknown,
11
+ type PreparedNavigatorAdvice,
12
+ } from "./package-contracts/navigator-output.ts";
13
+ import { runDirectoryFromHostContext, type HostContext } from "./host-contracts.ts";
14
+ import type { PublicSummonResult } from "./public-role-summons.ts";
15
+
16
+ export type NavigatorUnavailableKey = "session" | "unknown";
17
+
18
+ export class NavigatorUnavailableError extends Error {
19
+ readonly unavailableSource: NavigatorUnavailableKey;
20
+ readonly originalCause: unknown;
21
+
22
+ constructor(source: NavigatorUnavailableKey, message: string, originalCause?: unknown) {
23
+ super(message);
24
+ this.name = "NavigatorUnavailableError";
25
+ this.unavailableSource = source;
26
+ this.originalCause = originalCause;
27
+ }
28
+ }
29
+
30
+ export function navigatorUnavailableError(source: NavigatorUnavailableKey, error: unknown): NavigatorUnavailableError {
31
+ if (error instanceof NavigatorUnavailableError) return error;
32
+ const message = error instanceof Error ? error.message : String(error);
33
+ return new NavigatorUnavailableError(source, message, error);
34
+ }
35
+
36
+ /** One attendance lookup: the parent role's rows, or undefined when it has none. */
37
+ export type NavigatorLookup = (input: {
38
+ readonly context: HostContext;
39
+ readonly role: string;
40
+ }) => Promise<PreparedNavigatorAdvice | undefined>;
41
+
42
+ type NavigatorSummon = (options: {
43
+ readonly role: "navigator";
44
+ readonly argv: readonly string[];
45
+ readonly cwd: string;
46
+ readonly home?: string;
47
+ readonly signal?: AbortSignal;
48
+ }) => Promise<PublicSummonResult>;
49
+
50
+ /**
51
+ * Ledger process home for the nested summon: the admitted run directory first,
52
+ * else the parent session file under .ak-roles. Never ambient HOME (#852).
53
+ */
54
+ async function ledgerHomeFor(context: HostContext): Promise<string | undefined> {
55
+ const { tryHomeFromAkRolesPath } = await import("./activation-ledger-topology.ts");
56
+ for (const path of [runDirectoryFromHostContext(context), context.sessionManager?.getSessionFile?.()]) {
57
+ if (typeof path !== "string" || path.trim() === "") continue;
58
+ const home = tryHomeFromAkRolesPath(path);
59
+ if (home !== undefined && home.length > 0) return home;
60
+ }
61
+ return undefined;
62
+ }
63
+
64
+ export function createNavigatorLookup(deps?: { readonly summonPublicRole?: NavigatorSummon }): NavigatorLookup {
65
+ const summon: NavigatorSummon = deps?.summonPublicRole
66
+ ?? (async (options) => (await import("./public-role-summons.ts")).summonPublicRole(options));
67
+ return async ({ context, role }) => {
68
+ const home = await ledgerHomeFor(context);
69
+ let summoned: PublicSummonResult;
70
+ try {
71
+ summoned = await summon({
72
+ role: "navigator",
73
+ argv: ["--role", role],
74
+ cwd: context.cwd,
75
+ ...(home === undefined ? {} : { home }),
76
+ ...(context.signal === undefined ? {} : { signal: context.signal }),
77
+ });
78
+ } catch (error) {
79
+ throw navigatorUnavailableError("session", error);
80
+ }
81
+ const outcome = summoned.terminal?.roleOutcome;
82
+ if (outcome === undefined) {
83
+ const detail = summoned.stderr?.trim() || `exit ${summoned.exitCode}`;
84
+ throw new NavigatorUnavailableError("session", `Navigator lookup produced no terminal (${detail})`);
85
+ }
86
+ if (outcome.kind === "failure") {
87
+ throw new NavigatorUnavailableError("session", outcome.diagnostic);
88
+ }
89
+ if (outcome.kind !== "accepted") {
90
+ throw new NavigatorUnavailableError("session", `Navigator lookup settled ${outcome.kind}`);
91
+ }
92
+ let prepared: PreparedNavigatorAdvice | undefined;
93
+ for (const payload of outcome.payloads ?? []) {
94
+ prepared = mergePreparedAdvice(prepared, preparedAdviceFromUnknown(payload));
95
+ }
96
+ return prepared;
97
+ };
98
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * 游奕使的唯一路线表(#1192):(衙门角色, 结局 status) → 一句建议。
3
+ * 本表是路线的唯一真源;游奕使不调模型,只按行取出。建议只供调用者参考,
4
+ * 不执行下一站,也不读判词正文猜分支——需判词点名下一步的写「按判词处置」。
5
+ * 没有结局 status 的情况(如宿主故障;通进司的回执也没有 status 字段)与未列出的
6
+ * status(如 coder 的 planned)不设行,走无建议。
7
+ * 闸在同一次公开调用内自动传召的站(闸表真源:gatekeeper-role.ts 的
8
+ * GATE_OFFICER_BY_SUBJECT 与 judge-role.ts 的 JUDGE_GATES)不再建议;
9
+ * 建议写闸线之后的下一站。
10
+ * 每句 20 字以内。
11
+ */
12
+ import type { DiaristOutput } from "./diarist-contracts.ts";
13
+ import type { MergerOutput } from "./merger-contracts.ts";
14
+ import type { PackagedRole } from "./packaged-role-registry.ts";
15
+ import type { GatekeeperDirectOutput } from "./package-contracts/gatekeeper-output.ts";
16
+ import type { ReviewerIntent } from "./package-contracts/reviewer-output.ts";
17
+ import type { CoderOutput } from "./package-contracts/worker-output.ts";
18
+ import type { ReviewQueueWord } from "./review-submission.ts";
19
+
20
+ export const NAVIGATOR_ADVICE_MAX_CHARS = 20;
21
+
22
+ const DISPOSE_BY_VERDICT = "按判词处置";
23
+ const ESCALATE_TO_OWNER = "呈陛下裁决";
24
+ const DISPATCH_WORKER = "派将作监开工";
25
+ const SETTLED_TO_JUDGE = "可送大理寺审计";
26
+ const RESUME_SAME_WORKER = "续修,勿送大理寺";
27
+
28
+ export type NavigatorRouteRows = Readonly<Record<string, string>>;
29
+
30
+ /** Rows keyed by a seat's own status words; a lawful status may have no row. */
31
+ type Rows<Status extends string> = Readonly<Partial<Record<Status, string>>>;
32
+
33
+ /**
34
+ * Status keys follow each seat's existing receipt type. The fixer's output type
35
+ * leaves status open (its words are the coder's, per the fixer contract) and the
36
+ * secretariat's receipt has no status type, so those two stay on their words.
37
+ */
38
+ type RoleRows = {
39
+ readonly judge: Rows<ReviewQueueWord>;
40
+ readonly countersign: Rows<ReviewQueueWord>;
41
+ readonly notary: Rows<ReviewQueueWord>;
42
+ readonly inspector: Rows<ReviewQueueWord>;
43
+ readonly coder: Rows<CoderOutput["status"]>;
44
+ readonly fixer: Rows<CoderOutput["status"]>;
45
+ readonly diarist: Rows<DiaristOutput["status"]>;
46
+ readonly merger: Rows<MergerOutput["status"]>;
47
+ readonly secretariat: NavigatorRouteRows;
48
+ readonly reviewer: Rows<ReviewerIntent["status"]>;
49
+ readonly gatekeeper: Rows<GatekeeperDirectOutput["status"]>;
50
+ };
51
+
52
+ export type NavigatorRouteTable = Readonly<Partial<Record<PackagedRole, NavigatorRouteRows>>>;
53
+
54
+ const ROUTE_ROWS = {
55
+ secretariat: { converged: DISPATCH_WORKER, escalate: ESCALATE_TO_OWNER },
56
+ countersign: { converged: DISPATCH_WORKER, continue: DISPOSE_BY_VERDICT, escalate: ESCALATE_TO_OWNER },
57
+ notary: { converged: "按流程进下一站", continue: DISPOSE_BY_VERDICT, escalate: ESCALATE_TO_OWNER },
58
+ inspector: { converged: "按流程进下一站", continue: DISPOSE_BY_VERDICT, escalate: ESCALATE_TO_OWNER },
59
+ coder: {
60
+ completed: "派御史台",
61
+ partially_completed: SETTLED_TO_JUDGE,
62
+ refused: SETTLED_TO_JUDGE,
63
+ unfinished: RESUME_SAME_WORKER,
64
+ },
65
+ fixer: {
66
+ completed: "直回大理寺复审",
67
+ partially_completed: SETTLED_TO_JUDGE,
68
+ refused: SETTLED_TO_JUDGE,
69
+ unfinished: RESUME_SAME_WORKER,
70
+ },
71
+ reviewer: { completed: "派大理寺裁决", refused: "呈调用者处置" },
72
+ judge: {
73
+ converged: "调用者合并、关票",
74
+ continue: DISPOSE_BY_VERDICT,
75
+ escalate: ESCALATE_TO_OWNER,
76
+ },
77
+ gatekeeper: { dispatch: "召所点名的官员", pass: "放行,进下一站" },
78
+ merger: { completed: "调用者收尾、关票", escalate: ESCALATE_TO_OWNER },
79
+ diarist: { completed: "继续后续衙门", escalate: ESCALATE_TO_OWNER },
80
+ } as const satisfies RoleRows;
81
+
82
+ export const NAVIGATOR_ROUTE_TABLE: NavigatorRouteTable = ROUTE_ROWS;
83
+
84
+ /** 某衙门的全部行;表中无此衙门时为空对象。 */
85
+ export function navigatorRowsForRole(role: PackagedRole): NavigatorRouteRows {
86
+ return NAVIGATOR_ROUTE_TABLE[role] ?? {};
87
+ }
88
+
89
+ /** 整表:衙门 → (status → 建议)。 */
90
+ export function navigatorFullTable(): NavigatorRouteTable {
91
+ return NAVIGATOR_ROUTE_TABLE;
92
+ }
@@ -26,6 +26,10 @@ export const navigatorAdviceFields = {
26
26
  "按主衙门结局 status 预写的散文建议。键为 status 字面量,值为建议正文。"
27
27
  + "结算时代码只按实际 status 取对应原文,不解析建议语义。",
28
28
  }),
29
+ byRole: Type.Unknown({
30
+ description:
31
+ "整张路线表:键为衙门角色,值为该衙门 status → 建议。直接入口不给角色时使用。",
32
+ }),
29
33
  } as const;
30
34
 
31
35
  /** Open tool object for prose/byStatus (attendance prepare + public output body). */
@@ -73,13 +77,6 @@ function byStatusFromUnknown(value: unknown): Map<string, string> {
73
77
  return out;
74
78
  }
75
79
 
76
- /** Plain object for tool.execute / host payloads; Object.fromEntries keeps opaque keys. */
77
- export function byStatusToPlainObject(
78
- byStatus: ReadonlyMap<string, string>,
79
- ): Record<string, string> {
80
- return Object.fromEntries(byStatus);
81
- }
82
-
83
80
  /**
84
81
  * Project one Navigator advice receipt as prose (display / single-advice path).
85
82
  * Accepts { prose }, a bare string, or any object — objects stringify as the
@@ -114,14 +111,6 @@ export function validateRecordedNavigatorOutput(value: unknown): NavigatorAdvice
114
111
  return projected;
115
112
  }
116
113
 
117
- /** Extract display prose from any navigator payload / attendance body. */
118
- export function navigatorProseFromUnknown(value: unknown): string | undefined {
119
- const projected = projectLawfulNavigatorOutput(value);
120
- if (projected === undefined) return undefined;
121
- const trimmed = projected.prose.trim();
122
- return trimmed === "" ? undefined : projected.prose;
123
- }
124
-
125
114
  /**
126
115
  * Extract parallel-prepare advice. Empty / missing → undefined.
127
116
  * Never a rejection — shape is not an admission gate.
@@ -85,8 +85,6 @@ export const PUBLIC_ROLE_RECORDS = [
85
85
  settlement: "sealed",
86
86
  /** Factory board: court until another station has started, then marshal. */
87
87
  boardPlacement: "judge-history",
88
- /** Navigator subject is the public admitted instruction when the run is bound. */
89
- navigatorSubject: "public-instruction",
90
88
  activationStage: "load-and-install",
91
89
  receiptStatusKey: "status",
92
90
  sessionMaterials: [
@@ -115,8 +113,6 @@ export const PUBLIC_ROLE_RECORDS = [
115
113
  reportPhase: true,
116
114
  },
117
115
  boardPlacement: "marshal",
118
- /** #1168: dispatch is first message; navigator reads admitted instruction. */
119
- navigatorSubject: "public-instruction",
120
116
  activationFlags: [
121
117
  { field: "phase", flag: "ak-fixer-phase", binds: "phase" },
122
118
  ],
@@ -145,8 +141,6 @@ export const PUBLIC_ROLE_RECORDS = [
145
141
  artifactFace: {
146
142
  reportPhase: true,
147
143
  },
148
- /** #1168: dispatch is first message; navigator reads admitted instruction. */
149
- navigatorSubject: "public-instruction",
150
144
  activationFlags: [
151
145
  { field: "phase", flag: "ak-coder-phase", binds: "phase" },
152
146
  ],
@@ -242,8 +236,6 @@ export const PUBLIC_ROLE_RECORDS = [
242
236
  artifactFace: {
243
237
  doctorReportFacts: true,
244
238
  },
245
- /** Navigator subject is the doctor case, not the case-path file bytes. */
246
- navigatorSubject: "case",
247
239
  activationFlags: [
248
240
  { field: "casePath", from: "caseRunsPath", flag: "ak-doctor-case", binds: "input" },
249
241
  ],
@@ -299,8 +291,6 @@ export const PUBLIC_ROLE_RECORDS = [
299
291
  gateStageLabel: "符宝郎",
300
292
  /** Gate summon binds --source-run and keeps dialogue off the argv. */
301
293
  gateSummon: "source-run",
302
- /** Navigator subject is the source-run locator, not the path file bytes. */
303
- navigatorSubject: "source-run",
304
294
  activationFlags: [
305
295
  { field: "sourceRun", from: "sourceRunPath", flag: "ak-notary-source-run", binds: "input" },
306
296
  { field: "ticketNumber", flag: "ak-notary-ticket-number" },
@@ -438,12 +428,14 @@ export const PUBLIC_ROLE_RECORDS = [
438
428
  role: "navigator",
439
429
  inCallAutoResume: false,
440
430
  presentSettled: "always",
441
- summonResume: true,
431
+ summonResume: false,
442
432
  admission: "instruction",
443
433
  sameParent: "none",
444
434
  phases: [null],
445
435
  outputTool: NAVIGATOR_OUTPUT_TOOL_NAME,
446
436
  settlement: "accepted",
437
+ /** #1192: structured target seat for the lookup run; no host flag (no host runs it). */
438
+ activationFlags: [{ field: "forRole" }],
447
439
  activationStage: "load-and-install",
448
440
  sessionMaterials: ["CLAUDE.md", "souls/navigator.md"],
449
441
  },
@@ -584,10 +576,6 @@ export function packagedRoleActivationFlags(role: string): readonly PackagedActi
584
576
  return record.activationFlags;
585
577
  }
586
578
 
587
- export function packagedRoleInputFlag(role: string): string | undefined {
588
- return packagedRoleActivationFlags(role).find((spec) => spec.binds === "input")?.flag;
589
- }
590
-
591
579
  export function packagedRolePhaseFlag(role: string): string | undefined {
592
580
  return packagedRoleActivationFlags(role).find((spec) => spec.binds === "phase")?.flag;
593
581
  }
@@ -669,15 +657,6 @@ export function packagedAuditToolName(role: string): string | undefined {
669
657
  return record.auditTool;
670
658
  }
671
659
 
672
- /** Admitted-request role whose navigator subject is the public instruction. */
673
- export function packagedPublicInstructionSubject(role: unknown): boolean {
674
- return typeof role === "string" && PUBLIC_ROLE_RECORDS.some((record) =>
675
- "navigatorSubject" in record
676
- && record.navigatorSubject === "public-instruction"
677
- && record.role === role
678
- );
679
- }
680
-
681
660
  /** Resume restores an optional source-run path. */
682
661
  export function packagedResumeSourcePath(role: string): boolean {
683
662
  const record = packagedRoleMetadata(role);
@@ -692,14 +671,6 @@ export function packagedProjectsTargetBindRejection(role: string): boolean {
692
671
  && record.projectTargetBindRejection === true;
693
672
  }
694
673
 
695
- export function packagedNavigatorSubject(
696
- role: string,
697
- ): "file" | "case" | "source-run" | "public-instruction" {
698
- const record = packagedRoleMetadata(role);
699
- if (record !== undefined && "navigatorSubject" in record) return record.navigatorSubject;
700
- return "file";
701
- }
702
-
703
674
  export type PackagedBoardPlacement = "judge-history" | "coder" | "marshal" | "collector";
704
675
 
705
676
  /** Factory-board column for a packaged seat. Absent seats use the board's other bucket. */
@@ -35,6 +35,7 @@ import { NotarySourceRunError, resolveNotarySourceRunLocator } from "../notary-s
35
35
  import type { PackagedRole } from "../packaged-role-registry.ts";
36
36
  import {
37
37
  packagedAdmitsCountersign,
38
+ isNavigatorSeat,
38
39
  packagedBindsBoardTicket,
39
40
  packagedRebindSourceOnResume,
40
41
  packagedRoleMetadata,
@@ -431,6 +432,8 @@ async function reaskMissingTicketOnce(
431
432
  io: CliIo,
432
433
  ): Promise<SoftTicketReaskOutcome> {
433
434
  if ((env.ticketReasksSpent ?? 0) >= 1) return { kind: "none" };
435
+ // #1192: a table lookup has no one to ask, and a second turn would re-seal.
436
+ if (isNavigatorSeat(admitted.role)) return { kind: "none" };
434
437
  if (admitted.ticketNumber !== undefined) return { kind: "none" };
435
438
  if (!isUnboundRunDirectory(admitted.runDirectory)) return { kind: "none" };
436
439
  if (terminal?.roleOutcome.kind !== "accepted" && terminal?.roleOutcome.kind !== "audit_escalation") {
@@ -42,7 +42,6 @@ import {
42
42
  packagedBareToken,
43
43
  packagedBaseIsRevision,
44
44
  packagedEmitsAuthorityRefs,
45
- packagedPublicInstructionSubject,
46
45
  packagedRoleMetadata,
47
46
  packagedStoresSourceRunRaw,
48
47
  packagedSubjectChoices,
@@ -96,7 +95,7 @@ import {
96
95
  type TakenTypedOption,
97
96
  type TypedOptionConsumer,
98
97
  } from "./option-definitions.ts";
99
- import type { PublicThinkingLevel } from "./registry.ts";
98
+ import { PUBLIC_CALLABLE_ROLES, type PublicThinkingLevel } from "./registry.ts";
100
99
 
101
100
  import { errorText, isRecord } from "../unknown-value.ts";
102
101
 
@@ -188,6 +187,8 @@ export type AdmittedGatekeeperInvocation = AdmittedRoleInvocationBase & {
188
187
 
189
188
  export type AdmittedNavigatorInvocation = AdmittedRoleInvocationBase & {
190
189
  readonly role: "navigator";
190
+ /** Structured target seat for the lookup (#1192); absent = whole table. */
191
+ readonly forRole?: PackagedRole;
191
192
  };
192
193
 
193
194
  export type AdmittedAuditorInvocation = AdmittedRoleInvocationBase & {
@@ -901,6 +902,7 @@ function appendBareInstruction(
901
902
  export type PublicSeatArgvOwner = Exclude<OptionOwner, "global" | "analyst">;
902
903
 
903
904
  type SeatArgvFields = {
905
+ forRole?: PackagedRole;
904
906
  attachmentPaths: string[];
905
907
  project?: string;
906
908
  positional: string[];
@@ -953,6 +955,17 @@ function assignPublicSeatOption(
953
955
  fields.subject = subject;
954
956
  return;
955
957
  }
958
+ case "role": {
959
+ const raw = typeof value === "string" ? value.trim() : "";
960
+ const known = PUBLIC_CALLABLE_ROLES.find((role) => role === raw);
961
+ if (known === undefined) {
962
+ throw new CliUsageError(
963
+ `${owner} --role must be one of ${PUBLIC_CALLABLE_ROLES.join("|")}, got ${value ?? "(missing)"}`,
964
+ );
965
+ }
966
+ fields.forRole = known;
967
+ return;
968
+ }
956
969
  case "source-run": {
957
970
  const text = typeof value === "string" ? value : "";
958
971
  if (text.trim() === "") {
@@ -1107,6 +1120,7 @@ export function parsePublicSeatArgv(
1107
1120
  ...(fields.lens === undefined ? {} : { lens: fields.lens }),
1108
1121
  ...(packagedEmitsAuthorityRefs(owner) ? { authorityRefs: fields.authorityRefs } : {}),
1109
1122
  ...(fields.subject === undefined ? {} : { subject: fields.subject }),
1123
+ ...(fields.forRole === undefined ? {} : { forRole: fields.forRole }),
1110
1124
  };
1111
1125
  }
1112
1126
 
@@ -1347,6 +1361,7 @@ export type PublicSeatParse = {
1347
1361
  readonly lens?: "completeness" | "correctness";
1348
1362
  readonly authorityRefs?: readonly string[];
1349
1363
  readonly subject?: "judge" | "doctor";
1364
+ readonly forRole?: PackagedRole;
1350
1365
  };
1351
1366
 
1352
1367
  /**
@@ -1402,6 +1417,7 @@ export async function admitPublicRole(
1402
1417
  instruction,
1403
1418
  attachmentPaths,
1404
1419
  ...project,
1420
+ ...(parsed.forRole === undefined ? {} : { admittedFields: { forRole: parsed.forRole } }),
1405
1421
  },
1406
1422
  );
1407
1423
  case "court-materials":
@@ -2050,34 +2066,6 @@ export async function materializeCountersignInvocation(
2050
2066
  );
2051
2067
  }
2052
2068
 
2053
- /**
2054
- * Load the admitted section written at admission (Navigator work-context seam).
2055
- * Missing / wrong-shape → undefined. Control-plane read faults propagate (#1187 F2).
2056
- */
2057
- export async function loadAdmittedJudgeRequest(
2058
- runDirectory: string,
2059
- ): Promise<{
2060
- instruction: string;
2061
- instructionEmpty: boolean;
2062
- attachments: readonly AdmittedAttachment[];
2063
- } | undefined> {
2064
- const record = readPageSync(runDirectory, "admitted");
2065
- if (record === undefined) return undefined;
2066
- if (!packagedPublicInstructionSubject(record.role)) return undefined;
2067
- if (typeof record.instruction !== "string") return undefined;
2068
- if (typeof record.instructionEmpty !== "boolean") return undefined;
2069
- if (!Array.isArray(record.attachments)) return undefined;
2070
- const attachments = record.attachments
2071
- .map((item) => normalizeAdmittedAttachment(item))
2072
- .filter((item): item is AdmittedAttachment => item !== undefined);
2073
- return {
2074
- instruction: record.instruction,
2075
- instructionEmpty: record.instructionEmpty,
2076
- attachments,
2077
- };
2078
-
2079
- }
2080
-
2081
2069
  export type AdmitFixerInvocationOptions = {
2082
2070
  home: string;
2083
2071
  principalAuthority: DurablePrincipalAuthority;
@@ -594,6 +594,25 @@ const DOCTOR_OPTIONS = [
594
594
  },
595
595
  ] as const satisfies readonly PublicOptionDefinition[];
596
596
 
597
+ /** 游奕使: structured target seat for the lookup; instruction prose is never parsed (#1192). */
598
+ const NAVIGATOR_OPTIONS = [
599
+ ...projectAndAttach("navigator"),
600
+ {
601
+ id: "role",
602
+ owner: "navigator",
603
+ canonical: "--role",
604
+ aliases: [],
605
+ valueMetavar: "role",
606
+ required: false,
607
+ repeatable: false,
608
+ form: "option",
609
+ description: {
610
+ en: "Optional seat whose status → advice rows to show; omitted shows the whole table. The instruction text is not read.",
611
+ zh: "可选衙门角色:只显示该衙门的 status → 建议;缺省显示整张表。instruction 正文不读。",
612
+ },
613
+ },
614
+ ] as const satisfies readonly PublicOptionDefinition[];
615
+
597
616
  const NOTARY_OPTIONS = [
598
617
  bindOwner("notary", SHARED_PROJECT_SEMANTICS),
599
618
  {
@@ -799,6 +818,7 @@ const SEAT_OPTION_ROWS: Partial<
799
818
  notary: NOTARY_OPTIONS,
800
819
  auditor: AUDITOR_OPTIONS,
801
820
  inspector: INSPECTOR_OPTIONS,
821
+ navigator: NAVIGATOR_OPTIONS,
802
822
  };
803
823
 
804
824
  function seatOptionRows(role: PublicCallableRole): readonly PublicOptionDefinition[] {
@@ -1103,7 +1123,7 @@ export type PublicCommandHelpFacts = {
1103
1123
  * the role list like every other role).
1104
1124
  */
1105
1125
  export const PUBLIC_NAVIGATOR_HELP_NOTE =
1106
- "Navigator also attends automatically on top-level public entry legs; that sidecar is unchanged by direct `ak-role navigator` calls." as const;
1126
+ "Navigator also attends automatically on top-level public entry legs; the sidecar and direct `ak-role navigator` calls read the same status → advice table, with no model call." as const;
1107
1127
 
1108
1128
  const TOP_LEVEL_HELP = {
1109
1129
  command: "top",
@@ -1220,10 +1240,11 @@ const ROLE_COMMAND_HELP = {
1220
1240
  },
1221
1241
  navigator: {
1222
1242
  command: "navigator",
1223
- summary: "Direct Navigator (游奕使) free-form prose route advice.",
1224
- usage: ["ak-role navigator [options] [instruction]"],
1243
+ summary: "Direct Navigator (游奕使) status → next-station advice table lookup; no model call.",
1244
+ usage: ["ak-role navigator [--role <role>] [options] [instruction]"],
1225
1245
  examples: [
1226
- 'ak-role navigator "刚完成 coder apply 收敛,下一步?"',
1246
+ "ak-role navigator",
1247
+ "ak-role navigator --role coder",
1227
1248
  ],
1228
1249
  },
1229
1250
  auditor: {
@@ -485,8 +485,6 @@ export type PostAdmissionEnv = {
485
485
  boundTicketNumber?: number;
486
486
  /** Station child role run (#840): omit automatic navigator attendance. */
487
487
  stationChild?: boolean;
488
- /** #1160 attendance auto byStatus prepare (nested navigator only). */
489
- navigatorByStatusPrepare?: boolean;
490
488
  };
491
489
 
492
490
  /**
@@ -1374,9 +1372,6 @@ export async function dispatchPostAdmissionTurn<
1374
1372
  if (env.stationChild !== undefined) {
1375
1373
  turnRequest = { ...turnRequest, stationChild: env.stationChild };
1376
1374
  }
1377
- if (env.navigatorByStatusPrepare === true) {
1378
- turnRequest = { ...turnRequest, navigatorByStatusPrepare: true };
1379
- }
1380
1375
  // Selected host axis rides the shared Host envelope for in-turn tools
1381
1376
  // (detour usage ledger) — never a pre-spawn current.json reread.
1382
1377
  if (typeof env.host === "string" && env.host.trim() !== "") {
@@ -1734,9 +1729,6 @@ export function resumeTurnRequestProjectionOptions(
1734
1729
  prompt,
1735
1730
  },
1736
1731
  ...(env.stationChild === undefined ? {} : { stationChild: env.stationChild }),
1737
- ...(env.navigatorByStatusPrepare === true
1738
- ? { navigatorByStatusPrepare: true as const }
1739
- : {}),
1740
1732
  // #1132: one configured ceiling, already resolved by the caller (#422).
1741
1733
  deliveryRequestLimit: deliveryLimitFromConfig(env.autoResumeLimit),
1742
1734
  };
@@ -1757,9 +1749,6 @@ export function roleTurnOptions(
1757
1749
  : { correlationId }),
1758
1750
  continuation,
1759
1751
  ...(env.stationChild === undefined ? {} : { stationChild: env.stationChild }),
1760
- ...(env.navigatorByStatusPrepare === true
1761
- ? { navigatorByStatusPrepare: true as const }
1762
- : {}),
1763
1752
  // #1132: one configured ceiling, already resolved by the caller (#422).
1764
1753
  deliveryRequestLimit: deliveryLimitFromConfig(env.autoResumeLimit),
1765
1754
  };
@@ -4,6 +4,8 @@
4
4
  * then host-facing provider projection. Callers own how a missing model is shown.
5
5
  */
6
6
  import { pickEngineAxis } from "../package-resources/engine-material.ts";
7
+ import { createNavigatorLookupHost, NAVIGATOR_LOOKUP_HOST } from "../navigator-lookup-host.ts";
8
+ import { isNavigatorSeat } from "../packaged-role-registry.ts";
7
9
  import type { DurablePrincipalAuthority, RoleTurnHost } from "../host-contracts.ts";
8
10
  import {
9
11
  resolvedSeatWithModel,
@@ -44,6 +46,18 @@ export function openRoleSeatRuntime(input: {
44
46
  /** Already host-facing. A second projection would rename the provider again. */
45
47
  readonly hostFacingModel?: SeatModelConfig;
46
48
  }): OpenedRoleSeatRuntime {
49
+ // #1192: the navigator is a table lookup, not a model seat. Its seat axes
50
+ // (model / host / engine) are accepted and never consulted, so a mismatched
51
+ // or absent pair cannot stop it and no host session is started.
52
+ if (isNavigatorSeat(input.role)) {
53
+ input.afterHost?.();
54
+ return {
55
+ ok: true,
56
+ roleTurnHost: createNavigatorLookupHost(),
57
+ hostAdapters: [],
58
+ host: NAVIGATOR_LOOKUP_HOST,
59
+ };
60
+ }
47
61
  const hostAdapters = composeRoleTurnHostAdapters(
48
62
  input.resolution,
49
63
  input.principalAuthority,