DEV Community

Cover image for Dev OS v2: gates, a learning loop, and agents that check agents
Dhrupo Nil
Dhrupo Nil

Posted on Edited on

Dev OS v2: gates, a learning loop, and agents that check agents

In v1 I treated ~/.claude as an operating system around the model: a kernel (one rulebook), a scheduler (lanes), interrupts (hooks), a package manager (pinned skills). I asked you to run the starter kit for a week and tell me what broke.

I ran it too, every day. 213 commits later, this is the v2 release, and its main change fits on one line:

A rule the agent reads is a request. A rule something runs is a gate.

Both commenters on v1 had said the same thing. Here is what that changed, in order of how much it helped. The gates, hooks, agents and tests below ship in the kit (135 checks, bash tests/run.sh); the rest is how I run my own setup. The kit works with any agent that reads AGENTS.md (Codex, Cursor, Copilot), not only Claude Code.

1. One rulebook, every agent

The kernel is the same: one CLAUDE.md whose first section says it overrides every skill, plugin and repo file. What's new is that AGENTS.md is a symlink to it.

ln -s ~/.claude/CLAUDE.md ~/.codex/AGENTS.md   # Codex reads ~/.codex/AGENTS.md, then one per folder, root → cwd
Enter fullscreen mode Exit fullscreen mode

I checked how each tool loads it on 2026-11-01:

  • Codex concatenates AGENTS.md files up to 32 KiB by default. A long global rulebook steals room from repo rules. Mine is capped at 136 lines by a test.
  • Claude Code reads AGENTS.md only in a repo with no CLAUDE.md, so CLAUDE.md stays the source.
  • @lessons.md imports are Claude-only. Through the link, Codex gets the rulebook but not its imports.

2. Gates beat reminders

A test gate in plain git. v1's pre-commit hook only guarded ~/.claude itself. v2 installs one per project:

echo 'npm test' > .dev-os-test && ~/.claude/bin/gate-install
Enter fullscreen mode Exit fullscreen mode

The hook it writes (condensed from bin/gate-install):

TOP=$(git rev-parse --show-toplevel)
unset GIT_INDEX_FILE GIT_DIR GIT_WORK_TREE GIT_PREFIX
CMD=$(cat "$TOP/.dev-os-test" 2>/dev/null)
if ! printf '%s' "$CMD" | grep -q '[^[:space:]]'; then
  echo "dev-os test gate: .dev-os-test is missing or empty; commit refused" >&2; exit 1
fi
(cd "$TOP" && bash -o pipefail -c "$CMD") || { echo "dev-os test gate: commit refused" >&2; exit 1; }
Enter fullscreen mode Exit fullscreen mode

Every line there is a bug I hit:

  • pipefail: npm test | tail -5 exits with tail's 0, so a red suite commits.
  • Empty means refuse: a missing or empty test command fails loudly instead of passing quietly.
  • unset GIT_INDEX_FILE: git runs pre-commit with a temporary index during commit -a. A test suite that runs git add in a temp repo writes into your commit and breaks it with "Error building trees".
  • No global hooks: the installer refuses when a global core.hooksPath would turn one repo's gate into every repo's gate.

Permissions the agent can't talk past. permissions.ask rules still prompt under --dangerously-skip-permissions, and compound commands are split before matching:

"permissions": { "ask": ["Bash(git push:*)", "Bash(gh pr create:*)"] }
Enter fullscreen mode Exit fullscreen mode

One catch: rules match the command text, so git -C . push slips past Bash(git push:*). I add Bash(git -C * push *) in my own settings. Keep branch protection on the remote as the backstop.

A Stop hook for the cheap way out. v1's Stop hook sent the agent back when source changed with no test. v2 adds one more case: a weakened test. That means fewer assertions, a new .skip/.only, or a deleted test file.

ASSERT = re.compile(r"\bassert\w*\s*\(|\bexpect\s*\(|->assert\w*\(|\$this->assert|^[-+]\s*assert(_\w+)?\s")
SKIP   = re.compile(r"markTestSkipped|markTestIncomplete|->skip\(|\.(skip|only|fixme)\s*\(|\bx(it|describe|test)\s*\(")
if removed_asserts > added_asserts or any(SKIP.search(l) for l in added):
    weakened.append(path)        # → {"decision": "block", "reason": "TEST WEAKENED: …"}
Enter fullscreen mode Exit fullscreen mode

Codex hooks use the same Stop → {"decision":"block"} shape.

3. It learns by itself

In v1, "add a line to lessons.md" depended on me remembering to do it. Now the setup logs its own mistakes:

prompt starts "no, …" / "I said …"   ─┐
stop-check or test gate blocks       ─┼─▶ state/learn-<session>.jsonl
                                       │
