minimal-agent 0.2.0 → 0.3.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.
Files changed (108) hide show
  1. package/README.md +54 -72
  2. package/package.json +18 -13
  3. package/plugins/ralph-wiggum/plugin.js +205 -0
  4. package/plugins/ralph-wiggum/src/goalState.js +260 -0
  5. package/plugins/ralph-wiggum/src/{sentinels.ts → sentinels.js} +4 -7
  6. package/plugins/ralph-wiggum/src/stopHookRunner.js +104 -0
  7. package/plugins/ralph-wiggum/src/verificationGate.js +202 -0
  8. package/plugins/workflow-runner/commands/workflow.md +13 -3
  9. package/plugins/workflow-runner/{plugin.ts → plugin.js} +20 -26
  10. package/plugins/workflow-runner/src/expressions.js +369 -0
  11. package/plugins/workflow-runner/src/index.js +216 -0
  12. package/plugins/workflow-runner/src/loader.js +183 -0
  13. package/plugins/workflow-runner/src/runner.js +290 -0
  14. package/plugins/workflow-runner/src/stepExecutors/assert.js +28 -0
  15. package/plugins/workflow-runner/src/stepExecutors/llm.js +44 -0
  16. package/plugins/workflow-runner/src/stepExecutors/skill.js +103 -0
  17. package/plugins/workflow-runner/src/stepExecutors/{tool.ts → tool.js} +19 -25
  18. package/plugins/workflow-runner/src/types.js +59 -0
  19. package/plugins/workflow-runner/src/{workflowState.ts → workflowState.js} +21 -40
  20. package/src/bootstrap/cwdArg.js +22 -0
  21. package/src/bootstrap/workingDir.js +31 -0
  22. package/src/cli/configWizard.js +272 -0
  23. package/src/cli/print.js +197 -0
  24. package/src/config/configFile.js +78 -0
  25. package/src/config.js +118 -0
  26. package/src/context/compact.js +357 -0
  27. package/src/context/microCompactLite.js +151 -0
  28. package/src/context/persistContext.js +109 -0
  29. package/src/context/reactiveCompact.js +121 -0
  30. package/src/context/sessionPath.js +58 -0
  31. package/src/context/snipCompact.js +112 -0
  32. package/src/context/tokenCounter.js +66 -0
  33. package/src/llm/client.js +182 -0
  34. package/src/loop.js +230 -0
  35. package/src/main.js +116 -0
  36. package/src/plugin-sdk.js +24 -0
  37. package/src/plugins/commandRouter.js +169 -0
  38. package/src/plugins/hookEngine.js +258 -0
  39. package/src/plugins/pluginApi.js +23 -0
  40. package/src/plugins/pluginLoader.js +71 -0
  41. package/src/plugins/pluginRunner.js +65 -0
  42. package/src/plugins/transcript.js +171 -0
  43. package/src/prompts/projectInstructions.js +48 -0
  44. package/src/prompts/skillList.js +126 -0
  45. package/src/prompts/system.js +155 -0
  46. package/src/session/runTurn.js +41 -0
  47. package/src/session/sessionState.js +19 -0
  48. package/src/tools/bash/bash.js +352 -0
  49. package/src/tools/bash/semantics.js +85 -0
  50. package/src/tools/bash/warnings.js +98 -0
  51. package/src/tools/edit/edit.js +253 -0
  52. package/src/tools/edit/multi-edit.js +155 -0
  53. package/src/tools/glob/glob.js +97 -0
  54. package/src/tools/grep/grep.js +185 -0
  55. package/src/tools/grep/rgPath.js +173 -0
  56. package/src/tools/index.js +94 -0
  57. package/src/tools/read/read.js +209 -0
  58. package/src/tools/shared/fileState.js +61 -0
  59. package/src/tools/shared/fileUtils.js +281 -0
  60. package/src/tools/shared/schemas.js +16 -0
  61. package/src/tools/types.js +21 -0
  62. package/src/tools/webbrowser/browser.js +55 -0
  63. package/src/tools/webbrowser/webbrowser.js +194 -0
  64. package/src/tools/webfetch/preapproved.js +267 -0
  65. package/src/tools/webfetch/webfetch.js +317 -0
  66. package/src/tools/websearch/websearch.js +161 -0
  67. package/src/tools/write/write.js +125 -0
  68. package/src/types/turndown.d.ts +23 -0
  69. package/src/types.js +16 -0
  70. package/src/ui/App.js +37 -0
  71. package/src/ui/InputBox.js +240 -0
  72. package/src/ui/MessageList.js +28 -0
  73. package/src/ui/Root.js +70 -0
  74. package/src/ui/StatusLine.js +41 -0
  75. package/src/ui/ToolStatus.js +11 -0
  76. package/src/ui/hooks/useChat.js +234 -0
  77. package/src/ui/hooks/usePasteHandler.js +137 -0
  78. package/src/ui/hooks/useTextBuffer.js +55 -0
  79. package/src/ui/hooks/useTokenUsage.js +30 -0
  80. package/src/ui/textBuffer.js +217 -0
  81. package/src/utils/packageRoot.js +37 -0
  82. package/src/utils/resourcePaths.js +49 -0
  83. package/src/utils/zodToJson.js +29 -0
  84. package/dist/main.js +0 -5315
  85. package/plugins/ralph-wiggum/plugin.ts +0 -275
  86. package/plugins/ralph-wiggum/scripts/setup-ralph-loop.sh +0 -203
  87. package/plugins/ralph-wiggum/src/goalState.ts +0 -310
  88. package/plugins/ralph-wiggum/src/stopHookRunner.ts +0 -136
  89. package/plugins/ralph-wiggum/src/verificationGate.ts +0 -252
  90. package/plugins/ralph-wiggum/test/goalState.test.ts +0 -410
  91. package/plugins/ralph-wiggum/test/verificationGate.test.ts +0 -122
  92. package/plugins/workflow-runner/src/expressions.ts +0 -371
  93. package/plugins/workflow-runner/src/index.ts +0 -194
  94. package/plugins/workflow-runner/src/loader.ts +0 -193
  95. package/plugins/workflow-runner/src/runner.ts +0 -313
  96. package/plugins/workflow-runner/src/stepExecutors/assert.ts +0 -30
  97. package/plugins/workflow-runner/src/stepExecutors/llm.ts +0 -54
  98. package/plugins/workflow-runner/src/stepExecutors/skill.ts +0 -115
  99. package/plugins/workflow-runner/src/types.ts +0 -183
  100. package/plugins/workflow-runner/test/cli.e2e.test.ts +0 -114
  101. package/plugins/workflow-runner/test/e2e.test.ts +0 -268
  102. package/plugins/workflow-runner/test/expressions.test.ts +0 -140
  103. package/plugins/workflow-runner/test/fixtures/cli-e2e.yaml +0 -27
  104. package/plugins/workflow-runner/test/fixtures/hello-workflow.yaml +0 -49
  105. package/plugins/workflow-runner/test/graceful.test.ts +0 -139
  106. package/plugins/workflow-runner/test/loader.test.ts +0 -216
  107. package/plugins/workflow-runner/test/pluginRunner.isolation.test.ts +0 -230
  108. package/plugins/workflow-runner/test/runner.test.ts +0 -511
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  一个用于**学习和教学**的 Agent 项目,从 [kakadeai](../) 主仓库的 Claude Code 源码中抽取并简化而来,把"一个能调工具、能自动压缩上下文、能跑插件、能可视化编排 workflow 的 agent"在合理规模的 TypeScript 里讲清楚。
7
7
 
