@bolloon/bolloon-agent 0.6.1 → 0.6.3

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 (78) hide show
  1. package/README.md +128 -111
  2. package/dist/agents/gateway-group.js +231 -6
  3. package/dist/agents/group-autopilot.js +256 -0
  4. package/dist/agents/pi-sdk-tools.js +302 -0
  5. package/dist/agents/pi-sdk.js +89 -140
  6. package/dist/agents/task-group.js +21 -4
  7. package/dist/cli/log-gate.js +68 -4
  8. package/dist/cli/mention-data.js +1 -1
  9. package/dist/cli/model-selector.js +84 -104
  10. package/dist/cli/setup-wizard.js +2 -10
  11. package/dist/cli/tui-select.js +99 -10
  12. package/dist/cli-entry.js +1 -9
  13. package/dist/electron-build/agents/gateway-group.js +814 -0
  14. package/dist/electron-build/agents/gateway-group.js.map +1 -0
  15. package/dist/electron-build/agents/group-autopilot.js +302 -0
  16. package/dist/electron-build/agents/group-autopilot.js.map +1 -0
  17. package/dist/electron-build/agents/pi-sdk-tools.js +302 -0
  18. package/dist/electron-build/agents/pi-sdk-tools.js.map +1 -1
  19. package/dist/electron-build/agents/pi-sdk.js +88 -139
  20. package/dist/electron-build/agents/pi-sdk.js.map +1 -1
  21. package/dist/electron-build/agents/task-group.js +601 -0
  22. package/dist/electron-build/agents/task-group.js.map +1 -0
  23. package/dist/electron-build/agents/task-public-text.js +97 -0
  24. package/dist/electron-build/agents/task-public-text.js.map +1 -0
  25. package/dist/electron-build/efficode/compress.js +86 -0
  26. package/dist/electron-build/efficode/compress.js.map +1 -0
  27. package/dist/electron-build/efficode/crc32.js +44 -0
  28. package/dist/electron-build/efficode/crc32.js.map +1 -0
  29. package/dist/electron-build/efficode/did.js +74 -0
  30. package/dist/electron-build/efficode/did.js.map +1 -0
  31. package/dist/electron-build/efficode/lz77.js +175 -0
  32. package/dist/electron-build/efficode/lz77.js.map +1 -0
  33. package/dist/electron-build/efficode/negotiate.js +233 -0
  34. package/dist/electron-build/efficode/negotiate.js.map +1 -0
  35. package/dist/electron-build/efficode/ops.js +96 -0
  36. package/dist/electron-build/efficode/ops.js.map +1 -0
  37. package/dist/electron-build/efficode/packet.js +279 -0
  38. package/dist/electron-build/efficode/packet.js.map +1 -0
  39. package/dist/electron-build/efficode/types.js +50 -0
  40. package/dist/electron-build/efficode/types.js.map +1 -0
  41. package/dist/electron-build/kernel/code-write-gate.js +55 -0
  42. package/dist/electron-build/kernel/code-write-gate.js.map +1 -0
  43. package/dist/electron-build/kernel/run-lifecycle.js +23 -0
  44. package/dist/electron-build/kernel/run-lifecycle.js.map +1 -1
  45. package/dist/electron-build/kernel/run-resume.js +62 -0
  46. package/dist/electron-build/kernel/run-resume.js.map +1 -0
  47. package/dist/electron-build/kernel/session-lifecycle.js +223 -0
  48. package/dist/electron-build/kernel/session-lifecycle.js.map +1 -0
  49. package/dist/electron-build/kernel/turn-selfcheck.js +65 -0
  50. package/dist/electron-build/kernel/turn-selfcheck.js.map +1 -0
  51. package/dist/electron-build/llm/config-store.js +2 -2
  52. package/dist/electron-build/llm/config-store.js.map +1 -1
  53. package/dist/electron-build/llm/model-selection.js +16 -2
  54. package/dist/electron-build/llm/model-selection.js.map +1 -1
  55. package/dist/electron-build/llm/pi-ai.js +2 -2
  56. package/dist/electron-build/llm/pi-ai.js.map +1 -1
  57. package/dist/electron-build/orbitdb/cid-database.js +98 -19
  58. package/dist/electron-build/orbitdb/cid-database.js.map +1 -1
  59. package/dist/electron-build/orbitdb/group-access.js +399 -0
  60. package/dist/electron-build/orbitdb/group-access.js.map +1 -0
  61. package/dist/index.js +143 -10
  62. package/dist/ios/bolloon-web.json +3 -3
  63. package/dist/ios/sw.js +1 -1
  64. package/dist/kernel/code-write-gate.js +47 -0
  65. package/dist/kernel/gate-scan.js +5 -1
  66. package/dist/kernel/plan-harness.js +3 -2
  67. package/dist/kernel/roster.js +6 -6
  68. package/dist/kernel/run-lifecycle.js +21 -0
  69. package/dist/kernel/run-resume.js +58 -0
  70. package/dist/kernel/session-lifecycle.js +210 -0
  71. package/dist/kernel/turn-selfcheck.js +60 -0
  72. package/dist/llm/config-store.js +2 -2
  73. package/dist/llm/model-selection.js +16 -2
  74. package/dist/llm/pi-ai.js +2 -2
  75. package/dist/orbitdb/cid-database.js +95 -19
  76. package/dist/web/bolloon-web.json +3 -3
  77. package/dist/web/sw.js +1 -1
  78. package/package.json +1 -1
