@akagilnc/pi-workflow-roles 0.1.3262 → 0.1.3280

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 (35) hide show
  1. package/README.md +8 -122
  2. package/README.zh-CN.md +9 -123
  3. package/dist/gatekeeper-role.js +22 -16
  4. package/dist/grok/production-host.js +745 -571
  5. package/dist/package-contracts/gatekeeper-output.js +71 -0
  6. package/dist/package-contracts/navigator-output.js +48 -0
  7. package/dist/package-contracts/terminating-tools.js +18 -2
  8. package/dist/packaged-role-registry.js +24 -0
  9. package/dist/public-cli/config.js +7 -21
  10. package/dist/public-cli/main.js +689 -365
  11. package/dist/public-cli/registry.js +2 -14
  12. package/dist/session-opening-materials.js +6 -11
  13. package/extensions/role-runtime.ts +3 -1
  14. package/package.json +1 -1
  15. package/resources/navigator-route-playbook.md +1 -1
  16. package/src/gatekeeper-role.ts +32 -17
  17. package/src/grok/production-host.ts +3 -1
  18. package/src/host-contracts.ts +3 -1
  19. package/src/navigator-role.ts +17 -0
  20. package/src/package-contracts/gatekeeper-output.ts +89 -0
  21. package/src/package-contracts/navigator-output.ts +59 -0
  22. package/src/package-contracts/terminating-tools.ts +24 -2
  23. package/src/packaged-role-registry.ts +25 -2
  24. package/src/pi/role-turn-host.ts +4 -0
  25. package/src/public-cli/cli.ts +37 -10
  26. package/src/public-cli/config.ts +6 -30
  27. package/src/public-cli/instruction-seat-run.ts +147 -0
  28. package/src/public-cli/invocation.ts +47 -3
  29. package/src/public-cli/option-definitions.ts +36 -3
  30. package/src/public-cli/registry.ts +3 -26
  31. package/src/public-cli/run-lifecycle.ts +8 -2
  32. package/src/public-cli/settlement.ts +115 -3
  33. package/src/public-cli/terminal.ts +3 -1
  34. package/src/role-runtime.ts +64 -1
  35. package/src/session-opening-materials.ts +6 -11
@@ -6,22 +6,9 @@ import { PACKAGED_ROLE_REGISTRY, } from "../packaged-role-registry.js";
6
6
  /** Package-relative path of the Internal role entrypoint (explicit load only). */
7
7
  export const INTERNAL_ROLE_ENTRYPOINT_RELATIVE = "extensions/role-runtime.ts";
8
8
  export const PUBLIC_CALLABLE_ROLES = PACKAGED_ROLE_REGISTRY.map((entry) => entry.role);
9
- /** Automatic attendance seat — configurable, never a caller-selected command. */
10
- export const AUTOMATIC_NAVIGATOR_SEAT = "navigator";
11
- /** Automatic province seat — configurable model only; never a caller command. */
12
- export const AUTOMATIC_GATEKEEPER_SEAT = "gatekeeper";
13
- /** Automatic-only configurable seats. Inspector remains automatic-capable via its callable seat. */
14
- export const AUTOMATIC_CONFIGURABLE_SEATS = [
15
- AUTOMATIC_GATEKEEPER_SEAT,
16
- AUTOMATIC_NAVIGATOR_SEAT,
17
- ];
18
9
  export const PUBLIC_CONFIGURABLE_SEATS = [
19
10
  ...PUBLIC_CALLABLE_ROLES,
20
- ...AUTOMATIC_CONFIGURABLE_SEATS,
21
11
  ];
