@zhushanwen/pi-goal 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,20 +1,25 @@
1
1
  # goal
2
2
 
3
- Codex 风格的 `/goal` 命令 — 持久目标驱动自主循环,支持任务追踪、证据验证、Token/时间预算、阻塞检测。
3
+ Codex 风格的 `/goal` 命令 — 持久目标追踪:`goal_control` 工具创建/完成/阻塞目标,完成需逐条对照 successCriteria 的证据验证,仅 token 维度预算。
4
4
 
5
5
  ## 功能
6
6
 
7
- - **自主循环**:`/goal <目标>` 启动后,AI 自动拆分任务并持续执行
8
- - **证据验证**:完成任务必须提供具体证据,不能空口完成
9
- - **预算控制**:Token 和时间双预算,70% 预警、90% 收尾、100% 终止
10
- - **阻塞检测**:连续无进展自动阻塞,用户可手动 resume
11
- - **持久化**:状态通过 session entries 保存,重启后自动恢复
7
+ - **目标管理**:`goal_control` tool 是唯一创建入口(create / complete / report_blocked)。`/goal <objective>` 是提示词触发器——不直接启动,而是引导 AI 调 `goal_control create`(slug / objective / successCriteria 均由 AI 在 toolcall 时决定)
8
+ - **证据验证**:complete 必须提供具体 evidence(改动的文件、通过的测试、运行的命令),且满足 create 时定义的每条 successCriteria 条件,不能空口完成
9
+ - **预算控制**:仅 token 维度预算(time budget 已移除,耗时仅记账显示)。70% 预警、90% 发收尾 steering、100% 且已发收尾提示后进入 budget_limited 终态。默认不设预算,仅在用户显式要求时设置
10
+ - **阻塞处理**:无自动检测——blocked 只能由 agent 主动调 `goal_control report_blocked`(穷尽替代方案后);用户经 `/goal resume` 恢复(paused/blocked → active)
11
+ - **持久化**:状态通过 session entries 保存(append-only),重启后从最新一条 goal-state entry 恢复,崩溃后保持原状态
12
+
13
+ ## 状态机
14
+
15
+ 6 态:`active` / `paused` / `blocked` / `complete` / `budget_limited` / `cancelled`。
16
+ `complete` / `budget_limited` / `cancelled` 为终态(不可逆)。
12
17
 
13
18
  ## 安装
14
19
 
15
20
  ```bash
16
21
  # symlink 方式(开发推荐;globalExtDir 平铺布局,目标不带分组层)
17
- ln -s /path/to/xyz-pi-extensions-workspace/<branch>/extensions/universal/goal \
22
+ ln -s /path/to/xyz-agent/extensions/universal/goal \
18
23
  ~/.pi/agent/extensions/goal
19
24
 
20
25
  # npm 方式(正式)
@@ -25,26 +30,30 @@ pi install npm:@zhushanwen/pi-goal
25
30
 
26
31
  ```
27
32
  /goal 修复项目中所有失败的测试
28
- /goal 实现用户认证功能 --tokens 500000 --timeout 30
29
- /goal status # 查看进度
30
- /goal resume # 恢复(blocked → active)
31
- /goal clear # 清除
33
+ /goal 实现用户认证功能 --tokens 500000
34
+ /goal status # 查看进度(criteria 逐条编号显示)
35
+ /goal resume # 恢复(paused/blocked → active)
36
+ /goal pause # 暂停(active → paused)
37
+ /goal update <new-objective> [--criteria <a;b;c>] # 重塑目标
38
+ /goal history # 查看历史 goal
39
+ /goal clear # 清除(→ cancelled)
32
40
  ```
33
41
 
34
42
  ## 文件结构
35
43
 
36
44
  ```
37
45
  goal/
38
- ├── index.ts # 入口 — 命令、事件、工具注册 + __goalInit
46
+ ├── index.ts # 入口 — re-export src/index.ts
39
47
  └── src/
40
- ├── index.ts # 工厂入口(注册 tool/command/events,全部委托 adapters)
41
- ├── commands.ts # /goal 命令参数解析
48
+ ├── index.ts # 工厂入口(注册 command/tool/events + __goalInit,全部委托 adapters)
49
+ ├── commands.ts # /goal 命令参数解析(仅识别 --tokens flag)
42
50
  ├── constants.ts # 语义常量
43
51
  ├── ports.ts # Pi 能力抽象(Persistence/Ui/Messaging/Session)
44
- ├── service.ts # 协调层 — applyToolAction / applyEvent 双入口
52
+ ├── service.ts # 协调层 — createGoal / finalizeAndPersist / applyEvent
45
53
  ├── persistence.ts # serialize/deserialize + GoalHistoryEntry
