@akagilnc/pi-workflow-roles 0.1.4631 → 0.1.4646
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 +10 -0
- package/README.zh-CN.md +10 -0
- package/dist/acp-host/production-host.js +731 -329
- package/dist/gatekeeper-role.js +76 -6
- package/dist/headless-host/production-host.js +708 -306
- package/dist/migrate-book-topology.js +9 -0
- package/dist/public-cli/countersign-run.js +31 -10
- package/dist/public-cli/main.js +401 -287
- package/dist/public-cli/settlement.js +95 -2
- package/dist/public-role-summons.js +19 -0
- package/dist/secretariat-contracts.js +4 -0
- package/package.json +1 -1
- package/src/archivist-record-entry.ts +2 -2
- package/src/audit-escalation.ts +1 -1
- package/src/gatekeeper-pass-envelope.ts +37 -5
- package/src/gatekeeper-role.ts +125 -20
- package/src/host-contracts.ts +22 -2
- package/src/pi/adapter.ts +5 -1
- package/src/public-cli/countersign-run.ts +50 -13
- package/src/public-cli/settlement.ts +117 -1
- package/src/public-role-summons.ts +30 -4
- package/src/role-envelope.ts +27 -10
- package/src/role-runtime.ts +142 -4
- package/src/secretariat-contracts.ts +18 -0
|
@@ -32,7 +32,7 @@ import { NOTARY_OUTPUT_TOOL_NAME, } from "../notary-contracts.js";
|
|
|
32
32
|
import { COUNTERSIGN_OUTPUT_TOOL_NAME, } from "../countersign-contracts.js";
|
|
33
33
|
import { GLEANER_LEFT_OUTPUT_TOOL_NAME, } from "../gleaner-left-contracts.js";
|
|
34
34
|
import { DIARIST_OUTPUT_TOOL_NAME, } from "../diarist-contracts.js";
|
|
35
|
-
import { SECRETARIAT_OUTPUT_TOOL_NAME, } from "../secretariat-contracts.js";
|
|
35
|
+
import { SECRETARIAT_COUNTERSIGN_TERMINAL_FACT_KEY, SECRETARIAT_GATE_OFFICER_ENTRY_TYPE, SECRETARIAT_OUTPUT_TOOL_NAME, } from "../secretariat-contracts.js";
|
|
36
36
|
import { INSPECTOR_OUTPUT_TOOL_NAME, } from "../inspector-contracts.js";
|
|
37
37
|
import { GATEKEEPER_OUTPUT_TOOL_NAME, } from "../package-contracts/gatekeeper-output.js";
|
|
38
38
|
import { NAVIGATOR_OUTPUT_TOOL_NAME, } from "../package-contracts/navigator-output.js";
|
|
@@ -2254,11 +2254,104 @@ async function settleLawfulDiaristTerminalResult(admitted, authority, scope) {
|
|
|
2254
2254
|
export async function trySettleDiaristTerminalResult(admitted, authority, scope) {
|
|
2255
2255
|
return settleLawfulDiaristTerminalResult(admitted, authority, scope);
|
|
2256
2256
|
}
|
|
2257
|
+
/**
|
|
2258
|
+
* #969 seat projection sole authority: 给事中 terminal (署|上呈) booked as a
|
|
2259
|
+
* durable custom entry (envelope-safe) — ledger accepted stays LLM params (#836).
|
|
2260
|
+
* Never scan toolResult rows: those are memory-only on headless/ACP (#617/#959).
|
|
2261
|
+
* Receipt bytes stay original; nested runId rides beside them.
|
|
2262
|
+
* Caller must only apply this when the terminal-defining submission actually
|
|
2263
|
+
* produced the officer entry (gate-bound converged, or 给事中 audit_escalation).
|
|
2264
|
+
*/
|
|
2265
|
+
function countersignTerminalFromEntries(entries) {
|
|
2266
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
2267
|
+
const entry = entries[index];
|
|
2268
|
+
if (entry?.type !== "custom")
|
|
2269
|
+
continue;
|
|
2270
|
+
if (entry.customType !== SECRETARIAT_GATE_OFFICER_ENTRY_TYPE)
|
|
2271
|
+
continue;
|
|
2272
|
+
const data = entry.data !== null && typeof entry.data === "object" && !Array.isArray(entry.data)
|
|
2273
|
+
? entry.data
|
|
2274
|
+
: undefined;
|
|
2275
|
+
if (data?.officer !== "countersign")
|
|
2276
|
+
continue;
|
|
2277
|
+
if (data.receipt === undefined)
|
|
2278
|
+
continue;
|
|
2279
|
+
const runId = typeof data.runId === "string" && data.runId.trim() !== ""
|
|
2280
|
+
? data.runId
|
|
2281
|
+
: undefined;
|
|
2282
|
+
return {
|
|
2283
|
+
receipt: data.receipt,
|
|
2284
|
+
...(runId === undefined ? {} : { runId }),
|
|
2285
|
+
};
|
|
2286
|
+
}
|
|
2287
|
+
return undefined;
|
|
2288
|
+
}
|
|
2289
|
+
function withCountersignTerminalFact(roleOutcome, officer) {
|
|
2290
|
+
const prior = roleOutcome.decisiveFacts !== undefined && isRecord(roleOutcome.decisiveFacts)
|
|
2291
|
+
? roleOutcome.decisiveFacts
|
|
2292
|
+
: {};
|
|
2293
|
+
return {
|
|
2294
|
+
...roleOutcome,
|
|
2295
|
+
decisiveFacts: {
|
|
2296
|
+
...prior,
|
|
2297
|
+
[SECRETARIAT_COUNTERSIGN_TERMINAL_FACT_KEY]: officer,
|
|
2298
|
+
},
|
|
2299
|
+
};
|
|
2300
|
+
}
|
|
2301
|
+
/**
|
|
2302
|
+
* Terminal-defining secretariat submission owns officer projection.
|
|
2303
|
+
* Last readable secretariatStatus on accepted payloads decides: only gate-bound
|
|
2304
|
+
* converged projects a prior durable officer entry. Parent escalate bypasses the
|
|
2305
|
+
* gate and must not inherit a stale pass entry from an earlier turn (#969).
|
|
2306
|
+
*/
|
|
2307
|
+
function acceptedSecretariatDefinesOfficerProjection(roleOutcome) {
|
|
2308
|
+
const payloads = roleOutcome.payloads ?? [];
|
|
2309
|
+
for (let index = payloads.length - 1; index >= 0; index -= 1) {
|
|
2310
|
+
const payload = payloads[index];
|
|
2311
|
+
if (!isRecord(payload))
|
|
2312
|
+
continue;
|
|
2313
|
+
const status = payload.secretariatStatus;
|
|
2314
|
+
if (typeof status !== "string")
|
|
2315
|
+
continue;
|
|
2316
|
+
return status === "converged";
|
|
2317
|
+
}
|
|
2318
|
+
return false;
|
|
2319
|
+
}
|
|
2257
2320
|
async function settleLawfulSecretariatTerminalResult(admitted, authority, scope) {
|
|
2258
|
-
|
|
2321
|
+
const settled = await settleLawfulSeatAcceptedTerminalResult(admitted, authority, {
|
|
2259
2322
|
role: "secretariat",
|
|
2260
2323
|
toolName: SECRETARIAT_OUTPUT_TOOL_NAME,
|
|
2261
2324
|
}, scope);
|
|
2325
|
+
if (settled === undefined)
|
|
2326
|
+
return undefined;
|
|
2327
|
+
const coordinates = coordinatesFromAdmitted(authority, admitted);
|
|
2328
|
+
const entries = await readLawfulSettlementEntries(coordinates.sessionFile) ?? [];
|
|
2329
|
+
const officer = countersignTerminalFromEntries(entries);
|
|
2330
|
+
if (officer === undefined)
|
|
2331
|
+
return settled;
|
|
2332
|
+
// 给事中上呈: public payloads = officer receipt; runId + receipt fact beside.
|
|
2333
|
+
// This kind is produced only when beforeAccept booked the officer entry.
|
|
2334
|
+
if (settled.roleOutcome.kind === "audit_escalation") {
|
|
2335
|
+
return {
|
|
2336
|
+
...settled,
|
|
2337
|
+
roleOutcome: withCountersignTerminalFact({
|
|
2338
|
+
...settled.roleOutcome,
|
|
2339
|
+
payloads: [officer.receipt],
|
|
2340
|
+
}, officer),
|
|
2341
|
+
};
|
|
2342
|
+
}
|
|
2343
|
+
// Pass (署): keep 中书省 payloads; present 给事中 判词 + runId via decisiveFacts.
|
|
2344
|
+
// Parent escalate accepted terminal must not project a stale prior pass entry.
|
|
2345
|
+
if (settled.roleOutcome.kind === "accepted") {
|
|
2346
|
+
if (!acceptedSecretariatDefinesOfficerProjection(settled.roleOutcome)) {
|
|
2347
|
+
return settled;
|
|
2348
|
+
}
|
|
2349
|
+
return {
|
|
2350
|
+
...settled,
|
|
2351
|
+
roleOutcome: withCountersignTerminalFact(settled.roleOutcome, officer),
|
|
2352
|
+
};
|
|
2353
|
+
}
|
|
2354
|
+
return settled;
|
|
2262
2355
|
}
|
|
2263
2356
|
/** Try to settle a lawful Secretariat Terminal; undefined only for genuine absence. */
|
|
2264
2357
|
export async function trySettleSecretariatTerminalResult(admitted, authority, scope) {
|
|
@@ -691,6 +691,25 @@ async function summonGateOfficer(options) {
|
|
|
691
691
|
...common
|
|
692
692
|
});
|
|
693
693
|
}
|
|
694
|
+
if (options.officer === "countersign") {
|
|
695
|
+
const { runIdFromRunDirectory } = await import("./run-terminal-artifacts.js");
|
|
696
|
+
const correlationId = runIdFromRunDirectory(options.sourceRunDirectory);
|
|
697
|
+
if (correlationId === void 0 || correlationId.trim() === "") {
|
|
698
|
+
throw new Error(
|
|
699
|
+
`countersign gate summon requires parent runId from sourceRunDirectory: ${options.sourceRunDirectory}`
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
const { readBoardTicketNumber } = await import("./run-ticket-number.js");
|
|
703
|
+
const parentTicket = await readBoardTicketNumber(options.sourceRunDirectory);
|
|
704
|
+
const instruction = options.reask ?? gateReviewInstruction ?? "";
|
|
705
|
+
return summonPublicRole({
|
|
706
|
+
role: "countersign",
|
|
707
|
+
argv: ["--project", options.cwd, "--", instruction],
|
|
708
|
+
...common,
|
|
709
|
+
correlationId,
|
|
710
|
+
...parentTicket === void 0 ? {} : { boundTicketNumber: parentTicket }
|
|
711
|
+
});
|
|
712
|
+
}
|
|
694
713
|
return summonPublicRole({
|
|
695
714
|
role: "inspector",
|
|
696
715
|
argv: [`\u5377\u5B97\u6307\u9488\uFF1A${options.sourceRunDirectory}`],
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
const SECRETARIAT_OUTPUT_TOOL_NAME = "ak_secretariat_output";
|
|
2
2
|
const SECRETARIAT_SUMMON_COUNTERSIGN_TOOL_NAME = "ak_secretariat_summon_countersign";
|
|
3
3
|
const SECRETARIAT_ACCEPTED_TEXT = "\u4E2D\u4E66\u7701\u56DE\u6267\u5DF2\u63A5\u53D7";
|
|
4
|
+
const SECRETARIAT_GATE_OFFICER_ENTRY_TYPE = "ak-secretariat-gate-officer";
|
|
5
|
+
const SECRETARIAT_COUNTERSIGN_TERMINAL_FACT_KEY = "countersignTerminal";
|
|
4
6
|
export {
|
|
5
7
|
SECRETARIAT_ACCEPTED_TEXT,
|
|
8
|
+
SECRETARIAT_COUNTERSIGN_TERMINAL_FACT_KEY,
|
|
9
|
+
SECRETARIAT_GATE_OFFICER_ENTRY_TYPE,
|
|
6
10
|
SECRETARIAT_OUTPUT_TOOL_NAME,
|
|
7
11
|
SECRETARIAT_SUMMON_COUNTERSIGN_TOOL_NAME
|
|
8
12
|
};
|
package/package.json
CHANGED
|
@@ -479,7 +479,7 @@ export const DIRECT_OFFICER_RUN_POINTER_KIND = "direct-officer-run-pointer" as c
|
|
|
479
479
|
export type DirectOfficerRunPointer = {
|
|
480
480
|
readonly version: 1;
|
|
481
481
|
readonly kind: typeof DIRECT_OFFICER_RUN_POINTER_KIND;
|
|
482
|
-
readonly officer: "inspector" | "notary" | "auditor";
|
|
482
|
+
readonly officer: "inspector" | "notary" | "auditor" | "countersign";
|
|
483
483
|
/** Absolute path to the officer session.jsonl 正本. */
|
|
484
484
|
readonly sessionFile: string;
|
|
485
485
|
/** Officer run directory when known. */
|
|
@@ -497,7 +497,7 @@ export type DirectOfficerRunPointer = {
|
|
|
497
497
|
*/
|
|
498
498
|
export function bookDirectOfficerRunPointer(options: {
|
|
499
499
|
readonly parentSessionFile: string;
|
|
500
|
-
readonly officer: "inspector" | "notary" | "auditor";
|
|
500
|
+
readonly officer: "inspector" | "notary" | "auditor" | "countersign";
|
|
501
501
|
readonly sessionFile: string;
|
|
502
502
|
readonly runDirectory?: string;
|
|
503
503
|
}): DirectOfficerRunPointer {
|
package/src/audit-escalation.ts
CHANGED
|
@@ -38,7 +38,7 @@ export type AuditEscalationToolResult = {
|
|
|
38
38
|
|
|
39
39
|
export type OfficerAuditEscalationDecision = {
|
|
40
40
|
readonly status: "escalate";
|
|
41
|
-
readonly officer?: "inspector" | "notary";
|
|
41
|
+
readonly officer?: "inspector" | "notary" | "countersign";
|
|
42
42
|
readonly reason?: unknown;
|
|
43
43
|
readonly findings?: unknown;
|
|
44
44
|
readonly conflicts?: unknown;
|
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
* parent submit → summon officer → read conclusion field
|
|
8
8
|
* pass → accept end
|
|
9
9
|
* bounce | escalate → raw officer receipt as tool result back to parent
|
|
10
|
+
* — except secretariat_verdict escalate: throw → parent audit_escalation, not raw receipt (#969)
|
|
10
11
|
* not three-state → resume officer with plain-language re-ask (no round cap)
|
|
11
12
|
* transport / no_receipt → present honestly
|
|
12
|
-
*
|
|
13
|
+
* Four pairs: countersign↔notary, judge↔auditor, worker↔inspector, secretariat↔countersign (#969).
|
|
13
14
|
* Code does not judge content, map next-step for parent, or label unreadable/unusable.
|
|
14
15
|
*/
|
|
15
16
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
@@ -17,7 +18,7 @@ import { bookDirectOfficerRunPointer } from "./archivist-record-entry.ts";
|
|
|
17
18
|
import type { HostContext, RoleTurnHost } from "./host-contracts.ts";
|
|
18
19
|
import {
|
|
19
20
|
GatekeeperDecisionError,
|
|
20
|
-
|
|
21
|
+
officerConclusionReask,
|
|
21
22
|
projectGatekeeperRun,
|
|
22
23
|
type GatekeeperPassHostActions,
|
|
23
24
|
type GatekeeperResult,
|
|
@@ -37,6 +38,8 @@ export function createDefaultGateOfficerSummon(options: {
|
|
|
37
38
|
readonly home?: string;
|
|
38
39
|
readonly packageRoot?: string;
|
|
39
40
|
readonly roleTurnHost?: RoleTurnHost;
|
|
41
|
+
/** Composition-root adapters for nested court stations (tests / #969). */
|
|
42
|
+
readonly hostAdapters?: readonly import("./public-cli/role-turn-host-resolution.ts").NamedRoleTurnHostAdapter[];
|
|
40
43
|
readonly createRunId?: () => string;
|
|
41
44
|
}): GateOfficerSummon {
|
|
42
45
|
return async (officer, sourceRunDirectory, signal, reask, submission) => {
|
|
@@ -51,6 +54,7 @@ export function createDefaultGateOfficerSummon(options: {
|
|
|
51
54
|
...(options.home === undefined ? {} : { home: options.home }),
|
|
52
55
|
...(options.packageRoot === undefined ? {} : { packageRoot: options.packageRoot }),
|
|
53
56
|
...(options.roleTurnHost === undefined ? {} : { roleTurnHost: options.roleTurnHost }),
|
|
57
|
+
...(options.hostAdapters === undefined ? {} : { hostAdapters: options.hostAdapters }),
|
|
54
58
|
...(options.createRunId === undefined ? {} : { createRunId: options.createRunId }),
|
|
55
59
|
});
|
|
56
60
|
};
|
|
@@ -92,9 +96,21 @@ function bookDirectOfficerPointer(
|
|
|
92
96
|
});
|
|
93
97
|
}
|
|
94
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Pass snapshot returned to the parent seat (#969).
|
|
101
|
+
* Carries officer receipt + nested runId so seat settlement can project the
|
|
102
|
+
* public terminal without a second authority.
|
|
103
|
+
*/
|
|
104
|
+
export type GatekeeperPassOutcome = {
|
|
105
|
+
readonly officer: GateOfficer;
|
|
106
|
+
readonly receipt: unknown;
|
|
107
|
+
readonly runId?: string;
|
|
108
|
+
};
|
|
109
|
+
|
|
95
110
|
/**
|
|
96
111
|
* Shared envelope: project gate, book officer pointer, map onto host actions.
|
|
97
112
|
* Review loop has no round cap (#753 no-round-cap).
|
|
113
|
+
* On pass, returns the officer snapshot (receipt + nested runId) for seat projection.
|
|
98
114
|
*/
|
|
99
115
|
export async function requireGatekeeperPass(options: {
|
|
100
116
|
readonly context: ExtensionContext | HostContext;
|
|
@@ -109,7 +125,7 @@ export async function requireGatekeeperPass(options: {
|
|
|
109
125
|
readonly submission?: unknown;
|
|
110
126
|
/** Lowest seam: same as runGatekeeper options.summonOfficer — offline tracers only. */
|
|
111
127
|
readonly summonOfficer?: GateOfficerSummon;
|
|
112
|
-
}): Promise<void> {
|
|
128
|
+
}): Promise<GatekeeperPassOutcome | void> {
|
|
113
129
|
let reask: string | undefined;
|
|
114
130
|
// No round cap — end only on pass, bounce/escalate-to-parent, or real failure.
|
|
115
131
|
const summonOfficer =
|
|
@@ -140,9 +156,17 @@ export async function requireGatekeeperPass(options: {
|
|
|
140
156
|
options.hostActions.failInfrastructure(error, options.context, options.toolCallId);
|
|
141
157
|
}
|
|
142
158
|
}
|
|
143
|
-
if (gatekeeper.status === "pass")
|
|
159
|
+
if (gatekeeper.status === "pass") {
|
|
160
|
+
return {
|
|
161
|
+
officer: projected.officer,
|
|
162
|
+
receipt: gatekeeper.receipt,
|
|
163
|
+
...(typeof gatekeeper.runId === "string" && gatekeeper.runId.trim() !== ""
|
|
164
|
+
? { runId: gatekeeper.runId }
|
|
165
|
+
: {}),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
144
168
|
if (gatekeeper.status === "needs_reask") {
|
|
145
|
-
reask =
|
|
169
|
+
reask = officerConclusionReask(projected.officer);
|
|
146
170
|
continue;
|
|
147
171
|
}
|
|
148
172
|
if (gatekeeper.status === "transport_failure") {
|
|
@@ -152,6 +176,14 @@ export async function requireGatekeeperPass(options: {
|
|
|
152
176
|
});
|
|
153
177
|
options.hostActions.failInfrastructure(failure, options.context, options.toolCallId);
|
|
154
178
|
}
|
|
179
|
+
// #969: 给事中上呈 ends the secretariat parent — no bind/retry (不回中书省擅改).
|
|
180
|
+
// Parent beforeAccept converts this into audit_escalation with the officer receipt.
|
|
181
|
+
if (
|
|
182
|
+
gatekeeper.status === "escalate"
|
|
183
|
+
&& options.subject.kind === "secretariat_verdict"
|
|
184
|
+
) {
|
|
185
|
+
throw new GatekeeperDecisionError(gatekeeper);
|
|
186
|
+
}
|
|
155
187
|
// bounce | escalate | no_receipt: parent stands and may resubmit. Not a run abort.
|
|
156
188
|
options.hostActions.bindSubmissionNonPass(options.toolCallId, gatekeeper);
|
|
157
189
|
throw new GatekeeperDecisionError(gatekeeper);
|
package/src/gatekeeper-role.ts
CHANGED
|
@@ -12,30 +12,49 @@ import {
|
|
|
12
12
|
} from "./package-contracts/gatekeeper-output.ts";
|
|
13
13
|
import type { PublicSummonResult } from "./public-role-summons.ts";
|
|
14
14
|
import type { TerminalResult } from "./public-cli/terminal.ts";
|
|
15
|
+
import { runIdFromRunDirectory } from "./run-terminal-artifacts.ts";
|
|
15
16
|
export const INSPECTOR_OUTPUT_TOOL = INSPECTOR_OUTPUT_TOOL_NAME;
|
|
16
17
|
export const NOTARY_OUTPUT_TOOL = "ak_notary_output";
|
|
17
18
|
|
|
18
|
-
/** Gate review officers — 台院 / 符宝郎 / 审刑院 (#753 / #756). */
|
|
19
|
-
export type GateOfficer = "inspector" | "notary" | "auditor";
|
|
19
|
+
/** Gate review officers — 台院 / 符宝郎 / 审刑院 / 给事中 (#753 / #756 / #969). */
|
|
20
|
+
export type GateOfficer = "inspector" | "notary" | "auditor" | "countersign";
|
|
20
21
|
|
|
21
22
|
/** Officer routing only — content is self-fetched via the shared run-dossier tool (#632). */
|
|
22
23
|
export type GatekeeperSubject =
|
|
23
24
|
| { readonly kind: "worker_completion" }
|
|
24
25
|
| { readonly kind: "judge_draft" }
|
|
25
26
|
| { readonly kind: "judge_compliance" }
|
|
26
|
-
| { readonly kind: "countersign_verdict" }
|
|
27
|
+
| { readonly kind: "countersign_verdict" }
|
|
28
|
+
| { readonly kind: "secretariat_verdict" };
|
|
27
29
|
|
|
28
30
|
/**
|
|
29
31
|
* Gate projection for the review queue (#753 / #750).
|
|
30
32
|
* Code only reads the conclusion field for queueing. Officer words ride as
|
|
31
33
|
* `receipt` unchanged — no findings rewrite, no unreadable/unusable label,
|
|
32
34
|
* no next-step selection for the parent.
|
|
35
|
+
* `runId` is the nested officer run id when known (summoned.runDirectory /
|
|
36
|
+
* terminal.runId) — public terminal projection consumes it (#969).
|
|
33
37
|
*/
|
|
34
38
|
export type GatekeeperResult =
|
|
35
|
-
| {
|
|
39
|
+
| {
|
|
40
|
+
readonly status: "pass";
|
|
41
|
+
readonly officer: GateOfficer;
|
|
42
|
+
readonly receipt: unknown;
|
|
43
|
+
readonly runId?: string;
|
|
44
|
+
}
|
|
36
45
|
/** bounce | escalate: both return the officer receipt to the parent (#753 / #756). */
|
|
37
|
-
| {
|
|
38
|
-
|
|
46
|
+
| {
|
|
47
|
+
readonly status: "bounce";
|
|
48
|
+
readonly officer: GateOfficer;
|
|
49
|
+
readonly receipt: unknown;
|
|
50
|
+
readonly runId?: string;
|
|
51
|
+
}
|
|
52
|
+
| {
|
|
53
|
+
readonly status: "escalate";
|
|
54
|
+
readonly officer: GateOfficer;
|
|
55
|
+
readonly receipt: unknown;
|
|
56
|
+
readonly runId?: string;
|
|
57
|
+
}
|
|
39
58
|
| {
|
|
40
59
|
/**
|
|
41
60
|
* Accepted reply whose conclusion is not pass|bounce|escalate.
|
|
@@ -45,6 +64,7 @@ export type GatekeeperResult =
|
|
|
45
64
|
readonly status: "needs_reask";
|
|
46
65
|
readonly officer: GateOfficer;
|
|
47
66
|
readonly receipt: unknown;
|
|
67
|
+
readonly runId?: string;
|
|
48
68
|
}
|
|
49
69
|
| { readonly status: "no_receipt"; readonly stage: GateOfficer; readonly reason: string; readonly facts: NoReceiptLifecycleFacts }
|
|
50
70
|
| {
|
|
@@ -64,13 +84,15 @@ export type GatekeeperNonPassResult = Extract<
|
|
|
64
84
|
function gateSeatLabel(stage: GateOfficer): string {
|
|
65
85
|
if (stage === "inspector") return "台院";
|
|
66
86
|
if (stage === "auditor") return "审刑院";
|
|
87
|
+
if (stage === "countersign") return "给事中";
|
|
67
88
|
return "符宝郎";
|
|
68
89
|
}
|
|
69
90
|
|
|
70
|
-
/** Subject kind → review officer (#753 countersign/notary, #756 judge/auditor + worker/inspector). */
|
|
91
|
+
/** Subject kind → review officer (#753 countersign/notary, #756 judge/auditor + worker/inspector, #969 secretariat/countersign). */
|
|
71
92
|
export function gateOfficerForSubject(subject: GatekeeperSubject): GateOfficer {
|
|
72
93
|
if (subject.kind === "worker_completion") return "inspector";
|
|
73
94
|
if (subject.kind === "judge_compliance") return "auditor";
|
|
95
|
+
if (subject.kind === "secretariat_verdict") return "countersign";
|
|
74
96
|
return "notary";
|
|
75
97
|
}
|
|
76
98
|
|
|
@@ -183,6 +205,14 @@ function readRecord(value: unknown): Record<string, unknown> | undefined {
|
|
|
183
205
|
* `fallbackStatus` is the terminal outcome.status when the receipt body has no status key
|
|
184
206
|
* (keeps missing-args sentinel intact as the receipt).
|
|
185
207
|
*/
|
|
208
|
+
/** Map 给事中 countersignStatus onto the shared gate queue words (#969). */
|
|
209
|
+
function gateStatusFromCountersign(status: string): string | undefined {
|
|
210
|
+
if (status === "converged") return "pass";
|
|
211
|
+
if (status === "continue") return "bounce";
|
|
212
|
+
if (status === "escalate") return "escalate";
|
|
213
|
+
return undefined;
|
|
214
|
+
}
|
|
215
|
+
|
|
186
216
|
function projectOfficerDecision(
|
|
187
217
|
officer: GateOfficer,
|
|
188
218
|
decision: unknown,
|
|
@@ -190,6 +220,26 @@ function projectOfficerDecision(
|
|
|
190
220
|
): GatekeeperResult {
|
|
191
221
|
const receipt = retainedReceipt(decision);
|
|
192
222
|
const record = readRecord(decision);
|
|
223
|
+
// 给事中: only countersignStatus, strict three-word map; never generic status,
|
|
224
|
+
// never unmapped raw word, never fallbackStatus (#969 / ADR 0040/0055).
|
|
225
|
+
if (officer === "countersign") {
|
|
226
|
+
const countersignStatus =
|
|
227
|
+
record !== undefined && typeof record.countersignStatus === "string"
|
|
228
|
+
? record.countersignStatus
|
|
229
|
+
: undefined;
|
|
230
|
+
const status =
|
|
231
|
+
typeof countersignStatus === "string"
|
|
232
|
+
? gateStatusFromCountersign(countersignStatus)
|
|
233
|
+
: undefined;
|
|
234
|
+
if (status === "pass") {
|
|
235
|
+
return { status: "pass", officer, receipt };
|
|
236
|
+
}
|
|
237
|
+
if (status === "bounce" || status === "escalate") {
|
|
238
|
+
return { status, officer, receipt };
|
|
239
|
+
}
|
|
240
|
+
return { status: "needs_reask", officer, receipt };
|
|
241
|
+
}
|
|
242
|
+
// inspector / notary / auditor: shared gate words on generic status.
|
|
193
243
|
const status =
|
|
194
244
|
(record !== undefined && typeof record.status === "string" ? record.status : undefined)
|
|
195
245
|
?? fallbackStatus;
|
|
@@ -240,6 +290,41 @@ function projectOfficerPayloads(
|
|
|
240
290
|
return projectOfficerDecision(officer, payloads[payloads.length - 1], fallbackStatus);
|
|
241
291
|
}
|
|
242
292
|
|
|
293
|
+
/** Nested officer runId from summoned.runDirectory, else terminal.runId (#969). */
|
|
294
|
+
export function officerRunIdFromSummoned(summoned: PublicSummonResult): string | undefined {
|
|
295
|
+
if (typeof summoned.runDirectory === "string" && summoned.runDirectory.trim() !== "") {
|
|
296
|
+
const fromDir = runIdFromRunDirectory(summoned.runDirectory);
|
|
297
|
+
if (fromDir !== undefined) return fromDir;
|
|
298
|
+
}
|
|
299
|
+
const terminal = summoned.terminal;
|
|
300
|
+
if (
|
|
301
|
+
terminal !== undefined
|
|
302
|
+
&& typeof terminal.runId === "string"
|
|
303
|
+
&& terminal.runId.trim() !== ""
|
|
304
|
+
) {
|
|
305
|
+
return terminal.runId;
|
|
306
|
+
}
|
|
307
|
+
return undefined;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function withOfficerRunId(
|
|
311
|
+
result: GatekeeperResult,
|
|
312
|
+
summoned: PublicSummonResult,
|
|
313
|
+
): GatekeeperResult {
|
|
314
|
+
if (
|
|
315
|
+
result.status !== "pass"
|
|
316
|
+
&& result.status !== "bounce"
|
|
317
|
+
&& result.status !== "escalate"
|
|
318
|
+
&& result.status !== "needs_reask"
|
|
319
|
+
) {
|
|
320
|
+
return result;
|
|
321
|
+
}
|
|
322
|
+
if (typeof result.runId === "string" && result.runId.trim() !== "") return result;
|
|
323
|
+
const runId = officerRunIdFromSummoned(summoned);
|
|
324
|
+
if (runId === undefined) return result;
|
|
325
|
+
return { ...result, runId };
|
|
326
|
+
}
|
|
327
|
+
|
|
243
328
|
function projectOfficerTerminal(
|
|
244
329
|
officer: GateOfficer,
|
|
245
330
|
summoned: PublicSummonResult,
|
|
@@ -279,25 +364,34 @@ function projectOfficerTerminal(
|
|
|
279
364
|
};
|
|
280
365
|
}
|
|
281
366
|
if (outcome.kind === "audit_escalation") {
|
|
282
|
-
return
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
367
|
+
return withOfficerRunId(
|
|
368
|
+
{
|
|
369
|
+
status: "escalate",
|
|
370
|
+
officer,
|
|
371
|
+
// This-court receipt only (#879) — historical rows remain on terminal.submissions.
|
|
372
|
+
receipt: thisCourt.length > 0 ? thisCourt[thisCourt.length - 1] : retainedReceipt(outcome),
|
|
373
|
+
},
|
|
374
|
+
summoned,
|
|
375
|
+
);
|
|
288
376
|
}
|
|
289
377
|
if (outcome.kind === "accepted") {
|
|
290
378
|
// outcome.status is the fixture/compat leaf: production settlement leaves
|
|
291
379
|
// it undefined once payloads are recorded, so this only matters when a
|
|
292
380
|
// caller still supplies status without any recorded payload (#836 hang).
|
|
293
|
-
return
|
|
381
|
+
return withOfficerRunId(
|
|
382
|
+
projectOfficerPayloads(officer, thisCourt, outcome.status),
|
|
383
|
+
summoned,
|
|
384
|
+
);
|
|
294
385
|
}
|
|
295
|
-
return
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
386
|
+
return withOfficerRunId(
|
|
387
|
+
{
|
|
388
|
+
status: "needs_reask",
|
|
389
|
+
officer,
|
|
390
|
+
// This-court receipt only (#879).
|
|
391
|
+
receipt: thisCourt.length > 0 ? thisCourt[thisCourt.length - 1] : retainedReceipt(outcome),
|
|
392
|
+
},
|
|
393
|
+
summoned,
|
|
394
|
+
);
|
|
301
395
|
}
|
|
302
396
|
|
|
303
397
|
/** Projection carrier for the shared submit envelope (ADR 0018). No lifecycle book here. */
|
|
@@ -311,10 +405,21 @@ export type GatekeeperProjection = {
|
|
|
311
405
|
/**
|
|
312
406
|
* Plain-language re-ask when the officer conclusion is not pass|bounce|escalate.
|
|
313
407
|
* Not a packaged engine handbook line (#755 exception for 读不出三态).
|
|
408
|
+
* inspector / notary / auditor keep shared gate words; 给事中 uses own three-state
|
|
409
|
+
* field and words so reask does not invite non-contract values (#969 / ADR 0055).
|
|
314
410
|
*/
|
|
315
411
|
export const OFFICER_CONCLUSION_REASK =
|
|
316
412
|
"上次交卷的结论不是 pass、bounce、escalate 三态之一。请重新输出,结论字段写明其一;打回或上呈的话就是给对方看的原文。" as const;
|
|
317
413
|
|
|
414
|
+
/** 给事中 re-ask: countersignStatus converged|continue|escalate only (#969). */
|
|
415
|
+
export const COUNTERSIGN_CONCLUSION_REASK =
|
|
416
|
+
"上次交卷的 countersignStatus 不是 converged、continue、escalate 三态之一。请重新输出,countersignStatus 写明其一;封驳或上呈的话就是给对方看的原文。" as const;
|
|
417
|
+
|
|
418
|
+
/** Pick the re-ask line for the officer under review. */
|
|
419
|
+
export function officerConclusionReask(officer: GateOfficer): string {
|
|
420
|
+
return officer === "countersign" ? COUNTERSIGN_CONCLUSION_REASK : OFFICER_CONCLUSION_REASK;
|
|
421
|
+
}
|
|
422
|
+
|
|
318
423
|
/**
|
|
319
424
|
* Summon (via injected seam) + project. Default summonGateOfficer drive lives
|
|
320
425
|
* on the shared envelope (`gatekeeper-pass-envelope.ts`, ADR 0018 / #675) —
|
package/src/host-contracts.ts
CHANGED
|
@@ -337,7 +337,12 @@ type HostEventHandler<K extends keyof HostEventMap> = (event: HostEventMap[K], c
|
|
|
337
337
|
export type HostEventRegistration = { [K in keyof HostEventMap]: [event: K, handler: HostEventHandler<K>] }[keyof HostEventMap];
|
|
338
338
|
|
|
339
339
|
type HostGatekeeperSubject = {
|
|
340
|
-
readonly kind:
|
|
340
|
+
readonly kind:
|
|
341
|
+
| "worker_completion"
|
|
342
|
+
| "judge_draft"
|
|
343
|
+
| "judge_compliance"
|
|
344
|
+
| "countersign_verdict"
|
|
345
|
+
| "secretariat_verdict";
|
|
341
346
|
};
|
|
342
347
|
/** Gatekeeper non-pass faces returned to parent (#836 includes transport_failure; never kill leg). */
|
|
343
348
|
type HostGatekeeperNonPass = { readonly status: "bounce" | "escalate" | "no_receipt" | "transport_failure" } & Record<string, unknown>;
|
|
@@ -388,7 +393,22 @@ export interface RoleHost {
|
|
|
388
393
|
getAllTools(): Array<{ name: string; sourceInfo?: { path?: string } }>;
|
|
389
394
|
setActiveTools(names: string[]): void;
|
|
390
395
|
getActiveTools(): string[];
|
|
391
|
-
|
|
396
|
+
/**
|
|
397
|
+
* Shared gate envelope. On pass may return officer snapshot (receipt + nested
|
|
398
|
+
* runId) for seat public-terminal projection (#969); callers may ignore it.
|
|
399
|
+
*/
|
|
400
|
+
requireGatekeeperPass?(options: {
|
|
401
|
+
context: HostContext;
|
|
402
|
+
subject: HostGatekeeperSubject;
|
|
403
|
+
signal?: AbortSignal;
|
|
404
|
+
hostActions: HostGatekeeperActions;
|
|
405
|
+
toolCallId: string;
|
|
406
|
+
submission?: unknown;
|
|
407
|
+
}): Promise<void | {
|
|
408
|
+
readonly officer: string;
|
|
409
|
+
readonly receipt: unknown;
|
|
410
|
+
readonly runId?: string;
|
|
411
|
+
}>;
|
|
392
412
|
on(event: "before_agent_start", handler: HostEventHandler<"before_agent_start">): void;
|
|
393
413
|
on(event: "input", handler: HostEventHandler<"input">): void;
|
|
394
414
|
on(event: "tool_call", handler: HostEventHandler<"tool_call">): void;
|
package/src/pi/adapter.ts
CHANGED
|
@@ -92,7 +92,11 @@ function requirePiGatekeeperPass(options: {
|
|
|
92
92
|
hostActions: HostGatekeeperActions;
|
|
93
93
|
toolCallId: string;
|
|
94
94
|
submission?: unknown;
|
|
95
|
-
}): Promise<void
|
|
95
|
+
}): Promise<void | {
|
|
96
|
+
readonly officer: string;
|
|
97
|
+
readonly receipt: unknown;
|
|
98
|
+
readonly runId?: string;
|
|
99
|
+
}> {
|
|
96
100
|
return requireGatekeeperPass({
|
|
97
101
|
context: options.context,
|
|
98
102
|
subject: options.subject,
|