@zhushanwen/pi-goal 0.1.2 → 0.1.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhushanwen/pi-goal",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
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.",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
package/src/budget.ts CHANGED
@@ -147,7 +147,7 @@ export function checkProgress(state: GoalRuntimeState, tasksCompletedAtStart: nu
147
147
  return {
148
148
  allTasksDone: totalCount > 0 && incomplete.length === 0 && completedCount > 0,
149
149
  noTasksCreated: totalCount === 0,
150
- maxTurnsReached: state.turnCount >= state.budget.maxTurns,
150
+ maxTurnsReached: state.currentTurnIndex >= state.budget.maxTurns,
151
151
  isStalled: progressThisRound === 0,
152
152
  budgetTight: Boolean(
153
153
  state.budget.tokenBudget &&
package/src/index.ts CHANGED
@@ -12,7 +12,7 @@
12
12
  * 健壮性保障:
13
13
  * - goalId snapshot 防止旧回调操作新 goal
14
14
  * - 时间累计统一由 persistState 管理,无双写
15
- * - 防重入保护(hasPendingInjection)
15
+ * - before_agent_start 注入 context,agent_end 负责 continuation(预算检查/进度评估/续跑)
16
16
  * - deserializeState 向后兼容旧格式
17
17
  */
18
18
 
@@ -150,19 +150,19 @@ async function handleGoalCommand(pi: ExtensionAPI, session: GoalSession, args: s
150
150
  switch (parsed.action) {
151
151
  case "status": {
152
152
  if (!session.state) {
153
- ctx.ui.notify("Goal 模式未激活。使用 /goal <objective> 启动。", "info");
153
+ ctx.ui.notify("Goal mode not active. Use /goal <objective> to start.", "info");
154
154
  return;
155
155
  }
156
156
  const completed = getCompletedCount(session.state.tasks);
157
157
  const total = session.state.tasks.length;
158
158
  const elapsed = getElapsedTimeSeconds(session.state);
159
159
  const lines = [
160
- `目标: ${session.state.objective}`,
161
- `状态: ${session.state.status}`,
162
- `轮次: ${session.state.turnCount}/${session.state.budget.maxTurns}`,
163
- `任务: ${completed}/${total} 完成`,
164
- `无进展轮数: ${session.state.stallCount}`,
165
- `已用时间: ${Math.floor(elapsed / SECONDS_PER_MINUTE)}分${Math.floor(elapsed % SECONDS_PER_MINUTE)}秒`,
160
+ `Objective: ${session.state.objective}`,
161
+ `Status: ${session.state.status}`,
162
+ `Turn: ${session.state.currentTurnIndex}/${session.state.budget.maxTurns}`,
163
+ `Tasks: ${completed}/${total} completed`,
164
+ `Stall turns: ${session.state.stallCount}`,
165
+ `Time elapsed: ${Math.floor(elapsed / SECONDS_PER_MINUTE)}m${Math.floor(elapsed % SECONDS_PER_MINUTE)}s`,
166
166
  session.state.budget.tokenBudget ? `Token: ${session.state.tokensUsed}/${session.state.budget.tokenBudget}` : null,
167
167
  `Goal ID: ${session.state.goalId}`,
168
168
  ].filter(Boolean);
@@ -172,31 +172,31 @@ async function handleGoalCommand(pi: ExtensionAPI, session: GoalSession, args: s
172
172
 
173
173
  case "pause": {
174
174
  if (!session.state) {
175
- ctx.ui.notify("Goal 模式未激活。", "warning");
175
+ ctx.ui.notify("Goal mode not active.", "warning");
176
176
  return;
177
177
  }
178
178
  if (isTerminalStatus(session.state.status)) {
179
- ctx.ui.notify(`Goal 已处于终态 (${session.state.status}),无法暂停。`, "warning");
179
+ ctx.ui.notify(`Goal is in terminal state (${session.state.status}), cannot pause.`, "warning");
180
180
  return;
181
181
  }
182
182
  session.state.status = transitionStatus(session.state.status, "paused");
183
183
  persistGoalState(pi, session, ctx);
184
184
  updateWidget(session, ctx);
185
- ctx.ui.notify("Goal 已暂停。使用 /goal resume 恢复。", "info");
185
+ ctx.ui.notify("Goal paused. Use /goal resume to continue.", "info");
186
186
  return;
187
187
  }
188
188
 
189
189
  case "resume": {
190
190
  if (!session.state) {
191
- ctx.ui.notify("Goal 模式未激活。", "warning");
191
+ ctx.ui.notify("Goal mode not active.", "warning");
192
192
  return;
193
193
  }
194
194
  if (isTerminalStatus(session.state.status)) {
195
- ctx.ui.notify(`Goal 已处于终态 (${session.state.status}),无法恢复。`, "warning");
195
+ ctx.ui.notify(`Goal is in terminal state (${session.state.status}), cannot resume.`, "warning");
196
196
  return;
197
197
  }
198
198
  if (session.state.status !== "paused" && session.state.status !== "blocked") {
199
- ctx.ui.notify("Goal 未暂停或阻塞,无需恢复。", "info");
199
+ ctx.ui.notify("Goal is not paused or blocked, no need to resume.", "info");
200
200
  return;
201
201
  }
202
202
  session.state.status = "active";
@@ -210,7 +210,7 @@ async function handleGoalCommand(pi: ExtensionAPI, session: GoalSession, args: s
210
210
  session.state.status = transitionStatus(session.state.status, dim === "token" ? "budget_limited" : "time_limited");
211
211
  persistGoalState(pi, session, ctx);
212
212
  updateWidget(session, ctx);
213
- ctx.ui.notify(`${dim === "token" ? "Token" : "时间"} 预算已耗尽,无法恢复。使用 /goal clear 清除。`, "warning");
213
+ ctx.ui.notify(`${dim === "token" ? "Token" : "Time"} budget exhausted, cannot resume. Use /goal clear to reset.`, "warning");
214
214
  return;
215
215
  }
216
216
 
@@ -220,17 +220,17 @@ async function handleGoalCommand(pi: ExtensionAPI, session: GoalSession, args: s
220
220
  const incomplete = getIncompleteTasks(session.state.tasks);
221
221
  if (incomplete.length > 0) {
222
222
  pi.sendUserMessage(
223
- `Goal 已恢复。继续执行剩余 ${incomplete.length} 个任务。` +
223
+ `Goal resumed. Continuing with ${incomplete.length} remaining tasks.` +
224
224
  (session.state.lastBlockerReason ? `
225
225
 
226
- 上次阻塞原因: ${session.state.lastBlockerReason}。请尝试不同的方法。` : "") +
226
+ Previous blocker: ${session.state.lastBlockerReason}. Try a different approach.` : "") +
227
227
  `
228
228
 
229
- 目标: ${session.state.objective}`,
229
+ Objective: ${session.state.objective}`,
230
230
  { deliverAs: "followUp" },
231
231
  );
232
232
  } else {
233
- ctx.ui.notify("所有任务已完成。", "info");
233
+ ctx.ui.notify("All tasks completed.", "info");
234
234
  }
235
235
  return;
236
236
  }
@@ -250,13 +250,13 @@ async function handleGoalCommand(pi: ExtensionAPI, session: GoalSession, args: s
250
250
  }>>;
251
251
 
252
252
  if (historyEntries.length === 0) {
253
- ctx.ui.notify("暂无历史 Goal", "info");
253
+ ctx.ui.notify("No goal history", "info");
254
254
  return;
255
255
  }
256
256
 
257
257
  // 按时间倒序
258
258
  const sorted = [...historyEntries].reverse();
259
- const lines: string[] = ["历史 Goal:\n"];
259
+ const lines: string[] = ["Goal history:\n"];
260
260
  for (let i = 0; i < sorted.length; i++) {
261
261
  const h = sorted[i]!.data;
262
262
  if (!h) continue;
@@ -271,7 +271,7 @@ async function handleGoalCommand(pi: ExtensionAPI, session: GoalSession, args: s
271
271
  const mins = Math.floor(h.elapsedSeconds / SECONDS_PER_MINUTE);
272
272
  const secs = Math.floor(h.elapsedSeconds % SECONDS_PER_MINUTE);
273
273
  lines.push(`${i + 1}. ${statusIcon} ${objDisplay}`);
274
- lines.push(` ${h.completedTasks}/${h.totalTasks} 任务 | ${mins}分${secs} | ${h.status}`);
274
+ lines.push(` ${h.completedTasks}/${h.totalTasks} tasks | ${mins}m${secs}s | ${h.status}`);
275
275
  }
276
276
  ctx.ui.notify(lines.join("\n"), "info");
277
277
  return;
@@ -279,7 +279,7 @@ async function handleGoalCommand(pi: ExtensionAPI, session: GoalSession, args: s
279
279
 
280
280
  case "clear": {
281
281
  if (!session.state) {
282
- ctx.ui.notify("Goal 模式未激活。", "info");
282
+ ctx.ui.notify("Goal mode not active.", "info");
283
283
  return;
284
284
  }
285
285
  session.state.status = "cancelled";
@@ -287,17 +287,17 @@ async function handleGoalCommand(pi: ExtensionAPI, session: GoalSession, args: s
287
287
  writeGoalHistoryEntry(pi, session);
288
288
  persistGoalState(pi, session, ctx);
289
289
  clearGoalSession(session, ctx);
290
- ctx.ui.notify("Goal 已清除。", "info");
290
+ ctx.ui.notify("Goal cleared.", "info");
291
291
  return;
292
292
  }
293
293
 
294
294
  case "update": {
295
295
  if (!session.state) {
296
- ctx.ui.notify("Goal 模式未激活。", "warning");
296
+ ctx.ui.notify("Goal mode not active.", "warning");
297
297
  return;
298
298
  }
299
299
  if (!parsed.objective) {
300
- ctx.ui.notify("用法: /goal update <new-objective>", "warning");
300
+ ctx.ui.notify("Usage: /goal update <new-objective>", "warning");
301
301
  return;
302
302
  }
303
303
  const oldObjective = session.state.objective;
@@ -306,6 +306,7 @@ async function handleGoalCommand(pi: ExtensionAPI, session: GoalSession, args: s
306
306
  session.state.tasks = [];
307
307
  session.state.stallCount = 0;
308
308
  session.state.turnCount = 0;
309
+ session.state.currentTurnIndex = 0;
309
310
  session.state.lastProgressTurn = 0;
310
311
  session.state.budgetLimitSteeringSent = false;
311
312
  session.state.budgetWarning70Sent = false;
@@ -313,7 +314,7 @@ async function handleGoalCommand(pi: ExtensionAPI, session: GoalSession, args: s
313
314
  session.tasksCompletedAtAgentStart = 0;
314
315
  persistGoalState(pi, session, ctx);
315
316
  updateWidget(session, ctx);
316
- ctx.ui.notify(`目标已更新:\n旧: ${oldObjective}\n新: ${parsed.objective}`, "info");
317
+ ctx.ui.notify(`Objective updated:\nPrevious: ${oldObjective}\nNew: ${parsed.objective}`, "info");
317
318
 
318
319
  if (isActiveStatus(session.state.status)) {
319
320
  pi.sendUserMessage(objectiveUpdatedPrompt(session.state, oldObjective), { deliverAs: "steer" });
@@ -323,16 +324,16 @@ async function handleGoalCommand(pi: ExtensionAPI, session: GoalSession, args: s
323
324
 
324
325
  case "set": {
325
326
  if (!parsed.objective) {
326
- ctx.ui.notify("用法: /goal <objective> [--tokens N] [--timeout N]", "warning");
327
+ ctx.ui.notify("Usage: /goal <objective> [--tokens N] [--timeout N]", "warning");
327
328
  return;
328
329
  }
329
330
  if (!parsed.objective.trim()) {
330
- ctx.ui.notify("目标描述不能为空。", "warning");
331
+ ctx.ui.notify("Objective cannot be empty.", "warning");
331
332
  return;
332
333
  }
333
334
  if (session.state && !isTerminalStatus(session.state.status)) {
334
335
  ctx.ui.notify(
335
- `已取消旧 Goal: ${session.state.objective}\n(新目标已启动)`,
336
+ `Cancelled previous Goal: ${session.state.objective}\n(new goal started)`,
336
337
  "info",
337
338
  );
338
339
  session.state.status = "cancelled";
@@ -342,7 +343,7 @@ async function handleGoalCommand(pi: ExtensionAPI, session: GoalSession, args: s
342
343
  }
343
344
 
344
345
  if (parsed.budget?.tokenBudget !== undefined && parsed.budget.tokenBudget <= 0) {
345
- ctx.ui.notify("Token 预算必须大于 0", "warning");
346
+ ctx.ui.notify("Token budget must be greater than 0.", "warning");
346
347
  return;
347
348
  }
348
349
  const budget: Partial<BudgetConfig> = {};
@@ -359,11 +360,11 @@ async function handleGoalCommand(pi: ExtensionAPI, session: GoalSession, args: s
359
360
  updateWidget(session, ctx);
360
361
 
361
362
  const budgetNotice: string[] = [];
362
- if (budget.tokenBudget) budgetNotice.push(`Token 预算: ${budget.tokenBudget}`);
363
- if (budget.timeBudgetMinutes) budgetNotice.push(`时间预算: ${budget.timeBudgetMinutes} 分钟`);
363
+ if (budget.tokenBudget) budgetNotice.push(`Token budget: ${budget.tokenBudget}`);
364
+ if (budget.timeBudgetMinutes) budgetNotice.push(`Time budget: ${budget.timeBudgetMinutes} min`);
364
365
  const notice = [
365
- `Goal 已启动: ${parsed.objective}`,
366
- `最大轮次: ${budget.maxTurns}`,
366
+ `Goal started: ${parsed.objective}`,
367
+ `Max turns: ${budget.maxTurns}`,
367
368
  ...budgetNotice,
368
369
  ].join("\n");
369
370
  ctx.ui.notify(notice, "info");
@@ -477,11 +478,11 @@ async function handleBeforeAgentStart(pi: ExtensionAPI, session: GoalSession, ct
477
478
  message: {
478
479
  customType: "goal-context-exceeded",
479
480
  content:
480
- "[GOAL — 上下文空间不足,必须立即收尾]\n" +
481
- "1. goal_manager list_tasks 查看剩余任务\n" +
482
- "2. 只标记你真正完成且有证据的任务\n" +
483
- "3. 总结当前进度和剩余工作\n" +
484
- "不要再开始新任务。",
481
+ "[GOAL — context space low, must wrap up now]\n" +
482
+ "1. Use goal_manager's list_tasks to check remaining tasks\n" +
483
+ "2. Only mark tasks you genuinely completed with evidence\n" +
484
+ "3. Summarize current progress and remaining work\n" +
485
+ "Do not start new tasks.",
485
486
  display: false,
486
487
  },
487
488
  };
@@ -510,7 +511,7 @@ async function handleAgentEnd(pi: ExtensionAPI, session: GoalSession, ctx: Exten
510
511
  if (checkStale()) return;
511
512
  updateWidget(session, ctx);
512
513
  ctx.ui.notify(
513
- `目标已完成 ✓ (${getCompletedCount(session.state.tasks)}/${session.state.tasks.length} 任务, ${session.state.turnCount} )`,
514
+ `Objective completed ✓ (${getCompletedCount(session.state.tasks)}/${session.state.tasks.length} tasks, ${session.state.currentTurnIndex} turns)`,
514
515
  "info",
515
516
  );
516
517
  return;
@@ -520,18 +521,12 @@ async function handleAgentEnd(pi: ExtensionAPI, session: GoalSession, ctx: Exten
520
521
  persistGoalState(pi, session, ctx);
521
522
  if (checkStale()) return;
522
523
  updateWidget(session, ctx);
523
- ctx.ui.notify("Goal 被阻塞。使用 /goal resume 恢复或 /goal clear 清除。", "warning");
524
+ ctx.ui.notify("Goal blocked. Use /goal resume to continue or /goal clear to reset.", "warning");
524
525
  return;
525
526
  }
526
527
 
527
528
  if (!isActiveStatus(session.state.status)) return;
528
529
 
529
- // 防重入
530
- if (session.hasPendingInjection) {
531
- session.hasPendingInjection = false;
532
- return;
533
- }
534
-
535
530
  if (checkStale()) return;
536
531
 
537
532
  // ── 预算策略(集中检查)──
@@ -542,10 +537,10 @@ async function handleAgentEnd(pi: ExtensionAPI, session: GoalSession, ctx: Exten
542
537
  for (const w of budgetResult.warnings) {
543
538
  if (w.type === "warning90") {
544
539
  session.state.budgetWarning90Sent = true;
545
- ctx.ui.notify(`${w.dimension === "token" ? "Token" : "时间"} 预算已用 90%,请开始收尾。`, "warning");
540
+ ctx.ui.notify(`${w.dimension === "token" ? "Token" : "Time"} budget 90% used — start wrapping up.`, "warning");
546
541
  } else if (w.type === "warning70") {
547
542
  session.state.budgetWarning70Sent = true;
548
- ctx.ui.notify(`${w.dimension === "token" ? "Token" : "时间"} 预算已用 70%,注意控制范围。`, "info");
543
+ ctx.ui.notify(`${w.dimension === "token" ? "Token" : "Time"} budget 70% used — keep scope in check.`, "info");
549
544
  }
550
545
  }
551
546
 
@@ -560,8 +555,8 @@ async function handleAgentEnd(pi: ExtensionAPI, session: GoalSession, ctx: Exten
560
555
  updateWidget(session, ctx);
561
556
  ctx.ui.notify(
562
557
  dim === "token"
563
- ? "Token 预算已耗尽,Goal 已终止。"
564
- : `时间预算耗尽 (${session.state.budget.timeBudgetMinutes} 分钟)Goal 已终止。`,
558
+ ? "Token budget exhausted, Goal terminated."
559
+ : `Time budget exhausted (${session.state.budget.timeBudgetMinutes} min), Goal terminated.`,
565
560
  "warning",
566
561
  );
567
562
  return;
@@ -595,7 +590,7 @@ async function handleAgentEnd(pi: ExtensionAPI, session: GoalSession, ctx: Exten
595
590
  if (checkStale()) return;
596
591
  updateWidget(session, ctx);
597
592
  ctx.ui.notify(
598
- `所有任务已完成,Goal 自动结束。(${progress.completedCount}/${progress.totalCount} 任务, ${session.state.turnCount} )`,
593
+ `All tasks completed, Goal auto-closed. (${progress.completedCount}/${progress.totalCount} tasks, ${session.state.currentTurnIndex} turns)`,
599
594
  "info",
600
595
  );
601
596
  return;
@@ -603,15 +598,15 @@ async function handleAgentEnd(pi: ExtensionAPI, session: GoalSession, ctx: Exten
603
598
 
604
599
  if (progress.budgetTight) {
605
600
  pi.sendUserMessage(
606
- `所有任务已完成,且 token 预算已用 ${Math.round(session.state.tokensUsed / session.state.budget.tokenBudget! * PERCENT_FACTOR)}%。` +
607
- `请立即调用 goal_manager complete_goal 完成目标,提供整体 evidence。` +
608
- `\n\n目标: ${session.state.objective}`,
601
+ `All tasks completed, token budget ${Math.round(session.state.tokensUsed / session.state.budget.tokenBudget! * PERCENT_FACTOR)}% used.` +
602
+ `Call goal_manager's complete_goal now with overall evidence.` +
603
+ `\n\nObjective: ${session.state.objective}`,
609
604
  { deliverAs: "steer" },
610
605
  );
611
606
  } else {
612
607
  pi.sendUserMessage(
613
- `所有 ${progress.totalCount} 个任务已完成。请调用 goal_manager complete_goal 完成目标,提供整体 evidence。` +
614
- `\n\n目标: ${session.state.objective}`,
608
+ `All ${progress.totalCount} tasks completed. Call goal_manager's complete_goal with overall evidence.` +
609
+ `\n\nObjective: ${session.state.objective}`,
615
610
  { deliverAs: "followUp" },
616
611
  );
617
612
  }
@@ -630,14 +625,14 @@ async function handleAgentEnd(pi: ExtensionAPI, session: GoalSession, ctx: Exten
630
625
  if (checkStale()) return;
631
626
  updateWidget(session, ctx);
632
627
  ctx.ui.notify(
633
- `已达最大轮次 (${session.state.budget.maxTurns})LLM 未创建任务清单。`,
628
+ `Max turns reached (${session.state.budget.maxTurns}), LLM did not create task list.`,
634
629
  "warning",
635
630
  );
636
631
  return;
637
632
  }
638
633
  pi.sendUserMessage(
639
- `你尚未创建任务清单。请立即调用 goal_manager create_tasks 将工作拆分为具体可验证的任务步骤。` +
640
- `\n\n目标: ${session.state.objective}`,
634
+ `No task list created yet. Call goal_manager's create_tasks immediately to decompose the work into verifiable task steps.` +
635
+ `\n\nObjective: ${session.state.objective}`,
641
636
  { deliverAs: "followUp" },
642
637
  );
643
638
  persistGoalState(pi, session, ctx);
@@ -655,7 +650,7 @@ async function handleAgentEnd(pi: ExtensionAPI, session: GoalSession, ctx: Exten
655
650
  if (checkStale()) return;
656
651
  updateWidget(session, ctx);
657
652
  ctx.ui.notify(
658
- `已达最大轮次 (${session.state.budget.maxTurns}),还有 ${incomplete.length} 个任务未完成。`,
653
+ `Max turns reached (${session.state.budget.maxTurns}), ${incomplete.length} tasks still incomplete.`,
659
654
  "warning",
660
655
  );
661
656
  return;
@@ -666,7 +661,7 @@ async function handleAgentEnd(pi: ExtensionAPI, session: GoalSession, ctx: Exten
666
661
  session.state.stallCount++;
667
662
  } else {
668
663
  session.state.stallCount = 0;
669
- session.state.lastProgressTurn = session.state.turnCount;
664
+ session.state.lastProgressTurn = session.state.currentTurnIndex;
670
665
  }
671
666
 
672
667
  if (session.state.stallCount >= session.state.budget.maxStallTurns) {
@@ -675,7 +670,7 @@ async function handleAgentEnd(pi: ExtensionAPI, session: GoalSession, ctx: Exten
675
670
  if (checkStale()) return;
676
671
  updateWidget(session, ctx);
677
672
  ctx.ui.notify(
678
- `已连续 ${session.state.stallCount} 轮无进展,Goal 自动阻塞。使用 /goal resume 恢复或 /goal clear 清除。`,
673
+ `${session.state.stallCount} consecutive turns without progress, Goal auto-blocked. Use /goal resume to continue or /goal clear to reset.`,
679
674
  "warning",
680
675
  );
681
676
  return;
@@ -715,33 +710,33 @@ export default function goalExtension(pi: ExtensionAPI) {
715
710
  name: "goal_manager",
716
711
  label: "Goal Manager",
717
712
  description:
718
- "Goal 模式任务管理器。此工具仅在用户通过 /goal 命令启动目标后才可用,AI 不能主动触发此功能。如果 Goal 模式未激活,调用此工具会报错。" +
719
- "\n\n可用 action:" +
720
- "\n- create_tasks: 首次拆分目标为任务清单(每个 goal 开始时调用一次)。每条 task description 必须是一行简短摘要(不超过 60 字),不要包含换行、markdown、详细参数" +
721
- "\n- add_tasks: 向已有任务清单追加新任务(执行中发现遗漏时使用)。每条 task description 必须是一行简短摘要(不超过 60 字),不要包含换行、markdown、详细参数" +
722
- "\n- update_tasks: 批量更新任务状态(completed 必须带 evidencecancelled 不阻碍 goal 完成)" +
723
- "\n- list_tasks: 查看进度和剩余预算" +
724
- "\n- complete_goal: 标记目标达成(必须所有任务完成 + evidence" +
725
- "\n- cancel_goal: 取消当前目标(用户要求退出/停止时使用)" +
726
- "\n- report_blocked: 报告阻塞(遇到无法解决的问题时使用)" +
727
- "\n- add_subtasks: 给指定 task 添加 subtask(参数: taskId, texts[])。Goal 模式下用此替代 todo 工具" +
728
- "\n- update_subtasks: 批量更新 subtask 状态(参数: taskId, subUpdates[]" +
729
- "\n- delete_subtasks: 删除指定 task subtask(参数: taskId, subIds[]",
730
- promptSnippet: "管理 /goal 模式的任务清单、完成状态和退出",
713
+ "Goal mode task manager. This tool is only available after starting a goal via the /goal command. AI cannot trigger it proactively. If Goal mode is not active, calling this tool will error." +
714
+ "\n\nAvailable actions:" +
715
+ "\n- create_tasks: Decompose the objective into a task list (call once at goal start). Each task description must be a one-line summary (max 60 chars), no newlines or markdown" +
716
+ "\n- add_tasks: Append new tasks to the existing list (when omissions are discovered). Each task description must be a one-line summary (max 60 chars), no newlines or markdown" +
717
+ "\n- update_tasks: Batch update task statuses (completed requires evidence, cancelled does not block goal completion)" +
718
+ "\n- list_tasks: View progress and remaining budget" +
719
+ "\n- complete_goal: Mark the objective as achieved (all tasks must be completed + evidence)" +
720
+ "\n- cancel_goal: Cancel the current goal (use when user wants to exit/stop)" +
721
+ "\n- report_blocked: Report being blocked (use when encountering unsolvable issues)" +
722
+ "\n- add_subtasks: Add subtasks to a specified task (params: taskId, texts[]). Use this instead of todo tool in Goal mode" +
723
+ "\n- update_subtasks: Batch update subtask statuses (params: taskId, subUpdates[])" +
724
+ "\n- delete_subtasks: Delete subtasks from a specified task (params: taskId, subIds[])",
725
+ promptSnippet: "Manage task list, completion status, and exit for /goal mode",
731
726
  promptGuidelines: [
732
- "[工作流] 收到目标后,第一步必须调用 create_tasks 拆分任务。已有任务清单时不要重复调用",
733
- "[格式] 每个 task description 必须是一行简短摘要,不超过 60 个字符。不要包含换行符、markdown 格式、详细参数列表——这些放在执行阶段处理。示例: '修复 hook-registry 去重逻辑' 而不是 '修复 hook-registry 去重逻辑 + transport-execute enhancementConfig 防护 + failover-loop ...'",
734
- "[追加] 执行中发现遗漏的子任务时,使用 add_tasks 追加,不要尝试重新 create_tasks",
735
- "[完成] 每完成一个任务调用 update_tasks 将状态设为 completed,必须提供 evidence(具体证据,如'测试 X 通过''文件 F 已创建'",
736
- "[目标完成] 只有所有任务完成且有整体证据时,才能调用 complete_goal",
737
- "[退出] 当用户说'停止'、'退出'、'取消'、'stop''exit''cancel''不用了''结束'等表示不想继续时,立即调用 cancel_goal 取消目标,不要引导用户走 complete_goal 流程",
738
- "[阻塞] 遇到无法解决的技术问题时调用 report_blocked 说明原因",
739
- "[进度] 随时可用 list_tasks 查看剩余任务和预算,",
740
- "[取消] 取消任务时使用 update_tasks 将状态设为 cancelled,取消的任务不阻碍 goal 完成",
741
- "[禁止] 不要在没有 evidence 的情况下将任务标记为 completed,也不要在没有 evidence 时调用 complete_goal",
742
- "[禁止] 不要在用户明确想退出时强制要求完成任务——直接 cancel_goal",
743
- "[禁止] 不要重复调用 create_tasks 覆盖已有未完成任务,如需追加请用 add_tasks",
744
- "[subtask] Goal 模式下需要细粒度步骤追踪时,使用 add_subtasks task 添加 subtask,不要使用 todo 工具",
727
+ "[Workflow] After receiving the objective, the first step must be create_tasks to decompose. Do not re-call if task list already exists",
728
+ "[Format] Each task description must be a one-line summary, max 60 chars. No newlines, markdown, or detailed parameter lists — those go in execution phase. Example: 'Fix hook-registry dedup logic' not 'Fix hook-registry dedup + transport-execute enhancementConfig guard + failover-loop ...'",
729
+ "[Append] When discovering omissions during execution, use add_tasks to append — do not re-call create_tasks",
730
+ "[Completion] After completing a task, call update_tasks with status=completed and provide evidence (e.g. 'test X passed', 'file F created')",
731
+ "[Goal completion] Only call complete_goal when all tasks are completed with overall evidence",
732
+ "[Exit] When user says 'stop', 'exit', 'cancel', '不用了', '结束', etc. indicating they don't want to continue, immediately call cancel_goal do not guide them through complete_goal",
733
+ "[Blocked] When encountering unsolvable technical issues, call report_blocked with the reason",
734
+ "[Progress] Use list_tasks anytime to check remaining tasks and budget",
735
+ "[Cancel] To cancel a task, use update_tasks with status=cancelled. Cancelled tasks do not block goal completion",
736
+ "[Forbidden] Do not mark tasks as completed without evidence, and do not call complete_goal without evidence",
737
+ "[Forbidden] Do not force task completion when the user explicitly wants to exit — call cancel_goal directly",
738
+ "[Forbidden] Do not re-call create_tasks to overwrite existing incomplete tasks — use add_tasks to append",
739
+ "[Subtask] For fine-grained step tracking in Goal mode, use add_subtasks do not use the todo tool",
745
740
  ],
746
741
  parameters: GoalManagerParams,
747
742
 
@@ -777,7 +772,7 @@ export default function goalExtension(pi: ExtensionAPI) {
777
772
  }
778
773
  const tasks = details.tasks;
779
774
  const completed = tasks.filter((t) => t.status === "completed").length;
780
- const summary = theme.fg("success", `✓ ${completed}/${tasks.length} 完成`);
775
+ const summary = theme.fg("success", `✓ ${completed}/${tasks.length} completed`);
781
776
  if (!expanded || tasks.length === 0) {
782
777
  return new Text(summary, 0, 0);
783
778
  }
@@ -816,7 +811,7 @@ export default function goalExtension(pi: ExtensionAPI) {
816
811
 
817
812
  pi.registerCommand("goal", {
818
813
  description:
819
- "目标驱动模式: /goal <objective> [--tokens N] [--timeout N] [--max-turns N] | /goal pause | /goal resume | /goal clear | /goal update <new-objective> | /goal status | /goal history",
814
+ "Goal-driven mode: /goal <objective> [--tokens N] [--timeout N] [--max-turns N] | /goal pause | /goal resume | /goal clear | /goal update <new-objective> | /goal status | /goal history",
820
815
  handler: async (args: string | undefined, ctx: ExtensionCommandContext) => {
821
816
  await handleGoalCommand(pi, session, args, ctx);
822
817
  },
@@ -890,9 +885,9 @@ export default function goalExtension(pi: ExtensionAPI) {
890
885
  pi.registerMessageRenderer(customType, (message: any, _options: any, theme: Theme) => {
891
886
  const prefix =
892
887
  message.customType === "goal-context-exceeded"
893
- ? theme.fg("error", "[GOAL 预算] ")
888
+ ? theme.fg("error", "[GOAL Budget] ")
894
889
  : message.customType === "goal-staleness-reminder"
895
- ? theme.fg("warning", "[GOAL 提醒] ")
890
+ ? theme.fg("warning", "[GOAL Reminder] ")
896
891
  : theme.fg("accent", "[GOAL] ");
897
892
  const content = typeof message.content === "string" ? message.content : JSON.stringify(message.content);
898
893
  return new Text(prefix + theme.fg("dim", content), 0, 0);
package/src/templates.ts CHANGED
@@ -33,20 +33,20 @@ export function continuationPrompt(state: GoalRuntimeState): string {
33
33
 
34
34
  // Budget info (single line, Codex style)
35
35
  const budgetLine = formatBudgetLine(state);
36
- const stallLine = state.stallCount > 0 ? `\nStall: ${state.stallCount}/${state.budget.maxStallTurns}轮无进展` : "";
36
+ const stallLine = state.stallCount > 0 ? `\nStall: ${state.stallCount}/${state.budget.maxStallTurns} turns stalled` : "";
37
37
 
38
38
  // Task summary (only IDs, not full descriptions — descriptions in before_agent_start)
39
39
  const taskLine = total > 0
40
- ? `Tasks: ${completedCount}/${total}${incomplete.length > 0 ? ` (剩余: ${incomplete.map(t => `#${t.id}`).join(",")})` : " ✓"}`
41
- : "Tasks: 未创建。请立即 create_tasks";
40
+ ? `Tasks: ${completedCount}/${total}${incomplete.length > 0 ? ` (remaining: ${incomplete.map(t => `#${t.id}`).join(",")})` : " ✓"}`
41
+ : "Tasks: Not created. Call create_tasks immediately.";
42
42
 
43
43
  return (
44
44
  `<goal_context>\n` +
45
- `[GOAL] Turn ${state.turnCount}/${state.budget.maxTurns}${budgetLine}${stallLine}\n` +
45
+ `[GOAL] Turn ${state.currentTurnIndex}/${state.budget.maxTurns}${budgetLine}${stallLine}\n` +
46
46
  `<objective>${objective}</objective>\n` +
47
47
  `${taskLine}\n` +
48
- `Rules: create_tasks→update_tasks(evidence)→complete_goal(evidence). blocked→report_blocked(reason). subtask: add_subtasks/update_subtasks (替代 todo 工具).\n` +
49
- `Audit: 逐项验证每个需求有权威证据。不因预算耗尽标记完成,不因困难标记阻塞。\n` +
48
+ `Rules: create_tasks→update_tasks(evidence)→complete_goal(evidence). blocked→report_blocked(reason). subtask: add_subtasks/update_subtasks (replaces todo tool).\n` +
49
+ `Audit: Verify each requirement has authoritative evidence. Do not mark completed due to budget exhaustion, do not mark blocked due to difficulty.\n` +
50
50
  `</goal_context>`
51
51
  );
52
52
  }
@@ -62,24 +62,24 @@ export function budgetLimitPrompt(state: GoalRuntimeState, limitType: "token" |
62
62
  const incomplete = getIncompleteTasks(state.tasks);
63
63
  const incompleteSummary =
64
64
  incomplete.length > 0
65
- ? `未完成: ${incomplete.map((t) => `#${t.id}`).join(", ")}`
66
- : "所有任务已完成。";
65
+ ? `Incomplete: ${incomplete.map((t) => `#${t.id}`).join(", ")}`
66
+ : "All tasks completed.";
67
67
 
68
68
  return (
69
69
  `<goal_context>\n` +
70
- `[GOAL — ${limitType === "token" ? "TOKEN 预算" : "时间预算"}即将耗尽]\n\n` +
70
+ `[GOAL — ${limitType === "token" ? "TOKEN budget" : "time budget"} almost exhausted]\n\n` +
71
71
  `<objective>\n${objective}\n</objective>\n\n` +
72
- `当前进度: ${completedCount}/${total} 任务完成\n` +
72
+ `Current progress: ${completedCount}/${total} tasks completed\n` +
73
73
  `${incompleteSummary}\n` +
74
74
  (limitType === "token"
75
- ? `Token 已使用: ${state.tokensUsed} / ${state.budget.tokenBudget ?? "未知"}\n`
76
- : `已用时间: ${Math.floor(elapsed / SECONDS_PER_MINUTE)}分${Math.floor(elapsed % SECONDS_PER_MINUTE)} / ${state.budget.timeBudgetMinutes ?? "未知"}分钟\n`) +
77
- `\n你必须立即收尾:\n` +
78
- `1. goal_manager list_tasks 查看剩余任务\n` +
79
- `2. 只标记你真正完成且有证据的任务\n` +
80
- `3. 如果目标已达成,调用 goal_manager complete_goal 完成目标\n` +
81
- `4. 总结当前进度和剩余工作\n` +
82
- `不要再开始新任务。不要因为预算耗尽就标记完成。\n` +
75
+ ? `Tokens used: ${state.tokensUsed} / ${state.budget.tokenBudget ?? "unknown"}\n`
76
+ : `Time elapsed: ${Math.floor(elapsed / SECONDS_PER_MINUTE)}m${Math.floor(elapsed % SECONDS_PER_MINUTE)}s / ${state.budget.timeBudgetMinutes ?? "unknown"} min\n`) +
77
+ `\nYou must wrap up immediately:\n` +
78
+ `1. Use goal_manager's list_tasks to check remaining tasks\n` +
79
+ `2. Only mark tasks you have genuinely completed with evidence\n` +
80
+ `3. If the objective is met, call goal_manager's complete_goal\n` +
81
+ `4. Summarize current progress and remaining work\n` +
82
+ `Do not start new tasks. Do not mark completed due to budget exhaustion.\n` +
83
83
  `</goal_context>`
84
84
  );
85
85
  }
@@ -92,14 +92,14 @@ export function objectiveUpdatedPrompt(state: GoalRuntimeState, oldObjective: st
92
92
 
93
93
  return (
94
94
  `<goal_context>\n` +
95
- `[GOAL — 目标已更新]\n\n` +
96
- `旧目标: ${escapedOld}\n` +
95
+ `[GOAL — Objective updated]\n\n` +
96
+ `Previous objective: ${escapedOld}\n` +
97
97
  `<untrusted_objective>\n${newObjective}\n</untrusted_objective>\n\n` +
98
- `这个新目标取代了之前的所有目标上下文。你需要:\n` +
99
- `1. 立即停止朝旧目标方向的工作\n` +
100
- `2. 重新评估任务清单,必要时调用 goal_manager create_tasks 重新拆分\n` +
101
- `3. 只在旧目标的工作也对新目标有帮助时才继续\n` +
102
- `4. 按照新目标继续工作\n` +
98
+ `This new objective supersedes all prior objective context. You must:\n` +
99
+ `1. Immediately stop working toward the old objective\n` +
100
+ `2. Re-evaluate the task list — call goal_manager's create_tasks to re-decompose if needed\n` +
101
+ `3. Only continue old work if it also serves the new objective\n` +
102
+ `4. Proceed with the new objective\n` +
103
103
  `</goal_context>`
104
104
  );
105
105
  }
@@ -114,17 +114,17 @@ export function contextInjectionPrompt(state: GoalRuntimeState): string {
114
114
 
115
115
  return (
116
116
  `<goal_context>\n` +
117
- `[GOAL 模式已激活]\n\n` +
117
+ `[GOAL mode activated]\n\n` +
118
118
  `<objective>\n${objective}\n</objective>\n` +
119
- `状态: ${state.status}\n` +
120
- `轮次: ${state.turnCount}/${state.budget.maxTurns}${budgetInfo}\n` +
121
- `任务进度: ${completedCount}/${total}\n\n` +
122
- `严格规则:\n` +
123
- `1. 第一步必须调用 goal_manager create_tasks 拆分任务(如果尚未创建)\n` +
124
- `2. 每完成一个任务调用 update_tasks 将状态设为 completed,并提供 evidence\n` +
125
- `3. 只有提供具体证据时才能调用 complete_goal\n` +
126
- `4. 遇到阻塞调用 report_blocked\n` +
127
- `5. Goal 模式下不要使用 todo 工具,使用 add_subtasks / update_subtasks 追踪细粒度步骤\n` +
119
+ `Status: ${state.status}\n` +
120
+ `Turn: ${state.currentTurnIndex}/${state.budget.maxTurns}${budgetInfo}\n` +
121
+ `Task progress: ${completedCount}/${total}\n\n` +
122
+ `Strict rules:\n` +
123
+ `1. First step: call goal_manager's create_tasks to decompose tasks (if not yet created)\n` +
124
+ `2. After completing a task, call update_tasks with status=completed and provide evidence\n` +
125
+ `3. Only call complete_goal with concrete evidence\n` +
126
+ `4. If blocked, call report_blocked\n` +
127
+ `5. In Goal mode, do not use the todo tool — use add_subtasks / update_subtasks for fine-grained tracking\n` +
128
128
  `</goal_context>`
129
129
  );
130
130
  }
@@ -144,23 +144,23 @@ export function stalenessReminderPrompt(
144
144
  const lines: string[] = [];
145
145
 
146
146
  lines.push("<goal_context>");
147
- lines.push("[GOAL 提醒有任务停滞]\n");
147
+ lines.push("[GOAL remindertasks stalled]\n");
148
148
 
149
149
  if (allTerminal) {
150
- lines.push("所有任务已完成,但 goal_manager 未关闭。请调用 complete_goal cancel_goal");
150
+ lines.push("All tasks completed but goal_manager is still open. Call complete_goal or cancel_goal.");
151
151
  } else {
152
- lines.push(`以下任务已超过 ${TASK_STALL_TURN_THRESHOLD} turn 未更新:\n`);
152
+ lines.push(`The following tasks have exceeded ${TASK_STALL_TURN_THRESHOLD} turns without update:\n`);
153
153
  for (const item of staleTasks) {
154
- lines.push(` #${item.task.id}: ${item.task.description} (${item.staleTurns} turn 未操作)`);
154
+ lines.push(` #${item.task.id}: ${item.task.description} (${item.staleTurns} turns idle)`);
155
155
  for (const s of item.staleSubtasks) {
156
- lines.push(` - ${s.text} (${s.staleTurns} turn)`);
156
+ lines.push(` - ${s.text} (${s.staleTurns} turns)`);
157
157
  }
158
158
  }
159
- lines.push("\n请检查这些任务的状态,调用 update_tasks 更新进展或 cancel 不再需要的任务。");
159
+ lines.push("\nCheck these tasks — call update_tasks to report progress or cancel tasks that are no longer needed.");
160
160
  }
161
161
 
162
- lines.push(`\n目标: ${objective}`);
163
- lines.push(`Turn: ${state.turnCount}/${state.budget.maxTurns}`);
162
+ lines.push(`\nObjective: ${objective}`);
163
+ lines.push(`Turn: ${state.currentTurnIndex}/${state.budget.maxTurns}`);
164
164
  lines.push("</goal_context>");
165
165
 
166
166
  return lines.join("\n");
@@ -177,7 +177,7 @@ function formatBudgetInfo(state: GoalRuntimeState): string {
177
177
  if (state.budget.timeBudgetMinutes) {
178
178
  const elapsed = getElapsedTimeSeconds(state);
179
179
  const pct = Math.round((elapsed / (state.budget.timeBudgetMinutes * SECONDS_PER_MINUTE)) * PERCENT_FACTOR);
180
- parts.push(`时间: ${pct}%`);
180
+ parts.push(`Time: ${pct}%`);
181
181
  }
182
182
  return parts.length > 0 ? ` (${parts.join(", ")})` : "";
183
183
  }
@@ -197,13 +197,13 @@ function formatBudgetLine(state: GoalRuntimeState): string {
197
197
  }
198
198
 
199
199
  export function formatTaskList(tasks: GoalTask[]): string {
200
- if (tasks.length === 0) return "暂无任务。";
200
+ if (tasks.length === 0) return "No tasks yet.";
201
201
  const completed = tasks.filter(t => t.status === "completed");
202
202
  const active = tasks.filter(t => t.status === "in_progress" || t.status === "pending");
203
203
  const cancelled = tasks.filter(t => t.status === "cancelled");
204
204
  const lines: string[] = [];
205
205
  if (active.length > 0) {
206
- lines.push(`进行中/待执行 (${active.length}):`);
206
+ lines.push(`In progress / Pending (${active.length}):`);
207
207
  for (const t of active) {
208
208
  const icon = t.status === "in_progress" ? "●" : "☐";
209
209
  lines.push(` ${icon} #${t.id}: ${t.description}`);
@@ -216,17 +216,17 @@ export function formatTaskList(tasks: GoalTask[]): string {
216
216
  }
217
217
  }
218
218
  if (completed.length > 0) {
219
- lines.push(`已完成 (${completed.length}):`);
219
+ lines.push(`Completed (${completed.length}):`);
220
220
  for (const t of completed) {
221
221
  const evidence = t.evidence ? ` — ${t.evidence}` : "";
222
222
  lines.push(` ✓ #${t.id}: ${t.description}${evidence}`);
223
223
  }
224
224
  }
225
225
  if (cancelled.length > 0) {
226
- lines.push(`已取消 (${cancelled.length}):`);
226
+ lines.push(`Cancelled (${cancelled.length}):`);
227
227
  for (const t of cancelled) lines.push(` ✗ #${t.id}: ${t.description}`);
228
228
  }
229
- const summary = `${completed.length}/${tasks.length} 完成` + (cancelled.length > 0 ? `, ${cancelled.length} 已取消` : "");
229
+ const summary = `${completed.length}/${tasks.length} completed` + (cancelled.length > 0 ? `, ${cancelled.length} cancelled` : "");
230
230
  lines.push(summary);
231
231
  return lines.join("\n");
232
232
  }
@@ -61,19 +61,19 @@ export const GoalManagerParams = Type.Object({
61
61
  "update_subtasks",
62
62
  "delete_subtasks",
63
63
  ] as const),
64
- tasks: Type.Optional(Type.Array(Type.String(), { description: "Task descriptions. 每条必须是一行简短摘要(不超过 60 字),不含换行或 markdown" })),
64
+ tasks: Type.Optional(Type.Array(Type.String(), { description: "Task descriptions. Each must be a one-line summary (max 60 chars), no newlines or markdown" })),
65
65
  updates: Type.Optional(Type.Array(Type.Object({
66
66
  taskId: Type.Number(),
67
67
  status: StringEnum(GOAL_TASK_STATUSES),
68
68
  evidence: Type.Optional(Type.String()),
69
69
  }))),
70
- taskId: Type.Optional(Type.Number({ description: "Task IDsubtask 操作时必需)" })),
71
- texts: Type.Optional(Type.Array(Type.String(), { description: "Subtask 文本列表(add_subtasks 时使用)" })),
70
+ taskId: Type.Optional(Type.Number({ description: "Task ID (required for subtask operations)" })),
71
+ texts: Type.Optional(Type.Array(Type.String(), { description: "Subtask text list (for add_subtasks)" })),
72
72
  subUpdates: Type.Optional(Type.Array(Type.Object({
73
73
  subId: Type.Number(),
74
74
  status: StringEnum(SUBTASK_STATUSES),
75
75
  }))),
76
- subIds: Type.Optional(Type.Array(Type.Number(), { description: "Subtask ID 列表(delete_subtasks 时使用)" })),
76
+ subIds: Type.Optional(Type.Array(Type.Number(), { description: "Subtask ID list (for delete_subtasks)" })),
77
77
  evidence: Type.Optional(Type.String({ description: "Evidence for completion (required for complete_goal)" })),
78
78
  reason: Type.Optional(Type.String({ description: "Reason for being blocked (required for report_blocked)" })),
79
79
  cancelReason: Type.Optional(Type.String({ description: "Why the user wants to cancel (required for cancel_goal)" })),
@@ -160,12 +160,12 @@ export function makeGoalResult(session: GoalSession, text: string) {
160
160
  const budgetInfo: string[] = [];
161
161
  if (state.budget.tokenBudget) {
162
162
  const remaining = Math.max(state.budget.tokenBudget - state.tokensUsed, 0);
163
- budgetInfo.push(`Token: ${state.tokensUsed}/${state.budget.tokenBudget} (剩余 ${remaining})`);
163
+ budgetInfo.push(`Token: ${state.tokensUsed}/${state.budget.tokenBudget} (${remaining} remaining)`);
164
164
  }
165
165
  if (state.budget.timeBudgetMinutes) {
166
166
  const elapsed = getElapsedTimeSeconds(state);
167
167
  const remaining = Math.max(state.budget.timeBudgetMinutes * SECONDS_PER_MINUTE - elapsed, 0);
168
- budgetInfo.push(`时间: ${Math.floor(elapsed / SECONDS_PER_MINUTE)}分/${state.budget.timeBudgetMinutes} (剩余 ${Math.floor(remaining / SECONDS_PER_MINUTE)})`);
168
+ budgetInfo.push(`Time: ${Math.floor(elapsed / SECONDS_PER_MINUTE)}m/${state.budget.timeBudgetMinutes}m (${Math.floor(remaining / SECONDS_PER_MINUTE)}m remaining)`);
169
169
  }
170
170
  const suffix = budgetInfo.length > 0 ? `\n\n[Budget] ${budgetInfo.join(" | ")}` : "";
171
171
  return {
@@ -177,7 +177,7 @@ export function makeGoalResult(session: GoalSession, text: string) {
177
177
  status: state.status,
178
178
  _render: {
179
179
  type: "task-list" as const,
180
- summary: `${getCompletedCount(state.tasks)}/${state.tasks.length} 完成`,
180
+ summary: `${getCompletedCount(state.tasks)}/${state.tasks.length} completed`,
181
181
  data: {
182
182
  items: state.tasks.map((t) => ({
183
183
  id: t.id,
@@ -192,8 +192,8 @@ export function makeGoalResult(session: GoalSession, text: string) {
192
192
  })),
193
193
  meta: {
194
194
  ...(state.budget.tokenBudget ? { "Token": `${state.tokensUsed}/${state.budget.tokenBudget}` } : {}),
195
- ...(state.budget.timeBudgetMinutes ? { "时间": `${Math.floor(getElapsedTimeSeconds(state) / SECONDS_PER_MINUTE)}分/${state.budget.timeBudgetMinutes}分` } : {}),
196
- "轮次": `${state.turnCount}/${state.budget.maxTurns}`,
195
+ ...(state.budget.timeBudgetMinutes ? { "Time": `${Math.floor(getElapsedTimeSeconds(state) / SECONDS_PER_MINUTE)}m/${state.budget.timeBudgetMinutes}m` } : {}),
196
+ "Turn": `${state.currentTurnIndex}/${state.budget.maxTurns}`,
197
197
  },
198
198
  },
199
199
  },
@@ -222,7 +222,7 @@ export async function executeGoalAction(
222
222
  ) {
223
223
  const state = session.state;
224
224
  if (!state) {
225
- throw new Error("Goal 模式未激活。使用 /goal <objective> 启动。");
225
+ throw new Error("Goal mode not active. Use /goal <objective> to start.");
226
226
  }
227
227
 
228
228
  switch (params.action) {
@@ -233,8 +233,7 @@ export async function executeGoalAction(
233
233
  const existingIncomplete = getIncompleteTasks(state.tasks);
234
234
  if (state.tasks.length > 0 && existingIncomplete.length > 0) {
235
235
  throw new Error(
236
- `已有 ${state.tasks.length} 个任务(${existingIncomplete.length} 个未完成)。` +
237
- `如需追加任务请用 add_tasks,如需全部重新规划请用 /goal update。`,
236
+ `Already has ${state.tasks.length} tasks (${existingIncomplete.length} incomplete). Use add_tasks to append, or /goal update to re-plan.`,
238
237
  );
239
238
  }
240
239
  state.tasks = params.tasks.map((desc: string, i: number) => ({
@@ -245,7 +244,7 @@ export async function executeGoalAction(
245
244
  }));
246
245
  persistGoalState(pi, session, ctx);
247
246
  return makeGoalResult(session,
248
- `已创建 ${state.tasks.length} 个任务:\n${state.tasks.map((t) => ` #${t.id}: ${t.description}`).join("\n")}`,
247
+ `Created ${state.tasks.length} tasks:\n${state.tasks.map((t) => ` #${t.id}: ${t.description}`).join("\n")}`,
249
248
  );
250
249
  }
251
250
 
@@ -265,7 +264,7 @@ export async function executeGoalAction(
265
264
  state.tasks.push(...newTasks);
266
265
  persistGoalState(pi, session, ctx);
267
266
  return makeGoalResult(session,
268
- `已追加 ${newTasks.length} 个任务:\n${newTasks.map((t) => ` #${t.id}: ${t.description}`).join("\n")}`,
267
+ `Appended ${newTasks.length} tasks:\n${newTasks.map((t) => ` #${t.id}: ${t.description}`).join("\n")}`,
269
268
  );
270
269
  }
271
270
 
@@ -276,7 +275,7 @@ export async function executeGoalAction(
276
275
  const taskIds = params.updates.map((u: { taskId: number; status: string; evidence?: string }) => u.taskId);
277
276
  const duplicateIds = taskIds.filter((id: number, i: number) => taskIds.indexOf(id) !== i);
278
277
  if (duplicateIds.length > 0) {
279
- throw new Error(`重复的 taskId: ${[...new Set(duplicateIds)].join(", ")}`);
278
+ throw new Error(`Duplicate taskIds: ${[...new Set(duplicateIds)].join(", ")}`);
280
279
  }
281
280
  for (const u of params.updates) {
282
281
  const task = state.tasks.find((t) => t.id === u.taskId);
@@ -284,10 +283,10 @@ export async function executeGoalAction(
284
283
  throw new Error(`Task #${u.taskId} not found`);
285
284
  }
286
285
  if (isTerminalTaskStatus(task.status)) {
287
- throw new Error(`Task #${task.id} 已处于终态 (${task.status}),不可变更`);
286
+ throw new Error(`Task #${task.id} already in terminal state (${task.status}), cannot be changed`);
288
287
  }
289
288
  if (u.status === "completed" && (!u.evidence || u.evidence.trim() === "")) {
290
- throw new Error(`Task #${task.id}: completed 必须提供 evidence`);
289
+ throw new Error(`Task #${task.id}: completed requires evidence`);
291
290
  }
292
291
  }
293
292
  const results: string[] = [];
@@ -305,7 +304,7 @@ export async function executeGoalAction(
305
304
  }
306
305
  }
307
306
  persistGoalState(pi, session, ctx);
308
- return makeGoalResult(session, `已更新 ${results.length} 个任务:\n${results.join("\n")}`);
307
+ return makeGoalResult(session, `Updated ${results.length} tasks:\n${results.join("\n")}`);
309
308
  }
310
309
 
311
310
  case "list_tasks": {
@@ -314,54 +313,53 @@ export async function executeGoalAction(
314
313
 
315
314
  case "complete_goal": {
316
315
  if (!params.evidence || params.evidence.trim() === "") {
317
- throw new Error("complete_goal requires evidence — 提供具体的证据证明目标已达成");
316
+ throw new Error("complete_goal requires evidence — provide concrete proof that the objective has been achieved");
318
317
  }
319
318
  if (state.tasks.length === 0) {
320
- throw new Error("请先使用 create_tasks 创建任务清单,再完成目标。");
319
+ throw new Error("Create a task list with create_tasks before completing the goal.");
321
320
  }
322
321
  const incomplete = getIncompleteTasks(state.tasks);
323
322
  if (incomplete.length > 0) {
324
323
  throw new Error(
325
- `还有 ${incomplete.length} 个任务未完成:${incomplete.map((t) => `#${t.id}`).join(", ")}。` +
326
- `请先完成这些任务或提供理由说明为什么它们不需要完成。`,
324
+ `${incomplete.length} tasks still incomplete: ${incomplete.map((t) => `#${t.id}`).join(", ")}. Complete them first or explain why they don't need completion.`,
327
325
  );
328
326
  }
329
327
  const completedCount = getCompletedCount(state.tasks);
330
328
  if (completedCount === 0) {
331
- throw new Error("至少需要完成一个任务才能完成目标。全部取消不算达成。");
329
+ throw new Error("At least one task must be completed. All-cancelled does not count.");
332
330
  }
333
331
  state.status = transitionStatus(state.status, "complete");
334
332
  state.completedAtTurnIndex = state.currentTurnIndex;
335
333
  writeGoalHistoryEntry(pi, session);
336
334
  persistGoalState(pi, session, ctx);
337
335
  const budgetReport: string[] = [];
338
- budgetReport.push(`总轮次: ${state.turnCount}`);
339
- budgetReport.push(`任务完成: ${getCompletedCount(state.tasks)}/${state.tasks.length}`);
336
+ budgetReport.push(`Total turns: ${state.currentTurnIndex}`);
337
+ budgetReport.push(`Tasks completed: ${getCompletedCount(state.tasks)}/${state.tasks.length}`);
340
338
  if (state.budget.tokenBudget) {
341
- budgetReport.push(`Token 消耗: ${state.tokensUsed}/${state.budget.tokenBudget}`);
339
+ budgetReport.push(`Token usage: ${state.tokensUsed}/${state.budget.tokenBudget}`);
342
340
  }
343
341
  const elapsed = getElapsedTimeSeconds(state);
344
- budgetReport.push(`用时: ${Math.floor(elapsed / SECONDS_PER_MINUTE)}分${Math.floor(elapsed % SECONDS_PER_MINUTE)}秒`);
342
+ budgetReport.push(`Duration: ${Math.floor(elapsed / SECONDS_PER_MINUTE)}m${Math.floor(elapsed % SECONDS_PER_MINUTE)}s`);
345
343
  return makeGoalResult(session,
346
- `目标已完成!\n证据: ${params.evidence}\n\n--- Budget Report ---\n${budgetReport.join("\n")}`,
344
+ `Objective completed!\nEvidence: ${params.evidence}\n\n--- Budget Report ---\n${budgetReport.join("\n")}`,
347
345
  );
348
346
  }
349
347
 
350
348
  case "report_blocked": {
351
349
  if (!params.reason || params.reason.trim() === "") {
352
- throw new Error("report_blocked requires reason — 说明阻塞原因");
350
+ throw new Error("report_blocked requires reason — describe what is blocking you");
353
351
  }
354
352
  state.lastBlockerReason = params.reason;
355
353
  state.status = transitionStatus(state.status, "blocked");
356
354
  persistGoalState(pi, session, ctx);
357
- return makeGoalResult(session, `已报告阻塞。原因: ${params.reason}`);
355
+ return makeGoalResult(session, `Blocked reported. Reason: ${params.reason}`);
358
356
  }
359
357
 
360
358
  case "cancel_goal": {
361
359
  if (isTerminalStatus(state.status)) {
362
- throw new Error(`Goal 已处于终态 (${state.status})。`);
360
+ throw new Error(`Goal is already in terminal state (${state.status}).`);
363
361
  }
364
- const reason = params.cancelReason ?? "用户要求取消";
362
+ const reason = params.cancelReason ?? "User requested cancellation";
365
363
  const goalId = state.goalId;
366
364
  state.status = "cancelled";
367
365
  state.completedAtTurnIndex = state.currentTurnIndex;
@@ -369,7 +367,7 @@ export async function executeGoalAction(
369
367
  persistGoalState(pi, session, ctx);
370
368
  clearGoalSession(session, ctx);
371
369
  return {
372
- content: [{ type: "text" as const, text: `Goal 已取消: ${reason}` }],
370
+ content: [{ type: "text" as const, text: `Goal cancelled: ${reason}` }],
373
371
  details: {
374
372
  action: "cancel",
375
373
  tasks: [],
@@ -377,7 +375,7 @@ export async function executeGoalAction(
377
375
  status: "cancelled",
378
376
  _render: {
379
377
  type: "task-list" as const,
380
- summary: "已取消",
378
+ summary: "Cancelled",
381
379
  data: { items: [], meta: {} },
382
380
  },
383
381
  } satisfies GoalManagerDetails,
@@ -396,13 +394,13 @@ export async function executeGoalAction(
396
394
  throw new Error(`Task #${params.taskId} not found`);
397
395
  }
398
396
  if (isTerminalTaskStatus(parentTask.status)) {
399
- throw new Error(`Task #${parentTask.id} 已处于终态 (${parentTask.status}),不能添加 subtask`);
397
+ throw new Error(`Task #${parentTask.id} already in terminal state (${parentTask.status}), cannot add subtask`);
400
398
  }
401
399
  const subtasks = parentTask.subtasks ?? [];
402
400
  const startId = subtasks.length > 0 ? Math.max(...subtasks.map((s) => s.id)) + 1 : 1;
403
401
  const trimmed = params.texts.map((t: string) => t.trim()).filter((t: string) => t.length > 0);
404
402
  if (trimmed.length === 0) {
405
- throw new Error("texts 中至少需要一个非空字符串");
403
+ throw new Error("texts requires at least one non-empty string");
406
404
  }
407
405
  const newSubtasks: Subtask[] = trimmed.map((text: string, i: number) => ({
408
406
  id: startId + i,
@@ -413,7 +411,7 @@ export async function executeGoalAction(
413
411
  parentTask.subtasks = [...subtasks, ...newSubtasks];
414
412
  persistGoalState(pi, session, ctx);
415
413
  return makeGoalResult(session,
416
- `已给 Task #${parentTask.id} 添加 ${newSubtasks.length} 项 subtask:\n` +
414
+ `Added ${newSubtasks.length} subtasks to Task #${parentTask.id}:\n` +
417
415
  newSubtasks.map((s) => ` - #${parentTask.id}.${s.id}: ${s.text}`).join("\n"),
418
416
  );
419
417
  }
@@ -430,7 +428,7 @@ export async function executeGoalAction(
430
428
  throw new Error(`Task #${params.taskId} not found`);
431
429
  }
432
430
  if (!targetTask.subtasks || targetTask.subtasks.length === 0) {
433
- throw new Error(`Task #${params.taskId} 没有 subtask`);
431
+ throw new Error(`Task #${params.taskId} has no subtasks`);
434
432
  }
435
433
  const results: string[] = [];
436
434
  for (const u of params.subUpdates) {
@@ -439,7 +437,7 @@ export async function executeGoalAction(
439
437
  throw new Error(`Subtask #${params.taskId}.${u.subId} not found`);
440
438
  }
441
439
  if (sub.status === "completed") {
442
- throw new Error(`Subtask #${params.taskId}.${sub.id} 已完成,不可变更`);
440
+ throw new Error(`Subtask #${params.taskId}.${sub.id} already completed, cannot be changed`);
443
441
  }
444
442
  const prev = sub.status;
445
443
  sub.status = u.status;
@@ -448,7 +446,7 @@ export async function executeGoalAction(
448
446
  }
449
447
  persistGoalState(pi, session, ctx);
450
448
  return makeGoalResult(session,
451
- `已更新 ${results.length} 项 subtask:\n${results.join("\n")}`,
449
+ `Updated ${results.length} subtasks:\n${results.join("\n")}`,
452
450
  );
453
451
  }
454
452
 
@@ -464,7 +462,7 @@ export async function executeGoalAction(
464
462
  throw new Error(`Task #${params.taskId} not found`);
465
463
  }
466
464
  if (!delTask.subtasks || delTask.subtasks.length === 0) {
467
- throw new Error(`Task #${params.taskId} 没有 subtask`);
465
+ throw new Error(`Task #${params.taskId} has no subtasks`);
468
466
  }
469
467
  const uniqueIds = [...new Set(params.subIds)];
470
468
  const missing = uniqueIds.filter((id) => !delTask.subtasks!.some((s) => s.id === id));
@@ -477,7 +475,7 @@ export async function executeGoalAction(
477
475
  }
478
476
  persistGoalState(pi, session, ctx);
479
477
  return makeGoalResult(session,
480
- `已删除 ${uniqueIds.length} subtask,Task #${params.taskId} 剩余 ${delTask.subtasks?.length ?? 0} 项`,
478
+ `Deleted ${uniqueIds.length} subtasks, Task #${params.taskId} has ${delTask.subtasks?.length ?? 0} remaining`,
481
479
  );
482
480
  }
483
481
 
package/src/widget.ts CHANGED
@@ -39,13 +39,13 @@ export function renderStatusLine(state: GoalRuntimeState, th: ThemeLike): string
39
39
  const completedCount = getCompletedCount(state.tasks);
40
40
  const total = state.tasks.length;
41
41
 
42
- let text = th.fg("accent", `◆ Goal`) + th.fg("muted", ` ${state.turnCount}/${state.budget.maxTurns}`);
42
+ let text = th.fg("accent", `◆ Goal`) + th.fg("muted", ` ${state.currentTurnIndex}/${state.budget.maxTurns}`);
43
43
 
44
44
  if (total > 0) {
45
- text += th.fg("muted", ` | ${completedCount}/${total} 任务`);
45
+ text += th.fg("muted", ` | ${completedCount}/${total} tasks`);
46
46
  const cancelledCount = state.tasks.filter(t => t.status === "cancelled").length;
47
47
  if (cancelledCount > 0) {
48
- text += th.fg("dim", `, ${cancelledCount} 取消`);
48
+ text += th.fg("dim", `, ${cancelledCount} cancelled`);
49
49
  }
50
50
  }
51
51
 
@@ -60,25 +60,25 @@ export function renderStatusLine(state: GoalRuntimeState, th: ThemeLike): string
60
60
  }
61
61
 
62
62
  if (state.stallCount > 0) {
63
- text += th.fg("warning", ` | ⚠ ${state.stallCount}轮无进展`);
63
+ text += th.fg("warning", ` | ⚠ ${state.stallCount} turns stalled`);
64
64
  }
65
65
 
66
66
  // Status suffix
67
67
  switch (state.status) {
68
68
  case "paused":
69
- text += th.fg("warning", " | ⏸ 暂停");
69
+ text += th.fg("warning", " | ⏸ Paused");
70
70
  break;
71
71
  case "blocked":
72
- text += th.fg("error", " | ⊘ 阻塞");
72
+ text += th.fg("error", " | ⊘ Blocked");
73
73
  break;
74
74
  case "complete":
75
- text += th.fg("success", " | ✓ 完成");
75
+ text += th.fg("success", " | ✓ Completed");
76
76
  break;
77
77
  case "budget_limited":
78
- text += th.fg("error", " | ⊗ Token 预算耗尽");
78
+ text += th.fg("error", " | ⊗ Token budget exhausted");
79
79
  break;
80
80
  case "time_limited":
81
- text += th.fg("error", " | ⏱ 时间预算耗尽");
81
+ text += th.fg("error", " | ⏱ Time budget exhausted");
82
82
  break;
83
83
  }
84
84
 
@@ -96,19 +96,19 @@ export function renderTerminalStatusLine(state: GoalRuntimeState, th: ThemeLike)
96
96
  // 状态后缀
97
97
  switch (state.status) {
98
98
  case "complete":
99
- text += th.fg("success", " ✓ 完成");
99
+ text += th.fg("success", " ✓ Completed");
100
100
  break;
101
101
  case "budget_limited":
102
- text += th.fg("error", " ⊗ Token 预算耗尽");
102
+ text += th.fg("error", " ⊗ Token budget exhausted");
103
103
  break;
104
104
  case "time_limited":
105
- text += th.fg("error", " ⏱ 时间预算耗尽");
105
+ text += th.fg("error", " ⏱ Time budget exhausted");
106
106
  break;
107
107
  default:
108
108
  break;
109
109
  }
110
110
 
111
- text += th.fg("muted", ` | ${completedCount}/${total} 任务`);
111
+ text += th.fg("muted", ` | ${completedCount}/${total} tasks`);
112
112
 
113
113
  // 预算摘要
114
114
  if (state.budget.tokenBudget && state.budget.tokenBudget > 0) {
@@ -137,11 +137,11 @@ export function renderWidgetLines(state: GoalRuntimeState, th: ThemeLike): strin
137
137
  const objDisplay = objSingleLine.length > OBJECTIVE_DISPLAY_LIMIT
138
138
  ? objSingleLine.slice(0, OBJECTIVE_TRUNCATE_KEEP) + "..."
139
139
  : objSingleLine;
140
- lines.push(th.fg("dim", `目标: ${objDisplay}`));
140
+ lines.push(th.fg("dim", `Objective: ${objDisplay}`));
141
141
 
142
142
  // Task list
143
143
  if (total === 0) {
144
- lines.push(th.fg("dim", " 等待创建任务清单..."));
144
+ lines.push(th.fg("dim", " Waiting for task list creation..."));
145
145
  } else {
146
146
  for (const t of state.tasks) {
147
147
  const desc = toSingleLine(t.description);
@@ -178,7 +178,7 @@ export function renderWidgetLines(state: GoalRuntimeState, th: ThemeLike): strin
178
178
  const pct = getTimeUsagePercent(state) / PERCENT_FACTOR;
179
179
  const elapsed = getElapsedTimeSeconds(state);
180
180
  const mins = Math.floor(elapsed / SECONDS_PER_MINUTE);
181
- lines.push(` 时间: ${renderProgressBar(pct)} ${mins}/${state.budget.timeBudgetMinutes}分钟`);
181
+ lines.push(` Time: ${renderProgressBar(pct)} ${mins}/${state.budget.timeBudgetMinutes}min`);
182
182
  }
183
183
 
184
184
  return lines;