@sema-agent/server 7.43.0 → 7.44.0

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 (345) hide show
  1. package/USAGE.md +18 -0
  2. package/dist/adoption/plan.js +31 -192
  3. package/dist/adoption/quiesce.js +3 -86
  4. package/dist/adoption/runner.js +3 -140
  5. package/dist/adoption/sql.js +0 -74
  6. package/dist/adoption/wire.js +0 -80
  7. package/dist/approval-ask-machine.js +0 -75
  8. package/dist/approval-card.js +0 -323
  9. package/dist/approval-deny-reasons.js +0 -42
  10. package/dist/approval-hmac.js +0 -35
  11. package/dist/approval-reconciler.js +8 -166
  12. package/dist/approval.js +4 -67
  13. package/dist/audit.js +1 -44
  14. package/dist/auth-bridge.js +4 -47
  15. package/dist/auth-keys.js +0 -23
  16. package/dist/bake-runner/main.js +4 -65
  17. package/dist/bake-runner/protocol.js +3 -58
  18. package/dist/bake-runner/runner.js +5 -91
  19. package/dist/bench/l8/artifact.js +2 -61
  20. package/dist/bench/l8/escape.js +0 -25
  21. package/dist/bench/l8/index.js +0 -14
  22. package/dist/bench/l8/probes.js +0 -60
  23. package/dist/bench/l8/run-probes.js +2 -45
  24. package/dist/bench/s1/arms.js +14 -189
  25. package/dist/bench/s1/live-deps.js +11 -253
  26. package/dist/bench/s1/oracle.js +1 -36
  27. package/dist/bench/s1/repair-oracle-adapter.js +0 -30
  28. package/dist/bench/s1/reviewer.js +0 -34
  29. package/dist/bench/s1/row.js +0 -62
  30. package/dist/bench/s1/run-firm.js +4 -70
  31. package/dist/bench/s1/runner-ctx.js +0 -40
  32. package/dist/bench/s1/tasks.js +0 -115
  33. package/dist/boot/adoption.js +0 -20
  34. package/dist/boot/budget-tracing.js +3 -55
  35. package/dist/boot/config-center.js +43 -551
  36. package/dist/boot/coordinators.js +1 -69
  37. package/dist/boot/crash-last.js +0 -18
  38. package/dist/boot/deferred-sandbox-path-env.js +3 -119
  39. package/dist/boot/execution-env.js +5 -171
  40. package/dist/boot/governance-seams.js +0 -92
  41. package/dist/boot/leader.js +0 -69
  42. package/dist/boot/limit-sync.js +0 -2
  43. package/dist/boot/memory-boundary.js +3 -91
  44. package/dist/boot/org-memory.js +1 -25
  45. package/dist/boot/parked-revive-gate.js +0 -150
  46. package/dist/boot/permission-rules-audit.js +1 -56
  47. package/dist/boot/reapers.js +15 -273
  48. package/dist/boot/resolve-spec.js +9 -768
  49. package/dist/boot/retention-lane.js +4 -87
  50. package/dist/boot/runner-deps.js +3 -297
  51. package/dist/boot/runtime-caps.js +0 -40
  52. package/dist/boot/session-faces.js +2 -133
  53. package/dist/boot/shutdown.js +9 -83
  54. package/dist/boot/side-query-lane.js +2 -137
  55. package/dist/boot/stores.js +15 -215
  56. package/dist/boot/task-list-lane.js +0 -18
  57. package/dist/boot/webfetch-summarize-lane.js +0 -47
  58. package/dist/boot/workflow-orchestration.js +3 -99
  59. package/dist/boot-reclaim.js +0 -20
  60. package/dist/bounded-session-map.js +0 -19
  61. package/dist/brain.js +2 -139
  62. package/dist/budget.js +9 -244
  63. package/dist/capabilities/builtin-tools.js +0 -3
  64. package/dist/capabilities/center-plugins.js +6 -51
  65. package/dist/capabilities/center-prompts.js +6 -71
  66. package/dist/capabilities/code-review-council.js +7 -40
  67. package/dist/capabilities/collab-workflows.js +1 -44
  68. package/dist/capabilities/hands-lane.js +0 -65
  69. package/dist/capabilities/memory-notice.js +0 -70
  70. package/dist/capabilities/prompt.js +0 -14
  71. package/dist/capabilities/prompts/code-review.js +0 -14
  72. package/dist/capabilities/prompts/identity.js +0 -6
  73. package/dist/capabilities/prompts/team.js +0 -4
  74. package/dist/capabilities/repo-tools.js +0 -34
  75. package/dist/capabilities/sandbox-file-send.js +3 -92
  76. package/dist/capabilities/scenarios.d.ts +0 -1
  77. package/dist/capabilities/scenarios.js +18 -279
  78. package/dist/capabilities/select-environment-tool.js +0 -32
  79. package/dist/capabilities/send-user-file-tool.js +2 -66
  80. package/dist/capabilities/skills.d.ts +1 -2
  81. package/dist/capabilities/skills.js +3 -21
  82. package/dist/capabilities/team.d.ts +3 -11
  83. package/dist/capabilities/team.js +3 -51
  84. package/dist/capabilities/tool-defer.js +0 -3
  85. package/dist/config-center/apply-effective.js +15 -330
  86. package/dist/config-center/apply-ledger.js +5 -38
  87. package/dist/config-center/facade.js +0 -41
  88. package/dist/config-center/hot-keys-registry.js +0 -20
  89. package/dist/config-center/http-client.js +2 -125
  90. package/dist/config-center/mcp-revocation.js +2 -34
  91. package/dist/config-center/read-face.js +0 -59
  92. package/dist/config-center/restart-signal.js +1 -85
  93. package/dist/config-center/skills-mcp.d.ts +1 -1
  94. package/dist/config-center/skills-mcp.js +9 -76
  95. package/dist/config-center/stage-limits.js +8 -35
  96. package/dist/config-invariants.js +0 -16
  97. package/dist/config-lkg.js +0 -42
  98. package/dist/config-provider.js +3 -186
  99. package/dist/config-types.js +0 -5
  100. package/dist/config.js +107 -1145
  101. package/dist/degenerate-instrument.js +3 -67
  102. package/dist/deployment-governance.js +0 -124
  103. package/dist/digest-form.js +0 -12
  104. package/dist/elicitation.js +3 -86
  105. package/dist/env-facts.js +7 -75
  106. package/dist/fleet/fleet-bus.js +34 -507
  107. package/dist/fleet/fleet-reconciler.js +9 -149
  108. package/dist/fleet/fleet-terminal-window.js +10 -178
  109. package/dist/fleet/subagent-tail-bus.js +3 -72
  110. package/dist/fleet-client.js +10 -70
  111. package/dist/fleet-lease.js +5 -79
  112. package/dist/git-api-kind.js +0 -3
  113. package/dist/governance-ask-marks.js +2 -78
  114. package/dist/hooks/branch-transcript.js +0 -74
  115. package/dist/hooks/cc-agent-hook-prompt.js +0 -29
  116. package/dist/hooks/cc-stop-prompt.js +1 -46
  117. package/dist/hooks/hook-llm.js +1 -53
  118. package/dist/hooks/hook-runner.js +20 -414
  119. package/dist/http/active-run-conflict.js +4 -129
  120. package/dist/http/cursor-fingerprint.d.ts +5 -0
  121. package/dist/http/cursor-fingerprint.js +5 -0
  122. package/dist/http/idempotency.js +0 -37
  123. package/dist/http/principal-gate.js +3 -40
  124. package/dist/http/route-ctx.js +0 -9
  125. package/dist/http/routes/a2a-serve.js +7 -319
  126. package/dist/http/routes/admin-config-refresh.js +0 -4
  127. package/dist/http/routes/admin-drain.js +0 -10
  128. package/dist/http/routes/adoption.js +1 -29
  129. package/dist/http/routes/agents-roster.js +1 -47
  130. package/dist/http/routes/approvals-assistant.js +32 -395
  131. package/dist/http/routes/attachments.js +4 -22
  132. package/dist/http/routes/capabilities.js +3 -420
  133. package/dist/http/routes/diagnostics.js +1 -81
  134. package/dist/http/routes/fleet.js +9 -185
  135. package/dist/http/routes/images.js +14 -239
  136. package/dist/http/routes/leader.js +0 -13
  137. package/dist/http/routes/memory-bundle.js +1 -61
  138. package/dist/http/routes/memory-policy.js +9 -99
  139. package/dist/http/routes/notify-wake.js +3 -37
  140. package/dist/http/routes/observability.js +2 -19
  141. package/dist/http/routes/retention-ops.js +2 -34
  142. package/dist/http/routes/rules.js +1 -93
  143. package/dist/http/routes/runs.js +64 -873
  144. package/dist/http/routes/session-sync.js +19 -258
  145. package/dist/http/routes/sessions-list.js +17 -43
  146. package/dist/http/routes/sessions.js +34 -226
  147. package/dist/http/routes/shared-memory.js +5 -36
  148. package/dist/http/routes/side-query.js +1 -87
  149. package/dist/http/routes/tasks.js +52 -722
  150. package/dist/http/routes/trace-usage.js +32 -195
  151. package/dist/http/routes/workflows.js +19 -195
  152. package/dist/http/run-meta.js +0 -6
  153. package/dist/http/send.js +0 -32
  154. package/dist/http/server.js +92 -1597
  155. package/dist/http/sse-lifecycle.js +2 -13
  156. package/dist/http/sse-log.js +3 -48
  157. package/dist/http/tar.js +5 -21
  158. package/dist/http/verify-rounds.js +0 -5
  159. package/dist/http/wire-gate.js +0 -9
  160. package/dist/http/workspace-content.js +0 -10
  161. package/dist/images/bake-validate.js +1 -70
  162. package/dist/images/manifest.js +1 -6
  163. package/dist/index.js +0 -21
  164. package/dist/key-resolver.js +2 -17
  165. package/dist/leader/diffout.js +1 -20
  166. package/dist/leader/diffup.js +0 -47
  167. package/dist/leader/endpoint.js +2 -57
  168. package/dist/leader/fanout.js +3 -45
  169. package/dist/leader/grader-env-factory.js +3 -72
  170. package/dist/leader/leader.js +5 -156
  171. package/dist/leader/merge.js +8 -115
  172. package/dist/leader/planner.js +3 -54
  173. package/dist/leader/repair-oracle.js +1 -60
  174. package/dist/leader/repair-wire.js +2 -79
  175. package/dist/leader/wire.js +8 -307
  176. package/dist/lsp/e2b-bridge.js +4 -64
  177. package/dist/lsp/e2b-manager.js +6 -94
  178. package/dist/lsp/lsp-frames.js +0 -12
  179. package/dist/lsp/manager.js +4 -96
  180. package/dist/lsp/ws-transport.js +5 -55
  181. package/dist/lsp-evict.js +1 -15
  182. package/dist/main.js +57 -765
  183. package/dist/memory-bundle-engine.js +0 -55
  184. package/dist/memory-export.js +0 -4
  185. package/dist/memory-posture.js +1 -15
  186. package/dist/memory-scope.js +11 -148
  187. package/dist/memory-sync-client.js +2 -44
  188. package/dist/memory-sync.js +1 -80
  189. package/dist/model-select.js +3 -80
  190. package/dist/observability/cost-quota.js +1 -17
  191. package/dist/observability/cost-taxonomy.js +0 -34
  192. package/dist/observability/fail-open.js +7 -86
  193. package/dist/observability/logger.js +0 -6
  194. package/dist/observability/metrics.js +0 -94
  195. package/dist/observability/otel-exporter.js +3 -13
  196. package/dist/observability/principal-context.js +0 -9
  197. package/dist/observability/prompt-manifest.js +1 -37
  198. package/dist/observability/rate-limit.js +0 -4
  199. package/dist/observability/secret-env-scrub.js +2 -56
  200. package/dist/observability/tool-trace.js +1 -70
  201. package/dist/orchestration/hardened-vm-runner.js +4 -118
  202. package/dist/orchestration/hardened-vm-worker-runner.js +1 -26
  203. package/dist/orchestration/hardened-vm-worker.js +0 -27
  204. package/dist/orchestration/subagent-steer.js +1 -45
  205. package/dist/orchestration/workflow-agent-steer.js +1 -80
  206. package/dist/orchestration/workflow-completion-inbox.js +32 -285
  207. package/dist/orchestration/workflow-notify-journal.js +16 -259
  208. package/dist/org-memory-admission.js +3 -47
  209. package/dist/parent-watch.js +2 -48
  210. package/dist/parked-decide.js +1 -109
  211. package/dist/per-task-image.js +0 -57
  212. package/dist/plan-cache-probe.js +3 -27
  213. package/dist/plugins/adoption-log-sql.js +2 -119
  214. package/dist/plugins/approval-ask-store-memory.js +3 -38
  215. package/dist/plugins/approval-ask-store-sql.js +6 -188
  216. package/dist/plugins/approval-exemption-store.js +2 -28
  217. package/dist/plugins/background-agent-store-sql.js +4 -105
  218. package/dist/plugins/background-shell-support.js +14 -122
  219. package/dist/plugins/blob-backend.js +6 -169
  220. package/dist/plugins/breaker-state-sql.js +8 -46
  221. package/dist/plugins/caching-session-store.js +4 -106
  222. package/dist/plugins/checkpoint-store-sql.js +18 -505
  223. package/dist/plugins/e2b-orphan-reclaim.js +0 -45
  224. package/dist/plugins/file-outcome-sink.js +0 -9
  225. package/dist/plugins/file-resume-anchor-store.js +4 -43
  226. package/dist/plugins/file-run-store.js +26 -364
  227. package/dist/plugins/file-snapshot-store-sql.js +10 -181
  228. package/dist/plugins/fork-routing-session-store.js +8 -111
  229. package/dist/plugins/host-platform.js +2 -91
  230. package/dist/plugins/image-bake-store-sql.js +7 -250
  231. package/dist/plugins/image-index-sql.js +4 -123
  232. package/dist/plugins/k8s-bg-scripts.js +4 -88
  233. package/dist/plugins/k8s-exec-protocol.js +0 -41
  234. package/dist/plugins/leader-run-store-sql.js +0 -101
  235. package/dist/plugins/local-checkpoint-store.js +8 -128
  236. package/dist/plugins/local-session-store.js +34 -296
  237. package/dist/plugins/local-task-attachment-store.js +2 -16
  238. package/dist/plugins/mailbox-store-sql.js +8 -65
  239. package/dist/plugins/memory-embedder-fingerprint.js +5 -166
  240. package/dist/plugins/memory-embedder.js +4 -70
  241. package/dist/plugins/memory-engine-pg.js +6 -170
  242. package/dist/plugins/memory-engine-tidb.js +7 -155
  243. package/dist/plugins/memory-engine-vector-util.js +0 -10
  244. package/dist/plugins/memory-key-guards.js +0 -34
  245. package/dist/plugins/memory-origin-law.js +0 -187
  246. package/dist/plugins/memory-resume-anchor-store.js +0 -17
  247. package/dist/plugins/memory-run-store.js +12 -89
  248. package/dist/plugins/memory-session-policy-store.js +0 -17
  249. package/dist/plugins/memory-sync-store-pg.js +4 -49
  250. package/dist/plugins/memory-sync-store-tidb.js +3 -35
  251. package/dist/plugins/outcome-ledger-sql.js +3 -97
  252. package/dist/plugins/permission-rule-store-file.js +5 -133
  253. package/dist/plugins/permission-rule-store-sql.d.ts +3 -0
  254. package/dist/plugins/permission-rule-store-sql.js +50 -307
  255. package/dist/plugins/pg-cost-quota.js +0 -7
  256. package/dist/plugins/pg-pool.js +0 -92
  257. package/dist/plugins/pg-rate-limiter.js +2 -13
  258. package/dist/plugins/pg-safe-json.js +4 -40
  259. package/dist/plugins/pg-session-storage.js +25 -189
  260. package/dist/plugins/posix-shell-fs.js +1 -31
  261. package/dist/plugins/remote-env-adb.js +12 -101
  262. package/dist/plugins/remote-env-e2b.js +40 -371
  263. package/dist/plugins/remote-env-file-error.js +0 -37
  264. package/dist/plugins/remote-env-host.js +63 -483
  265. package/dist/plugins/remote-env-k8s.js +32 -326
  266. package/dist/plugins/remote-env-local-docker.js +24 -157
  267. package/dist/plugins/remote-env-ssh.js +20 -128
  268. package/dist/plugins/remote-scratchpad.js +2 -32
  269. package/dist/plugins/remote-shell.js +1 -32
  270. package/dist/plugins/resume-anchor-store-sql.js +0 -11
  271. package/dist/plugins/retention-lane-store-sql.js +0 -108
  272. package/dist/plugins/retention-store-sql.js +4 -383
  273. package/dist/plugins/roster-store-sql.js +0 -55
  274. package/dist/plugins/run-store-sql.js +14 -251
  275. package/dist/plugins/s3-presign.js +2 -49
  276. package/dist/plugins/scheduler-support.js +3 -80
  277. package/dist/plugins/send-file-ledger.js +4 -53
  278. package/dist/plugins/send-user-file.js +4 -94
  279. package/dist/plugins/session-placement.js +1 -89
  280. package/dist/plugins/session-policy-store-sql.js +3 -81
  281. package/dist/plugins/session-store.js +0 -57
  282. package/dist/plugins/shared-memory-store-sql.js +4 -167
  283. package/dist/plugins/sql-driver.js +0 -17
  284. package/dist/plugins/sql-errors.js +0 -7
  285. package/dist/plugins/sql-escape.js +0 -8
  286. package/dist/plugins/sql-row-helpers.js +0 -25
  287. package/dist/plugins/store-backend.js +42 -222
  288. package/dist/plugins/store-contracts.js +2 -46
  289. package/dist/plugins/task-attachment-store.js +3 -49
  290. package/dist/plugins/task-list-store-sql.js +0 -79
  291. package/dist/plugins/tidb-cost-quota.js +1 -4
  292. package/dist/plugins/tidb-pool.js +1 -207
  293. package/dist/plugins/tidb-rate-limiter.js +3 -9
  294. package/dist/plugins/tidb-session-storage.js +4 -70
  295. package/dist/plugins/tidb-session-store.js +19 -341
  296. package/dist/plugins/tool-result-store-sql.js +4 -179
  297. package/dist/plugins/usage-window-store-sql.js +0 -8
  298. package/dist/plugins/web-search.js +10 -132
  299. package/dist/plugins/workflow-journal-store-sql.js +2 -58
  300. package/dist/plugins/workflow-run-store-sql.js +6 -91
  301. package/dist/plugins/worktree-isolation.js +6 -126
  302. package/dist/plugins/write-behind-counter.js +16 -75
  303. package/dist/principal-jwt.js +5 -60
  304. package/dist/project-memory.js +15 -146
  305. package/dist/prompts-domain-validate.js +1 -51
  306. package/dist/question.js +2 -111
  307. package/dist/resource-suspend.js +0 -18
  308. package/dist/router/route-orchestration.js +0 -77
  309. package/dist/rules-consent.d.ts +44 -5
  310. package/dist/rules-consent.js +75 -215
  311. package/dist/run-local.js +14 -381
  312. package/dist/runs.js +29 -617
  313. package/dist/runtime-caps-resolver.js +6 -132
  314. package/dist/runtime-governance.js +1 -232
  315. package/dist/sandbox-pkg-source.js +0 -37
  316. package/dist/sealed-key.js +3 -68
  317. package/dist/security.js +6 -291
  318. package/dist/session-leaf-bus.js +0 -32
  319. package/dist/session-sync-content.js +1 -69
  320. package/dist/session-sync-kernel.js +3 -60
  321. package/dist/session-sync.js +3 -66
  322. package/dist/session-titler.js +7 -41
  323. package/dist/session-watch.js +9 -73
  324. package/dist/shared-memory-scope-authorizer.js +0 -18
  325. package/dist/sighup-idle.js +1 -12
  326. package/dist/spec-fields.js +4 -143
  327. package/dist/store-live-probe.js +3 -49
  328. package/dist/task-a2a.js +1 -126
  329. package/dist/task-cwd.js +1 -103
  330. package/dist/task-mcp.js +1 -89
  331. package/dist/task-settings.js +10 -300
  332. package/dist/task-workflow.js +8 -75
  333. package/dist/tool-approval.d.ts +28 -1
  334. package/dist/tool-approval.js +70 -1430
  335. package/dist/trace/artifacts.js +5 -20
  336. package/dist/trace/engine-notice-wire.js +6 -138
  337. package/dist/trace/ledger-sink.js +7 -96
  338. package/dist/trace/project.js +4 -448
  339. package/dist/trace/redact.js +9 -77
  340. package/dist/turn-activity.js +1 -27
  341. package/dist/usage-analytics.js +4 -34
  342. package/dist/wall-clock-jump-guard.js +1 -68
  343. package/package.json +3 -3
  344. package/dist/capabilities/scenario-alias.d.ts +0 -27
  345. package/dist/capabilities/scenario-alias.js +0 -61