@@ -62,6 +62,8 @@ const transport_js_1 = require("../kernel/transport.js");
62
62
  const control_js_1 = require("../kernel/control.js");
63
63
  // K10 ②: 回合的模型经**内核运行时**只读取租约 (传输仍是 Pi 客户端 ⇒ 行为不变; 取不到如实回落)
64
64
  const model_runtime_js_1 = require("../kernel/model-runtime.js");
65
+ // K10 余项: 会话生命周期的**规则**归内核 (落盘映射 · key 校验 · 读回过滤/水合 · 运行种子合并顺序), I/O 留本类经端口注入
66
+ const session_lifecycle_js_1 = require("../kernel/session-lifecycle.js");
65
67
  const background_notices_js_1 = require("./background-notices.js");
66
68
  const skill_ledger_js_1 = require("./skill-ledger.js");
67
69
  const node_crypto_1 = require("node:crypto");
@@ -69,7 +71,12 @@ const experience_review_js_1 = require("./experience-review.js");
69
71
  const lesson_to_skill_js_1 = require("./lesson-to-skill.js");
70
72
  const managed_services_js_1 = require("./managed-services.js");
71
73
  const tool_subset_js_1 = require("./tool-subset.js");
72
- const code_write_gate_js_1 = require("./code-write-gate.js");
74
+ // K10 余项: 收尾自检的**规则**归内核 (`kernel/code-write-gate.ts`) —— 原来是 agents 侧的一个模块
75
+ const code_write_gate_js_1 = require("../kernel/code-write-gate.js");
76
+ // K10 余项: 收尾自检的**编排与政策**也归内核 (该不该跑 · 系统自检也过门 · 被拒不执行且可见 · 失败不影响回合)
77
+ const turn_selfcheck_js_1 = require("../kernel/turn-selfcheck.js");
78
+ // K10 余项: 恢复一次运行的**编排与政策**也归内核 (漂移上报口径 · 只在漂了/核对不了时装 · 恢复态一定被清)
79
+ const run_resume_js_1 = require("../kernel/run-resume.js");
73
80
  // 2026-10-01: 身份解析必须**静态导入** —— 之前在函数体里用 require(), 而打包后是 ESM ⇒
74
81
  // require 是 undefined ⇒ 抛错被 catch 静默吞掉 ⇒ "自愈"根本没跑, DID 一直是空的 ✗。
75
82
  const agent_identity_js_1 = require("./agent-identity.js");
