@zhushanwen/pi-goal 0.15.0 → 0.16.1

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 (36) hide show
  1. package/README.md +2 -8
  2. package/package.json +5 -4
  3. package/src/__tests__/active-path.test.ts +2 -2
  4. package/src/__tests__/circuit-breaker.test.ts +10 -10
  5. package/src/__tests__/criteria-array-prompt-guidance.test.ts +2 -2
  6. package/src/__tests__/deserialize-state.test.ts +2 -2
  7. package/src/__tests__/event-adapter.test.ts +8 -8
  8. package/src/__tests__/goal-control-adapter.test.ts +6 -6
  9. package/src/__tests__/goal-control-rpc.test.ts +18 -4
  10. package/src/__tests__/goal-control-summary.test.ts +26 -0
  11. package/src/__tests__/index.test.ts +1 -1
  12. package/src/__tests__/liveness.test.ts +1 -1
  13. package/src/__tests__/message-end.test.ts +3 -3
  14. package/src/__tests__/ports.test.ts +1 -1
  15. package/src/__tests__/service.test.ts +2 -2
  16. package/src/adapters/command-adapter.ts +1 -1
  17. package/src/adapters/event-handlers/agent-end.ts +12 -12
  18. package/src/adapters/event-handlers/before-agent-start.ts +1 -1
  19. package/src/adapters/event-handlers/message-end.ts +3 -3
  20. package/src/adapters/event-handlers/session-shutdown.ts +1 -1
  21. package/src/adapters/event-handlers/turn-end.ts +3 -3
  22. package/src/adapters/goal-control-adapter.ts +29 -34
  23. package/src/adapters/ports.ts +2 -2
  24. package/src/constants.ts +1 -1
  25. package/src/engine/__tests__/budget.test.ts +1 -1
  26. package/src/engine/__tests__/goal.test.ts +64 -10
  27. package/src/engine/goal.ts +82 -6
  28. package/src/engine/liveness.ts +2 -2
  29. package/src/engine/types.ts +0 -13
  30. package/src/index.ts +3 -3
  31. package/src/persistence.ts +1 -1
  32. package/src/ports.ts +2 -2
  33. package/src/projection/__tests__/widget.test.ts +3 -3
  34. package/src/projection/widget.ts +2 -2
  35. package/src/service.ts +4 -4
  36. package/src/session.ts +5 -5
package/README.md CHANGED
@@ -26,15 +26,9 @@ ln -s /path/to/taiji/extensions/universal/goal \
26
26
  pi install npm:@zhushanwen/pi-goal
