@akagilnc/pi-workflow-roles 0.1.4673 → 0.1.4698

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.
@@ -17,7 +17,11 @@ export function exitCodeForTerminalOutcome(outcome) {
17
17
  * auto-resumes occurred during this single LLM call; it is not persisted to
18
18
  * run-state.json and does not participate in limit decisions.
19
19
  */
20
- /** Original payloads on a terminal — role result for accepted/audit; coexist on failure. */
20
+ /**
21
+ * Current-result payloads on a terminal — accepted/audit role result, or a
22
+ * rare intentional failure face. Run history is TerminalResult.submissions
23
+ * (#836 / #953), not this helper.
24
+ */
21
25
  export function roleResultPayloads(outcome) {
22
26
  if (outcome.kind === "accepted" || outcome.kind === "audit_escalation")
23
27
  return outcome.payloads ?? [];
@@ -25,6 +29,18 @@ export function roleResultPayloads(outcome) {
25
29
  return outcome.payloads ?? [];
26
30
  return [];
27
31
  }
32
+ /**
33
+ * Prefer non-empty current-result payloads; an empty array must not shadow
34
+ * top-level submissions (#953 / #836). Callers that need failure history must
35
+ * read TerminalResult.submissions directly — do not coalesce into failure.payloads.
36
+ */
37
+ export function coalesceSubmissionRows(payloads, submissions) {
38
+ if (payloads !== undefined && payloads.length > 0)
39
+ return payloads;
40
+ if (submissions !== undefined && submissions.length > 0)
41
+ return submissions;
42
+ return payloads ?? submissions ?? [];
43
+ }
28
44
  /**
29
45
  * Build an advice navigator fact (#959). Prose is presented as submitted;
30
46
  * code does not parse route/next or judge usability.
@@ -109,15 +125,26 @@ export function formatTerminalResult(result) {
109
125
  }
110
126
  // Role-result block: original payloads, newest first for humans (#961).
111
127
  // Typed payloads/submissions stay ledger order; only this presentation reverses.
112
- const payloads = result.roleOutcome.kind === "accepted" || result.roleOutcome.kind === "audit_escalation"
113
- ? result.roleOutcome.payloads ?? result.submissions ?? []
114
- : result.roleOutcome.kind === "failure"
115
- ? result.roleOutcome.payloads ?? result.submissions ?? []
128
+ // #953: failure history is the top-level submissions carrier (#836) — present
129
+ // as recorded-submission, never as this-turn submission/receipt.
130
+ if (result.roleOutcome.kind === "failure") {
131
+ const recorded = result.submissions ?? [];
132
+ for (let i = recorded.length - 1; i >= 0; i -= 1) {
133
+ const payload = recorded[i];
134
+ const rendered = typeof payload === "string" ? payload : JSON.stringify(payload);
135
+ lines.push(`recorded-submission\t${encodeTerminalField(rendered)}`);
136
+ }
137
+ }
138
+ else {
139
+ const payloads = result.roleOutcome.kind === "accepted" ||
140
+ result.roleOutcome.kind === "audit_escalation"
141
+ ? coalesceSubmissionRows(result.roleOutcome.payloads, result.submissions)
116
142
  : result.submissions ?? [];
117
- for (let i = payloads.length - 1; i >= 0; i -= 1) {
118
- const payload = payloads[i];
119
- const rendered = typeof payload === "string" ? payload : JSON.stringify(payload);
120
- lines.push(`submission\t${encodeTerminalField(rendered)}`);
143
+ for (let i = payloads.length - 1; i >= 0; i -= 1) {
144
+ const payload = payloads[i];
145
+ const rendered = typeof payload === "string" ? payload : JSON.stringify(payload);
146
+ lines.push(`submission\t${encodeTerminalField(rendered)}`);
147
+ }
121
148
  }
122
149
  return `${lines.join("\n")}\n`;
123
150
  }
@@ -137,6 +137,7 @@ export function runIdFromRunDirectory(runDirectory) {
137
137
  * Shared parent-directory unique fallback may be adopted only when the
138
138
  * publisher-owned body.runId equals this run directory's runId. Same-run
139
139
  * artifactsDir / runDirectory candidates keep path ownership and skip this.
140
+ * expectedRunId undefined (unparseable run dir) → never bound.
140
141
  */
141
142
  function presentUniqueFallbackBoundToRun(body, expectedRunId) {
142
143
  if (expectedRunId === undefined)
@@ -144,49 +145,87 @@ function presentUniqueFallbackBoundToRun(body, expectedRunId) {
144
145
  return typeof body.runId === "string" && body.runId === expectedRunId;
145
146
  }
146
147
  /**
147
- * Read the first present typed terminal artifact for a run directory.
148
- * Order:
148
+ * Sole authority for seam-owned unique error.<uuid>.json candidates.
149
+ * Used by both clearOpposite (settlement) and readRunTerminalArtifact — do not
150
+ * re-enumerate the same-run / parent unique set elsewhere.
151
+ * Ownership:
152
+ * - same-run dirs (artifacts/, runDir): path ownership — all unique names
153
+ * - parent runs/: only body.runId-bound faces; unparseable runId or unreadable body → none
154
+ */
155
+ export async function listSeamOwnedUniqueErrorFacePaths(runDirectory) {
156
+ const artifactsDir = roleRunArtifactsDirectory(runDirectory);
157
+ const owned = await listUniqueErrorFallbackPaths([
158
+ artifactsDir,
159
+ runDirectory,
160
+ ]);
161
+ const expectedRunId = runIdFromRunDirectory(runDirectory);
162
+ for (const path of await listUniqueErrorFallbackPaths([dirname(runDirectory)])) {
163
+ const read = await readTerminalArtifactAtPath(path, "error.json");
164
+ if (read === undefined || read.status !== "present")
165
+ continue;
166
+ if (!presentUniqueFallbackBoundToRun(read.body, expectedRunId))
167
+ continue;
168
+ owned.push(path);
169
+ }
170
+ return owned;
171
+ }
172
+ /**
173
+ * Publish contract (#953): only failure publish continues after clearOpposite
174
+ * failure, so a multi-class residue (residual report/audit beside a new error
175
+ * face) means the current settlement is failure. Prefer failure-class faces
176
+ * over success/audit — never filesystem mtime (copy/restore/utimes can lie).
177
+ */
178
+ function failureClassRank(file) {
179
+ return file === "error.json" ? 1 : 0;
180
+ }
181
+ /**
182
+ * Read the current typed terminal artifact for a run directory.
183
+ *
184
+ * Candidate set (publisher-owned faces only):
149
185
  * 1) conventional artifacts/{report,error,audit-incomplete}.json
150
186
  * 2) publisher fixed failure fallbacks (error.settlement.json faces)
151
- * 3) publisher unique error.<uuid>.json fallbacks under same-run dirs
152
- * 4) shared parent-directory unique fallbacks bound by body.runId
187
+ * 3) seam-owned unique error.<uuid>.json via listSeamOwnedUniqueErrorFacePaths
188
+ * (same-run path ownership + parent body.runId binding — shared with clear)
153
189
  *
154
- * Absence of every known durable face is a valid no-receipt state (not unreadable).
155
- * A present file that cannot be parsed as a usable typed JSON object is unreadable.
190
+ * Invariant: when more than one present face remains (e.g. clearOpposite failed
191
+ * during failure publish and a fallback was settled beside a residual report),
192
+ * adopt failure-class over success/audit by the publish contract — not mtime.
193
+ * Same-class ties keep candidate enumeration order (conventional before
194
+ * fallbacks before unique).
195
+ *
196
+ * Unreadable faces never outrank a present face. Parent unique unreadable
197
+ * files never enter the shared enumerator (cannot prove run identity).
198
+ * Absence of every known durable face is a valid no-receipt state.
156
199
  */
157
200
  export async function readRunTerminalArtifact(runDirectory) {
158
201
  const artifactsDir = roleRunArtifactsDirectory(runDirectory);
202
+ const present = [];
203
+ const unreadable = [];
204
+ const consider = (read) => {
205
+ if (read === undefined || read.status === "absent")
206
+ return;
207
+ if (read.status === "present") {
208
+ present.push(read);
209
+ return;
210
+ }
211
+ unreadable.push(read);
212
+ };
159
213
  for (const file of RUN_TERMINAL_ARTIFACT_FILES) {
160
- const path = join(artifactsDir, file);
161
- const read = await readTerminalArtifactAtPath(path, file);
162
- if (read !== undefined)
163
- return read;
214
+ consider(await readTerminalArtifactAtPath(join(artifactsDir, file), file));
164
215
  }
165
- // Publisher settled a durable failure outside the conventional error.json name.
166
216
  for (const relative of RUN_TERMINAL_ERROR_FALLBACK_RELATIVE_PATHS) {
167
- const path = join(runDirectory, relative);
168
- const read = await readTerminalArtifactAtPath(path, "error.json");
169
- if (read !== undefined)
170
- return read;
217
+ consider(await readTerminalArtifactAtPath(join(runDirectory, relative), "error.json"));
171
218
  }
172
- // Same-run unique faces: path ownership is the run itself — no cross-run risk.
173
- for (const path of await listUniqueErrorFallbackPaths([artifactsDir, runDirectory])) {
174
- const read = await readTerminalArtifactAtPath(path, "error.json");
175
- if (read !== undefined)
176
- return read;
219
+ // Unique same-run + parent-bound faces: one enumerator shared with clear.
220
+ for (const path of await listSeamOwnedUniqueErrorFacePaths(runDirectory)) {
221
+ consider(await readTerminalArtifactAtPath(path, "error.json"));
177
222
  }
178
- // Shared parent (runs/) unique faces: require publisher runId binding.
179
- const expectedRunId = runIdFromRunDirectory(runDirectory);
180
- for (const path of await listUniqueErrorFallbackPaths([dirname(runDirectory)])) {
181
- const read = await readTerminalArtifactAtPath(path, "error.json");
182
- if (read === undefined)
183
- continue;
184
- if (read.status === "present") {
185
- if (!presentUniqueFallbackBoundToRun(read.body, expectedRunId))
186
- continue;
187
- return read;
188
- }
189
- // Unreadable parent unique file cannot prove run identity — do not adopt.
223
+ if (present.length > 0) {
224
+ present.sort((a, b) => failureClassRank(b.file) - failureClassRank(a.file));
225
+ return present[0];
226
+ }
227
+ if (unreadable.length > 0) {
228
+ return unreadable[0];
190
229
  }
191
230
  return { status: "absent" };
192
231
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akagilnc/pi-workflow-roles",
3
- "version": "0.1.4673",
3
+ "version": "0.1.4698",
4
4
  "description": "Soul-bound workflow roles for Pi",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -6,7 +6,10 @@
6
6
 
7
7
  ## 取证授权
8
8
 
9
- 一切取证席位可为取证运行验证,用毕需清理干净并如实记载即可放行。
9
+ > owner 2026-09-20 逐字:「说了无数次不准限制审核衙门的工具和方式。只有事后清理干净就行」
10
+ > 指针:源卷 `~/.claude/projects/-Users-akagilnc-WorkSpace-ak-pi-workflow-roles/e4b50c8e-15c7-4c92-9263-959efadb24eb.jsonl`,owner uuid `54e54b19-27d8-48f9-a334-fc1409b66558`。
11
+
12
+ 一切取证席位的工具与方式不受限:读、写、装、跑、下探针皆可。唯一义务是**用毕清理干净并如实记载**,包括清理本次取证在 worktree 与临时目录之外创建的任何东西。
10
13
 
11
14
  ## 审官纪律
12
15
 
@@ -18,4 +21,6 @@ finding 提**类别**;实例只作证据。一次找齐全,不逐轮抽样
18
21
 
19
22
  ## 取证边界(owner 2026-09-05)
20
23
 
21
- 探针、临时文件、临时家只准落在本次工作区(worktree)与系统临时目录之内;**这两处之外的任何路径一律不得删除、覆写或移动**,「用完恢复原样」。
24
+ > 逐字:「角色的 bash 不该有权删 worktree 和 tmpdir 之外的东西……加上这个限制」
25
+
26
+ 本条只禁销毁**既存之物**:本次工作区(worktree)与系统临时目录之外的任何既存路径,不得删除、覆写或移动。本次取证自己在那之外创建的东西不在此列——按《取证授权》用毕清理干净。这两处之内不设限制。
@@ -23,7 +23,7 @@
23
23
 
24
24
  **五、符合陛下意图** 先根据起居录对话,判断陛下意图。再证实票面是否是陛下想要的,对于修复票,判断 根因是什么、证据在哪 。对于新功能设计票,判断设计是不是合理,是不是陛下想要的方向。以及 因果链本身站不站得住
25
25
 
26
- 各步取证按《审法·取证边界》:读码、开卷、传召皆可;探针只落 worktree 与临时目录,不得删改其外任何路径。交付时候清理回原样
26
+ 各步取证按《审法·取证授权》:工具与方式不受限,读码、开卷、传召、下探针皆可;不得删除、覆写或移动 worktree 与临时目录之外的既存路径(《审法·取证边界》)。交付时候清理回原样
27
27
 
28
28
  ## 审读五问
29
29
 
@@ -6,5 +6,5 @@
6
6
  - 无锚定:不接收票面、判词与庭审史。案件=合并候选的全幅 diff 与仓库现状,看其本身对不对劲,此外不接收方向性指令。
7
7
  - 拾遗之责:bug 与边界、安全、命名与一致性、文档漂移、复杂度增长、测试质量、跨片接缝等。
8
8
  - 量尺:宿主宪法、仓级 ADR 与《复杂度与测试质量法》。
9
- - 取证工具一切自取,按《审法·取证边界》:只读;探针只落 worktree 与临时目录,不得删改其外任何路径,如实记录。
9
+ - 取证工具与方式不受限,一切自取,按《审法·取证授权》:用毕清理干净并如实记载。不得删除、覆写或移动 worktree 与临时目录之外的既存路径(《审法·取证边界》)。
10
10
  - 只上弹章,不修复、不裁决。
package/souls/notary.md CHANGED
@@ -23,7 +23,7 @@
23
23
  任一病成立,点名该条驳回;拿不准陛下本意的,上呈。
24
24
 
25
25
  方法:按条封驳。
26
- 取证按《审法·取证边界》。无法解决的上抛陛下。
26
+ 取证按《审法·取证授权》与《取证边界》。无法解决的上抛陛下。
27
27
 
28
28
  ## 结论
29
29
 
@@ -8,6 +8,15 @@
8
8
 
9
9
  取舍位次:过测试 → 显意图 → 无重复 → 最少元素。
10
10
 
11
+ ## 先搜,再想简单的(owner 2026-09-20)
12
+
13
+ > 逐字:「请你们思考实现方式的时候。多搜搜互联网,多想简单的方式,不是在那加加加,加一堆烂机制最终还要烧我的token来删!」
14
+ > 指针:源卷 `~/.claude/projects/-Users-akagilnc-WorkSpace-ak-pi-workflow-roles/e4b50c8e-15c7-4c92-9263-959efadb24eb.jsonl`,owner uuid `04255552-6b2a-45ee-9e9e-59a068b960cc`。
15
+
16
+ 动手写机制之前:先搜互联网,确认这是不是已知问题、生态里有没有现成解法(官方文档优先)。再问最简单的做法是什么——包括「换一个前置选择」而不是「给现有选择打补丁」。
17
+
18
+ 自撰机制是最后手段。同一处只增不减时,回查根因,不得靠再补一层收场。
19
+
11
20
  ## 测试对行为负责
12
21
 
13
22
  测试只保**调用输入正确**的最短确定性案:从真实入口进入,在外部可见的结构化结果上断言,秒级完成。涉真实宿主、真实 LLM、真实并发、真实网络的行为,不写测试;以真跑为证。
package/souls/reviewer.md CHANGED
@@ -4,6 +4,6 @@
4
4
 
5
5
  - 风闻奏事,言不为狱:弹章(finding)不是权威、不负坐实义务。弹章须指明所劾之处(文件/条文指针),说错无罪。
6
6
  - 独立机关,直属陛下:不接收输入 prompt。案件=固定的被评审目标,目标或事实前提无法成立时如实拒绝;方法=canonical Skill;其余一切自取。
7
- - 取证工具一切自取,按《审法·取证边界》:只读;探针只落 worktree 与临时目录,不得删改其外任何路径。有权抽查≠有责核实:对存疑处可开卷、可下探针,如实记录即可。不跑全量测试套件。
7
+ - 取证工具与方式不受限,一切自取,按《审法·取证授权》:读、写、装、跑、下探针皆可,用毕清理干净并如实记载。不得删除、覆写或移动 worktree 与临时目录之外的既存路径(《审法·取证边界》)。有权抽查≠有责核实。
8
8
  - 劾前逐项过改动清单。固定目标内每项改动都要标明由票面哪条驱动:标不出来的即为夹带(默认值、护栏、平行机制、仅测试用的生产钩子同论);票面有要求而清单中没有的即为缺口。二者同为 finding。
9
9
  - 重点劾三事:违宪(与既裁法源 ADR、铁律相抵)、测试必要性(最小必要成本法)、复杂度(可删不删、同形状第二份)。劾的范围=眼前一切:票面、实现、测试、输入 prompt 与给你的派单命令本身。唯一例外是陛下亲笔——援引此例外必须引陛下原话为证。
@@ -5,6 +5,7 @@ import type { HostContext } from "./host-contracts.ts";
5
5
  import type { NoReceiptLifecycleFacts } from "./receipt-delivery-policy.ts";
6
6
  import type { PublicSummonResult } from "./public-role-summons.ts";
7
7
  import { OFFICER_CONCLUSION_REASK } from "./gatekeeper-role.ts";
8
+ import { coalesceSubmissionRows } from "./public-cli/terminal.ts";
8
9
 
9
10
  export type ComplianceNoReceipt = NoReceiptLifecycleFacts & { status: "no-receipt"; usage?: Usage };
10
11
  /**
@@ -149,7 +150,9 @@ async function projectAuditorTerminal(summoned: PublicSummonResult): Promise<Com
149
150
  };
150
151
  }
151
152
  if (outcome.kind === "failure") {
152
- const rows = outcome.payloads ?? summoned.terminal?.submissions ?? [];
153
+ // #953: history stays on the historical carrier; do not read failure.payloads
154
+ // as if they were the current failure result (#836 A.3 beside-channel).
155
+ const rows = summoned.terminal?.submissions ?? [];
153
156
  return {
154
157
  status: "transport_failure",
155
158
  diagnostic: outcome.diagnostic,
@@ -159,7 +162,10 @@ async function projectAuditorTerminal(summoned: PublicSummonResult): Promise<Com
159
162
  };
160
163
  }
161
164
  if (outcome.kind === "accepted") {
162
- const rows = outcome.payloads ?? summoned.terminal?.submissions ?? [];
165
+ const rows = coalesceSubmissionRows(
166
+ outcome.payloads,
167
+ summoned.terminal?.submissions,
168
+ );
163
169
  if (rows.length === 0) {
164
170
  return readComplianceCandidate({}, usage);
165
171
  }
@@ -260,20 +260,20 @@ function projectOfficerDecision(
260
260
  */
261
261
  function thisCourtOfficerPayloads(terminal: TerminalResult | undefined): readonly unknown[] {
262
262
  const outcome = terminal?.roleOutcome;
263
+ // This-court identity only from accepted/audit_escalation settlement payloads
264
+ // (#879). Failure history is not this-court (#953) — it rides submissions.
263
265
  if (outcome !== undefined && (outcome.kind === "accepted" || outcome.kind === "audit_escalation")) {
264
266
  if (outcome.payloads !== undefined && outcome.payloads.length > 0) return outcome.payloads;
265
267
  }
266
- if (outcome?.kind === "failure" && outcome.payloads !== undefined && outcome.payloads.length > 0) {
267
- return outcome.payloads;
268
- }
269
268
  // No sole-row identity guess: undivided submissions are not this-court (#879).
270
269
  return [];
271
270
  }
272
271
 
273
- /** Failure/transport channel may still surface every recorded payload beside the failure (#836 A.3). */
272
+ /**
273
+ * Failure/transport channel surfaces recorded history beside the failure
274
+ * (#836 A.3) via the historical carrier only (#953 — not failure.payloads).
275
+ */
274
276
  function officerFailurePayloads(terminal: TerminalResult | undefined): readonly unknown[] {
275
- const outcome = terminal?.roleOutcome;
276
- if (outcome?.kind === "failure") return outcome.payloads ?? terminal?.submissions ?? [];
277
277
  return terminal?.submissions ?? [];
278
278
  }
279
279
 
@@ -31,6 +31,7 @@ import {
31
31
  } from "../package-resources/engine-material.ts";
32
32
  import { CliUsageError } from "./cli-errors.ts";
33
33
  import { projectCourtTicketNumbers } from "../diarist-contracts.ts";
34
+ import { readableGateItem } from "../readable-gate-item.ts";
34
35
  import { isSafePositiveTicketNumber } from "../run-ticket-number.ts";
35
36
  import {
36
37
  admitCountersignInvocation,
@@ -139,13 +140,74 @@ export type CourtDiaristIdentity =
139
140
  readonly courtTicketNumbers?: readonly number[];
140
141
  }
141
142
  | { readonly kind: "unbound" }
142
- | { readonly kind: "escalate" };
143
+ | {
144
+ readonly kind: "escalate";
145
+ /** Honest diagnostic from diarist escalate payload (#953) — never invent 认不出. */
146
+ readonly diagnostic: string;
147
+ };
143
148
 
144
149
  export type CourtDiaristInvocationResult = {
145
150
  readonly identity: CourtDiaristIdentity;
146
151
  readonly failedWithoutEscalate?: { readonly diagnostic: string };
147
152
  };
148
153
 
154
+ function isRecord(value: unknown): value is Record<string, unknown> {
155
+ return typeof value === "object" && value !== null && !Array.isArray(value);
156
+ }
157
+
158
+ function isEscalatePayload(payload: unknown): boolean {
159
+ if (!isRecord(payload)) return false;
160
+ return (
161
+ payload.status === "escalate" || payload.countersignStatus === "escalate"
162
+ );
163
+ }
164
+
165
+ /**
166
+ * Rows a parent may read from a nested diarist terminal (#836 / #953).
167
+ * Accepted/audit use role result payloads; failure history is submissions only.
168
+ */
169
+ function courtDiaristPayloadRows(
170
+ roleOutcome: TerminalRoleOutcome | undefined,
171
+ submissions: readonly unknown[] | undefined,
172
+ ): readonly unknown[] {
173
+ if (roleOutcome === undefined) return submissions ?? [];
174
+ if (roleOutcome.kind === "accepted" || roleOutcome.kind === "audit_escalation") {
175
+ return roleOutcome.payloads ?? [];
176
+ }
177
+ if (roleOutcome.kind === "failure") return submissions ?? [];
178
+ return [];
179
+ }
180
+
181
+ /**
182
+ * Parent diagnostic for court-diarist escalate (#953 / 失败诚实 / 传话).
183
+ * Relays diarist escalate payload(s) via readableGateItem — never invents
184
+ * "cannot identify court target" when a payload already carries other facts
185
+ * (e.g. ticketNumber present with a different reason).
186
+ *
187
+ * Multiple escalate payloads remain reachable after the run (ADR 0003/0041;
188
+ * no courtAttempt sole-filter on diarist). Same law as secretariat parent face:
189
+ * every escalate receipt as-is, last = currentConclusion.
190
+ */
191
+ function courtDiaristEscalateDiagnostic(
192
+ roleOutcome: TerminalRoleOutcome | undefined,
193
+ submissions?: readonly unknown[],
194
+ ): string {
195
+ const payloads = courtDiaristPayloadRows(roleOutcome, submissions);
196
+ const escalatePayloads: unknown[] = [];
197
+ for (const payload of payloads) {
198
+ if (isEscalatePayload(payload)) escalatePayloads.push(payload);
199
+ }
200
+ if (escalatePayloads.length === 0) return "court diarist station escalated";
201
+ // Single escalate: prior carrier (payload body only) — no shape churn.
202
+ if (escalatePayloads.length === 1) {
203
+ return `court diarist station escalated: ${readableGateItem(escalatePayloads[0])}`;
204
+ }
205
+ return `court diarist station escalated: ${readableGateItem({
206
+ receipts: escalatePayloads,
207
+ currentConclusion: escalatePayloads[escalatePayloads.length - 1],
208
+ })}`;
209
+ }
210
+
149
211
  /** Env slice shared by court diarist identity summons (countersign / secretariat). */
150
212
  export type CourtDiaristSummonEnv = Pick<
151
213
  CountersignRunEnv,
@@ -171,14 +233,10 @@ export type CourtDiaristSummonEnv = Pick<
171
233
  function courtTicketNumbersFromOutcome(
172
234
  roleOutcome: TerminalRoleOutcome | undefined,
173
235
  principalTicket: number,
236
+ submissions?: readonly unknown[],
174
237
  ): readonly number[] | undefined {
175
238
  if (roleOutcome === undefined) return undefined;
176
- const payloads =
177
- roleOutcome.kind === "accepted" ||
178
- roleOutcome.kind === "audit_escalation" ||
179
- roleOutcome.kind === "failure"
180
- ? (roleOutcome.payloads ?? [])
181
- : [];
239
+ const payloads = courtDiaristPayloadRows(roleOutcome, submissions);
182
240
  let latest: readonly number[] | undefined;
183
241
  for (const payload of payloads) {
184
242
  if (
@@ -216,18 +274,7 @@ function courtDiaristEscalated(
216
274
  if (roleOutcome === undefined) return false;
217
275
  if (roleOutcome.kind === "audit_escalation") return true;
218
276
  if (roleOutcome.kind !== "accepted") return false;
219
- return (roleOutcome.payloads ?? []).some((payload) => {
220
- if (
221
- typeof payload !== "object" ||
222
- payload === null ||
223
- Array.isArray(payload)
224
- )
225
- return false;
226
- const record = payload as Record<string, unknown>;
227
- return (
228
- record.status === "escalate" || record.countersignStatus === "escalate"
229
- );
230
- });
277
+ return (roleOutcome.payloads ?? []).some(isEscalatePayload);
231
278
  }
232
279
 
233
280
  /**
@@ -292,11 +339,15 @@ export async function invokeCourtDiarist(
292
339
  });
293
340
 
294
341
  const roleOutcome = result.terminal?.roleOutcome;
342
+ const submissions = result.terminal?.submissions;
295
343
  // Escalate routing is a boolean over the preserved sequence (#881). Reasons and
296
344
  // payload bodies stay on roleOutcome — never rewritten into a sole identity reason.
297
345
  if (courtDiaristEscalated(roleOutcome)) {
298
346
  return {
299
- identity: { kind: "escalate" },
347
+ identity: {
348
+ kind: "escalate",
349
+ diagnostic: courtDiaristEscalateDiagnostic(roleOutcome, submissions),
350
+ },
300
351
  };
301
352
  }
302
353
 
@@ -319,6 +370,7 @@ export async function invokeCourtDiarist(
319
370
  const courtTicketNumbers = courtTicketNumbersFromOutcome(
320
371
  roleOutcome,
321
372
  asserted,
373
+ submissions,
322
374
  );
323
375
  return {
324
376
  identity: {
@@ -390,9 +442,7 @@ export async function runCountersignCourtDiaristStation(
390
442
  );
391
443
 
392
444
  if (outcome.identity.kind === "escalate") {
393
- throw new StationChildExhaustedError(
394
- "court diarist station escalated (cannot identify court target)",
395
- );
445
+ throw new StationChildExhaustedError(outcome.identity.diagnostic);
396
446
  }
397
447
  if (outcome.failedWithoutEscalate !== undefined) {
398
448
  throw new StationChildExhaustedError(
@@ -509,6 +559,7 @@ export async function runPublicCountersign(
509
559
 
510
560
  if (outcome.identity.kind === "escalate") {
511
561
  // 御批: 识别不了就上抛 — materialize and settle this countersign run.
562
+ // Diagnostic relays diarist escalate payload facts (#953).
512
563
  await materializeAdmission();
513
564
  await markRunAdmitted(admitted, env.principalAuthority);
514
565
  return await presentControlledFailure(
@@ -517,9 +568,7 @@ export async function runPublicCountersign(
517
568
  timedOut: false,
518
569
  code: null,
519
570
  stderr: "",
520
- thrown: new Error(
521
- "court diarist station escalated (cannot identify court target)",
522
- ),
571
+ thrown: new Error(outcome.identity.diagnostic),
523
572
  },
524
573
  countersignAdapters(),
525
574
  env.principalAuthority,
@@ -139,15 +139,14 @@ export async function runPublicSecretariat(
139
139
  );
140
140
 
141
141
  if (outcome.identity.kind === "escalate") {
142
+ // Diagnostic relays diarist escalate payload facts (#953) — never invent 认不出.
142
143
  return await presentControlledFailure(
143
144
  admitted,
144
145
  {
145
146
  timedOut: false,
146
147
  code: null,
147
148
  stderr: "",
148
- thrown: new Error(
149
- "court diarist station escalated (cannot identify court target)",
150
- ),
149
+ thrown: new Error(outcome.identity.diagnostic),
151
150
  },
152
151
  secretariatAdapters(),
153
152
  env.principalAuthority,