@sema-agent/client-core 0.12.2 → 0.14.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 (131) hide show
  1. package/README.md +15 -2
  2. package/dist/abortableSleep.d.ts +29 -0
  3. package/dist/abortableSleep.js +43 -0
  4. package/dist/adapt/arms.d.ts +67 -0
  5. package/dist/adapt/arms.js +623 -0
  6. package/dist/adapt/ids.d.ts +22 -0
  7. package/dist/adapt/ids.js +34 -0
  8. package/dist/adapt/instanceLedger.d.ts +36 -0
  9. package/dist/adapt/instanceLedger.js +50 -0
  10. package/dist/adapt/panelTasks.d.ts +59 -0
  11. package/dist/adapt/panelTasks.js +193 -0
  12. package/dist/adapt/textStream.d.ts +63 -0
  13. package/dist/adapt/textStream.js +141 -0
  14. package/dist/adapt/toolCards.d.ts +65 -0
  15. package/dist/adapt/toolCards.js +100 -0
  16. package/dist/adapt/turnFlags.d.ts +37 -0
  17. package/dist/adapt/turnFlags.js +55 -0
  18. package/dist/adapt/wireShapes.d.ts +96 -0
  19. package/dist/adapt/wireShapes.js +167 -0
  20. package/dist/adapt.d.ts +33 -60
  21. package/dist/adapt.js +84 -1207
  22. package/dist/adapter/downstream/eventToSdkMessage.d.ts +57 -13
  23. package/dist/adapter/downstream/eventToSdkMessage.js +174 -106
  24. package/dist/adapter/downstream/terminalToSdkResult.js +151 -160
  25. package/dist/adapter/downstream/turnUsageToModelUsage.d.ts +36 -0
  26. package/dist/adapter/downstream/turnUsageToModelUsage.js +34 -6
  27. package/dist/adapter/runStream.d.ts +36 -4
  28. package/dist/adapter/runStream.js +195 -13
  29. package/dist/adapter/types.d.ts +28 -2
  30. package/dist/agentSession/backgroundView.js +4 -15
  31. package/dist/agentsWireCaps.d.ts +10 -6
  32. package/dist/agentsWireCaps.js +21 -7
  33. package/dist/argvFlagValue.d.ts +41 -0
  34. package/dist/argvFlagValue.js +69 -0
  35. package/dist/attachmentsWireCaps.d.ts +3 -2
  36. package/dist/attachmentsWireCaps.js +5 -3
  37. package/dist/classifierVerdictWire.d.ts +8 -0
  38. package/dist/classifierVerdictWire.js +8 -0
  39. package/dist/cloudConfigWireCaps.d.ts +28 -1
  40. package/dist/cloudConfigWireCaps.js +51 -11
  41. package/dist/controlRouter.d.ts +14 -8
  42. package/dist/controlRouter.js +15 -21
  43. package/dist/detachWire.d.ts +15 -5
  44. package/dist/detachWire.js +17 -7
  45. package/dist/effortWire.d.ts +0 -21
  46. package/dist/effortWire.js +6 -20
  47. package/dist/engineInlineTaskStats.d.ts +12 -6
  48. package/dist/engineWireSdk.d.ts +12 -0
  49. package/dist/env/localeGeo.js +2 -1
  50. package/dist/envFlag.d.ts +39 -0
  51. package/dist/envFlag.js +51 -0
  52. package/dist/finalVerifyWire.d.ts +7 -5
  53. package/dist/finalVerifyWire.js +6 -5
  54. package/dist/fleet/fleetLedger.d.ts +32 -9
  55. package/dist/fleet/fleetLedger.js +119 -34
  56. package/dist/fleet/fleetProjection.d.ts +44 -6
  57. package/dist/fleet/fleetProjection.js +52 -10
  58. package/dist/fleetAgentPanelProjection.d.ts +18 -1
  59. package/dist/fleetAgentPanelProjection.js +61 -14
  60. package/dist/forkWireCaps.d.ts +2 -1
  61. package/dist/forkWireCaps.js +5 -11
  62. package/dist/goalStopHook.d.ts +142 -0
  63. package/dist/goalStopHook.js +258 -0
  64. package/dist/headlessPermissionModeWire.d.ts +10 -0
  65. package/dist/headlessPermissionModeWire.js +24 -21
  66. package/dist/headlessReconnectWire.d.ts +7 -1
  67. package/dist/headlessReconnectWire.js +20 -2
  68. package/dist/hitl/approvalsFeed.d.ts +22 -1
  69. package/dist/hitl/approvalsFeed.js +103 -9
  70. package/dist/hitl/askGateWire.d.ts +27 -96
  71. package/dist/hitl/askGateWire.js +69 -618
  72. package/dist/hitl/frameRouter.d.ts +86 -0
  73. package/dist/hitl/frameRouter.js +383 -0
  74. package/dist/hitl/gateLedger.d.ts +119 -0
  75. package/dist/hitl/gateLedger.js +113 -0
  76. package/dist/hitl/hitlBridge.d.ts +75 -15
  77. package/dist/hitl/hitlBridge.js +94 -22
  78. package/dist/hitl/hitlHostSurface.d.ts +49 -0
  79. package/dist/hitl/hitlHostSurface.js +155 -0
  80. package/dist/hitl/parkResolver.d.ts +74 -0
  81. package/dist/hitl/parkResolver.js +250 -0
  82. package/dist/hitl/planReviewWire.d.ts +60 -2
  83. package/dist/hitl/planReviewWire.js +197 -91
  84. package/dist/hitl/toolApprovalWire.d.ts +67 -4
  85. package/dist/hitl/toolApprovalWire.js +129 -31
  86. package/dist/hooksWireCaps.d.ts +1 -82
  87. package/dist/hooksWireCaps.js +44 -237
  88. package/dist/host.d.ts +16 -5
  89. package/dist/index.d.ts +2 -0
  90. package/dist/index.js +15 -1
  91. package/dist/interactiveToolsWire.d.ts +15 -4
  92. package/dist/interactiveToolsWire.js +24 -26
  93. package/dist/limitsWire.js +7 -40
  94. package/dist/liveInitToolFace.d.ts +51 -6
  95. package/dist/liveQuestionStore.d.ts +18 -6
  96. package/dist/liveQuestionStore.js +15 -0
  97. package/dist/model/providerPresets.js +11 -1
  98. package/dist/notifications.d.ts +62 -2
  99. package/dist/notifications.js +306 -49
  100. package/dist/printToolResultFrame.d.ts +12 -4
  101. package/dist/printToolResultFrame.js +11 -21
  102. package/dist/retainBackgroundWireCaps.d.ts +3 -2
  103. package/dist/retainBackgroundWireCaps.js +5 -9
  104. package/dist/sandboxWire.d.ts +9 -31
  105. package/dist/sandboxWire.js +51 -50
  106. package/dist/scenarioWire.d.ts +1 -1
  107. package/dist/scenarioWire.js +25 -36
  108. package/dist/seam.d.ts +23 -6
  109. package/dist/seam.js +40 -30
  110. package/dist/seatContract.d.ts +369 -83
  111. package/dist/seatContract.js +585 -198
  112. package/dist/selfOrchestrationWireCaps.d.ts +6 -5
  113. package/dist/selfOrchestrationWireCaps.js +8 -12
  114. package/dist/sessionSlot.d.ts +8 -0
  115. package/dist/sessionSlot.js +1 -0
  116. package/dist/steering.js +2 -2
  117. package/dist/subagent/engineTaskHandleWire.d.ts +3 -0
  118. package/dist/subagent/engineTaskHandleWire.js +17 -2
  119. package/dist/subagentContentStore.d.ts +5 -5
  120. package/dist/toolResult.d.ts +89 -8
  121. package/dist/toolResult.js +99 -30
  122. package/dist/typePins.d.ts +17 -0
  123. package/dist/typePins.js +1 -0
  124. package/dist/ultracodeWireCaps.js +5 -6
  125. package/dist/unrefTimer.d.ts +30 -0
  126. package/dist/unrefTimer.js +5 -0
  127. package/dist/workflow.d.ts +3 -2
  128. package/dist/workflow.js +3 -2
  129. package/dist/workflowClient.d.ts +7 -0
  130. package/dist/workflowClient.js +47 -12
  131. package/package.json +3 -3