@@ -1,45 +1,3 @@
1
- /**
2
- * hooks hook-runner — hooks 支持的 service 半场,阶段二。
3
- *
4
- * 契约 = `@sema-agent/registry-core/hooks`(0.1.51,30 事件全集、CC 2.1.198 逐字形状);runner 把
5
- * `TaskRequest.settings.hooks` 里的 **command 类型** 条目翻成 core `TaskSpec.hooks`(types §hooks,
6
- * 9 个 engine-owned 回调 core 1.254 已收满)的进程内回调。
7
- *
8
- * 阶段二范围(engine-owned 9 事件全点亮;加事件=纯实现不动契约):
9
- * - 事件:PreToolUse/PostToolUse(阶段一,语义不动)+ UserPromptSubmit/Stop/StopFailure/
10
- * PostToolUseFailure/PostToolBatch/PreCompact/PostCompact(阶段二)。matcher 按契约
11
- * HOOK_EVENT_MATCHER_FIELD:null 事件(UserPromptSubmit/Stop/PostToolBatch)matcher 被忽略、
12
- * 全条目都跑;StopFailure 对 error(AssistantTurnError 枚举)、PreCompact/PostCompact 对 trigger、
13
- * PostToolUseFailure 对 tool_name 匹配。core 回调面是 observe-only 的事件(StopFailure/PostCompact)
14
- * 的决策类输出记 `hook_result_unsupported`,warn 不吞。
15
- * - shell-owned 事件(其余 21 个)的配置到达引擎=正常(壳自己跑,owner-single):记一条
16
- * `hook_events_shell_owned` debug 可见,不告警;engine-owned 且未点亮的事件(阶段二后=空集,
17
- * 机制保留防契约扩容)才记 `hook_events_not_fired` warn,绝不静默。
18
- * - 条目类型:`command`(bash 族)+ `http`(阶段三a,见 {@link runHttpHook}——POST 输入 JSON、
19
- * 2xx JSON body 走同一 SyncHookOutput 决策面)。`prompt`/`agent`(需模型调用设计:默认模型/用量
20
- * 归属/catalog 门=阶段三b 挂账)与 `shell:"powershell"` 跳过(`hook_entries_skipped` 记账);
21
- * `async` 支持为 fire-and-forget;`asyncRewake` = async + exit 2 时经 ctx.wake 唤醒模型(live
22
- * TaskStream steer 注入;无活流/无管道则记账降级);`if` 条件=CC-exact permission-rule 模式
23
- * (见 {@link hookIfMatches}:不匹配/评不出→跳过该条目记账,对照 CC 2.1.187/198 实测行为核验);
24
- * `statusMessage` 是壳 UI 字段、服务端无面。
25
- *
26
- * 🔒 安全边界(design/107 class ② 能力授予):hook 命令跑在 **worker host**(server 进程环境),不在
27
- * 沙箱——与 CC 语义一致(CC hooks 跑在用户机器上,哪怕工具在远端执行)。因此挂载点(main.ts)用
28
- * **单用户闸** `requirePrincipal !== true`,与 MCP 注入/shellEnv/scheduler 同一姿势;多租户 lane
29
- * 收到 hooks 配置只警告忽略(`task_hooks_ignored`),绝不执行。
30
- *
31
- * 🔒 整槽覆盖陷阱(core runtask `spec.hooks ?? deps.hooks`):task 级 hooks 一旦挂上会 shadow 部署级
32
- * hooks(TOOL_TRACE 观测就挂在 deps.hooks)。{@link composeHooks} 把部署基线折进 task hooks——部署槽
33
- * 先跑(观测在前,看到真实执行),用户槽后跑,结果按槽语义合并。
34
- *
35
- * CC exit-code 语义(与 CC 对齐):
36
- * - exit 0:stdout 若是 JSON → SyncHookOutput(decision/hookSpecificOutput/continue);非 JSON → 忽略。
37
- * - exit 2:阻断——PreToolUse → deny;UserPromptSubmit → block 提交;Stop → block 停止(继续干活);
38
- * PreCompact → block 本次压缩(stderr 均作为模型/追溯可读原因);PostToolUse/PostToolUseFailure/
39
- * PostToolBatch → 调用已落地无从阻断,stderr 作为 additionalContext 反馈给模型;observe-only 事件
40
- * (StopFailure/PostCompact)无阻断面,按 hook 故障记账。
41
- * - 其他 exit / 超时:非阻断,警告记录(hook 故障绝不误伤任务)。
42
- */
43
1
  import { spawn } from "node:child_process";
44
2
  import { hostShell, resolveHostShell } from "../plugins/host-platform.js";
45
3
  import { HooksConfig, DEFAULT_HOOK_TIMEOUT_SECONDS, HOOK_EVENT_OWNER, } from "@sema-agent/registry-core/hooks";
@@ -47,62 +5,23 @@ import { redactSecrets } from "../trace/redact.js";
47
5
  import { ccPromptSystemFor, wrapCondition, parseCcVerdict, CC_EVALUATOR_MAX_OUTPUT_TOKENS } from "./cc-stop-prompt.js";
48
6
  import { ccAgentHookSystemFor } from "./cc-agent-hook-prompt.js";
49
7
  import { renderBranchTranscript } from "./branch-transcript.js";
50
- /** 服务侧防线上限(契约本身不设量纲;不设界=单请求可塞任意大配置/任意长命令)。超界=校验错(fail-loud)。 */
51
- // S1([1870]):hook 的 shell 与 host lane 同源(core getShellConfig,bash 优先)。模块装载即预热
52
- // 解析(fire-and-forget;未解析先到的 spawn 拿 sh 兜底=修前字节形,解析后恒 bash)。
53
8
  void resolveHostShell().catch(() => undefined);
54
9
  export const MAX_HOOK_ENTRIES_PER_EVENT = 32;
55
10
  export const MAX_HOOK_COMMAND_CHARS = 8_192;
56
- /** 单条 hook 超时上限(秒;条目可自设 timeout,但被此值夹住——一个 86400 的 timeout 会挂死工具门)。 */
57
11
  export const MAX_HOOK_TIMEOUT_SECONDS = 600;
