minimal-agent 0.1.8 → 0.2.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 (51) hide show
  1. package/README.md +405 -122
  2. package/dist/main.js +423 -941
  3. package/package.json +5 -2
  4. package/plugins/HOW-TO-WRITE-A-PLUGIN.md +186 -0
  5. package/plugins/ralph-wiggum/.claude-plugin/plugin.json +9 -0
  6. package/plugins/ralph-wiggum/README.md +179 -0
  7. package/plugins/ralph-wiggum/commands/cancel-ralph.md +18 -0
  8. package/plugins/ralph-wiggum/commands/help.md +126 -0
  9. package/plugins/ralph-wiggum/commands/ralph-loop.md +59 -0
  10. package/plugins/ralph-wiggum/hooks/hooks.json +15 -0
  11. package/plugins/ralph-wiggum/hooks/stop-hook.sh +191 -0
  12. package/plugins/ralph-wiggum/plugin.ts +275 -0
  13. package/plugins/ralph-wiggum/scripts/setup-ralph-loop.sh +203 -0
  14. package/plugins/ralph-wiggum/src/goalState.ts +310 -0
  15. package/plugins/ralph-wiggum/src/sentinels.ts +24 -0
  16. package/plugins/ralph-wiggum/src/stopHookRunner.ts +136 -0
  17. package/plugins/ralph-wiggum/src/verificationGate.ts +252 -0
  18. package/plugins/ralph-wiggum/test/goalState.test.ts +410 -0
  19. package/plugins/ralph-wiggum/test/verificationGate.test.ts +122 -0
  20. package/plugins/workflow-runner/.claude-plugin/plugin.json +5 -0
  21. package/plugins/workflow-runner/commands/workflow.md +15 -0
  22. package/plugins/workflow-runner/commands/workflows.md +8 -0
  23. package/plugins/workflow-runner/plugin.ts +42 -0
  24. package/plugins/workflow-runner/src/expressions.ts +371 -0
  25. package/plugins/workflow-runner/src/index.ts +194 -0
  26. package/plugins/workflow-runner/src/loader.ts +193 -0
  27. package/plugins/workflow-runner/src/runner.ts +313 -0
  28. package/plugins/workflow-runner/src/stepExecutors/assert.ts +30 -0
  29. package/plugins/workflow-runner/src/stepExecutors/llm.ts +54 -0
  30. package/plugins/workflow-runner/src/stepExecutors/skill.ts +115 -0
  31. package/plugins/workflow-runner/src/stepExecutors/tool.ts +41 -0
  32. package/plugins/workflow-runner/src/types.ts +183 -0
  33. package/plugins/workflow-runner/src/workflowState.ts +65 -0
  34. package/plugins/workflow-runner/test/cli.e2e.test.ts +114 -0
  35. package/plugins/workflow-runner/test/e2e.test.ts +268 -0
  36. package/plugins/workflow-runner/test/expressions.test.ts +140 -0
  37. package/plugins/workflow-runner/test/fixtures/cli-e2e.yaml +27 -0
  38. package/plugins/workflow-runner/test/fixtures/hello-workflow.yaml +49 -0
  39. package/plugins/workflow-runner/test/graceful.test.ts +139 -0
  40. package/plugins/workflow-runner/test/loader.test.ts +216 -0
  41. package/plugins/workflow-runner/test/pluginRunner.isolation.test.ts +230 -0
  42. package/plugins/workflow-runner/test/runner.test.ts +511 -0
  43. package/skills/config/SKILL.md +27 -1
  44. package/skills/image-gen-openrouter/SKILL.md +121 -0
  45. package/skills/subtitle-srt/SKILL.md +134 -0
  46. package/skills/tts-zh/SKILL.md +137 -0
  47. package/skills/video-compose/SKILL.md +139 -0
  48. package/workflows/book-review-short.yaml +99 -0
  49. package/workflows/e2e-write-greet.yaml +27 -0
  50. package/workflows/schema.json +74 -0
  51. package/workflows/youtube-shorts.yaml +171 -0
