@akagilnc/pi-workflow-roles 0.1.4977 → 0.1.4989
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/dist/acp-host/production-host.js +460 -462
- package/dist/analyst-gate-cycles-read.js +16 -11
- package/dist/analyst-metric-families/gate-cycles.js +2 -2
- package/dist/audit-escalation.js +19 -4
- package/dist/compliance-transport.js +2 -2
- package/dist/countersign-contracts.js +6 -5
- package/dist/doctor-evidence.js +2 -1
- package/dist/gatekeeper-role.js +22 -43
- package/dist/headless-host/production-host.js +460 -462
- package/dist/inspector-contracts.js +3 -2
- package/dist/migrate-book-topology.js +59 -54
- package/dist/navigator-invocation-identity.js +6 -4
- package/dist/notary-contracts.js +3 -19
- package/dist/package-contracts/auditor-output.js +4 -19
- package/dist/package-contracts/judge-output.js +6 -5
- package/dist/package-contracts/terminating-tools.js +9 -3
- package/dist/packaged-role-registry.js +10 -12
- package/dist/public-cli/countersign-run.js +1 -1
- package/dist/public-cli/main.js +95 -87
- package/dist/public-cli/option-definitions.js +2 -2
- package/dist/public-cli/settlement.js +15 -12
- package/dist/review-submission.js +20 -0
- package/dist/submission-errors.js +2 -2
- package/dist/submission-ledger.js +16 -2
- package/package.json +1 -1
- package/souls/doctor-auditor.md +1 -1
- package/souls/fixer-auditor.md +4 -4
- package/souls/gate-output-guide.md +2 -2
- package/souls/inspector.md +1 -1
- package/souls/judge-auditor.md +2 -2
- package/souls/notary.md +1 -1
- package/souls/quality-law.md +1 -1
- package/src/analyst-gate-cycles-read.ts +17 -9
- package/src/analyst-metric-families/gate-cycles.ts +2 -2
- package/src/audit-escalation.ts +27 -7
- package/src/auditor-role.ts +1 -1
- package/src/compliance-transport.ts +7 -7
- package/src/countersign-contracts.ts +10 -8
- package/src/countersign-role.ts +6 -35
- package/src/doctor-evidence.ts +2 -1
- package/src/doctor-role.ts +1 -2
- package/src/gatekeeper-role.ts +32 -57
- package/src/host-contracts.ts +4 -3
- package/src/inspector-contracts.ts +7 -6
- package/src/inspector-role.ts +3 -20
- package/src/judge-role.ts +31 -51
- package/src/navigator-invocation-identity.ts +16 -4
- package/src/notary-contracts.ts +8 -26
- package/src/notary-role.ts +1 -1
- package/src/package-contracts/auditor-output.ts +6 -27
- package/src/package-contracts/judge-output.ts +10 -8
- package/src/package-contracts/terminating-tools.ts +9 -3
- package/src/packaged-role-registry.ts +12 -13
- package/src/pi/adapter.ts +6 -5
- package/src/public-cli/countersign-run.ts +1 -1
- package/src/public-cli/option-definitions.ts +2 -2
- package/src/public-cli/settlement.ts +18 -15
- package/src/review-submission.ts +29 -0
- package/src/role-envelope.ts +17 -9
- package/src/role-runtime.ts +32 -15
- package/src/submission-errors.ts +6 -6
- package/src/{gatekeeper-pass-envelope.ts → submission-gate.ts} +29 -16
- package/src/submission-ledger.ts +18 -3
- package/src/ticket-trajectory.ts +9 -4
- package/src/worker-role.ts +19 -5
package/souls/inspector.md
CHANGED
package/souls/judge-auditor.md
CHANGED
|
@@ -6,6 +6,6 @@
|
|
|
6
6
|
二、错封——封驳或所加约束指不出法源,或超出票面与陛下的话。
|
|
7
7
|
三、事实错——判词所引代码事实、法源引文与源头不符。
|
|
8
8
|
|
|
9
|
-
三者皆无即
|
|
9
|
+
三者皆无即 converged,一轮了结。判词少写一句而事实立得住,不是错;施工没做完,是大理寺封驳的理由,不是你打回判词的理由。打回与上一轮相同、对方只换措辞重交,上呈。
|
|
10
10
|
|
|
11
|
-
调卷宗与取证、纪律依《审刑院法典》。合规则 `
|
|
11
|
+
调卷宗与取证、纪律依《审刑院法典》。合规则 `converged`;有错则 `continue`,逐条指出是哪一种错和你查到的原因;无法判断时 `escalate`。
|
package/souls/notary.md
CHANGED
package/souls/quality-law.md
CHANGED
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
|
|
48
48
|
unfinished 只认两因:**缺前置**——处置当下仍缺少已声明的前置产物、决定或答复,且该缺失使剩余派工当前无法合法执行;**违宪约束**——继续施工将违反 authority 或既裁法源。两因之外的一切理由(执行窗口、时间、工作量、难度等),不得作为 unfinished 成立依据,审官一律封驳。
|
|
49
49
|
|
|
50
|
-
据实以两因交卷不构成违规,但不自动
|
|
50
|
+
据实以两因交卷不构成违规,但不自动 converged;所称阻断须可由卷宗独立核对,声明与引用本身不能证明缺失或因果。
|
|
51
51
|
|
|
52
52
|
## 预警参考线
|
|
53
53
|
|
|
@@ -140,7 +140,7 @@ type GateToolCall = {
|
|
|
140
140
|
};
|
|
141
141
|
|
|
142
142
|
function isGateTerminatingToolName(toolName: string): boolean {
|
|
143
|
-
return DISPATCH_TOOLS.has(toolName) || OFFICER_TOOL_TO_FACE[toolName] !== undefined;
|
|
143
|
+
return toolName === "ak_submission_output" || DISPATCH_TOOLS.has(toolName) || OFFICER_TOOL_TO_FACE[toolName] !== undefined;
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
/**
|
|
@@ -278,6 +278,7 @@ function projectAcceptedGateCall(
|
|
|
278
278
|
readonly attemptEntryId?: string;
|
|
279
279
|
readonly parentSessionFile?: string;
|
|
280
280
|
},
|
|
281
|
+
pointerOfficer?: "inspector" | "notary",
|
|
281
282
|
): ClassifiedVolume | undefined {
|
|
282
283
|
// Continuous memory volumes carry many summons; span only this binding's interval.
|
|
283
284
|
const span = requireAcceptedGateSpan(intervalRowsForGateCall(rows, call.rowIndex), filePath);
|
|
@@ -312,7 +313,9 @@ function projectAcceptedGateCall(
|
|
|
312
313
|
};
|
|
313
314
|
}
|
|
314
315
|
|
|
315
|
-
const officer =
|
|
316
|
+
const officer = call.toolName === "ak_submission_output"
|
|
317
|
+
? pointerOfficer
|
|
318
|
+
: OFFICER_TOOL_TO_FACE[call.toolName];
|
|
316
319
|
if (officer === undefined) {
|
|
317
320
|
// isGateTerminatingToolName already screened; keep loud if tables drift.
|
|
318
321
|
throw new Error(
|
|
@@ -334,6 +337,7 @@ function projectAcceptedGateCall(
|
|
|
334
337
|
|
|
335
338
|
async function classifyAuditorVolume(
|
|
336
339
|
filePath: string,
|
|
340
|
+
pointerOfficer?: "inspector" | "notary",
|
|
337
341
|
): Promise<readonly ClassifiedVolume[]> {
|
|
338
342
|
// Canonical JSONL errors propagate — failure honesty (never wash to fewer rounds).
|
|
339
343
|
const rows = await readLedgerSessionJsonl(filePath);
|
|
@@ -342,7 +346,9 @@ async function classifyAuditorVolume(
|
|
|
342
346
|
if (accepted.length === 0) return [];
|
|
343
347
|
const volumes: ClassifiedVolume[] = [];
|
|
344
348
|
for (const call of accepted) {
|
|
345
|
-
const projected =
|
|
349
|
+
const projected = call.toolName === "ak_submission_output" && pointerOfficer === undefined
|
|
350
|
+
? undefined // Shared tool alone carries no officer identity; a typed pointer does.
|
|
351
|
+
: projectAcceptedGateCall(filePath, rows, call, pointerOfficer);
|
|
346
352
|
if (projected !== undefined) volumes.push(projected);
|
|
347
353
|
}
|
|
348
354
|
return volumes;
|
|
@@ -425,7 +431,7 @@ function pairGateRounds(
|
|
|
425
431
|
/** Resolve a direct-officer-run-pointer file to the officer session 正本 path. */
|
|
426
432
|
async function resolveOfficerSessionFromPointerFile(
|
|
427
433
|
pointerPath: string,
|
|
428
|
-
): Promise<string |
|
|
434
|
+
): Promise<{ sessionFile: string; officer?: "inspector" | "notary" }> {
|
|
429
435
|
const { readFile } = await import("node:fs/promises");
|
|
430
436
|
let raw: unknown;
|
|
431
437
|
try {
|
|
@@ -443,7 +449,9 @@ async function resolveOfficerSessionFromPointerFile(
|
|
|
443
449
|
if (typeof sessionFile !== "string" || sessionFile.trim() === "") {
|
|
444
450
|
throw new Error(`direct officer run pointer missing sessionFile in ${pointerPath}`);
|
|
445
451
|
}
|
|
446
|
-
|
|
452
|
+
const officer = raw.officer === "inspector" || raw.officer === "notary"
|
|
453
|
+
? raw.officer : undefined;
|
|
454
|
+
return { sessionFile, ...(officer === undefined ? {} : { officer }) };
|
|
447
455
|
}
|
|
448
456
|
|
|
449
457
|
/**
|
|
@@ -488,15 +496,15 @@ export async function readAnalystGateCyclesFromAuditorRoles(
|
|
|
488
496
|
for (const name of names) {
|
|
489
497
|
const path = join(directory, name);
|
|
490
498
|
const fromPointer = name.endsWith(".pointer.json");
|
|
491
|
-
const
|
|
499
|
+
const pointer = fromPointer
|
|
492
500
|
? await resolveOfficerSessionFromPointerFile(path)
|
|
493
|
-
:
|
|
494
|
-
|
|
501
|
+
: undefined;
|
|
502
|
+
const sessionPath = pointer?.sessionFile ?? path;
|
|
495
503
|
if (fromPointer) {
|
|
496
504
|
if (classifiedOfficerSessions.has(sessionPath)) continue;
|
|
497
505
|
classifiedOfficerSessions.add(sessionPath);
|
|
498
506
|
}
|
|
499
|
-
const classified = await classifyAuditorVolume(sessionPath);
|
|
507
|
+
const classified = await classifyAuditorVolume(sessionPath, pointer?.officer);
|
|
500
508
|
for (const volume of classified) {
|
|
501
509
|
if (
|
|
502
510
|
options.parentSessionFile !== undefined &&
|
|
@@ -93,8 +93,8 @@ function emptyAccum(): OfficerAccum {
|
|
|
93
93
|
function absorbRound(accum: OfficerAccum, round: AnalystGateCyclesRoundRow): void {
|
|
94
94
|
accum.rounds += 1;
|
|
95
95
|
accum.wallSum += round.officerWallMs;
|
|
96
|
-
if (round.status === "bounce") accum.bounceCount += 1;
|
|
97
|
-
if (round.status === "pass") accum.passCount += 1;
|
|
96
|
+
if (round.status === "bounce" || round.status === "continue") accum.bounceCount += 1;
|
|
97
|
+
if (round.status === "pass" || round.status === "converged") accum.passCount += 1;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
function finishOfficer(
|
package/src/audit-escalation.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
ComplianceDecision,
|
|
5
5
|
} from "./compliance-transport.ts";
|
|
6
6
|
import { GatekeeperDecisionError } from "./submission-errors.ts";
|
|
7
|
+
import type { SubmissionGateNonPassResult } from "./gatekeeper-role.ts";
|
|
7
8
|
|
|
8
9
|
export const AUDIT_ESCALATION_KIND = "audit_escalation" as const;
|
|
9
10
|
|
|
@@ -37,7 +38,7 @@ export type AuditEscalationToolResult = {
|
|
|
37
38
|
|
|
38
39
|
export type OfficerAuditEscalationDecision = {
|
|
39
40
|
readonly status: "escalate";
|
|
40
|
-
readonly officer?: "inspector" | "notary" | "countersign";
|
|
41
|
+
readonly officer?: "inspector" | "notary" | "auditor" | "countersign";
|
|
41
42
|
readonly reason?: unknown;
|
|
42
43
|
readonly findings?: unknown;
|
|
43
44
|
readonly conflicts?: unknown;
|
|
@@ -113,6 +114,25 @@ export function projectAuditEscalation(
|
|
|
113
114
|
};
|
|
114
115
|
}
|
|
115
116
|
|
|
117
|
+
/** Preserve the escalating officer's own gate alongside the raw receipt. */
|
|
118
|
+
export function projectGatekeeperEscalation(
|
|
119
|
+
result: Extract<SubmissionGateNonPassResult, { status: "escalate" }>,
|
|
120
|
+
deliveredOutput?: unknown,
|
|
121
|
+
): AuditEscalationToolResult {
|
|
122
|
+
const receipt = result.receipt;
|
|
123
|
+
const record = receipt !== null && typeof receipt === "object" && !Array.isArray(receipt)
|
|
124
|
+
? receipt as Record<string, unknown>
|
|
125
|
+
: undefined;
|
|
126
|
+
return projectAuditEscalation({
|
|
127
|
+
status: "escalate",
|
|
128
|
+
officer: result.officer,
|
|
129
|
+
conflicts: receipt,
|
|
130
|
+
...(record !== undefined && Object.hasOwn(record, "decisionGate")
|
|
131
|
+
? { decisionGate: record.decisionGate }
|
|
132
|
+
: {}),
|
|
133
|
+
}, deliveredOutput);
|
|
134
|
+
}
|
|
135
|
+
|
|
116
136
|
/**
|
|
117
137
|
* Discriminator-only recognition (ADR 0040). Shape of conflicts/options/gate
|
|
118
138
|
* is not a reject gate — element types and cardinality are delivery content.
|
|
@@ -127,7 +147,7 @@ export function isAuditEscalationResult(
|
|
|
127
147
|
}
|
|
128
148
|
|
|
129
149
|
export type ComplianceDecisionHandlers<T> = {
|
|
130
|
-
|
|
150
|
+
converged: (usage: Usage | undefined) => T | PromiseLike<T>;
|
|
131
151
|
/** Project the accepted parent candidate beside the typed audit-leg facts. */
|
|
132
152
|
noReceipt?: (
|
|
133
153
|
facts: Extract<ComplianceDecision, { status: "no-receipt" }>,
|
|
@@ -141,7 +161,7 @@ export type ComplianceDecisionHandlers<T> = {
|
|
|
141
161
|
facts: Extract<ComplianceDecision, { status: "received" }>,
|
|
142
162
|
usageProjection: { usage?: Usage },
|
|
143
163
|
) => T | PromiseLike<T>;
|
|
144
|
-
|
|
164
|
+
continue: (violations: readonly unknown[]) => T | PromiseLike<T>;
|
|
145
165
|
escalate: (result: AuditEscalationToolResult) => T | PromiseLike<T>;
|
|
146
166
|
/** Host failure beside any recorded auditor payloads — parent is not accepted. */
|
|
147
167
|
transportFailure?: (
|
|
@@ -159,8 +179,8 @@ export async function disposeComplianceDecision<T>(
|
|
|
159
179
|
deliveredOutput?: unknown,
|
|
160
180
|
): Promise<Awaited<T>> {
|
|
161
181
|
switch (decision.status) {
|
|
162
|
-
case "
|
|
163
|
-
return await handlers.
|
|
182
|
+
case "converged":
|
|
183
|
+
return await handlers.converged(decision.usage);
|
|
164
184
|
case "no-receipt":
|
|
165
185
|
// The parent candidate remains accepted, but its parallel audit leg is a
|
|
166
186
|
// public typed fact and must not be collapsed into an ordinary pass.
|
|
@@ -180,8 +200,8 @@ export async function disposeComplianceDecision<T>(
|
|
|
180
200
|
decision,
|
|
181
201
|
decision.usage === undefined ? {} : { usage: decision.usage },
|
|
182
202
|
);
|
|
183
|
-
case "
|
|
184
|
-
return await handlers.
|
|
203
|
+
case "continue":
|
|
204
|
+
return await handlers.continue(decision.violations);
|
|
185
205
|
case "escalate":
|
|
186
206
|
return await handlers.escalate(
|
|
187
207
|
projectAuditEscalation(decision, deliveredOutput),
|
package/src/auditor-role.ts
CHANGED
|
@@ -15,7 +15,7 @@ export type AuditorRuntimeDependencies = {
|
|
|
15
15
|
export const AUDITOR_TOOL_SPEC = {
|
|
16
16
|
name: AUDITOR_OUTPUT_TOOL_NAME,
|
|
17
17
|
label: "审刑院输出",
|
|
18
|
-
description: "
|
|
18
|
+
description: "审刑院终局回执,status 为 converged、continue 或 escalate。",
|
|
19
19
|
promptSnippet: "审刑院终局回执",
|
|
20
20
|
parameters: auditorOutputSchema,
|
|
21
21
|
} as const;
|
|
@@ -11,7 +11,7 @@ import { readableGateItem } from "./readable-gate-item.ts";
|
|
|
11
11
|
export type ComplianceNoReceipt = NoReceiptLifecycleFacts & { status: "no-receipt"; usage?: Usage };
|
|
12
12
|
/**
|
|
13
13
|
* #757 / #750: no unreadable/unusable judgment on auditor replies.
|
|
14
|
-
* Known three-state (
|
|
14
|
+
* Known three-state (converged/continue/escalate) is read for queueing only.
|
|
15
15
|
* Unknown accepted shape rides as `received` with the raw reply — parent stands,
|
|
16
16
|
* no forged pass, no shape-death label. Resume-speaker for three pairs is #753/#756.
|
|
17
17
|
*/
|
|
@@ -29,8 +29,8 @@ export type ComplianceTransportFailure = {
|
|
|
29
29
|
readonly usage?: Usage;
|
|
30
30
|
};
|
|
31
31
|
export type ComplianceDecision =
|
|
32
|
-
| { status: "
|
|
33
|
-
| { status: "
|
|
32
|
+
| { status: "converged"; receipt?: unknown; usage?: Usage }
|
|
33
|
+
| { status: "continue"; violations: readonly unknown[]; receipt?: unknown; usage?: Usage }
|
|
34
34
|
| { status: "escalate"; conflicts?: unknown; decisionGate?: unknown; receipt?: unknown; usage?: Usage }
|
|
35
35
|
| ComplianceNoReceipt
|
|
36
36
|
| ComplianceReceived
|
|
@@ -60,15 +60,15 @@ export type AuditorParentAttemptBinding = {
|
|
|
60
60
|
|
|
61
61
|
function readListField(value: unknown): readonly unknown[] { return Array.isArray(value) ? value : value === undefined ? [] : [value]; }
|
|
62
62
|
|
|
63
|
-
/** Try to project a known three-state compliance decision; undefined when not
|
|
63
|
+
/** Try to project a known three-state compliance decision; undefined when not converged/continue/escalate. */
|
|
64
64
|
export function tryReadComplianceCandidate(arguments_: unknown, usage?: Usage): ComplianceDecision | undefined {
|
|
65
65
|
if (typeof arguments_ !== "object" || arguments_ === null || Array.isArray(arguments_)) {
|
|
66
66
|
return undefined;
|
|
67
67
|
}
|
|
68
68
|
const args = arguments_ as Record<string, unknown>;
|
|
69
69
|
const status = args.status;
|
|
70
|
-
if (status === "
|
|
71
|
-
if (status === "
|
|
70
|
+
if (status === "converged") return { status, receipt: arguments_, ...(usage === undefined ? {} : { usage }) };
|
|
71
|
+
if (status === "continue") return { status, violations: readListField(args.violations), receipt: arguments_, ...(usage === undefined ? {} : { usage }) };
|
|
72
72
|
if (status === "escalate") {
|
|
73
73
|
return {
|
|
74
74
|
status,
|
|
@@ -129,7 +129,7 @@ async function usageFromSummonedSession(summoned: PublicSummonResult): Promise<U
|
|
|
129
129
|
|
|
130
130
|
/**
|
|
131
131
|
* Project a public auditor terminal onto the parent compliance decision.
|
|
132
|
-
* Known
|
|
132
|
+
* Known converged/continue/escalate/no-receipt flow through for queueing.
|
|
133
133
|
* Accepted-but-not-three-state → `received` with raw reply (#757) — no unreadable label,
|
|
134
134
|
* never forged pass, never parent abort. Real provider/engine/disk failures stay loud.
|
|
135
135
|
* Accepted audits always carry real session usage when present (#675 metering).
|
|
@@ -5,18 +5,20 @@
|
|
|
5
5
|
* is defaulted, rewritten, or dropped (ADR 0055).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
import { REVIEW_SUBMISSION_OUTPUT_TOOL_NAME } from "./review-submission.ts";
|
|
9
|
+
|
|
10
|
+
export const COUNTERSIGN_OUTPUT_TOOL_NAME = REVIEW_SUBMISSION_OUTPUT_TOOL_NAME;
|
|
9
11
|
|
|
10
12
|
export type CountersignVerdict =
|
|
11
|
-
| {
|
|
13
|
+
| { status: "converged"; note?: string; evidence?: unknown }
|
|
12
14
|
| {
|
|
13
|
-
|
|
15
|
+
status: "continue";
|
|
14
16
|
fix?: { summary: string };
|
|
15
17
|
note?: string;
|
|
16
18
|
evidence?: unknown;
|
|
17
19
|
}
|
|
18
20
|
| {
|
|
19
|
-
|
|
21
|
+
status: "escalate";
|
|
20
22
|
decisionGate?: { question: string; options: string[] };
|
|
21
23
|
note?: string;
|
|
22
24
|
evidence?: unknown;
|
|
@@ -26,16 +28,16 @@ export function validateRecordedCountersignOutput(verdict: unknown): Countersign
|
|
|
26
28
|
if (verdict === null || typeof verdict !== "object" || Array.isArray(verdict)) {
|
|
27
29
|
throw new Error("Countersign verdict has no execution discriminator");
|
|
28
30
|
}
|
|
29
|
-
let
|
|
31
|
+
let status: unknown;
|
|
30
32
|
try {
|
|
31
|
-
|
|
33
|
+
status = (verdict as Record<string, unknown>).status;
|
|
32
34
|
} catch {
|
|
33
35
|
throw new Error("Countersign verdict has no execution discriminator");
|
|
34
36
|
}
|
|
35
|
-
if (typeof
|
|
37
|
+
if (typeof status !== "string") {
|
|
36
38
|
throw new Error("Countersign verdict has no execution discriminator");
|
|
37
39
|
}
|
|
38
|
-
if (["converged", "continue", "escalate"].includes(
|
|
40
|
+
if (["converged", "continue", "escalate"].includes(status)) {
|
|
39
41
|
return verdict as CountersignVerdict;
|
|
40
42
|
}
|
|
41
43
|
throw new Error("Countersign verdict has no execution discriminator");
|
package/src/countersign-role.ts
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { Type } from "typebox";
|
|
3
|
-
|
|
4
|
-
import { withTerminatingOutputDeclarations } from "./package-contracts/terminating-infrastructure.ts";
|
|
1
|
+
import { reviewSubmissionSchema } from "./review-submission.ts";
|
|
5
2
|
import {
|
|
6
3
|
COUNTERSIGN_OUTPUT_TOOL_NAME,
|
|
7
4
|
validateRecordedCountersignOutput,
|
|
@@ -12,44 +9,18 @@ export { COUNTERSIGN_OUTPUT_TOOL_NAME } from "./countersign-contracts.ts";
|
|
|
12
9
|
export type { CountersignVerdict };
|
|
13
10
|
|
|
14
11
|
// #836 r16 class 1: fix/note/decisionGate are LLM/human-read narrative content —
|
|
15
|
-
// no code branches on their length or nested presence. `
|
|
16
|
-
//
|
|
12
|
+
// no code branches on their length or nested presence. `status` alone is the
|
|
13
|
+
// machine discriminator the queue reads.
|
|
17
14
|
// #836 (2026-09-11 御批 / ADR 0003 Amendment): registered-to-provider closed
|
|
18
|
-
// value domains reject an unknown
|
|
15
|
+
// value domains reject an unknown status before the submission
|
|
19
16
|
// ledger ever records it, defeating 读不出三态→resume 说话者本人. Kept open
|
|
20
17
|
// (Type.Unknown) like every other gate-queue status field in this package
|
|
21
18
|
// (notary/auditor/gatekeeper/navigator); src/role-runtime.ts:997-1025 still
|
|
22
19
|
// classifies the recorded value and reasks the countersign itself when it
|
|
23
20
|
// isn't one of the three states — code, not the transport, does that work.
|
|
24
21
|
/** 给事中票庭审读五问的交卷形状(ADR 0074);形状指引,非 schema 闸。 */
|
|
25
|
-
export const countersignVerdictSchema =
|
|
26
|
-
|
|
27
|
-
{
|
|
28
|
-
countersignStatus: Type.Unknown({ description: "converged | continue | escalate。非三态时请重读后重交,勿改标。" }),
|
|
29
|
-
fix: Type.Optional(
|
|
30
|
-
Type.Object(
|
|
31
|
-
{ summary: Type.Optional(Type.String({ description: "退回摘要" })) },
|
|
32
|
-
{ additionalProperties: true, description: "退回说明" },
|
|
33
|
-
),
|
|
34
|
-
),
|
|
35
|
-
note: Type.Optional(Type.String({ description: "附注" })),
|
|
36
|
-
evidence: Type.Optional(Type.Unknown({ description: "留存证据" })),
|
|
37
|
-
decisionGate: Type.Optional(
|
|
38
|
-
Type.Object(
|
|
39
|
-
{
|
|
40
|
-
question: Type.Optional(Type.String()),
|
|
41
|
-
options: Type.Optional(Type.Array(Type.String())),
|
|
42
|
-
},
|
|
43
|
-
{ additionalProperties: true, description: "需陛下处置的问题与选项" },
|
|
44
|
-
),
|
|
45
|
-
),
|
|
46
|
-
},
|
|
47
|
-
{ additionalProperties: true },
|
|
48
|
-
),
|
|
49
|
-
);
|
|
50
|
-
(countersignVerdictSchema as unknown as { required: string[] }).required = [];
|
|
51
|
-
|
|
52
|
-
export type CountersignVerdictParameters = Static<typeof countersignVerdictSchema>;
|
|
22
|
+
export const countersignVerdictSchema = reviewSubmissionSchema;
|
|
23
|
+
export type CountersignVerdictParameters = import("./review-submission.ts").ReviewSubmission;
|
|
53
24
|
|
|
54
25
|
export type CountersignRuntimeDependencies = {
|
|
55
26
|
loadSoul(): Promise<string>;
|
package/src/doctor-evidence.ts
CHANGED
|
@@ -20,10 +20,11 @@ function deriveSession(content: string, id: string): SessionDerivation {
|
|
|
20
20
|
let accepted: Record<string, unknown> | undefined, observedCommit: string | undefined, turns = 0, calls = 0, tokens = 0;
|
|
21
21
|
const statuses: DoctorCaseCost["statuses"] = [], commits: DoctorCaseCost["commits"] = [];
|
|
22
22
|
for (const row of rows) {
|
|
23
|
+
if (row.type === "custom" && row.customType === "ak-role-submission-closure" && record(row.data) && row.data.isError === false && typeof row.data.toolName === "string" && isTerminatingToolName(row.data.toolName)) accepted = row;
|
|
23
24
|
const message = record(row.message) ? row.message : undefined;
|
|
24
25
|
if (message?.role === "assistant") { for (const part of Array.isArray(message.content) ? message.content : []) if (record(part) && part.type === "toolCall") calls++; if (typeof message.responseId === "string") { turns++; const usage = record(message.usage) ? message.usage : undefined; if (usage && typeof usage.output === "number") tokens += usage.output; } }
|
|
25
26
|
if (message?.role === "toolResult" && message.isError !== true && typeof message.toolName === "string" && isTerminatingToolName(message.toolName) && record(message.details)) { let details; try { details = validateAcceptedDetails(message.toolName, message.details); } catch (error) { // Contract: README.md#Doctor — non-accepted terminating receipts are expected-negative evidence and are skipped; all other validation failures propagate with their cause.
|
|
26
|
-
if (error instanceof AcceptedDetailsContractError) continue; throw error; }
|
|
27
|
+
if (error instanceof AcceptedDetailsContractError) continue; throw error; } const facts = acceptedFacts(message.toolName, details); if (facts.commit && facts.commit !== observedCommit) { commits.push({ source: id, commit: facts.commit }); observedCommit = facts.commit; } /* #836 B10.3: do not wipe earlier statuses or invent a code status sentence */ if (facts.status !== undefined) { statuses.push({ source: id, status: facts.status }); } }
|
|
27
28
|
}
|
|
28
29
|
const acceptedAt = accepted && timestamp(accepted); const final = acceptedAt ? accepted! : rows.at(-1); const endedAt = final && timestamp(final); const wall = startedAt && endedAt ? Date.parse(endedAt) - Date.parse(startedAt) : undefined;
|
|
29
30
|
if (wall !== undefined && wall < 0) degradationReasons.push(`non-monotonic session timestamps in ${id}`);
|
package/src/doctor-role.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { RoleHost, HostContext, HostToolResult } from "./host-contracts.ts";
|
|
2
2
|
import { disposeComplianceDecision } from "./audit-escalation.ts";
|
|
3
3
|
import { ComplianceResponseRetentionError, type ComplianceDecision } from "./compliance-transport.ts";
|
|
4
|
-
import { GatekeeperDecisionError } from "./submission-errors.ts";
|
|
5
4
|
import { DOCTOR_CANDIDATE_ENTRY_TYPE } from "./dossier-resolution.ts";
|
|
6
5
|
import { readableGateItem } from "./readable-gate-item.ts";
|
|
7
6
|
import { DOCTOR_EVIDENCE_TOOL_NAME, DOCTOR_OUTPUT_TOOL_DESCRIPTION, DOCTOR_OUTPUT_TOOL_NAME, DoctorEvidenceStore, doctorEvidenceReadSchema, doctorSubmissionSchema, validateDoctorOutput, type DoctorCase } from "./doctor-contracts.ts";
|
|
@@ -37,7 +36,7 @@ export function createDoctorRoleRuntime(pi: RoleHost, dependencies: DoctorRoleDe
|
|
|
37
36
|
let activation: { soul: string; patient: DoctorCase; store: DoctorEvidenceStore } | undefined; let registered = false; pi.registerFlag(DOCTOR_CASE_FLAG.name, DOCTOR_CASE_FLAG.definition);
|
|
38
37
|
return { async activate() { const path = pi.getFlag(DOCTOR_CASE_FLAG.name); if (typeof path !== "string" || !path.trim()) throw new Error("Doctor requires --ak-doctor-case"); const soul = (await dependencies.loadSoul()).trim(); if (!soul) throw new Error("Doctor soul is empty"); const patient = await dependencies.loadCase(path); activation = { soul, patient, store: new DoctorEvidenceStore(patient) };
|
|
39
38
|
if (!registered) { registered = true; pi.registerTool({ name: DOCTOR_EVIDENCE_TOOL_NAME, label: "太医署证据", description: "分页读取留存的 Pi session 字节。", parameters: doctorEvidenceReadSchema, async execute(_id: string, params: { evidenceId: string; offset?: number; limit?: number }) { if (!activation) throw new Error("太医署未激活"); const details = activation.store.read(params.evidenceId, params.offset, params.limit); return { content: [{ type: "text" as const, text: JSON.stringify(details) }], details }; } });
|
|
40
|
-
pi.registerTool({ name: DOCTOR_OUTPUT_TOOL_NAME, label: "太医署输出", description: DOCTOR_OUTPUT_TOOL_DESCRIPTION, parameters: doctorSubmissionSchema, async execute(id: string, params: unknown, signal: AbortSignal | undefined, _update: unknown, ctx: HostContext): Promise<HostToolResult<unknown>> { if (!activation) throw new Error("太医署未激活"); const active = activation; const testimony = validateDoctorOutput(params, active.patient, active.store); try { appendCandidate(ctx, { version: 1, testimony, cost: active.patient.cost, readRecord: active.store.readRecord(), patientIdentity: active.patient.identity }); } catch (error) { host.failInfrastructure(error, ctx, id); } let audit: ComplianceDecision; try { audit = await dependencies.auditCompliance(signal === undefined ? { context: ctx, submission: testimony } : { context: ctx, signal, submission: testimony }); } catch (error) { host.failInfrastructure(error, ctx, id); } const acceptedDetails = testimony; return disposeComplianceDecision<HostToolResult<unknown>>(audit, {
|
|
39
|
+
pi.registerTool({ name: DOCTOR_OUTPUT_TOOL_NAME, label: "太医署输出", description: DOCTOR_OUTPUT_TOOL_DESCRIPTION, parameters: doctorSubmissionSchema, async execute(id: string, params: unknown, signal: AbortSignal | undefined, _update: unknown, ctx: HostContext): Promise<HostToolResult<unknown>> { if (!activation) throw new Error("太医署未激活"); const active = activation; const testimony = validateDoctorOutput(params, active.patient, active.store); try { appendCandidate(ctx, { version: 1, testimony, cost: active.patient.cost, readRecord: active.store.readRecord(), patientIdentity: active.patient.identity }); } catch (error) { host.failInfrastructure(error, ctx, id); } let audit: ComplianceDecision; try { audit = await dependencies.auditCompliance(signal === undefined ? { context: ctx, submission: testimony } : { context: ctx, signal, submission: testimony }); } catch (error) { host.failInfrastructure(error, ctx, id); } const acceptedDetails = testimony; return disposeComplianceDecision<HostToolResult<unknown>>(audit, { converged: (usage) => ({ content: audit.status === "converged" && audit.receipt !== undefined ? [{ type: "text" as const, text: readableGateItem(audit.receipt) }] : [], details: acceptedDetails, terminate: true as const, ...(usage === undefined ? {} : { usage }) }), noReceipt: (auditNoReceipt, usageProjection) => { try { appendCandidate(ctx, { version: 1, testimony, cost: active.patient.cost, auditNoReceipt, readRecord: active.store.readRecord(), patientIdentity: active.patient.identity }); } catch (error) { host.failInfrastructure(error, ctx, id); } return { content: [], details: acceptedDetails, terminate: true as const, ...usageProjection }; }, received: () => { throw new Error("审刑院非三态应交 runComplianceAudit 重问说话者"); }, continue: (violations) => ({ content: [{ type: "text" as const, text: readableGateItem(audit.status === "continue" ? audit.receipt ?? violations : violations) }], details: acceptedDetails, terminate: false as const }), escalate: (result) => result, transportFailure: (facts) => { host.failInfrastructure(Object.assign(new Error(facts.diagnostic), { name: "ComplianceTransportFailure", ...(facts.submissions === undefined && facts.terminal === undefined ? {} : { submission: facts.submissions ?? facts.terminal }) }), ctx, id); } }, acceptedDetails); } });
|
|
41
40
|
pi.on("before_agent_start", (event) => { if (!activation) throw new Error("太医署未激活"); const catalog = { version: activation.patient.version, identity: activation.patient.identity, admittedMetrics: { cost: activation.patient.cost }, lawfulTargetKeys: ["case", ...activation.patient.cost.invocations.sources], evidence: activation.patient.evidence.map(({ id, kind, sha256, byteLength, contentLength }) => ({ id, kind, sha256, byteLength, contentLength })) }; return { systemPrompt: `${event.systemPrompt}\n\n<doctor_soul>\n${activation.soul}\n</doctor_soul>\n\n<doctor_case>\n${JSON.stringify(catalog)}\n</doctor_case>` }; }); }
|
|
42
41
|
const required = [DOCTOR_EVIDENCE_TOOL_NAME, DOCTOR_OUTPUT_TOOL_NAME]; const names = pi.getAllTools().map((tool) => tool.name); for (const name of required) if (names.filter((item) => item === name).length !== 1) throw new Error(`Doctor required tool collision or missing: ${name}`); pi.setActiveTools(required); const active = pi.getActiveTools?.() ?? required; if (active.length !== 2 || !required.every((name) => active.includes(name))) throw new Error("Doctor active tool narrowing failed"); } };
|
|
43
42
|
}
|
package/src/gatekeeper-role.ts
CHANGED
|
@@ -2,10 +2,11 @@ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
|
2
2
|
import type { HostContext } from "./host-contracts.ts";
|
|
3
3
|
|
|
4
4
|
import { auditorRunDirectory } from "./auditor-dossier-tool.ts";
|
|
5
|
-
import {
|
|
5
|
+
import { packagedGateStageLabel } from "./packaged-role-registry.ts";
|
|
6
6
|
import type { NoReceiptLifecycleFacts } from "./receipt-delivery-policy.ts";
|
|
7
7
|
import { GatekeeperDecisionError } from "./submission-errors.ts";
|
|
8
8
|
import { INSPECTOR_OUTPUT_TOOL_NAME } from "./inspector-contracts.ts";
|
|
9
|
+
import { REVIEW_SUBMISSION_OUTPUT_TOOL_NAME } from "./review-submission.ts";
|
|
9
10
|
import {
|
|
10
11
|
GATEKEEPER_OUTPUT_TOOL_NAME,
|
|
11
12
|
gatekeeperDecisionSchema,
|
|
@@ -15,7 +16,7 @@ import type { PublicSummonResult } from "./public-role-summons.ts";
|
|
|
15
16
|
import type { TerminalResult } from "./public-cli/terminal.ts";
|
|
16
17
|
import { runIdFromRunDirectory } from "./run-terminal-artifacts.ts";
|
|
17
18
|
export const INSPECTOR_OUTPUT_TOOL = INSPECTOR_OUTPUT_TOOL_NAME;
|
|
18
|
-
export const NOTARY_OUTPUT_TOOL =
|
|
19
|
+
export const NOTARY_OUTPUT_TOOL = REVIEW_SUBMISSION_OUTPUT_TOOL_NAME;
|
|
19
20
|
|
|
20
21
|
/** Gate review officers — 台院 / 符宝郎 / 审刑院 / 给事中 (#753 / #756 / #969). */
|
|
21
22
|
export type GateOfficer = "inspector" | "notary" | "auditor" | "countersign";
|
|
@@ -38,14 +39,14 @@ export type GatekeeperSubject =
|
|
|
38
39
|
*/
|
|
39
40
|
export type GatekeeperResult =
|
|
40
41
|
| {
|
|
41
|
-
readonly status: "
|
|
42
|
+
readonly status: "converged";
|
|
42
43
|
readonly officer: GateOfficer;
|
|
43
44
|
readonly receipt: unknown;
|
|
44
45
|
readonly runId?: string;
|
|
45
46
|
}
|
|
46
|
-
/**
|
|
47
|
+
/** continue | escalate: both return the officer receipt to the parent (#753 / #756). */
|
|
47
48
|
| {
|
|
48
|
-
readonly status: "
|
|
49
|
+
readonly status: "continue";
|
|
49
50
|
readonly officer: GateOfficer;
|
|
50
51
|
readonly receipt: unknown;
|
|
51
52
|
readonly runId?: string;
|
|
@@ -58,9 +59,9 @@ export type GatekeeperResult =
|
|
|
58
59
|
}
|
|
59
60
|
| {
|
|
60
61
|
/**
|
|
61
|
-
* Accepted reply whose conclusion is not
|
|
62
|
+
* Accepted reply whose conclusion is not converged|continue|escalate.
|
|
62
63
|
* Envelope resumes the officer with plain-language re-ask — never parent-stands,
|
|
63
|
-
* never forges
|
|
64
|
+
* never forges continue (#753 / unreadable-conclusion-resume-speaker).
|
|
64
65
|
*/
|
|
65
66
|
readonly status: "needs_reask";
|
|
66
67
|
readonly officer: GateOfficer;
|
|
@@ -77,9 +78,9 @@ export type GatekeeperResult =
|
|
|
77
78
|
};
|
|
78
79
|
|
|
79
80
|
/** Non-pass faces returned to the parent session (correctable; #836 never kill leg). */
|
|
80
|
-
export type
|
|
81
|
+
export type SubmissionGateNonPassResult = Extract<
|
|
81
82
|
GatekeeperResult,
|
|
82
|
-
{ status: "
|
|
83
|
+
{ status: "continue" | "escalate" | "no_receipt" | "transport_failure" }
|
|
83
84
|
>;
|
|
84
85
|
|
|
85
86
|
function gateSeatLabel(stage: GateOfficer): string {
|
|
@@ -138,16 +139,16 @@ export type RunGatekeeperOptions = {
|
|
|
138
139
|
readonly submission?: unknown;
|
|
139
140
|
/**
|
|
140
141
|
* Officer summon seam. Production default lives on the shared envelope
|
|
141
|
-
* (`createDefaultGateOfficerSummon` /
|
|
142
|
+
* (`createDefaultGateOfficerSummon` / requireSubmissionGate — ADR 0018).
|
|
142
143
|
* Role module only projects; callers must supply the summon.
|
|
143
144
|
*/
|
|
144
145
|
readonly summonOfficer: GateOfficerSummon;
|
|
145
146
|
};
|
|
146
147
|
|
|
147
|
-
export type
|
|
148
|
+
export type SubmissionGateHostActions = {
|
|
148
149
|
failInfrastructure(error: unknown, ctx: ExtensionContext | HostContext, toolCallId?: string): never;
|
|
149
150
|
/** Envelope-owned execute→tool_result bridge (role-runtime); role module only throws typed error. */
|
|
150
|
-
bindSubmissionNonPass(toolCallId: string, result:
|
|
151
|
+
bindSubmissionNonPass(toolCallId: string, result: SubmissionGateNonPassResult): void;
|
|
151
152
|
};
|
|
152
153
|
|
|
153
154
|
/**
|
|
@@ -204,15 +205,15 @@ function readRecord(value: unknown): Record<string, unknown> | undefined {
|
|
|
204
205
|
|
|
205
206
|
/**
|
|
206
207
|
* Read only the conclusion field for queueing (#753).
|
|
207
|
-
*
|
|
208
|
+
* converged | continue | escalate → queue signal + raw receipt.
|
|
208
209
|
* Anything else accepted → needs_reask (resume speaker), never unreadable/parent-stand.
|
|
209
210
|
* `fallbackStatus` is the terminal outcome.status when the receipt body has no status key
|
|
210
211
|
* (keeps missing-args sentinel intact as the receipt).
|
|
211
212
|
*/
|
|
212
|
-
/** Map
|
|
213
|
-
function
|
|
214
|
-
if (status === "converged") return "
|
|
215
|
-
if (status === "continue") return "
|
|
213
|
+
/** Map the shared review verdict onto the gate queue words (#1028). */
|
|
214
|
+
function reviewQueueStatus(status: string): string | undefined {
|
|
215
|
+
if (status === "converged") return "converged";
|
|
216
|
+
if (status === "continue") return "continue";
|
|
216
217
|
if (status === "escalate") return "escalate";
|
|
217
218
|
return undefined;
|
|
218
219
|
}
|
|
@@ -224,34 +225,15 @@ function projectOfficerDecision(
|
|
|
224
225
|
): GatekeeperResult {
|
|
225
226
|
const receipt = retainedReceipt(decision);
|
|
226
227
|
const record = readRecord(decision);
|
|
227
|
-
// 给事中: only countersignStatus, strict three-word map; never generic status,
|
|
228
|
-
// never unmapped raw word, never fallbackStatus (#969 / ADR 0040/0055).
|
|
229
|
-
if (packagedGateDecision(officer) === "countersign-status") {
|
|
230
|
-
const countersignStatus =
|
|
231
|
-
record !== undefined && typeof record.countersignStatus === "string"
|
|
232
|
-
? record.countersignStatus
|
|
233
|
-
: undefined;
|
|
234
|
-
const status =
|
|
235
|
-
typeof countersignStatus === "string"
|
|
236
|
-
? gateStatusFromCountersign(countersignStatus)
|
|
237
|
-
: undefined;
|
|
238
|
-
if (status === "pass") {
|
|
239
|
-
return { status: "pass", officer, receipt };
|
|
240
|
-
}
|
|
241
|
-
if (status === "bounce" || status === "escalate") {
|
|
242
|
-
return { status, officer, receipt };
|
|
243
|
-
}
|
|
244
|
-
return { status: "needs_reask", officer, receipt };
|
|
245
|
-
}
|
|
246
|
-
// inspector / notary / auditor: shared gate words on generic status.
|
|
247
228
|
const status =
|
|
248
229
|
(record !== undefined && typeof record.status === "string" ? record.status : undefined)
|
|
249
230
|
?? fallbackStatus;
|
|
250
|
-
|
|
251
|
-
|
|
231
|
+
const queueStatus = typeof status === "string" ? reviewQueueStatus(status) : undefined;
|
|
232
|
+
if (queueStatus === "converged") {
|
|
233
|
+
return { status: "converged", officer, receipt };
|
|
252
234
|
}
|
|
253
|
-
if (
|
|
254
|
-
return { status, officer, receipt };
|
|
235
|
+
if (queueStatus === "continue" || queueStatus === "escalate") {
|
|
236
|
+
return { status: queueStatus, officer, receipt };
|
|
255
237
|
}
|
|
256
238
|
return { status: "needs_reask", officer, receipt };
|
|
257
239
|
}
|
|
@@ -316,8 +298,8 @@ function withOfficerRunId(
|
|
|
316
298
|
summoned: PublicSummonResult,
|
|
317
299
|
): GatekeeperResult {
|
|
318
300
|
if (
|
|
319
|
-
result.status !== "
|
|
320
|
-
&& result.status !== "
|
|
301
|
+
result.status !== "converged"
|
|
302
|
+
&& result.status !== "continue"
|
|
321
303
|
&& result.status !== "escalate"
|
|
322
304
|
&& result.status !== "needs_reask"
|
|
323
305
|
) {
|
|
@@ -382,8 +364,8 @@ function projectOfficerTerminal(
|
|
|
382
364
|
}
|
|
383
365
|
if (outcome.kind === "accepted") {
|
|
384
366
|
// outcome.status is the fixture/compat leaf: production settlement leaves
|
|
385
|
-
// it undefined once payloads are recorded
|
|
386
|
-
//
|
|
367
|
+
// it undefined once payloads are recorded. It may interpret a receipt
|
|
368
|
+
// lacking its own status, never substitute for a missing receipt.
|
|
387
369
|
return withOfficerRunId(
|
|
388
370
|
projectOfficerPayloads(officer, thisCourt, outcome.status),
|
|
389
371
|
summoned,
|
|
@@ -409,28 +391,21 @@ export type GatekeeperProjection = {
|
|
|
409
391
|
};
|
|
410
392
|
|
|
411
393
|
/**
|
|
412
|
-
* Plain-language re-ask when the officer conclusion is not
|
|
394
|
+
* Plain-language re-ask when the officer conclusion is not converged|continue|escalate.
|
|
413
395
|
* Not a packaged engine handbook line (#755 exception for 读不出三态).
|
|
414
|
-
*
|
|
415
|
-
* field and words so reask does not invite non-contract values (#969 / ADR 0055).
|
|
396
|
+
* Every review officer uses the same `status` field and three queue words (#1028).
|
|
416
397
|
*/
|
|
417
398
|
export const OFFICER_CONCLUSION_REASK =
|
|
418
|
-
"
|
|
419
|
-
|
|
420
|
-
/** 给事中 re-ask: countersignStatus converged|continue|escalate only (#969). */
|
|
421
|
-
export const COUNTERSIGN_CONCLUSION_REASK =
|
|
422
|
-
"上次交卷的 countersignStatus 不是 converged、continue、escalate 三态之一。请重新输出,countersignStatus 写明其一;封驳或上呈的话就是给对方看的原文。" as const;
|
|
399
|
+
"上次交卷的 status 不是 converged、continue、escalate 三态之一。请重新输出,status 写明其一;continue 或 escalate 的话就是给对方看的原文。" as const;
|
|
423
400
|
|
|
424
401
|
/** Pick the re-ask line for the officer under review. */
|
|
425
402
|
export function officerConclusionReask(officer: GateOfficer): string {
|
|
426
|
-
return
|
|
427
|
-
? COUNTERSIGN_CONCLUSION_REASK
|
|
428
|
-
: OFFICER_CONCLUSION_REASK;
|
|
403
|
+
return OFFICER_CONCLUSION_REASK;
|
|
429
404
|
}
|
|
430
405
|
|
|
431
406
|
/**
|
|
432
407
|
* Summon (via injected seam) + project. Default summonGateOfficer drive lives
|
|
433
|
-
* on the shared envelope (`
|
|
408
|
+
* on the shared envelope (`submission-gate.ts`, ADR 0018 / #675) —
|
|
434
409
|
* role module keeps projection only.
|
|
435
410
|
*/
|
|
436
411
|
export async function projectGatekeeperRun(
|