@@ -500,7 +507,9 @@ class PiAgentSession {
500
507
  * · K5 Channel Actor 完成后改为 `createRunContext({ runId: request.resumeRunId })`, 届时删除 `currentRunId` 字段本体。
501
508
  */
502
509
  seedRunContext(extra = {}) {
503
- return (0, run_context_js_1.createRunContext)({ runId: this.actor.state.activeRun, ...extra });
510
+ // **K10 余项**: 合并顺序 (活跃运行打底 · extra 覆盖) 是内核规则 —— 与 `seed-run` 端口共用 `composeRunSeed`
511
+ // "活跃运行"这一处读取仍**只在本方法体内** (K2 播种的唯一读点, 有门钉住); 合并顺序由内核给。
512
+ return (0, run_context_js_1.createRunContext)((0, session_lifecycle_js_1.composeRunSeed)(this.actor.state.activeRun, extra));
504
513
  }
505
514
  /**
506
515
  * **K5 迁移中**: 本会话所属 channel 的 Actor (由 session factory 在创建时绑定)。
@@ -756,7 +765,7 @@ class PiAgentSession {
756
765
  if (this.actor) {
757
766
  const n = await this.actor.hydrateHistory({
758
767
  load: () => this._sessionStore.loadMessages(sessionKey),
759
- filter: (loaded) => this._filterToMessage(loaded),
768
+ filter: (loaded) => (0, session_lifecycle_js_1.filterSessionMessages)(loaded),
760
769
  maxMessages,
761
770
  });
762
771
  if (n > 0)
@@ -768,7 +777,7 @@ class PiAgentSession {
768
777
  console.log(`[PiAgent] hydrate: 没有 ${sessionKey} 的历史`);
769
778
  return;
770
779
  }
771
- const hydrated = this._filterToMessage(loaded).slice(-maxMessages);
780
+ const hydrated = (0, session_lifecycle_js_1.hydrateSessionMessages)(loaded, maxMessages);
772
781
  if (hydrated.length > 0) {
773
782
  this.replaceHistory(hydrated);
774
783
  console.log(`[PiAgent] 从 ${sessionKey} 回灌 ${hydrated.length} 条历史`);
@@ -789,20 +798,24 @@ class PiAgentSession {
789
798
  * 公开方法 — claude code / 外部 harness 在每次 prompt 完成后调一下,
790
799
  * 即可获得"重启 / 跨进程接续"的语义.
791
800
  */
801
+ /**
802
+ * **K10 余项**: 取数拍 (Actor 快照, 走邮箱 ⇒ 与 append 串行) 与真写入仍是本类的;
803
+ * 而「消息 → 持久形态」的**映射规则**与 key 校验搬进内核 (`submitSessionOp` 的 `save-session`)。
804
+ * 写不进 ⇒ 响亮失败 (`assertSessionOk`), 不静默继续。
805
+ */
792
806
  async saveCurrentSession(key) {
793
- // **K5 第 4 步**: 取数拍走 Actor 的快照 (走邮箱 ⇒ 与 append 串行, 不会抓到"边写边读"的半截状态);
794
- // 未绑定 actor 的会话走原路径 (行为不变)。
795
- const source = this.actor ? await this.actor.historySnapshot() : this.actor.state.messageHistory;
796
- const persisted = source.map((m) => ({
797
- role: m.role,
798
- content: m.content,
799
- toolCall: m.toolCall,
800
- toolResult: m.toolResult,
801
- toolCallId: m.toolCallId,
802
- timestamp: Date.now(),
803
- source: 'pi-session',
804
- }));
805
- await this._sessionStore.saveMessages(key, persisted);
807
+ const out = await (0, session_lifecycle_js_1.submitSessionOp)({ op: 'save-session', origin: 'pi-session', key }, this.sessionPorts());
808
+ (0, session_lifecycle_js_1.assertSessionOk)(out);
809
+ }
810
+ /** K10 余项: 会话生命周期端口实现 —— I/O 留本类, 规则在内核 (`kernel/session-lifecycle.ts`) */
811
+ sessionPorts() {
812
+ return {
813
+ historySnapshot: () => (this.actor ? this.actor.historySnapshot() : this.actor.state.messageHistory),
814
+ saveMessages: (key, messages) => this._sessionStore.saveMessages(key, messages),
815
+ loadMessages: (key) => this._sessionStore.loadMessages(key),
816
+ // 注意: **故意不提供** `activeRunId` / `newRunContext` —— "活跃运行"的读取必须只有一处 (K2 播种唯一读点, 门钉住),
817
+ // 本类走的是**同步** `seedRunContext` + 内核的 `composeRunSeed`。异步 `seed-run` 端口留给别的调用方 (其语义由内核门覆盖)。
818
+ };
806
819
  }
807
820
  /**
808
821
  * 2026-06-30: 从 disk 拉历史覆盖当前 messageHistory.
@@ -820,47 +833,12 @@ class PiAgentSession {
820
833
  * 返回 Message[] 数组 (空数组表示无历史).
821
834
  */
822
835
  async peekSessionHistory(key, maxMessages = 30) {
823
- try {
824
- const loaded = await this._sessionStore.loadMessages(key);
825
- if (!loaded)
826
- return [];
827
- return this._filterToMessage(loaded).slice(-maxMessages);
828
- }
829
- catch {
830
- return [];
831
- }
832
- }
833
- /** hydrateMessageHistory 用的过滤逻辑 — 提到外面复用 */
834
- _filterToMessage(loaded) {
835
- const hydrated = [];
836
- const VALID_ROLES = new Set(['user', 'assistant', 'tool', 'system']);
837
- for (const m of loaded) {
838
- // role 必须合法 (拒绝旧 schema {type:'user'} 没 role 字段的)
839
- if (!VALID_ROLES.has(m.role))
840
- continue;
841
- // 跳过污染消息
842
- if (typeof m.content === 'string' && m.content.startsWith('[AI 服务调用失败]'))
843
- continue;
844
- if (typeof m.content === 'string' && m.content.startsWith('[错误:'))
845
- continue;
846
- // 注意: '!m.content' 会跳过 content='' 的 tool call 消息 (assistant role + toolCall 字段),
847
- // 这种是合法的 (LLM 输出只有 tool call, 没有正文) — 必须保留.
848
- // 这里只跳过"无内容 + 也没 tool call/tool result"的废消息.
849
- if (!m.content && !m.toolCall && !m.toolResult)
850
- continue;
851
- // 跳过空 tool role (tool result 占位但没有任何内容)
852
- if (m.role === 'tool' && !m.toolResult)
853
- continue;
854
- hydrated.push({
855
- role: m.role,
856
- content: m.content ?? '',
857
- toolCall: m.toolCall,
858
- toolResult: m.toolResult,
859
- toolCallId: m.toolCallId,
860
- });
861
- }
862
- return hydrated;
836
+ // **K10 余项**: 读回过滤/水合规则归内核; 读失败 ⇒ 空数组, 但**原因进内核审计** (不再完全静默)
837
+ const out = await (0, session_lifecycle_js_1.submitSessionOp)({ op: 'peek-history', origin: 'pi-session', key, maxMessages }, this.sessionPorts());
838
+ return out.ok ? out.result : [];
863
839
  }
840
+ // **K10 余项 (减法)**: 原来的 `_filterToMessage` (合法 role 白名单 · 剔污染消息 · 保留"只有 tool call 无正文"的合法消息)
841
+ // 已整条搬进内核 `kernel/session-lifecycle.ts` 的 `filterSessionMessages` —— 规则一处一份, 这里不再留副本。
864
842
  /** 暴露 store 给测试 / 高级集成用. */
865
843
  get sessionStoreInstance() {
866
844
  return this._sessionStore;
@@ -1168,7 +1146,14 @@ class PiAgentSession {
1168
1146
  return `[初始化未就绪] ${why}\n${hint}`;
1169
1147
  }
1170
1148
  }
1171
- catch { /* 门禁自身异常 → 按"不能判定"处理会阻塞一切; 这里只在能读到状态时才拦 */ }
1149
+ catch (err) {
1150
+ // fail-closed: 门禁异常不能绕过初始化,否则会出现"身份未就绪但 Agent 已调用工具"。
1151
+ const why = `初始化门禁读取失败: ${String(err?.message ?? err).slice(0, 240)}`;
1152
+ this.pushHistory({ role: 'user', content: input });
1153
+ this.pushHistory({ role: 'assistant', content: `[初始化未就绪] ${why}\n请运行 bolloon setup status;修复后再重试。` });
1154
+ console.warn(`[PiAgent] 拒绝执行: ${why}`);
1155
+ return `[初始化未就绪] ${why}\n请运行 bolloon setup status;修复后再重试。`;
1156
+ }
1172
1157
  }