8
- > 项目由**两个独立的 npm 包**组成:`minimal-agent`(主体 ReAct CLI)+ `workflow-ui`(可视化 workflow 编辑器)。两边只通过 `workflows/*.yaml` 文件耦合,零运行时依赖。
8
+ > 项目由**两个独立的 npm 包**组成:`minimal-agent`(主体 ReAct CLI)+ `minimal-workflw`(可视化 workflow 编辑器)。两边只通过 `workflows/*.yaml` 文件耦合,零运行时依赖。
9
9
 
10
10
  ---
11
11
 
@@ -30,7 +30,7 @@
30
30
  ↓ 读写 workflows/*.yaml
31
31
 
32
32
  ┌──────────────────────────────────┴───────────────────────────────┐
33
- workflow-ui (独立编辑器包, editor/) │
33
+ minimal-workflw (独立编辑器包, editor/) │
34
34
  │ │
35
35
  │ Bun + Vite + React + React Flow │
36
36
  │ ┌────────┐ ┌────────┐ ┌────────┐ │
@@ -54,13 +54,15 @@
54
54
  | 🧰 Skills 系统 (23) | 扫描 `skills/<name>/SKILL.md` frontmatter,按需懒加载完整 prompt |
55
55
  | 🔌 插件系统 | `plugins/<id>/` drop-in 目录,声明式命令 / 富插件双契约;自带 ralph-loop + workflow-runner |
56
56
  | 🎬 Workflow 引擎 | YAML DAG,7 种 step 类型(tool / llm / skill / assert / pause / branch / loop) |
57
- | 🖼️ 可视化编辑器 | 独立 `workflow-ui` 包,拖拽组装 workflow,节点面板从 `src/tools/` + `skills/` 自动派生 |
57
+ | 🖼️ 可视化编辑器 | 独立 `minimal-workflw` 包,拖拽组装 workflow,节点面板从 `src/tools/` + `skills/` 自动派生 |
58
58
 
59
59
  ---
60
60
 
61
+ > ⚠️ **0.2.0 已发布到 npm 但插件系统不可用**(dynamic import .ts 在 node_modules 下被 Node 拒绝)。请使用 **0.3.0+**:源码 import 统一 `.js` 后缀 + tsc 原地编译,Bun(dev 模式)和 Node(install 模式)共用一份目录布局。
62
+
61
63
  ## 系统要求
62
64
 
63
- - **Bun ≥ 1.1**(推荐,主运行时);Node.js ≥ 20 用户走 `npm i -g minimal-agent` 安装预构建版
65
+ - **Node.js ≥ 20**(npm install 后跑 bin 的运行时)或 **Bun ≥ 1.1**(推荐,dev 模式必需)
64
66
  - 一个 OpenAI 兼容的 LLM API(MiniMax / OpenAI / DeepSeek / Kimi / 自建 vLLM 均可)
65
67
  - *可选*:[Tavily](https://tavily.com) API key(开启 WebSearch)
66
68
  - *可选*:[OpenRouter](https://openrouter.ai) API key(用 `image-gen-openrouter` skill 生成图片)
@@ -80,14 +82,16 @@
80
82
  npm install -g minimal-agent
81
83
 
82
84
  # 2. (可选)装可视化 workflow 编辑器
83
- npm install -g workflow-ui
85
+ # ⚠ 当前 0.1 版编辑器 bin 直接吃 .ts,运行需要 Bun ≥1.1 在 PATH 上
86
+ # (主包 minimal-agent 在纯 Node 下完全可用,不依赖 Bun)
87
+ npm install -g minimal-workflw # 或:bun install -g minimal-workflw
84
88
 
85
89
  # 3. 跑起来
86
90
  minimal-agent # 启 TUI;首次会弹配置向导
87
- workflow-ui ~/my-agent-project # 在指定项目目录里启编辑器
91
+ minimal-workflw ~/my-agent-project # 在指定项目目录里启编辑器
88
92
  ```
89
93
 
90
- 两个包**独立发版、独立安装**。只想要 ReAct CLI 就装 `minimal-agent`;想拖拽编排 workflow 再加 `workflow-ui`。两者通过 `<project>/workflows/*.yaml` 文件耦合,没有进程间通信。
94
+ 两个包**独立发版、独立安装**。只想要 ReAct CLI 就装 `minimal-agent`;想拖拽编排 workflow 再加 `minimal-workflw`。两者通过 `<project>/workflows/*.yaml` 文件耦合,没有进程间通信。
91
95
 
92
96
  ### B. 从源码运行(贡献者 / 想读源码)
93
97
 
@@ -254,9 +258,13 @@ YAML DAG workflow 执行器,支持 7 种 step:
254
258
 
255
259
  ```bash
256
260
  /workflow youtube-shorts --input topic="猫咪赛博朋克城市探险"
261
+ /workflow youtube-shorts "猫咪赛博朋克城市探险" # 位置参数:按 inputs 声明顺序映射,.yaml 后缀可省
257
262
  /workflows # 列出所有 workflow
258
263
  ```
259
264
 
265
+ > ⚠ **位置参数依赖 yaml 文件 `inputs:` 数组的声明顺序**。重排 inputs 顺序会破坏所有现有 CLI 位置参数调用(位置参数会映射到错误的 input)。
266
+ > 多 input workflow 推荐使用 `--input key=value` 显式 KV 形式,更稳。
267
+
260
268
  ### 写自己的插件
261
269
 
262
270
  参考 `plugins/HOW-TO-WRITE-A-PLUGIN.md`。两种契约任选其一即可生效:
@@ -268,7 +276,7 @@ YAML DAG workflow 执行器,支持 7 种 step:
268
276
 
269
277
  ---
270
278
 
271
- ## workflow-ui 可视化编辑器(editor/ 子项目)
279
+ ## minimal-workflw 可视化编辑器(editor/ 子项目)
272
280
 
273
281
  独立的 npm 包,运行时与 `minimal-agent` **零耦合**,仅通过 `workflows/*.yaml` + `.editor-cache/manifest.json` 两个文件交换数据。
274
282
 
@@ -276,9 +284,9 @@ YAML DAG workflow 执行器,支持 7 种 step:
276
284
 
277
285
  ```bash
278
286
  # 已全局安装时
279
- workflow-ui # 当前 cwd 当 minimal-agent 项目根
280
- workflow-ui ~/my-project # 指定项目根
281
- workflow-ui --refresh-manifest # 强制刷新工具/skill 清单
287
+ minimal-workflw # 当前 cwd 当 minimal-agent 项目根
288
+ minimal-workflw ~/my-project # 指定项目根
289
+ minimal-workflw --refresh-manifest # 强制刷新工具/skill 清单
282
290
 
283
291
  # 从源码跑(在 editor/ 目录下)
284
292
  bun scripts/cli.ts --project .. # 一键起后端 + Vite
@@ -289,7 +297,7 @@ bun run server # 只起 Bun.serve() 后端
289
297
  > **为什么是 `bun scripts/cli.ts` 而不是 `bun run dev`?**
290
298
  > - `bun run dev` 只起 Vite 前端,UI 能渲染但**没有后端**,文件列表 / 读写 / manifest 端点全是 404
291
299
  > - `bun scripts/cli.ts` 是 bin 入口,**同时**起 `Bun.serve()` 文件 IO 后端 + Vite 前端,完整可用
292
- > - npm 发布后用户执行的 `workflow-ui` 命令就是这个 cli.ts
300
+ > - npm 发布后用户执行的 `minimal-workflw` 命令就是这个 cli.ts
293
301
 
294
302
  ### 启动序列(编辑器 CLI 内部)
295
303
 
@@ -335,7 +343,7 @@ export OPENROUTER_API_KEY=sk-or-...
335
343
  bun run src/main.tsx -p "/workflow youtube-shorts --input topic='猫咪赛博朋克城市探险'"
336
344
  ```
337
345
 
338
- 整条流水线**零代码改动** —— 4 个 skill 都是 markdown,1 个 workflow 是 yaml。在 `workflow-ui` 里打开 `youtube-shorts.yaml`,可视化拖拽改 prompt、换 skill、加节点。
346
+ 整条流水线**零代码改动** —— 4 个 skill 都是 markdown,1 个 workflow 是 yaml。在 `minimal-workflw` 里打开 `youtube-shorts.yaml`,可视化拖拽改 prompt、换 skill、加节点。
339
347
 
340
348
  ---
341
349
 
@@ -364,8 +372,8 @@ minimal-agent/
364
372
  │ ├── book-review-short.yaml
365
373
  │ ├── e2e-write-greet.yaml
366
374
  │ └── youtube-shorts.yaml
367
- ├── editor/ # workflow-ui 独立子包
368
- │ ├── package.json # name: workflow-ui, bin: workflow-ui
375
+ ├── editor/ # minimal-workflw 独立子包
376
+ │ ├── package.json # name: minimal-workflw, bin: minimal-workflw
369
377
  │ ├── scripts/
370
378
  │ │ ├── cli.ts # bin 入口(一键起后端+前端)
371
379
  │ │ └── server.ts # Bun.serve() 文件 IO + manifest 端点
@@ -387,9 +395,22 @@ minimal-agent/
387
395
  | 包 | 路径 | 命令 | 职责 |
388
396
  |---|---|---|---|
389
397
  | `minimal-agent` | 仓库根 | `minimal-agent` | ReAct CLI + 工具 + 插件 + skills |
390
- | `workflow-ui` | `editor/` | `workflow-ui` | 可视化 workflow 编辑器 |
398
+ | `minimal-workflw` | `editor/` | `minimal-workflw` | 可视化 workflow 编辑器 |
399
+
400
+ > **为什么不打成一个包**?90% 的 ReAct CLI 用户不需要编辑器,编辑器的依赖(React / Vite / @xyflow/react / Monaco 等)有几十 MB,不应该污染主包。两边解耦后用户按需取用,通过 `<project>/workflows/*.yaml` 文件交流,无进程间通信。
401
+
402
+ ### 模块解析模式(dev vs install 通用范式)
403
+
404
+ 主包采用**标准 NodeNext + tsc 原地编译**模式,解决了 npm 包"dev 跑 .ts,install 跑 .js"的通用目录差异问题:
405
+
406
+ - 源码(仓库 / dev)里 import 统一写 `.js` 后缀:`import { x } from './foo.js'`
407
+ - dev 模式跑 `bun run dev`:Bun 把 `'./foo.js'` 透明解析到 `./foo.ts` 源文件,**无需任何构建**
408
+ - publish 前 `tsc --build`:原地输出 `./foo.js`(与 .ts 兄弟位置,路径布局完全一致)
409
+ - `.npmignore` 排除 `*.ts/*.tsx`,tarball 里**只发 .js**
410
+ - install 后 Node 直接吃 .js,Bun 也吃 .js(行为统一)
411
+ - 源码 `plugin.ts` 里 `import '../../src/plugin-sdk.js'` 在 dev / install 两种模式下都指向同一份模块(dev=plugin-sdk.ts,install=plugin-sdk.js),**模块身份天然 singleton**
391
412
 
392
- > **为什么不打成一个包**?因为 90% 的 ReAct CLI 用户不需要编辑器,编辑器的依赖(React / Vite / @xyflow/react / Monaco 等)有几十 MB,不应该污染主包。两边解耦后用户按需取用。
413
+ 这是 `execa` / `p-queue` / `ink` 等纯 ESM TypeScript 库的通用做法。
393
414
 
394
415
  ### 用户安装(终端)
395
416
 
@@ -399,23 +420,19 @@ npm install -g minimal-agent
399
420
  minimal-agent # 启 ReAct TUI
400
421
 
401
422
  # 加装编辑器
402
- npm install -g workflow-ui
403
- workflow-ui /path/to/your-project # 启编辑器
423
+ npm install -g minimal-workflw
424
+ minimal-workflw /path/to/your-project # 启编辑器
404
425
  ```
405
426
 
406
- > npm 的 `bin` 字段支持一个包注册多个命令;但本项目选择**两个独立包**而不是"一个包两个 bin",是为了**依赖隔离**(编辑器的 React 依赖不该跟着 ReAct CLI 一起装到所有人的机器上)。
407
-
408
427
  ### 开发者发布流程
409
428
 
410
429
  #### 1. 发布主包
411
430
 
412
431
  ```bash
413
432
  cd minimal-agent
414
- # 改完代码后
415
- npm version patch # 0.1.6 0.1.7
416
- bun run build # tsup src/main.tsx 打成 dist/main.js
417
- npm publish --dry-run # 验证 tarball 内容(dist + vendor + skills + plugins)
418
- npm publish
433
+ npm version patch # 0.3.0 → 0.3.1
434
+ npm publish --dry-run # 验证 tarball(src/*.js + plugins/*.js + skills + workflows + vendor)
435
+ npm publish # prepublishOnly 自动跑 clean + tsc --build
419
436
  ```
420
437
 
421
438
  主包 `package.json` 关键字段:
@@ -423,67 +440,32 @@ npm publish
423
440
  ```json
424
441
  {
425
442
  "name": "minimal-agent",
426
- "bin": { "minimal-agent": "dist/main.js" },
427
- "files": ["dist", "vendor/ripgrep", "skills", "plugins", "README.md", "LICENSE"],
443
+ "bin": { "minimal-agent": "src/main.js" },
444
+ "files": ["src", "plugins", "skills", "workflows", "vendor/ripgrep", "README.md", "LICENSE"],
428
445
  "engines": { "node": ">=20" },
429
446
  "scripts": {
430
- "build": "tsup",
431
- "prepublishOnly": "npm run build"
447
+ "build": "tsc --build",
448
+ "clean": "bun scripts/clean-build.ts",
449
+ "prepublishOnly": "bun run clean && bun run build"
432
450
  }
433
451
  }
434
452
  ```
435
453
 
436
- `tsup` `src/main.tsx` 单文件 bundle 成 ESM CJS 双格式输出到 `dist/`,首行加 `#!/usr/bin/env node` shebang。`prepublishOnly` 保证 `npm publish` 前自动跑构建。
454
+ `prepublishOnly` 先清旧产物再跑 tsc 原地编译,`.npmignore` 排除源码 .ts/.tsx 让 tarball 里**只有 .js**。`src/main.tsx` 首行 `#!/usr/bin/env node` tsc 编译产物保留,直接作为 bin。
437
455
 
438
456
  #### 2. 发布编辑器包
439
457
 
440
458
  ```bash
441
459
  cd minimal-agent/editor
442
- # 改完代码后
443
- # 先把 package.json 里的 "private": true 删掉
444
- npm version patch # 0.1.0 → 0.1.1
460
+ npm version patch
445
461
  bun run build # Vite 把前端打到 editor/dist/
446
462
  npm publish --dry-run
447
463
  npm publish
448
464
  ```
449
465
 
450
- 编辑器包需要在 publish 前完成:
451
-
452
- - 删 `"private": true`
453
- - 加 `"files": ["dist", "scripts", "public/manifest.fallback.json", "index.html", "vite.config.ts"]`(控制 tarball)
454
- - 加 `"engines": { "bun": ">=1.1" }`(因为 bin 是 .ts,需要 bun shebang)
455
- - `scripts/cli.ts` 首行确认有 `#!/usr/bin/env bun`
456
-
457
- #### 3. 一键发布脚本(可选)
458
-
459
- `scripts/publish-all.ts`(建议加上):
460
-
461
- ```ts
462
- #!/usr/bin/env bun
463
- import { $ } from 'bun';
464
-
465
- const bump = process.argv[2] ?? 'patch'; // patch | minor | major
466
-
467
- console.log('▶ 发布主包...');
468
- await $`npm version ${bump}`;
469
- await $`bun run build`;
470
- await $`npm publish`;
471
-
472
- console.log('▶ 发布编辑器...');
473
- await $`cd editor && npm version ${bump}`;
474
- await $`cd editor && bun run build`;
475
- await $`cd editor && npm publish`;
476
-
477
- console.log('✅ 完成。两个包都已发布到 npm。');
478
- ```
479
-
480
- 跑法:
481
-
482
- ```bash
483
- bun scripts/publish-all.ts patch # 两个包都升 patch 版并发布
484
- ```
466
+ 编辑器是独立子项目,Bun `.ts` 源码作为 bin(`scripts/cli.ts` 首行有 `#!/usr/bin/env bun`),与主包发版策略不同但**目录耦合零依赖**——只通过文件系统读 `<project>/workflows/*.yaml`。
485
467
 
486
- #### 4. 版本独立
468
+ #### 3. 版本独立
487
469
 
488
470
  两个包的 version 字段**互相独立**,不必同步。改主包内核 → 只升主包;改编辑器 UI → 只升编辑器。
489
471
 
@@ -564,8 +546,8 @@ minimal-agent 是 Claude Code 源码的**教学版**:
564
546
  | 改完 `~/.minimal-agent/config.json` 没生效 | 当前进程仍持有旧配置;Ctrl+C 退出后重新 `bun run start` |
565
547
  | `rg: command not found` | 不该出现——项目自带 ripgrep;若出现说明 vendor 目录未随 npm tarball 复制 |
566
548
  | `WebBrowser 报"无法启动浏览器"` | 需 `npm install playwright-core && npx playwright install chromium`;或改用 WebFetch |
567
- | `workflow-ui` 命令找不到 | 没装编辑器包;`npm install -g workflow-ui` 即可 |
568
- | 编辑器空白 / 节点面板没工具 | `.editor-cache/manifest.json` 缺失;`workflow-ui --refresh-manifest` 强制刷新 |
549
+ | `minimal-workflw` 命令找不到 | 没装编辑器包;`npm install -g minimal-workflw` 即可 |
550
+ | 编辑器空白 / 节点面板没工具 | `.editor-cache/manifest.json` 缺失;`minimal-workflw --refresh-manifest` 强制刷新 |
569
551
  | 编辑器看不到自定义 skill | 检查 `<project>/skills/<name>/SKILL.md` frontmatter 是否有 `name` 和 `description` 两个必填字段 |
570
552
  | ffmpeg / ffprobe 未找到 | `winget install Gyan.FFmpeg` / `brew install ffmpeg` / `apt install ffmpeg` |
571
553
  | 历史"被吃掉"了 | 上次进程崩溃前未保存;`~/.minimal-agent/last-context.json` 是最后写入的一次 |
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "minimal-agent",
3
- "version": "0.2.0",
4
- "description": "最小化 Agent 系统 —— 单对话 + 10 工具 + 插件系统 + workflow DSL + 自动压缩 + OpenAI 兼容 + Ink TUI(学习/教学用)",
3
+ "version": "0.3.1",
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>",
7
7
  "repository": {
@@ -27,28 +27,34 @@
27
27
  ],
28
28
  "type": "module",
29
29
  "bin": {
30
- "minimal-agent": "dist/main.js"
30
+ "minimal-agent": "src/main.js"
31
31
  },
32
- "main": "dist/main.js",
33
32
  "engines": {
34
33
  "node": ">=20"
35
34
  },
36
35
  "files": [
37
- "dist",
38
- "vendor/ripgrep",
39
- "skills",
40
- "plugins",
41
- "workflows",
36
+ "src/**/*.js",
37
+ "src/**/*.d.ts",
38
+ "plugins/**/*.js",
39
+ "plugins/**/*.d.ts",
40
+ "plugins/**/.claude-plugin/**",
41
+ "plugins/**/commands/**",
42
+ "plugins/**/hooks/**",
43
+ "plugins/HOW-TO-WRITE-A-PLUGIN.md",
44
+ "skills/**",
45
+ "workflows/**",
46
+ "vendor/ripgrep/**",
42
47
  "README.md",
