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
18 changes: 11 additions & 7 deletions .claude/.claude-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@ that ship to downstream installs are what runs the live self-hosted PR loop here
`/orchestrator:harden`, etc. No file renames are needed for this — the namespace comes
from `name` in `plugin.json`, not from filenames.
- `plugin.json` carries an explicit `commands` allowlist (issue #76) so only consumer-facing
commands ship downstream. `.claude/self/pr-loop-self.md` — this repo's own self-hosting loop
prompt — is deliberately excluded: it lives under `.claude/self/` (not `.claude/commands/`), so
it is never auto-discovered as a project slash command either. See `.claude/self/README.md` for
how to run it in-repo.
commands ship downstream. `self/pr-loop-self.md` — this repo's own self-hosting loop
prompt — is deliberately excluded: it lives at the top-level `self/` (outside `.claude/`
entirely, issue #138), not `.claude/commands/`, so it is never auto-discovered as a project
slash command either — nor copied into the plugin payload at all, since the marketplace
source is `./.claude`. See `self/README.md` for how to run it in-repo.

## The `${CLAUDE_PLUGIN_ROOT:-.claude}` fallback
Agent/command prompts invoke scripts as:
Expand All @@ -35,9 +36,12 @@ consumer-facing command files — this disables the default directory-wide auto-

Not distributed by this plugin (repo-scaffolded, project-specific):
- `.claude/workflows/*.js` — deterministic fan-out workflows, not plugin-portable.
- `.claude/self/*` — this repo's OWN self-hosting adapter (gates, checks, and the
`pr-loop-self.md` loop prompt), not for downstream projects; downstream adopters get the
placeholder `.claude/gates.json` instead.
- top-level `self/*` (issue #138) — this repo's OWN self-hosting adapter (gates, checks, and
the `pr-loop-self.md` loop prompt), not for downstream projects; downstream adopters get the
placeholder `.claude/gates.json` instead. Unlike the other exclusions above, this one is
structural rather than allowlist-based: `self/` lives outside `.claude/` (the marketplace
`source`), so it is physically absent from any consumer's plugin cache — no risk of
accidental inclusion via a future auto-discovery change.

## Enabling in a consuming project

Expand Down
2 changes: 1 addition & 1 deletion .claude/gates.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,6 @@
"_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" },

"_protectedPaths_note": "issue #94 Layer 2 — deterministic protected-paths guard. Array of glob patterns (\"*\" = one path segment, \"**\" = any depth). An agent-authored PR whose diff touches ANY matching path is NEVER auto-merged: merge-ready.sh blocks the merge and labels the PR needs-human, and reviewers hard-reject it (see .claude/agents/reviewer.md). Protects the loop's own control plane from an injected instruction that tries to edit it. EMPTY ARRAY = disabled (same empty-means-skip convention as `gates`/`notify`) — self-hosted repos whose harness files ARE the product override this to [] in .claude/self/gates.json. Read from the adapter resolved via GATES_FILE (falls back to this root adapter). Documented in docs/HARDENING.md.",
"_protectedPaths_note": "issue #94 Layer 2 — deterministic protected-paths guard. Array of glob patterns (\"*\" = one path segment, \"**\" = any depth). An agent-authored PR whose diff touches ANY matching path is NEVER auto-merged: merge-ready.sh blocks the merge and labels the PR needs-human, and reviewers hard-reject it (see .claude/agents/reviewer.md). Protects the loop's own control plane from an injected instruction that tries to edit it. EMPTY ARRAY = disabled (same empty-means-skip convention as `gates`/`notify`) — self-hosted repos whose harness files ARE the product override this to [] in self/gates.json. Read from the adapter resolved via GATES_FILE (falls back to this root adapter). Documented in docs/HARDENING.md.",
"protectedPaths": [".claude/**", ".github/workflows/**", "gates.json", "**/gates.json"]
}
2 changes: 1 addition & 1 deletion .claude/scripts/arm-loop.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
# bash .claude/scripts/arm-loop.sh [--gates-file <path>] [--permission-mode <mode>] [--capacity N] [--rc-name <name>] [--spawn <mode>] [--stop-after-days N]
#
# --gates-file <path> passed to pr-loop.service as GATES_FILE (e.g.
# .claude/self/gates.json for the self-hosted
# self/gates.json for the self-hosted
# loop). Omit for the default project adapter.
# --permission-mode <mode> passed to `claude remote-control --permission-mode`.
# Defaults to permissions.defaultMode in
Expand Down
2 changes: 1 addition & 1 deletion .claude/scripts/cockpit.sh
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ gh() { "$gh_bin" "$@"; }

