@pencil-agent/nano-pencil 1.13.8 → 1.13.9

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 (42) hide show
  1. package/dist/build-meta.json +3 -3
  2. package/dist/builtin-extensions.js +10 -0
  3. package/dist/core/config/settings-manager.d.ts +7 -0
  4. package/dist/core/sub-agent/index.d.ts +1 -1
  5. package/dist/core/sub-agent/sub-agent-backend.js +78 -12
  6. package/dist/core/sub-agent/sub-agent-types.d.ts +44 -1
  7. package/dist/core/sub-agent/sub-agent-types.js +1 -1
  8. package/dist/extensions/defaults/CLAUDE.md +5 -4
  9. package/dist/extensions/defaults/grub/grub-controller.d.ts +2 -0
  10. package/dist/extensions/defaults/grub/grub-controller.js +79 -24
  11. package/dist/extensions/defaults/grub/grub-i18n.d.ts +128 -0
  12. package/dist/extensions/defaults/grub/grub-i18n.js +167 -0
  13. package/dist/extensions/defaults/grub/grub-parser.d.ts +2 -1
  14. package/dist/extensions/defaults/grub/grub-parser.js +5 -3
  15. package/dist/extensions/defaults/grub/grub-types.d.ts +3 -0
  16. package/dist/extensions/defaults/grub/index.js +133 -78
  17. package/dist/extensions/defaults/idle-think/curiosity.d.ts +46 -0
  18. package/dist/extensions/defaults/idle-think/curiosity.js +137 -0
  19. package/dist/extensions/defaults/idle-think/index.d.ts +15 -0
  20. package/dist/extensions/defaults/idle-think/index.js +169 -0
  21. package/dist/extensions/defaults/idle-think/insights.d.ts +40 -0
  22. package/dist/extensions/defaults/idle-think/insights.js +123 -0
  23. package/dist/extensions/defaults/idle-think/thinker.d.ts +26 -0
  24. package/dist/extensions/defaults/idle-think/thinker.js +208 -0
  25. package/dist/extensions/defaults/presence/index.d.ts +1 -0
  26. package/dist/extensions/defaults/presence/index.js +83 -10
  27. package/dist/extensions/defaults/team/CLAUDE.md +7 -6
  28. package/dist/extensions/defaults/team/index.d.ts +1 -0
  29. package/dist/extensions/defaults/team/index.js +92 -3
  30. package/dist/extensions/defaults/team/team-dashboard.js +6 -0
  31. package/dist/extensions/defaults/team/team-parser.d.ts +1 -1
  32. package/dist/extensions/defaults/team/team-parser.js +3 -2
  33. package/dist/extensions/defaults/team/team-presets.d.ts +14 -2
  34. package/dist/extensions/defaults/team/team-presets.js +124 -4
  35. package/dist/extensions/defaults/team/team-runtime.d.ts +15 -2
  36. package/dist/extensions/defaults/team/team-runtime.js +62 -1
  37. package/dist/extensions/defaults/team/team-types.d.ts +9 -0
  38. package/dist/modes/interactive/components/pencil-loader.d.ts +0 -2
  39. package/dist/modes/interactive/components/pencil-loader.js +1 -14
  40. package/dist/node_modules/@pencil-agent/ai/models.generated.d.ts +0 -17
  41. package/dist/node_modules/@pencil-agent/ai/models.generated.js +1 -18
  42. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "1.13.8",
3
- "commitHash": "66ecf7f",
2
+ "version": "1.13.9",
3
+ "commitHash": "593bf56",
4
4
  "branch": "main",
5
- "builtAt": "2026-04-26T15:41:55.811Z"
5
+ "builtAt": "2026-04-26T17:04:12.832Z"
6
6
  }
