An internal refunds page sat in the team wiki for six weeks. The curl example still parsed. The retry sentence did not.
On a Tuesday rollback, an on-call engineer followed a generated note that called POST /v2/refunds "safe to retry." The handler was not idempotent. Two credits posted against one charge. The example was only stale. The promise was invented.
Generated API docs fail on two clocks. Shape docs rot when fields move. Promise docs fail when a model writes operational guarantees that nobody signed. Mixing those clocks in a single Markdown file is the usual accident.
This article proposes a capability card: one short pair of files per internal endpoint or agent tool. The model may draft shape. A human must own side effects. A checker fails the build when the lanes mix. The workflow below is a proposal. Scripts are local examples, not production telemetry.
Two clocks, not one page
Treat each endpoint as two documents glued by a stable id.
Clock A — shape (regenerable)
- Purpose line restated from the OpenAPI summary
- Parameter table
- Example request and response
- Links to sibling paths
Clock B — promise (frozen until a human edits it)
- Authentication and authorization
- Side effects: money, email, deletes, lock state
- Idempotency and retry
- PII and retention
- On-call owner
- Deprecation window
Clock A can be rewritten whenever the spec hash changes. Clock B must survive that rewrite. If a model edits Clock B, the card is invalid even when the prose looks cleaner.
Agent tool catalogs make the split more urgent, not less. Teams now paste the same paragraphs into tool descriptions. A hallucinated retry line is no longer only an on-call footnote. It becomes a policy the agent will execute.
Ownership matrix
| Field | Owner | Regen | Fail the build when |
|---|---|---|---|
summary |
model | yes | missing, or ignores the path |
params |
model | yes | name absent from inventory |
examples |
model | yes | method or path mismatch |
authn |
human | no | empty |
side_effects |
human | no | empty |
retry |
human | no | hedging language |
owner |
human | no | missing on-call id |
signed_at |
human | no | absent |
Keep the matrix in git. Do not keep it only inside a prompt. Prompts drift. Tables in the repo do not, at least not silently.
File layout
Do not give the model a single file that also holds promises. Split directories.
cards/
human/ # Clock B, humans only
refunds.create.yaml
draft/ # Clock A, model output only
refunds.create.md
forbidden-draft.txt
inventory.json
capability_card_check.py
Human lane:
id: refunds.create
path: POST /v2/refunds
owner: payments-oncall
signed_at: 2026-09-12
authn: service-token + finance.refund.write
side_effects:
- posts a credit to the ledger
- emits refund.created
- may send a customer email
retry: do-not-retry; read GET /v2/refunds/{id}
pii: customer_id, last4
deprecation: null
Draft lane:
<!-- CARD_ID: refunds.create -->
<!-- SPEC_HASH: demo -->
## POST /v2/refunds
Creates a refund against a captured charge.
### Parameters
| name | in | type | required |
| --- | --- | --- | --- |
| charge_id | body | string | yes |
| amount_cents | body | integer | yes |
### Example
curl -X POST https://api.internal.example/v2/refunds \
-H "Authorization: Bearer $TOKEN" \
-d '{"charge_id":"ch_123","amount_cents":1500}'
The example may be wrong tomorrow. That is acceptable if CI regenerates it from the spec. The retry line must not appear in the Markdown body.
Forbidden phrases in Clock A
A short linter beats another system prompt. Scan only cards/draft.
idempotent
safe to retry
no side effects
does not charge
guarantee
SLA
PII-free
never fails
exactly-once
can be called from a bot
If any phrase hits, discard the draft. Do not hand-edit the leak back into the .md file. The next regeneration will restore it.
Inventory from a reduced OpenAPI file
You do not need a full parser to start. A reduced spec is enough for the checker.
#!/usr/bin/env python3
"""inventory_from_openapi.py — proposed extractor, fixture-oriented."""
from __future__ import annotations
import json
import sys
from pathlib import Path
def main() -> int:
spec = json.loads(Path(sys.argv[1]).read_text())
endpoints = []
for path, methods in (spec.get("paths") or {}).items():
for method, op in methods.items():
if method.startswith("x-") or not isinstance(op, dict):
continue
op_id = op.get("operationId") or f"{method}.{path}"
endpoints.append(
{
"id": op_id,
"path": f"{method.upper()} {path}",
"summary": op.get("summary") or "",
}
)
Path("inventory.json").write_text(
json.dumps({"endpoints": endpoints}, indent=2) + "\n"
)
print(f"wrote {len(endpoints)} endpoints")
return 0
if __name__ == "__main__":
raise SystemExit(main())
Run:
python3 inventory_from_openapi.py openapi.fixture.json
Every inventory id must have a matching cards/human/<id>.yaml. Missing YAML is a failed build, not a TODO comment.
Checker
The checker loads inventory, requires human fields, and rejects draft files that contain frozen concerns.
#!/usr/bin/env python3
"""capability_card_check.py — fail if draft and promise lanes mix."""
from __future__ import annotations
import hashlib
import json
import re
import sys
from pathlib import Path
try:
import yaml
except ImportError:
print("pip install pyyaml", file=sys.stderr)
sys.exit(2)
FORBIDDEN = [
ln.strip().lower()
for ln in Path("forbidden-draft.txt").read_text().splitlines()
if ln.strip()
]
HUMAN_REQUIRED = ("owner", "signed_at", "authn", "side_effects", "retry")
def load_inventory(path: Path) -> dict:
data = json.loads(path.read_text())
return {item["id"]: item for item in data["endpoints"]}
def check_human(card: dict, endpoint: dict, errors: list[str]) -> None:
cid = card.get("id", "<missing>")
for key in HUMAN_REQUIRED:
if not card.get(key):
errors.append(f"{cid}: missing human field {key}")
if card.get("path") != endpoint.get("path"):
errors.append(f"{cid}: path does not match inventory")
def check_draft(md: str, card_id: str, errors: list[str]) -> None:
low = md.lower()
for phrase in FORBIDDEN:
if phrase in low:
errors.append(f"{card_id}: draft lane contains '{phrase}'")
if re.search(r"^retry:", md, re.M | re.I):
errors.append(f"{card_id}: retry belongs in cards/human")
if "CARD_ID:" in md and card_id not in md:
errors.append(f"{card_id}: CARD_ID mismatch")
def main() -> int:
human_dir = Path("cards/human")
draft_dir = Path("cards/draft")
inventory = load_inventory(Path("inventory.json"))
errors: list[str] = []
yaml_ids = set()
for yml in sorted(human_dir.glob("*.yaml")):
card = yaml.safe_load(yml.read_text()) or {}
cid = card.get("id")
if not cid:
errors.append(f"{yml.name}: no id")
continue
yaml_ids.add(cid)
if cid not in inventory:
errors.append(f"{cid}: not in inventory")
continue
check_human(card, inventory[cid], errors)
md_path = draft_dir / f"{cid}.md"
if not md_path.exists():
errors.append(f"{cid}: missing draft markdown")
continue
check_draft(md_path.read_text(), cid, errors)
for mid in sorted(set(inventory) - yaml_ids):
errors.append(f"{mid}: inventory path has no signed card")
spec_hash = hashlib.sha256(Path("inventory.json").read_bytes()).hexdigest()[:12]
if errors:
print("FAIL")
print("\n".join(errors))
return 1
print(f"ok {len(yaml_ids)} cards inventory={spec_hash}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
pip install pyyaml
python3 capability_card_check.py
A red build is the control. Green prose in a wiki is not.
Test plan (fixture, not a live service)
- Drop
retry: do-not-retryfrom the YAML. Expect fail onmissing human field retry. - Add "safe to retry" under the curl example. Expect fail on forbidden phrase.
- Add an inventory endpoint with no YAML. Expect fail on unsigned card.
- Restore a clean pair. Expect
ok 1 cards. - Point the model at
cards/human. If it writes YAML, treat that as a pipeline bug, not a content fix.
Do not run step 5 against production credentials. The draft job should have write access only to cards/draft.
Draft loop
When inventory.json changes, regenerate only cards/draft/*.md. Leave YAML untouched.
Using inventory.json and the OpenAPI snippet, rewrite cards/draft/<id>.md.
Do not mention retry, auth, SLA, PII, or side effects.
Keep the CARD_ID comment.
Output Markdown only.
If the checker fails, discard the file and rerun. Do not patch the leak by hand in the draft lane.
The job is mechanical: spec fragment to table and curl. A long agent loop is unnecessary. A hosted free model is enough for Clock A. Clock B still needs a named human.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
MonkeyCode is an open-source project with free model access and a free server option. Those two properties fit this workflow in a narrow way. The model drafts Clock A. The server can run capability_card_check.py on a schedule so a closed laptop does not skip the gate. Neither property fills owner or signed_at. If a run rewrites YAML, that is an incident.
Limits and who should skip this
- The checker does not prove the YAML is true. A human can sign a wrong retry policy.
- Inventory is a file you maintain. This is not traffic discovery.
- Public customer docs, legal copy, and localization are out of scope.
- The linter is string-based. It will miss a paraphrased promise that avoids the phrase list. Extend the list when you find a miss. Do not pretend the list is complete.
- It does not make an agent understand the API. It only stops the agent-facing page from claiming a retry nobody signed.
Skip the approach if you maintain one public README and no money-moving or delete paths. Skip it if you cannot name an on-call owner per path. A card without owner is another wiki page.
If examples and promises are already split in review, the useful next step is a CI checker, not a longer prompt. Free model access plus a free server is enough to keep the draft lane regenerable. The frozen lane still needs a person.
Top comments (0)