# Adapter to read for model/skill routing — same override contract as
# gate.sh/worktree.sh (GATES_FILE env, relative paths resolve from repo root),
# so self-hosting this repo can point it at .claude/self/gates.json.
# so self-hosting this repo can point it at self/gates.json.
gates_ref="${GATES_FILE:-.claude/gates.json}"
case "$gates_ref" in
/*) gates="$gates_ref" ;;
Expand Down
10 changes: 5 additions & 5 deletions .claude/scripts/cockpit.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@
set -uo pipefail

# Isolate from the CALLER's environment: this test is now wired into
# .claude/self/checks.sh's `test` case, which itself typically runs under
# `GATES_FILE=.claude/self/gates.json` (the self-host loop). Since env vars
# self/checks.sh's `test` case, which itself typically runs under
# `GATES_FILE=self/gates.json` (the self-host loop). Since env vars
# set before a command propagate to every child process it spawns, an
# ambient GATES_FILE would silently redirect the DEFAULT-adapter assertions
# below (section 2) onto the self-adapter. Section 3 sets GATES_FILE
Expand Down Expand Up @@ -466,8 +466,8 @@ check "all-clear run: no awaiting-your-review group heading" bash -c '! grep -qF
# (no gh/network either way).
# ---------------------------------------------------------------------------
html_self="$work/cockpit-self.html"
GATES_FILE=.claude/self/gates.json bash "$cockpit" --fixtures "$work/fixtures" "$html_self" >/dev/null 2>"$work/stderr-self.log"
check "GATES_FILE override honored in generator output" grep -q 'Adapter: <code>.claude/self/gates.json</code>' "$html_self"
GATES_FILE=self/gates.json bash "$cockpit" --fixtures "$work/fixtures" "$html_self" >/dev/null 2>"$work/stderr-self.log"
check "GATES_FILE override honored in generator output" grep -q 'Adapter: <code>self/gates.json</code>' "$html_self"

# ---------------------------------------------------------------------------
# 4. Graceful degrade when gh is unavailable — stub COCKPIT_GH_BIN so this is
Expand Down Expand Up @@ -658,7 +658,7 @@ alias_npm_pid=""
# event timeline + latest breadcrumbs + live worktree forensics, entirely
# offline. Forensics are computed by cockpit-serve.sh shelling out to git
# against a SYNTHETIC temp git repo/worktree built here under $TMPDIR
# (mirrors .claude/self/smoke-fanout.sh's own git-init/worktree-add
# (mirrors self/smoke-fanout.sh's own git-init/worktree-add
# pattern) -- never against this checkout, so this stays deterministic and
# isolated. Covers BOTH worktree-lookup strategies documented in
# cockpit-serve.sh: by conventional directory name
Expand Down
4 changes: 2 additions & 2 deletions .claude/scripts/gate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ key="${1:?usage: gate.sh <gate-name>}"
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/resolve-roots.sh"

# Which adapter to read. Defaults to the project adapter; set GATES_FILE to run a
# different one (e.g. GATES_FILE=.claude/self/gates.json for the self-host loop —
# see .claude/self/README.md). Relative paths resolve from the repo root.
# different one (e.g. GATES_FILE=self/gates.json for the self-host loop —
# see self/README.md). Relative paths resolve from the repo root.
gates_ref="${GATES_FILE:-.claude/gates.json}"
case "$gates_ref" in
/*) gates="$gates_ref" ;;
Expand Down
4 changes: 2 additions & 2 deletions .claude/scripts/hooks-parity.test.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
# hooks-parity.test.sh — offline smoke test for the hooks-parity check
# (issue #140) added to .claude/self/checks.sh's `do_hooks_parity` (run as
# (issue #140) added to self/checks.sh's `do_hooks_parity` (run as
# part of `do_build`, and therefore under `gate.sh build`/`gate.sh test`).
#
# Asserts:
Expand All @@ -21,7 +21,7 @@ set -uo pipefail

script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
repo_root="$(cd "$script_dir/../.." && pwd)"
checks_sh="$repo_root/.claude/self/checks.sh"
checks_sh="$repo_root/self/checks.sh"

work="$(mktemp -d "${TMPDIR:-/tmp}/hooks-parity-test.XXXXXX")"
trap 'rm -rf "$work"' EXIT
Expand Down
2 changes: 1 addition & 1 deletion .claude/scripts/loop-census.sh
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@
#
# 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.
# (GATES_FILE=self/gates.json) and downstream adopters.
#
# WHY THIS EXISTS (issue: loop stalled 13h with two planned issues): ticks that
# "optimized" STEP 0 away — or piped the cursor-advancing notify-poll.sh through
Expand Down
2 changes: 1 addition & 1 deletion .claude/scripts/loop-census.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ git -C "$fixture" branch feat/issue-100-w main >/dev/null

# Unset GATES_FILE explicitly: loop-census.sh reads it straight from the
# environment, and this test may itself be run from inside a gate invocation
# that exports GATES_FILE=.claude/self/gates.json for the OUTER repo — which
# that exports GATES_FILE=self/gates.json for the OUTER repo — which
# would leak in here and make census look for a gates.json this fixture never
# created. Force it back to the fixture's own default-relative gates.json.
out="$(env -u GATES_FILE bash "$scripts_dir/loop-census.sh" "acme/repo")"
Expand Down
2 changes: 1 addition & 1 deletion .claude/scripts/loop-daemon.sh
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@
# Env:
# LOOP_MODEL model for the driver (default sonnet; read by loop-event.sh)
# GATES_FILE adapter override, passed straight through the environment
# (self-hosting: .claude/self/gates.json)
# (self-hosting: self/gates.json)
# LOOP_DRIVER_TIMEOUT wall-clock cap per driver (default 90m); becomes
# systemd-run's `-p RuntimeMaxSec=` when systemd-run
# is on PATH, else `timeout`'s duration (issue #119)
Expand Down
4 changes: 2 additions & 2 deletions .claude/scripts/loop-event.sh
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ 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.
# (self-hosting). Mirrors the wording in self/pr-loop-self.md.
adapter=""
if [ -n "${GATES_FILE:-}" ]; then
adapter="Export GATES_FILE=$GATES_FILE for every gate/orchestration step, and instruct every spawned agent (orchestrator, implementers, reviewers) to read $GATES_FILE — NOT the placeholder root .claude/gates.json — as its adapter (module map, gates, review lenses). Every gate.sh invocation MUST run as: GATES_FILE=$GATES_FILE bash $script_dir/gate.sh <name>. "
Expand Down Expand Up @@ -201,7 +201,7 @@ NEVER force-merge, and NEVER merge, in either outcome."
Issue #$n was flagged STALLED by loop-census.sh (no recorded activity for the adapter's stall_minutes window) and a PRIOR driver already left partial work on a local \`feat/issue-$n-*\` branch. $branch_clause This is a CONTINUATION, NOT a fresh advance: do NOT create a new branch, do NOT re-scope the issue from scratch, and do NOT start a new worktree implementer with a clean slate — inspect what is ALREADY THERE (git log/diff on that branch) and pick up from it.
1. Locate and reattach to the EXISTING worktree for that branch (or re-create a worktree checked out on the SAME branch if the prior one was already cleaned up) — the commits/WIP on the branch are the authoritative starting point.
2. Assess what's actually done vs. still missing against the issue's acceptance criteria, then finish the implementation on that SAME branch.
3. Re-run the module's gates before publishing: \`GATES_FILE=.claude/self/gates.json bash $script_dir/gate.sh <name>\` for build/lint/typecheck/test/coverage per the adapter (per the adapter clause above) — do not skip this just because some gates may have passed in a prior, now-stale attempt.
3. Re-run the module's gates before publishing: \`GATES_FILE=self/gates.json bash $script_dir/gate.sh <name>\` for build/lint/typecheck/test/coverage per the adapter (per the adapter clause above) — do not skip this just because some gates may have passed in a prior, now-stale attempt.
4. Route the finished diff through the reviewer lenses per the adapter (consensus as configured), addressing any reject before proceeding.
5. Publish: push the branch and open the bot PR if none exists yet (\`bash $script_dir/bot-gh.sh pr create ...\`), or push the update to refresh an already-open PR for this issue. The work product MUST land on GitHub before you end your turn — a resume that ends without a pushed branch/PR just re-stalls the issue for the next tick.
Do NOT merge."
Expand Down
4 changes: 2 additions & 2 deletions .claude/scripts/loop-event.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -288,10 +288,10 @@ check "scenario 5: no prompt-file line emitted" bash -c '! printf "%s\n" "$1" |
# ---------------------------------------------------------------------------
dir6="$(new_fixture scenario6 'cadence=FAST cron=* * * * *
action=advance issue=9')"
out6="$(cd "$dir6" && PATH="/usr/bin:/bin" GATES_FILE=.claude/self/gates.json bash .claude/scripts/loop-event.sh)"; rc6=$?
out6="$(cd "$dir6" && PATH="/usr/bin:/bin" GATES_FILE=self/gates.json bash .claude/scripts/loop-event.sh)"; rc6=$?
pf6="$(printf '%s\n' "$out6" | sed -n 's/^loop-event: prompt-file=//p')"
check "scenario 6 (GATES_FILE): exits 0" [ "$rc6" -eq 0 ]
check "scenario 6: prompt file exports the adapter's GATES_FILE" bash -c 'grep -q "GATES_FILE=.claude/self/gates.json" "$1"' _ "$pf6"
check "scenario 6: prompt file exports the adapter's GATES_FILE" bash -c 'grep -q "GATES_FILE=self/gates.json" "$1"' _ "$pf6"

echo ""
if [ "$fail" -eq 0 ]; then
Expand Down
2 changes: 1 addition & 1 deletion .claude/scripts/merge-ready.sh
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ repo="${1:-$(gh repo view --json nameWithOwner -q .nameWithOwner)}"
if [ -f "$script_dir/needs-human.sh" ]; then . "$script_dir/needs-human.sh"; fi
owner="${MERGE_APPROVER:-${repo%%/*}}" # the approver whose APPROVED review authorizes a merge

# Adapter file: honor GATES_FILE (the self-host loop points at .claude/self/gates.json),
# Adapter file: honor GATES_FILE (the self-host loop points at self/gates.json),
# fall back to the shipped root adapter. Both merge.baseBranch and protectedPaths
# (issue #94 Layer 2) are read from it, so the self-adapter's permissive protectedPaths
# override applies when the loop runs self-hosted.
Expand Down
4 changes: 2 additions & 2 deletions .claude/scripts/needs-human.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@
set -uo pipefail

# Isolate from the CALLER's environment (mirrors cockpit.test.sh/notify.test.sh):
# this test is wired into .claude/self/checks.sh's `test` case, which itself
# often runs under `GATES_FILE=.claude/self/gates.json` (the self-host loop).
# this test is wired into self/checks.sh's `test` case, which itself
# often runs under `GATES_FILE=self/gates.json` (the self-host loop).
# An ambient GATES_FILE would silently redirect notify.sh's config lookup
# (called by needs_human_flag/needs_human_clear below) onto the SELF adapter
# instead of each fixture's own hand-written .claude/gates.json.
Expand Down
4 changes: 2 additions & 2 deletions .claude/scripts/notify.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
set -uo pipefail

# Isolate from the CALLER's environment (mirrors cockpit.test.sh): this test
# is wired into .claude/self/checks.sh's `test` case, which itself often runs
# under `GATES_FILE=.claude/self/gates.json` (the self-host loop). Since env
# is wired into self/checks.sh's `test` case, which itself often runs
# under `GATES_FILE=self/gates.json` (the self-host loop). Since env
# vars set before a command propagate to every child process, an ambient
# GATES_FILE would silently redirect notify.sh's config lookup onto the SELF
# adapter instead of each fixture's own hand-written .claude/gates.json below.
Expand Down
2 changes: 1 addition & 1 deletion .claude/scripts/plan-gate.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ EOF

run_census() {
# $1 = fixture dir. GATES_FILE unset explicitly (a test invoked from inside
# a GATES_FILE=.claude/self/gates.json gate run must not leak that into the
# a GATES_FILE=self/gates.json gate run must not leak that into the
# fixture's own default-relative gates.json — same guard as
# loop-census.test.sh).
env -u GATES_FILE bash "$1/.claude/scripts/loop-census.sh" "acme/repo"
Expand Down
2 changes: 1 addition & 1 deletion .claude/scripts/pr-comment-fix.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ EOF
chmod +x "$scripts_dir/pr-feedback.sh"

# Unset GATES_FILE explicitly: this test may itself run from inside a gate
# invocation that exports GATES_FILE=.claude/self/gates.json for the OUTER
# invocation that exports GATES_FILE=self/gates.json for the OUTER
# repo (mirrors loop-census.test.sh's own guard) — which would leak in here
# and make pr-comment-fix.sh read the outer self-adapter's commentFix.botAllowlist
# instead of this fixture's own gates.json.
Expand Down
4 changes: 2 additions & 2 deletions .claude/scripts/sync-managed-files.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,7 @@ JSON
# takes an absolute GATES_FILE as-is, same convention as gate.sh, see gate.test.sh's
# write_gates_file/run_gate). Scenarios 12/12b/15 must be deterministic regardless of an
# ambient GATES_FILE leaking in from the caller's environment (e.g. the self-hosted test
# gate runs *.test.sh with GATES_FILE=.claude/self/gates.json already exported) — without
# gate runs *.test.sh with GATES_FILE=self/gates.json already exported) — without
# this pin, derive_module_labels would resolve the ambient adapter relative to $t12 instead
# of this fixture's own gates.json and silently fail to derive any labels.
out12="$(GATES_FILE="$t12/.claude/gates.json" bash "$plugin_v_ok/skills/sync/sync.sh" "$t12" 2>&1)"
Expand Down Expand Up @@ -597,7 +597,7 @@ cat >"$t9/.claude/gates.json" <<'JSON'
JSON
# Pin GATES_FILE to this self-hosting fixture's OWN adapter (absolute path — same isolation
# rationale as scenario 12) so this scenario is deterministic regardless of an ambient
# GATES_FILE (e.g. the real self-hosted test gate exports GATES_FILE=.claude/self/gates.json
# GATES_FILE (e.g. the real self-hosted test gate exports GATES_FILE=self/gates.json
# before invoking this file, which would otherwise make derive_module_labels look in the
# wrong place relative to $t9 and change which advisory lines print).
out15="$(GATES_FILE="$t9/.claude/gates.json" bash "$t9/.claude/skills/sync/sync.sh" "$t9" 2>&1)"
Expand Down
2 changes: 1 addition & 1 deletion .claude/scripts/worktree.sh
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ if [ "$phase" = setup ]; then
fi
fi
# Which adapter to read. Defaults to the project adapter; set GATES_FILE to run a
# different one (e.g. GATES_FILE=.claude/self/gates.json). Relative paths resolve
# different one (e.g. GATES_FILE=self/gates.json). Relative paths resolve
# from the repo root.
gates_ref="${GATES_FILE:-.claude/gates.json}"
case "$gates_ref" in
Expand Down
2 changes: 1 addition & 1 deletion .claude/skills/setup/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ Two ways to fire the loop; ask the user which one (`AskUserQuestion`), presentin
```
bash .claude/scripts/arm-loop.sh
```
(Self-hosting: `bash .claude/scripts/arm-loop.sh --gates-file .claude/self/gates.json`.) This one script
(Self-hosting: `bash .claude/scripts/arm-loop.sh --gates-file self/gates.json`.) This one script
installs both `pr-loop-<repo>.service` (the loop daemon) and `claude-rc-<repo>.service` (`claude
remote-control` in a detached tmux session, so planning sessions can be spawned from claude.ai/mobile),
enables + starts them, and runs `loginctl enable-linger $USER` so they keep running without an open login
Expand Down
Loading
Loading