@@ -1,6 +1,10 @@
1
1
  /**
2
2
  * ⇄ B1 批搬迁(2026-07-27,多端改造设计稿 §2.4.1-B):cli src/sema/fleetAgentPanelProjection.ts 逐字搬入(唯一 import = 同批搬入的
3
3
  * engineAgentPanelStore,包内相对路径不变;壳侧改 re-export)。
4
+ * 🔴 REF-CC-041/042(P3 wave2,fleet2-03/04):P1 迁移之后本文件不再是「唯一 import」——同批把
5
+ * `TERMINAL_STATUSES` 改成 import `./fleet/fleetProjection.js` 的 `TERMINAL_FLEET_TASK_STATUSES`
6
+ * (单源,原副本零测试锁着,分叉理由已不成立)、`rowIdTail` 改 import `./workflow.js`(同包另两个
7
+ * 消费者 `fleet/fleetLedger.ts` / `fleet/fleetProjection.ts` 早就这么做,只有本文件是例外)。
4
8
  * ⚠️ 本文件含**两个字面 NUL 字节**(`${ct.toolName}\0${ct.target}` 的 currentTool 判等键分隔符,
5
9
  * 语义承载位,搬迁时逐字节保留)—— 副作用:BSD `grep`/`file` 把本文件当二进制,`grep` 不加 `-a`
6
10
  * 会**静默零命中**(不是「没有」,是「没在找」)。改本文件的探针一律 `grep -a`。
@@ -36,37 +40,72 @@
36
40
  * 安全网不是真相源(真终态来自行帧终态 / bg_notification),宽限窗内的重连空窗(snapshot 清表)
37
41
  * 不会误结。lastSeen 恒**先**按本次在场行刷新、再判缺席,所以 footer 长时间卸载后重挂也不误结。
38
42
  * ④ 行 id 尾段判(`${runId} ${taskId}` 复合形取尾;真机 1.277.4 上子行 id 是裸 a+16hex,复合形是
39
- * run 发布器双生行的形)——与 footerRowBelt.rowIdTail 同语义就地实现,保持本模块零依赖。
43
+ * run 发布器双生行的形)——REF-CC-042:与同包 `workflow.rowIdTail` 同一个函数(import,不再
44
+ * 就地维护第二份;该函数出过等价性事故,0.1.1 修,见 `workflow.ts` 头注)。
40
45
  */
41
46
  import { publishEngineAgentPanelEvent } from './engineAgentPanelStore.js';
42
- /** fleetClient.TERMINAL_FLEET_TASK_STATUSES 同源词汇(刻意双份,零模块图污染;drift 由本套锁)。 */
43
- const TERMINAL_STATUSES = new Set(['completed', 'failed', 'killed']);
47
+ import { rowIdTail } from './workflow.js';
48
+ import { TERMINAL_FLEET_TASK_STATUSES } from './fleet/fleetProjection.js';
49
+ import { DEFAULT_SESSION_KEY } from './sessionSlot.js';
44
50
  /** 行从投影集消失多久之后按 completed 兜底 settle(重连空窗 / snapshot 清表远小于此)。 */
45
51
  export const ABSENT_SETTLE_MS = 30_000;
52
+ /** REF-CC-045(fleet2-07):settled 条目的回收期 —— settle 后这么久仍未再被投影 ⇒ 台账整条
53
+ * delete,给 `fleetAgentProjectionSizeFor()` 一个上界(此前 `seen` 只增不减,长会话里每个
54
+ * 跑过的 bg 子代永久占一条)。settle 事件已经发过,消费端 settle 幂等,delete 对外无事件。 */
55
+ const SETTLED_RETENTION_MS = ABSENT_SETTLE_MS * 4;
46
56
  /** currentTool 判等键(POST-MERGE 行每秒重发同值,只有键变了才算变化)。 */
47
57
  function currentToolKeyOf(ct) {
48
58
  if (!ct || typeof ct.toolName !== 'string' || ct.toolName.length === 0)
49
59
  return undefined;
50
60
  return `${ct.toolName}\u0000${ct.target ?? ''}`;
51
61
  }
62
+ /** REF-CC-045(fleet2-07):per-sessionKey 台账(W1 同款 `Map<sessionKey, Map<...>>` 形)。 */
52
63
  const seen = new Map();