@@ -24,6 +24,7 @@ const BUNDLED_SAL_EXTENSION = join(__dirname, "extensions", "defaults", "sal", "
24
24
  const BUNDLED_GRUB_EXTENSION = join(__dirname, "extensions", "defaults", "grub", "index.js");
25
25
  const BUNDLED_SUBAGENT_EXTENSION = join(__dirname, "extensions", "defaults", "subagent", "index.js");
26
26
  const BUNDLED_TEAM_EXTENSION = join(__dirname, "extensions", "defaults", "team", "index.js");
27
+ const BUNDLED_IDLE_THINK_EXTENSION = join(__dirname, "extensions", "defaults", "idle-think", "index.js");
27
28
  const BUNDLED_BTW_EXTENSION = join(__dirname, "extensions", "defaults", "btw", "index.js");
28
29
  const BUNDLED_DEBUG_EXTENSION = join(__dirname, "extensions", "defaults", "debug", "index.js");
29
30
  const BUNDLED_MCP_EXTENSION = join(__dirname, "extensions", "defaults", "mcp", "index.js");
@@ -211,6 +212,15 @@ export function getBuiltinExtensionPaths() {
211
212
  if (existsSync(teamTs))
212
213
  paths.push(teamTs);
213
214
  }
215
+ // === IdleThink extension (background code exploration during idle) ===
216
+ if (existsSync(BUNDLED_IDLE_THINK_EXTENSION)) {
217
+ paths.push(BUNDLED_IDLE_THINK_EXTENSION);
218
+ }
219
+ else {
220
+ const idleThinkTs = join(__dirname, "extensions", "defaults", "idle-think", "index.ts");
221
+ if (existsSync(idleThinkTs))
222
+ paths.push(idleThinkTs);
223
+ }
214
224
  // === BTW extension (quick side question without interrupting) ===
215
225
  if (existsSync(BUNDLED_BTW_EXTENSION)) {
216
226
  paths.push(BUNDLED_BTW_EXTENSION);
@@ -105,6 +105,13 @@ export interface Settings {
105
105
  presence?: {
106
106
  enabled?: boolean;
107
107
  };
108
+ /** IdleThink extension settings - background code exploration during idle */
109
+ idleThink?: {
110
+ enabled?: boolean;
111
+ idleMinutes?: number;
112
+ dailyBudget?: number;
113
+ maxDurationMinutes?: number;
114
+ };
108
115
  /** Auto-update setting: 'always' = auto-update on startup, 'prompt' = ask user (default), 'never' = never check */
109
116
  autoUpdate?: "always" | "prompt" | "never";
110
117
  /** Last skipped version for update prompts */
@@ -8,4 +8,4 @@ export { SubAgentRuntime, subAgentRuntime } from "./sub-agent-runtime.js";
8
8
  export { InProcessSubAgentBackend } from "./sub-agent-backend.js";
9
9
  export { SubprocessSubAgentBackend } from "./subprocess-backend.js";
10
10
  export type { SubprocessBackendOptions } from "./subprocess-backend.js";
11
- export type { SubAgentSpec, SubAgentResult, SubAgentHandle, SubAgentBackend, } from "./sub-agent-types.js";
11
+ export type { SubAgentSpec, SubAgentEvent, SubAgentResult, SubAgentHandle, SubAgentBackend, } from "./sub-agent-types.js";
@@ -32,6 +32,12 @@ export class InProcessSubAgentBackend {
32
32
  model: spec.model,
33
33
  };
34
34
  const { session } = await createAgentSession(options);
35
+ const unsubscribe = session.subscribe((event) => {
36
+ const subAgentEvent = toSubAgentEvent(id, event);
37
+ if (subAgentEvent) {
38
+ spec.onEvent?.(subAgentEvent);
39
+ }
40
+ });
35
41
  const timeoutMs = spec.timeoutMs;
36
42
  let status = "running";
37
43
  let result;
@@ -44,21 +50,10 @@ export class InProcessSubAgentBackend {
44
50
  }
45
51
  }, timeoutMs);
46
52
  }
47
- // Extract text from assistant message content
48
- const extractTextFromContent = (content) => {
49
- if (typeof content === "string")
50
- return content;
51
- if (Array.isArray(content)) {
52
- return content
53
- .filter((part) => typeof part === "object" && part !== null && "type" in part && part.type === "text" && typeof part.text === "string")
54
- .map((part) => part.text)
55
- .join("\n");
56
- }
57
- return "";
58
- };
59
53
  // Start the prompt
60
54
  const promptPromise = (async () => {
61
55
  try {
56
+ spec.onEvent?.({ type: "agent_start", subAgentId: id, timestamp: Date.now() });
62
57
  await session.prompt(prompt, {
63
58
  images: spec.images,
64
59
  });
@@ -107,6 +102,14 @@ export class InProcessSubAgentBackend {
107
102
  }
108
103
  // Clean up signal handler
109
104
  spec.signal.removeEventListener("abort", signalHandler);
105
+ unsubscribe();
106
+ spec.onEvent?.({
107
+ type: "agent_end",
108
+ subAgentId: id,
109
+ timestamp: Date.now(),
110
+ success: result?.success ?? false,
111
+ error: result?.error,
112
+ });
110
113
  }
111
114
  })();
112
115
  return {
@@ -132,6 +135,69 @@ export class InProcessSubAgentBackend {
132
135
  };
133
136
  }
134
137
  }
