@yeaft/webchat-agent 1.0.514 → 1.0.516

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.
@@ -57,9 +57,7 @@ import listAgents from './list-agents.js';
57
57
  // --- P1 Routing tools (task-334d) ---
58
58
  import routeForward from './route-forward.js';
59
59
 
60
- // --- P1 Progress tracking ---
61
- import todoWrite from './todo-write.js';
62
- import startPlan from './start-plan.js';
60
+ // --- P1 Durable work ---
63
61
  import createWorkItem from './create-work-item.js';
64
62
 
65
63
  // H2.f.4: user-facing thread tools (spawnThread/switchThread/listThreads/...)
@@ -69,9 +67,8 @@ import createWorkItem from './create-work-item.js';
69
67
  // Feature tools (FeatureCreate/Update/List/Get/Progress/Memory + Followup
70
68
  // + UpdatePlan + feature_summary_post) and the FeatureArc auto-creation
71
69
  // system were removed in 2026-05-13 — they were defined but never used in
72
- // production, contributing ~2900 lines of dead code. The TodoWrite tool
73
- // above replaces them as the actual progress-tracking surface the LLM
74
- // uses for multi-step tasks.
70
+ // production, contributing ~2900 lines of dead code. Native checklist tools
71
+ // are also retired; durable execution belongs to Work Center.
75
72
 
76
73
  // --- P2 Auxiliary tools ---
77
74
  // task-333b L1 delete: ToolSearch and WriteStdin removed — the function-call
@@ -127,9 +124,7 @@ export const allTools = [
127
124
  // P1 Routing (task-334d)
128
125
  routeForward,
129
126
 
130
- // P1 Progress tracking
131
- todoWrite,
132
- startPlan,
127
+ // P1 Durable work
133
128
  createWorkItem,
134
129
 
135
130
  // P2 Auxiliary
@@ -41,9 +41,8 @@ import { createHash } from 'crypto';
41
41
  * @property {'fast'|'primary'|undefined} modelHint
42
42
  * @property {string} persona — markdown body (persona / system prompt seed)
43
43
  * @property {string} personaHash — sha256(persona).slice(0,8); changes when persona body changes
44
- * @property {string} planInstruction — optional per-VP planning style (used by `StartPlan`
45
- * tool); '' means "fall back to the default template".
46
- * Frontmatter scalar key `planInstruction`.
44
+ * @property {string} planInstruction — legacy planning metadata, preserved verbatim for
45
+ * compatibility; not consumed by native tools.
47
46
  * @property {string} dir — absolute path to VP dir
48
47
  * @property {string} memoryDir — absolute path to VP memory dir
49
48
  * @property {number} mtimeMs — role.md mtime (for hot-reload)
@@ -187,10 +186,8 @@ export function loadVpFromDir(dir) {
187
186
  modelHint,
188
187
  persona: body,
189
188
  personaHash: personaHashValue,
190
- // Optional per-VP planning style for the `StartPlan` tool. Stored as a
191
- // raw scalar string in role.md frontmatter (`planInstruction: "..."`).
192
- // Empty / missing → the tool falls back to the default template. Kept
193
- // verbatim — the tool itself is responsible for any framing.
189
+ // Preserve legacy planning metadata without rewriting user-owned role.md.
190
+ // The retired native planning tools no longer consume this field.
194
191
  planInstruction: typeof meta.planInstruction === 'string' ? String(meta.planInstruction) : '',
195
192
  dir,
196
193
  memoryDir,
@@ -760,25 +760,6 @@ function legacyProjectContext(yeaftDir, sessionId) {
760
760
  }, sessionId);
761
761
  }
762
762
 
