1实现原理 · 为什么它能做到
Stop hook 是循环真正的引擎,而不是 SKILL 文本:插件用 matcher "*" 注册 Claude Code 的 Stop 事件,命令指向插件内的 stop-hook.sh(timeout 30s)。agent 每次想结束回合时该脚本都会被宿主执行,脚本以 stdout 的 JSON 决定放行或把 agent 拽回下一轮——『不停歇』由宿主 hook 协议实现。
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/stop-hook.sh",
循环状态落在项目内的 .claude/ralph-marketer-loop.local.md:YAML frontmatter 携带 iteration / max_iterations / completion_promise,脚本用 grep+sed 读出并自增 iteration(先试 BSD 的 sed -i '' 再回退 GNU sed -i)。状态文件不存在即视为无活动循环,直接放行。
if [ ! -f "$LOOP_STATE_FILE" ]; then
终止条件有三重兜底:① 上一条助手消息(jq 读 stdin 的 .transcript[-1].content)命中 <promise>COMPLETION_PROMISE</promise> → 删状态文件并返回 {"decision":"allow"};② NEXT_ITERATION 超过 max_iterations 时同样删文件放行;③ iteration 字段非数字则判定状态文件损坏,打印告警、删除、放行,避免坏状态锁死会话。
"decision": "allow", "message": "Ralph loop completed successfully!"
未终止时脚本把拼好的 SYSTEM_MSG 以 {"decision": "block", "message": "$SYSTEM_MSG"} 输出,宿主据此拒绝停止、把 message 当作新一轮输入:消息写明第 N 轮/共 M 轮、提示『同样的任务再次喂给你,上一轮成果在文件与 git 历史里』、列出 prd.json 与 progress.txt 路径,并附完成信号格式。
"decision": "block",
状态与素材持久化在 SQLite(./data/content.db):init.js 先 mkdirSync(dirname(DB_PATH), { recursive: true }),开启 foreign_keys,再建覆盖 DISCOVER/LEARN/RESEARCH/IDEATE/WRITE-CRITIQUE-ITERATE-PUBLISH 全流程的 14 张表(founder_content、competitor_content、trends、research、communications、voice_profile、content_patterns、research_briefs、content_ideas、content_plan、drafts、critique_results、published、agent_log),状态字段用 CHECK 约束把管线状态机固化进 schema。
status TEXT DEFAULT 'planned' CHECK(status IN ('planned', 'researching', 'writing', 'critiquing', 'iterating', 'review', 'published', 'cancelled')),
seed.js 让项目开箱即有输入:先 DELETE FROM 各表清空,再注入样例 founder 内容、1 份 voice DNA(语气/句式/口头禅/回避模式)、竞品 gap、趋势、研究、公司 com 与 patterns,最后写一条 agent_log 'database_seeded'——全是虚构样例(example.com / TechCorp),用于拉起管线而非真实客户数据。
INSERT INTO voice_profile ( profile_name, tone, formality, sentence_patterns, paragraph_style,
任务分解走 PRD:templates/prd.json 以 userStories 数组给出 12 条故事(SETUP/PLAN/WRITE/REVIEW/PUBLISH/SOCIAL/NEWSLETTER/METRICS),每条含 description、acceptanceCriteria 列表、priority、passes、notes;agent 每轮取 priority 最小且 passes:false 的那条执行,完成后回写 passes:true 并向 progress.txt 追加日志。
"Create draft in drafts table linked to plan", "Draft is at least 800 words", "Includes all key messages from the communication",
跨轮记忆只靠文件与 git:每轮都是全新上下文窗口,记忆由 SQLite + prd.json + progress.txt + git 提交承载;progress.txt 模板要求每次完成 story 后追加『日期 / STORY-ID / 字数 / 数据库变更 / 产出文件 / learnings』,并保持 Codebase Patterns 段可复用。
Each iteration is a fresh context window. Memory persists through files.
四个 slash command 各司其职并做模型/工具分级:ralph-init 建目录、从 ${CLAUDE_PLUGIN_ROOT} 复制脚本与模板、npm install + npm run db:reset;ralph-marketer 定义迭代协议(model sonnet,allowed-tools 含 WebFetch);ralph-status 只读汇报(model haiku,仅 Bash/Read);ralph-cancel 删除状态文件停止循环。
rm -f .claude/ralph-marketer-loop.local.md 2>/dev/null && echo "Loop state cleared" || echo "No active loop found"
2核心能力
3外部依赖
| 类型 | 依赖 |
|---|---|
| package | better-sqlite3 ^11.0.0(唯一运行时依赖,同步 SQLite 驱动) |
| network | npm registry(安装依赖时拉包) |
| package | prebuild-install / node-gyp 链路(better-sqlite3 安装期原生模块构建,hasInstallScript=true) |
| cli | node |
| cli | npm(install / run db:reset / test) |
| cli | jq(stop-hook.sh 解析宿主传入的 transcript JSON;缺失时静默降级为空串) |
| cli | sed/grep/tr/rm(POSIX 工具链,hook 内解析与改写状态文件) |
| cli | git(每轮 add/commit,兼作记忆与回滚点) |
| network | WebFetch(宿主工具,仅 /ralph-marketer 命令显式授权;插件脚本自身无任何网络调用,竞品/趋势研究抓哪些公开网页由 agent 决定) |
4风险提醒 风险提醒:黄色 · 留意使用
- 无上限循环 = 无上限 token 账单 — 默认 unlimited(commands/ralph-marketer.md '--max-iterations <n>: Stop after N iterations (default: unlimited)',正文并写 'You have unlimited iterations.')。每轮都要读库 + 读 PRD + 读 progress + 跑 npm test + 生成长文草稿,token 消耗远超一次性 skill;自停依赖模型每轮自评全部 story 都 passes:true,若 PRD 与库状态不一致(或 jq 缺失使完成信号检测静默失效)则永不自停,只能靠 /ralph-cancel 或显式 --max-iterations 兜底。建议始终显式传 --max-iterations。
- hook 每次停止都执行 shell,且带写操作 — matcher "*" 使其在所有会话的每次 Stop 事件触发(有状态文件时),脚本会 sed -i 改写 .claude/ralph-marketer-loop.local.md,必要时 rm 之。虽是本地文件、逻辑简单,但『每次停下都被执行』的风险画像与纯 prompt skill 完全不同;宿主若放宽 hook 权限需自行评估。
- 状态文件即指令源(prompt 注入面) — hook 把状态文件 frontmatter 之后的文本原样拼进回注消息当新一轮输入,prompt.md 亦然。任何能写这两个文件的人或进程(含共享仓库/共享工作目录的其他人)都能向循环注入指令;多 agent 或多用户共用目录时尤需注意。
- 对标外内容无隔离:间接注入 + 事实污染 — trends/research/competitor_content 与 WebFetch 抓回的网页文本会被 agent 当写作依据(prompt.md『Read source materials』),既是 prompt 注入面,也是事实错误与版权风险的入口:竞品文案、抓取片段可能被改写成对外发布内容。发布前必须人工复核(这正是流水线里 REVIEW/PUBLISH 分工存在的意义)。
- 写入路径相对 cwd,且 git add -A — db 落在 ./data/content.db、产出落在 content/**,均由当前工作目录决定;prompt.md Step 5 要求 'git add -A',在错误目录运行会把无关改动一并提交。使用前应确认目录干净并先 commit 基线。
- 工程细节缺口会直接让功能不工作 — ① 无任何命令创建 .claude/ralph-marketer-loop.local.md → 按仓库代码循环不会自行启动;② hook 依赖 jq 但未声明,缺失时完成信号检测静默失效;③ README 的 schema 段与实际 14 张表不同步;④ templates/progress.txt 引用不存在的 scripts/ralph/ralph.sh。以上属可用性缺陷,不是安全问题。
- 许可证证据不完整 — 仓库无 LICENSE 文件,只有 README/package.json/plugin.json/marketplace.json 的 MIT 文本声明,GitHub API license=null。企业内二次分发前建议要求作者补文件(本侦查不构成法律意见)。
5第二遍独立确认
- [ok] Stop hook 注册与执行时机 — hooks/hooks.json 仅一条 hook:事件 Stop、matcher "*"、command ${CLAUDE_PLUGIN_ROOT}/hooks/stop-hook.sh、timeout 30;stop-hook.sh 逐行确认接收 stdin JSON、无状态文件即 exit 0、有状态文件才介入。
- [discrepancy] 「循环如何被启动」——第一遍默认成立,第二遍证伪 — 全仓 grep ralph-marketer-loop 只命中 3 处:stop-hook.sh 读取/更新、ralph-cancel.md 删除、.gitignore 忽略。没有任何命令或脚本创建该状态文件(ralph-marketer.md 全文仅描述迭代协议与 $ARGUMENTS,无写入指令)。结论:按仓库代码,/ralph-marketer 执行后若无外部手段创建状态文件,hook 不会拦停、循环不会启动——插件缺『arm loop』这一步,使用前需自行创建 .claude/ralph-marketer-loop.local.md(或依赖未入库的 ralph.sh,见下条)。
- [discrepancy] progress.txt 引用的 scripts/ralph/ralph.sh — templates/progress.txt 第 37 行 'Loop script: `scripts/ralph/ralph.sh`' 与 README 的『Ralph is a Bash loop』叙事互相印证,但 find 全量文件确认仓库内不存在 ralph.sh;实际循环由 Stop hook 实现。该引用是遗留/未入库说明,照模板找脚本会扑空。
- [unlocatable] 脚本内是否有网络调用或凭证读取 — 对 .js/.sh 检索 fetch/axios/http./API_KEY/api_key/process.env/curl/wget/token/secret 零命中——即『未发现任何一处』;但无法据此证明运行期绝对无网络(npm 安装与 agent 的 WebFetch 属外部工具面),故按 unlocatable 记录,并在 security.reason 中以这两条外部面说明档位。
- [discrepancy] jq 依赖未声明,缺失时完成信号静默失效 — stop-hook.sh 用 jq 提取 .transcript[-1].content,但 README/命令文档均未提 jq 依赖,脚本以 '2>/dev/null || echo ""' 静默降级——jq 缺失时 LAST_OUTPUT 恒为空,<promise>COMPLETE</promise> 永不触发,循环只能靠 max_iterations 或 /ralph-cancel 结束。属真实使用陷阱(安全上无害,安全上是『无法自停』的风险)。
- [ok] 状态文件容错与 sed -i 跨平台兼容 — iteration 非数字 → 告警 + rm 状态文件 + exit 0(防坏状态锁死);max_iterations 非数字 → 回退 999999;sed -i 先试 BSD 写法(sed -i '')再回退 GNU 写法;set -e 与 `||` 组合不会误退。
- [ok] 回注内容来源与拼装是否如实 — PROMPT=$(sed -n '/^---$/,/^---$/!p' "$LOOP_STATE_FILE") 取 frontmatter 之后全文;空则 cat scripts/ralph/prompt.md;再空则一行默认文案。最终 cat << EOF 输出 {"decision": "block", "message": "$SYSTEM_MSG"} —— 与 Claude Code Stop hook 协议相符(协议本身由宿主定义,不在本仓可校验范围)。
- [ok] SQLite 表数与 schema 完整性 — init.js 内共 14 个 CREATE TABLE(founder_content、competitor_content、trends、research、communications、voice_profile、content_patterns、research_briefs、content_ideas、content_plan、drafts、critique_results、published、agent_log),与其 console.log 自述一致;content_plan/drafts 有 CHECK 枚举与外键,foreign_keys 已开启,DB 路径相对 cwd。
6结论
525719530027f17d…4c1f5f3bdf