@sema-agent/client-core 0.6.0 → 0.7.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.
package/dist/adapt.d.ts CHANGED
@@ -84,11 +84,13 @@ export declare const ADAPTER_COVERAGE: {
84
84
  readonly b3: readonly ["T38 三清第三清 + T57 第五处断闸:prompt_suggestions(turn 开场作废 + 子流断闸)", "T40 IDLE-FLUSH 1.5s 竞速(需 ctx.setTimer;缺席=不启用)", "T58 手动 iterator 的 finally cleanup(early break/throw 都要关上游生成器)", "T59 abort 半场(ctx.signal;中断后不产终答)", "T60 无 result 收尾腿(泄尾增量 → 清 liveness → 尾思考 → 尾文本段)"];
85
85
  /** B4(0.6.0)新落码的臂/半场 —— A 层收官批,同样别再在 todo 里留同名条目。 */
86
86
  readonly b4: readonly ["A 层第 14 臂 task_progress:workflow lane 三级门 + 判据④ belt 回植 + C1 alias + bindTaskCard(显式父/FIFO/含糊即 unbound)+ inline stats + T50 双形读 + SubagentStart + #6 resident 台账 + 面板 tick", "settlePanelTasks 三处 sweep 全接线(tool_end 显式关卡带报告 / result 防御 sweep / turn 末 + abort sweep)", "P0-2 tool label 侧信道(sanitizeToolUseBlock 摘 label + engineToolLabelStore 登记;0.5.0 真缺口,label 会随 tool_use block 落转录 → /compact 直发 provider 400)", "task_notification 的两件宿主义务收回库内自持:dropQueuedNotificationsForRun + clearEnginePanelTaskResident;stop hook 的 if-started 门由库解", "T8 flattenWireOutput 提为独立导出(A 层 settle 报告与 B 层卡体同一份)"];
87
+ /** B5(0.7.0)新落码的臂/半场 —— B/D/E 层收批,同样别再在 todo 里留同名条目。 */
88
+ readonly b5: readonly ["B 层 structuredToToolUseResult 14 case(ask-user-question/bash/edit/create/update/text/grep/glob/notebook-edit/cron-create/cron-delete/cron-list/agent/task-output)+ workflow-run 登记副作用", "D 层 wireOutputToBody 3 臂(bash / taskoutput / 泛化);structured 白名单在场时 T11+T14 正则退位,缺席保回落", "E 层 4 臂:#117a bg Bash 回执(判定在库→chrome bgshell_register)· TodoWrite oldTodos 富卡 · ReportFindings 按名认领 · Remember→memory_saved 系统消息", "tool_end_result → user tool_result 正常路径(卡本体);result 与 turn 末两处开卡兜底关闭(abort 不兜底,cli 同)", "task/task-list structured → chrome task_ledger_sync{source:\"structured\"}(判定归一在库、落盘留宿主)", "T7 诚实缺席(§8-4:不搬 mock 合成器,改 degraded 标记)· T20 diff hunks 进包(src/diff/patch.ts)"];
87
89
  /** 如实留白——写不动/依赖未搬运资产的臂,#52b 起分批接。 */
88
- readonly todo: readonly ["tool_end_result user tool_result 正常路径(需搬 wireOutputToBody/structuredToToolUseResult/mock 合成器 + TodoWrite/ReportFindings 富卡片,约 600 行;workflow 轮询信封的人话投影已在 workflow.ts 就位,等这条接线)= B5", "tool_end_result 的两个侧产物:Remember → memory_saved 系统消息、task/task-list structured 面板台账全量同步 = B5", "result/turn 末的开卡兜底关闭(同上,依赖合成器)= B5", "#117a bg Bash 回执探测(detectEngineBgShellReceipt 文案锚定探测器)= B5(E 层按工具名分臂)", "abort 半场里依赖开卡合成器的那件(兜底关卡)= B5;行 sweep 半场 B4 已接", "macrotask 让渡(cli 在提交前让出宏任务给 Ink;宿主渲染策略,不进库)"];
90
+ readonly todo: readonly ["T13 **D 层** 半场(taskoutput block content)未随 structured 退位:退位会改 block content 形状(cli 那里是裸对象 = 件1 隐患),属行为面改动,无真实需求驱动 DIVERGENCE-8", "macrotask 让渡(cli 在提交前让出宏任务给 Ink;宿主渲染策略,不进库)"];
89
91
  };
90
92
  /** 本批**已知的、有意的**行为分歧(差分守卫显式登记,绝不当等价) */
91
- export declare const ADAPTER_DIVERGENCES: readonly [string, string, string, string, string, string];
93
+ export declare const ADAPTER_DIVERGENCES: readonly [string, string, string, string, string, string, string, string, string];
92
94
  /** 有台账序列化位的适配器(seam 的 WireToCcAdapter 只定了 exportLedger——见 SEAM-GAP-1)。 */
