From ed6cf6839c50b676a0f7aecb47d58335529b5b59 Mon Sep 17 00:00:00 2001 From: Roberto Cano <3525807+robercano@users.noreply.github.com> Date: Wed, 1 Jul 2026 21:12:55 +0200 Subject: [PATCH] feat(self): add self-adapter so the template can dogfood itself (issue #11, Phase 1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lets this repo run its own PR loop against its harness/docs WITHOUT polluting the shipped placeholder .claude/gates.json (which stays pristine for adopters). - .claude/self/gates.json — real adapter for this repo: modules (docs, harness->.claude, examples, ci) + node/bash-only gate commands. - .claude/self/checks.sh — build (JSON/adapter validation), lint (bash -n + node --check), test (build+lint smoke). No external linters, so it runs in a bare environment. - .claude/scripts/gate.sh — non-breaking GATES_FILE env override (defaults to .claude/gates.json); relative paths resolve from repo root. - .claude/self/README.md — how to run gates/loop self-hosted + the restart-to-adopt note. Verified: `GATES_FILE=.claude/self/gates.json gate.sh build|lint|test` all pass; the default (placeholder) path still skips as before. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_011HosUeuSvhetARboEfDW6K --- .claude/scripts/gate.sh | 9 ++++++- .claude/self/README.md | 42 +++++++++++++++++++++++++++++++ .claude/self/checks.sh | 55 +++++++++++++++++++++++++++++++++++++++++ .claude/self/gates.json | 36 +++++++++++++++++++++++++++ 4 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 .claude/self/README.md create mode 100644 .claude/self/checks.sh create mode 100644 .claude/self/gates.json diff --git a/.claude/scripts/gate.sh b/.claude/scripts/gate.sh index 45d91cd..14dd72c 100755 --- a/.claude/scripts/gate.sh +++ b/.claude/scripts/gate.sh @@ -10,7 +10,14 @@ key="${1:?usage: gate.sh }" # 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 diff --git a/.claude/self/README.md b/.claude/self/README.md new file mode 100644 index 0000000..935e75d --- /dev/null +++ b/.claude/self/README.md @@ -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. diff --git a/.claude/self/checks.sh b/.claude/self/checks.sh new file mode 100644 index 0000000..f1bb3d2 --- /dev/null +++ b/.claude/self/checks.sh @@ -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 diff --git a/.claude/self/gates.json b/.claude/self/gates.json new file mode 100644 index 0000000..1d1a393 --- /dev/null +++ b/.claude/self/gates.json @@ -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" } +}