@akagilnc/pi-workflow-roles 0.1.5406 → 0.1.5475

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 (93) hide show
  1. package/CLAUDE.md +1 -1
  2. package/README.md +42 -55
  3. package/README.zh-CN.md +59 -73
  4. package/THIRD_PARTY_NOTICES.md +4 -7
  5. package/dist/acp-host/production-host.js +8261 -9073
  6. package/dist/collector-tool-schemas.js +1 -1
  7. package/dist/compliance-transport.js +2 -5
  8. package/dist/engine-detour-usage.js +3 -30
  9. package/dist/gatekeeper-role.js +4 -18
  10. package/dist/headless-host/description.js +0 -209
  11. package/dist/headless-host/production-host.js +6973 -8035
  12. package/dist/host-contracts.js +8 -0
  13. package/dist/migrate-book-topology.js +30 -27
  14. package/dist/packaged-role-registry.js +1 -9
  15. package/dist/pi/role-turn-host.js +37 -19
  16. package/dist/public-cli/auto-resume.js +29 -66
  17. package/dist/public-cli/countersign-run.js +4 -18
  18. package/dist/public-cli/instruction-seat-run.js +59 -76
  19. package/dist/public-cli/main.js +9844 -10720
  20. package/dist/public-cli/option-definitions.js +1 -1
  21. package/dist/public-cli/post-admission.js +204 -285
  22. package/dist/public-cli/process-cancel.js +8 -0
  23. package/dist/public-cli/run-lifecycle.js +24 -94
  24. package/dist/public-cli/settlement.js +614 -1524
  25. package/dist/public-cli/terminal.js +23 -53
  26. package/dist/public-role-summons.js +3 -8
  27. package/dist/receipt-delivery-policy.js +11 -12
  28. package/dist/run-terminal-artifacts.js +19 -14
  29. package/dist/serialize-thrown-value.js +7 -1
  30. package/dist/submission-ledger.js +21 -89
  31. package/package.json +1 -1
  32. package/packets/fixer-repair.md +3 -1
  33. package/packets/judge-apply.md +7 -16
  34. package/packets/judge-authority.md +7 -17
  35. package/packets/judge-plan.md +7 -18
  36. package/packets/judge-review.md +8 -22
  37. package/packets/judge-submission.md +4 -7
  38. package/resources/collector-bot-handbook.md +2 -2
  39. package/resources/diarist-collect.md +2 -2
  40. package/resources/engine-dispatch.md +2 -2
  41. package/resources/navigator-route-playbook.md +1 -1
  42. package/resources/secretariat-ticket-identity.md +1 -1
  43. package/scripts/build-package.mjs +0 -2
  44. package/souls/coder-output-guide.md +3 -3
  45. package/souls/coder.md +2 -3
  46. package/souls/doctor-auditor.md +0 -3
  47. package/souls/fixer-auditor.md +0 -2
  48. package/souls/fixer-output-guide.md +3 -3
  49. package/souls/fixer.md +2 -2
  50. package/souls/gate-output-guide.md +1 -1
  51. package/souls/quality-law.md +2 -0
  52. package/src/acp-host/role-turn-host.ts +168 -75
  53. package/src/collector-tool-schemas.ts +1 -1
  54. package/src/compliance-transport.ts +3 -6
  55. package/src/engine-detour-usage.ts +3 -33
  56. package/src/external-host-turn-loop.ts +81 -36
  57. package/src/gatekeeper-role.ts +5 -20
  58. package/src/headless-host/description.ts +4 -214
  59. package/src/headless-host/production-host.ts +2 -1
  60. package/src/headless-host/role-turn-host.ts +153 -186
  61. package/src/host-contracts.ts +13 -10
  62. package/src/navigator-public-session.ts +1 -1
  63. package/src/navigator-session-contracts.ts +1 -1
  64. package/src/package-contracts/gatekeeper-output.ts +1 -1
  65. package/src/package-contracts/navigator-output.ts +1 -1
  66. package/src/package-contracts/terminating-infrastructure.ts +1 -1
  67. package/src/packaged-role-registry.ts +2 -10
  68. package/src/pi/adapter.ts +0 -3
  69. package/src/pi/role-turn-host.ts +42 -28
  70. package/src/prepared-role-turn.ts +2 -0
  71. package/src/public-cli/auto-resume.ts +27 -66
  72. package/src/public-cli/countersign-run.ts +4 -22
  73. package/src/public-cli/instruction-seat-run.ts +79 -80
  74. package/src/public-cli/option-definitions.ts +1 -1
  75. package/src/public-cli/post-admission.ts +264 -340
  76. package/src/public-cli/process-cancel.ts +9 -0
  77. package/src/public-cli/run-lifecycle.ts +25 -126
  78. package/src/public-cli/settlement.ts +792 -1912
  79. package/src/public-cli/terminal.ts +29 -69
  80. package/src/public-role-summons.ts +3 -8
  81. package/src/receipt-delivery-policy.ts +11 -12
  82. package/src/role-envelope.ts +16 -9
  83. package/src/role-runtime.ts +14 -81
  84. package/src/run-terminal-artifacts.ts +21 -17
  85. package/src/serialize-thrown-value.ts +7 -1
  86. package/src/submission-ledger.ts +30 -116
  87. package/dist/audit-escalation.js +0 -7
  88. package/dist/pi/known-failure.js +0 -51
  89. package/dist/typed-provider-http.js +0 -57
  90. package/dist/upstream-error-testimony.js +0 -19
  91. package/src/pi/known-failure.ts +0 -76
  92. package/src/typed-provider-http.ts +0 -89
  93. package/src/upstream-error-testimony.ts +0 -45