1173
1158
  this.minimaxAvailable = this.checkMinimax();
1174
1159
  this.actor.state.channelId = options?.channelId ?? this.actor.state.channelId;
@@ -1333,8 +1318,10 @@ class PiAgentSession {
1333
1318
  .catch(() => { });
1334
1319
  }
1335
1320
  catch { /* 落日志失败不影响主流程 */ }
1321
+ // 2026-10-02 (leo: 「cli 的 UI 需要有渲染过滤」): 这条是**内部运行日志** (复盘过程), 不是用户要看的结论
1322
+ // ⇒ 不再进用户可见对话流; 走 console (由 CLI 的渲染过滤只落盘, `--verbose` 下照原样上屏)。
1336
1323
  try {
1337
- this.runCtx.eventSink?.({ type: 'status', content: `📚 复盘: ${m}`, tool: 'system' });
1324
+ console.log(`📚 复盘: ${m}`);
1338
1325
  }
1339
1326
  catch { /* 上屏失败不打断 */ }
1340
1327
  };
@@ -2135,7 +2122,8 @@ ${await this.renderActivePlansSection()}
2135
2122
  * 而 Supervisor / 控制面要在运行结束后才知道"刚才跑的是哪条 run" (否则会拿旧 run 做决策)。
2136
2123
  */
