{"slug": "same-task-two-endings-a-payment-integration-with-and-without-the-planning", "title": "Same Task, Two Endings: A Payment Integration With and Without the Planning Gateway", "summary": "A developer tasked with adding Stripe as a payment method to a checkout service violates four architectural decision records (ADRs) because the AI agent's context window loses critical rules over a long session, leading to rework after human review catches the violations days later.", "body_md": "## The task\n\nThe developer types one sentence: *“Add Stripe as a payment method to the checkout service.”* The agent captures it, and from this point on, everything depends on what surrounds that sentence.\n\nA payment method touches an external provider (ADR-042 applies), needs a schema change (ADR-051 applies), lands in Go code (ADR-060 applies), and sits one directory away from frozen legacy files (ADR-070 applies). Four rules, all relevant, none of them in the sentence.\n\n## Act 1: two hundred lines of hope\n\nWithout a gateway, the first layer of governance is front-loaded. Somewhere in the project there is a `CLAUDE.md`\n\nthat says, among two hundred other things:\n\n```\n...\n- All external calls MUST go through security-egress-proxy (see ADR-042)\n- Never modify internal/old_payment.go or internal/auth/ (frozen, ADR-070)\n- DB migrations before code (ADR-051); always add tests (ADR-060)\n- Use table-driven tests; prefer errors.As; run goimports; ...\n...\n```\n\nThese files act once, at minute zero. The proxy rule is real, but it is page 3 of a wall of text, competing for attention with formatting conventions and tribal lore.\n\n## The team did it right: a skill\n\nTo be fair to Act 1: this team follows the state of the art. The platform team packaged the payment workflow as a skill, and the developer invokes it: `/add-payment-method Stripe`\n\n. Its `SKILL.md`\n\ninjects the right rules at the right moment, not buried on page 3:\n\n```\n---\nname: add-payment-method\ndescription: Adds a payment provider to checkout, following platform ADRs.\n---\n1. Route every provider call through security-egress-proxy (ADR-042).\n2. Generate the schema migration BEFORE the code that uses it (ADR-051).\n3. Add an integration test step (ADR-060).\n4. Never touch internal/old_payment.go or internal/auth/ (ADR-070).\n```\n\nThis is genuinely better than the wall: focused, versioned, reviewed. But look at what it is: **text, injected into the context**. A semantic directive the agent is trusted to follow, with no mechanism that verifies it followed it. The skill states the rules; nothing in the loop checks the result against them.\n\n## The agent gets to work\n\nThe agent is competent, and it starts well: the skill’s instructions are fresh, the migration comes first, the proxy is used. Then the session gets long. A refactor here, a follow-up question there; forty minutes and a few context compactions later, the skill’s four rules are just more tokens far behind.\n\nBecause agents finish what they find: *“while I’m here, internal/old_payment.go has a helper that almost fits; I’ll adapt it”*. A small tweak in\n\n`internal/auth/`\n\nto expose the customer id. And the second Stripe endpoint gets called directly, `api.stripe.com`\n\n, no proxy. Each step violates an instruction that is *still in the context*; nothing is there to notice.\n\n## Everything looks fine\n\nThe code compiles. Local tests are green. The diff is tidy and well-commented. The agent’s Observe step checks everything it can see, and everything it can see is fine.\n\nThat is the trap: nothing in the loop can observe “this file is frozen” or “this call bypasses the egress proxy”. Those are organizational facts, and the loop has no channel for them.\n\n## Days later: the late gate\n\nThe pull request meets CI and a human reviewer. The verdict is correct, and it arrives at the worst possible moment:\n\n`internal/old_payment.go`\n\nand`internal/auth/`\n\nare frozen (ADR-070); please revert.- The Stripe client calls the API directly; route it through\n`security-egress-proxy`\n\n(ADR-042).- The migration was added in the same commit as the code that needs it; split and reorder (ADR-051).\n\nThree architectural decisions, all documented, all violated, all detected after the work was done.\n\n## The bill\n\nThe agent’s session is long gone; its context has evaporated. The rework cannot be absorbed as one more iteration of the loop: it re-enters at the top, as a new intent, carried by a frustrated human who now has to explain what “frozen” means to a fresh session.\n\nNote what failed. Not the rules (they were correct), not their delivery (CLAUDE.md *and* a well-written skill put them in the context), not the agent (it read them). What failed is that every rule was **text**: advisory by construction. As the [previous article](https://blog.owulveryck.info/2026/07/07/amplified-agentic-loop.html) put it: feedback that arrives inside the loop costs one iteration; feedback that arrives after the loop costs the whole loop. Act 1 just paid the whole loop.\n\n## Act 2: same task, governed loop\n\nReplay. The repository is reset, the intent is identical, the agent is the same stock Claude Code. One thing has changed: a **Platform Planning Gateway** now sits between the agent and the work, and the rules changed form. The responsibilities split cleanly in three:\n\n- the\n**platform team** operates the gateway and exposes its gates (over HTTP, and as[MCP](https://modelcontextprotocol.io/)tools the agent sees natively); - each\n**stream team** writes its rules twice (a semantic directive plus an executable policy) and pairs its skills with a policy; - the\n**agent** executes the skill, and every decision it makes passes through the gateway’s endpoints.\n\nThe next two scenes show what “written twice” and “paired with a policy” mean, concretely.\n\n## The rule, written twice\n\nTake ADR-060 (“a Go change ships with tests”). In Act 1 it was one line of prose. In Act 2 it is a **dual-representation artifact**: the Markdown invariant stays (the agent will reason over it at planning time), and next to it the team wrote `ADR-060.rego`\n\n, an executable policy in [Rego](https://www.openpolicyagent.org/docs/latest/policy-language/):\n\n``` python\npackage ppg.linter\n\nimport rego.v1\n\nviolation contains v if {\n    input.repository_context.tech_stack[_] == \"Go\"\n    not plan_has_go_test\n    v := {\n        \"policy_id\": \"go_tests_present\",\n        \"message\":   \"SDLC invariant violated: the plan has no test step. Add a step whose tool is \\\"go-test\\\", or whose action runs 'go test'.\",\n        \"nature\":    \"amplifier\",\n    }\n}\n\nplan_has_go_test if {\n    input.steps[_].tool == \"go-test\"\n}\n\nplan_has_go_test if {\n    some step in input.steps\n    contains(lower(step.action), \"go test\")\n}\n```\n\nRead it as a sentence: *if the stack is Go and no step satisfies plan_has_go_test, emit this violation.* The two helper rules are an OR: the canonical\n\n`go-test`\n\ntool, or any step whose action runs `go test`\n\n(agents describe steps with their own tool names; the policy meets them where they are). Note that the message contains the exact criterion: a rejected agent should never have to guess. The `input`\n\nis the agent’s plan; no LLM anywhere: the gateway loads every ADR-paired `.rego`\n\ninto an embedded [OPA](https://www.openpolicyagent.org/)engine, and evaluation is deterministic. Keep this policy in mind: you will see it fire three scenes from now.\n\n## The skill comes back, with its policy\n\nRemember the skill from Act 1? It is not thrown away: it is **promoted**. The team ships version 2, where the body is no longer a list of rules to remember but a workflow that puts the gateway inside the loop, and pairs it with a companion policy:\n\n```\n---\nname: add-payment-method\nversion: 2.0.0\n---\n1. Call get_platform_guidelines_for_intent with the intent and repo context.\n2. Draft the plan honoring the invariants; submit it through lock_in_plan.\n3. Use Edit to implement, staying within the ticket scope.\n```\n\nPublication goes through the platform’s validation gate, `POST /validate_skill`\n\n. Because the skill instructs file modifications, the gate requires the companion `SKILL.rego`\n\n; with it, the gate answers:\n\n```\n{ \"status\": \"SKILL_VALID\", \"tier\": 1 }\n```\n\nThat is the division of labor: **the team ships the capability and its policy; the platform ships the gate**. (In the PoC the companion policy is enforced at this publish gate; evaluating it again when a plan declares which skill built it is the documented next step.)\n\n## enrich(): the architect chat, automated\n\nThe developer types the same command as in Act 1: `/add-payment-method Stripe`\n\n. The skill executes, and its first instruction sends the intent to the gateway: *“here is what I am about to do; which of our decisions apply?”* The word “payment” in the intent matches the scope selectors of two ADRs, and the gateway answers with invariants (never recipes):\n\n```\n{\n  \"status\": \"CONTEXT_ENRICHED\",\n  \"amplifier_context\": {\n    \"architectural_invariants\": [\n      { \"adr_id\": \"ADR-042\",\n        \"invariant\": \"Every outbound call to a third-party service (payment, KYC,\n         notification) MUST go through the corporate security egress proxy...\" },\n      { \"adr_id\": \"ADR-070\",\n        \"invariant\": \"The following paths are frozen and MUST NOT be modified:\n         internal/old_payment.go, internal/auth/...\" }\n    ]\n  }\n}\n```\n\nIt is the fifteen-minute chat with the staff architect before starting a piece of work: automated, exhaustive, and scoped to this task. The two rules that Act 1 buried on page 3 are now the freshest thing in the agent’s planning context.\n\n## The gate publishes its contract\n\nHow does the agent know what a valid plan looks like? Nobody explains it in prose. The gateway’s Go type for a plan has a language-neutral twin, a JSON Schema, and the MCP server serves it to the agent as the `lock_in_plan`\n\ntool schema at session start:\n\n```\n{\n  \"title\": \"AgentPlan\",\n  \"required\": [\"session_id\", \"intent\", \"repository_context\", \"steps\"],\n  \"properties\": {\n    \"steps\": {\n      \"type\": \"array\", \"minItems\": 1,\n      \"items\": { \"required\": [\"id\", \"action\", \"tool\", \"targets\"] }\n    }\n  }\n}\n```\n\nThree layers, and none of them overlap: the skill says **when** to call the gate; the tool schema says **how** to format the plan; the enrich invariants say **what** the plan must contain. The platform publishes contracts; the agent fills in the content.\n\n## First plan: rejected\n\nThe agent submits its plan as a structured JSON contract. The gateway’s linter evaluates every ADR-paired Rego policy against it, and one fires: `ADR-060.rego`\n\n, the exact policy you read three scenes ago. No test step for a Go stack.\n\n```\n{\n  \"status\": \"PLAN_REJECTED\",\n  \"violations\": [\n    { \"policy_id\": \"go_tests_present\",\n      \"message\": \"SDLC invariant violated: the plan has no test step. Add a\n       step whose tool is \\\"go-test\\\", or whose action runs 'go test'.\",\n      \"nature\": \"amplifier\" }\n  ],\n  \"guidance\": \"Fix the violations above and resubmit the plan.\"\n}\n```\n\nNote the register: not “no”, but “here is what is missing”, down to the machine-checkable criterion. A semantic violation reads like a compiler error, and agents are very good at compiler errors; deprive them of the criterion and they guess, give it to them and they fix it in one round-trip.\n\n## Self-correction in one iteration\n\nThe agent reads the violation, adds the missing step, resubmits:\n\n```\n\"steps\": [\n  { \"id\": \"s1\", \"action\": \"create the payment_methods migration for Stripe\",\n    \"tool\": \"db-migration-generator\", \"targets\": [\"migrations/001_stripe.sql\"] },\n  { \"id\": \"s2\", \"action\": \"add Stripe client and route it in the payment router\",\n    \"tool\": \"patch_code\", \"targets\": [\"internal/payment/router.go\"] },\n  { \"id\": \"s3\", \"action\": \"go test ./...\",\n    \"tool\": \"go-test\", \"targets\": [\"tests/integration_payment_test.go\"] }\n]\n```\n\nNo human touched anything. The correction cost one round-trip, measured in seconds; in Act 1 the equivalent feedback cost a review cycle, measured in days.\n\n## PLAN_LOCKED: the capability ticket\n\nThe plan passes. The gateway locks it and issues a signed ticket (an ephemeral JWT) that encodes exactly what was agreed, and nothing more:\n\n```\n{\n  \"plan_hash\": \"283bcbcfce9405ac805d29aa539a8b2eef98...\",\n  \"scope\": {\n    \"allow_modify\": [\n      \"migrations/001_stripe.sql\",\n      \"internal/payment/router.go\",\n      \"tests/integration_payment_test.go\"\n    ],\n    \"allow_tool\": [\"db-migration-generator\", \"patch_code\", \"go-test\"]\n  }\n}\n```\n\nThree files, three tools, fifteen minutes of validity. Least privilege, derived mechanically from the plan the agent itself proposed.\n\n## Execution inside the rails\n\nEvery `Edit`\n\nand `Write`\n\nnow passes through a `PreToolUse`\n\nhook (`ppg-guard`\n\n) that checks the target against the ticket. In scope: silent pass, zero friction; the agent does not even notice the guard exists.\n\nAnd the Stripe call goes through `security-egress-proxy`\n\n. Not because a gate forced it: because ADR-042 was in the planning context when the plan was written. The soft move did the steering; the hard moves are only there for the day it fails.\n\n## The drift, blocked in real time\n\nMid-session, the “while I’m here” reflex strikes again: the agent tries to touch `internal/auth/login.go`\n\n. The hook blocks the call before it executes (exit code 2) and the message goes straight back to the model:\n\n```\nOUT_OF_PLAN_SCOPE: \"internal/auth/login.go\" is not part of the locked plan\n(allowed: migrations/001_stripe.sql, internal/payment/router.go,\ntests/integration_payment_test.go). Nothing was modified. If this change is\ngenuinely needed, re-plan through lock_in_plan.\n```\n\nThis is the same violation that cost Act 1 a review cycle. Here it costs nothing: nothing was modified, and the refusal contains its own remediation path (re-plan, or stay in scope). The agent course-corrects and moves on.\n\n## When it fails legitimately: a deterministic mentor\n\nNot every failure is a governance failure. The agent submits a patch with a syntax error; the platform tool catches it in a sandbox and answers with structure, not with `exit 1`\n\n:\n\n```\n{\n  \"error_category\": \"GO_SYNTAX_ERROR\",\n  \"message\": \"The patched file does not parse as valid Go.\",\n  \"remediation_guidance\": {\n    \"allowed_actions\": [\n      \"Fix the syntax error reported below and resubmit the patch.\",\n      \"internal/payment/router.go:2:22: expected ')', found '{'\"\n    ]\n  }\n}\n```\n\nThe failure becomes one guided iteration instead of a guessing loop. The tool behaves like a mentor with perfect knowledge of the environment: the exact file, the exact line, the exact next action.\n\n## The platform watches itself\n\nOne question remains: is all this governance a durable asset, or scaffolding that compensates for today’s model limitations? The gateway answers about itself:\n\n```\n{\n  \"transition_debt_ratio\": 0.4,\n  \"pending_sunsets\": [\n    { \"artifact_id\": \"explicit_frozen_files_enumeration\",\n      \"sunset_condition\": \"Model honors '@deprecated' annotations semantically\n       on >95% of an internal benchmark.\" }\n  ],\n  \"health\": \"DEBT_ALERT\"\n}\n```\n\nADR-070’s frozen-file list is tagged compensatory: the day models infer “deprecated” from annotations, the list is deleted and the ratio drops. The platform ships with its own demolition plan for every crutch it contains.\n\n## Two endings, one metric\n\nSame task, same agent, same mistakes attempted: the missing test, the frozen file, the direct external call. The difference between the two endings is a single variable: **the form the rules take**. In Act 1 they were text (a context file, a skill), read and then outrun. In Act 2 they were data, checked at three points of the loop.\n\nAct 1: feedback arrives in days, outside the loop, and costs a full rework carried by a human. Act 2: feedback arrives in seconds, inside the loop, and is absorbed as ordinary iterations. The agent did not get smarter between the acts; the governance changed form and place.", "url": "https://wpnews.pro/news/same-task-two-endings-a-payment-integration-with-and-without-the-planning", "canonical_source": "https://blog.owulveryck.info/2026/07/09/same-task-two-endings.html", "published_at": "2026-07-09 08:00:00+00:00", "updated_at": "2026-07-10 09:17:10.747743+00:00", "lang": "en", "topics": ["ai-agents", "developer-tools"], "entities": ["Stripe"], "alternates": {"html": "https://wpnews.pro/news/same-task-two-endings-a-payment-integration-with-and-without-the-planning", "markdown": "https://wpnews.pro/news/same-task-two-endings-a-payment-integration-with-and-without-the-planning.md", "text": "https://wpnews.pro/news/same-task-two-endings-a-payment-integration-with-and-without-the-planning.txt", "jsonld": "https://wpnews.pro/news/same-task-two-endings-a-payment-integration-with-and-without-the-planning.jsonld"}}