27
27
  ```
28
28
 
29
- ## 工具激活(deferred)
29
+ ## 工具激活
30
30
 
31
- `goal_control` 工具为 `exposure: "deferred"`——不占常驻工具声明,由 pi 内置 `tool_search` 工具检索命中后按需激活。taiji 宿主内开箱即用(宿主启动时已激活 `tool_search`);独立 pi 使用需在 `<agentDir>/settings.json`(默认 `~/.pi/agent/settings.json`)开启:
32
-
33
- ```json
34
- { "defaultTools": ["+tool_search"] }
35
- ```
36
-
37
- `+` 前缀是增量修饰(保留 read/bash/edit/write 默认集)。未开启 `tool_search` 时该工具对模型不可见。
31
+ `goal_control` 工具为 direct 常驻——注册不声明 `exposure` 字段(pi SDK 缺省 `direct`),工具声明随会话常驻、对模型默认可见,无需经 `tool_search` 检索激活,独立 pi 开箱即用。
38
32
 
39
33
  ## 使用
40
34
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhushanwen/pi-goal",
3
- "version": "0.15.0",
3
+ "version": "0.16.1",
4
4
  "description": "Codex-style /goal command for Pi — persistent goal-driven autonomous loop with evidence-based completion, token budget, agent-reported blocking, and steering templates.",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -29,12 +29,13 @@
29
29
  "@earendil-works/pi-coding-agent": "^1.0.0",
30
30
  "@earendil-works/pi-tui": "^1.0.0",
31
31
  "typebox": "1.3.27",
32
- "@zhushanwen/pi-pending-notifications": "0.9.0"
32
+ "@zhushanwen/pi-pending-notifications": "0.10.1"
33
33
  },
34
34
  "dependencies": {
35
- "@zhushanwen/extension-protocol": "0.21.0",
35
+ "@zhushanwen/extension-protocol": "0.23.0",
36
36
  "@zhushanwen/pi-extension-logger": "0.6.2",
37
- "@zhushanwen/pi-session-path": "0.1.0"
37
+ "@zhushanwen/pi-session-path": "0.1.0",
38
+ "@zhushanwen/state-machine": "0.2.0"
38
39
  },
39
40
  "peerDependenciesMeta": {
40
41
  "@earendil-works/pi-tui": {
@@ -127,7 +127,7 @@ interface HandlerFixture {
127
127
  appendEntry: ReturnType<typeof vi.fn>;
128
128
  }
129
129
 
130
- /** 实例化 goal 工厂并捕获事件 handler(最小 pi:注册面 vi.fn + 副作用面 vi.fn) */
130
+ /** 实例化 goal 工厂并捕获事件 handler(最小 pi:注册点 vi.fn + 副作用侧 vi.fn) */
131
131
  function makeHandlerFixture(): HandlerFixture {
132
132
  const handlers = new Map<string, (event: unknown, ctx: ExtensionContext) => Promise<unknown>>();
133
133
  const pi = {
@@ -145,7 +145,7 @@ function makeHandlerFixture(): HandlerFixture {
145
145
  return { handlers, sendMessage: pi.sendMessage, appendEntry: pi.appendEntry };
146
146
  }
147
147
 
148
- /** 最小 ctx:sessionManager(分支树)+ ui 记录面 + getContextUsage(before_agent_start 用) */
148
+ /** 最小 ctx:sessionManager(分支树)+ ui 记录侧 + getContextUsage(before_agent_start 用) */
149
149
  function makeCtx(entries: SessionEntryLike[], leafId: string | null) {
150
150
  const setWidget = vi.fn();
151
151
  const setStatus = vi.fn();
@@ -1,5 +1,5 @@
1
1
  /**
2
- * 轮次活性熔断行为测试(chat-domain-v1x 设计 §3.2 D4 / W5,验收 §4 A10 的单测面)
2
+ * 轮次活性熔断行为测试(chat-domain-v1x 设计 §3.2 D4 / W5,验收 §4 A10 的单测)
3
3
  *
4
4
  * 四断言(对齐 impl-plan W5 验收条款②):
5
5
  * a. 主判据 50 次封顶必停发——含「每轮调一次工具」序列同样被封顶(正交性:
@@ -43,7 +43,7 @@ interface FakeHarness {
43
43
  entries: unknown[];
44
44
  piCalls: RecordedCall[];
45
45
  ctxCalls: RecordedCall[];
46
- /** 可控 isIdle(MF-6① 守卫断言用:延迟窗内有活动 turn 时置 false) */
46
+ /** 可控 isIdle(MF-6① 检查断言用:延迟窗内有活动 turn 时置 false) */
47
47
  isIdle: { value: boolean };
48
48
  /**
49
49
  * 可控 appendEntry 抛错(MF-R2-1 测试前提修正):pi.appendEntry 与 ctx 同生命周期
@@ -336,11 +336,11 @@ async function enterBackoff(): Promise<{ h: FakeHarness; session: GoalSession }>
336
336
  return { h, session };
337
337
  }
338
338
 
339
- describe("MF-6① idle 守卫:ctx.signal 实时求值失效场景由 isIdle 拦截", () => {
339
+ describe("MF-6① idle 检查:ctx.signal 实时求值失效场景由 isIdle 拦截", () => {
340
340
  it("延迟窗内有活动 turn(isIdle=false、signal=undefined)→ 到期不误发", async () => {
341
341
  const { h, session } = await enterBackoff();
342
342
 
343
- // agent idle 后 SDK get signal 实时求值为 undefined(旧 signal?.aborted 守卫恒放行);
343
+ // agent idle 后 SDK get signal 实时求值为 undefined(旧 signal?.aborted 检查恒放行);
344
344
  // 用户在延迟窗内发新消息 → 活动 turn 占用 → isIdle=false
345
345
  h.ctx.signal = undefined;
346
346
  h.isIdle.value = false;
@@ -350,7 +350,7 @@ describe("MF-6① idle 守卫:ctx.signal 实时求值失效场景由 isIdle
350
350
  expect(session.state!.continuationsSent).toBe(4);
351
351
  });
352
352
 
353
- it("延迟窗内 idle(isIdle=true、signal=undefined)→ 正常发出(守卫不误伤退避路径)", async () => {
353
+ it("延迟窗内 idle(isIdle=true、signal=undefined)→ 正常发出(检查不误伤退避路径)", async () => {
354
354
  const { h, session } = await enterBackoff();
355
355
 
356
356
  h.ctx.signal = undefined;
@@ -403,7 +403,7 @@ describe("MF-6② stale 保护:timer 回调内异常不冒泡(catch 块自
403
403
  });
404
404
  });
405
405
 
406
- describe("MF-R2-1 根修:session_shutdown 取消旧退避 timer", () => {
406
+ describe("MF-R2-1 从根源修复:session_shutdown 取消旧退避 timer", () => {
407
407
  it("session_shutdown(invalidate 前触发)后旧 timer 到期不发;全量 stale 下不崩、不冒泡", async () => {
408
408
  const { h, session } = await enterBackoff();
409
409
 
@@ -430,7 +430,7 @@ describe("MF-R2-1 根修:session_shutdown 取消旧退避 timer", () => {
430
430
  });
431
431
  });
432
432
 
433
- describe("MF-6③ timer 清理面:session_start / before_agent_start 取消旧 timer", () => {
433
+ describe("MF-6③ timer 清理侧:session_start / before_agent_start 取消旧 timer", () => {
434
434
  it("session_start 重建 → 旧退避 timer 被取消,到期不发", async () => {
435
435
  const { h, session } = await enterBackoff();
436
436
 
@@ -459,13 +459,13 @@ describe("MF-6③ timer 清理面:session_start / before_agent_start 取消旧
459
459
  // 端口改造前 backoff timer 到期伪装用户消息投递;改造后
460
460
  // deliverContinuation 经端口发 custom message 且 options 含 triggerTurn: true——
461
461
  // 非 streaming idle 场景真实开轮(改造前 append 不开轮 = 死消息)。
462
- // isIdle=false 拦截见 MF-6①;封顶记账守卫见「主判据」describe;此处锁定到期
462
+ // isIdle=false 拦截见 MF-6①;封顶记账检查见「主判据」describe;此处锁定到期
463
463
  // 发出路径的 options 契约与记账恰好 +1。
464
464
  describe("A8:退避 timer 到期 → triggerTurn: true 开轮发出", () => {
465
- it("isIdle 守卫通过后发出:custom message 四要素齐备,记账恰好 +1", async () => {
465
+ it("isIdle 检查通过后发出:custom message 四要素齐备,记账恰好 +1", async () => {
466
466
  const { h, session } = await enterBackoff();
467
467
 
468
- vi.advanceTimersByTime(20_000); // 2×base 到期,isIdle 默认 true → 守卫放行
468
+ vi.advanceTimersByTime(20_000); // 2×base 到期,isIdle 默认 true → 检查放行
469
469
 
470
470
  const sends = followUpSends(h);
471
471
  expect(sends).toHaveLength(5); // 4 立即 + 1 到期发出
@@ -48,12 +48,12 @@ const successCriteriaSchema = GoalControlParams.properties.successCriteria;
48
48
  const PARAM_DESCRIPTION =
49
49
  typeof successCriteriaSchema.description === "string" ? successCriteriaSchema.description : "";
50
50
 
51
- // ── 第 2 层切片:工具 description 模板字面量("description: `" 到闭合反引号)──
51
+ // ── 第 2 层切片:工具 description 模板字面量("description: `" 到结束反引号)──
52
52
 
