@akagilnc/pi-workflow-roles 0.1.4321 → 0.1.4387

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.
Files changed (71) hide show
  1. package/README.md +4 -1
  2. package/README.zh-CN.md +5 -1
  3. package/dist/acp-host/production-host.js +1308 -587
  4. package/dist/doctor-contracts.js +2 -2
  5. package/dist/engine-detour-tool.js +2 -8
  6. package/dist/gatekeeper-role.js +4 -19
  7. package/dist/headless-host/production-host.js +2613 -1892
  8. package/dist/merger-contracts.js +2 -2
  9. package/dist/migrate-book-topology.js +34 -9
  10. package/dist/notary-contracts.js +2 -2
  11. package/dist/package-contracts/auditor-output.js +2 -2
  12. package/dist/package-contracts/fixer-output.js +2 -2
  13. package/dist/package-contracts/gatekeeper-output.js +2 -2
  14. package/dist/package-contracts/navigator-output.js +2 -2
  15. package/dist/package-contracts/terminating-infrastructure.js +7 -2
  16. package/dist/package-contracts/terminating-tools.js +6 -0
  17. package/dist/packaged-role-registry.js +20 -1
  18. package/dist/public-cli/case-dossier-delivery.js +45 -28
  19. package/dist/public-cli/countersign-run.js +439 -0
  20. package/dist/public-cli/invocation.js +10 -0
  21. package/dist/public-cli/main.js +388 -101
  22. package/dist/public-cli/option-definitions.js +14 -0
  23. package/dist/public-cli/post-admission.js +44 -49
  24. package/dist/public-cli/run-lifecycle.js +13 -1
  25. package/dist/public-cli/settlement.js +14 -53
  26. package/dist/public-role-summons.js +14 -0
  27. package/dist/secretariat-contracts.js +8 -0
  28. package/dist/sitian-appender.js +21 -4
  29. package/extensions/role-runtime.ts +1 -0
  30. package/package.json +1 -1
  31. package/scripts/build-package.mjs +1 -0
  32. package/souls/secretariat.md +26 -0
  33. package/souls/ticket-law.md +9 -0
  34. package/src/collector-tool-schemas.ts +2 -2
  35. package/src/countersign-role.ts +2 -2
  36. package/src/diarist-role.ts +2 -2
  37. package/src/doctor-contracts.ts +2 -2
  38. package/src/engine-detour-tool.ts +3 -9
  39. package/src/gatekeeper-pass-envelope.ts +35 -2
  40. package/src/gatekeeper-role.ts +8 -31
  41. package/src/gleaner-left-role.ts +2 -2
  42. package/src/host-contracts.ts +2 -1
  43. package/src/inspector-role.ts +2 -2
  44. package/src/judge-role.ts +2 -2
  45. package/src/merger-contracts.ts +2 -2
  46. package/src/notary-contracts.ts +2 -2
  47. package/src/package-contracts/auditor-output.ts +2 -2
  48. package/src/package-contracts/fixer-output.ts +2 -2
  49. package/src/package-contracts/gatekeeper-output.ts +2 -2
  50. package/src/package-contracts/navigator-output.ts +2 -2
  51. package/src/package-contracts/terminating-infrastructure.ts +13 -3
  52. package/src/package-contracts/terminating-tools.ts +9 -1
  53. package/src/packaged-role-registry.ts +20 -1
  54. package/src/public-cli/case-dossier-delivery.ts +48 -29
  55. package/src/public-cli/cli.ts +10 -0
  56. package/src/public-cli/countersign-run.ts +29 -5
  57. package/src/public-cli/invocation.ts +28 -2
  58. package/src/public-cli/option-definitions.ts +17 -0
  59. package/src/public-cli/post-admission.ts +45 -58
  60. package/src/public-cli/run-lifecycle.ts +29 -2
  61. package/src/public-cli/secretariat-run.ts +266 -0
  62. package/src/public-cli/settlement.ts +31 -55
  63. package/src/public-cli/terminal.ts +2 -1
  64. package/src/public-role-summons.ts +23 -1
  65. package/src/reviewer-role.ts +2 -2
  66. package/src/role-runtime-dependencies.ts +1 -0
  67. package/src/role-runtime.ts +203 -18
  68. package/src/secretariat-contracts.ts +27 -0
  69. package/src/secretariat-role.ts +210 -0
  70. package/src/sitian-appender.ts +36 -3
  71. package/src/worker-role.ts +2 -2
@@ -46,6 +46,7 @@ import {
46
46
  } from "./engine-detour.ts";
47
47
  import { engineSessionMaterialFromOptions } from "./package-resources/engine-material.ts";
