@zhushanwen/pi-goal 0.15.0 → 0.16.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 (34) hide show
  1. package/package.json +5 -4
  2. package/src/__tests__/active-path.test.ts +2 -2
  3. package/src/__tests__/circuit-breaker.test.ts +10 -10
  4. package/src/__tests__/criteria-array-prompt-guidance.test.ts +2 -2
  5. package/src/__tests__/deserialize-state.test.ts +2 -2
  6. package/src/__tests__/event-adapter.test.ts +8 -8
  7. package/src/__tests__/goal-control-adapter.test.ts +6 -6
  8. package/src/__tests__/goal-control-rpc.test.ts +4 -4
  9. package/src/__tests__/index.test.ts +1 -1
  10. package/src/__tests__/liveness.test.ts +1 -1
  11. package/src/__tests__/message-end.test.ts +3 -3
  12. package/src/__tests__/ports.test.ts +1 -1
  13. package/src/__tests__/service.test.ts +2 -2
  14. package/src/adapters/command-adapter.ts +1 -1
  15. package/src/adapters/event-handlers/agent-end.ts +12 -12
  16. package/src/adapters/event-handlers/before-agent-start.ts +1 -1
  17. package/src/adapters/event-handlers/message-end.ts +3 -3
  18. package/src/adapters/event-handlers/session-shutdown.ts +1 -1
  19. package/src/adapters/event-handlers/turn-end.ts +3 -3
  20. package/src/adapters/goal-control-adapter.ts +11 -11
  21. package/src/adapters/ports.ts +2 -2
  22. package/src/constants.ts +1 -1
  23. package/src/engine/__tests__/budget.test.ts +1 -1
  24. package/src/engine/__tests__/goal.test.ts +64 -10
  25. package/src/engine/goal.ts +82 -6
  26. package/src/engine/liveness.ts +2 -2
  27. package/src/engine/types.ts +0 -13
  28. package/src/index.ts +3 -3
  29. package/src/persistence.ts +1 -1
  30. package/src/ports.ts +2 -2
  31. package/src/projection/__tests__/widget.test.ts +3 -3
  32. package/src/projection/widget.ts +2 -2
  33. package/src/service.ts +4 -4
  34. package/src/session.ts +5 -5
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.0",
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", () => ({
@@ -150,9 +150,9 @@ describe("goal_control execute — RPC 模式 tool result 无 __gui__", () => {
150
150
  });
151
151
 
