diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 52d0706..3e68181 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,8 +26,16 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + # Both suites drive scripts that call rg, which the runner image lacks. + - name: Install ripgrep + run: sudo apt-get update -qq && sudo apt-get install -y -qq ripgrep - name: Run golden-input tests run: bash claude/test-hooks.sh + # The reconciler edits CLAUDE.md and agent frontmatter, so a suite CI + # never runs is a suite that rots. It drives a throwaway CLAUDE_DIR and + # touches nothing outside it. + - name: Run override tests + run: bash claude/test-overrides.sh installer: name: Installer smoke test @@ -39,7 +47,7 @@ jobs: # actually exist — the drift that broke the installer before. - name: Parse-check every script run: | - for f in install.sh scripts/*.sh claude/*.sh claude/hooks/*.sh; do + for f in install.sh scripts/*.sh claude/*.sh claude/hooks/*.sh claude/gentle-ai-overrides/*.sh; do bash -n "$f" || exit 1 done - name: Verify every script install.sh invokes exists diff --git a/README.md b/README.md index a990b75..38b5c78 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,9 @@ This will install Homebrew, packages from the Brewfile, Oh My Zsh with plugins, **Claude Code** - Global `PreToolUse` guard hooks (secret scanning, destructive-command blocking) merged into `~/.claude/settings.json` — see [claude/README.md](claude/README.md) +- Local SDD rules that outrank `gentle-ai`'s own, with a reconciler that + restores them after every sync +- Workflow scripts for the `Workflow` tool, symlinked into `~/.claude/workflows/` #### Installing a new tool ```bash diff --git a/claude/README.md b/claude/README.md index ecc39ff..da90546 100644 --- a/claude/README.md +++ b/claude/README.md @@ -15,8 +15,11 @@ Installed by `scripts/claude.sh`, which is called from `install.sh`. | `hooks/bash-guard.sh` | `PreToolUse` on `Bash` — commit attribution, destructive commands, CLI preference | | `hooks/write-guard.sh` | `PreToolUse` on `Edit\|Write\|MultiEdit` — credential scan | | `statusline.sh` | Status line: model, branch, session cost, context and rate-limit budget | -| `settings.fragment.json` | The `hooks` and `statusLine` blocks merged into `~/.claude/settings.json` | +| `settings.fragment.json` | The `hooks`, `statusLine` and `subagentPromptCacheTtl` keys merged into `~/.claude/settings.json` | | `test-hooks.sh` | 35 golden inputs, both directions | +| `gentle-ai-overrides/` | Local SDD rules that outrank gentle-ai's own, and the reconciler that keeps them alive | +| `workflows/` | Scripts for the `Workflow` tool, symlinked into `~/.claude/workflows/` | +| `test-overrides.sh` | 22 golden inputs for the reconciler, run against a throwaway `CLAUDE_DIR` | ## bash-guard.sh @@ -65,10 +68,16 @@ passes everything — silently. ## What is deliberately NOT here Everything under `~/.claude` carrying a `` marker — -`skills/`, `agents/`, `commands/`, `output-styles/`, and the global -`CLAUDE.md` — is generated and owned by `gentle-ai sync`. Tracking it here -would vendor a package manager's output and fight the next sync. The Brewfile -declares `gentleman-programming/tap/gentle-ai`; the tool owns its own files. +`skills/`, `agents/`, `commands/`, `output-styles/`, and the marked regions of +the global `CLAUDE.md` — is generated and owned by `gentle-ai sync`. Tracking +it here would vendor a package manager's output and fight the next sync. The +Brewfile declares `gentleman-programming/tap/gentle-ai`; the tool owns its own +files. + +The exception is [`gentle-ai-overrides/`](#gentle-ai-overrides), which tracks +only what gentle-ai provably does not own: the region of `CLAUDE.md` past its +last marker, plus one frontmatter key the reconciler re-applies after each +sync. Nothing generated is copied into this repo. Session state (`projects/`, `sessions/`, `history.jsonl`, `security/`) is neither config nor portable. @@ -102,3 +111,73 @@ not pass. Remove its entry from `settings.fragment.json` and rerun `scripts/claude.sh`, or edit `~/.claude/settings.json` directly for a one-machine change. Hooks reload mid-session, so the change takes effect on the next tool call. + +## gentle-ai-overrides/ + +`gentle-ai` owns `~/.claude/skills/`, `~/.claude/agents/` and most of +`~/.claude/CLAUDE.md`. This directory is the one seam where local rules can +outrank it without forking the tool. + +### Why it can work at all + +`gentle-ai sync` rewrites `CLAUDE.md` through +`filemerge.InjectMarkdownSection`, which rebuilds the file as +`before + block + after` for one `` region at a time. +Anything outside every marker is never touched. So `CLAUDE.local-overrides.md` +is appended past the last marker under its own +`` sentinels, and survives on its own. + +Agent definitions get no such courtesy: every file in gentle-ai's embed is +overwritten wholesale on each install (`WriteFileAtomic`, no merge). The +`memory: project` key on the reviewer agents therefore has to be re-applied, +which is the reconciler's second job. + +### The rules + +`CLAUDE.local-overrides.md` carries L1–L7. They come from measuring 10 archived +SDD changes in `reels-lab`, where the pipeline found zero implementation +defects by code review: every FAIL was a missing test, and the one change that +needed six remediation rounds was a single requirement that never enumerated +its entry points. The rules push work upstream — test tasks per scenario, +entry points per validation requirement — instead of paying for it in +verification rounds. The raw table is in the knowledge vault under +`03_Resources/Tech/IA Engineering/`. + +### Keeping them alive + +```bash +gentle-ai-overrides/reconcile.sh # apply, print what changed +gentle-ai-overrides/reconcile.sh --quiet # apply, print only on change +gentle-ai-overrides/reconcile.sh --check # report drift, exit 1, change nothing +``` + +Three things run it, so it should never need running by hand: + +- `scripts/claude.sh`, on install. +- The `gentle-ai` wrapper in `zsh/functions.zsh`, after `install`, `sync` and + `upgrade` — the only commands that cause drift. It forwards every argument + and preserves the exit code. +- `--check` in CI or a pre-commit hook, if drift should ever fail a build. + +It refuses to touch a `CLAUDE.md` whose gentle-ai markers are unbalanced: that +means a sync was interrupted, and splicing into a half-written file would put +the block inside a region about to be rewritten. + +## workflows/ + +`~/.claude/workflows/` holds scripts for the `Workflow` tool, which runs a DAG +of agents deterministically instead of leaving the orchestration to the model. +gentle-ai never writes there, so these are plain symlinks. + +`sdd-chain.js` encodes the SDD pipeline: exploration fanned out across three +scoped readers, spec and design in parallel (neither depends on the other — +both read only the proposal), a task schema that forces every work unit to +declare the files it writes, apply and verify pipelined per unit, and archive +only at zero CRITICALs. It proves the file partition before running writers in +parallel and degrades any overlapping units back to serial. Its verify schema +requires each CRITICAL to be classified `coverage`, `spec_ambiguity` or +`implementation`, which is what keeps the measurement behind L1-L7 current +instead of a one-off. + +Running a workflow needs explicit opt-in per invocation; installing the script +does not run anything. diff --git a/claude/gentle-ai-overrides/CLAUDE.local-overrides.md b/claude/gentle-ai-overrides/CLAUDE.local-overrides.md new file mode 100644 index 0000000..da2d1b9 --- /dev/null +++ b/claude/gentle-ai-overrides/CLAUDE.local-overrides.md @@ -0,0 +1,69 @@ +## Local SDD Overrides (take precedence over the gentle-ai blocks above) + +Derived from a measured baseline of 10 archived changes in `reels-lab` (2026-08-30). +Across that corpus the pipeline found **zero implementation defects by code review**: +all 3 FAIL verdicts were missing-test findings, and the single 6-round change was one +requirement that never enumerated its entry points. The raw table lives in the +knowledge vault under `03_Resources/Tech/IA Engineering/Auditoría SDD - Rondas hasta PASS.md`. + +### L1. One test task per spec scenario (`sdd-tasks`) + +`sdd-verify` marks any scenario without a test that actually ran as CRITICAL, however +correct the code is proven to be by other means. So `sdd-tasks` MUST emit an explicit +test task for every scenario in the delta specs, named with the scenario it covers. +A task list whose test tasks do not cover every scenario is incomplete — say so and fix +it before `sdd-apply`, rather than letting verify discover the gap later. + +### L2. Validation requirements MUST enumerate entry points (`sdd-spec`) + +A requirement of the form "X is validated" is incomplete. Every requirement that +constrains inbound data MUST carry one scenario per entry surface that can reach the +code path — CLI command, worker/queue boundary, HTTP handler, library API, each named +explicitly. Enumerate the surfaces by searching the codebase for call sites, never from +the proposal's prose alone. + +### L3. Tiered preflight (overrides the four-question hard gate) + +The `SDD Session Preflight` hard gate is scaled to what the command can actually do: + +| Command | Preflight collected | +| --- | --- | +| `/sdd-status`, `/sdd-explore` | none — read-only, no artifacts, no PR | +| `/sdd-new`, `/sdd-ff`, `propose` → `tasks` | pace + artifact store only | +| `/sdd-apply` and beyond | all four, asked once the tasks forecast exists | + +Collect the later groups when first needed and cache them, in one `AskUserQuestion` +call per tier. Never block a read-only command on a delivery decision. + +### L4. Fan out `sdd-explore` + +Exploration is breadth-first and read-only, so run it as three scoped explorers in +parallel — current state, prior art, constraints/entry points — then synthesize one +report. No file conflicts are possible. + +### L5. Partitioned apply, and only when the partition is proven + +`sdd-tasks` declares work units with the exact list of files each one writes. Units +whose file sets are disjoint may run as parallel writers in the same tree; any unit +that shares a file with another, or declares a dependency, stays serial. Never +parallelize writers on an unproven partition — two agents on one file overwrite each +other, which costs a whole run rather than saving one. + +Verification of a unit may overlap with implementation of the next: it runs against +that unit's own frozen candidate, which the receipt machinery already guarantees. + +### L6. Agent teams stay per-session + +Never add `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS` to `settings.json`. Enabling it makes +every subagent Claude names launch as a full teammate at 3–5× the tokens, including in +delegation never framed as team work. Enable it on the command line for a session that +specifically wants debate — a bug with an unclear root cause, an architecture review. +Default delegation stays on subagents. + +### L7. Classify every CRITICAL by origin + +When `sdd-verify` raises a CRITICAL, state its origin: `coverage` (the code is correct +but no test that ran covers the scenario), `spec_ambiguity` (the requirement was +under-specified), or `implementation` (the code is actually wrong). Do not default to +`implementation`. That classification is the measurement the rules above were tuned on, +and it is what keeps the next tuning honest instead of guessed. diff --git a/claude/gentle-ai-overrides/reconcile.sh b/claude/gentle-ai-overrides/reconcile.sh new file mode 100755 index 0000000..e33c22c --- /dev/null +++ b/claude/gentle-ai-overrides/reconcile.sh @@ -0,0 +1,196 @@ +#!/usr/bin/env bash + +# Reconciles the local SDD overrides against whatever gentle-ai last installed. +# +# WHY THIS EXISTS +# `gentle-ai install|sync|upgrade` rewrites everything it ships. Two different +# durability rules apply, and the split is why this script has two jobs: +# +# * ~/.claude/CLAUDE.md — gentle-ai only replaces the regions between its own +# `` / `` markers +# (internal/components/filemerge/section.go :: InjectMarkdownSection rebuilds +# the file as before + block + after). Anything outside every marker survives, +# so the overrides block is appended at EOF under its own sentinels. +# +# * ~/.claude/agents/*.md — every file that ships in gentle-ai's embed is +# overwritten wholesale on each install (WriteFileAtomic per file, no merge). +# The reviewer agents therefore need `memory: project` re-applied every time. +# +# MODES +# reconcile.sh apply, printing what changed +# reconcile.sh --quiet apply, printing only on change or error +# reconcile.sh --check report drift and exit 1; change nothing + +set -euo pipefail + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +BLOCK_SRC="$SCRIPT_DIR/CLAUDE.local-overrides.md" + +CLAUDE_DIR="${CLAUDE_DIR:-$HOME/.claude}" +CLAUDE_MD="$CLAUDE_DIR/CLAUDE.md" +AGENTS_DIR="$CLAUDE_DIR/agents" + +OPEN='' +CLOSE='' + +# Reviewer agents that benefit from cross-session project memory. Judges stay +# blind to *each other* — separate memory dirs — never to the codebase history. +REVIEWERS=( + jd-judge-a + jd-judge-b + review-risk + review-readability + review-reliability + review-resilience +) + +MODE=apply +case "${1:-}" in + --check) MODE=check ;; + --quiet) MODE=quiet ;; + '') ;; + *) printf 'usage: reconcile.sh [--check|--quiet]\n' >&2; exit 2 ;; +esac + +changed=0 +drift=() + +say() { [ "$MODE" = quiet ] || printf '%s\n' "$1"; } +note() { printf '%s\n' "$1"; } + +[ -f "$BLOCK_SRC" ] || { note "reconcile: missing $BLOCK_SRC"; exit 1; } + +# -------------------------------------------------------------------------- +# 1. CLAUDE.md overrides block +# -------------------------------------------------------------------------- + +if [ ! -f "$CLAUDE_MD" ]; then + say "reconcile: no $CLAUDE_MD yet — skipping the overrides block" +else + desired="$OPEN +$(cat "$BLOCK_SRC") +$CLOSE" + + current="" + if rg -qNF "$OPEN" "$CLAUDE_MD"; then + current="$(awk -v o="$OPEN" -v c="$CLOSE" ' + $0 == o { inside = 1 } + inside { print } + $0 == c { inside = 0 } + ' "$CLAUDE_MD")" + fi + + if [ "$current" = "$desired" ]; then + say "ok CLAUDE.md overrides block up to date" + elif [ "$MODE" = check ]; then + drift+=("CLAUDE.md overrides block ${current:+stale}${current:-absent}") + else + # Refuse to splice into a file whose gentle-ai markers are unbalanced — + # that means a sync was interrupted, and appending would land inside a + # half-written region. + opens=$(rg -cN '^$' "$CLAUDE_MD" || true) + closes=$(rg -cN '^$' "$CLAUDE_MD" || true) + if [ "${opens:-0}" != "${closes:-0}" ]; then + note "reconcile: ERROR — $CLAUDE_MD has ${opens:-0} gentle-ai open markers and ${closes:-0} close markers." + note " Refusing to edit a file mid-sync. Re-run 'gentle-ai sync', then this script." + exit 1 + fi + + tmp="$CLAUDE_MD.reconcile.tmp" + if [ -n "$current" ]; then + # Replace in place, keeping the block wherever it already sits. + # + # Split into head/tail and reassemble rather than substituting inside awk: + # BSD awk rejects a multi-line string passed through -v ("newline in + # string"), so the block can never travel as an awk variable. + awk -v o="$OPEN" '$0 == o { exit } { print }' "$CLAUDE_MD" > "$tmp" + printf '%s\n' "$desired" >> "$tmp" + awk -v c="$CLOSE" 'seen { print } $0 == c { seen = 1 }' "$CLAUDE_MD" >> "$tmp" + else + # First install: append at EOF, which is outside every gentle-ai region. + { cat "$CLAUDE_MD"; printf '\n'; printf '%s\n' "$desired"; } > "$tmp" + fi + + # A splice that lost the gentle-ai markers means the awk went wrong. + newopens=$(rg -cN '^$' "$tmp" || true) + if [ "${newopens:-0}" != "${opens:-0}" ]; then + rm -f "$tmp" + note "reconcile: ERROR — splice would have dropped gentle-ai markers. Left $CLAUDE_MD untouched." + exit 1 + fi + + command mv -f "$tmp" "$CLAUDE_MD" + say "patched CLAUDE.md overrides block" + changed=1 + fi +fi + +# -------------------------------------------------------------------------- +# 2. `memory: project` on the reviewer agents +# -------------------------------------------------------------------------- + +for name in "${REVIEWERS[@]}"; do + file="$AGENTS_DIR/$name.md" + + if [ ! -f "$file" ]; then + say "skipped $name (not installed)" + continue + fi + + if rg -qN '^memory:' "$file"; then + say "ok $name (memory already set)" + continue + fi + + if [ "$MODE" = check ]; then + drift+=("$name is missing 'memory: project'") + continue + fi + + # Insert before the closing `---` of the YAML frontmatter, i.e. the second + # line that is exactly `---`. + awk ' + BEGIN { fence = 0; done = 0 } + /^---[[:space:]]*$/ { + fence++ + if (fence == 2 && !done) { print "memory: project"; done = 1 } + print; next + } + { print } + ' "$file" > "$file.tmp" + + # Refuse a result that lost the frontmatter or failed to grow by exactly the + # inserted line. + if ! head -1 "$file.tmp" | rg -qN '^---[[:space:]]*$' \ + || ! rg -qN '^memory: project$' "$file.tmp" \ + || [ "$(wc -l < "$file.tmp")" -ne "$(( $(wc -l < "$file") + 1 ))" ]; then + rm -f "$file.tmp" + note "reconcile: WARNING — patch for $name produced an unexpected result; left untouched" + continue + fi + + command mv -f "$file.tmp" "$file" + say "patched $name (memory: project)" + changed=1 +done + +# -------------------------------------------------------------------------- +# Result +# -------------------------------------------------------------------------- + +if [ "$MODE" = check ]; then + if [ ${#drift[@]} -eq 0 ]; then + printf 'reconcile: no drift\n' + exit 0 + fi + printf 'reconcile: %d item(s) drifted\n' "${#drift[@]}" + printf ' - %s\n' "${drift[@]}" + printf "run 'dotfiles/claude/gentle-ai-overrides/reconcile.sh' to restore\n" + exit 1 +fi + +if [ "$changed" = 1 ]; then + note "reconcile: local SDD overrides restored" +else + say "reconcile: nothing to do" +fi diff --git a/claude/settings.fragment.json b/claude/settings.fragment.json index 54023e6..32003ea 100644 --- a/claude/settings.fragment.json +++ b/claude/settings.fragment.json @@ -39,5 +39,6 @@ "type": "command", "command": "~/.claude/statusline.sh", "refreshInterval": 30 - } + }, + "subagentPromptCacheTtl": "1h" } diff --git a/claude/test-overrides.sh b/claude/test-overrides.sh new file mode 100755 index 0000000..aad81a3 --- /dev/null +++ b/claude/test-overrides.sh @@ -0,0 +1,196 @@ +#!/bin/bash +# The assertion helpers are invoked indirectly through "${@:3}" in want(), so +# the linter cannot see the call sites. SC2317 and SC2329 are the same +# "unreachable function" finding, renumbered between versions; pre-commit pins +# an older one than Homebrew ships. +# shellcheck disable=SC2317,SC2329 + +# Golden-input tests for gentle-ai-overrides/reconcile.sh. +# +# The reconciler edits ~/.claude/CLAUDE.md, so every case runs against a +# throwaway CLAUDE_DIR instead. That is what the CLAUDE_DIR override in +# reconcile.sh exists for — CI has no ~/.claude, and a test that mutated the +# real one would be worse than no test. + +HERE="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +R="$HERE/gentle-ai-overrides/reconcile.sh" +BLOCK="$HERE/gentle-ai-overrides/CLAUDE.local-overrides.md" +SENTINEL='' +fail=0 + +# Assertions take the command itself rather than a bare $?, so the exit status +# they read is always the one they asked for. +want() { # want