93
95
  export interface WireToCcAdapterWithLedger extends WireToCcAdapter {
94
96
  exportLedger(): AdapterLedgerState & Record<string, unknown>;
package/dist/adapt.js CHANGED
@@ -1,10 +1,12 @@
1
1
  import { deriveTranscriptId } from './seam.js';
2
- import { dropQueuedNotificationsForRun, normalizeTaskNotification, renderTaskNotificationXml, taskNotificationDedupKey, } from './notifications.js';
2
+ import { dropQueuedNotificationsForRun, isEngineWorkflowNotified, isWorkflowCompletionCardEnqueued, listNotifiedRuns, listWorkflowCompletionCardsEnqueued, markEngineWorkflowNotified, normalizeTaskNotification, noteWorkflowCompletionCardEnqueued, renderTaskNotificationXml, taskNotificationDedupKey, } from './notifications.js';
3
3
  import { mapBrainStatusToRetry } from './retryStatus.js';
4
4
  import { steeringInjectedToAttachments } from './steering.js';
5
5
  import { projectDiagnosticsFrame } from './diagnostics.js';
6
6
  // ── B4:A 层搬入后适配器**自持**的四个本包台账(写者与读者同包 = R4 单实例纪律;
7
7
  // 这四件都没有 UI 订阅面,做成 chrome 臂只会凭空长出一条「宿主不实现就静默失效」的义务)。
8
+ // ── B5:B/D/E 层(工具结果卡)。纯投影 + 三个本包台账副作用,零宿主依赖。
9
+ import { degradedToolResultBody, detectEngineBgShellReceipt, reportFindingsToolUseResult, structuredToToolUseResult, todoWriteToolUseResult, wireOutputToBody, } from './toolResult.js';
8
10
  import { recordEngineToolLabel } from './engineToolLabelStore.js';
9
11
  import { registerSubagentAlias } from './subagentContentStore.js';
10
12
  import { isWorkflowAgentTaskId, recordWorkflowAgentTaskId } from './workflow.js';
@@ -212,13 +214,18 @@ export const ADAPTER_COVERAGE = {
212
214
  'task_notification 的两件宿主义务收回库内自持:dropQueuedNotificationsForRun + clearEnginePanelTaskResident;stop hook 的 if-started 门由库解',
213
215
  'T8 flattenWireOutput 提为独立导出(A 层 settle 报告与 B 层卡体同一份)',
214
216
  ],
217
+ /** B5(0.7.0)新落码的臂/半场 —— B/D/E 层收批,同样别再在 todo 里留同名条目。 */
218
+ b5: [
219
+ 'B 层 structuredToToolUseResult 14 case(ask-user-question/bash/edit/create/update/text/grep/glob/notebook-edit/cron-create/cron-delete/cron-list/agent/task-output)+ workflow-run 登记副作用',
220
+ 'D 层 wireOutputToBody 3 臂(bash / taskoutput / 泛化);structured 白名单在场时 T11+T14 正则退位,缺席保回落',
221
+ 'E 层 4 臂:#117a bg Bash 回执(判定在库→chrome bgshell_register)· TodoWrite oldTodos 富卡 · ReportFindings 按名认领 · Remember→memory_saved 系统消息',
222
+ 'tool_end_result → user tool_result 正常路径(卡本体);result 与 turn 末两处开卡兜底关闭(abort 不兜底,cli 同)',
223
+ 'task/task-list structured → chrome task_ledger_sync{source:"structured"}(判定归一在库、落盘留宿主)',
224
+ 'T7 诚实缺席(§8-4:不搬 mock 合成器,改 degraded 标记)· T20 diff hunks 进包(src/diff/patch.ts)',
225
+ ],
215
226
  /** 如实留白——写不动/依赖未搬运资产的臂,#52b 起分批接。 */
216
227
  todo: [
217
- 'tool_end_result user tool_result 正常路径(需搬 wireOutputToBody/structuredToToolUseResult/mock 合成器 + TodoWrite/ReportFindings 富卡片,约 600 行;workflow 轮询信封的人话投影已在 workflow.ts 就位,等这条接线)= B5',
218
- 'tool_end_result 的两个侧产物:Remember → memory_saved 系统消息、task/task-list structured → 面板台账全量同步 = B5',
219
- 'result/turn 末的开卡兜底关闭(同上,依赖合成器)= B5',
220
- '#117a bg Bash 回执探测(detectEngineBgShellReceipt 文案锚定探测器)= B5(E 层按工具名分臂)',
221
- 'abort 半场里依赖开卡合成器的那件(兜底关卡)= B5;行 sweep 半场 B4 已接',
228
+ 'T13 **D 层** 半场(taskoutput block content)未随 structured 退位:退位会改 block content 形状(cli 那里是裸对象 = 件1 隐患),属行为面改动,无真实需求驱动 ⇒ 见 DIVERGENCE-8',
222
229
  'macrotask 让渡(cli 在提交前让出宏任务给 Ink;宿主渲染策略,不进库)',
223
230
  ],
224
231
  };
@@ -241,7 +248,23 @@ export const ADAPTER_DIVERGENCES = [
241
248
  'DIVERGENCE-6(B4):`subagent_lifecycle` 的**去重与 if-started 门在库里解**,cli 在 module 级 Set 上解。' +
242
249
  '可观测后果相同(同一 taskId 的 Start 恒一次;没 Start 过的 taskId 收不到通知臂的 Stop),' +
243
250
  '差在「谁持台账」:cli=进程级、本包=适配器实例级(= 一个 session)。多 session 宿主(web 一页两会话)' +
244
- '上本包语义**更正确**——cli 的 module 形会让两个会话互吞对方的 Start。',
251
+ '上本包语义**更正确**——cli 的 module 形会让两个会话互吞对方的 Start。' +
252
+ '⚠️ B5 更正范围:同批把 `notifiedRuns`/`cardEnqueuedRuns` **折回 module 台账**(它们与队列 port/' +
253
+ 'idle watcher/outstanding 药丸配对,per-session 会造成前提不共享);本条只剩 ' +
254
+ '`firedSubagentStartHookTaskIds` 与 `renderedNotifications` 两项仍是实例级。',
255
+ 'DIVERGENCE-7(B5,§8-4 裁定的**有意**分歧):`tool_end.output === undefined` 时 —— cli 用 mock ' +
256
+ 'toolIO 注册表**反造**结果体(卡片显示「看起来像真的、但不是这次执行产物」的内容);本包**诚实缺席**:' +
257
+ '`content: \'\'` + 消息上打 `_sema_degraded: \'wire_carried_no_output\'`,显示决策留端。' +
258
+ '依据=宪法「诚实优先于产出」+ 多端复制会各自编不同的假话。差分守卫按「哨兵成对替换 + 独立 oracle」' +
259
+ '比对(标记集必须恰等于「有 tool_end 但没带 output 的开卡集」,少标/多标两向都红)。',
260
+ 'DIVERGENCE-8(B5):**structured 在场时正则反解退位**(T11/T14;[1840]§一 白名单 + 设计稿 R9②)。' +
261
+ '同一帧上 cli 从模型面框架文本反解 {stdout,stderr,exitCode},本包直接读 `structured.bash.*`。' +
262
+ '框架**自洽**时两者逐字节相等(差分守卫 6 条 bash 双路 fixture 实证);不自洽时本包跟 structured ' +
263
+ '(框架里的尾换行等是渲染副产物)。T13/T24 的 **B 层**半场同款惰性退位(structured 带齐 ' +
264
+ 'status+content 就根本不解模型面文本);T13 的 **D 层** block-content 半场本批**未**退位 —— ' +
265
+ '那一步会改 block content 形状(cli 在这里是裸对象,件1 隐患),无真实需求驱动,如实留白。',
266
+ 'DIVERGENCE-9(B5):`tool_result` 消息的 uuid —— cli 在缺 `eventId` 时铸随机 v4,本包派生 ' +
267
+ '`wtc_<toolUseId>`(DIVERGENCE-2 同族:同流重放 ⇒ 同 id 序列,[1653])。守卫按别名结构比,不比字面。',
245
268
  ];
