@akagilnc/pi-workflow-roles 0.1.2296 → 0.1.2310

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.
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Public Notary Role run: admit source-run locator → shared one-shot dispatch
3
+ * → settle Terminal result (#448). Zero caller prompt/attachment. Lifecycle is
4
+ * the shared Doctor-isomorphic seam; this module keeps only Notary adapters.
5
+ */
6
+ import { engineSessionMaterialFromOptions } from "../package-resources/engine-material.ts";
7
+ import { CliUsageError } from "./cli-errors.ts";
8
+ import {
9
+ admitNotaryInvocation,
10
+ buildNotaryTransportPrompt,
11
+ type AdmittedNotaryInvocation,
12
+ type ParseNotaryArgvResult,
13
+ } from "./invocation.ts";
14
+ import {
15
+ buildSeatModelCliArgs,
16
+ type SeatModelConfig,
17
+ } from "./config.ts";
18
+ import {
19
+ runAdmittedOneShotRole,
20
+ type OneShotRunEnv,
21
+ } from "./one-shot-dispatch.ts";
22
+ import {
23
+ presentStructuralRejection,
24
+ trySettleNotaryTerminalResult,
25
+ } from "./settlement.ts";
26
+ import type { CliIo } from "./cli-io.ts";
27
+ import type { TerminalResult } from "./terminal.ts";
28
+
29
+ export type NotaryRunEnv = OneShotRunEnv & {
30
+ createRunId?: () => string;
31
+ extraPiArgs?: readonly string[];
32
+ };
33
+
34
+ export function buildNotaryActivationExtraArgs(
35
+ admitted: AdmittedNotaryInvocation,
36
+ options: {
37
+ model?: SeatModelConfig;
38
+ engine?: string;
39
+ packageRoot?: string;
40
+ extraPiArgs?: readonly string[];
41
+ } = {},
42
+ ): string[] {
43
+ const prompt = buildNotaryTransportPrompt(
44
+ admitted,
45
+ engineSessionMaterialFromOptions(options),
46
+ );
47
+ return [
48
+ "--no-skills",
49
+ "--no-prompt-templates",
50
+ "--no-themes",
51
+ "--no-context-files",
52
+ "--session",
53
+ admitted.sessionFile,
54
+ "--session-dir",
55
+ admitted.sessionDirectory,
56
+ ...(options.extraPiArgs ?? []),
57
+ "--ak-role",
58
+ "notary",
59
+ "--ak-notary-source-run",
60
+ admitted.sourceRunPath,
61
+ "--mode",
62
+ "json",
63
+ ...buildSeatModelCliArgs(options.model),
64
+ prompt,
65
+ ];
66
+ }
67
+
68
+ export async function runPublicNotary(
69
+ argv: readonly string[],
70
+ env: NotaryRunEnv,
71
+ io: CliIo,
72
+ parseNotaryArgv: (args: readonly string[]) => ParseNotaryArgvResult,
73
+ ): Promise<{
74
+ exitCode: number;
75
+ admitted?: AdmittedNotaryInvocation;
76
+ terminal?: TerminalResult;
77
+ }> {
78
+ let admitted: AdmittedNotaryInvocation;
79
+ try {
80
+ const parsed = parseNotaryArgv(argv);
81
+ admitted = await admitNotaryInvocation({
82
+ home: env.home,
83
+ cwd: env.cwd,
84
+ sourceRun: parsed.sourceRun,
85
+ ...(parsed.project === undefined ? {} : { project: parsed.project }),
86
+ ...(env.createRunId === undefined ? {} : { createRunId: env.createRunId }),
87
+ ...(env.model === undefined ? {} : { model: env.model }),
88
+ ...(env.correlationId === undefined ? {} : { correlationId: env.correlationId }),
89
+ });
90
+ } catch (error) {
91
+ if (error instanceof CliUsageError) {
92
+ presentStructuralRejection(error, io);
93
+ return { exitCode: 2 };
94
+ }
95
+ throw error;
96
+ }
97
+
98
+ const extraArgs = buildNotaryActivationExtraArgs(admitted, {
99
+ packageRoot: env.packageRoot,
100
+ ...(env.model === undefined ? {} : { model: env.model }),
101
+ ...(env.engine === undefined ? {} : { engine: env.engine }),
102
+ ...(env.extraPiArgs === undefined ? {} : { extraPiArgs: env.extraPiArgs }),
103
+ });
104
+
105
+ return await runAdmittedOneShotRole({
106
+ admitted,
107
+ env,
108
+ io,
109
+ extraArgs,
110
+ adapters: {
111
+ trySettle: trySettleNotaryTerminalResult,
112
+ // Accepted receipts and residual incomplete share one present path.
113
+ shouldPresentSettled: () => true,
114
+ },
115
+ ...(env.engine === undefined ? {} : { effectiveEngine: env.engine }),
116
+ });
117
+ }
@@ -0,0 +1,300 @@
1
+ /**
2
+ * Shared one-shot public Role dispatch seam (Doctor-isomorphic path).
3
+ * Post-admission lifecycle — mark admitted → writer lease → running → spawn →
4
+ * settle/fail → terminal → release — lives here once. Role runners supply only
5
+ * activation args and settlement adapters (ADR 0018 / #448).
6
+ */
7
+ import { writeFile } from "node:fs/promises";
8
+ import { join } from "node:path";
9
+
10
+ import { applyEngineChildEnv } from "../engine-detour.ts";
11
+ import {
12
+ runExplicitInternalActivation,
13
+ type ExplicitInternalKnownFailure,
14
+ type ExplicitInternalPiRunner,
15
+ type ExplicitInternalPiResult,
16
+ } from "./explicit-internal.ts";
17
+ import type { CredentialProviders, SeatModelConfig } from "./config.ts";
18
+ import {
19
+ missingCredentialPreDispatchFailure,
20
+ postRunMissingCredentialFailure,
21
+ } from "./public-run-credentials.ts";
22
+ import {
23
+ acquireRunWriterLease,
24
+ clearTypedProviderHttpObservation,
25
+ markRunAdmitted,
26
+ markRunRunning,
27
+ markRunTerminal,
28
+ RunWriterLeaseHeldError,
29
+ type RunWriterLease,
30
+ } from "./run-lifecycle.ts";
31
+ import {
32
+ classifyPostAdmissionFailure,
33
+ exitCodeForTerminalOutcome,
34
+ formatTerminalResult,
35
+ inspectJudgeSession,
36
+ presentFailureTerminal,
37
+ presentStructuralRejection,
38
+ resolveAuditedRunnerKnownFailure,
39
+ explicitInternalKnownFailureClassificationInput,
40
+ settleFailureTerminalResult,
41
+ } from "./settlement.ts";
42
+ import type { CliIo } from "./cli-io.ts";
43
+ import type { AdmittedRoleInvocation } from "./invocation.ts";
44
+ import type { TerminalResult } from "./terminal.ts";
45
+
46
+ export type OneShotRunEnv = {
47
+ home: string;
48
+ agentDir: string;
49
+ packageRoot: string;
50
+ cwd: string;
51
+ correlationId?: string;
52
+ piRunner?: ExplicitInternalPiRunner;
53
+ model?: SeatModelConfig;
54
+ engine?: string;
55
+ credentials?: CredentialProviders;
56
+ timeoutMs?: number;
57
+ };
58
+
59
+ /**
60
+ * Role-specific settlement hooks. Lifecycle coordination stays in this module.
61
+ * - Doctor: present only lawful typed outcomes; optional compliance-audit secondary.
62
+ * - Notary: present any settled terminal (accepted + residual incomplete).
63
+ */
64
+ export type OneShotSettlementAdapters<A extends AdmittedRoleInvocation> = {
65
+ trySettle: (admitted: A) => Promise<TerminalResult | undefined>;
66
+ /** Default true. Doctor sets false-filter via predicate. */
67
+ shouldPresentSettled?: (terminal: TerminalResult) => boolean;
68
+ trySettleSecondary?: (admitted: A) => Promise<TerminalResult | undefined>;
69
+ };
70
+
71
+ async function presentControlledFailure<A extends AdmittedRoleInvocation>(
72
+ admitted: A,
73
+ failureInput: {
74
+ timedOut: boolean;
75
+ code: number | null;
76
+ stderr: string;
77
+ thrown?: unknown;
78
+ knownFailure?: ExplicitInternalKnownFailure;
79
+ },
80
+ io: CliIo,
81
+ ): Promise<{
82
+ exitCode: number;
83
+ admitted: A;
84
+ terminal: TerminalResult;
85
+ }> {
86
+ const hasThrown = Object.hasOwn(failureInput, "thrown");
87
+ const session =
88
+ !hasThrown &&
89
+ !failureInput.timedOut &&
90
+ failureInput.knownFailure === undefined
91
+ ? await inspectJudgeSession(admitted.sessionFile)
92
+ : undefined;
93
+ const failure = classifyPostAdmissionFailure({
94
+ timedOut: failureInput.timedOut,
95
+ code: failureInput.code,
96
+ stderr: failureInput.stderr,
97
+ ...(hasThrown ? { thrown: failureInput.thrown } : {}),
98
+ ...explicitInternalKnownFailureClassificationInput(failureInput.knownFailure),
99
+ ...(session === undefined ? {} : { session }),
100
+ });
101
+
102
+ // One-shot roles do not support resume — always terminal.
103
+ await markRunTerminal(admitted.runDirectory).catch(() => undefined);
104
+
105
+ const terminal = await settleFailureTerminalResult(admitted, failure);
106
+ presentFailureTerminal(terminal, io);
107
+ return {
108
+ exitCode: exitCodeForTerminalOutcome(terminal.roleOutcome),
109
+ admitted,
110
+ terminal,
111
+ };
112
+ }
113
+
114
+ function presentSecondaryTerminal(terminal: TerminalResult, io: CliIo): void {
115
+ if (terminal.roleOutcome.kind === "failure") {
116
+ presentFailureTerminal(terminal, io);
117
+ } else {
118
+ io.stdout(formatTerminalResult(terminal));
119
+ }
120
+ }
121
+
122
+ async function dispatchAdmittedOneShotRole<A extends AdmittedRoleInvocation>(input: {
123
+ admitted: A;
124
+ env: OneShotRunEnv;
125
+ io: CliIo;
126
+ extraArgs: string[];
127
+ lease: RunWriterLease;
128
+ effectiveEngine?: string;
129
+ adapters: OneShotSettlementAdapters<A>;
130
+ }): Promise<{
131
+ exitCode: number;
132
+ admitted: A;
133
+ terminal?: TerminalResult;
134
+ }> {
135
+ const { admitted, env, io, extraArgs, lease, effectiveEngine, adapters } = input;
136
+ const shouldPresent =
137
+ adapters.shouldPresentSettled ?? ((_terminal: TerminalResult) => true);
138
+ try {
139
+ const missingCredential = missingCredentialPreDispatchFailure(
140
+ env.model,
141
+ env.credentials,
142
+ );
143
+ if (missingCredential !== undefined) {
144
+ return await presentControlledFailure(admitted, missingCredential, io);
145
+ }
146
+ await markRunRunning(admitted.runDirectory, env.model, effectiveEngine);
147
+ await clearTypedProviderHttpObservation(admitted.runDirectory);
148
+
149
+ const childEnv: NodeJS.ProcessEnv = {
150
+ ...process.env,
151
+ HOME: env.home,
152
+ PI_CODING_AGENT_DIR: env.agentDir,
153
+ AK_ROLE_RUN_DIR: admitted.runDirectory,
154
+ };
155
+ applyEngineChildEnv(childEnv, env.engine);
156
+ if (env.correlationId !== undefined && env.correlationId.trim() !== "") {
157
+ childEnv.AK_CORRELATION_ID = env.correlationId;
158
+ }
159
+
160
+ let result: ExplicitInternalPiResult;
161
+ try {
162
+ result = await runExplicitInternalActivation({
163
+ packageRoot: env.packageRoot,
164
+ extraArgs,
165
+ cwd: admitted.projectRoot,
166
+ home: env.home,
167
+ agentDir: env.agentDir,
168
+ env: childEnv,
169
+ timeoutMs: env.timeoutMs,
170
+ ...(env.piRunner === undefined ? {} : { runner: env.piRunner }),
171
+ });
172
+ } catch (error) {
173
+ return await presentControlledFailure(
174
+ admitted,
175
+ {
176
+ timedOut: false,
177
+ code: null,
178
+ stderr: "",
179
+ thrown: error,
180
+ },
181
+ io,
182
+ );
183
+ }
184
+
185
+ try {
186
+ await writeFile(
187
+ join(admitted.runDirectory, "stderr.log"),
188
+ result.stderr,
189
+ "utf8",
190
+ );
191
+ } catch {
192
+ // continue to lawful / controlled-failure settlement
193
+ }
194
+
195
+ let settled: TerminalResult | undefined;
196
+ try {
197
+ settled = await adapters.trySettle(admitted);
198
+ } catch (error) {
199
+ return await presentControlledFailure(
200
+ admitted,
201
+ {
202
+ timedOut: false,
203
+ code: result.code,
204
+ stderr: result.stderr,
205
+ thrown: error,
206
+ },
207
+ io,
208
+ );
209
+ }
210
+ if (settled !== undefined && shouldPresent(settled)) {
211
+ await markRunTerminal(admitted.runDirectory).catch(() => undefined);
212
+ io.stdout(formatTerminalResult(settled));
213
+ return {
214
+ exitCode: exitCodeForTerminalOutcome(settled.roleOutcome),
215
+ admitted,
216
+ terminal: settled,
217
+ };
218
+ }
219
+
220
+ if (adapters.trySettleSecondary !== undefined) {
221
+ const secondary = await adapters.trySettleSecondary(admitted);
222
+ if (secondary !== undefined) {
223
+ await markRunTerminal(admitted.runDirectory).catch(() => undefined);
224
+ presentSecondaryTerminal(secondary, io);
225
+ return {
226
+ exitCode: exitCodeForTerminalOutcome(secondary.roleOutcome),
227
+ admitted,
228
+ terminal: secondary,
229
+ };
230
+ }
231
+ }
232
+
233
+ const credentialFailure = postRunMissingCredentialFailure(
234
+ result,
235
+ env.model,
236
+ env.credentials,
237
+ );
238
+ const knownFailure = await resolveAuditedRunnerKnownFailure({
239
+ runner: result.knownFailure,
240
+ sessionFile: admitted.sessionFile,
241
+ credential: credentialFailure,
242
+ runDirectory: admitted.runDirectory,
243
+ });
244
+ return await presentControlledFailure(
245
+ admitted,
246
+ {
247
+ timedOut: result.timedOut,
248
+ code: result.code,
249
+ stderr: result.stderr,
250
+ ...(knownFailure === undefined ? {} : { knownFailure }),
251
+ },
252
+ io,
253
+ );
254
+ } finally {
255
+ await lease.release();
256
+ }
257
+ }
258
+
259
+ /**
260
+ * Shared post-admission one-shot path: durable admitted mark, writer lease,
261
+ * then Doctor-isomorphic dispatch. Role runners call this after role-specific admit.
262
+ */
263
+ export async function runAdmittedOneShotRole<A extends AdmittedRoleInvocation>(input: {
264
+ admitted: A;
265
+ env: OneShotRunEnv;
266
+ io: CliIo;
267
+ extraArgs: string[];
268
+ adapters: OneShotSettlementAdapters<A>;
269
+ effectiveEngine?: string;
270
+ }): Promise<{
271
+ exitCode: number;
272
+ admitted: A;
273
+ terminal?: TerminalResult;
274
+ }> {
275
+ const { admitted, env, io, extraArgs, adapters, effectiveEngine } = input;
276
+ await markRunAdmitted(admitted);
277
+
278
+ let lease: RunWriterLease;
279
+ try {
280
+ lease = await acquireRunWriterLease(admitted.runDirectory, (diagnostic) =>
281
+ io.stderr(diagnostic),
282
+ );
283
+ } catch (error) {
284
+ if (error instanceof RunWriterLeaseHeldError) {
285
+ presentStructuralRejection(error, io);
286
+ return { exitCode: 2, admitted };
287
+ }
288
+ throw error;
289
+ }
290
+
291
+ return await dispatchAdmittedOneShotRole({
292
+ admitted,
293
+ env,
294
+ io,
295
+ extraArgs,
296
+ lease,
297
+ adapters,
298
+ ...(effectiveEngine === undefined ? {} : { effectiveEngine }),
299
+ });
300
+ }
@@ -28,6 +28,7 @@ export type OptionOwner =
28
28
  | "collector"
29
29
  | "doctor"
30
30
  | "merger"
31
+ | "notary"
31
32
  | "analyst";
32
33
 
33
34
  /**
@@ -537,6 +538,24 @@ const DOCTOR_OPTIONS = [
537
538
  },
538
539
  ] as const satisfies readonly PublicOptionDefinition[];
539
540
 
541
+ const NOTARY_OPTIONS = [
542
+ bindOwner("notary", SHARED_PROJECT_SEMANTICS),
543
+ {
544
+ id: "source-run",
545
+ owner: "notary",
546
+ canonical: "--source-run",
547
+ aliases: [],
548
+ valueMetavar: "runId@role|path",
549
+ required: true,
550
+ repeatable: false,
551
+ form: "option",
552
+ description: {
553
+ en: "Required source run locator (runId@role under the book home, or path to that run directory). Zero prompt/attachment projection.",
554
+ zh: "必填源 run 定位符(簿内 runId@role,或该 run 目录路径)。零 prompt/附件投影。",
555
+ },
556
+ },
557
+ ] as const satisfies readonly PublicOptionDefinition[];
558
+
540
559
  const MERGER_OPTIONS = [
541
560
  // Merger project face differs: requires an in-progress ordinary merge root.
542
561
  {
@@ -701,6 +720,7 @@ export const PUBLIC_OPTION_TABLE = {
701
720
  collector: COLLECTOR_OPTIONS,
702
721
  doctor: DOCTOR_OPTIONS,
703
722
  merger: MERGER_OPTIONS,
723
+ notary: NOTARY_OPTIONS,
704
724
  analyst: ANALYST_OPTIONS,
705
725
  } as const satisfies Record<OptionOwner, readonly PublicOptionDefinition[]>;
706
726
 
@@ -715,6 +735,7 @@ export const PUBLIC_ROLE_OPTION_OWNERS = [
715
735
  "collector",
716
736
  "doctor",
717
737
  "merger",
738
+ "notary",
718
739
  "analyst",
719
740
  ] as const satisfies readonly PublicRoleOptionOwner[];
720
741
 
@@ -1068,6 +1089,14 @@ const ROLE_COMMAND_HELP = {
1068
1089
  'ak-role merger --project /path/to/worktree "Reconcile the active merge."',
1069
1090
  ],
1070
1091
  },
1092
+ notary: {
1093
+ command: "notary",
1094
+ summary: "Direct Notary document check (quote fidelity + ticket alignment); zero prompt/attachment.",
1095
+ usage: ["ak-role notary --source-run <runId@role|path> [options]"],
1096
+ examples: [
1097
+ "ak-role notary --source-run 01a034f1-75bf-71a6-bcf5-d1299145b1a5@judge",
1098
+ ],
1099
+ },
1071
1100
  analyst: {
1072
1101
  command: "analyst",
1073
1102
  summary: "Deterministic analysis seat (issue / sweep / cohort).",
@@ -86,6 +86,10 @@ const STARTUP_CANDIDATES: Record<PublicConfigurableSeat, readonly ModelRef[]> =
86
86
  { provider: "openai-codex", model: "gpt-5.6-luna", thinking: "high" },
87
87
  { provider: "xai", model: "grok-4.5", thinking: "high" },
88
88
  ],
89
+ notary: [
90
+ { provider: "openai-codex", model: "gpt-5.6-luna", thinking: "high" },
91
+ { provider: "xai", model: "grok-4.5", thinking: "high" },
92
+ ],
89
93
  navigator: [
90
94
  { provider: "openai-codex", model: "gpt-5.6-luna", thinking: "medium" },
91
95
  { provider: "xai", model: "grok-4.5", thinking: "high" },
@@ -67,7 +67,8 @@ export type RoleRunRecord = {
67
67
  | "collector"
68
68
  | "doctor"
69
69
  | "reviewer"
70
- | "merger";
70
+ | "merger"
71
+ | "notary";
71
72
  readonly state: RoleRunState;
72
73
  readonly bookKey: string;
73
74
  readonly projectRoot: string;
@@ -161,7 +162,8 @@ export async function readRoleRunState(
161
162
  record.role !== "collector" &&
162
163
  record.role !== "doctor" &&
163
164
  record.role !== "reviewer" &&
164
- record.role !== "merger"
165
+ record.role !== "merger" &&
166
+ record.role !== "notary"
165
167
  ) {
166
168
  return undefined;
167
169
  }
@@ -995,6 +997,7 @@ export async function peekRoleRunRole(
995
997
  | "doctor"
996
998
  | "reviewer"
997
999
  | "merger"
1000
+ | "notary"
998
1001
  | undefined
999
1002
  > {
1000
1003
  const runDirectory = await findRunDirectoryById(home, runId);
@@ -74,6 +74,12 @@ import {
74
74
  validateMergerOutput,
75
75
  type MergerOutput,
76
76
  } from "../merger-contracts.ts";
77
+ import {
78
+ NOTARY_OUTPUT_TOOL_NAME,
79
+ notaryDecisiveFacts,
80
+ validateRecordedNotaryOutput,
81
+ type NotaryOutput,
82
+ } from "../notary-contracts.ts";
77
83
  import {
78
84
  observePackagedMethodSkillInvocation,
79
85
  type ObservedPackagedMethodSkillInvocation,
@@ -98,6 +104,7 @@ import {
98
104
  type AdmittedFixerInvocation,
99
105
  type AdmittedJudgeInvocation,
100
106
  type AdmittedMergerInvocation,
107
+ type AdmittedNotaryInvocation,
101
108
  type AdmittedReviewerInvocation,
102
109
  type AdmittedRoleInvocation,
103
110
  } from "./invocation.ts";
@@ -3206,6 +3213,100 @@ export async function trySettleDoctorTerminalResult(
3206
3213
  return settleLawfulDoctorTerminalResult(admitted);
3207
3214
  }
3208
3215
 
3216
+ /** Lawful Notary accepted outcome (pass/bounce/incomplete-with-reason). */
3217
+ export type LawfulNotaryRoleOutcome = {
3218
+ kind: "accepted";
3219
+ role: "notary";
3220
+ status: string;
3221
+ decisiveFacts: Readonly<Record<string, unknown>>;
3222
+ };
3223
+
3224
+ export function extractNotaryRoleOutcome(
3225
+ entries: readonly SessionEntry[],
3226
+ ): { outcome: LawfulNotaryRoleOutcome; output: NotaryOutput } | undefined {
3227
+ if (!isReceiptSettlementBindingClear(entries)) return undefined;
3228
+ for (let i = entries.length - 1; i >= 0; i -= 1) {
3229
+ const entry = entries[i];
3230
+ if (entry?.type !== "message") continue;
3231
+ const message = entry.message;
3232
+ if (message?.role !== "toolResult") continue;
3233
+ if (message.toolName !== NOTARY_OUTPUT_TOOL_NAME) continue;
3234
+ if (!isAcceptedPackagedRoleTerminalResult(message)) continue;
3235
+ try {
3236
+ const output = validateRecordedNotaryOutput(message.details);
3237
+ const outcome: LawfulNotaryRoleOutcome = {
3238
+ kind: "accepted",
3239
+ role: "notary",
3240
+ status: String(output.status),
3241
+ decisiveFacts: notaryDecisiveFacts(output),
3242
+ };
3243
+ return { output, outcome };
3244
+ } catch {
3245
+ continue;
3246
+ }
3247
+ }
3248
+ return undefined;
3249
+ }
3250
+
3251
+ async function settleLawfulNotaryTerminalResult(
3252
+ admitted: AdmittedNotaryInvocation,
3253
+ ): Promise<TerminalResult | undefined> {
3254
+ const entries = await readLawfulSettlementEntries(admitted);
3255
+ if (entries === undefined) return undefined;
3256
+ const extracted = extractNotaryRoleOutcome(entries);
3257
+ if (extracted === undefined) {
3258
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
3259
+ const message = entries[index]?.message;
3260
+ if (message?.role !== "toolResult") continue;
3261
+ const residual = boundErroredToolCandidate(
3262
+ entries,
3263
+ index,
3264
+ message,
3265
+ NOTARY_OUTPUT_TOOL_NAME,
3266
+ );
3267
+ if (residual === undefined) continue;
3268
+ return {
3269
+ roleOutcome: buildResidualIncompleteTerminalOutcome({
3270
+ role: "notary",
3271
+ candidate: residual.candidate,
3272
+ diagnostic: residual.diagnostic,
3273
+ }),
3274
+ navigator: extractNavigatorFact(entries),
3275
+ artifacts: [],
3276
+ runId: admitted.runId,
3277
+ };
3278
+ }
3279
+ return undefined;
3280
+ }
3281
+ const navigator = extractNavigatorFact(entries);
3282
+ return {
3283
+ roleOutcome: extracted.outcome,
3284
+ navigator,
3285
+ artifacts: [],
3286
+ runId: admitted.runId,
3287
+ };
3288
+ }
3289
+
3290
+ /** Settle a lawful Notary Terminal from the admitted session. */
3291
+ export async function settleNotaryTerminalResult(
3292
+ admitted: AdmittedNotaryInvocation,
3293
+ ): Promise<TerminalResult> {
3294
+ const settled = await settleLawfulNotaryTerminalResult(admitted);
3295
+ if (settled === undefined) {
3296
+ throw new Error(
3297
+ "Notary Role run completed without a lawful typed terminal result",
3298
+ );
3299
+ }
3300
+ return settled;
3301
+ }
3302
+
3303
+ /** Try to settle a lawful Notary Terminal; undefined only for genuine absence. */
3304
+ export async function trySettleNotaryTerminalResult(
3305
+ admitted: AdmittedNotaryInvocation,
3306
+ ): Promise<TerminalResult | undefined> {
3307
+ return settleLawfulNotaryTerminalResult(admitted);
3308
+ }
3309
+
3209
3310
  /** Try to settle a lawful Coder Terminal; undefined only for genuine absence. */
3210
3311
  export async function trySettleCoderTerminalResult(
3211
3312
  admitted: AdmittedCoderInvocation,
@@ -38,11 +38,12 @@ export type TerminalRoleName =
38
38
  | "collector"
39
39
  | "doctor"
40
40
  | "reviewer"
41
- | "merger";
41
+ | "merger"
42
+ | "notary";
42
43
 
43
44
  export type ResidualIncompleteTerminalOutcome = {
44
45
  kind: "incomplete";
45
- role: "merger" | "collector";
46
+ role: "merger" | "collector" | "notary";
46
47
  status: "incomplete";
47
48
  decision: "no-usable-result";
48
49
  candidate: unknown;
@@ -130,7 +131,7 @@ export function exitCodeForTerminalOutcome(
130
131
  }
131
132
 
132
133
  export function buildResidualIncompleteTerminalOutcome(input: {
133
- role: "merger" | "collector";
134
+ role: "merger" | "collector" | "notary";
134
135
  candidate: unknown;
135
136
  diagnostic: string;
136
137
  }): ResidualIncompleteTerminalOutcome {