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
9 changes: 8 additions & 1 deletion .claude/scripts/gate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,14 @@ key="${1:?usage: gate.sh <gate-name>}"
# robust whether or not we're nested inside another git repo.
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
root="$(cd "$script_dir/../.." && pwd)"
gates="$root/.claude/gates.json"
# 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.
gates_ref="${GATES_FILE:-.claude/gates.json}"
case "$gates_ref" in
/*) gates="$gates_ref" ;;
*) gates="$root/$gates_ref" ;;
esac

if [ ! -f "$gates" ]; then
echo "gate.sh: no $gates found — skipping '$key'"; exit 0
Expand Down
42 changes: 42 additions & 0 deletions .claude/self/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Self-adapter — the template dogfooding itself (issue #11)

This directory lets **this repo run its own PR loop against its own harness/docs**, without touching the
shipped placeholder `.claude/gates.json` (which stays pristine for downstream adopters).

## Why a separate adapter
`.claude/gates.json` is the file a *new project* fills in. If we filled it with this repo's own modules and
gates, every clone of the template would inherit our self-config. So the self-config lives here instead, and
the tooling reads it only when explicitly pointed at it.

## Files
- **`gates.json`** — the real adapter for THIS repo: modules (`docs`, `harness`→`.claude`, `examples`, `ci`)
and node/bash-only gate commands, so they run with no extra linters installed.
- **`checks.sh`** — implements `build` / `lint` / `test`:
- `build` — every JSON config parses and each adapter (`gates.json` + `self/gates.json`) is well-shaped.
- `lint` — `bash -n` every shell script + `node --check` every workflow.
- `test` — `build` + `lint` smoke (validates the harness end-to-end on itself).

## Running gates against the self-adapter
`gate.sh` honors a `GATES_FILE` env override (defaults to `.claude/gates.json`):

```bash
GATES_FILE=.claude/self/gates.json bash .claude/scripts/gate.sh build
GATES_FILE=.claude/self/gates.json bash .claude/scripts/gate.sh lint
GATES_FILE=.claude/self/gates.json bash .claude/scripts/gate.sh test
```

## Running the loop self-hosted
To have the autonomous loop work this repo's own `module:*` backlog:
1. Label the target issue with a self module — `module:docs`, `module:harness`, `module:examples`, or `module:ci`.
2. Drive the tick with `GATES_FILE=.claude/self/gates.json` exported, and tell the orchestrator to read
**`.claude/self/gates.json`** as its adapter (module map + gates) for this repo. The generic agents/scripts
otherwise behave identically — worker boundaries come from this file's `modules`, gates from its `gates`.

> A dedicated self-host loop command that wires this automatically is the next increment on issue #11; for now
> the mechanism above is explicit.

## Self-hosting promotion (the one gotcha)
Agent-definition / `settings.json` / hook changes only take effect on a **fresh session**. So when the loop
changes the harness itself, treat it like a compiler compiling its successor: land the change on a branch,
then **restart** the session to adopt it. Worktree isolation means the *driving* session keeps the definitions
it loaded at start, so an in-flight change can't break the loop mid-run — but you must restart to run under it.
55 changes: 55 additions & 0 deletions .claude/self/checks.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#!/usr/bin/env bash
# Self-host gate implementations (issue #11). node + bash only — no external
# linters — so the loop can validate harness changes in a bare environment.
# Invoked via .claude/self/gates.json, e.g. `bash .claude/self/checks.sh lint`.
#
# build → every JSON config parses and each adapter has the required shape
# lint → `bash -n` every shell script + `node --check` every workflow
# test → build + lint smoke (validates the harness end-to-end on itself)
set -uo pipefail

root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "$root"
cmd="${1:?usage: checks.sh build|lint|test}"

json_parse() { node -e "JSON.parse(require('fs').readFileSync(process.argv[1],'utf8'))" "$1"; }

do_build() {
local rc=0
for f in .claude/gates.json .claude/self/gates.json .claude/settings.json; do
if [ ! -f "$f" ]; then echo "build: missing $f"; rc=1; continue; fi
if ! json_parse "$f" 2>/dev/null; then echo "build: invalid JSON — $f"; rc=1; fi
done
# each ADAPTER must have the shape the generic agents rely on
node -e '
for (const f of [".claude/gates.json", ".claude/self/gates.json"]) {
const g = require(process.cwd() + "/" + f);
if (!g.project || !Array.isArray(g.modules) || typeof g.gates !== "object") {
console.error("build: bad adapter shape —", f); process.exit(1);
}
}
' || rc=1
[ "$rc" -eq 0 ] && echo "build: JSON configs valid + adapters well-shaped"
return "$rc"
}

do_lint() {
local rc=0 f
for f in .claude/scripts/*.sh .claude/self/*.sh; do
[ -e "$f" ] || continue
bash -n "$f" || { echo "lint: shell syntax error — $f"; rc=1; }
done
for f in .claude/workflows/*.js; do
[ -e "$f" ] || continue
node --check "$f" || { echo "lint: JS syntax error — $f"; rc=1; }
done
[ "$rc" -eq 0 ] && echo "lint: shell + workflow syntax OK"
return "$rc"
}

case "$cmd" in
build) do_build ;;
lint) do_lint ;;
test) do_build && do_lint && echo "test: harness smoke OK" ;;
*) echo "checks.sh: unknown check '$cmd' (build|lint|test)"; exit 2 ;;
esac
36 changes: 36 additions & 0 deletions .claude/self/gates.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"_README": "SELF-ADAPTER (issue #11). Lets this template dogfood itself WITHOUT polluting the shipped placeholder .claude/gates.json (which stays pristine for downstream adopters). The loop uses THIS file only when pointed at it via GATES_FILE=.claude/self/gates.json (see .claude/self/README.md). Gate commands are node/bash-only so they run with no extra tooling installed.",

"project": { "name": "ai-project-orchestrator", "language": "shell+markdown+js", "packageManager": "none" },

"modules": [
{ "name": "docs", "path": "docs", "description": "User-facing docs: GETTING_STARTED, USAGE, HARDENING, PROMPTS, ARCHITECTURE, TOKEN_BUDGET." },
{ "name": "harness", "path": ".claude", "description": "Orchestrator machinery: agents, commands, scripts, workflows, self-adapter." },
{ "name": "examples", "path": "examples", "description": "Worked adapter examples (may not exist yet; created by issue #6)." },
{ "name": "ci", "path": ".github", "description": "Server-side gate workflows and composite actions." }
],

"gates": {
"_note": "Implemented in .claude/self/checks.sh — node + bash only, no external linters, so they pass in a bare environment.",
"install": "",
"build": "bash .claude/self/checks.sh build",
"lint": "bash .claude/self/checks.sh lint",
"typecheck": "",
"test": "bash .claude/self/checks.sh test",
"test_affected": "bash .claude/self/checks.sh test",
"coverage": "",
"coverage_threshold": 0,
"e2e": "",
"security": ""
},

"review": { "lenses": ["correctness", "tests"], "consensus": "all", "skills": [] },

"budget": {
"orchestrator_model": "opus", "worker_model": "sonnet",
"explorer_model": "haiku", "reviewer_model": "opus",
"max_parallel_workers": 2
},

"merge": { "policy": "pr-per-agent", "baseBranch": "main" }
}
Loading