2137
2124
  getLastRunId() {
2138
- return this.lastRunId || this.actor.state.activeRun;
2125
+ // K10 余项: 取值优先级 (显式 lastRunId 优先 · 回落活跃运行) 是**内核规则**
2126
+ return (0, run_lifecycle_js_1.pickRunIdForResume)(this.lastRunId, this.actor.state.activeRun);
2139
2127
  }
2140
2128
  /**
2141
2129
  * 2026-09-16 (M2): 从 checkpoint 恢复一次运行 —— **不是**重发原 prompt。
@@ -2148,49 +2136,32 @@ ${await this.renderActivePlansSection()}
2148
2136
  * 并把结论放进返回值 (`modelDrift`) 让调用方能上报; 一致时也有一句明确结论。
2149
2137
  * 判断失败 (读不到 Run 等) 不阻塞恢复。
2150
2138
  */
2139
+ /**
2140
+ * **K10 余项**: 编排与政策归内核 (`kernel/run-resume.ts`) —— 这里只做七件事:
2141
+ * 核对漂移 · 准备 · 按需装配 · 落恢复态 · 驱动同一 runId 继续 · 清态 · 日志。
2142
+ * 语义 (漂移上报口径 · 只在漂了/核对不了时装 · 装配失败不阻塞 · 恢复态一定被清) 由内核拥有。
2143
+ */
2151
2144
  async resumeRun(runId) {
2152
- let modelDrift;
2153
- let modelApplied;
2154
- try {
2155
- const { detectRunConfigDrift } = await Promise.resolve().then(() => __importStar(require('../llm/model-selection.js')));
2156
- modelDrift = (await detectRunConfigDrift(runId)) || undefined;
2157
- if (modelDrift?.drifted) {
2158
- console.warn('[pi-sdk] 恢复时模型配置已偏离 Run 快照:', modelDrift.message);
2159
- }
2160
- else if (modelDrift) {
2161
- console.log('[pi-sdk] 恢复前核对:', modelDrift.message);
2162
- }
2163
- }
2164
- catch { /* 核对本身失败不阻塞恢复 */ }
2165
- const prep = await (0, run_store_js_1.prepareResume)(runId);
2166
- if (!prep.ok || !prep.plan)
2167
- return { ok: false, reason: prep.reason, ...(modelDrift ? { modelDrift } : {}) };
2168
- // 2026-09-26 (P6 × P7): 恢复一个在跑的 Run, 用的是**它自己那份快照**, 不是盘上现在的全局默认 ——
2169
- // 否则这次恢复就是"带着漂移继续跑", 之后再存快照会把漂移固化成历史。
2170
- // 只在"漂了"或"核对不了"时重装 (一致时运行时本来就是对的, 重装是白跑一趟)。
2171
- // 装配只有一处实现: `applyRunModelConfigToRuntime` → `installRuntime` (这里不自己 initMinimax)。
2172
- if (modelDrift && (modelDrift.drifted || !modelDrift.verified)) {
2173
- try {
2145
+ const out = await (0, run_resume_js_1.resumeRunViaKernel)(runId, {
2146
+ detectDrift: async (id) => {
2147
+ const { detectRunConfigDrift } = await Promise.resolve().then(() => __importStar(require('../llm/model-selection.js')));
2148
+ return ((await detectRunConfigDrift(id)) || undefined);
2149
+ },
2150
+ prepareResume: (id) => (0, run_store_js_1.prepareResume)(id),
2151
+ applySnapshot: async (snapshot) => {
2174
2152
  const { applyRunModelConfigToRuntime } = await Promise.resolve().then(() => __importStar(require('../llm/model-selection.js')));
2175
- modelApplied = await applyRunModelConfigToRuntime(modelDrift.snapshot);
2176
- console.log(`[pi-sdk] 已按 Run 快照装配运行时: ${modelApplied.provider}/${modelApplied.model} (configHash ${String(modelApplied.configHash).slice(0, 12)})`);
2177
- }
2178
- catch (e) {
2179
- // 装配失败不阻塞恢复 (如实说; 调用方可以从返回值里看出没装成)
2180
- console.warn('[pi-sdk] 按 Run 快照装配运行时失败, 保持当前运行时:', String(e?.message || e).slice(0, 160));
2181
- }
2182
- }
2183
- this.resumeRunId = runId;
2184
- this.resumePlan = prep.plan;
2185
- this.actor.state.goalBinding = prep.plan.goalId || this.actor.state.goalBinding;
2186
- try {
2187
- const reply = await this.prompt((0, run_store_js_1.buildResumeInstruction)(prep.plan), {});
2188
- return { ok: true, reply, ...(modelDrift ? { modelDrift } : {}), ...(modelApplied ? { modelApplied } : {}) };
2189
- }
2190
- finally {
2191
- this.resumeRunId = '';
2192
- this.resumePlan = null;
2193
- }
2153
+ return (await applyRunModelConfigToRuntime(snapshot));
2154
+ },
2155
+ applyPlan: (plan) => {
2156
+ this.resumeRunId = runId;
2157
+ this.resumePlan = plan;
2158
+ this.actor.state.goalBinding = plan.goalId || this.actor.state.goalBinding;
2159
+ },
2160
+ continueRun: (plan) => this.prompt((0, run_store_js_1.buildResumeInstruction)(plan), {}),
2161
+ clearPlan: () => { this.resumeRunId = ''; this.resumePlan = null; },
2162
+ log: (level, body) => (level === 'warn' ? console.warn('[pi-sdk] ' + body) : console.log('[pi-sdk] ' + body)),
2163
+ });
2164
+ return out;
2194
2165
  }