22
- export function isAutomaticConfigurableSeat(value) {
23
- return AUTOMATIC_CONFIGURABLE_SEATS.includes(value);
24
- }
25
12
  export const PUBLIC_CLI_SUPPORT_COMMANDS = [
26
13
  "roles",
27
14
  "config",
@@ -82,7 +69,8 @@ const STARTUP_CANDIDATES = {
82
69
  ],
83
70
  // #620: subordinate officers inherit gatekeeper; no package startup model.
84
71
  notary: [],
85
- // #453/#620: gatekeeper unset inherits audited session (province path only).
72
+ // #453/#620/#639: gatekeeper callable but no package startup model — caller
73
+ // configures it or the institutional resolution applies on the province path.
86
74
  gatekeeper: [],
87
75
  inspector: [],
88
76
  navigator: [
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Session opening materials (#443 / #524). Missing files fail as native readFile
3
- * errors. Main-role materials derive from PUBLIC_ROLE_RECORDS; gatekeeper seats
4
- * stay independent except notary (shared NOTARY_SESSION_MATERIALS). Navigator is
5
- * name-only here, not a public role record. Auditor composition stays in
3
+ * errors. Main-role materials derive from PUBLIC_ROLE_RECORDS (including
4
+ * gatekeeper and navigator). Province map reuses that public gatekeeper record
5
+ * plus officer material constants (notary shared). Auditor composition stays in
6
6
  * loadAuditorSoul; loaders share joinPackageMaterials.
7
7
  */
8
8
  import { existsSync } from "node:fs";
@@ -42,22 +42,17 @@ export async function joinPackageMaterials(relativePaths) {
42
42
  }
43
43
  return chunks.join("\n\n");
44
44
  }
45
- /** Derived projection: public records + navigator name-only materials. */
45
+ /** Derived projection: public records (#639 includes navigator/gatekeeper). */
46
46
  export const MAIN_ROLE_SESSION_MATERIALS = {
47
47
  ...Object.fromEntries(PUBLIC_ROLE_RECORDS.map((record) => [record.role, record.sessionMaterials])),
48
- navigator: ["CLAUDE.md", "souls/navigator.md"],
49
48
  };
50
49
  export function loadMainRoleSessionMaterials(role) {
51
50
  return joinPackageMaterials(MAIN_ROLE_SESSION_MATERIALS[role]);
52
51
  }
53
52
  /** Gatekeeper province; notary reuses the public notary materials definition. */
54
53
  export const GATEKEEPER_SESSION_MATERIALS = {
55
- gatekeeper: [
56
- "CLAUDE.md",
57
- "souls/gatekeeper.md",
58
- "souls/quality-law.md",
59
- "souls/gate-output-guide.md",
60
- ],
54
+ // #639: single authority — the public gatekeeper record owns the province list.
55
+ gatekeeper: PUBLIC_ROLE_RECORDS.find((entry) => entry.role === "gatekeeper").sessionMaterials,
61
56
  inspector: INSPECTOR_SESSION_MATERIALS,
62
57
  notary: NOTARY_SESSION_MATERIALS,
63
58
  };
@@ -36,7 +36,7 @@ import {
36
36
  formatNavigatorRoleHelp,
37
37
  } from "../src/role-runtime.ts";
38
38
  import { createPiJudgeAuditor } from "../src/judge-auditor.ts";
39
- import { loadMainRoleSessionMaterials } from "../src/session-opening-materials.ts";
39
+ import { loadGatekeeperSessionMaterials, loadMainRoleSessionMaterials } from "../src/session-opening-materials.ts";
40
40
  const extensionPath = fileURLToPath(import.meta.url);
41
41
  const packageRoot = fileURLToPath(new URL("..", import.meta.url));
42
42
  const navigatorRoutePlaybookPath = fileURLToPath(new URL("../resources/navigator-route-playbook.md", import.meta.url));
@@ -128,6 +128,8 @@ export default function roleRuntime(pi: ExtensionAPI): void {
128
128
  loadCountersignSoul: () => loadMainRoleSessionMaterials("countersign"),
129
129
  loadGleanerLeftSoul: () => loadMainRoleSessionMaterials("gleaner-left"),
130
130
  loadInspectorSoul: () => loadMainRoleSessionMaterials("inspector"),
131
+ loadGatekeeperSoul: () => loadGatekeeperSessionMaterials("gatekeeper"),
132
+ loadNavigatorSoul: () => loadMainRoleSessionMaterials("navigator"),
131
133
  loadNotarySourceRun: loadNotarySourceRunLocator,
132
134
  loadNavigatorWorkContext: (options) => loadNavigatorWorkContext(pi, options),
133
135
  createNavigatorAttendance: (options) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akagilnc/pi-workflow-roles",
3
- "version": "0.1.3262",
3
+ "version": "0.1.3280",
4
4
  "description": "Soul-bound workflow roles for Pi",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -1,6 +1,6 @@
1
1
  ## 宿主轴(与角色命令面)
2
2
 
3
- 游奕使自动出席,不另立调用命令。主会话宿主由公开角色的 host 轴决定:调用 `--host` → 席位 `config set-host` → 默认 `pi`。配置默认 host 后,调用者仍用与 Pi 相同的 `ak-role <role> …` 命令面;游奕使随该宿主下的角色跑次出席,席位模型走 run 目录 `institutional-resolution.json` 的 navigator 座(显式 `config set navigator` 优先,否则继承父席有效模型),不再依赖父宿主 ExtensionContext。
3
+ 游奕使自动出席,亦可 `ak-role navigator` 直调(自动出席不变)。主会话宿主由公开角色的 host 轴决定:调用 `--host` → 席位 `config set-host` → 默认 `pi`。配置默认 host 后,调用者仍用与 Pi 相同的 `ak-role <role> …` 命令面;游奕使随该宿主下的角色跑次出席,席位模型走 run 目录 `institutional-resolution.json` 的 navigator 座(显式 `config set navigator` 优先,否则继承父席有效模型),不再依赖父宿主 ExtensionContext。
4
4
 
5
5
  ## 常用交付线
6
6
 
@@ -8,8 +8,12 @@ import type { NoReceiptLifecycleFacts } from "./receipt-delivery-policy.ts";
8
8
  import { loadGatekeeperSessionMaterials } from "./session-opening-materials.ts";
9
9
  import { GatekeeperDecisionError } from "./submission-errors.ts";
10
10
  import { INSPECTOR_OUTPUT_TOOL_NAME } from "./inspector-contracts.ts";
11
-
12
- export const GATEKEEPER_OUTPUT_TOOL = "ak_gatekeeper_output";
11
+ import {
12
+ GATEKEEPER_OUTPUT_TOOL_NAME,
13
+ gatekeeperDecisionSchema,
14
+ gatekeeperOutputSchema,
15
+ projectLawfulGatekeeperOutput,
16
+ } from "./package-contracts/gatekeeper-output.ts";
13
17
  export const INSPECTOR_OUTPUT_TOOL = INSPECTOR_OUTPUT_TOOL_NAME;
14
18
  export const NOTARY_OUTPUT_TOOL = "ak_notary_output";
15
19
  const SUBJECT_TOOL = "ak_gatekeeper_subject";
@@ -82,11 +86,23 @@ const officerDecisionSchema = openToolObject(Type.Object({
82
86
  findings: Type.Unknown({ description: "string[] findings,随 pass 或 bounce 留存" }),
83
87
  }));
84
88
 
85
- const gatekeeperDecisionSchema = openToolObject(Type.Object({
86
- status: Type.Unknown({ description: "dispatch | pass — 形状指引,非 schema 闸" }),
87
- officer: Type.Unknown({ description: "status 为 dispatch 时为 inspector | notary" }),
88
- findings: Type.Unknown({ description: "status 为 pass 时可选 string[] findings" }),
89
- }));
89
+
90
+ /**
91
+ * Direct-seat decision tool spec (#639). Lifecycle assembly stays on the
92
+ * registration envelope — src/role-runtime.ts (ADR 0018). Schema authority is
93
+ * the shared contract module (with infrastructure-failure declaration).
94
+ */
95
+ export const GATEKEEPER_TOOL_SPEC = {
96
+ name: GATEKEEPER_OUTPUT_TOOL_NAME,
97
+ label: "门下省决议",
98
+ description: "门下省终局决议,状态为 dispatch 或 pass。",
99
+ promptSnippet: "门下省决议",
100
+ parameters: gatekeeperOutputSchema,
101
+ } as const;
102
+
103
+ export type GatekeeperRuntimeDependencies = {
104
+ loadSoul(): Promise<string>;
105
+ };
90
106
 
91
107
  function result(content: string, details: unknown) {
92
108
  return { content: [{ type: "text" as const, text: content }], details };
@@ -111,10 +127,10 @@ export function createOfficerDecisionTool(name: string): AuditorDecisionTool {
111
127
  };
112
128
  }
113
129
 
114
- /** Gatekeeper province decision tool — open transport; projection owns legality. */
130
+ /** Gatekeeper province decision tool — open transport; package-contract projection owns legality. */
115
131
  export function createGatekeeperOutputTool(): AuditorDecisionTool {
116
132
  return {
117
- name: GATEKEEPER_OUTPUT_TOOL,
133
+ name: GATEKEEPER_OUTPUT_TOOL_NAME,
118
134
  description: "提交门下省派官决定。",
119
135
  parameters: gatekeeperDecisionSchema,
120
136
  async execute(_id, args) { return result(`已收 ${String((args as { status?: unknown })?.status)}`, args); },
@@ -174,17 +190,16 @@ function readRecord(value: unknown): Record<string, unknown> | undefined {
174
190
  }
175
191
 
176
192
  function projectProvinceDecision(decision: unknown): GatekeeperResult | { status: "dispatch"; officer: "inspector" | "notary" } {
177
- const record = readRecord(decision);
178
- if (record === undefined) return noUsableReleaseFailure("gatekeeper", decision);
179
- if (record.status === "dispatch" && (record.officer === "inspector" || record.officer === "notary")) {
180
- return { status: "dispatch", officer: record.officer };
193
+ // Lawful dispatch/pass discriminant is owned by package-contracts; province
194
+ // wraps undefined into transport_failure while retaining the submission.
195
+ const projected = projectLawfulGatekeeperOutput(decision);
196
+ if (projected === undefined) return noUsableReleaseFailure("gatekeeper", decision);
197
+ if (projected.status === "dispatch") {
198
+ return { status: "dispatch", officer: projected.officer };
181
199
  }
182
200
  // Lawful non-dispatch release — province may pass without dispatching an officer
183
201
  // (ADR 0074 gate-non-mandatory; gate-output-guide pass = 正常放行; #597).
184
- if (record.status === "pass") {
185
- return { status: "pass", findings: asStringArray(record.findings) };
186
- }
187
- return noUsableReleaseFailure("gatekeeper", decision);
202
+ return { status: "pass", findings: projected.findings ?? [] };
188
203
  }
189
204
 
190
205
  function projectOfficerDecision(
@@ -32,7 +32,7 @@ import { loadPackagedCanonicalSkillBinding } from "../package-resources/method-s
32
32
  import { createPerDispatchReviewerAgent } from "../reviewer-agent.ts";
33
33
  import { formatNavigatorRoleHelp, type RoleRuntimeDependencies } from "../role-runtime.ts";
34
34
  import { createReviewerPinnedGitReader } from "../reviewer-pinned-git.ts";
35
- import { loadMainRoleSessionMaterials } from "../session-opening-materials.ts";
35
+ import { loadGatekeeperSessionMaterials, loadMainRoleSessionMaterials } from "../session-opening-materials.ts";
36
36
  import { createComposedGrokRoleTurnHost } from "./role-envelope.ts";
37
37
  import {
38
38
  assertControlledGrokAuthIsNotSymlink,
@@ -221,6 +221,8 @@ export function createGrokRoleRuntimeDependencies(packageRoot: string): RoleRunt
221
221
  loadDoctorSoul: () => loadMainRoleSessionMaterials("doctor"),
222
222
  loadDoctorCase,
223
223
  loadInspectorSoul: () => loadMainRoleSessionMaterials("inspector"),
224
+ loadGatekeeperSoul: () => loadGatekeeperSessionMaterials("gatekeeper"),
225
+ loadNavigatorSoul: () => loadMainRoleSessionMaterials("navigator"),
224
226
  loadNotarySoul: () => loadMainRoleSessionMaterials("notary"),
225
227
  loadCountersignSoul: () => loadMainRoleSessionMaterials("countersign"),
226
228
  loadGleanerLeftSoul: () => loadMainRoleSessionMaterials("gleaner-left"),
@@ -123,7 +123,9 @@ export type RoleTurnActivation =
123
123
  /** Required comparison-base revision for the unanchored merge-candidate diff. */
124
124
  readonly baseRevision: string;
125
125
  }
126
- | { readonly role: "inspector" };
126
+ | { readonly role: "inspector" }
127
+ | { readonly role: "gatekeeper" }
128
+ | { readonly role: "navigator" };
127
129
 
128
130
  export type RoleTurnContinuation =
129
131
  | { readonly kind: "initial"; readonly prompt: string }
@@ -0,0 +1,17 @@
1
+ import { NAVIGATOR_OUTPUT_TOOL_NAME, navigatorOutputSchema } from "./package-contracts/navigator-output.ts";
2
+
3
+ /**
4
+ * 决定工具规格。生命周期装配归注册信封 owner——src/role-runtime.ts(ADR 0018)。
5
+ * Candidates share the route-advice shape the attendance prepare tool owns.
6
+ */
7
+ export const NAVIGATOR_TOOL_SPEC = {
8
+ name: NAVIGATOR_OUTPUT_TOOL_NAME,
9
+ label: "游奕使建议",
10
+ description: "游奕使终局回执:排好序的下一步角色路线建议。",
11
+ promptSnippet: "游奕使建议",
12
+ parameters: navigatorOutputSchema,
13
+ } as const;
14
+
15
+ export type NavigatorRuntimeDependencies = {
16
+ loadSoul(): Promise<string>;
17
+ };
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Public Gatekeeper (门下省) terminating receipt contracts (#639).
3
+ * Direct public seat shares the province decision shape: dispatch | pass.
4
+ * No usable result is infrastructure failure via public settlement, not a judgment status (#475).
5
+ */
6
+ import { Type } from "typebox";
7
+
8
+ import { openToolObject } from "../open-tool-schema.ts";
9
+ import { withInfrastructureFailureDeclaration } from "./terminating-infrastructure.ts";
10
+
11
+ export const GATEKEEPER_OUTPUT_TOOL_NAME = "ak_gatekeeper_output";
12
+ export const GATEKEEPER_ACCEPTED_TEXT = "门下省决议已受理";
13
+
14
+ /** Same open decision shape the province uses inside audit sessions. */
15
+ export const gatekeeperDecisionSchema = openToolObject(
16
+ Type.Object({
17
+ status: Type.Unknown({
18
+ description: "dispatch | pass — 形状指引,非 schema 闸",
19
+ }),
20
+ officer: Type.Unknown({
21
+ description: "status 为 dispatch 时为 inspector | notary",
22
+ }),
23
+ findings: Type.Unknown({
24
+ description: "status 为 pass 时可选 string[] findings",
25
+ }),
26
+ }),
27
+ );
28
+
29
+ export const gatekeeperOutputSchema = withInfrastructureFailureDeclaration(
30
+ gatekeeperDecisionSchema,
31
+ );
32
+
33
+ export type GatekeeperDirectOutput =
34
+ | { readonly status: "dispatch"; readonly officer: "inspector" | "notary" }
35
+ | { readonly status: "pass"; readonly findings?: readonly string[] };
36
+
37
+ function isRecord(value: unknown): value is Record<string, unknown> {
38
+ return typeof value === "object" && value !== null && !Array.isArray(value);
39
+ }
40
+
41
+ function asStringArray(value: unknown): readonly string[] {
42
+ if (!Array.isArray(value)) return [];
43
+ return value.filter((item): item is string => typeof item === "string");
44
+ }
45
+
46
+ /**
47
+ * Project one lawful explicit Gatekeeper decision (dispatch | pass).
48
+ * No throw on shape — ADR 0055 / 第 0 条: already-submitted params are retained as-is;
49
+ * public-terminal projects non-usable releases via typed failure cause.
50
+ */
51
+ export function projectLawfulGatekeeperOutput(value: unknown): GatekeeperDirectOutput | undefined {
52
+ if (!isRecord(value)) return undefined;
53
+ if (value.status === "pass") {
54
+ return Array.isArray(value.findings)
55
+ ? { status: "pass", findings: asStringArray(value.findings) }
56
+ : { status: "pass" };
57
+ }
58
+ if (
59
+ value.status === "dispatch" &&
60
+ (value.officer === "inspector" || value.officer === "notary")
61
+ ) {
62
+ return { status: "dispatch", officer: value.officer };
63
+ }
64
+ return undefined;
65
+ }
66
+
67
+ /**
68
+ * Settlement/recording path: only lawful recorded dispatch/pass.
69
+ * Does not gate role admission — callers must not use this to reject a submission.
70
+ */
71
+ export function validateRecordedGatekeeperOutput(value: unknown): GatekeeperDirectOutput {
72
+ const projected = projectLawfulGatekeeperOutput(value);
73
+ if (projected === undefined) {
74
+ throw new Error("Gatekeeper output has no recognized execution discriminator");
75
+ }
76
+ return projected;
77
+ }
78
+
79
+ export function gatekeeperDecisiveFacts(
80
+ output: GatekeeperDirectOutput,
81
+ ): Record<string, unknown> {
82
+ const facts: Record<string, unknown> = { status: output.status };
83
+ if (output.status === "dispatch") {
84
+ facts.officer = output.officer;
85
+ } else if (Array.isArray(output.findings)) {
86
+ facts.findingsCount = output.findings.length;
87
+ }
88
+ return facts;
89
+ }
@@ -0,0 +1,59 @@
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.
5
+ * No usable result is infrastructure failure via public settlement, not a judgment status (#475).
6
+ */
7
+ import { Type } from "typebox";
8
+
9
+ import { openToolObject } from "../open-tool-schema.ts";
10
+ import { withInfrastructureFailureDeclaration } from "./terminating-infrastructure.ts";
11
+
12
+ export const NAVIGATOR_OUTPUT_TOOL_NAME = "ak_navigator_output";
13
+ export const NAVIGATOR_ACCEPTED_TEXT = "游奕使建议已受理";
14
+
15
+ export const navigatorOutputSchema = withInfrastructureFailureDeclaration(
16
+ openToolObject(
17
+ Type.Object({
18
+ status: Type.Unknown({
19
+ description: "advice — 形状指引,非 schema 闸",
20
+ }),
21
+ candidates: Type.Unknown({
22
+ description: "排好序的路线建议数组,元素含 next/phase/reason — 形状指引,非 schema 闸",
23
+ }),
24
+ }),
25
+ ),
26
+ );
27
+
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[] };
33
+
34
+ /**
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 条).
38
+ */
39
+ export function projectLawfulNavigatorOutput(value: unknown): NavigatorAdvice | undefined {
40
+ if (!isRecord(value) || !Array.isArray(value.candidates)) return undefined;
41
+ if (value.status !== "advice") return undefined;
42
+ return { status: "advice", candidates: value.candidates.filter(isRecord) };
43
+ }
44
+
45
+ /**
46
+ * Settlement/recording path: only lawful recorded candidate arrays.
47
+ * Does not gate role admission — callers must not use this to reject a submission.
48
+ */
49
+ export function validateRecordedNavigatorOutput(value: unknown): NavigatorAdvice {
50
+ const projected = projectLawfulNavigatorOutput(value);
51
+ if (projected === undefined) {
52
+ throw new Error("Navigator output has no recognized execution discriminator");
53
+ }
54
+ return projected;
55
+ }
56
+
57
+ export function navigatorDecisiveFacts(output: { readonly candidates: readonly unknown[] }): Record<string, unknown> {
58
+ return { status: "advice", candidates: output.candidates };
59
+ }
@@ -27,6 +27,8 @@ import {
27
27
  import { isAuditEscalationResult } from "../audit-escalation.ts";
28
28
  import { CorrectableSubmissionError } from "../submission-correctable-error.ts";
29
29
  import { DOCTOR_ACCEPTED_TEXT, DOCTOR_OUTPUT_TOOL_NAME, validateDoctorSubmissionShape, validateRecordedDoctorOutput, type DoctorOutput, type DoctorSubmission } from "../doctor-contracts.ts";
30
+ import { GATEKEEPER_ACCEPTED_TEXT, GATEKEEPER_OUTPUT_TOOL_NAME, validateRecordedGatekeeperOutput, type GatekeeperDirectOutput } from "./gatekeeper-output.ts";
31
+ import { NAVIGATOR_ACCEPTED_TEXT, NAVIGATOR_OUTPUT_TOOL_NAME, validateRecordedNavigatorOutput, type NavigatorAdvice } from "./navigator-output.ts";
30
32
  import { MERGER_ACCEPTED_TEXT, MERGER_OUTPUT_TOOL_NAME, validateMergerOutput, type MergerOutput } from "../merger-contracts.ts";
31
33
  import { NOTARY_ACCEPTED_TEXT, NOTARY_OUTPUT_TOOL_NAME, validateRecordedNotaryOutput, type NotaryOutput } from "../notary-contracts.ts";
32
34
  import { COUNTERSIGN_ACCEPTED_TEXT, COUNTERSIGN_OUTPUT_TOOL_NAME, validateRecordedCountersignOutput, type CountersignVerdict } from "../countersign-contracts.ts";
@@ -67,6 +69,8 @@ export {
67
69
  validateRecordedCountersignOutput,
68
70
  validateRecordedGleanerLeftOutput,
69
71
  validateRecordedInspectorOutput,
72
+ validateRecordedGatekeeperOutput,
73
+ validateRecordedNavigatorOutput,
70
74
  };
71
75
  export type {
72
76
  CollectorReceipt,
@@ -81,6 +85,8 @@ export type {
81
85
  CountersignVerdict,
82
86
  GleanerLeftOutput,
83
87
  InspectorOutput,
88
+ GatekeeperDirectOutput,
89
+ NavigatorAdvice,
84
90
  };
85
91
 
86
92
  export const TERMINATING_TOOL_NAMES = [
@@ -95,6 +101,8 @@ export const TERMINATING_TOOL_NAMES = [
95
101
  COUNTERSIGN_OUTPUT_TOOL_NAME,
96
102
  GLEANER_LEFT_OUTPUT_TOOL_NAME,
97
103
  INSPECTOR_OUTPUT_TOOL_NAME,
104
+ GATEKEEPER_OUTPUT_TOOL_NAME,
105
+ NAVIGATOR_OUTPUT_TOOL_NAME,
98
106
  ] as const;
99
107
 
100
108
  export type TerminatingToolName = (typeof TERMINATING_TOOL_NAMES)[number];
@@ -109,7 +117,9 @@ export type AcceptedDetails =
109
117
  | NotaryOutput
110
118
  | CountersignVerdict
111
119
  | GleanerLeftOutput
112
- | InspectorOutput;
120
+ | InspectorOutput
121
+ | GatekeeperDirectOutput
122
+ | NavigatorAdvice;
113
123
 
114
124
  export function isTerminatingToolName(
115
125
  name: string,
@@ -141,6 +151,10 @@ export function acceptedTextFor(toolName: TerminatingToolName): string {
141
151
  return GLEANER_LEFT_ACCEPTED_TEXT;
142
152
  case INSPECTOR_OUTPUT_TOOL_NAME:
143
153
  return INSPECTOR_ACCEPTED_TEXT;
154
+ case GATEKEEPER_OUTPUT_TOOL_NAME:
155
+ return GATEKEEPER_ACCEPTED_TEXT;
156
+ case NAVIGATOR_OUTPUT_TOOL_NAME:
157
+ return NAVIGATOR_ACCEPTED_TEXT;
144
158
  }
145
159
  }
146
160
 
@@ -197,6 +211,8 @@ export function validateAcceptedDetails(
197
211
  [COUNTERSIGN_OUTPUT_TOOL_NAME]: ["converged", "continue", "escalate"],
198
212
  [GLEANER_LEFT_OUTPUT_TOOL_NAME]: ["completed"],
199
213
  [INSPECTOR_OUTPUT_TOOL_NAME]: ["pass", "bounce"],
214
+ [GATEKEEPER_OUTPUT_TOOL_NAME]: ["dispatch", "pass"],
215
+ [NAVIGATOR_OUTPUT_TOOL_NAME]: ["advice"],
200
216
  };
201
217
  const collectorDiscriminator = toolName === COLLECTOR_OUTPUT_TOOL && Array.isArray(candidate?.groups);
202
218
  const baseDiscriminator = discriminator;
@@ -233,6 +249,10 @@ export function validateAcceptedDetails(
233
249
  return validateRecordedGleanerLeftOutput(details);
234
250
  case INSPECTOR_OUTPUT_TOOL_NAME:
235
251
  return validateRecordedInspectorOutput(details);
252
+ case GATEKEEPER_OUTPUT_TOOL_NAME:
253
+ return validateRecordedGatekeeperOutput(details);
254
+ case NAVIGATOR_OUTPUT_TOOL_NAME:
255
+ return validateRecordedNavigatorOutput(details);
236
256
  }
237
257
  } catch (error) {
238
258
  if (error instanceof Error && error.constructor === Error) throw new AcceptedDetailsContractError(error.message, { cause: error });
@@ -279,7 +299,9 @@ export function acceptedFacts(toolName: TerminatingToolName, details: AcceptedDe
279
299
  case DOCTOR_OUTPUT_TOOL_NAME:
280
300
  case NOTARY_OUTPUT_TOOL_NAME:
281
301
  case GLEANER_LEFT_OUTPUT_TOOL_NAME:
282
- case INSPECTOR_OUTPUT_TOOL_NAME: return { status: (details as { status: string }).status };
302
+ case INSPECTOR_OUTPUT_TOOL_NAME:
303
+ case GATEKEEPER_OUTPUT_TOOL_NAME:
304
+ case NAVIGATOR_OUTPUT_TOOL_NAME: return { status: (details as { status: string }).status };
283
305
  case JUDGE_OUTPUT_TOOL_NAME: return { status: (details as { judgeStatus: string }).judgeStatus };
284
306
  case COUNTERSIGN_OUTPUT_TOOL_NAME: return { status: (details as { countersignStatus: string }).countersignStatus };
285
307
  case MERGER_OUTPUT_TOOL_NAME: {
@@ -1,5 +1,7 @@
1
1
  /** Composition-root unique authoritative public-role records (#509 / #524). */
2
2
  import { COLLECTOR_OUTPUT_TOOL } from "./package-contracts/collector-output.ts";
3
+ import { GATEKEEPER_OUTPUT_TOOL_NAME } from "./package-contracts/gatekeeper-output.ts";
4
+ import { NAVIGATOR_OUTPUT_TOOL_NAME } from "./package-contracts/navigator-output.ts";
3
5
  import { JUDGE_OUTPUT_TOOL_NAME } from "./package-contracts/judge-output.ts";
4
6
  import { REVIEWER_OUTPUT_TOOL_NAME } from "./package-contracts/reviewer-output.ts";
5
7
  import { CODER_OUTPUT_TOOL_NAME, FIXER_OUTPUT_TOOL_NAME } from "./package-contracts/worker-output.ts";
@@ -28,8 +30,7 @@ export const INSPECTOR_SESSION_MATERIALS = [
28
30
  ] as const;
29
31
 
30
32
  /**
31
- * One record per public callable role. Navigator is absent (name-only materials
32
- * live on the session-opening projection).
33
+ * One record per public callable role (#639: includes gatekeeper and navigator).
33
34
  */
34
35
  export const PUBLIC_ROLE_RECORDS = [
35
36
  {
@@ -162,6 +163,28 @@ export const PUBLIC_ROLE_RECORDS = [
162
163
  activationStage: "load-and-install",
163
164
  sessionMaterials: INSPECTOR_SESSION_MATERIALS,
164
165
  },
166
+ // #639: gatekeeper and navigator are roles like any other — public ak-role
167
+ // entries; automatic attendance (province dispatch, navigator sidecar) is
168
+ // unchanged and orthogonal to callability.
169
+ {
170
+ role: "gatekeeper",
171
+ phases: [null],
172
+ outputTool: GATEKEEPER_OUTPUT_TOOL_NAME,
173
+ inputFlag: undefined,
174
+ phaseFlag: undefined,
175
+ activationStage: "load-and-install",
176
+ // Province materials; officers reuse their own public records below.
177
+ sessionMaterials: ["CLAUDE.md", "souls/gatekeeper.md", "souls/quality-law.md", "souls/gate-output-guide.md"],
178
+ },
179
+ {
180
+ role: "navigator",
181
+ phases: [null],
182
+ outputTool: NAVIGATOR_OUTPUT_TOOL_NAME,
183
+ inputFlag: undefined,
184
+ phaseFlag: undefined,
185
+ activationStage: "load-and-install",
186
+ sessionMaterials: ["CLAUDE.md", "souls/navigator.md"],
187
+ },
165
188
  ] as const;
166
189
 
167
190
  export type PublicRoleRecord = (typeof PUBLIC_ROLE_RECORDS)[number];
@@ -142,6 +142,10 @@ function buildActivationFlagArgs(activation: RoleTurnActivation): string[] {
142
142
  ];
143
143
  case "inspector":
144
144
  return ["--ak-role", "inspector"];
145
+ case "gatekeeper":
146
+ return ["--ak-role", "gatekeeper"];
147
+ case "navigator":
148
+ return ["--ak-role", "navigator"];
145
149
  default: {
146
150
  const _exhaustive: never = activation;
147
151
  return _exhaustive;
@@ -52,9 +52,11 @@ import {
52
52
  parseGleanerLeftArgv,
53
53
  parseDoctorArgv,
54
54
  parseFixerArgv,
55
+ parseGatekeeperArgv,
55
56
  parseJudgeArgv,
56
57
  parseInspectorArgv,
57
58
  parseMergerArgv,
59
+ parseNavigatorArgv,
58
60
  parseNotaryArgv,
59
61
  parseReviewerArgv,
60
62
  recordLaunchedPiIdentity,
@@ -71,6 +73,7 @@ import {
71
73
  type TypedOptionConsumer,
72
74
  } from "./option-definitions.ts";
73
75
  import { runPublicCoder, runPublicCoderResume } from "./coder-run.ts";
76
+ import { runPublicInstructionSeat } from "./instruction-seat-run.ts";
74
77
  import { runPublicCollector } from "./collector-run.ts";
75
78
  import { runPublicCountersign, runPublicCountersignResume } from "./countersign-run.ts";
76
79
  import { runPublicGleanerLeft, runPublicGleanerLeftResume } from "./gleaner-left-run.ts";
@@ -90,7 +93,6 @@ import {
90
93
  } from "./run-lifecycle.ts";
91
94
  import {
92
95
  INTERNAL_ROLE_ENTRYPOINT_RELATIVE,
93
- isAutomaticConfigurableSeat,
94
96
  isPublicCallableRole,
95
97
  isPublicCliSupportCommand,
96
98
  isPublicConfigurableSeat,
@@ -128,6 +130,8 @@ export const PUBLIC_ROLE_ARGV = {
128
130
  notary: { parse: parseNotaryArgv, options: optionsForOwner("notary") },
129
131
  inspector: { parse: parseInspectorArgv, options: optionsForOwner("inspector") },
130
132
  reviewer: { parse: parseReviewerArgv, options: optionsForOwner("reviewer") },
133
+ gatekeeper: { parse: parseGatekeeperArgv, options: optionsForOwner("gatekeeper") },
134
+ navigator: { parse: parseNavigatorArgv, options: optionsForOwner("navigator") },
131
135
  /** Deterministic analysis seat (#336) — argv parse only; no LLM admission. */
132
136
  analyst: { parse: parseAnalystArgv, options: optionsForOwner("analyst") },
133
137
  } as const;
@@ -596,17 +600,12 @@ function requireLegalEngineName(name: string): string {
596
600
  }
597
601
  }
598
602
 
599
- /** Callable seats own persistent call axes; automatic seats have no call path. */
603
+ /** Callable seats own persistent call axes (checked against the callable predicate). */
600
604
  function requireCallableSeat(
601
605
  seat: string,
602
606
  axis: "engine" | "host",
603
607
  verb: "set-engine" | "unset-engine" | "set-host" | "unset-host",
604
608
  ): asserts seat is PublicCallableRole {
605
- if (isAutomaticConfigurableSeat(seat)) {
606
- throw new CliUsageError(
607
- `config ${verb} refuses ${seat}: no independent activation path; storing would be silently ineffective`,
608
- );
609
- }
610
609
  if (!isPublicCallableRole(seat)) {
611
610
  throw new CliUsageError(`unknown ${axis}-axis seat: ${seat}`);
612
611
  }
@@ -765,12 +764,11 @@ function renderCommandHelp(command: string): string | undefined {
765
764
  }
766
765
 
767
766
  function renderRoles(seats: readonly EffectiveSeat[]): string {
768
- const lines: string[] = ["seat\tkind\tsource\tmodel"];
767
+ const lines: string[] = ["seat\tsource\tmodel"];
769
768
  for (const seat of seats) {
770
- const kind = seat.automatic ? "automatic" : "callable";
771
769
  const model =
772
770
  seat.selection === undefined ? "-" : formatModelSpec(seat.selection);
773
- lines.push(`${seat.seat}\t${kind}\t${seat.source}\t${model}`);
771
+ lines.push(`${seat.seat}\t${seat.source}\t${model}`);
774
772
  }
775
773
  return `${lines.join("\n")}\n`;
776
774
  }
@@ -1569,6 +1567,35 @@ export async function runAkRole(
1569
1567
  };
1570
1568
  }
1571
1569
 
1570
+ // Gatekeeper/Navigator direct public run paths (#639) — instruction seats,
1571
+ // a role like any other; one parameterized branch for both.
1572
+ if (parsed.command === "gatekeeper" || parsed.command === "navigator") {
1573
+ const agentDir = resolveAgentDir(env, home);
1574
+ const cwd = env.cwd ?? process.cwd();
1575
+ const config = await loadAndValidateConfig(home, env.packageRoot);
1576
+ const credentials =
1577
+ env.credentials ?? (await loadCredentialProviders(agentDir));
1578
+ const seat = resolveEffectiveSeat(
1579
+ config,
1580
+ parsed.command,
1581
+ credentials,
1582
+ invocationFromParsed(parsed),
1583
+ );
1584
+ const result = await runPublicInstructionSeat(
1585
+ parsed.args,
1586
+ createRoleEnvironment(env, { role: parsed.command, home, agentDir, cwd, credentials, seat, config }),
1587
+ io,
1588
+ parsed.command,
1589
+ parsed.command === "gatekeeper"
1590
+ ? PUBLIC_ROLE_ARGV.gatekeeper.parse
1591
+ : PUBLIC_ROLE_ARGV.navigator.parse,
1592
+ );
1593
+ return {
1594
+ exitCode: result.exitCode,
1595
+ ...(result.terminal === undefined ? {} : { terminal: result.terminal }),
1596
+ };
1597
+ }
1598
+
1572
1599
  // Analyst public run path: deterministic analysis seat (#336 issue / #337 sweep).
1573
1600
  // Not an LLM PUBLIC_CALLABLE_ROLE — registered only on PUBLIC_ROLE_ARGV (#176).
1574
1601
  if (parsed.command === "analyst") {