minimal-agent 0.8.0 → 0.8.1

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": "minimal-agent",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "最小化 Agent 系统 —— 10 工具 + 插件系统 + workflow DSL + 自动压缩 + OpenAI 兼容 + Ink TUI;NodeNext + tsc 原地编译,dev 用 Bun .ts、install 用 Node .js(学习/教学用)",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": "Bill Wang <leiwang0359@gmail.com>",
@@ -22,8 +22,8 @@
22
22
  import { fileURLToPath } from 'node:url';
23
23
  import { dirname } from 'node:path';
24
24
  import { runQuery, getWorkingDir, } from '../../src/plugin-sdk.js';
25
+ import { parseVerifyArgs, runVerification } from '../../src/infra/verification.js';
25
26
  import { GoalState, Phase } from './src/goalState.js';
26
- import { parseVerifyArgs, runVerification } from './src/verificationGate.js';
27
27
  import { makeCompletionMatcher, hasNeedReplanSentinel, hasNeedGoalRevisionSentinel, extractLearnings, } from './src/sentinels.js';
28
28
  import { executeStopHook } from './src/stopHookRunner.js';
29
29
  const PLUGIN_NAME = 'ralph-wiggum';
@@ -12,6 +12,7 @@
12
12
  * 缓存这次 import,所以 cold start 后 dynamic import 只跑一次。
13
13
  * ============================================================
14
14
  */
15
+ import { ALL_TOOLS, getWorkingDir } from '../../src/plugin-sdk.js';
15
16
  import { runWorkflowFromCommand, runWorkflowsList } from './src/index.js';
16
17
  const api = {
17
18
  async *runCommand(commandName, args, ctx) {
@@ -24,6 +25,8 @@ const api = {
24
25
  provider: ctx.provider,
25
26
  history: ctx.history,
26
27
  signal: ctx.signal,
28
+ cwd: getWorkingDir(),
29
+ tools: ALL_TOOLS,
27
30
  });
28
31
  return;
29
32
  }