2195
2166
  /**
2196
2167
  * 2026-10-02 (K7): 把**本会话的 Harness 判定**包成 skill 门, 供 `BollharnessIntegration.setSkillGuard` 注入。
@@ -2245,46 +2216,24 @@ ${await this.renderActivePlansSection()}
2245
2216
  * (`usePivotLoop: true`, `promptWithPivotLoop` 提前 return) ⇒ 这条"系统自检过门"在**生产根本没触发过**。
2246
2217
  * 抽成方法后两条 loop 路径都调它 (一次一轮, 由 `typecheckRanThisTurn` 保证)。
2247
2218
  */
2219
+ /**
2220
+ * **K10 余项**: 编排与政策归内核 (`kernel/turn-selfcheck.ts`) —— 这里只做三件事: 取工具 · 问门 · 上报。
2221
+ * 语义 (该不该跑 · 系统自检也过门 · 被拒不执行且可见 · 失败不影响回合) 由内核拥有, 换入口不必各写一份。
2222
+ * 状态维护 (一轮只跑一次 / 清空本回合触达) 跟内核的 `decided` 走: 与迁移前逐条一致。
2223
+ */
2248
2224
  async runTurnEndTypecheck() {
2249
- // 2026-10-01: **本回合改过 TS ⇒ 收尾自动类型检查** (一轮一次) —— 复用已有的 tsc_check 工具
2250
- if ((0, code_write_gate_js_1.decideTypecheck)(this.tsTouchedThisTurn, this.typecheckRanThisTurn)) {
2225
+ const out = await (0, turn_selfcheck_js_1.runTurnEndSelfCheck)({ touched: this.tsTouchedThisTurn, ranThisTurn: this.typecheckRanThisTurn }, {
2226
+ getTool: (name) => this.tools.get(name),
2227
+ askGate: async (req) => this.piHarness().beforeToolCall({
2228
+ tool: req.tool,
2229
+ args: req.args,
2230
+ ctx: this.harnessCtx(),
2231
+ permissionMode: this.currentPermissionMode,
2232
+ }),
2233
+ emit: (evt) => this.runCtx.eventSink?.({ type: 'status', content: evt.content, tool: 'system' }),
2234
+ });
2235
+ if (out.decided) {
2251
2236
  this.typecheckRanThisTurn = true;
2252
- try {
2253
- const tscTool = this.tools.get('tsc_check');
2254
- if (tscTool?.execute) {
2255
- // 2026-10-02 (K7): **系统自检也过门** —— 统一成"任何工具执行都走同一扇 Harness 门"。
2256
- // 这里不是模型发起的工具调用 (args 恒空, 是本回合改过 TS 后的收尾自检), 所以**更**不能例外:
2257
- // 例外一旦靠"没人知道它绕过"活着, 门就不再是唯一的执行咽喉。
2258
- // 语义: 被拒或门抛错 ⇒ **不执行** (fail-closed), 且**可见**地报出来 (拒绝不许静默)。
2259
- let tscAllowed = true;
2260
- let tscWhy = '';
2261
- try {
2262
- const d = await this.piHarness().beforeToolCall({
2263
- tool: 'tsc_check',
2264
- args: {},
2265
- ctx: this.harnessCtx(),
2266
- permissionMode: this.currentPermissionMode,
2267
- });
2268
- tscAllowed = d.allow;
2269
- tscWhy = d.reason || d.rejectedBy || '';
2270
- }
2271
- catch (gateErr) {
2272
- tscAllowed = false;
2273
- tscWhy = `harness-error: ${String(gateErr?.message || gateErr)}`;
2274
- }
2275
- if (!tscAllowed) {
2276
- this.runCtx.eventSink?.({ type: 'status', content: `🔎 类型检查被门拒绝, 未执行: ${tscWhy.slice(0, 120)}`, tool: 'system' });
2277
- }
2278
- else {
2279
- const r = await tscTool.execute({});
2280
- const line = (0, code_write_gate_js_1.formatTypecheckResult)(r?.success !== false && !/error TS\d+/.test(String(r?.output || '')), String(r?.output || ''));
2281
- this.runCtx.eventSink?.({ type: 'status', content: `🔎 ${line}`, tool: 'system' });
2282
- }
2283
- }
2284
- }
2285
- catch (e) {
2286
- this.runCtx.eventSink?.({ type: 'status', content: `🔎 类型检查没能跑起来: ${String(e?.message || e).slice(0, 100)} (改动已落盘, 记得自己跑一次)`, tool: 'system' });
2287
- }
2288
2237
  this.tsTouchedThisTurn = [];
2289
2238
  }
2290
2239
  }