46
- ├── session.ts # 运行时句柄 + 状态重建 + entry GC
47
- ├── engine/ # 零 Pi 依赖的纯状态机(goal/task/budget/types)
48
- ├── adapters/ # Pi 桥接(tool/command/event-adapter + actions)
49
- └── projection/ # 渲染(widget/prompts/result)
54
+ ├── session.ts # 运行时句柄 + 状态重建(append-only 只读最新一条,无 entry GC)
55
+ ├── engine/ # 零 Pi 依赖的纯状态机(goal/budget/types)
56
+ ├── adapters/ # Pi 桥接(goal-control-adapter / command-adapter / event-adapter
57
+ │ # + event-handlers/ + ports / success-criteria)
58
+ └── projection/ # 渲染(widget / prompts / gui)
50
59
  ```
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@zhushanwen/pi-goal",
3
- "version": "0.11.0",
4
- "description": "Codex-style /goal command for Pi — persistent goal-driven autonomous loop with evidence-based completion, token/time budgets, blocked detection, and steering templates.",
3
+ "version": "0.12.0",
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",
7
7
  "xyz-agent": {
@@ -25,14 +25,14 @@
25
25
  "index.ts"
26
26
  ],
27
27
  "peerDependencies": {
28
- "@earendil-works/pi-ai": "^0.84.1",
29
- "@earendil-works/pi-coding-agent": "^0.84.1",
30
- "@earendil-works/pi-tui": "^0.84.1",
28
+ "@earendil-works/pi-ai": "^0.84.4",
29
+ "@earendil-works/pi-coding-agent": "^0.84.4",
30
+ "@earendil-works/pi-tui": "^0.84.4",
31
31
  "typebox": "*",
32
- "@zhushanwen/pi-pending-notifications": "0.5.0"
32
+ "@zhushanwen/pi-pending-notifications": "0.5.2"
33
33
  },
34
34
  "dependencies": {
35
- "@xyz-agent/extension-protocol": "0.7.0"
35
+ "@xyz-agent/extension-protocol": "0.8.0"
36
36
  },
37
37
  "peerDependenciesMeta": {
38
38
  "@earendil-works/pi-tui": {
@@ -110,7 +110,7 @@ function makeRunningState(overrides?: Partial<GoalRuntimeState>): GoalRuntimeSta
110
110
  // ── handleAgentEnd:ESC 守卫 ──────────────────────────
111
111
 
112
112
  describe("handleAgentEnd — FR-6.7 ESC 守卫", () => {
113
- it("aborted=true:不发 continuation、不递增 stall、不做 budget 检查、goal 保持 active", async () => {
113
+ it("aborted=true:不发 continuation、不做 budget 检查、goal 保持 active", async () => {
114
114
  const { pi, calls: piCalls, states } = makeFakePi();
115
115
  const { ctx, calls: ctxCalls } = makeFakeCtx({ aborted: true });
116
116
  const session = createGoalSession();
@@ -118,7 +118,7 @@ describe("handleComplete — active 守卫 + evidence + finalizeAndPersist", ()
118
118
 
119
119
  // ── handleCreate ─────────────────────────────────────
120
120
 
121
- describe("handleCreate — slug+objective 必填 + 非终态守卫 + createGoal", () => {
121
+ describe("handleCreate — objective/successCriteria 必填(handler 运行时校验)+ slug 真 optional + 非终态守卫 + createGoal", () => {
122
122
  it("无旧 goal + slug + objective → status active + slug 写入 + 持久化 + notify", () => {
123
123
  const session = createGoalSession();
124
124
  const ports = makeFakePorts();
@@ -3,8 +3,8 @@
3
3
  //
4
4
  // agent(LLM)唯一能看到的 tool 元信息就是 description + 报错文案。description 用中文
5
5
  // 重写(动作 create/complete/report_blocked + 规则 + §2.5 终态语义),删除英文 Examples/Don't
6
- // 段;runtime throw 保留 .trim() 空串校验 + 'Correct:' 纠正正例(缺失必填已前移到 schema
7
- // discriminated union 层拒绝,C3)。
6
+ // 段;runtime throw 保留 .trim() 空串校验 + 'Correct:' 纠正正例(schema 为扁平 Type.Object
7
+ // + action 字段级 union,字段全部 Optional——缺失必填由 handler 运行时校验兜底)。
8
8
  //
9
9
  // 本测试用源码断言(读 .ts 文件文本)锁定这些约束,防止后续重构把中文语义/纠错文案/
10
10
  // union 结构删掉。读源码而非 import,避免 mock 链(goal-control-adapter.ts 依赖
@@ -55,8 +55,8 @@ describe("goal_control description — 删除 Examples/Don't 英文段", () => {
55
55
  });
56
56
  });
57
57
 
58
- describe("goal_control schema — discriminated union(C3)", () => {
59
- it("Type.Union + 各分支 additionalProperties:false + 无 timeBudgetMinutes", () => {
58
+ describe("goal_control schema — 扁平 Type.Object(OpenAI 兼容)+ handler 运行时校验", () => {
59
+ it("字段级 Type.Union + additionalProperties:false + 无 timeBudgetMinutes", () => {
60
60
  expect(ADAPTER_SRC).toMatch(/Type\.Union\(/);
61
61
  expect(ADAPTER_SRC).toContain("additionalProperties: false");
62
62
  expect(ADAPTER_SRC).not.toMatch(/timeBudgetMinutes/);
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * index.ts 测试 — 工厂入口 + 跨扩展 API(pi.__goalInit)
3
3
  *
4
- * 覆盖 T1.8 (NFR-AC-8):__goalInit 签名 (objective, budget, ctx),结构上无法接收 tasks
5
- * (task CRUD 已删除,D-16/FR-4 双轨消除)。active 守卫由 service.test.ts 覆盖。
4
+ * 覆盖 T1.8 (NFR-AC-8):__goalInit 签名 (objective, budget, ctx, slug?, successCriteria?),
5
+ * 结构上无法接收 tasks(task CRUD 已删除,D-16/FR-4 双轨消除)。active 守卫由 service.test.ts 覆盖。
6
6
  *
7
7
  * 用最小 fake pi + fake ctx 实例化 goalExtension 工厂,再调 pi.__goalInit。
8
8
  */
@@ -247,7 +247,7 @@ describe("E1 全链路集成:goal_control create 数组 → 落盘 → 重载
247
247
 
248
248
  // 5. contextInjectionPrompt → <successCriteria> 段含编号行(plan 需求 3:编号列表)
249
249
  const state = reloaded.state!;
250
- const prompt = contextInjectionPrompt(state, state.timeUsedSeconds);
250
+ const prompt = contextInjectionPrompt(state);
251
251
  expect(prompt).toContain("<successCriteria>");
252
252
  expect(prompt).toContain("1. e2e cond a");
253
253
  expect(prompt).toContain("2. e2e cond b");
@@ -2,7 +2,7 @@
2
2
  * session.ts 测试 — reconstructGoalState
3
3
  *
4
4
  * 覆盖:
5
- * - MF-7: reconstructGoalState 3 条 FR(G-006 entry GC / G-015 强制激活 / G-024 throw→null)
5
+ * - MF-7: reconstructGoalState 3 条 FR(G-006 append-only 不 splice / G-015 崩溃后非 active 保持原状 / G-024 throw→null)
6
6
  *
7
7
  * [REMOVED W4] isStaleContextError + STALE_CONTEXT_PATTERNS 测试随实现删除——
8
8
  * 双重废(无生产调用方 + patterns 与 pi 0.84.1 真实 stale 文案零匹配),见
@@ -33,6 +33,7 @@ import type { GoalHistoryEntry } from "../ports";
33
33
  import type { GoalSession } from "../session";
34
34
  import { clearGoalSession } from "../session";
35
35
  import { buildPorts } from "./ports";
36
+ import { CRITERIA_HINTS_SLASH, validateSuccessCriteriaItems } from "./success-criteria";
36
37
 
37
38
  // ── Orchestrator ──────────────────────────────────────
38
39
 
@@ -282,29 +283,11 @@ function handleUpdate(
282
283
  return;
283
284
  }
284
285
  // 校验前置(mutate state 之前):任一条目非法 → throw,objective 等重塑字段全部不动。
285
- // 规则与 handleCreate 一致(typeof string / trim 非空 / 单行),文案带 Correct: 正例。
286
+ // 逐条规则(typeof string / trim 非空 / 单行)与 goal_control create 共享
287
+ // validateSuccessCriteriaItems,slash 通道正例见 CRITERIA_HINTS_SLASH。
286
288
  let validatedCriteria: string[] | undefined;
287
289
  if (criteria !== undefined && criteria.length > 0) {
288
- validatedCriteria = [];
289
- for (const item of criteria) {
290
- if (typeof item !== "string") {
291
- throw new Error(
292
- "'successCriteria' must be an array of strings. Correct: {\"action\":\"update\",\"objective\":\"...\",\"successCriteria\":[\"<condition 1>\",\"<condition 2>\"]}",
293
- );
294
- }
295
- const trimmed = item.trim();
296
- if (!trimmed) {
297
- throw new Error(
298
- "'successCriteria' items must not be empty. Each condition should be a short, single-line, checkable statement. Correct: {\"action\":\"update\",\"objective\":\"...\",\"successCriteria\":[\"tests pass\",\"tsc clean\"]}",
299
- );
300
- }
301
- if (/[\r\n]/.test(trimmed)) {
302
- throw new Error(
303
- "'successCriteria' items must be single-line (no line breaks). Split multi-line text into separate semicolon-separated items, one condition per item. Correct: /goal update <objective> --criteria \"line 1; line 2\"",
304
- );
305
- }
306
- validatedCriteria.push(trimmed);
307
- }
290
+ validatedCriteria = validateSuccessCriteriaItems(criteria, CRITERIA_HINTS_SLASH);
308
291
  }
309
292
  const state = session.state;
310
293
  const oldObjective = state.objective;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Event adapter — 薄路由(#4 拆分后)。
3
3
  *
4
- * 6 个事件 handler 拆到 event-handlers/,本文件只做 re-export。
4
+ * 5 个事件 handler + 1 个共享辅助拆在 event-handlers/,本文件只做 re-export。
5
5
  * handler 逻辑见各自文件。pi.on 注册在 index.ts(保持不变)。
6
6
  *
7
7
  * 行为等价(#4 验收):handler 逻辑原样搬迁,仅 persistAndUpdate 签名 ports 化
@@ -193,7 +193,7 @@ async function handleContinuation(
193
193
  }
194
194
  // 发 continuation(FR-8.7: 去 debounce 后才发)
195
195
  ports.messaging.sendContextMessage(
196
- continuationPrompt(state, state.timeUsedSeconds),
196
+ continuationPrompt(state),
197
197
  "followUp",
198
198
  );
199
199
  }
@@ -66,7 +66,7 @@ export async function handleBeforeAgentStart(
66
66
  return {
67
67
  message: {
68
68
  customType: "goal-context",
69
- content: contextInjectionPrompt(session.state, session.state.timeUsedSeconds),
69
+ content: contextInjectionPrompt(session.state),
70
70
  display: false,
71
71
  },
72
72
  };
@@ -42,8 +42,9 @@ import { updateWidget } from "../projection/widget";
42
42
  import { createGoal, finalizeAndPersist, persistState, type ServicePorts, tickState } from "../service";
43
43
  import type { GoalSession } from "../session";
44
44
  import { buildPorts } from "./ports";
45
+ import { CRITERIA_HINTS_TOOL, validateSuccessCriteriaItems } from "./success-criteria";
45
46
 
46
- // ── Params schema(discriminated union,C3)────────────
47
+ // ── Params schema(扁平 Type.Object,OpenAI 兼容)────────────
47
48
 
48
49
  /**
49
50
  * goal_control 参数 schema。扁平 Type.Object + action 字段级 Type.Union(等价 enum)。
@@ -156,7 +157,8 @@ function requireCreateObjective(params: GoalControlParamsT): string {
156
157
 
157
158
  /**
158
159
  * create 的 successCriteria 校验:必填 + 非空数组 + 逐条 string/非空/单行,返回 trim 后数组。
159
- * 从 handleCreate 提取(圈复杂度拆分),校验顺序与错误消息不变。
160
+ * 逐条校验共享 validateSuccessCriteriaItems(与 /goal update 同规则),tool 通道正例
161
+ * 见 CRITERIA_HINTS_TOOL;校验顺序与错误消息不变。
160
162
  */
161
163
  function requireCreateSuccessCriteria(params: GoalControlParamsT): string[] {
162
164
  if (params.successCriteria === undefined) {
@@ -168,30 +170,7 @@ function requireCreateSuccessCriteria(params: GoalControlParamsT): string[] {
168
170
  "'successCriteria' must be a non-empty array of 1~8 checkable conditions. Correct: {\"action\":\"create\",\"slug\":\"refactor-auth\",\"objective\":\"...\",\"successCriteria\":[\"tests pass\",\"tsc clean\",\"src/auth.ts uses JWT\"]}",
169
171
  );
170
172
  }
171
- const successCriteria: string[] = [];
172
- for (const item of successCriteriaRaw) {
173
- // 运行时 typeof 防御(U28②):测试/直调可能绕过 schema 传脏元素,
174
- // 此处给业务错误而非让下方 .trim() 抛 TypeError
175
- if (typeof item !== "string") {
176
- throw new Error(
177
- "'successCriteria' must be an array of strings. Correct: {\"action\":\"create\",\"objective\":\"...\",\"successCriteria\":[\"<condition 1>\",\"<condition 2>\"]}",
178
- );
179
- }
180
- const trimmed = item.trim();
181
- if (!trimmed) {
182
- throw new Error(
183
- "'successCriteria' items must not be empty. Each condition should be a short, single-line, checkable statement. Correct: {\"action\":\"create\",\"objective\":\"...\",\"successCriteria\":[\"tests pass\",\"tsc clean\"]}",
184
- );
185
- }
186
- // 换行校验(U7):/[\r\n]/ 覆盖 \n / \r\n / \r 三形态,schema pattern ^[^\r\n]+$ 的 handler 兜底
187
- if (/[\r\n]/.test(trimmed)) {
188
- throw new Error(
189
- "'successCriteria' items must be single-line (no line breaks). Split multi-line text into separate array items, one condition per item. Correct: {\"action\":\"create\",\"objective\":\"...\",\"successCriteria\":[\"line 1\",\"line 2\"]}",
190
- );
191
- }
192
- successCriteria.push(trimmed);
193
- }
194
- return successCriteria;
173
+ return validateSuccessCriteriaItems(successCriteriaRaw, CRITERIA_HINTS_TOOL);
195
174
  }
196
175
 
197
176
  /** create 的 budget 解析:非法预算直接拒绝(不静默截断),未提供时返回空配置。 */
@@ -53,7 +53,12 @@ export function buildPorts(pi: ExtensionAPI, ctx: ExtensionContext): ServicePort
53
53
  // host 侧 event-adapter 检测 marker 解码还原。guiSetWidget 无 isGui 守卫
54
54
  // (helpers.ts 仅查 ctx.ui?.setWidget 存在性),isGui 判定在 updateWidget
55
55
  // 外层(projection/widget.ts)。
56
- guiSetWidget(ctx as unknown as GuiContext, name, result);
56
+ //
57
+ // 断言根因:pi ExtensionUIContext.custom 是泛型方法(返回 Promise<T>),
58
+ // 与 GuiContext.ui.custom 的具体返回类型静态不兼容,但 mode/hasUI/ui 其余
59
+ // 成员形状一致(ExtensionMode 与 GuiContext.mode union 完全相同)——
60
+ // 单层直接断言可过 tsc(guiSetWidget 仅读 ui.setWidget,custom 不参与)。
61
+ guiSetWidget(ctx as GuiContext, name, result);
57
62
  },
58
63
  setStatus(name: string, text: string | undefined): void {
59
64
  ctx.ui.setStatus(name, text);
@@ -65,8 +70,9 @@ export function buildPorts(pi: ExtensionAPI, ctx: ExtensionContext): ServicePort
65
70
  return Boolean(ctx.hasUI);
66
71
  },
67
72
  get isGui(): boolean {
68
- // RPC 模式 = GUI 渲染通道有效;TUI/json/print 走 pi 原生渲染
69
- return isGuiCapable(ctx as unknown as GuiContext);
73
+ // RPC 模式 = GUI 渲染通道有效;TUI/json/print 走 pi 原生渲染。
74
+ // 断言同 setGuiWidget:custom 泛型签名静态不兼容,mode 单字段运行时可靠。
75
+ return isGuiCapable(ctx as GuiContext);
70
76
  },
71
77
  // ThemeLike 形状:透传 ctx.ui.theme 的 fg/bold。
72
78
  // Theme.fg 只接受 ThemeColor 字面量 union(SDK 契约);projection 层保证传入 union 内字面量,
@@ -0,0 +1,71 @@
1
+ /**
2
+ * successCriteria 逐条校验(adapters 层双通道共享,DRY):
3
+ * - slash 通道:/goal update --criteria(command-adapter.ts handleUpdate)
4
+ * - tool 通道:goal_control create(goal-control-adapter.ts requireCreateSuccessCriteria)
5
+ *
6
+ * 三条校验规则(typeof string / trim 非空 / 单行)与错误消息前半句在两通道逐字一致;
7
+ * 「Correct:」恢复正例经 CriteriaUsageHints 参数化——notString/empty 两通道都是 JSON 形式
8
+ * (action 字段区分 update/create),仅 multiline 的 Correct 正例随通道不同
9
+ * (slash 用法提示 vs tool JSON 正例)。校验顺序与错误文案与折叠前完全一致。
10
+ */
11
+
12
+ /** 各通道的恢复正例(「Correct:」之后的部分) */
13
+ export interface CriteriaUsageHints {
14
+ /** 非 string 元素错误的 Correct: 正例 */
15
+ notString: string;
16
+ /** 空串项错误的 Correct: 正例 */
17
+ empty: string;
18
+ /** 单行校验错误的 Split 说明 + Correct: 正例 */
19
+ multiline: string;
20
+ }
21
+
22
+ /** slash 通道(/goal update)正例 */
23
+ export const CRITERIA_HINTS_SLASH: CriteriaUsageHints = {
24
+ notString: '{"action":"update","objective":"...","successCriteria":["<condition 1>","<condition 2>"]}',
25
+ empty: '{"action":"update","objective":"...","successCriteria":["tests pass","tsc clean"]}',
26
+ multiline:
27
+ 'Split multi-line text into separate semicolon-separated items, one condition per item. Correct: /goal update <objective> --criteria "line 1; line 2"',
28
+ };
29
+
30
+ /** tool 通道(goal_control create)正例 */
31
+ export const CRITERIA_HINTS_TOOL: CriteriaUsageHints = {
32
+ notString: '{"action":"create","objective":"...","successCriteria":["<condition 1>","<condition 2>"]}',
33
+ empty: '{"action":"create","objective":"...","successCriteria":["tests pass","tsc clean"]}',
34
+ multiline:
35
+ 'Split multi-line text into separate array items, one condition per item. Correct: {"action":"create","objective":"...","successCriteria":["line 1","line 2"]}',
36
+ };
37
+
38
+ /**
39
+ * 逐条校验并归一(trim),任一条非法即 throw(校验前置语义由调用方保证:
40
+ * mutate state 之前调用,任一条目非法时 state 不动)。
41
+ *
42
+ * 运行时 typeof 防御(U28②):测试/直调可能绕过 schema 传脏元素,
43
+ * 此处给业务错误而非让下方 .trim() 抛 TypeError。
44
+ * 换行校验(U7):/[\r\n]/ 覆盖 \n / \r\n / \r 三形态,schema pattern ^[^\r\n]+$ 的 handler 兜底。
45
+ */
46
+ export function validateSuccessCriteriaItems(
47
+ items: readonly unknown[],
48
+ hints: CriteriaUsageHints,
49
+ ): string[] {
50
+ const validated: string[] = [];
51
+ for (const item of items) {
52
+ if (typeof item !== "string") {
53
+ throw new Error(
54
+ `'successCriteria' must be an array of strings. Correct: ${hints.notString}`,
55
+ );
56
+ }
57
+ const trimmed = item.trim();
58
+ if (!trimmed) {
59
+ throw new Error(
60
+ `'successCriteria' items must not be empty. Each condition should be a short, single-line, checkable statement. Correct: ${hints.empty}`,
61
+ );
62
+ }
63
+ if (/[\r\n]/.test(trimmed)) {
64
+ throw new Error(
65
+ `'successCriteria' items must be single-line (no line breaks). ${hints.multiline}`,
66
+ );
67
+ }
68
+ validated.push(trimmed);
69
+ }
70
+ return validated;
71
+ }
package/src/constants.ts CHANGED
@@ -15,11 +15,6 @@ export const BUDGET_RATIO_HIGH = 0.9; // 90% — 触发预警/收尾
15
15
  export const BUDGET_RATIO_LOW = 0.7; // 70% — 触发提醒
16
16
  export const CONTEXT_USAGE_RATIO_LIMIT = 0.85; // 85% — 上下文空间不足阈值
17
17
 
18
- // ── 预算百分比阈值 (0-100) ──────────────────────────
19
-
20
- export const BUDGET_PERCENT_HIGH = 90; // widget 颜色变红
21
- export const BUDGET_PERCENT_LOW = 70; // widget 颜色变黄
22
-
23
18
  // ── 长度/数量上限 ───────────────────────────────────
24
19
 
25
20
  export const UPDATE_PREFIX_LENGTH = 7; // "update ".length
package/src/ports.ts CHANGED
@@ -26,7 +26,10 @@ export interface GoalHistoryEntry {
26
26
  // ── PersistencePort ──────────────────────────────────
27
27
 
28
28
  export interface PersistencePort {
29
- /** 写入 goal-state entry(最新 1 条,GC 由 session 层管) */
29
+ /**
30
+ * 写入 goal-state entry。无 GC:session entries append-only,每次追加完整 snapshot,
31
+ * 不轮转、不删旧 entry(重建时只读最新一条,旧 entry 留存)。
32
+ */
30
33
  appendState(state: GoalRuntimeState): void;
31
34
  /** 写入 goal-history 归档 entry */
32
35
  appendHistory(entry: GoalHistoryEntry): void;
@@ -40,13 +40,13 @@ describe("formatBudget — 2 styles (FR-3.4)", () => {
40
40
  budget: { tokenBudget: 1000 },
41
41
  tokensUsed: 500,
42
42
  });
43
- const out = formatBudget(state, 300, "percent");
43
+ const out = formatBudget(state, "percent");
44
44
  expect(out).toContain("Token: 50%");
45
45
  });
46
46
 
47
47
  it("percent: 无预算 → 空字符串", () => {
48
48
  const state = makeState();
49
- expect(formatBudget(state, 0, "percent")).toBe("");
49
+ expect(formatBudget(state, "percent")).toBe("");
50
50
  });
51
51
 
52
52
  it("line: 剩余/总量格式", () => {
@@ -54,7 +54,7 @@ describe("formatBudget — 2 styles (FR-3.4)", () => {
54
54
  budget: { tokenBudget: 1000 },
55
55
  tokensUsed: 300,
56
56
  });
57
- const out = formatBudget(state, 120, "line");
57
+ const out = formatBudget(state, "line");
58
58
  expect(out).toContain("Tokens: 700/1000");
59
59
  });
60
60
  });
@@ -64,7 +64,7 @@ describe("formatBudget — 2 styles (FR-3.4)", () => {
64
64
  describe("XML escaping in prompts", () => {
65
65
  it("objective 中的 <>& 被转义(continuationPrompt)", () => {
66
66
  const state = makeState({ objective: "<script>alert('x')</script> & data" });
67
- const out = continuationPrompt(state, 0);
67
+ const out = continuationPrompt(state);
68
68
  expect(out).toContain("&lt;script&gt;");
69
69
  expect(out).toContain("&amp; data");
70
70
  expect(out).not.toContain("<script>");
@@ -90,7 +90,7 @@ describe("XML escaping in prompts", () => {
90
90
  describe("continuationPrompt", () => {
91
91
  it("含 objective + Turn + Completion audit 段落", () => {
92
92
  const state = makeState({ currentTurnIndex: 3 });
93
- const out = continuationPrompt(state, 0);
93
+ const out = continuationPrompt(state);
94
94
  expect(out).toContain("Turn 3");
95
95
  expect(out).toContain("test objective");
96
96
  expect(out).toContain("Completion audit");
@@ -98,7 +98,7 @@ describe("continuationPrompt", () => {
98
98
 
99
99
  it("对标 Codex 三约束:Completion audit / Fidelity / Blocked 完整", () => {
100
100
  const state = makeState();
101
- const out = continuationPrompt(state, 0);
101
+ const out = continuationPrompt(state);
102
102
  // Completion audit: 逐项证据验证
103
103
  expect(out).toContain("Completion audit");
104
104
  expect(out).toContain("Evidence must prove completion");
@@ -115,14 +115,14 @@ describe("continuationPrompt", () => {
115
115
 
116
116
  it("软建议 complete 前完成所有 todo(FR-6,全解耦后非强制)", () => {
117
117
  const state = makeState();
118
- const out = continuationPrompt(state, 0);
118
+ const out = continuationPrompt(state);
119
119
  expect(out).toContain("Recommend finishing all todos");
120
120
  expect(out).toContain("verification todos");
121
121
  });
122
122
 
123
123
  it("含 plan audit 软提醒(FR-7/D27 prompt 驱动)", () => {
124
124
  const state = makeState();
125
- const out = continuationPrompt(state, 0);
125
+ const out = continuationPrompt(state);
126
126
  expect(out).toContain("plan.md step was executed");
127
127
  });
128
128
  });
@@ -165,7 +165,7 @@ describe("contextInjectionPrompt", () => {
165
165
  budget: { tokenBudget: 1000 },
166
166
  tokensUsed: 200,
167
167
  });
168
- const out = contextInjectionPrompt(state, 60);
168
+ const out = contextInjectionPrompt(state);
169
169
  expect(out).toContain("GOAL mode activated");
170
170
  expect(out).toContain("Status: active");
171
171
  expect(out).toContain("Turn: 2");
@@ -186,13 +186,13 @@ describe("contextInjectionPrompt", () => {
186
186
  budget: { tokenBudget: 1000 },
187
187
  tokensUsed: 200,
188
188
  });
189
- const out = contextInjectionPrompt(state, 60);
189
+ const out = contextInjectionPrompt(state);
190
190
  expect(out.length).toBeLessThanOrEqual(600);
191
191
  });
192
192
 
193
193
  it("删除冗余段(TC3):不含 todo 引导/plan 提示/Fidelity/Audit", () => {
194
194
  const state = makeState();
195
- const out = contextInjectionPrompt(state, 0);
195
+ const out = contextInjectionPrompt(state);
196
196
  // todo 引导段已删(收敛到 continuation 软建议)
197
197
  expect(out).not.toContain("Track work with todos");
198
198
  expect(out).not.toContain("todo tool");
@@ -206,17 +206,17 @@ describe("contextInjectionPrompt", () => {
206
206
 
207
207
  it("无 goal_manager 引用(#1 清理后)", () => {
208
208
  const state = makeState();
209
- const out = contextInjectionPrompt(state, 0);
209
+ const out = contextInjectionPrompt(state);
210
210
  expect(out).not.toContain("goal_manager");
211
211
  expect(out).not.toContain("create_tasks");
212
212
  expect(out).not.toContain("add_subtasks");
213
213
  });
214
214
 
215
- it("无 planAvailable 参数(TC4 签名精简:仅 state + timeUsedSeconds)", () => {
216
- // planAvailable 恒 true 死分支已删;签名仅 (state, timeUsedSeconds)
215
+ it("无 planAvailable 参数(TC4 签名精简:仅 state)", () => {
216
+ // planAvailable 恒 true 死分支已删;签名仅 (state)
217
217
  const state = makeState();
218
- expect(() => contextInjectionPrompt(state, 0)).not.toThrow();
219
- const out = contextInjectionPrompt(state, 0);
218
+ expect(() => contextInjectionPrompt(state)).not.toThrow();
219
+ const out = contextInjectionPrompt(state);
220
220
  expect(out).toContain("[GOAL mode activated]");
221
221
  });
222
222
  });
@@ -226,7 +226,7 @@ describe("contextInjectionPrompt", () => {
226
226
  describe("successCriteria 注入(<successCriteria> 段 + 条件文案)", () => {
227
227
  it("continuationPrompt:有 successCriteria → 含 <successCriteria> 段 + 条件文案", () => {
228
228
  const state = makeState({ successCriteria: ["pnpm test passes", "tsc clean"] });
229
- const out = continuationPrompt(state, 0);
229
+ const out = continuationPrompt(state);
230
230
  expect(out).toContain("<successCriteria>");
231
231
  expect(out).toContain("</successCriteria>");
232
232
  expect(out).toContain("1. pnpm test passes");
@@ -249,7 +249,7 @@ describe("successCriteria 注入(<successCriteria> 段 + 条件文案)", ()
249
249
 
250
250
  it("contextInjectionPrompt:有 successCriteria → 含 <successCriteria> 段 + 条件文案", () => {
251
251
  const state = makeState({ successCriteria: ["file X exists"] });
252
- const out = contextInjectionPrompt(state, 0);
252
+ const out = contextInjectionPrompt(state);
253
253
  expect(out).toContain("<successCriteria>");
254
254
  expect(out).toContain("file X exists");
255
255
  // 条件文案(contextInjectionPrompt 专属)
@@ -266,24 +266,24 @@ describe("successCriteria 注入(<successCriteria> 段 + 条件文案)", ()
266
266
 
267
267
  it("无 successCriteria → 不含 <successCriteria> 段(锁定 fallback)", () => {
268
268
  const state = makeState(); // createGoalState 不带 successCriteria
269
- const out = continuationPrompt(state, 0);
269
+ const out = continuationPrompt(state);
270
270
  expect(out).not.toContain("<successCriteria>");
271
271
  expect(out).not.toContain("every condition there must be met");
272
272
  });
273
273
 
274
274
  it("U17: successCriteria 空数组 → 与 undefined 同为空态,prompt 不增 <successCriteria> 段", () => {
275
275
  const state = makeState({ successCriteria: [] });
276
- expect(continuationPrompt(state, 0)).not.toContain("<successCriteria>");
276
+ expect(continuationPrompt(state)).not.toContain("<successCriteria>");
277
277
  expect(budgetLimitPrompt(state)).not.toContain("<successCriteria>");
278
278
  expect(objectiveUpdatedPrompt(state, "old obj")).not.toContain("<successCriteria>");
279
- expect(contextInjectionPrompt(state, 0)).not.toContain("<successCriteria>");
279
+ expect(contextInjectionPrompt(state)).not.toContain("<successCriteria>");
280
280
  // 条件文案同样不出现(continuationPrompt 专属分支)
281
- expect(continuationPrompt(state, 0)).not.toContain("every condition there must be met");
281
+ expect(continuationPrompt(state)).not.toContain("every condition there must be met");
282
282
  });
283
283
 
284
284
  it("successCriteria 中的 <>& 被转义(防注入)", () => {
285
285
  const state = makeState({ successCriteria: ["<x> & y"] });
286
- const out = continuationPrompt(state, 0);
286
+ const out = continuationPrompt(state);
287
287
  expect(out).toContain("&lt;x&gt;");
288
288
  expect(out).toContain("&amp; y");
289
289
  expect(out).not.toContain("<x>");
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * 覆盖:
5
5
  * - toSingleLine: 多行压缩
6
- * - renderStatusLine: 状态后缀 + cancelled 短路 + 预算/停滞指标
7
- * - renderWidgetLines: cancelled 短路 + objective + 预算进度条
6
+ * - renderStatusLine: slug 标题(fallback objective 截断)+ 状态后缀 + cancelled 短路 + token/耗时显示
7
+ * - renderWidgetLines: cancelled 短路 + 标题行 + criteria 摘要单行 + 预算进度条(Objective 全文行已移除,GAP-8)
8
8
  * - renderTerminalStatusLine: 终态单行 + cancelled 短路
9
9
  * - updateWidget: FR-6.6 hasUI 守卫 + 终态折叠 + cancelled 清除
10
10
  *
@@ -37,7 +37,7 @@ import { formatTokens } from "./widget";
37
37
  * 维度说明:status→severity(本函数)与 percent→color(widget.getBudgetColor)输入输出
38
38
  * 双维度不同,不合并——本函数只管「状态语义」的严重度。
39
39
  */
40
- export function goalStatusSeverity(status: GoalStatus): "ok" | "warn" | "danger" {
40
+ function goalStatusSeverity(status: GoalStatus): "ok" | "warn" | "danger" {
41
41
  switch (status) {
42
42
  case "active":
43
43
  case "complete":
@@ -1,8 +1,7 @@
1
1
  /**
2
2
  * Steering prompt 模板(projection 层)
3
3
  *
4
- * 类型 import 自 engine/types.ts;时间计算参数化(调用方传
5
- * timeUsedSeconds,不调 Date.now())。FR-3.4:formatBudget 单一出口。
4
+ * 类型 import 自 engine/types.ts。FR-3.4:formatBudget 单一出口。
6
5
  *
7
6
  * 设计原则(来自 Codex 调研):
8
7
  * - Completion audit: 逐项证据验证,intent/partial progress 不是 evidence
@@ -55,15 +54,12 @@ export type BudgetFormatStyle = "percent" | "line";
55
54
  * - "line" → ` | Tokens: remaining/total`(continuationPrompt 用)
56
55
  *
57
56
  * @param state runtime state(读 budget / tokensUsed)
58
- * @param timeUsedSeconds 累计耗时秒数(保留位,FR-3.4 统一签名;percent/line 当前不消费)
59
57
  * @param style 输出形式
60
58
  */
61
59
  export function formatBudget(
62
60
  state: GoalRuntimeState,
63
- timeUsedSeconds: number,
64
61
  style: BudgetFormatStyle,
65
62
  ): string {
66
- void timeUsedSeconds; // FR-3.4 统一签名占位(percent/line 不消费,保留以便未来样式扩展)
67
63
  if (style === "percent") {
68
64
  return formatBudgetPercent(state);
69
65
  }
@@ -88,9 +84,9 @@ function formatBudgetLine(state: GoalRuntimeState): string {
88
84
 
89
85
  // ── Continuation Prompt ───────────────────────────────
90
86
 
91
- export function continuationPrompt(state: GoalRuntimeState, timeUsedSeconds: number): string {
87
+ export function continuationPrompt(state: GoalRuntimeState): string {
92
88
  const objective = escapeXmlText(state.objective);
93
- const budgetLine = formatBudget(state, timeUsedSeconds, "line");
89
+ const budgetLine = formatBudget(state, "line");
94
90
  const criteria = successCriteriaBlock(state);
95
91
 
96
92
  return (
@@ -192,12 +188,9 @@ export function objectiveUpdatedPrompt(
192
188
  * turn 末详述),避免两 prompt 重复。
193
189
  * 删 planAvailable 参数(恒 true 死分支;plan 引导收敛到 continuation)。
194
190
  */
195
- export function contextInjectionPrompt(
196
- state: GoalRuntimeState,
197
- timeUsedSeconds: number,
198
- ): string {
191
+ export function contextInjectionPrompt(state: GoalRuntimeState): string {
199
192
  const objective = escapeXmlText(state.objective);
200
- const budgetInfo = formatBudget(state, timeUsedSeconds, "percent");
193
+ const budgetInfo = formatBudget(state, "percent");
201
194
  const criteria = successCriteriaBlock(state);
202
195
 
203
196
  return (
@@ -67,7 +67,7 @@ export function formatTokens(n: number): string {
67
67
  /**
68
68
  * 返回 widget 标题:优先 slug,无 slug fallback objective 截断(单行)。
69
69
  */
70
- export function getTitle(state: GoalRuntimeState): string {
70
+ function getTitle(state: GoalRuntimeState): string {
71
71
  if (state.slug) return state.slug;
72
72
  const objSingleLine = toSingleLine(state.objective);
73
73
  return objSingleLine.length > OBJECTIVE_DISPLAY_LIMIT
package/src/service.ts CHANGED
@@ -12,13 +12,7 @@
12
12
  * FR-6.5: persist 前调 tick 累计时间
13
13
  */
14
14
 
15
- import {
16
- accumulateTokens,
17
- checkBudgetOnResume,
18
- checkBudgetOnTurnEnd,
19
- tick,
20
- } from "./engine/budget";
21
- import type { BudgetDimension } from "./engine/budget";
15
+ import { accumulateTokens, checkBudgetOnTurnEnd, tick } from "./engine/budget";
22
16
  import { createGoalState, isActiveStatus, transitionStatus } from "./engine/goal";
23
17
  import type { BudgetConfig, GoalRuntimeState, GoalStatus } from "./engine/types";
24
18
  import { makeHistoryEntry, serializeState } from "./persistence";
@@ -277,11 +271,3 @@ export function applyEvent(
277
271
  break;
278
272
  }
279
273
  }
280
-
281
- // ── resume 预算重检(供 command-adapter 调用)─────────
282
-
283
- export function checkResumeBudget(
284
- state: GoalRuntimeState,
285
- ): { type: "exceeded"; dimension: BudgetDimension } | null {
286
- return checkBudgetOnResume(state);
287
- }
package/src/session.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Session 层 — 运行时句柄 + 状态重建 + entry GC
2
+ * Session 层 — 运行时句柄 + 状态重建(entry 不做 GC,只读最新一条)
3
3
  *
4
4
  * GoalSession 是进程内瞬态句柄(不持久化)。
5
5
  * reconstructGoalState 从 entry 恢复状态(session_start 时调)。