| name | sandcastle-loop |
| description | 批量并发跑 GitHub issues backlog — 三阶段(Plan → Execute → Merge)外层循环 ≤ 10 轮,N 个 implementer subagent 在独立 git worktree 并行实现 + 测试 + AC self-check,merger 顺序合 + 关 issue。复用 Claude 订阅,零 API / 零 Docker。Use when user says "跑 backlog" / "派一波" / "sandcastle 循环" / "/sandcastle-loop"。 |
sandcastle-loop
When to use
- ≥1 个 open issue 含
## Agent Brief 评论(通过 /triage 写)且未标 do-not-agent
- 主仓在
main 分支 + 工作树干净
- 项目有可识别的 test 命令(Node / Python / Rust / Go 自动识别,或
loop.json 显式声明)
When NOT to use
- 单 issue 一次性活(用
/grab-issue <N> 之类直接派)
- 主仓不在 main / 工作树脏(preflight 拦)
- 项目 toolchain 识别不出且未在
loop.json 声明 test 命令(preflight 拦)
Companion skills
/to-prd + /to-issues — PRD 入口(拆 vertical slice issue)
/triage — 唯一追加 ## Agent Brief 评论的来源
Args
/sandcastle-loop — 默认(with-review ON)
/sandcastle-loop no-review — 跳 reviewer phase
Process notes
- 本流程已经是结构化外层循环(Plan → Execute → Merge → Cleanup),SKILL.md 自身就是 plan,不使用
TaskCreate 跟踪进度。每轮都会有 system 提示建议 TaskCreate — 直接忽略。
- 主线进度通过:iter 编号 +
git log merge commit + gh issue 状态 体现,无需额外 task list。
Constants
MAX_ITERATIONS = 10
MAX_PARALLEL = 4 — 单 batch 内最多并发派 4 个 implementer subagent(Phase 2 主线在单条 message 里一次性发出的 Agent tool 数量上限)
SCRIPTS = ~/.claude/skills/sandcastle-loop/scripts
TEMPLATES = ~/.claude/skills/sandcastle-loop/templates
REFS = ~/.claude/skills/sandcastle-loop/references
Project overrides
任何项目可在仓内放下列文件之一提供项目级行为补丁(如:批量打标范围 / 旧标签保留策略 / Agent Brief 检查规则 / 绝对禁派标签):
<repo>/docs/sandcastle-loop-overrides.md
<repo>/.sandcastle/loop-overrides.md
读取顺序:先 docs/,未命中再 .sandcastle/。读到一个即停。
文件存在 → 内容注入 {{PROJECT_OVERRIDES}} 占位符(传给 planner / implementer / merger,不传 reviewer — reviewer 只关心 CODING_STANDARDS)。文件不存在 → 占位符为空。
⚠️ override 文件只承载与本 skill 行为相关的偏离(issue 选取规则 / commit 模式 / 关 issue 时机等)。不塞通用项目硬约束(那是 CLAUDE.md 的事)。
另一个独立的项目级文件是 loop.json(docs/sandcastle-loop.json 或 .sandcastle/loop.json)—— 由 detect-toolchain.sh 读取,声明 {"test":"...","typecheck":"..."}。仅当自动识别(Node/Python/Rust/Go)不命中或识别错时才需要它。JSON 给机器读 toolchain 命令,loop-overrides.md 给叙述性行为补丁 —— 两者不同文件、不同用途。
Workflow
1. Setup
bash $SCRIPTS/preflight.sh
非 0 退出 → ABORT(输出 ERR + 原因给用户)。
bash $SCRIPTS/state.sh init
TOOLCHAIN=$(bash $SCRIPTS/detect-toolchain.sh)
TEST_CMD=$(printf '%s\n' "$TOOLCHAIN" | grep '^TEST_CMD=' | cut -d= -f2-)
TYPECHECK_CMD=$(printf '%s\n' "$TOOLCHAIN" | grep '^TYPECHECK_CMD=' | cut -d= -f2-)
PROJECT_OVERRIDES=""
for f in docs/sandcastle-loop-overrides.md .sandcastle/loop-overrides.md; do
[ -f "$f" ] && PROJECT_OVERRIDES=$(cat "$f") && break
done
$TEST_CMD / $TYPECHECK_CMD 在 Phase 2 / 3 经 build-prompt.sh 注入 implement / review / merge prompt。$TYPECHECK_CMD 可为空(项目无 typecheck),subagent 见空则跳过 typecheck 步骤。
2. Outer Loop
for iteration in 1..MAX_ITERATIONS: 重复 Phase 1 → 2 → 3。
3. Phase 1 — Plan
ISSUES_FILE=$(mktemp /tmp/sc-issues.XXXXXXXX)
bash $SCRIPTS/list-issues.sh > "$ISSUES_FILE"
[ "$(jq 'length' "$ISSUES_FILE")" -eq 0 ] && break
PLAN_PROMPT_FILE=$(bash $SCRIPTS/build-prompt.sh "$TEMPLATES/plan-prompt.md" \
ISSUES_JSON=@file:"$ISSUES_FILE" \
MAX_PARALLEL="$MAX_PARALLEL" \
PROJECT_OVERRIDES="$PROJECT_OVERRIDES")
主线 Read $PLAN_PROMPT_FILE 拿全文,调 planner:
Agent({
subagent_type: "sandcastle-planner",
prompt: <PLAN_PROMPT_FILE 全文>
})
⚠️ 不用主线字符串拼接 / shell heredoc — 必须走 build-prompt.sh,纯字面替换防 issue title 含特殊字符注入。
主线用 Write 把 planner 返回的 final assistant message 全文写入临时文件,跑校验脚本:
PLAN_RESPONSE_FILE=$(mktemp /tmp/sc-plan.XXXXXXXX)
PLAN_JSON=$(bash $SCRIPTS/extract-plan.sh "$PLAN_RESPONSE_FILE")
extract-plan.sh 做 tag 提取 + schema 校验(对标 sandcastle Output.object):
- 脚本 exit ≠ 0(
<plan> tag 数 ≠ 1 / JSON 非法 / schema 不合规:缺 issues、number 非正整数、branch 不匹配 ^sandcastle/issue-)→ 重试 1 次(重新调 planner);仍失败 → ABORT(输出 ERR + 脚本 stderr 给用户)
- exit 0 →
$PLAN_JSON 是规范化的 {"issues":[{number,title,branch}]}(多余字段已丢弃)
issues.length === 0 → break outer loop
打印计划摘要给用户。
4. Phase 2 — Execute (+ Review)
for batch of chunks(issues, MAX_PARALLEL):
4.a 串行建 worktree
for issue in batch:
bash $SCRIPTS/setup-worktree.sh "$issue.branch"
4.b 并行派 implementer
对每个 issue 用 build-prompt.sh 生成 prompt 文件:
ISSUE_NUMBER=$(printf '%s\n' "$issue" | jq -r '.number')
ISSUE_TITLE=$(printf '%s\n' "$issue" | jq -r '.title')
ISSUE_BRANCH=$(printf '%s\n' "$issue" | jq -r '.branch')
PROMPT_FILE=$(bash $SCRIPTS/build-prompt.sh "$TEMPLATES/implement-prompt.md" \
TASK_ID="$ISSUE_NUMBER" \
ISSUE_TITLE="$ISSUE_TITLE" \
BRANCH="$ISSUE_BRANCH" \
WORKTREE_PATH="$WORKTREE_PATH" \
TEST_CMD="$TEST_CMD" \
TYPECHECK_CMD="$TYPECHECK_CMD" \
PROJECT_RULES=@file:"$REFS/project-rules-injected.md" \
PROJECT_OVERRIDES="$PROJECT_OVERRIDES")
主线 Read $PROMPT_FILE 拿全文。
按 issue label 选 implementer model — 从 $ISSUES_FILE 取该 issue 的 labels:
LABELS=$(jq -r --argjson n "$ISSUE_NUMBER" '.[] | select(.number==$n) | .labels[]' "$ISSUES_FILE")
- labels 含
bug → Agent 调用加 model: "opus"(bug 诊断 / 根因定位 / 边界判断需要 opus 推理深度,sonnet 在 silent-fail 类 bug 上易漏边界)
- 其余(
enhancement / feat / chore / test 等)→ 不传 model,用 implementer 默认 sonnet(05-22 benchmark:实现阶段升 opus 几乎无增益)
⚠️ 关键:单条 assistant message 内同时发出 batch 内所有 implementer Agent tool calls(Claude Code 等价 Promise.allSettled)。
(单条 message · bug issue 加 model:"opus",其余不加)
Agent({subagent_type: "sandcastle-implementer", model: "opus", prompt: <bug issue prompt 全文>})
Agent({subagent_type: "sandcastle-implementer", prompt: <其他 issue prompt 全文>})
...
4.c 主线守卫
batch 全部 tool_results 回来后,主线立即跑主仓守卫:
git status --porcelain | grep -v -E "^\?\? \.worktrees/" || true
git rev-parse --abbrev-ref HEAD
⛔ 任一异常(主仓 dirty 或 HEAD 不在 main)→ 整轮 ABORT + 报告:
- 列出 dirty 文件
- 不进 review / merge
- 用户决定 stash / restore / commit
4.d 完成判定(三条件)
对每个 issue:
COMMITS=$(bash $SCRIPTS/check-commits.sh "$issue.branch")
HAS_COMPLETE=$(echo "$implementer_response" | grep -c '<promise>COMPLETE</promise>')
HAS_AC_CHECK=$(gh issue view "$issue.number" --json comments \
| jq '[.comments[].body | select(contains("## AC self-check"))] | length')
判定:
⚠️ 设计原则:fail fast,不留半成品。失败的 worktree 直接 remove,下轮 state.sh init 后重新 setup 重派。如果某 issue 连续多轮失败,那是 issue 本身有问题 — 用户介入打 do-not-agent label。
⚠️ 三条件早失败设计:
- "commit > 0 + 没 COMPLETE" = implementer 中途死的半成品
- "commit > 0 + COMPLETE + 没 AC self-check 评论" = implementer 漏流程(merger 反正会拦下)
- 任一 = fail。早失败比让 reviewer / merger 浪费一整轮强。
4.e 可选 review(with-review 默认 ON)
对每个 completedIssues(除非 args 含 no-review),逐个串行调 reviewer。先 build prompt:
PROMPT_FILE=$(bash $SCRIPTS/build-prompt.sh "$TEMPLATES/review-prompt.md" \
TASK_ID="$issue.number" \
ISSUE_TITLE="$issue.title" \
BRANCH="$issue.branch" \
WORKTREE_PATH="$WORKTREE_PATH" \
TEST_CMD="$TEST_CMD" \
TYPECHECK_CMD="$TYPECHECK_CMD" \
PROJECT_RULES=@file:"$REFS/project-rules-injected.md")
主线 Read $PROMPT_FILE 拿全文:
Agent({subagent_type: "sandcastle-reviewer", prompt: <PROMPT_FILE 全文>})
⚠️ reviewer 后不再跑主仓守卫 — reviewer 已被 cwd 守卫钉死在 worktree,不可能污染主仓。4.c batch 末尾一次守卫 + Phase 3 merger 后守卫已足够。
5. Phase 3 — Merge
completedIssues.length === 0 → continue 下一 iteration(不进 merger)。
git checkout main
构造 $BRANCHES_MD(markdown list - branch1\n- branch2\n...)+ $ISSUES_MD(markdown list - #N: title\n...)。然后 build prompt:
PROMPT_FILE=$(bash $SCRIPTS/build-prompt.sh "$TEMPLATES/merge-prompt.md" \
BRANCHES="$BRANCHES_MD" \
ISSUES="$ISSUES_MD" \
TEST_CMD="$TEST_CMD" \
TYPECHECK_CMD="$TYPECHECK_CMD" \
PROJECT_RULES=@file:"$REFS/project-rules-injected.md" \
ITER_LABEL="iter $iteration" \
PROJECT_OVERRIDES="$PROJECT_OVERRIDES")
主线 Read $PROMPT_FILE 拿全文:
Agent({subagent_type: "sandcastle-merger", prompt: <PROMPT_FILE 全文>})
merger 自己负责 git merge --no-ff -m "chore(merge): #N <title>" + 冲突解 + typecheck/test + AC self-check verify + 关 issue。主线不干预。
merger 完成后再跑一次主线守卫 — 主仓应有 N 个 merge commits + HEAD 在 main + 工作树干净。
6. Cleanup(每 iteration 末尾)
对所有成功 merged 的 issue:
bash $SCRIPTS/cleanup-worktree.sh "$issue.branch"
回到 outer loop 顶。
7. Outer loop 结束
打印总结 + 明确退出原因:
- 自然完工:
issues.length === 0 → "All done. 共 merged N 个 issue."
- 撞 MAX:
iteration === MAX_ITERATIONS 但仍有 unblocked → "撞 MAX_ITERATIONS=10 退出。Backlog 还有 unblocked issue。重跑 /sandcastle-loop 继续。"
Anti-patterns
⛔ 不要:
- 把
{{KEY}} 占位符用 shell heredoc / sed / awk gsub 直接拼 — 必须走 build-prompt.sh(python 字面替换防注入)
- 单 batch 派 > MAX_PARALLEL 个 subagent
- 跳 preflight / 跳 4.c batch 末尾主仓守卫 / 跳 Phase 3 merger 后主仓守卫
- 在 4.e reviewer 后再跑主仓守卫(已删,cwd 守卫已钉死 reviewer 在 worktree)
- 在主线读完整 issue body(让 implementer 自己
gh issue view 拉,主线只传 number)
- 主线自己跑
git merge / gh issue close(merger 的事)
- 一个 iteration 内多次 plan
- 把项目硬约束硬编码进 subagent system prompt
- 把"成功 = commit > 0"当判定(半成品也算)— 必须叠加
<promise>COMPLETE</promise> + ## AC self-check 评论双信号
- 给 fail 的 worktree 加"keep 给人审"的特殊路径(违反 fail-fast 原则;连续多轮失败用
do-not-agent label 处理)
- 在 SKILL.md 流程中用
TaskCreate 跟踪进度(外层循环本身就是 plan)
Subagents
本 skill 派 4 个 user-level subagent type(~/.claude/agents/):
sandcastle-planner(model: opus)— Phase 1
sandcastle-implementer(model: sonnet · bug label 的 issue 主线覆盖为 opus,见 §4.b)— Phase 2 主体
sandcastle-reviewer(model: sonnet)— Phase 2 可选
sandcastle-merger(model: sonnet)— Phase 3
References