minimal-agent 0.8.2 → 0.9.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 (55) hide show
  1. package/README.md +4 -5
  2. package/package.json +1 -1
  3. package/plugins/workflow-runner/plugin.js +1 -0
  4. package/plugins/workflow-runner/src/agentCommon.js +82 -47
  5. package/plugins/workflow-runner/src/expressions.js +48 -2
  6. package/plugins/workflow-runner/src/index.js +1 -0
  7. package/plugins/workflow-runner/src/jsonPayload.js +1 -1
  8. package/plugins/workflow-runner/src/loader.js +70 -8
  9. package/plugins/workflow-runner/src/recovery.js +43 -35
  10. package/plugins/workflow-runner/src/runner.js +143 -34
  11. package/plugins/workflow-runner/src/stepExecutors/agent.js +64 -35
  12. package/plugins/workflow-runner/src/stepExecutors/assert.js +14 -2
  13. package/plugins/workflow-runner/src/stepExecutors/llm.js +48 -107
  14. package/plugins/workflow-runner/src/stepExecutors/skill.js +50 -39
  15. package/plugins/workflow-runner/src/stepExecutors/tool.js +20 -7
  16. package/plugins/workflow-runner/src/stepExecutors/verify.js +11 -3
  17. package/plugins/workflow-runner/src/taskIdentity.js +141 -0
  18. package/plugins/workflow-runner/src/types.js +1 -2
  19. package/plugins/workflow-runner/src/workflowState.js +211 -28
  20. package/skills/adversarial-review/SKILL.md +0 -1
  21. package/skills/pptx/scripts/office/__pycache__/soffice.cpython-314.pyc +0 -0
  22. package/src/askBridge.js +10 -0
  23. package/src/context/archive.js +4 -0
  24. package/src/infra/verification.js +25 -13
  25. package/src/llm/client.js +160 -12
  26. package/src/loop.js +15 -12
  27. package/src/plugins/pluginLoader.js +2 -1
  28. package/src/plugins/pluginRunner.js +9 -21
  29. package/src/prompts/pluginList.js +5 -5
  30. package/src/prompts/promptAssembly.js +3 -38
  31. package/src/prompts/skillList.js +1 -1
  32. package/src/prompts/system.js +0 -1
  33. package/src/ui/AskQuestion.js +20 -3
  34. package/src/ui/InputBox.js +70 -3
  35. package/src/ui/hooks/useChat.js +13 -0
  36. package/workflows/adaptive-research.yaml +4 -3
  37. package/workflows/book-review-short.yaml +5 -3
  38. package/workflows/e2e-write-greet.yaml +3 -2
  39. package/workflows/plan.yaml +219 -168
  40. package/workflows/ralph-loop.yaml +169 -0
  41. package/workflows/youtube-shorts.yaml +8 -5
  42. package/plugins/HOW-TO-WRITE-A-PLUGIN.md +0 -186
  43. package/plugins/ralph-wiggum/.claude-plugin/plugin.json +0 -9
  44. package/plugins/ralph-wiggum/README.md +0 -197
  45. package/plugins/ralph-wiggum/commands/cancel-ralph.md +0 -18
  46. package/plugins/ralph-wiggum/commands/help.md +0 -137
  47. package/plugins/ralph-wiggum/commands/ralph-loop.md +0 -85
  48. package/plugins/ralph-wiggum/hooks/hooks.json +0 -15
  49. package/plugins/ralph-wiggum/hooks/stop-hook.sh +0 -191
  50. package/plugins/ralph-wiggum/plugin.js +0 -280
  51. package/plugins/ralph-wiggum/src/goalState.js +0 -258
  52. package/plugins/ralph-wiggum/src/sentinels.js +0 -76
  53. package/plugins/ralph-wiggum/src/stopHookRunner.js +0 -104
  54. package/plugins/ralph-wiggum/src/verificationGate.js +0 -202
  55. package/plugins/workflow-runner/src/miniReAct.js +0 -223
