@akagilnc/pi-workflow-roles 0.1.4631 → 0.1.4641

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.
@@ -88,6 +88,22 @@ export type CountersignRunEnv = PostAdmissionEnv & {
88
88
  * replace — never union. Production leaves unset outside that handoff.
89
89
  */
90
90
  pendingCourtTicketNumbers?: readonly number[];
91
+ /**
92
+ * #969 gate path: plain-language re-ask when prior 给事中 reply was not three-state.
93
+ * Wins over gateReviewInstruction when both present (notary/inspector precedent).
94
+ */
95
+ reviewReask?: string;
96
+ /**
97
+ * #969/#879 Secretariat submission-gate body: verbatim parent payload as dialogue
98
+ * content when reviewReask is absent. Binding stays ticket / argv instruction.
99
+ */
100
+ gateReviewInstruction?: string;
101
+ /**
102
+ * #969 gate path: parent run durable board ticket handoff. Same-ticket resume
103
+ * key and identity 起居郎 bind under this key — never re-derived from receipt
104
+ * ticketNumber (optional / omit-able payload field).
105
+ */
106
+ boundTicketNumber?: number;
91
107
  };
92
108
 
93
109
  /** Project admitted invocation onto the host-neutral turn request. */
@@ -470,6 +486,10 @@ export async function runPublicCountersign(
470
486
  instruction: parsed.instruction,
471
487
  projectRoot: admitted.projectRoot,
472
488
  failureLabel: "unbound summons",
489
+ // #969: parent durable ticket is the resume/bind key (ADR 0079).
490
+ ...(env.boundTicketNumber === undefined
491
+ ? {}
492
+ : { boundTicketNumber: env.boundTicketNumber }),
473
493
  },
474
494
  env,
475
495
  io,
@@ -528,16 +548,30 @@ export async function runPublicCountersign(
528
548
  }
529
549
 
530
550
  // Missing lawful 起居郎 terminal is not countersign body failure: leave