58
- /** 一次 hook 事件(一次 pre/post 调用)所有同步条目的墙钟总预算(秒;32 条 × 单条 600s
59
- * 最坏可串行卡住工具门 ~320min。到点后不再起后续条目,记账降级)。 */
60
12
  export const MAX_HOOK_EVENT_TOTAL_SECONDS = 120;
61
- /** 一次事件的 matcher 组数上限(配置广度的另一维,防组数爆炸)。 */
62
13
  export const MAX_HOOK_MATCHER_GROUPS = 16;
63
- /** http 条目的 header 条数 / allowedEnvVars 个数上限(构造放大面)。 */
64
14
  export const MAX_HOOK_HTTP_HEADERS = 32;
65
- /** hook 进程 stdout/stderr 各自的采集上限(字节)——防输出洪泛打爆内存/日志。 */
66
15
  export const MAX_HOOK_OUTPUT_BYTES = 1024 * 1024;
67
- /** 喂给模型的 deny 原因/additionalContext 的单条长度上限(字符)。 */
68
16
  const MAX_HOOK_FEEDBACK_CHARS = 4_096;
69
- /**
70
- * 一次事件里所有 hook 上下文片段**聚合后**喂给模型的总量上限(字符)。
71
- *
72
- * 为什么单条帽不够:一次事件最多 {@link MAX_HOOK_ENTRIES_PER_EVENT} 条(parseHooksConfig 的广度闸),
73
- * 每条最多推两段(`decision:"block"` 的 reason + `hookSpecificOutput.additionalContext`),
74
- * {@link composeHooks} 再把部署槽与 task 槽的**成品**拼一次 —— 单条 4096 相乘后一次注入可达 ~256KB,
75
- * 而这些字符是**逐次工具调用**进模型可见文本的(core `hooks.js` 的 preToolContext / `prepare-task.js`
76
- * 的 tool_result 追加 / `runtask.js` 的 Stop follow-up),不是日志。
77
- *
78
- * 帽值取 10_000 的依据:模型可见文本在本栈的既有预算量纲就是 10k —— core `truncateError`
79
- * (`src/core/tool-errors.ts`)的 10k 中截,逐字锚 CC 序列化器 `A7e` 的 `1e4`;#181 战役里 core 还要给
80
- * `beforeToolCall` block reason 补一道同值兜底闸。取同一量级 = 「一次 hook 注入 ≤ 一份错误文本预算」,
81
- * 且 4096 的单条帽保证**前两条**钩子的话仍整段进得去(截断是配置广度的代价,不是常态)。
82
- */
83
17
  export const MAX_HOOK_CONTEXT_TOTAL_CHARS = 10_000;
84
- /** 帽满时仍值得保留的最短残段(字符)。比这更短的尾巴不是信息:模型会把半句话读成完整指令,
85
- * 宁可整条落进"省略"计数。 */
86
18
  const MIN_HOOK_CONTEXT_PARTIAL_CHARS = 256;
87
- /** PostToolUse payload 里 tool_response 文本投影的上限(字符)——工具结果可能是 MB 级。 */
88
19
  const MAX_TOOL_RESPONSE_CHARS = 32_768;
89
- /** stdin payload 里 tool_input JSON 投影的上限(字符;巨大 tool_input 经 JSON.stringify 造成
90
- * 内存/阻塞放大——超限替换为截断标记)。 */
91
20
  const MAX_TOOL_INPUT_CHARS = 262_144;
92
- /**
93
- * command hook 不再继承完整 `process.env`——worker 进程环境里有模型/DB/service-auth 令牌,
94
- * 全量透传等于把它们交给任务侧 hook 命令。改成基础运行时 env 的 allowlist(PATH/HOME/SHELL/终端/locale)+
95
- * CC 约定注入(CLAUDE_PROJECT_DIR)。用户自己要给 hook 的变量走 settings.env(shellEnv,同样单用户闸),
96
- * 不靠继承服务进程密钥。
97
- */
98
21
  const HOOK_ENV_ALLOWLIST = [
99
22
  "PATH", "HOME", "SHELL", "USER", "LOGNAME", "TERM", "TZ",
100
23
  "LANG", "LC_ALL", "LC_CTYPE", "LC_MESSAGES", "TMPDIR",
101
24
  ];
