minimal-agent 0.2.0 → 0.3.0

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 (107) hide show
  1. package/README.md +50 -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/{plugin.ts → plugin.js} +20 -26
  9. package/plugins/workflow-runner/src/expressions.js +369 -0
  10. package/plugins/workflow-runner/src/index.js +174 -0
  11. package/plugins/workflow-runner/src/loader.js +183 -0
  12. package/plugins/workflow-runner/src/runner.js +290 -0
  13. package/plugins/workflow-runner/src/stepExecutors/assert.js +28 -0
  14. package/plugins/workflow-runner/src/stepExecutors/llm.js +44 -0
  15. package/plugins/workflow-runner/src/stepExecutors/skill.js +103 -0
  16. package/plugins/workflow-runner/src/stepExecutors/{tool.ts → tool.js} +19 -25
  17. package/plugins/workflow-runner/src/types.js +59 -0
  18. package/plugins/workflow-runner/src/{workflowState.ts → workflowState.js} +21 -40
  19. package/src/bootstrap/cwdArg.js +22 -0
  20. package/src/bootstrap/workingDir.js +31 -0
  21. package/src/cli/configWizard.js +272 -0
  22. package/src/cli/print.js +192 -0
  23. package/src/config/configFile.js +78 -0
  24. package/src/config.js +118 -0
  25. package/src/context/compact.js +357 -0
  26. package/src/context/microCompactLite.js +151 -0
  27. package/src/context/persistContext.js +109 -0
  28. package/src/context/reactiveCompact.js +121 -0
  29. package/src/context/sessionPath.js +58 -0
  30. package/src/context/snipCompact.js +112 -0
  31. package/src/context/tokenCounter.js +66 -0
  32. package/src/llm/client.js +182 -0
  33. package/src/loop.js +230 -0
  34. package/src/main.js +116 -0
  35. package/src/plugin-sdk.js +24 -0
  36. package/src/plugins/commandRouter.js +169 -0
  37. package/src/plugins/hookEngine.js +258 -0
  38. package/src/plugins/pluginApi.js +23 -0
  39. package/src/plugins/pluginLoader.js +71 -0
  40. package/src/plugins/pluginRunner.js +65 -0
  41. package/src/plugins/transcript.js +171 -0
  42. package/src/prompts/projectInstructions.js +48 -0
  43. package/src/prompts/skillList.js +126 -0
  44. package/src/prompts/system.js +155 -0
  45. package/src/session/runTurn.js +41 -0
  46. package/src/session/sessionState.js +19 -0
  47. package/src/tools/bash/bash.js +352 -0
  48. package/src/tools/bash/semantics.js +85 -0
  49. package/src/tools/bash/warnings.js +98 -0
  50. package/src/tools/edit/edit.js +253 -0
  51. package/src/tools/edit/multi-edit.js +155 -0
  52. package/src/tools/glob/glob.js +97 -0
  53. package/src/tools/grep/grep.js +185 -0
  54. package/src/tools/grep/rgPath.js +173 -0
  55. package/src/tools/index.js +94 -0
  56. package/src/tools/read/read.js +209 -0
  57. package/src/tools/shared/fileState.js +61 -0
  58. package/src/tools/shared/fileUtils.js +281 -0
  59. package/src/tools/shared/schemas.js +16 -0
  60. package/src/tools/types.js +21 -0
  61. package/src/tools/webbrowser/browser.js +55 -0
  62. package/src/tools/webbrowser/webbrowser.js +194 -0
  63. package/src/tools/webfetch/preapproved.js +267 -0
  64. package/src/tools/webfetch/webfetch.js +317 -0
  65. package/src/tools/websearch/websearch.js +161 -0
  66. package/src/tools/write/write.js +125 -0
  67. package/src/types/turndown.d.ts +23 -0
  68. package/src/types.js +16 -0
  69. package/src/ui/App.js +37 -0
  70. package/src/ui/InputBox.js +240 -0
  71. package/src/ui/MessageList.js +28 -0
  72. package/src/ui/Root.js +70 -0
  73. package/src/ui/StatusLine.js +41 -0
  74. package/src/ui/ToolStatus.js +11 -0
  75. package/src/ui/hooks/useChat.js +234 -0
  76. package/src/ui/hooks/usePasteHandler.js +137 -0
  77. package/src/ui/hooks/useTextBuffer.js +55 -0
  78. package/src/ui/hooks/useTokenUsage.js +30 -0
  79. package/src/ui/textBuffer.js +217 -0
  80. package/src/utils/packageRoot.js +37 -0
  81. package/src/utils/resourcePaths.js +49 -0
  82. package/src/utils/zodToJson.js +29 -0
  83. package/dist/main.js +0 -5315
  84. package/plugins/ralph-wiggum/plugin.ts +0 -275
  85. package/plugins/ralph-wiggum/scripts/setup-ralph-loop.sh +0 -203
  86. package/plugins/ralph-wiggum/src/goalState.ts +0 -310
  87. package/plugins/ralph-wiggum/src/stopHookRunner.ts +0 -136
  88. package/plugins/ralph-wiggum/src/verificationGate.ts +0 -252
  89. package/plugins/ralph-wiggum/test/goalState.test.ts +0 -410
  90. package/plugins/ralph-wiggum/test/verificationGate.test.ts +0 -122
  91. package/plugins/workflow-runner/src/expressions.ts +0 -371
  92. package/plugins/workflow-runner/src/index.ts +0 -194
  93. package/plugins/workflow-runner/src/loader.ts +0 -193
  94. package/plugins/workflow-runner/src/runner.ts +0 -313
  95. package/plugins/workflow-runner/src/stepExecutors/assert.ts +0 -30
  96. package/plugins/workflow-runner/src/stepExecutors/llm.ts +0 -54
  97. package/plugins/workflow-runner/src/stepExecutors/skill.ts +0 -115
  98. package/plugins/workflow-runner/src/types.ts +0 -183
  99. package/plugins/workflow-runner/test/cli.e2e.test.ts +0 -114
  100. package/plugins/workflow-runner/test/e2e.test.ts +0 -268
  101. package/plugins/workflow-runner/test/expressions.test.ts +0 -140
  102. package/plugins/workflow-runner/test/fixtures/cli-e2e.yaml +0 -27
  103. package/plugins/workflow-runner/test/fixtures/hello-workflow.yaml +0 -49
  104. package/plugins/workflow-runner/test/graceful.test.ts +0 -139
  105. package/plugins/workflow-runner/test/loader.test.ts +0 -216
  106. package/plugins/workflow-runner/test/pluginRunner.isolation.test.ts +0 -230
  107. 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
 