43
48
  "LICENSE"
44
49
  ],
45
50
  "scripts": {
46
51
  "start": "bun run src/main.tsx",
47
52
  "dev": "bun --watch src/main.tsx",
48
- "build": "tsup",
53
+ "build": "tsc --build",
54
+ "clean": "bun scripts/clean-build.ts",
49
55
  "test": "bun test",
50
- "typecheck": "tsc --noEmit",
51
- "prepublishOnly": "npm run build"
56
+ "typecheck": "tsc --noEmit -p tsconfig.typecheck.json",
57
+ "prepublishOnly": "bun run clean && bun run build"
52
58
  },
53
59
  "dependencies": {
54
60
  "fast-glob": "^3.3.2",
@@ -63,7 +69,6 @@
63
69
  "@types/node": "^20.14.0",
64
70
  "@types/react": "^18.3.0",
65
71
  "bun-types": "^1.3.13",
66
- "tsup": "^8.5.1",
67
72
  "typescript": "^5.5.0"
68
73
  }
69
74
  }
@@ -0,0 +1,205 @@
1
+ /**
2
+ * ============================================================
3
+ * plugins/ralph-wiggum/plugin.ts —— ralph-wiggum 真·插件入口
4
+ * ------------------------------------------------------------
5
+ * 把原 src/plugins/pluginRunner.ts 的 do-while 循环驱动整段搬来。
6
+ * 对外通过 PluginApi.runCommand 暴露 /ralph-loop。
7
+ *
8
+ * 循环契约:
9
+ * - 每轮把 history 重置成进入循环前的快照(fresh context)
10
+ * - 用 GoalState.composeContext() 拼 PLAN/BUILD/VERIFY/HEAL 阶段信息
11
+ * - 跑 runQuery
12
+ * - 检测 <promise>DONE</promise> → runVerification → 通过则退出
13
+ * - 检测 <PROMISE>NEED_REPLAN</PROMISE> → forceSetPhase(PLAN)
14
+ * - executeStopHook 是咨询式:block 才把 reason 注入下一轮,pass 不退出
15
+ *
16
+ * 终止(独占):sentinel + verify 通过 / 达 max-iterations / abort / 安全天花板
17
+ *
18
+ * Windows 上 hooks/stop-hook.sh 不可用,但循环不依赖 hook,功能完整,
19
+ * 只是 hook 的咨询通道失效。
20
+ * ============================================================
21
+ */
22
+ import { fileURLToPath } from 'node:url';
23
+ import { dirname } from 'node:path';
24
+ import { runQuery, getWorkingDir, } from '../../src/plugin-sdk.js';
25
+ import { GoalState, Phase } from './src/goalState.js';
26
+ import { parseVerifyArgs, runVerification } from './src/verificationGate.js';
27
+ import { hasCompleteSentinel, hasNeedReplanSentinel, } from './src/sentinels.js';
28
+ import { executeStopHook } from './src/stopHookRunner.js';
29
+ const PLUGIN_NAME = 'ralph-wiggum';
30
+ const DEFAULT_MAX_ITERATIONS = 50;
31
+ const SAFETY_CEILING = 200;
32
+ const PLUGIN_ROOT = dirname(fileURLToPath(import.meta.url));
33
+ function extractMaxIterations(args) {
34
+ const match = args.match(/--max-iterations\s+(\d+)/i);
35
+ return match ? parseInt(match[1], 10) : undefined;
36
+ }
37
+ async function* runRalphLoop(args, ctx) {
38
+ const { provider, history, signal } = ctx;
39
+ const maxIter = Math.min(extractMaxIterations(args) ?? DEFAULT_MAX_ITERATIONS, SAFETY_CEILING);
40
+ // 没有 args(用户只敲 /ralph-loop)→ 留个最小 goal placeholder,避免 GoalState.init 拒空串
41
+ const userGoal = args.trim() || '(未提供目标)';
42
+ yield {
43
+ type: 'plugin_progress',
44
+ pluginId: PLUGIN_NAME,
45
+ current: 0,
46
+ max: maxIter,
47
+ message: 'Ralph Wiggum copy-task loop 启动',
48
+ };
49
+ const checks = parseVerifyArgs(args);
50
+ // sessionTag = 插件名 → 多插件可并发不打架,/new 也能扫到清掉
51
+ const goalState = new GoalState(getWorkingDir(), PLUGIN_NAME);
52
+ await goalState.reset();
53
+ await goalState.init(userGoal, checks);
54
+ await goalState.appendProgress(`=== Loop 启动 === 目标: ${userGoal.slice(0, 120)}...`);
55
+ // 进循环前快照 history —— 每轮 runQuery 前用它重置,保证 fresh context
56
+ const baseHistory = history.slice();
57
+ let iterationCount = 0;
58
+ let consecutiveFailures = 0;
59
+ let currentInput = userGoal;
60
+ let finalAssistantMsg = null;
61
+ try {
62
+ do {
63
+ iterationCount++;
64
+ if (iterationCount > maxIter) {
65
+ await goalState.forceSetPhase(Phase.DONE, `达到迭代上限 ${maxIter}`);
66
+ await goalState.appendLearning(`[迭代上限] 循环在 ${iterationCount - 1} 轮后强制终止,可能目标过大或陷入死循环`);
67
+ yield {
68
+ type: 'error',
69
+ error: `Loop 已达迭代上限 ${maxIter},自动停止`,
70
+ };
71
+ return;
72
+ }
73
+ if (signal?.aborted) {
74
+ yield { type: 'interrupted' };
75
+ return;
76
+ }
77
+ yield {
78
+ type: 'plugin_progress',
79
+ pluginId: PLUGIN_NAME,
80
+ current: iterationCount,
81
+ max: maxIter,
82
+ };
83
+ // fresh context:清空 history,重置为入循环前的快照
84
+ history.length = 0;
85
+ history.push(...baseHistory);
86
+ const freshContext = goalState.composeContext(iterationCount);
87
+ const enhancedInput = `${freshContext}\n\n${currentInput}`;
88
+ yield* runQuery(enhancedInput, {
89
+ provider,
90
+ history,
91
+ signal,
92
+ maxTurns: ctx.maxTurns,
93
+ sessionState: ctx.sessionState,
94
+ });
95
+ if (signal?.aborted) {
96
+ yield { type: 'interrupted' };
97
+ return;
98
+ }
99
+ // 从本轮 history 抓最后一个 assistant 消息(runQuery 已 push 进去)
100
+ const lastAssistantIdx = (() => {
101
+ for (let i = history.length - 1; i >= 0; i--) {
102
+ if (history[i].role === 'assistant')
103
+ return i;
104
+ }
105
+ return -1;
106
+ })();
107
+ finalAssistantMsg =
108
+ lastAssistantIdx >= 0 ? history[lastAssistantIdx] : null;
109
+ const lastAssistantText = finalAssistantMsg
110
+ ? typeof finalAssistantMsg.content === 'string'
111
+ ? finalAssistantMsg.content
112
+ : JSON.stringify(finalAssistantMsg.content)
113
+ : '';
114
+ if (hasCompleteSentinel(lastAssistantText)) {
115
+ // 哨兵:可能来自任意阶段(包括 iter 1 的 PLAN),用 force 跳到 VERIFY
116
+ await goalState.forceSetPhase(Phase.VERIFY, '检测到完成哨兵,进入验证');
117
+ await goalState.appendProgress(`迭代 ${iterationCount}: 检测到完成哨兵,运行验证门...`);
118
+ if (checks.length > 0) {
119
+ const vResult = await runVerification(checks);
120
+ if (!vResult.passed) {
121
+ consecutiveFailures++;
122
+ await goalState.appendLearning(`[迭代 ${iterationCount}] 声称完成但验证未通过: ${vResult.summary}`);
123
+ yield {
124
+ type: 'error',
125
+ error: `⚠️ 验证未通过: ${vResult.summary}。继续尝试...`,
126
+ };
127
+ if (consecutiveFailures >= 3) {
128
+ await goalState.forceSetPhase(Phase.HEAL, `连续 ${consecutiveFailures} 次验证失败`);
129
+ }
130
+ else {
131
+ await goalState.setPhase(Phase.BUILD, '验证未通过,返回构建');
132
+ }
133
+ continue;
134
+ }
135
+ await goalState.appendProgress(`✅ 验证通过: ${vResult.summary}`);
136
+ }
137
+ await goalState.setPhase(Phase.DONE, 'goal complete & verified');
138
+ yield {
139
+ type: 'plugin_iteration',
140
+ pluginName: PLUGIN_NAME,
141
+ current: iterationCount,
142
+ max: maxIter,
143
+ };
144
+ return;
145
+ }
146
+ if (hasNeedReplanSentinel(lastAssistantText)) {
147
+ await goalState.forceSetPhase(Phase.PLAN, 'agent 请求重新规划');
148
+ await goalState.appendLearning('[NEED_REPLAN] Agent 认为当前方案不可行,需要重新规划');
149
+ await goalState.appendProgress('Agent 请求 NEED_REPLAN,回 PLAN 阶段');
150
+ consecutiveFailures = 0;
151
+ continue;
152
+ }
153
+ // PLAN 阶段跑过一轮还没哨兵,认为规划已完成,自动推进 BUILD
154
+ if (goalState.currentPhase === Phase.PLAN && iterationCount >= 2) {
155
+ await goalState.setPhase(Phase.BUILD, '规划阶段已完成,进入构建');
156
+ }
157
+ // Stop-hook 咨询式调用:block 才把 reason 注入下一轮 prompt
158
+ // pass / 报错都不再终止循环;终止由 sentinel/maxIter/abort/NEED_REPLAN 独占
159
+ const hookResult = await executeStopHook(PLUGIN_ROOT, lastAssistantText);
160
+ if (hookResult.decision === 'block' && hookResult.reason) {
161
+ currentInput = hookResult.reason;
162
+ consecutiveFailures = 0;
163
+ await goalState.recordDecision({
164
+ iteration: iterationCount,
165
+ phase: goalState.currentPhase,
166
+ summary: 'stop-hook 反馈',
167
+ }, ['继续循环', '终止'], '继续循环', hookResult.reason.slice(0, 200));
168
+ if (hookResult.systemMessage) {
169
+ baseHistory.push({
170
+ role: 'user',
171
+ content: `[Plugin Stop Hook] ${hookResult.systemMessage}`,
172
+ });
173
+ }
174
+ await goalState.appendProgress(`迭代 ${iterationCount}: Stop hook block,注入反馈继续`);
175
+ }
176
+ else {
177
+ await goalState.appendProgress(`迭代 ${iterationCount}: 无哨兵 / hook pass,继续下一轮`);
178
+ }
179
+ } while (true);
180
+ }
181
+ finally {
182
+ // 收尾:把循环里临时累积的 history 还原成 baseHistory + 最后一轮 assistant
183
+ // 这样 TUI 上看到的就是"一次问答",而不是 N 轮重复
184
+ history.length = 0;
185
+ history.push(...baseHistory);
186
+ if (finalAssistantMsg) {
187
+ history.push(finalAssistantMsg);
188
+ }
189
+ await goalState.cleanup();
190
+ }
191
+ }
192
+ const api = {
193
+ async *runCommand(commandName, args, ctx) {
194
+ if (commandName === 'ralph-loop') {
195
+ yield* runRalphLoop(args, ctx);
196
+ return;
197
+ }
198
+ // 其它命令(help / cancel-ralph)→ 不接管,让框架走声明式 fallback
199
+ yield {
200
+ type: 'error',
201
+ error: `ralph-wiggum: 命令 /${commandName} 未由 plugin.ts 接管`,
202
+ };
203
+ },
204
+ };
205
+ export default api;