{"slug": "stop-asking-your-coding-agent-to-behave-gates-not-prompts", "title": "Stop Asking Your Coding Agent to Behave: Gates, Not Prompts", "summary": "A developer built sdlc-playbooks, an open-source, file-based delivery procedure for agentic coding that runs in both Claude Code and Codex from a single source. The workflow replaces prompt-based behavioral rules with seven phases, each backed by a script gate that exits non-zero when artifacts are missing or state is inconsistent, keeping human approval between prototype and implementation. The project stores all workflow state in repo artifacts so a human can finish the work even if every playbook is deleted.", "body_md": "Agentic coding is fast at producing code and bad at producing *the right* code.\n\nIn my experience the usual failure isn't a bug. It's a feature nobody specified, built from a design nobody approved, against a mock-up that changed after the work started. The code is fine. It just answers the wrong question.\n\nI built [**sdlc-playbooks**](https://github.com/srnux/sdlc-playbooks) to catch those failures mechanically. It's a file-based delivery procedure (design system → requirements → prototype → human approval → product) that runs in Claude Code and in Codex from one source. This post covers how it works and why it's shaped the way it is.\n\n**The repo defines playbooks instead of specialised agents.** Each playbook describes a phase: what it needs, which checks must pass, what work happens, and what artifact it produces. The coding agent executes that procedure. Moving from requirements to implementation means changing the playbook, without needing a separate \"product owner\" or \"engineer\" agent definition.\n\nOne idea runs through the whole thing:\n\n**A rule written only as prose for a model to honour will eventually be skipped.**\n\nSo I move checkable preconditions into scripts that exit non-zero. The scripts catch missing artifacts and inconsistent state; people still decide whether the requirements and the result are right.\n\n```\n  design system                 PO requirements (text)\n          │                              │\n          ▼                              ▼\n   0 lock-design-system          1 capture-requirements\n   tokens.css                    REQ-### + acceptance criteria\n   components.md                          │\n          └──────────────┬─────────────────┘\n                         ▼\n                 2 build-prototype\n             single-file React page\n            every screen, every state\n                         │\n                         ▼\n                  ◆ HUMAN APPROVES ◆\n                   3 freeze-approval\n            frozen snapshot v<N> + stories\n                         │\n                         ▼\n        4 plan-story → 5 implement-story → 6 review-change\n                         │\n                         ▼\n                    the product\n```\n\nThere are seven phases, each with one playbook and one gate. Everything the flow knows is stored in a file:\n\n| Artifact | Written by | \n|---|---|\n| `design-system/tokens.css` ,`components.md` | phase 0, then locked | \n| `work/items/REQ-###.md` | phase 1 | \n| `prototype/<slug>.html` | phase 2 | \n| `prototype/.approved/<slug>-v<N>.html` | phase 3, versioned baseline | \n| `work/items/ST-###.md` | phase 3, one story per screen | \n| `work/plans/ST-###.md` | phase 4 | \n\nWith the default file tracker, the workflow state lives in the repo. **If you delete every playbook, a human can still finish the work from the artifacts.** That's the intended direction of dependency.\n\nMost agent setups I've seen start with personas: *\"You are a senior reviewer.\"* That describes an identity, but leaves the working procedure to be specified elsewhere. Here, the playbook is the unit of organisation. It names the inputs, gate, outputs, and done-condition for a phase. For example:\n\n```\n## Contract\n\n|                |                                                              |\n|----------------|--------------------------------------------------------------|\n| **Phase**      | 5 — the only phase that writes product code                  |\n| **Gate**       | `node .claude/tools/gate.mjs implement ST-###`               |\n| **Inputs**     | the plan, the pinned snapshot, the catalog, the component lib |\n| **Produces**   | product code, one test per AC                                |\n| **Done when**  | every `verify.commands` entry is green, story is `in-review` |\n| **Never**      | copy the prototype into the product; build against the live prototype |\n```\n\nEach playbook also routes neighbouring decisions: new tokens go to `lock-design-system`, changed scope goes to `capture-requirements`, and finished work goes to `review-change`.\n\nThe earlier version had separate agent definitions. Most of their instructions already existed in the matching playbooks; the remaining ownership rules belonged at phase boundaries. Removing those files left one place to maintain each procedure. Keeping two copies of a rule means you have one rule and one future contradiction.\n\nHandoffs happen through saved artifacts. Requirements supply acceptance criteria to prototyping; an approved snapshot supplies the baseline for planning and implementation; the plan and resulting code supply the inputs to review. The next phase can run in a fresh session because its inputs are in the repo. It doesn't need the previous agent's persona or conversation history to reconstruct what was agreed.\n\nEvery phase begins with:\n\n```\nnode .claude/tools/gate.mjs <phase> [id] [flags]\n```\n\nThe gate checks the phase's preconditions against the repo. If one fails, it **exits 2 with a named reason and a fix**. The Claude Code command files invoke it through inline shell expansion:\n\n```\n---\ndescription: Record a human's approval — freeze the prototype and cut the stories.\n---\n\nGate — this runs before you read any further; a non-zero exit aborts the command:\n\n!`node .claude/tools/gate.mjs approve $0 $1 2>&1`\n```\n\nThat puts a concrete check at the command entry point. The playbook instructs the agent to stop on failure. Direct script calls and other editing paths still need their own controls.\n\n| Phase | Refuses when | \n|---|---|\n| `design-system` | already locked and `--refresh` wasn't passed | \n| `requirements` | no input | \n| `prototype` | design system unlocked · wrong status · **no acceptance criteria** | \n| `approve` | not `prototyped` · no`data-screen` markup ·**no `--human-approved`** | \n| `plan` | parent not approved · no version pin · **snapshot missing** | \n| `implement` | **no plan** · story not`in-progress` · snapshot missing | \n| `review` | story not `in-review` ·**already at `loops.reviewRounds`** | \n\nThe old version of `/approve` said: *\"This command represents a human decision. Do not run it on your own judgment.\"*\n\nThe gate now requires an explicit flag:\n\n```\nif (!args['human-approved']) {\n  throw new Blocked(\n    `approval is a human decision and has not been recorded for ${id}`,\n    `ask the person to open ${req.prototype} and say yes, then run: /approve ${id} --human-approved`\n  );\n}\n```\n\nThe bundled seaside example illustrates the check: even with a prototype ready to inspect, an approval gate call without the flag is refused:\n\n``` bash\n$ node .claude/tools/gate.mjs approve REQ-003\nBLOCKED — approve: approval is a human decision and has not been recorded for REQ-003\n\n  ask the person to open prototype/seaside-zadar-landing.html and say yes,\n  then run: /approve REQ-003 --human-approved\n\nNothing was changed. Report this reason verbatim and stop; do not work around it.\n$ echo $?\n2\n```\n\nThe flag means \"a person said yes in this conversation.\" It makes that assertion explicit and reviewable, but does not verify it independently: the agent can supply the flag, and the snapshot utility can be called directly. Respecting human approval still depends on following the procedure.\n\nReview can approve a story or send it back. The gate reads the highest `round N:` recorded in the story's notes and refuses a round beyond `loops.reviewRounds` (two by default). That bound depends on the reviewer recording each round. When the cap is reached, the procedure calls for handing the disagreement to a person.\n\nOn success, gates print resolved context as JSON: paths, the pinned prototype version, acceptance-criterion ids, and the review round where relevant. The agent gets concrete inputs for its next step.\n\nHooks add checks at supported edit entry points. In Claude Code, two shell scripts are wired as `PreToolUse` hooks on `Edit|Write`:\n\n**`require-plan.sh` blocks a guarded edit when it identifies a story with no plan.** It finds the story from the branch name (`story/ST-007-…`) or, failing that, from exactly one `in-progress` story in the tracker. For example:\n\n```\nBLOCKED — no plan for ST-007.\n\n  expected: work/plans/ST-007.md\n  editing:  src/contacts/list/ArchiveButton.tsx\n\nRun the plan-story skill first. The plan is a gate, not paperwork.\n```\n\nThe reasoning: a plan written after the code is just a summary, and a summary never catches the thing you'd have noticed before starting.\n\n**`check-hardcoded-colors.sh` warns.** It flags a raw `#hex`, `rgb()` or `hsl()` at the moment it's written, while fixing it is still a one-line change. The blocking version of this rule runs at review time.\n\nThe hooks have limits: shell writes fall outside Claude's `Edit|Write` matcher, some file types are excluded, and the plan hook allows an edit when no story can be identified. Both scripts accept `file_path` or `path`; actual coverage also depends on the harness invoking them with a supported payload.\n\n**`check-tokens.mjs`** scans prototypes and configured product directories for raw hex/RGB/HSL colours and literal values in selected properties: font size, radius, shadow, padding, margin, and gap. It also checks for undefined token references. `width: 100%` and `z-index` are outside those design-property checks.\n\n``` bash\n$ node .claude/tools/check-tokens.mjs\nok — 139 tokens defined, 134 used, no raw design values in 1 files\n```\n\nThe procedure's rule is that **a design value can only be created in phase 0**. The scanner catches common violations; it is not a complete CSS validator. It permits token-definition blocks for inlined prototypes without verifying that their values match the locked file.\n\n**`check-coverage.mjs`** checks structural links:\n\n```\nrequirement → approved snapshot → screen → story → plan file\n```\n\nIt reports gaps such as `screen-without-story`, `story-without-plan`, `stale-pin`, and `done-story-screen-vanished`, exiting 2 when it finds one. It checks that acceptance criteria exist, but does not map each AC to a screen or inspect implementation and test coverage.\n\nThe prototype convention puts two attributes on every screen root:\n\n```\n<section data-screen=\"SCR-booking-enquiry\" data-req=\"REQ-003\">\n```\n\nThe checker discovers screens through `data-screen`; the approve gate refuses a prototype with no screen markers. `data-req` records the intended requirement association.\n\n**Structural gaps become queryable.** Whether the product satisfies the requirements still needs tests and review.\n\nPhase 2 calls for one HTML file per requirement: React 18 UMD plus `@babel/standalone` from a CDN, tokens inlined, catalog components, hash routing, and realistic mock data. It needs no build step and opens with a double-click, but loading its CDN dependencies requires network access. The playbook requires **every state to be drawn**: empty, loading, populated, error, *and success*. \"The list refreshes\" doesn't show a person what success looks like.\n\nAfter human approval, the procedure uses `approve.mjs` to create `prototype/.approved/<slug>-v<N>.html`, then cuts one story per screen with a `prototypeVersion: v<N>` pin. These are versioned snapshots treated as immutable by the procedure. The utility checks for an existing destination before copying; the resulting files remain editable on disk.\n\n**The implementation playbook directs the agent to build against the pinned snapshot.** If someone makes \"one more tweak\" to the live file, `gate.mjs implement` reports the difference:\n\n```\n{\n  \"liveHasMovedPastThePin\": true,\n  \"note\": \"prototype/bulk-archive.html has moved past v2. You build v2. The difference is new scope for the PO — record it under Divergence, do not absorb it.\"\n}\n```\n\nBuilding the latest prototype when the story pinned v2 is scope drift, and you won't see it in the diff unless someone says so. This is how a two-week feature quietly becomes a four-week one without anyone deciding it should.\n\nProcedures live in exactly one place: `playbooks/<name>/SKILL.md`, written without reference to any particular tool. `sync.mjs` copies that tree into `.claude/skills/` for Claude Code and `.agents/skills/` for Codex:\n\n```\nnode .claude/tools/sync.mjs          # write the projections\nnode .claude/tools/sync.mjs --check  # exit 2 if one is stale (for CI)\n```\n\nEach generated directory has a `.generated` marker holding the source hash, so a hand-edit to a projection is detected, not silently kept.\n\nCopies avoid the extra permissions or developer-mode setup that symlinks can require on Windows.\n\nClaude Code has slash commands such as `/requirements`, `/prototype`, and `/build`. The projected skills expose the same playbooks, including `build-prototype`, with a gate invocation as step 0. The shared procedure does not make the harnesses' enforcement identical.\n\nNo playbook talks to a tracker directly. Everything goes through `node .claude/tools/tracker.mjs <verb>`, which prints JSON and dispatches to a provider:\n\n```\nnode .claude/tools/tracker.mjs create --type requirement --title \"Bulk archive contacts\"\nnode .claude/tools/tracker.mjs set ST-007 --status in-review\nnode .claude/tools/tracker.mjs next --type story\n```\n\nThe default provider is `files`, storing Markdown with YAML front matter in `work/items/`. Jira support is unfinished: mapping and REST code exist, but the gate's asynchronous provider handling and the coverage checker's direct file reads still need integration work.\n\nTwo procedural rules keep updates consistent: **never hand-edit a work item, and never pick an id yourself.** `create` allocates IDs centrally so agents don't choose them manually. Allocation is not protected against concurrent writers.\n\nThe adapter is intended to let status and conversation move to another tracker while design, prototype, and plan artifacts stay in the repo.\n\nNone of the enforcement needs an LLM. You can check the state of the repo from a plain shell:\n\n```\nnode .claude/tools/status.mjs --gaps\nnode .claude/tools/check-coverage.mjs\nnode .claude/tools/check-tokens.mjs\nnode .claude/tools/gate.mjs plan ST-001\n```\n\nThe local tools use Node 18+ and plain ESM, with no npm dependencies. The hooks require Bash; on Windows, that means a Bash installation such as Git Bash.\n\nThere's no CI/deploy stage, no QA phase, no parallel fan-out, no worktree isolation, and no board integration. Those are all real and sometimes necessary. They're also where a procedure stops being easy to read.\n\nThe rule for adding one: do it when a specific failure demands it, and write down which failure.\n\n**A gate with no incident behind it is ceremony.**\n\nClone the repo and generate the skill projections in your terminal:\n\n```\ngit clone https://github.com/srnux/sdlc-playbooks\ncd sdlc-playbooks\nnode .claude/tools/sync.mjs\n```\n\nEdit `.claude/sdlc.config.json` for your product and verification commands, and rewrite the stack-specific half of `.claude/rules/coding-standards.md`.\n\nThe repo includes a locked seaside-rental design system, a prototype with nine marked screens, and three requirements. You can inspect that example first. For your own design, update the configured design source and run `/design-system --refresh` in Claude Code. A bare `/design-system` refuses because the bundled system is already locked.\n\nThen, in Claude Code:\n\n```\n/requirements 'PO text here…'\n```\n\nUse the requirement ID returned by that command in `/prototype <returned-id>`. Open the result and review its screens and states. Only after you approve it, run `/approve <returned-id> --human-approved`, then `/build`.\n\nMy one piece of advice: **run one real requirement end to end before you load up the backlog.** That gives you a concrete way to find gaps in the procedure while the scope is still small.\n\nIf you've built something similar, or tried to hold an agent to a process with prompts alone and watched it slip, I'd like to hear what broke first.", "url": "https://wpnews.pro/news/stop-asking-your-coding-agent-to-behave-gates-not-prompts", "canonical_source": "https://dev.to/srnux/stop-asking-your-coding-agent-to-behave-gates-not-prompts-daa", "published_at": "2026-09-25 15:17:22+00:00", "updated_at": "2026-09-25 15:30:55.794008+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "developer-tools", "mlops"], "entities": ["sdlc-playbooks", "Claude Code", "Codex", "GitHub"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/stop-asking-your-coding-agent-to-behave-gates-not-prompts", "markdown": "https://wpnews.pro/news/stop-asking-your-coding-agent-to-behave-gates-not-prompts.md", "text": "https://wpnews.pro/news/stop-asking-your-coding-agent-to-behave-gates-not-prompts.txt", "jsonld": "https://wpnews.pro/news/stop-asking-your-coding-agent-to-behave-gates-not-prompts.jsonld"}}