@@ -0,0 +1,191 @@
1
+ #!/bin/bash
2
+
3
+ # Ralph Wiggum Stop Hook
4
+ # Prevents session exit when a ralph-loop is active
5
+ # Feeds Claude's output back as input to continue the loop
6
+ #
7
+ # NOTE (minimal-agent):
8
+ # This hook is the original Claude Code contract — kept here so the plugin
9
+ # stays portable to upstream. In minimal-agent the loop is driven natively
10
+ # by src/plugins/pluginRunner.ts; this hook is consulted as an *advisory*
11
+ # signal (block → inject reason into next iteration; pass / errors → no
12
+ # effect on loop termination). The loop's real exit conditions are:
13
+ # - <promise>DONE</promise> sentinel + verification pass
14
+ # - --max-iterations reached
15
+ # - user abort / NEED_REPLAN
16
+ # On Windows minimal-agent skips this hook entirely (bash unavailable).
17
+ # The .minimal-agent/ralph-loop.local.md state file referenced below is
18
+ # NOT created by minimal-agent — GoalState writes goal.md/phase.md/etc
19
+ # instead, so this hook simply exits 0 in minimal-agent runs.
20
+
21
+ set -euo pipefail
22
+
23
+ # Read hook input from stdin (advanced stop hook API)
24
+ HOOK_INPUT=$(cat)
25
+
26
+ # Check if ralph-loop is active
27
+ RALPH_STATE_FILE=".minimal-agent/ralph-loop.local.md"
28
+
29
+ if [[ ! -f "$RALPH_STATE_FILE" ]]; then
30
+ # No active loop - allow exit
31
+ exit 0
32
+ fi
33
+
34
+ # Parse markdown frontmatter (YAML between ---) and extract values
35
+ FRONTMATTER=$(sed -n '/^---$/,/^---$/{ /^---$/d; p; }' "$RALPH_STATE_FILE")
36
+ ITERATION=$(echo "$FRONTMATTER" | grep '^iteration:' | sed 's/iteration: *//')
37
+ MAX_ITERATIONS=$(echo "$FRONTMATTER" | grep '^max_iterations:' | sed 's/max_iterations: *//')
38
+ # Extract completion_promise and strip surrounding quotes if present
39
+ COMPLETION_PROMISE=$(echo "$FRONTMATTER" | grep '^completion_promise:' | sed 's/completion_promise: *//' | sed 's/^"\(.*\)"$/\1/')
40
+
41
+ # Validate numeric fields before arithmetic operations
42
+ if [[ ! "$ITERATION" =~ ^[0-9]+$ ]]; then
43
+ echo "⚠️ Ralph loop: State file corrupted" >&2
44
+ echo " File: $RALPH_STATE_FILE" >&2
45
+ echo " Problem: 'iteration' field is not a valid number (got: '$ITERATION')" >&2
46
+ echo "" >&2
47
+ echo " This usually means the state file was manually edited or corrupted." >&2
48
+ echo " Ralph loop is stopping. Run /ralph-loop again to start fresh." >&2
49
+ rm "$RALPH_STATE_FILE"
50
+ exit 0
51
+ fi
52
+
53
+ if [[ ! "$MAX_ITERATIONS" =~ ^[0-9]+$ ]]; then
54
+ echo "⚠️ Ralph loop: State file corrupted" >&2
55
+ echo " File: $RALPH_STATE_FILE" >&2
56
+ echo " Problem: 'max_iterations' field is not a valid number (got: '$MAX_ITERATIONS')" >&2
57
+ echo "" >&2
58
+ echo " This usually means the state file was manually edited or corrupted." >&2
59
+ echo " Ralph loop is stopping. Run /ralph-loop again to start fresh." >&2
60
+ rm "$RALPH_STATE_FILE"
61
+ exit 0
62
+ fi
63
+
64
+ # Check if max iterations reached
65
+ if [[ $MAX_ITERATIONS -gt 0 ]] && [[ $ITERATION -ge $MAX_ITERATIONS ]]; then
66
+ echo "🛑 Ralph loop: Max iterations ($MAX_ITERATIONS) reached."
67
+ rm "$RALPH_STATE_FILE"
68
+ exit 0
69
+ fi
70
+
71
+ # Get transcript path from hook input
72
+ TRANSCRIPT_PATH=$(echo "$HOOK_INPUT" | jq -r '.transcript_path')
73
+
74
+ if [[ ! -f "$TRANSCRIPT_PATH" ]]; then
75
+ echo "⚠️ Ralph loop: Transcript file not found" >&2
76
+ echo " Expected: $TRANSCRIPT_PATH" >&2
77
+ echo " This is unusual and may indicate a Claude Code internal issue." >&2
78
+ echo " Ralph loop is stopping." >&2
79
+ rm "$RALPH_STATE_FILE"
80
+ exit 0
81
+ fi
82
+
83
+ # Read last assistant message from transcript (JSONL format - one JSON per line)
84
+ # First check if there are any assistant messages
85
+ if ! grep -q '"role":"assistant"' "$TRANSCRIPT_PATH"; then
86
+ echo "⚠️ Ralph loop: No assistant messages found in transcript" >&2
87
+ echo " Transcript: $TRANSCRIPT_PATH" >&2
88
+ echo " This is unusual and may indicate a transcript format issue" >&2
89
+ echo " Ralph loop is stopping." >&2
90
+ rm "$RALPH_STATE_FILE"
91
+ exit 0
92
+ fi
93
+
94
+ # Extract last assistant message with explicit error handling
95
+ LAST_LINE=$(grep '"role":"assistant"' "$TRANSCRIPT_PATH" | tail -1)
96
+ if [[ -z "$LAST_LINE" ]]; then
97
+ echo "⚠️ Ralph loop: Failed to extract last assistant message" >&2
98
+ echo " Ralph loop is stopping." >&2
99
+ rm "$RALPH_STATE_FILE"
100
+ exit 0
101
+ fi
102
+
103
+ # Parse JSON with error handling
104
+ LAST_OUTPUT=$(echo "$LAST_LINE" | jq -r '
105
+ .message.content |
106
+ map(select(.type == "text")) |
107
+ map(.text) |
108
+ join("\n")
109
+ ' 2>&1)
110
+
111
+ # Check if jq succeeded
112
+ if [[ $? -ne 0 ]]; then
113
+ echo "⚠️ Ralph loop: Failed to parse assistant message JSON" >&2
114
+ echo " Error: $LAST_OUTPUT" >&2
115
+ echo " This may indicate a transcript format issue" >&2
116
+ echo " Ralph loop is stopping." >&2
117
+ rm "$RALPH_STATE_FILE"
118
+ exit 0
119
+ fi
120
+
121
+ if [[ -z "$LAST_OUTPUT" ]]; then
122
+ echo "⚠️ Ralph loop: Assistant message contained no text content" >&2
123
+ echo " Ralph loop is stopping." >&2
124
+ rm "$RALPH_STATE_FILE"
125
+ exit 0
126
+ fi
127
+
128
+ # Check for completion promise (only if set)
129
+ if [[ "$COMPLETION_PROMISE" != "null" ]] && [[ -n "$COMPLETION_PROMISE" ]]; then
130
+ # Extract text from <promise> tags using Perl for multiline support
131
+ # -0777 slurps entire input, s flag makes . match newlines
132
+ # .*? is non-greedy (takes FIRST tag), whitespace normalized
133
+ PROMISE_TEXT=$(echo "$LAST_OUTPUT" | perl -0777 -pe 's/.*?<promise>(.*?)<\/promise>.*/$1/s; s/^\s+|\s+$//g; s/\s+/ /g' 2>/dev/null || echo "")
134
+
135
+ # Use = for literal string comparison (not pattern matching)
136
+ # == in [[ ]] does glob pattern matching which breaks with *, ?, [ characters
137
+ if [[ -n "$PROMISE_TEXT" ]] && [[ "$PROMISE_TEXT" = "$COMPLETION_PROMISE" ]]; then
138
+ echo "✅ Ralph loop: Detected <promise>$COMPLETION_PROMISE</promise>"
139
+ rm "$RALPH_STATE_FILE"
140
+ exit 0
141
+ fi
142
+ fi
143
+
144
+ # Not complete - continue loop with SAME PROMPT
145
+ NEXT_ITERATION=$((ITERATION + 1))
146
+
147
+ # Extract prompt (everything after the closing ---)
148
+ # Skip first --- line, skip until second --- line, then print everything after
149
+ # Use i>=2 instead of i==2 to handle --- in prompt content
150
+ PROMPT_TEXT=$(awk '/^---$/{i++; next} i>=2' "$RALPH_STATE_FILE")
151
+
152
+ if [[ -z "$PROMPT_TEXT" ]]; then
153
+ echo "⚠️ Ralph loop: State file corrupted or incomplete" >&2
154
+ echo " File: $RALPH_STATE_FILE" >&2
155
+ echo " Problem: No prompt text found" >&2
156
+ echo "" >&2
157
+ echo " This usually means:" >&2
158
+ echo " • State file was manually edited" >&2
159
+ echo " • File was corrupted during writing" >&2
160
+ echo "" >&2
161
+ echo " Ralph loop is stopping. Run /ralph-loop again to start fresh." >&2
162
+ rm "$RALPH_STATE_FILE"
163
+ exit 0
164
+ fi
165
+
166
+ # Update iteration in frontmatter (portable across macOS and Linux)
167
+ # Create temp file, then atomically replace
168
+ TEMP_FILE="${RALPH_STATE_FILE}.tmp.$$"
169
+ sed "s/^iteration: .*/iteration: $NEXT_ITERATION/" "$RALPH_STATE_FILE" > "$TEMP_FILE"
170
+ mv "$TEMP_FILE" "$RALPH_STATE_FILE"
171
+
172
+ # Build system message with iteration count and completion promise info
173
+ if [[ "$COMPLETION_PROMISE" != "null" ]] && [[ -n "$COMPLETION_PROMISE" ]]; then
174
+ SYSTEM_MSG="🔄 Ralph iteration $NEXT_ITERATION | To stop: output <promise>$COMPLETION_PROMISE</promise> (ONLY when statement is TRUE - do not lie to exit!)"
175
+ else
176
+ SYSTEM_MSG="🔄 Ralph iteration $NEXT_ITERATION | No completion promise set - loop runs infinitely"
177
+ fi
178
+
179
+ # Output JSON to block the stop and feed prompt back
180
+ # The "reason" field contains the prompt that will be sent back to Claude
181
+ jq -n \
182
+ --arg prompt "$PROMPT_TEXT" \
183
+ --arg msg "$SYSTEM_MSG" \
184
+ '{
185
+ "decision": "block",
186
+ "reason": $prompt,
187
+ "systemMessage": $msg
188
+ }'
189
+
190
+ # Exit 0 for successful hook execution
191
+ exit 0
@@ -0,0 +1,275 @@
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
+
23
+ import { fileURLToPath } from 'node:url';
24
+ import { dirname } from 'node:path';
25
+
26
+ import {
27
+ runQuery,
28
+ getWorkingDir,
29
+ type PluginApi,
30
+ type PluginContext,
31
+ type LoopEvent,
32
+ type Message,
33
+ } from '../../src/plugin-sdk.ts';
34
+
35
+ import { GoalState, Phase } from './src/goalState.ts';
36
+ import { parseVerifyArgs, runVerification } from './src/verificationGate.ts';
37
+ import {
38
+ hasCompleteSentinel,
39
+ hasNeedReplanSentinel,
40
+ } from './src/sentinels.ts';
41
+ import { executeStopHook } from './src/stopHookRunner.ts';
42
+
43
+ const PLUGIN_NAME = 'ralph-wiggum';
44
+ const DEFAULT_MAX_ITERATIONS = 50;
45
+ const SAFETY_CEILING = 200;
46
+
47
+ const PLUGIN_ROOT = dirname(fileURLToPath(import.meta.url));
48
+
49
+ function extractMaxIterations(args: string): number | undefined {
50
+ const match = args.match(/--max-iterations\s+(\d+)/i);
51
+ return match ? parseInt(match[1], 10) : undefined;
52
+ }
53
+
54
+ async function* runRalphLoop(
55
+ args: string,
56
+ ctx: PluginContext,
57
+ ): AsyncGenerator<LoopEvent, void, void> {
58
+ const { provider, history, signal } = ctx;
59
+
60
+ const maxIter = Math.min(
61
+ extractMaxIterations(args) ?? DEFAULT_MAX_ITERATIONS,
62
+ SAFETY_CEILING,
63
+ );
64
+
65
+ // 没有 args(用户只敲 /ralph-loop)→ 留个最小 goal placeholder,避免 GoalState.init 拒空串
66
+ const userGoal = args.trim() || '(未提供目标)';
67
+
68
+ yield {
69
+ type: 'plugin_progress',
70
+ pluginId: PLUGIN_NAME,
71
+ current: 0,
72
+ max: maxIter,
73
+ message: 'Ralph Wiggum copy-task loop 启动',
74
+ };
75
+
76
+ const checks = parseVerifyArgs(args);
77
+
78
+ // sessionTag = 插件名 → 多插件可并发不打架,/new 也能扫到清掉
79
+ const goalState = new GoalState(getWorkingDir(), PLUGIN_NAME);
80
+ await goalState.reset();
81
+ await goalState.init(userGoal, checks);
82
+ await goalState.appendProgress(
83
+ `=== Loop 启动 === 目标: ${userGoal.slice(0, 120)}...`,
84
+ );
85
+
86
+ // 进循环前快照 history —— 每轮 runQuery 前用它重置,保证 fresh context
87
+ const baseHistory = history.slice();
88
+
89
+ let iterationCount = 0;
90
+ let consecutiveFailures = 0;
91
+ let currentInput = userGoal;
92
+ let finalAssistantMsg: Message | null = null;
93
+
94
+ try {
95
+ do {
96
+ iterationCount++;
97
+ if (iterationCount > maxIter) {
98
+ await goalState.forceSetPhase(Phase.DONE, `达到迭代上限 ${maxIter}`);
99
+ await goalState.appendLearning(
100
+ `[迭代上限] 循环在 ${iterationCount - 1} 轮后强制终止,可能目标过大或陷入死循环`,
101
+ );
102
+ yield {
103
+ type: 'error',
104
+ error: `Loop 已达迭代上限 ${maxIter},自动停止`,
105
+ };
106
+ return;
107
+ }
108
+
109
+ if (signal?.aborted) {
110
+ yield { type: 'interrupted' };
111
+ return;
112
+ }
113
+
114
+ yield {
115
+ type: 'plugin_progress',
116
+ pluginId: PLUGIN_NAME,
117
+ current: iterationCount,
118
+ max: maxIter,
119
+ };
120
+
121
+ // fresh context:清空 history,重置为入循环前的快照
122
+ history.length = 0;
123
+ history.push(...baseHistory);
124
+
125
+ const freshContext = goalState.composeContext(iterationCount);
126
+ const enhancedInput = `${freshContext}\n\n${currentInput}`;
127
+
128
+ yield* runQuery(enhancedInput, {
129
+ provider,
130
+ history,
131
+ signal,
132
+ maxTurns: ctx.maxTurns,
133
+ sessionState: ctx.sessionState,
134
+ });
135
+
136
+ if (signal?.aborted) {
137
+ yield { type: 'interrupted' };
138
+ return;
139
+ }
140
+
141
+ // 从本轮 history 抓最后一个 assistant 消息(runQuery 已 push 进去)
142
+ const lastAssistantIdx = (() => {
143
+ for (let i = history.length - 1; i >= 0; i--) {
144
+ if (history[i].role === 'assistant') return i;
145
+ }
146
+ return -1;
147
+ })();
148
+ finalAssistantMsg =
149
+ lastAssistantIdx >= 0 ? history[lastAssistantIdx] : null;
150
+ const lastAssistantText = finalAssistantMsg
151
+ ? typeof finalAssistantMsg.content === 'string'
152
+ ? finalAssistantMsg.content
153
+ : JSON.stringify(finalAssistantMsg.content)
154
+ : '';
155
+
156
+ if (hasCompleteSentinel(lastAssistantText)) {
157
+ // 哨兵:可能来自任意阶段(包括 iter 1 的 PLAN),用 force 跳到 VERIFY
158
+ await goalState.forceSetPhase(Phase.VERIFY, '检测到完成哨兵,进入验证');
159
+ await goalState.appendProgress(
160
+ `迭代 ${iterationCount}: 检测到完成哨兵,运行验证门...`,
161
+ );
162
+
163
+ if (checks.length > 0) {
164
+ const vResult = await runVerification(checks);
165
+
166
+ if (!vResult.passed) {
167
+ consecutiveFailures++;
168
+ await goalState.appendLearning(
169
+ `[迭代 ${iterationCount}] 声称完成但验证未通过: ${vResult.summary}`,
170
+ );
171
+ yield {
172
+ type: 'error',
173
+ error: `⚠️ 验证未通过: ${vResult.summary}。继续尝试...`,
174
+ };
175
+
176
+ if (consecutiveFailures >= 3) {
177
+ await goalState.forceSetPhase(
178
+ Phase.HEAL,
179
+ `连续 ${consecutiveFailures} 次验证失败`,
180
+ );
181
+ } else {
182
+ await goalState.setPhase(Phase.BUILD, '验证未通过,返回构建');
183
+ }
184
+ continue;
185
+ }
186
+
187
+ await goalState.appendProgress(`✅ 验证通过: ${vResult.summary}`);
188
+ }
189
+
190
+ await goalState.setPhase(Phase.DONE, 'goal complete & verified');
191
+ yield {
192
+ type: 'plugin_iteration',
193
+ pluginName: PLUGIN_NAME,
194
+ current: iterationCount,
195
+ max: maxIter,
196
+ };
197
+ return;
198
+ }
199
+
200
+ if (hasNeedReplanSentinel(lastAssistantText)) {
201
+ await goalState.forceSetPhase(Phase.PLAN, 'agent 请求重新规划');
202
+ await goalState.appendLearning(
203
+ '[NEED_REPLAN] Agent 认为当前方案不可行,需要重新规划',
204
+ );
205
+ await goalState.appendProgress(
206
+ 'Agent 请求 NEED_REPLAN,回 PLAN 阶段',
207
+ );
208
+ consecutiveFailures = 0;
209
+ continue;
210
+ }
211
+
212
+ // PLAN 阶段跑过一轮还没哨兵,认为规划已完成,自动推进 BUILD
213
+ if (goalState.currentPhase === Phase.PLAN && iterationCount >= 2) {
214
+ await goalState.setPhase(Phase.BUILD, '规划阶段已完成,进入构建');
215
+ }
216
+
217
+ // Stop-hook 咨询式调用:block 才把 reason 注入下一轮 prompt
218
+ // pass / 报错都不再终止循环;终止由 sentinel/maxIter/abort/NEED_REPLAN 独占
219
+ const hookResult = await executeStopHook(PLUGIN_ROOT, lastAssistantText);
220
+
221
+ if (hookResult.decision === 'block' && hookResult.reason) {
222
+ currentInput = hookResult.reason;
223
+ consecutiveFailures = 0;
224
+ await goalState.recordDecision(
225
+ {
226
+ iteration: iterationCount,
227
+ phase: goalState.currentPhase,
228
+ summary: 'stop-hook 反馈',
229
+ },
230
+ ['继续循环', '终止'],
231
+ '继续循环',
232
+ hookResult.reason.slice(0, 200),
233
+ );
234
+ if (hookResult.systemMessage) {
235
+ baseHistory.push({
236
+ role: 'user',
237
+ content: `[Plugin Stop Hook] ${hookResult.systemMessage}`,
238
+ });
239
+ }
240
+ await goalState.appendProgress(
241
+ `迭代 ${iterationCount}: Stop hook block,注入反馈继续`,
242
+ );
243
+ } else {
244
+ await goalState.appendProgress(
245
+ `迭代 ${iterationCount}: 无哨兵 / hook pass,继续下一轮`,
246
+ );
247
+ }
248
+ } while (true);
249
+ } finally {
250
+ // 收尾:把循环里临时累积的 history 还原成 baseHistory + 最后一轮 assistant
251
+ // 这样 TUI 上看到的就是"一次问答",而不是 N 轮重复
252
+ history.length = 0;
253
+ history.push(...baseHistory);
254
+ if (finalAssistantMsg) {
255
+ history.push(finalAssistantMsg);
256
+ }
257
+ await goalState.cleanup();
258
+ }
259
+ }
260
+
261
+ const api: PluginApi = {
262
+ async *runCommand(commandName, args, ctx) {
263
+ if (commandName === 'ralph-loop') {
264
+ yield* runRalphLoop(args, ctx);
265
+ return;
266
+ }
267
+ // 其它命令(help / cancel-ralph)→ 不接管,让框架走声明式 fallback
268
+ yield {
269
+ type: 'error',
270
+ error: `ralph-wiggum: 命令 /${commandName} 未由 plugin.ts 接管`,
271
+ };
272
+ },
273
+ };
274
+
275
+ export default api;
@@ -0,0 +1,203 @@
1
+ #!/bin/bash
2
+
3
+ # Ralph Loop Setup Script
4
+ # Creates state file for in-session Ralph loop
5
+
6
+ set -euo pipefail
7
+
8
+ # Parse arguments
9
+ PROMPT_PARTS=()
10
+ MAX_ITERATIONS=0
11
+ COMPLETION_PROMISE="null"
12
+
13
+ # Parse options and positional arguments
14
+ while [[ $# -gt 0 ]]; do
15
+ case $1 in
16
+ -h|--help)
17
+ cat << 'HELP_EOF'
18
+ Ralph Loop - Interactive self-referential development loop
19
+
20
+ USAGE:
21
+ /ralph-loop [PROMPT...] [OPTIONS]
22
+
23
+ ARGUMENTS:
24
+ PROMPT... Initial prompt to start the loop (can be multiple words without quotes)
25
+
26
+ OPTIONS:
27
+ --max-iterations <n> Maximum iterations before auto-stop (default: unlimited)
28
+ --completion-promise '<text>' Promise phrase (USE QUOTES for multi-word)
29
+ -h, --help Show this help message
30
+
31
+ DESCRIPTION:
32
+ Starts a Ralph Wiggum loop in your CURRENT session. The stop hook prevents
33
+ exit and feeds your output back as input until completion or iteration limit.
34
+
35
+ To signal completion, you must output: <promise>YOUR_PHRASE</promise>
36
+
37
+ Use this for:
38
+ - Interactive iteration where you want to see progress
39
+ - Tasks requiring self-correction and refinement
40
+ - Learning how Ralph works
41
+
42
+ EXAMPLES:
43
+ /ralph-loop Build a todo API --completion-promise 'DONE' --max-iterations 20
44
+ /ralph-loop --max-iterations 10 Fix the auth bug
45
+ /ralph-loop Refactor cache layer (runs forever)
46
+ /ralph-loop --completion-promise 'TASK COMPLETE' Create a REST API
47
+
48
+ STOPPING:
49
+ Only by reaching --max-iterations or detecting --completion-promise
50
+ No manual stop - Ralph runs infinitely by default!
51
+
52
+ MONITORING:
53
+ # View current iteration:
54
+ grep '^iteration:' .minimal-agent/ralph-loop.local.md
55
+
56
+ # View full state:
57
+ head -10 .minimal-agent/ralph-loop.local.md
58
+ HELP_EOF
59
+ exit 0
60
+ ;;
61
+ --max-iterations)
62
+ if [[ -z "${2:-}" ]]; then
63
+ echo "❌ Error: --max-iterations requires a number argument" >&2
64
+ echo "" >&2
65
+ echo " Valid examples:" >&2
66
+ echo " --max-iterations 10" >&2
67
+ echo " --max-iterations 50" >&2
68
+ echo " --max-iterations 0 (unlimited)" >&2
69
+ echo "" >&2
70
+ echo " You provided: --max-iterations (with no number)" >&2
71
+ exit 1
72
+ fi
73
+ if ! [[ "$2" =~ ^[0-9]+$ ]]; then
74
+ echo "❌ Error: --max-iterations must be a positive integer or 0, got: $2" >&2
75
+ echo "" >&2
76
+ echo " Valid examples:" >&2
77
+ echo " --max-iterations 10" >&2
78
+ echo " --max-iterations 50" >&2
79
+ echo " --max-iterations 0 (unlimited)" >&2
80
+ echo "" >&2
81
+ echo " Invalid: decimals (10.5), negative numbers (-5), text" >&2
82
+ exit 1
83
+ fi
84
+ MAX_ITERATIONS="$2"
85
+ shift 2
86
+ ;;
87
+ --completion-promise)
88
+ if [[ -z "${2:-}" ]]; then
89
+ echo "❌ Error: --completion-promise requires a text argument" >&2
90
+ echo "" >&2
91
+ echo " Valid examples:" >&2
92
+ echo " --completion-promise 'DONE'" >&2
93
+ echo " --completion-promise 'TASK COMPLETE'" >&2
94
+ echo " --completion-promise 'All tests passing'" >&2
95
+ echo "" >&2
96
+ echo " You provided: --completion-promise (with no text)" >&2
97
+ echo "" >&2
98
+ echo " Note: Multi-word promises must be quoted!" >&2
99
+ exit 1
100
+ fi
101
+ COMPLETION_PROMISE="$2"
102
+ shift 2
103
+ ;;
104
+ *)
105
+ # Non-option argument - collect all as prompt parts
106
+ PROMPT_PARTS+=("$1")
107
+ shift
108
+ ;;
109
+ esac
110
+ done
111
+
112
+ # Join all prompt parts with spaces
113
+ PROMPT="${PROMPT_PARTS[*]}"
114
+
115
+ # Validate prompt is non-empty
116
+ if [[ -z "$PROMPT" ]]; then
117
+ echo "❌ Error: No prompt provided" >&2
118
+ echo "" >&2
119
+ echo " Ralph needs a task description to work on." >&2
120
+ echo "" >&2
121
+ echo " Examples:" >&2
122
+ echo " /ralph-loop Build a REST API for todos" >&2
123
+ echo " /ralph-loop Fix the auth bug --max-iterations 20" >&2
124
+ echo " /ralph-loop --completion-promise 'DONE' Refactor code" >&2
125
+ echo "" >&2
126
+ echo " For all options: /ralph-loop --help" >&2
127
+ exit 1
128
+ fi
129
+
130
+ # Create state file for stop hook (markdown with YAML frontmatter)
131
+ mkdir -p .minimal-agent
132
+
133
+ # Quote completion promise for YAML if it contains special chars or is not null
134
+ if [[ -n "$COMPLETION_PROMISE" ]] && [[ "$COMPLETION_PROMISE" != "null" ]]; then
135
+ COMPLETION_PROMISE_YAML="\"$COMPLETION_PROMISE\""
136
+ else
137
+ COMPLETION_PROMISE_YAML="null"
138
+ fi
139
+
140
+ cat > .minimal-agent/ralph-loop.local.md <<EOF
141
+ ---
142
+ active: true
143
+ iteration: 1
144
+ max_iterations: $MAX_ITERATIONS
145
+ completion_promise: $COMPLETION_PROMISE_YAML
146
+ started_at: "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
147
+ ---
148
+
149
+ $PROMPT
150
+ EOF
151
+
152
+ # Output setup message
153
+ cat <<EOF
154
+ 🔄 Ralph loop activated in this session!
155
+
156
+ Iteration: 1
157
+ Max iterations: $(if [[ $MAX_ITERATIONS -gt 0 ]]; then echo $MAX_ITERATIONS; else echo "unlimited"; fi)
158
+ Completion promise: $(if [[ "$COMPLETION_PROMISE" != "null" ]]; then echo "${COMPLETION_PROMISE//\"/} (ONLY output when TRUE - do not lie!)"; else echo "none (runs forever)"; fi)
159
+
160
+ The stop hook is now active. When you try to exit, the SAME PROMPT will be
161
+ fed back to you. You'll see your previous work in files, creating a
162
+ self-referential loop where you iteratively improve on the same task.
163
+
164
+ To monitor: head -10 .minimal-agent/ralph-loop.local.md
165
+
166
+ ⚠️ WARNING: This loop cannot be stopped manually! It will run infinitely
167
+ unless you set --max-iterations or --completion-promise.
168
+
169
+ 🔄
170
+ EOF
171
+
172
+ # Output the initial prompt if provided
173
+ if [[ -n "$PROMPT" ]]; then
174
+ echo ""
175
+ echo "$PROMPT"
176
+ fi
177
+
178
+ # Display completion promise requirements if set
179
+ if [[ "$COMPLETION_PROMISE" != "null" ]]; then
180
+ echo ""
181
+ echo "═══════════════════════════════════════════════════════════"
182
+ echo "CRITICAL - Ralph Loop Completion Promise"
183
+ echo "═══════════════════════════════════════════════════════════"
184
+ echo ""
185
+ echo "To complete this loop, output this EXACT text:"
186
+ echo " <promise>$COMPLETION_PROMISE</promise>"
187
+ echo ""
188
+ echo "STRICT REQUIREMENTS (DO NOT VIOLATE):"
189
+ echo " ✓ Use <promise> XML tags EXACTLY as shown above"
190
+ echo " ✓ The statement MUST be completely and unequivocally TRUE"
191
+ echo " ✓ Do NOT output false statements to exit the loop"
192
+ echo " ✓ Do NOT lie even if you think you should exit"
193
+ echo ""
194
+ echo "IMPORTANT - Do not circumvent the loop:"
195
+ echo " Even if you believe you're stuck, the task is impossible,"
196
+ echo " or you've been running too long - you MUST NOT output a"
197
+ echo " false promise statement. The loop is designed to continue"
198
+ echo " until the promise is GENUINELY TRUE. Trust the process."
199
+ echo ""
200
+ echo " If the loop should stop, the promise statement will become"
201
+ echo " true naturally. Do not force it by lying."
202
+ echo "═══════════════════════════════════════════════════════════"
203
+ fi