diff --git a/AGENT_FRAMEWORK.md b/AGENT_FRAMEWORK.md index 9f9ed30..046e816 100644 --- a/AGENT_FRAMEWORK.md +++ b/AGENT_FRAMEWORK.md @@ -1,4 +1,4 @@ -# Agent Operating Framework v1.6 +# Agent Operating Framework v1.7 > A behavioral operating system for AI coding agents — born from production failures, not theory. > @@ -327,6 +327,12 @@ Five of six rules ship with hook backing as of v1.5. The single remaining adviso **Meta-hooks** (not bound to a single rule): - [`deprecated-field-gate.sh`](examples/hooks/deprecated-field-gate.sh) — template for blocking writes that reference deprecated DB columns or API fields *(fail-mode: closed, blast-radius: destructive)* - [`empty-rule-body-gate.sh`](examples/hooks/empty-rule-body-gate.sh) — pre-merge CI check that rejects rule files with empty bodies (< 200 bytes) or missing `## Why` sections *(fail-mode: closed, blast-radius: security — protects framework integrity against false-positive "applied" claims)* +- [`startup-gate.sh`](examples/hooks/startup-gate.sh) — SessionStart hook; checks all four governance surfaces (repo, AGENTS.md, active plan, hook registration) and writes a drift report to `~/.claude/startup-gate-report.md` *(fail-mode: open, blast-radius: advisory)* +- [`hook-telemetry-stop.sh`](examples/hooks/hook-telemetry-stop.sh) — Stop hook; reads per-hook fire/block breadcrumbs and bulk-INSERTs session telemetry *(fail-mode: open, blast-radius: advisory)* + +**Libraries** (source into hooks, do not execute directly): +- [`breadcrumb-lib.sh`](examples/hooks/breadcrumb-lib.sh) — session-scoped breadcrumb API +- [`examples/hooks/lib/normalize-hook-input.sh`](examples/hooks/lib/normalize-hook-input.sh) — cross-runtime payload normalization (Claude Code + Grok field names and tool-name literals) ### 5.4 Rule Consolidation Rules accumulate naturally as lessons are captured. Left unchecked, they become unreadable — too many rules means none get followed. When the rule count grows past 15-20: @@ -379,9 +385,17 @@ guides/ auto-optimization.md ← How the self-improvement loop works enforcement-architecture.md ← Memory → Rules → Hooks design patterns rule-consolidation.md ← How to cluster and compress rules at scale + hook-operations.md ← The three operational questions (v1.6) + advanced/ + done-criteria-schema.md ← Done Criteria spec and validator + hook-audit-methodology.md ← 4-track audit pattern (v1.6) + silent-failure-discipline.md ← Every fail-open path must log (v1.7) + agents-md-standard.md ← Three-level repo governance contract (v1.7) examples/ claude-code-rules/ ← Sample rule files for Claude Code hooks/ ← Reference hook implementations (Claude Code-specific) + lib/ + normalize-hook-input.sh ← Cross-runtime payload normalization (v1.7) ``` **Claude Code note:** Hooks use Claude Code's `PreToolUse`/`PostToolUse` lifecycle. Rule prose is portable to any agent platform. @@ -392,6 +406,7 @@ Full per-release notes live in [CHANGELOG.md](CHANGELOG.md). The framework file Headline changes from recent versions: +- **v1.7** — Provable hooks release: `startup-gate.sh` (SessionStart governance check), `normalize-hook-input.sh` (cross-runtime payload normalization), `hook-telemetry-stop.sh` (session-end telemetry). New guides: `silent-failure-discipline.md` (ADR 0012 — every fail-open path must log) and `agents-md-standard.md` (three-level repo governance contract). Incidents #36–#38. - **v1.6** — Hook operations layer: `breadcrumb-lib.sh` shared session library, `CLAUDE_HOOKS_SAFE_MODE` emergency bypass pattern, `# fail-mode: silent-skip` taxonomy tier for watcher-class hooks. New guides: `hook-operations.md` (the three operational questions) and `hook-audit-methodology.md` (4-track audit pattern). §5.2 updated with bypass + breadcrumb protocol as standard requirements. AOF self-eval harness (`b3d8451`) on main. - **v1.5** — `secure-config-gate.sh`, `focus-breadcrumb.sh` + `focus-confirmation-gate.sh`, `dormant-code-gate.sh`. §5.3 coverage moves from 3-of-6 enforced to **5-of-6 enforced**. `no-local-infrastructure` rewritten as a hosting decision framework (advisory by design). - **v1.4** — §5.3 Rule-to-Hook Coverage matrix added (accurate enforced-vs-advisory accounting). §5.2 fail-mode taxonomy. §1.3 precedence rule. §0.5 italic scope-anchor (Phase 1 Step 4), Done Criteria pre-condition (Phase 3 Step 1), doctor-clean log entry (Phase 3 Step 5). CI shipped: `doc-link-check.yml`, `rules-lint.yml`, `validate-done-criteria.py`. `empty-rule-body-gate.sh` added. diff --git a/CHANGELOG.md b/CHANGELOG.md index 05cd4cd..2c8b158 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,26 +4,56 @@ All notable changes to this framework follow [Keep a Changelog](https://keepacha --- -## [1.7] — TBD (Notes) +## [1.7] — 2026-06 -### Candidate content +### Background -The hooks × memory × skills design document (`smokin-os/spec/hooks-memory-skills-design.md`, written 2026-06-07) is the architectural blueprint for v1.7. Five concrete items emerge from it: +The hooks × memory × skills design document (`smokin-os/spec/hooks-memory-skills-design.md`, written 2026-06-07) is the architectural blueprint for v1.7. Five concrete items emerged from it; three ship with this release as public reference implementations. The remaining two (FORGET mechanism, `distill-memory.py` fix) are private-infrastructure items deferred to v1.8. -**1. FORGET mechanism** — the confirmed missing leg of memory integrity (READ + WRITE are built; FORGET is not). Required components: `valid-as-of` + `falsification-pointer` fields on every significant memory entry; an invalidation signal hook (PostToolUse, fires on file rename/delete, flags matching memory entries as stale — writes to a sidecar path, never to MEMORY.md directly per g-immutable); a reconciliation detector script that produces stale/ok/unverified report across the full Hindsight store; Hindsight store expiry path (TTL + last-accessed). This is the hard prerequisite for Phase D (reorg/global deploy) — Phase D must not start until steps 0.1–0.5 of the FORGET track are complete. +### What v1.7 covers (the "provable hooks" release) -**2. Startup gate hook** — a SessionStart hook that checks all four surfaces (memory, hooks, AGENTS.md, skills) at cold start and reports drift before any task begins. 7 checks: repo detection, AGENTS.md verification, active plan detection, memory conflict check, skills manifest currency, hook registration gap, conflict resolution. Key design decision: output goes to `~/.claude/startup-gate-report.md` (NOT `additionalContext` — confirmed ineffective by DC3 live test 2026-06-07 where two fresh sessions never surfaced the alert). Session-lifecycle.md Phase 1 reads the file explicitly. Blocked until the hook normalization helper (item 3) exists. +v1.6 shipped hooks. v1.7 answers: *can you prove they're working, can they survive across runtimes, and do you know what to do at cold start?* Three new components: -**3. Hook normalization helper** — `normalize-hook-input.sh` — a shared library that normalizes hook input JSON across runtimes. Hooks written for Claude Code's `tool_name`/`tool_input` snake_case + `"Bash"`/`"Edit"` literals silently bypass on Grok's `toolName`/`toolInput` camelCase + `"run_terminal_cmd"`/`"search_replace"` shape. Without this helper, every new hook has the same silent-bypass failure mode on non-Claude-Code runtimes. +### Added + +- **`examples/hooks/lib/normalize-hook-input.sh`** — shared payload normalization library. Hooks written for Claude Code's `tool_name`/`tool_input` snake_case + `"Bash"`/`"Edit"` literals silently bypass on Grok's `toolName`/`toolInput` camelCase + `"run_terminal_cmd"`/`"search_replace"` shape. This library normalizes both field names and tool-name literals so every hook that sources it works regardless of runtime. Provides two functions: `nh_normalize` (full normalization, stdout), `nh_tool_name` (quick dispatch helper). Fail-open: if python3 is missing or JSON parse fails, returns input unchanged — pre-normalization behavior, no regression. + +- **`examples/hooks/startup-gate.sh`** — SessionStart hook. Checks all four governance surfaces at cold start and writes a drift report to `~/.claude/startup-gate-report.md`. 7 checks: repo detection, AGENTS.md verification, active plan detection, memory conflict detection (stub — FORGET mechanism pending), skills manifest currency, hook registration gap (disk vs settings.json diff), conflict resolution. Key design decision: output goes to a file (NOT `additionalContext` — confirmed ineffective by live test where fresh sessions never surfaced the alert). Session-lifecycle.md Phase 1 reads the file explicitly. Annotations: `# fail-mode: open`, `# blast-radius: advisory`. + +- **`examples/hooks/hook-telemetry-stop.sh`** — Stop hook. Runs at session end; reads fire and block breadcrumbs from `breadcrumb-lib.sh`, bulk-INSERTs one row per hook into a telemetry store. Answers v1.6's "are hooks theater?" question with operational data. Sanitized to use a configurable `AOF_TELEMETRY_URL` + `AOF_TELEMETRY_KEY` environment variable pattern (private implementation uses smokin-ops; adapters plug in at the env-var layer). Annotations: `# fail-mode: open`, `# blast-radius: advisory`. + +- **`guides/advanced/silent-failure-discipline.md`** — documents ADR 0012: every hook fail-open path must write a tab-separated line to `.errors.log` via a `bc_write_error` call or equivalent direct write. Pattern, self-test command, and the 1441-entry error log as proof of the contract working. Sourced from a full governance audit of 63 hooks (2026-06-07) that found ~50 silent fail-opens. + +- **`guides/advanced/agents-md-standard.md`** — codifies the AGENTS.md governance contract. Three levels: Level 1 (identity sentence only), Level 2 (identity + subagent routing table), Level 3 (identity + routing + procedures + ADR register). Reference implementations from `smokin-os` and `smokin-memory` repos (2026-06-07). Explains why a repo-level governance declaration matters for multi-agent environments. + +- **3 new sanitized incidents** in `INCIDENTS.md` (#36, #37, #38). + +### Changed -**4. Per-invocation telemetry + opportunity counter** — extends `telemetry.hook_events` with `repo_cwd` + `tool_outcome` columns; adds `eval.opportunities` table for the AOF self-eval DPMO denominator. This feeds the self-eval harness (`b3d8451`, already on main) with real operational data — rules scoreboard, DPMO chart, hook compliance scoring. Has a mandatory day-7 goal DC requiring an active fire mechanism (a reminder hook that persists until the DC is confirmed by live query). Neither this item nor the self-eval Phase 1 dashboard close without a unified migration covering both the telemetry columns and the self-eval columns (`latency_ms_avg`, `latency_ms_p95`, `first_block_at`, `exit_codes_seen`). +- **`AGENT_FRAMEWORK.md`** — version bump to v1.7. §5.3 matrix updated: `startup-gate.sh` added as a meta-hook covering the session-lifecycle open step; `normalize-hook-input.sh` added as a cross-runtime library. Framework structure section updated with new `examples/hooks/lib/` directory and two new advanced guides. +- **`README.md`** — v1.7 references, 38 incidents, new guides in library table, `startup-gate.sh` and `normalize-hook-input.sh` in hook table. +- **`examples/hooks/README.md`** — 3 new inventory rows. -**5. `distill-memory.py` regex fix (PREREQ-B1)** — the Phase B memory distiller has been silently no-op'ing since ship (2026-05-28) because the commit SHA regex matches YYYYMMDD dates as hex strings, firing the info-loss invariant on every cron run. Fix: tighten the regex to require ≥1 non-decimal hex digit. This unblocks both the FORGET track and the telemetry track — neither can be properly validated while the distiller is dead. +### What is NOT in v1.7 (deferred to v1.8) -**Plus (from v1.6 deferred list):** -- `telemetry.hook_events` schema — sanitized port to `examples/` (coordinates with item 4) -- `hook-telemetry-stop.sh` — sanitized port to `examples/hooks/` -- Silent failure discipline (ADR 0012) — 63-hook governance audit + ~50 fail-opens fixed 2026-06-07; the "we can prove it works" release. Pattern: every fail-open path writes to `.errors.log` via `bc_write_error`; watchdog surfaces on session close. +- **FORGET mechanism** — the missing leg of memory integrity (READ + WRITE are built; FORGET is not). Requires `valid-as-of` + `falsification-pointer` fields on memory entries, an invalidation signal hook, and a reconciliation detector. This is the hard prerequisite for Phase D (global deploy). +- **`distill-memory.py` regex fix** — Phase B memory distiller has been silently no-op'ing since ship (2026-05-28): commit SHA regex matches YYYYMMDD dates as hex strings, firing the info-loss invariant on every cron run. Fix: tighten regex to require ≥1 non-decimal hex digit. +- **AOF self-eval Phase 1 dashboard** — harness (`b3d8451`) is on main; full operational data integration requires the telemetry columns + opportunity counter migration that `hook-telemetry-stop.sh` feeds. + +### Release checklist + +- [x] Fix CHANGELOG duplicate [1.7] section +- [x] Port `examples/hooks/lib/normalize-hook-input.sh` +- [x] Port `examples/hooks/startup-gate.sh` +- [x] Port `examples/hooks/hook-telemetry-stop.sh` +- [x] Write `guides/advanced/silent-failure-discipline.md` +- [x] Write `guides/advanced/agents-md-standard.md` +- [x] Add incidents #36, #37, #38 +- [x] Update `AGENT_FRAMEWORK.md` v1.6 → v1.7 +- [x] Update `README.md` +- [x] Update `examples/hooks/README.md` +- [x] `git tag v1.7 && git push --tags` +- [x] Create GitHub release --- @@ -106,16 +136,6 @@ This v1.6 release notes draft consolidates two consecutive overnight sessions (2 --- -## [1.7] — TBD (Notes) - -Candidate themes and material surfaced during v1.6 work. Not committed — placeholder only. - -- **AOF self-eval harness** — `b3d8451` on main, untagged by v1.6. The framework measuring itself: rules scoreboard, DPMO chart, hook compliance scoring (`eval.rules`, `eval.aof_eval_checkpoints`, `eval.opportunities`). Large enough to anchor v1.7. -- **Silent failure discipline** — full governance audit of 63 hooks done 2026-06-07; ~50 silent fail-opens fixed and wired to the error log via ADR 0012 (silent-hook watchdog). Directly answers v1.6's "are hooks theater?" question with a systematic, measurable fix. -- **AGENTS.md governance standard** — repos (`smokin-os`, `smokin-memory`) shipped Level 3 `AGENTS.md` files 2026-06-07. New pattern AOF doesn't document yet: how a repo declares its agent governance contract. v1.7 could codify the spec. - ---- - ## [1.5] — 2026-05 ### Added diff --git a/INCIDENTS.md b/INCIDENTS.md index cbbd902..7ed0ee9 100644 --- a/INCIDENTS.md +++ b/INCIDENTS.md @@ -43,6 +43,10 @@ Each row shows the incident, the lesson, and the rule it hardened. Dates are mon | 34 | 2026-05 | A breadcrumb library used an undocumented env var (`CLAUDE_SESSION_ID`) as its primary session key. The var was set on Mac via shell init but empty on Win11, causing source-truth-write gates to silently reject all writes on Win11 for ~14 days. Registration and annotation checks both passed; the failure was invisible until a separate audit. | Hook libraries must document their runtime dependencies as inline `SCHEMA ASSUMPTION` blocks naming the canonical env var, any legacy backstops, and the conditions under which each may be removed. Cross-subprocess PID stability must be verified by a regression smoke that tests two separate subprocess invocations, not just one. | [`breadcrumb-lib.sh`](examples/hooks/breadcrumb-lib.sh) + [`guides/advanced/hook-audit-methodology.md`](guides/advanced/hook-audit-methodology.md) Track 3 | | 35 | 2026-05 | A watcher hook auto-pushed to a branch-protected repo and logged `PUSH-FAIL` approximately every 30 seconds for ~14 days. The watcher continued running — it committed locally on each cycle and then failed push — so there was no crash to notice. Discovered only during an unrelated hook audit. | Watcher-class hooks that auto-push to a protected branch must use the `PUSH-SKIPPED` pattern: keep the commit step (local backup), skip the push step, log `PUSH-SKIPPED branch-protection-active`, and document the manual PR step. Without telemetry on hook fires, this class of silent degradation is undetectable. | [`guides/hook-operations.md`](guides/hook-operations.md) + `# fail-mode: silent-skip` taxonomy tier | +| 36 | 2026-06 | A SessionStart hook emitted a detailed governance report via `additionalContext`. Live test with two fresh sessions confirmed the content was never visible to the model — the harness assembles the system prompt before the hook's output is available. The hook author had assumed `additionalContext` was equivalent to a system-prompt injection; it is not. | SessionStart hook output that needs to reach the agent must be written to a file (e.g., `~/.claude/startup-gate-report.md`) and read explicitly in Phase 1 of session-lifecycle. Never rely on `additionalContext` as the delivery mechanism for governance content. | [`examples/hooks/startup-gate.sh`](examples/hooks/startup-gate.sh) | +| 37 | 2026-06 | Hooks written for Claude Code silently bypassed on a Grok runtime because Grok uses camelCase field names (`toolName`, `toolInput`) and different tool-name literals (`run_terminal_cmd` instead of `Bash`). Every hook that checked `tool_name == "Bash"` never matched on Grok — gates that should have blocked allowed unconditionally. Discovery was delayed because both runtimes produce the same exit-0 behavior on a successful gate pass. | Any hook intended to be multi-runtime must normalize the input payload before checking field names or tool-name literals. The normalization library (`normalize-hook-input.sh`) handles Claude Code → Grok field mapping and tool-name remapping; source it near the top of every hook that will run on more than one runtime. | [`examples/hooks/lib/normalize-hook-input.sh`](examples/hooks/lib/normalize-hook-input.sh) | +| 38 | 2026-06 | A governance audit of 63 hooks found ~50 fail-open paths that exited 0 without writing to the error log. The watchdog hook (ADR 0012) could only report on hooks that already wrote error-log entries — the ~50 silent hooks were invisible to it. The watchdog's "no errors found" output was therefore meaningless: it was reporting on a biased sample (the hooks that already knew how to fail noisily), not on the full population. | The error-log contract is only valuable if it covers every fail-open path, not just the paths that already knew about it. Audit each hook at ship time: force the fail-open path and confirm a row appears in `.errors.log`. See `guides/advanced/silent-failure-discipline.md` for the full pattern and self-test command. | [`guides/advanced/silent-failure-discipline.md`](guides/advanced/silent-failure-discipline.md) | + --- *This log is maintained alongside the rules. When a new rule is added, the incident that produced it belongs here.* diff --git a/README.md b/README.md index b4c14ba..01d9d6d 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ See [guides/getting-started.md](guides/getting-started.md) for the full adoption You've set up CLAUDE.md. You've built a few skills. You're using Projects Memory. But outputs are still inconsistent, the agent ignores rules under pressure, and you're manually reviewing everything. -This framework is the next step. It adds rules with documented enforcement contracts (some advisory by design), circuit breakers (stop after 3 failures), and an escalation model (advice → law → barriers) that makes your CLAUDE.md actually stick. See the [rule-to-hook coverage matrix](AGENT_FRAMEWORK.md#53-rule-to-hook-coverage) for what is system-enforced versus advisory in v1.6 — five of six rules ship with hooks; one (`no-local-infrastructure`) is a decision framework that is advisory by design. +This framework is the next step. It adds rules with documented enforcement contracts (some advisory by design), circuit breakers (stop after 3 failures), and an escalation model (advice → law → barriers) that makes your CLAUDE.md actually stick. See the [rule-to-hook coverage matrix](AGENT_FRAMEWORK.md#53-rule-to-hook-coverage) for what is system-enforced versus advisory in v1.7 — five of six rules ship with hooks; one (`no-local-infrastructure`) is a decision framework that is advisory by design. If you're just getting started with Claude Code, read the beginner guides first. If you've hit the wall where your CLAUDE.md "stops working," [start here](guides/from-beginner-to-framework.md). @@ -49,7 +49,7 @@ Every rule exists because its absence caused a specific, documented failure. See ## Library Contents ### The Framework -- **[AGENT_FRAMEWORK.md](AGENT_FRAMEWORK.md)** — The complete framework (v1.6). Use as your project's CLAUDE.md. +- **[AGENT_FRAMEWORK.md](AGENT_FRAMEWORK.md)** — The complete framework (v1.7). Use as your project's CLAUDE.md. ### Guides - **[From Beginner to Framework](guides/from-beginner-to-framework.md)** — You've built CLAUDE.md and skills but outputs are inconsistent. Here's why and what to do next. @@ -60,6 +60,8 @@ Every rule exists because its absence caused a specific, documented failure. See - **[Auto-Optimizing Skills](guides/auto-optimization.md)** — Eval-driven skill improvement: triage, iteration loop, 7 guardrails, data model - **[Hook Operations](guides/hook-operations.md)** — The three operational questions: what happens when a hook fails, how to tell if hooks are working, and how to escape when one goes rogue - **[Hook Audit Methodology](guides/advanced/hook-audit-methodology.md)** — 4-track audit pattern (registration, annotations, live fire, telemetry) with triage table and Codex batch audit pattern +- **[Silent Failure Discipline](guides/advanced/silent-failure-discipline.md)** — Every fail-open hook path must write to the errors log. Pattern, self-test, and the watchdog that reads it. +- **[AGENTS.md Standard](guides/advanced/agents-md-standard.md)** — Three-level repo governance contract (identity → routing → procedures + ADR register) ### Copy-Paste Rules Individual rule files for `~/.claude/rules/` or `.claude/rules/`. Each absorbs multiple earlier rules into a single file with sub-gates: @@ -88,13 +90,16 @@ Shell scripts that enforce rules at the tool-call level — the third tier of th | [deprecated-field-gate.sh](examples/hooks/deprecated-field-gate.sh) | PreToolUse hard block | Template for blocking writes that reference deprecated DB columns or API fields | | [empty-rule-body-gate.sh](examples/hooks/empty-rule-body-gate.sh) | CI meta-hook hard block | Pre-merge gate rejecting rule files < 200 bytes or missing `## Why` (closes the empty-stub loophole) | | [breadcrumb-lib.sh](examples/hooks/breadcrumb-lib.sh) | Library (source only) | Shared session-scoped breadcrumb API — source into any hook that needs to log or check session state | +| [startup-gate.sh](examples/hooks/startup-gate.sh) | SessionStart advisory | Checks repo, AGENTS.md, active plan, and hook registration gap at session start; writes drift report | +| [hook-telemetry-stop.sh](examples/hooks/hook-telemetry-stop.sh) | Stop advisory | Reads fire/block breadcrumbs at session end; bulk-INSERTs telemetry rows | +| [lib/normalize-hook-input.sh](examples/hooks/lib/normalize-hook-input.sh) | Library (source only) | Normalizes hook payload field names and tool-name literals across Claude Code and Grok runtimes | See [§5.3 Rule-to-Hook Coverage](AGENT_FRAMEWORK.md#53-rule-to-hook-coverage) for which rule each hook backs and the full enforced-vs-advisory accounting. See [examples/hooks/README.md](examples/hooks/README.md) for setup instructions and the breadcrumb pattern. ### Incident Log -- **[INCIDENTS.md](INCIDENTS.md)** — 35 sanitized incidents linking real failures to the rules they produced. Month-precision dates. +- **[INCIDENTS.md](INCIDENTS.md)** — 38 sanitized incidents linking real failures to the rules they produced. Month-precision dates. ## The Key Insight diff --git a/examples/hooks/README.md b/examples/hooks/README.md index 64ae091..b42de2a 100644 --- a/examples/hooks/README.md +++ b/examples/hooks/README.md @@ -144,6 +144,9 @@ When a hook blocks (exit 2), its stdout becomes the agent's error context. Write | `dormant-code-gate.sh` | CI lint | Hard block (exit 1) | Rejects PRs that modify code files whose every extracted symbol has zero callers elsewhere in the repo. Backs scope-discipline Gate 5. | | `empty-rule-body-gate.sh` | CI meta-hook | Hard block (exit 1) | Pre-merge gate that rejects rule files (`examples/claude-code-rules/*.md`) with body bytes < 200 or missing a `## Why` section. Closes the empty-stub loophole flagged in INCIDENTS #25. | | `breadcrumb-lib.sh` | Library | — (source only) | Shared session-scoped breadcrumb API: `bc_session_key`, `bc_dir`, `bc_write`, `bc_exists`, `bc_read`. Source into any hook that needs to log or check session state. | +| `startup-gate.sh` | SessionStart | Advisory (exit 0) | Checks repo, AGENTS.md, active plan, skills manifest, and hook registration at session start. Writes drift report to `~/.claude/startup-gate-report.md`. | +| `hook-telemetry-stop.sh` | Stop | Advisory (exit 0) | Reads fire/block breadcrumbs at session end; bulk-INSERTs one row per hook into a configurable telemetry store (set `AOF_TELEMETRY_URL` + `AOF_TELEMETRY_KEY`). | +| `lib/normalize-hook-input.sh` | Library | — (source only) | Normalizes hook payload field names (camelCase → snake_case) and tool-name literals (Grok → Claude Code). Source before any `tool_name` check for multi-runtime hooks. | ## The Focus-Confirmation Pair diff --git a/examples/hooks/hook-telemetry-stop.sh b/examples/hooks/hook-telemetry-stop.sh new file mode 100644 index 0000000..fc81753 --- /dev/null +++ b/examples/hooks/hook-telemetry-stop.sh @@ -0,0 +1,178 @@ +#!/usr/bin/env bash +# fail-mode: open +# blast-radius: advisory +# ============================================================================= +# hook-telemetry-stop.sh — Stop hook (session-end telemetry writer) +# +# Runs at session end. Reads per-hook fire and block breadcrumbs written by +# blocking hooks during the session, then bulk-INSERTs one row per hook into +# a telemetry table via the Supabase REST API. +# +# This answers v1.6's "are hooks theater?" question with operational data: +# after 2 weeks of deployment you can query fire_count and block_count per +# hook and know which gates are actually firing. +# +# Prerequisites: +# - breadcrumb-lib.sh (examples/hooks/breadcrumb-lib.sh) +# - AOF_TELEMETRY_URL env var — your Supabase project URL +# - AOF_TELEMETRY_KEY env var — a low-privilege anon/service key with +# INSERT permission on the telemetry table (see Schema below) +# - python3 in PATH +# +# Schema (create this table before enabling the hook): +# +# CREATE TABLE telemetry.hook_events ( +# id bigserial PRIMARY KEY, +# session_id text NOT NULL, +# hook_name text NOT NULL, +# fire_count integer NOT NULL DEFAULT 0, +# block_count integer NOT NULL DEFAULT 0, +# safe_mode_used boolean NOT NULL DEFAULT false, +# session_date date NOT NULL, +# machine text, +# created_at timestamptz NOT NULL DEFAULT now() +# ); +# ALTER TABLE telemetry.hook_events ENABLE ROW LEVEL SECURITY; +# -- RLS: allow anon INSERT, deny all reads from anon +# CREATE POLICY "anon_insert" ON telemetry.hook_events +# FOR INSERT TO anon WITH CHECK (true); +# +# Breadcrumb contract: +# Each blocking hook writes a breadcrumb on fire: bc_write "-fire" +# Each blocking hook writes a breadcrumb on block: bc_write "-block" +# This hook counts lines in each breadcrumb file to get fire/block counts. +# +# BLOCKING_HOOKS: customize this list to match the hooks you have installed. +# Only hooks that write breadcrumbs will produce meaningful counts. +# Hooks not in this list are silently omitted from telemetry. +# +# SAFE_MODE: +# If CLAUDE_HOOKS_SAFE_MODE=1 was active during the session, that fact +# is recorded as safe_mode_used=true for every hook row. +# +# Exit semantics: EXIT 0 on all paths (advisory, never blocks session close). +# +# Hook type: Stop (no matcher — fires on every session end) +# ============================================================================= + +# H1: no set -e — fail-open advisory hook; unhandled errors must not block close +IFS=$'\n\t' + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/breadcrumb-lib.sh" 2>/dev/null || true + +ERRORS_LOG="${HOME:-$USERPROFILE}/.claude/migration-breadcrumbs/.errors.log" + +if ! command -v bc_session_key >/dev/null 2>&1; then + echo "[hook-telemetry] WARN: breadcrumb-lib.sh not loaded — skipping telemetry" >&2 + printf '%s\thook-telemetry-stop\tbreadcrumb-lib-load-failed\n' \ + "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$ERRORS_LOG" 2>/dev/null || true + exit 0 +fi + +# ── Config ──────────────────────────────────────────────────────────────────── +# Set AOF_TELEMETRY_URL and AOF_TELEMETRY_KEY in your environment. +# Example (add to ~/.claude/env or your shell profile): +# export AOF_TELEMETRY_URL="https://.supabase.co" +# export AOF_TELEMETRY_KEY="" +TELEMETRY_URL="${AOF_TELEMETRY_URL:-}" +TELEMETRY_KEY="${AOF_TELEMETRY_KEY:-}" + +if [ -z "$TELEMETRY_URL" ] || [ -z "$TELEMETRY_KEY" ]; then + echo "[hook-telemetry] WARN: AOF_TELEMETRY_URL or AOF_TELEMETRY_KEY not set — skipping" >&2 + printf '%s\thook-telemetry-stop\ttelemetry-env-vars-not-set\n' \ + "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$ERRORS_LOG" 2>/dev/null || true + exit 0 +fi + +# ── Customize: list the blocking hooks you have installed ──────────────────── +BLOCKING_HOOKS=( + "read-gate" + "search-gate" + "delivery-gate" + "secure-config-gate" + "focus-confirmation-gate" + "dormant-code-gate" + "empty-rule-body-gate" +) + +# ── Session identity ────────────────────────────────────────────────────────── +SESSION_ID=$(bc_session_key 2>/dev/null) || SESSION_ID="unknown-$$" +BC_DIR=$(bc_dir 2>/dev/null) || BC_DIR="/tmp/claude-hooks-${USER:-unknown}" +SESSION_DATE=$(date +%Y-%m-%d) +SAFE_MODE="${CLAUDE_HOOKS_SAFE_MODE:-0}" +SAFE_MODE_FLAG="false" +[ "$SAFE_MODE" = "1" ] && SAFE_MODE_FLAG="true" + +MACHINE="unknown" +case "$(uname -s 2>/dev/null || true)" in + Darwin) MACHINE="mac" ;; + Linux) MACHINE="linux" ;; + CYGWIN*|MINGW*|MSYS*) MACHINE="win11" ;; +esac + +# ── Count fires and blocks per hook ────────────────────────────────────────── +count_breadcrumb() { + local file="$1" + local n + if [ -f "$file" ]; then + n=$(wc -l < "$file" | tr -d ' ') + [[ "$n" =~ ^[0-9]+$ ]] && echo "$n" || echo "0" + else + echo "0" + fi +} + +json_escape() { + python3 -c "import json,sys; print(json.dumps(sys.stdin.read()))" <<< "$1" +} + +# ── Build JSON rows ─────────────────────────────────────────────────────────── +SESSION_ID_JSON=$(json_escape "$SESSION_ID") +MACHINE_JSON=$(json_escape "$MACHINE") + +ROWS="[" +FIRST=1 +for HOOK in "${BLOCKING_HOOKS[@]}"; do + FIRE_FILE="${BC_DIR}/${SESSION_ID}-${HOOK}-fire" + BLOCK_FILE="${BC_DIR}/${SESSION_ID}-${HOOK}-block" + FIRES=$(count_breadcrumb "$FIRE_FILE") + BLOCKS=$(count_breadcrumb "$BLOCK_FILE") + HOOK_JSON=$(json_escape "$HOOK") + + ROW="{\"session_id\":${SESSION_ID_JSON},\"hook_name\":${HOOK_JSON},\"fire_count\":${FIRES},\"block_count\":${BLOCKS},\"safe_mode_used\":${SAFE_MODE_FLAG},\"session_date\":\"${SESSION_DATE}\",\"machine\":${MACHINE_JSON}}" + + if [ "$FIRST" -eq 1 ]; then + ROWS="${ROWS}${ROW}" + FIRST=0 + else + ROWS="${ROWS},${ROW}" + fi +done +ROWS="${ROWS}]" + +# ── POST to Supabase REST ───────────────────────────────────────────────────── +TMPFILE=$(mktemp /tmp/hook-telemetry-XXXXXX.txt 2>/dev/null) || TMPFILE="/tmp/hook-telemetry-$$.txt" +trap 'rm -f "$TMPFILE"' EXIT + +HTTP_STATUS=$(curl -s -o "$TMPFILE" -w "%{http_code}" \ + -X POST \ + -H "apikey: ${TELEMETRY_KEY}" \ + -H "Authorization: Bearer ${TELEMETRY_KEY}" \ + -H "Content-Type: application/json" \ + -H "Accept-Profile: telemetry" \ + -H "Content-Profile: telemetry" \ + -H "Prefer: return=minimal" \ + "${TELEMETRY_URL}/rest/v1/hook_events" \ + -d "$ROWS" 2>/dev/null) || HTTP_STATUS="000" + +if [ "$HTTP_STATUS" = "201" ]; then + echo "[hook-telemetry] session ${SESSION_ID} → ${#BLOCKING_HOOKS[@]} hook rows written (${SESSION_DATE})" >&2 +elif [ "$HTTP_STATUS" = "000" ]; then + echo "[hook-telemetry] WARN: curl failed — no network or telemetry endpoint unreachable" >&2 +else + BODY=$(cat "$TMPFILE" 2>/dev/null | head -c 200) + echo "[hook-telemetry] WARN: HTTP ${HTTP_STATUS} — ${BODY}" >&2 +fi + +exit 0 diff --git a/examples/hooks/lib/normalize-hook-input.sh b/examples/hooks/lib/normalize-hook-input.sh new file mode 100644 index 0000000..bc9c36d --- /dev/null +++ b/examples/hooks/lib/normalize-hook-input.sh @@ -0,0 +1,147 @@ +#!/usr/bin/env bash +# fail-mode: open +# blast-radius: advisory +# ============================================================================= +# normalize-hook-input.sh — Cross-runtime hook payload normalization library +# +# SOURCE with: +# source "$(dirname "${BASH_SOURCE[0]}")/lib/normalize-hook-input.sh" 2>/dev/null || true +# +# Problem: Claude Code hooks receive JSON payloads in snake_case with +# "Bash"/"Edit"/"Write" tool-name literals. Other runtimes (Grok and future +# agents) emit camelCase payloads with different tool-name literals: +# +# Claude Code: {"tool_name": "Bash", "tool_input": {...}} +# Grok: {"toolName": "run_terminal_cmd", "toolInput": {...}} +# +# Without normalization, a hook that checks `tool_name == "Bash"` silently +# bypasses on Grok — the check never fires, the gate never blocks. +# +# What this library normalizes: +# +# Field names (camelCase → snake_case): +# toolName → tool_name +# toolInput → tool_input +# toolResult → tool_result +# sessionId → session_id +# +# Tool-name literals (Grok → Claude Code canonical): +# run_terminal_cmd → Bash +# search_replace → Edit +# create_file → Write +# read_file → Read +# grep_search → Bash +# list_dir → Bash +# +# What this library does NOT normalize: +# - tool_input field structure (command, file_path, etc.) — hooks handle these +# - MCP tool names (mcp__*) — namespaced, no conflicts +# +# Usage: +# 1. Source this file near the top of your hook, after breadcrumb-lib.sh. +# 2. Read raw stdin into a variable (do not consume stdin twice). +# 3. Call nh_normalize "$RAW_INPUT" to get normalized JSON on stdout. +# 4. Pass the normalized JSON to your existing parser. +# +# Example: +# source "$(dirname "${BASH_SOURCE[0]}")/lib/normalize-hook-input.sh" 2>/dev/null || true +# RAW=$(cat) +# INPUT=$(nh_normalize "$RAW") +# TOOL_NAME=$(echo "$INPUT" | python3 -c "import json,sys; print(json.load(sys.stdin).get('tool_name',''))") +# +# Fail-open guarantee: if python3 is missing or JSON parse fails, nh_normalize +# returns the original input unchanged. Every hook that sources this library +# degrades to Claude Code-only behavior — the pre-normalization status quo. +# No regression. +# +# Library exit semantics: none — this file provides functions only. +# The calling hook owns all exit codes. +# +# Hook type: library (source, do not execute directly) +# ============================================================================= + +# Do NOT set -euo pipefail in a sourced library — it affects the calling hook's +# errexit behavior. Let the caller control error handling. + +# nh_normalize +# +# Emits a normalized JSON string on stdout. +# On any failure, emits the original input unchanged (fail-open). +nh_normalize() { + local raw="${1:-}" + if [ -z "$raw" ]; then + printf '%s' "$raw" + return + fi + + local result + result=$(printf '%s' "$raw" | python3 -c ' +import json, sys + +TOOL_NAME_MAP = { + "run_terminal_cmd": "Bash", + "search_replace": "Edit", + "create_file": "Write", + "read_file": "Read", + "grep_search": "Bash", + "list_dir": "Bash", +} + +FIELD_MAP = { + "toolName": "tool_name", + "toolInput": "tool_input", + "toolResult": "tool_result", + "sessionId": "session_id", +} + +try: + d = json.loads(sys.stdin.read()) +except Exception: + sys.exit(0) # fail-open: caller receives empty, returns original + +normalized = {} +for k, v in d.items(): + normalized[FIELD_MAP.get(k, k)] = v + +if "tool_name" in normalized: + normalized["tool_name"] = TOOL_NAME_MAP.get(normalized["tool_name"], normalized["tool_name"]) + +print(json.dumps(normalized), end="") +' 2>/dev/null) || result="" + + if [ -z "$result" ]; then + printf '%s' "$raw" + else + printf '%s' "$result" + fi +} + +# nh_tool_name +# +# Convenience: extract the normalized tool_name without a full normalize pass. +# Useful for quick dispatch checks in PreToolUse hooks. +# +# Example: +# tool=$(nh_tool_name "$RAW") +# [[ "$tool" == "Bash" ]] && echo "bash call" +nh_tool_name() { + local raw="${1:-}" + [ -z "$raw" ] && { printf ''; return; } + printf '%s' "$raw" | python3 -c ' +import json, sys +TOOL_NAME_MAP = { + "run_terminal_cmd": "Bash", + "search_replace": "Edit", + "create_file": "Write", + "read_file": "Read", + "grep_search": "Bash", + "list_dir": "Bash", +} +try: + d = json.load(sys.stdin) + raw_name = d.get("tool_name") or d.get("toolName", "") + print(TOOL_NAME_MAP.get(raw_name, raw_name), end="") +except Exception: + print("", end="") +' 2>/dev/null || printf '' +} diff --git a/examples/hooks/startup-gate.sh b/examples/hooks/startup-gate.sh new file mode 100644 index 0000000..fff71db --- /dev/null +++ b/examples/hooks/startup-gate.sh @@ -0,0 +1,265 @@ +#!/usr/bin/env bash +# fail-mode: open +# blast-radius: advisory +# ============================================================================= +# startup-gate.sh — SessionStart hook +# +# Checks all four governance surfaces at session start and writes a drift +# report to ~/.claude/startup-gate-report.md. Session-lifecycle.md Phase 1 +# reads that file; if "Action required: yes", Phase 1 surfaces drift items +# before any task begins. +# +# Output contract: +# 1. PRIMARY: writes ~/.claude/startup-gate-report.md +# 2. Emits minimal JSON with a one-line pointer to the report file +# (additionalContext is NOT the report — it is a pointer) +# 3. g-immutable: this hook MUST NOT write to MEMORY.md, CLAUDE.md, +# settings.json, or any other instruction surface. The report file is +# operator-read-only and is never auto-injected. +# +# Why output goes to a file, not additionalContext: +# Live test (2026-06-07) confirmed that additionalContext injected by a +# SessionStart hook does NOT surface in fresh sessions — the harness loads +# the hook output after the system prompt is assembled, so it is invisible +# to the model on the first turn. File-based output + explicit Phase 1 read +# is the only reliable pattern. +# +# Seven checks: +# 1. Repo detection (git rev-parse + AGENTS.md presence) +# 2. AGENTS.md verification (identity sentence + procedure section) +# 3. Active plan detection (Status: Active/In Progress in spec/*.md) +# 4. Memory conflict detection (stub — FORGET mechanism pending) +# 5. Skills manifest currency (checks catalog-freshness breadcrumb) +# 6. Hook registration gap (disk vs settings.json diff) +# 7. Conflict resolution (surface precedence summary if drift detected) +# +# Exit semantics: EXIT 0 on ALL paths (informational, never a blocker). +# +# Customization: +# REPORT_FILE — where the report is written (default: ~/.claude/startup-gate-report.md) +# ERRORS_LOG — fail-open errors log (default: ~/.claude/migration-breadcrumbs/.errors.log) +# HOOKS_DIR — directory of installed hooks (default: ~/.claude/hooks) +# SETTINGS_JSON — Claude Code settings file (default: ~/.claude/settings.json) +# +# Prerequisites: +# - breadcrumb-lib.sh (examples/hooks/breadcrumb-lib.sh) +# - normalize-hook-input.sh (examples/hooks/lib/normalize-hook-input.sh) +# - python3 in PATH +# +# Hook type: SessionStart (no matcher needed — fires on every session) +# ============================================================================= + +set -euo pipefail +IFS=$'\n\t' + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/breadcrumb-lib.sh" 2>/dev/null || true +source "$SCRIPT_DIR/lib/normalize-hook-input.sh" 2>/dev/null || true + +HOME_DIR="${HOME:-$USERPROFILE}" +REPORT_FILE="${REPORT_FILE:-$HOME_DIR/.claude/startup-gate-report.md}" +ERRORS_LOG="${ERRORS_LOG:-$HOME_DIR/.claude/migration-breadcrumbs/.errors.log}" +SETTINGS_JSON="${SETTINGS_JSON:-$HOME_DIR/.claude/settings.json}" +HOOKS_DIR="${HOOKS_DIR:-$HOME_DIR/.claude/hooks}" + +log_error() { + local reason="$1" + mkdir -p "$(dirname "$ERRORS_LOG")" 2>/dev/null || true + printf '%s\tstartup-gate\t%s\n' \ + "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$reason" \ + >> "$ERRORS_LOG" 2>/dev/null || true +} + +# Read and normalize stdin (SessionStart passes a JSON event) +RAW_INPUT=$(cat 2>/dev/null || true) +INPUT=$(nh_normalize "$RAW_INPUT" 2>/dev/null || printf '%s' "$RAW_INPUT") + +# Extract CWD from session event +CWD=$(printf '%s' "$INPUT" | python3 -c " +import json, sys +try: + d = json.load(sys.stdin) + print(d.get('cwd', d.get('session_info', {}).get('cwd', ''))) +except Exception: + print('') +" 2>/dev/null || true) + +TS=$(date -u +%Y-%m-%dT%H:%M:%SZ) +DRIFT_COUNT=0 +LINES=() + +append() { LINES+=("$1"); } + +# ── Check 1: Repo detection ────────────────────────────────────────────────── +[ -n "${CWD:-}" ] && [ -d "$CWD" ] && cd "$CWD" 2>/dev/null || true + +check1_output="[CHECK 1: FAIL_OPEN — git not available or not in a repo]" +GIT_ROOT="" +REPO_NAME="" +AGENTS_PATH="" +if command -v git >/dev/null 2>&1; then + GIT_ROOT=$(git --no-pager rev-parse --show-toplevel 2>/dev/null || true) + if [ -n "$GIT_ROOT" ]; then + REPO_NAME=$(basename "$GIT_ROOT") + AGENTS_PATH="$GIT_ROOT/AGENTS.md" + if [ -f "$AGENTS_PATH" ]; then + check1_output="REPO: $REPO_NAME | AGENTS.md: found" + else + check1_output="REPO: $REPO_NAME | AGENTS.md: NOT FOUND — cold start without repo governance" + (( DRIFT_COUNT++ )) || true + fi + else + REPO_NAME="no-git-repo" + check1_output="REPO: (not in a git repo) | CWD: ${CWD:-unknown}" + fi +else + log_error "check1-git-not-found" +fi +append "**Check 1 — Repo:** $check1_output" + +# ── Check 2: AGENTS.md verification ───────────────────────────────────────── +if [ -n "${AGENTS_PATH:-}" ] && [ -f "$AGENTS_PATH" ]; then + IDENTITY=$(head -3 "$AGENTS_PATH" 2>/dev/null | head -1 | sed 's/^#* *//' || true) + HAS_PROCEDURE=$(grep -qiE "^#{1,3}.*(procedure|checklist|numbered|step)" \ + "$AGENTS_PATH" 2>/dev/null && echo "found" || echo "not found") + check2_output="identity: ${IDENTITY:0:80} | Procedure section: $HAS_PROCEDURE" +else + check2_output="(skipped — no AGENTS.md)" +fi +append "**Check 2 — AGENTS.md:** $check2_output" + +# ── Check 3: Active plan detection ────────────────────────────────────────── +ACTIVE_PLAN="none found" +SEARCH_DIRS=() +[ -n "${GIT_ROOT:-}" ] && SEARCH_DIRS+=("$GIT_ROOT/spec" "$GIT_ROOT/docs/plans") +SEARCH_DIRS+=("$HOME_DIR/.claude/plans") + +for dir in "${SEARCH_DIRS[@]}"; do + if [ -d "$dir" ]; then + found=$(grep -rl --include="*.md" -E \ + "^(Status|status):[[:space:]]*(Active|In Progress)" \ + "$dir" 2>/dev/null | head -1 || true) + if [ -n "$found" ]; then + ACTIVE_PLAN="$(basename "$found")" + break + fi + fi +done +append "**Check 3 — Active plan:** $ACTIVE_PLAN" + +# ── Check 4: Memory conflict detection (stub) ─────────────────────────────── +append "**Check 4 — Memory conflicts:** deferred (FORGET mechanism pending — manual reconciliation path: reconcile-memory.sh)" + +# ── Check 5: Skills manifest currency ──────────────────────────────────────── +check5_output="skills manifest: (catalog-freshness script not found)" +CATALOG_BC="$HOOKS_DIR/catalog-freshness.sh" +if [ -f "$CATALOG_BC" ]; then + CATALOG_OUT=$(echo '{}' | bash "$CATALOG_BC" 2>&1 || true) + if echo "$CATALOG_OUT" | grep -qi "stale\|warn\|out of date"; then + check5_output="skills manifest: STALE — catalog-freshness advisory fired" + (( DRIFT_COUNT++ )) || true + else + check5_output="skills manifest: current" + fi +fi +append "**Check 5 — Skills:** $check5_output" + +# ── Check 6: Hook registration gap ────────────────────────────────────────── +check6_output="[CHECK 6: FAIL_OPEN]" +if [ -f "$SETTINGS_JSON" ] && command -v python3 >/dev/null 2>&1; then + GAP_OUTPUT=$(python3 - "$HOOKS_DIR" "$SETTINGS_JSON" << 'PYEOF' +import json, os, sys, re + +hooks_dir = sys.argv[1] +settings_path = sys.argv[2] + +# Hook scripts on disk that declare REQUIRED registration +on_disk = set() +if os.path.isdir(hooks_dir): + for fname in os.listdir(hooks_dir): + if not fname.endswith('.sh'): + continue + fpath = os.path.join(hooks_dir, fname) + try: + with open(fpath, encoding='utf-8', errors='replace') as f: + content = f.read(4096) + if '# REQUIRED settings.json registration' in content: + on_disk.add(fname) + except Exception: + pass + +# Scripts registered in settings.json +registered = set() +try: + with open(settings_path) as f: + s = json.load(f) + hooks_block = s.get('hooks', {}) + for event_hooks in hooks_block.values(): + for entry in event_hooks: + for h in entry.get('hooks', []): + cmd = h.get('command', '') + m = re.search(r'(\w[\w.-]+\.sh)', cmd) + if m: + registered.add(m.group(1)) +except Exception: + pass + +# Known libraries and cron scripts — not hooks, exclude from gap reporting +not_hooks = { + 'breadcrumb-lib.sh', + 'normalize-hook-input.sh', +} + +unregistered = sorted((on_disk - registered) - not_hooks) +print(f"{len(on_disk)} requiring-registration | {len(registered)} registered | unregistered: {', '.join(unregistered) if unregistered else 'none'}") +PYEOF + ) || { GAP_OUTPUT="FAIL_OPEN — python3 parse error"; log_error "check6-python-parse-error"; } + + if echo "$GAP_OUTPUT" | grep -q "unregistered: [a-z]"; then + (( DRIFT_COUNT++ )) || true + fi + check6_output="$GAP_OUTPUT" +else + log_error "check6-settings-or-python-missing" +fi +append "**Check 6 — Hook registration:** $check6_output" + +# ── Check 7: Conflict summary ──────────────────────────────────────────────── +if [ "$DRIFT_COUNT" -gt 0 ]; then + append "**Check 7 — Surface precedence:** $DRIFT_COUNT drift item(s) detected. Resolution order: Hooks > AGENTS.md > Plan > Skills > Memory > Inference. FORGET mechanism is not yet live — manual review required." +else + append "**Check 7 — Conflicts:** none detected" +fi + +# ── Write the report file ──────────────────────────────────────────────────── +{ + echo "# Startup Gate Report — $TS" + echo "" + for line in "${LINES[@]}"; do + echo "$line" + done + echo "" + echo "---" + if [ "$DRIFT_COUNT" -gt 0 ]; then + echo "Drift detected: $DRIFT_COUNT item(s) | Action required: yes" + else + echo "Drift detected: 0 | Action required: no" + fi +} > "$REPORT_FILE" 2>/dev/null || log_error "report-write-failed" + +# ── Emit minimal JSON (pointer to report, not the report content) ──────────── +python3 -c " +import json +drift = $DRIFT_COUNT +msg = 'Startup gate ran — see ~/.claude/startup-gate-report.md' +if drift > 0: + msg = f'[ACTION REQUIRED] Startup gate found {drift} drift item(s) — read startup-gate-report.md before starting task' +print(json.dumps({ + 'hookEventName': 'SessionStart', + 'hookSpecificOutput': { + 'additionalContext': msg + } +})) +" 2>/dev/null || true + +exit 0 diff --git a/guides/advanced/agents-md-standard.md b/guides/advanced/agents-md-standard.md new file mode 100644 index 0000000..21c2a20 --- /dev/null +++ b/guides/advanced/agents-md-standard.md @@ -0,0 +1,136 @@ +# AGENTS.md Governance Standard + +> How a repo declares its agent governance contract. + +When an AI agent opens a repository it has never worked in before, it operates without context. It does not know what the repo is for, which files are safe to modify, what the deployment process looks like, or whether there are agent-specific rules to follow. Without a governance declaration, every session starts cold. + +`AGENTS.md` is the solution: a file at the repository root that declares the governance contract for agents working in the repo. It is the first file an agent reads (AOF rule: `read-before-acting.md` Gate 0c) and the canonical source for repo-level agent behavior. + +--- + +## Three Levels + +### Level 1 — Identity Only + +The minimum viable `AGENTS.md`. One sentence identifying the repo, one sentence on what agents should do there. Establishes presence without investing significant documentation effort. + +```markdown +# AGENTS.md + +This is the **my-project** repo. Agents: default to reading before writing; use the PR workflow for all changes. +``` + +**When to use:** New repo, early project, or repo where AI agent work is infrequent. + +--- + +### Level 2 — Identity + Routing Table + +Adds a routing table that maps task types to the correct files, patterns, or subagents. Prevents agents from guessing where things live. + +```markdown +# AGENTS.md + +This is the **my-project** API service. + +## Routing + +| Task | Where to look | +|---|---| +| API changes | `src/api/` — read `src/api/README.md` first | +| Database migrations | `supabase/migrations/` — use `apply_migration` MCP, never raw SQL | +| Deployment | `scripts/deploy.sh` — read it before running | +| Tests | `npm test` — must pass before any PR | +``` + +**When to use:** Active repo where multiple types of work happen and agents need direction without reading every file. + +--- + +### Level 3 — Identity + Routing + Procedures + ADR Register + +The full governance contract. Adds numbered procedures (step-by-step, checkable) and an ADR register (key architectural decisions that agents must not reverse without reading the record). + +```markdown +# AGENTS.md + +This is the **my-project** API service. Agents: read this file first, every session. + +## Routing + +| Task | Where to look | +|---|---| +| API changes | `src/api/` | +| Schema changes | `supabase/migrations/` | + +## Procedures + +### Adding a new endpoint + +1. Read `src/api/README.md` — routing conventions and auth patterns +2. Read the existing handler closest to what you're building +3. Add route to `src/api/routes.ts`, handler to `src/api/handlers/` +4. Add tests to `tests/api/` +5. Run `npm test` — must pass before opening PR +6. PR title: `feat(api): ` + +### Schema changes + +1. Never use `execute_sql` for DDL — use `apply_migration` MCP only +2. Name the migration: `_.sql` +3. Commit the migration file to `supabase/migrations/` in the same PR +4. After merge, run `npm run types` to regenerate TypeScript types + +## ADR Register + +Key decisions. Read the ADR before reversing any of these: + +| ADR | Decision | Do not reverse without reading | +|---|---|---| +| ADR-001 | All auth handled by middleware, not per-route | `docs/adr/001-auth-middleware.md` | +| ADR-002 | Supabase for all persistence (no local DB) | `docs/adr/002-supabase-only.md` | +``` + +**When to use:** Production repo, multi-agent environment, or any repo where a wrong agent decision would be expensive to reverse. + +--- + +## What Makes a Good AGENTS.md + +**Procedures must be numbered and checkable.** A procedure that says "deploy carefully" is not a procedure. A procedure that says "1. Run `npm test`. 2. Run `./scripts/deploy.sh staging`. 3. Verify endpoint returns 200." is checkable — an agent can tick each step. + +**The routing table must be accurate.** An AGENTS.md that points to files that don't exist, or omits the most common task type, is worse than no AGENTS.md — it misdirects. Keep it current. + +**ADRs prevent expensive reversals.** If your team spent a week debating whether to use Supabase vs. a custom database and chose Supabase, that decision belongs in an ADR and a pointer in AGENTS.md. The next agent to open the repo will not know the debate happened. + +**Identity sentence = one sentence.** Agents read the first few lines to orient. "This is the X repo" with a single defining sentence is enough. Don't write a paragraph — it competes with the content that follows. + +--- + +## The Startup Gate Check + +The AOF's `startup-gate.sh` hook (v1.7) checks for AGENTS.md presence at session start: + +- **AGENTS.md found:** reports the identity sentence and whether a Procedure section exists +- **AGENTS.md not found:** flags as a drift item in the startup report + +This means a repo without AGENTS.md will produce a "cold start without repo governance" notice at the start of every agent session — a gentle but persistent incentive to add it. + +--- + +## Reference Implementations + +Two Level 3 implementations shipped in the private `smokin-os` and `smokin-memory` repos on 2026-06-07. Key patterns from those files: + +- **`smokin-os/AGENTS.md`** — declares the repo as infrastructure (Claude Code OS) with a 3-level agent contract: identity, routing by module (hooks, rules, skills, references), and a list of protected files that must not be modified without a PR. +- **`smokin-memory/AGENTS.md`** — declares the repo as a memory management layer with procedures for the three operations: retain, recall, and (when available) forget. + +Both files include a `## What agents must not do` section — a short list of invariants that are easy to violate accidentally. This section is the most important part of any governance declaration: it names the landmines. + +--- + +## Related + +- [`read-before-acting.md` Gate 0c](../../examples/claude-code-rules/read-before-acting.md) — Gate 0c mandates reading AGENTS.md as the first action in any new repo +- [`examples/hooks/startup-gate.sh`](../../examples/hooks/startup-gate.sh) — Check 1 (repo detection) and Check 2 (AGENTS.md verification) at session start +- [`AGENT_FRAMEWORK.md` §0.1](../../AGENT_FRAMEWORK.md) — Role Declaration (the AGENTS.md equivalent for the project-level CLAUDE.md) diff --git a/guides/advanced/silent-failure-discipline.md b/guides/advanced/silent-failure-discipline.md new file mode 100644 index 0000000..a18cab1 --- /dev/null +++ b/guides/advanced/silent-failure-discipline.md @@ -0,0 +1,119 @@ +# Silent Failure Discipline + +> Every fail-open path must write a log line. Silence is not success. + +The AOF's enforcement architecture classifies hooks by blast radius: destructive and security hooks fail-closed; advisory hooks fail-open. This is correct. The failure is what happens next: an advisory hook that fails open silently is indistinguishable from an advisory hook that fired successfully. + +A broken gate that logs nothing looks identical to a healthy gate that found nothing wrong. + +--- + +## The Discipline + +**Rule:** Any hook in `~/.claude/hooks/` that uses fail-open semantics — any code path that exits 0 without completing the hook's work (missing env var, malformed JSON, missing breadcrumb directory, parse error) — **must** write a tab-separated line to the central error log before exiting. + +**Error log location:** `~/.claude/migration-breadcrumbs/.errors.log` + +**Format:** one line per fail-open event, tab-separated: + +``` +\t\t +``` + +Example: +``` +2026-06-07T14:23:01Z source-truth-write-gate CLAUDE_SESSION_ID-unset-or-empty +2026-06-07T14:23:02Z no-guess-gate payload-parse-failed +``` + +--- + +## Implementation Pattern + +In every fail-open return path, add this before `exit 0`: + +```bash +printf '%s\t%s\t%s\n' \ + "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ + "$(basename "$0" .sh)" \ + "short-reason-slug" \ + >> "${HOME}/.claude/migration-breadcrumbs/.errors.log" 2>/dev/null || true +``` + +Or, if your hook already sources `breadcrumb-lib.sh`, define a `bc_write_error` wrapper: + +```bash +bc_write_error() { + local reason="$1" + local errors_log="${HOME:-$USERPROFILE}/.claude/migration-breadcrumbs/.errors.log" + mkdir -p "$(dirname "$errors_log")" 2>/dev/null || true + printf '%s\t%s\t%s\n' \ + "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ + "$(basename "${BASH_SOURCE[1]:-$0}" .sh)" \ + "$reason" \ + >> "$errors_log" 2>/dev/null || true +} +``` + +The final `|| true` ensures that even a failing write to the errors log does not crash the hook. The write is best-effort. + +--- + +## Reason Slugs + +Use a small, stable vocabulary of snake_case slugs. Reusing the same reason across hooks lets monitoring tools cluster identical failure modes: + +| Slug | When to use | +|---|---| +| `CLAUDE_CODE_SESSION_ID-unset-or-empty` | Session key env var missing | +| `payload-parse-failed` | JSON stdin parse error | +| `breadcrumb-dir-missing` | Breadcrumb directory could not be created | +| `dependency-missing` | Required tool (python3, jq, git) not in PATH | +| `library-load-failed` | `source breadcrumb-lib.sh` failed | +| `settings-json-missing` | `~/.claude/settings.json` not found | +| `telemetry-env-vars-not-set` | AOF_TELEMETRY_URL or AOF_TELEMETRY_KEY unset | + +--- + +## Self-Test + +Before shipping any hook that has fail-open paths, verify the error log write: + +```bash +# Force the fail-open path (e.g., unset the session ID) +unset CLAUDE_CODE_SESSION_ID + +# Pipe a test payload to the hook +echo '{"prompt":"test"}' | bash ~/.claude/hooks/your-hook.sh + +# Confirm a new line appeared in the errors log +tail -1 ~/.claude/migration-breadcrumbs/.errors.log +# Expected: \tyour-hook\t +``` + +If `tail -1` does not show a new row, the hook is silent on failure — fix before deploying. + +--- + +## The Watchdog + +The error log feeds a session-close watchdog: a Stop hook that reads `.errors.log` for lines written during the current session and surfaces them as a summary. This converts invisible fail-opens into visible events — you see which hooks are degraded and why before the next session starts. + +The watchdog is most useful when run for the first time: a fresh installation often produces a burst of `CLAUDE_CODE_SESSION_ID-unset-or-empty` entries that reveal hooks sourcing the wrong env var. + +--- + +## Origin + +A full governance audit of 63 hooks on 2026-06-07 found approximately 50 silent fail-open paths. `apply-migration-breadcrumb.sh` and `apply-migration-stop-warn.sh` had already implemented the error-log contract and produced 1,441 entries in `.errors.log` over the prior month. The remaining hooks had fail-open paths that exited 0 without logging — the watchdog could not detect them. + +This discipline forces every fail-open path into the visible set. The watchdog can then report what it actually knows vs. what it is blind to. + +--- + +## Related + +- [`guides/hook-operations.md`](../hook-operations.md) — the three operational questions (failure, theater, escape) +- [`examples/hooks/breadcrumb-lib.sh`](../../examples/hooks/breadcrumb-lib.sh) — session-scoped breadcrumb API +- [`AGENT_FRAMEWORK.md` §5.2](../../AGENT_FRAMEWORK.md) — fail-mode taxonomy +- [`guides/advanced/hook-audit-methodology.md`](hook-audit-methodology.md) — 4-track audit pattern