@@ -268,7 +272,7 @@ YAML DAG workflow 执行器,支持 7 种 step:
268
272
 
269
273
  ---
270
274
 
271
- ## workflow-ui 可视化编辑器(editor/ 子项目)
275
+ ## minimal-workflw 可视化编辑器(editor/ 子项目)
272
276
 
273
277
  独立的 npm 包,运行时与 `minimal-agent` **零耦合**,仅通过 `workflows/*.yaml` + `.editor-cache/manifest.json` 两个文件交换数据。
274
278
 
@@ -276,9 +280,9 @@ YAML DAG workflow 执行器,支持 7 种 step:
276
280
 
277
281
  ```bash
278
282
  # 已全局安装时
279
- workflow-ui # 当前 cwd 当 minimal-agent 项目根
280
- workflow-ui ~/my-project # 指定项目根
281
- workflow-ui --refresh-manifest # 强制刷新工具/skill 清单
283
+ minimal-workflw # 当前 cwd 当 minimal-agent 项目根
284
+ minimal-workflw ~/my-project # 指定项目根
285
+ minimal-workflw --refresh-manifest # 强制刷新工具/skill 清单
282
286
 
283
287
  # 从源码跑(在 editor/ 目录下)
284
288
  bun scripts/cli.ts --project .. # 一键起后端 + Vite
@@ -289,7 +293,7 @@ bun run server # 只起 Bun.serve() 后端
289
293
  > **为什么是 `bun scripts/cli.ts` 而不是 `bun run dev`?**
290
294
  > - `bun run dev` 只起 Vite 前端,UI 能渲染但**没有后端**,文件列表 / 读写 / manifest 端点全是 404
291
295
  > - `bun scripts/cli.ts` 是 bin 入口,**同时**起 `Bun.serve()` 文件 IO 后端 + Vite 前端,完整可用
292
- > - npm 发布后用户执行的 `workflow-ui` 命令就是这个 cli.ts
296
+ > - npm 发布后用户执行的 `minimal-workflw` 命令就是这个 cli.ts
293
297
 
294
298
  ### 启动序列(编辑器 CLI 内部)
295
299
 
@@ -335,7 +339,7 @@ export OPENROUTER_API_KEY=sk-or-...
335
339
  bun run src/main.tsx -p "/workflow youtube-shorts --input topic='猫咪赛博朋克城市探险'"
336
340
  ```
