Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/agents/orchestrator.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ EVERY `gh` invocation — by you and by every agent you spawn — MUST go throug
## Your loop
1. **Scope.** Decompose the task into sub-tasks that are *independent* and *non-overlapping at the file level*. Use the `modules` map in `gates.json` to assign each sub-task to exactly one module/path. If two sub-tasks would touch the same files, either merge them into one sub-task or sequence them (declare the dependency). Scale effort to complexity: a trivial task gets ONE worker and no parallelism — do not fan out for its own sake.
2. **Present the plan and WAIT.** Output the plan: each sub-task's title, target module/path, owner boundary, dependencies, and which reviewers will gate it. Enter plan mode and wait for human approval before any code is written. This is the planning checkpoint.
3. **Delegate.** For each approved sub-task, spawn an `implementer` (it runs in its own git worktree/branch, so workers never clash). Respect `budget.max_parallel_workers` from `gates.json` — queue the rest. Give each implementer: the objective, its module boundary ("never edit outside `<path>`"), the definition of done, and the required gates.
3. **Delegate.** For each approved sub-task, spawn an `implementer` (it runs in its own git worktree/branch, so workers never clash). Respect `budget.max_parallel_workers` from `gates.json` — queue the rest. Give each implementer: the objective, its module boundary ("never edit outside `<path>`"), the definition of done, and the required gates. If an approved plan (issue #100's plan gate) was provided for this issue, forward it VERBATIM to the implementer and to every reviewer in step 4 as authoritative scope.
4. **Review gate.** When an implementer reports done, route its change through `reviewer` agents (one per lens in `gates.json.review.lenses`). Spawn each reviewer with the model from `budget.reviewer_models[<lens>]`, falling back to `budget.reviewer_model`. Require the configured majority/consensus to approve. On reject, feed the reasons back to the same implementer; on the re-review, re-run ONLY the lenses that rejected — an approval stands unless the fix touched files outside what that lens already approved. Do not advance a sub-task until its gates pass.
5. **Integrate.** Use the merge discipline from `CLAUDE.md` (default: PR-per-agent). Surface conflicts to the user; do not force-merge.
6. **Report.** End with a structured status block (see below).
Expand Down
3 changes: 3 additions & 0 deletions .claude/agents/reviewer.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ If you touch GitHub at all (e.g. `gh pr diff`, `gh pr view`, `gh api`), route it
1. Read the diff and the surrounding code it affects. Stay scoped: the diff plus what it touches — don't crawl the repo. For long test/build logs, filter to the relevant lines (`grep`/`tail`) instead of reading whole outputs into context.
2. Apply ONLY your assigned lens — go deep, not broad:
- **correctness**: logic errors, edge cases, off-by-one, error handling, race conditions, broken invariants.
If the task provides an APPROVED PLAN / authoritative scope (e.g. issue #100's plan gate), also verify
the diff stays within it — a diff that exceeds the approved plan's declared files or approach is a
valid reject under this lens ("exceeds approved scope").
- **tests**: do tests actually exercise the change? coverage of edge/failure paths? meaningful assertions, not just "it runs"? Run the test gate if needed.
- **security**: injection, auth/access control, unsafe input, secrets, dependency risk, (for smart contracts) reentrancy/overflow/access — defer to the project security skill if configured.
- **performance**: needless work, N+1, allocations, blocking calls, complexity regressions.
Expand Down
5 changes: 4 additions & 1 deletion .claude/gates.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,5 +71,8 @@
},

"_notify_note": "issue #99 — needs-human push-notification seam, read by .claude/scripts/notify.sh (GATES_FILE-aware; same empty-means-skip convention as `gates`). Empty string = disabled (offline/CI-safe no-op) — the default; fill in a command to enable push notifications when the autonomous loop blocks on YOU (PR ready for review, CHANGES_REQUESTED addressed and awaiting re-review, attempt-budget/stall escalation, ...). Contract: severity/title/body-line reach the command BOTH as positional args ($1/$2/$3) and as NOTIFY_SEVERITY/NOTIFY_TITLE/NOTIFY_BODY env vars; throttled to one notification per (kind,target) per window (default 1800s — see notify.sh). Example commands (uncomment/adapt ONE):\n ntfy: curl -s -d \"$NOTIFY_BODY\" -H \"Title: $NOTIFY_TITLE\" -H \"Priority: $NOTIFY_SEVERITY\" ntfy.sh/<your-topic>\n notify-send: notify-send \"$NOTIFY_TITLE\" \"$NOTIFY_BODY\"\n webhook: curl -s -X POST -H 'Content-Type: application/json' -d \"{\\\"severity\\\":\\\"$NOTIFY_SEVERITY\\\",\\\"title\\\":\\\"$NOTIFY_TITLE\\\",\\\"body\\\":\\\"$NOTIFY_BODY\\\"}\" https://example.invalid/hook",
"notify": ""
"notify": "",

"_plan_note": "issue #100 — optional spec/plan gate before implementation starts. plan.gate: off (default; today's single-pass advance behavior, unchanged) | label (gate only planned issues that ALSO carry a plan-first label) | always (gate every planned+module:* issue). Unknown/missing value falls back to off. When gated, the loop posts ONE structured plan comment on the issue (marked `<!-- plan-gate:plan -->`) and labels it plan-review + needs-human instead of implementing; the owner reviews the comment and either replaces plan-review with plan-approved (approve — the next tick implements with the plan injected into the implementer and reviewers as authoritative scope) or removes plan-review (request changes — the loop re-plans). Owner-only approval is NOT identity-enforced — same limitation as the `planned` label itself (see docs/USAGE.md). Read by loop-census.sh/loop-tick.sh/loop-event.sh.",
"plan": { "gate": "off" }
}
84 changes: 84 additions & 0 deletions .claude/scripts/loop-census.sh
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,31 @@
# advance_ready=<n|none> lowest-numbered planned issue with no branch,
# only when open_prs=0 (the ADVANCE precondition)
# AND not blocked by an open "Blocked by #N" edge
# plan_wait=<n> one line per candidate that would otherwise be
# advance_ready but is awaiting owner review of a
# posted plan (labelled `plan-review`, no
# `plan-approved` yet) — issue #100's plan gate,
# see PLAN GATE below. Only emitted when
# plan.gate != "off".
# advance_mode=plan|implement-gated|implement only emitted alongside a
# non-"none" advance_ready when plan.gate !=
# "off" (issue #100) — tells the tick which
# driver prompt variant to build.
# cadence=FAST|WATCH|IDLE cron=<expr> desired cadence per the loop policy
#
# --- PLAN GATE (issue #100) -------------------------------------------------
# Optional, adapter-configured via plan.gate ("off" default | "label" |
# "always"; see gates.json). When enabled, each candidate's labels classify it
# as needs-plan (no plan posted yet — advance_mode=plan gates it into a
# PLAN-ONLY driver turn), awaiting-owner (`plan-review` label present, no
# `plan-approved` yet — treated like an open "Blocked by" edge: skipped for
# BOTH advance_ready and fallback_ready, reported via plan_wait=<n>),
# gated-approved (`plan-approved` present — advance_mode=implement-gated, the
# approved plan comment is injected into the implementer/reviewers as
# authoritative scope), or ungated (advance_mode=implement, today's behavior).
# plan.gate="off" makes every line above a no-op — census output stays
# byte-identical to pre-#100 behavior.
#
# The module label set is derived from $GATES_FILE (default .claude/gates.json)
# → modules[].name, so the same script serves the self-hosted loop
# (GATES_FILE=.claude/self/gates.json) and downstream adopters.
Expand Down Expand Up @@ -114,6 +137,19 @@ stall_minutes=$(node -e '
' "$gates" 2>/dev/null)
case "$stall_minutes" in ''|*[!0-9]*) stall_minutes=30 ;; esac