@@ -130,6 +130,7 @@ function validate(obj, file) {
130
130
  // editor 因 D2 红线禁止 import 后端、手抄了同一份;后端在此演进时该测试会红,提示同步 editor。
131
131
  export const CONTROL_FLOW_TYPES = new Set([
132
132
  'assert',
133
+ 'verify',
133
134
  'branch',
134
135
  'loop',
135
136
  'pause',
@@ -138,6 +139,7 @@ export const CONTROL_FLOW_TYPES = new Set([
138
139
  ]);
139
140
  export const VALID_STEP_TYPES = new Set([
140
141
  'assert',
142
+ 'verify',
141
143
  'branch',
142
144
  'loop',
143
145
  'pause',
@@ -207,6 +209,16 @@ function validateSteps(steps, file, pathPrefix) {
207
209
  if (s.type === 'assert' && s.condition === undefined) {
208
210
  throw new WorkflowLoadError(`${p} type=assert 必须带 'condition'`, file);
209
211
  }
212
+ if (s.type === 'verify') {
213
+ if (!Array.isArray(s.checks) || s.checks.length === 0) {
214
+ throw new WorkflowLoadError(`${p} type=verify 必须带非空 'checks' 数组`, file);
215
+ }
216
+ for (const [ci, c] of s.checks.entries()) {
217
+ if (typeof c !== 'string' || !c.includes(':')) {
218
+ throw new WorkflowLoadError(`${p}.checks[${ci}] 格式错误,应为 "type:value"(如 "file_exists:./x")`, file);
219
+ }
220
+ }
221
+ }
210
222
  if (s.type === 'branch') {
211
223
  if (s.then !== undefined && !Array.isArray(s.then)) {
212
224
  throw new WorkflowLoadError(`${p}.then 必须是数组`, file);
@@ -27,6 +27,7 @@ import { evalExpr, interpolate } from './expressions.js';
27
27
  import { runRecovery } from './recovery.js';
28
28
  import { execAgentStep } from './stepExecutors/agent.js';
29
29
  import { execAssertStep } from './stepExecutors/assert.js';
30
+ import { execVerifyStep } from './stepExecutors/verify.js';
30
31
  import { execLlmStep } from './stepExecutors/llm.js';
31
32
  import { execSkillStep } from './stepExecutors/skill.js';
32
33
  import { execToolStep } from './stepExecutors/tool.js';
@@ -78,6 +79,8 @@ function bindCapture(captureMap, result, vars) {
78
79
  async function* dispatchActionStep(step, vars, ctx) {
79
80
  if (step.type === 'assert')
80
81
  return execAssertStep(step, vars);
82
+ if (step.type === 'verify')
83
+ return await execVerifyStep(step, vars);
81
84
  if (step.tool)
82
85
  return yield* execToolStep(step, vars, ctx);
83
86
  if (step.agent)
@@ -86,7 +89,7 @@ async function* dispatchActionStep(step, vars, ctx) {
86
89
  return yield* execLlmStep(step, vars, ctx);
87
90
  if (step.skill)
88
91
  return yield* execSkillStep(step, vars, ctx);
89
- throw new Error(`step ${step.id}: 无可执行字段(tool/agent/llm/skill/assert)`);
92
+ throw new Error(`step ${step.id}: 无可执行字段(tool/agent/llm/skill/assert/verify)`);
90
93
  }
91
94
  /**
92
95
  * 递归执行 steps[]。返回 true 表示该 block 已 halt(上游应中止)。
@@ -527,14 +530,41 @@ export async function* runWorkflow(def, inputs, ctx) {
527
530
  };
528
531
  yield { type: 'workflow_done', name: def.name, vars: vars.snapshot() };
529
532
  succeeded = true;
533
+ // ---- 层1:写入摘要到对话历史(让后续对话记住这次 workflow 做了什么)----
534
+ const snapshot = vars.snapshot();
535
+ const summaryText = typeof snapshot.summary_text === 'string'
536
+ ? snapshot.summary_text
537
+ : '';
538
+ // 收集关键产出物(从 vars 中提取文件路径)
539
+ const artifacts = [];
540
+ if (typeof snapshot.plan_text === 'string')
541
+ artifacts.push('./PLAN.md');
542
+ // 检查是否有执行结果(execute_plan 步的产出)
543
+ if (typeof snapshot.execution_result === 'string') {
544
+ // 尝试从执行结果中提取文件路径(常见模式:产出物路径)
545
+ const fileMatches = snapshot.execution_result.match(/`[^`]*\.(pptx|docx|pdf|xlsx|md|ts|js|py)[^`]*`/g);
546
+ if (fileMatches)
547
+ artifacts.push(...fileMatches.map((f) => f.replace(/`/g, '')));
548
+ }
549
+ const summaryLines = [
550
+ `**工作流「${def.name}」执行完成**`,
551
+ ``,
552
+ `- 步骤: ${def.steps.length}/${def.steps.length} 全部完成`,
553
+ ...(artifacts.length > 0 ? [`- 产出物: ${artifacts.join(', ')}`] : []),
554
+ ...(summaryText ? [`- 摘要: ${summaryText}`] : []),
555
+ ];
556
+ yield {
557
+ type: 'assistant_message',
558
+ message: {
559
+ role: 'assistant',
560
+ content: summaryLines.join('\n'),
561
+ },
562
+ };
530
563
  }
531
564
  finally {
532
- // 只在「干净跑完」时清现场;halt / 中断 / 抛错都**保留** .minimal-agent-workflow/
533
- // (current.json / inputs.json / vars.json / progress.md,含失败那步的 ✗ 行)供事后排查
534
- // ——这正是这些文件的设计目的(workflowState.ts 头注);失败时删掉等于自废武功。
535
- // 残留不会永久堆积:下次同名 workflow 的 init() 会先 rm,/new 也按 .minimal-agent-* 前缀统一清。
565
+ // 成功时归档(保留 vars.json/progress.md 供后续回溯);失败/中断时保留原状供排查
536
566
  if (succeeded)
537
- await state.cleanup();
567
+ await state.archive();
538
568
  }
539
569
  }
540
570
  /**
@@ -11,6 +11,7 @@
11
11
  * - agent 的产出经过验证闸门(interpretMiniReActResult),llm 直接透传
12
12
  * ============================================================
13
13
  */
14
+ import { interpolate } from '../expressions.js';
14
15
  import { runMiniReAct } from '../miniReAct.js';
15
16
  import { AGENT_OPENING, buildWorkflowAgentSystem, describeCaptureContract, interpretMiniReActResult, } from '../agentCommon.js';
16
17
  /**
@@ -25,13 +26,17 @@ import { AGENT_OPENING, buildWorkflowAgentSystem, describeCaptureContract, inter
25
26
  * 这解决了之前 workflow 模式下模型不知道有 pptx/pdf 等 skill 的问题。
26
27
  */
27
28
  export async function* execAgentStep(step, vars, ctx) {
28
- const tools = (ctx.tools ?? []).filter((t) => step.allowed_tools?.includes(t.name));
29
+ // 工具白名单:allowed_tools 声明时按白名单过滤;未声明时默认全量(不限制)
30
+ const allTools = ctx.tools ?? [];
31
+ const tools = step.allowed_tools
32
+ ? allTools.filter((t) => step.allowed_tools.includes(t.name))
33
+ : allTools;
29
34
  // ✅ Phase 2: 异步构建完整 system prompt(含 skill/plugin 列表)
30
35
  const systemContent = await buildWorkflowAgentSystem(ctx.cwd, // 新增:传入 cwd 用于 skill/plugin 扫描
31
36
  AGENT_OPENING, tools);
32
- const userMsg = step.prompt +
37
+ const userMsg = interpolate(step.agent ?? '', vars) +
33
38
  describeCaptureContract(step) +
34
- (step.input ? `\n\n上下文变量:${step.input}` : '');
39
+ (step.input ? `\n\n上下文变量:${interpolate(step.input, vars)}` : '');
35
40
  const messages = [
36
41
  { role: 'system', content: systemContent },
37
42
  { role: 'user', content: userMsg },
@@ -0,0 +1,41 @@
1
+ /**
2
+ * ============================================================
3
+ * stepExecutors/verify.ts —— verify step 执行
4
+ * ------------------------------------------------------------
5
+ * 复用 src/infra/verification.ts 共享验证基础设施,
6
+ * 支持 shell / file_exists / file_contains / test_count 四种检查。
7
+ *
8
+ * 与 assert 的区别:
9
+ * - assert: evalExpr 表达式(轻量,适合简单文件存在判断)
10
+ * - verify: 完整检查器(重量,支持命令执行/正则匹配/测试计数)
11
+ * ============================================================
12
+ */
13
+ import { parseCheckString, runVerification } from '../../../../src/infra/verification.js';
14
+ import { interpolate } from '../expressions.js';
15
+ export async function execVerifyStep(step, vars) {
16
+ if (!Array.isArray(step.checks) || step.checks.length === 0) {
17
+ throw new Error(`step ${step.id}: verify 缺少 checks 数组`);
18
+ }
19
+ // 解析并插值每条 check 字符串
20
+ const checks = [];
21
+ for (const raw of step.checks) {
22
+ const interpolated = interpolate(raw, vars);
23
+ const check = parseCheckString(interpolated);
24
+ if (!check) {
25
+ throw new Error(`step ${step.id}: 无法解析 check "${interpolated}",格式应为 "type:value"`);
26
+ }
27
+ checks.push(check);
28
+ }
29
+ // 执行所有检查
30
+ const result = await runVerification(checks);
31
+ if (!result.passed) {
32
+ const msg = step.onFail
33
+ ? interpolate(step.onFail, vars)
34
+ : `verify 失败: ${result.summary}`;
35
+ throw new Error(msg);
36
+ }
37
+ return {
38
+ raw: { passed: true, summary: result.summary, details: result.details },
39
+ preview: result.summary,
40
+ };
41
+ }
@@ -18,16 +18,19 @@
18
18
  * 因为执行流程是确定性的,没有 LLM 自规划过程。
19
19
  * ============================================================
20
20
  */
21
- import { mkdir, writeFile, appendFile, rm } from 'node:fs/promises';
21
+ import { mkdir, writeFile, appendFile, rm, rename } from 'node:fs/promises';
22
22
  import { join } from 'node:path';
23
23
  const STATE_DIR = '.minimal-agent-workflow';
24
+ const DONE_SUFFIX = '.done';
24
25
  export class WorkflowState {
25
26
  dir;
26
27
  constructor(workingDir) {
27
28
  this.dir = join(workingDir, STATE_DIR);
28
29
  }
29
30
  async init(name, inputs) {
30
- await rm(this.dir, { recursive: true, force: true });
31
+ // 启动新 workflow 时清理旧状态(含之前的归档)
32
+ await rm(this.dir, { recursive: true, force: true }).catch(() => { });
33
+ await rm(this.dir + DONE_SUFFIX, { recursive: true, force: true }).catch(() => { });
31
34
  await mkdir(this.dir, { recursive: true });
32
35
  await writeFile(join(this.dir, 'current.json'), JSON.stringify({ name, startedAt: new Date().toISOString() }, null, 2), 'utf8');
33
36
  await writeFile(join(this.dir, 'inputs.json'), JSON.stringify(inputs, null, 2), 'utf8');
@@ -40,7 +43,32 @@ export class WorkflowState {
40
43
  async writeVars(snapshot) {
41
44
  await writeFile(join(this.dir, 'vars.json'), JSON.stringify(snapshot, null, 2), 'utf8').catch(() => { });
42
45
  }
46
+ /**
47
+ * 归档(成功完成时调用):重命名状态目录为 .done 后缀,保留所有中间产物。
48
+ * vars.json / progress.md / inputs.json 全部保留——供后续对话 Read 回溯。
49
+ */
50
+ async archive() {
51
+ const doneDir = this.dir + DONE_SUFFIX;
52
+ try {
53
+ // 先清理旧的归档(如果存在)
54
+ await rm(doneDir, { recursive: true, force: true });
55
+ }
56
+ catch {
57
+ // ignore
58
+ }
59
+ try {
60
+ await rename(this.dir, doneDir);
61
+ }
62
+ catch {
63
+ // rename 失败时静默(某些文件系统/权限问题),保留原目录
64
+ }
65
+ }
66
+ /**
67
+ * 强制清理(/new 或新 workflow init 时调用):删除状态目录(含 .done 归档)。
68
+ */
43
69
  async cleanup() {
44
70
  await rm(this.dir, { recursive: true, force: true }).catch(() => { });
71
+ // 同时清理可能的归档目录
72
+ await rm(this.dir + DONE_SUFFIX, { recursive: true, force: true }).catch(() => { });
45
73
  }
46
74
  }
@@ -1,7 +1,6 @@
1
1
  ---
2
2
  name: commit
3
3
  description: 当用户说"提交"、"commit"、"git commit"、"保存改动"、"帮我提交代码"、"生成提交信息"时使用。自动分析 git diff,生成规范的 Conventional Commit message,支持 feat/fix/docs/refactor/chore/style/test 等类型前缀。
4
- triggers: 提交代码、git commit、生成 commit message、版本管理、commit message
5
4
  ---
6
5
 
7
6
  # /commit —— Git 提交规范化
@@ -1,7 +1,6 @@
1
1
  ---
2
2
  name: config
3
3
  description: 配置向导技能。当用户输入 "/config" 且没有任何其他内容时触发。用于重新配置 LLM provider(BASE_URL / API_KEY / MODEL)。
4
- triggers: 配置、初始化、设置、重新配置、config
5
4
  ---
6
5
 
7
6
  # Config Skill
@@ -1,7 +1,6 @@
1
1
  ---
2
2
  name: diff
3
3
  description: 当用户问"改了什么"、"有什么变化"、"show me changes"、"看看改动"、"diff"、"差异"时使用。显示工作区或暂存区的变更内容,帮助用户理解当前代码状态。
4
- triggers: 代码差异、对比、变更审查、diff 分析、diff、代码对比
5
4
  ---
6
5
 
7
6
  # /diff —— 查看 Git 变更
@@ -1,7 +1,6 @@
1
1
  ---
2
2
  name: docx
3
3
  description: "Use this skill whenever the user wants to create, read, edit, or manipulate Word documents (.docx files). Triggers include: any mention of 'Word doc', 'word document', '.docx', or requests to produce professional documents with formatting like tables of contents, headings, page numbers, or letterheads. Also use when extracting or reorganizing content from .docx files, inserting or replacing images in documents, performing find-and-replace in Word files, working with tracked changes or comments, or converting content into a polished Word document. If the user asks for a 'report', 'memo', 'letter', 'template', or similar deliverable as a Word or .docx file, use this skill. Do NOT use for PDFs, spreadsheets, Google Docs, or general coding tasks unrelated to document generation."
4
- triggers: Word 文档、docx、文档编辑、报告撰写、合同、公文、批量文档处理、Word 文件
5
4
  license: Proprietary. LICENSE.txt has complete terms
6
5
  ---
7
6
 
@@ -1,7 +1,6 @@
1
1
  ---
2
2
  name: image-gen-openrouter
3
3
  description: 当用户说"生成图片"、"画一张图"、"配图"、"OpenRouter 生图"、"nano banana"、"image-gen-openrouter",或在 workflow 里要按提示词生成图片时使用。调用 OpenRouter 的 Chat Completions 接口(OpenAI 兼容 + image modalities),按提示词生成任意比例图片并落盘。
4
- triggers: 生成图片、画一张图、配图、OpenRouter 生图、image-gen-openrouter、文生图、AI画图
5
4
  ---
6
5
 
7
6
  # image-gen-openrouter —— OpenRouter 文生图
@@ -1,7 +1,6 @@
1
1
  ---
2
2
  name: pdf
3
3
  description: Use this skill whenever the user wants to do anything with PDF files. This includes reading or extracting text/tables from PDFs, combining or merging multiple PDFs into one, splitting PDFs apart, rotating pages, adding watermarks, creating new PDFs, filling PDF forms, encrypting/decrypting PDFs, extracting images, and OCR on scanned PDFs to make them searchable. If the user mentions a .pdf file or asks to produce one, use this skill.
4
- triggers: PDF 文件、PDF 转换、PDF 提取、PDF 生成、pdf 文件处理、PDF 操作
5
4
  license: Proprietary. LICENSE.txt has complete terms
6
5
  ---
7
6
 
@@ -1,7 +1,6 @@
1
1
  ---
2
2
  name: pptx
3
3
  description: "Use this skill any time a .pptx file is involved in any way — as input, output, or both. This includes: creating slide decks, pitch decks, or presentations; reading, parsing, or extracting text from any .pptx file (even if the extracted content will be used elsewhere, like in an email or summary); editing, modifying, or updating existing presentations; combining or splitting slide files; working with templates, layouts, speaker notes, or comments. Trigger whenever the user mentions \"deck,\" \"slides,\" \"presentation,\" or references a .pptx filename, regardless of what they plan to do with the content afterward. If a .pptx file needs to be opened, created, or touched, use this skill."
4
- triggers: PPT、幻灯片、演示文稿、deck、slides、presentation、pptx 文件、做汇报材料、演讲稿
5
4
  license: Proprietary. LICENSE.txt has complete terms
6
5
  ---
7
6
 
@@ -1,7 +1,6 @@
1
1
  ---
2
2
  name: simplify
3
3
  description: 当用户说"简化"、"审查"、"review"、"优化这段代码"、"太复杂了"、"能不能更简洁"、"代码质量"时使用。启动多维度代码审查,识别冗余、反模式、性能问题,给出具体修改建议。
4
- triggers: 简化代码、重构、代码精简、降低复杂度、simplify、代码简化
5
4
  ---
6
5
 
7
6
  # /simplify —— 代码审查与简化
@@ -1,7 +1,6 @@
1
1
  ---
2
2
  name: subtitle-srt
3
3
  description: 当用户说"生成字幕"、"做 SRT"、"字幕文件"、"subtitle-srt",或在 workflow 里要把分段文本 + 时长信息转成标准 SRT 字幕文件时使用。给定 cue 列表(每条带文本和起止时间或时长),生成符合 SRT 规范的字幕文件。
4
- triggers: 生成字幕、做 SRT、字幕文件、subtitle-srt、字幕生成
5
4
  ---
6
5
 
7
6
  # subtitle-srt —— SRT 字幕生成
@@ -1,7 +1,6 @@
1
1
  ---
2
2
  name: tts-zh
3
3
  description: 当用户说"中文配音"、"中文 TTS"、"语音合成"、"文字转语音"、"tts-zh",或在 workflow 里要把中文文本转成 mp3 时使用。优先用 MiniMax T2A API,失败时 fallback 到 edge-tts 离线方案,输出 mp3 到指定路径。
4
- triggers: 中文配音、中文 TTS、语音合成、文字转语音、tts-zh、配音、朗读
5
4
  ---
6
5
 
7
6
  # tts-zh —— 中文 TTS(minimax 主 + edge-tts 兜底)
@@ -1,7 +1,6 @@
1
1
  ---
2
2
  name: video-compose
3
3
  description: 当用户说"合成视频"、"做 mp4"、"图片转视频"、"ffmpeg 拼视频"、"video-compose",或在 workflow 里要把图片序列 + 可选音轨 + 可选字幕合成成 mp4 时使用。本 skill 是 ffmpeg 的 declarative 封装,所有参数(分辨率、字体、码率、转场)都可配置,不绑定特定视频格式。
4
- triggers: 合成视频、做 mp4、图片转视频、ffmpeg 拼视频、video-compose、视频制作
5
4
  ---
6
5
 
7
6
  # video-compose —— 通用 ffmpeg 视频合成
@@ -1,7 +1,6 @@
1
1
  ---
2
2
  name: web-artifacts-builder
3
3
  description: Suite of tools for creating elaborate, multi-component web artifacts using modern frontend web technologies (React, Tailwind CSS, shadcn/ui). Use for complex artifacts requiring state management, routing, or shadcn/ui components - not for simple single-file HTML/JSX artifacts. Trigger when user asks to build a web app, React component, interactive UI, landing page, dashboard, or any frontend project.
4
- triggers: Web 应用、React 组件、交互式 UI、落地页、仪表盘、前端项目、web-artifacts-builder、构建网页
5
4
  license: Complete terms in LICENSE.txt
6
5
  ---
7
6
 
@@ -1,7 +1,6 @@
1
1
  ---
2
2
  name: webapp-testing
3
3
  description: Toolkit for interacting with and testing local web applications using Playwright. Supports verifying frontend functionality, debugging UI behavior, capturing browser screenshots, and viewing browser logs.
4
- triggers: Web 测试、浏览器测试、Playwright、截图、UI 调试、webapp-testing、网页测试
5
4
  license: Complete terms in LICENSE.txt
6
5
  ---
7
6
 
@@ -1,7 +1,6 @@
1
1
  ---
2
2
  name: xlsx
3
3
  description: "Use this skill any time a spreadsheet file is the primary input or output. This means any task where the user wants to: open, read, edit, or fix an existing .xlsx, .xlsm, .csv, or .tsv file (e.g., adding columns, computing formulas, formatting, charting, cleaning messy data); create a new spreadsheet from scratch or from other data sources; or convert between tabular file formats. Trigger especially when the user references a spreadsheet file by name or path — even casually (like \"the xlsx in my downloads\") — and wants something done to it or produced from it. Also trigger for cleaning or restructuring messy tabular data files (malformed rows, misplaced headers, junk data) into proper spreadsheets. The deliverable must be a spreadsheet file. Do NOT trigger when the primary deliverable is a Word document, HTML report, standalone Python script, database pipeline, or Google Sheets API integration, even if tabular data is involved."
4
- triggers: Excel、表格、xlsx、数据处理、图表、批量数据、CSV 导入导出、电子表格
5
4
  license: Proprietary. LICENSE.txt has complete terms
6
5
  ---
7
6
 
@@ -0,0 +1,228 @@
1
+ /**
2
+ * ============================================================
3
+ * src/infra/verification.ts —— 共享验证基础设施
4
+ * ------------------------------------------------------------
5
+ * 从 plugins/ralph-wiggum/src/verificationGate.ts 提升为共享模块。
6
+ *
7
+ * 支持四种检查器(跨平台):
8
+ * - shell:<cmd> 跨平台 spawn(Windows 走 cmd /c, 其余 bash -c)
9
+ * - file_exists:<path> fs.existsSync
10
+ * - file_contains:<file>:<re> 正则匹配文件内容
11
+ * - test_count:<min> 跑 `bun test`,从 stdout 解析 "N pass"
12
+ *
13
+ * 消费者:
14
+ * - ralph-wiggum 插件(循环验证门)
15
+ * - workflow-runner 插件(verify 步骤类型)
16
+ * - 未来任何需要声明式验证的插件或模块
17
+ *
18
+ * 设计原则:
19
+ * - 纯工具函数,无全局状态,无副作用(除了 shell 子进程)
20
+ * - 不依赖任何插件代码,可被 src/ 和 plugins/ 同时导入
21
+ * - 工作目录隔离:所有相对路径以 getWorkingDir() 为锚
22
+ * ============================================================
23
+ */
24
+ import { existsSync, readFileSync } from 'node:fs';
25
+ import { spawn } from 'node:child_process';
26
+ import { getWorkingDir } from '../bootstrap/workingDir.js';
27
+ /** 从 "type:value" 字符串解析为 Check 对象。 */
28
+ function parseVerifyArg(arg) {
29
+ const colonIdx = arg.indexOf(':');
30
+ if (colonIdx < 0)
31
+ return null;
32
+ const type = arg.slice(0, colonIdx).trim().toLowerCase();
33
+ const value = arg.slice(colonIdx + 1).trim();
34
+ switch (type) {
35
+ case 'shell':
36
+ return { type: 'shell', command: value, timeout: 30_000 };
37
+ case 'file_exists':
38
+ return { type: 'file_exists', file: value };
39
+ case 'file_contains': {
40
+ const sep = value.indexOf(':');
41
+ if (sep < 0)
42
+ return null;
43
+ return {
44
+ type: 'file_contains',
45
+ file: value.slice(0, sep),
46
+ pattern: value.slice(sep + 1),
47
+ };
48
+ }
49
+ case 'test_count': {
50
+ const count = parseInt(value, 10);
51
+ if (isNaN(count) || count < 0)
52
+ return null;
53
+ return { type: 'test_count', minCount: count };
54
+ }
55
+ default:
56
+ return null;
57
+ }
58
+ }
59
+ /**
60
+ * 从命令行参数字符串中提取所有 --verify 项。
61
+ * 兼容两种调用方式:
62
+ * - ralph-loop 的 args 字符串:`"goal --verify 'shell:bun test'"`
63
+ * - workflow verify 步骤的 checks 数组元素:`"shell:bun test"`
64
+ */
65
+ export function parseVerifyArgs(args) {
66
+ const checks = [];
67
+ const regex = /--verify\s+("[^"]*"|\S+)/gi;
68
+ let match;
69
+ while ((match = regex.exec(args)) !== null) {
70
+ const raw = match[1].replace(/^"|"$/g, '');
71
+ const check = parseVerifyArg(raw);
72
+ if (check)
73
+ checks.push(check);
74
+ }
75
+ return checks;
76
+ }
77
+ /** 从单个 check 字符串解析(workflow verify 步骤用)。 */
78
+ export function parseCheckString(raw) {
79
+ return parseVerifyArg(raw);
80
+ }
81
+ function runShell(command, timeout) {
82
+ return new Promise((resolve) => {
83
+ const isWin = process.platform === 'win32';
84
+ const child = isWin
85
+ ? spawn('cmd', ['/c', command], { timeout, env: process.env })
86
+ : spawn('bash', ['-c', command], { timeout, env: process.env });
87
+ let stdout = '';
88
+ let stderr = '';
89
+ child.stdout.on('data', (d) => {
90
+ stdout += d.toString();
91
+ });
92
+ child.stderr.on('data', (d) => {
93
+ stderr += d.toString();
94
+ });
95
+ child.on('error', () => {
96
+ resolve({ exitCode: null, stdout, stderr, errored: true });
97
+ });
98
+ child.on('close', (code) => {
99
+ resolve({ exitCode: code, stdout, stderr, errored: false });
100
+ });
101
+ });
102
+ }
103
+ export async function verifyShell(command, timeout) {
104
+ const r = await runShell(command, timeout);
105
+ if (r.errored) {
106
+ return {
107
+ check: { type: 'shell', command },
108
+ passed: false,
109
+ output: `执行失败`,
110
+ };
111
+ }
112
+ return {
113
+ check: { type: 'shell', command },
114
+ passed: r.exitCode === 0,
115
+ output: r.stdout.trim() || r.stderr.trim() || `exit code ${r.exitCode}`,
116
+ };
117
+ }
118
+ export function verifyFileExists(file) {
119
+ // 相对路径以 workingDir 为锚
120
+ const resolved = file.startsWith('/') || /^[A-Za-z]:/.test(file)
121
+ ? file
122
+ : `${getWorkingDir()}/${file}`;
123
+ const exists = existsSync(resolved);
124
+ return {
125
+ check: { type: 'file_exists', file },
126
+ passed: exists,
127
+ output: exists ? '文件存在' : `文件不存在: ${resolved}`,
128
+ };
129
+ }
130
+ export function verifyFileContains(file, pattern) {
131
+ const resolved = file.startsWith('/') || /^[A-Za-z]:/.test(file)
132
+ ? file
133
+ : `${getWorkingDir()}/${file}`;
134
+ try {
135
+ const content = readFileSync(resolved, 'utf8');
136
+ const regex = new RegExp(pattern);
137
+ const matched = regex.test(content);
138
+ return {
139
+ check: { type: 'file_contains', file, pattern },
140
+ passed: matched,
141
+ output: matched ? `文件包含 "${pattern}"` : `文件不包含 "${pattern}"`,
142
+ };
143
+ }
144
+ catch {
145
+ return {
146
+ check: { type: 'file_contains', file, pattern },
147
+ passed: false,
148
+ output: `无法读取文件: ${resolved}`,
149
+ };
150
+ }
151
+ }
152
+ async function verifyTestCount(minCount) {
153
+ const r = await runShell(`bun test`, 60_000);
154
+ const combined = `${r.stdout}\n${r.stderr}`;
155
+ if (r.errored) {
156
+ return {
157
+ check: { type: 'test_count', minCount },
158
+ passed: false,
159
+ output: '无法执行 bun test',
160
+ };
161
+ }
162
+ const matches = [...combined.matchAll(/(\d+)\s+pass/gi)];
163
+ const count = matches.length > 0 ? parseInt(matches[matches.length - 1][1], 10) : 0;
164
+ const passed = count >= minCount;
165
+ return {
166
+ check: { type: 'test_count', minCount },
167
+ passed,
168
+ output: `${count} pass (需要 >=${minCount})`,
169
+ };
170
+ }
171
+ /**
172
+ * 执行一组检查,返回聚合结果。
173
+ * 全部通过 → passed=true;任一失败 → passed=false + summary 列出失败项。
174
+ */
175
+ export async function runVerification(checks) {
176
+ if (checks.length === 0) {
177
+ return { passed: true, details: [], summary: '无验证项' };
178
+ }
179
+ const results = [];
180
+ for (const check of checks) {
181
+ let result;
182
+ switch (check.type) {
183
+ case 'shell':
184
+ result = await verifyShell(check.command ?? '', check.timeout ?? 30_000);
185
+ break;
186
+ case 'file_exists':
187
+ result = verifyFileExists(check.file ?? '');
188
+ break;
189
+ case 'file_contains':
190
+ result = verifyFileContains(check.file ?? '', check.pattern ?? '');
191
+ break;
192
+ case 'test_count':
193
+ result = await verifyTestCount(check.minCount ?? 0);
194
+ break;
195
+ default:
196
+ result = {
197
+ check,
198
+ passed: false,
199
+ output: `未知验证类型: ${check.type}`,
200
+ };
201
+ }
202
+ results.push(result);
203
+ }
204
+ const allPassed = results.every((r) => r.passed);
205
+ const failedNames = results.filter((r) => !r.passed).map((r) => formatCheckName(r.check));
206
+ let summary;
207
+ if (allPassed) {
208
+ summary = `✅ 全部通过 (${results.length}/${results.length})`;
209
+ }
210
+ else {
211
+ summary = `❌ 验证未通过: ${failedNames.join(', ')}`;
212
+ }
213
+ return { passed: allPassed, details: results, summary };
214
+ }
215
+ function formatCheckName(check) {
216
+ switch (check.type) {
217
+ case 'shell':
218
+ return `shell(${(check.command ?? '').slice(0, 40)})`;
219
+ case 'file_exists':
220
+ return `exists(${check.file})`;
221
+ case 'file_contains':
222
+ return `contains(${check.file}:${check.pattern})`;
223
+ case 'test_count':
224
+ return `tests(>=${check.minCount})`;
225
+ default:
226
+ return check.type;
227
+ }
228
+ }
@@ -28,10 +28,27 @@ export async function* runWithPlugins(userInput, options) {
28
28
  });
29
29
  return;
30
30
  }
31
- await discoverPlugins();
31
+ const manifests = await discoverPlugins();
32
32
  const matched = resolveCommand(userInput);
33
33
  if (!matched) {
34
- // 未知 / 命令直接喂 LLM,让模型自然回复(等同于普通文本)
34
+ // 提取命令名用于错误提示
35
+ const cmdName = userInput.trimStart().split(/\s+/)[0]; // "/workflow" 或 "/plan"
36
+ // 有插件但未命中该命令 → 明确提示可用命令
37
+ if (manifests.length > 0) {
38
+ const cmdList = manifests.flatMap((m) => m.commands.map((c) => `/${c.name}`)).join('、');
39
+ yield {
40
+ type: 'error',
41
+ error: `未找到命令 "${cmdName}"。已安装的插件命令:${cmdList || '(无)'}。`,
42
+ };
43
+ }
44
+ else {
45
+ // 无任何插件 → 告知用户
46
+ yield {
47
+ type: 'error',
48
+ error: `当前未安装任何插件。命令 "${cmdName}" 无法执行。`,
49
+ };
50
+ }
51
+ // fallback 到普通 TAOR 对话,用户可继续操作
35
52
  yield* runQuery(userInput, {
36
53
  provider,
37
54
  history,
@@ -37,8 +37,8 @@ export function formatPluginHint(plugins) {
37
37
  const lines = [
38
38
  '# 可用插件(/命令)',
39
39
  '',
40
- '以下是当前已安装的插件。**注意:插件命令必须由用户主动通过 /xxx 输入触发**,你不要自行模拟或伪造插件执行。',
41
- '当用户消息以 / 开头且匹配以下某个命令时,表示用户要触发对应插件功能。你可以根据上下文建议用户使用某个插件,但不能代替用户执行 / 命令。',
40
+ '以下是当前已安装的插件。**插件只能由用户通过 /xxx 命令触发——你绝对不能代替用户执行、模拟或建议使用插件功能**。',
41
+ '只有当用户明确询问"有哪些命令/插件可用"时,才列出以下内容。其他时候不要提及插件。',
42
42
  '',
43
43
  ];
44
44
  for (const p of plugins) {
@@ -38,7 +38,11 @@ const CORE_IDENTITY = `# 思维与立场(核心:像专家一样独立思考
38
38
 
39
39
  # 行为约束
40
40
  - 没有用户许可不要执行破坏性操作(rm -rf / git reset --hard / git push --force 等)。
41
- - 如果用户的请求不清晰、或有多个合理方向需要取舍:优先用 AskUserQuestion 让用户拍板(带推荐项),而不是替他猜着干;只缺一点小信息时一句话澄清即可。
41
+ - **提问克制原则(重要!)**:只在以下**三种情况**才使用 AskUserQuestion 提问用户,其他情况一律自己判断、选推荐项继续:
42
+ 1. **信息不全且无法推断**:用户请求缺少关键信息(如"部署到哪个环境"),且无法从上下文/代码/文件中找到答案;
43
+ 2. **多方案各有真实优劣**:存在多个可行方案,每个都有明确 trade-off(不是你懒得选),且选择会影响后续方向;
44
+ 3. **不可逆破坏性操作前确认**:涉及数据丢失或不可回滚的操作(如删除数据库、覆盖生产配置)。
45
+ **不符合以上三条的,不要提问。** "有取舍就问"是错误的理解——大多数取舍你应该自己判断并选推荐项继续。只缺一点小信息时自己推断或用一句话默认假设即可。
42
46
  - 任务完成后用一两句话总结改了什么,不要长篇大论。
43
47
 
44
48
  # 修改 vs 讨论(动手边界,重要)
@@ -59,7 +63,21 @@ const CORE_IDENTITY = `# 思维与立场(核心:像专家一样独立思考
59
63
  1. **这是新任务还是上一任务的延续?**——"继续/接着做/再加一个/还有"=延续,保持当前任务上下文;"帮我做 X/这次做 Y/另外"=多半是新任务;"重新开始/不算了,做 X"=立即放下旧任务。
60
64
  2. **历史里哪些和当前问题相关?**——只把与当前问题直接相关的消息、文件、工具结果当活动上下文;无关的旧任务细节当背景,**不要拿旧任务的结论/文件/方案硬套当前问题**(典型翻车:上个任务在改 A,这次问的是 B,却惯性去翻 A)。
61
65
  3. **判不准边界就先确认**:一句话澄清"你是继续刚才的 X,还是开始新的?",别默默假设。
62
- - 当前用户消息 + 紧邻的最近几条消息,权重永远高于更早历史;越早、越不相关的内容参考价值越低。`;
66
+ - 当前用户消息 + 紧邻的最近几条消息,权重永远高于更早历史;越早、越不相关的内容参考价值越低。
67
+
68
+ # 上下文隔离(重要!工作流/命令执行完毕后的行为规则)
69
+ - /plan、/workflow 等命令执行完后**自动回到普通对话模式**——下一条用户输入默认是全新任务。
70
+ - **只有当用户明确提到延续意图时**("继续"、"接着做"、"按照计划执行"、"读一下 PLAN.md"、"修改刚才的结果"),才去关联之前的工作流产物(PLAN.md、生成的文件等)。
71
+ - 用户提出全新任务时(如"帮我写个函数"、"这个bug怎么修"、"review 下代码"),**不要主动 Read 之前的 PLAN.md 或引用上次工作流的结果**——那是上一次的产出,与当前任务无关。
72
+ - 当前目录可能存在之前 /plan 留下的 PLAN.md 文件:判断标准是**用户消息是否包含对之前产出的引用**?没有就当新任务处理。
73
+ - 如果你不确定是新任务还是延续,优先按**新任务**处理——宁可多问一句也不要用旧上下文污染新工作。
74
+
75
+ # 中断后调整(重要!)
76
+ - 用户可能在上一步 /plan 或 /workflow 执行中途按 ESC/Ctrl+C 中断,然后立即输入调整指令(如"把第3步改成xxx"、"换个方向重新计划")。
77
+ - **检测方法**:如果用户消息包含调整/修改/重做/换方向 等词 + 历史中有 /plan 或 /workflow 的痕迹 → 这是**中断后调整**,不是全新任务。
78
+ - **小调整处理**:先 Read ./PLAN.md 了解当前计划状态,然后直接在当前 TAOR 对话中完成修改(Edit 写入 PLAN.md 或用工具产出新文件)。不需要重新跑 /plan 工作流。
79
+ - **大改/重做处理**:如果用户要求完全重做或换方向,建议用户用 '/plan 新目标' 重新跑完整流程——这会覆盖旧的 PLAN.md 并从头开始。
80
+ - **不要混淆**:"中断后调整"和"全新任务"的关键区别是**用户是否提到了对之前产出的修改意图**。提到修改→读 PLAN.md 继续接着做;没提→当新任务。`;
63
81
  // ==================== Layer 1: 环境感知 ====================
64
82
  function buildEnvLayer(cwd) {
65
83
  const today = new Date().toISOString().slice(0, 10);
@@ -154,12 +172,18 @@ export async function assembleBasePrompt(options) {
154
172
  const skillHint = formatSkillHint(skills);
155
173
  if (skillHint)
156
174
  parts.push(skillHint);
157
- // Layer 4: 插件目录(实时扫描 plugins/)
175
+ // Layer 4: 插件目录(可选增量,异常时不阻塞基础内核)
158
176
  if (includePlugins) {
159
- const plugins = await getPluginMetaList();
160
- const pluginHint = formatPluginHint(plugins);
161
- if (pluginHint)
162
- parts.push(pluginHint);
177
+ try {
178
+ const plugins = await getPluginMetaList();
179
+ const pluginHint = formatPluginHint(plugins);
180
+ if (pluginHint)
181
+ parts.push(pluginHint);
182
+ }
183
+ catch {
184
+ // 插件扫描失败(目录损坏/权限错误/yaml语法错误等)不影响基础内核
185
+ // dlog('prompt', 'plugin scan failed, skipping Layer 4');
186
+ }
163
187
  }
164
188
  // Layer 5: 项目指令(可选)
165
189
  if (includeProjectInstructions) {
@@ -198,12 +222,17 @@ export async function assembleCapabilityHint(cwd) {
198
222
  parts.push(skillHint);
199
223
  hasContent = true;
200
224
  }
201
- // Layer 4: Plugin 目录
202
- const plugins = await getPluginMetaList();
203
- const pluginHint = formatPluginHint(plugins);
204
- if (pluginHint) {
205
- parts.push(pluginHint);
206
- hasContent = true;
225
+ // Layer 4: Plugin 目录(可选增量,异常时跳过)
226
+ try {
227
+ const plugins = await getPluginMetaList();
228
+ const pluginHint = formatPluginHint(plugins);
229
+ if (pluginHint) {
230
+ parts.push(pluginHint);
231
+ hasContent = true;
232
+ }
233
+ }
234
+ catch {
235
+ // 插件扫描失败不影响 skill hint
207
236
  }
208
237
  if (!hasContent)
209
238
  return '';
@@ -1,17 +1,16 @@
1
1
  /**
2
2
  * ============================================================
3
- * src/prompts/skillList.ts —— Skill 列表生成(积极使用版)
3
+ * src/prompts/skillList.ts —— Skill 列表生成
4
4
  * ------------------------------------------------------------
5
- * 设计原则(v3 重构):
5
+ * 设计原则:
6
6
  * 1. System Prompt 里只注入 frontmatter 元数据轻量列表
7
- * → 让 T 阶段的 LLM 知道"有哪些技能可用 + 何时触发 + 是什么类型"
7
+ * → 让 LLM 知道"有哪些技能可用 + 功能是什么"
8
8
  * → 不加载完整 SKILL.md(省 token,按需加载)
9
9
  * 2. LLM 决定使用某个 skill 时,用 Read 工具读取对应 SKILL.md
10
10
  * → 类似 lazy loading:知道存在 → 需要时才加载详情
11
- * 3. 格式对齐 Tools 的风格:每个 skill 固定 3 行信息
12
- * 4. 完全依赖 frontmatter 声明(name / description / type / triggers),
13
- * 不再做任何启发式推断。新增 skill 时必须在 frontmatter 显式声明
14
- * type 和 triggers 字段(缺省时给出占位提示)。
11
+ * 3. 格式简洁:每个 skill 固定 2-3 行信息
12
+ * 4. 仅依赖 frontmatter 的 name / description 字段,
13
+ * 不依赖 triggers(并非所有 skill 都声明触发词)。
15
14
  * ============================================================
16
15
  */
17
16
  import { readFile, readdir } from 'node:fs/promises';
@@ -33,14 +32,12 @@ function parseFrontmatter(content, sourceDir) {
33
32
  const nameMatch = frontmatter.match(/^name:\s*(.+)$/m);
34
33
  const descMatch = frontmatter.match(/^description:\s*(.+)$/m);
35
34
  const typeMatch = frontmatter.match(/^type:\s*(.+)$/m);
36
- const triggersMatch = frontmatter.match(/^triggers:\s*(.+)$/m);
37
35
  if (!nameMatch || !descMatch)
38
36
  return null;
39
37
  return {
40
38
  name: stripQuotes(nameMatch[1]),
41
39
  description: stripQuotes(descMatch[1]),
42
40
  type: typeMatch ? stripQuotes(typeMatch[1]) : undefined,
43
- triggers: triggersMatch ? stripQuotes(triggersMatch[1]) : undefined,
44
41
  sourceDir,
45
42
  };
46
43
  }
@@ -75,18 +72,18 @@ export async function getSkillList() {
75
72
  return skills;
76
73
  }
77
74
  /**
78
- * 生成积极的 skill 列表提示词。
75
+ * 生成 skill 列表提示词。
79
76
  *
80
- * 输出格式(每个 skill 固定 3 行):
81
- * ## /skill-name —— 一句话标题
82
- * - **触发时机**: 用户说 "xxx"、"yyy" 时使用
77
+ * 输出格式(每个 skill 固定 2-3 行):
78
+ * ## /skill-name
83
79
  * - **功能**: 一句话描述这个 skill 做什么
84
80
  * - **类型**: prompt / local / local-jsx
85
81
  *
86
82
  * 关键设计决策:
87
83
  * - 不加载 SKILL.md 全文(节省 ~1500+ tokens/skill)
88
- * - 只输出 name + description(每个 skill ~40 tokens)
89
- * - 明确告诉 LLM "决定用时再 Read SKILL.md"
84
+ * - 只输出 name + description(每个 skill ~30 tokens)
85
+ * - 不依赖 triggers 字段(并非所有 skill 都声明触发词)
86
+ * - LLM 根据 description 语义匹配决定是否使用
90
87
  */
91
88
  export function formatSkillHint(skills) {
92
89
  if (skills.length === 0) {
@@ -96,15 +93,13 @@ export function formatSkillHint(skills) {
96
93
  // ---- 标题段 ----
97
94
  lines.push('# 可用技能(/命令)');
98
95
  lines.push('');
99
- lines.push('以下是当前可用的技能(**双源**:用户项目目录 + minimal-agent 包目录,同名时项目优先)。**当用户意图匹配某个技能的触发时机时,你应该主动使用它**。');
96
+ lines.push('以下是当前可用的技能(**双源**:用户项目目录 + minimal-agent 包目录,同名时项目优先)。**当用户需求与某个技能的功能描述匹配时,你应该主动使用它**。');
100
97
  lines.push('在使用前,先用 Read 工具读取下方每个技能卡片里的 `**技能目录**` 路径 + `/SKILL.md` 了解完整流程。');
101
98
  lines.push('');
102
99
  // ---- 每个技能的卡片 ----
103
100
  for (const skill of skills) {
104
101
  const type = skill.type ?? 'prompt(展开为系统提示词,指导模型执行复杂任务)';
105
- const triggers = skill.triggers ?? '(详见 SKILL.md)';
106
102
  lines.push(`## ${skill.name}`);
107
- lines.push(`- **触发时机**: ${triggers}`);
108
103
  lines.push(`- **功能**: ${skill.description}`);
109
104
  lines.push(`- **类型**: ${type}`);
110
105
  lines.push(`- **技能目录**: \`${skill.sourceDir}\``);
@@ -114,8 +109,8 @@ export function formatSkillHint(skills) {
114
109
  lines.push('## 技能调用流程');
115
110
  lines.push('');
116
111
  lines.push('```');
117
- lines.push('1. 在 T 阶段判断用户意图是否匹配某个技能的触发时机');
118
- lines.push('2. 匹配 → 在 A 阶段用 Read 工具读取该技能卡片的"技能目录"/SKILL.md');
112
+ lines.push('1. 判断用户意图是否与某个技能的功能描述匹配');
113
+ lines.push('2. 匹配 → 用 Read 工具读取该技能卡片的"技能目录"/SKILL.md');
119
114
  lines.push('3. 按照 SKILL.md 的 Quick Reference 和流程执行');
120
115
  lines.push('4. 技能脚本(scripts/)与 SKILL.md 位于同一目录下,用绝对路径执行');
121
116
  lines.push('5. 如果技能类型是 prompt,将其内容作为额外上下文指导后续操作');
package/src/ui/App.js CHANGED
@@ -190,7 +190,7 @@ export function App({ provider, initialHistory }) {
190
190
  // 它必须是兄弟节点中第一个,否则 above-sibling 会跟 Static commit 一起被重打
191
191
  // 到 stdout(实测看到 banner 反复打印 N 次)。
192
192
  const bannerText = `minimal-agent · ${provider.name}/${provider.model} · /new 清空 · /compact 压缩 · /exit 退出 · Ctrl+C 中断`;
193
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(MessageList, { history: deferredHistory, compactGeneration: chat.compactGeneration, bannerText: bannerText, terminalRows: terminalRows }), _jsxs(Box, { flexDirection: "column", width: Math.max(1, terminalCols - 1), children: [_jsx(LiveArea, { progressStage: chat.progressStage, reasoningPreview: chat.reasoningPreview, streamingText: chat.streamingText, runningTools: chat.runningTools, pluginProgress: chat.pluginProgress, terminalCols: terminalCols, terminalRows: terminalRows }), chat.error && (_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: "red", children: ["\u26A0 ", chat.error] }) })), chat.interrupted && (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "yellow", children: "\u26A1 \u64CD\u4F5C\u88AB\u7528\u6237\u4E2D\u65AD\uFF0C\u7B49\u5F85\u65B0\u7684\u4EFB\u52A1\u8F93\u5165..." }) })), chat.pendingAsk ? (_jsx(AskQuestion, { questions: chat.pendingAsk, onAnswer: chat.answerAsk, onCancel: chat.abort })) : (
193
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(MessageList, { history: deferredHistory, compactGeneration: chat.compactGeneration, bannerText: bannerText, terminalRows: terminalRows }), _jsxs(Box, { flexDirection: "column", width: Math.max(1, terminalCols - 1), children: [_jsx(LiveArea, { progressStage: chat.progressStage, reasoningPreview: chat.reasoningPreview, streamingText: chat.streamingText, runningTools: chat.runningTools, pluginProgress: chat.pluginProgress, terminalCols: terminalCols, terminalRows: terminalRows }), chat.error && (_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: "red", children: ["\u26A0 ", chat.error] }) })), chat.interrupted && (_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: "yellow", children: "\u26A1 \u64CD\u4F5C\u88AB\u7528\u6237\u4E2D\u65AD\uFF0C\u7B49\u5F85\u65B0\u7684\u4EFB\u52A1\u8F93\u5165..." }), _jsx(Text, { dimColor: true, children: " \u8C03\u6574\u4E0A\u4E00\u6B21\u8BA1\u5212\uFF1A\u76F4\u63A5\u8BF4\u8C03\u6574\u5185\u5BB9 / \u91CD\u65B0\u8DD1 /plan \u76EE\u6807 / \u65B0\u4EFB\u52A1\u76F4\u63A5\u8BF4" })] })), chat.pendingAsk ? (_jsx(AskQuestion, { questions: chat.pendingAsk, onAnswer: chat.answerAsk, onCancel: chat.abort })) : (
194
194
  /* handleSubmit 包装做 slash 命令拦截,非命令透传 chat.submit */
195
195
  _jsx(InputBox, { onSubmit: handleSubmit, disabled: chat.isLoading, onAbort: chat.abort, onClear: handleClear, onCompact: chat.compactNow })), _jsx(StatusLine, { provider: provider, history: chat.history, pluginProgress: chat.pluginProgress })] })] }));
196
196
  }
@@ -1,29 +1,22 @@
1
1
  # ============================================================
2
- # workflows/plan.yaml —— 复刻 Claude Code「plan」流程(Clarify→Analyze→Verify→Synthesize→Critic→Gate)
2
+ # workflows/plan.yaml —— 零交互计划流程(Analyze→Verify→Synthesize→Critic→Gate→Execute→Summary)
3
3
  # ------------------------------------------------------------
4
- # 设计目标:① 跨基座模型稳(弱模型也能跑完,不依赖强 agent 多轮工具循环);
5
- # ② 提问只在「开头一次性」发生,之后全自主无打断,直到产出可提交的 PLAN.md。
4
+ # 设计目标:① 跨模型稳(弱模型也能跑完);② 零用户交互——全程自主;
5
+ # ③ 计划详细程度从 goal 自动推断,不提问。
6
6
  # 全用现有 StepKind(不改引擎):
7
- # Clarify : clarify_depth(AskUserQuestion 开头一次性问深度——全流程唯一的人机交互点)
8
7
  # Analyze : analyze(agent 只读探索) → extract_findings(llm+schema 固化成可验证 claims)
9
8
  # Verify : verify_findings(type:vote 红队对每条 finding 反向提问证伪,存活的才进下游)
10
9
  # Synthesize: draft_plan(llm 纯文本出计划正文) → read_existing_plan + write_plan(tool 确定性落盘)
11
10
  # Critic : critic_plan(skill:adversarial-review 反向提问审稿,追加进 PLAN.md)
12
11
  # Gate : verify(assert 守门确认 PLAN.md 生成)
12
+ # Execute : execute_plan(agent 按计划执行,产出最终交付物)
13
+ # Summary : exit_summary(llm 退出摘要)
13
14
  # ------------------------------------------------------------
14
- # 模型兼容性要点(为什么这样切分——上一版在火山 GLM-5.1 上两处翻车的根治):
15
- # 1. 写盘用 "llm 出正文 + tool:Write 落盘" 而非 agent。原 agent+Write 写法在弱模型上
16
- # 会反复调 Write 触发 miniReAct 重复检测 halt;真因还叠加 Write 的"覆盖前须先 Read"
17
- # 守卫——PLAN.md 已存在时首个 Write 被拒、模型重试同参 Write→3 次→halt。
18
- # 故 write 前置一个 tool:Read(onError:continue,首跑文件不存在则略过)满足守卫。
19
- # 2. 结构化输出(extract_findings / vote)由 client.ts 自动退化:provider 不支持
20
- # response_format.json_schema(火山 GLM)时去掉它、靠 prompt 内嵌 schema + 容错解析。
21
- # 3. 仍是 agent 的只剩 analyze(只读探索)与 critic(审稿,onError:continue 兜底),
22
- # 它们在弱模型上最坏只是 halt/跳过,不破坏已产出的计划。
23
- # ------------------------------------------------------------
24
- # 提问节奏:唯一提问点 clarify_depth 放最前——问完之后全程自主无打断。任务专属、
25
- # 本工作流没问的点(受众/时长/范围等)由 draft 自做合理默认假设并在正文开头列出,
26
- # 绝不中途打断(契合"明确有优劣自动判断、只有真取舍才问、开头问完即自主")。
15
+ # 提问策略:本工作流**零交互**。计划深度从用户 goal 自动推断:
16
+ # - 目标含"概览/快速/简单/简短" → 快速概览
17
+ # - 目标含"详细/完整/分步/彻底/带风险" → 带风险回滚评估
18
+ # - 其他 → 详细分步(默认推荐)
19
+ # 不询问用户选择深度——自行判断。
27
20
  # ------------------------------------------------------------
28
21
  # 注:critic 用 skill: 节点 dogfood 仓库自带的 adversarial-review skill。
29
22
  # skill 双源查找(cwd 优先、packageRoot fallback);异地 cwd 缺该 skill 时
@@ -31,10 +24,10 @@
31
24
  # ============================================================
32
25
  name: plan
33
26
  description: |
34
- 受工作流指导 + 红队对抗验证 + 反向提问审稿的动态计划流程(跨模型稳、开头一次性提问后全自主)。
35
- Clarify(开头问深度) → Analyze(探索+固化) → Verify(vote 红队证伪) → Synthesize(出正文+落盘)
36
- → Critic(反向提问审稿追加) → Gate(assert 守门)。minimal-agent 的旗舰演示。
37
- version: "0.3"
27
+ 零交互计划流程(跨模型稳、全程自主、自动推断深度)。
28
+ Analyze(探索+固化) → Verify(vote 红队证伪) → Synthesize(出正文+落盘)
29
+ → Critic(反向提问审稿追加) → Gate(assert 守门) → Execute(按计划执行) → Summary(退出摘要)。
30
+ version: "0.4"
38
31
 
39
32
  inputs:
40
33
  - name: goal
@@ -43,26 +36,6 @@ inputs:
43
36
  description: "要做的事 / 目标"
44
37
 
45
38
  steps:
46
- # ===================== Clarify(开头唯一提问点)=====================
47
- # [Clarify] 一次性问"计划深度"(这是真有取舍、无统一最优的点;第一项=推荐,非交互自动选它)。
48
- # 放最前:问完之后全程自主。其它任务专属细节(受众/时长/范围)由 draft 自做合理默认并列出,
49
- # 不再中途打断——契合"明确有优劣自动判断、只有真取舍才问"。
50
- - id: clarify_depth
51
- tool: AskUserQuestion
52
- args:
53
- questions:
54
- - header: "计划深度"
55
- question: "希望这份实现计划做到多详细?"
56
- options:
57
- - label: "详细分步 (推荐)"
58
- description: "拆成可执行的有序步骤,每步说清动作、涉及文件与验收点"
59
- - label: "快速概览"
60
- description: "只给高层方向与关键决策,不展开到每一步"
61
- - label: "带风险/回滚评估"
62
- description: "在详细分步之外,额外标注每步风险与回滚方案"
63
- capture:
64
- content: depth_answer
65
-
66
39
  # ===================== Analyze =====================
67
40
  # [Analyze-1] 只读探索:agent 自主用 Read/Grep/Glob 摸清现状(不写盘、不副作用)
68
41
  - id: analyze
@@ -128,17 +101,18 @@ steps:
128
101
  —— 仍未决的问题 ——
129
102
  ${open_questions}
130
103
 
131
- —— 用户选择的计划深度 ——
132
- ${depth_answer}
104
+ ## 计划详细程度(自动推断,不询问用户)
105
+ 根据用户目标「${inputs.goal}」中的关键词自动判断:
106
+ - 如果目标包含"概览"、"快速"、"简单"、"简短"、"简要"、"大致"等词 → 输出快速概览(高层方向与关键决策,不展开到每一步)
107
+ - 如果目标包含"详细"、"完整"、"分步"、"彻底"、"风险"、"回滚"、"全面"等词 → 输出带风险/回滚评估的详细计划
108
+ - 其他情况 → 默认输出**详细分步**计划(可执行的有序步骤,每步说清动作、涉及文件与验收点)
133
109
 
134
110
  要求:
135
- - 详细程度遵循用户选择的深度;
111
+ - 按上述推断的详细程度输出;
136
112
  - "存活结论"为空时不要硬编:按目标常识给出合理的计划骨架;
137
113
  - 凡需按需求拍板、但本工作流未询问的点(如受众/时长/范围),**自行做合理默认假设并继续**,
138
114
  在正文开头用「## 假设(如需调整请告知)」一节列出假设,**绝不中断提问**。
139
115
  capture:
140
- # llm 步(无 output_schema)的 raw 只有 {text,result},没有 content(那是 tool 步的字段)。
141
- # 用错字段会 capture 到 undefined → 下游 Write 写 0 字节(已被端到端测试抓到)。
142
116
  text: plan_text
143
117
 
144
118
  # [Synthesize-2] 满足 Write 的"覆盖前须先 Read"守卫:PLAN.md 已存在则先 Read;
@@ -149,7 +123,7 @@ steps:
149
123
  file_path: ./PLAN.md
150
124
  onError: continue
151
125
 
152
- # [Synthesize-3] 落盘:tool:Write 确定性写入(不经 agent 循环,弱模型也稳)
126
+ # [Synthesis-3] 落盘:tool:Write 确定性写入(不经 agent 循环,弱模型也稳)
153
127
  - id: write_plan
154
128
  tool: Write
155
129
  args:
@@ -174,3 +148,41 @@ steps:
174
148
  type: assert
175
149
  condition: 'fileExists("./PLAN.md")'
176
150
  onFail: "计划文件 ./PLAN.md 未生成,检查 draft_plan/write_plan 步是否成功"
151
+
152
+ # ===================== Execute(按计划执行)=====================
153
+ # [Execute] 读取已落盘的 PLAN.md,agent 自主按计划步骤逐一执行,
154
+ # 直到完成所有任务并产出最终交付物。
155
+ # onError: recover —— 执行中撞上预料之外的错误就就地降级为 agent 自适应恢复。
156
+ - id: execute_plan
157
+ agent: |
158
+ 下面是一份已经过完整工作流验证的实现计划(./PLAN.md)。
159
+ 请先 Read ./PLAN.md 了解完整计划内容和步骤。
160
+ 然后严格按照计划的步骤逐一执行,直到完成所有任务并产出最终交付物。
161
+ 执行过程中:
162
+ 1. 如果计划中提到使用某个 skill(如 pptx/pdf/docx),先用 Read 读取对应 skills/ 目录下的 SKILL.md 了解用法
163
+ 2. 每完成一个主要步骤后简要汇报进度
164
+ 3. 如果遇到计划中未预见的情况,自行判断合理方案并继续(不要中断等待)
165
+ 4. 全部完成后用一两句话总结产出物位置和内容
166
+ allowed_tools: [Read, Write, Edit, Bash, Grep, Glob, WebSearch, WebFetch]
167
+ maxTurns: 40
168
+ onError: recover
169
+ capture:
170
+ content: execution_result
171
+
172
+ # ===================== Exit Summary(退出摘要)=====================
173
+ # [Exit Summary] 用 llm 产出一句话摘要,让用户知道"做了什么、产物在哪、如何继续"。
174
+ # plan 工作流到此彻底结束,之后回到正常 TUI 对话模式。
175
+ - id: exit_summary
176
+ llm: |
177
+ 用户的目标是「${inputs.goal}」。
178
+ 计划已制定(PLAN.md)并执行完毕。
179
+ 执行结果:${execution_result}
180
+
181
+ 请用简洁中文写一段最终摘要(3-5句话),包含:
182
+ 1. 完成了什么
183
+ 2. 最终产物路径(文件名和目录)
184
+ 3. 如有后续建议也简短提及
185
+
186
+ 直接输出摘要正文,不要寒暄。
187
+ capture:
188
+ text: summary_text