337
341
 
338
- 整条流水线**零代码改动** —— 4 个 skill 都是 markdown,1 个 workflow 是 yaml。在 `workflow-ui` 里打开 `youtube-shorts.yaml`,可视化拖拽改 prompt、换 skill、加节点。
342
+ 整条流水线**零代码改动** —— 4 个 skill 都是 markdown,1 个 workflow 是 yaml。在 `minimal-workflw` 里打开 `youtube-shorts.yaml`,可视化拖拽改 prompt、换 skill、加节点。
339
343
 
340
344
  ---
341
345
 
@@ -364,8 +368,8 @@ minimal-agent/
364
368
  │ ├── book-review-short.yaml
365
369
  │ ├── e2e-write-greet.yaml
366
370
  │ └── youtube-shorts.yaml
367
- ├── editor/ # workflow-ui 独立子包
368
- │ ├── package.json # name: workflow-ui, bin: workflow-ui
371
+ ├── editor/ # minimal-workflw 独立子包
372
+ │ ├── package.json # name: minimal-workflw, bin: minimal-workflw
369
373
  │ ├── scripts/
370
374
  │ │ ├── cli.ts # bin 入口(一键起后端+前端)
371
375
  │ │ └── server.ts # Bun.serve() 文件 IO + manifest 端点
@@ -387,9 +391,22 @@ minimal-agent/
387
391
  | 包 | 路径 | 命令 | 职责 |
388
392
  |---|---|---|---|
389
393
  | `minimal-agent` | 仓库根 | `minimal-agent` | ReAct CLI + 工具 + 插件 + skills |
390
- | `workflow-ui` | `editor/` | `workflow-ui` | 可视化 workflow 编辑器 |
394
+ | `minimal-workflw` | `editor/` | `minimal-workflw` | 可视化 workflow 编辑器 |
395
+
396
+ > **为什么不打成一个包**?90% 的 ReAct CLI 用户不需要编辑器,编辑器的依赖(React / Vite / @xyflow/react / Monaco 等)有几十 MB,不应该污染主包。两边解耦后用户按需取用,通过 `<project>/workflows/*.yaml` 文件交流,无进程间通信。
397
+
398
+ ### 模块解析模式(dev vs install 通用范式)
399
+
400
+ 主包采用**标准 NodeNext + tsc 原地编译**模式,解决了 npm 包"dev 跑 .ts,install 跑 .js"的通用目录差异问题:
401
+
402
+ - 源码(仓库 / dev)里 import 统一写 `.js` 后缀:`import { x } from './foo.js'`
403
+ - dev 模式跑 `bun run dev`:Bun 把 `'./foo.js'` 透明解析到 `./foo.ts` 源文件,**无需任何构建**
404
+ - publish 前 `tsc --build`:原地输出 `./foo.js`(与 .ts 兄弟位置,路径布局完全一致)
405
+ - `.npmignore` 排除 `*.ts/*.tsx`,tarball 里**只发 .js**
406
+ - install 后 Node 直接吃 .js,Bun 也吃 .js(行为统一)
407
+ - 源码 `plugin.ts` 里 `import '../../src/plugin-sdk.js'` 在 dev / install 两种模式下都指向同一份模块(dev=plugin-sdk.ts,install=plugin-sdk.js),**模块身份天然 singleton**
391
408
 
392
- > **为什么不打成一个包**?因为 90% 的 ReAct CLI 用户不需要编辑器,编辑器的依赖(React / Vite / @xyflow/react / Monaco 等)有几十 MB,不应该污染主包。两边解耦后用户按需取用。
409
+ 这是 `execa` / `p-queue` / `ink` 等纯 ESM TypeScript 库的通用做法。
393
410
 
394
411
  ### 用户安装(终端)
395
412
 
@@ -399,23 +416,19 @@ npm install -g minimal-agent
399
416
  minimal-agent # 启 ReAct TUI
400
417
 
401
418
  # 加装编辑器
