Discipline for the seam between two SDKs (or two sides of one contract) that the same hand writes. The failure mode: "we own both sides" produces dirty contracts no foreign reviewer would accept. The exercise: pretend the other side is FFI, IPC, or a network protocol you cannot rewrite. Spawn an adversarial subagent profiled as the producer's maintainer; negotiate the change as a feature request, not a PR. Companion to $sdk-design. Language-agnostic — applies to a TS package + its consumer, a Rust crate + its WASM binding, two services sharing a wire format, or any other boundary the same author writes both ends of.
61
72%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Fix and improve this skill with Tessl
tessl review fix ./.agents/skills/sdk-seam/SKILL.mdCompanion to
sdk-design. Read that first; its deciding table and disciplines are the foundation this skill builds on. Wheresdk-designis about a single SDK's surface, this skill is about the seam — the joint between two SDKs (or two sides of one contract) — and what it takes to keep that joint clean when the same author writes both sides.
When you control both sides of a boundary, you produce dirty contracts — simply because you can. A field gets added on the producer side because the consumer needs it; the consumer reaches into the producer's internal shape because no public view exposes the slice; a one-off helper crosses the boundary because "we're going to refactor it later." Six months later the contract is unrecoverable, and the two sides can only be deployed together.
The honest test: if we couldn't shotgun-edit both sides at once, this design wouldn't be possible. That's not a virtue. It's a warning.
If the contract had been an IPC channel, an FFI ABI, a network protocol, or a published-versus-consumed package boundary from the start, the design would have been clean from the beginning, and it would have evolved cleanly. Cross-boundary work inside one repo, one workspace, or one mono-language project doesn't get that discipline for free — you have to manufacture it.
This applies to:
packages/grida-* packages where one consumes the other.crates/* crate + a binary that links it; an npm package + a Next.js app).Before editing, restate the work as if you only owned one side.
For every change that crosses a seam, write down:
down_doc to the translate_tangent gesture so absolute-position commits can detect click-no-drag").If you can't fill in (3) credibly, you haven't designed the change — you've just typed the diff.
When the change is non-trivial, delegate the work to a subagent profiled as the producer's maintainer. This is not a review step; it is the actual implementation handoff. The subagent defends, decides, AND ships the producer-side change. The main agent never touches the producer's files.
This is the mechanism that keeps the contract unopinionated and agnostic: the subagent doesn't have your consumer-side context, so it can't be tempted to "just add the field." It has to reason from the producer's own invariants — its README, its tests, its anti-goals — and respond as if it were any other foreign maintainer fielding a feature request from any other consumer.
┌─────────────────────┐ ┌──────────────────────┐
│ Main agent │ │ Subagent │
│ (consumer side) │ │ ("you are the │
│ │ │ maintainer of X") │
│ 1. Writes │ FEEDBACKS.md → │ 3. Reads README + │
│ FEEDBACKS.md │ │ FEEDBACKS.md │
│ 2. Spawns subagent │ │ 4. Defends / accepts│
│ │ │ / counter- │
│ │ │ proposes │
│ │ │ 5. Implements the │
│ │ │ producer change │
│ │ ← decision + │ 6. Writes producer │
│ 7. Reads decision │ diff summary │ tests │
│ 8. Updates │ │ 7. Returns │
│ consumer side │ │ verdict + diff │
│ against the │ │ │
│ SHIPPED contract│ │ │
└─────────────────────┘ └──────────────────────┘The requester writes a self-contained feature request. Name it
whatever fits the workflow — FEEDBACKS.md, REQUEST.md,
<package>/_inbox/<date>-<topic>.md, a GitHub-style issue draft.
The format matters less than the contents.
The artifact MUST contain:
The artifact MUST NOT contain:
Spawn the subagent with a brief that names the producer and its authoritative docs:
You are the maintainer of
[package/crate X]. Your authoritative doctrine is[path/to/X/README.md]and[any AGENTS.md, design docs]. Read those before responding to any feature request.A consumer has filed
[path/to/FEEDBACKS.md]. Process it as you would any external feature request:
- Read the FEEDBACKS.md and the producer's README/AGENTS.md.
- Decide: accept, counter-propose, or refuse.
- Accept: implement the requested shape, possibly with tightened naming or added invariants.
- Counter-propose: implement an alternative shape that solves the same observable problem but fits the producer's design better.
- Refuse: cite the anti-goal or invariant violated, propose how the consumer can absorb the problem differently.
- If accepting or counter-proposing, ship the change: edit the producer's source, add producer-side tests that lock the new contract in producer-only terms (no naming the consumer), update the producer's README/doctrine if the rule generalizes.
- Return a verdict (accept / counter / refuse), a one-paragraph rationale, and a summary of the diff (file paths + what changed). Do NOT touch consumer-side files.
The subagent's tool access should be scoped to the producer's files only — or, if that's not enforceable, the instruction must be unambiguous. Consumer-side files are off-limits for this subagent.
The subagent returns one of three outcomes. The main agent's next move depends on which:
Three things only this flow gets right:
Each outcome produces a cleaner contract than "just add the field because we control the file."
These are language-agnostic. They apply whether the boundary is a TypeScript module export, a Rust trait, a FlatBuffers schema, a JSON-RPC method, or a C ABI.
When the contract has to change, change the contract first, in its own commit (or its own logical unit of work). Ship the producer side with new tests against the new shape. Only after the contract is locked do you update consumers against it.
Anti-pattern: "I added the field and the consumer that needs it in the same hunk." The producer-side test for the field is the consumer's test by accident, and the contract isn't really specified — it's just whatever the consumer happened to need.
Every new field, every new variant, every relaxation of an existing shape — the producer adds a test that pins the new behavior in producer-only terms. "Given input X, the API returns Y with field Z set" — without naming the consumer that asked for it.
A producer test that mentions only the consumer's use case is a contract that breaks when the consumer leaves. The doctrine generalizes the grep-contract idea from $sdk-design: scenario names belong in test text, not consumer references.
If you have two files open from two sides of a boundary and you're editing them in tandem, stop. Either:
This is the boring procedural step that prevents the bad design. The reason seams in foreign systems stay clean is that the deploy boundary forces this sequencing. Manufacture the same sequencing here by hand.
For a Rust crate ↔ WASM binding, this means: change the crate's public function signature, regenerate bindings as a separate step, then update the binding's callers. Not all three in one edit.
A producer that exposes "subscribe to anything" or "raw state" surfaces is one a consumer will inevitably reach into. $sdk-design D1 ("Subscribe to outcomes, not events") is the prevention; this subskill is the discipline when the prevention hasn't fully landed yet.
If the consumer wants something not in the public observation
surface, the consumer files a feature request, doesn't reach.
"There's no public view for [internal field X], so I'll just access
it via the internal property / via reflection / via pub(crate)" is
the moment a contract dies.
The session that produced this skill added a down_doc field to a
gesture struct on one side of a boundary to fix a click-no-drag
mutation on the other. The fix was correct, but the way it landed
was clean only because the procedural steps were followed:
| Step | What happened | If it had gone wrong |
|---|---|---|
| Diagnose | $etiology ladder: symptom is "control moves on bare press." Proximate: absolute-position commit writes pointer position even when pointer didn't move. API contract: absolute vs delta gesture asymmetry. | Skipping the ladder, we'd have added if (dx === 0 && dy === 0) in the consumer — bandaid that leaks. |
| Decide who owns the fix | Producer owns gesture state; the no-drag guard belongs in the producer's commit handler. Field is added to the producer's gesture struct, with doc explaining why it's distinct from existing fields. | We could have written the guard on the consumer side. The next consumer would re-trigger. |
| Lock the contract | New test on the producer side: click-no-drag does NOT emit the commit intent. Producer-only — doesn't name the consumer. | Test on the consumer only — producer could regress silently. |
| Update the doctrine | Spec amendment added a Conformance rule: "Absolute-gesture click-no-drag is mute." Future consumers (other applications of the same producer) inherit the rule for free. | Rule lives in someone's head; the next consumer re-discovers the bug. |
The work that produced these clean outcomes was procedural. None of it required new tooling.
The trap avoided — and that this skill exists to prevent — was the version where, because we controlled both files, we silently muted the commit on the consumer side and moved on. That version would have shipped, the test suite would have stayed green, and the next consumer of the same producer would have re-hit the bug with no breadcrumb back.
<consumer> for <feature>" — the field is leaking the consumer's concern into the contract.index.*, lib.rs's pub use, the schema's public namespace, etc.).Any one of these is a stop-and-reset. Two or more is a redesign signal.
If neither applies, you're inside the boundary and the discipline holds.
$pedantic — when defending a contract change, pedantic probes catch unfalsifiable rationale ("this field will be useful for future flexibility") and leaked-uncertainty ("we might need to extend this later" — quarantine and ship the minimum that's clear).$etiology — most cross-seam bandaids are API-contract bugs (rung 3 of the diagnostic ladder). The temptation to "just patch the consumer" almost always means the producer's contract is the real defect.2e0d276
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.