# Plan gate mode (issue #100), adapter-configurable via plan.gate: "off"
# (default — this whole feature is a no-op, census output stays byte-identical
# to pre-#100 behavior) | "label" (gate only candidates that ALSO carry the
# plan-first label) | "always" (gate every planned+module candidate). Unknown/
# missing value falls back to "off". Read ONCE, same node -e / require(gates)
# pattern as base/module_labels/stall_minutes above.
plan_mode=$(node -e '
const g = require(process.argv[1]);
const v = (g.plan && g.plan.gate) || "off";
console.log(["off", "label", "always"].includes(v) ? v : "off");
' "$gates" 2>/dev/null)
case "$plan_mode" in off|label|always) ;; *) plan_mode=off ;; esac

events_file="${CLAUDE_EVENTS_FILE:-$root/.claude/state/events.jsonl}"

# --- stall detection helper (issue #98) --------------------------------------
Expand Down Expand Up @@ -213,6 +249,9 @@ detail=""
in_flight=""
stalled_lines=""
blocked_lines=""
plan_wait_lines=""
advance_plan_state=""
fallback_plan_state=""
while IFS=$'\t' read -r num labels title; do
[ -z "${num:-}" ] && continue
hit=0
Expand All @@ -234,10 +273,41 @@ while IFS=$'\t' read -r num labels title; do
if [ "$branch" = "none" ] && [ "$open_prs" -eq 0 ] && ! driver_unit_active "$num"; then
eligible=1
fi