246
269
  const TASK_NOTIFICATION_TAG = 'task-notification';
247
270
  const TASK_ID_TAG = 'task-id';
@@ -252,8 +275,10 @@ const STATUS_TAG = 'status';
252
275
  */
253
276
  class WireToCcAdapterImpl {
254
277
  renderedNotifications = new Set();
255
- notifiedRuns = new Set();
256
- cardEnqueuedRuns = new Set();
278
+ // 🔴 B5 ①:`notifiedRuns` / `cardEnqueuedRuns` **不在实例上** —— 它们的配对半场(清账的
279
+ // dropQueuedNotificationsForRun、记账的两个 enqueue、idle watcher)全在 notifications.ts 的
280
+ // module 台账上,放实例 = 前提不共享 = 双卡/零显示/模型二收(0.6.0 真缺口,[1857])。
281
+ // 适配器一律经 notifications.ts 的四个口读写,别在这里再长一份影子副本。
257
282
  /** W1 三级判据素材:本壳见过的 Workflow 工具 tool_use id(module 级台账的实例化)。 */
258
283
  seenWorkflowToolCallIds = new Set();
259
284
  /**
@@ -268,8 +293,9 @@ class WireToCcAdapterImpl {
268
293
  return {
269
294
  version: 1,
270
295
  renderedNotifications: [...this.renderedNotifications],
271
- notifiedRuns: [...this.notifiedRuns],
272
- cardEnqueuedRuns: [...this.cardEnqueuedRuns],
296
+ // B5 ①:这两项是 module 台账的快照(不是实例私有),跨实例一致。
297
+ notifiedRuns: listNotifiedRuns(),
298
+ cardEnqueuedRuns: listWorkflowCompletionCardsEnqueued(),
273
299
  };
274
300
  }
275
301
  importLedger(state) {
@@ -284,10 +310,21 @@ class WireToCcAdapterImpl {
284
310
  }
285
311
  }
286
312
  };
313
+ /** B5 ①:module 台账那两项经真源写口回植(has 判重 = 与 take 同样只数新增)。 */
314
+ const takeModule = (v, has, add) => {
315
+ if (!Array.isArray(v))
316
+ return;
317
+ for (const k of v) {
318
+ if (typeof k === 'string' && k.length > 0 && !has(k)) {
319
+ add(k);
320
+ n++;
321
+ }
322
+ }
323
+ };
287
324
  const s = state;
288
325
  take(s.renderedNotifications, this.renderedNotifications);
289
- take(s.notifiedRuns, this.notifiedRuns);
290
- take(s.cardEnqueuedRuns, this.cardEnqueuedRuns);
326
+ takeModule(s.notifiedRuns, isEngineWorkflowNotified, markEngineWorkflowNotified);
327
+ takeModule(s.cardEnqueuedRuns, isWorkflowCompletionCardEnqueued, noteWorkflowCompletionCardEnqueued);
291
328
  return n;
292
329
  }
293
330
  /**
@@ -336,7 +373,9 @@ class WireToCcAdapterImpl {
336
373
  const external = /\btype="external"/.test(attrs);
337
374
  this.renderedNotifications.add(`${external ? 'external:' : ''}${taskId}:${status}`);
338
375
  if (!external && (status === 'completed' || status === 'failed' || status === 'killed')) {
339
- this.notifiedRuns.add(taskId);
376
+ // cli seedEngineTaskNotificationDedupFromTranscript 同位:回植走 module 台账
377
+ // (outstanding 药丸 / idle watcher 收摊那半场读的就是它)。
378
+ markEngineWorkflowNotified(taskId);
340
379
  }
341
380
  seeded++;
342
381
  }
@@ -345,7 +384,9 @@ class WireToCcAdapterImpl {
345
384
  return seeded;
346
385
  }
347
386
  noteCardEnqueued(runId) {
348
- this.cardEnqueuedRuns.add(runId);
387
+ // B5 ①:写 module 台账(与 dropQueuedNotificationsForRun 的清账半场共享前提)
388
+ // 壳 B4 的队列端口回钩仍可调它 —— 三条入队链本来就写同一个集合,回钩因此变成幂等 no-op。
389
+ noteWorkflowCompletionCardEnqueued(runId);
349
390
  }
350
391
  async *adapt(frames, ctx) {
351
392
  // 下面几个 `function*` 表达式(非箭头 —— generator 不能写成箭头)要访问实例台账,
@@ -524,6 +565,79 @@ class WireToCcAdapterImpl {
524
565
  });
525
566
  }
526
567
  };
568
+ // ══ B5 —— B/D/E 层:开卡 → `tool_result` 转录消息 ═══════════════════════════════════════
569
+ /**
570
+ * TodoWrite 划删线 diff 的 `oldTodos`。
571
+ * 🔴 **turn 级**(cli 同:`let lastLiveTodos` 声明在 generator 体内,不是 module 级 ——
572
+ * cli 的注释写着「module-level, per-process」但代码不是,以**代码**为准,差分守卫认代码)。
573
+ */
574
+ let lastLiveTodos = [];
575
+ /**
576
+ * 一张开卡的收口(cli `buildToolResult` + 它内联的 #117a 桥)。产出顺序与 cli 的
577
+ * 「副作用先于 yield」一致:先 chrome(bg shell 建行),再 transcript(tool_result 卡)。
578
+ */
579
+ const buildToolResult = function* (p, end) {
580
+ const isError = end?.isError ?? false;
581
+ // D 层 / T7:wire 带正文 ⇒ 真身投影;没带 ⇒ **诚实缺席**(§8-4,不再 mock 反造)。
582
+ const body = end && end.output !== undefined
583
+ ? wireOutputToBody(p.name, end.output, isError, end.truncated === true, flattenWireOutput, end.structured)
584
+ : degradedToolResultBody(isError);
585
+ // ── E 层 ③:#117a 模型发起的后台 Bash 回执 → ctrl+B 面板行(判定在库、执行留宿主)。
586
+ if (p.name === 'Bash' && !isError && end?.output !== undefined) {
587
+ const reg = detectEngineBgShellReceipt(p.rawInput, flattenWireOutput(end.output));
588
+ if (reg) {
589
+ yield chrome({ kind: 'bgshell_register', laneProof: MAIN, taskId: reg.taskId, registration: reg });
590
+ }
591
+ }
592
+ // B 层:structured → 逐工具 typed toolUseResult(卡的 outputSchema safeParse 过才渲富卡)。
593
+ let rich = end?.structured !== undefined
594
+ ? structuredToToolUseResult(end.structured, end.output !== undefined ? flattenWireOutput(end.output) : undefined)
595
+ : null;
596
+ // E 层 ④/⑤:两条按**工具名**的补位(引擎不发 type 判别位,B 层 switch 认领不了)。
597
+ if (rich === null && p.name === 'TodoWrite') {
598
+ const todo = todoWriteToolUseResult(p.rawInput, lastLiveTodos, isError);
599
+ if (todo !== null) {
600
+ rich = { toolUseResult: todo.toolUseResult, isError: todo.isError };
601
+ lastLiveTodos = todo.newTodos;
602
+ }
603
+ }
604
+ if (rich === null && !isError && p.name === 'ReportFindings') {
605
+ rich = reportFindingsToolUseResult(end?.structured, p.rawInput);
606
+ }
607
+ const eventId = end?.eventId ?? p.eventId;
608
+ const parentToolCallId = end?.parentToolCallId ?? p.parentToolCallId;
609
+ yield transcript({
610
+ type: 'user',
611
+ message: {
612
+ role: 'user',
613
+ content: [
614
+ {
615
+ type: 'tool_result',
616
+ tool_use_id: p.id,
617
+ content: body.content,
618
+ is_error: rich?.isError === true || body.isError,
619
+ },
620
+ ],
621
+ },
622
+ // DIVERGENCE-2:cli 在缺 eventId 时铸随机 v4,本包一律**稳定键派生**
623
+ // (`wtc_<toolUseId>` —— 一张卡一个结果,天然唯一;同流重放 ⇒ 同 id 序列,[1653])。
624
+ uuid: eventId ?? deriveTranscriptId({ toolCallId: p.id }, ctx),
625
+ session_id: ctx.sessionId,
626
+ ...(parentToolCallId !== undefined ? { parent_tool_use_id: parentToolCallId } : {}),
627
+ // CC 结果卡读 sidecar 上的 typed 输出(不是 block content)。
628
+ toolUseResult: rich !== null ? rich.toolUseResult : (body.toolUseResult ?? body.content),
629
+ // T7:wire 没带正文的诚实标记 —— 在场 = 这张卡没有真实执行结果(UI 显示决策留端)。
630
+ ...(body.degraded !== undefined ? { _sema_degraded: body.degraded } : {}),
631
+ });
632
+ };
633
+ /** 关掉所有还开着的卡(流被截断 / 无 result 收尾;cli 两处同形,abort 路径**不做**)。 */
634
+ const sweepOpenCards = function* () {
635
+ for (const [id, p] of pending) {
636
+ pending.delete(id);
637
+ assistantResponseId = null; // S1
638
+ yield* buildToolResult(p);
639
+ }
640
+ };
527
641
  /** 提交累积的思考块(cli takeThinking:committed 形 + 关活体块 + 收活动行)。 */