152
152
  it("RPC + session.state = null → 前置 handler throw(分支不可达)", async () => {
153
- // execute 路径上,create 总会 set session.state;complete/report_blocked 前置守卫
153
+ // execute 路径上,create 总会 set session.state;complete/report_blocked 前置检查
154
154
  // 要求 session.state 非 null 否则 throw——所以 session.state=null 时不会走到返回路径。
155
- // 验证:全新 session(state=null)下,complete 前置守卫先 throw。
155
+ // 验证:全新 session(state=null)下,complete 前置检查先 throw。
156
156
  const { pi, ctx } = makeFixture("rpc");
157
157
  let captured: CapturedTool | undefined;
158
158
  const capturePi = {
@@ -163,7 +163,7 @@ describe("goal_control execute — RPC 模式 tool result 无 __gui__", () => {
163
163
  } as unknown as ExtensionAPI;
164
164
  registerGoalControlTool(capturePi, createGoalSession());
165
165
  const tool2 = captured!;
166
- // complete 在 state=null 时 throw(handleComplete 前置守卫)
166
+ // complete 在 state=null 时 throw(handleComplete 前置检查)
167
167
  await expect(
168
168
  tool2.execute(
169
169
  "id",
@@ -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);
@@ -4,9 +4,9 @@
4
4
  * #3:替代已删除的 goal_manager tool。
5
5
  *
6
6
  * 职责分层:
7
- * - execute 层(adapter 职责):signal 守卫
7
+ * - execute 层(adapter 职责):signal 检查
8
8
  * - handler 层(goal 业务,契约对齐 code-architecture §3 handleCreate/handleComplete/handleReportBlocked):
9
- * active 守卫 + evidence/reason 空串校验 + 状态转换
9
+ * active 检查 + evidence/reason 空串校验 + 状态转换
10
10
  *
11
11
  * 全解耦:goal 不再读 todo/plan 状态。complete 不做 todo 完成前置硬检查——
12
12
  * todo 是否全完成由 AI 自行判断,goal 仅通过 prompt 软建议(见 prompts.ts)。
@@ -176,7 +176,7 @@ export function buildGoalControlSummary(
176
176
 
177
177
  /**
178
178
  * create 业务逻辑:objective + successCriteria 必填(schema 层已强制,此处仅校验空串)
179
- * + 非终态旧 goal 守卫 + service.createGoal。
179
+ * + 非终态旧 goal 检查 + service.createGoal。
180
180
  *
181
181
  * slug:AI 生成的短标识,仅 widget 标题 + history 用,不注入 prompt。真 optional。
182
182
  * objective:完整描述,注入每轮 context prompt(保证方向感)。
@@ -184,9 +184,9 @@ export function buildGoalControlSummary(
184
184
  * 全解耦:不读 todo/plan。toolcall 时 AI 已在 turn 中,**不**发消息注入
185
185
  * (AI 返回后自行续跑,对齐 Codex create_goal)。
186
186
  *
187
- * 守卫用 D25 严格语义:非终态 active/paused/blocked 全挡,提示用 /goal resume 或
187
+ * 检查用 D25 严格语义:非终态 active/paused/blocked 全挡,提示用 /goal resume 或
188
188
  * /goal clear——防 AI 静默覆盖含未完成工作的 goal。
189
- * 终态旧 goal 走 createGoal 快速路径覆盖(createGoal 内部 active 守卫,终态可覆盖)。
189
+ * 终态旧 goal 走 createGoal 快速路径覆盖(createGoal 内部 active 检查,终态可覆盖)。
190
190
  *
191
191
  * 参数校验拆分至 requireCreateObjective / requireCreateSuccessCriteria /
192
192
  * resolveCreateBudget(圈复杂度拆分),校验顺序与错误消息不变。
@@ -247,7 +247,7 @@ export function handleCreate(
247
247
  const slugInput = params.slug?.trim();
248
248
  const successCriteria = requireCreateSuccessCriteria(params);
249
249
 
250
- // D25 严格守卫:非终态旧 goal(active/paused/blocked)→ 拒绝创建(防静默覆盖未完成工作)
250
+ // D25 严格检查:非终态旧 goal(active/paused/blocked)→ 拒绝创建(防静默覆盖未完成工作)
251
251
  if (session.state && !isTerminalStatus(session.state.status)) {
252
252
  throw new Error(
253
253
  `Goal already active (status: ${session.state.status}). Use /goal resume to continue or /goal clear to reset before creating a new one.`,
@@ -260,7 +260,7 @@ export function handleCreate(
260
260
  // FR-3.1: 唯一创建入口。终态旧 goal 走覆盖快速路径。
261
261
  const created = createGoal(session, objective, budget, ports, slugInput, successCriteria);
262
262
  if (!created) {
263
- // createGoal 内部 active 守卫兜底(理论上上面守卫已挡;防御性)
263
+ // createGoal 内部 active 检查兜底(理论上上面检查已挡;防御性)
264
264
  throw new Error("Goal already active. Cannot create a new one.");
265
265
  }
266
266
 
@@ -278,7 +278,7 @@ export function handleCreate(
278
278
  }
279
279
 
280
280
  /**
281
- * complete 业务逻辑:active 守卫 + evidence 空串校验 + finalizeAndPersist。
281
+ * complete 业务逻辑:active 检查 + evidence 空串校验 + finalizeAndPersist。
282
282
  *
283
283
  * 全解耦后不再做 todo 完成前置检查——todo 是否全完成由 AI 自行判断(prompt 软建议)。
284
284
  */
@@ -313,7 +313,7 @@ export function handleComplete(
313
313
  }
314
314
 
315
315
  /**
316
- * report_blocked 业务逻辑:active 守卫 + reason 空串校验 + tickState + transitionStatus + persistState。
316
+ * report_blocked 业务逻辑:active 检查 + reason 空串校验 + tickState + transitionStatus + persistState。
317
317
  *
318
318
  * 必须在 transitionStatus **之前** tickState,使 tick 看到 active 状态并累加当前运行段;
319
319
  * 否则转 blocked 后 persistState 内部的 tick 因 status≠active 不累加,丢失最后一段运行时间。
@@ -352,7 +352,7 @@ export function handleReportBlocked(
352
352
  return { action: "report_blocked", goalId: state.goalId, status: state.status };
353
353
  }
354
354
 
355
- /** renderResult 的 result 是否含 details 字段(类型守卫,替代全可选结构断言 as {details?})。
355
+ /** renderResult 的 result 是否含 details 字段(类型检查,替代全可选结构断言 as {details?})。
356
356
  * 收紧:除检查 "details" in r 外,还验证其值为 object 或 undefined(防 details 是 string/number
357
357
  * 时下游读 d.status 得到 undefined 却被类型系统当作 GoalControlDetails)。 */
358
358
  function hasGoalDetails(r: unknown): r is { details?: GoalControlDetails } {
@@ -435,7 +435,7 @@ export function registerGoalControlTool(pi: ExtensionAPI, session: GoalSession):
435
435
 
436
436
  renderCall(args: Record<string, unknown>, theme: Theme): Text {
437
437
  // TUI 渲染先于 schema 校验(args 是未经 schema 收窄的原始形态):非对象整体
438
- // 走安全占位(不抛错);action/slug 逐个 typeof 守卫替代裸断言——非法 action
438
+ // 走安全占位(不抛错);action/slug 逐个 typeof 检查替代裸断言——非法 action
439
439
  // 落 report_blocked 回落分支(与原行为一致)
440
440
  if (typeof args !== "object" || args === null) {
441
441
  return new Text(
@@ -42,8 +42,8 @@ export function buildPorts(pi: ExtensionAPI, ctx: ExtensionContext): ServicePort
42
42
  const uiPort: UiPort = {
43
43
  setWidget(name, content): void {
44
44
  // 双模 payload 委托 protocol setWidgetDual:推送/清屏 × GUI/TUI 的模式分派
45
- // 由 helper 单点内化(守卫单点化说明见 extension-protocol helpers.ts),
46
- // 本 adapter 不再自持 isGui 判定/守卫注释。
45
+ // 由 helper 单点内化(检查单点化说明见 extension-protocol helpers.ts),
46
+ // 本 adapter 不再自持 isGui 判定/检查注释。
47
47
  //
48
48
  // 断言根因:pi ExtensionUIContext.custom 是泛型方法(返回 Promise<T>),
49
49
  // 与 GuiContext.ui.custom 的具体返回类型静态不兼容,但 mode/hasUI/ui 其余
package/src/constants.ts CHANGED
@@ -38,7 +38,7 @@ export const MAX_HISTORY_ENTRIES = 20; // goal-history entry GC 上限
38
38
 
39
39
  // ── 轮次活性熔断(chat-domain-v1x D4 / W5)─────────────
40
40
  //
41
- // 事故背景(2026-09-08,64 分钟 ~320 turn 空转烧 2.93M token):pending 守卫被
41
+ // 事故背景(2026-09-08,64 分钟 ~320 turn 空转烧 2.93M token):pending 检查被
42
42
  // 「等待已注销的后台任务」击穿后,goal 每轮注入 continuation、主 agent 每轮回一句
43
43
  // 等待,无任何无进展熔断。双维度封顶:主判据总量上限(与是否调工具正交,不可被
44
44
  // 任何目标行为绕过)+ 辅判据无进展退避(间隔 ×2 递增)。全部可经 PI_GOAL_* env 覆盖。
@@ -125,7 +125,7 @@ describe("getBudgetSeverity", () => {
125
125
  const [r1, r2] = a <= b ? [a, b] : [b, a];
126
126
  return RANK[getBudgetSeverity(r1)] <= RANK[getBudgetSeverity(r2)];
127
127
  });
128
- // 三分区 property 钉死阈值边界(捕捉 0.9/0.7 被改)
128
+ // 三分区 property 固定阈值边界(捕捉 0.9/0.7 被改)
129
129
  it.prop([f64(0.9, 1)])("r ≥ 0.9 → danger", (r) => getBudgetSeverity(r) === "danger");
130
130
  it.prop([f64(0.7, 0.9).filter((r) => r < 0.9)])("0.7 ≤ r < 0.9 → warn", (r) => getBudgetSeverity(r) === "warn");
131
131
  it.prop([f64(0, 0.7).filter((r) => r < 0.7)])("r < 0.7 → ok", (r) => getBudgetSeverity(r) === "ok");
@@ -1,8 +1,11 @@
1
1
  /**
2
2
  * engine/goal.ts 测试 — Goal 状态机 + createGoalState
3
3
  *
4
- * transitionStatus 完备性用 fast-check 覆盖 GoalStatus×GoalStatus 全 36 组合
5
- * (不 throw ⟺ to ∈ VALID_TRANSITIONS[from]),替代手写合法 9 + 非法 25 = 34 条枚举。
4
+ * transitionStatus 完备性两层:fast-check 抽样(不 throw ⟺ (from,to) ∈ 期望合法边表)
5
+ * + (from,target) 6×6 穷举基线(M3 迁移对账矩阵,36 组合全跑)。期望边表硬编码自
6
+ * 领域语义、独立于实现(不 import 实现侧转移表——M3 迁移前后同一期望表对账)。
7
+ * 机器表级另设 generateTableTests 穷举生成组(D5/D6 每步判据第二款)——直测
8
+ * GOAL_MACHINE 全状态×全事件,与包装层基线互为独立防线。
6
9
  */
7
10
  import { describe, expect } from "vitest";
8
11
  import { it, fc } from "@fast-check/vitest";
@@ -10,8 +13,8 @@ import { readdirSync, readFileSync } from "node:fs";
10
13
  import { join } from "node:path";
11
14
  import { fileURLToPath } from "node:url";
12
15
 
13
- import { createGoalState, isActiveStatus, isTerminalStatus, transitionStatus } from "../goal";
14
- import { VALID_TRANSITIONS } from "../types";
16
+ import { generateTableTests } from "@zhushanwen/state-machine/testing";
17
+ import { GOAL_MACHINE, createGoalState, isActiveStatus, isTerminalStatus, transitionStatus } from "../goal";
15
18
  import type { GoalStatus } from "../types";
16
19
 
17
20
  const STATUS_VALUES: GoalStatus[] = ["active", "paused", "blocked", "complete", "budget_limited", "cancelled"];
@@ -20,6 +23,21 @@ const statusArb = fc.constantFrom(...STATUS_VALUES);
20
23
  const TERMINAL: ReadonlySet<GoalStatus> = new Set(["complete", "budget_limited", "cancelled"]);
21
24
  const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
22
25
 
26
+ // 期望合法边表(M3 迁移基线 oracle):9 条合法边硬编码自领域语义——
27
+ // 用户 pause/resume/cancel、agent report_blocked/complete、token 预算耗尽。
28
+ const EXPECTED_LEGAL_EDGES: ReadonlyArray<readonly [GoalStatus, GoalStatus]> = [
29
+ ["active", "paused"],
30
+ ["active", "blocked"],
31
+ ["active", "complete"],
32
+ ["active", "budget_limited"],
33
+ ["active", "cancelled"],
34
+ ["paused", "active"],
35
+ ["paused", "cancelled"],
36
+ ["blocked", "active"],
37
+ ["blocked", "cancelled"],
38
+ ];
39
+ const expectedLegalEdge = new Set(EXPECTED_LEGAL_EDGES.map(([from, to]) => `${from}→${to}`));
40
+
23
41
  describe("isTerminalStatus", () => {
24
42
  it.prop([statusArb])("⟺ 在 {complete, budget_limited, cancelled} 内", (s) => isTerminalStatus(s) === TERMINAL.has(s));
25
43
  });
@@ -29,9 +47,9 @@ describe("isActiveStatus", () => {
29
47
  });
30
48
 
31
49
  describe("transitionStatus", () => {
32
- // ⭐ 完备性:自动枚举全 36 组合,不 throw ⟺ to ∈ VALID_TRANSITIONS[from]
33
- it.prop([statusArb, statusArb])("完备性:不 throw ⟺ to ∈ VALID_TRANSITIONS[from]", (from, to) => {
34
- const inTable = VALID_TRANSITIONS[from].includes(to);
50
+ // ⭐ 完备性(抽样):不 throw ⟺ (from, to) ∈ 期望合法边表(独立 oracle,非循环)
51
+ it.prop([statusArb, statusArb])("完备性:不 throw ⟺ (from,to) ∈ 期望合法边表", (from, to) => {
52
+ const inTable = expectedLegalEdge.has(`${from}→${to}`);
35
53
  let threw = false;
36
54
  try {
37
55
  transitionStatus(from, to);
@@ -49,6 +67,42 @@ describe("transitionStatus", () => {
49
67
  });
50
68
  });
51
69
 
70
+ // ── M3 迁移基线(V7 对账矩阵):(from,target) 6×6 穷举 ──────────────
71
+ // 迁移前测存量邻接表实现(基线),迁移后测机器包装(to 等价 + throw 形态);
72
+ // 期望合法边表(expectedLegalEdge,文件顶部)迁移前后同一份。
73
+
74
+ describe("transitionStatus (from,target) 6×6 穷举基线(V7 对账矩阵)", () => {
75
+ for (const from of STATUS_VALUES) {
76
+ for (const target of STATUS_VALUES) {
77
+ const legal = expectedLegalEdge.has(`${from}→${target}`);
78
+ it(`${from} → ${target}:${legal ? `合法,返回 ${target}` : "非法,throw"}`, () => {
79
+ if (legal) {
80
+ expect(transitionStatus(from, target)).toBe(target);
81
+ } else {
82
+ expect(() => transitionStatus(from, target)).toThrow();
83
+ }
84
+ });
85
+ }
86
+ }
87
+ });
88
+
89
+ // ── 机器级穷举生成组(D5/D6 每步判据第二款):全状态 × 全事件 ──────────
90
+ // generateTableTests(GOAL_MACHINE) 直测机器表(词表/表行误改、terminal
91
+ // 拒绝语义),与上方包装层 6×6(transitionStatus 行为对账)互为独立防线。
92
+ // 必须在收集阶段(模块顶层)调用——describe/it 不能在测试回调内注册。
93
+ const generatedCases = generateTableTests(GOAL_MACHINE);
94
+
95
+ describe("GOAL_MACHINE 穷举覆盖对账", () => {
96
+ it("全组合 36(6 态 × 6 事件)= 合法 9 + 表外 27 + 不变量破坏 0", () => {
97
+ // 合法 9 ⟺ 上方 EXPECTED_LEGAL_EDGES 的 9 条 (from,to) 边(本表
98
+ // from×event→to 单射,两维度同源对账);覆盖数对账防生成器静默漏覆盖。
99
+ expect(generatedCases).toHaveLength(36);
100
+ expect(generatedCases.filter((c) => c.expectation === "transition")).toHaveLength(9);
101
+ expect(generatedCases.filter((c) => c.expectation === "rejected")).toHaveLength(27);
102
+ expect(generatedCases.filter((c) => c.expectation === "invariant-broken")).toHaveLength(0);
103
+ });
104
+ });
105
+
52
106
  describe("createGoalState", () => {
53
107
  it("默认值:status active + 计数器清零 + 预警 flag false", () => {
54
108
  expect(createGoalState("my obj")).toMatchObject({
@@ -87,9 +141,9 @@ describe("createGoalState", () => {
87
141
  });
88
142
  });
89
143
 
90
- // ── E1 守卫:状态变更唯一执行点(.status 裸赋值扫描)──
144
+ // ── E1 检查:状态变更唯一执行点(.status 裸赋值扫描)──
91
145
 
92
- describe("E1 守卫:src 内禁止 .status 裸赋值(必须走 transitionStatus 查表)", () => {
146
+ describe("E1 检查:src 内禁止 .status 裸赋值(必须走 transitionStatus 查表)", () => {
93
147
  it("扫描 src/**/*.ts(排除 __tests__):0 命中", () => {
94
148
  // 正则逐字来自设计定稿,不得自行修改:
95
149
  // (?![=>]) 排除 ==/===/=>(比较与箭头函数形态);(?!\s*transitionStatus\b) 放行白名单查表赋值。
@@ -118,7 +172,7 @@ describe("E1 守卫:src 内禁止 .status 裸赋值(必须走 transitionStat
118
172
  walk(srcDir);
119
173
  expect(
120
174
  offenders,
121
- `检测到 ${offenders.length} 处 .status 裸赋值(状态变更必须走 transitionStatus 查表,见设计 §5.4 E1 / VALID_TRANSITIONS engine/types.ts):\n`
175
+ `检测到 ${offenders.length} 处 .status 裸赋值(状态变更必须走 transitionStatus 查表,见设计 §5.4 E1 / goal 状态机声明 engine/goal.ts):\n`
122
176
  + offenders.join("\n"),
123
177
  ).toEqual([]);
124
178
  });
@@ -4,8 +4,76 @@
4
4
  * 零 Pi 依赖。import from "./types"。
5
5
  */
6
6
 
7
+ import { defineStateMachine } from "@zhushanwen/state-machine";
8
+
7
9
  import type { BudgetConfig, GoalRuntimeState, GoalStatus } from "./types";
8
- import { DEFAULT_BUDGET, TERMINAL_GOAL_STATUSES, VALID_TRANSITIONS } from "./types";
10
+ import { DEFAULT_BUDGET, TERMINAL_GOAL_STATUSES } from "./types";
11
+
12
+ // ── Goal 状态机声明(@zhushanwen/state-machine 行式表,M3 迁移)────────
13
+ // 事件词表从 transitionStatus 调用点目标态语义派生(D6 M3):
14
+ // - pause / report_blocked / hit_budget_limit / complete / cancel = 用户或 agent
15
+ // 的领域动作(/goal pause、goal_control report_blocked、token 预算耗尽、
16
+ // goal_control complete、/goal clear);
17
+ // - cancel 与 resume 各覆盖多源态(clear/resume 对 paused、blocked 对称)。
18
+ // 终态标志与 TERMINAL_GOAL_STATUSES(types.ts,域查询用)是同一事实的两处
19
+ // 声明。表行为由两层穷举锁定(__tests__/goal.test.ts):机器级
20
+ // generateTableTests 全状态×全事件生成组(D5/D6)+ 包装层 (from,target)
21
+ // 6×6 基线(V7 对账矩阵)。新增状态或转移时必须同步更新此表与两层基线
22
+ // (forcing function)。
23
+
24
+ /** Goal 机器事件词表(类型层与下方 events 声明键集互为对账)。 */
25
+ type GoalEvent =
26
+ | "pause"
27
+ | "report_blocked"
28
+ | "complete"
29
+ | "hit_budget_limit"
30
+ | "cancel"
31
+ | "resume";
32
+
33
+ export const GOAL_MACHINE = defineStateMachine<GoalStatus, GoalEvent>({
34
+ name: "goal",
35
+ states: {
36
+ active: {},
37
+ paused: {},
38
+ blocked: {},
39
+ complete: { terminal: true },
40
+ budget_limited: { terminal: true },
41
+ cancelled: { terminal: true },
42
+ },
43
+ initial: "active",
44
+ events: {
45
+ pause: {},
46
+ report_blocked: {},
47
+ complete: {},
48
+ hit_budget_limit: {},
49
+ cancel: {},
50
+ resume: {},
51
+ },
52
+ transitions: [
53
+ { from: "active", on: "pause", to: "paused" },
54
+ { from: "active", on: "report_blocked", to: "blocked" },
55
+ { from: "active", on: "complete", to: "complete" },
56
+ { from: "active", on: "hit_budget_limit", to: "budget_limited" },
57
+ { from: ["active", "paused", "blocked"], on: "cancel", to: "cancelled" },
58
+ { from: ["paused", "blocked"], on: "resume", to: "active" },
59
+ ],
60
+ errorPolicy: "throw",
61
+ });
62
+
63
+ /** (from, target) → 事件名索引:从机器行式表派生,transitionStatus 据此查边。 */
64
+ const edgeEventIndex = new Map<GoalStatus, Map<GoalStatus, GoalEvent>>();
65
+ for (const row of GOAL_MACHINE.definition.transitions) {
66
+ if (row.to === null) continue; // goal 域无实体消亡行(to: null)
67
+ const froms = Array.isArray(row.from) ? row.from : [row.from];
68
+ for (const from of froms) {
69
+ let byTarget = edgeEventIndex.get(from);
70
+ if (!byTarget) {
71
+ byTarget = new Map<GoalStatus, GoalEvent>();
72
+ edgeEventIndex.set(from, byTarget);
73
+ }
74
+ byTarget.set(row.to, row.on);
75
+ }
76
+ }
9
77
 
10
78
  export function isTerminalStatus(status: GoalStatus): boolean {
11
79
  return TERMINAL_GOAL_STATUSES.has(status);
@@ -16,18 +84,26 @@ export function isActiveStatus(status: GoalStatus): boolean {
16
84
  }
17
85
 
18
86
  /**
19
- * 安全的状态转换。查 VALID_TRANSITIONS 表,非法转换 throw。
87
+ * 安全的状态转换(M3 迁移后形态:机器保形包装)。
20
88
  *
89
+ * 签名与语义不变:查 (current, next) 边的事件名喂机器 transition 并断言
90
+ * to === next;表外 (current, next) throw(消息与迁移前一致)。
21
91
  * 调用方必须在终态转换前自行检查 isTerminalStatus(如 finalizeAndPersist),
22
92
  * 因为终态→终态不是合法转换(表为空)。对 active→终态、active→paused/blocked、
23
93
  * paused/blocked→active/cancelled 这些合法路径直接返回 next。
24
94
  */
25
95
  export function transitionStatus(current: GoalStatus, next: GoalStatus): GoalStatus {
26
- const allowed = VALID_TRANSITIONS[current];
27
- if (allowed && allowed.includes(next)) {
28
- return next;
96
+ const event = edgeEventIndex.get(current)?.get(next);
97
+ if (event === undefined) {
98
+ throw new Error(`Invalid goal state transition: ${current} → ${next}`);
99
+ }
100
+ const result = GOAL_MACHINE.transition(current, event);
101
+ if (result.state !== next) {
102
+ throw new Error(
103
+ `Goal state machine invariant broken: transition(${current}, ${event}) returned ${String(result.state)}, expected ${next}.`,
104
+ );
29
105
  }
30
- throw new Error(`Invalid goal state transition: ${current} → ${next}`);
106
+ return next;
31
107
  }
32
108
 
33
109
  /**
@@ -78,14 +78,14 @@ const BACKOFF_MULTIPLIER = 2;
78
78
  * message.toolCalls 顶层字段从未存在)。这里只数块个数——调用方以「相邻两次
79
79
  * agent_end 的差分」判定本 turn 是否有真实工具活动,不需要 name/arguments。
80
80
  *
81
- * duck-typed 逐层守卫(对齐 pending-notifications state.ts 的 EntryLike 风格),
81
+ * duck-typed 逐层检查(对齐 pending-notifications state.ts 的 EntryLike 风格),
82
82
  * 不依赖 SDK 具体类型;畸形 entry 跳过不计。
83
83
  */
84
84
  export function countToolCallBlocks(entries: unknown[]): number {
85
85
  let count = 0;
86
86
  for (const raw of entries) {
87
87
  if (!raw || typeof raw !== "object") continue;
88
- // 断言目标带必填 unknown 字段(typeof object 已守卫),非全可选形状
88
+ // 断言目标带必填 unknown 字段(typeof object 已检查),非全可选形状
89
89
  const entry = raw as { type: unknown; message: unknown };
90
90
  if (entry.type !== "message") continue;
91
91
  if (!entry.message || typeof entry.message !== "object") continue;
@@ -22,19 +22,6 @@ export const TERMINAL_GOAL_STATUSES: ReadonlySet<GoalStatus> = new Set([
22
22
  "cancelled",
23
23
  ]);
24
24
 
25
- /**
26
- * 显式状态转换表(system-architecture §5)。终态映射空数组——不可逆。
27
- * transitionStatus 据此查表,非法转换 throw。新增状态时必须更新此表(forcing function)。
28
- */
29
- export const VALID_TRANSITIONS: Record<GoalStatus, GoalStatus[]> = {
30
- active: ["paused", "blocked", "complete", "budget_limited", "cancelled"],
31
- paused: ["active", "cancelled"],
32
- blocked: ["active", "cancelled"],
33
- complete: [],
34
- budget_limited: [],
35
- cancelled: [],
36
- };
37
-
38
25
  // ── 预算配置 ────────────────────────────────────────
39
26
 
40
27
  export interface BudgetConfig {
package/src/index.ts CHANGED
@@ -14,7 +14,7 @@
14
14
  *
15
15
  * FR-4.2/D-16:ctx 必填,移除 lastCtx 模块级可变状态。
16
16
  * FR-6.4:移除 hasPendingInjection。
17
- * FR-6.7:移除 pendingPause(ESC 改用 ctx.signal.aborted 守卫,在各 event handler)。
17
+ * FR-6.7:移除 pendingPause(ESC 改用 ctx.signal.aborted 检查,在各 event handler)。
18
18
  */
19
19
 
20
20
  import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext, MessageEndEvent } from "@earendil-works/pi-coding-agent";
@@ -104,7 +104,7 @@ export default function goalExtension(pi: ExtensionAPI) {
104
104
  updateWidget(session, ports.ui);
105
105
  });
106
106
 
107
- // MF-R2-1 根修:session_shutdown 在 runner invalidate 之前触发(此刻 pi/ctx
107
+ // MF-R2-1 从根源修复:session_shutdown 在 runner invalidate 之前触发(此刻 pi/ctx
108
108
  // 仍可用),取消待发的退避 continuation timer——覆盖 reload / new / resume /
109
109
  // fork / quit 全部失效路径,旧 timer 不再携 stale ctx 闭包存活到到期
110
110
  pi.on("session_shutdown", async (_event, _ctx: ExtensionContext) => {
@@ -138,7 +138,7 @@ export default function goalExtension(pi: ExtensionAPI) {
138
138
  * FR-4.2/D-16: ctx 必填(消除 lastCtx 模块级可变状态)。
139
139
  * ports 构造复用 adapters/ports.buildPorts(DRY:单一 ports 构造点)。
140
140
  *
141
- * 通道形态(C-ext-06 globalThis slot 惯例):裸函数直挂——plan 侧使用点以 typeof 守卫
141
+ * 通道形态(C-ext-06 globalThis slot 惯例):裸函数直挂——plan 侧使用点以 typeof 检查
142
142
  * 读取 `globalThis[Symbol.for("@zhushanwen/pi-goal.goalInit")]`(plan/src/compact.ts)。
143
143
  * 不挂 pi API 对象:pi 为每个扩展创建独立 ExtensionAPI(1.0.0 复核同构),挂 pi 对象的字段
144
144
  * 跨扩展不可见。
@@ -31,7 +31,7 @@ export function serializeState(state: GoalRuntimeState): GoalRuntimeState {
31
31
  * npm pi-goal 0.7.x(base 968b9d76)经 finalizeAndPersist 真实写入过 `time_limited`(时间预算
32
32
  * 维度,新状态机已删除该状态)。若不归一化,升级用户持有该状态的 goal 会功能死锁:
33
33
  * /goal clear 的 transitionStatus 查表 throw、goal_control create 误报 already active、
34
- * resume/update 被 isActiveStatus 守卫拒绝——唯一出口是手动删 session 文件。
34
+ * resume/update 被 isActiveStatus 检查拒绝——唯一出口是手动删 session 文件。
35
35
  * 归一化为语义最接近的 `budget_limited`(预算耗尽终态),clear 走 isTerminalStatus 快速路径。
36
36
  * 其余值原样透传(不做值域校验,保持现状)。
37
37
  */
package/src/ports.ts CHANGED
@@ -53,8 +53,8 @@ export interface UiPort {
53
53
  /**
54
54
  * 设置 widget(双模单调用)。content = { gui, text } 双模 payload / undefined 清除;
55
55
  * 模式分派(RPC→gui 经 marker 通道 / TUI→text 原生文本行)由 protocol setWidgetDual
56
- * 单点内化(守卫单点化说明见 extension-protocol core/helpers.ts,adapter 委托 helper)。
57
- * hasUI 守卫留在调用方(FR-6.6)。
56
+ * 单点内化(检查单点化说明见 extension-protocol core/helpers.ts,adapter 委托 helper)。
57
+ * hasUI 检查留在调用方(FR-6.6)。
58
58
  */
59
59
  setWidget(name: string, content: DualWidgetContent | undefined): void;
60
60
  /** 设置 status bar */
@@ -6,7 +6,7 @@
6
6
  * - renderStatusLine: slug 标题(fallback objective 截断)+ 状态后缀 + cancelled 短路 + token/耗时显示
7
7
  * - renderWidgetLines: cancelled 短路 + 标题行 + criteria 摘要单行 + 预算进度条(Objective 全文行已移除,GAP-8)
8
8
  * - renderTerminalStatusLine: 终态单行 + cancelled 短路
9
- * - updateWidget: FR-6.6 hasUI 守卫 + 终态折叠 + cancelled 清除
9
+ * - updateWidget: FR-6.6 hasUI 检查 + 终态折叠 + cancelled 清除
10
10
  *
11
11
  * 用 passthrough ThemeLike(fg/bold 返回原文),断言渲染逻辑而非颜色。
12
12
  */
@@ -197,7 +197,7 @@ describe("renderWidgetLines", () => {
197
197
  });
198
198
  });
199
199
 
200
- // ── updateWidget(FR-6.6 hasUI 守卫 + dual payload)──
200
+ // ── updateWidget(FR-6.6 hasUI 检查 + dual payload)──
201
201
 
202
202
  interface RecordedCall {
203
203
  method: "setWidget" | "setStatus";
@@ -225,7 +225,7 @@ function makeUiPort(hasUI: boolean): { ui: UiPort; calls: RecordedCall[] } {
225
225
  }
226
226
 
227
227
  describe("updateWidget (FR-6.6 hasUI guard)", () => {
228
- it("hasUI=false → 不调 setWidget/setStatus(headless 守卫)", () => {
228
+ it("hasUI=false → 不调 setWidget/setStatus(headless 检查)", () => {
229
229
  const { ui, calls } = makeUiPort(false);
230
230
  const session = createGoalSession();
231
231
  session.state = makeState({ status: "active" });
@@ -6,7 +6,7 @@
6
6
  * - 类型 import 自 engine/types.ts 与 ports.ts(ThemeLike 上移至接口层)
7
7
  * - 工具函数 import 自 engine/budget.ts
8
8
  * - 时间计算基于 state.timeUsedSeconds(不含 Date.now() 副作用段)
9
- * - updateWidget(session, uiPort) 含 FR-6.6 hasUI 守卫
9
+ * - updateWidget(session, uiPort) 含 FR-6.6 hasUI 检查
10
10
  * - 仅 token 维度预算(time budget 已移除);耗时仅作记账显示
11
11
  *
12
12
  * slug 精简(widget 显示优化):
@@ -184,7 +184,7 @@ export function renderWidgetLines(state: GoalRuntimeState, th: ThemeLike): strin
184
184
  return lines;
185
185
  }
186
186
 
187
- // ── updateWidget(FR-6.6 hasUI 守卫)──
187
+ // ── updateWidget(FR-6.6 hasUI 检查)──
188
188
 
189
189
  /**
190
190
  * 刷新 widget + status bar。
package/src/service.ts CHANGED
@@ -154,14 +154,14 @@ export function createGoal(
154
154
  /**
155
155
  * 唯一终态序列入口(FR-3.3 / AC-3)。
156
156
  *
157
- * 收口所有 active→terminal 转换的完整副作用序列——service(budget 终态)与
157
+ * 收尾所有 active→terminal 转换的完整副作用序列——service(budget 终态)与
158
158
  * command-adapter(clear / resume 超预算)、goal-control-adapter(complete)共用
159
159
  * 同一 `transitionStatus + completedAtTurnIndex + writeHistory + appendState` 序列。
160
160
  *
161
161
  * 序列(严格顺序):
162
162
  * 1. tickState(state)(FR-6.5:转 terminal 前累加当前运行段——此时 status 仍为 active)
163
163
  * 2. finalizeGoal(state, terminalStatus, ports)
164
- * — transitionStatus(终态守卫) + completedAtTurnIndex= + appendHistory(FR-8.7 矩阵)
164
+ * — transitionStatus(终态检查) + completedAtTurnIndex= + appendHistory(FR-8.7 矩阵)
165
165
  * 3. ports.persistence.appendState(serializeState(state))(持久化终态 state)
166
166
  *
167
167
  * @param state runtime state(mutate)
@@ -255,10 +255,10 @@ export function applyEvent(
255
255
 
256
256
  switch (eventType) {
257
257
  case "message_end": {
258
- // token 累加(FR-8.6 G-R2-001)—— 仅 active 时累加(回归修复:原缺 isActiveStatus 守卫,
258
+ // token 累加(FR-8.6 G-R2-001)—— 仅 active 时累加(回归修复:原缺 isActiveStatus 检查,
259
259
  // blocked 等 non-active 状态会错误累加 token)。复用 engine 纯函数。
260
260
  if (!isActiveStatus(session.state.status)) break;
261
- // 运行时守卫解析事件数据(未知形状 → 可选字段),替代全可选结构断言
261
+ // 运行时检查解析事件数据(未知形状 → 可选字段),替代全可选结构断言
262
262
  const data = toMessageEndData(eventData);
263
263
  if (data?.message?.role !== "assistant") break;
264
264
  const usage = data.message.usage;
package/src/session.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  * U6c:重建输入接活跃路径裁剪——被撤子树的 goal-state 不进重建态)。
7
7
  *
8
8
  * FR-6.4: 删除 hasPendingInjection(僵尸字段)
9
- * FR-6.7: 删除 pendingPause(ESC 改用 aborted 守卫)
9
+ * FR-6.7: 删除 pendingPause(ESC 改用 aborted 检查)
10
10
  * FR-8.1 G-006: session append-only——entry GC 不生效(生产),history 显示侧截断
11
11
  * FR-3: 崩溃后保持原状态(active 重启计时;paused/blocked 保持;终态保持)
12
12
  */
@@ -26,7 +26,7 @@ export interface GoalSession {
26
26
  /**
27
27
  * W5 退避延迟发送的 continuation 定时器(无进展退避时 continuation 不立即发出,
28
28
  * 按间隔 ×2 递增延迟)。非 null 表示有一个待发的退避 continuation——调度前必须
29
- * 先清(防双发)。清理面(cancelContinuationTimer 统一入口):clearGoalSession、
29
+ * 先清(防双发)。清理侧(cancelContinuationTimer 统一入口):clearGoalSession、
30
30
  * session_start(新 session 生命周期旧 timer 作废)、before_agent_start(新用户
31
31
  * 活动使旧退避作废)、下一轮 agent_end 调度前。
32
32
  */
@@ -71,7 +71,7 @@ export interface ActivePathSessionView {
71
71
  // ── reconstructGoalState(session_start / session_tree 时调)──────────
72
72
 
73
73
  /**
74
- * 从 session entries 恢复 goal state(U6c:重建输入 = 活跃路径投影,非全文件)。
74
+ * 从 session entries 恢复 goal state(U6c:重建输入 = 活跃路径派生视图,非全文件)。
75
75
  *
76
76
  * Pi SDK 的 session 是 append-only,getEntries() 返回 filter-copy——splice
77
77
  * 无法修改真实 entries。Entry GC(goal-state 留 1、goal-history 留 20)在生产
@@ -127,7 +127,7 @@ function isGoalStateEntry(entry: SessionEntryLike): boolean {
127
127
  // ── clearGoalSession ──────────────────────────────────
128
128
 
129
129
  /**
130
- * 取消待发的退避 continuation timer(全部清理点走本函数,防止清理面遗漏):
130
+ * 取消待发的退避 continuation timer(全部清理点走本函数,防止清理侧遗漏):
131
131
  * - clearGoalSession:goal 已终态/清除,发出即僵尸 turn
132
132
  * - session_start:session 句柄跨 session 复用,新 session 生命周期旧 timer 作废
133
133
  * - before_agent_start:新用户活动使旧退避作废,该轮 agent_end 按最新状态重新决策
@@ -145,7 +145,7 @@ export function clearGoalSession(session: GoalSession, uiPort: UiPort): void {
145
145
  cancelContinuationTimer(session);
146
146
  session.state = null;
147
147
  session.isProcessing = false;
148
- // FR-6.6: hasUI 守卫
148
+ // FR-6.6: hasUI 检查
149
149
  if (uiPort.hasUI) {
150
150
  uiPort.setWidget("goal", undefined);
151
151
  uiPort.setStatus("goal", undefined);