53
53
  const TOOL_DESCRIPTION = sliceBetween(ADAPTER_SRC, "description: `", "`");
54
54
 
55
55
  // ── 第 3 层切片:promptGuidelines 赋值块 ─────────────────────
56
- // 闭合 "]" 不能用 indexOf 找——块内 guideline 文本自身含 "string[]" 的 "]",
56
+ // 结束 "]" 不能用 indexOf 找——块内 guideline 文本自身含 "string[]" 的 "]",
57
57
  // 故按行扫描 trim() === "]," 的行作块尾(缩进无关)。
58
58
 
59
59
  function extractPromptGuidelinesBlock(src: string): string {
@@ -181,9 +181,9 @@ describe("deserializeState — 旧数据迁移(time_limited → budget_limited
181
181
  expect((state as unknown as { timeWarning90Sent?: unknown }).timeWarning90Sent).toBeUndefined();
182
182
  });
183
183
 
184
- it("归一化后是合法终态:/goal clear 守卫(!isTerminalStatus)跳过 transitionStatus,不 throw", () => {
184
+ it("归一化后是合法终态:/goal clear 检查(!isTerminalStatus)跳过 transitionStatus,不 throw", () => {
185
185
  const state = deserializeState(LEGACY_DATA);
186
- // handleClear(command-adapter.ts)的守卫:!isTerminalStatus 为 false → 直接 clearSession,
186
+ // handleClear(command-adapter.ts)的检查:!isTerminalStatus 为 false → 直接 clearSession,
187
187
  // 不再走 finalizeAndPersist("cancelled") 的 transitionStatus 查表(旧值会 throw)。
188
188
  expect(isTerminalStatus(state.status)).toBe(true);
189
189
  });
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * event-handlers 行为测试 — agent_end / before_agent_start 核心逻辑
3
3
  *
4
- * 覆盖 agent_end 的 continuation + ESC 守卫(FR-6.7)+
4
+ * 覆盖 agent_end 的 continuation + ESC 检查(FR-6.7)+
5
5
  * before_agent_start 的 AUTO_CLEAR/context wrap-up。
6
6
  *
7
7
  * 全解耦后不再测 allTasksDone followUp(原依赖 pi.__todoGetList,已删)。
@@ -100,9 +100,9 @@ function makeRunningState(overrides?: Partial<GoalRuntimeState>): GoalRuntimeSta
100
100
  };
101
101
  }
102
102
 
103
- // ── handleAgentEnd:ESC 守卫 ──────────────────────────
103
+ // ── handleAgentEnd:ESC 检查 ──────────────────────────
104
104
 