402
- npm install -g workflow-ui
403
- workflow-ui /path/to/your-project # 启编辑器
419
+ npm install -g minimal-workflw
420
+ minimal-workflw /path/to/your-project # 启编辑器
404
421
  ```
405
422
 
406
- > npm 的 `bin` 字段支持一个包注册多个命令;但本项目选择**两个独立包**而不是"一个包两个 bin",是为了**依赖隔离**(编辑器的 React 依赖不该跟着 ReAct CLI 一起装到所有人的机器上)。
407
-
408
423
  ### 开发者发布流程
409
424
 
410
425
  #### 1. 发布主包
411
426
 
412
427
  ```bash
413
428
  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
429
+ npm version patch # 0.3.0 → 0.3.1
430
+ npm publish --dry-run # 验证 tarball(src/*.js + plugins/*.js + skills + workflows + vendor)
431
+ npm publish # prepublishOnly 自动跑 clean + tsc --build
419
432
  ```
420
433
 
421
434
  主包 `package.json` 关键字段:
@@ -423,67 +436,32 @@ npm publish
423
436
  ```json
424
437
  {
425
438
  "name": "minimal-agent",
426
- "bin": { "minimal-agent": "dist/main.js" },
427
- "files": ["dist", "vendor/ripgrep", "skills", "plugins", "README.md", "LICENSE"],
439
+ "bin": { "minimal-agent": "src/main.js" },
440
+ "files": ["src", "plugins", "skills", "workflows", "vendor/ripgrep", "README.md", "LICENSE"],
428
441
  "engines": { "node": ">=20" },
429
442
  "scripts": {
430
- "build": "tsup",
431
- "prepublishOnly": "npm run build"
443
+ "build": "tsc --build",
444
+ "clean": "bun scripts/clean-build.ts",
445
+ "prepublishOnly": "bun run clean && bun run build"
432
446
  }
433
447
  }
434
448
  ```
435
449
 
436
- `tsup` `src/main.tsx` 单文件 bundle 成 ESM CJS 双格式输出到 `dist/`,首行加 `#!/usr/bin/env node` shebang。`prepublishOnly` 保证 `npm publish` 前自动跑构建。
450
+ `prepublishOnly` 先清旧产物再跑 tsc 原地编译,`.npmignore` 排除源码 .ts/.tsx 让 tarball 里**只有 .js**。`src/main.tsx` 首行 `#!/usr/bin/env node` tsc 编译产物保留,直接作为 bin。
437
451
 
438
452
  #### 2. 发布编辑器包
439
453
 
440
454
  ```bash
441
455
  cd minimal-agent/editor
442
- # 改完代码后
443
- # 先把 package.json 里的 "private": true 删掉
444
- npm version patch # 0.1.0 → 0.1.1
456
+ npm version patch
445
457
  bun run build # Vite 把前端打到 editor/dist/
446
458
  npm publish --dry-run
447
459
  npm publish
448
460
  ```
449
461
 
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
- ```
462
+ 编辑器是独立子项目,Bun `.ts` 源码作为 bin(`scripts/cli.ts` 首行有 `#!/usr/bin/env bun`),与主包发版策略不同但**目录耦合零依赖**——只通过文件系统读 `<project>/workflows/*.yaml`。
485
463
 
486
- #### 4. 版本独立
464
+ #### 3. 版本独立
487
465
 
488
466
  两个包的 version 字段**互相独立**,不必同步。改主包内核 → 只升主包;改编辑器 UI → 只升编辑器。
489
467
 
@@ -564,8 +542,8 @@ minimal-agent 是 Claude Code 源码的**教学版**:
564
542
  | 改完 `~/.minimal-agent/config.json` 没生效 | 当前进程仍持有旧配置;Ctrl+C 退出后重新 `bun run start` |
565
543
  | `rg: command not found` | 不该出现——项目自带 ripgrep;若出现说明 vendor 目录未随 npm tarball 复制 |
566
544
  | `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` 强制刷新 |
545
+ | `minimal-workflw` 命令找不到 | 没装编辑器包;`npm install -g minimal-workflw` 即可 |
546
+ | 编辑器空白 / 节点面板没工具 | `.editor-cache/manifest.json` 缺失;`minimal-workflw --refresh-manifest` 强制刷新 |
569
547
  | 编辑器看不到自定义 skill | 检查 `<project>/skills/<name>/SKILL.md` frontmatter 是否有 `name` 和 `description` 两个必填字段 |
570
548
  | ffmpeg / ffprobe 未找到 | `winget install Gyan.FFmpeg` / `brew install ffmpeg` / `apt install ffmpeg` |
571
549
  | 历史"被吃掉"了 | 上次进程崩溃前未保存;`~/.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.0",
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;