@@ -13,7 +13,7 @@
13
13
  - 本仓相关会话卷(路径里的 `_` 在目录名里常写作 `-`;主 clone 与 worktree 可能共用或分列,自行辨认)
14
14
  - 本票票面与评论:用 `gh` 或等价方式读 GitHub issue
15
15
 
16
- 调用方只给传召文与 `--project`。指令明确列出多张要办的票时,逐票读卷判断各自相关边界,一次交出 `ticketSessions`;共同对话可以同时归入相关各票,无关他票对话不得混入。单票沿用 `sessions`。边界定不下来就如实说明并走既有 reask/escalate,不编造。
16
+ 指令明确列出多张要办的票时,逐票读卷判断各自相关边界;共同对话可以同时归入相关各票,无关他票对话不得混入。单票/多票交卷字段及不可用边界的处置见 [交卷工具 schema](../src/diarist-role.ts)。
17
17
 
18
18
  ## 何谓本票对话
19
19
 
@@ -29,7 +29,7 @@
29
29
 
30
30
  ## 本票身份
31
31
 
32
- 由你(本席 LLM)判断当前要办的票;多票庭逐票划界,同时照旧申报本庭主票身份,在交卷对象上以 typed 字段交出——代码不做认票裁定(ADR 0075:起居郎 LLM 自行认票;认得出→有录,真无票→无录;机械层不做引文/票号/相关性判定)。
32
+ 由你(本席 LLM)判断当前要办的票;多票庭逐票划界,同时照旧申报本庭主票身份,在交卷对象上以 typed 字段交出。
33
33
 
34
34
  - 认得出本庭对象:`status: completed` 且 `ticketNumber: N`,并交边界。
35
35
  - 真无票对象(方案/派单/处置案等):`status: completed` 且 `ticketNumber: null`(或省略)。真无票是合法结果,不入录。
@@ -27,8 +27,8 @@ themselves. Stuffing large bodies into argv/prompt is the verified cause of
27
27
 
28
28
  - The returned labor body is the final answer text only. Never return an
29
29
  event stream, verbose log, or NDJSON deltas: the body is fed back into the