# --- plan gate (issue #100): derive this candidate's plan state from its
# labels. When gated (mode==always, or mode==label+plan-first) and not yet
# plan-reviewed/approved, an "awaiting-owner" candidate (plan posted, owner
# hasn't approved/rejected it yet) is NOT eligible for advance — the same
# treatment as a "Blocked by" dependency: it's blocked on a human, not ready
# to advance. Entirely a no-op (plan_state stays "ungated", eligible
# untouched) when plan_mode=off, so this feature costs nothing on the
# default path and census output stays byte-identical.
plan_state="ungated"
if [ "$plan_mode" != "off" ]; then
case ",$labels," in
*",plan-approved,"*) plan_state="gated-approved" ;;
*",plan-review,"*) plan_state="awaiting-owner" ;;
*)
gated=0
if [ "$plan_mode" = "always" ]; then
gated=1
else
case ",$labels," in *",plan-first,"*) gated=1 ;; esac
fi
[ "$gated" -eq 1 ] && plan_state="needs-plan"
;;
esac
if [ "$plan_state" = "awaiting-owner" ]; then
eligible=0
plan_wait_lines+="plan_wait=$num"$'\n'
fi
fi

# fallback_ready: lowest-numbered otherwise-eligible candidate, IGNORING the
# blocking-graph gate — used only if the gate leaves advance_ready="none".
if [ "$eligible" -eq 1 ] && [ "$fallback_ready" = "none" ]; then
fallback_ready="$num"
fallback_plan_state="$plan_state"
fi
if [ "$eligible" -eq 1 ] && [ "$advance_ready" = "none" ]; then
# Fetch this candidate's body only now — we're actually considering it.
Expand All @@ -260,6 +330,7 @@ while IFS=$'\t' read -r num labels title; do
blocked_lines+="blocked=$num by=$first_open_blocker"$'\n'
else
advance_ready="$num"
advance_plan_state="$plan_state"
fi
fi

Expand Down Expand Up @@ -295,13 +366,26 @@ done <<< "$planned"
if [ "$advance_ready" = "none" ] && [ "$fallback_ready" != "none" ]; then
echo "census: all planned candidates blocked (possible cycle); falling back to lowest-number #$fallback_ready" >&2
advance_ready="$fallback_ready"
advance_plan_state="$fallback_plan_state"
fi