53
- /** 复合行键(`<runId> <taskId>`)的尾段 = 引擎 taskId(handle)。 */
54
- function rowIdTail(id) {
55
- return id.split(/\s+/).pop() ?? id;
64
+ function seenFor(sessionKey) {
65
+ let m = seen.get(sessionKey);
66
+ if (!m) {
67
+ m = new Map();
68
+ seen.set(sessionKey, m);
69
+ }
70
+ return m;
56
71
  }
57
- /** 测试钩:清投影台账。 */
72
+ /**
73
+ * 测试钩:清投影台账 —— **全部 sessionKey**,不是只清 default 桶。
74
+ *
75
+ * 🔴 回炉定谳(P3 wave2 复审 blocker④):per-session 化那一批一度把它收窄成
76
+ * `resetFleetAgentPanelProjectionFor(DEFAULT_SESSION_KEY)`。它的契约是「清投影台账」,不是一个
77
+ * 会话语义位 —— 收窄等于给多会话宿主的用例之间留一条跨用例串扰通道(用例 A 先
78
+ * `projectFleetAgentRowsFor('sess-b', …)`,用例间调零参 reset,`sess-b` 桶存活 ⇒ 下个用例首次
79
+ * 投影同 taskId 时 `prev` 非空且 `settled:true`,`fleet-row`/`end` 被去重门吞掉)。
80
+ * 对位实现同形:`fleet/fleetLedger.__resetFleetLedgerRegistryForTests()` = `liveBgViews.clear()`;
81
+ * W1 段五个槽位的现成规约也逐个钉的是「reset 清全部键」。
82
+ * 要只清一个会话请用 `resetFleetAgentPanelProjectionFor(sessionKey)`。
83
+ */
58
84
  export function resetFleetAgentPanelProjection() {
59
85
  seen.clear();
60
86
  }
87
+ /** W1 keyed variant(REF-CC-045):只清**这一个会话**的桶。 */
88
+ export function resetFleetAgentPanelProjectionFor(sessionKey) {
89
+ seen.delete(sessionKey);
90
+ }
61
91
  /** 台账读面(测试/诊断):当前记着的行数。 */
62
92
  export function fleetAgentProjectionSize() {
63
- return seen.size;
93
+ return fleetAgentProjectionSizeFor(DEFAULT_SESSION_KEY);
94
+ }
95
+ /** W1 keyed variant。 */
96
+ export function fleetAgentProjectionSizeFor(sessionKey) {
97
+ return seen.get(sessionKey)?.size ?? 0;
64
98
  }
65
99
  /**
66
100
  * 把一次 fleet 投影(已经过 own 判别 + workflow belt 的 FleetTask[])喂给面板事件流。
67
101
  * 幂等、无副作用地重复调用安全 —— 每秒被 footer 调用两次就是常态。
68
102
  */
69
103
  export function projectFleetAgentRows(rows, nowMs = Date.now()) {
104
+ projectFleetAgentRowsFor(DEFAULT_SESSION_KEY, rows, nowMs);
105
+ }
106
+ /** W1 keyed variant(REF-CC-045):多会话宿主每会话一份台账,互不顶盖。 */
107
+ export function projectFleetAgentRowsFor(sessionKey, rows, nowMs = Date.now()) {
108
+ const seenMap = seenFor(sessionKey);
70
109
  const present = new Set();
71
110
  for (const row of rows) {
72
111
  if (!row?.id)
@@ -83,8 +122,8 @@ export function projectFleetAgentRows(rows, nowMs = Date.now()) {
83
122
  const startedAt = typeof row.startedAt === 'number' && row.startedAt > 0 ? row.startedAt : undefined;
84
123
  const currentTool = currentToolKeyOf(row.currentTool) !== undefined ? row.currentTool : undefined;
85
124
  const currentToolKey = currentToolKeyOf(row.currentTool);
86
- const prev = seen.get(taskId);
87
- if (TERMINAL_STATUSES.has(status)) {
125
+ const prev = seenMap.get(taskId);
126
+ if (TERMINAL_FLEET_TASK_STATUSES.has(status)) {
88
127
  // ② 终态一次性 settle(幂等台账;消费端 settle 也幂等)。
89
128
  if (!prev?.settled) {
90
129
  // 终态行也把最后一拍的累计工具数/转录锚带上:详情页在行翻终态后仍要显示真计数与
@@ -109,7 +148,7 @@ export function projectFleetAgentRows(rows, nowMs = Date.now()) {
109
148
  isError: status === 'failed' || status === 'killed',
110
149
  });
111
150
  }
112
- seen.set(taskId, {
151
+ seenMap.set(taskId, {
113
152
  status,
114
153
  tokens,
115
154
  toolUses: toolUses !== undefined ? toolUses : prev?.toolUses,
@@ -147,7 +186,7 @@ export function projectFleetAgentRows(rows, nowMs = Date.now()) {
147
186
  });
148
187
  }
149
188
  // 台账对这几位是**粘性**的:缺席帧不擦掉已知值(同上的单向语义)。
150
- seen.set(taskId, {
189
+ seenMap.set(taskId, {
151
190
  status,
152
191
  tokens,
153
192
  toolUses: toolUses !== undefined ? toolUses : prev?.toolUses,
@@ -160,12 +199,20 @@ export function projectFleetAgentRows(rows, nowMs = Date.now()) {
160
199
  }
161
200
  // ③ 缺席兜底 —— 必须在【本次在场行全部刷完 lastSeen 之后】判,否则 footer 长时间卸载后重挂
162
201
  // 会把仍在跑的行按「陈旧」误结。
163
- for (const [taskId, s] of seen) {
202
+ for (const [taskId, s] of seenMap) {
164
203
  if (present.has(taskId) || s.settled)
165
204
  continue;
166
205
  if (nowMs - s.lastSeenAt <= ABSENT_SETTLE_MS)
167
206
  continue;
168
207
  publishEngineAgentPanelEvent({ kind: 'end', taskId, isError: false });
169
- seen.set(taskId, { ...s, settled: true });
208
+ seenMap.set(taskId, { ...s, settled: true });
209
+ }
210
+ // ④ REF-CC-045(fleet2-07):settled 条目回收 —— 给台账一个上界(此前只增不减)。settle 已发过
211
+ // 事件,delete 对外无事件,消费端 settle 本身幂等。
212
+ for (const [taskId, s] of seenMap) {
213
+ if (s.settled && nowMs - s.lastSeenAt > SETTLED_RETENTION_MS)
214
+ seenMap.delete(taskId);
170
215
  }
216
+ if (seenMap.size === 0)
217
+ seen.delete(sessionKey);
171
218
  }
@@ -21,5 +21,6 @@ import { type EnvLike } from './hostEnv.js';
21
21
  /** The env key the shell reads. SEMA_-namespaced so it never collides with a worker's own deploy env. */
22
22
  export declare const ENABLE_FORK_ENV: "SEMA_ENABLE_FORK";
23
23
  /** ENV source → explicit `true`/`false` when the user set SEMA_ENABLE_FORK either way, else
24
- * `undefined` (⇒ no stamp ⇒ the engine default applies — GRANTED on core ≥1.257, CC parity). */
24
+ * `undefined` (⇒ no stamp ⇒ the engine default applies — GRANTED on core ≥1.257, CC parity).
25
+ * Spelling set = {@link envFlagOn}/{@link envFlagOff}(REF-CC-141 dup-02 单源)。 */
25
26
  export declare function enableForkFromEnv(env?: EnvLike): boolean | undefined;
@@ -18,25 +18,19 @@
18
18
  * request shape never changes.
19
19
  */
20
20
  import { hostEnv } from './hostEnv.js';
21
+ import { envFlagOff, envFlagOn } from './envFlag.js';
21
22
  /** The env key the shell reads. SEMA_-namespaced so it never collides with a worker's own deploy env. */
22
23
  export const ENABLE_FORK_ENV = 'SEMA_ENABLE_FORK';
23
- function isTruthy(v) {
24
- const s = v.trim().toLowerCase();
25
- return s === '1' || s === 'true' || s === 'yes' || s === 'on';
26
- }
27
- function isFalsy(v) {
28
- const s = v.trim().toLowerCase();
29
- return s === '0' || s === 'false' || s === 'no' || s === 'off';
30
- }
31
24
  /** ENV source → explicit `true`/`false` when the user set SEMA_ENABLE_FORK either way, else
32
- * `undefined` (⇒ no stamp ⇒ the engine default applies — GRANTED on core ≥1.257, CC parity). */
25
+ * `undefined` (⇒ no stamp ⇒ the engine default applies — GRANTED on core ≥1.257, CC parity).
26
+ * Spelling set = {@link envFlagOn}/{@link envFlagOff}(REF-CC-141 dup-02 单源)。 */
33
27
  export function enableForkFromEnv(env = hostEnv()) {
34
28
  const raw = env[ENABLE_FORK_ENV];
35
29
  if (typeof raw !== 'string' || raw.trim() === '')
36
30
  return undefined;
37
- if (isTruthy(raw))
31
+ if (envFlagOn(raw))
38
32
  return true;
39
- if (isFalsy(raw))
33
+ if (envFlagOff(raw))
40
34
  return false;
41
35
  return undefined; // unrecognized spelling — don't guess, let the engine default apply
42
36
  }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * REF-CC-156(midband-04,split,2026-08-02):拆自 `hooksWireCaps.ts`——那个文件里约 220 行(占其
3
+ * 387 行的 57%)是 `/goal` 命令专属的 Stop-hook 版本判档 + prompt 组装 + 模块级单实例状态,与
4
+ * `hooksWireCaps.ts` 其余部分(通用 settings-file hooks 投影 + 治理门)是两个不同的关注点,原文件
5
+ * 只在 `hooksForWire()` 尾部合流一次。纯搬迁,行为不变:所有导出名字原样保留(`hooksWireCaps.ts`
6
+ * 显式 re-export,外部 import 路径零改动),模块级状态(`wireSessionStopHookPrompt`/
7
+ * `wireSessionStopHookCcSemantics`)随代码一起搬到本文件,`docs/refactor/p1-scan/singleton-manifest.json`
8
+ * 的两条登记同批把 `file` 改成本文件坐标。
9
+ *
10
+ * ── 以下为原文件的领域说明(逐字保留)────────────────────────────────────────────────────────
11
+ *
12
+ * SESSION-SCOPED Stop hook overlay(`/goal`,任务 #75 · C-7,2026-07-26)
13
+ * CC 220 的 `/goal <condition>` 会往 `sessionHooksRegistry` 注册一条会话级 Stop×prompt 钩子
14
+ * (`t.sessionHooksRegistry.add(sessionId,"Stop","",{type:"prompt",prompt})`),那才是「干到条件满足
15
+ * 再停」的机制半边。CC 是单进程,注册进壳内 registry 就够。
16
+ *
17
+ * 🔴 **sema 是双进程壳,壳侧那条钩子在 live 模式 fire 不到**(实证,不是推测):壳的 Stop 钩子只有
18
+ * `src/query/stopHooks.ts handleStopHooks` 一个执行点,调用方是 `src/query.ts` —— 壳自己的 query
19
+ * 循环;live 模式 turn 循环在引擎里跑,壳的 query.ts 不参与。upstreamBridge 只 fire SubagentStop 且
20
+ * observe-only。所以引擎腿必须走 `settings.hooks` 投影,这个 overlay 就是那条腿。
21
+ *
22
+ * 🟢 **本 overlay 默认开**(2026-07-26 翻默认;逃生口 `SEMA_GOAL_STOP_HOOK_WIRE=0/false/no/off`)。
23
+ *
24
+ * 修前默认关,理由是两条**前提**——两条都已被 server 侧行为取证**证伪**(黑板 [1721],6 条行为
25
+ * 用例已进 server 仓;cli 仓另有 scripts/run-goal-stop-hook-live-test.mjs 的真引擎+真模型复验):
26
+ * ① 旧论断「SDK 类型 DEFERRED ⇒ 服务端会 400」**是把类型面当成了运行时校验面**,对 server
27
+ * 不成立:`parseHooksConfig({Stop:[{hooks:[{type:"prompt",prompt}]}]})` 无 error、真解析;
28
+ * prompt 形在 server 的条目白名单里与 command/http/agent **并列**
29
+ * (@sema-agent/server hooks/hook-runner `entry.type !== "command" && … !== "prompt" && …`
30
+ * 那道 type_not_supported 闸口把 prompt 放行,1.277.1 dist 对应 hook-runner.js:390)。
31
+ * 卡着的只是 **SDK / registry-core 的类型**(见下方 cast),与 server 运行时接受与否两件事。
32
+ * ② 旧论断「引擎侧 Stop×prompt 是否真 fire 未取证」已取证:**真 fire**(Stop 触发时 prompt
33
+ * 载体调用一次,`$ARGUMENTS` 已替换为 payload JSON)、**能拦停**(模型输出
34
+ * `{"decision":"block","reason":…}` → `{block: reason}`,core `runner/runtask.js:1663`
35
+ * `(spec.hooks ?? deps.hooks)?.stop` 起 stop gate,整条链通)、非决策 JSON 不拦(与 command
36
+ * 形 exit-0 非 JSON 同语义)、`stop_hook_active:true` 如实进 payload(模型可自己收手;core
37
+ * 另有 STOP_HOOK_BLOCK_CAP=8 连续拦停硬上限兜底,不会无限拦停)。
38
+ * ③ 最坏情况**不是** 400,而是**钩子不生效**:部署没组装 prompt 载体(`ctx.hookLlm` 缺席)⇒
39
+ * server 记账 `hook_entries_skipped{reason:"llm_carrier_unavailable"}` 并跳过,**非阻断**。
40
+ * ⇒ 默认开。等件(不阻断本腿):registry-core hooks 契约建模 prompt 形([1717]① 在飞)、SDK
41
+ * `SettingsHookCommand` 放开 prompt 形 —— 到位后把下方 cast 换成直类型即可。
42
+ *
43
+ * 纪律:
44
+ * - 只放 goal 这一条(单值,不是通用会话钩子通道)——通用会话/插件钩子仍按 `hooksWireCaps.ts`
45
+ * 模块头所述不投影;
46
+ * - 走 `hooksForWire()` 的**同一套治理门**(trust / disableAllHooks / allowManagedHooksOnly):
47
+ * overlay 在门之后才合并,策略禁钩子时一并禁掉;
48
+ * - `/goal clear` 传 null 收回。
49
+ */
50
+ import { type EnvLike } from './hostEnv.js';
51
+ import type { WireHooksConfig } from './finalVerifyWire.js';
52
+ /** The env ESCAPE HATCH for the engine leg (see the block comment above). */
53
+ export declare const GOAL_STOP_HOOK_WIRE_ENV = "SEMA_GOAL_STOP_HOOK_WIRE";
54
+ export declare const CC_STOP_SEMANTICS_MIN_SERVER = "1.279.3";
55
+ /**
56
+ * 引擎自报版本是否达到 CC Stop 语义线({@link CC_STOP_SEMANTICS_MIN_SERVER})。
57
+ *
58
+ * 🔴 三条纪律:
59
+ * · **数值比较**,不是字典序(`1.9.0` < `1.279.0`,字符串比较会判反);
60
+ * · **预发版不算达标**(`1.279.0-rc.1` 里那套可能还在改)——正则只认三段纯数字,带 `-` 的直接落
61
+ * null ⇒ false;
62
+ * · 读不出/探不到 ⇒ **false**(降级方向恒安全:老形在新引擎上仍能拦,新形在老引擎上一次都拦不住)。
63
+ */
64
+ export declare function ccStopSemanticsFromVersion(version: string | undefined | null): boolean;
65
+ /**
66
+ * 本会话连着的引擎是否具备 CC Stop 语义?判据 = live 车道的 `/v1/capabilities` 自报 `version`
67
+ * (engineCapsCache 的 boot-kick 缓存,与 `/health.version` 同源)。
68
+ *
69
+ * 🔴 「未判即降级」:caps 探测是构造期 fire-and-forget,`/goal` 真被敲下时通常早已落袋;万一没落袋
70
+ * 就按老引擎走(meta 也同步退档)——**宁可少承诺,不可多承诺**。
71
+ */
72
+ export declare function engineCcStopSemantics(env?: EnvLike): boolean;
73
+ /**
74
+ * Is the `/goal` Stop-hook engine leg armed? **Default ON** since 2026-07-26 — both of the premises
75
+ * that kept it opt-in were disproved by server-side behavior evidence ([1721]) and re-verified against
76
+ * a real engine + real model (cli scripts/run-goal-stop-hook-live-test.mjs). Set
77
+ * `SEMA_GOAL_STOP_HOOK_WIRE` to an {@link envFlagOff} spelling(REF-CC-141 dup-02 单源)to disarm
78
+ * (an operator escape hatch, not a default).
79
+ */
80
+ export declare function isGoalStopHookWireArmed(env?: EnvLike): boolean;
81
+ /**
82
+ * Register (or clear, with `null`) the session-scoped `/goal` Stop prompt hook that rides
83
+ * `settings.hooks.Stop` to the engine. Single-valued: a new goal replaces the previous one,
84
+ * exactly like CC 220's `Xdr` (remove-then-add). Projection is gated by
85
+ * {@link isGoalStopHookWireArmed} — storing is always safe, sending is not (yet).
86
+ */
87
+ export declare function setWireSessionStopHook(prompt: string | null, opts?: {
88
+ /**
89
+ * 这条目标登记时,引擎是否已判定为 CC 语义档({@link engineCcStopSemantics})?
90
+ *
91
+ * 🔴 **为什么要在 set 那一刻锁存,而不是在 `hooksForWire()` 里现读**:`/goal` 的 meta
92
+ * (对模型讲「会不会拦到条件成立为止」)是在 set 那一刻发出去的,而投影发生在随后每个 turn。
93
+ * 两处各读一次的话,caps 探测中途落袋就会造出「meta 承诺拦到底、投出去的却还是老门」的错配 ——
94
+ * 判据必须锚在**真正决定结果的那个量**上,而它只有一个:**这条目标登记时的档位**。
95
+ * 缺省 false ⇒ 不传就是老引擎档:翻档只能靠证据打开,不能靠默认值。
96
+ */
97
+ ccSemantics?: boolean;
98
+ }): void;
99
+ /** The currently registered `/goal` Stop prompt, or null (registered ≠ projected — see the gate). */
100
+ export declare function getWireSessionStopHook(): string | null;
101
+ /** 当前这条目标登记时锁存的档位(真 = 投裸条件,假 = 投壳自造的门)。 */
102
+ export declare function getWireSessionStopHookCcSemantics(): boolean;
103
+ /**
104
+ * 把 `/goal` 的**原始条件**包成一条对引擎 prompt 载体**可执行**的 Stop 门 prompt。
105
+ *
106
+ * 🔴 适用面(2026-07-26 提货批后):**只用于未达标线的引擎档**(见 {@link CC_STOP_SEMANTICS_MIN_SERVER})。
107
+ * 达标档起引擎自己按 CC
108
+ * 220 逐字组装(系统提示 + 会话转录 + `wrapCondition`),那一档投的是**裸条件** —— 见
109
+ * {@link ccStopSemanticsFromVersion} 与 `hooksForWire()` 的分道(见 {@link goalStopHookMatcher})。
110
+ * 本函数保留,是 tolerate-absent 的实体:老引擎上退回它,仍能拦一次;删掉它等于在老引擎上静默失效。
111
+ *
112
+ * 🔴 为什么必须包(2026-07-26 live 围栏实测,不是设计偏好):CC 单进程的 prompt 钩子执行器
113
+ * (`src/utils/hooks/execPromptHook.ts`)会给条件**套一层系统提示**(「You are evaluating a hook…
114
+ * 回 `{ok:true}` / `{ok:false,reason}`」)**并把整段会话前置**,所以裸条件在 CC 那边能被真评估。
115
+ * 引擎侧的载体两样都没有:
116
+ * · 无系统提示、无会话历史;
117
+ * · Stop payload 的 `transcript_path` 是**空串**(server basePayload),评估者对本会话是**瞎的**;
118
+ * · 判据 schema 也不同 —— core 只认 stdout JSON 的 `decision:"block"`。
119
+ * 实测(钉版 server 1.277.1 + core 1.416 + 真模型):裸条件 `the project contains a file named
120
+ * DONE.txt` 送过去,载体照样被调用,模型回的是**散文**(「I don't have direct access to your file
121
+ * system…」)⇒ `parseHookStdout` 拿不到 decision ⇒ **一次都不拦**。即:不包 = 每次停机白烧一次
122
+ * 模型调用、零效果。
123
+ *
124
+ * 所以这里把条件包成引擎契约里**能落地**的形状。诚实标注它**够不到 CC 的完整语义**:
125
+ * · 能做到:每个 turn 的**第一次**停机尝试被拦下,目标原文重新注入给模型,逼它自查后再收尾
126
+ * (`stop_hook_active` 为真时门自己收手 ⇒ 不会无限拦停);真正「条件是否成立」的判断交给
127
+ * **有会话、有工具的主模型**,而不是让瞎的评估者假装判断;
128
+ * · 做不到:CC 那种「拦到条件真成立为止」(需要引擎把 transcript 交给载体)、以及「条件满足后
129
+ * 自动清除目标」。这两条是**等件**,meta 文案因此不照抄 CC `Pwo`(见 cmd-goal.tsx)。
130
+ *
131
+ * 台账仍存**原始条件**({@link getWireSessionStopHook}),所以 `/goal clear` 读回的是用户写的那句。
132
+ */
133
+ export declare function buildGoalStopHookPrompt(condition: string): string;
134
+ /**
135
+ * `hooksWireCaps.ts` 的 `hooksForWire()` 尾部合流点(REF-CC-156 拆分的接口):治理门跑完之后,
136
+ * 调用方传入 `managedOnly`(allowManagedHooksOnly 是否命中),本函数原样返回要 concat 进
137
+ * `merged['Stop']` 的那一条 matcher,或 `null`(未登记 / 被 managedOnly 挡 / overlay 未 armed)。
138
+ * 非导出到包外(不进 `index.ts` 的公开面)——只是本文件与 `hooksWireCaps.ts` 之间的私有接口。
139
+ */
140
+ export declare function goalStopHookMatcher(managedOnly: boolean): NonNullable<WireHooksConfig[string]>[number] | null;
141
+ /** 测试钩:清 `/goal` 锁存(壳里靠进程边界隔离;包内同进程多组断言必须能清)。 */
142
+ export declare function __resetGoalStopHookForTests(): void;
@@ -0,0 +1,258 @@
1
+ /**
2
+ * REF-CC-156(midband-04,split,2026-08-02):拆自 `hooksWireCaps.ts`——那个文件里约 220 行(占其
3
+ * 387 行的 57%)是 `/goal` 命令专属的 Stop-hook 版本判档 + prompt 组装 + 模块级单实例状态,与
4
+ * `hooksWireCaps.ts` 其余部分(通用 settings-file hooks 投影 + 治理门)是两个不同的关注点,原文件
5
+ * 只在 `hooksForWire()` 尾部合流一次。纯搬迁,行为不变:所有导出名字原样保留(`hooksWireCaps.ts`
6
+ * 显式 re-export,外部 import 路径零改动),模块级状态(`wireSessionStopHookPrompt`/
7
+ * `wireSessionStopHookCcSemantics`)随代码一起搬到本文件,`docs/refactor/p1-scan/singleton-manifest.json`
8
+ * 的两条登记同批把 `file` 改成本文件坐标。
9
+ *
10
+ * ── 以下为原文件的领域说明(逐字保留)────────────────────────────────────────────────────────
11
+ *
12
+ * SESSION-SCOPED Stop hook overlay(`/goal`,任务 #75 · C-7,2026-07-26)
13
+ * CC 220 的 `/goal <condition>` 会往 `sessionHooksRegistry` 注册一条会话级 Stop×prompt 钩子
14
+ * (`t.sessionHooksRegistry.add(sessionId,"Stop","",{type:"prompt",prompt})`),那才是「干到条件满足
15
+ * 再停」的机制半边。CC 是单进程,注册进壳内 registry 就够。
16
+ *
17
+ * 🔴 **sema 是双进程壳,壳侧那条钩子在 live 模式 fire 不到**(实证,不是推测):壳的 Stop 钩子只有
18
+ * `src/query/stopHooks.ts handleStopHooks` 一个执行点,调用方是 `src/query.ts` —— 壳自己的 query
19
+ * 循环;live 模式 turn 循环在引擎里跑,壳的 query.ts 不参与。upstreamBridge 只 fire SubagentStop 且
20
+ * observe-only。所以引擎腿必须走 `settings.hooks` 投影,这个 overlay 就是那条腿。
21
+ *
22
+ * 🟢 **本 overlay 默认开**(2026-07-26 翻默认;逃生口 `SEMA_GOAL_STOP_HOOK_WIRE=0/false/no/off`)。
23
+ *
24
+ * 修前默认关,理由是两条**前提**——两条都已被 server 侧行为取证**证伪**(黑板 [1721],6 条行为
25
+ * 用例已进 server 仓;cli 仓另有 scripts/run-goal-stop-hook-live-test.mjs 的真引擎+真模型复验):
26
+ * ① 旧论断「SDK 类型 DEFERRED ⇒ 服务端会 400」**是把类型面当成了运行时校验面**,对 server
27
+ * 不成立:`parseHooksConfig({Stop:[{hooks:[{type:"prompt",prompt}]}]})` 无 error、真解析;
28
+ * prompt 形在 server 的条目白名单里与 command/http/agent **并列**
29
+ * (@sema-agent/server hooks/hook-runner `entry.type !== "command" && … !== "prompt" && …`
30
+ * 那道 type_not_supported 闸口把 prompt 放行,1.277.1 dist 对应 hook-runner.js:390)。
31
+ * 卡着的只是 **SDK / registry-core 的类型**(见下方 cast),与 server 运行时接受与否两件事。
32
+ * ② 旧论断「引擎侧 Stop×prompt 是否真 fire 未取证」已取证:**真 fire**(Stop 触发时 prompt
33
+ * 载体调用一次,`$ARGUMENTS` 已替换为 payload JSON)、**能拦停**(模型输出
34
+ * `{"decision":"block","reason":…}` → `{block: reason}`,core `runner/runtask.js:1663`
35
+ * `(spec.hooks ?? deps.hooks)?.stop` 起 stop gate,整条链通)、非决策 JSON 不拦(与 command
36
+ * 形 exit-0 非 JSON 同语义)、`stop_hook_active:true` 如实进 payload(模型可自己收手;core
37
+ * 另有 STOP_HOOK_BLOCK_CAP=8 连续拦停硬上限兜底,不会无限拦停)。
38
+ * ③ 最坏情况**不是** 400,而是**钩子不生效**:部署没组装 prompt 载体(`ctx.hookLlm` 缺席)⇒
39
+ * server 记账 `hook_entries_skipped{reason:"llm_carrier_unavailable"}` 并跳过,**非阻断**。
40
+ * ⇒ 默认开。等件(不阻断本腿):registry-core hooks 契约建模 prompt 形([1717]① 在飞)、SDK
41
+ * `SettingsHookCommand` 放开 prompt 形 —— 到位后把下方 cast 换成直类型即可。
42
+ *
43
+ * 纪律:
44
+ * - 只放 goal 这一条(单值,不是通用会话钩子通道)——通用会话/插件钩子仍按 `hooksWireCaps.ts`
45
+ * 模块头所述不投影;
46
+ * - 走 `hooksForWire()` 的**同一套治理门**(trust / disableAllHooks / allowManagedHooksOnly):
47
+ * overlay 在门之后才合并,策略禁钩子时一并禁掉;
48
+ * - `/goal clear` 传 null 收回。
49
+ */
50
+ import { hostEnv } from './hostEnv.js';
51
+ import { envFlagOff } from './envFlag.js';
52
+ import { engineCapString } from './engineCapsCache.js';
53
+ let wireSessionStopHookPrompt = null;
54
+ /**
55
+ * CC 语义档的**锁存位**(见 {@link setWireSessionStopHook})。投影形按它分道:
56
+ * 真 ⇒ 裸条件(引擎自己包 CC 形);假 ⇒ 壳自造的门 prompt(老引擎唯一能落地的形)。
57
+ */
58
+ let wireSessionStopHookCcSemantics = false;
59
+ /** The env ESCAPE HATCH for the engine leg (see the block comment above). */
60
+ export const GOAL_STOP_HOOK_WIRE_ENV = 'SEMA_GOAL_STOP_HOOK_WIRE';
61
+ // ── 引擎档位:CC 220 Stop×prompt 完整语义(达标线 = CC_STOP_SEMANTICS_MIN_SERVER,见下)────────
62
+ //
63
+ // server 1.279.0 把 CC 220 的 prompt-hook 半场搬进了引擎(dist/hooks/cc-stop-prompt.js +
64
+ // branch-transcript.js,源码直读):
65
+ // ① Stop×prompt 触发时先取 `sctx.getBranch()`(core 1.419+ 就在 stop gate ctx 里传了它)渲染成
66
+ // 会话转录;**证据面为空则整条 prompt 条目被过滤掉、评估者根本不被调用**
67
+ // (`hook_entries_skipped{reason:"stop_prompt_no_evidence:<why>"}`)——这正是老引擎那条
68
+ // 「白烧一次模型调用、零效果」的反面;
69
+ // ② 调用时带 CC 220 **逐字**的系统提示(`ccPromptSystemFor("Stop")` = CC 的 full 档)+ CC 逐字的
70
+ // 条件包装(`wrapCondition`);
71
+ // ③ 判词 `{ok,reason,impossible}` 在**引擎翻译层**映射:ok ⇒ 放行、impossible ⇒ 记 warn 后**放行**
72
+ // (真 fail-open,既不 block 也不产 additionalContext)、其余 ⇒ block(reason 喂回模型)。
73
+ //
74
+ // ⇒ 到这一档,壳侧唯一正确的动作是把 CC `Xdr` 的**原形(裸条件)**投出去 —— 引擎会自己包。继续投
75
+ // 壳自造的门 prompt 只会被再包一层、并与 CC 系统提示要求的判词 schema 打架。
76
+ // 🔴 但老引擎**没有这套**(1.278.0 的 hooks/ 目录里既无 cc-stop-prompt 也无 branch-transcript,
77
+ // hook-llm 连 `system` 形参都没有):裸条件送过去评估者对本会话是瞎的,回散文 ⇒ 一次都不拦。
78
+ // 所以判档失败/探不到时**必须**退回老形,绝不「因为代码里写了 enforced 就真拦」。
79
+ //
80
+ // 🔴 **为什么达标线钉 1.279.3 而不是 CC 面首次出现的 1.279.0** —— 两道闸,缺一道都不许翻档:
81
+ //
82
+ // ① **core 下限**(1.279.1 才补上):CC Stop 语义在 1.279.0 就在了,但 1.279.1 才把
83
+ // `@sema-agent/core` 的下限提到 `^1.422.0`。core <1.422 上,一条只带 `additionalContext` 的
84
+ // Stop 反馈**不结束 + 不计数 + 清零计数** ⇒ block 与 additionalContext 交替时计数呈锯齿
85
+ // (`[0,1,0,1,0]`)、`STOP_HOOK_BLOCK_CAP` 永远到不了 ⇒ 无界循环;1.422 起它计入上限且不再清零。
86
+ // (注:goal 这条路本身产不出 additionalContext —— CC 判词的 ok/impossible 两支在引擎翻译层都
87
+ // 直接 `continue`;但那条通道对 command 形钩子是开着的,而**上限修好的是最坏值,不是这条通道的
88
+ // 性质**:它仍然会让那一轮不结束。所以措辞面一步都不许放松,见 cmd-goal 的 meta 四档。)
89
+ //
90
+ // ② **判词额度**(1.279.3 才补上):1.279.1/1.279.2 的引擎侧 hook 载体把 `max_tokens` 钳死在
91
+ // `Math.min(…, 1024)`(dist/hooks/hook-llm.js)。而 CC 的评估者系统提示要求「quote evidence
92
+ // from the transcript」,推理档模型的 thinking 还与输出**共用**这份额度 ⇒ 判词被截断(JSON 不
93
+ // 闭合)或干脆零 text 块 ⇒ `parseCcVerdict` 拿不到 ⇒ 引擎**丢弃整条判词并 fail-open 放行**。
94
+ // 🔴 这条缺口的形状与 ① 不同,但危害更隐蔽:它让 enforced 档对模型讲的那两句承诺在**守卫失效
95
+ // 的那些轮里变成空话,而且没有任何人知道**(引擎侧只留一行 `hook_llm_unparsed_verdict` /
96
+ // `hook_llm_failed{returned no content}` 记账,壳侧、用户侧、模型侧全都看不到)。
97
+ // 1.279.3 起 `maxOutputTokens` 由调用方声明,CC 评估者路声明 `CC_EVALUATOR_MAX_OUTPUT_TOKENS`
98
+ // = 4096,并对「无内容」重试一次(**不**重试「有内容但读不懂」)。
99
+ //
100
+ // ⇒ 达标线钉的是「**承诺能兑现**的那个组合」,不是「能力面在场」的那一版。差一档就翻,等于把用户
101
+ // 放到一个「守卫会静默失效」的搭配上,还对着模型把话说满。
102
+ export const CC_STOP_SEMANTICS_MIN_SERVER = '1.279.3';
103
+ /** `x.y.z` → 数值三元组;非法形 ⇒ null(预发标记按「未达标」处理,见下)。 */
104
+ function parseSemverCore(v) {
105
+ const m = /^(\d+)\.(\d+)\.(\d+)$/.exec(v.trim());
106
+ return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
107
+ }
108
+ /**
109
+ * 引擎自报版本是否达到 CC Stop 语义线({@link CC_STOP_SEMANTICS_MIN_SERVER})。
110
+ *
111
+ * 🔴 三条纪律:
112
+ * · **数值比较**,不是字典序(`1.9.0` < `1.279.0`,字符串比较会判反);
113
+ * · **预发版不算达标**(`1.279.0-rc.1` 里那套可能还在改)——正则只认三段纯数字,带 `-` 的直接落
114
+ * null ⇒ false;
115
+ * · 读不出/探不到 ⇒ **false**(降级方向恒安全:老形在新引擎上仍能拦,新形在老引擎上一次都拦不住)。
116
+ */
117
+ export function ccStopSemanticsFromVersion(version) {
118
+ if (typeof version !== 'string' || version.length === 0)
119
+ return false;
120
+ const got = parseSemverCore(version);
121
+ const min = parseSemverCore(CC_STOP_SEMANTICS_MIN_SERVER);
122
+ if (got === null || min === null)
123
+ return false;
124
+ const [gotMajor, gotMinor, gotPatch] = got;
125
+ const [minMajor, minMinor, minPatch] = min;
126
+ if (gotMajor !== minMajor)
127
+ return gotMajor > minMajor;
128
+ if (gotMinor !== minMinor)
129
+ return gotMinor > minMinor;
130
+ if (gotPatch !== minPatch)
131
+ return gotPatch > minPatch;
132
+ return true;
133
+ }
134
+ /**
135
+ * 本会话连着的引擎是否具备 CC Stop 语义?判据 = live 车道的 `/v1/capabilities` 自报 `version`
136
+ * (engineCapsCache 的 boot-kick 缓存,与 `/health.version` 同源)。
137
+ *
138
+ * 🔴 「未判即降级」:caps 探测是构造期 fire-and-forget,`/goal` 真被敲下时通常早已落袋;万一没落袋
139
+ * 就按老引擎走(meta 也同步退档)——**宁可少承诺,不可多承诺**。
140
+ */
141
+ export function engineCcStopSemantics(env = hostEnv()) {
142
+ const base = env.SEMA_LIVE_BASEURL;
143
+ if (!base)
144
+ return false;
145
+ return ccStopSemanticsFromVersion(engineCapString(base, 'version'));
146
+ }
147
+ /**
148
+ * Is the `/goal` Stop-hook engine leg armed? **Default ON** since 2026-07-26 — both of the premises
149
+ * that kept it opt-in were disproved by server-side behavior evidence ([1721]) and re-verified against
150
+ * a real engine + real model (cli scripts/run-goal-stop-hook-live-test.mjs). Set
151
+ * `SEMA_GOAL_STOP_HOOK_WIRE` to an {@link envFlagOff} spelling(REF-CC-141 dup-02 单源)to disarm
152
+ * (an operator escape hatch, not a default).
153
+ */
154
+ export function isGoalStopHookWireArmed(env = hostEnv()) {
155
+ return !envFlagOff(env[GOAL_STOP_HOOK_WIRE_ENV]);
156
+ }
157
+ /**
158
+ * Register (or clear, with `null`) the session-scoped `/goal` Stop prompt hook that rides
159
+ * `settings.hooks.Stop` to the engine. Single-valued: a new goal replaces the previous one,
160
+ * exactly like CC 220's `Xdr` (remove-then-add). Projection is gated by
161
+ * {@link isGoalStopHookWireArmed} — storing is always safe, sending is not (yet).
162
+ */
163
+ export function setWireSessionStopHook(prompt, opts) {
164
+ wireSessionStopHookPrompt = prompt && prompt.length > 0 ? prompt : null;
165
+ wireSessionStopHookCcSemantics = wireSessionStopHookPrompt === null ? false : opts?.ccSemantics === true;
166
+ }
167
+ /** The currently registered `/goal` Stop prompt, or null (registered ≠ projected — see the gate). */
168
+ export function getWireSessionStopHook() {
169
+ return wireSessionStopHookPrompt;
170
+ }
171
+ /** 当前这条目标登记时锁存的档位(真 = 投裸条件,假 = 投壳自造的门)。 */
172
+ export function getWireSessionStopHookCcSemantics() {
173
+ return wireSessionStopHookCcSemantics;
174
+ }
175
+ /**
176
+ * 把 `/goal` 的**原始条件**包成一条对引擎 prompt 载体**可执行**的 Stop 门 prompt。
177
+ *
178
+ * 🔴 适用面(2026-07-26 提货批后):**只用于未达标线的引擎档**(见 {@link CC_STOP_SEMANTICS_MIN_SERVER})。
179
+ * 达标档起引擎自己按 CC
180
+ * 220 逐字组装(系统提示 + 会话转录 + `wrapCondition`),那一档投的是**裸条件** —— 见
181
+ * {@link ccStopSemanticsFromVersion} 与 `hooksForWire()` 的分道(见 {@link goalStopHookMatcher})。
182
+ * 本函数保留,是 tolerate-absent 的实体:老引擎上退回它,仍能拦一次;删掉它等于在老引擎上静默失效。
183
+ *
184
+ * 🔴 为什么必须包(2026-07-26 live 围栏实测,不是设计偏好):CC 单进程的 prompt 钩子执行器
185
+ * (`src/utils/hooks/execPromptHook.ts`)会给条件**套一层系统提示**(「You are evaluating a hook…
186
+ * 回 `{ok:true}` / `{ok:false,reason}`」)**并把整段会话前置**,所以裸条件在 CC 那边能被真评估。
187
+ * 引擎侧的载体两样都没有:
188
+ * · 无系统提示、无会话历史;
189
+ * · Stop payload 的 `transcript_path` 是**空串**(server basePayload),评估者对本会话是**瞎的**;
190
+ * · 判据 schema 也不同 —— core 只认 stdout JSON 的 `decision:"block"`。
191
+ * 实测(钉版 server 1.277.1 + core 1.416 + 真模型):裸条件 `the project contains a file named
192
+ * DONE.txt` 送过去,载体照样被调用,模型回的是**散文**(「I don't have direct access to your file
193
+ * system…」)⇒ `parseHookStdout` 拿不到 decision ⇒ **一次都不拦**。即:不包 = 每次停机白烧一次
194
+ * 模型调用、零效果。
195
+ *
196
+ * 所以这里把条件包成引擎契约里**能落地**的形状。诚实标注它**够不到 CC 的完整语义**:
197
+ * · 能做到:每个 turn 的**第一次**停机尝试被拦下,目标原文重新注入给模型,逼它自查后再收尾
198
+ * (`stop_hook_active` 为真时门自己收手 ⇒ 不会无限拦停);真正「条件是否成立」的判断交给
199
+ * **有会话、有工具的主模型**,而不是让瞎的评估者假装判断;
200
+ * · 做不到:CC 那种「拦到条件真成立为止」(需要引擎把 transcript 交给载体)、以及「条件满足后
201
+ * 自动清除目标」。这两条是**等件**,meta 文案因此不照抄 CC `Pwo`(见 cmd-goal.tsx)。
202
+ *
203
+ * 台账仍存**原始条件**({@link getWireSessionStopHook}),所以 `/goal clear` 读回的是用户写的那句。
204
+ */
205
+ export function buildGoalStopHookPrompt(condition) {
206
+ const reason = `[session goal] Before you stop: verify that this goal actually holds — ${condition}. ` +
207
+ `If it does hold, say so plainly and finish. If it does not, keep working toward it and do not ` +
208
+ `pause to ask the user what to do.`;
209
+ return ('You are the Stop gate for an autonomous coding agent session. The user set this session goal, ' +
210
+ 'which the agent is supposed to satisfy before it stops:\n' +
211
+ `<goal>\n${condition}\n</goal>\n` +
212
+ 'Stop-hook input JSON: $ARGUMENTS\n' +
213
+ 'Reply with RAW JSON only — no prose, no explanation, no code fences.\n' +
214
+ 'If the input JSON has "stop_hook_active" set to true, this gate already fired once for this turn: ' +
215
+ 'reply with exactly {"continue":true} so the agent may stop.\n' +
216
+ 'Otherwise reply with exactly:\n' +
217
+ JSON.stringify({ decision: 'block', reason }));
218
+ }
219
+ /**
220
+ * `hooksWireCaps.ts` 的 `hooksForWire()` 尾部合流点(REF-CC-156 拆分的接口):治理门跑完之后,
221
+ * 调用方传入 `managedOnly`(allowManagedHooksOnly 是否命中),本函数原样返回要 concat 进
222
+ * `merged['Stop']` 的那一条 matcher,或 `null`(未登记 / 被 managedOnly 挡 / overlay 未 armed)。
223
+ * 非导出到包外(不进 `index.ts` 的公开面)——只是本文件与 `hooksWireCaps.ts` 之间的私有接口。
224
+ */
225
+ export function goalStopHookMatcher(managedOnly) {
226
+ if (wireSessionStopHookPrompt === null || managedOnly || !isGoalStopHookWireArmed())
227
+ return null;
228
+ // 🔴 这个双 cast 是**类型面的最小且诚实的局部处置**,不是「绕过校验」:
229
+ // · server **运行时接受** prompt 形(hook-runner 条目白名单与 command/http/agent 并列,
230
+ // [1721] 6 条行为用例 + cli 仓 live 围栏双证);
231
+ // · 卡着的是 **SDK 类型**:`SettingsHookCommand.type` 仍是字面量 `"command"`
232
+ // (@sema-agent/sdk dist/settings.d.ts,prompt 形 v1 标 DEFERRED);
233
+ // · **registry-core 建模 prompt 形在 [1717]① 等件中**,SDK 随后放开。
234
+ // 到位后这里换成直类型、去掉 cast(唯一改动点,调用面不动)。
235
+ return {
236
+ matcher: '',
237
+ // 🔴 投影形**按引擎档位分道**(锁存于 set 那一刻,见 setWireSessionStopHook):
238
+ // · CC 语义档(server ≥ CC_STOP_SEMANTICS_MIN_SERVER)⇒ **裸条件** —— 与 CC `Xdr` 注册的
239
+ // `{type:"prompt",prompt:condition}` 逐字同形;引擎自己套 CC 系统提示 + 会话转录 +
240
+ // `wrapCondition`,再包一层只会打架;
241
+ // · 老引擎档 ⇒ 包装后的可执行门 prompt(裸条件在老引擎上一次都拦不住 —— 见
242
+ // buildGoalStopHookPrompt 的实测说明)。
243
+ // 台账两档都存**原始条件**,供 `/goal clear` 读回。
244
+ hooks: [
245
+ {
246
+ type: 'prompt',
247
+ prompt: wireSessionStopHookCcSemantics
248
+ ? wireSessionStopHookPrompt
249
+ : buildGoalStopHookPrompt(wireSessionStopHookPrompt),
250
+ },
251
+ ],
252
+ };
253
+ }
254
+ /** 测试钩:清 `/goal` 锁存(壳里靠进程边界隔离;包内同进程多组断言必须能清)。 */
255
+ export function __resetGoalStopHookForTests() {
256
+ wireSessionStopHookPrompt = null;
257
+ wireSessionStopHookCcSemantics = false;
258
+ }
@@ -42,6 +42,16 @@ export type HeadlessPermissionResolution = {
42
42
  * argv 里的 `--permission-mode <v>` / `--permission-mode=<v>`(后者/后出现者赢,commander 同语义)与
43
43
  * `--dangerously-skip-permissions`(= bypassPermissions;`--permission-mode` 在场时让位)。
44
44
  * 返回 undefined = flag 车道未显式表态。
45
+ *
46
+ * REF-CC-143(dup-07)收编:`--permission-mode` 的取值改吃共用 lastFlagValue(argvFlagValue.ts,
47
+ * `onMissingValue:'latch-previous'`)——旧手写循环对「某一次 occurrence 缺值」的处置是跳过这一次、
48
+ * 锁存(latch)住之前已经取到的合法值,不是把整个旗塌缩成「从没写过」(`--permission-mode plan
49
+ * --permission-mode` 的旧行为是 `mode==='plan'`,不是 `undefined`)。共用叶的缺省策略
50
+ * (`'invalidate'`,sandboxWire/scenarioWire 用)会把这个情形塌缩成「有旗无值」,与本函数原有语义
51
+ * 不符,所以本调用点显式传 `'latch-previous'` 保住旧行为——这才是任务书说的「各调用方保持自己现有
52
+ * 对缺值的处置」(wave1 回炉修复,blocker:此前误用了共用叶的 invalidate 缺省,详见 commit 记账)。
53
+ * `--dangerously-skip-permissions` 的存在性判别不属于「取某旗最后值」这个不变量,单独一个小循环即
54
+ * 可,零重复。
45
55
  */
46
56
  export declare function permissionModeFromArgv(argv: readonly string[]): string | undefined;
47
57
  /**