528
642
  function* takeThinking() {
529
643
  if (thinking.length === 0)
@@ -794,15 +908,60 @@ class WireToCcAdapterImpl {
794
908
  // B4 起本臂做三件:① P0-2 label 补位(只有 tool_end 带 label 的场景;台账自身幂等不覆盖)
795
909
  // ② MF-10 **关卡 settle**(子代从不发终态 tick,父卡的 tool_end 就是完成信号;卡的 output
796
910
  // 文本 = 子代最终报告,串到行 end 事件上)③ 卡台账出栈 + response-id 复位(S1)。
797
- // tool_result 消息本体(B 层结构化卡)仍见 ADAPTER_COVERAGE.todo B5。
911
+ // 🆕 B5:④ **B/D/E 层** —— `tool_result` 卡本体 + 三个侧产物
912
+ // (Remember→memory_saved / task 族 structured 台账 / #117a bg Bash 桥)。
798
913
  const callId = typeof m.toolCallId === 'string' ? m.toolCallId : undefined;
799
914
  recordEngineToolLabel(callId, m.label);
800
915
  if (callId !== undefined) {
801
916
  yield* settlePanelTasks(callId, m.isError === true, m.output !== undefined ? flattenWireOutput(m.output) : undefined);
802
917
  }
803
- if (callId !== undefined && pending.has(callId)) {
804
- pending.delete(callId);
805
- assistantResponseId = null;
918
+ const p = callId !== undefined ? pending.get(callId) : undefined;
919
+ // 无对应开卡的 tool_end(乱序 / 重复)直接丢(cli 同)。
920
+ if (p === undefined || callId === undefined)
921
+ break;
922
+ pending.delete(callId);
923
+ // S1:结果关闭该 model response —— **yield 之前**复位(生成器会在 yield 上挂起)。
924
+ assistantResponseId = null;
925
+ const structured = m.structured;
926
+ yield* buildToolResult(p, {
927
+ output: m.output,
928
+ structured,
929
+ truncated: m.truncated === true,
930
+ isError: m.isError === true,
931
+ eventId: typeof m.eventId === 'string' ? m.eventId : undefined,
932
+ parentToolCallId: typeof m.parentToolCallId === 'string' ? m.parentToolCallId : undefined,
933
+ });
934
+ const s = structured !== undefined && structured !== null && typeof structured === 'object'
935
+ ? structured
936
+ : undefined;
937
+ // ── E 层 ①:CC198 gap #2 —— 引擎 Remember 工具 → CC198 automemory「Saved N memories」卡
938
+ // (system{subtype:'memory_saved'})。引擎不发写入路径,note 本身就是那一条 writtenPaths。
939
+ // 上面的 tool_result 照旧走泛化兜底卡(双覆盖);structuredToToolUseResult 的白名单不动
940
+ // (Remember 从不进那个 switch)。core 1.217 形 {type:'memory-saved', ok, note, …}。
941
+ if (p.name === 'Remember' && m.isError !== true) {
942
+ if (s && s.ok === true && typeof s.note === 'string' && s.note.length > 0) {
943
+ yield transcript({
944
+ type: 'system',
945
+ subtype: 'memory_saved',
946
+ writtenPaths: [s.note],
947
+ // cli 用 `new Date().toISOString()` + randomUUID();本包一律走 ctx(可重放)。
948
+ // 见 DIVERGENCE-7。
949
+ timestamp: new Date(ctx.now()).toISOString(),
950
+ uuid: idOf(m, 'memory-saved'),
951
+ isMeta: false,
952
+ });
953
+ }
954
+ }
955
+ // ── E 层 ②:core 1.217 task 族 structured —— 权威全量态精确同步 ctrl+t 面板(引擎真实 id)。
956
+ // 落盘是宿主职责(T44 拆缝:判定+归一进库、落盘留宿主)。
957
+ if (s && (s.type === 'task' || s.type === 'task-list')) {
958
+ yield chrome({
959
+ kind: 'task_ledger_sync',
960
+ laneProof: MAIN,
961
+ source: 'structured',
962
+ structuredType: s.type,
963
+ structured: s,
964
+ });
806
965
  }
807
966
  break;
808
967
  }
@@ -968,6 +1127,8 @@ class WireToCcAdapterImpl {
968
1127
  // MF-10 防御 sweep(sweep 点 1/3):结掉那些卡从没关过的行(流被截断)。
969
1128
  // cli 同位:setRetryStatus(null) 之后、开卡兜底关闭之前。
970
1129
  yield* settlePanelTasks(null, false);
1130
+ // B5:开卡兜底关闭(cli 同位 —— 在终答文本**之前**,卡才不会挂着空白)。
1131
+ yield* sweepOpenCards();
971
1132
  yield* takeThinking();
972
1133
  if (!endEmitted) {
973
1134
  const u = m.usage;
@@ -1094,13 +1255,15 @@ class WireToCcAdapterImpl {
1094
1255
  }
1095
1256
  }
1096
1257
  // 双通道反向去重:probe 合成的完成卡已入队 ⇒ 后到的推送帧只做上面的记账,不渲第二条。
1097
- if (n.task_type !== 'external' && this.cardEnqueuedRuns.has(taskId))
1258
+ // 🔴 B5 ①:读的是 **module 台账**(cli `isWorkflowCompletionCardEnqueued` 逐字)——
1259
+ // 记账方(两个 enqueue + idle watcher)与清账方(上面的 drop)都在那儿。
1260
+ if (n.task_type !== 'external' && isWorkflowCompletionCardEnqueued(taskId))
1098
1261
  break;
1099
1262
  // 跨通道记账(cli markEngineWorkflowNotified 同位、同条件:非 external,不看终态):
1100
1263
  // 这帧到达 = 引擎已 server-side steer-inject 过,补发通道(workflow_complete)不得再注入。
1101
1264
  // external 豁免([769] 对抗复审收紧:external 从不经补发通道,mark 只会缴械 probe 兜底)。
1102
1265
  if (n.task_type !== 'external')
1103
- this.notifiedRuns.add(taskId);
1266
+ markEngineWorkflowNotified(taskId);
1104
1267
  yield transcript({
1105
1268
  type: 'user',
1106
1269
  message: { role: 'user', content: [{ type: 'text', text: renderTaskNotificationXml(fields) }] },
@@ -1114,10 +1277,12 @@ class WireToCcAdapterImpl {
1114
1277
  if (runId === undefined)
1115
1278
  break;
1116
1279
  // 跨通道去重(cli enqueueEngineWorkflowNotification 首行):该 run 已由推送帧记账过
1117
- // (引擎 server-side 注入过)⇒ Path B 绝不再喂模型一遍。适配器自持,不摊给宿主。
1118
- if (this.notifiedRuns.has(runId))
1280
+ // (引擎 server-side 注入过)⇒ Path B 绝不再喂模型一遍。
1281
+ // 🔴 B5 ①:门与记账都在 module 台账 —— cli 里这一整段就是 enqueueEngineWorkflowNotification
1282
+ // 的前三行,probe/idle watcher 合成的那两条链写的也是它。
1283
+ if (isEngineWorkflowNotified(runId))
1119
1284
  break;
1120
- this.notifiedRuns.add(runId);
1285
+ markEngineWorkflowNotified(runId);
1121
1286
  const wfStatus = m.status === 'failed' ? 'failed' : 'completed';
1122
1287
  const wfSummary = typeof m.summary === 'string' ? m.summary : '';
1123
1288
  yield chrome({
@@ -1129,7 +1294,7 @@ class WireToCcAdapterImpl {
1129
1294
  // 模型面正文照铸(宿主直接入队即可,不必各自拼 XML;与 core 的 tag/段序同形)
1130
1295
  message: renderTaskNotificationXml({ taskId: runId, status: wfStatus, summary: wfSummary }),
1131
1296
  });
1132
- this.cardEnqueuedRuns.add(runId);
1297
+ noteWorkflowCompletionCardEnqueued(runId);
1133
1298
  break;
1134
1299
  }
1135
1300
  case 'diagnostics': {
@@ -1245,6 +1410,8 @@ class WireToCcAdapterImpl {
1245
1410
  yield chrome({ kind: 'retry_status', laneProof: MAIN, status: null });
1246
1411
  // 复审修 #2a 孪生:没有 `result` 臂的收尾路径同样 sweep(sweep 点 3/3)。
1247
1412
  yield* settlePanelTasks(null, false);
1413
+ // B5:无 `result` 收尾腿同样关掉还开着的卡(cli 同位;abort 路径**不做**,见上)。
1414
+ yield* sweepOpenCards();
1248
1415
  yield* takeThinking();
1249
1416
  yield* takeAnswerSegment();
1250
1417
  ctx.log?.('debug', `turn done · emittedAssistantText=${emittedAssistantText} answerLen=${answer.length}`);
@@ -0,0 +1,29 @@
1
+ /**
2
+ * src/diff/patch.ts — T20:**diff hunks 在客户端算**(core 已裁定归客户端,零上游依赖)。
3
+ *
4
+ * 为什么必须进本包:core 明确「引擎发 originalFile(客户端永远重建不出来的那一半),hunks 由客户端
5
+ * 算」—— 那么每个端(TUI/web/桌面)各算一遍就是三份实现三种 hunk;进包之后是**一份**。
6
+ * 搬自 cli `src/utils/diff.ts:81 getPatchFromContents` + `src/utils/file.ts:137
7
+ * convertLeadingTabsToSpaces`,**逐字**(含 & / $ 转义 token 与 CONTEXT_LINES/TIMEOUT 常量),
8
+ * 只丢掉宿主侧的 analytics / LOC 计数副作用(那两条在 cli 里也不在本函数内)。
9
+ *
10
+ * 🔴 唯一的运行时依赖 `diff`(jsdiff 9,零传递依赖、纯 JS、浏览器可用)—— 本包第一个 runtime dep。
11
+ * 判据:它不是「顺手拉进来的 hub」,而是 T20 这条能力**本身**;不引它就只能把能力退回各端。
12
+ * portability 门的 ALLOWED_PACKAGES 因此显式登记 `diff` 一条(登记 = 可审计,不是放行一切)。
13
+ */
14
+ import { type StructuredPatchHunk } from 'diff';
15
+ export declare const CONTEXT_LINES = 3;
16
+ export declare const DIFF_TIMEOUT_MS = 5000;
17
+ /**
18
+ * 渲染前把**行首** tab 折成两空格 —— CC 自己的 Edit 卡在 structuredPatch 之前做同一件事
19
+ * (FileEditTool/utils.ts),不做的话 tab 缩进文件两边算出来的 hunk 对不上。
20
+ */
21
+ export declare function convertLeadingTabsToSpaces(content: string): string;
22
+ export declare function getPatchFromContents({ filePath, oldContent, newContent, ignoreWhitespace, singleHunk, }: {
23
+ filePath: string;
24
+ oldContent: string;
25
+ newContent: string;
26
+ ignoreWhitespace?: boolean;
27
+ singleHunk?: boolean;
28
+ }): StructuredPatchHunk[];
29
+ export type { StructuredPatchHunk };
@@ -0,0 +1,45 @@
1
+ /**
2
+ * src/diff/patch.ts — T20:**diff hunks 在客户端算**(core 已裁定归客户端,零上游依赖)。
3
+ *
4
+ * 为什么必须进本包:core 明确「引擎发 originalFile(客户端永远重建不出来的那一半),hunks 由客户端
5
+ * 算」—— 那么每个端(TUI/web/桌面)各算一遍就是三份实现三种 hunk;进包之后是**一份**。
6
+ * 搬自 cli `src/utils/diff.ts:81 getPatchFromContents` + `src/utils/file.ts:137
7
+ * convertLeadingTabsToSpaces`,**逐字**(含 & / $ 转义 token 与 CONTEXT_LINES/TIMEOUT 常量),
8
+ * 只丢掉宿主侧的 analytics / LOC 计数副作用(那两条在 cli 里也不在本函数内)。
9
+ *
10
+ * 🔴 唯一的运行时依赖 `diff`(jsdiff 9,零传递依赖、纯 JS、浏览器可用)—— 本包第一个 runtime dep。
11
+ * 判据:它不是「顺手拉进来的 hub」,而是 T20 这条能力**本身**;不引它就只能把能力退回各端。
12
+ * portability 门的 ALLOWED_PACKAGES 因此显式登记 `diff` 一条(登记 = 可审计,不是放行一切)。
13
+ */
14
+ import { structuredPatch } from 'diff';
15
+ export const CONTEXT_LINES = 3;
16
+ export const DIFF_TIMEOUT_MS = 5_000;
17
+ // cli 原注:`&` 会让 diff 库犯迷糊,先换成 token、算完再换回来。`$` 同理(替换模式转义位)。
18
+ const AMPERSAND_TOKEN = '<<:AMPERSAND_TOKEN:>>';
19
+ const DOLLAR_TOKEN = '<<:DOLLAR_TOKEN:>>';
20
+ function escapeForDiff(s) {
21
+ return s.replaceAll('&', AMPERSAND_TOKEN).replaceAll('$', DOLLAR_TOKEN);
22
+ }
23
+ function unescapeFromDiff(s) {
24
+ return s.replaceAll(AMPERSAND_TOKEN, '&').replaceAll(DOLLAR_TOKEN, '$');
25
+ }
26
+ /**
27
+ * 渲染前把**行首** tab 折成两空格 —— CC 自己的 Edit 卡在 structuredPatch 之前做同一件事
28
+ * (FileEditTool/utils.ts),不做的话 tab 缩进文件两边算出来的 hunk 对不上。
29
+ */
30
+ export function convertLeadingTabsToSpaces(content) {
31
+ // 无 tab 的常见情形直接短路(/gm 正则会逐行扫)。
32
+ if (!content.includes('\t'))
33
+ return content;
34
+ return content.replace(/^\t+/gm, _ => ' '.repeat(_.length));
35
+ }
36
+ export function getPatchFromContents({ filePath, oldContent, newContent, ignoreWhitespace = false, singleHunk = false, }) {
37
+ const result = structuredPatch(filePath, filePath, escapeForDiff(oldContent), escapeForDiff(newContent), undefined, undefined, {
38
+ ignoreWhitespace,
39
+ context: singleHunk ? 100_000 : CONTEXT_LINES,
40
+ timeout: DIFF_TIMEOUT_MS,
41
+ });
42
+ if (!result)
43
+ return [];
44
+ return result.hunks.map(_ => ({ ..._, lines: _.lines.map(unescapeFromDiff) }));
45
+ }
@@ -0,0 +1,7 @@
1
+ /** 当前会话的 session 参数值;undefined = 两源都缺席(动态 import 未落地的头几毫秒)——
2
+ * 调用方此时不带参(legacy 面软拍放行;强制面 404 自回落,下次重试自然带上)。 */
3
+ export declare function engineSessionParam(): string | undefined;
4
+ /** spread 便捷形:`{ ...engineSessionParamSpread() }` → `{ session: '<sid>' }` 或 `{}`。 */
5
+ export declare function engineSessionParamSpread(): {
6
+ session?: string;
7
+ };
@@ -0,0 +1,35 @@
1
+ /**
2
+ * engineSessionParam — durable run 面 `?session=` 参数的统一派生([1501]§三,server 1.246 起
3
+ * legacy 八面软拍纪律:带了且不匹配=404,没带=warn-once 放行,二拍将把「没带」也 404;新面
4
+ * A/B(taskOutput/taskStop)与 subagentOutput 则出生即强制)。
5
+ *
6
+ * 取值语义 = run 绑定的会话(本壳所有 durable 动词的目标 run 都是本会话自有 run,见扩散面复审
7
+ * 镜头二 W-A:寻址 id 逐跳自有,T13 不变量保证会话 id turn 内不漂移)。优先级与
8
+ * fleetClient.effectiveScopeSessionId() / engineSubagentOutput 同源:引擎捕获值
9
+ * (getLiveSessionId,首轮 done 回填的权威值)优先,未捕获时退壳 boot-time getSessionId()
10
+ * (SB-5:同命名空间,引擎首轮认养壳自报 id)。动态 import 姿势同两处先例,避免把
11
+ * bootstrap/state 重图拖进静态依赖。
12
+ *
13
+ * 🔴 [1501] 空串陷阱:`?session=`(空值)按「带了且不匹配」处理=404 —— 本 helper 恒不返回
14
+ * 空串:要么真值要么 undefined(调用方 spread 姿势 `...(s ? { session: s } : {})`)。
15
+ */
16
+ import { getLiveSessionId } from './liveSessionStore.js';
17
+ let shellSessionIdFn;
18
+ void import('../bootstrap/state.js')
19
+ .then(m => {
20
+ shellSessionIdFn = m.getSessionId;
21
+ })
22
+ .catch(() => {
23
+ // 极端环境缺模块:退化为只用 getLiveSessionId(),与 fleetClient/engineSubagentOutput 同款容错
24
+ });
25
+ /** 当前会话的 session 参数值;undefined = 两源都缺席(动态 import 未落地的头几毫秒)——
26
+ * 调用方此时不带参(legacy 面软拍放行;强制面 404 自回落,下次重试自然带上)。 */
27
+ export function engineSessionParam() {
28
+ const sid = getLiveSessionId() ?? shellSessionIdFn?.();
29
+ return typeof sid === 'string' && sid.length > 0 ? sid : undefined;
30
+ }
31
+ /** spread 便捷形:`{ ...engineSessionParamSpread() }` → `{ session: '<sid>' }` 或 `{}`。 */
32
+ export function engineSessionParamSpread() {
33
+ const s = engineSessionParam();
34
+ return s ? { session: s } : {};
35
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * src/sema/finalVerifyWire.ts — headless `-p` finalVerification wire (TB2.0 反馈批 A · P1-1, 2026-07-15):
3
+ * stamp the ENGINE-EXISTING `TaskRequest.finalVerification` field on the headless `-p` path, DEFAULT ON.
4
+ *
5
+ * WHY: TB2.0 无人值守回归里 14/19 题 agent 自认完成判 0(写完不验/即兴自测即收尾)。引擎的
6
+ * finalVerification stop-gate(core runtask.js:1429-1493 —— 最多追加 2 条 '[final verification]'
7
+ * system-reminder,wroteThisRun && 非 structured-output && 未贴 maxTurns 才触发)+ server wire
8
+ * (http/server.js:3711 校验 / main.js:1383 转发)早已在线,壳侧从未 stamp——本模块补上这最后一段 wire。
9
+ *
10
+ * PRECEDENCE(off 优先,任何一条命中即不 stamp;否则默认 true):
11
+ * a) `--no-final-verify` flag → off(一键回上游 2.1.207 严格对齐态:上游
12
+ * `-p` 全文 0 处 final verification,默认开属超集;上游对应扩展点是 Stop hook)。
13
+ * b) `SEMA_HEADLESS_FINAL_VERIFY` env('false'/'0') → off。settings.json `env` 块 → 1a-envseed →
14
+ * process.env,其它 wire 配置(SEMA_HEADLESS_SCENARIO 等)同一车道。
15
+ * c) 用户配置了 Stop hook(settings hooks.Stop,经 hooksForWire 投影)→ off【对抗轮硬性 amendment】:
16
+ * 引擎 finalVerification 分支在 Stop hook 查询之前 return(runtask.js:1430-1443),默认开会把
17
+ * 用户 Stop hook 延迟至多 2 turn——对上游迁移来的 hook 用户是真行为变化,故有 Stop hook 即让位。
18
+ * d) 都没有 → true(TB 默认态吃满终验闸杠杆)。
19
+ *
20
+ * LIVE-GATE:与 scenarioWire 同款——调用点(seamQueryEngine.ask)以 SEMA_LIVE_BASEURL 为门,
21
+ * mock/pty fixture 车道永不 stamp 该字段(离线投影字节等价)。
22
+ *
23
+ * 语义澄清(分诊报告):finalVerification 是同会话 stop-gate 追加,不是独立上下文 verifier;
24
+ * 纯读任务(wroteThisRun gate)与 structured-output 任务被 core 自身豁免,回归面天然收窄。
25
+ */
26
+ import type { SemaSettings } from '@sema-agent/sdk';
27
+ import { type EnvLike } from './hostEnv.js';
28
+ /**
29
+ * B5:`hooksWireCaps` 的宿主耦合半场(settings 三层合并 + trust 门)还没搬,但这个**纯类型**
30
+ * 先落本包 —— 它就是 SDK 的 `settings.hooks`,与 hooksWireCaps 无耦合。
31
+ * 🔴 hooksWireCaps 后续进包时 **import 本条**,别再定义第二份(两份类型 = 漂移温床)。
32
+ */
33
+ export type WireHooksConfig = NonNullable<SemaSettings['hooks']>;
34
+ /** Settings-lane env knob(settings.json `env` 块 → 1a-envseed → process.env)。'false'/'0' = off。 */
35
+ export declare const HEADLESS_FINAL_VERIFY_ENV = "SEMA_HEADLESS_FINAL_VERIFY";
36
+ /**
37
+ * Parse `--no-final-verify` out of an argv slice(everything before a bare `--` — positionals are
38
+ * never flags,scenarioWire 同款纪律)。Boolean flag,无值形态,重复无害。
39
+ */
40
+ export declare function parseNoFinalVerifyArgv(argv: string[]): boolean;
41
+ /**
42
+ * The settings-lane off-switch:`SEMA_HEADLESS_FINAL_VERIFY` = 'false' or '0'(case-insensitive)
43
+ * disables the stamp。其它任何值(含空串/'true'/typo)= 默认态 on——fail-soft:一个 settings typo
44
+ * 只会落回默认开,绝不 brick 任何 run(scenarioWire 的 settings-lane 纪律)。
45
+ */
46
+ export declare function headlessFinalVerifyDisabledByEnv(env?: EnvLike): boolean;
47
+ /**
48
+ * 用户 Stop hook 在场判定:hooksForWire() 投影(policy→user→project→local 合并、trust/managed 门后)
49
+ * 里 `Stop` 事件带 ≥1 个 matcher 组即认定。判定输入用 wire 投影而非裸 settings——被 trust/
50
+ * disableAllHooks 门掉的 Stop hook 引擎侧同样不会 fire,不构成让位理由。
51
+ */
52
+ export declare function hasUserStopHook(wireHooks: WireHooksConfig | undefined): boolean;
53
+ /**
54
+ * Resolve the finalVerification stamp for a headless `-p` submit:默认 true;
55
+ * `--no-final-verify` flag / SEMA_HEADLESS_FINAL_VERIFY=false|0 / 用户 Stop hook 在场 → false。
56
+ * 调用点必须 live-gate(SEMA_LIVE_BASEURL)——mock/offline 形状不变。
57
+ */
58
+ export declare function resolveHeadlessFinalVerify(argv: string[], env?: EnvLike, wireHooks?: WireHooksConfig): boolean;
59
+ /**
60
+ * #106 裁 B(clay 2026-07-26):让位维持 + 诚实告知。detail 形暴露 off 原因供调用点分诊 ——
61
+ * flag/env 关闭是用户显式意愿(不聒噪);`stop-hook` 让位是用户**没显式要求**的关闭
62
+ * (Stop hook 可能来自 /goal 自注册或 settings hook),必须告知(修前静默,用户不知道自己
63
+ * 失去了终验)。方向注记:finalVerification 后续默认关(CC Stop hook 机制同,不叠超集)——
64
+ * 见记忆 final-verification-direction;届时本函数的默认臂翻转,detail 形不动。
65
+ */
66
+ export declare function resolveHeadlessFinalVerifyDetail(argv: string[], env?: EnvLike, wireHooks?: WireHooksConfig): {
67
+ on: boolean;
68
+ offReason?: 'flag' | 'env' | 'stop-hook';
69
+ };
70
+ /**
71
+ * stop-hook 让位的告知文案(单一措辞源;stderr 一行)。措辞纪律与 hook_notice 同族:
72
+ * 「让位/接管」向,绝不写「守卫失效」向 —— 后者会让用户以为链路坏了。
73
+ */
74
+ export declare const FINAL_VERIFY_YIELD_NOTICE = "final verification is off for this run \u2014 a Stop hook is active and takes over end-of-turn gating (it would otherwise be delayed by up to 2 turns)";
@@ -0,0 +1,63 @@
1
+ import { hostEnv } from './hostEnv.js';
2
+ /** Settings-lane env knob(settings.json `env` 块 → 1a-envseed → process.env)。'false'/'0' = off。 */
3
+ export const HEADLESS_FINAL_VERIFY_ENV = 'SEMA_HEADLESS_FINAL_VERIFY';
4
+ /**
5
+ * Parse `--no-final-verify` out of an argv slice(everything before a bare `--` — positionals are
6
+ * never flags,scenarioWire 同款纪律)。Boolean flag,无值形态,重复无害。
7
+ */
8
+ export function parseNoFinalVerifyArgv(argv) {
9
+ for (const a of argv) {
10
+ if (a === '--')
11
+ break; // positionals — never flags
12
+ if (a === '--no-final-verify')
13
+ return true;
14
+ }
15
+ return false;
16
+ }
17
+ /**
18
+ * The settings-lane off-switch:`SEMA_HEADLESS_FINAL_VERIFY` = 'false' or '0'(case-insensitive)
19
+ * disables the stamp。其它任何值(含空串/'true'/typo)= 默认态 on——fail-soft:一个 settings typo
20
+ * 只会落回默认开,绝不 brick 任何 run(scenarioWire 的 settings-lane 纪律)。
21
+ */
22
+ export function headlessFinalVerifyDisabledByEnv(env = hostEnv()) {
23
+ const v = env[HEADLESS_FINAL_VERIFY_ENV]?.trim().toLowerCase();
24
+ return v === 'false' || v === '0';
25
+ }
26
+ /**
27
+ * 用户 Stop hook 在场判定:hooksForWire() 投影(policy→user→project→local 合并、trust/managed 门后)
28
+ * 里 `Stop` 事件带 ≥1 个 matcher 组即认定。判定输入用 wire 投影而非裸 settings——被 trust/
29
+ * disableAllHooks 门掉的 Stop hook 引擎侧同样不会 fire,不构成让位理由。
30
+ */
31
+ export function hasUserStopHook(wireHooks) {
32
+ const groups = wireHooks?.['Stop'];
33
+ return Array.isArray(groups) && groups.length > 0;
34
+ }
35
+ /**
36
+ * Resolve the finalVerification stamp for a headless `-p` submit:默认 true;
37
+ * `--no-final-verify` flag / SEMA_HEADLESS_FINAL_VERIFY=false|0 / 用户 Stop hook 在场 → false。
38
+ * 调用点必须 live-gate(SEMA_LIVE_BASEURL)——mock/offline 形状不变。
39
+ */
40
+ export function resolveHeadlessFinalVerify(argv, env = hostEnv(), wireHooks) {
41
+ return resolveHeadlessFinalVerifyDetail(argv, env, wireHooks).on;
42
+ }
43
+ /**
44
+ * #106 裁 B(clay 2026-07-26):让位维持 + 诚实告知。detail 形暴露 off 原因供调用点分诊 ——
45
+ * flag/env 关闭是用户显式意愿(不聒噪);`stop-hook` 让位是用户**没显式要求**的关闭
46
+ * (Stop hook 可能来自 /goal 自注册或 settings hook),必须告知(修前静默,用户不知道自己
47
+ * 失去了终验)。方向注记:finalVerification 后续默认关(CC Stop hook 机制同,不叠超集)——
48
+ * 见记忆 final-verification-direction;届时本函数的默认臂翻转,detail 形不动。
49
+ */
50
+ export function resolveHeadlessFinalVerifyDetail(argv, env = hostEnv(), wireHooks) {
51
+ if (parseNoFinalVerifyArgv(argv))
52
+ return { on: false, offReason: 'flag' };
53
+ if (headlessFinalVerifyDisabledByEnv(env))
54
+ return { on: false, offReason: 'env' };
55
+ if (hasUserStopHook(wireHooks))
56
+ return { on: false, offReason: 'stop-hook' };
57
+ return { on: true };
58
+ }
59
+ /**
60
+ * stop-hook 让位的告知文案(单一措辞源;stderr 一行)。措辞纪律与 hook_notice 同族:
61
+ * 「让位/接管」向,绝不写「守卫失效」向 —— 后者会让用户以为链路坏了。
62
+ */
63
+ export const FINAL_VERIFY_YIELD_NOTICE = 'final verification is off for this run — a Stop hook is active and takes over end-of-turn gating (it would otherwise be delayed by up to 2 turns)';