48
48
  import { registerEngineDetourTool } from "./engine-detour-tool.ts";
49
+ import { runIdFromRunDirectory } from "./run-terminal-artifacts.ts";
49
50
  import { createReceiptDeliveryPolicy, NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, RECEIPT_DELIVERY_PROMPT } from "./receipt-delivery-policy.ts";
50
51
  import type { AnyCanonicalSkillBinding } from "./canonical-skill-binding.ts";
51
52
  import type { CollectorClock } from "./collector-evidence.ts";
@@ -84,6 +85,18 @@ import {
84
85
  DIARIST_TOOL_SPEC,
85
86
  type DiaristRuntimeDependencies,
86
87
  } from "./diarist-role.ts";
88
+ import {
89
+ SECRETARIAT_OUTPUT_TOOL_SPEC,
90
+ SECRETARIAT_SUMMON_COUNTERSIGN_TOOL_SPEC,
91
+ projectSecretariatSummonResult,
92
+ type SecretariatRuntimeDependencies,
93
+ type SecretariatSummonCountersignParameters,
94
+ } from "./secretariat-role.ts";
95
+ import {
96
+ SECRETARIAT_ACCEPTED_TEXT,
97
+ SECRETARIAT_OUTPUT_TOOL_NAME,
98
+ SECRETARIAT_SUMMON_COUNTERSIGN_TOOL_NAME,
99
+ } from "./secretariat-contracts.ts";
87
100
  import {
88
101
  DIARIST_ACCEPTED_TEXT,
89
102
  projectDiaristAmendments,
@@ -459,6 +472,7 @@ type ActivationRuntime = {
459
472
  navigator: { activate(): Promise<void> };
460
473
  auditor: { activate(): Promise<void> };
461
474
  diarist: { activate(): Promise<void> };
475
+ secretariat: { activate(): Promise<void> };
462
476
  merger(): Promise<void>;
463
477
  };
464
478
 
@@ -488,6 +502,7 @@ function activationStage(role: PackagedRole, runtime: ActivationRuntime): { id:
488
502
  case "navigator": return { id: "load-and-install", run: async () => runtime.navigator.activate() };
489
503
  case "auditor": return { id: "load-and-install", run: async () => runtime.auditor.activate() };
490
504
  case "diarist": return { id: "load-and-install", run: async () => runtime.diarist.activate() };
505
+ case "secretariat": return { id: "load-and-install", run: async () => runtime.secretariat.activate() };
491
506
  case "merger": return { id: "prepare-git-and-install", run: async () => runtime.merger() };
492
507
  }
493
508
  }