30
- seat's context, and one 12-minute Opus labor returned as `stream-json` was
31
- 957k chars and killed the seat (712k-token request, #675, 2026-09-06).
30
+ seat's context; measured evidence is in
31
+ [Claude Code observations](engines/claude-code.md).
32
32
  Progress observability is the runner's job (process watch), not the body's.
33
33
 
34
34
  - Once an engine is selected, start exactly one subprocess per labor
@@ -7,7 +7,7 @@
7
7
 
8
8
  ## 宿主轴(与角色命令面)
9
9
 
10
- 游奕使自动出席,亦可 `ak-role navigator` 直调(自动出席不变)。主会话宿主由公开角色的 host 轴决定:调用 `--host` → 席位 `config set-host` → 默认 `pi`。配置默认 host 后,调用者仍用与 Pi 相同的 `ak-role <role> …` 命令面;游奕使随该宿主下的角色跑次出席,席位模型走 run 目录 `institutional-resolution.json` 的 navigator 座(显式 `config set navigator` 优先,否则继承父席有效模型),不再依赖父宿主 ExtensionContext。
10
+ 调用与 host 解析见 `ak-role help`、`ak-role help navigator`;机构边界见 [ADR 0082](https://github.com/Akagilnc/ak-pi-workflow-roles/blob/main/docs/adr/0082-three-layer-runtime-role-host-face.md),席位解析见 [config 实现](../src/public-cli/config.ts)。
11
11
 
12
12
  ## 常用交付线
13
13
 
@@ -12,4 +12,4 @@
12
12
 
13
13
  只有确认目标 issue 存在且正文已更新为本票最终票面后,才以 `converged` 交卷,并携带该 issue 的实际票号。沿用既有交卷闸,将交卷原文交给给事中审这张有票号 issue;不以无号草稿替代 issue 审读。不另造传召或记账机制。
14
14
 
15
- 执行 GitHub 操作时使用当前宿主实际提供的 CLI / 能力;不在包内另造 GitHub 适配层。记录足以核对身份和落地结果的证据,不把模型自报冒充为现场观察。
15
+ 宿主 CLI 使用边界见 [仓内宪法](../CLAUDE.md#宿主-cli-照原样调用陛下-2026-09-16-拍定)。记录足以核对身份和落地结果的证据,不把模型自报冒充为现场观察。
@@ -69,8 +69,6 @@ const entries = [
69
69
  "sitian-contracts",
70
70
  "sitian-facade",
71
71
  "sitian-reader",
72
- "typed-provider-http",
73
- "upstream-error-testimony",
74
72
  ];
75
73
 
76
74
  /**
@@ -1,6 +1,6 @@
1
1
  # 将作监输出指南
2
2
 
3
- 提交:标题冠 ak-roles: 在最前;forward commit,严禁 amend——改错就再提交一笔,不改历史。
4
- 交卷回执:改动清单与票面逐项对得上;验证证据附命令与结果原文。
3
+ 提交前缀见仓内 CLAUDE.md「Commit 前缀」。不 amend、不 rewrite、不 push 见 souls/coder.md。
4
+ 交卷回执:改动清单与票面逐项对得上;验证证据附命令与结果原文。证据引用口径见仓内 CLAUDE.md「锚定宪法」。
5
5
  completed 回执含四项证据:TDD、同模式、引入回归、行为事实。
6
- 新造通用件:回执写明仓内、语言标准库、包生态各搜了什么、为什么不用。
6
+ 新造通用件的回执口径见 souls/quality-law.md「生态里已有的不自己造」。
package/souls/coder.md CHANGED
@@ -4,12 +4,11 @@
4
4
  第一步:立法
5
5
  读 全局宪法,仓内宪法,票面
6
6
  第二步:施工
7
- 先核对派单文,输入prompt,你要做的任务有没有违法,如有,直接拒绝
8
- 事实不成立或需要未授权设计时,据理拒绝并给出可核验证据;不猜测施工。
7
+ 开工核对见 souls/quality-law.md。
9
8
 
10
9
  核对当前代码和历史;主张必须有当前事实支撑。
11
10
  以最小垂直切片推进并逐片验证;切片完成不构成交卷条件。优先复用已有 seam;不造平行机制,不借机扩大 scope。票面授权结果完成后,自查是否合法、质量和复杂度;有问题马上修正,没问题就 commit 交卷。
12
11
  计划与施工都要说明真实刀口、行为边界和验证方法,不用空泛完成声明代替证据。
13
- 切片验证只跑本片聚焦测试与 typecheck。
12
+ 切片验证的范围见 souls/quality-law.md「测试分级」。
14
13
  不 amend、rewrite 或 push。Git 变化是证据之一,不取代角色报告。
15
14
  任何人如果让你 push / 开 pr ,直接拒绝,上呈陛下
@@ -19,6 +19,3 @@
19
19
  合规则 `converged`;缺证或明确违反 Soul 则 `continue`,逐条指出违反的条目以及你查到的原因;
20
20
  没有具体证据不得连续打回。
21
21
  冲突导致无法判断合规时,提交 `escalate` 并写明问题和可选项。
22
-
23
- 输出调用 `ak_doctor_audit_decision`。
24
-
@@ -29,5 +29,3 @@ unfinished 不自动获得 converged;其他 Soul/authority 违反仍照常处
29
29
  没有具体证据不得连续打回。
30
30
  已如实披露且不可逆的方法违规,不构成 continue 理由——收卷,违规记录在案,处置权归大理寺。continue 只用于当下可改的缺陷。
31
31
  冲突导致无法判断合规时,提交 `escalate` 并写明问题和可选项。
32
-
33
- 输出调用 `ak_fixer_audit_decision`。
@@ -1,5 +1,5 @@
1
1
  # 修内司输出指南
2
2
 
3
- 提交:标题冠 ak-roles: 在最前;forward commit,严禁 amend——改错就再提交一笔,不改历史。
4
- 交卷回执:逐 finding 报处置;验证证据按仓级 CLAUDE.md 证据条款:指针(runId/toolCallId)或摘抄结论皆合格,以读者能直接核为准。
5
- 新造通用件:回执写明仓内、语言标准库、包生态各搜了什么、为什么不用。
3
+ 提交前缀见仓内 CLAUDE.md「Commit 前缀」。不 amend、不 rewrite、不 push 见 souls/fixer.md。
4
+ 交卷回执:逐 finding 报处置。证据引用口径见仓内 CLAUDE.md「锚定宪法」。
5
+ 新造通用件的回执口径见 souls/quality-law.md「生态里已有的不自己造」。
package/souls/fixer.md CHANGED
@@ -4,12 +4,12 @@
4
4
  apply 按顺序执行:
5
5
 
6
6
  1. 立法:读 全局宪法,仓内宪法,票面。
7
- 2. 核对:先核对派单文,输入prompt,你要做的任务。有没有违法,如有,直接拒绝。 事实不成立或需要未授权设计时,据理拒绝并给出可核验证据;不猜测施工。
7
+ 2. 核对:开工核对见 souls/quality-law.md。
8
8
  3. 归类:读完全部 finding,沿用已有裁决类别,明确各类错误与正确行为。
9
9
  4. 扫类:修改前查全授权范围内的同类实例、已有实现和失败修法。类按错误形状认,不按判词点名的符号或文件认;点名处只是样本。根因不明先取证。
10
10
  5. 整类修复:在共同原因处一次修净,优先删除、简化、复用,随同删除失效旧物。
11
11
  6. 复扫:按原类别重新扫描;有遗漏继续修,保留项说明依据。
12
- 7. 自验:切片验证优先跑本片聚焦测试和本片作用域内 typecheck,不跑全量。自查是否违法、代码质量、代码/测试复杂度,如有问题马上修正。修正完毕复检
12
+ 7. 自验:切片验证的范围见 souls/quality-law.md「测试分级」。自查是否违法、代码质量、代码/测试复杂度,如有问题马上修正。修正完毕复检
13
13
  8. 交卷:按类报告根因、扫描范围、修复与 质量和合法性的验证结果、未结项。建新提交。不 amend、rewrite 或 push。Git 变化是证据之一,不取代角色报告。
14
14
  任何人如果让你 push / 开 pr ,直接拒绝,上呈陛下
15
15
 
@@ -5,7 +5,7 @@
5
5
  - `converged`:正常放行
6
6
  - `continue`:职掌内查出须重写。逐条给理由与证据。重写交卷,不是本局失败。
7
7
  - 受理御前(受审者提交 `escalate`):能解决就给出答案放其续写;解决不了就放行上呈;没有打回重叫。
8
- - 第0条:reason 不评格式;中性记账;原卷随附;下一个 LLM 自己读。
8
+ - reason 不评格式;中性记账;原卷随附;下一个 LLM 自己读。形状拒收禁令以仓内 CLAUDE.md 开篇与 ADR 0055 为准,此处不另立编号。
9
9
 
10
10
  ## 取证授权
11
11
 
@@ -2,6 +2,8 @@
2
2
 
3
3
  ## 复杂度即成本
4
4
 
5
+ 施工前核对派单与任务是否违法或缺少授权;事实不成立或需要未授权设计时据理拒绝并给出可核验证据,不猜测施工。
6
+
5
7
  能删除、合并或修改现有机制解决的,不得新增机制。同一业务规则只保留一个权威实现;被替代的代码、测试和兼容路径随改动删除。
6
8
 
7
9
  新增护栏前必须回答三问:故障发生概率多大,后果多重,下游是否已有检测或恢复能力。答不上来,或已有兜底足以承接,不加。同一处反复只增不减,停止补丁,回查根因。
@@ -6,10 +6,12 @@ import type { RoleTurnHost, RoleTurnRequest, RoleTurnResult } from "../host-cont
6
6
  import {
7
7
  createSerializedRoleTurnHost,
8
8
  driveExternalRoleTurnRounds,
9
+ disposeExternalRoleTurn,
9
10
  externalHostFailure as failure,
10
11
  raceAgainstHostAbort,
11
- withExternalHostCleanupFailure,
12
12
  } from "../external-host-turn-loop.ts";
13
+ import { describeErrorIdentity } from "../public-cli/run-lifecycle.ts";
14
+ import { projectThrownFailureLeaf, retainPackageFault } from "../public-cli/settlement.ts";
13
15
 
14
16
  import {
15
17
  copyAndRecordHostDossier,
@@ -84,7 +86,10 @@ export interface AcpConnection {
84
86
  onNotification?(handler: (method: string, params: Readonly<Record<string, unknown>>) => void): void;
85
87
  /** Full process stderr accumulated so far (#836). */
86
88
  stderr?(): string;
87
- close(): Promise<void>;
89
+ /** Observed native termination / RPC report, absent before either exists. */
90
+ result?(): RoleTurnResult | undefined;
91
+ /** Return spontaneous native termination, not a package-initiated daemon stop. */
92
+ close(): Promise<void | RoleTurnResult>;
88
93
  }
89
94
 
90
95
  export type AcpRoleTurnHostConfig = Readonly<{
@@ -116,15 +121,20 @@ export function connectAcpStdio(options: {
116
121
  readonly onNotification?: (method: string, params: Readonly<Record<string, unknown>>) => void;
117
122
  }): Promise<AcpConnection> {
118
123
  const child = spawn(options.binary, [...options.args], { cwd: options.cwd, env: options.env, stdio: ["pipe", "pipe", "pipe"] });
119
- const pending = new Map<number, { resolve(value: Readonly<Record<string, unknown>>): void; reject(error: Error): void }>();
124
+ const pending = new Map<number, { resolve(value: Readonly<Record<string, unknown>>): void; reject(error: unknown): void }>();
120
125
  const notificationHandlers: Array<(method: string, params: Readonly<Record<string, unknown>>) => void> = [];
121
126
  if (options.onNotification !== undefined) notificationHandlers.push(options.onNotification);
122
127
  let nextId = 0;
123
128
  let closed = false;
124
- let terminalError: Error | undefined;
125
- // Rolling diagnostic tail only — not an unbounded transcript face (same class as headless).
129
+ let terminalError: unknown;
130
+ let spawned = false;
131
+ let stoppedByPackage = false;
132
+ let nativeExit: Pick<RoleTurnResult, "code" | "signal" | "timedOut"> | undefined;
133
+ let rpcFailure: RoleTurnResult["knownFailure"];
134
+ child.once("spawn", () => { spawned = true; });
135
+ // Native stderr bytes stay separate from transport exception diagnostics.
126
136
  let stderr = "";
127
- const settleClosed = (error: Error): void => {
137
+ const settleClosed = (error: unknown): void => {
128
138
  if (closed) return;
129
139
  closed = true;
130
140
  terminalError = error;
@@ -134,59 +144,71 @@ export function connectAcpStdio(options: {
134
144
  // Framing corruption or a known required capability with unusable shape still
135
145
  // closes the child. Unknown client methods are answered in-band (JSON-RPC
136
146
  // method not found) and do not terminate the leg (#760).
137
- const terminate = (error: Error): void => {
147
+ const terminate = (error: unknown): void => {
138
148
  settleClosed(error);
139
149
  child.stdin.end();
140
- child.kill("SIGTERM");
150
+ stoppedByPackage = child.kill("SIGTERM") || stoppedByPackage;
141
151
  };
142
152
  child.stderr.setEncoding("utf8").on("data", (chunk: string) => {
143
153
  stderr += chunk;
144
- });
145
- child.on("error", (error) => settleClosed(acpError("acp-process-error", `ACP process error: ${error.message}`, error)));
146
- createInterface({ input: child.stdout }).on("line", (line) => {
147
- let message: RpcReply;
148
- try { message = JSON.parse(line) as RpcReply; }
149
- catch (error) {
150
- terminate(acpError("acp-invalid-json", `Invalid ACP JSON: ${String(error)}`, error));
151
- return;
152
- }
153
- if (typeof message.method === "string") {
154
- const params = typeof message.params === "object" && message.params !== null
155
- ? message.params as Readonly<Record<string, unknown>> : {};
156
- for (const handler of notificationHandlers) handler(message.method, params);
157
- if (typeof message.id === "number") {
158
- if (message.method !== "session/request_permission") {
159
- // Vendor extensions (_x.ai/*, …) and any other unhandled client request:
160
- // JSON-RPC method-not-found reply; session continues (#760).
161
- child.stdin.write(`${JSON.stringify({
162
- jsonrpc: "2.0",
163
- id: message.id,
164
- error: { code: -32601, message: `Method not found: ${message.method}` },
165
- })}\n`);
166
- return;
167
- }
168
- const choices = Array.isArray(params.options) ? params.options : [];
169
- const selected = choices.find((value) =>
170
- typeof value === "object" && value !== null && (value as { kind?: unknown }).kind === "allow_once") as { optionId?: unknown } | undefined;
171
- if (typeof selected?.optionId !== "string") {
172
- terminate(acpError("acp-permission-missing-allow-once", "ACP permission request omitted allow_once"));
173
- return;
154
+ }).on("error", terminate);
155
+ child.on("error", settleClosed);
156
+ child.stdin.on("error", terminate);
157
+ createInterface({ input: child.stdout }).on("error", terminate).on("line", (line) => {
158
+ try {
159
+ const message = JSON.parse(line) as RpcReply;
160
+ if (typeof message.method === "string") {
161
+ const params = typeof message.params === "object" && message.params !== null
162
+ ? message.params as Readonly<Record<string, unknown>> : {};
163
+ for (const handler of notificationHandlers) handler(message.method, params);
164
+ if (typeof message.id === "number") {
165
+ if (message.method !== "session/request_permission") {
166
+ // Vendor extensions (_x.ai/*, …) and any other unhandled client request:
167
+ // JSON-RPC method-not-found reply; session continues (#760).
168
+ child.stdin.write(`${JSON.stringify({
169
+ jsonrpc: "2.0",
170
+ id: message.id,
171
+ error: { code: -32601, message: `Method not found: ${message.method}` },
172
+ })}\n`);
173
+ return;
174
+ }
175
+ const choices = Array.isArray(params.options) ? params.options : [];
176
+ const selected = choices.find((value) =>
177
+ typeof value === "object" && value !== null && (value as { kind?: unknown }).kind === "allow_once") as { optionId?: unknown } | undefined;
178
+ if (typeof selected?.optionId !== "string") {
179
+ terminate(acpError("acp-permission-missing-allow-once", "ACP permission request omitted allow_once"));
180
+ return;
181
+ }
182
+ child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id: message.id, result: { outcome: { outcome: "selected", optionId: selected.optionId } } })}\n`);
174
183
  }
175
- child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id: message.id, result: { outcome: { outcome: "selected", optionId: selected.optionId } } })}\n`);
184
+ return;
176
185
  }
177
- return;
186
+ if (typeof message.id !== "number") return;
187
+ const waiter = pending.get(message.id);
188
+ if (waiter === undefined) return;
189
+ pending.delete(message.id);
190
+ if (message.error !== undefined) {
191
+ const diagnostic = typeof message.error === "object" && message.error !== null
192
+ && "message" in message.error && typeof message.error.message === "string"
193
+ ? message.error.message : JSON.stringify(message.error);
194
+ rpcFailure ??= { diagnostic, details: message };
195
+ waiter.reject(acpError("acp-upstream-error", diagnostic, message));
196
+ } else waiter.resolve((message.result ?? {}) as Readonly<Record<string, unknown>>);
197
+ } catch (error) {
198
+ terminate(error);
178
199
  }
179
- if (typeof message.id !== "number") return;
180
- const waiter = pending.get(message.id);
181
- if (waiter === undefined) return;
182
- pending.delete(message.id);
183
- if (message.error !== undefined) waiter.reject(acpError("acp-upstream-error", `ACP error: ${JSON.stringify(message.error)}`));
184
- else waiter.resolve((message.result ?? {}) as Readonly<Record<string, unknown>>);
185
200
  });
186
- child.on("close", (code) => settleClosed(acpError("acp-closed", `ACP closed (${String(code)}): ${stderr}`)));
201
+ const processClosed = new Promise<void>((resolve) => {
202
+ child.once("close", (code, signal) => {
203
+ // ENOENT also emits close, but no host process was launched in that case.
204
+ if (spawned) nativeExit = { code, timedOut: false, ...(signal === null ? {} : { signal }) };
205
+ settleClosed(acpError("acp-closed", "ACP process closed"));
206
+ resolve();
207
+ });
208
+ });
187
209
  return Promise.resolve({
188
210
  request(method, params) {
189
- if (closed) return Promise.reject(terminalError ?? acpError("acp-connection-closed", "ACP connection is closed"));
211
+ if (closed) return Promise.reject(terminalError);
190
212
  const id = ++nextId;
191
213
  return new Promise((resolve, reject) => {
192
214
  pending.set(id, { resolve, reject });
@@ -195,12 +217,12 @@ export function connectAcpStdio(options: {
195
217
  const waiter = pending.get(id);
196
218
  if (waiter === undefined) return;
197
219
  pending.delete(id);
198
- waiter.reject(acpError("acp-write-failed", `ACP write failed: ${error.message}`, error));
220
+ waiter.reject(error);
199
221
  });
200
222
  });
201
223
  },
202
224
  notify(method, params) {
203
- if (closed) throw terminalError ?? acpError("acp-connection-closed", "ACP connection is closed");
225
+ if (closed) throw terminalError;
204
226
  child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method, params })}\n`);
205
227
  },
206
228
  onNotification(handler) {
@@ -209,12 +231,18 @@ export function connectAcpStdio(options: {
209
231
  stderr() {
210
232
  return stderr;
211
233
  },
234
+ result() {
235
+ if (nativeExit === undefined && rpcFailure === undefined) return undefined;
236
+ return {
237
+ ...(nativeExit ?? { code: null, timedOut: false }),
238
+ stderr,
239
+ ...(rpcFailure === undefined ? {} : { knownFailure: rpcFailure }),
240
+ };
241
+ },
212
242
  async close() {
213
- if (closed) return;
214
- settleClosed(acpError("acp-connection-closed", "ACP connection is closed"));
215
- child.stdin.end();
216
- child.kill("SIGTERM");
217
- await new Promise<void>((resolve) => child.once("close", () => resolve()));
243
+ if (!closed) terminate(acpError("acp-connection-closed", "ACP connection is closed"));
244
+ await processClosed;
245
+ return !stoppedByPackage && nativeExit !== undefined ? { ...nativeExit, stderr } : undefined;
218
246
  },
219
247
  });
220
248
  }
@@ -229,8 +257,9 @@ export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHo
229
257
  let sessionOpened = false;
230
258
  let accepted = false;
231
259
  const sessionParent = config.sessionIdentity.resolveSessionFile(request.principal);
232
- // Mutable so dispose failure can outrank a clean turn.
233
260
  let outcome: RoleTurnResult = failure("session", "AcpNoOutcome", "no-outcome");
261
+ let reportedFailure: RoleTurnResult["knownFailure"];
262
+ let packageFailure: RoleTurnResult["knownFailure"];
234
263
  try {
235
264
  if (prepared.mcpServers.length === 0) {
236
265
  outcome = failure("activation", "UncontrolledAcpSession", "ak-config-missing");
@@ -361,9 +390,15 @@ export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHo
361
390
  collectAgentProse = false;
362
391
  if (result.stopReason === "refusal") {
363
392
  agentProseChunks.length = 0;
393
+ reportedFailure = { diagnostic: result.stopReason, details: result };
364
394
  return {
365
395
  status: "terminal",
366
- result: failure("output", "AcpRefusal", "refusal", { sessionId }),
396
+ result: {
397
+ code: null,
398
+ stderr: activeConnection.stderr?.() ?? "",
399
+ timedOut: false,
400
+ knownFailure: reportedFailure,
401
+ },
367
402
  };
368
403
  }
369
404
  // #959: navigator prose exit when the model spoke without the output tool.
@@ -389,32 +424,90 @@ export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHo
389
424
  } // sessionReady
390
425
  } // model match else
391
426
  } // mcpServers else
427
+ } catch (error) {
428
+ const native = connection?.result?.();
429
+ reportedFailure = native?.knownFailure;
430
+ outcome = native ?? { code: null, stderr: connection?.stderr?.() ?? "", timedOut: false };
431
+ await retainPackageFault({
432
+ runDirectory: request.runDirectory,
433
+ diagnostic: `ACP turn exception beside native host facts: ${describeErrorIdentity(error)}`,
434
+ error,
435
+ });
436
+ // Native abnormal exit / RPC report stays primary. Otherwise this is a
437
+ // real package exception, not a host cause inferred from missing output.
438
+ if (outcome.knownFailure === undefined && !outcome.timedOut && outcome.signal === undefined
439
+ && (native === undefined || outcome.code === 0 || outcome.code === null)) {
440
+ packageFailure = projectThrownFailureLeaf(error);
441
+ outcome = { ...outcome, knownFailure: packageFailure };
442
+ }
392
443
  } finally {
444
+ let naturalTermination: RoleTurnResult | void = undefined;
393
445
  if (connection !== undefined) {
394
446
  if (sessionId !== undefined && !accepted) {
395
447
  try { connection.notify("session/cancel", { sessionId }); }
396
- catch { /* keep turn result */ }
448
+ catch (error) {
449
+ await retainPackageFault({
450
+ runDirectory: request.runDirectory,
451
+ diagnostic: `session cancel failed beside host terminal: ${describeErrorIdentity(error)}`,
452
+ error,
453
+ });
454
+ }
455
+ }
456
+ try { naturalTermination = await connection.close(); }
457
+ catch (error) {
458
+ await retainPackageFault({
459
+ runDirectory: request.runDirectory,
460
+ diagnostic: `connection close failed beside host terminal: ${describeErrorIdentity(error)}`,
461
+ error,
462
+ });
397
463
  }
398
- try { await connection.close(); }
399
- catch { /* keep turn result */ }
400
464
  }
401
- if (config.hostName !== "hermes" && sessionOpened && sessionId !== undefined) {
402
- copyAndRecordHostDossier({
403
- host: config.hostName,
404
- sessionId,
405
- cwd: request.cwd,
406
- sessionDirectory: join(request.runDirectory, "session"),
407
- sessionParent,
408
- continuation: request.continuation,
409
- ...(request.model !== undefined ? { model: request.model } : {}),
410
- ...(request.home !== undefined ? { home: request.home } : {}),
411
- });
465
+ // Both success and failure drain the native stderr stream, including
466
+ // bytes arriving after the last RPC response.
467
+ outcome = { ...outcome, stderr: connection?.stderr?.() ?? outcome.stderr };
468
+ // On abnormal paths, teardown has now drained the real close/stderr.
469
+ // A successful ACP protocol turn remains successful: its ordinary
470
+ // connection cleanup is not reclassified as a role failure.
471
+ const native = connection?.result?.() ?? naturalTermination;
472
+ if (native !== undefined && (naturalTermination !== undefined || outcome.code !== 0
473
+ || outcome.knownFailure !== undefined || outcome.timedOut || outcome.signal !== undefined)) {
474
+ // A late native failure stays primary over a package failure. An own
475
+ // SIGTERM stop alone is ordinary disposal, not a guessed host cause.
476
+ const nativeFailed = (native.code !== null && native.code !== 0)
477
+ || (naturalTermination !== undefined && (native.timedOut || native.signal !== undefined));
478
+ let knownFailure = outcome.knownFailure;
479
+ if (nativeFailed && knownFailure !== undefined && knownFailure !== reportedFailure) {
480
+ if (knownFailure !== packageFailure) await retainPackageFault({
481
+ runDirectory: request.runDirectory,
482
+ diagnostic: knownFailure.diagnostic ?? "ACP package failure beside late native termination",
483
+ error: knownFailure,
484
+ });
485
+ knownFailure = reportedFailure;
486
+ }
487
+ const { knownFailure: _nativeFailure, ...facts } = native;
488
+ outcome = { ...facts, ...(knownFailure === undefined ? {} : { knownFailure }) };
412
489
  }
413
- try {
414
- await prepared.dispose?.();
415
- } catch (cleanupError) {
416
- outcome = withExternalHostCleanupFailure(outcome, cleanupError, "AcpDisposeFailure");
490
+ if (config.hostName !== "hermes" && sessionOpened && sessionId !== undefined) {
491
+ try {
492
+ copyAndRecordHostDossier({
493
+ host: config.hostName,
494
+ sessionId,
495
+ cwd: request.cwd,
496
+ sessionDirectory: join(request.runDirectory, "session"),
497
+ sessionParent,
498
+ continuation: request.continuation,
499
+ ...(request.model !== undefined ? { model: request.model } : {}),
500
+ ...(request.home !== undefined ? { home: request.home } : {}),
501
+ });
502
+ } catch (error) {
503
+ await retainPackageFault({
504
+ runDirectory: request.runDirectory,
505
+ diagnostic: `host dossier copy failed beside host terminal: ${describeErrorIdentity(error)}`,
506
+ error,
507
+ });
508
+ }
417
509
  }
510
+ outcome = await disposeExternalRoleTurn(prepared, request, outcome);
418
511
  }
419
512
  return outcome;
420
513
  });
@@ -4,7 +4,7 @@ import { withTerminatingOutputDeclarations } from "./package-contracts/terminati
4
4
 
5
5
  /**
6
6
  * #1088: LLM gathers evidence via host CLI and submits the receipt.
7
- * Field declarations guide the model — host must not pure-shape-reject (第 0 条 / ADR 0057).
7
+ * Field declarations guide the model — host must not pure-shape-reject (仓内 CLAUDE.md 开篇 / ADR 0057).
8
8
  * Presence of `groups` remains the Collector terminal discriminator for settlement/analyst.
9
9
  */
10
10
  export const collectorOutputBaseSchema = openToolObject(
@@ -6,12 +6,12 @@ import type { NoReceiptLifecycleFacts } from "./receipt-delivery-policy.ts";
6
6
  import type { PublicSummonResult } from "./public-role-summons.ts";
7
7
  import { officerConclusionReask } from "./gatekeeper-role.ts";
8
8
  import { receivedDiscriminator } from "./submission-errors.ts";
9
- import { coalesceSubmissionRows } from "./public-cli/terminal.ts";
9
+ import { currentReplyRows } from "./public-cli/terminal.ts";
10
10
  import { readableGateItem } from "./readable-gate-item.ts";
11
11
  import { deliveryLimitFromConfig } from "./receipt-delivery-policy.ts";
12
12
  import { isRecord } from "./unknown-value.ts";
13
13
 
14
- export type ComplianceNoReceipt = NoReceiptLifecycleFacts & { status: "no-receipt"; usage?: Usage };
14
+ export type ComplianceNoReceipt = Partial<NoReceiptLifecycleFacts> & { status: "no-receipt"; usage?: Usage };
15
15
  /**
16
16
  * #757 / #750: no unreadable/unusable judgment on auditor replies.
17
17
  * Known three-state (converged/continue/escalate) is read for queueing only.
@@ -172,10 +172,7 @@ async function projectAuditorTerminal(summoned: PublicSummonResult): Promise<Com
172
172
  };
173
173
  }
174
174
  if (outcome.kind === "accepted") {
175
- const rows = coalesceSubmissionRows(
176
- outcome.payloads,
177
- summoned.terminal?.submissions,
178
- );
175
+ const rows = currentReplyRows(summoned.terminal);
179
176
  if (rows.length === 0) {
180
177
  return readComplianceCandidate({}, usage);
181
178
  }
@@ -37,8 +37,8 @@ export const ENGINE_DETOUR_TOOL_USAGE_FACT_KEY = "engineDetourToolUsage" as cons
37
37
 
38
38
  /**
39
39
  * Run-relative sitian volume path for one detour call.
40
- * Openable once the run directory is known (resume.command / top-level runId);
41
- * contains no runId bytes itself (#108 + #537 AC8).
40
+ * Openable once the run directory is known from the terminal's run id.
41
+ * Contains no runId bytes itself (#108 + #537).
42
42
  */
43
43
  export const ENGINE_DETOUR_CALL_RECORD_FILE_RELATIVE =
44
44
  `session/${ENGINE_DETOUR_CALL_KIND}/records.jsonl` as const;
@@ -232,7 +232,7 @@ export async function readEngineDetourToolUsage(input: {
232
232
  }
233
233
  const pointer: RecordPointer = {
234
234
  identity: record.identity,
235
- recordFile,
235
+ recordFile: ENGINE_DETOUR_CALL_RECORD_FILE_RELATIVE,
236
236
  kind: record.kind,
237
237
  level: record.level,
238
238
  };
@@ -246,36 +246,6 @@ export async function readEngineDetourToolUsage(input: {
246
246
  return { callCount: calls.length, calls };
247
247
  }
248
248
 
249
- /**
250
- * Project usage onto the public Terminal face.
251
- * Non-resumable: keep absolute recordFile (runId already public via top-level runId).
252
- * Resumable: keep an openable run-relative recordFile and identity with no runId
253
- * bytes (#108 single disclosure + #537 AC8 reopen).
254
- */
255
- export function projectEngineDetourToolUsageForPublicTerminal(
256
- usage: EngineDetourToolUsageFact,
257
- options: { readonly discloseRecordFile: boolean },
258
- ): EngineDetourToolUsageFact {
259
- if (options.discloseRecordFile) return usage;
260
- return {
261
- callCount: usage.callCount,
262
- calls: usage.calls.map((call) => ({
263
- toolCallId: call.toolCallId,
264
- durationMs: call.durationMs,
265
- ...(call.code === undefined ? {} : { code: call.code }),
266
- ...(call.stdoutByteLength === undefined
267
- ? {}
268
- : { stdoutByteLength: call.stdoutByteLength }),
269
- recordPointer: {
270
- identity: call.recordPointer.identity,
271
- kind: call.recordPointer.kind,
272
- level: call.recordPointer.level,
273
- recordFile: ENGINE_DETOUR_CALL_RECORD_FILE_RELATIVE,
274
- },
275
- })),
276
- };
277
- }
278
-
279
249
  /** One shared invocation.json reader (ENOENT → undefined; other errors propagate). */
280
250
  function readInvocationRecord(
281
251
  runDirectory: string,