@akagilnc/pi-workflow-roles 0.1.5295 → 0.1.5301

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.
@@ -14,7 +14,6 @@ import { CliUsageError } from "./cli-errors.js";
14
14
  import { readLatestTypedProviderHttpObservation, } from "../typed-provider-http.js";
15
15
  export { clearTypedProviderHttpObservation, recordTypedProviderHttpStatus, readLatestTypedProviderHttpObservation, } from "../typed-provider-http.js";
16
16
  import { parseCollectorRepository } from "../collector-config.js";
17
- import { interpretDurableCourtTicketNumbers, sameCourtTicketNumbers, } from "../diarist-contracts.js";
18
17
  import { rewriteAdmittedRoleRunPage, rewriteRunDirectoryPathValue, } from "../role-run-relocation.js";
19
18
  import { appendEngineSessionMaterial, engineSessionMaterialFromOptions, pickEngineAxis, } from "../package-resources/engine-material.js";
20
19
  import { packagedResumeSourcePath, packagedRoleMetadata, } from "../packaged-role-registry.js";
@@ -865,64 +864,10 @@ function parsePersistedTicketIdentity(record) {
865
864
  ...(ticketNumber === undefined ? {} : { ticketNumber }),
866
865
  };
867
866
  }
868
- /**
869
- * #871 durable set from one run page.
870
- * Absent → absent. Present damage keeps the original reason (no throw here).
871
- */
872
- function readDurableCourtTicketNumbersPage(record, principalTicket, pageLabel) {
873
- const interpreted = interpretDurableCourtTicketNumbers(record, {
874
- ...(principalTicket === undefined ? {} : { principalTicket }),
875
- });
876
- if (interpreted.kind === "damage") {
877
- return {
878
- kind: "damage",
879
- reason: `role run ${pageLabel} courtTicketNumbers is damaged: ${interpreted.reason}`,
880
- };
881
- }
882
- if (interpreted.kind === "ok") {
883
- return { kind: "ok", tickets: interpreted.tickets };
884
- }
885
- return { kind: "absent" };
886
- }
887
- /**
888
- * Merge admitted + invocation #871 sets.
889
- * Both present must match; one present wins; both absent stays legacy.
890
- * Damage / cross-page mismatch return a diagnostic — countersign resume settles
891
- * controlled failure; load itself does not structural-reject.
892
- */
893
- function mergeDurableCourtTicketNumbers(input) {
894
- const fromAdmitted = input.admittedRecord === undefined
895
- ? { kind: "absent" }
896
- : readDurableCourtTicketNumbersPage(input.admittedRecord, input.principalTicket, "admitted-request");
897
- const fromInvocation = input.invocationRecord === undefined
898
- ? { kind: "absent" }
899
- : readDurableCourtTicketNumbersPage(input.invocationRecord, input.principalTicket, "invocation");
900
- if (fromAdmitted.kind === "damage")
901
- return fromAdmitted;
902
- if (fromInvocation.kind === "damage")
903
- return fromInvocation;
904
- if (fromAdmitted.kind === "ok" && fromInvocation.kind === "ok") {
905
- if (!sameCourtTicketNumbers(fromAdmitted.tickets, fromInvocation.tickets)) {
906
- return {
907
- kind: "damage",
908
- reason: "role run courtTicketNumbers differs between admitted-request and invocation",
909
- };
910
- }
911
- return { kind: "ok", tickets: fromAdmitted.tickets };
912
- }
913
- if (fromAdmitted.kind === "ok")
914
- return { kind: "ok", tickets: fromAdmitted.tickets };
915
- if (fromInvocation.kind === "ok")
916
- return { kind: "ok", tickets: fromInvocation.tickets };
917
- return { kind: "ok" };
918
- }
919
867
  function restoredTicketFields(fields) {
920
868
  return {
921
869
  ...(fields.correlationId === undefined ? {} : { correlationId: fields.correlationId }),
922
870
  ...(fields.ticketNumber === undefined ? {} : { ticketNumber: fields.ticketNumber }),
923
- ...(fields.courtTicketNumbers === undefined
924
- ? {}
925
- : { courtTicketNumbers: fields.courtTicketNumbers }),
926
871
  };
927
872
  }