531
- // unbound and continue (true-unbound face). Escalate / typed failure above;
532
- // bound refresh still fails honest via runCountersignCourtDiaristStation.
551
+ // unbound and continue (true-unbound face) — unless parent gate handoff
552
+ // already carries the durable board ticket (#969). Escalate / typed
553
+ // failure above; bound refresh still fails honest via station.
533
554
  if (outcome.identity.kind === "ticket") {
534
- const assertedTicket = outcome.identity.ticketNumber;
535
- typedTicket = assertedTicket;
555
+ // #969: parent durable handoff wins over 起居郎 re-assert when both
556
+ // present (receipt/assert mismatch → parent resume key).
557
+ typedTicket = isSafePositiveTicketNumber(env.boundTicketNumber)
558
+ ? env.boundTicketNumber
559
+ : outcome.identity.ticketNumber;
536
560
  // #871: identity may hand a co-review set; absent field defaults to [main].
537
561
  typedCourtTicketNumbers = outcome.identity.courtTicketNumbers;
562
+ } else if (isSafePositiveTicketNumber(env.boundTicketNumber)) {
563
+ // Parent board handoff present; 起居郎 returned true-unbound — still
564
+ // resume/bind under the parent key (omitted receipt ticketNumber path).
565
+ typedTicket = env.boundTicketNumber;
566
+ }
567
+ if (typedTicket !== undefined) {
568
+ // #969/#879: gate re-ask / parent submission body share summons.instruction
569
+ // (reask wins; argv instruction remains the 起居郎 identity face above).
570
+ const resumeInstruction =
571
+ env.reviewReask ?? env.gateReviewInstruction ?? parsed.instruction;
538
572
  const summons: SameTicketSummonsMaterials = {
539
- instruction: parsed.instruction,
540
- instructionEmpty: parsed.instruction.trim() === "",
573
+ instruction: resumeInstruction,
574
+ instructionEmpty: resumeInstruction.trim() === "",
541
575
  };
542
576
  const resumed = await tryResumeSameTicketSeatRun({
543
577
  home: env.home,
@@ -641,13 +675,16 @@ export async function runPublicCountersign(
641
675
  : { correlationId: env.correlationId }),
642
676
  continuation: {
643
677
  kind: "initial",
644
- prompt: buildCountersignTransportPrompt(
645
- admitted,
646
- engineSessionMaterialFromOptions({
647
- ...pickEngineAxis(env),
648
- packageRoot: env.packageRoot,
649
- }),
650
- ),
678
+ // #969/#879: gate path first mint carries parent payload as dialogue content;
679
+ // ordinary public entry keeps package transport prompt.
680
+ prompt: (env.reviewReask ?? env.gateReviewInstruction)
681
+ ?? buildCountersignTransportPrompt(
682
+ admitted,
683
+ engineSessionMaterialFromOptions({
684
+ ...pickEngineAxis(env),
685
+ packageRoot: env.packageRoot,
686
+ }),
687
+ ),
651
688
  },
652
689
  };
653
690
  // Mutable shell: ticket bind re-projects activation before executeTurn.
@@ -94,6 +94,8 @@ import {
94
94
  DIARIST_OUTPUT_TOOL_NAME,
95
95
  } from "../diarist-contracts.ts";
96
96
  import {
97
+ SECRETARIAT_COUNTERSIGN_TERMINAL_FACT_KEY,
98
+ SECRETARIAT_GATE_OFFICER_ENTRY_TYPE,
97
99
  SECRETARIAT_OUTPUT_TOOL_NAME,
98
100
  } from "../secretariat-contracts.ts";
99
101
  import {
@@ -3333,15 +3335,129 @@ export async function trySettleDiaristTerminalResult(
3333
3335
  return settleLawfulDiaristTerminalResult(admitted, authority, scope);
3334
3336
  }
3335
3337
 
3338
+ /**
3339
+ * #969 seat projection sole authority: 给事中 terminal (署|上呈) booked as a
3340
+ * durable custom entry (envelope-safe) — ledger accepted stays LLM params (#836).
3341
+ * Never scan toolResult rows: those are memory-only on headless/ACP (#617/#959).
3342
+ * Receipt bytes stay original; nested runId rides beside them.
3343
+ * Caller must only apply this when the terminal-defining submission actually
3344
+ * produced the officer entry (gate-bound converged, or 给事中 audit_escalation).
3345
+ */
3346
+ function countersignTerminalFromEntries(
3347
+ entries: readonly {
3348
+ type?: string;
3349
+ customType?: string;
3350
+ data?: unknown;
3351
+ }[],
3352
+ ): import("../secretariat-contracts.ts").SecretariatCountersignTerminalFact | undefined {
3353
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
3354
+ const entry = entries[index];
3355
+ if (entry?.type !== "custom") continue;
3356
+ if (entry.customType !== SECRETARIAT_GATE_OFFICER_ENTRY_TYPE) continue;
3357
+ const data =
3358
+ entry.data !== null && typeof entry.data === "object" && !Array.isArray(entry.data)
3359
+ ? (entry.data as Record<string, unknown>)
3360
+ : undefined;
3361
+ if (data?.officer !== "countersign") continue;
3362
+ if (data.receipt === undefined) continue;
3363
+ const runId =
3364
+ typeof data.runId === "string" && data.runId.trim() !== ""
3365
+ ? data.runId
3366
+ : undefined;
3367
+ return {
3368
+ receipt: data.receipt,
3369
+ ...(runId === undefined ? {} : { runId }),
3370
+ };
3371
+ }
3372
+ return undefined;
3373
+ }
3374
+
3375
+ function withCountersignTerminalFact(
3376
+ roleOutcome: Extract<
3377
+ import("./terminal.ts").TerminalRoleOutcome,
3378
+ { kind: "accepted" | "audit_escalation" }
3379
+ >,
3380
+ officer: import("../secretariat-contracts.ts").SecretariatCountersignTerminalFact,
3381
+ ): typeof roleOutcome {
3382
+ const prior =
3383
+ roleOutcome.decisiveFacts !== undefined && isRecord(roleOutcome.decisiveFacts)
3384
+ ? roleOutcome.decisiveFacts
3385
+ : {};
3386
+ return {
3387
+ ...roleOutcome,
3388
+ decisiveFacts: {
3389
+ ...prior,
3390
+ [SECRETARIAT_COUNTERSIGN_TERMINAL_FACT_KEY]: officer,
3391
+ },
3392
+ };
3393
+ }
3394
+
3395
+ /**
3396
+ * Terminal-defining secretariat submission owns officer projection.
3397
+ * Last readable secretariatStatus on accepted payloads decides: only gate-bound
3398
+ * converged projects a prior durable officer entry. Parent escalate bypasses the
3399
+ * gate and must not inherit a stale pass entry from an earlier turn (#969).
3400
+ */
3401
+ function acceptedSecretariatDefinesOfficerProjection(
3402
+ roleOutcome: Extract<
3403
+ import("./terminal.ts").TerminalRoleOutcome,
3404
+ { kind: "accepted" }
3405
+ >,
3406
+ ): boolean {
3407
+ const payloads = roleOutcome.payloads ?? [];
3408
+ for (let index = payloads.length - 1; index >= 0; index -= 1) {
3409
+ const payload = payloads[index];
3410
+ if (!isRecord(payload)) continue;
3411
+ const status = payload.secretariatStatus;
3412
+ if (typeof status !== "string") continue;
3413
+ return status === "converged";
3414
+ }
3415
+ return false;
3416
+ }
3417
+
3336
3418
  async function settleLawfulSecretariatTerminalResult(
3337
3419
  admitted: AdmittedSecretariatInvocation,
3338
3420
  authority: DurablePrincipalAuthority,
3339
3421
  scope?: SettlementCourtScope,
3340
3422
  ): Promise<TerminalResult | undefined> {
3341
- return settleLawfulSeatAcceptedTerminalResult(admitted, authority, {
3423
+ const settled = await settleLawfulSeatAcceptedTerminalResult(admitted, authority, {
3342
3424
  role: "secretariat",
3343
3425
  toolName: SECRETARIAT_OUTPUT_TOOL_NAME,
3344
3426
  }, scope);
3427
+ if (settled === undefined) return undefined;
3428
+ const coordinates = coordinatesFromAdmitted(authority, admitted);
3429
+ const entries = await readLawfulSettlementEntries(coordinates.sessionFile) ?? [];
3430
+ const officer = countersignTerminalFromEntries(entries);
3431
+ if (officer === undefined) return settled;
3432
+
3433
+ // 给事中上呈: public payloads = officer receipt; runId + receipt fact beside.
3434
+ // This kind is produced only when beforeAccept booked the officer entry.
3435
+ if (settled.roleOutcome.kind === "audit_escalation") {
3436
+ return {
3437
+ ...settled,
3438
+ roleOutcome: withCountersignTerminalFact(
3439
+ {
3440
+ ...settled.roleOutcome,
3441
+ payloads: [officer.receipt],
3442
+ },
3443
+ officer,
3444
+ ),
3445
+ };
3446
+ }
3447
+
3448
+ // Pass (署): keep 中书省 payloads; present 给事中 判词 + runId via decisiveFacts.
3449
+ // Parent escalate accepted terminal must not project a stale prior pass entry.
3450
+ if (settled.roleOutcome.kind === "accepted") {
3451
+ if (!acceptedSecretariatDefinesOfficerProjection(settled.roleOutcome)) {
3452
+ return settled;
3453
+ }
3454
+ return {
3455
+ ...settled,
3456
+ roleOutcome: withCountersignTerminalFact(settled.roleOutcome, officer),
3457
+ };
3458
+ }
3459
+
3460
+ return settled;
3345
3461
  }
3346
3462
 
3347
3463
  /** Try to settle a lawful Secretariat Terminal; undefined only for genuine absence. */
@@ -1040,9 +1040,9 @@ export async function summonParallelReviewerLenses(options: {
1040
1040
  return results;
1041
1041
  }
1042
1042
 
1043
- /** Gate officer summons: notary/auditor via --source-run; inspector via pointer instruction. */
1043
+ /** Gate officer summons: notary/auditor via --source-run; inspector via pointer; countersign via ticket (#969). */
1044
1044
  export async function summonGateOfficer(options: {
1045
- readonly officer: "inspector" | "notary" | "auditor";
1045
+ readonly officer: "inspector" | "notary" | "auditor" | "countersign";
1046
1046
  readonly sourceRunDirectory: string;
1047
1047
  readonly cwd: string;
1048
1048
  readonly home?: string;
@@ -1052,14 +1052,14 @@ export async function summonGateOfficer(options: {
1052
1052
  readonly signal?: AbortSignal;
1053
1053
  /**
1054
1054
  * Plain-language re-ask when the prior officer reply was not three-state (#753 / #756).
1055
- * All three officers: reviewReask → same-ticket resume summons.instruction.
1055
+ * Officers: reviewReask → same-ticket resume summons.instruction.
1056
1056
  * Never concatenated into argv (inspector parentRunPath is the pure 卷宗指针).
1057
1057
  */
1058
1058
  readonly reask?: string;
1059
1059
  /**
1060
1060
  * In-flight parent 交卷 body (tool-call arguments). Relayed verbatim as officer
1061
1061
  * dialogue content when reask is absent (#879). Binding pointer stays on
1062
- * --source-run / 卷宗指针 argv — never a content substitute.
1062
+ * --source-run / 卷宗指针 / ticket argv — never a content substitute.
1063
1063
  */
1064
1064
  readonly submission?: unknown;
1065
1065
  /** Pi-adapter inject — forwarded to summonPublicRole (not a parent-host override). */
@@ -1121,6 +1121,32 @@ export async function summonGateOfficer(options: {
1121
1121
  ...common,
1122
1122
  });
1123
1123
  }
1124
+ if (options.officer === "countersign") {
1125
+ // #969: Secretariat submission gate → 给事中.
1126
+ // Dialogue / identity instruction = parent typed payload 原话 (ADR 0079) or reask;
1127
+ // no code-authored summons prose (#924). Ticket identity = parent run durable
1128
+ // board binding only (readBoardTicketNumber) — receipt ticketNumber is payload
1129
+ // content, never the sole identity source; parent true-unbound stays unbound.
1130
+ // correlationId only on this officer — notary/auditor/inspector stay untouched.
1131
+ const { runIdFromRunDirectory } = await import("./run-terminal-artifacts.ts");
1132
+ const correlationId = runIdFromRunDirectory(options.sourceRunDirectory);
1133
+ if (correlationId === undefined || correlationId.trim() === "") {
1134
+ throw new Error(
1135
+ `countersign gate summon requires parent runId from sourceRunDirectory: ${options.sourceRunDirectory}`,
1136
+ );
1137
+ }
1138
+ const { readBoardTicketNumber } = await import("./run-ticket-number.ts");
1139
+ const parentTicket = await readBoardTicketNumber(options.sourceRunDirectory);
1140
+ // Argv instruction = reask or parent payload bytes only (never a fabricated line).
1141
+ const instruction = options.reask ?? gateReviewInstruction ?? "";
1142
+ return summonPublicRole({
1143
+ role: "countersign",
1144
+ argv: ["--project", options.cwd, "--", instruction],
1145
+ ...common,
1146
+ correlationId,
1147
+ ...(parentTicket === undefined ? {} : { boundTicketNumber: parentTicket }),
1148
+ });
1149
+ }
1124
1150
  // Inspector: argv stays pure 卷宗指针 (#747 parentRunPath lookup key).
1125
1151
  // Content (body/reask) rides env → summons.instruction / first-mint prompt.
1126
1152
  return summonPublicRole({
@@ -9,7 +9,10 @@ import {
9
9
  ENGINE_MODEL_FLAG_NAME,
10
10
  normalizeEngineName,
11
11
  } from "./engine-detour.ts";
12
- import { requireGatekeeperPass } from "./gatekeeper-pass-envelope.ts";
12
+ import {
13
+ createDefaultGateOfficerSummon,
14
+ requireGatekeeperPass,
15
+ } from "./gatekeeper-pass-envelope.ts";
13
16
  import type {
14
17
  HostContext,
15
18
  HostEventRegistration,
@@ -229,18 +232,32 @@ export async function prepareRoleEnvelope(options: {
229
232
  // seat remains reachable through MCP.
230
233
  setActiveTools(names) { preferredTools = [...names]; },
231
234
  getActiveTools() { return [...preferredTools]; },
232
- async requireGatekeeperPass(options) {
233
- await requireGatekeeperPass({
234
- context: options.context,
235
- subject: options.subject,
236
- ...(options.signal === undefined ? {} : { signal: options.signal }),
235
+ async requireGatekeeperPass(gateOptions) {
236
+ const packageRoot =
237
+ typeof options.dependencies.packageRoot === "string"
238
+ ? options.dependencies.packageRoot
239
+ : undefined;
240
+ return requireGatekeeperPass({
241
+ context: gateOptions.context,
242
+ subject: gateOptions.subject,
243
+ ...(gateOptions.signal === undefined ? {} : { signal: gateOptions.signal }),
237
244
  hostActions: {
238
245
  failInfrastructure: (error, _context, toolCallId) =>
239
- options.hostActions.failInfrastructure(error, options.context, toolCallId),
240
- bindSubmissionNonPass: options.hostActions.bindSubmissionNonPass,
246
+ gateOptions.hostActions.failInfrastructure(error, gateOptions.context, toolCallId),
247
+ bindSubmissionNonPass: gateOptions.hostActions.bindSubmissionNonPass,
241
248
  },
242
- toolCallId: options.toolCallId,
243
- ...(options.submission === undefined ? {} : { submission: options.submission }),
249
+ toolCallId: gateOptions.toolCallId,
250
+ ...(gateOptions.submission === undefined ? {} : { submission: gateOptions.submission }),
251
+ // #969: package root + home reach nested 给事中/符宝郎 summons.
252
+ // hostAdapters (when set on deps) forward nested seat selection — production unset.
253
+ summonOfficer: createDefaultGateOfficerSummon({
254
+ cwd: gateOptions.context.cwd ?? request.cwd,
255
+ home: request.home,
256
+ ...(packageRoot === undefined ? {} : { packageRoot }),
257
+ ...(options.dependencies.hostAdapters === undefined
258
+ ? {}
259
+ : { hostAdapters: options.dependencies.hostAdapters }),
260
+ }),
244
261
  });
245
262
  },
246
263
  on(...registration: HostEventRegistration) {
@@ -314,15 +314,23 @@ export {
314
314
  writeToolExecutionObservationRecord,
315
315
  } from "./tool-execution-observation.ts";
316
316
  export type { ToolExecutionObservationRecord, ToolExecutionObservationWriter } from "./tool-execution-observation.ts";
317
- export {
317
+ import {
318
318
  NOTARY_OUTPUT_TOOL,
319
319
  INSPECTOR_OUTPUT_TOOL,
320
320
  GatekeeperDecisionError,
321
321
  createGatekeeperOutputTool,
322
322
  runGatekeeper,
323
+ gateOfficerForSubject,
323
324
  } from "./gatekeeper-role.ts";
325
+ export {
326
+ NOTARY_OUTPUT_TOOL,
327
+ INSPECTOR_OUTPUT_TOOL,
328
+ GatekeeperDecisionError,
329
+ createGatekeeperOutputTool,
330
+ runGatekeeper,
331
+ gateOfficerForSubject,
332
+ };
324
333
  export type { GatekeeperResult, GatekeeperSubject, GatekeeperNonPassResult, GateOfficer, RunGatekeeperOptions } from "./gatekeeper-role.ts";
325
- export { gateOfficerForSubject } from "./gatekeeper-role.ts";
326
334
  import { ParentQueueReaskError } from "./submission-errors.ts";
327
335
 
328
336
  export {
@@ -546,6 +554,13 @@ type NavigatorAttendanceDependency = Omit<NavigatorAttendance, "knownRoutePlaybo
546
554
  export type RoleRuntimeDependencies = {
547
555
  /** Package root for packaged engine-note resolution (#879). */
548
556
  packageRoot?: string;
557
+ /**
558
+ * Composition-root adapter table for nested gate officer summons (#969).
559
+ * Production leaves unset (default pi + packaged externals). Tests inject
560
+ * faux nested hosts so prepareRoleEnvelope.requireGatekeeperPass is bitten
561
+ * without reimplementing the envelope summon closure.
562
+ */
563
+ hostAdapters?: readonly import("./public-cli/role-turn-host-resolution.ts").NamedRoleTurnHostAdapter[];
549
564
  /** Non-identity opening materials delivered in the ordinary role brief. */
550
565
  loadRoleReferenceMaterials?(role: PackagedRole): Promise<string>;
551
566
  loadJudgeSoul(): Promise<string>;
@@ -999,18 +1014,137 @@ const COUNTERSIGN_QUEUE_STATUSES = new Set(["converged", "continue", "escalate"]
999
1014
  const COUNTERSIGN_STATUS_REASK =
1000
1015
  "countersignStatus 不是 converged、continue、escalate 三态之一。请重新交卷,status 写明其一。" as const;
1001
1016
 
1017
+ /**
1018
+ * #969 authorized non-pi hosts: Secretariat converged is a candidate ticket —
1019
+ * AK-side submission gate summons 给事中. pi keeps mid-turn summon tool only.
1020
+ */
1021
+ const SECRETARIAT_SUBMISSION_GATE_HOSTS = new Set(["codex", "claude", "grok-build"]);
1022
+
1023
+ /** Secretariat status words the non-pi submission gate reads (#969). */
1024
+ const SECRETARIAT_QUEUE_STATUSES = new Set(["converged", "escalate"]);
1025
+
1026
+ /**
1027
+ * Plain-language re-ask when non-pi secretariatStatus is not a known queue word.
1028
+ * Back to secretariat itself — 给事中 is not summoned (#969 / ADR 0055).
1029
+ */
1030
+ const SECRETARIAT_STATUS_REASK =
1031
+ "secretariatStatus 不是 converged、escalate 之一。请重新交卷,status 写明其一。" as const;
1032
+
1002
1033
  /**
1003
1034
  * #924 Secretariat on the shared filed-officer envelope + non-terminating
1004
1035
  * summon-countersign tool (auditor dossier extension pattern).
1005
1036
  * Nested countersign lifecycle stays on summonPublicRole (ADR 0018).
1006
- * #924 / 第 0 条: output tool records the receipt as submitted — no status
1037
+ * #924 / 第 0 条 (pi): output tool records the receipt as submitted — no status
1007
1038
  * shape/value reject or reask; legality is content-layer / downstream.
1039
+ * #969 (codex/claude/grok-build): converged arms 既有交卷闸 → 给事中; escalate
1040
+ * skips the gate; other values reask secretariat (ADR 0055).
1008
1041
  */
1009
1042
  export function createSecretariatRoleRuntime(
1010
1043
  roleHost: RoleHost,
1011
1044
  dependencies: SecretariatRuntimeDependencies,
1045
+ hostActions?: import("./host-contracts.ts").HostGatekeeperActions,
1012
1046
  ) {
1013
1047
  let parentInstruction = "";
1048
+ // #969: non-pi submission gate only — pi mid-turn summon path stays untouched.
1049
+ const beforeAccept: FiledOfficerBeforeAccept | undefined =
1050
+ hostActions !== undefined && roleHost.requireGatekeeperPass !== undefined
1051
+ ? async ({ toolCallId, parameters, signal, ctx }) => {
1052
+ const host =
1053
+ typeof ctx.host === "string" && ctx.host.trim() !== ""
1054
+ ? ctx.host.trim()
1055
+ : undefined;
1056
+ if (host === undefined || !SECRETARIAT_SUBMISSION_GATE_HOSTS.has(host)) {
1057
+ return undefined;
1058
+ }
1059
+ const record =
1060
+ parameters !== null && typeof parameters === "object" && !Array.isArray(parameters)
1061
+ ? (parameters as Record<string, unknown>)
1062
+ : undefined;
1063
+ const status =
1064
+ record !== undefined && typeof record.secretariatStatus === "string"
1065
+ ? record.secretariatStatus
1066
+ : undefined;
1067
+ if (status === undefined || !SECRETARIAT_QUEUE_STATUSES.has(status)) {
1068
+ throw new ParentQueueReaskError(SECRETARIAT_STATUS_REASK);
1069
+ }
1070
+ if (status === "escalate") {
1071
+ // Parent escalate → throw to caller as-is; 给事中 does not attend (#969).
1072
+ return undefined;
1073
+ }
1074
+ try {
1075
+ const pass = await roleHost.requireGatekeeperPass!({
1076
+ context: ctx,
1077
+ subject: { kind: "secretariat_verdict" },
1078
+ ...(signal === undefined ? {} : { signal }),
1079
+ hostActions,
1080
+ toolCallId,
1081
+ // #879: this-turn typed payload — identity-bound at submit site.
1082
+ submission: parameters,
1083
+ });
1084
+ // #969: book 给事中 署 snapshot for seat settlement projection (receipt + runId).
1085
+ // Ledger accepted stays LLM params (#836); public terminal reads this entry.
1086
+ if (
1087
+ pass !== undefined
1088
+ && pass !== null
1089
+ && typeof pass === "object"
1090
+ && "receipt" in pass
1091
+ ) {
1092
+ const {
1093
+ SECRETARIAT_GATE_OFFICER_ENTRY_TYPE,
1094
+ } = await import("./secretariat-contracts.ts");
1095
+ const runId =
1096
+ typeof pass.runId === "string" && pass.runId.trim() !== ""
1097
+ ? pass.runId
1098
+ : undefined;
1099
+ ctx.sessionManager.appendCustomEntry?.(SECRETARIAT_GATE_OFFICER_ENTRY_TYPE, {
1100
+ officer: "countersign",
1101
+ receipt: pass.receipt,
1102
+ ...(runId === undefined ? {} : { runId }),
1103
+ });
1104
+ }
1105
+ return undefined;
1106
+ } catch (error) {
1107
+ // #969: 给事中上呈 ends parent with officer receipt (no retry / 不擅改).
1108
+ if (
1109
+ error instanceof GatekeeperDecisionError
1110
+ && error.result.status === "escalate"
1111
+ ) {
1112
+ const receipt = error.result.receipt;
1113
+ const receiptRecord =
1114
+ receipt !== null && typeof receipt === "object" && !Array.isArray(receipt)
1115
+ ? (receipt as Record<string, unknown>)
1116
+ : undefined;
1117
+ const runId =
1118
+ typeof error.result.runId === "string" && error.result.runId.trim() !== ""
1119
+ ? error.result.runId
1120
+ : undefined;
1121
+ // Durable on every host: envelope persists custom entries only
1122
+ // (toolResult rows are memory-only on headless/ACP — #617/#959).
1123
+ const {
1124
+ SECRETARIAT_GATE_OFFICER_ENTRY_TYPE,
1125
+ } = await import("./secretariat-contracts.ts");
1126
+ ctx.sessionManager.appendCustomEntry?.(SECRETARIAT_GATE_OFFICER_ENTRY_TYPE, {
1127
+ officer: "countersign",
1128
+ receipt,
1129
+ ...(runId === undefined ? {} : { runId }),
1130
+ });
1131
+ const { buildAuditEscalationResult } = await import("./audit-escalation.ts");
1132
+ return buildAuditEscalationResult(
1133
+ {
1134
+ status: "escalate",
1135
+ officer: "countersign",
1136
+ ...(receiptRecord !== undefined
1137
+ && Object.prototype.hasOwnProperty.call(receiptRecord, "decisionGate")
1138
+ ? { decisionGate: receiptRecord.decisionGate }
1139
+ : {}),
1140
+ },
1141
+ receipt,
1142
+ );
1143
+ }
1144
+ throw error;
1145
+ }
1146
+ }
1147
+ : undefined;
1014
1148
  const base = createFiledOfficerRuntime(
1015
1149
  roleHost,
1016
1150
  {
@@ -1018,6 +1152,7 @@ export function createSecretariatRoleRuntime(
1018
1152
  tool: SECRETARIAT_OUTPUT_TOOL_SPEC,
1019
1153
  acceptedText: SECRETARIAT_ACCEPTED_TEXT,
1020
1154
  soulTag: "secretariat",
1155
+ ...(beforeAccept === undefined ? {} : { beforeAccept }),
1021
1156
  },
1022
1157
  dependencies,
1023
1158
  );
@@ -1858,7 +1993,10 @@ export function createRoleRuntimeExtension(
1858
1993
  ...(dependencies.packageRoot === undefined
1859
1994
  ? {}
1860
1995
  : { packageRoot: dependencies.packageRoot }),
1861
- });
1996
+ ...(dependencies.hostAdapters === undefined
1997
+ ? {}
1998
+ : { hostAdapters: dependencies.hostAdapters }),
1999
+ }, hostActions);
1862
2000
  const merger = createMergerRoleRuntime(roleHost, {
1863
2001
  async loadSoul() { if (!dependencies.loadMergerSoul) throw new Error("Merger runtime dependencies are not configured"); return dependencies.loadMergerSoul(); },
1864
2002
  async loadInput(path) { if (!dependencies.loadMergerInput) throw new Error("Merger runtime dependencies are not configured"); return dependencies.loadMergerInput(path); },
@@ -11,6 +11,24 @@ export const SECRETARIAT_OUTPUT_TOOL_NAME = "ak_secretariat_output";
11
11
  export const SECRETARIAT_SUMMON_COUNTERSIGN_TOOL_NAME =
12
12
  "ak_secretariat_summon_countersign";
13
13
  export const SECRETARIAT_ACCEPTED_TEXT = "中书省回执已接受";
14
+ /**
15
+ * #969 durable custom entry: 给事中 terminal (署|上呈) for seat settlement projection.
16
+ * Carries original receipt bytes + nested runId. Envelope persists custom entries
17
+ * (not toolResult rows) — headless/ACP safe. Single authority for public terminal
18
+ * officer projection (pass and escalate share this entry).
19
+ */
20
+ export const SECRETARIAT_GATE_OFFICER_ENTRY_TYPE =
21
+ "ak-secretariat-gate-officer" as const;
22
+
23
+ /** Decisive-facts key for nested 给事中 terminal on public Secretariat settlement. */
24
+ export const SECRETARIAT_COUNTERSIGN_TERMINAL_FACT_KEY = "countersignTerminal" as const;
25
+
26
+ export type SecretariatCountersignTerminalFact = {
27
+ /** Officer receipt original bytes — never rewritten. */
28
+ readonly receipt: unknown;
29
+ /** Nested 给事中 runId when known. */
30
+ readonly runId?: string;
31
+ };
14
32
 
15
33
  export type SecretariatVerdict =
16
34
  | {