echo "planned_issues=$planned_count"
[ -n "$detail" ] && printf '%s' "$detail"
[ -n "$in_flight" ] && printf '%s' "$in_flight"
[ -n "$stalled_lines" ] && printf '%s' "$stalled_lines"
[ -n "$blocked_lines" ] && printf '%s' "$blocked_lines"
[ -n "$plan_wait_lines" ] && printf '%s' "$plan_wait_lines"
# advance_mode (issue #100): only emitted when the plan gate is on AND a
# candidate was actually chosen — a tick reading this defaults to "implement"
# when the line is absent (plan_mode=off, or advance_ready=none), which is
# exactly today's ungated single-pass behavior.
if [ "$plan_mode" != "off" ] && [ "$advance_ready" != "none" ]; then
case "$advance_plan_state" in
needs-plan) echo "advance_mode=plan" ;;
gated-approved) echo "advance_mode=implement-gated" ;;
*) echo "advance_mode=implement" ;;
esac
fi
echo "advance_ready=$advance_ready"

# Desired cadence per the loop policy: FAST only when the loop can ACT now.
Expand Down
51 changes: 49 additions & 2 deletions .claude/scripts/loop-event.sh
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,16 @@
# Honors $GATES_FILE: not read directly here beyond quoting it into the
# self-hosting adapter clause baked into the prompt below (loop-tick.sh and
# loop-census.sh are what actually act on it).
#
# Plan gate (issue #100): for action=advance, loop-tick.sh's stdout carries an
# advance_mode=plan|implement-gated|implement line (present only when
# plan.gate != "off" — see gates.json) that this script greps out (never
# tail -1 — that's the verdict) to pick one of three ADVANCE prompt variants:
# a PLAN-ONLY turn that posts a structured plan comment + plan-review/
# needs-human labels and writes no code, a normal implement turn with the
# owner-approved plan comment injected into the implementer/every reviewer as
# authoritative scope, or (mode absent/"implement") today's unchanged
# single-pass prompt.
set -uo pipefail

# Two-root derivation (issue #63): script_dir = sibling scripts, root = consumer project.
Expand Down Expand Up @@ -88,6 +98,15 @@ case "$n" in
;;
esac

# Plan-gate mode (issue #100): loop-tick.sh echoes advance_mode=<mode> into
# its own stdout ONLY alongside a genuine action=advance verdict -- grep the
# FULL tick output for it (never tail -1; that line is not the verdict).
# Defaults to "implement" (today's ungated single-pass prompt) whenever the
# line is absent -- action=feedback verdicts, and every advance verdict when
# plan.gate=off (the vast majority of ticks).
mode="$(printf '%s\n' "$tick_out" | sed -n 's/^advance_mode=//p' | tail -1)"
mode="${mode:-implement}"

# Adapter clause: only when this loop runs against a non-default adapter
# (self-hosting). Mirrors the wording in .claude/self/pr-loop-self.md.
adapter=""
Expand All @@ -98,9 +117,37 @@ common="The tick (loop-tick.sh) already ran census/poll/merge/feedback-detection