928
873
  async function loadResumableRunRecord(home, runId, authority) {
@@ -958,10 +903,6 @@ async function loadResumableRunRecord(home, runId, authority) {
958
903
  let derived;
959
904
  let correlationId;
960
905
  let ticketNumber;
961
- let courtTicketNumbers;
962
- let courtTicketNumbersDamage;
963
- let admittedIdentityRecord;
964
- let invocationIdentityRecord;
965
906
  let prNumber;
966
907
  let repository;
967
908
  let repositoryDisplay;
@@ -1106,7 +1047,6 @@ async function loadResumableRunRecord(home, runId, authority) {
1106
1047
  };
1107
1048
  }
1108
1049
  }
1109
- admittedIdentityRecord = record;
1110
1050
  const fromAdmitted = parsePersistedTicketIdentity(record);
1111
1051
  correlationId = fromAdmitted.correlationId;
1112
1052
  ticketNumber = fromAdmitted.ticketNumber;
@@ -1125,7 +1065,6 @@ async function loadResumableRunRecord(home, runId, authority) {
1125
1065
  typeof invocationRaw === "object" &&
1126
1066
  !Array.isArray(invocationRaw)) {
1127
1067
  const rec = invocationRaw;
1128
- invocationIdentityRecord = rec;
1129
1068
  if (typeof rec.provider === "string" && typeof rec.model === "string") {
1130
1069
  model = {
1131
1070
  provider: rec.provider,
@@ -1154,20 +1093,6 @@ async function loadResumableRunRecord(home, runId, authority) {
1154
1093
  throw new CliUsageError(`role run invocation identity is unreadable: ${runId}`, { cause: error });
1155
1094
  }
1156
1095
  }
1157
- // #871: resolve set after main ticket so principal membership can be checked.
1158
- // Present-but-damaged / cross-page mismatch keep diagnostic for countersign
1159
- // controlled-failure; both-absent stays legacy; single lawful page restores.
1160
- const mergedSet = mergeDurableCourtTicketNumbers({
1161
- admittedRecord: admittedIdentityRecord,
1162
- invocationRecord: invocationIdentityRecord,
1163
- principalTicket: ticketNumber,
1164
- });
1165
- if (mergedSet.kind === "damage") {
1166
- courtTicketNumbersDamage = mergedSet.reason;
1167
- }
1168
- else if (mergedSet.tickets !== undefined) {
1169
- courtTicketNumbers = mergedSet.tickets;
1170
- }
1171
1096
  const sourceRunLeaf = basename(sourceRunPath ?? "").split("@");
1172
1097
  const referencedRunId = sourceRun?.runId ?? sourceRunLeaf[0];
1173
1098
  const referencedRole = sourceRun?.role ?? sourceRunLeaf[1];
@@ -1200,10 +1125,6 @@ async function loadResumableRunRecord(home, runId, authority) {
1200
1125
  ...(derived === undefined ? {} : { derived }),
1201
1126
  ...(correlationId === undefined ? {} : { correlationId }),
1202
1127
  ...(ticketNumber === undefined ? {} : { ticketNumber }),
1203
- ...(courtTicketNumbers === undefined ? {} : { courtTicketNumbers }),
1204
- ...(courtTicketNumbersDamage === undefined
1205
- ? {}
1206
- : { courtTicketNumbersDamage }),
1207
1128
  ...(prNumber === undefined ? {} : { prNumber }),
1208
1129
  ...(repository === undefined ? {} : { repository }),
1209
1130
  ...(repositoryDisplay === undefined ? {} : { repositoryDisplay }),
@@ -1296,12 +1217,6 @@ function admitResumedRole(loaded) {
1296
1217
  const admitted = {
1297
1218
  role: "countersign",
1298
1219
  ...base,
1299
- ...(base.courtTicketNumbers === undefined
1300
- ? {}
1301
- : { courtTicketNumbers: base.courtTicketNumbers }),
1302
- ...(fields.courtTicketNumbersDamage === undefined
1303
- ? {}
1304
- : { courtTicketNumbersDamage: fields.courtTicketNumbersDamage }),
1305
1220
  };
1306
1221
  return admitted;
1307
1222
  }
@@ -198,8 +198,8 @@ async function summonPublicRole(options) {
198
198
  // Nested court stations keep station-child semantics; dual-lens
199
199
  // ordinary Reviewer axes opt out (#946 / ADR 0082).
200
200
  ...options.stationChild === false ? {} : { stationChild: true },
201
- // Forward composition-root adapters so nested court stations (e.g.
202
- // countersign → diarist) select the same faux/production table (#924).
201
+ // Forward composition-root adapters so nested stations (e.g.
202
+ // secretariat → diarist) select the same faux/production table (#924).
203
203
  ...options.hostAdapters === void 0 ? {} : { hostAdapters: options.hostAdapters },
204
204
  ...config.autoResumeLimit === void 0 ? {} : { autoResumeLimit: config.autoResumeLimit },
205
205
  ...options.signal === void 0 ? {} : { signal: options.signal },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akagilnc/pi-workflow-roles",
3
- "version": "0.1.5295",
3
+ "version": "0.1.5301",
4
4
  "description": "Soul-bound workflow roles for Pi",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -11,10 +11,7 @@ import {
11
11
  projectTicketProvenanceSessions,
12
12
  type TicketProvenanceSession,
13
13
  } from "./ticket-provenance-contracts.ts";
14
- import {
15
- isSafePositiveTicketNumber,
16
- requireSafePositiveTicketNumber,
17
- } from "./run-ticket-number.ts";
14
+ import { isSafePositiveTicketNumber } from "./run-ticket-number.ts";
18
15
 
19
16
  export const DIARIST_OUTPUT_TOOL_NAME = "ak_diarist_output";
20
17
 
@@ -27,15 +24,6 @@ export type DiaristOutput =
27
24
  * LLM owns recognition; mechanical layer does not re-judge the number.
28
25
  */
29
26
  readonly ticketNumber?: number | null;
30
- /**
31
- * #871 typed co-review set (ADR 0075 `diarist-resolves-ticket-llm-layer` narrow extension).
32
- * Positive integers = tickets that should each receive a diary this court (includes main).
33
- * Field absent → no new set this turn (resume keeps stored run fact; first court defaults to [main]).
34
- * Explicit empty array → single-ticket face [main] whole-set replace.
35
- * Non-empty with zero lawful members after type projection is not a new set (call site).
36
- * Mechanical layer only type-projects/dedupes and guarantees principal membership when applying a set.
37
- */
38
- readonly courtTicketNumbers?: readonly number[] | null;
39
27
  /**
40
28
  * Dialogue bounds: one entry per session volume, each with one or more ranges.
41
29
  * Empty / absent = lawful empty selection (no dialogue this turn).
@@ -54,109 +42,6 @@ export type DiaristOutput =
54
42
  readonly reason: string;
55
43
  };
56
44
 
57
- /**
58
- * Sole shape projection for a typed co-review ticket set (#871).
59
- * Positive safe integers, first-seen order, duplicates dropped. Not content judgment.
60
- * Returns null when `raw` is not an array (callers classify absent vs durable damage).
61
- * Returns a possibly empty list when `raw` is an array (unlawful members skipped).
62
- * When `principalTicket` is set, that ticket is guaranteed as a member (prepended if missing).
63
- */
64
- export function projectCourtTicketNumbers(
65
- raw: unknown,
66
- options?: { readonly principalTicket?: number },
67
- ): readonly number[] | null {
68
- if (!Array.isArray(raw)) return null;
69
- const out: number[] = [];
70
- const seen = new Set<number>();
71
- for (const item of raw) {
72
- if (!isSafePositiveTicketNumber(item)) continue;
73
- if (seen.has(item)) continue;
74
- seen.add(item);
75
- out.push(item);
76
- }
77
- const principal = options?.principalTicket;
78
- if (principal === undefined) return out;
79
- requireSafePositiveTicketNumber(principal, "projectCourtTicketNumbers principalTicket");
80
- if (seen.has(principal)) return out;
81
- return [principal, ...out];
82
- }
83
-
84
- /**
85
- * Durable-field interpreter for #871 co-review sets.
86
- * Absent key (old-format run) is not damage. Present but empty / non-array /
87
- * partially unlawful / missing principal is damage — callers must fail closed,
88
- * never wash into main-only.
89
- */
90
- export function interpretDurableCourtTicketNumbers(
91
- record: Record<string, unknown>,
92
- options?: { readonly principalTicket?: number },
93
- ):
94
- | { readonly kind: "absent" }
95
- | { readonly kind: "ok"; readonly tickets: readonly number[] }
96
- | { readonly kind: "damage"; readonly reason: string } {
97
- if (!Object.hasOwn(record, "courtTicketNumbers")) {
98
- return { kind: "absent" };
99
- }
100
- const raw = record.courtTicketNumbers;
101
- if (!Array.isArray(raw)) {
102
- return {
103
- kind: "damage",
104
- reason: "courtTicketNumbers is present but not an array",
105
- };
106
- }
107
- if (raw.length === 0) {
108
- return {
109
- kind: "damage",
110
- reason: "courtTicketNumbers is present but empty",
111
- };
112
- }
113
- for (let i = 0; i < raw.length; i += 1) {
114
- const item = raw[i];
115
- if (!isSafePositiveTicketNumber(item)) {
116
- return {
117
- kind: "damage",
118
- reason: `courtTicketNumbers[${i}] is not a safe positive integer`,
119
- };
120
- }
121
- }
122
- // Members are all lawful numbers; sole projection owns dedupe/order.
123
- const tickets = projectCourtTicketNumbers(raw);
124
- if (tickets === null || tickets.length === 0) {
125
- return {
126
- kind: "damage",
127
- reason: "courtTicketNumbers projected empty after lawful members",
128
- };
129
- }
130
- const principal = options?.principalTicket;
131
- if (principal !== undefined) {
132
- if (!isSafePositiveTicketNumber(principal)) {
133
- return {
134
- kind: "damage",
135
- reason: "courtTicketNumbers principal ticket is not a safe positive integer",
136
- };
137
- }
138
- if (!tickets.includes(principal)) {
139
- return {
140
- kind: "damage",
141
- reason: `courtTicketNumbers is missing principal ticket #${principal}`,
142
- };
143
- }
144
- }
145
- return { kind: "ok", tickets: Object.freeze([...tickets]) };
146
- }
147
-
148
- /** True when two durable #871 sets are byte-identical sequences. */
149
- export function sameCourtTicketNumbers(
150
- left: readonly number[],
151
- right: readonly number[],
152
- ): boolean {
153
- if (left.length !== right.length) return false;
154
- for (let i = 0; i < left.length; i += 1) {
155
- if (left[i] !== right[i]) return false;
156
- }
157
- return true;
158
- }
159
-
160
45
  export function validateRecordedDiaristOutput(value: unknown): DiaristOutput {
161
46
  if (value === null || typeof value !== "object" || Array.isArray(value)) {
162
47
  throw new Error("Diarist output has no execution discriminator");
@@ -63,12 +63,6 @@ export const diaristOutputSchema = withTerminatingOutputDeclarations(
63
63
  "本庭对象票号(正整数)或 null/省略=真无票;下游走 typed 键;认不出用 status=escalate,不洗成无录。机械层不重判。",
64
64
  }),
65
65
  ),
66
- courtTicketNumbers: Type.Optional(
67
- Type.Unknown({
68
- description:
69
- "#871 本次合审应各自成录的票号集合(正整数数组,含主票);单票可省略。机械层只做类型/去重投影,不从散文猜票。",
70
- }),
71
- ),
72
66
  reason: Type.Optional(
73
67
  Type.String({
74
68
  description: "status 为 escalate 时:认不出本庭对象的原因",
@@ -1,40 +1,17 @@
1
1
  /**
2
- * Court diarist station for the shared countersign entry (#572 / ADR 0074 /
3
- * ADR 0075 / #742 / #771). The public seat itself admits and settles through
4
- * instruction-seat-run. This module only summons 起居郎 and refreshes a bound
5
- * court diary.
6
- *
7
- * Court admission auto-runs 起居郎 so the 起居郎 LLM asserts the court target;
8
- * mechanical layer only verifies; countersign reuses that typed identity for bind
9
- * (ADR 0075 / 0081). Code never matches instruction text against book-known
10
- * numbers. Who may call 起居郎 and in what order is not written into law
11
- * (ADR 0075 不规定谁调用起居郎、顺序归调用者). #1092: code no longer freezes or
12
- * delivers 起居录 paths — roles locate records by ticket. Bound refresh hands the
13
- * typed key to 起居郎 so freeze loads issue face (ADR 0075: 每次过庭都跑是调用者用法 /
14
- * typed handoff).
2
+ * Secretariat's pre-ticket diarist summons. Countersign no longer summons
3
+ * the diarist; callers refresh the diary before court when needed (ADR 0081).
15
4
  */
16
5
  import type { DurablePrincipalAuthority } from "../host-contracts.ts";
17
- import { projectCourtTicketNumbers } from "../diarist-contracts.ts";
18
6
  import { readableGateItem } from "../readable-gate-item.ts";
19
- import { isSafePositiveTicketNumber } from "../run-ticket-number.ts";
20
7
  import type { AdmittedCountersignInvocation } from "./invocation.ts";
21
- import {
22
- type PostAdmissionEnv,
23
- StationChildExhaustedError,
24
- } from "./post-admission.ts";
8
+ import type { PostAdmissionEnv } from "./post-admission.ts";
25
9
  import type { CliIo } from "./cli-io.ts";
26
10
  import type { TerminalRoleOutcome } from "./terminal.ts";
27
11
 
28
12
  export type CountersignRunEnv = PostAdmissionEnv & {
29
13
  principalAuthority: DurablePrincipalAuthority;
30
14
  createRunId?: () => string;
31
- /**
32
- * Test seam: replace the court-pipeline 起居郎 station.
33
- * Production leaves this unset and runs the public diarist seat.
34
- */
35
- runCourtDiaristStation?: (
36
- admitted: AdmittedCountersignInvocation,
37
- ) => Promise<void>;
38
15
  /**
39
16
  * #969 gate path: plain-language re-ask when prior 给事中 reply was not three-state.
40
17
  * Wins over gateReviewInstruction when both present (notary/inspector precedent).
@@ -60,16 +37,6 @@ export type CountersignRunEnv = PostAdmissionEnv & {
60
37
 
61
38
  /** 起居郎 identity outcome — escalate stays distinct from missing terminal. */
62
39
  export type CourtDiaristIdentity =
63
- | {
64
- readonly kind: "ticket";
65
- readonly ticketNumber: number;
66
- /**
67
- * #871 typed co-review set when the LLM explicitly submitted one.
68
- * Absent means "no new set this turn" — resume must keep the stored run fact
69
- * (never silently degrade a multi-ticket set to [main]).
70
- */
71
- readonly courtTicketNumbers?: readonly number[];
72
- }
73
40
  | { readonly kind: "unbound" }
74
41
  | {
75
42
  readonly kind: "escalate";
@@ -141,7 +108,7 @@ function courtDiaristEscalateDiagnostic(
141
108
  })}`;
142
109
  }
143
110
 
144
- /** Env slice shared by court diarist identity summons (countersign / secretariat). */
111
+ /** Env slice for the secretariat's pre-ticket diarist summons. */
145
112
  export type CourtDiaristSummonEnv = Pick<
146
113
  CountersignRunEnv,
147
114
  | "cwd"
@@ -153,53 +120,6 @@ export type CourtDiaristSummonEnv = Pick<
153
120
  | "hostAdapters"
154
121
  >;
155
122
 
156
- /**
157
- * Read #871 set from preserved diarist payloads.
158
- * - Field absent / null / non-array → undefined (no new set; resume keeps store).
159
- * - Explicit [] → single-ticket [main] whole-set replace (signed empty-set contract).
160
- * - Non-empty array with zero lawful typed members after projection → not a new set
161
- * (do not impersonate explicit empty and wipe a stored multi-ticket fact).
162
- * - Non-empty array with ≥1 lawful member → sole type/dedupe projection + principal guarantee.
163
- * - Multiple submissions: last qualifying set wins.
164
- * Live path never shape-rejects the role turn; durable damage is a separate seam.
165
- */
166
- export function courtTicketNumbersFromOutcome(
167
- roleOutcome: TerminalRoleOutcome | undefined,
168
- principalTicket: number,
169
- submissions?: readonly unknown[],
170
- ): readonly number[] | undefined {
171
- if (roleOutcome === undefined) return undefined;
172
- const payloads = courtDiaristPayloadRows(roleOutcome, submissions);
173
- let latest: readonly number[] | undefined;
174
- for (const payload of payloads) {
175
- if (
176
- typeof payload !== "object" ||
177
- payload === null ||
178
- Array.isArray(payload)
179
- ) {
180
- continue;
181
- }
182
- const record = payload as Record<string, unknown>;
183
- if (!Object.hasOwn(record, "courtTicketNumbers")) continue;
184
- const raw = record.courtTicketNumbers;
185
- // Only a real array is a candidate set. Non-array is not explicit empty.
186
- if (!Array.isArray(raw)) continue;
187
- // Members only — do not inject principal yet, or all-invalid non-empty becomes [main].
188
- const members = projectCourtTicketNumbers(raw);
189
- if (members === null) continue;
190
- if (members.length === 0) {
191
- // Literal [] is the intentional principal-only replace; non-empty garbage is not.
192
- if (raw.length === 0) {
193
- latest =
194
- projectCourtTicketNumbers([], { principalTicket }) ?? undefined;
195
- }
196
- continue;
197
- }
198
- latest = projectCourtTicketNumbers(raw, { principalTicket }) ?? undefined;
199
- }
200
- return latest;
201
- }
202
-
203
123
  /** Latest payload only. An earlier escalate must not block a later non-escalate submission. */
204
124
  export function latestPayloadEscalated(
205
125
  roleOutcome: TerminalRoleOutcome | undefined,
@@ -223,11 +143,8 @@ export function courtDiaristEscalated(
223
143
 
224
144
  /**
225
145
  * Invoke public 起居郎 under the court-pipeline quiet face.
226
- * Returns typed identity: ticket assertion, true-unbound, or escalate.
227
- * Non-zero exit without escalate status is not rethrown here — caller decides
228
- * whether refresh must fail or first-entry settles controlled failure.
229
- * When `boundTicketNumber` is set (typed handoff from countersign), diarist
230
- * binds under that key before the turn — never mechanical recognition from prose.
146
+ * Returns escalate or unbound; the secretariat needs no ticket handoff.
147
+ * Non-zero exit without escalate status is not rethrown here — caller settles controlled failure.
231
148
  */
232
149
  export async function invokeCourtDiarist(
233
150
  input: {
@@ -237,13 +154,11 @@ export async function invokeCourtDiarist(
237
154
  readonly attachmentPaths?: readonly string[];
238
155
  /** Direct parent run id for durable child lineage (ADR 0010). */
239
156
  readonly correlationId?: string;
240
- /** Already-verified typed key from countersign (refresh / post-assert handoff). */
241
- readonly boundTicketNumber?: number;
242
157
  },
243
158
  env: CourtDiaristSummonEnv,
244
159
  io: CliIo,
245
160
  ): Promise<CourtDiaristInvocationResult> {
246
- // Quiet face: the countersign caller must not see diarist CLI chatter.
161
+ // Quiet face: the secretariat caller must not see diarist CLI chatter.
247
162
  const quietIo: CliIo = {
248
163
  stdout() {},
249
164
  stderr(text: string) {
@@ -272,9 +187,6 @@ export async function invokeCourtDiarist(
272
187
  ...(input.correlationId === undefined
273
188
  ? {}
274
189
  : { correlationId: input.correlationId }),
275
- ...(input.boundTicketNumber === undefined
276
- ? {}
277
- : { boundTicketNumber: input.boundTicketNumber }),
278
190
  // Child seat selects from the composition-root table. Do not pass the
279
191
  // already-selected parent adapter (#840 / ADR 0082: --host 旗标>席位配置>缺省 pi).
280
192
  ...(env.hostAdapters === undefined
@@ -311,97 +223,8 @@ export async function invokeCourtDiarist(
311
223
  };
312
224
  }
313
225
 
314
- const asserted = (result.admitted as { ticketNumber?: number } | undefined)
315
- ?.ticketNumber;
316
- if (isSafePositiveTicketNumber(asserted)) {
317
- const courtTicketNumbers = courtTicketNumbersFromOutcome(
318
- roleOutcome,
319
- asserted,
320
- submissions,
321
- );
322
- return {
323
- identity: {
324
- kind: "ticket",
325
- ticketNumber: asserted,
326
- ...(courtTicketNumbers === undefined ? {} : { courtTicketNumbers }),
327
- },
328
- ...(result.admitted === undefined ? {} : { admitted: result.admitted }),
329
- };
330
- }
331
226
  return {
332
227
  identity: { kind: "unbound" },
333
228
  ...(result.admitted === undefined ? {} : { admitted: result.admitted }),
334
229
  };
335
230
  }
336
-
337
- /**
338
- * Court-pipeline prior station: refresh this ticket's 起居录 before the
339
- * countersign body turn when already bound (ADR 0075 每次过庭都跑)。
340
- * Caller-invisible — no diarist argv on the countersign command line.
341
- *
342
- * Missing ticketNumber (true-unbound / identity deferred) skips the refresh
343
- * station — no diary is minted for a true-unbound run. First-entry identity
344
- * lives on the shared countersign entry (typed 起居郎 key for bind). Bound refresh:
345
- * technical failure propagates; LLM escalation remains a recorded conclusion.
346
- * Path delivery onto materials is not this station's job — post-admission owns it.
347
- */
348
- export async function runCountersignCourtDiaristStation(
349
- admitted: AdmittedCountersignInvocation,
350
- env: CountersignRunEnv,
351
- io: CliIo,
352
- afterTicketNumber?: number,
353
- ): Promise<{ readonly terminal: import("./terminal.ts").TerminalResult; readonly childRunId?: string } | undefined> {
354
- if (env.runCourtDiaristStation !== undefined) {
355
- await env.runCourtDiaristStation(admitted);
356
- return;
357
- }
358
- // Production court refresh (ADR 0075: 每次过庭都跑是调用者用法) only under a known ticket identity.
359
- // First-entry unbound identity is owned by the shared countersign entry.
360
- if (admitted.ticketNumber === undefined) return;
361
-
362
- // #871: refresh every member of the typed co-review set; single-ticket face
363
- // is just the set [main]. Present-but-empty / missing-principal is damage —
364
- // never silently fall back to main-only (legacy absent field still may).
365
- let refreshTickets: readonly number[];
366
- if (admitted.courtTicketNumbers !== undefined) {
367
- if (
368
- admitted.courtTicketNumbers.length === 0 ||
369
- !admitted.courtTicketNumbers.includes(admitted.ticketNumber)
370
- ) {
371
- throw new StationChildExhaustedError(
372
- `court diarist station: courtTicketNumbers is damaged (empty or missing principal #${admitted.ticketNumber})`,
373
- );
374
- }
375
- refreshTickets = admitted.courtTicketNumbers;
376
- } else {
377
- refreshTickets = [admitted.ticketNumber];
378
- }
379
-
380
- const completedIndex = afterTicketNumber === undefined ? -1 : refreshTickets.indexOf(afterTicketNumber);
381
- for (const ticketNumber of refreshTickets.slice(completedIndex + 1)) {
382
- const outcome = await invokeCourtDiarist(
383
- {
384
- instruction: admitted.instruction,
385
- projectRoot: admitted.projectRoot,
386
- failureLabel: `ticket #${ticketNumber}`,
387
- correlationId: admitted.runId,
388
- // Refresh holds a typed key — hand it off so identity is bound before turn.
389
- boundTicketNumber: ticketNumber,
390
- },
391
- env,
392
- io,
393
- );
394
-
395
- if (outcome.failedWithoutEscalate !== undefined) {
396
- throw new StationChildExhaustedError(
397
- outcome.failedWithoutEscalate.diagnostic,
398
- );
399
- }
400
- if (outcome.identity.kind === "escalate" && outcome.terminal !== undefined) {
401
- return {
402
- terminal: outcome.terminal,
403
- ...(outcome.admitted === undefined ? {} : { childRunId: outcome.admitted.runId }),
404
- };
405
- }
406
- }
407
- }