diff --git a/.claude/docs/GOAL_PROTOCOL.md b/.claude/docs/GOAL_PROTOCOL.md index da42508..b43476d 100644 --- a/.claude/docs/GOAL_PROTOCOL.md +++ b/.claude/docs/GOAL_PROTOCOL.md @@ -146,7 +146,7 @@ The hook fires when a worker tries to terminate (report done). It looks up the a - `signed_off: true` → release. - Anything else → block. Worker must either fix the underlying failure and re-verify, or hand off to the operator with the kickback reason. -A reference hook config ships at `.claude/settings.example.json`. Copy it into `.claude/settings.json` (or merge into existing) and adjust paths if your harness exposes task-id differently. +A reference hook config ships at `.claude/settings.example.json`. The installer also ships `.claude/hooks/goal-gate.sh` as the real executable gate. Copy the settings example into `.claude/settings.json` (or merge into existing), ensure `jq` is installed, and adjust task-id resolution if your harness exposes it differently. ## Kickback routing diff --git a/.claude/hooks/goal-gate.sh b/.claude/hooks/goal-gate.sh new file mode 100755 index 0000000..11c6481 --- /dev/null +++ b/.claude/hooks/goal-gate.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# goal-gate.sh — Stop-hook gate for claude-layers /goal contracts. +# +# Blocks worker completion when CLAUDE_GOAL_ID points to a goal whose proof is +# missing or whose signed_off value is not the literal JSON boolean true. + +set -euo pipefail + +TASK_ID="${CLAUDE_GOAL_ID:-}" + +if [ -z "$TASK_ID" ]; then + echo '[goal-gate] no CLAUDE_GOAL_ID set; releasing (no contract to enforce).' >&2 + exit 0 +fi + +PROOF=".claude/goals/${TASK_ID}.proof.json" + +if [ ! -f "$PROOF" ]; then + echo "[goal-gate] BLOCK: no proof at $PROOF. Run /goal-verify $TASK_ID first." >&2 + exit 2 +fi + +if ! command -v jq >/dev/null 2>&1; then + echo '[goal-gate] BLOCK: jq is required to read the proof artifact.' >&2 + exit 2 +fi + +SIGNED="$(jq -r '.signed_off' "$PROOF")" + +if [ "$SIGNED" = "true" ]; then + echo "[goal-gate] RELEASE: $TASK_ID signed off." >&2 + exit 0 +fi + +REASON="$(jq -r '.kickback_reason // "unspecified"' "$PROOF")" +echo "[goal-gate] BLOCK: signed_off=$SIGNED; reason: $REASON" >&2 +exit 2 diff --git a/.claude/settings.example.json b/.claude/settings.example.json index edc0daf..524745c 100644 --- a/.claude/settings.example.json +++ b/.claude/settings.example.json @@ -1,5 +1,5 @@ { - "_comment": "Reference Stop-hook config for the /goal protocol. Copy into .claude/settings.json (or merge into existing) and adjust the task-id resolution if your harness exposes it differently. The hook blocks the worker from reporting done until .claude/goals/.proof.json exists with signed_off === true.", + "_comment": "Reference Stop-hook config for the /goal protocol. Copy into .claude/settings.json (or merge into existing). The installer provides .claude/hooks/goal-gate.sh; adjust that script if your harness resolves task IDs differently. The hook requires jq and blocks worker completion until .claude/goals/.proof.json exists with signed_off === true.", "hooks": { "Stop": [ { @@ -12,34 +12,5 @@ ] } ] - }, - "_hook_script_example": { - "_comment": "Save as .claude/hooks/goal-gate.sh and chmod +x. Resolves task-id from $CLAUDE_GOAL_ID (set by the operator or /goal command), reads the proof, blocks unless signed_off is the literal boolean true.", - "_script": [ - "#!/usr/bin/env bash", - "set -euo pipefail", - "", - "TASK_ID=\"${CLAUDE_GOAL_ID:-}\"", - "if [ -z \"$TASK_ID\" ]; then", - " echo '[goal-gate] no CLAUDE_GOAL_ID set; releasing (no contract to enforce).' >&2", - " exit 0", - "fi", - "", - "PROOF=\".claude/goals/${TASK_ID}.proof.json\"", - "if [ ! -f \"$PROOF\" ]; then", - " echo \"[goal-gate] BLOCK: no proof at $PROOF. Run /goal-verify $TASK_ID first.\" >&2", - " exit 2", - "fi", - "", - "SIGNED=$(jq -r '.signed_off' \"$PROOF\")", - "if [ \"$SIGNED\" = \"true\" ]; then", - " echo \"[goal-gate] RELEASE: $TASK_ID signed off.\" >&2", - " exit 0", - "fi", - "", - "REASON=$(jq -r '.kickback_reason // \"unspecified\"' \"$PROOF\")", - "echo \"[goal-gate] BLOCK: signed_off=$SIGNED; reason: $REASON\" >&2", - "exit 2" - ] } } diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 0000000..32ead4d --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,70 @@ +name: validate + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + shellcheck: + name: shellcheck installer + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Run shellcheck + run: | + sudo apt-get update + sudo apt-get install -y shellcheck + shellcheck install.sh + + install-test: + name: install into temp dir + runs-on: ubuntu-latest + strategy: + matrix: + mode: [full, minimal] + steps: + - uses: actions/checkout@v4 + - name: Create target dir + run: mkdir -p /tmp/install-target + - name: Run installer (${{ matrix.mode }}) + run: | + if [ "${{ matrix.mode }}" = "minimal" ]; then + ./install.sh /tmp/install-target --minimal --yes + else + ./install.sh /tmp/install-target --yes + fi + - name: Verify required files + run: | + test -f /tmp/install-target/CLAUDE.md + test -f /tmp/install-target/.claude/agents/FORGE.md + test -f /tmp/install-target/.claude/agents/QUILL.md + test -f /tmp/install-target/.claude/agents/SCOUT.md + test -f /tmp/install-target/.claude/agents/WARDEN.md + test -f /tmp/install-target/.claude/commands/goal.md + test -f /tmp/install-target/.claude/commands/goal-verify.md + test -d /tmp/install-target/.claude/goals + test -f /tmp/install-target/.claude/projects/PROJECT_TEMPLATE.md + test -f /tmp/install-target/.claude/orchestrator/ARGENT.md + test -f /tmp/install-target/.claude/hooks/goal-gate.sh + test -x /tmp/install-target/.claude/hooks/goal-gate.sh + test -f /tmp/install-target/.claude/docs/LAYERING_MODEL.md + test -f /tmp/install-target/.claude/docs/ROUTING_PATTERNS.md + test -f /tmp/install-target/.claude/docs/CREATING_OVERLAYS.md + test -f /tmp/install-target/.claude/docs/GOAL_PROTOCOL.md + test -f /tmp/install-target/.claude/settings.example.json + - name: Verify example projects (full only) + if: matrix.mode == 'full' + run: | + test -f /tmp/install-target/.claude/projects/PROJECT_WEBAPP.md + test -f /tmp/install-target/.claude/projects/PROJECT_API.md + test -f /tmp/install-target/.claude/projects/PROJECT_CLI.md + - name: Verify example projects absent (minimal only) + if: matrix.mode == 'minimal' + run: | + test ! -f /tmp/install-target/.claude/projects/PROJECT_WEBAPP.md + test ! -f /tmp/install-target/.claude/projects/PROJECT_API.md + test ! -f /tmp/install-target/.claude/projects/PROJECT_CLI.md + - name: Test idempotency + run: ./install.sh /tmp/install-target --yes diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c1ef11..8c59898 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ All notable changes to claude-layers will be documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +## [Unreleased] + +### Added + +- Installed `.claude/hooks/goal-gate.sh` as a real Stop-hook gate instead of only documenting the script inside `settings.example.json` +- Active GitHub Actions workflow at `.github/workflows/validate.yml` for installer and hook checks +- README 30-second mental model for how `CLAUDE.md`, role overlays, project overlays, `/goal`, Warden, and `goal-gate.sh` fit together + +### Changed + +- Installer now copies `goal-gate.sh`, marks it executable, and reminds users that enabled Stop-hook gating requires `jq` and `CLAUDE_GOAL_ID` +- Validation now checks that the installed hook exists and is executable + ## [1.1.0] - 2026-05-19 ### Added diff --git a/README.md b/README.md index 6534bd8..35e56b3 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,14 @@ This precedence model is the whole point. It means you can write project overlay ## What gets installed +The 30-second model: + +- `CLAUDE.md` tells the agent how to behave. +- Role overlays tell it what job it has. +- Project overlays tell it what matters here. +- `/goal` tells it what done means. +- Warden and `goal-gate.sh` keep it from claiming done without proof. + ``` your-project/ ├── CLAUDE.md # Layer 1: universal discipline @@ -77,6 +85,8 @@ your-project/ │ └── PROJECT_CLI.md # example: command-line tool ├── orchestrator/ │ └── ARGENT.md # routing and context composition + ├── hooks/ + │ └── goal-gate.sh # Stop hook gate for /goal proofs ├── docs/ │ ├── LAYERING_MODEL.md # how layers compose at runtime │ ├── ROUTING_PATTERNS.md # when to use which agent @@ -131,9 +141,9 @@ The "what you do not own" sections in each overlay are deliberately heavier than ## The `/goal` contract (v1.1.0+) -Every code-changing slice gets a manifest at `.claude/goals/.yaml` that declares its `done_when` checks (tests, commands, file constraints, diff constraints). Workers cannot self-assess "done" — Warden runs the manifest, writes `.claude/goals/.proof.json`, and a Stop hook refuses to release the worker until `signed_off: true`. For Scout investigations and design slices where the verifier is operator judgment, Warden emits `signed_off: "pending"` plus a reviewer checklist; the operator flips it manually. +Every code-changing slice gets a manifest at `.claude/goals/.yaml` that declares its `done_when` checks (tests, commands, file constraints, diff constraints). Workers cannot self-assess "done" — Warden runs the manifest, writes `.claude/goals/.proof.json`, and the installed Stop hook at `.claude/hooks/goal-gate.sh` refuses to release the worker until `signed_off: true`. For Scout investigations and design slices where the verifier is operator judgment, Warden emits `signed_off: "pending"` plus a reviewer checklist; the operator flips it manually. -Full spec lives at `.claude/docs/GOAL_PROTOCOL.md`. Reference Stop-hook config at `.claude/settings.example.json`. +Full spec lives at `.claude/docs/GOAL_PROTOCOL.md`. Reference Stop-hook config at `.claude/settings.example.json`; copy or merge it into `.claude/settings.json` when you want gating enabled. The hook requires `jq` and a `CLAUDE_GOAL_ID` environment variable for sessions that should be gated. ## The orchestrator (optional but useful) diff --git a/install.sh b/install.sh index 1df0f0b..523b3b6 100755 --- a/install.sh +++ b/install.sh @@ -81,6 +81,7 @@ What gets installed: .claude/projects/PROJECT_API.md Layer 3: example (full install only) .claude/projects/PROJECT_CLI.md Layer 3: example (full install only) .claude/orchestrator/ARGENT.md Orchestrator config + .claude/hooks/goal-gate.sh Stop hook gate for /goal proof enforcement .claude/docs/LAYERING_MODEL.md How layers compose .claude/docs/ROUTING_PATTERNS.md When to use which agent .claude/docs/CREATING_OVERLAYS.md How to write a new overlay @@ -203,6 +204,12 @@ echo "" echo " [Orchestrator]" copy_with_check "$SCRIPT_DIR/.claude/orchestrator/ARGENT.md" "$TARGET_DIR/.claude/orchestrator/ARGENT.md" +# Hooks +echo "" +echo " [Hooks]" +copy_with_check "$SCRIPT_DIR/.claude/hooks/goal-gate.sh" "$TARGET_DIR/.claude/hooks/goal-gate.sh" +chmod +x "$TARGET_DIR/.claude/hooks/goal-gate.sh" 2>/dev/null || true + # Docs echo "" echo " [Docs]" @@ -227,7 +234,8 @@ fi echo " 3. Copy PROJECT_TEMPLATE.md to PROJECT_.md for new projects" echo " 4. Update path-to-project mapping in .claude/orchestrator/ARGENT.md" echo " 5. Read .claude/docs/GOAL_PROTOCOL.md and decide whether to wire the Stop hook from .claude/settings.example.json" -echo " 6. Commit the .claude directory to your repo" +echo " 6. If you enable the Stop hook, ensure jq is installed and set CLAUDE_GOAL_ID for goal-scoped sessions" +echo " 7. Commit the .claude directory to your repo" echo "" echo "Quick test in Claude Code:" echo " Read CLAUDE.md, then summarize in three sentences what behaviors it requires." diff --git a/validate.yml b/validate.yml index 7735305..32ead4d 100644 --- a/validate.yml +++ b/validate.yml @@ -47,6 +47,8 @@ jobs: test -d /tmp/install-target/.claude/goals test -f /tmp/install-target/.claude/projects/PROJECT_TEMPLATE.md test -f /tmp/install-target/.claude/orchestrator/ARGENT.md + test -f /tmp/install-target/.claude/hooks/goal-gate.sh + test -x /tmp/install-target/.claude/hooks/goal-gate.sh test -f /tmp/install-target/.claude/docs/LAYERING_MODEL.md test -f /tmp/install-target/.claude/docs/ROUTING_PATTERNS.md test -f /tmp/install-target/.claude/docs/CREATING_OVERLAYS.md