@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.
@@ -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
- return settleLawfulSeatAcceptedTerminalResult(admitted, authority, {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akagilnc/pi-workflow-roles",
3
- "version": "0.1.4631",
3
+ "version": "0.1.4646",
4
4
  "description": "Soul-bound workflow roles for Pi",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -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 {
@@ -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
- * Three pairs: countersign↔notary, judge↔auditor, worker↔inspector.
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
- OFFICER_CONCLUSION_REASK,
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") return;
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 = OFFICER_CONCLUSION_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);
@@ -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
- | { readonly status: "pass"; readonly officer: GateOfficer; readonly receipt: unknown }
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
- | { readonly status: "bounce"; readonly officer: GateOfficer; readonly receipt: unknown }
38
- | { readonly status: "escalate"; readonly officer: GateOfficer; readonly receipt: unknown }
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
- status: "escalate",
284
- officer,
285
- // This-court receipt only (#879) — historical rows remain on terminal.submissions.
286
- receipt: thisCourt.length > 0 ? thisCourt[thisCourt.length - 1] : retainedReceipt(outcome),
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 projectOfficerPayloads(officer, thisCourt, outcome.status);
381
+ return withOfficerRunId(
382
+ projectOfficerPayloads(officer, thisCourt, outcome.status),
383
+ summoned,
384
+ );
294
385
  }
295
- return {
296
- status: "needs_reask",
297
- officer,
298
- // This-court receipt only (#879).
299
- receipt: thisCourt.length > 0 ? thisCourt[thisCourt.length - 1] : retainedReceipt(outcome),
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) —
@@ -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: "worker_completion" | "judge_draft" | "judge_compliance" | "countersign_verdict";
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
- requireGatekeeperPass?(options: { context: HostContext; subject: HostGatekeeperSubject; signal?: AbortSignal; hostActions: HostGatekeeperActions; toolCallId: string; submission?: unknown }): Promise<void>;
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,