763
- function buildProjectSharedBlock(projectContext, summaries = '') {
764
- const context = normalizeProjectContext(projectContext, null);
765
- const body = typeof summaries === 'string' ? summaries.trim() : '';
766
- if (!context?.projectId && !body) return '';
767
- const lines = ['[Project Shared Context]'];
768
- if (context?.projectId) {
769
- const label = context.projectName
770
- ? `${context.projectName} (${context.projectId})`
771
- : context.projectId;
772
- lines.push(`Project: ${label}`);
773
- lines.push('Sharing boundary: sibling Sessions in this Project on this Agent only.');
774
- } else {
775
- lines.push('Sharing boundary: sibling Sessions in the same Project on this Agent only.');
776
- }
777
- lines.push('Read-only memory summaries preserve each source Session identity.');
778
- if (body) lines.push('', body);
779
- return lines.join('\n');
780
- }
781
-
782
763
  function vpKey(sessionId, vpId) {
783
764
  return `${sessionId}::${vpId}`;
784
765
  }
@@ -5721,16 +5702,9 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
5721
5702
  ? `${projectContext.projectName} (${projectContext.projectId})`
5722
5703
  : (projectContext?.projectId || '');
5723
5704
  queryOpts.projectInstruction = projectContext?.projectInstruction || '';
5724
- // Related Session summaries now enter through Engine's single AMS
5725
- // memory outlet. Keep this announcement limited to Project identity and
5726
- // sharing boundaries so parent VP prompts do not duplicate the same prose
5727
- // that sub-agents receive through memory.
5728
- const sharedBlock = buildProjectSharedBlock(projectContext);
5729
- if (sharedBlock) {
5730
- queryOpts.sessionAnnouncement = queryOpts.sessionAnnouncement
5731
- ? `${queryOpts.sessionAnnouncement}\n\n${sharedBlock}`
5732
- : sharedBlock;
5733
- }
5705
+ // Project sibling identity remains available to Engine memory recall via
5706
+ // projectSessionIds. Do not mirror internal sharing metadata into the
5707
+ // user-authored Session announcement shown to the model.
5734
5708
  }
5735
5709
  let turnSessionMeta = null;
5736
5710
  try { turnSessionMeta = sessionCoordinator?.group?.getMeta?.() || null; } catch { turnSessionMeta = null; }