138
+ function toSubAgentEvent(subAgentId, event) {
139
+ const timestamp = Date.now();
140
+ switch (event.type) {
141
+ case "message_update":
142
+ return {
143
+ type: "message_update",
144
+ subAgentId,
145
+ timestamp,
146
+ text: extractMessageText(event.message),
147
+ deltaType: event.assistantMessageEvent.type,
148
+ };
149
+ case "message_end":
150
+ return {
151
+ type: "message_end",
152
+ subAgentId,
153
+ timestamp,
154
+ text: extractMessageText(event.message),
155
+ };
156
+ case "tool_execution_start":
157
+ return {
158
+ type: "tool_start",
159
+ subAgentId,
160
+ timestamp,
161
+ toolName: event.toolName,
162
+ args: event.args,
163
+ };
164
+ case "tool_execution_update":
165
+ return {
166
+ type: "tool_update",
167
+ subAgentId,
168
+ timestamp,
169
+ toolName: event.toolName,
170
+ partialResult: event.partialResult,
171
+ };
172
+ case "tool_execution_end":
173
+ return {
174
+ type: "tool_end",
175
+ subAgentId,
176
+ timestamp,
177
+ toolName: event.toolName,
178
+ isError: event.isError,
179
+ };
180
+ default:
181
+ return undefined;
182
+ }
183
+ }
184
+ function extractTextFromContent(content) {
185
+ if (typeof content === "string")
186
+ return content;
187
+ if (Array.isArray(content)) {
188
+ return content
189
+ .filter((part) => typeof part === "object" && part !== null && "type" in part && part.type === "text" && typeof part.text === "string")
190
+ .map((part) => part.text)
191
+ .join("\n");
192
+ }
193
+ return "";
194
+ }
195
+ function extractMessageText(message) {
196
+ if (typeof message !== "object" || message === null || !("content" in message)) {
197
+ return "";
198
+ }
199
+ return extractTextFromContent(message.content);
200
+ }
135
201
  async function buildPromptWithContextFiles(spec) {
136
202
  if (!spec.contextFiles?.length) {
137
203
  return spec.prompt;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * [WHO]: SubAgent types - SubAgentSpec, SubAgentHandle, SubAgentBackend, SubAgentResult
2
+ * [WHO]: SubAgent types - SubAgentSpec, SubAgentEvent, SubAgentHandle, SubAgentBackend, SubAgentResult
3
3
  * [FROM]: Depends on @pencil-agent/agent-core, @pencil-agent/ai, core/tools
4
4
  * [TO]: Consumed by ./sub-agent-runtime, ./sub-agent-backend, ./index.ts, extensions/defaults/subagent/*, extensions/defaults/team/*
5
5
  * [HERE]: core/sub-agent/sub-agent-types.ts - SubAgent type definitions
@@ -7,6 +7,47 @@
7
7
  */
8
8
  import type { ImageContent, Model } from "@pencil-agent/ai";
9
9
  import type { Tool } from "../tools/index.js";
10
+ /** Realtime lifecycle event emitted by a running SubAgent. */
11
+ export type SubAgentEvent = {
12
+ type: "agent_start";
13
+ subAgentId: string;
14
+ timestamp: number;
15
+ } | {
16
+ type: "message_update";
17
+ subAgentId: string;
18
+ timestamp: number;
19
+ text: string;
20
+ deltaType?: string;
21
+ } | {
22
+ type: "message_end";
23
+ subAgentId: string;
24
+ timestamp: number;
25
+ text: string;
26
+ } | {
27
+ type: "tool_start";
28
+ subAgentId: string;
29
+ timestamp: number;
30
+ toolName: string;
31
+ args: unknown;
32
+ } | {
33
+ type: "tool_update";
34
+ subAgentId: string;
35
+ timestamp: number;
36
+ toolName: string;
37
+ partialResult: unknown;
38
+ } | {
39
+ type: "tool_end";
40
+ subAgentId: string;
41
+ timestamp: number;
42
+ toolName: string;
43
+ isError: boolean;
44
+ } | {
45
+ type: "agent_end";
46
+ subAgentId: string;
47
+ timestamp: number;
48
+ success: boolean;
49
+ error?: string;
50
+ };
10
51
  /**
11
52
  * Specification for spawning a SubAgent.
12
53
  */
@@ -29,6 +70,8 @@ export interface SubAgentSpec {
29
70
  contextFiles?: string[];
30
71
  /** Optional callback invoked after the run result is available */
31
72
  exitHook?: (result: SubAgentResult) => Promise<void> | void;
73
+ /** Optional realtime observer for TUI/status integrations */
74
+ onEvent?: (event: SubAgentEvent) => void;
32
75
  }
33
76
  /**
34
77
  * Result from a completed SubAgent run.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * [WHO]: SubAgent types - SubAgentSpec, SubAgentHandle, SubAgentBackend, SubAgentResult
2
+ * [WHO]: SubAgent types - SubAgentSpec, SubAgentEvent, SubAgentHandle, SubAgentBackend, SubAgentResult
3
3
  * [FROM]: Depends on @pencil-agent/agent-core, @pencil-agent/ai, core/tools
4
4
  * [TO]: Consumed by ./sub-agent-runtime, ./sub-agent-backend, ./index.ts, extensions/defaults/subagent/*, extensions/defaults/team/*
5
5
  * [HERE]: core/sub-agent/sub-agent-types.ts - SubAgent type definitions
@@ -17,10 +17,11 @@ security-audit/engine/interceptor.ts: Request/response interception, Interceptor
17
17
  security-audit/engine/logger.ts: Security event logging, JSON file audit trail
18
18
  security-audit/engine/detector.ts: Vulnerability detection, pattern matching for dangerous commands
19
19
  soul/index.ts: AI personality evolution extension, persistent personality across sessions
20
- grub/index.ts: Grub extension entry - long-running autonomous harness, dual-phase system prompts (initializer/coding), /grub command (start/status/resume/stop) + grub renderer, session_start auto-adopt, git harness commit, pruneStale cleanup
21
- grub/grub-controller.ts: GrubController - state machine for /grub iterations, durable persistState on every transition, adoptResumedTask for cross-session resume, validateCompletion downgrades premature complete when feature-list still has pending entries
22
- grub/grub-parser.ts: Grub command parsing - parseGrubCommand/buildGrubHelp with resume subcommand, status --json, --max-iter/--max-fail flags
23
- grub/grub-types.ts: Grub types - GrubStatus/GrubDecisionStatus/GrubDecision/GrubPhase/GrubTaskState/GrubTaskSnapshot/ParsedGrubCommand + FeatureItem/FeatureList (version 1 schema) + PersistedGrubState envelope
20
+ grub/index.ts: Grub extension entry - long-running autonomous harness, locale-aware dual-phase system prompts (initializer/coding), /grub command (start/status/resume/stop) + grub renderer, session_start auto-adopt, git harness commit, pruneStale cleanup
21
+ grub/grub-controller.ts: GrubController - state machine for /grub iterations, locale-persisted prompt generation, durable persistState on every transition, adoptResumedTask for cross-session resume, validateCompletion downgrades premature complete when feature-list still has pending entries
22
+ grub/grub-parser.ts: Grub command parsing - parseGrubCommand/buildGrubHelp with localized help, resume subcommand, status --json, --max-iter/--max-fail flags
23
+ grub/grub-types.ts: Grub types - GrubStatus/GrubDecisionStatus/GrubDecision/GrubPhase/GrubLocale/GrubTaskState/GrubTaskSnapshot/ParsedGrubCommand + FeatureItem/FeatureList (version 1 schema) + PersistedGrubState envelope
24
+ grub/grub-i18n.ts: Grub localization helper - detectGrubLocale(), grubText(), languageName(), English/Chinese TUI strings
24
25
  grub/grub-feature-list.ts: feature-list.json IO - readFeatureList/writeFeatureList atomic write, validateFeatureListDiff enforces passes/evidence-only mutations, createInitialFeatureList placeholder, migrateChecklistToFeatureList legacy converter, countPassing/allPassing/firstPending helpers
25
26
  grub/grub-persistence.ts: Cross-session persistence - persistState atomic JSON write to .grub/<id>/state.json, loadState shape-validated read, discoverActiveTasks scans .grub/ for running records, pruneStale removes terminal harnesses older than 30 days by default
26
27
  grub/README.md: Grub extension documentation - long-running harness contract, feature-list.json schema, completion guard, cross-session resume, legacy migration
@@ -4,10 +4,12 @@
4
4
  * [TO]: Consumed by extension entry point (./index.ts)
5
5
  * [HERE]: extensions/defaults/grub/grub-controller.ts - state machine for /grub iterations with cross-session persistence and feature-list-gated completion
6
6
  */
7
+ import { type GrubLocale } from "./grub-i18n.js";
7
8
  import type { GrubControllerState, GrubDecision, GrubTaskSnapshot, GrubTaskState } from "./grub-types.js";
8
9
  export interface GrubStartOptions {
9
10
  maxIterations?: number;
10
11
  maxConsecutiveFailures?: number;
12
+ locale?: GrubLocale;
11
13
  }
12
14
  export declare class GrubController {
13
15
  private activeTask?;
@@ -7,6 +7,7 @@
7
7
  import { randomBytes } from "node:crypto";
8
8
  import { join } from "node:path";
9
9
  import { allPassing, firstPending, readFeatureList } from "./grub-feature-list.js";
10
+ import { languageName, grubText } from "./grub-i18n.js";
10
11
  import { persistState, stateFilePathFor } from "./grub-persistence.js";
11
12
  const DEFAULT_MAX_ITERATIONS = 25;
12
13
  const DEFAULT_MAX_CONSECUTIVE_FAILURES = 3;
@@ -39,6 +40,7 @@ export class GrubController {
39
40
  const task = {
40
41
  id,
41
42
  goal: trimmedGoal,
43
+ locale: options.locale ?? "en",
42
44
  status: "running",
43
45
  phase: "initializer",
44
46
  startedAt: now,
@@ -67,7 +69,7 @@ export class GrubController {
67
69
  if (this.activeTask && this.activeTask.id !== task.id) {
68
70
  throw new Error(`Cannot adopt task ${task.id}; ${this.activeTask.id} is already active.`);
69
71
  }
70
- const resumed = { ...task, awaitingTurn: false, updatedAt: Date.now() };
72
+ const resumed = { ...task, locale: task.locale ?? "en", awaitingTurn: false, updatedAt: Date.now() };
71
73
  this.activeTask = resumed;
72
74
  this.safePersist(resumed);
73
75
  return resumed;
@@ -82,6 +84,7 @@ export class GrubController {
82
84
  const snapshot = {
83
85
  id: finalTask.id,
84
86
  goal: finalTask.goal,
87
+ locale: finalTask.locale,
85
88
  status,
86
89
  phase: finalTask.phase,
87
90
  startedAt: finalTask.startedAt,
@@ -109,36 +112,68 @@ export class GrubController {
109
112
  throw new Error("No active grub task.");
110
113
  }
111
114
  const task = this.activeTask;
115
+ const text = grubText(task.locale);
112
116
  const sections = [
113
117
  `${this.getPromptPrefix(task.id)}${task.currentIteration}]`,
114
118
  "",
115
- "Autonomous grub goal:",
119
+ task.locale === "zh" ? "自主 Grub 目标:" : "Autonomous grub goal:",
116
120
  task.goal,
117
121
  "",
118
- "You are inside a managed grub harness. Keep making concrete progress on the same goal.",
119
- "Use tools, edit files, run checks, and verify results as needed.",
122
+ task.locale === "zh"
123
+ ? "你正在一个受控的 grub harness 中工作。请围绕同一个目标持续推进具体进展。"
124
+ : "You are inside a managed grub harness. Keep making concrete progress on the same goal.",
125
+ task.locale === "zh"
126
+ ? "按需使用工具、编辑文件、运行检查并验证结果。所有面向用户的总结、进度和说明都必须使用中文。"
127
+ : "Use tools, edit files, run checks, and verify results as needed.",
128
+ `User language: ${languageName(task.locale)}.`,
120
129
  "",
121
- "Harness files (must stay up to date every iteration):",
122
- `- Feature list (JSON): ${task.featureListPath}`,
123
- `- Progress log: ${task.progressLogPath}`,
124
- `- Session init script: ${task.initScriptPath}`,
130
+ task.locale === "zh" ? "Harness 文件(每轮都必须保持最新):" : "Harness files (must stay up to date every iteration):",
131
+ `- ${text.featureList}: ${task.featureListPath}`,
132
+ `- ${text.progressLog}: ${task.progressLogPath}`,
133
+ `- ${text.initScript}: ${task.initScriptPath}`,
125
134
  ];
126
135
  if (task.phase === "initializer") {
127
- sections.push("", "Initializer phase requirements:", "1. Replace the placeholder feature-list.json with 15-40 concrete, testable slices. Every entry MUST keep the schema {id, category, description, steps[], passes:false}.", "2. Ensure init.sh contains reliable startup checks and make it executable.", "3. Append a clear initialization summary in progress-log.md.", "4. Do not attempt broad implementation yet; prepare a strong harness first.", "5. End this turn with loop-state status=continue unless the goal is already complete/blocked.");
136
+ sections.push("", task.locale === "zh" ? "初始化阶段要求:" : "Initializer phase requirements:", task.locale === "zh"
137
+ ? "1. 将 feature-list.json 的占位内容替换为 15-40 个具体、可测试的切片。每项必须保持 {id, category, description, steps[], passes:false}。"
138
+ : "1. Replace the placeholder feature-list.json with 15-40 concrete, testable slices. Every entry MUST keep the schema {id, category, description, steps[], passes:false}.", task.locale === "zh"
139
+ ? "2. 确保 init.sh 包含可靠的启动检查,并设置为可执行。"
140
+ : "2. Ensure init.sh contains reliable startup checks and make it executable.", task.locale === "zh"
141
+ ? "3. 在 progress-log.md 中追加清晰的初始化总结。"
142
+ : "3. Append a clear initialization summary in progress-log.md.", task.locale === "zh"
143
+ ? "4. 先建立强 harness,不要开始大范围实现。"
144
+ : "4. Do not attempt broad implementation yet; prepare a strong harness first.", task.locale === "zh"
145
+ ? "5. 除非目标已经完成或阻塞,否则本轮以 loop-state status=continue 结束。"
146
+ : "5. End this turn with loop-state status=continue unless the goal is already complete/blocked.");
128
147
  }
129
148
  else {
130
- sections.push("", "Execution phase requirements:", "1. Start by running the init script, then read feature-list.json and progress-log.md.", "2. Pick exactly one feature with passes:false and execute it end-to-end.", "3. Run relevant verification (tests, smoke checks, or runtime checks).", "4. Flip ONLY the passes/evidence fields for that feature; other fields are immutable.", "5. Append progress log and git-commit before finishing the turn.", "6. Keep each iteration incremental and production-safe.");
149
+ sections.push("", task.locale === "zh" ? "执行阶段要求:" : "Execution phase requirements:", task.locale === "zh"
150
+ ? "1. 先运行 init.sh,再读取 feature-list.json 和 progress-log.md。"
151
+ : "1. Start by running the init script, then read feature-list.json and progress-log.md.", task.locale === "zh"
152
+ ? "2. 只选择一个 passes:false 的 feature,并端到端完成它。"
153
+ : "2. Pick exactly one feature with passes:false and execute it end-to-end.", task.locale === "zh"
154
+ ? "3. 运行相关验证(测试、烟测或运行时检查)。"
155
+ : "3. Run relevant verification (tests, smoke checks, or runtime checks).", task.locale === "zh"
156
+ ? "4. 只能修改该 feature 的 passes/evidence 字段;其他字段不可变。"
157
+ : "4. Flip ONLY the passes/evidence fields for that feature; other fields are immutable.", task.locale === "zh"
158
+ ? "5. 本轮结束前追加进度日志并 git commit。"
159
+ : "5. Append progress log and git-commit before finishing the turn.", task.locale === "zh"
160
+ ? "6. 每轮都保持增量、安全、可回退。"
161
+ : "6. Keep each iteration incremental and production-safe.");
131
162
  }
132
163
  if (task.lastDecision?.summary) {
133
- sections.push("", "Previous summary:", task.lastDecision.summary);
164
+ sections.push("", task.locale === "zh" ? "上次总结:" : "Previous summary:", task.lastDecision.summary);
134
165
  }
135
166
  if (task.lastDecision?.nextStep) {
136
- sections.push("", "Previous planned next step:", task.lastDecision.nextStep);
167
+ sections.push("", task.locale === "zh" ? "上次计划的下一步:" : "Previous planned next step:", task.lastDecision.nextStep);
137
168
  }
138
169
  if (task.lastError) {
139
- sections.push("", "Recovery note:", task.lastError);
170
+ sections.push("", task.locale === "zh" ? "恢复提示:" : "Recovery note:", task.lastError);
140
171
  }
141
- sections.push("", "Do not stop just because one query finished. Only decide `complete` when every feature in feature-list.json has passes:true.", "If you need another autonomous pass, end with a valid <loop-state> block so the system can continue automatically.");
172
+ sections.push("", task.locale === "zh"
173
+ ? "不要因为一次查询结束就停止。只有 feature-list.json 中每个 feature 都 passes:true 时,才可以决定 `complete`。"
174
+ : "Do not stop just because one query finished. Only decide `complete` when every feature in feature-list.json has passes:true.", task.locale === "zh"
175
+ ? "如果还需要下一轮自主推进,请以有效的 <loop-state> 块结束,让系统自动继续。"
176
+ : "If you need another autonomous pass, end with a valid <loop-state> block so the system can continue automatically.");
142
177
  return sections.join("\n");
143
178
  }
144
179
  markDispatched() {
@@ -165,12 +200,14 @@ export class GrubController {
165
200
  const rewritten = {
166
201
  status: "continue",
167
202
  summary: decision.summary,
168
- nextStep: "feature-list.json is missing or invalid; the initializer must produce it before claiming complete.",
203
+ nextStep: this.activeTask.locale === "zh"
204
+ ? "feature-list.json 缺失或无效;初始化阶段必须先生成它,不能直接声明完成。"
205
+ : "feature-list.json is missing or invalid; the initializer must produce it before claiming complete.",
169
206
  };
170
207
  return {
171
208
  decision: rewritten,
172
209
  downgraded: true,
173
- reason: "feature-list.json missing or invalid",
210
+ reason: this.activeTask.locale === "zh" ? "feature-list.json 缺失或无效" : "feature-list.json missing or invalid",
174
211
  };
175
212
  }
176
213
  if (allPassing(list)) {
@@ -181,13 +218,19 @@ export class GrubController {
181
218
  status: "continue",
182
219
  summary: decision.summary,
183
220
  nextStep: pending
184
- ? `Complete pending feature: ${pending.id} (${pending.description})`
185
- : "Complete the remaining pending features before declaring done.",
221
+ ? this.activeTask.locale === "zh"
222
+ ? `完成待处理 feature:${pending.id}(${pending.description})`
223
+ : `Complete pending feature: ${pending.id} (${pending.description})`
224
+ : this.activeTask.locale === "zh"
225
+ ? "先完成剩余待处理 feature,再声明完成。"
226
+ : "Complete the remaining pending features before declaring done.",
186
227
  };
187
228
  return {
188
229
  decision: rewritten,
189
230
  downgraded: true,
190
- reason: `feature-list still has ${list.features.length - list.features.filter((f) => f.passes).length} pending entries`,
231
+ reason: this.activeTask.locale === "zh"
232
+ ? `feature-list 仍有 ${list.features.length - list.features.filter((f) => f.passes).length} 个待处理条目`
233
+ : `feature-list still has ${list.features.length - list.features.filter((f) => f.passes).length} pending entries`,
191
234
  };
192
235
  }
193
236
  finishTurn(decision) {
@@ -204,15 +247,23 @@ export class GrubController {
204
247
  task.phase = "execution";
205
248
  }
206
249
  if (decision.status === "complete") {
207
- return { action: "stop", snapshot: this.stop("Grub goal completed.", "complete") };
250
+ return {
251
+ action: "stop",
252
+ snapshot: this.stop(task.locale === "zh" ? "Grub 目标已完成。" : "Grub goal completed.", "complete"),
253
+ };
208
254
  }
209
255
  if (decision.status === "blocked") {
210
- return { action: "stop", snapshot: this.stop("Grub reported it is blocked.", "blocked") };
256
+ return {
257
+ action: "stop",
258
+ snapshot: this.stop(task.locale === "zh" ? "Grub 报告任务被阻塞。" : "Grub reported it is blocked.", "blocked"),
259
+ };
211
260
  }
212
261
  if (task.currentIteration >= task.maxIterations) {
213
262
  return {
214
263
  action: "stop",
215
- snapshot: this.stop(`Grub hit the iteration limit (${task.maxIterations}).`, "failed"),
264
+ snapshot: this.stop(task.locale === "zh"
265
+ ? `Grub 达到轮次上限(${task.maxIterations})。`
266
+ : `Grub hit the iteration limit (${task.maxIterations}).`, "failed"),
216
267
  };
217
268
  }
218
269
  task.currentIteration += 1;
@@ -231,13 +282,17 @@ export class GrubController {
231
282
  if (task.consecutiveFailures >= task.maxConsecutiveFailures) {
232
283
  return {
233
284
  action: "stop",
234
- snapshot: this.stop(`Grub stopped after ${task.consecutiveFailures} consecutive failures. Last error: ${message}`, "failed"),
285
+ snapshot: this.stop(task.locale === "zh"
286
+ ? `Grub 连续失败 ${task.consecutiveFailures} 次后停止。最近错误:${message}`
287
+ : `Grub stopped after ${task.consecutiveFailures} consecutive failures. Last error: ${message}`, "failed"),
235
288
  };
236
289
  }
237
290
  if (task.currentIteration >= task.maxIterations) {
238
291
  return {
239
292
  action: "stop",
240
- snapshot: this.stop(`Grub hit the iteration limit (${task.maxIterations}).`, "failed"),
293
+ snapshot: this.stop(task.locale === "zh"
294
+ ? `Grub 达到轮次上限(${task.maxIterations})。`
295
+ : `Grub hit the iteration limit (${task.maxIterations}).`, "failed"),
241
296
  };
242
297
  }
243
298
  task.currentIteration += 1;
@@ -0,0 +1,128 @@
1
+ /**
2
+ * [WHO]: Provides detectGrubLocale(), grubText(), type GrubLocale for localized /grub prompts and TUI messages
3
+ * [FROM]: Depends on core/i18n locale type
4
+ * [TO]: Consumed by grub-controller.ts, grub-parser.ts, index.ts for user-language-aware Grub UX
5
+ * [HERE]: extensions/defaults/grub/grub-i18n.ts - small locale helper scoped to the Grub extension
6
+ */
7
+ import type { Locale } from "../../../core/i18n/index.js";
8
+ export type GrubLocale = Locale;
9
+ export declare function detectGrubLocale(text: string, fallback?: Locale): GrubLocale;
10
+ export declare function languageName(locale: GrubLocale): string;
11
+ export declare function grubText(locale: GrubLocale): (typeof GRUB_TEXT)[GrubLocale];
12
+ declare const GRUB_TEXT: {
13
+ readonly en: {
14
+ readonly prefix: "[Grub]";
15
+ readonly missingGoal: "Missing grub goal.";
16
+ readonly usage: readonly ["[Grub] Usage:", " /grub <goal> [--max-iter N] [--max-fail N] Start an autonomous digging task", " /grub status [--json] Show the active or last finished task", " /grub resume Resume an adopted task from disk", " /grub stop Stop the active task", "", "[Grub] Harness artifacts under .grub/<task-id>/:", " feature-list.json structured features (agent may only flip passes/evidence)", " progress-log.md append-only progress notes", " init.sh per-iteration get-bearings + smoke script", " state.json durable GrubController state (for cross-session resume)", "", "[Grub] The agent keeps iterating until it reports complete, reports blocked,", "or hits a safety limit (iterations / consecutive failures). Declaring complete", "is rejected unless every feature in feature-list.json has passes:true."];
17
+ readonly activeTask: "Active task";
18
+ readonly lastTask: "Last task";
19
+ readonly status: "Status";
20
+ readonly phase: "Phase";
21
+ readonly goal: "Goal";
22
+ readonly started: "Started";
23
+ readonly updated: "Updated";
24
+ readonly currentIteration: "Current iteration";
25
+ readonly completedIterations: "Completed iterations";
26
+ readonly awaitingResult: "Awaiting result";
27
+ readonly yes: "yes";
28
+ readonly no: "no";
29
+ readonly consecutiveFailures: "Consecutive failures";
30
+ readonly maxIterations: "Max iterations";
31
+ readonly harnessDir: "Harness dir";
32
+ readonly featureList: "Feature list";
33
+ readonly progressLog: "Progress log";
34
+ readonly initScript: "Init script";
35
+ readonly stateFile: "State file";
36
+ readonly featuresPassing: (passing: number, total: number) => string;
37
+ readonly lastSummary: "Last summary";
38
+ readonly lastNextStep: "Last next step";
39
+ readonly lastError: "Last error";
40
+ readonly noActive: "No grub task is active.";
41
+ readonly noStarted: "No grub task has been started in this session.";
42
+ readonly decision: "Decision";
43
+ readonly summary: "Summary";
44
+ readonly nextStep: "Next step";
45
+ readonly resumeSummary: (id: string, iteration: number, phase: string) => string;
46
+ readonly resumeHint: "Use /grub status to inspect, /grub resume to continue dispatch, or /grub stop to abandon.";
47
+ readonly startingIteration: (iteration: number, id: string) => string;
48
+ readonly startedTask: (id: string) => string;
49
+ readonly initPhase: "Init phase: expand feature-list.json / init.sh / progress-log.md before broad implementation.";
50
+ readonly safetyLimits: (maxIterations: number, maxFailures: number) => string;
51
+ readonly resuming: (id: string) => string;
52
+ readonly stopped: (id: string) => string;
53
+ readonly noActiveRunning: "No active grub task is running.";
54
+ readonly noPersisted: "No adopted or persisted grub task to resume.";
55
+ readonly failedResume: (id: string, message: string) => string;
56
+ readonly failedAdopt: (message: string) => string;
57
+ readonly failedNoAssistant: "Grub run ended without an assistant message.";
58
+ readonly iterationFailedRetry: (iteration: number | undefined) => string;
59
+ readonly invalidLoopState: "Assistant response did not include a valid <loop-state> block.";
60
+ readonly invalidLoopRetry: (iteration: number | undefined) => string;
61
+ readonly prematureComplete: (reason: string) => string;
62
+ readonly harnessCreated: "- Harness created by /grub.";
63
+ readonly structuredFeatureNote: "- Structured feature list lives in feature-list.json; only passes/evidence may change.";
64
+ readonly initScriptNote: "- init.sh performs get-bearings + smoke before every iteration.";
65
+ readonly iterationsHeading: "## Iterations";
66
+ readonly appendIterationNote: "- (append one short entry per iteration with verification evidence)";
67
+ readonly progressLogTitle: (id: string) => string;
68
+ readonly initializationHeading: "## Initialization";
69
+ };
70
+ readonly zh: {
71
+ readonly prefix: "[Grub]";
72
+ readonly missingGoal: "缺少 grub 目标。";
73
+ readonly usage: readonly ["[Grub] 用法:", " /grub <目标> [--max-iter N] [--max-fail N] 启动一个自主长任务", " /grub status [--json] 查看当前或最近结束的任务", " /grub resume 继续磁盘中恢复的任务", " /grub stop 停止当前任务", "", "[Grub] 任务产物位于 .grub/<task-id>/:", " feature-list.json 结构化功能清单(agent 只能修改 passes/evidence)", " progress-log.md 追加式进度记录", " init.sh 每轮开始前的环境定位和烟测脚本", " state.json 持久化控制器状态(用于跨会话恢复)", "", "[Grub] agent 会持续迭代,直到完成、阻塞、用户停止,或触发安全上限。", "只有 feature-list.json 中所有功能都 passes:true 时,才允许声明完成。"];
74
+ readonly activeTask: "当前任务";
75
+ readonly lastTask: "最近任务";
76
+ readonly status: "状态";
77
+ readonly phase: "阶段";
78
+ readonly goal: "目标";
79
+ readonly started: "开始时间";
80
+ readonly updated: "更新时间";
81
+ readonly currentIteration: "当前轮次";
82
+ readonly completedIterations: "已完成轮次";
83
+ readonly awaitingResult: "等待结果";
84
+ readonly yes: "是";
85
+ readonly no: "否";
86
+ readonly consecutiveFailures: "连续失败";
87
+ readonly maxIterations: "最大轮次";
88
+ readonly harnessDir: "Harness 目录";
89
+ readonly featureList: "功能清单";
90
+ readonly progressLog: "进度日志";
91
+ readonly initScript: "初始化脚本";
92
+ readonly stateFile: "状态文件";
93
+ readonly featuresPassing: (passing: number, total: number) => string;
94
+ readonly lastSummary: "上次总结";
95
+ readonly lastNextStep: "下一步";
96
+ readonly lastError: "最近错误";
97
+ readonly noActive: "当前没有 grub 任务。";
98
+ readonly noStarted: "本会话还没有启动 grub 任务。";
99
+ readonly decision: "决策";
100
+ readonly summary: "总结";
101
+ readonly nextStep: "下一步";
102
+ readonly resumeSummary: (id: string, iteration: number, phase: string) => string;
103
+ readonly resumeHint: "可用 /grub status 查看,/grub resume 继续派发,或 /grub stop 放弃。";
104
+ readonly startingIteration: (iteration: number, id: string) => string;
105
+ readonly startedTask: (id: string) => string;
106
+ readonly initPhase: "初始化阶段:先完善 feature-list.json / init.sh / progress-log.md,再开始大范围实现。";
107
+ readonly safetyLimits: (maxIterations: number, maxFailures: number) => string;
108
+ readonly resuming: (id: string) => string;
109
+ readonly stopped: (id: string) => string;
110
+ readonly noActiveRunning: "当前没有正在运行的 grub 任务。";
111
+ readonly noPersisted: "没有可恢复的 grub 任务。";
112
+ readonly failedResume: (id: string, message: string) => string;
113
+ readonly failedAdopt: (message: string) => string;
114
+ readonly failedNoAssistant: "Grub 本轮结束时没有 assistant 消息。";
115
+ readonly iterationFailedRetry: (iteration: number | undefined) => string;
116
+ readonly invalidLoopState: "Assistant 回复缺少有效的 <loop-state> 块。";
117
+ readonly invalidLoopRetry: (iteration: number | undefined) => string;
118
+ readonly prematureComplete: (reason: string) => string;
119
+ readonly harnessCreated: "- Harness 由 /grub 创建。";
120
+ readonly structuredFeatureNote: "- 结构化功能清单位于 feature-list.json;后续只能修改 passes/evidence。";
121
+ readonly initScriptNote: "- 每轮开始前由 init.sh 执行环境定位和烟测。";
122
+ readonly iterationsHeading: "## 迭代记录";
123
+ readonly appendIterationNote: "- (每轮追加一条简短记录,包含验证证据)";
124
+ readonly progressLogTitle: (id: string) => string;
125
+ readonly initializationHeading: "## 初始化";
126
+ };
127
+ };
128
+ export {};