case "$verdict" in
action=advance*)
prompt="Run the ADVANCE step of the autonomous PR loop for issue #$n. $common
Drive issue #$n through the orchestrator: scope → worktree implementer → gate.sh gates → reviewer lenses → bot PR. One issue in flight at a time — work ONLY issue #$n. \`backlog\` issues are owner-unapproved: if you file an issue yourself, label it backlog — NEVER planned (that label is the owner's formal approval, assigned by the owner alone)."
action_line="action=advance issue=$n"
case "$mode" in
# --- plan.gate: needs-plan -> PLAN-ONLY turn (issue #100) -------------
# Scope the issue and post ONE structured plan comment; apply the
# plan-review + needs-human labels for owner review; then STOP. No
# code, no branch, no PR -- the driver's job this turn is the plan
# artifact and the labels, nothing else.
plan)
prompt="Run the PLAN step of the autonomous PR loop for issue #$n (plan.gate). $common
This is a PLAN ONLY phase (plan.gate). Do NOT implement — write no code, create no feat/issue-$n-* branch, and open no PR. STOP once the plan comment and labels below are posted; do not spawn an implementer or any reviewer this turn.
1. Read issue #$n (\`bash $script_dir/bot-gh.sh issue view $n\`) and scope it: which module (per gates.json's \`modules[]\`) it belongs to, the files you expect the eventual implementation to touch, the implementation approach, and how each of the issue's acceptance criteria maps to that approach.
2. Post exactly ONE structured plan comment on issue #$n via \`bash $script_dir/bot-gh.sh issue comment $n --body \"...\"\`. The comment body MUST begin with the literal marker \`<!-- plan-gate:plan -->\` on its own first line, followed by the module, expected files, approach, and acceptance-criteria mapping from step 1 — this is the durable, reviewable plan artifact the owner and the later implement turn both read.
3. Create the plan-gate labels if they don't already exist (idempotent, mirrors needs-human.sh's own pattern): \`bash $script_dir/bot-gh.sh label create plan-review --color fbca04 --description \"Plan posted, awaiting owner review (plan.gate)\" --force\` and \`bash $script_dir/bot-gh.sh label create needs-human --color b60205 --description \"Loop is blocked on owner judgment\" --force\`.
4. Apply both labels to issue #$n: \`bash $script_dir/bot-gh.sh issue edit $n --add-label plan-review --add-label needs-human\`.
5. Report done and STOP. The owner reviews the plan comment on GitHub and either replaces \`plan-review\` with \`plan-approved\` (approve — the next tick implements it, with your plan injected as authoritative scope) or removes \`plan-review\` (request changes — the loop re-plans on a later tick)."
;;
# --- plan.gate: gated-approved -> normal implement turn, PLUS the
# approved plan is authoritative scope for the implementer AND every
# reviewer (issue #100). ----------------------------------------------
implement-gated)
prompt="Run the ADVANCE step of the autonomous PR loop for issue #$n. $common
Drive issue #$n through the orchestrator: scope → worktree implementer → gate.sh gates → reviewer lenses → bot PR. One issue in flight at a time — work ONLY issue #$n. \`backlog\` issues are owner-unapproved: if you file an issue yourself, label it backlog — NEVER planned (that label is the owner's formal approval, assigned by the owner alone).
This issue is plan-gated and APPROVED (plan.gate). Before implementing, fetch the approved plan: \`bash $script_dir/bot-gh.sh issue view $n --json comments\` and locate the comment whose body begins with the marker \`<!-- plan-gate:plan -->\`. Treat that plan as the AUTHORITATIVE scope for this issue. Inject the plan text VERBATIM into the implementer's spawn prompt as its authoritative scope, AND into every reviewer's spawn prompt. Instruct the correctness reviewer explicitly: a diff that exceeds the approved plan's declared files or approach is a valid reject reason under the correctness lens (\"exceeds approved scope\")."
;;
# --- ungated (plan.gate=off, or label mode without plan-first) -------
# today's single-pass prompt, byte-identical to pre-#100 behavior.
*)
prompt="Run the ADVANCE step of the autonomous PR loop for issue #$n. $common
Drive issue #$n through the orchestrator: scope → worktree implementer → gate.sh gates → reviewer lenses → bot PR. One issue in flight at a time — work ONLY issue #$n. \`backlog\` issues are owner-unapproved: if you file an issue yourself, label it backlog — NEVER planned (that label is the owner's formal approval, assigned by the owner alone)."
;;
esac
;;
*)
prompt="Run the ADDRESS FEEDBACK step of the autonomous PR loop for PR #$n. $common
Expand Down
Loading
Loading