@@ -599,6 +614,7 @@ export type RoleRuntimeDependencies = {
599
614
  loadNavigatorSoul?(): Promise<string>;
600
615
  loadAuditorSoul?(): Promise<string>;
601
616
  loadDiaristSoul?(): Promise<string>;
617
+ loadSecretariatSoul?(): Promise<string>;
602
618
  loadDoctorCase?(path: string): Promise<import("./doctor-contracts.ts").DoctorCase>;
603
619
  loadMergerSoul?(): Promise<string>;
604
620
  loadMergerInput?(path: string): Promise<unknown>;
@@ -677,7 +693,8 @@ export function publicNavigatorSettlement(role: string, phase: NavigatorPhase, e
677
693
  const status = typeof details.status === "string"
678
694
  ? details.status
679
695
  : typeof details.judgeStatus === "string" ? details.judgeStatus
680
- : typeof details.countersignStatus === "string" ? details.countersignStatus : undefined;
696
+ : typeof details.countersignStatus === "string" ? details.countersignStatus
697
+ : typeof details.secretariatStatus === "string" ? details.secretariatStatus : undefined;
681
698
  if (status !== undefined && status === "escalate") {
682
699
  return { kind: "human_decision", role, phase, status };
683
700
  }
@@ -909,35 +926,46 @@ function readDiaristTicketAssertion(
909
926
  return { kind: "invalid" };
910
927
  }
911
928
 
912
- /** Run coordinates + optional pre-bound ticket from durable pages (#779). */
913
- function readDiaristRunCoordinates(ctx: HostContext): {
929
+ /** Shared durable coordinates for role tools that summon another public role. */
930
+ function readRoleRunCoordinates(ctx: HostContext, label: string): {
914
931
  readonly runDirectory: string;
915
932
  readonly projectRoot: string;
916
933
  readonly home: string;
917
- readonly boundTicketNumber?: number;
934
+ readonly admitted: Record<string, unknown>;
918
935
  } {
919
936
  const runDirectory = runDirectoryFromHostContext(ctx);
920
- if (runDirectory === undefined) {
921
- throw new Error("diarist accept requires AK_ROLE_RUN_DIR");
922
- }
937
+ if (runDirectory === undefined) throw new Error(`${label} requires AK_ROLE_RUN_DIR`);
923
938
  const admittedPath = join(runDirectory, "admitted-request.json");
924
- const admitted = JSON.parse(readFileSync(admittedPath, "utf8")) as Record<
925
- string,
926
- unknown
927
- >;
939
+ const admitted = JSON.parse(readFileSync(admittedPath, "utf8")) as Record<string, unknown>;
928
940
  if (typeof admitted.projectRoot !== "string" || admitted.projectRoot.trim() === "") {
929
- throw new Error(`diarist admitted-request missing projectRoot (${admittedPath})`);
941
+ throw new Error(`${label} admitted-request missing projectRoot (${admittedPath})`);
930
942
  }
931
- const bound =
932
- typeof admitted.ticketNumber === "number" &&
933
- Number.isSafeInteger(admitted.ticketNumber) &&
934
- admitted.ticketNumber >= 1
935
- ? admitted.ticketNumber
936
- : undefined;
937
943
  return {
938
944
  runDirectory,
939
945
  projectRoot: admitted.projectRoot,
940
946
  home: homeFromRunDirectory(runDirectory),
947
+ admitted,
948
+ };
949
+ }
950
+
951
+ /** Run coordinates + optional pre-bound ticket from durable pages (#779). */
952
+ function readDiaristRunCoordinates(ctx: HostContext): {
953
+ readonly runDirectory: string;
954
+ readonly projectRoot: string;
955
+ readonly home: string;
956
+ readonly boundTicketNumber?: number;
957
+ } {
958
+ const coordinates = readRoleRunCoordinates(ctx, "diarist accept");
959
+ const bound =
960
+ typeof coordinates.admitted.ticketNumber === "number" &&
961
+ Number.isSafeInteger(coordinates.admitted.ticketNumber) &&
962
+ coordinates.admitted.ticketNumber >= 1
963
+ ? coordinates.admitted.ticketNumber
964
+ : undefined;
965
+ return {
966
+ runDirectory: coordinates.runDirectory,
967
+ projectRoot: coordinates.projectRoot,
968
+ home: coordinates.home,
941
969
  ...(bound === undefined ? {} : { boundTicketNumber: bound }),
942
970
  };
943
971
  }
@@ -1038,6 +1066,145 @@ const COUNTERSIGN_QUEUE_STATUSES = new Set(["converged", "continue", "escalate"]
1038
1066
  const COUNTERSIGN_STATUS_REASK =
1039
1067
  "countersignStatus 不是 converged、continue、escalate 三态之一。请重新交卷,status 写明其一。" as const;
1040
1068
 
1069
+ /**
1070
+ * #924 Secretariat on the shared filed-officer envelope + non-terminating
1071
+ * summon-countersign tool (auditor dossier extension pattern).
1072
+ * Nested countersign lifecycle stays on summonPublicRole (ADR 0018).
1073
+ * #924 / 第 0 条: output tool records the receipt as submitted — no status
1074
+ * shape/value reject or reask; legality is content-layer / downstream.
1075
+ */
1076
+ export function createSecretariatRoleRuntime(
1077
+ roleHost: RoleHost,
1078
+ dependencies: SecretariatRuntimeDependencies,
1079
+ ) {
1080
+ let parentInstruction = "";
1081
+ const base = createFiledOfficerRuntime(
1082
+ roleHost,
1083
+ {
1084
+ role: "secretariat",
1085
+ tool: SECRETARIAT_OUTPUT_TOOL_SPEC,
1086
+ acceptedText: SECRETARIAT_ACCEPTED_TEXT,
1087
+ soulTag: "secretariat",
1088
+ },
1089
+ dependencies,
1090
+ );
1091
+ let summonRegistered = false;
1092
+ return {
1093
+ async activate() {
1094
+ await base.activate();
1095
+ if (!summonRegistered) {
1096
+ summonRegistered = true;
1097
+ // Capture parent prompt for default summon instruction (envelope already
1098
+ // owns soul inject; this handler only records the assignment text).
1099
+ roleHost.on("before_agent_start", (event) => {
1100
+ if (typeof event.prompt === "string" && event.prompt.trim() !== "") {
1101
+ parentInstruction = event.prompt;
1102
+ }
1103
+ });
1104
+ roleHost.registerTool({
1105
+ name: SECRETARIAT_SUMMON_COUNTERSIGN_TOOL_NAME,
1106
+ label: SECRETARIAT_SUMMON_COUNTERSIGN_TOOL_SPEC.label,
1107
+ description: SECRETARIAT_SUMMON_COUNTERSIGN_TOOL_SPEC.description,
1108
+ promptSnippet: SECRETARIAT_SUMMON_COUNTERSIGN_TOOL_SPEC.promptSnippet,
1109
+ parameters: SECRETARIAT_SUMMON_COUNTERSIGN_TOOL_SPEC.parameters as never,
1110
+ async execute(
1111
+ _toolCallId,
1112
+ parameters: SecretariatSummonCountersignParameters,
1113
+ signal,
1114
+ _onUpdate,
1115
+ ctx,
1116
+ ): Promise<HostToolResult<unknown>> {
1117
+ const fromArgs =
1118
+ typeof parameters?.instruction === "string"
1119
+ ? parameters.instruction.trim()
1120
+ : "";
1121
+ // #924: instruction comes from tool args or parent prompt only — no
1122
+ // unauthored default summons prose (票号写在传召里; no fabricated fallback).
1123
+ const instruction =
1124
+ fromArgs !== "" ? fromArgs : parentInstruction.trim();
1125
+ const correlationId = (() => {
1126
+ const runDirectory = runDirectoryFromHostContext(ctx);
1127
+ if (runDirectory === undefined) return undefined;
1128
+ return runIdFromRunDirectory(runDirectory);
1129
+ })();
1130
+ const summon = dependencies.summonCountersign;
1131
+ const summoned = summon === undefined
1132
+ ? await (async () => {
1133
+ const coordinates = readRoleRunCoordinates(ctx, "secretariat summons");
1134
+ const { summonPublicRole } = await import("./public-role-summons.ts");
1135
+ return summonPublicRole({
1136
+ role: "countersign",
1137
+ argv: ["--project", coordinates.projectRoot, "--", instruction],
1138
+ cwd: coordinates.projectRoot,
1139
+ home: coordinates.home,
1140
+ ...(signal === undefined ? {} : { signal }),
1141
+ ...(correlationId === undefined ? {} : { correlationId }),
1142
+ ...(dependencies.packageRoot === undefined
1143
+ ? {}
1144
+ : { packageRoot: dependencies.packageRoot }),
1145
+ ...(dependencies.hostAdapters === undefined
1146
+ ? {}
1147
+ : { hostAdapters: dependencies.hostAdapters }),
1148
+ });
1149
+ })()
1150
+ : await summon({
1151
+ instruction,
1152
+ cwd: ctx.cwd,
1153
+ ...(signal === undefined ? {} : { signal }),
1154
+ ...(correlationId === undefined ? {} : { correlationId }),
1155
+ ...(dependencies.home === undefined ? {} : { home: dependencies.home }),
1156
+ ...(dependencies.packageRoot === undefined
1157
+ ? {}
1158
+ : { packageRoot: dependencies.packageRoot }),
1159
+ });
1160
+ const details = projectSecretariatSummonResult(summoned);
1161
+ const outcomeKind =
1162
+ typeof details.outcomeKind === "string" ? details.outcomeKind : "unknown";
1163
+ const contentText =
1164
+ outcomeKind === "accepted" || outcomeKind === "audit_escalation"
1165
+ ? "给事中回执已送达中书省"
1166
+ : outcomeKind === "failure"
1167
+ ? "给事中传召失败"
1168
+ : outcomeKind === "no_receipt"
1169
+ ? "给事中无回执"
1170
+ : "给事中传召未得终局";
1171
+ return {
1172
+ content: [{ type: "text" as const, text: contentText }],
1173
+ details,
1174
+ };
1175
+ },
1176
+ });
1177
+ }
1178
+ const packageRequired = [
1179
+ SECRETARIAT_OUTPUT_TOOL_NAME,
1180
+ SECRETARIAT_SUMMON_COUNTERSIGN_TOOL_NAME,
1181
+ ] as const;
1182
+ const all = roleHost.getAllTools().map((tool) => tool.name);
1183
+ for (const name of packageRequired) {
1184
+ if (all.filter((item) => item === name).length !== 1) {
1185
+ throw new Error(`secretariat required tool collision or missing: ${name}`);
1186
+ }
1187
+ }
1188
+ // Host-neutral minimum package surface (#924 G6): declare package tools via
1189
+ // setActiveTools without hardcoding host builtin names. Preserve any host
1190
+ // surface already visible on getAllTools/getActiveTools so body rewrite
1191
+ // (public gh path) stays reachable on hosts that expose it.
1192
+ const priorActive = roleHost.getActiveTools();
1193
+ const hostSurface = priorActive.length > 0 ? priorActive : all;
1194
+ const nextActive = [
1195
+ ...new Set([...hostSurface, ...packageRequired]),
1196
+ ];
1197
+ roleHost.setActiveTools(nextActive);
1198
+ const active = roleHost.getActiveTools();
1199
+ for (const name of packageRequired) {
1200
+ if (!active.includes(name)) {
1201
+ throw new Error(`secretariat failed to activate required tool ${name}`);
1202
+ }
1203
+ }
1204
+ },
1205
+ };
1206
+ }
1207
+
1041
1208
  export function createCountersignRoleRuntime(
1042
1209
  roleHost: RoleHost,
1043
1210
  dependencies: CountersignRuntimeDependencies,
@@ -1694,6 +1861,15 @@ export function createRoleRuntimeExtension(
1694
1861
  },
1695
1862
  },
1696
1863
  );
1864
+ const secretariat = createSecretariatRoleRuntime(roleHost, {
1865
+ async loadSoul() {
1866
+ if (!dependencies.loadSecretariatSoul) throw new Error("Secretariat runtime dependencies are not configured");
1867
+ return dependencies.loadSecretariatSoul();
1868
+ },
1869
+ ...(dependencies.packageRoot === undefined
1870
+ ? {}
1871
+ : { packageRoot: dependencies.packageRoot }),
1872
+ });
1697
1873
  const merger = createMergerRoleRuntime(roleHost, {
1698
1874
  async loadSoul() { if (!dependencies.loadMergerSoul) throw new Error("Merger runtime dependencies are not configured"); return dependencies.loadMergerSoul(); },
1699
1875
  async loadInput(path) { if (!dependencies.loadMergerInput) throw new Error("Merger runtime dependencies are not configured"); return dependencies.loadMergerInput(path); },
@@ -1985,6 +2161,7 @@ export function createRoleRuntimeExtension(
1985
2161
  navigator,
1986
2162
  auditor,
1987
2163
  diarist,
2164
+ secretariat,
1988
2165
  merger: async () => {
1989
2166
  await merger.activate();
1990
2167
  },
@@ -2097,6 +2274,14 @@ export function createRoleRuntimeExtension(
2097
2274
  if (!engineDetourRegistered) {
2098
2275
  engineDetourRegistered = registerEngineDetourTool(roleHost, hostActions);
2099
2276
  }
2277
+ // Secretariat owns a declared active surface. The shared engine detour is
2278
+ // registered after role activation, so include it here rather than leave
2279
+ // a newly registered optional tool unreachable until a later reload.
2280
+ if (entry.role === "secretariat" && engineDetourRegistered) {
2281
+ roleHost.setActiveTools([
2282
+ ...new Set([...roleHost.getActiveTools(), ENGINE_DETOUR_TOOL_NAME]),
2283
+ ]);
2284
+ }
2100
2285
  // Worker gates ①②: arm records baseline and runs private one-shot hook uninstall (ADR 0070).
2101
2286
  // Parent session feeds #216 createRecordSession so baseline/bounce survive resume.
2102
2287
  if (entry.role === "coder" || entry.role === "fixer") {
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Public Secretariat (中书省) terminating receipt contracts.
3
+ * Public terminal domain: converged (署) | escalate (上呈).
4
+ * A countersign continue is an internal rewrite-and-resubmit fact, not a
5
+ * Secretariat terminal.
6
+ * (#924) — 原卷保真: the verdict is recognized read-only; no field
7
+ * is defaulted, rewritten, or dropped (ADR 0055).
8
+ */
9
+
10
+ export const SECRETARIAT_OUTPUT_TOOL_NAME = "ak_secretariat_output";
11
+ export const SECRETARIAT_SUMMON_COUNTERSIGN_TOOL_NAME =
12
+ "ak_secretariat_summon_countersign";
13
+ export const SECRETARIAT_ACCEPTED_TEXT = "中书省回执已接受";
14
+
15
+ export type SecretariatVerdict =
16
+ | {
17
+ secretariatStatus: "converged";
18
+ ticketNumber?: number;
19
+ note?: string;
20
+ evidence?: unknown;
21
+ }
22
+ | {
23
+ secretariatStatus: "escalate";
24
+ decisionGate?: { question: string; options: string[] };
25
+ note?: string;
26
+ evidence?: unknown;
27
+ };
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Secretariat (中书省) label, soul shape, decision-tool specs, result projection.
3
+ * Lifecycle assembly (activate, register, prompt inject, inventory) lives on the
4
+ * shared envelope — src/role-runtime.ts (ADR 0018 / #924).
5
+ */
6
+ import type { Static } from "typebox";
7
+ import { Type } from "typebox";
8
+
9
+ import { withTerminatingOutputDeclarations } from "./package-contracts/terminating-infrastructure.ts";
10
+ import type { NamedRoleTurnHostAdapter } from "./public-cli/role-turn-host-resolution.ts";
11
+ import type { PublicSummonResult } from "./public-role-summons.ts";
12
+ import {
13
+ SECRETARIAT_ACCEPTED_TEXT,
14
+ SECRETARIAT_OUTPUT_TOOL_NAME,
15
+ SECRETARIAT_SUMMON_COUNTERSIGN_TOOL_NAME,
16
+ type SecretariatVerdict,
17
+ } from "./secretariat-contracts.ts";
18
+ import { runIdFromRunDirectory } from "./run-terminal-artifacts.ts";
19
+
20
+ export {
21
+ SECRETARIAT_ACCEPTED_TEXT,
22
+ SECRETARIAT_OUTPUT_TOOL_NAME,
23
+ SECRETARIAT_SUMMON_COUNTERSIGN_TOOL_NAME,
24
+ } from "./secretariat-contracts.ts";
25
+ export type { SecretariatVerdict };
26
+
27
+ /** 中书省终局回执形状;形状指引,非 schema 闸。 */
28
+ export const secretariatVerdictSchema = withTerminatingOutputDeclarations(
29
+ Type.Object(
30
+ {
31
+ secretariatStatus: Type.Unknown({
32
+ description: "converged | escalate(终局形状指引,非闸)",
33
+ }),
34
+ ticketNumber: Type.Optional(
35
+ Type.Number({ description: "本票号;署时指向最终正文所在票" }),
36
+ ),
37
+ note: Type.Optional(Type.String({ description: "附注" })),
38
+ evidence: Type.Optional(Type.Unknown({ description: "留存证据" })),
39
+ decisionGate: Type.Optional(
40
+ Type.Object(
41
+ {
42
+ question: Type.Optional(Type.String()),
43
+ options: Type.Optional(Type.Array(Type.String())),
44
+ },
45
+ { additionalProperties: true, description: "需陛下处置的问题与选项" },
46
+ ),
47
+ ),
48
+ },
49
+ { additionalProperties: true },
50
+ ),
51
+ );
52
+ (secretariatVerdictSchema as unknown as { required: string[] }).required = [];
53
+
54
+ export type SecretariatVerdictParameters = Static<typeof secretariatVerdictSchema>;
55
+
56
+ /** 传召给事中参数;形状指引,非 schema 闸。 */
57
+ export const secretariatSummonCountersignSchema = Type.Object(
58
+ {
59
+ instruction: Type.Optional(
60
+ Type.String({
61
+ description: "传召散文;票号写在其中。空则沿用本局指令。",
62
+ }),
63
+ ),
64
+ },
65
+ { additionalProperties: true },
66
+ );
67
+ (secretariatSummonCountersignSchema as unknown as { required: string[] }).required = [];
68
+
69
+ export type SecretariatSummonCountersignParameters = Static<
70
+ typeof secretariatSummonCountersignSchema
71
+ >;
72
+
73
+ export const SECRETARIAT_OUTPUT_TOOL_SPEC = {
74
+ name: SECRETARIAT_OUTPUT_TOOL_NAME,
75
+ label: "中书省输出",
76
+ description: "中书省终局回执(署或上呈);给事中封驳须改票重送,不在此终局。",
77
+ promptSnippet: "中书省终局回执",
78
+ parameters: secretariatVerdictSchema,
79
+ } as const;
80
+
81
+ export const SECRETARIAT_SUMMON_COUNTERSIGN_TOOL_SPEC = {
82
+ name: SECRETARIAT_SUMMON_COUNTERSIGN_TOOL_NAME,
83
+ label: "传召给事中",
84
+ description: "经共享执行接缝传召给事中审票;封驳后续同一 run 再送。",
85
+ promptSnippet: "传召给事中",
86
+ parameters: secretariatSummonCountersignSchema,
87
+ } as const;
88
+
89
+ export type SecretariatSummonCountersign = (input: {
90
+ readonly instruction: string;
91
+ readonly cwd: string;
92
+ readonly signal?: AbortSignal;
93
+ readonly correlationId?: string;
94
+ readonly home?: string;
95
+ readonly packageRoot?: string;
96
+ }) => Promise<PublicSummonResult>;
97
+
98
+ export type SecretariatRuntimeDependencies = {
99
+ loadSoul(): Promise<string>;
100
+ /** Package root for nested public summons. */
101
+ packageRoot?: string;
102
+ /**
103
+ * Nested countersign summon. Production uses shared summonPublicRole;
104
+ * tests may inject a tracer. Prefer hostAdapters on the default path.
105
+ */
106
+ summonCountersign?: SecretariatSummonCountersign;
107
+ /** Optional home override for nested summons (tests). */
108
+ home?: string;
109
+ /**
110
+ * Composition-root adapter table forwarded by the default summonPublicRole
111
+ * path (tests). Production leaves unset.
112
+ */
113
+ hostAdapters?: readonly NamedRoleTurnHostAdapter[];
114
+ };
115
+
116
+ function latestObjectPayload(
117
+ payloads: readonly unknown[] | undefined,
118
+ ): Record<string, unknown> | undefined {
119
+ if (payloads === undefined || payloads.length === 0) return undefined;
120
+ const latest = payloads[payloads.length - 1];
121
+ if (latest === null || typeof latest !== "object" || Array.isArray(latest)) {
122
+ return undefined;
123
+ }
124
+ return latest as Record<string, unknown>;
125
+ }
126
+
127
+ /**
128
+ * Project nested countersign PublicSummonResult onto tool details.
129
+ * Authority: keep typed terminal kind + payloads/diagnostic intact (gatekeeper
130
+ * projectOfficerTerminal precedent / ADR 0052 / 失败诚实). No content gate.
131
+ */
132
+ export function projectSecretariatSummonResult(
133
+ summoned: PublicSummonResult,
134
+ ): Record<string, unknown> {
135
+ const terminal = summoned.terminal;
136
+ const roleOutcome = terminal?.roleOutcome;
137
+ const runDirectory = summoned.runDirectory;
138
+ const runId =
139
+ typeof runDirectory === "string" && runDirectory.trim() !== ""
140
+ ? runIdFromRunDirectory(runDirectory)
141
+ : undefined;
142
+ const base: Record<string, unknown> = {
143
+ exitCode: summoned.exitCode,
144
+ ...(runId === undefined ? {} : { runId }),
145
+ ...(runDirectory === undefined ? {} : { runDirectory }),
146
+ ...(summoned.stderr === undefined || summoned.stderr === ""
147
+ ? {}
148
+ : { stderr: summoned.stderr }),
149
+ };
150
+
151
+ if (roleOutcome === undefined) {
152
+ return { ...base, outcomeKind: "no_terminal" };
153
+ }
154
+
155
+ if (roleOutcome.kind === "accepted" || roleOutcome.kind === "audit_escalation") {
156
+ const payloads = Array.isArray(roleOutcome.payloads) ? roleOutcome.payloads : undefined;
157
+ const latest = latestObjectPayload(payloads);
158
+ const countersignStatus =
159
+ latest !== undefined && typeof latest.countersignStatus === "string"
160
+ ? latest.countersignStatus
161
+ : undefined;
162
+ return {
163
+ ...base,
164
+ outcomeKind: roleOutcome.kind,
165
+ // audit_escalation carries typed status; accepted may carry fixture/compat status.
166
+ ...("status" in roleOutcome &&
167
+ typeof roleOutcome.status === "string" &&
168
+ roleOutcome.status.length > 0
169
+ ? { status: roleOutcome.status }
170
+ : {}),
171
+ ...(countersignStatus === undefined ? {} : { countersignStatus }),
172
+ ...(latest === undefined ? {} : { receipt: latest }),
173
+ ...(payloads === undefined ? {} : { payloads }),
174
+ ...(roleOutcome.decisiveFacts === undefined
175
+ ? {}
176
+ : { decisiveFacts: roleOutcome.decisiveFacts }),
177
+ };
178
+ }
179
+
180
+ if (roleOutcome.kind === "failure") {
181
+ const payloads = Array.isArray(roleOutcome.payloads) ? roleOutcome.payloads : undefined;
182
+ return {
183
+ ...base,
184
+ outcomeKind: "failure",
185
+ diagnostic: roleOutcome.diagnostic,
186
+ ...(roleOutcome.cause === undefined ? {} : { cause: roleOutcome.cause }),
187
+ ...(roleOutcome.decisiveFacts === undefined
188
+ ? {}
189
+ : { decisiveFacts: roleOutcome.decisiveFacts }),
190
+ ...(payloads === undefined ? {} : { payloads }),
191
+ ...(latestObjectPayload(payloads) === undefined
192
+ ? {}
193
+ : { receipt: latestObjectPayload(payloads) }),
194
+ };
195
+ }
196
+
197
+ // no_receipt (and any future kind still carries decisiveFacts when present)
198
+ return {
199
+ ...base,
200
+ outcomeKind: roleOutcome.kind,
201
+ ...("status" in roleOutcome &&
202
+ typeof roleOutcome.status === "string" &&
203
+ roleOutcome.status.length > 0
204
+ ? { status: roleOutcome.status }
205
+ : {}),
206
+ ...("decisiveFacts" in roleOutcome && roleOutcome.decisiveFacts !== undefined
207
+ ? { decisiveFacts: roleOutcome.decisiveFacts }
208
+ : {}),
209
+ };
210
+ }
@@ -210,6 +210,22 @@ type SitianRecordPath = {
210
210
  readonly ledgerHome: string;
211
211
  };
212
212
 
213
+ /** Sole records leaf under every sitian volume directory. */
214
+ const SITIAN_RECORDS_LEAF = "records.jsonl" as const;
215
+
216
+ /**
217
+ * Ticket-provenance under-book paths (docs/dossier-topology.md sole authority).
218
+ * Writer ticket branch and owner-visible shape share this join — one code path.
219
+ */
220
+ function ticketProvenanceUnderBookPaths(
221
+ ledgerHome: string,
222
+ bookKey: string,
223
+ ticketId: string,
224
+ ): { sessionDir: string; recordFile: string } {
225
+ const sessionDir = join(activationBookDirectory(ledgerHome, bookKey), ticketId);
226
+ return { sessionDir, recordFile: join(sessionDir, SITIAN_RECORDS_LEAF) };
227
+ }
228
+
213
229
  /** Pure topology owner shared by ambient writes and explicit-home submission reads. */
214
230
  export function resolveSitianRecordPathInLedger(
215
231
  input: SitianRecordInput,
@@ -228,13 +244,16 @@ export function resolveSitianRecordPathInLedger(
228
244
  : undefined;
229
245
 
230
246
  let sessionDir: string;
247
+ let recordFile: string;
231
248
  if (ticketNumber !== undefined) {
232
249
  // docs/dossier-topology.md: ticket dir holds the unique records.jsonl directly (#900).
233
- const bookDir = activationBookDirectory(
250
+ const paths = ticketProvenanceUnderBookPaths(
234
251
  ledgerHome,
235
252
  resolveBookKeyFromGit(input.cwd ?? process.cwd()),
253
+ ticketNumber,
236
254
  );
237
- sessionDir = join(bookDir, ticketNumber);
255
+ sessionDir = paths.sessionDir;
256
+ recordFile = paths.recordFile;
238
257
  } else {
239
258
  if (
240
259
  input.sessionParent === undefined
@@ -244,12 +263,26 @@ export function resolveSitianRecordPathInLedger(
244
263
  throw new Error("Sitian record ownership requires a parent session inside the ledger home");
245
264
  }
246
265
  sessionDir = join(dirname(input.sessionParent), category);
266
+ recordFile = join(sessionDir, SITIAN_RECORDS_LEAF);
247
267
  }
248
268
 
249
- const recordFile = join(sessionDir, "records.jsonl");
250
269
  return { sessionDir, recordFile, ledgerHome };
251
270
  }
252
271
 
272
+ /**
273
+ * Owner-visible ticket 起居录 path shape via the writer ticket joins
274
+ * (ticketProvenanceUnderBookPaths). Ledger leaf is the package-owned `.ak-roles`
275
+ * name (ADR 0048); variable slots keep owner labels; no fake-home path reverse.
276
+ */
277
+ export function projectTicketRecordsPathShape(): string {
278
+ const { recordFile } = ticketProvenanceUnderBookPaths(
279
+ join("~", ".ak-roles"),
280
+ "<簿>",
281
+ "<票号>",
282
+ );
283
+ return recordFile.replace(/\\/g, "/");
284
+ }
285
+
253
286
  /** Compute a write destination from ambient ledger topology (ADR 0065). */
254
287
  export function resolveSitianRecordPath(input: SitianRecordInput): SitianRecordPath {
255
288
  const ledgerHome =
@@ -1,7 +1,7 @@
1
1
  import type { RoleHost, HostContext, HostToolResult, HostGatekeeperActions } from "./host-contracts.ts";
2
2
  import { Type, type Static } from "typebox";
3
3
  import { openToolObjectFromUnion } from "./open-tool-schema.ts";
4
- import { withInfrastructureFailureDeclaration } from "./package-contracts/terminating-infrastructure.ts";
4
+ import { withTerminatingOutputDeclarations } from "./package-contracts/terminating-infrastructure.ts";
5
5
  import { CorrectableSubmissionError } from "./submission-correctable-error.ts";
6
6
 
7
7
  import type {
@@ -73,7 +73,7 @@ const coderOutputVariants = Type.Union([
73
73
  })),
74
74
  }, { additionalProperties: false }),
75
75
  ]);
76
- export const coderOutputSchema = withInfrastructureFailureDeclaration(
76
+ export const coderOutputSchema = withTerminatingOutputDeclarations(
77
77
  openToolObjectFromUnion(coderOutputVariants),
78
78
  );
79
79
  export type { FixerOutput, CoderOutput };