@akagilnc/pi-workflow-roles 0.1.4802 → 0.1.4874

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 (142) hide show
  1. package/dist/acp-host/production-host.js +19434 -18218
  2. package/dist/analyst-book-key.js +63 -0
  3. package/dist/analyst-cohort.js +167 -0
  4. package/dist/analyst-entry.js +320 -0
  5. package/dist/analyst-index.js +247 -0
  6. package/dist/analyst-ledger.js +561 -0
  7. package/dist/analyst-median.js +16 -0
  8. package/dist/analyst-metric-families/acceptance-success-rework.js +249 -0
  9. package/dist/analyst-metric-families/b2-frame-buckets-actions.js +189 -0
  10. package/dist/analyst-metric-families/gate-cycles.js +77 -0
  11. package/dist/analyst-metric-families/leg-wall-clock.js +56 -0
  12. package/dist/analyst-metric-families/round-timeline.js +126 -0
  13. package/dist/analyst-metric-families.js +23 -0
  14. package/dist/analyst-metric-family.js +11 -0
  15. package/dist/analyst-model-groups.js +135 -0
  16. package/dist/analyst-page.js +182 -0
  17. package/dist/atomic-write.js +23 -0
  18. package/dist/auditor-soul.js +1 -1
  19. package/dist/countersign-contracts.js +0 -2
  20. package/dist/diarist-contracts.js +0 -1
  21. package/dist/doctor-contracts.js +2 -2
  22. package/dist/gatekeeper-role.js +15 -16
  23. package/dist/gleaner-left-contracts.js +0 -2
  24. package/dist/headless-host/production-host.js +16967 -15749
  25. package/dist/host-contracts.js +0 -4
  26. package/dist/inspector-contracts.js +0 -2
  27. package/dist/institutional-resolution.js +4 -2
  28. package/dist/merger-contracts.js +0 -2
  29. package/dist/migrate-book-topology.js +3908 -3089
  30. package/dist/navigator-public-session.js +4 -3
  31. package/dist/notary-contracts.js +0 -2
  32. package/dist/package-contracts/auditor-output.js +0 -1
  33. package/dist/package-contracts/collector-output.js +0 -2
  34. package/dist/package-contracts/fixer-output.js +0 -2
  35. package/dist/package-contracts/gatekeeper-output.js +0 -2
  36. package/dist/package-contracts/judge-output.js +0 -2
  37. package/dist/package-contracts/navigator-output.js +0 -2
  38. package/dist/package-contracts/reviewer-output.js +0 -2
  39. package/dist/package-contracts/terminating-tools.js +32 -92
  40. package/dist/package-contracts/worker-output.js +0 -4
  41. package/dist/packaged-role-registry.js +518 -36
  42. package/dist/pi/role-turn-host.js +2 -1
  43. package/dist/public-cli/analyst-run.js +185 -0
  44. package/dist/public-cli/cli.js +885 -0
  45. package/dist/public-cli/config.js +12 -9
  46. package/dist/public-cli/countersign-run.js +4 -311
  47. package/dist/public-cli/instruction-seat-run.js +628 -158
  48. package/dist/public-cli/invocation.js +806 -1550
  49. package/dist/public-cli/main.js +6962 -9882
  50. package/dist/public-cli/option-definitions.js +51 -99
  51. package/dist/public-cli/post-admission.js +30 -2
  52. package/dist/public-cli/run-lifecycle.js +234 -353
  53. package/dist/public-cli/seat-ticket-binding.js +6 -0
  54. package/dist/public-cli/settlement.js +261 -675
  55. package/dist/public-cli/turn-request.js +76 -0
  56. package/dist/public-role-summons.js +38 -112
  57. package/dist/role-activation-flags.js +23 -45
  58. package/dist/secretariat-contracts.js +0 -2
  59. package/dist/session-identity.js +17 -8
  60. package/dist/session-opening-materials.js +3 -2
  61. package/extensions/role-runtime.ts +6 -80
  62. package/package.json +1 -1
  63. package/src/acp-host/role-turn-host.ts +4 -1
  64. package/src/analyst-metric-families/acceptance-success-rework.ts +3 -2
  65. package/src/auditor-role.ts +1 -2
  66. package/src/auditor-soul.ts +1 -1
  67. package/src/collector-role.ts +2 -2
  68. package/src/countersign-contracts.ts +0 -1
  69. package/src/countersign-role.ts +1 -1
  70. package/src/diarist-contracts.ts +0 -1
  71. package/src/diarist-role.ts +0 -1
  72. package/src/doctor-auditor.ts +1 -2
  73. package/src/doctor-contracts.ts +1 -1
  74. package/src/doctor-role.ts +3 -2
  75. package/src/external-host-turn-loop.ts +1 -1
  76. package/src/factory-board.ts +10 -7
  77. package/src/gatekeeper-role.ts +16 -10
  78. package/src/gleaner-left-contracts.ts +0 -1
  79. package/src/gleaner-left-role.ts +0 -1
  80. package/src/headless-host/role-turn-host.ts +7 -1
  81. package/src/host-contracts.ts +10 -6
  82. package/src/inspector-contracts.ts +0 -1
  83. package/src/inspector-role.ts +0 -2
  84. package/src/institutional-resolution.ts +6 -4
  85. package/src/judge-role.ts +3 -3
  86. package/src/merger-contracts.ts +0 -1
  87. package/src/merger-role.ts +2 -2
  88. package/src/navigator-public-session.ts +7 -6
  89. package/src/navigator-work-context.ts +6 -8
  90. package/src/notary-contracts.ts +0 -1
  91. package/src/notary-role.ts +2 -3
  92. package/src/package-contracts/auditor-output.ts +0 -1
  93. package/src/package-contracts/collector-output.ts +0 -1
  94. package/src/package-contracts/fixer-output.ts +0 -1
  95. package/src/package-contracts/gatekeeper-output.ts +0 -1
  96. package/src/package-contracts/judge-output.ts +0 -1
  97. package/src/package-contracts/navigator-output.ts +0 -1
  98. package/src/package-contracts/reviewer-output.ts +0 -1
  99. package/src/package-contracts/terminating-tools.ts +27 -99
  100. package/src/package-contracts/worker-output.ts +0 -2
  101. package/src/packaged-role-registry.ts +604 -35
  102. package/src/pi/role-turn-host.ts +2 -1
  103. package/src/public-cli/cli.ts +64 -259
  104. package/src/public-cli/config.ts +18 -10
  105. package/src/public-cli/countersign-run.ts +14 -449
  106. package/src/public-cli/instruction-seat-run.ts +758 -277
  107. package/src/public-cli/invocation.ts +1097 -2051
  108. package/src/public-cli/option-definitions.ts +62 -127
  109. package/src/public-cli/post-admission.ts +47 -2
  110. package/src/public-cli/run-lifecycle.ts +303 -613
  111. package/src/public-cli/seat-ticket-binding.ts +6 -0
  112. package/src/public-cli/settlement.ts +399 -1185
  113. package/src/public-cli/turn-request.ts +92 -1
  114. package/src/public-role-summons.ts +44 -128
  115. package/src/reviewer-role.ts +3 -2
  116. package/src/role-activation-flags.ts +20 -41
  117. package/src/role-runtime-dependencies.ts +25 -26
  118. package/src/role-runtime.ts +121 -232
  119. package/src/secretariat-contracts.ts +0 -1
  120. package/src/secretariat-role.ts +0 -2
  121. package/src/session-identity.ts +23 -8
  122. package/src/session-opening-materials.ts +2 -1
  123. package/src/worker-role.ts +3 -4
  124. package/dist/doctor-auditor.js +0 -25
  125. package/dist/public-cli/diarist-run.js +0 -130
  126. package/dist/public-cli/doctor-run.js +0 -87
  127. package/dist/public-cli/inspector-run.js +0 -151
  128. package/dist/public-cli/judge-run.js +0 -114
  129. package/dist/public-cli/notary-run.js +0 -174
  130. package/dist/public-cli/reviewer-run.js +0 -339
  131. package/src/public-cli/coder-run.ts +0 -255
  132. package/src/public-cli/collector-run.ts +0 -189
  133. package/src/public-cli/diarist-run.ts +0 -233
  134. package/src/public-cli/doctor-run.ts +0 -167
  135. package/src/public-cli/fixer-run.ts +0 -265
  136. package/src/public-cli/gleaner-left-run.ts +0 -167
  137. package/src/public-cli/inspector-run.ts +0 -261
  138. package/src/public-cli/judge-run.ts +0 -196
  139. package/src/public-cli/merger-run.ts +0 -294
  140. package/src/public-cli/notary-run.ts +0 -279
  141. package/src/public-cli/reviewer-run.ts +0 -480
  142. package/src/public-cli/secretariat-run.ts +0 -265
