@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.
@@ -3,6 +3,7 @@ import { fileURLToPath } from "node:url";
3
3
  import { resolve } from "node:path";
4
4
 
5
5
  import { loadDoctorCase } from "../src/doctor-evidence.ts";
6
+ import { loadNotarySourceRunLocator } from "../src/notary-source-run.ts";
6
7
  import { loadAdmittedJudgeRequest } from "../src/public-cli/invocation.ts";
7
8
 
8
9
  import {
@@ -145,7 +146,9 @@ export async function loadNavigatorWorkContext(
145
146
  options: { context: ExtensionContext; role: string },
146
147
  ): Promise<{ subjectKey: string; subject: string; authority: string; subjectProvenance: NavigatorSubjectProvenance }> {
147
148
  const reference = navigatorInputReference(pi as ExtensionAPI, options.role);
148
- const input = reference === undefined || options.role === "doctor" ? undefined : await readFile(reference, "utf8");
149
+ const input = reference === undefined || options.role === "doctor" || options.role === "notary"
150
+ ? undefined
151
+ : await readFile(reference, "utf8");
149
152
  const subjectRoot = subjectPath(reference ?? options.context.sessionManager.getSessionDir(), options.context.cwd);
150
153
  let subjectKey = reference === undefined
151
154
  ? subjectRoot
@@ -157,6 +160,11 @@ export async function loadNavigatorWorkContext(
157
160
  subject = JSON.stringify({ identity: patient.identity, cost: patient.cost });
158
161
  subjectProvenance = "role_input";
159
162
  }
163
+ if (options.role === "notary" && reference !== undefined) {
164
+ const locator = await loadNotarySourceRunLocator(reference);
165
+ subject = JSON.stringify({ sourceRun: locator });
166
+ subjectProvenance = "role_input";
167
+ }
160
168
  // Public ak-role run: admitted request is the typed Navigator work-context source.
161
169
  // Classification failure here stays source=context (distinct from model/session/transport).
162
170
  const publicRunDir = process.env.AK_ROLE_RUN_DIR;
@@ -257,6 +265,8 @@ export default function roleRuntime(pi: ExtensionAPI): void {
257
265
  loadDoctorSoul: () => loadMainRoleSessionMaterials("doctor"),
258
266
  loadDoctorCase,
259
267
  auditDoctorCompliance: createPiDoctorAuditor(),
268
+ loadNotarySoul: () => loadMainRoleSessionMaterials("notary"),
269
+ loadNotarySourceRun: loadNotarySourceRunLocator,
260
270
  loadNavigatorWorkContext: (options) => loadNavigatorWorkContext(pi, options),
261
271
  createNavigatorAttendance: (options) => {
262
272
  return createNavigatorAttendance({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akagilnc/pi-workflow-roles",
3
- "version": "0.1.2296",
3
+ "version": "0.1.2310",
4
4
  "description": "Soul-bound workflow roles for Pi",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -3,5 +3,8 @@
3
3
  拟判正文应自带:
4
4
 
5
5
  - 凡称陛下拍定:附可追溯真源。
6
- - 承重主张(引用的法源、声称的代码事实):附原文并列引用,开卷可核。
6
+ - 承重主张(引用的法源、声称的代码事实):给指针(路径/行号/runId),或从本轮已打开的源头逐字节复制引文——禁止凭记忆改写、缩写或转述;开卷核对相符即成立。
7
+ - 每句「已核/已对齐」:对应本轮工具轨迹中可见的打开动作;写不出对应动作的句子删去。
8
+ - 因误引/未核被打回后重交:被点名的每个事实重新打开源头再引,不许只改措辞重交。
9
+ - 票面或 OWNER 点名的交付项:逐项列清单处置后再交;漏一项即不完整。
7
10
  - 对票面的裁决:先答它解决什么真问题;讲不清的,打回签发方,不得结清。
@@ -14,8 +14,10 @@
14
14
  * An accepted gate terminating receipt (isError:false pair on dispatch/officer
15
15
  * tool) whose required typed facts are unusable — status, dispatch officer, or
16
16
  * first/last span missing/unknown/unparseable/inverted — also fails loudly via
17
- * the same throw→ledger `auditor-roles` unreadable seam. True non-gate volumes
18
- * (soul-audit noise, etc.) stay omitted from pairing.
17
+ * the same throw→ledger `auditor-roles` unreadable seam. Lawful Gatekeeper
18
+ * typed `incomplete` is a recognizable terminal (omit from pairing, leg stays
19
+ * readable) — only unknown/non-contract dispatch status stays loud. True
20
+ * non-gate volumes (soul-audit noise, etc.) stay omitted from pairing.
19
21
  */
20
22
  import { readdir } from "node:fs/promises";
21
23
  import { join } from "node:path";
@@ -202,6 +204,9 @@ async function classifyAuditorVolume(
202
204
  const findingsCount = Array.isArray(findings) ? findings.length : 0;
203
205
 
204
206
  if (DISPATCH_TOOLS.has(call.toolName)) {
207
+ // Gatekeeper contract terminals: dispatch | incomplete. Incomplete is a
208
+ // lawful zero-pair terminal (#434-436 / #458) — never wash the parent leg.
209
+ if (status === "incomplete") return undefined;
205
210
  if (status !== "dispatch") {
206
211
  throw new Error(
207
212
  `accepted dispatch receipt has non-dispatch status ${JSON.stringify(status)} in ${filePath}`,
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Public Notary (符宝郎) terminating receipt contracts.
3
+ * Lawful explicit releases: pass | bounce | incomplete(with non-empty reason).
4
+ * Residual incomplete (no explicit release) is projected by public settlement, not here.
5
+ */
6
+ import { Type } from "typebox";
7
+
8
+ import {
9
+ readActivationEngineLaborFallbackField,
10
+ seatFallbackBaseStatus,
11
+ seatFallbackStatusHasLawfulEvidence,
12
+ withEngineLaborFallbackField,
13
+ type WithEngineLaborFallback,
14
+ } from "./engine-labor-fallback.ts";
15
+ import { openToolObject } from "./open-tool-schema.ts";
16
+
17
+ export const NOTARY_OUTPUT_TOOL_NAME = "ak_notary_output";
18
+ export const NOTARY_ACCEPTED_TEXT = "Notary output accepted";
19
+ export const NOTARY_SOURCE_RUN_FLAG = {
20
+ name: "ak-notary-source-run",
21
+ definition: {
22
+ description: "Absolute source run directory bound for Notary self-fetch",
23
+ type: "string" as const,
24
+ },
25
+ } as const;
26
+
27
+ /** Package-owned kickoff only — callers supply zero prompt bytes (ADR 0067 / #448). */
28
+ export const NOTARY_FIXED_KICKOFF =
29
+ "Notary review. Bound source-run locator is on the session materials; fetch authoritative ticket, git, and dossier evidence yourself; submit one typed decision.";
30
+
31
+ export const notaryOutputSchema = openToolObject(
32
+ Type.Object({
33
+ status: Type.Unknown({
34
+ description: "pass | bounce | incomplete — guidance, not a schema gate.",
35
+ }),
36
+ findings: Type.Unknown({
37
+ description: "string[] findings retained with pass or bounce.",
38
+ }),
39
+ reason: Type.Unknown({
40
+ description: "Why the notary decision is incomplete.",
41
+ }),
42
+ }),
43
+ );
44
+
45
+ export type NotarySourceRunLocator = {
46
+ readonly runDirectory: string;
47
+ readonly runId: string;
48
+ readonly role: string;
49
+ };
50
+
51
+ type NotaryOutputClean =
52
+ | { readonly status: "pass"; readonly findings: readonly string[] }
53
+ | {
54
+ readonly status: "bounce";
55
+ readonly disposition: "rewrite";
56
+ readonly findings: readonly string[];
57
+ }
58
+ | { readonly status: "incomplete"; readonly reason: string };
59
+
60
+ /** Clean submission or seat-fallback tainted accepted receipt (ADR 0071). */
61
+ export type NotaryOutput =
62
+ | NotaryOutputClean
63
+ | WithEngineLaborFallback<NotaryOutputClean>;
64
+
65
+ function isRecord(value: unknown): value is Record<string, unknown> {
66
+ return typeof value === "object" && value !== null && !Array.isArray(value);
67
+ }
68
+
69
+ function asStringArray(value: unknown): readonly string[] {
70
+ if (!Array.isArray(value)) return [];
71
+ return value.filter((item): item is string => typeof item === "string");
72
+ }
73
+
74
+ /**
75
+ * Project one explicit Notary release. Throws when there is no lawful explicit
76
+ * pass / bounce / incomplete(reason) — callers map that to residual incomplete.
77
+ */
78
+ export function validateNotaryOutput(value: unknown): NotaryOutput {
79
+ if (!isRecord(value)) {
80
+ throw new Error("Notary output has no recognized execution discriminator");
81
+ }
82
+ const statusRaw = typeof value.status === "string" ? value.status : undefined;
83
+ if (statusRaw === undefined || !seatFallbackStatusHasLawfulEvidence(statusRaw, value)) {
84
+ throw new Error("Notary output has no recognized execution discriminator");
85
+ }
86
+ const status = seatFallbackBaseStatus(statusRaw);
87
+ if (status === "incomplete") {
88
+ const reason = value.reason;
89
+ if (typeof reason !== "string" || reason.trim() === "") {
90
+ throw new Error("Notary incomplete requires a non-empty reason");
91
+ }
92
+ return structuredClone(value) as NotaryOutput;
93
+ }
94
+ if (status === "bounce") {
95
+ const clone = structuredClone(value) as Record<string, unknown>;
96
+ if (clone.disposition === undefined) clone.disposition = "rewrite";
97
+ if (!Array.isArray(clone.findings)) clone.findings = asStringArray(clone.findings);
98
+ return clone as NotaryOutput;
99
+ }
100
+ if (status === "pass") {
101
+ const clone = structuredClone(value) as Record<string, unknown>;
102
+ if (!Array.isArray(clone.findings)) clone.findings = asStringArray(clone.findings);
103
+ return clone as NotaryOutput;
104
+ }
105
+ throw new Error("Notary output has no recognized execution discriminator");
106
+ }
107
+
108
+ /** Shape check for recorded accepted details (may include engineLaborFallback). */
109
+ export function validateRecordedNotaryOutput(value: unknown): NotaryOutput {
110
+ return validateNotaryOutput(value);
111
+ }
112
+
113
+ export function withNotaryAcceptedDetails(output: NotaryOutput): NotaryOutput {
114
+ return withEngineLaborFallbackField(
115
+ output,
116
+ readActivationEngineLaborFallbackField(),
117
+ ) as NotaryOutput;
118
+ }
119
+
120
+ export function notaryDecisiveFacts(output: NotaryOutput): Record<string, unknown> {
121
+ const status = seatFallbackBaseStatus(String(output.status));
122
+ const facts: Record<string, unknown> = { status, officer: "notary" };
123
+ if (status === "pass" || status === "bounce") {
124
+ const findings = (output as { findings?: unknown }).findings;
125
+ facts.findingsCount = Array.isArray(findings) ? findings.length : 0;
126
+ }
127
+ if (status === "bounce") {
128
+ facts.disposition = "rewrite";
129
+ }
130
+ if (status === "incomplete") {
131
+ facts.reason = (output as { reason: string }).reason;
132
+ }
133
+ return facts;
134
+ }
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Public Notary role runtime — direct officer seat (not through Gatekeeper province).
3
+ * Caller supplies only a source-run locator; Notary self-fetches authoritative materials.
4
+ */
5
+ import type {
6
+ AgentToolResult,
7
+ ExtensionAPI,
8
+ ExtensionContext,
9
+ } from "@earendil-works/pi-coding-agent";
10
+
11
+ import {
12
+ NOTARY_ACCEPTED_TEXT,
13
+ NOTARY_OUTPUT_TOOL_NAME,
14
+ NOTARY_SOURCE_RUN_FLAG,
15
+ notaryOutputSchema,
16
+ validateNotaryOutput,
17
+ withNotaryAcceptedDetails,
18
+ type NotarySourceRunLocator,
19
+ } from "./notary-contracts.ts";
20
+
21
+ export {
22
+ NOTARY_ACCEPTED_TEXT,
23
+ NOTARY_OUTPUT_TOOL_NAME,
24
+ NOTARY_SOURCE_RUN_FLAG,
25
+ };
26
+
27
+ export type NotaryRoleDependencies = {
28
+ loadSoul(): Promise<string>;
29
+ loadSourceRunLocator(path: string): Promise<NotarySourceRunLocator>;
30
+ };
31
+
32
+ export type NotaryRoleHostActions = {
33
+ failInfrastructure(
34
+ error: unknown,
35
+ ctx: ExtensionContext,
36
+ toolCallId?: string,
37
+ ): never;
38
+ };
39
+
40
+ function requireSingletonSubmissionCall(
41
+ toolCallId: string,
42
+ ctx: ExtensionContext,
43
+ ): void {
44
+ const leaf = ctx.sessionManager.getLeafEntry();
45
+ if (leaf?.type !== "message" || leaf.message.role !== "assistant") {
46
+ throw new Error("Notary output must be the sole final tool call");
47
+ }
48
+ const calls = leaf.message.content.filter((part) => part.type === "toolCall");
49
+ if (
50
+ calls.length !== 1 ||
51
+ calls[0]?.id !== toolCallId ||
52
+ calls[0]?.name !== NOTARY_OUTPUT_TOOL_NAME
53
+ ) {
54
+ throw new Error("Notary output must be the sole final tool call");
55
+ }
56
+ }
57
+
58
+ export function createNotaryRoleRuntime(
59
+ pi: ExtensionAPI,
60
+ dependencies: NotaryRoleDependencies,
61
+ host: NotaryRoleHostActions,
62
+ ) {
63
+ let activation:
64
+ | { soul: string; sourceRun: NotarySourceRunLocator }
65
+ | undefined;
66
+ let registered = false;
67
+ pi.registerFlag(
68
+ NOTARY_SOURCE_RUN_FLAG.name,
69
+ NOTARY_SOURCE_RUN_FLAG.definition,
70
+ );
71
+
72
+ return {
73
+ async activate() {
74
+ const path = pi.getFlag(NOTARY_SOURCE_RUN_FLAG.name);
75
+ if (typeof path !== "string" || path.trim() === "") {
76
+ throw new Error("Notary requires --ak-notary-source-run");
77
+ }
78
+ const soul = (await dependencies.loadSoul()).trim();
79
+ if (soul.length === 0) throw new Error("Notary soul is empty");
80
+ const sourceRun = await dependencies.loadSourceRunLocator(path);
81
+ activation = { soul, sourceRun };
82
+
83
+ if (!registered) {
84
+ registered = true;
85
+ pi.registerTool({
86
+ name: NOTARY_OUTPUT_TOOL_NAME,
87
+ label: "Notary Output",
88
+ description:
89
+ "Submit one typed pass, bounce, or incomplete decision on quote fidelity and ticket alignment.",
90
+ promptSnippet: "Submit the Notary decision",
91
+ promptGuidelines: [
92
+ `Use ${NOTARY_OUTPUT_TOOL_NAME} as the sole final action.`,
93
+ "Fetch ticket, git, and dossier evidence yourself from the bound source-run locator.",
94
+ ],
95
+ parameters: notaryOutputSchema,
96
+ async execute(
97
+ toolCallId,
98
+ parameters,
99
+ _signal,
100
+ _onUpdate,
101
+ ctx,
102
+ ): Promise<AgentToolResult<unknown>> {
103
+ if (activation === undefined) {
104
+ throw new Error("Notary is not activated");
105
+ }
106
+ let output;
107
+ try {
108
+ requireSingletonSubmissionCall(toolCallId, ctx);
109
+ output = validateNotaryOutput(parameters);
110
+ } catch (error) {
111
+ // Non-explicit release stays a rejected terminating call so public
112
+ // settlement can project residual incomplete (layer ②).
113
+ throw error instanceof Error
114
+ ? error
115
+ : new Error(String(error));
116
+ }
117
+ const acceptedDetails = withNotaryAcceptedDetails(output);
118
+ return {
119
+ content: [{ type: "text" as const, text: NOTARY_ACCEPTED_TEXT }],
120
+ details: acceptedDetails,
121
+ terminate: true as const,
122
+ };
123
+ },
124
+ });
125
+ pi.on("before_agent_start", (event) => {
126
+ if (activation === undefined) {
127
+ throw new Error("Notary is not activated");
128
+ }
129
+ // Locator only — never preload ticket/diff/draft body (self-fetch contract).
130
+ const bound = {
131
+ sourceRun: activation.sourceRun,
132
+ };
133
+ return {
134
+ systemPrompt: `${event.systemPrompt}\n\n<notary_soul>\n${activation.soul}\n</notary_soul>\n\n<notary_source_run>\n${JSON.stringify(bound)}\n</notary_source_run>`,
135
+ };
136
+ });
137
+ }
138
+
139
+ // Evidence role: keep Pi default tools + notary output (ADR 0064 unrestricted).
140
+ const all = pi.getAllTools().map((tool) => tool.name);
141
+ if (all.filter((name) => name === NOTARY_OUTPUT_TOOL_NAME).length !== 1) {
142
+ throw new Error(
143
+ `Notary required tool collision or missing: ${NOTARY_OUTPUT_TOOL_NAME}`,
144
+ );
145
+ }
146
+ },
147
+ };
148
+ }
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Resolve and validate the public Notary source-run locator.
3
+ * Only machine-ledger retained runs are authoritative: no project-tree projection,
4
+ * no attachment substitute. Caller supplies a run/case pointer; Notary self-fetches.
5
+ */
6
+ import { dirname, isAbsolute, join, resolve, basename } from "node:path";
7
+ import { lstat, realpath } from "node:fs/promises";
8
+
9
+ import { resolveBookKeyFromGit } from "./activation-ledger-git.ts";
10
+ import {
11
+ activationBookDirectory,
12
+ physicalPathIdentity,
13
+ resolveActivationLedgerHome,
14
+ } from "./activation-ledger-topology.ts";
15
+ import type { NotarySourceRunLocator } from "./notary-contracts.ts";
16
+ import { readRoleRunState } from "./public-cli/run-lifecycle.ts";
17
+
18
+ const RUN_DIR_NAME =
19
+ /^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})@([A-Za-z][A-Za-z0-9_-]*)$/i;
20
+
21
+ export class NotarySourceRunError extends Error {
22
+ constructor(message: string, options?: ErrorOptions) {
23
+ super(message, options);
24
+ this.name = "NotarySourceRunError";
25
+ }
26
+ }
27
+
28
+ function parseRunDirectoryName(name: string): { runId: string; role: string } | undefined {
29
+ const match = RUN_DIR_NAME.exec(name);
30
+ if (match === null) return undefined;
31
+ return { runId: match[1]!, role: match[2]! };
32
+ }
33
+
34
+ async function requireRunDirectory(candidate: string, display: string): Promise<string> {
35
+ let real: string;
36
+ try {
37
+ real = await realpath(candidate);
38
+ } catch (error) {
39
+ throw new NotarySourceRunError(
40
+ `notary --source-run is not a readable run directory: ${display}`,
41
+ { cause: error },
42
+ );
43
+ }
44
+ let stat;
45
+ try {
46
+ stat = await lstat(real);
47
+ } catch (error) {
48
+ throw new NotarySourceRunError(
49
+ `notary --source-run is not a readable run directory: ${display}`,
50
+ { cause: error },
51
+ );
52
+ }
53
+ if (!stat.isDirectory()) {
54
+ throw new NotarySourceRunError(
55
+ `notary --source-run must be a run directory: ${display}`,
56
+ );
57
+ }
58
+ const identity = parseRunDirectoryName(basename(real));
59
+ if (identity === undefined) {
60
+ throw new NotarySourceRunError(
61
+ `notary --source-run must be named <runId>@<role>: ${basename(real)}`,
62
+ );
63
+ }
64
+ return real;
65
+ }
66
+
67
+ /**
68
+ * Resolve `--source-run` to a machine-ledger retained run directory and typed identity.
69
+ * Accepts:
70
+ * - bare `<runId>@<role>` under the project's book runs home
71
+ * - absolute/relative path that realpath's to that same book runs slot
72
+ *
73
+ * Rejects project-tree projections and any path outside the project's ledger book runs.
74
+ * Retained `run-state.json` must match basename identity and book binding.
75
+ */
76
+ export async function resolveNotarySourceRunLocator(options: {
77
+ readonly projectRoot: string;
78
+ readonly sourceRun: string;
79
+ readonly home?: string;
80
+ }): Promise<NotarySourceRunLocator> {
81
+ const raw = options.sourceRun.trim();
82
+ if (raw === "") {
83
+ throw new NotarySourceRunError("notary --source-run requires a run locator");
84
+ }
85
+
86
+ const ledgerHome = resolveActivationLedgerHome(
87
+ options.home === undefined ? undefined : () => options.home!,
88
+ );
89
+ const bookKey = resolveBookKeyFromGit(options.projectRoot);
90
+ const bookRunsRoot = join(activationBookDirectory(ledgerHome, bookKey), "runs");
91
+
92
+ let candidate: string;
93
+ const bare = parseRunDirectoryName(raw);
94
+ if (bare !== undefined && !raw.includes("/") && !raw.includes("\\")) {
95
+ candidate = join(bookRunsRoot, `${bare.runId}@${bare.role}`);
96
+ } else {
97
+ candidate = isAbsolute(raw) ? raw : resolve(options.projectRoot, raw);
98
+ }
99
+
100
+ const real = await requireRunDirectory(candidate, raw);
101
+ const identity = parseRunDirectoryName(basename(real))!;
102
+
103
+ const runsRootIdentity = physicalPathIdentity(bookRunsRoot);
104
+ const parentIdentity = physicalPathIdentity(dirname(real));
105
+ if (parentIdentity !== runsRootIdentity) {
106
+ throw new NotarySourceRunError(
107
+ "notary --source-run must resolve to a retained run under the project machine-ledger book",
108
+ );
109
+ }
110
+
111
+ // Authoritative retained record only — legal role/state via shared reader (DRY #14).
112
+ const runState = await readRoleRunState(real);
113
+ if (runState === undefined) {
114
+ throw new NotarySourceRunError(
115
+ "notary --source-run lacks retained run-state identity",
116
+ );
117
+ }
118
+ if (runState.runId !== identity.runId || runState.role !== identity.role) {
119
+ throw new NotarySourceRunError(
120
+ "notary --source-run retained identity does not match directory name",
121
+ );
122
+ }
123
+ if (runState.bookKey !== bookKey) {
124
+ throw new NotarySourceRunError(
125
+ "notary --source-run retained book binding does not match project case",
126
+ );
127
+ }
128
+ if (physicalPathIdentity(runState.runDirectory) !== physicalPathIdentity(real)) {
129
+ throw new NotarySourceRunError(
130
+ "notary --source-run retained runDirectory does not match locator path",
131
+ );
132
+ }
133
+
134
+ return {
135
+ runDirectory: real,
136
+ runId: identity.runId,
137
+ role: identity.role,
138
+ };
139
+ }
140
+
141
+ /** Internal activation loader: path is the admitted absolute source run directory. */
142
+ export async function loadNotarySourceRunLocator(
143
+ path: string,
144
+ ): Promise<NotarySourceRunLocator> {
145
+ const real = await requireRunDirectory(path, path);
146
+ const identity = parseRunDirectoryName(basename(real))!;
147
+ const runState = await readRoleRunState(real);
148
+ if (runState === undefined) {
149
+ throw new NotarySourceRunError(
150
+ "notary source-run lacks retained run-state identity",
151
+ );
152
+ }
153
+ if (runState.runId !== identity.runId || runState.role !== identity.role) {
154
+ throw new NotarySourceRunError(
155
+ "notary source-run retained identity does not match directory name",
156
+ );
157
+ }
158
+ if (physicalPathIdentity(runState.runDirectory) !== physicalPathIdentity(real)) {
159
+ throw new NotarySourceRunError(
160
+ "notary source-run retained runDirectory does not match locator path",
161
+ );
162
+ }
163
+ return {
164
+ runDirectory: real,
165
+ runId: identity.runId,
166
+ role: identity.role,
167
+ };
168
+ }
@@ -31,6 +31,7 @@ import {
31
31
  } from "../engine-labor-fallback.ts";
32
32
  import { DOCTOR_OUTPUT_TOOL_NAME, validateDoctorSubmissionShape, validateRecordedDoctorOutput, type DoctorOutput, type DoctorSubmission } from "../doctor-contracts.ts";
33
33
  import { MERGER_ACCEPTED_TEXT, MERGER_OUTPUT_TOOL_NAME, validateMergerOutput, type MergerOutput } from "../merger-contracts.ts";
34
+ import { NOTARY_ACCEPTED_TEXT, NOTARY_OUTPUT_TOOL_NAME, validateRecordedNotaryOutput, type NotaryOutput } from "../notary-contracts.ts";
34
35
  import {
35
36
  CODER_ACCEPTED_TEXT,
36
37
  CODER_OUTPUT_TOOL_NAME,
@@ -62,6 +63,7 @@ export {
62
63
  validateDoctorSubmissionShape,
63
64
  validateRecordedDoctorOutput,
64
65
  validateMergerOutput,
66
+ validateRecordedNotaryOutput,
65
67
  };
66
68
  export type {
67
69
  CollectorReceipt,
@@ -72,6 +74,7 @@ export type {
72
74
  DoctorOutput,
73
75
  DoctorSubmission,
74
76
  MergerOutput,
77
+ NotaryOutput,
75
78
  };
76
79
 
77
80
  export const TERMINATING_TOOL_NAMES = [
@@ -82,6 +85,7 @@ export const TERMINATING_TOOL_NAMES = [
82
85
  COLLECTOR_OUTPUT_TOOL,
83
86
  DOCTOR_OUTPUT_TOOL_NAME,
84
87
  MERGER_OUTPUT_TOOL_NAME,
88
+ NOTARY_OUTPUT_TOOL_NAME,
85
89
  ] as const;
86
90
 
87
91
  export type TerminatingToolName = (typeof TERMINATING_TOOL_NAMES)[number];
@@ -92,7 +96,8 @@ export type AcceptedDetails =
92
96
  | JudgeVerdict
93
97
  | CollectorReceipt
94
98
  | DoctorOutput
95
- | MergerOutput;
99
+ | MergerOutput
100
+ | NotaryOutput;
96
101
 
97
102
  export function isTerminatingToolName(
98
103
  name: string,
@@ -116,6 +121,8 @@ export function acceptedTextFor(toolName: TerminatingToolName): string {
116
121
  return "Doctor output accepted";
117
122
  case MERGER_OUTPUT_TOOL_NAME:
118
123
  return MERGER_ACCEPTED_TEXT;
124
+ case NOTARY_OUTPUT_TOOL_NAME:
125
+ return NOTARY_ACCEPTED_TEXT;
119
126
  }
120
127
  }
121
128
 
@@ -161,6 +168,7 @@ export function validateAcceptedDetails(
161
168
  [COLLECTOR_OUTPUT_TOOL]: [],
162
169
  [DOCTOR_OUTPUT_TOOL_NAME]: ["completed", "refused"],
163
170
  [MERGER_OUTPUT_TOOL_NAME]: ["completed", "escalate"],
171
+ [NOTARY_OUTPUT_TOOL_NAME]: ["pass", "bounce", "incomplete"],
164
172
  };
165
173
  const collectorDiscriminator = toolName === COLLECTOR_OUTPUT_TOOL && Array.isArray(candidate?.groups);
166
174
  const baseDiscriminator = typeof discriminator === "string" ? seatFallbackBaseStatus(discriminator) : discriminator;
@@ -194,6 +202,8 @@ export function validateAcceptedDetails(
194
202
  return validateRecordedDoctorOutput(details);
195
203
  case MERGER_OUTPUT_TOOL_NAME:
196
204
  return validateMergerOutput(details);
205
+ case NOTARY_OUTPUT_TOOL_NAME:
206
+ return validateRecordedNotaryOutput(details);
197
207
  }
198
208
  } catch (error) {
199
209
  if (error instanceof Error && error.constructor === Error) throw new AcceptedDetailsContractError(error.message, { cause: error });
@@ -237,7 +247,8 @@ export function acceptedFacts(toolName: TerminatingToolName, details: AcceptedDe
237
247
  case CODER_OUTPUT_TOOL_NAME:
238
248
  case FIXER_OUTPUT_TOOL_NAME:
239
249
  case REVIEWER_OUTPUT_TOOL_NAME:
240
- case DOCTOR_OUTPUT_TOOL_NAME: return { status: (details as { status: string }).status };
250
+ case DOCTOR_OUTPUT_TOOL_NAME:
251
+ case NOTARY_OUTPUT_TOOL_NAME: return { status: (details as { status: string }).status };
241
252
  case JUDGE_OUTPUT_TOOL_NAME: return { status: (details as { judgeStatus: string }).judgeStatus };
242
253
  case MERGER_OUTPUT_TOOL_NAME: {
243
254
  const output = details as unknown as Record<string, unknown>;
@@ -5,6 +5,7 @@ import { REVIEWER_OUTPUT_TOOL_NAME } from "./package-contracts/reviewer-output.t
5
5
  import { CODER_OUTPUT_TOOL_NAME, FIXER_OUTPUT_TOOL_NAME } from "./package-contracts/worker-output.ts";
6
6
  import { DOCTOR_OUTPUT_TOOL_NAME } from "./doctor-contracts.ts";
7
7
  import { MERGER_OUTPUT_TOOL_NAME } from "./merger-contracts.ts";
8
+ import { NOTARY_OUTPUT_TOOL_NAME } from "./notary-contracts.ts";
8
9
 
9
10
  export const PACKAGED_ROLE_REGISTRY = [
10
11
  { role: "judge", phases: [null], outputTool: JUDGE_OUTPUT_TOOL_NAME, inputFlag: undefined, phaseFlag: undefined, activationStage: "load-and-install" },
@@ -14,6 +15,7 @@ export const PACKAGED_ROLE_REGISTRY = [
14
15
  { role: "collector", phases: [null], outputTool: COLLECTOR_OUTPUT_TOOL, inputFlag: "ak-collector-repo", phaseFlag: undefined, activationStage: "load-and-install" },
15
16
  { role: "doctor", phases: [null], outputTool: DOCTOR_OUTPUT_TOOL_NAME, inputFlag: "ak-doctor-case", phaseFlag: undefined, activationStage: "load-and-install" },
16
17
  { role: "merger", phases: [null], outputTool: MERGER_OUTPUT_TOOL_NAME, inputFlag: "ak-merger-input", phaseFlag: undefined, activationStage: "prepare-git-and-install" },
18
+ { role: "notary", phases: [null], outputTool: NOTARY_OUTPUT_TOOL_NAME, inputFlag: "ak-notary-source-run", phaseFlag: undefined, activationStage: "load-and-install" },
17
19
  ] as const;
18
20
 
19
21
  export type PackagedRole = (typeof PACKAGED_ROLE_REGISTRY)[number]["role"];