Imported from mcereal/inkwell (
AGENTS.md). Install upstream withnpx skills add mcereal/inkwell. Copyright stays with the author.
AGENTS.md
Contributor guide for inkwell. README.md says what the project is and why; this
says how to work in it.
The one-paragraph version
inkwell is the systems layer under inkcell. C17, Linux
only, no threads, one epoll loop. Four areas: base/ (the leaves), runtime/ (the loop, the
signals, the crash report), codec/ (bytes in, bytes out), net/ (one hostname, one socket,
one TLS session, one request). Arrows point down and scripts/check-layers.py holds them
there. make test before every push.
One optional dependency: Mbed TLS, a submodule. Without it net/tls.h refuses every session and
everything else builds and tests exactly as it does with it.
Layout
include/inkwell/<area>/ the public surface of an area, flat
src/<area>/ the sources; a header and its source always share a filename
third_party/ somebody else's code, and our configuration of it
tests/framework/ the self-registering case runner
tests/suites/<area>_*.c one file per subject
tests/support/ fixtures a second suite needed
tests/data/ captured bytes a codec is tested against
scripts/check-layers.py the layering rule the compiler cannot see
scripts/check-vendor.py the digest rule a vendored file is held to
include/inkwell/<area>/ is flat and is the interface. How a source is filed under src/<area>/
is not part of it: moving a file between groups inside an area is not an API change. Find a
header's source by filename, never by path.
Style
-
clang-format 18, config in
.clang-format.make formatbefore pushing; CI checks it. -
inkwell_on everything public,INKWELL_on macros and enum members. A symbol carries the prefix of whoever owns it — that is the whole point of the split, so a name that came from somewhere else gets renamed on the way in rather than keeping its old spelling. -
Every public header is
#pragma onceor a guard, wrapped inextern "C", and says in prose why it works the way it does. The reasoning is the valuable part; a signature can be read off the line below it. -
Functions return
0or a negativeerrno. A refusal that a caller must distinguish gets its own enum rather than being folded into a generic failure -net/reason.his the worked example, and the one thing it is strict about is that a reason never carries a word.That is a rule about status. A function whose answer is a quantity returns the quantity, and a negative errno when it has none -
inkwell_base64_encode(),inkwell_log_file_compact(),inkwell_text_utf8_next()andinkwell_stream_pump()all do. The test is whether a caller wants the number: if it does, hiding it behind an out-parameter to satisfy the shape of the rule makes every call site longer and none of them clearer.
Rules that compile fine when broken
base/includes nothing of inkwell's. It is the floor.- No threads. Anything that would block gets a descriptor and a callback instead.
- No application vocabulary. Not in code, not in comments. Say "a peer", "a protocol", "a transport". The day a comment in here says "the radio" is the day this stopped being a platform layer.
- A new area under
src/needs an entry inALLOWEDinscripts/check-layers.py. Adding one is a decision about the shape of the stack. #include <inkcell/...>and#include <mesh/...>are build failures. inkwell is the bottom; there is nothing below it but Linux. The layer check fails on both spellings.- A vendored file is upstream's.
third_party/is excluded frommake formatand its digest is checked against its own README byscripts/check-vendor.py, whichmake testruns. A fix goes upstream and comes back as a new revision; an edit in place is invisible in review and fails the check. The exclusion is not tidiness - clang-format over 3.6 MB of generated C is both an unreadable diff and a digest mismatch, and it happened once. The same applies tothird_party/mbedtls, where the pinned SHA is what a digest is for a vendored file; the configuration of it next door inthird_party/mbedtls-config/is ours and is edited freely. - A dependency may not be compulsory. Mbed TLS is the only submodule here and
net/tls.ccompiles to a refusing stub without it, so a plaingit cloneis still a complete, buildable, testable inkwell. CI has a job that builds that way, because the claim is worth nothing if nobody checks it. A second dependency, if it ever arrives, arrives the same way.
Extracting something from mesh-client
docs/extraction.md is the running map: what has already come down, what
is next, and the evidence for each candidate. The error-vocabulary question that used to block
several rows is answered - net/reason.h - and where inkcell ends is the one still open. Read
it before picking something up.
Most of what lands here arrives the same way, and the order matters:
- Check what it actually depends on.
grep -h '#include "' <file>over the source and its header, then check what the source calls across the line. A candidate that includes an application header is not ready; find the seam first. Reading only the source is howstream_link.csat indocs/extraction.mdas a file move for a tranche - it includes one application header, its own, which includes two more. - Move the tests with it. A component that arrives without the cases that held it is a downgrade, however clean the diff looks. If the cases live inside a larger suite, slice out the ones that belong to the component and leave the rest behind.
- Rename on the way in.
mesh_*becomesinkwell_*, and a name that was shaped by the application gets the better name now rather than later:mesh_event_loopisinkwell_loop. - Generalise the prose, do not delete it. The comments explain real decisions and are worth more than the code. Rewrite "the radio" as "a peer"; keep the war story about why a rearming timer used to starve the loop, because the next person will hit it too.
- Leave the seam in the application. Where the component had an application-specific convenience — a lookup by one well-known UUID, say — the general form comes here and the two-line wrapper stays there.
Tests
make test # the whole verify step
./build/tests/inkwell_tests --list
./build/tests/inkwell_tests --filter loop # substring match
./build/tests/inkwell_tests --suite codec_zip
Cases register themselves from constructors, so adding one is a single INKWELL_TEST_CASE in a
single file. A new suite file goes in INKWELL_TEST_SUITES in tests/CMakeLists.txt — the
suite files are compiled straight into the executable rather than into a library, because a
linker is free to drop a library member nothing references and would take every case inside it
with it, silently.
A helper used by one suite stays static in it and moves to tests/support/ when a second
suite needs it.
Pull requests
make testgreen,make formatclean.- Conventional Commits (
feat:,fix:,refactor:,docs:,test:,chore:). - An extraction PR says what it moved, from where, and what stayed behind — the last of those is the part a reviewer cannot get from the diff.