102
- /** hook 可见的 env = 净化基础 allowlist + CLAUDE_PROJECT_DIR + 请求自带的 `settings.env`(shellEnv,
103
- * 同一单用户闸)——上面注释承诺的"用户变量走 settings.env"通道在这里真兑现(阶段三补齐:
104
- * 之前 hookEnv 没接 shellEnv,承诺是空头的)。shellEnv 后铺(用户显式给的赢基础值);服务进程密钥
105
- * 永远不在这张表里。http 条目的 $NAME 插值也从这张表取(见 {@link runHttpHook})。 */
106
25
  function hookEnv(ctx) {
107
26
  const env = { CLAUDE_PROJECT_DIR: ctx.cwd };
108
27
  for (const k of HOOK_ENV_ALLOWLIST) {
@@ -112,9 +31,6 @@ function hookEnv(ctx) {
112
31
  }
113
32
  return { ...env, ...ctx.shellEnv };
114
33
  }
115
- /** 阶段二点亮的事件 = engine-owned 9 事件全集。与契约 ENGINE_OWNED_HOOK_EVENTS 今天相等但故意独立
116
- * 声明:契约扩容(新增 engine-owned 事件)时 `hook_events_not_fired` 机制自动把缺口暴露出来,而不是
117
- * 静默装作点亮。shell-owned 事件不进此表(壳自己跑,owner-single)。 */
118
34
  const LIT_EVENTS = [
119
35
  "PreToolUse",
120
36
  "PostToolUse",
@@ -126,10 +42,6 @@ const LIT_EVENTS = [
126
42
  "PreCompact",
127
43
  "PostCompact",
128
44
  ];
129
- /** core BrainErrorCode(auth/rate_limit/invalid_request/server/network/http,brain/errors 单一来源)→
130
- * 契约 AssistantTurnError(CC SDKAssistantMessageError 枚举)。network/http 在 CC 枚举里没有对应
131
- * 类别 → "unknown"(不妄断编造类别;原始人话错误走 error_details)。StopFailure 的 matcher 对映射
132
- * 【后】的枚举值匹配(契约 HOOK_EVENT_MATCHER_FIELD.StopFailure = "error" = payload 字段)。 */
133
45
  const BRAIN_TO_TURN_ERROR = {
134
46
  auth: "authentication_failed",
135
47
  rate_limit: "rate_limit",
@@ -140,10 +52,6 @@ const BRAIN_TO_TURN_ERROR = {
140
52
  function toAssistantTurnError(errorKind) {
141
53
  return (errorKind !== undefined ? BRAIN_TO_TURN_ERROR[errorKind] : undefined) ?? "unknown";
142
54
  }
143
- /**
144
- * 把 wire 上的 untrusted `settings.hooks` 校验成契约 {@link HooksConfig} + 服务侧上限。
145
- * 错误返回 error 字符串而不 throw——submit 路径 400 fail-loud、resume 防御路径 warn-drop,由调用方选。
146
- */
147
55
  export function parseHooksConfig(raw) {
148
56
  const parsed = HooksConfig.safeParse(raw);
149
57
  if (!parsed.success) {
@@ -155,7 +63,6 @@ export function parseHooksConfig(raw) {
155
63
  if (!groups)
156
64
  continue;
157
65
  if (groups.length > MAX_HOOK_MATCHER_GROUPS) {
158
- // 广度维:组数不设界=条目 cap 可被"多组少条"绕开一半(每组重复扫 matcher)。
159
66
  return { error: `settings.hooks.${event}: ${groups.length} matcher groups exceeds cap ${MAX_HOOK_MATCHER_GROUPS}` };
160
67
  }
161
68
  let entries = 0;
@@ -166,7 +73,6 @@ export function parseHooksConfig(raw) {
166
73
  return { error: `settings.hooks.${event}: command exceeds ${MAX_HOOK_COMMAND_CHARS} chars` };
167
74
  }
168
75
  if (entry.type === "http") {
169
- // headers/allowedEnvVars 不设界=插值与 fetch header 构造前的内存放大面。
170
76
  if (entry.url.length > MAX_HOOK_COMMAND_CHARS) {
171
77
  return { error: `settings.hooks.${event}: url exceeds ${MAX_HOOK_COMMAND_CHARS} chars` };
172
78
  }
@@ -190,8 +96,6 @@ export function parseHooksConfig(raw) {
190
96
  }
191
97
  return { config };
192
98
  }
193
- /** CC matcher 语义:精确名、`a|b` 交替、或 JS 正则(锚定全匹配,防 "Bash" 误配 "BashOutput");
194
- * 空/`*`/缺省匹配一切;非法正则 → 该条不匹配(fail-closed 到"不跑",由调用方记账一次)。 */
195
99
  export function hookMatcherMatches(matcher, value) {
196
100
  if (matcher === undefined || matcher === "" || matcher === "*")
197
101
  return true;
@@ -204,21 +108,13 @@ export function hookMatcherMatches(matcher, value) {
204
108
  return "invalid";
205
109
  }
206
110
  }
207
- /** `if` 条目条件(CC-exact,对照 CC 2.1.187/198 实测行为核验的真语义,两版一致):
208
- * 值 = permission-rule 模式(`ToolName` 或 `ToolName(arg-模式)`),【不是】表达式语言——按当前工具调用
209
- * 求值:名部走 permission matcher(交替/正则同 matcher 字段),括号内 arg-模式(`*` 通配)匹配工具输入的
210
- * 主字符串(Bash=command,文件工具=file_path/path)。CC 行为逐条目:不匹配 → 跳过该条目(记日志);
211
- * 非工具事件(payload 无 tool_name)→ "cannot be evaluated" 跳过;解析失败 → 跳过。返回:
212
- * true=跑 / "not_matched" / "unevaluable"(调用方分别记账;跳过永不静默)。 */
213
111
  export function hookIfMatches(cond, payload) {
214
- // glob→`.*` 拼正则有回溯放大面(长模式×多星号×长主字符串)——模式长度与星号数设上限,
215
- // 超限=unevaluable(跳过记账;正常 permission-rule 远在限内)。
216
112
  if (cond.length > 500 || (cond.match(/\*/g)?.length ?? 0) > 16)
217
113
  return "unevaluable";
218
114
  const p = (payload ?? {});
219
115
  const toolName = typeof p.tool_name === "string" ? p.tool_name : undefined;
220
116
  if (!toolName)
221
- return "unevaluable"; // CC:非工具事件上 if 无法求值 → 跳过(非静默,调用方记账)
117
+ return "unevaluable";
222
118
  const m = /^([^(]+?)(?:\((.*)\))?$/s.exec(cond.trim());
223
119
  if (!m)
224
120
  return "unevaluable";
@@ -229,13 +125,12 @@ export function hookIfMatches(cond, payload) {
229
125
  return "not_matched";
230
126
  const spec = m[2];
231
127
  if (spec === undefined || spec === "*" || spec === "")
232
- return true; // 纯名 / Tool(*) = 名中即中
128
+ return true;
233
129
  const input = p.tool_input;
234
130
  const primary = input && (typeof input.command === "string" ? input.command : typeof input.file_path === "string" ? input.file_path : typeof input.path === "string" ? input.path : undefined);
235
131
  if (primary === undefined)
236
- return "unevaluable"; // 该工具无主字符串可配(CC 同样评不出来)
132
+ return "unevaluable";
237
133
  try {
238
- // glob(`*`)→ 锚定正则;其余字符按字面转义(permission specifier 不是裸正则)。
239
134
  const re = new RegExp(`^${spec.split("*").map((s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*")}$`);
240
135
  return re.test(primary) ? true : "not_matched";
241
136
  }
@@ -243,27 +138,12 @@ export function hookIfMatches(cond, payload) {
243
138
  return "unevaluable";
244
139
  }
245
140
  }
246
- /** 阶段三b:LLM 条目(prompt+agent 合计)每事件上限——config 滥写不至于把一个工具门拖进 N 次模型调用。 */
247
141
  const MAX_LLM_ENTRIES_PER_EVENT = 4;
248
- /** prompt 条目默认超时(一次判定调用;entry.timeout 覆盖,MAX_HOOK_TIMEOUT_SECONDS 仍是硬顶)。 */
249
142
  const DEFAULT_PROMPT_HOOK_TIMEOUT_SECONDS = 30;
250
- /** agent 条目默认超时(读-only 子代理跑到收敛)。 */
251
143
  const DEFAULT_AGENT_HOOK_TIMEOUT_SECONDS = 120;
252
144
  function clip(s, n) {
253
145
  return s.length > n ? `${s.slice(0, n)}…(${s.length})` : s;
254
146
  }
255
- /**
256
- * hook 上下文片段 → 喂给模型的单串。**所有** `additionalContext` 聚合点的唯一属主(此前是同一个
257
- * `contexts.join("\n")` 表达式抄在二十余处,总量帽无处可挂 —— 那正是漂移成因)。
258
- *
259
- * 三条语义,都是承重的:
260
- * 1. **顺序保留、只截尾**:先来的钩子先说话;一旦装不下就停,后面的条目一律不再挤进来(哪怕更短)。
261
- * 按长度重排会让"第 3 条钩子的话"随别人的长度忽隐忽现,排障时无从复现。
262
- * 2. **截断可见**:尾巴挂一行标记说明省了几条、丢了多少字符。静默丢弃会让 hook 作者以为自己的
263
- * context 生效了,而模型那头根本没见过 —— 这类"以为配好了"的缺席比长文本本身更贵。
264
- * 3. **标记不计预算**:与 core `truncateError` / CC `A7e` 同姿势(marker 不占那 10k),否则帽值的
265
- * 含义会随标记文案长度漂。
266
- */
267
147
  export function buildHookContext(parts) {
268
148
  const present = parts.filter((p) => p.length > 0).map(readHookContextPart);
269
149
  if (present.length === 0)
@@ -275,12 +155,11 @@ export function buildHookContext(parts) {
275
155
  let omitted = 0;
276
156
  let droppedChars = 0;
277
157
  for (const part of present) {
278
- // 内层的账先并进来(标记读回,见 {@link readHookContextPart}),再谈这一轮自己丢了什么。
279
158
  messages += part.messages;
280
159
  omitted += part.omitted;
281
160
  droppedChars += part.dropped;
282
161
  if (!truncating) {
283
- const sep = kept.length > 0 ? 1 : 0; // join 的 "\n" 也占预算
162
+ const sep = kept.length > 0 ? 1 : 0;
284
163
  const room = MAX_HOOK_CONTEXT_TOTAL_CHARS - used - sep;
285
164
  if (room >= part.body.length) {
286
165
  if (part.body.length > 0) {
@@ -290,9 +169,6 @@ export function buildHookContext(parts) {
290
169
  continue;
291
170
  }
292
171
  truncating = true;
293
- // 只有**单条**发言可以切尾:切了它仍是"部分在场",账目诚实(dropped 记尾巴,omitted 不变)。
294
- // 已聚合的成品代表内层多条、正文里却没有条目边界 —— 切一刀会把第 2..N 条整个抹掉却记 0 条省略,
295
- // 标记恰好在最常走的两层合流路上说谎(codex 复审 R4,亲核属实)。⇒ 它要么整段留,要么整条丢。
296
172
  if (part.messages === 1 && room >= MIN_HOOK_CONTEXT_PARTIAL_CHARS) {
297
173
  kept.push(part.body.slice(0, room));
298
174
  used += sep + room;
@@ -300,8 +176,6 @@ export function buildHookContext(parts) {
300
176
  continue;
301
177
  }
302
178
  }
303
- // 整条不见 = 它代表的**全部**条目都不见。内层已经报过的那几条上面加过了,这里只补差额
304
- // ——否则一个"3 of 5"的成品被整条丢掉会记成 8 条,报出"省掉的比总数还多"这种自相矛盾的账。
305
179
  omitted += part.messages - part.omitted;
306
180
  droppedChars += part.body.length;
307
181
  }
@@ -311,27 +185,8 @@ export function buildHookContext(parts) {
311
185
  const marker = `[hook context truncated at ${MAX_HOOK_CONTEXT_TOTAL_CHARS} chars: ${omitted} of ${messages} hook messages omitted, ${droppedChars} characters dropped]`;
312
186
  return kept.length > 0 ? `${body}\n${marker}` : marker;
313
187
  }
314
- /** 截断标记的尾锚形。帽值**逐字写进模式**(不是 `\d+`):只认"我们自己这一版才会写出的那一份",
315
- * 别处飘来的同类文字连模式都不匹配。 */
316
188
  const HOOK_CONTEXT_MARKER_RE = new RegExp(`\\n\\[hook context truncated at ${MAX_HOOK_CONTEXT_TOTAL_CHARS} chars: (\\d+) of (\\d+) hook messages omitted, (\\d+) characters dropped\\]$`);
317
- /** 读回标记时接受的数字上界(2^42)。比任何真值大若干数量级,又小到几十条相加也绝不越过安全整数
318
- * —— 伪造的巨数因此根本进不来,合并加法也不必每一步复验溢出。 */
319
189
  const MAX_HOOK_CONTEXT_PARSED_COUNT = 2 ** 42;
320
- /**
321
- * 片段 → 内容 + 它自带的截断账。
322
- *
323
- * 为什么必须能**读回**自己写的标记:`composeHooks` 会把 {@link buildHookContext} 的成品当片段再喂一次
324
- * (部署槽在场即走这条路 —— 是常态不是边角)。标记按内容处理会犯两桩:①切在标记中间 ⇒ 模型读到半截
325
- * 标记;②外层重新记账 ⇒ 内层丢掉的那一万多字符凭空消失,标记数字变成谎。标记既然不计预算,读回时也
326
- * 不能算内容;把它携带的账并进外层,函数对自己的输出就是幂等的。
327
- *
328
- * 🔴 这把尺子对着的是**不可信文本**(钩子想吐什么就吐什么),所以只认"我们自己才写得出的那一份":
329
- * 帽值逐字相等(在模式里)、`dropped ≥ 1`(我们从不在零丢弃时写标记)、`omitted ≤ messages`、
330
- * `messages ≥ 1`、数字都在 {@link MAX_HOOK_CONTEXT_PARSED_COUNT} 内。任一条不合 ⇒ **整段当普通内容**。
331
- * 认下之后也不会有"内容静默少一行":认下 ⇒ `dropped > 0` ⇒ 输出必然带标记 —— 摘掉的那一行总是被
332
- * **重写**回去(数字可能被合并),从不是被删掉。伪造得刚好合规只会让记账口径偏,不改任何裁决;
333
- * 而且钩子配置在单用户闸内,伪造者就是这份账的读者本人。
334
- */
335
190
  function readHookContextPart(part) {
336
191
  const m = HOOK_CONTEXT_MARKER_RE.exec(part);
337
192
  if (!m)
@@ -345,23 +200,19 @@ function readHookContextPart(part) {
345
200
  }
346
201
  return { body: part.slice(0, part.length - m[0].length), messages, omitted, dropped };
347
202
  }
348
- /** {@link buildHookContext} 的字段形(聚合点全是 `...spread` 进结果对象):空 → 不产字段。 */
349
203
  function buildHookContextField(parts) {
350
204
  const text = buildHookContext(parts);
351
205
  return text === undefined ? {} : { additionalContext: text };
352
206
  }
353
- /** 跑一条 command hook:sh -c,stdin=payload JSON(CC-verbatim),超时 SIGKILL,输出各限 1MiB。 */
354
207
  function runCommandHook(entry, payload, ctx) {
355
208
  return new Promise((resolve) => {
356
209
  const timeoutMs = Math.min(entry.timeout ?? DEFAULT_HOOK_TIMEOUT_SECONDS, MAX_HOOK_TIMEOUT_SECONDS) * 1000;
357
210
  let child;
358
211
  try {
359
- // detached=true 建独立进程组:超时击杀用负 pid 连孙进程一起收(否则 SIGKILL 只杀 sh,一个
360
- // `sleep 30` 孙进程抓着继承的 stdout 管道,`close` 事件要等它退出——单测实抓的挂死)。
361
- const sh = hostShell(); // S1:不再硬编码 /bin/sh(Debian=dash 静默错字节),host 同源 bash
212
+ const sh = hostShell();
362
213
  child = spawn(sh.shell, [...sh.args, entry.command], {
363
214
  cwd: ctx.cwd,
364
- env: hookEnv(ctx), // allowlist+shellEnv,不透传服务进程密钥
215
+ env: hookEnv(ctx),
365
216
  stdio: ["pipe", "pipe", "pipe"],
366
217
  detached: true,
367
218
  });
@@ -385,14 +236,14 @@ function runCommandHook(entry, payload, ctx) {
385
236
  timedOut = true;
386
237
  try {
387
238
  if (child.pid)
388
- process.kill(-child.pid, "SIGKILL"); // 整组击杀(见 spawn 注释)
239
+ process.kill(-child.pid, "SIGKILL");
389
240
  else
390
241
  child.kill("SIGKILL");
391
242
  }
392
243
  catch {
393
244
  child.kill("SIGKILL");
394
245
  }
395
- settle(null); // 超时=非阻断故障,立即返回,不等孙进程放开管道
246
+ settle(null);
396
247
  }, timeoutMs);
397
248
  child.stdout?.on("data", (d) => {
398
249
  if (stdout.length < MAX_HOOK_OUTPUT_BYTES)
@@ -410,29 +261,20 @@ function runCommandHook(entry, payload, ctx) {
410
261
  }
411
262
  });
412
263
  child.on("close", (code) => settle(code));
413
- // stdin=hook 输入 JSON(CC 语义)。EPIPE(hook 不读 stdin 就退出)吞掉——不是错误。
414
- // 巨大 tool_input 经 boundedToolInputPayload 截断防内存放大。
415
264
  child.stdin?.on("error", () => { });
416
265
  child.stdin?.end(`${JSON.stringify(boundedToolInputPayload(payload))}\n`);
417
266
  });
418
267
  }
419
- /** async 条目:fire-and-forget(不等待、不消费输出、不参与决策)。仍加 detached 进程组 +
420
- * timeout 整组击杀——否则一个每次工具调用都 spawn 的 async hook 会在长任务里无限堆积不退出的后台进程。
421
- *
422
- * asyncRewake(契约:Background + wake the model on exit 2,implies async):与纯 async 的唯一差别=
423
- * 捕获 stderr(有界)+ close 时 code===2 → ctx.wake(stderr)——CC 的 exit-2-语义在同步条目是"stderr 是给
424
- * 模型的阻断理由",搬到后台即"stderr 是给模型的唤醒消息"。投递走部署组装的 wake 管道(live TaskStream
425
- * steer,下个 turn 边界注入);没有活流(任务已结束/挂起)或 wake 缺席 ⇒ warn 记账不重试。 */
426
268
  function fireAsyncCommandHook(entry, payload, ctx, event) {
427
269
  try {
428
270
  const rewake = entry.asyncRewake === true;
429
271
  const timeoutMs = Math.min(entry.timeout ?? DEFAULT_HOOK_TIMEOUT_SECONDS, MAX_HOOK_TIMEOUT_SECONDS) * 1000;
430
- const sh = hostShell(); // S1:同上,async hook 腿同源
272
+ const sh = hostShell();
431
273
  const child = spawn(sh.shell, [...sh.args, entry.command], {
432
274
  cwd: ctx.cwd,
433
- env: hookEnv(ctx), // allowlist+shellEnv,不透传服务进程密钥
275
+ env: hookEnv(ctx),
434
276
  stdio: ["pipe", "ignore", rewake ? "pipe" : "ignore"],
435
- detached: true, // 进程组隔离,超时可整组收
277
+ detached: true,
436
278
  });
437
279
  const timer = setTimeout(() => {
438
280
  try {
@@ -452,22 +294,19 @@ function fireAsyncCommandHook(entry, payload, ctx, event) {
452
294
  if (stderr.length < MAX_HOOK_OUTPUT_BYTES)
453
295
  stderr += d.toString("utf8").slice(0, MAX_HOOK_OUTPUT_BYTES - stderr.length);
454
296
  });
455
- // pipe 的 socket 句柄会 hold 事件循环——unref 让慢 hook 不拖进程关停(数据事件照收,
456
- // 超时路径的进程组 SIGKILL 会触发 close 收尾)。
457
297
  child.stderr?.unref?.();
458
298
  }
459
299
  child.on("error", () => clearTimeout(timer));
460
300
  child.on("close", (code) => {
461
301
  clearTimeout(timer);
462
302
  if (!rewake || code !== 2)
463
- return; // 契约:只有 exit 2 唤醒
303
+ return;
464
304
  const text = stderr.trim();
465
305
  if (!ctx.wake) {
466
306
  ctx.logger.warn("hook_async_rewake_dropped", { event, reason: "no_wake_pipeline" });
467
307
  return;
468
308
  }
469
309
  if (!text) {
470
- // exit 2 但 stderr 空:没内容可注入——记账(与同步条目 exit-2-无-stderr 的空理由同类,不造消息)。
471
310
  ctx.logger.warn("hook_async_rewake_dropped", { event, reason: "empty_stderr" });
472
311
  return;
473
312
  }
@@ -483,7 +322,6 @@ function fireAsyncCommandHook(entry, payload, ctx, event) {
483
322
  child.unref();
484
323
  }
485
324
  catch {
486
- /* fire-and-forget:spawn 失败不影响任务 */
487
325
  }
488
326
  }
489
327
  async function runLlmHook(entry, payload, ctx, extra) {
@@ -494,17 +332,7 @@ async function runLlmHook(entry, payload, ctx, extra) {
494
332
  const timeoutMs = Math.min(entry.timeout ?? defSec, MAX_HOOK_TIMEOUT_SECONDS) * 1000;
495
333
  const args = clip(JSON.stringify(boundedToolInputPayload(payload)), MAX_TOOL_INPUT_CHARS);
496
334
  const substituted = entry.prompt.split("$ARGUMENTS").join(args);
497
- // Stop×prompt(CC 形):会话 transcript 在前、被包装过的条件在后 —— 顺序是承重的,
498
- // 因为 CC 的包装句逐字写着 "Based on the conversation transcript **above**"。
499
- // CC 形:transcript 在前,条件被 CC 的包装句包起来("Based on the conversation transcript **above**…")。
500
- // 顺序是承重的 —— 包装句逐字预设会话在上文,放反了那句话本身就是在骗模型。
501
335
  const prompt = extra?.transcript !== undefined ? `${extra.transcript}\n\n---\n\n${wrapCondition("Stop", substituted)}` : substituted;
502
- // 载体外再包一层硬顶:契约说载体自己兜超时,但一个部署组装 bug 不该能挂死工具门(纵深)。
503
- // A-002.12:硬顶只 unref 不清 ⇒ 每次被门到的工具调用留一只带闭包的定时器,最长挂
504
- // MAX_HOOK_TIMEOUT_SECONDS+5s=605s(工具门是热路径,一个任务几十上百次)。整个函数收在
505
- // try/finally 里,任何出口都 clearTimeout。
506
- // 🔴 硬顶**一只到底**、两次 invoke 共用(语义原样不动):它兜的是「本次 hook 判决整体不许挂死」,
507
- // 不是「每次载体调用各给一个新窗」——改成 per-invoke 会把最坏墙钟翻倍。
508
336
  let hardTopTimer;
509
337
  const hardTop = new Promise((r) => {
510
338
  hardTopTimer = setTimeout(() => r({ ok: false, error: "hook llm hard-timeout backstop fired (carrier did not settle within timeoutMs+5s — deployment assembly bug)", code: "hard_timeout" }), timeoutMs + 5_000);
@@ -521,14 +349,9 @@ async function runLlmHook(entry, payload, ctx, extra) {
521
349
  hardTop,
522
350
  ]).catch((e) => ({ ok: false, error: String(e) }));
523
351
  let res = await invoke();
524
- // 🔴 **"一个字都没吐出来"重试一次**(仅 CC 评估者路)。这个失败形的后果是**判词被丢弃 ⇒ fail-open**
525
- // (该拦没拦),而它是**瞬态**的(推理档模型偶尔把额度用在 thinking 上)。
526
- // ⚠️ 只重试**无内容**,不重试"有内容但读不懂" —— 后者重试一次多半还是读不懂,而且那一格按设计就该放行。
527
- if (extra && !res.ok && res.code === "no_content") { // B8:判别走码不走文案(v3.1 批2)
352
+ if (extra && !res.ok && res.code === "no_content") {
528
353
  ctx.logger.warn("hook_llm_no_content_retry", { event: extra.event });
529
354
  res = await invoke();
530
- // 重试之后**仍然**没有内容 ⇒ 这一轮的守卫确实没能评估。发观测帧(纯 observe,不改变运行)——
531
- // 方向仍是 fail-open,但用户/壳侧要能知道「这轮没看住」,否则那个放行与「已达成」无法区分。
532
355
  if (!res.ok && res.code === "no_content") {
533
356
  ctx.onHookNotice?.({ kind: "hook_decision_unavailable", event: extra.event, reason: "no_content", detail: "carrier returned no content (after one retry)" });
534
357
  }
@@ -548,14 +371,9 @@ async function runLlmHook(entry, payload, ctx, extra) {
548
371
  async function runHttpHook(entry, payload, ctx) {
549
372
  const timeoutMs = Math.min(entry.timeout ?? DEFAULT_HOOK_TIMEOUT_SECONDS, MAX_HOOK_TIMEOUT_SECONDS) * 1000;
550
373
  const allowed = new Set(entry.allowedEnvVars ?? []);
551
- // 插值源=hookEnv(净化 allowlist + shellEnv),【不是】裸 process.env——否则任务
552
- // 配置可用 allowedEnvVars 点名服务密钥(MODEL_GATEWAY_KEY 等)插进 header 发往任意 URL。用户要给
553
- // webhook 的 token 走同请求 settings.env(shellEnv),与 command hook 的 env 姿势同一单源。
554
374
  const envSource = hookEnv(ctx);
555
375
  const headers = { "content-type": "application/json" };
556
376
  for (const [k, v] of Object.entries(entry.headers ?? {})) {
557
- // $NAME 插值:只认 allowedEnvVars 列出【且】在 hookEnv 表里的名字;其余原样保留(不是插值点,
558
- // fail-closed 到字面)。
559
377
  headers[k] = v.replace(/\$([A-Za-z_][A-Za-z0-9_]*)/g, (whole, name) => {
560
378
  if (!allowed.has(name))
561
379
  return whole;
@@ -573,8 +391,6 @@ async function runHttpHook(entry, payload, ctx) {
573
391
  signal: ac.signal,
574
392
  redirect: "error",
575
393
  });
576
- // body 有界读取(不 res.text() 一口吞——恶意/故障端点可回无限流)。按【字节】
577
- // 计预算并先截 Uint8Array 再 decode(字符计数挡不住单个巨 chunk 的先行分配),收尾 flush decoder。
578
394
  let body = "";
579
395
  const reader = res.body?.getReader();
580
396
  if (reader) {
@@ -595,9 +411,6 @@ async function runHttpHook(entry, payload, ctx) {
595
411
  await reader.cancel().catch(() => undefined);
596
412
  }
597
413
  if (res.ok) {
598
- // CC-exact 定谳:http hook 是硬 JSON 契约(≠ command 的 plainText 优雅降级)。
599
- // - 空 body → 特判 `{}` 成功(无决策,干净放行);
600
- // - 非空且非 JSON → non-blocking error 记账(不吞成静默通过),stdout 置空让折叠无决策。
601
414
  const trimmed = body.trim();
602
415
  if (trimmed === "")
603
416
  return { code: 0, stdout: "{}", stderr: "", timedOut: false };
@@ -607,7 +420,6 @@ async function runHttpHook(entry, payload, ctx) {
607
420
  }
608
421
  return { code: 0, stdout: body, stderr: "", timedOut: false };
609
422
  }
610
- // 非 2xx 不 parse body,`HTTP ${status} from ${url}` non-blocking(exit-2 阻断语义不撞)。
611
423
  return { code: res.status, stdout: "", stderr: body, timedOut: false };
612
424
  }
613
425
  catch (e) {
@@ -619,9 +431,6 @@ async function runHttpHook(entry, payload, ctx) {
619
431
  clearTimeout(timer);
620
432
  }
621
433
  }
622
- /** 输入截断的单值形式:一个 tool_input 值若 stringify 超上限/不可序列化,换成截断/占位标记。
623
- * 返回原引用=没动(调用方以此保住常态零拷贝)。PostToolBatch 的 tool_calls[].tool_input 逐条走这里
624
- * (顶层 {@link boundedToolInputPayload} 只看 payload.tool_input,批量形状会绕过它)。 */
625
434
  function boundedInputValue(v) {
626
435
  let s;
627
436
  try {
@@ -634,8 +443,6 @@ function boundedInputValue(v) {
634
443
  return v;
635
444
  return { __truncated__: true, chars: s.length };
636
445
  }
637
- /** stdin payload 若 stringify 超上限,把 tool_input 替换成截断标记(防内存/阻塞放大)。
638
- * 只在真的超限时重建对象——常态零拷贝。 */
639
446
  function boundedToolInputPayload(payload) {
640
447
  if (!payload || typeof payload !== "object")
641
448
  return payload;
@@ -645,7 +452,6 @@ function boundedToolInputPayload(payload) {
645
452
  const bounded = boundedInputValue(p.tool_input);
646
453
  return bounded === p.tool_input ? payload : { ...p, tool_input: bounded };
647
454
  }
648
- /** stdout 的 SyncHookOutput 宽松读取(exit 0 且可解析才有意义;形状错就当没有——CC 同姿势)。 */
649
455
  function parseHookStdout(stdout) {
650
456
  const t = stdout.trim();
651
457
  if (!t.startsWith("{"))
@@ -658,18 +464,9 @@ function parseHookStdout(stdout) {
658
464
  return undefined;
659
465
  }
660
466
  }
661
- /** 一次事件里跑一组匹配条目的公共骨架:once 去重、if 忽略记账、类型/shell 跳过记账、async 旁路。
662
- * 返回每条【同步 command 条目】的运行结果 + 该条目引用(供事件专属的语义映射)。 */
663
- async function runMatchingEntries(event, groups, matchValue, payload, ctx, onceFired,
664
- /** 深度维:本次事件的墙钟截止(epoch ms)——32 条 × 单条 600s 最坏串行卡工具门 ~320min;
665
- * 到点后不再起【同步】条目(async 旁路不占墙钟照常),记账降级。 */
666
- deadlineAt,
667
- /** once 去重键的组维前缀——PreToolUse 逐组合成单组数组调用时必须传真实组号,否则跨组键碰撞。 */
668
- keyBase,
669
- /** Stop×prompt 的额外输入(CC 系统提示 + 会话 transcript)。缺席 ⇒ prompt 条目按此前逐位不变。 */
670
- llmExtra) {
467
+ async function runMatchingEntries(event, groups, matchValue, payload, ctx, onceFired, deadlineAt, keyBase, llmExtra) {
671
468
  const results = [];
672
- let llmEntries = 0; // 阶段三b:prompt+agent 合计每事件 cap(config 滥写不至于一个门 N 次模型调用)
469
+ let llmEntries = 0;
673
470
  let gi = -1;
674
471
  for (const group of groups) {
675
472
  gi++;
@@ -687,12 +484,10 @@ llmExtra) {
687
484
  if (entry.once && onceFired.has(onceKey))
688
485
  continue;
689
486
  if (entry.type !== "command" && entry.type !== "http" && entry.type !== "prompt" && entry.type !== "agent") {
690
- // registry-core 未来新增条目类型的前向降级:跳过记账可见,绝不静默。
691
487
  ctx.logger.warn("hook_entries_skipped", { event, type: entry.type, reason: "type_not_supported" });
692
488
  continue;
693
489
  }
694
490
  if (entry.type === "prompt" || entry.type === "agent") {
695
- // 阶段三b:载体缺席(部署没组装模型面)/每事件 LLM cap——都跳过记账可见,非阻断。
696
491
  const carrier = entry.type === "prompt" ? ctx.hookLlm : ctx.hookAgent;
697
492
  if (!carrier) {
698
493
  ctx.logger.warn("hook_entries_skipped", { event, type: entry.type, reason: "llm_carrier_unavailable" });
@@ -708,8 +503,6 @@ llmExtra) {
708
503
  continue;
709
504
  }
710
505
  if (entry.if !== undefined) {
711
- // CC-exact(对照 CC 2.1.187/198 实测行为核验,两版一致):if=permission-rule 模式,按当前工具求值;不匹配/评不出
712
- // → 跳过【该条目】并记日志(CC 行为;取代早期"语义未定忽略照跑"的保守占位)。
713
506
  const iv = hookIfMatches(entry.if, payload);
714
507
  if (iv !== true) {
715
508
  ctx.logger.warn("hook_entries_skipped", { event, type: entry.type, reason: iv === "unevaluable" ? "if_unevaluable" : "if_not_matched", if: clip(entry.if, 200) });
@@ -717,28 +510,20 @@ llmExtra) {
717
510
  }
718
511
  }
719
512
  if (entry.type === "command" && (entry.async || entry.asyncRewake)) {
720
- // asyncRewake:wake 管道就位后不再降级——exit 2 唤醒在 fireAsyncCommandHook 内兑现(无管道时它自记账)。
721
513
  if (entry.once)
722
514
  onceFired.add(onceKey);
723
515
  fireAsyncCommandHook(entry, payload, ctx, event);
724
516
  continue;
725
517
  }
726
518
  if (Date.now() > deadlineAt) {
727
- // 事件墙钟预算耗尽——剩余同步条目不再起,警告记账(与超时同姿势:非阻断降级)。
728
519
  ctx.logger.warn("hook_event_deadline_exceeded", { event, budgetSec: MAX_HOOK_EVENT_TOTAL_SECONDS });
729
520
  return results;
730
521
  }
731
- // once 标记必须在 deadline 检查【后】——预算耗尽被跳过的 once 条目没真跑过,
732
- // 标了=下次事件永久哑火(可能是安全阻断钩子)。标在 spawn 尝试前:spawn 失败也算消耗
733
- // (once=一次尝试;失败已 warn 记账,重试语义交给用户重发配置)。
734
522
  if (entry.once)
735
523
  onceFired.add(onceKey);
736
524
  if (entry.type === "prompt" || entry.type === "agent")
737
525
  llmEntries++;
738
526
  const run = entry.type === "http" ? await runHttpHook(entry, payload, ctx)
739
- // agent 臂每事件供 CC 两档系统提示(A-065 P2-5:此前恒 undefined=评估者裸跑);顺带获得
740
- // no_content 一次重试与 CC_EVALUATOR_MAX_OUTPUT_TOKENS(载体不消费则忽略,agent 载体的
741
- // 输出预算=limits,非 completion 概念)。prompt 臂维持 Stop×prompt 专属 llmExtra(带 transcript)。
742
527
  : entry.type === "prompt" || entry.type === "agent" ? await runLlmHook(entry, payload, ctx, entry.type === "prompt" ? llmExtra : { system: ccAgentHookSystemFor(event), event })
743
528
  : await runCommandHook(entry, payload, ctx);
744
529
  if (run.spawnError) {
@@ -747,47 +532,28 @@ llmExtra) {
747
532
  }
748
533
  if (run.timedOut) {
749
534
  ctx.logger.warn("hook_command_timeout", { event, timeoutSec: Math.min(entry.timeout ?? DEFAULT_HOOK_TIMEOUT_SECONDS, MAX_HOOK_TIMEOUT_SECONDS) });
750
- continue; // CC:超时=非阻断故障
535
+ continue;
751
536
  }
752
537
  results.push({ entry, run });
753
538
  }
754
539
  }
755
540
  return results;
756
541
  }
757
- /** payload 公共字段(CC BaseHookInput;transcript_path 服务端无壳 jsonl → 空串,契约只定字段名)。 */
758
542
  function basePayload(ctx) {
759
543
  return {
760
544
  session_id: ctx.sessionId,
761
- // 🔴 **恒空串,而且它有真实后果**(cli 黑板 [1730] 的 live 取证):CC 用这个键让 **command 形**钩子
762
- // (一个能读磁盘的脚本)自己去读会话 JSONL。本服务是**无状态副本、hook 跑在服务端**,这里
763
- // **没有那样一个本地文件** ⇒ 给不出真路径。
764
- // 后果不是"少个字段":用 `type:"prompt"` 写「基于会话状态判断」的钩子,其载体(server 自己发起的
765
- // 模型调用)对本会话**完全是瞎的** —— cli 真跑实录:模型回「I don't have direct access to your
766
- // file system…」这类散文 ⇒ `parseHookStdout` 拿不到 decision ⇒ **一次都不拦**。整类 prompt 条件钩子
767
- // 在这个输入面下**结构上不可能工作**。
768
- // ⚠️ 正确的修**不是**把路径填上(prompt 载体要的是**内容**不是路径),而是给载体喂会话内容;
769
- // 那是行为面新增(送多少 / 脱敏口径 / token 记账 / 旋钮 / 喂不到时诚实降级),按本仓规矩先答三问,
770
- // 不在这里顺手改。**键本身保留**是为了 payload 形状与 CC 对位(CC 恒发这个键)——
771
- // 值为空是一处**已登记的**偏离,不是遗漏。见 test/hook-transcript-path-gap.test.ts。
772
545
  transcript_path: "",
773
546
  cwd: ctx.cwd,
774
547
  ...(ctx.permissionMode ? { permission_mode: ctx.permissionMode } : {}),
775
548
  };
776
549
  }
777
- /** matcher=null 事件(契约 HOOK_EVENT_MATCHER_FIELD 为 null:UserPromptSubmit/Stop/PostToolBatch)的
778
- * 契约语义 = matcher 被忽略、每个条目都跑——把组上的 matcher 抹掉再进公共骨架(否则拿空串当
779
- * matchValue 会让带 matcher 的条目 fail-closed 漏跑,违背"忽略"语义)。 */
780
550
  function ignoreMatchers(groups) {
781
551
  return groups.map((g) => (g.matcher === undefined ? g : { ...g, matcher: undefined }));
782
552
  }
783
- /** observe-only 事件(StopFailure/PostCompact——core 回调面返回 void,契约也没定这两个事件的输出面)
784
- * 的公共收尾:命令照常跑(通知型价值在副作用),非零 exit 记账;stdout 里的决策类输出无处落 →
785
- * `hook_result_unsupported` warn 不吞(别让用户以为 block/decision 生效了)。 */
786
553
  async function runObserveOnlyEvent(event, groups, matchValue, payload, ctx, onceFired) {
787
554
  const singles = await runMatchingEntries(event, groups, matchValue, payload, ctx, onceFired, Date.now() + MAX_HOOK_EVENT_TOTAL_SECONDS * 1000);
788
555
  for (const { run } of singles) {
789
556
  if (run.code !== 0) {
790
- // 含 exit 2:这两个事件没有 CC 阻断面,非零一律按 hook 故障记账(非阻断原则)。
791
557
  ctx.logger.warn("hook_command_failed", { event, code: run.code, stderr: clip(run.stderr, 300) });
792
558
  continue;
793
559
  }
@@ -801,19 +567,12 @@ async function runObserveOnlyEvent(event, groups, matchValue, payload, ctx, once
801
567
  }
802
568
  }
803
569
  }
804
- /**
805
- * 把校验过的 {@link HooksConfig} 翻成 core `Hooks` 回调(阶段二:engine-owned 9 事件全点亮)。
806
- * 没有可点亮的条目 → undefined(spec 不挂 hooks,deps 路径原样)。
807
- */
808
570
  export function createTaskHooks(config, ctx) {
809
571
  const configured = Object.keys(config).filter((e) => (config[e]?.length ?? 0) > 0);
810
- // engine-owned 且本 runner 未点亮的事件(阶段二后=空集,契约扩容时自动复活)才是缺口 → warn。
811
572
  const dark = configured.filter((e) => HOOK_EVENT_OWNER[e] === "engine" && !LIT_EVENTS.includes(e));
812
573
  if (dark.length > 0) {
813
574
  ctx.logger.warn("hook_events_not_fired", { events: dark, phase: 2 });
814
575
  }
815
- // shell-owned 事件的配置到达引擎=正常(owner-single:壳自己跑,引擎绝不 double-fire)——
816
- // debug 可见记账,不告警(warn 会把合法配置读成缺口)。
817
576
  const shellOwned = configured.filter((e) => HOOK_EVENT_OWNER[e] === "shell");
818
577
  if (shellOwned.length > 0) {
819
578
  ctx.logger.debug("hook_events_shell_owned", { events: shellOwned });
@@ -841,14 +600,9 @@ export function createTaskHooks(config, ctx) {
841
600
  let currentInput = input;
842
601
  let rewritten = false;
843
602
  let asked;
844
- // #157 fail-open 治理(P 类,tag `server.hooks.pretooluse-unknown-permission-word`):本轮见过
845
- // 词表外的 `permissionDecision`。安全域闭集词表的 miss 不是"没意见"——钩子作者**表过态**,只是
846
- // 用了我们不认识的词;把它折成放行等于替他改判。置位后禁止本轮铸 `action:"allow"`(见末尾折叠)。
847
603
  let unknownDecision = false;
848
604
  const contexts = [];
849
- const deadlineAt = Date.now() + MAX_HOOK_EVENT_TOTAL_SECONDS * 1000; // 跨全部组的事件级预算
850
- // 逐条串行(deny 立即短路;updatedInput 串进后续条目 payload——确定性优先,CC 的并行聚合语义
851
- // 在 deny/ask/allow 折叠上与此等价)。
605
+ const deadlineAt = Date.now() + MAX_HOOK_EVENT_TOTAL_SECONDS * 1000;
852
606
  let gi = -1;
853
607
  for (const group of pre) {
854
608
  gi++;
@@ -869,13 +623,12 @@ export function createTaskHooks(config, ctx) {
869
623
  const singles = await runMatchingEntries("PreToolUse", [{ ...group, matcher: undefined }], toolName, payload, ctx, onceFired, deadlineAt, `PreToolUse:${gi}`);
870
624
  for (const { run } of singles) {
871
625
  if (run.code === 2) {
872
- // CC:exit 2 = 阻断,stderr 喂给模型
873
626
  const reason = clip(run.stderr.trim(), MAX_HOOK_FEEDBACK_CHARS) || "blocked by a PreToolUse hook";
874
627
  return { action: "deny", message: reason, ...buildHookContextField(contexts) };
875
628
  }
876
629
  if (run.code !== 0) {
877
630
  ctx.logger.warn("hook_command_failed", { event: "PreToolUse", code: run.code, stderr: clip(run.stderr, 300) });
878
- continue; // 非阻断
631
+ continue;
879
632
  }
880
633
  const out = parseHookStdout(run.stdout);
881
634
  if (!out)
@@ -884,7 +637,6 @@ export function createTaskHooks(config, ctx) {
884
637
  ctx.logger.warn("hook_system_message", { event: "PreToolUse", message: clip(out.systemMessage, 500) });
885
638
  if (out.continue === false) {
886
639
  const reason = clip(typeof out.stopReason === "string" ? out.stopReason : "hook requested stop", MAX_HOOK_FEEDBACK_CHARS);
887
- // core 的 preToolUse seam 无"终止整个 run"能力(阶段一诚实近似:deny 本次调用)。
888
640
  return { action: "deny", message: reason, ...buildHookContextField(contexts) };
889
641
  }
890
642
  if (out.decision === "block") {
@@ -911,9 +663,6 @@ export function createTaskHooks(config, ctx) {
911
663
  }
912
664
  }
913
665
  else if (decision !== "deny" && decision !== "ask") {
914
- // 闭集 miss(拼错大小写的 deny / 自造词 / 非字符串 / null)。该条目的裁决**整条不采信**:
915
- // 它的 updatedInput 也不进串(上面的 if 已排除),并置位禁 allow。CC 的 `allow`/`deny`/
916
- // `ask` 三词是唯一词表;新词入表须同时进这里,否则它在本仓的语义就是"不采信"。
917
666
  unknownDecision = true;
918
667
  ctx.logger.warn("hook_permission_decision_unknown", {
919
668
  event: "PreToolUse",
@@ -926,9 +675,6 @@ export function createTaskHooks(config, ctx) {
926
675
  }
927
676
  }
928
677
  if (asked !== undefined) {
929
- // ask 不短路(后续条目可能 deny——deny > ask 已由上面的立即返回保证),折叠成最终 ask。
930
- // ask 也携带改写(PermissionResult 的 ask 变体同样接受 updatedInput)——否则
931
- // "改写 + 请示"组合在 approve 后执行的是【原始】args,钩子的 redact/clamp 被静默丢弃。
932
678
  return {
933
679
  action: "ask",
934
680
  message: asked,
@@ -937,14 +683,6 @@ export function createTaskHooks(config, ctx) {
937
683
  };
938
684
  }
939
685
  if (unknownDecision) {
940
- // #157:本轮有一条**不采信**的裁决 ⇒ 一律折 `ask`(人在环)。两条都不行,各有其害:
941
- // · `allow` —— 显式放行短路,引擎门不再裁决,等于替钩子作者把 DENY 改判成放行;
942
- // · `undefined`(无裁决)—— 看着中立,其实**不是** fail-closed(codex 复审 HIGH,2026-08-07):
943
- // 引擎拿到无裁决就走普通门,而 bypassPermissions / acceptEdits 档下普通门就是放行,
944
- // 钩子作者写的 DENY 照样跑。词表 miss 的语义是「保护型判词坏了」而不是「钩子没表态」。
945
- // 已有改写则随 `ask` 投递:改写通常正是 redact/clamp,丢了它 approve 后执行的是【原始】args
946
- // —— `ask` 是唯一既 fail-closed 又能带 updatedInput 的裁决形。不采信的那条自己的改写不在其中
947
- // (上面的 if 已把它挡在串外),否则一个坏判词就是一条免审的 args 改写通道。
948
686
  return {
949
687
  action: "ask",
950
688
  message: "a PreToolUse hook returned an unrecognized permissionDecision — approval required",
@@ -959,7 +697,7 @@ export function createTaskHooks(config, ctx) {
959
697
  ...buildHookContextField(contexts),
960
698
  };
961
699
  }
962
- return undefined; // 无意见(≠ allow 短路;core 门照常裁决)
700
+ return undefined;
963
701
  };
964
702
  hooks.preToolUse = preToolUseHook;
965
703
  }
@@ -974,14 +712,12 @@ export function createTaskHooks(config, ctx) {
974
712
  hook_event_name: "PostToolUse",
975
713
  tool_name: toolName,
976
714
  tool_input: input,
977
- // 有界文本投影(契约字段是 unknown;整个 details 图可能 MB 级且含不可序列化句柄)。
978
715
  tool_response: { isError: output.isError, text: clip(responseText, MAX_TOOL_RESPONSE_CHARS) },
979
716
  tool_use_id: tctx.toolCallId,
980
717
  };
981
718
  const singles = await runMatchingEntries("PostToolUse", post, toolName, payload, ctx, onceFired, Date.now() + MAX_HOOK_EVENT_TOTAL_SECONDS * 1000);
982
719
  for (const { run } of singles) {
983
720
  if (run.code === 2) {
984
- // CC:PostToolUse exit 2 = stderr 作为反馈喂给模型(工具已执行,无从阻断)
985
721
  const fb = clip(run.stderr.trim(), MAX_HOOK_FEEDBACK_CHARS);
986
722
  if (fb)
987
723
  contexts.push(fb);
@@ -997,7 +733,6 @@ export function createTaskHooks(config, ctx) {
997
733
  if (typeof out.systemMessage === "string")
998
734
  ctx.logger.warn("hook_system_message", { event: "PostToolUse", message: clip(out.systemMessage, 500) });
999
735
  if (out.continue === false) {
1000
- // core postToolUse seam 无 run 终止能力;记账可见(阶段一缺口),不吞。
1001
736
  ctx.logger.warn("hook_continue_false_unsupported", { event: "PostToolUse" });
1002
737
  }
1003
738
  if (out.decision === "block" && typeof out.reason === "string")
@@ -1008,8 +743,6 @@ export function createTaskHooks(config, ctx) {
1008
743
  if (typeof h.additionalContext === "string")
1009
744
  contexts.push(clip(h.additionalContext, MAX_HOOK_FEEDBACK_CHARS));
1010
745
  if (h.updatedMCPToolOutput !== undefined) {
1011
- // CC 把它限定在 MCP 工具的输出重写;core PostToolUseResult.updatedOutput 期望 content 数组,
1012
- // 语义不同型 → 阶段一不映射,记账可见。
1013
746
  ctx.logger.warn("hook_updated_mcp_output_unsupported", { event: "PostToolUse" });
1014
747
  }
1015
748
  }
@@ -1020,8 +753,6 @@ export function createTaskHooks(config, ctx) {
1020
753
  hooks.postToolUse = postToolUseHook;
1021
754
  }
1022
755
  if (postFailure) {
1023
- // CC 语义类比 PostToolUse:调用已失败无从阻断——exit 2 的 stderr / decision:block 的 reason 都折成
1024
- // additionalContext 反馈给模型(core 回调面就是 additionalContext-only,CC-exact)。
1025
756
  const postToolUseFailureHook = async (toolName, input, f, tctx) => {
1026
757
  const contexts = [];
1027
758
  const payload = {
@@ -1030,9 +761,7 @@ export function createTaskHooks(config, ctx) {
1030
761
  tool_name: toolName,
1031
762
  tool_input: input,
1032
763
  tool_use_id: tctx.toolCallId,
1033
- // 契约 error=模型面错误文本;有界截断(失败结果可能带 MB 级 tool 输出,同 tool_response 姿势)。
1034
764
  error: clip(f.error, MAX_TOOL_RESPONSE_CHARS),
1035
- // core isInterrupt 是宽任意-abort 语义(⊇ CC is_interrupt),原样透传——契约字段名 CC-verbatim。
1036
765
  is_interrupt: f.isInterrupt,
1037
766
  };
1038
767
  const singles = await runMatchingEntries("PostToolUseFailure", postFailure, toolName, payload, ctx, onceFired, Date.now() + MAX_HOOK_EVENT_TOTAL_SECONDS * 1000);
@@ -1069,9 +798,6 @@ export function createTaskHooks(config, ctx) {
1069
798
  hooks.postToolUseFailure = postToolUseFailureHook;
1070
799
  }
1071
800
  if (postBatch) {
1072
- // matcher=null 事件:契约语义=matcher 被忽略、全条目跑。core 回调面支持 additionalContext(一次性
1073
- // 注入在 turn 边界)→ exit 2 stderr / decision:block reason 都折进去;契约没定 PostToolBatch 的
1074
- // hookSpecificOutput 变体 → 出现即记账不吞。
1075
801
  const postToolBatchHook = async (calls) => {
1076
802
  const contexts = [];
1077
803
  const payload = {
@@ -1079,10 +805,8 @@ export function createTaskHooks(config, ctx) {
1079
805
  hook_event_name: "PostToolBatch",
1080
806
  tool_calls: calls.map((c) => ({
1081
807
  tool_name: c.toolName,
1082
- // 逐条截断(顶层 boundedToolInputPayload 只看 payload.tool_input,批量形状会绕过它)。
1083
808
  tool_input: boundedInputValue(c.input),
1084
809
  tool_use_id: c.toolCallId,
1085
- // core 的 response 已是有界文本 digest;投影成与 PostToolUse tool_response 同形({isError,text})。
1086
810
  tool_response: { isError: c.isError, ...(c.response !== undefined ? { text: clip(c.response, MAX_TOOL_RESPONSE_CHARS) } : {}) },
1087
811
  })),
1088
812
  };
@@ -1116,12 +840,8 @@ export function createTaskHooks(config, ctx) {
1116
840
  hooks.postToolBatch = postToolBatchHook;
1117
841
  }
1118
842
  if (promptSubmit) {
1119
- // matcher=null 事件(全条目跑)。CC:exit 2 = 阻断本次提交(stderr=模型可读理由;core 语义=task 以
1120
- // 此理由 fail);decision:"block"+reason 同;hookSpecificOutput.additionalContext 注入在 prompt 前。
1121
843
  const userPromptSubmitHook = async (prompt) => {
1122
844
  const contexts = [];
1123
- // prompt 是用户任意长输入(粘贴大文件),不设界=绕过 stdin 放大防线——与
1124
- // tool_input 同一上限截断。
1125
845
  const payload = { ...basePayload(ctx), hook_event_name: "UserPromptSubmit", prompt: clip(prompt, MAX_TOOL_INPUT_CHARS) };
1126
846
  const singles = await runMatchingEntries("UserPromptSubmit", ignoreMatchers(promptSubmit), "", payload, ctx, onceFired, Date.now() + MAX_HOOK_EVENT_TOTAL_SECONDS * 1000);
1127
847
  for (const { run } of singles) {
@@ -1139,7 +859,6 @@ export function createTaskHooks(config, ctx) {
1139
859
  if (typeof out.systemMessage === "string")
1140
860
  ctx.logger.warn("hook_system_message", { event: "UserPromptSubmit", message: clip(out.systemMessage, 500) });
1141
861
  if (out.continue === false) {
1142
- // continue:false=终止整个 run;userPromptSubmit 的 block 语义正是"任务带理由收场"——同义直映射。
1143
862
  const reason = clip(typeof out.stopReason === "string" ? out.stopReason : "hook requested stop", MAX_HOOK_FEEDBACK_CHARS);
1144
863
  return { block: reason, ...buildHookContextField(contexts) };
1145
864
  }
@@ -1160,40 +879,16 @@ export function createTaskHooks(config, ctx) {
1160
879
  hooks.userPromptSubmit = userPromptSubmitHook;
1161
880
  }
1162
881
  if (stopEntries) {
1163
- // matcher=null 事件(全条目跑)。CC:exit 2 = 阻断停止(stderr=继续干活的理由,模型再来一轮);
1164
- // decision:"block"+reason 同。防环(cap 8/stopHookActive)由 core 管,runner 只翻译。
1165
882
  const stopHook = async (sctx) => {
1166
883
  const payload = { ...basePayload(ctx), hook_event_name: "Stop", stop_hook_active: sctx.stopHookActive };
1167
- // ── Stop × `type:"prompt"` 的 CC 形(逐字文案见 cc-stop-prompt.ts;渲染与证据面判据见 branch-transcript.ts)──
1168
- // core 1.419 起 `StopHookContext.getBranch()` 交出本次运行的会话分支;没有它,一个"基于会话状态判断"的
1169
- // 条件钩子**结构上不可能工作**(cli live 实证:模型只会回「I don't have direct access…」这类散文)。
1170
- //
1171
- // 🔴 **证据面为空时不进这条路**,这一条是承重的:CC 的提示词**明确指示**模型在证据不足时回
1172
- // `{"ok": false, "reason": "insufficient evidence in transcript"}`,而 Stop 的 `ok:false` = **拦住停止**。
1173
- // 那**不是解析失败**(解析会成功、判词有效),所以解析层的任何防线都救不了 —— 唯一的防线是不启动它。
1174
- // 判据挂在**最终送进模型的那段文本**上,不是挂在 `getBranch` 的存在性上(core [1758]:
1175
- // 访问器在、返回 `[]`、或者被我们自己的过滤渲没了,是同一个后果,后者还是纯自伤)。
1176
- //
1177
- // ── 三问(行为面新增)──
1178
- // · 谁需要:任何用 `type:"prompt"` 写「基于会话状态判断」的 Stop 条件钩子 —— 这是**整类**,不是某个功能;
1179
- // · 谁被伤:①会话内容多进一次**已经在发生**的模型调用(增量是 transcript 的 token,不是新增一次调用);
1180
- // ②今天"从不拦"的钩子会开始按配置拦 —— 那**正是操作方配置它要做的事**,不是意外;
1181
- // · 什么补偿:`SEMA_STOP_PROMPT_TRANSCRIPT=0` 显式关;transcript 走 `redactSecrets` 与 fleet 行同纪律;
1182
- // 上限 `BRANCH_TRANSCRIPT_MAX_CHARS` 且截断**明说**;最坏情况仍被 core 的 `STOP_HOOK_BLOCK_CAP=8` 兜住。
1183
884
  let llmExtra;
1184
885
  const wantsPrompt = stopEntries.some((g) => g.hooks.some((h) => h.type === "prompt"));
1185
886
  if (wantsPrompt) {
1186
887
  if (ctx.stopPromptTranscript === false) {
1187
- // 旋钮关 ⇒ **整条路不启用**(而不是"退回旧的、送裸条件进去")。
1188
- // 选这个语义而不是"逐位退回旧行为":旧行为是**已知失效**的(评估者对会话是瞎的),
1189
- // 退回它等于留下一条"看起来在跑、永远给不出判决"的路 —— 而那正是这次要修掉的东西。
1190
- // 关掉就干净地不跑,并记账可见。
1191
888
  ctx.logger.warn("hook_entries_skipped", { event: "Stop", type: "prompt", reason: "stop_prompt_transcript_disabled" });
1192
889
  ctx.onHookNotice?.({ kind: "hook_decision_unavailable", event: "Stop", reason: "skipped", detail: "transcript disabled" });
1193
890
  }
1194
891
  else {
1195
- // ⚠️ `getBranch()` **抛错**与「分支为空」是两回事(引擎故障 vs 一条还没内容的分支),
1196
- // 记成同一个原因会让排障时分不清该看哪一边。分开记。
1197
892
  let branch;
1198
893
  try {
1199
894
  branch = await sctx.getBranch?.();
@@ -1206,43 +901,18 @@ export function createTaskHooks(config, ctx) {
1206
901
  llmExtra = { system: ccPromptSystemFor("Stop"), event: "Stop", transcript: rendered.text };
1207
902
  }
1208
903
  else {
1209
- // 诚实降级:**跳过 prompt 条目**(记账可见),而不是"送个空会话进去看看模型怎么说"。
1210
904
  ctx.logger.warn("hook_entries_skipped", { event: "Stop", type: "prompt", reason: `stop_prompt_no_evidence:${rendered.reason}` });
1211
905
  ctx.onHookNotice?.({ kind: "hook_decision_unavailable", event: "Stop", reason: "skipped", detail: `no evidence: ${rendered.reason}` });
1212
906
  }
1213
907
  }
1214
908
  }
1215
909
  const stopGroups = ignoreMatchers(stopEntries);
1216
- // 拿不到证据面(或旋钮关)⇒ 把 **prompt** 条目滤掉;其余类型(command/http/agent)照跑 ——
1217
- // 证据面判据只管"需要会话才能判"的那一类,不该让整个 Stop 事件停摆。
1218
- // 滤在这里而不是在骨架里:骨架是跨事件公共的,这条判据是 Stop×prompt 专属。
1219
910
  const effectiveGroups = llmExtra || !wantsPrompt
1220
911
  ? stopGroups
1221
912
  : stopGroups.map((g) => ({ ...g, hooks: g.hooks.filter((h) => h.type !== "prompt") })).filter((g) => g.hooks.length > 0);
1222
913
  const singles = await runMatchingEntries("Stop", effectiveGroups, "", payload, ctx, onceFired, Date.now() + MAX_HOOK_EVENT_TOTAL_SECONDS * 1000, undefined, llmExtra);
1223
- // CC 是并行聚合后 continue:false【全局】压过 block("就让它停")——block 若
1224
- // 短路返回,后到条目的 continue:false 就没机会翻案。改成跑完整匹配集再折叠:首个 block 记下,
1225
- // 任一 continue:false 最终放行。
1226
914
  let block;
1227
915
  let sawContinueFalse = false;
1228
- // ⚠️⚠️ **这个通道带副作用,别随手用**(亲验 core 源码,两版都读过):
1229
- // `additionalContext` **单独出现**(无 block)时,引擎返回的是**非空** follow-up 消息
1230
- // (`agent-harness.js:506` 的 `getFollowUpMessages`)⇒ **这一轮不结束**。
1231
- // · **core < 1.422**:`if (!result?.block)` 那支还会把 `consecutiveBlocks` **清零**
1232
- // —— 不是"不计入上限",是**抵消掉之前累积的** ⇒ 任何**能持续为真**的条件走这个通道,
1233
- // 会造出一条 `STOP_HOOK_BLOCK_CAP` **完全管不住**的循环(最坏烧完 maxTurns 缺省 1000)。
1234
- // · **core ≥ 1.422(本仓下限)**:改成 `if (messages.length === 0 && !result?.block)`
1235
- // ⇒ **任何非空反馈都计数、不再清零**(与 CC 2.1.220 一致)⇒ 最坏被 cap 8 兜住。
1236
- // ⚠️ 即便有了 cap,**这个通道仍然会让这一轮不结束** —— 它在功能上与 block 同效(该停的没停),
1237
- // 只是多了个上限。所以判据不变:**我们自己不得用它去"顺便告知一声"**;
1238
- // 命令形钩子作者**显式**这么配是他自己的选择(CC 对位)。
1239
- // 🔴 本仓把 core 下限提到 `^1.422.0` 正是为了这条 —— 因为 command 形的透传是**我们 1.277.3 加的**,
1240
- // 是我们引入了这个曝露面(见 `test/core-dependency-floor.test.ts` 顶注第二条理由)。
1241
- // 🔴 `StopHookResult` 是 `{ block?, additionalContext? }` 两个**解耦**的字段(core `core/hooks.d.ts:67`):
1242
- // `additionalContext` 注入给模型但**不计入**连续 block 上限(CC 2.1.201 对位)。此前本折叠只产出
1243
- // `block`,于是一条「只想补上下文、不想拦停」的 Stop 钩子输出被**静默丢弃** —— 而紧邻的
1244
- // UserPromptSubmit 折叠一直是带上它的(同一个 `hookSpecificOutput.additionalContext` 形)。
1245
- // 对 `/goal` 这类用法这正是常见形(「还差验收用例」既可以拦、也可以只提醒),所以按同构补齐。
1246
916
  const contexts = [];
1247
917
  for (const { entry, run } of singles) {
1248
918
  if (run.code === 2) {
@@ -1254,32 +924,11 @@ export function createTaskHooks(config, ctx) {
1254
924
  ctx.logger.warn("hook_command_failed", { event: "Stop", code: run.code, stderr: clip(run.stderr, 300) });
1255
925
  continue;
1256
926
  }
1257
- // CC 形的 prompt 条目回的是 `{ok, reason, impossible?}`,不是 `{decision}` —— 分开解析。
1258
- // 🔴 `parseCcVerdict` 解析不出来 ⇒ undefined ⇒ **无判决**(继续下一条),绝不臆测成 ok:false:
1259
- // Stop 的 ok:false = 拦停,把"读不懂"当成"没满足"会让一次解析失败变成一次拦停。
1260
- // ⚠️ **两种判词形都认**,而且它们**无歧义**:CC 形有 `ok`,既有形有 `decision`/`continue`/`hookSpecificOutput`,
1261
- // 两者的键不重叠。只认 CC 形会**破坏**已经按既有形写好的 prompt 钩子(那是对既有配置的回归),
1262
- // 而只认既有形就没有 CC 对位。⇒ 先试 CC 形;不是 CC 形就**落到下面的既有解析**,不早退。
1263
927
  if (llmExtra && entry.type === "prompt" && parseCcVerdict(run.stdout)) {
1264
928
  const v = parseCcVerdict(run.stdout);
1265
929
  if (v.ok)
1266
- continue; // 条件已达成 ⇒ 不拦
930
+ continue;
1267
931
  if (v.impossible) {
1268
- // 🔴 第三态:**永远不可能达成** ⇒ **真 fail-open:既不 block,也不注入** —— 理由只进日志。
1269
- //
1270
- // ⚠️ 我第一版把理由走了 `additionalContext`,想的是"静默放行会让用户以为钩子没生效"。
1271
- // **那个选择是错的**,core [1766] 指出、我亲读 core 源码核实(1.419.0):
1272
- // · `additionalContext` 单独出现 ⇒ 引擎返回**非空** follow-up 消息
1273
- // (`agent-harness.js:506` 的 `getFollowUpMessages`)⇒ **这一轮不结束**;
1274
- // · 它**不计入** `STOP_HOOK_BLOCK_CAP`,而且 `runtask.js` 里 `if (!result?.block)` 那支
1275
- // 会把 `consecutiveBlocks` **清零** —— 不是"不计入",是**抵消掉之前累积的**。
1276
- // ⇒ 一个**能持续为真**的 `impossible`(模型这段时间就是这么判)会:注入 → 续跑 → 又到停车点 →
1277
- // 又 impossible → 又注入 → 计数又清零……**专为兜住「该停而不停」建的那个上限,在这条道上保护为零**,
1278
- // 最坏情况被 `maxTurns`(缺省 1000)或墙钟兜住 = **把整个预算烧完**,而且**看起来是在忙不是卡住**。
1279
- //
1280
- // ⇒ 教训:**别用一个「带副作用的通道」去传递信息**。我要传的是"钩子判定不可达",
1281
- // 而我选的通道本身会让运行继续 —— 那正是 `impossible` 要防的事。
1282
- // 用户想知道"钩子为什么没生效"应该去日志里找,而不是靠一条让运行继续下去的注入来暗示。
1283
932
  ctx.logger.warn("hook_stop_condition_impossible", { event: "Stop", reason: clip(v.reason?.trim() ?? "", 500) });
1284
933
  continue;
1285
934
  }
@@ -1289,8 +938,6 @@ export function createTaskHooks(config, ctx) {
1289
938
  }
1290
939
  const out = parseHookStdout(run.stdout);
1291
940
  if (!out) {
1292
- // 🔴 **两种形都解析不出来 ⇒ 无判决**(绝不臆测成"没满足")。prompt 条目单独记账:
1293
- // 模型回散文是这条路最常见的失败形,静默丢弃会让排障无从下手。
1294
941
  if (llmExtra && entry.type === "prompt") {
1295
942
  ctx.logger.warn("hook_llm_unparsed_verdict", { event: "Stop", head: clip(run.stdout.trim(), 200) });
1296
943
  ctx.onHookNotice?.({ kind: "hook_decision_unavailable", event: "Stop", reason: "unparsed" });
@@ -1311,8 +958,6 @@ export function createTaskHooks(config, ctx) {
1311
958
  contexts.push(clip(h.additionalContext, MAX_HOOK_FEEDBACK_CHARS));
1312
959
  }
1313
960
  }
1314
- // continue:false > block(CC 全局优先序)——「就让它停」。⚠️ 此时 additionalContext 也一并丢弃:
1315
- // 本 run 已经要结束了,注入给"模型的下一轮"的上下文没有下一轮可去。
1316
961
  if (sawContinueFalse)
1317
962
  return undefined;
1318
963
  if (block === undefined && contexts.length === 0)
@@ -1325,16 +970,12 @@ export function createTaskHooks(config, ctx) {
1325
970
  hooks.stop = stopHook;
1326
971
  }
1327
972
  if (stopFailureEntries) {
1328
- // observe-only(core 回调 void,契约无 StopFailure 输出面);matcher 对契约 error 枚举匹配
1329
- // (BRAIN_TO_TURN_ERROR 映射后的值);原始人话错误进 error_details(契约可选字段)。
1330
973
  const stopFailureHook = async (fctx) => {
1331
974
  const turnError = toAssistantTurnError(fctx.errorKind);
1332
975
  const payload = {
1333
976
  ...basePayload(ctx),
1334
977
  hook_event_name: "StopFailure",
1335
978
  error: turnError,
1336
- // brain 错误串可能回显 provider 的 Authorization/token/URL 凭据——脱敏后再截断
1337
- // (与 elicitation/project-memory 同用 trace/redact 单源)。
1338
979
  ...(fctx.error ? { error_details: clip(redactSecrets(fctx.error), MAX_HOOK_FEEDBACK_CHARS) } : {}),
1339
980
  };
1340
981
  await runObserveOnlyEvent("StopFailure", stopFailureEntries, turnError, payload, ctx, onceFired);
@@ -1342,20 +983,16 @@ export function createTaskHooks(config, ctx) {
1342
983
  hooks.stopFailure = stopFailureHook;
1343
984
  }
1344
985
  if (preCompactEntries) {
1345
- // matcher 对 trigger 匹配。core 的 trigger 可为 "forced"(契约枚举只有 auto|manual 的超集)——原样
1346
- // 透传(matcher "*" 仍可见);block 在 forced 上由 core 忽略并 trace(其契约注明),runner 不重复裁决。
1347
986
  const preCompactHook = async (pctx) => {
1348
987
  const payload = {
1349
988
  ...basePayload(ctx),
1350
989
  hook_event_name: "PreCompact",
1351
990
  trigger: pctx.trigger,
1352
- // 契约 custom_instructions 是 nullable 必填(CC-verbatim)——没有就送 null,不缺字段。
1353
991
  custom_instructions: pctx.customInstructions ?? null,
1354
992
  };
1355
993
  const singles = await runMatchingEntries("PreCompact", preCompactEntries, pctx.trigger, payload, ctx, onceFired, Date.now() + MAX_HOOK_EVENT_TOTAL_SECONDS * 1000);
1356
994
  for (const { run } of singles) {
1357
995
  if (run.code === 2) {
1358
- // CC:exit 2 = 阻断本次压缩(stderr=理由,core trace 记录)。
1359
996
  return { block: clip(run.stderr.trim(), MAX_HOOK_FEEDBACK_CHARS) || "blocked by a PreCompact hook" };
1360
997
  }
1361
998
  if (run.code !== 0) {
@@ -1368,7 +1005,6 @@ export function createTaskHooks(config, ctx) {
1368
1005
  if (typeof out.systemMessage === "string")
1369
1006
  ctx.logger.warn("hook_system_message", { event: "PreCompact", message: clip(out.systemMessage, 500) });
1370
1007
  if (out.continue === false) {
1371
- // "终止整个 run"≠"跳过这次压缩"——不偷换成 block,记账可见。
1372
1008
  ctx.logger.warn("hook_continue_false_unsupported", { event: "PreCompact" });
1373
1009
  }
1374
1010
  if (out.decision === "block") {
@@ -1380,8 +1016,6 @@ export function createTaskHooks(config, ctx) {
1380
1016
  hooks.preCompact = preCompactHook;
1381
1017
  }
1382
1018
  if (postCompactEntries) {
1383
- // observe-only(core 回调 void,契约无 PostCompact 输出面);matcher 对 trigger 匹配;
1384
- // compact_summary 有界截断(压缩摘要可长)。core 的 tokensBefore/After 契约无字段 → 宁缺毋滥。
1385
1019
  const postCompactHook = async (cctx) => {
1386
1020
  const payload = {
1387
1021
  ...basePayload(ctx),
@@ -1395,18 +1029,6 @@ export function createTaskHooks(config, ctx) {
1395
1029
  }
1396
1030
  return hooks;
1397
1031
  }
1398
- /**
1399
- * 把部署级 hooks 基线(deps.hooks,如 TOOL_TRACE 观测)折进 task 级 hooks——core 是整槽覆盖
1400
- * (`spec.hooks ?? deps.hooks`),不折叠就静默 shadow 部署观测(runtask gateBaseline 注释点名的坑)。
1401
- * 语义:部署槽先跑(观测看到原始输入/输出),task 槽后跑;结果合并:
1402
- * - preToolUse:deny > ask > allow 折叠;updatedInput 串行线程(部署重写喂给 task 槽);context 拼接。
1403
- * - postToolUse/postToolUseFailure/postToolBatch:双跑,additionalContext 拼接,updatedOutput 取
1404
- * 后者(task 槽)优先。
1405
- * - userPromptSubmit/stop:部署 block 优先短路(不再跑 task 槽),否则双跑、context 拼接、task block 生效。
1406
- * - preCompact:双跑,block 部署优先,additionalInstructions 拼接。
1407
- * - stopFailure/postCompact(observe-only void):顺序双跑,无返回可折。
1408
- * 单在则透传。
1409
- */
1410
1032
  export function composeHooks(deployment, task) {
1411
1033
  if (!deployment)
1412
1034
  return task;
@@ -1414,13 +1036,6 @@ export function composeHooks(deployment, task) {
1414
1036
  const t = task;
1415
1037
  const out = {};
1416
1038
  if (d.preToolUse || t.preToolUse) {
1417
- // 🔴 观察标(core 5.20.0 `Hooks.preToolUseObservational`)的传承折:复合体只有在**每一只在场的
1418
- // `preToolUse` 都声明了纯观察**时才继续声明。方向是 fail-closed 的——
1419
- // · 漏传承(该声明没声明)= 退回 5.19.0 行为:多一条链条目,跨副本赎回被响亮拒(吵,但不失守);
1420
- // · 错传承(有真裁决者却声明了)= core 把复合体的判词整条**拒绝采纳**,客户端 `settings.hooks`
1421
- // 的 deny/ask 静默蒸发 —— 这是安全轴上的静默 fail-open,比原缺陷坏得多。
1422
- // 故判据是「无人反对」而不是「有人赞成」:任一在场面未声明 ⇒ 复合体不声明。
1423
- // 缺席的槽不算反对票(`d.preToolUse` 不在 = 部署侧没有面可裁决,不影响 task 侧的性质)。
1424
1039
  const bothObservational = (d.preToolUse === undefined || d.preToolUseObservational === true) &&
1425
1040
  (t.preToolUse === undefined || t.preToolUseObservational === true);
1426
1041
  if (bothObservational)
@@ -1446,7 +1061,6 @@ export function composeHooks(deployment, task) {
1446
1061
  }
1447
1062
  }
1448
1063
  if (asked) {
1449
- // ask 槽自己没带改写、但前面槽已改写 → 把线程中的改写挂上,approve 才执行改写后 args。
1450
1064
  const askedUpdated = asked.updatedInput;
1451
1065
  return {
1452
1066
  ...asked,
@@ -1499,8 +1113,6 @@ export function composeHooks(deployment, task) {
1499
1113
  if (d.userPromptSubmit || t.userPromptSubmit) {
1500
1114
  out.userPromptSubmit = async (prompt) => {
1501
1115
  const rd = d.userPromptSubmit ? await d.userPromptSubmit(prompt) : undefined;
1502
- // 短路的是**槽序**(部署拦下就不再跑 task 槽),不是预算:`additionalContext` 与 `block` 在 core
1503
- // 是解耦两字段(见下方 stop 注),原样外送就是一条绕过总帽的道。
1504
1116
  if (rd?.block)
1505
1117
  return { ...rd, ...buildHookContextField(rd.additionalContext ? [rd.additionalContext] : []) };
1506
1118
  const rt = t.userPromptSubmit ? await t.userPromptSubmit(prompt) : undefined;
@@ -1511,8 +1123,6 @@ export function composeHooks(deployment, task) {
1511
1123
  };
1512
1124
  }
1513
1125
  if (d.stop || t.stop) {
1514
- // 与 userPromptSubmit 同折叠:部署 block 优先短路;否则双跑、additionalContext(CC 2.1.201,与
1515
- // block 解耦)拼接、task 的 block 生效——`rt ?? rd` 会把"rd 有 context、rt 有 block"折丢一侧。
1516
1126
  out.stop = async (sctx) => {
1517
1127
  const rd = d.stop ? await d.stop(sctx) : undefined;
1518
1128
  if (rd?.block)
@@ -1525,7 +1135,6 @@ export function composeHooks(deployment, task) {
1525
1135
  };
1526
1136
  }
1527
1137
  if (d.preCompact || t.preCompact) {
1528
- // 双跑(部署观测也想看到每次压缩);block 部署优先,additionalInstructions 拼接(core 语义=追加)。
1529
1138
  out.preCompact = async (pctx) => {
1530
1139
  const rd = d.preCompact ? await d.preCompact(pctx) : undefined;
1531
1140
  const rt = t.preCompact ? await t.preCompact(pctx) : undefined;
@@ -1540,9 +1149,6 @@ export function composeHooks(deployment, task) {
1540
1149
  };
1541
1150
  }
1542
1151
  for (const slot of ["stopFailure", "postCompact", "permissionDenied"]) {
1543
- // observe-only void 槽:两侧都在=顺序双跑(部署先),无返回可折;单在则透传。
1544
- // permissionDenied(core 1.257 新增,gate deny 短路观测)也走这里——createTaskHooks 不产它
1545
- // (PermissionDenied 是 shell-owned 事件),但部署侧 tracer 未来若设它,整槽覆盖不透传就会被 shadow。
1546
1152
  const ds = d[slot];
1547
1153
  const ts = t[slot];
1548
1154
  if (ds && ts) {