@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.
- package/README.md +7 -0
- package/README.zh-CN.md +7 -0
- package/dist/notary-contracts.js +96 -0
- package/dist/package-contracts/terminating-tools.js +10 -2
- package/dist/packaged-role-registry.js +3 -1
- package/dist/public-cli/main.js +3345 -2778
- package/extensions/role-runtime.ts +11 -1
- package/package.json +1 -1
- package/souls/judge-output-guide.md +4 -1
- package/src/analyst-gate-cycles-read.ts +7 -2
- package/src/notary-contracts.ts +134 -0
- package/src/notary-role.ts +148 -0
- package/src/notary-source-run.ts +168 -0
- package/src/package-contracts/terminating-tools.ts +13 -2
- package/src/packaged-role-registry.ts +2 -0
- package/src/public-cli/cli.ts +51 -0
- package/src/public-cli/doctor-run.ts +18 -250
- package/src/public-cli/invocation.ts +170 -0
- package/src/public-cli/notary-run.ts +117 -0
- package/src/public-cli/one-shot-dispatch.ts +300 -0
- package/src/public-cli/option-definitions.ts +29 -0
- package/src/public-cli/registry.ts +4 -0
- package/src/public-cli/run-lifecycle.ts +5 -2
- package/src/public-cli/settlement.ts +101 -0
- package/src/public-cli/terminal.ts +4 -3
- package/src/role-runtime.ts +16 -0
- package/src/session-opening-materials.ts +1 -0
|
@@ -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"
|
|
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
|
@@ -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.
|
|
18
|
-
*
|
|
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:
|
|
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"];
|