105
- describe("handleAgentEnd — FR-6.7 ESC 守卫", () => {
105
+ describe("handleAgentEnd — FR-6.7 ESC 检查", () => {
106
106
  it("aborted=true:不发 continuation、不做 budget 检查、goal 保持 active", async () => {
107
107
  const { pi, calls: piCalls, states } = makeFakePi();
108
108
  const { ctx, calls: ctxCalls } = makeFakeCtx({ aborted: true });
@@ -174,9 +174,9 @@ describe("handleAgentEnd — continuation", () => {
174
174
  expect(sendContext[0]!.content).toContain("[GOAL]");
175
175
  });
176
176
 
177
- // ── pending 守卫:background subagent/workflow 运行时不死循环 ──
177
+ // ── pending 检查:background subagent/workflow 运行时不死循环 ──
178
178
 
179
- it("有活跃 pending → 不发 continuation(pending 守卫生效)", async () => {
179
+ it("有活跃 pending → 不发 continuation(pending 检查生效)", async () => {
180
180
  const { pi, calls: piCalls } = makeFakePi();
181
181
  const { ctx, calls: ctxCalls } = makeFakeCtx({
182
182
  entries: [
@@ -258,7 +258,7 @@ describe("handleAgentEnd — continuation", () => {
258
258
  expect(data?.ids).toEqual(["bg-1", "bg-2"]); // ids 透出
259
259
  });
260
260
 
261
- it("重复 register 同 id → 去重计为 1(seen.has 守卫)", async () => {
261
+ it("重复 register 同 id → 去重计为 1(seen.has 检查)", async () => {
262
262
  const { pi, calls: piCalls, states } = makeFakePi();
263
263
  const { ctx, calls: ctxCalls } = makeFakeCtx({
264
264
  entries: [
@@ -272,7 +272,7 @@ describe("handleAgentEnd — continuation", () => {
272
272
  await handleAgentEnd(pi, session, ctx);
273
273
  const all = allCalls(piCalls, ctxCalls);
274
274
 
275
- // 守卫触发:count>0 不发 continuation
275
+ // 检查触发:count>0 不发 continuation
276
276
  const followUp = all.filter(
277
277
  (c) => c.kind === "sendContext" && (c.payload as { deliverAs?: string } | undefined)?.deliverAs === "followUp",
278
278
  );
@@ -298,7 +298,7 @@ describe("handleAgentEnd — continuation", () => {
298
298
  await handleAgentEnd(pi, session, ctx);
299
299
  const all = allCalls(piCalls, ctxCalls);
300
300
 
301
- // count=0,守卫不触发,走正常 continuation
301
+ // count=0,检查不触发,走正常 continuation
302
302
  const followUp = all.filter(
303
303
  (c) => c.kind === "sendContext" && (c.payload as { deliverAs?: string } | undefined)?.deliverAs === "followUp",
304
304
  );
@@ -2,8 +2,8 @@
2
2
  * goal_control adapter 测试 — handler 用 fake ports
3
3
  *
4
4
  * 覆盖 #3 验收:
5
- * - complete: active 守卫 + evidence 必填 + finalizeAndPersist(status→complete + history)
6
- * - report_blocked: active 守卫 + reason 必填 + tickState + transitionStatus(active→blocked) + persistState
5
+ * - complete: active 检查 + evidence 必填 + finalizeAndPersist(status→complete + history)
6
+ * - report_blocked: active 检查 + reason 必填 + tickState + transitionStatus(active→blocked) + persistState
7
7
  *
8
8
  * 全解耦后不再测 todo 前置检查(checkCompletePrerequisites / buildProgressInput / findIncompleteTodos 已删)。
9
9
  * complete 不做 todo 完成硬检查——AI 自行判断(prompt 软建议)。
@@ -74,7 +74,7 @@ const activeState = (overrides: Partial<GoalRuntimeState> = {}): GoalRuntimeStat
74
74
 
75
75
  // ── handleComplete ───────────────────────────────────
76
76
 
77
- describe("handleComplete — active 守卫 + evidence + finalizeAndPersist", () => {
77
+ describe("handleComplete — active 检查 + evidence + finalizeAndPersist", () => {
78
78
  it("active + evidence → status complete + 写 history", () => {
79
79
  const session = createGoalSession();
80
80
  session.state = activeState();
@@ -117,7 +117,7 @@ describe("handleComplete — active 守卫 + evidence + finalizeAndPersist", ()
117
117
 
118
118
  // ── handleCreate ─────────────────────────────────────
119
119
 
120
- describe("handleCreate — objective/successCriteria 必填(handler 运行时校验)+ slug 真 optional + 非终态守卫 + createGoal", () => {
120
+ describe("handleCreate — objective/successCriteria 必填(handler 运行时校验)+ slug 真 optional + 非终态检查 + createGoal", () => {
121
121
  it("无旧 goal + slug + objective → status active + slug 写入 + 持久化 + notify", () => {
122
122
  const session = createGoalSession();
123
123
  const ports = makeFakePorts();
@@ -175,7 +175,7 @@ describe("handleCreate — objective/successCriteria 必填(handler 运行时
175
175
  });
176
176
 
177
177
  const NON_TERMINAL_STATUSES: GoalRuntimeState["status"][] = ["active", "paused", "blocked"];
178
- it.each(NON_TERMINAL_STATUSES)("已有 %s goal → throw(D25 非终态守卫)", (status) => {
178
+ it.each(NON_TERMINAL_STATUSES)("已有 %s goal → throw(D25 非终态检查)", (status) => {
179
179
  const session = createGoalSession();
180
180
  session.state = activeState({ status });
181
181
  expect(() =>
@@ -232,7 +232,7 @@ describe("handleCreate — objective/successCriteria 必填(handler 运行时
232
232
 
233
233
  // ── handleReportBlocked ──────────────────────────────
234
234
 
235
- describe("handleReportBlocked — active 守卫 + tick + transition + persist", () => {
235
+ describe("handleReportBlocked — active 检查 + tick + transition + persist", () => {
236
236
  it("active + reason → status blocked + reason 记录 + tick 累加", () => {
237
237
  const session = createGoalSession();
238
238
  const past = Date.now() - 4000;
@@ -17,7 +17,7 @@ import { describe, expect, it, vi, beforeEach } from "vitest";
17
17
  import { registerGoalControlTool, type GoalControlDetails } from "../adapters/goal-control-adapter";
18
18
  import { createGoalSession } from "../session";
19
19
 
20
- // ── mock:logger + updateWidget(UI 通道降级用例断言面;其余用例下两者为 no-op,
20
+ // ── mock:logger + updateWidget(UI 通道降级用例断言范围;其余用例下两者为 no-op,
21
21
  // 与真实路径行为等价——fixture 的 setWidget/notify 本就是 no-op)──
22
22
  const loggerWarnSpy = vi.hoisted(() => vi.fn());
23
23
  vi.mock("@zhushanwen/pi-extension-logger", () => ({
@@ -36,6 +36,8 @@ type ExecuteResult = {
36
36
 
37
37
  interface CapturedTool {
38
38
  name: string;
39
+ /** pi SDK 的 exposure 缺省值 "direct"(常驻)——不写字段时 SDK 侧默认(pi-semantics PS-91),本视图可缺省。 */
40
+ exposure?: string;
39
41
  execute: (
40
42
  toolCallId: string,
41
43
  params: Record<string, unknown>,
@@ -123,6 +125,18 @@ function createViaHandler(
123
125
 
124
126
  // ── 测试场景 ─────────────────────────────────────────
125
127
 
128
+ // exposure 重划终态锚(C-ext-05,2026-10-23 ADR-0136):goal_control 保持 direct 常驻 =
129
+ // 注册时不写 exposure 字段(SDK 缺省 direct,pi-semantics PS-91)——误写 "deferred" 会把
130
+ // 目标管理入口从模型默认可见集收走(tool_search 才可激活),此处红。
131
+ describe("goal_control SDK 契约 — exposure 注册终态(C-ext-05)", () => {
132
+ it("direct 常驻:不写 exposure 字段(SDK 缺省 direct)", () => {
133
+ const { pi } = makeFixture("rpc");
134
+ const tool = captureTool(pi);
135
+ expect(tool.name).toBe("goal_control");
136
+ expect(tool.exposure).toBeUndefined();
137
+ });
138
+ });
139
+
126
140
  describe("goal_control execute — RPC 模式 tool result 无 __gui__", () => {
127
141
  it("RPC + 有 budget 的 create → details 无 __gui__ 字段(content 仍正常)", async () => {
128
142
  const { pi, ctx } = makeFixture("rpc");
@@ -150,9 +164,9 @@ describe("goal_control execute — RPC 模式 tool result 无 __gui__", () => {
150
164
  });
151
165
 
152
166
  it("RPC + session.state = null → 前置 handler throw(分支不可达)", async () => {
153
- // execute 路径上,create 总会 set session.state;complete/report_blocked 前置守卫
167
+ // execute 路径上,create 总会 set session.state;complete/report_blocked 前置检查
154
168
  // 要求 session.state 非 null 否则 throw——所以 session.state=null 时不会走到返回路径。
155
- // 验证:全新 session(state=null)下,complete 前置守卫先 throw。
169
+ // 验证:全新 session(state=null)下,complete 前置检查先 throw。
156
170
  const { pi, ctx } = makeFixture("rpc");
157
171
  let captured: CapturedTool | undefined;
158
172
  const capturePi = {
@@ -163,7 +177,7 @@ describe("goal_control execute — RPC 模式 tool result 无 __gui__", () => {
163
177
  } as unknown as ExtensionAPI;
164
178
  registerGoalControlTool(capturePi, createGoalSession());
165
179
  const tool2 = captured!;
166
- // complete 在 state=null 时 throw(handleComplete 前置守卫)
180
+ // complete 在 state=null 时 throw(handleComplete 前置检查)
167
181
  await expect(
168
182
  tool2.execute(
169
183
  "id",
@@ -53,6 +53,32 @@ describe("buildGoalControlSummary", () => {
53
53
  );
54
54
  });
55
55
 
56
+ it("report_blocked reason 含换行 → summary 归一单行(S-1)", () => {
57
+ const details = { action: "report_blocked", goalId: "g1", status: "blocked" } as GoalControlDetails;
58
+ const summary = buildGoalControlSummary(
59
+ { action: "report_blocked", reason: "第一行\n第二段\n\n尾行" } as never,
60
+ details,
61
+ );
62
+ expect(summary).toBe("Blocked: 第一行 第二段 尾行");
63
+ });
64
+
65
+ it("report_blocked reason 含 emoji 超长 → 截断不劈代理对(S-3)", () => {
66
+ const details = { action: "report_blocked", goalId: "g1", status: "blocked" } as GoalControlDetails;
67
+ // "x🎉" = 3 个 UTF-16 码元;UTF-16 slice(0,40) 会在第 40 码元处劈开 🎉 的代理对。
68
+ // 码点口径:50 码点 > 40 → 保留前 40 码点 = 20 组完整 "x🎉"
69
+ const summary = buildGoalControlSummary(
70
+ { action: "report_blocked", reason: "x🎉".repeat(25) } as never,
71
+ details,
72
+ );
73
+ expect(summary).not.toContain("\n");
74
+ // Array.from 按码点切分后,孤立代理项表现为单码元的代理字符
75
+ const loneSurrogates = Array.from(summary).filter(
76
+ (ch) => ch.length === 1 && /[\uD800-\uDFFF]/.test(ch),
77
+ );
78
+ expect(loneSurrogates).toHaveLength(0);
79
+ expect(summary).toBe(`Blocked: ${"x🎉".repeat(20)}…`);
80
+ });
81
+
56
82
  it("report_blocked 无 reason → 省略后缀", () => {
57
83
  const details = { action: "report_blocked", goalId: "g1", status: "blocked" } as GoalControlDetails;
58
84
  expect(buildGoalControlSummary({ action: "report_blocked" } as never, details)).toBe("Blocked");
@@ -2,7 +2,7 @@
2
2
  * index.ts 测试 — 工厂入口 + 跨扩展 API(goalInit slot)
3
3
  *
4
4
  * 覆盖 T1.8 (NFR-AC-8):goalInit 签名 (objective, budget, ctx, slug?, successCriteria?),
5
- * 结构上无法接收 tasks(task CRUD 已删除,D-16/FR-4 双轨消除)。active 守卫由 service.test.ts 覆盖。
5
+ * 结构上无法接收 tasks(task CRUD 已删除,D-16/FR-4 双轨消除)。active 检查由 service.test.ts 覆盖。
6
6
  *
7
7
  * 用最小 fake pi + fake ctx 实例化 goalExtension 工厂,再从 globalThis slot 读取 goalInit——
8
8
  * mock 形态与真实通道同构(goal-bridge-cross-extension.md §3.4:桥的元教训是 mock 世界
@@ -2,7 +2,7 @@
2
2
  * engine/liveness.ts 纯函数测试 — 轮次活性熔断判定单元(chat-domain-v1x D4 / W5)
3
3
  *
4
4
  * 覆盖:env 配置解析(合法/非法/缺省)、退避等级数学(第 N 轮起 ×2、指数天花板)、
5
- * 进展信号边界、toolCall 块计数(duck-typed 守卫)、pending 集合变化判定。
5
+ * 进展信号边界、toolCall 块计数(duck-typed 检查)、pending 集合变化判定。
6
6
  * 行为级四断言(封顶/退避/恢复/defer 去重经 handleAgentEnd)见 circuit-breaker.test.ts。
7
7
  */
8
8
  import { describe, expect, it } from "vitest";
@@ -1,7 +1,7 @@
1
1
  /**
2
- * message_end handler 测试(D13① role 过滤 + FR-6.7 ESC 守卫)。
2
+ * message_end handler 测试(D13① role 过滤 + FR-6.7 ESC 检查)。
3
3
  *
4
- * 断言面:仅 user/assistant 的 message_end 进入 service 消费;system(pi 1.0
4
+ * 断言范围:仅 user/assistant 的 message_end 进入 service 消费;system(pi 1.0
5
5
  * 新形态)/ toolResult / 缺 role 在 handler 层显式拒绝;assistant 正常透传。
6
6
  */
7
7
 
@@ -73,7 +73,7 @@ describe("handleMessageEnd(D13① role 过滤)", () => {
73
73
  expect(spy).not.toHaveBeenCalled();
74
74
  });
75
75
 
76
- it("ESC 守卫先于 role 过滤(aborted 直接返回)", async () => {
76
+ it("ESC 检查先于 role 过滤(aborted 直接返回)", async () => {
77
77
  const session = makeSession();
78
78
  const spy = vi.mocked(applyEvent);
79
79
  await handleMessageEnd(session, ctx(true), messageEndEvent("assistant"));
@@ -6,7 +6,7 @@
6
6
  * - tui 模式:text 臂透传(pi 原生文本行,无 marker)
7
7
  * - undefined:清除语义(模式无关)
8
8
  *
9
- * 模式分派守卫单点在 protocol helpers.ts(守卫单点化),本测试只验证委托接线。
9
+ * 模式分派检查单点在 protocol helpers.ts(检查单点化),本测试只验证委托接线。
10
10
  *
11
11
  * 运行:cd extensions/universal/goal && npx vitest run src/__tests__/ports.test.ts
12
12
  */
@@ -123,7 +123,7 @@ describe("finalizeGoal — history 写入矩阵", () => {
123
123
  const ports = makeFakePorts();
124
124
  const state = makeState();
125
125
  state.status = "complete";
126
- // transitionStatus 查表:终态不可转,调用方须先 isTerminalStatus 守卫
126
+ // transitionStatus 查表:终态不可转,调用方须先 isTerminalStatus 检查
127
127
  expect(() => finalizeGoal(state, "cancelled", ports)).toThrow();
128
128
  expect(state.status).toBe("complete");
129
129
  });
@@ -175,7 +175,7 @@ describe("applyEvent — 简单事件", () => {
175
175
  usage: { input: 100, output: 50, cacheRead: 20 },
176
176
  },
177
177
  });
178
- // 回归修复:blocked 状态不累加 token(原 bug:缺 isActiveStatus 守卫)
178
+ // 回归修复:blocked 状态不累加 token(原 bug:缺 isActiveStatus 检查)
179
179
  expect(session.state.tokensUsed).toBe(before);
180
180
  });
181
181
 
@@ -353,7 +353,7 @@ function handleUpdate(
353
353
  * 这样 goal 创建的唯一路径是 goal_control toolcall(统一入口),
354
354
  * slug/objective/budget 都由 AI 在 toolcall 时决定。
355
355
  *
356
- * D25 守卫仍在此处预检:非终态旧 goal(active/paused/blocked)→ 拒绝,
356
+ * D25 检查仍在此处预检:非终态旧 goal(active/paused/blocked)→ 拒绝,
357
357
  * 提示 /goal resume 或 /goal clear(避免 AI 在已有未完成 goal 时重复创建)。
358
358
  *
359
359
  * budget flag(--tokens)写入消息体,让 AI 原样传给 create。
@@ -1,10 +1,10 @@
1
1
  /**
2
- * 事件 6: agent_end(FR-8.7 + ESC 守卫 + 并发保护)。
2
+ * 事件 6: agent_end(FR-8.7 + ESC 检查 + 并发保护)。
3
3
  *
4
4
  * 关键约定:
5
5
  * - FR-8.2 G-021 防重入:session.isProcessing 入口加锁,finally 释放
6
6
  * - FR-8.2 G-020 stale 快照:入口 makeStaleChecker snapshot goalId,每个副作用前 checkStale
7
- * - FR-6.7 ESC 守卫(最关键):ctx.signal?.aborted → 不发 continuation、不做 budget 检查、
7
+ * - FR-6.7 ESC 检查(最关键):ctx.signal?.aborted → 不发 continuation、不做 budget 检查、
8
8
  * 不做任何状态变更,goal 保持 active,等用户下次输入恢复
9
9
  *
10
10
  * #8 后流程:budget 预警/steering → continuation 去抖 + 轮次活性熔断(W5:
@@ -15,7 +15,7 @@
15
15
  * 全解耦:不再做 allTasksDone followUp(原依赖 pi.__todoGetList,跨 ext 失效)。
16
16
  * todo 是否全完成由 AI 自行判断(prompt 软建议)。
17
17
  *
18
- * ESC 路径:aborted 时直接 return(goal 保持 active)。注意 ESC 守卫在终态/非 active
18
+ * ESC 路径:aborted 时直接 return(goal 保持 active)。注意 ESC 检查在终态/非 active
19
19
  * 检查之后——终态 goal 仍走终态 notify(不被 ESC 影响),非 active 状态直接返回。
20
20
  */
21
21
 
@@ -63,7 +63,7 @@ export async function handleAgentEnd(
63
63
  }
64
64
  if (!isActiveStatus(session.state.status)) return;
65
65
 
66
- // FR-6.7 ESC 守卫(最关键):aborted 时 goal 保持 active,不做任何副作用
66
+ // FR-6.7 ESC 检查(最关键):aborted 时 goal 保持 active,不做任何副作用
67
67
  if (ctx.signal?.aborted) {
68
68
  return;
69
69
  }
@@ -152,7 +152,7 @@ async function handleBudgetChecks(
152
152
  * - W5 主判据:continuation 连发总次数封顶(默认 50,PI_GOAL_CONTINUATION_CAP)——
153
153
  * 封顶必停发 + notify 用户 + goal 保持 active(不擅自终态)。与「是否调工具」
154
154
  * 正交:计数只在 deliverContinuation 实发时 +1,绝不被工具调用清零
155
- * (2026-09-08 事故:pending 守卫击穿后 64 分钟 ~320 turn 空转,无任何熔断)。
155
+ * (2026-09-08 事故:pending 检查击穿后 64 分钟 ~320 turn 空转,无任何熔断)。
156
156
  * - W5 辅判据:连续 N 轮(默认 5)无工具调用且 tokenDelta 低于阈值 → continuation
157
157
  * 延迟 base×2^level 发出(间隔 ×2 递增);真实进展清零退避计数(不动总次数)。
158
158
  * - W5 defer 通知去重:pending 集合不变的连续 defer 轮不重复 notify。
@@ -188,7 +188,7 @@ async function handleContinuation(
188
188
  isProgressSignal(toolDelta, tokenDelta, cfg),
189
189
  );
190
190
 
191
- // pending 守卫:有活跃 background subagent/workflow 时不发 continuation。
191
+ // pending 检查:有活跃 background subagent/workflow 时不发 continuation。
192
192
  // 靠 subagent/workflow 完成时 sendMessage({triggerTurn:true},缺省 deliverAs 即 steer) 自然唤醒主 agent;
193
193
  // goal 抢先催会与异步任务完成通知叠加,形成“停不下来”的死循环——continuation 排入
194
194
  // _followUpMessages 队列 → _handlePostAgentRun 的 hasQueuedMessages() 返回 true → agent.continue()
@@ -196,8 +196,8 @@ async function handleContinuation(
196
196
  // 刻意不校验 expiresAt:长任务 subagent(>1h TTL)完成时仍 triggerTurn 唤醒主 agent,
197
197
  // 按 TTL 判非活跃会让死循环在长任务场景复现。
198
198
  // [W4 读侧过滤①消费口] 传当前 session 基准:fork 继承的父级注册残留不进差集
199
- // (守卫不幻 defer);设计 D4「读侧过滤一刀」在本消费口的落地(W6 探针实测
200
- // registry-fork-filter.test.ts 证不传则虚增,A9② 验收口径)。
199
+ // (检查不幻 defer);设计 D4「读侧过滤一刀」在本消费口的落地(W6 探针实测
200
+ // registry-fork-filter.test.ts 证不传则虚增,A9② 验收标准)。
201
201
  const pendingOps = countActiveFromEntries(entries, {
202
202
  currentSessionId: ctx.sessionManager.getSessionId(),
203
203
  });
@@ -239,7 +239,7 @@ async function handleContinuation(
239
239
  // 空 turn:已 persist,不发 continuation
240
240
  return;
241
241
  }
242
- // 终态守卫:persistAndUpdate 可能把 goal 转为 budget_limited 终态。
242
+ // 终态检查:persistAndUpdate 可能把 goal 转为 budget_limited 终态。
243
243
  // 此时不应发 continuation(deliverAs:"followUp" 会触发新 turn,让已耗尽预算的 agent 再跑一轮)。
244
244
  if (isTerminalStatus(state.status)) return;
245
245
 
@@ -319,11 +319,11 @@ async function handleContinuation(
319
319
  }
320
320
 
321
321
  /**
322
- * 退避 timer 到期后的发射前守卫 + 实发(延迟期间状态可能已变):
322
+ * 退避 timer 到期后的发射前检查 + 实发(延迟期间状态可能已变):
323
323
  * - goal 未被覆盖/清除(goalId 比对)、仍 active
324
- * - MF-6① idle 守卫:ctx.signal 为实时求值(pi-agent-core agent.js get signal 返回
324
+ * - MF-6① idle 检查:ctx.signal 为实时求值(pi-agent-core agent.js get signal 返回
325
325
  * activeRun?.abortController.signal),agent idle 后恒 undefined,`signal?.aborted`
326
- * 恒放行、不可作守卫;改用 ctx.isIdle()(SDK types.d.ts L232)——延迟窗内有活动
326
+ * 恒放行、不可作检查;改用 ctx.isIdle()(SDK types.d.ts L232)——延迟窗内有活动
327
327
  * turn 时不发,该轮 agent_end 会按最新状态重新决策
328
328
  * - 未封顶、无活跃 pending(等待期新出现的后台任务由其完成通知唤醒,goal 不抢先催)
329
329
  */
@@ -8,7 +8,7 @@
8
8
  * 2. ADR-002:Context 使用率 > 85% → 保持 active + 注入 wrap-up 指令
9
9
  * 3. 正常:注入 contextInjectionPrompt
10
10
  *
11
- * 无 ESC 守卫(before_agent_start 是 agent 开始前的信号,此时无 aborted 可能)。
11
+ * 无 ESC 检查(before_agent_start 是 agent 开始前的信号,此时无 aborted 可能)。
12
12
  *
13
13
  * 全解耦:不再做 staleness 检测(原依赖 pi.__todoGetList,跨 ext 失效),
14
14
  * 不再探测 plan extension(原 typeof pi.__planStart,跨 ext 失效)。
@@ -1,7 +1,7 @@
1
1
  /**
2
- * 事件 3: message_end(FR-6.7 ESC 守卫 + token 累加)。
2
+ * 事件 3: message_end(FR-6.7 ESC 检查 + token 累加)。
3
3
  *
4
- * FR-6.7 ESC 守卫:ctx.signal.aborted 时跳过 token 累加。
4
+ * FR-6.7 ESC 检查:ctx.signal.aborted 时跳过 token 累加。
5
5
  * FR-8.6: token 累加算法(委托 service.applyEvent("message_end"))。
6
6
  *
7
7
  * role 过滤(D13①,rename-session 先例同形态):仅 user/assistant 的 message_end
@@ -37,7 +37,7 @@ export async function handleMessageEnd(
37
37
  event: MessageEndEvent,
38
38
  ): Promise<void> {
39
39
  if (!session.state) return;
40
- // FR-6.7 ESC 守卫
40
+ // FR-6.7 ESC 检查
41
41
  if (ctx.signal?.aborted) return;
42
42
 
43
43
  // D13① role 过滤:仅对话轴两形态(AgentMessage role 域为开放联合,宽比较)
@@ -1,5 +1,5 @@
1
1
  /**
2
- * 事件 7: session_shutdown(MF-R2-1 根修:invalidate 前取消旧退避 timer)。
2
+ * 事件 7: session_shutdown(MF-R2-1 从根源修复:invalidate 前取消旧退避 timer)。
3
3
  *
4
4
  * SDK 触发序(@earendil-works/pi-coding-agent 0.84.4 实装核对)——所有使旧
5
5
  * runner 失效的路径都先 emit session_shutdown 再 invalidate,此刻 pi/ctx 仍可用:
@@ -1,7 +1,7 @@
1
1
  /**
2
- * 事件 2: turn_end(FR-6.7 ESC 守卫 + 递增)。
2
+ * 事件 2: turn_end(FR-6.7 ESC 检查 + 递增)。
3
3
  *
4
- * FR-6.7 ESC 守卫:ctx.signal.aborted 时跳过递增(ESC 不算 goal turn)。
4
+ * FR-6.7 ESC 检查:ctx.signal.aborted 时跳过递增(ESC 不算 goal turn)。
5
5
  * 正常路径:currentTurnIndex++ + updateWidget。
6
6
  *
7
7
  * 委托 service.applyEvent("turn_end") 递增 currentTurnIndex(H3:applyEvent 改 void,
@@ -23,7 +23,7 @@ export async function handleTurnEnd(
23
23
  ctx: ExtensionContext,
24
24
  ): Promise<void> {
25
25
  if (!session.state) return;
26
- // FR-6.7 ESC 守卫
26
+ // FR-6.7 ESC 检查
27
27
  if (ctx.signal?.aborted) return;
28
28
 
29
29
  const ports = buildPorts(pi, ctx);