Stop: learn.sh → any entry newer than the last `reflect`? → block once: "run reflect"
Enter fullscreen mode Exit fullscreen mode

reflect routes each mistake to the lowest layer that would have caught it: first a test or hook, then one line in the skill that owns the job, and only last a line in lessons.md. lessons.md has a 60-line budget. When it fills up, meditate audits the rules for stale, duplicate and contradicting lines, and deletes nothing without my yes. 59 of the 213 commits are Lesson: commits, and many of them removed a lesson by turning it into a check.

The loop gets tested like everything else. A cold review caught the correction detector flagging "No tests exist for the parser yet" as a correction. Now no counts only when followed by punctuation, and only the first line of a prompt is read.

4. Agents check agents

One agent reviewing its own diff is a student grading their own exam. The kit ships two read-only agent shapes:

  • scout: one angle per scout (callers, tests, history, config). Each returns file:line facts and what it couldn't check.
  • review-lens: Skeptic always, Architect at 50+ changed lines, Minimalist at 200+. Then one verifier per finding that starts from "false positive".

Two skills run them. pre-merge-review is relay step 7 on your own branch: lenses sized by the diff, a verifier per finding, one saved report. post-pr-review is for someone else's PR: each finding must reproduce on both the base and the PR branch before it goes in a draft, and nothing is posted without a yes. /code-review stays the per-slice check inside build-check, and it never posts on other people's PRs.

Every subagent brief has the same fields: goal, scope, what's known, what's been ruled out, the exact ask, the output format, what's out of scope. Parallel builders get one git worktree each, and only for plan slices that share no files.

How well does this work? On the v2 kit itself, which already passed all its own tests:

Pass Found
/code-review 9 (6 fixed, 3 left with reasons)
3 lenses, each reproducing its findings in a temp dir 14 more
a cold reviewer that saw none of the above 4 more

Every behaviour fix got a red test first.

5. Prove it by running it

9. A bug or security claim counts only when reproduced by running it. No repro, no fix.
Enter fullscreen mode Exit fullscreen mode

For bugs, the first job is a red-capable feedback loop, in the order from Matt Pocock's diagnosing-bugs: failing test → curl script → CLI snapshot diff → headless browser → replayed payload → harness → fuzz → git bisect run. Hypotheses come after.

I also break every gate on purpose before I trust it. That caught a test that passed before the code existed. My helper matched substrings, so rc=127 ("command not found") satisfied a check for rc=1.

6. Project memory: graph first

Rule 8: before grep, ask a code graph "who calls this, and what breaks if I change it?"

Tool Used for
graphify query / explain / path / merge-graphs Finding code, explaining a node, call paths, one graph across repos
code-review-graph get_impact_radius_tool Who else a fix reaches (the no-regressions rule)
get_review_context_tool, get_affected_flows_tool Review context; which E2E flows a diff touches

The scouts, review lenses and review-reply step all start there. The graph holds structure. What the project taught lives in lessons.md and in plan folders kept outside the repo.

7. Stay lean

  • A context budget test: rulebook plus skill descriptions, measured in bytes, against a baseline. It went from 59,287 to 46,927 while the setup grew.
  • Routing is tested: one rulebook sentence ("call the owner skill before any other tool") took my routing eval from 0-1 to 7-8 of 11 prompts.
  • Effort is a budget too: "effortLevel": "medium" by default, in every modelSettings entry as well, since a per-model value wins. High effort lives in skill frontmatter only where a wrong answer is expensive: grilling, writing and checking the plan, whole-plugin bug hunts. Hooks can read the effort level but not set it, so serious cases are handled by a rule: payment or refund, data loss, auth or a security finding mean the verifiers are spawned with effort: "high".
  • Smaller output: RTK trims git/gh/test output before the agent reads it. Compressed output can hide the one line you need, so "never report a result you couldn't see" is a rule.

The userland

Most of my 103 skills are other people's work: 39 are pinned to an upstream commit in skills.lock, with local edits in overlays that a sync script re-applies.

Skills use the Agent Skills format. Codex reads them from .agents/skills, and Claude Code from .claude/skills, so one folder with two symlinks serves both.

Upgrade or start

New: follow the kit's README "Start here". The core (rulebook, test gate, plan template) takes about 10 minutes, and the Claude Code layer is optional.
From v1: the README has an "Upgrading from v1" block. Nothing was renamed. Two hooks and two skills get replaced after a "Before v2" commit, so it's one git revert to undo.

Same ask as v1: run it for a week and tell me what broke. And what's the one rule your agent keeps breaking? I'll tell you which gate I'd put behind it.

Top comments (0)