@@ -0,0 +1,885 @@
1
+ import { piDurablePrincipalAuthority } from "../pi/durable-principal.js";
2
+ /**
3
+ * Public ak-role CLI dispatcher (roles / config / layered help / Judge run).
4
+ */
5
+ import { realpath } from "node:fs/promises";
6
+ import { join } from "node:path";
7
+ import { packageMachineHome } from "../activation-ledger-topology.js";
8
+ import { assertLegalEngineModel, assertLegalEngineName, pickEngineAxis, } from "../package-resources/engine-material.js";
9
+ import { resolveConfiguredProvinceOfficer, } from "../institutional-resolution.js";
10
+ import { clearPersistentSeatConfig, effectiveSeatConfigurations, formatModelSpec, isGateOfficerSeat, loadCredentialProviders, loadPublicCliConfig, missingResolvedSeatModelMessage, parseModelSpec, resolveEffectiveSeat, resolvedSeatWithModel, savePublicCliConfig, setAutoResumeLimit, setPersistentSeatConfig, setPersistentSeatEngine, setPersistentSeatEngineModel, setPersistentSeatHost, validatePublicCliConfigAxes, } from "./config.js";
11
+ import { loadHostProvidersTable, projectHostFacingProvider, renderHostProvidersTable, } from "./host-providers.js";
12
+ import { packagedModelParent } from "../packaged-role-registry.js";
13
+ import { seatModelOnly } from "./registry.js";
14
+ import { CliUsageError } from "./cli-errors.js";
15
+ import { appendPiSessionCustomEntry } from "../pi/role-turn-host.js";
16
+ import { composeRoleTurnHostAdapters, formatHostSelectionFailure, HostSelectionError, selectRoleTurnHost, } from "./role-turn-host-resolution.js";
17
+ import { parseAnalystArgv, parsePublicSeatArgv, } from "./invocation.js";
18
+ import { createTypedOptionConsumer, optionsForOwner, projectCommandHelp, projectOwnerOptions, PUBLIC_NAVIGATOR_HELP_NOTE, PUBLIC_ROLE_OPTION_OWNERS, renderHumanOwnerOptionLines, } from "./option-definitions.js";
19
+ import { runPublicInstructionSeat, runPublicInstructionSeatResume } from "./instruction-seat-run.js";
20
+ import { runPublicAnalyst } from "./analyst-run.js";
21
+ import { AUTO_RESUME_LIMIT, peekRoleRunRole, } from "./run-lifecycle.js";
22
+ import { INTERNAL_ROLE_ENTRYPOINT_RELATIVE, PUBLIC_CALLABLE_ROLES, isPublicCallableRole, isPublicCliSupportCommand, isPublicConfigurableSeat, listHelpCapabilities, } from "./registry.js";
23
+ import { formatCliDiagnostic, formatErrorCauseDetail, presentStructuralRejection, } from "./settlement.js";
24
+ export { buildExplicitInternalActivationArgs, resolveInternalRoleEntrypoint, } from "../pi/role-turn-host.js";
25
+ export { CliUsageError } from "./cli-errors.js";
26
+ /**
27
+ * Sole production map: public role command → one argv parse + that owner's option row.
28
+ * Seat option values are assigned in parsePublicSeatArgv. Analyst stays deterministic.
29
+ */
30
+ function publicSeatArgv(owner) {
31
+ return {
32
+ parse: (args) => parsePublicSeatArgv(owner, args),
33
+ options: optionsForOwner(owner),
34
+ };
35
+ }
36
+ /**
37
+ * One argv row per public owner. Seats come from PUBLIC_ROLE_OPTION_OWNERS
38
+ * (composition-root roles plus the deterministic analyst command).
39
+ */
40
+ export const PUBLIC_ROLE_ARGV = Object.fromEntries(PUBLIC_ROLE_OPTION_OWNERS.map((owner) => [
41
+ owner,
42
+ owner === "analyst"
43
+ ? { parse: parseAnalystArgv, options: optionsForOwner("analyst") }
44
+ : publicSeatArgv(owner),
45
+ ]));
46
+ /** Global public options — same typed table as role rows (#342). */
47
+ export const PUBLIC_GLOBAL_OPTIONS = optionsForOwner("global");
48
+ /**
49
+ * If `argv[index]` is a public global flag, describe its span and payload.
50
+ * Spellings + repeatable come solely from PUBLIC_OPTION_TABLE.global via the
51
+ * shared typed consumer (#342).
52
+ */
53
+ function takePublicGlobalFlag(argv, index, options) {
54
+ const tokens = argv.slice(index);
55
+ const taken = options.takeDashed(tokens);
56
+ if (taken === undefined)
57
+ return undefined;
58
+ const consumed = argv.length - index - tokens.length;
59
+ if (taken.def.id === "help") {
60
+ return { flag: "help", consume: consumed };
61
+ }
62
+ if (taken.def.id === "model") {
63
+ return {
64
+ flag: "model",
65
+ consume: consumed,
66
+ value: taken.value,
67
+ };
68
+ }
69
+ if (taken.def.id === "thinking") {
70
+ return {
71
+ flag: "thinking",
72
+ consume: consumed,
73
+ raw: taken.value,
74
+ };
75
+ }
76
+ if (taken.def.id === "engine" || taken.def.id === "host") {
77
+ return {
78
+ flag: taken.def.id,
79
+ consume: consumed,
80
+ value: taken.value,
81
+ };
82
+ }
83
+ return undefined;
84
+ }
85
+ function createRoleEnvironment(env, options,
86
+ /** #178: after host selection, before missing-model — structural argv parse once. */
87
+ afterHost) {
88
+ const role = options.role;
89
+ const extraPiArgs = env.extraPiArgs;
90
+ const timeoutMs = env.timeoutMs;
91
+ // #617/#178/#788/#840: host first, then model; nested child seat selects own host.
92
+ const hostAdapters = composeRoleTurnHostAdapters({
93
+ packageRoot: env.packageRoot,
94
+ ...(env.roleTurnHost === undefined ? {} : { roleTurnHost: env.roleTurnHost }),
95
+ ...(env.hostAdapters === undefined ? {} : { hostAdapters: env.hostAdapters }),
96
+ ...(extraPiArgs === undefined ? {} : { extraPiArgs }),
97
+ ...(timeoutMs === undefined ? {} : { timeoutMs }),
98
+ }, env.principalAuthority);
99
+ const roleTurnHost = selectRoleTurnHost(hostAdapters, {
100
+ role,
101
+ seat: options.seat,
102
+ });
103
+ // #178: host → argv (afterHost) → missing-model → provider projection.
104
+ afterHost?.();
105
+ const seatWithModel = resolvedSeatWithModel(options.seat);
106
+ if (seatWithModel === undefined) {
107
+ // Direct CLI has --model; dual remediation is intentional here (#178 / #916).
108
+ throw new CliUsageError(missingResolvedSeatModelMessage(options.seat.seat, "invocation-or-config"));
109
+ }
110
+ const hostFacingSelection = projectHostFacingProvider(seatWithModel.selection, seatWithModel.host, loadHostProvidersTable(options.home), options.home);
111
+ return {
112
+ home: options.home,
113
+ principalAuthority: env.principalAuthority,
114
+ agentDir: options.agentDir,
115
+ sessionAppender: appendPiSessionCustomEntry,
116
+ packageRoot: env.packageRoot,
117
+ roleTurnHost,
118
+ hostAdapters,
119
+ cwd: options.cwd,
120
+ ...(options.credentials === undefined ? {} : { credentials: options.credentials }),
121
+ ...(env.correlationId === undefined ? {} : { correlationId: env.correlationId }),
122
+ ...(hostFacingSelection === undefined ? {} : { model: hostFacingSelection }),
123
+ ...projectSeatEngine(options.seat),
124
+ ...projectSeatHost(options.seat),
125
+ ...(timeoutMs === undefined ? {} : { timeoutMs }),
126
+ ...(env.createRunId === undefined ? {} : { createRunId: env.createRunId }),
127
+ ...(options.config?.autoResumeLimit === undefined
128
+ ? {}
129
+ : { autoResumeLimit: options.config.autoResumeLimit }),
130
+ ...(env.freshSummons === true ? { freshSummons: true } : {}),
131
+ ...(env.boundTicketNumber === undefined
132
+ ? {}
133
+ : { boundTicketNumber: env.boundTicketNumber }),
134
+ ...(env.signal === undefined ? {} : { signal: env.signal }),
135
+ };
136
+ }
137
+ /**
138
+ * #178 single spine: resolve seat → host select → argv parse once → missing-model → run.
139
+ * Keeps host-unregistered ahead of bad argv and missing-model.
140
+ */
141
+ async function dispatchPublicRoleCommand(env, home, io, parsed, role, parse, run) {
142
+ const agentDir = resolveAgentDir(env, home);
143
+ const cwd = env.cwd ?? process.cwd();
144
+ const config = await loadAndValidateConfig(home, env.packageRoot);
145
+ const credentials = env.credentials ?? (await loadCredentialProviders(agentDir));
146
+ const seat = resolveEffectiveSeat(config, role, credentials, invocationFromParsed(parsed));
147
+ let parsedRoleArgv;
148
+ const result = await run(parsed.args, createRoleEnvironment(env, { role, home, agentDir, cwd, credentials, seat, config }, () => {
149
+ parsedRoleArgv = parse(parsed.args);
150
+ }), () => parsedRoleArgv);
151
+ return {
152
+ exitCode: result.exitCode,
153
+ ...(result.terminal === undefined ? {} : { terminal: result.terminal }),
154
+ };
155
+ }
156
+ function cliResultFromRoleRun(result) {
157
+ return {
158
+ exitCode: result.exitCode,
159
+ ...(result.terminal === undefined ? {} : { terminal: result.terminal }),
160
+ };
161
+ }
162
+ function defaultIo() {
163
+ return {
164
+ stdout: (text) => {
165
+ process.stdout.write(text);
166
+ },
167
+ stderr: (text) => {
168
+ process.stderr.write(text);
169
+ },
170
+ };
171
+ }
172
+ function resolveHome(env) {
173
+ return env.home ?? packageMachineHome();
174
+ }
175
+ function resolveAgentDir(env, home) {
176
+ return (env.agentDir ??
177
+ process.env.PI_CODING_AGENT_DIR ??
178
+ join(home, ".pi", "agent"));
179
+ }
180
+ function parseThinking(value) {
181
+ // #683: opaque pass-through — no local whitelist.
182
+ return value;
183
+ }
184
+ function parseArgv(argv) {
185
+ const args = [...argv];
186
+ let model;
187
+ let thinking;
188
+ let engine;
189
+ let host;
190
+ let help = false;
191
+ const positional = [];
192
+ const globalOptions = createTypedOptionConsumer(PUBLIC_GLOBAL_OPTIONS);
193
+ // Global flags may appear before or after the subcommand
194
+ // (`ak-role --model x roles` and `ak-role roles --model x`).
195
+ // Grammar authority: shared typed consumer over PUBLIC_OPTION_TABLE.global.
196
+ // #471: after `resume <runId>`, remaining argv is the opaque message segment
197
+ // and must not re-enter the global-option consumer — including bare `--`,
198
+ // which is a legal opaque message token, not an argv delimiter here.
199
+ while (args.length > 0) {
200
+ if (positional[0] === "resume" && positional.length >= 2) {
201
+ positional.push(...args);
202
+ break;
203
+ }
204
+ if (args[0] === "--") {
205
+ args.shift();
206
+ positional.push(...args);
207
+ break;
208
+ }
209
+ const taken = takePublicGlobalFlag(args, 0, globalOptions);
210
+ if (taken !== undefined) {
211
+ if (taken.flag === "help") {
212
+ help = true;
213
+ args.splice(0, taken.consume);
214
+ continue;
215
+ }
216
+ if (taken.flag === "model") {
217
+ if (taken.value === undefined) {
218
+ throw new CliUsageError("--model requires a value");
219
+ }
220
+ model = taken.value;
221
+ args.splice(0, taken.consume);
222
+ continue;
223
+ }
224
+ if (taken.flag === "thinking") {
225
+ if (taken.raw === undefined) {
226
+ throw new CliUsageError("--thinking requires a value");
227
+ }
228
+ thinking = parseThinking(taken.raw);
229
+ args.splice(0, taken.consume);
230
+ continue;
231
+ }
232
+ if (taken.flag === "engine" || taken.flag === "host") {
233
+ if (taken.value === undefined || taken.value.trim() === "") {
234
+ throw new CliUsageError(`--${taken.flag} requires a value`);
235
+ }
236
+ if (taken.flag === "engine")
237
+ engine = taken.value;
238
+ else
239
+ host = taken.value;
240
+ args.splice(0, taken.consume);
241
+ continue;
242
+ }
243
+ // Exhaustive for known global flags; unknown id is a table bug.
244
+ throw new CliUsageError(`unhandled global option: ${String(taken.flag)}`);
245
+ }
246
+ // Subcommands may own additional flags later; unknown dashed tokens stay
247
+ // positional here (same as pre-unification parseArgv).
248
+ positional.push(args.shift());
249
+ }
250
+ const [command, ...rest] = positional;
251
+ return {
252
+ ...(command === undefined ? {} : { command }),
253
+ args: rest,
254
+ ...(model === undefined ? {} : { model }),
255
+ ...(thinking === undefined ? {} : { thinking }),
256
+ ...(engine === undefined ? {} : { engine }),
257
+ ...(host === undefined ? {} : { host }),
258
+ help,
259
+ };
260
+ }
261
+ /**
262
+ * Unique public resume request parser (#471).
263
+ * One optional argv after runId is the opaque message; no further positionals.
264
+ */
265
+ const RESUME_USAGE_ERROR = "usage: ak-role resume <runId> [message] (put --model/--thinking/--host/--engine before <runId>; the one argv after <runId> is the opaque message, #471)";
266
+ function parseResumeRequest(args) {
267
+ const runId = args[0];
268
+ if (runId === undefined || runId.trim() === "" || runId.startsWith("-")) {
269
+ throw new CliUsageError(RESUME_USAGE_ERROR);
270
+ }
271
+ if (args.length > 2) {
272
+ throw new CliUsageError(RESUME_USAGE_ERROR);
273
+ }
274
+ if (args.length === 2) {
275
+ return { runId, message: args[1] };
276
+ }
277
+ return { runId };
278
+ }
279
+ function invocationFromParsed(parsed) {
280
+ if (parsed.model === undefined &&
281
+ parsed.thinking === undefined &&
282
+ parsed.engine === undefined &&
283
+ parsed.host === undefined) {
284
+ return undefined;
285
+ }
286
+ return {
287
+ ...(parsed.model === undefined ? {} : { model: parsed.model }),
288
+ ...(parsed.thinking === undefined ? {} : { thinking: parsed.thinking }),
289
+ ...(parsed.engine === undefined ? {} : { engine: parsed.engine }),
290
+ ...(parsed.host === undefined ? {} : { host: parsed.host }),
291
+ };
292
+ }
293
+ function requireLegalEngineModel(model) {
294
+ try {
295
+ return assertLegalEngineModel(model);
296
+ }
297
+ catch (error) {
298
+ throw new CliUsageError(error instanceof Error ? error.message : String(error), { cause: error });
299
+ }
300
+ }
301
+ function requireLegalEngineName(name) {
302
+ try {
303
+ return assertLegalEngineName(name);
304
+ }
305
+ catch (error) {
306
+ throw new CliUsageError(error instanceof Error ? error.message : String(error), { cause: error });
307
+ }
308
+ }
309
+ /** Callable seats own persistent call axes (checked against the callable predicate). */
310
+ function requireCallableSeat(seat, axis, verb) {
311
+ if (!isPublicCallableRole(seat)) {
312
+ throw new CliUsageError(`unknown ${axis}-axis seat: ${seat}`);
313
+ }
314
+ }
315
+ /** Single seat engine axis → run-options projection (#391 E2 / #883). */
316
+ function projectSeatEngine(seat) {
317
+ return pickEngineAxis(seat);
318
+ }
319
+ /** Single seat.host → run-options projection (#595 / #617). */
320
+ function projectSeatHost(seat) {
321
+ return { host: seat.host };
322
+ }
323
+ function loadAndValidateConfig(home, packageRoot) {
324
+ return loadPublicCliConfig(home).then((config) => {
325
+ try {
326
+ validatePublicCliConfigAxes(config, packageRoot);
327
+ }
328
+ catch (error) {
329
+ throw new CliUsageError(error instanceof Error ? error.message : String(error), { cause: error });
330
+ }
331
+ return config;
332
+ });
333
+ }
334
+ /** Typed facts used by help presentation — not layout. */
335
+ export function helpDocument() {
336
+ return {
337
+ executable: "ak-role",
338
+ capabilities: listHelpCapabilities(),
339
+ internalEntrypoint: INTERNAL_ROLE_ENTRYPOINT_RELATIVE,
340
+ /** #342 structured global options from the sole option table. */
341
+ globalOptions: projectOwnerOptions("global"),
342
+ };
343
+ }
344
+ /** Structured help facts for one public command/role (#342). */
345
+ export function helpDocumentForCommand(command) {
346
+ if (command === "global") {
347
+ return {
348
+ command: "global",
349
+ kind: "global",
350
+ options: projectOwnerOptions("global"),
351
+ };
352
+ }
353
+ if (command in PUBLIC_ROLE_ARGV) {
354
+ const owner = command;
355
+ return {
356
+ command: owner,
357
+ kind: owner === "analyst" ? "deterministic" : "role",
358
+ options: projectOwnerOptions(owner),
359
+ };
360
+ }
361
+ return undefined;
362
+ }
363
+ /** Append USAGE + EXAMPLES blocks from the sole public help-copy owner. */
364
+ function appendUsageAndExamples(lines, topic) {
365
+ const facts = projectCommandHelp(topic);
366
+ if (facts === undefined)
367
+ return;
368
+ lines.push("", "USAGE");
369
+ for (const line of facts.usage) {
370
+ lines.push(` ${line}`);
371
+ }
372
+ if (facts.examples.length > 0) {
373
+ lines.push("", "EXAMPLES");
374
+ for (const example of facts.examples) {
375
+ lines.push(` ${example}`);
376
+ }
377
+ }
378
+ }
379
+ function renderHelp() {
380
+ const doc = helpDocument();
381
+ // "top" is a required PUBLIC_COMMAND_HELP topic — no fallback prose (#412/397-F3).
382
+ const top = projectCommandHelp("top");
383
+ const lines = [
384
+ `ak-role — ${top.summary}`,
385
+ ];
386
+ appendUsageAndExamples(lines, "top");
387
+ lines.push("", PUBLIC_NAVIGATOR_HELP_NOTE);
388
+ lines.push("", "Support commands:");
389
+ for (const cap of doc.capabilities) {
390
+ if (cap.kind === "support") {
391
+ lines.push(` ${cap.name}`);
392
+ }
393
+ }
394
+ lines.push("", "Callable roles:");
395
+ for (const cap of doc.capabilities) {
396
+ if (cap.kind === "role") {
397
+ const phaseText = cap.phases.length === 1 && cap.phases[0] === null
398
+ ? "no phase"
399
+ : `phases ${cap.phases.filter((p) => p !== null).join("|")}` +
400
+ (cap.defaultPhase ? ` (default ${cap.defaultPhase})` : "");
401
+ lines.push(` ${cap.name} — ${phaseText}`);
402
+ }
403
+ }
404
+ lines.push("", "Deterministic commands:");
405
+ for (const cap of doc.capabilities) {
406
+ if (cap.kind === "deterministic") {
407
+ lines.push(` ${cap.name}`);
408
+ }
409
+ }
410
+ lines.push("", "OPTIONS");
411
+ lines.push(...renderHumanOwnerOptionLines("global"));
412
+ lines.push("", "Role options: ak-role help <command>", "Persistent config: ak-role config set <seat> <provider/model[:thinking]> | unset <gatekeeper|inspector|notary>", "Persistent engine (callable roles): ak-role config set-engine <seat> <name> [model] | unset-engine <seat> | set-engine-model <seat> <model> | unset-engine-model <seat>", "Persistent host (callable roles): ak-role config set-host <seat> <name> | unset-host <seat>", "Host providers: ~/.ak-roles/host-providers.json (owner-edited; table > unique host directory > fail)", "Model resolution: --model → persistent seat → officer inherit (#620); still none → error (#178)", "Host resolution: --host → persistent seat host → pi (resume uses the same order; #617)", "Resume flag position: --model/--thinking/--host/--engine go before <runId> (before `resume` or between `resume` and <runId>); the one argv after <runId> is always the opaque message, never a flag (#471)", "Effective seats: ak-role roles");
413
+ return `${lines.join("\n")}\n`;
414
+ }
415
+ function renderCommandHelp(command) {
416
+ const caps = listHelpCapabilities();
417
+ const match = caps.find((cap) => cap.name === command);
418
+ if (match === undefined)
419
+ return undefined;
420
+ const facts = projectCommandHelp(command);
421
+ const lines = [];
422
+ if (facts !== undefined) {
423
+ lines.push(`ak-role ${facts.command} — ${facts.summary}`);
424
+ }
425
+ else if (match.kind === "support") {
426
+ lines.push(`ak-role ${match.name}`);
427
+ }
428
+ else if (match.kind === "deterministic") {
429
+ lines.push(`ak-role ${match.name}`);
430
+ }
431
+ else {
432
+ lines.push(`ak-role ${match.name}`);
433
+ }
434
+ appendUsageAndExamples(lines, command);
435
+ // listHelpCapabilities never yields "global"; only role owners carry OPTIONS (#412/397-F3).
436
+ if (command in PUBLIC_ROLE_ARGV) {
437
+ const owner = command;
438
+ lines.push("", "OPTIONS");
439
+ lines.push(...renderHumanOwnerOptionLines(owner));
440
+ }
441
+ return `${lines.join("\n")}\n`;
442
+ }
443
+ function renderRoles(seats) {
444
+ const lines = ["seat\tsource\tmodel"];
445
+ for (const seat of seats) {
446
+ const model = seat.selection === undefined ? "-" : formatModelSpec(seat.selection);
447
+ lines.push(`${seat.seat}\t${seat.source}\t${model}`);
448
+ }
449
+ return `${lines.join("\n")}\n`;
450
+ }
451
+ function diskAxes(disk) {
452
+ return {
453
+ ...pickEngineAxis(disk ?? {}),
454
+ ...(disk?.host === undefined ? {} : { host: disk.host }),
455
+ };
456
+ }
457
+ /**
458
+ * Single-seat config projection (#620).
459
+ * - notary/inspector: institutional authority result (own > gatekeeper)
460
+ * - all other seats: disk model only (persistent face; no package fill-in, #178)
461
+ */
462
+ export function projectConfigSeatDisplay(config, seat) {
463
+ const disk = config.seats[seat];
464
+ if (packagedModelParent(seat) !== undefined) {
465
+ const resolved = resolveConfiguredProvinceOfficer(config, seat);
466
+ return {
467
+ seat,
468
+ source: resolved.source,
469
+ ...(resolved.selection === undefined ? {} : { selection: resolved.selection }),
470
+ ...diskAxes(disk),
471
+ };
472
+ }
473
+ const own = seatModelOnly(disk);
474
+ if (own !== undefined) {
475
+ return { seat, source: "persistent", selection: own, ...diskAxes(disk) };
476
+ }
477
+ if (disk === undefined) {
478
+ return { seat, source: "unconfigured" };
479
+ }
480
+ return { seat, source: "unconfigured", ...diskAxes(disk) };
481
+ }
482
+ /**
483
+ * Bulk config show projection (#620).
484
+ * Disk seats only, plus notary/inspector inherit rows when gatekeeper supplies a
485
+ * model and the subordinate has no own pin — never invent province rows from an
486
+ * unrelated seat like coder.
487
+ */
488
+ export function projectConfigDisplaySeats(config) {
489
+ const diskSeats = Object.keys(config.seats).filter((seat) => isPublicConfigurableSeat(seat));
490
+ const rows = new Map();
491
+ for (const seat of diskSeats) {
492
+ rows.set(seat, projectConfigSeatDisplay(config, seat));
493
+ }
494
+ for (const seat of PUBLIC_CALLABLE_ROLES) {
495
+ if (packagedModelParent(seat) === undefined || rows.has(seat))
496
+ continue;
497
+ const projected = projectConfigSeatDisplay(config, seat);
498
+ if (projected.source === "inherit-gatekeeper") {
499
+ rows.set(seat, projected);
500
+ }
501
+ }
502
+ return [...rows.keys()].sort().map((seat) => rows.get(seat));
503
+ }
504
+ function renderConfigDisplaySeat(row) {
505
+ const model = row.selection === undefined ? "-" : formatModelSpec(row.selection);
506
+ const engine = row.engine === undefined ? "-" : row.engine;
507
+ const engineModel = row.engineModel === undefined ? "-" : row.engineModel;
508
+ const host = row.host === undefined ? "-" : row.host;
509
+ return `${row.seat}\t${row.source}\t${model}\t${engine}\t${engineModel}\t${host}`;
510
+ }
511
+ function renderConfig(config, home) {
512
+ const lines = ["seat\tsource\tmodel\tengine\tengineModel\thost"];
513
+ const rows = projectConfigDisplaySeats(config);
514
+ if (rows.length === 0) {
515
+ lines.push("(empty)");
516
+ }
517
+ else {
518
+ for (const row of rows) {
519
+ lines.push(renderConfigDisplaySeat(row));
520
+ }
521
+ }
522
+ // #422: show the effective auto-resume ceiling (configured value or default).
523
+ lines.push(`autoResumeLimit\t${config.autoResumeLimit ?? AUTO_RESUME_LIMIT}`);
524
+ // #788: owner host-providers table as written on disk (separate file).
525
+ const hostProvidersBlock = renderHostProvidersTable(loadHostProvidersTable(home));
526
+ return `${lines.join("\n")}\n${hostProvidersBlock}`;
527
+ }
528
+ async function runConfigCommand(args, home, packageRoot, io) {
529
+ if (args.length === 0 || args[0] === "get" || args[0] === "list" || args[0] === "show") {
530
+ const config = await loadAndValidateConfig(home, packageRoot);
531
+ if (args[0] === "get" && args[1] !== undefined) {
532
+ if (!isPublicConfigurableSeat(args[1])) {
533
+ throw new CliUsageError(`unknown configurable seat: ${args[1]}`);
534
+ }
535
+ io.stdout(`${renderConfigDisplaySeat(projectConfigSeatDisplay(config, args[1]))}\n`);
536
+ return 0;
537
+ }
538
+ io.stdout(renderConfig(config, home));
539
+ return 0;
540
+ }
541
+ if (args[0] === "set") {
542
+ if (args.length < 3) {
543
+ throw new CliUsageError("usage: ak-role config set <seat> <provider/model[:thinking]>");
544
+ }
545
+ // Bulk: repeated seat spec pairs after `set`
546
+ const pairs = args.slice(1);
547
+ if (pairs.length % 2 !== 0) {
548
+ throw new CliUsageError("config set requires seat/spec pairs: ak-role config set <seat> <spec> [<seat> <spec> ...]");
549
+ }
550
+ let config = await loadAndValidateConfig(home, packageRoot);
551
+ for (let i = 0; i < pairs.length; i += 2) {
552
+ const seat = pairs[i];
553
+ const spec = pairs[i + 1];
554
+ if (!isPublicConfigurableSeat(seat)) {
555
+ throw new CliUsageError(`unknown configurable seat: ${seat}`);
556
+ }
557
+ // #384: persistent seat config shares the invocation model grammar.
558
+ // Bare provider/model stores as-is; :thinking suffix still required only when colon present.
559
+ config = setPersistentSeatConfig(config, seat, parseModelSpec(spec));
560
+ }
561
+ await savePublicCliConfig(config, home);
562
+ io.stdout(renderConfig(config, home));
563
+ return 0;
564
+ }
565
+ // #453: clear gate officer model override only (engine axis preserved).
566
+ if (args[0] === "unset") {
567
+ if (args.length !== 2) {
568
+ throw new CliUsageError("usage: ak-role config unset <gatekeeper|inspector|notary>");
569
+ }
570
+ const seat = args[1];
571
+ if (!isGateOfficerSeat(seat)) {
572
+ throw new CliUsageError(`config unset serves gate officer overrides only (gatekeeper|inspector|notary); got ${seat}`);
573
+ }
574
+ const config = clearPersistentSeatConfig(await loadAndValidateConfig(home, packageRoot), seat);
575
+ await savePublicCliConfig(config, home);
576
+ io.stdout(renderConfig(config, home));
577
+ return 0;
578
+ }
579
+ if (args[0] === "set-host" || args[0] === "unset-host") {
580
+ const unset = args[0] === "unset-host";
581
+ if (args.length !== (unset ? 2 : 3)) {
582
+ throw new CliUsageError(`usage: ak-role config ${args[0]} <seat>${unset ? "" : " <name>"}`);
583
+ }
584
+ const seat = args[1];
585
+ requireCallableSeat(seat, "host", unset ? "unset-host" : "set-host");
586
+ let config = await loadAndValidateConfig(home, packageRoot);
587
+ try {
588
+ config = setPersistentSeatHost(config, seat, unset ? undefined : args[2]);
589
+ }
590
+ catch (error) {
591
+ throw new CliUsageError(error instanceof Error ? error.message : String(error), { cause: error });
592
+ }
593
+ await savePublicCliConfig(config, home);
594
+ io.stdout(renderConfig(config, home));
595
+ return 0;
596
+ }
597
+ if (args[0] === "set-engine") {
598
+ if (args.length !== 3 && args.length !== 4) {
599
+ throw new CliUsageError("usage: ak-role config set-engine <seat> <name> [model]");
600
+ }
601
+ const seat = args[1];
602
+ const name = args[2];
603
+ const engineModel = args[3];
604
+ requireCallableSeat(seat, "engine", "set-engine");
605
+ requireLegalEngineName(name);
606
+ if (engineModel !== undefined)
607
+ requireLegalEngineModel(engineModel);
608
+ let config = await loadAndValidateConfig(home, packageRoot);
609
+ try {
610
+ config = setPersistentSeatEngine(config, seat, name, engineModel);
611
+ }
612
+ catch (error) {
613
+ throw new CliUsageError(error instanceof Error ? error.message : String(error), { cause: error });
614
+ }
615
+ await savePublicCliConfig(config, home);
616
+ io.stdout(renderConfig(config, home));
617
+ return 0;
618
+ }
619
+ if (args[0] === "unset-engine") {
620
+ if (args.length !== 2) {
621
+ throw new CliUsageError("usage: ak-role config unset-engine <seat>");
622
+ }
623
+ const seat = args[1];
624
+ requireCallableSeat(seat, "engine", "unset-engine");
625
+ let config = await loadAndValidateConfig(home, packageRoot);
626
+ try {
627
+ config = setPersistentSeatEngine(config, seat, undefined);
628
+ }
629
+ catch (error) {
630
+ throw new CliUsageError(error instanceof Error ? error.message : String(error), { cause: error });
631
+ }
632
+ await savePublicCliConfig(config, home);
633
+ io.stdout(renderConfig(config, home));
634
+ return 0;
635
+ }
636
+ if (args[0] === "set-engine-model") {
637
+ if (args.length !== 3) {
638
+ throw new CliUsageError("usage: ak-role config set-engine-model <seat> <model>");
639
+ }
640
+ const seat = args[1];
641
+ const model = args[2];
642
+ requireCallableSeat(seat, "engine", "set-engine-model");
643
+ requireLegalEngineModel(model);
644
+ let config = await loadAndValidateConfig(home, packageRoot);
645
+ try {
646
+ config = setPersistentSeatEngineModel(config, seat, model);
647
+ }
648
+ catch (error) {
649
+ throw new CliUsageError(error instanceof Error ? error.message : String(error), { cause: error });
650
+ }
651
+ await savePublicCliConfig(config, home);
652
+ io.stdout(renderConfig(config, home));
653
+ return 0;
654
+ }
655
+ if (args[0] === "unset-engine-model") {
656
+ if (args.length !== 2) {
657
+ throw new CliUsageError("usage: ak-role config unset-engine-model <seat>");
658
+ }
659
+ const seat = args[1];
660
+ requireCallableSeat(seat, "engine", "unset-engine-model");
661
+ let config = await loadAndValidateConfig(home, packageRoot);
662
+ try {
663
+ config = setPersistentSeatEngineModel(config, seat, undefined);
664
+ }
665
+ catch (error) {
666
+ throw new CliUsageError(error instanceof Error ? error.message : String(error), { cause: error });
667
+ }
668
+ await savePublicCliConfig(config, home);
669
+ io.stdout(renderConfig(config, home));
670
+ return 0;
671
+ }
672
+ // #422: standalone verb per the set-engine/unset-engine precedent — the
673
+ // existing `config set` grammar stays strictly even-position seat/spec pairs.
674
+ if (args[0] === "set-auto-resume-limit") {
675
+ if (args.length !== 2) {
676
+ throw new CliUsageError("usage: ak-role config set-auto-resume-limit <N>");
677
+ }
678
+ const raw = args[1];
679
+ if (!/^[0-9]+$/.test(raw)) {
680
+ throw new CliUsageError(`auto-resume limit must be a non-negative integer, got ${raw}`);
681
+ }
682
+ // #422 fidelity boundary (not an upper bound — ADR 0035 stays intact): the
683
+ // persisted representation is a JS number, and Number() silently rounds
684
+ // integers beyond 2^53-1 (e.g. 9007199254740993 → 9007199254740992). Refuse
685
+ // loudly instead of persisting a different N; every exactly-representable
686
+ // non-negative integer remains legal. The regex above guarantees pure
687
+ // digits, so BigInt(raw) has no leading-zero ambiguity.
688
+ const converted = Number(raw);
689
+ if (!Number.isFinite(converted) || BigInt(converted) !== BigInt(raw)) {
690
+ throw new CliUsageError(`auto-resume limit ${raw} is not exactly representable as a number; refusing to silently round the value`);
691
+ }
692
+ let config = await loadAndValidateConfig(home, packageRoot);
693
+ config = setAutoResumeLimit(config, converted);
694
+ await savePublicCliConfig(config, home);
695
+ io.stdout(renderConfig(config, home));
696
+ return 0;
697
+ }
698
+ throw new CliUsageError(`unknown config subcommand: ${args[0]}`);
699
+ }
700
+ /**
701
+ * #855: process-cancel handlers only for commands that run a role turn
702
+ * (callable roles / resume / new). analyst/roles/config/help keep Node's
703
+ * default SIGINT/SIGTERM termination — installing handlers would swallow Ctrl+C.
704
+ */
705
+ export function commandNeedsProcessCancel(argv) {
706
+ // Decision-time parse must not escape the CLI usage boundary. Malformed
707
+ // global flags throw CliUsageError here; return false and let runAkRole
708
+ // present the same structured usage reject (exit 2). Handlers are moot
709
+ // when parse fails — install-or-not has no subsequent effect.
710
+ let parsed;
711
+ try {
712
+ parsed = parseArgv(argv);
713
+ }
714
+ catch (error) {
715
+ if (error instanceof CliUsageError)
716
+ return false;
717
+ throw error;
718
+ }
719
+ if (parsed.help || parsed.command === undefined || parsed.command === "help") {
720
+ return false;
721
+ }
722
+ if (parsed.command === "resume" || parsed.command === "new")
723
+ return true;
724
+ return isPublicCallableRole(parsed.command);
725
+ }
726
+ export async function runAkRole(argv, env) {
727
+ const io = env.io ?? defaultIo();
728
+ try {
729
+ // Select the installed package identity once, before any role-owned Skill,
730
+ // runtime entry, activation argv, or invocation provenance is derived.
731
+ env = {
732
+ ...env,
733
+ packageRoot: await realpath(env.packageRoot),
734
+ principalAuthority: env.principalAuthority ?? piDurablePrincipalAuthority,
735
+ };
736
+ let parsed = parseArgv(argv);
737
+ // Host/engine axes: callable roles + resume + new (#617 DK-3; #724 new shares seat axes).
738
+ // Support commands (roles/config/…) still refuse both flags.
739
+ const acceptsSeatAxes = parsed.command !== undefined &&
740
+ (isPublicCallableRole(parsed.command) ||
741
+ parsed.command === "resume" ||
742
+ parsed.command === "new");
743
+ if (parsed.host !== undefined &&
744
+ !parsed.help &&
745
+ parsed.command !== undefined &&
746
+ parsed.command !== "help" &&
747
+ !acceptsSeatAxes) {
748
+ throw new CliUsageError(`host axis is role commands only; refused command ${parsed.command}`);
749
+ }
750
+ if (parsed.engine !== undefined) {
751
+ requireLegalEngineName(parsed.engine);
752
+ if (!parsed.help &&
753
+ parsed.command !== undefined &&
754
+ parsed.command !== "help" &&
755
+ !acceptsSeatAxes) {
756
+ throw new CliUsageError(`engine axis is role commands only; refused command ${parsed.command}`);
757
+ }
758
+ }
759
+ if (parsed.help ||
760
+ parsed.command === undefined ||
761
+ parsed.command === "help") {
762
+ // Layered help: `help <topic>` derives from the typed registry + option table (#342).
763
+ // Home-free path: never touch passwd/user profile for help/bare/--help.
764
+ if (parsed.command === "help" && parsed.args[0] !== undefined) {
765
+ const topic = parsed.args[0];
766
+ const rendered = renderCommandHelp(topic);
767
+ if (rendered === undefined) {
768
+ throw new CliUsageError(`unknown help topic: ${topic}`);
769
+ }
770
+ io.stdout(rendered);
771
+ return { exitCode: 0 };
772
+ }
773
+ io.stdout(renderHelp());
774
+ return { exitCode: 0 };
775
+ }
776
+ // Profile home only after home-free paths return. Failures keep real identity
777
+ // and settle through the outer catch — no $HOME fallback (#604).
778
+ const home = resolveHome(env);
779
+ if (parsed.command === "roles") {
780
+ if (parsed.args.length > 0) {
781
+ throw new CliUsageError("roles takes no arguments");
782
+ }
783
+ const config = await loadAndValidateConfig(home, env.packageRoot);
784
+ const credentials = env.credentials ??
785
+ (await loadCredentialProviders(resolveAgentDir(env, home)));
786
+ const seats = effectiveSeatConfigurations(config, credentials, invocationFromParsed(parsed));
787
+ io.stdout(renderRoles(seats));
788
+ return { exitCode: 0 };
789
+ }
790
+ if (parsed.command === "config") {
791
+ return {
792
+ exitCode: await runConfigCommand(parsed.args, home, env.packageRoot, io),
793
+ };
794
+ }
795
+ // #724 explicit fresh summons: `ak-role new <role> …` — same role argv, always mint.
796
+ // Rewrites onto the role command with freshSummons; auto-resume and resume stay intact.
797
+ if (parsed.command === "new") {
798
+ const role = parsed.args[0];
799
+ if (role === undefined) {
800
+ throw new CliUsageError("usage: ak-role new <role> …");
801
+ }
802
+ if (!isPublicCallableRole(role)) {
803
+ throw new CliUsageError(`usage: ak-role new <role> …; unknown role: ${role}`);
804
+ }
805
+ parsed = {
806
+ ...parsed,
807
+ command: role,
808
+ args: parsed.args.slice(1),
809
+ };
810
+ env = { ...env, freshSummons: true };
811
+ }
812
+ // Resume reopens an exact Role run (#416): caller decides; session principal
813
+ // must still exist. Seat and dispatch follow the durable admitted role.
814
+ // #471: unique parser owns {runId, message?}; five role paths only consume it.
815
+ if (parsed.command === "resume") {
816
+ const agentDir = resolveAgentDir(env, home);
817
+ const cwd = env.cwd ?? process.cwd();
818
+ const config = await loadAndValidateConfig(home, env.packageRoot);
819
+ const credentials = env.credentials ?? (await loadCredentialProviders(agentDir));
820
+ const resumeRequest = parseResumeRequest(parsed.args);
821
+ const resumeRole = await peekRoleRunRole(home, resumeRequest.runId);
822
+ // Missing durable role keeps the judge seat table. The resume entry
823
+ // itself is one function; it reads the stored run.
824
+ const seatRole = resumeRole ?? "judge";
825
+ const seat = resolveEffectiveSeat(config, seatRole, credentials, invocationFromParsed(parsed));
826
+ const result = await runPublicInstructionSeatResume(resumeRequest, createRoleEnvironment(env, {
827
+ role: seatRole,
828
+ home,
829
+ agentDir,
830
+ cwd,
831
+ credentials,
832
+ seat,
833
+ config,
834
+ }), io);
835
+ return cliResultFromRoleRun(result);
836
+ }
837
+ if (parsed.command !== undefined &&
838
+ isPublicCliSupportCommand(parsed.command)) {
839
+ throw new CliUsageError(`unhandled support command: ${parsed.command}`);
840
+ }
841
+ // Public LLM role commands share one spine: registry parser → one runner.
842
+ if (parsed.command !== undefined && isPublicCallableRole(parsed.command)) {
843
+ const role = parsed.command;
844
+ return await dispatchPublicRoleCommand(env, home, io, parsed, role, (args) => PUBLIC_ROLE_ARGV[role].parse(args), (args, roleEnv, once) => runPublicInstructionSeat(args, roleEnv, io, role, once));
845
+ }
846
+ // Analyst public run path: deterministic analysis seat (#336 issue / #337 sweep).
847
+ // Not an LLM PUBLIC_CALLABLE_ROLE — registered only on PUBLIC_ROLE_ARGV (#176).
848
+ if (parsed.command === "analyst") {
849
+ const result = await runPublicAnalyst(parsed.args, { home }, io, PUBLIC_ROLE_ARGV.analyst.parse);
850
+ return { exitCode: result.exitCode };
851
+ }
852
+ // #115: every PUBLIC_CALLABLE_ROLE has a completed handler above. Unknown
853
+ // tokens (including misspelled role names) are structural rejects.
854
+ throw new CliUsageError(`unknown command: ${parsed.command}`);
855
+ }
856
+ catch (error) {
857
+ if (error instanceof HostSelectionError) {
858
+ io.stderr(formatCliDiagnostic(formatHostSelectionFailure(error.failure)));
859
+ return { exitCode: 1, hostFailure: error.failure };
860
+ }
861
+ if (error instanceof CliUsageError) {
862
+ // Non-judge structural paths share the same rejection presenter as Judge.
863
+ presentStructuralRejection(error, io);
864
+ return { exitCode: 2 };
865
+ }
866
+ // Unrecognized outer failure: retain actual name/message identity (no wash).
867
+ // #676 B: cause (HTTP body/headers, git stderr, nested Error) must reach the
868
+ // caller via the same formatErrorCauseDetail face as structural rejection.
869
+ if (error instanceof Error) {
870
+ let label = error.name !== "" && error.name !== "Error"
871
+ ? `${error.name}: ${error.message}`
872
+ : error.message;
873
+ if (error.cause !== undefined) {
874
+ const detail = formatErrorCauseDetail(error.cause);
875
+ if (detail.trim().length > 0) {
876
+ label = `${label || error.name || "exception"}; cause: ${detail}`;
877
+ }
878
+ }
879
+ io.stderr(formatCliDiagnostic(label || error.name || "exception"));
880
+ return { exitCode: 1 };
881
+ }
882
+ io.stderr(formatCliDiagnostic(String(error)));
883
+ return { exitCode: 1 };
884
+ }
885
+ }