@@ -8238,7 +8212,6 @@ export function handleYeaftMcpReload(msg = {}) {
8238
8212
  export const __testHooks = {
8239
8213
  loadProjects,
8240
8214
  sharedProjectContext,
8241
- buildProjectSharedBlock,
8242
8215
  normalizeProjectContext,
8243
8216
  handleProjectContextSyncForTest(msg) {
8244
8217
  handleYeaftProjectContextSync(msg);
@@ -1,45 +0,0 @@
1
- <!-- lang:en -->
2
-
3
- # Planning Mode
4
-
5
- You have just entered **planning mode** for the topic below. Your job is to think through the work, land a concrete plan via `TodoWrite`, and then continue executing the first step in the same turn. Do not stop after writing the plan unless the first step is genuinely blocked by user input.
6
-
7
- ## How to think
8
-
9
- 1. **Restate the problem in one sentence**: what success looks like, in plain language.
10
- 2. **Surface the real constraints**: what is fixed, what is flexible, and where you would push back if the requirement is wrong.
11
- 3. **Identify blocking unknowns**: list only the unknowns that materially affect the plan. If one blocks all progress, make the first step resolving it.
12
- 4. **Choose an approach**: briefly compare it with one alternative and pick the simplest path that fits the scope.
13
- 5. **Break the work into 3-7 ordered steps**. Each step should be one concrete unit of work.
14
- 6. **Name the risks and validation**: what could go wrong, and what command, test, review, or inspection will prove the work is correct.
15
-
16
- ## Required flow
17
-
18
- 1. Write a short prose plan: problem, approach, risks.
19
- 2. Call `TodoWrite` with the ordered steps. Mark exactly one item as `in_progress`.
20
- 3. Emit `TodoWrite` with a first work-tool call in the same assistant response only when that call is already necessary and its arguments and safety do not depend on another result. Start with the smallest such call. If its result can change the next action, inspect it before issuing more calls; do not speculative-batch the investigation.
21
-
22
- If the first step is to ask the user a blocking question, ask it and stop. Otherwise keep moving.
23
-
24
- <!-- lang:zh -->
25
-
26
- # 规划模式
27
-
28
- 你刚进入下面主题的**规划模式**。你的任务是先想清楚工作,使用 `TodoWrite` 写下可执行步骤,然后在同一轮继续执行第一步。不要只写完计划就停下,除非第一步确实需要用户输入才能继续。
29
-
30
- ## 怎么思考
31
-
32
- 1. **用一句话重述问题**:成功完成后应该是什么样子。
33
- 2. **列出现实约束**:哪些固定、哪些可调整、哪些需求如果不合理需要明确指出。
34
- 3. **识别阻塞未知**:只列真正影响计划的未知。如果某个未知阻塞全部进展,第一步就应该先解决它。
35
- 4. **选择方案**:和一个备选方案简单比较,然后选择符合范围的最简单路径。
36
- 5. **拆成 3-7 个有序步骤**:每一步都应该是一个具体工作单元。
37
- 6. **说明风险和验证**:可能出什么问题,以及用什么命令、测试、review 或检查证明结果正确。
38
-
39
- ## 必须执行的流程
40
-
41
- 1. 写一段简短计划:问题、方案、风险。
42
- 2. 调用 `TodoWrite` 写入有序步骤,并且只能把一个条目标记为 `in_progress`。
43
- 3. 只有第一个工作工具调用已经确定有必要,且其参数和安全性都不依赖其他结果时,才在同一个 assistant response 中把它与 `TodoWrite` 一起发出。先执行满足条件的最小调用;如果它的结果可能改变下一动作,应先检查结果,不要推测性批量展开调查。
44
-
45
- 如果第一步是向用户询问阻塞问题,那就提问并停下。否则继续推进。
@@ -1,11 +0,0 @@
1
- <!-- lang:en -->
2
-
3
- ## Active Tool Guidance
4
-
5
- {{guidance}}
6
-
7
- <!-- lang:zh -->
8
-
9
- ## 当前工具指引
10
-
11
- {{guidance}}
@@ -1,182 +0,0 @@
1
- /**
2
- * start-plan.js — StartPlan tool.
3
- *
4
- * Lightweight planning entry point inspired by Claude Code's plan mode.
5
- * Unlike Claude Code, we do NOT swap tools or change conversation state —
6
- * `StartPlan` is a regular tool. Its job is to push a planning instruction
7
- * back into the model's tool-result stream so the same turn produces a
8
- * short prose plan, a `TodoWrite` call to land the steps, and then keeps
9
- * going by starting the first step. The plan is the runway, not the
10
- * destination — the loop continues until the work is done (or until the
11
- * model legitimately needs the user, e.g. an unresolved unknown).
12
- *
13
- * Design:
14
- * - Anyone can call it; the tool description tells the LLM when to.
15
- * - The instruction text comes from one of two places, in order:
16
- * 1. The active VP's `planInstruction` frontmatter override
17
- * (threaded through ctx.vpPersona.planInstruction by the
18
- * engine; see engine.js #buildToolContext).
19
- * 2. The default template `templates/plan-instruction.md`
20
- * loaded at module init by prompts.js.
21
- * - The caller may pass guiding fields (stuckAt, userProblem,
22
- * expectedScale, additionalContext) to help the model think — these
23
- * are echoed back in the tool result so the planning turn can read
24
- * them without re-asking the user.
25
- * - Output is plain text (the instruction + the echo). No side effects,
26
- * no persistence — the same turn that called StartPlan produces the
27
- * plan, lands the steps via TodoWrite, and starts executing the first
28
- * step (unless that first step is "ask the user", which is the one
29
- * legitimate reason to stop here).
30
- *
31
- * The expected integration is TodoWrite: during the planning turn the LLM
32
- * issues a `TodoWrite` call enumerating the 1..N steps, then keeps going
33
- * and works the first step. The frontend already renders TodoWrite as a
34
- * checkbox-style list, so the user sees the plan materialize without any
35
- * new UI.
36
- */
37
-
38
- import { defineTool } from './types.js';
39
- import { getDefaultPlanInstruction } from '../prompts.js';
40
-
41
- export default defineTool({
42
- name: 'StartPlan',
43
- description: {
44
- en: `Enter planning mode for a non-trivial task. Use BEFORE you start working when the request needs multiple steps, has unclear scope, or the user said "make a plan" / "think through this first".
45
-
46
- This tool returns a planning instruction. Use it to land a structured plan, then keep working in the same turn. The expected flow is:
47
- 1. Produce a short prose plan (problem, approach, risks).
48
- 2. Call \`TodoWrite\` with the ordered steps. Mark the first concrete step "in_progress", the rest "pending".
49
- 3. Emit \`TodoWrite\` with a first work-tool call only when that call is already necessary and its arguments and safety do not depend on another result. Start with the smallest such call; inspect its result before issuing calls it could change or make unnecessary.
50
-
51
- WHEN TO USE:
52
- - Multi-step implementation (3+ steps), refactor, or open-ended investigation.
53
- - User explicitly asks for a plan, a TODO list, or to "think through" the work.
54
- - You're about to start a large change and want a checkpoint before diving in.
55
-
56
- WHEN NOT TO USE:
57
- - Single trivial change, single command run, lookup-style question.
58
- - Mid-execution — once you're past the first step, use TodoWrite directly.
59
-
60
- EXCEPTION — stop after the plan only if the first step is genuinely "ask the user" (an unresolved unknown that blocks every other step). Otherwise keep moving.
61
-
62
- The tool takes the topic plus optional guiding fields (stuckAt, userProblem, expectedScale, additionalContext) that help you think; they're echoed back verbatim, so don't repeat the full user request in \`topic\`.`,
63
- zh: `进入规划模式,用于非平凡任务。在开始工作之前使用——当需求涉及多步骤、范围不明确,或用户说"先做个计划"/"先想清楚"时。
64
-
65
- 此工具返回规划指令。用它产出一份结构化计划,然后在同一个 turn 中继续工作。预期流程是:
66
- 1. 产出简短文字计划(问题、方法、风险)。
67
- 2. 调用 TodoWrite 写出有序步骤。将第一个具体步骤标记为 "in_progress",其余标记为 "pending"。
68
- 3. 只有第一个工作工具调用已经确定有必要,且其参数和安全性都不依赖其他结果时,才在同一个 assistant response 中把它与 TodoWrite 一起发出。先执行满足条件的最小调用;如果结果可能改变或使后续调用不再必要,应先检查结果。
69
-
70
- 何时使用:
71
- - 多步骤实现(3+ 步)、重构或开放式调查。
72
- - 用户明确要求计划、TODO 列表或"想清楚"再动手。
73
- - 你即将开始一个大型改动,想在动手前有个检查点。
74
-
75
- 何时不使用:
76
- - 单个琐碎改动、单个命令执行、查询式问题。
77
- - 中途执行——一旦过了第一步,直接用 TodoWrite。
78
-
79
- 例外——只有当第一步确实是"询问用户"(一个未解决的未知因素,阻塞所有其他步骤)时才在计划后停止。否则继续前进。
80
-
81
- 此工具接收 topic 加上可选引导字段(stuckAt、userProblem、expectedScale、additionalContext)
82
- 帮助你思考;它们会被原样回显,所以不要在 topic 中重复完整的用户请求。`
83
- },
84
- parameters: {
85
- type: 'object',
86
- properties: {
87
- topic: {
88
- type: 'string',
89
- description: {
90
- en: 'One-sentence statement of what is being planned (e.g. "Add dark-mode toggle to YeaftPage settings").',
91
- zh: '用一句话说明要规划什么(如"为 YeaftPage 设置添加深色模式切换")',
92
- },
93
- },
94
- userProblem: {
95
- type: 'string',
96
- description: {
97
- en: 'Optional. The underlying problem the user is trying to solve (often broader than the immediate ask).',
98
- zh: '可选。用户试图解决的根本问题(通常比即时请求更宽泛)',
99
- },
100
- },
101
- stuckAt: {
102
- type: 'string',
103
- description: {
104
- en: 'Optional. If you are blocked or unsure, the specific decision or unknown that needs resolving first.',
105
- zh: '可选。如果你被阻塞或不确定,需要首先解决的具体决策或未知点',
106
- },
107
- },
108
- expectedScale: {
109
- type: 'string',
110
- description: {
111
- en: 'Optional. Rough scope estimate — number of files touched, lines of code, time horizon, etc.',
112
- zh: '可选。粗略范围估计 — 涉及文件数、代码行数、时间预期等',
113
- },
114
- },
115
- additionalContext: {
116
- type: 'string',
117
- description: {
118
- en: 'Optional. Any other facts that shape the plan (constraints, deadlines, related prior work).',
119
- zh: '可选。影响计划的其他事实(约束、截止日期、相关先前工作)',
120
- },
121
- },
122
- },
123
- required: ['topic'],
124
- },
125
- isConcurrencySafe: () => true,
126
- isReadOnly: () => true,
127
- async execute(input, ctx) {
128
- const topic = typeof input?.topic === 'string' ? input.topic.trim() : '';
129
- if (!topic) {
130
- // Plain-text error — same shape as the success path so the LLM
131
- // doesn't need a JSON-vs-text branch to read this tool's output.
132
- return 'Error: topic is required (one-sentence statement of what is being planned).';
133
- }
134
-
135
- // Resolve the planning instruction: per-VP override first, then default.
136
- // `ctx.vpPersona.planInstruction` is wired by engine.js #buildToolContext;
137
- // it's the empty string when the VP has no override, or when the call is
138
- // not VP-scoped (test ctx, sub-agent ctx without persona). We tolerate
139
- // both shapes and fall through to the default template silently.
140
- const language = typeof ctx?.config?.language === 'string' ? ctx.config.language : 'en';
141
- const vpOverride = typeof ctx?.vpPersona?.planInstruction === 'string'
142
- ? ctx.vpPersona.planInstruction.trim()
143
- : '';
144
- const instruction = vpOverride || getDefaultPlanInstruction(language);
145
-
146
- // Echo the optional guiding fields back so the planning turn has them
147
- // without re-reading the user's original message. Skip empty strings.
148
- const echoed = {};
149
- for (const key of ['userProblem', 'stuckAt', 'expectedScale', 'additionalContext']) {
150
- const v = typeof input?.[key] === 'string' ? input[key].trim() : '';
151
- if (v) echoed[key] = v;
152
- }
153
-
154
- // Plain text is friendlier to the LLM than JSON for an instructional
155
- // result. The shape: a tagged instruction block, then a compact
156
- // YAML-ish echo of the guiding fields, then a one-line nudge to land
157
- // the plan via TodoWrite.
158
- const lines = [];
159
- lines.push('<plan-instruction>');
160
- lines.push(instruction);
161
- lines.push('</plan-instruction>');
162
- lines.push('');
163
- lines.push('<topic>');
164
- lines.push(topic);
165
- lines.push('</topic>');
166
- if (Object.keys(echoed).length > 0) {
167
- lines.push('');
168
- lines.push('<guiding-context>');
169
- for (const [k, v] of Object.entries(echoed)) {
170
- lines.push(`${k}: ${v}`);
171
- }
172
- lines.push('</guiding-context>');
173
- }
174
- lines.push('');
175
- const nextInstruction = String(language).toLowerCase().startsWith('zh')
176
- ? '下一步:产出计划并调用 `TodoWrite`。只有第一个工作工具调用已经确定有必要,且其参数和安全性都不依赖其他结果时,才在同一个 assistant response 中发出这个最小调用;如果结果可能改变后续调用,先检查结果。只有第一步必须询问用户时才在计划后停下。'
177
- : 'Next: produce the plan and call `TodoWrite`. Emit only the smallest first work-tool call whose necessity, arguments, and safety do not depend on another result; inspect its result before calls it could change. Stop after the plan only when the first step must ask the user.';
178
- lines.push(nextInstruction);
179
-
180
- return lines.join('\n');
181
- },
182
- });
@@ -1,174 +0,0 @@
1
- /**
2
- * todo-write.js — TodoWrite tool: per-VP multi-step task tracking.
3
- *
4
- * Mirrors Claude Code's `TodoWrite` tool 1:1 in shape (name + `todos[]`
5
- * with `content` / `status` / `activeForm`) so the existing frontend
6
- * rendering pipeline (`MessageList.js:691` → `AssistantTurn.js:53-63`
7
- * → `ToolLine.js:150`) renders a checkmark-style list automatically
8
- * without any new UI code. The frontend reads the *input* of the
9
- * `tool_use` event — not the result — so this tool's persistence story
10
- * is "stamp into the LLM event stream and cache on ctx for replay."
11
- *
12
- * Per-thread isolation: each running VP thread keeps its own current todo
13
- * list. The web-bridge injects `ctx.getCurrentTodos()` /
14
- * `ctx.setCurrentTodos()` pointing at a per-(sessionId,vpId,threadId) slot so
15
- * two concurrent threads for the same VP cannot overwrite each other's
16
- * progress.
17
- *
18
- * Reference: plan §2 (2026-05-13 — Feature system retired, TodoWrite
19
- * added as the actual progress-tracking surface for the LLM).
20
- */
21
-
22
- import { defineTool } from './types.js';
23
-
24
- const VALID_STATUS = new Set(['pending', 'in_progress', 'completed']);
25
-
26
- export default defineTool({
27
- name: 'TodoWrite',
28
- description: {
29
- en: `Track multi-step task progress with a checklist that the user can see ticked off in real time.
30
-
31
- WHEN TO USE:
32
- - The task has 3+ meaningful steps, or
33
- - The user gave you a list of things to do (numbered/comma-separated), or
34
- - You're about to start a non-trivial, multi-file change.
35
-
36
- FIRST CALL — PLAN WITHOUT AN EXTRA MODEL ROUND:
37
- - Write a short visible prose plan in the same assistant response: problem, approach, and risks.
38
- - Call TodoWrite directly; do not call a separate planning-mode tool first.
39
- - Emit TodoWrite beside the first work-tool call only when that call is already necessary and its arguments and safety do not depend on another result. Start with the smallest such call.
40
-
41
- HOW TO USE:
42
- - First call: enumerate all the todos with status "pending", set exactly one to "in_progress".
43
- - Each subsequent call: rewrite the FULL list — mark the just-finished item "completed", mark the next item "in_progress".
44
- - AT MOST one item may be "in_progress" at any time.
45
- - \`content\` is the imperative form ("Run tests"); \`activeForm\` is the present-continuous shown during execution ("Running tests").
46
-
47
- BATCH WITH WORK:
48
- - Avoid an intermediate TodoWrite-only model round only when the next work-tool call passes the necessity, argument-independence, and safety-independence test. Emit that minimal call beside TodoWrite.
49
- - Do not speculative-batch an investigation. Mark work completed only after evidence, and inspect a pending result before issuing any call it could change, invalidate, or make unnecessary.
50
- - A standalone TodoWrite remains valid when no work tool should follow, including final completion or a blocking user question.
51
-
52
- WHEN NOT TO USE:
53
- - Single trivial change, single command run, pure conversation/question.`,
54
- zh: `用 checklist 跟踪多步骤任务进度,用户可以实时看到勾选。
55
-
56
- 何时使用:
57
- - 任务有 3 步或以上有意义步骤,或
58
- - 用户给了你一份待办列表(编号/逗号分隔),或
59
- - 你即将开始一个非平凡的多文件改动。
60
-
61
- 首次调用——不要浪费额外模型回合进入规划模式:
62
- - 在同一个 assistant response 中先写简短可见计划:问题、方案和风险。
63
- - 直接调用 TodoWrite,不要先调用单独的规划模式工具。
64
- - 只有第一个工作工具调用已经确定有必要,且其参数和安全性都不依赖其他结果时,才把它与 TodoWrite 在同一响应中发出;先执行满足条件的最小调用。
65
-
66
- 如何使用:
67
- - 首次调用:枚举所有 todo,状态为 "pending",将其中恰好一个设为 "in_progress"。
68
- - 每次后续调用:重写完整列表——将刚完成的项标记为 "completed",将下一项标记为 "in_progress"。
69
- - 任何时候最多只能有一个 "in_progress"。
70
- - content 是祈使形式(如 "Run tests");activeForm 是执行时显示的进行时态(如 "Running tests")。
71
-
72
- 和工作工具合批:
73
- - 只有下一个工作工具调用通过必要性、参数独立性和安全独立性检查时,才避免让中间状态的 TodoWrite 单独占一个模型回合;把这个最小调用与 TodoWrite 一起发出。
74
- - 不要推测性批量展开调查。只有已有证据时才能把工作标记为完成;如果待返回结果可能改变、否定或使后续调用不再必要,应先检查该结果。
75
- - 没有工作工具应继续执行时(包括记录最终完成态或询问阻塞问题),TodoWrite 仍可单独调用。
76
-
77
- 何时不使用:
78
- - 单个琐碎改动、单个命令执行、纯对话/提问。`
79
- },
80
- parameters: {
81
- type: 'object',
82
- properties: {
83
- todos: {
84
- type: 'array',
85
- description: {
86
- en: 'The full current todo list. Always send the entire list, not a diff.',
87
- zh: '当前完整的待办清单。始终发送整个列表,而非增量。',
88
- },
89
- items: {
90
- type: 'object',
91
- properties: {
92
- content: {
93
- type: 'string',
94
- description: {
95
- en: 'Imperative description of the step (e.g. "Run tests").',
96
- zh: '步骤的命令式描述(如 "Run tests")。',
97
- },
98
- },
99
- status: {
100
- type: 'string',
101
- enum: ['pending', 'in_progress', 'completed'],
102
- description: {
103
- en: 'Current state. At most one item may be "in_progress".',
104
- zh: '当前状态。最多只能有一项为 "in_progress"。',
105
- },
106
- },
107
- activeForm: {
108
- type: 'string',
109
- description: {
110
- en: 'Present-continuous form shown while executing (e.g. "Running tests").',
111
- zh: '执行中展示的进行时描述(如 "Running tests")。',
112
- },
113
- },
114
- },
115
- required: ['content', 'status', 'activeForm'],
116
- },
117
- },
118
- },
119
- required: ['todos'],
120
- },
121
- isConcurrencySafe: () => false,
122
- isReadOnly: () => true,
123
- async execute(input, ctx) {
124
- const todos = input && Array.isArray(input.todos) ? input.todos : null;
125
- if (!todos || todos.length === 0) {
126
- return JSON.stringify({ error: 'todos must be a non-empty array' });
127
- }
128
-
129
- let inProgressCount = 0;
130
- for (let i = 0; i < todos.length; i++) {
131
- const t = todos[i];
132
- if (!t || typeof t !== 'object') {
133
- return JSON.stringify({ error: `todos[${i}] must be an object` });
134
- }
135
- if (typeof t.content !== 'string' || !t.content.trim()) {
136
- return JSON.stringify({ error: `todos[${i}].content must be a non-empty string` });
137
- }
138
- if (typeof t.activeForm !== 'string' || !t.activeForm.trim()) {
139
- return JSON.stringify({ error: `todos[${i}].activeForm must be a non-empty string` });
140
- }
141
- if (!VALID_STATUS.has(t.status)) {
142
- return JSON.stringify({
143
- error: `todos[${i}].status must be one of: pending, in_progress, completed`,
144
- });
145
- }
146
- if (t.status === 'in_progress') inProgressCount += 1;
147
- }
148
-
149
- if (inProgressCount > 1) {
150
- return JSON.stringify({
151
- error: `at most one todo may be in_progress at a time (found ${inProgressCount})`,
152
- });
153
- }
154
-
155
- // Cache the current todo list onto the per-VP slot if the web-bridge
156
- // provided one. Best-effort: this is the cache the frontend may pull
157
- // on reconnect / VP-switch. Tools should not crash if the slot is
158
- // missing — sub-agent ctx or test ctx may lack it.
159
- if (ctx && typeof ctx.setCurrentTodos === 'function') {
160
- try { ctx.setCurrentTodos(todos.slice()); } catch { /* swallow */ }
161
- }
162
-
163
- const counts = { pending: 0, in_progress: 0, completed: 0 };
164
- for (const t of todos) counts[t.status] += 1;
165
-
166
- return JSON.stringify({
167
- success: true,
168
- count: todos.length,
169
- pending: counts.pending,
170
- in_progress: counts.in_progress,
171
- completed: counts.completed,
172
- });
173
- },
174
- });