@@ -1,280 +0,0 @@
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 { parseVerifyArgs, runVerification } from '../../src/infra/verification.js';
26
- import { GoalState, Phase } from './src/goalState.js';
27
- import { makeCompletionMatcher, hasNeedReplanSentinel, hasNeedGoalRevisionSentinel, extractLearnings, } 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
- /** 解析 --completion-promise "TEXT"(带引号优先)或 --completion-promise TOKEN。 */
38
- function extractCompletionPromise(args) {
39
- const quoted = args.match(/--completion-promise\s+"([^"]*)"/i);
40
- if (quoted)
41
- return quoted[1].trim() || undefined;
42
- const token = args.match(/--completion-promise\s+(\S+)/i);
43
- return token ? token[1].trim() || undefined : undefined;
44
- }
45
- /** 从 args 里剥掉控制 flag,得到喂给 LLM 的"干净目标文本"(避免 --verify 等泄漏进 prompt)。 */
46
- function stripControlFlags(args) {
47
- return args
48
- .replace(/--max-iterations\s+\d+/gi, '')
49
- .replace(/--completion-promise\s+"[^"]*"/gi, '')
50
- .replace(/--completion-promise\s+\S+/gi, '')
51
- .replace(/--verify\s+"[^"]*"/gi, '')
52
- .replace(/--verify\s+\S+/gi, '')
53
- .replace(/\s{2,}/g, ' ')
54
- .trim();
55
- }
56
- async function* runRalphLoop(args, ctx) {
57
- const { provider, history, signal } = ctx;
58
- const maxIter = Math.min(extractMaxIterations(args) ?? DEFAULT_MAX_ITERATIONS, SAFETY_CEILING);
59
- // 没有 args(用户只敲 /ralph-loop)→ 留个最小 goal placeholder,避免 GoalState.init 拒空串
60
- // 控制 flag(--max-iterations / --verify / --completion-promise)都从原始 args 解析,
61
- // userGoal 才用剥过的干净文本,避免 flag 泄漏进喂给 LLM 的 prompt
62
- const userGoal = stripControlFlags(args) || '(未提供目标)';
63
- yield {
64
- type: 'plugin_progress',
65
- pluginId: PLUGIN_NAME,
66
- current: 0,
67
- max: maxIter,
68
- message: 'Ralph Wiggum copy-task loop 启动',
69
- };
70
- const checks = parseVerifyArgs(args);
71
- // 完成哨兵判定器:不传 --completion-promise 只认写死集合 DONE|COMPLETE|GOAL_COMPLETE,
72
- // 传了则在写死集合基础上叠加自定义短语(超集,向后兼容)
73
- const completePhrase = extractCompletionPromise(args);
74
- const isComplete = makeCompletionMatcher(completePhrase);
75
- // sessionTag = 插件名 → 多插件可并发不打架,/new 也能扫到清掉
76
- const goalState = new GoalState(getWorkingDir(), PLUGIN_NAME);
77
- await goalState.reset();
78
- await goalState.init(userGoal, checks);
79
- await goalState.appendProgress(`=== Loop 启动 === 目标: ${userGoal.slice(0, 120)}...`);
80
- // 进循环前快照 history —— 每轮 runQuery 前用它重置,保证 fresh context
81
- const baseHistory = history.slice();
82
- let iterationCount = 0;
83
- let consecutiveFailures = 0;
84
- let currentInput = userGoal;
85
- let finalAssistantMsg = null;
86
- try {
87
- do {
88
- iterationCount++;
89
- if (iterationCount > maxIter) {
90
- await goalState.forceSetPhase(Phase.DONE, `达到迭代上限 ${maxIter}`);
91
- await goalState.appendLearning(`[迭代上限] 循环在 ${iterationCount - 1} 轮后强制终止,可能目标过大或陷入死循环`);
92
- yield {
93
- type: 'error',
94
- error: `Loop 已达迭代上限 ${maxIter},自动停止`,
95
- code: 'max_turns',
96
- };
97
- return;
98
- }
99
- if (signal?.aborted) {
100
- yield { type: 'interrupted' };
101
- return;
102
- }
103
- yield {
104
- type: 'plugin_progress',
105
- pluginId: PLUGIN_NAME,
106
- current: iterationCount,
107
- max: maxIter,
108
- };
109
- // fresh context:清空 history,重置为入循环前的快照
110
- history.length = 0;
111
- history.push(...baseHistory);
112
- const freshContext = goalState.composeContext(iterationCount);
113
- const enhancedInput = `${freshContext}\n\n${currentInput}`;
114
- yield* runQuery(enhancedInput, {
115
- provider,
116
- history,
117
- signal,
118
- maxTurns: ctx.maxTurns,
119
- sessionState: ctx.sessionState,
120
- });
121
- if (signal?.aborted) {
122
- yield { type: 'interrupted' };
123
- return;
124
- }
125
- // 从本轮 history 抓最后一个 assistant 消息(runQuery 已 push 进去)
126
- const lastAssistantIdx = (() => {
127
- for (let i = history.length - 1; i >= 0; i--) {
128
- if (history[i].role === 'assistant')
129
- return i;
130
- }
131
- return -1;
132
- })();
133
- finalAssistantMsg =
134
- lastAssistantIdx >= 0 ? history[lastAssistantIdx] : null;
135
- const lastAssistantText = finalAssistantMsg
136
- ? typeof finalAssistantMsg.content === 'string'
137
- ? finalAssistantMsg.content
138
- : JSON.stringify(finalAssistantMsg.content)
139
- : '';
140
- // 每轮提取 <learning> 并落盘,让经验跨 fresh-context 迭代存活
141
- for (const lesson of extractLearnings(lastAssistantText)) {
142
- await goalState.appendLearning(`[迭代 ${iterationCount}] ${lesson}`);
143
- }
144
- if (isComplete(lastAssistantText)) {
145
- // 哨兵:可能来自任意阶段(包括 iter 1 的 PLAN),用 force 跳到 VERIFY
146
- await goalState.forceSetPhase(Phase.VERIFY, '检测到完成哨兵,进入验证');
147
- await goalState.appendProgress(`迭代 ${iterationCount}: 检测到完成哨兵,运行验证门...`);
148
- if (checks.length > 0) {
149
- const vResult = await runVerification(checks);
150
- if (!vResult.passed) {
151
- consecutiveFailures++;
152
- await goalState.appendLearning(`[迭代 ${iterationCount}] 声称完成但验证未通过: ${vResult.summary}`);
153
- // 验证未通过不是致命错误:降级为 warning(不映射 stop_reason),
154
- // 再补一条进度保活进度条,避免 UI 误以为循环停了
155
- yield {
156
- type: 'warning',
157
- message: `⚠️ 验证未通过: ${vResult.summary}。继续尝试...`,
158
- };
159
- yield {
160
- type: 'plugin_progress',
161
- pluginId: PLUGIN_NAME,
162
- current: iterationCount,
163
- max: maxIter,
164
- message: '验证未通过,回到构建修复',
165
- };
166
- // 跨平台决策记录(不再只靠 POSIX stop-hook)
167
- await goalState.recordDecision({
168
- iteration: iterationCount,
169
- phase: Phase.VERIFY,
170
- summary: '验证门',
171
- }, ['判定完成', '回退继续'], '回退继续', vResult.summary.slice(0, 200));
172
- if (consecutiveFailures >= 3) {
173
- await goalState.forceSetPhase(Phase.HEAL, `连续 ${consecutiveFailures} 次验证失败`);
174
- }
175
- else {
176
- // 语义诚实:VERIFY→BUILD 不是合法边,用 force 而非借合法边
177
- await goalState.forceSetPhase(Phase.BUILD, '验证未通过,回到构建修复');
178
- }
179
- continue;
180
- }
181
- await goalState.appendProgress(`✅ 验证通过: ${vResult.summary}`);
182
- }
183
- await goalState.setPhase(Phase.DONE, 'goal complete & verified');
184
- // 成功路径决策记录(vResult 定义在 checks.length>0 块内、此处作用域不可见,
185
- // 故 reasoning 统一写 '目标完成',不引用 vResult)
186
- await goalState.recordDecision({
187
- iteration: iterationCount,
188
- phase: Phase.VERIFY,
189
- summary: '验证门',
190
- }, ['判定完成', '回退继续'], '判定完成', '目标完成');
191
- yield {
192
- type: 'plugin_progress',
193
- pluginId: PLUGIN_NAME,
194
- current: iterationCount,
195
- max: maxIter,
196
- message: '✅ 完成并通过验证',
197
- };
198
- return;
199
- }
200
- // NEED_GOAL_REVISION 逃生门:目标本身不可达,干净终止请人工修订
201
- // (区别于 NEED_REPLAN —— 那是换方案,这是换目标)
202
- if (hasNeedGoalRevisionSentinel(lastAssistantText)) {
203
- await goalState.forceSetPhase(Phase.DONE, 'agent 报告目标本身需修订');
204
- await goalState.appendLearning('[NEED_GOAL_REVISION] Agent 认为目标本身有问题,需人工修订');
205
- await goalState.recordDecision({
206
- iteration: iterationCount,
207
- phase: goalState.currentPhase,
208
- summary: 'NEED_GOAL_REVISION',
209
- }, ['继续硬上', '停止请人工改目标'], '停止请人工改目标', '目标本身不可达');
210
- yield {
211
- type: 'warning',
212
- message: '🛑 Agent 判断目标本身需要人工修订,循环已停止。请检查并调整目标后重试。',
213
- };
214
- return;
215
- }
216
- if (hasNeedReplanSentinel(lastAssistantText)) {
217
- await goalState.forceSetPhase(Phase.PLAN, 'agent 请求重新规划');
218
- await goalState.appendLearning('[NEED_REPLAN] Agent 认为当前方案不可行,需要重新规划');
219
- await goalState.appendProgress('Agent 请求 NEED_REPLAN,回 PLAN 阶段');
220
- await goalState.recordDecision({
221
- iteration: iterationCount,
222
- phase: goalState.currentPhase,
223
- summary: 'NEED_REPLAN',
224
- }, ['继续当前方案', '回 PLAN 重规划'], '回 PLAN 重规划', 'agent 认为方案不可行');
225
- consecutiveFailures = 0;
226
- continue;
227
- }
228
- // PLAN 阶段跑过一轮还没哨兵,认为规划已完成,自动推进 BUILD
229
- if (goalState.currentPhase === Phase.PLAN && iterationCount >= 2) {
230
- await goalState.setPhase(Phase.BUILD, '规划阶段已完成,进入构建');
231
- }
232
- // Stop-hook 咨询式调用:block 才把 reason 注入下一轮 prompt
233
- // pass / 报错都不再终止循环;终止由 sentinel/maxIter/abort/NEED_REPLAN 独占
234
- const hookResult = await executeStopHook(PLUGIN_ROOT, lastAssistantText);
235
- if (hookResult.decision === 'block' && hookResult.reason) {
236
- currentInput = hookResult.reason;
237
- consecutiveFailures = 0;
238
- await goalState.recordDecision({
239
- iteration: iterationCount,
240
- phase: goalState.currentPhase,
241
- summary: 'stop-hook 反馈',
242
- }, ['继续循环', '终止'], '继续循环', hookResult.reason.slice(0, 200));
243
- if (hookResult.systemMessage) {
244
- baseHistory.push({
245
- role: 'user',
246
- content: `[Plugin Stop Hook] ${hookResult.systemMessage}`,
247
- });
248
- }
249
- await goalState.appendProgress(`迭代 ${iterationCount}: Stop hook block,注入反馈继续`);
250
- }
251
- else {
252
- await goalState.appendProgress(`迭代 ${iterationCount}: 无哨兵 / hook pass,继续下一轮`);
253
- }
254
- } while (true);
255
- }
256
- finally {
257
- // 收尾:把循环里临时累积的 history 还原成 baseHistory + 最后一轮 assistant
258
- // 这样 TUI 上看到的就是"一次问答",而不是 N 轮重复
259
- history.length = 0;
260
- history.push(...baseHistory);
261
- if (finalAssistantMsg) {
262
- history.push(finalAssistantMsg);
263
- }
264
- await goalState.cleanup();
265
- }
266
- }
267
- const api = {
268
- async *runCommand(commandName, args, ctx) {
269
- if (commandName === 'ralph-loop') {
270
- yield* runRalphLoop(args, ctx);
271
- return;
272
- }
273
- // 其它命令(help / cancel-ralph)→ 不接管,让框架走声明式 fallback
274
- yield {
275
- type: 'error',
276
- error: `ralph-wiggum: 命令 /${commandName} 未由 plugin.ts 接管`,
277
- };
278
- },
279
- };
280
- export default api;
@@ -1,258 +0,0 @@
1
- import { mkdir, appendFile, writeFile, unlink, rmdir } from 'node:fs/promises';
2
- import { existsSync, readFileSync } from 'node:fs';
3
- import { join } from 'node:path';
4
- export var Phase;
5
- (function (Phase) {
6
- Phase["PLAN"] = "plan";
7
- Phase["BUILD"] = "build";
8
- Phase["VERIFY"] = "verify";
9
- Phase["HEAL"] = "heal";
10
- Phase["DONE"] = "done";
11
- })(Phase || (Phase = {}));
12
- const VALID_PHASES = new Set(Object.values(Phase));
13
- /**
14
- * 阶段转移表使用说明(诚实注释):
15
- *
16
- * 这张表里的 event 名(plan_complete / tests_failing / all_pass 等)是**描述性的**,
17
- * 阶段实际由驱动(plugin.ts)用启发式推进或 `forceSetPhase` 直接设置——
18
- * **不是 LLM 驱动的事件状态机**。
19
- *
20
- * `setPhase` 只用这张表做「目标阶段是否可达」的合法性校验:
21
- * 只要 PHASE_TRANSITIONS[from] 的 values 里包含 to,就视为合法,跟传入的 `reason` 字符串无关。
22
- * `reason` 仅是给人看的日志文本,不参与任何匹配或校验。
23
- *
24
- * 想跳过校验强制切阶段,用 `forceSetPhase`。
25
- */
26
- const PHASE_TRANSITIONS = {
27
- [Phase.PLAN]: {
28
- plan_complete: Phase.BUILD,
29
- stuck: Phase.PLAN,
30
- },
31
- [Phase.BUILD]: {
32
- task_complete: Phase.VERIFY,
33
- need_replan: Phase.PLAN,
34
- tests_failing: Phase.HEAL,
35
- },
36
- [Phase.VERIFY]: {
37
- all_pass: Phase.BUILD,
38
- failures: Phase.HEAL,
39
- goal_complete: Phase.DONE,
40
- },
41
- [Phase.HEAL]: {
42
- fixed: Phase.VERIFY,
43
- cannot_fix_locally: Phase.PLAN,
44
- },
45
- [Phase.DONE]: {},
46
- };
47
- function isLegalTransition(from, to) {
48
- if (from === to)
49
- return true;
50
- const allowed = PHASE_TRANSITIONS[from];
51
- if (!allowed)
52
- return false;
53
- return Object.values(allowed).includes(to);
54
- }
55
- const LEARNINGS_TAIL_LINES = 20;
56
- /** 每个阶段给 LLM 的一行行动指令(fresh-context 下 LLM 看不到 SKILL.md,靠这个 steer 行为)。 */
57
- const PHASE_DIRECTIVES = {
58
- [Phase.PLAN]: '分析任务、拆解步骤、识别风险与未知项;先想清楚再动手。',
59
- [Phase.BUILD]: '按计划实现,一次只聚焦一个子任务,落到具体文件改动。',
60
- [Phase.VERIFY]: '运行测试 / 检查输出 / 核对验收标准,用证据确认,别凭感觉。',
61
- [Phase.HEAL]: '已多次验证失败:停止猜测,定位根因(读报错、加日志、做最小复现)再修,避免回归。',
62
- [Phase.DONE]: '目标已达成。',
63
- };
64
- export class GoalState {
65
- dir;
66
- constructor(workspaceDir, sessionTag) {
67
- const suffix = sessionTag ? `-${sessionTag}` : '';
68
- this.dir = join(workspaceDir, `.minimal-agent${suffix}`);
69
- }
70
- async reset() {
71
- const files = ['goal.md', 'completion.md', 'phase.md', 'progress.md', 'learnings.md', 'decisions.md'];
72
- for (const f of files) {
73
- try {
74
- await unlink(join(this.dir, f));
75
- }
76
- catch {
77
- }
78
- }
79
- }
80
- async init(goal, criteria) {
81
- await mkdir(this.dir, { recursive: true });
82
- const files = {
83
- goal: `# 不可变目标 (IMMUTABLE GOAL)\n\n${goal}\n`,
84
- completion: JSON.stringify(criteria, null, 2),
85
- phase: Phase.PLAN,
86
- progress: '',
87
- learnings: '',
88
- decisions: '',
89
- };
90
- for (const [name, content] of Object.entries(files)) {
91
- const path = join(this.dir, `${name}.md`);
92
- if (!existsSync(path)) {
93
- await writeFile(path, content);
94
- }
95
- }
96
- }
97
- get goal() {
98
- try {
99
- return readFileSync(join(this.dir, 'goal.md'), 'utf8').trim();
100
- }
101
- catch {
102
- return '';
103
- }
104
- }
105
- get completionCriteria() {
106
- try {
107
- const raw = readFileSync(join(this.dir, 'completion.md'), 'utf8').trim();
108
- return JSON.parse(raw);
109
- }
110
- catch {
111
- return [];
112
- }
113
- }
114
- get currentPhase() {
115
- try {
116
- const raw = readFileSync(join(this.dir, 'phase.md'), 'utf8').trim();
117
- if (VALID_PHASES.has(raw)) {
118
- return raw;
119
- }
120
- }
121
- catch {
122
- }
123
- return Phase.PLAN;
124
- }
125
- /**
126
- * 切换阶段。`reason` 是给人看的日志文本,不参与校验。
127
- * 校验只看 from→to 是否在 PHASE_TRANSITIONS 表里有路径(任意 event 通向 to 即合法)。
128
- * 想绕开 FSM 用 forceSetPhase。
129
- */
130
- async setPhase(phase, reason) {
131
- if (!VALID_PHASES.has(phase)) {
132
- throw new Error(`Invalid phase: ${phase}`);
133
- }
134
- const current = this.currentPhase;
135
- if (!isLegalTransition(current, phase)) {
136
- throw new Error(`非法阶段切换: ${current} → ${phase}。该目标阶段不在 PHASE_TRANSITIONS[${current}] 的可达集合内,需要走 forceSetPhase。`);
137
- }
138
- await writeFile(join(this.dir, 'phase.md'), phase);
139
- await this.appendProgress(`PHASE → ${phase}: ${reason}`);
140
- }
141
- async forceSetPhase(phase, reason) {
142
- if (!VALID_PHASES.has(phase)) {
143
- throw new Error(`Invalid phase: ${phase}`);
144
- }
145
- await writeFile(join(this.dir, 'phase.md'), phase);
146
- await this.appendProgress(`PHASE → ${phase}: ${reason}`);
147
- }
148
- async appendProgress(line) {
149
- const timestamp = new Date().toLocaleTimeString('en-US', { hour12: false });
150
- await appendFile(join(this.dir, 'progress.md'), `[${timestamp}] ${line}\n`);
151
- }
152
- tailProgress(n) {
153
- try {
154
- const content = readFileSync(join(this.dir, 'progress.md'), 'utf8');
155
- const lines = content.trim().split('\n').filter(Boolean);
156
- return lines.slice(-n).join('\n');
157
- }
158
- catch {
159
- return '';
160
- }
161
- }
162
- async appendLearning(lesson) {
163
- await appendFile(join(this.dir, 'learnings.md'), `- ${lesson}\n`);
164
- }
165
- get learnings() {
166
- try {
167
- const raw = readFileSync(join(this.dir, 'learnings.md'), 'utf8').trim();
168
- if (!raw)
169
- return '';
170
- const lines = raw.split('\n').filter(Boolean);
171
- return lines.slice(-LEARNINGS_TAIL_LINES).join('\n');
172
- }
173
- catch {
174
- return '';
175
- }
176
- }
177
- async recordDecision(ctx, options, chosen, reasoning) {
178
- const entry = {
179
- iteration: ctx.iteration,
180
- phase: ctx.phase,
181
- contextSummary: ctx.summary,
182
- options,
183
- chosen,
184
- reasoning,
185
- };
186
- const line = JSON.stringify(entry);
187
- await appendFile(join(this.dir, 'decisions.md'), `${line}\n`);
188
- }
189
- summarizeDecisions(maxEntries = 5) {
190
- try {
191
- const content = readFileSync(join(this.dir, 'decisions.md'), 'utf8');
192
- const lines = content.trim().split('\n').filter(Boolean);
193
- const entries = [];
194
- for (const line of lines.slice(-maxEntries * 2)) {
195
- try {
196
- entries.push(JSON.parse(line));
197
- }
198
- catch {
199
- }
200
- }
201
- const sliced = entries.slice(-maxEntries);
202
- // 无有效条目(文件存在但为空)时也统一返回占位文本
203
- if (sliced.length === 0)
204
- return '(无决策记录)';
205
- return sliced
206
- .map((e) => `迭代 ${e.iteration}(${e.phase}): 在 [${e.options.join(', ')}] 中选择了 **${e.chosen}**,原因:${e.reasoning.slice(0, 80)}`)
207
- .join('\n');
208
- }
209
- catch {
210
- return '(无决策记录)';
211
- }
212
- }
213
- composeContext(iteration) {
214
- const sections = [];
215
- // 不可变目标段:去掉 NEED_GOAL_REVISION 提示(已移到通信协议段统一说明)
216
- sections.push(`# 不可变目标 (IMMUTABLE GOAL)\n${this.goal}\n⚠️ 你不能修改或扩大上述目标。`);
217
- // 当前阶段 + 对应的一行行动指令
218
- sections.push(`# 当前阶段: ${this.currentPhase.toUpperCase()}\n${PHASE_DIRECTIVES[this.currentPhase]}`);
219
- // 与循环驱动的通信协议:哨兵约定 + learning 标记
220
- sections.push([
221
- '# 与循环驱动的通信协议(务必遵守)',
222
- '- 任务**真正完成**时输出 `<promise>DONE</promise>`,驱动会运行验证门确认。',
223
- '- 当前方案走不通、要换打法(目标不变)时输出 `<PROMISE>NEED_REPLAN</PROMISE>`。',
224
- '- **目标本身**有问题、不改目标就无法完成时输出 `<PROMISE>NEED_GOAL_REVISION</PROMISE>`,驱动会停止并请人工修订。',
225
- '- 踩到的坑 / 教训用 `<learning>一句话</learning>` 标记,驱动会自动存档供后续迭代参考。',
226
- '⚠️ 切勿为逃出循环而谎报完成——只有陈述完全为真时才输出完成哨兵。',
227
- ].join('\n'));
228
- const lrn = this.learnings;
229
- if (lrn) {
230
- sections.push(`# 关键教训(必须遵守,避免重复踩坑)\n${lrn}`);
231
- }
232
- const decisions = this.summarizeDecisions(3);
233
- if (decisions !== '(无决策记录)') {
234
- sections.push(`# 历史关键决策(请参考,避免重复试错)\n${decisions}`);
235
- }
236
- const recentProgress = this.tailProgress(10);
237
- if (recentProgress) {
238
- sections.push(`# 最近进度\n${recentProgress}`);
239
- }
240
- sections.push(`---\n\n# 本轮任务 (迭代 ${iteration})`);
241
- return sections.join('\n\n---\n\n');
242
- }
243
- async cleanup() {
244
- const files = ['goal.md', 'completion.md', 'phase.md', 'progress.md', 'learnings.md', 'decisions.md'];
245
- for (const f of files) {
246
- try {
247
- await unlink(join(this.dir, f));
248
- }
249
- catch {
250
- }
251
- }
252
- try {
253
- await rmdir(this.dir);
254
- }
255
- catch {
256
- }
257
- }
258
- }
@@ -1,76 +0,0 @@
1
- /**
2
- * ============================================================
3
- * plugins/ralph-wiggum/src/sentinels.ts —— LLM 输出哨兵协议
4
- * ------------------------------------------------------------
5
- * Ralph 通过"在回答里输出特定标签"与循环驱动(plugin.ts)通信:
6
- *
7
- * <promise>DONE</promise> / <promise>COMPLETE</promise> / <promise>GOAL_COMPLETE</promise>
8
- * (或用户经 `--completion-promise "<短语>"` 指定的自定义短语)
9
- * → 当前轮 LLM 认为目标完成;驱动进入验证门(--verify)确认后才真正退出。
10
- *
11
- * <PROMISE>NEED_REPLAN</PROMISE>
12
- * → 当前方案走不通、需要换打法(**目标不变**);驱动强转回 PLAN 阶段。
13
- *
14
- * <PROMISE>NEED_GOAL_REVISION</PROMISE>
15
- * → **目标本身**有问题,无法在不改目标的前提下完成;驱动**干净终止**,请人工修订目标
16
- * (区别于 NEED_REPLAN:那是换方案,这是换目标)。
17
- *
18
- * <learning>一句话</learning>(可多次)
19
- * → LLM 主动记录的踩坑 / 教训;驱动**每轮**提取并追加到 learnings.md,
20
- * 让经验跨 fresh-context 迭代存活——这是 Ralph 能收敛的关键。
21
- *
22
- * 哨兵集中在这里方便审计与扩展;匹配全部 case-insensitive。
23
- * ⚠️ rich 插件路径下 commands/ralph-loop.md 的 prompt body **不会**喂给 LLM
24
- * (pluginRunner 命中 runCommand 即接管),故以上协议由 GoalState.composeContext
25
- * 注入 prompt,本文件只负责"解析 LLM 回吐的标签"。
26
- * ============================================================
27
- */
28
- /** 写死的完成短语集合(不带 --completion-promise 时只认这三个,逐字节兼容旧行为)。 */
29
- const BUILTIN_COMPLETE_RE = /<promise>(?:COMPLETE|DONE|GOAL_COMPLETE)<\/promise>/i;
30
- const NEED_REPLAN_RE = /<PROMISE>NEED_REPLAN<\/PROMISE>/i;
31
- const NEED_GOAL_REVISION_RE = /<PROMISE>NEED_GOAL_REVISION<\/PROMISE>/i;
32
- const LEARNING_RE = /<learning>([\s\S]*?)<\/learning>/gi;
33
- /** 转义正则元字符,让用户自定义的 promise 短语按字面量匹配(含空格 / 标点也安全)。 */
34
- function escapeRegExp(s) {
35
- return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
36
- }
37
- /**
38
- * 构造完成哨兵判定器。
39
- * - 不传 / 空白 customPhrase → 只认写死集合(与旧行为逐字节一致)。
40
- * - 传 customPhrase → 写死集合 ∪ `<promise><customPhrase></promise>`(**超集**,向后兼容)。
41
- * customPhrase 内外空白裁剪;正则元字符转义;标签内允许首尾空白;大小写不敏感。
42
- *
43
- * 设计:始终接受写死集合,所以即便用户设了自定义短语,LLM 输出 `<promise>DONE</promise>`
44
- * 仍然有效——composeContext 因此可以无脑教 LLM 用 DONE,不必知道自定义短语是什么。
45
- */
46
- export function makeCompletionMatcher(customPhrase) {
47
- const phrase = customPhrase?.trim();
48
- if (!phrase) {
49
- return (text) => BUILTIN_COMPLETE_RE.test(text);
50
- }
51
- const customRe = new RegExp(`<promise>\\s*${escapeRegExp(phrase)}\\s*<\\/promise>`, 'i');
52
- return (text) => BUILTIN_COMPLETE_RE.test(text) || customRe.test(text);
53
- }
54
- /** 旧调用兼容:等价于 makeCompletionMatcher()(text),只认写死集合。 */
55
- export function hasCompleteSentinel(text) {
56
- return BUILTIN_COMPLETE_RE.test(text);
57
- }
58
- export function hasNeedReplanSentinel(text) {
59
- return NEED_REPLAN_RE.test(text);
60
- }
61
- export function hasNeedGoalRevisionSentinel(text) {
62
- return NEED_GOAL_REVISION_RE.test(text);
63
- }
64
- /**
65
- * 提取 LLM 本轮输出里所有 `<learning>…</learning>` 文本。
66
- * 多空白折叠成单空格、去首尾、丢弃空项;无标签返回空数组。
67
- */
68
- export function extractLearnings(text) {
69
- const out = [];
70
- for (const m of text.matchAll(LEARNING_RE)) {
71
- const lesson = m[1].replace(/\s+/g, ' ').trim();
72
- if (lesson)
73
- out.push(lesson);
74
- }
75
- return out;
76
- }