Crafter.Debug/AGENTS.template.md
Jorijn van der Graaf 15e0b4954d
docs: the QUIC transport, where Crafter.Network comes from, and limits
The agent notes describe the wire, list :Transport and :Quic, explain
how to build against a Network worktree with CRAFTER_DEBUG_NETWORK, and
how to test a listening session. They add the one thread Crafter.Debug
starts, the TLS and size limits, and gotchas: Crafter.Network must be
named alike by a consumer that uses it too, last 0, and replying before
raising a flag the main loop acts on. The README says the Session needs
a pool worker, that a project pinning its own Crafter.Network can't add
Crafter.Debug yet, and what ShouldServeOverQuic covers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 03:07:09 +01:00

11 KiB

AGENTS.md

Notes for agents working on Crafter.Debug itself. The README is the user manual. tooling/AGENTS.consumer.md is what consumers' agents get.

Project overview

Crafter.Debug lets an agent inspect and drive an app while it runs. project.cpp makes four configurations:

Configuration Holds
Crafter.Debug.Core (static) What the app and the client share. Partitions:
• :Value: JSON value with its own parser and printer
• :Reflect: aggregate members and scoped-enum names, without <meta>
• :Codec: encode, decode, describe
• :Result: Error, Result, Async
• :Protocol: envelope, codes, versions, base64
• :BuildInfo: source id and client path, generated
• :Discovery: instance files and the lifetime lock
Crafter.Debug.Cli (static) The client's logic:
• :Client: Cli::Run, with the transport injected
• :Quic: ConnectQuic, the transport main injects
crafter.debug-cli (executable) crafter-debug: main.cpp plus the transport.
Crafter.Debug (static, returned) What an app links. Partitions:
• :Registry: commands, watches, samples, Group
• :Timeline: Log, Check, Emit
• :Condition: what wait and pause --when evaluate
• :Session: serving requests, Pump, Advance
• :Transport: the QUIC listener, exporting nothing

The client is one of the library's dependencies only so that a consumer's debug build compiles it in the same pass.

The wire: QUIC on 127.0.0.1, ALPN crafter-debug/1, through Crafter.Network. One request per stream the client opens: the request object and a FIN; back come zero or more stream lines ({"entry": ...}), then the reply ({"result"} or {"error"}) and a FIN. Every object is one line of JSON. implementations/Crafter.Debug-Transport.cpp opens with the rules on who may hold which lock.

tooling/ is a separate project, Crafter.Debug.Build. Consumers load it with #pragma crafter dependency ... project "tooling/project.cpp". Its Debug::Use adds the library under the consumer's flag and writes the consumer's build/crafter-debug launcher at load time.

tooling/AGENTS.consumer.md is the only notes file consumers get, and it covers both driving the app and coding with it. Crafter.Build merges a dependency's notes only when that dependency is in the build:

  • The tooling comes in through the pragma, which is unconditional, so its notes are there after every build.
  • The root library comes in only under the consumer's debug flag. Notes beside the root project.cpp would vanish after plain builds and appear twice after debug builds, so the root has none.

examples/Basic is a consumer that uses this checkout through a local pragma path.

Dev environment

crafter-build comes from its Flatpak. Spell it out, since the Bash tool may not expand aliases: flatpak run --die-with-parent net.catcrafts.CrafterBuild. The host is Linux x86_64.

Tests run inside the Flatpak sandbox, where:

  • /tmp is private;
  • the toolchain is clang 23 with libc++;
  • /.flatpak-info exists.

Every flatpak run is its own sandbox with its own pid namespace.

Crafter.Network comes from forgejo, unpinned, so every load git pulls it. --local takes ../Crafter.Network, and CRAFTER_DEBUG_NETWORK=<path to a project.cpp> takes any checkout, a worktree say. The environment reaches the build inside the Flatpak.

The transport needs Network's callback API (OnRecieve, SendAsync, OnClosed, ListenerQUIC with a bind address and Port()) and its fix that lets stopped streams finish before ~ClientQUIC closes. Until those reach Network's master, build with CRAFTER_DEBUG_NETWORK pointing at a checkout of the branch that has them.

Build and test commands

flatpak run --die-with-parent net.catcrafts.CrafterBuild                  # the library, core, client
flatpak run --die-with-parent net.catcrafts.CrafterBuild test             # every test
flatpak run --die-with-parent net.catcrafts.CrafterBuild test 'Should*Client*'
CRAFTER_DEBUG_NETWORK=../Crafter.Network-quic-callbacks/project.cpp flatpak run --die-with-parent net.catcrafts.CrafterBuild test   # Network from a worktree
cd examples/Basic && flatpak run --die-with-parent net.catcrafts.CrafterBuild --debug-api   # a consumer's debug build

A test's own output appears only when it fails. To see its PASS lines, run the binary from bin/<Test>-*/ with flatpak run --die-with-parent --command=<path> net.catcrafts.CrafterBuild.

Testing instructions

Registration: tests are declared in project.cpp, and only when this is the top-level project:

  • coreTests link the core;
  • libraryTests link the library;
  • ShouldRunClientCommands and ShouldServeOverQuic link the library and the client. The second listens for real and drives the session over loopback with the client's own transport.

Shape of a test:

  • Name it Should<Verb><What>.
  • Open main.cpp with a comment on the behaviour and the failure it guards against.
  • Use a Check(ok, what) helper that prints PASS or FAIL, and return nonzero at the end.

Session tests use Session({ .listen = false }) and hand requests to Session::Handle through a fake Detail::Exchange. Pump from the test's own thread, or from one thread playing the app.

A listening session needs ThreadPool::Start() with a worker first, CRAFTER_DEBUG_DIR set, and std::_Exit at the end after std::fflush(stdout): msquic's teardown at exit waits for connections. Run it 30 times in a row before trusting it; one transport race showed in 4 runs of 30.

Files and discovery:

  • Temporary files go under fs::temp_directory_path() / "crafter-debug-<Test>".
  • Discovery tests set CRAFTER_DEBUG_DIR there.

Hangs: anything that can hang gets a watchdog that calls std::_Exit(1) before the runner's 60 s.

Pointers into temporaries: Value::Find returns a pointer into its value. Never keep one from a temporary such as ResultOf(...).Find(...) past the full expression; copy the value into a local first. Two tests failed on exactly this.

Code style

Nothing enforces it. No lint rules are registered: a #pragma crafter dependency on Crafter.LintRules would be built on every load of every consumer.

Headers and imports:

  • // SPDX-License-Identifier: LGPL-3.0-only, with a space after //, then the copyright line, then a blank line. Examples are MIT.
  • import std;. C headers go only in a global module fragment, or before the imports in a test.

Formatting and naming:

  • Four-space indentation.
  • PascalCase for types and functions, camelCase for variables, members and constants.
  • std::println and std::format.
  • Fixed-width integers.
  • enum class.

Comments say why: the failure mode, the race, the rejected alternative. Long comments are deliberate.

Design rules

  1. Release builds never link Crafter.Debug. Consumers gate it through Debug::Use and their own define. The library has no no-op mode.
  2. No thread of Crafter.Debug's own, and nothing ever blocks one. Transport threads only queue work or answer from the session's own state; the Pump thread runs app code; Detail::Exchange::Reply and Stream never block. The thread pool belongs to the host app. Network's own threads are ListenerQUIC's accept loop and msquic's workers. The one thread Crafter.Debug starts is the timer that ends the process 200 ms after quit --force has sent its reply.
  3. App code runs only on the Pump thread: handlers, getters, setters, and condition evaluation. Never hold the session mutex while calling it, since app code logs, and logging re-enters the session.
  4. Nothing the Flatpak compiles runs on the host. The launcher is POSIX sh and starts the client with flatpak run. On older hosts, the bundled libc++ and the runtime's glibc do not load.
  5. The client is built with the app. debug.hello, the ALPN crafter-debug/1 and the discovery record's protocol and sourceId are frozen. Bump protocolVersion on any wire change.
  6. Errors name the next step. Decoding never throws. A request that timed out before Pump never runs.
  7. Discovery names instances by random ids and tells liveness by flock, never by pid. Pids collide across flatpak run sandboxes.

Commit and PR guidelines

  • Branch: off master.
  • Subject: type(scope): imperative summary. Types: feat, fix, docs, test. Scopes in use: core, registry, session, discovery, cli, tooling, transport.
  • Body: what, why, and why this shape, wrapped at about 75 columns. Add a Co-Authored-By: trailer when an agent wrote it.
  • Never commit bin/, build/ or AGENTS.md.
  • Push and open PRs only when asked.

Security

  • Debug builds only. The session listens on 127.0.0.1, and every request must carry the instance's 128-bit token.
  • TLS uses Crafter.Network's per-user self-signed certificate, which the client does not check. The token is what proves who asks.
  • Size limits: a request is at most 1 MiB, answered with an error past that. A log --follow client more than 8 MiB behind is reset.
  • Discovery files are 0600 in a 0700 directory.
  • Debuggers: the session calls prctl(PR_SET_PTRACER, PR_SET_PTRACER_ANY), so a debugger the agent starts can attach. That is another reason it never ships in a release build.

Gotchas

  • Group is neither copyable nor movable, and neither is its owner. Its watches refer to the owner's members.
  • Inside a Codec<T> specialization, call the free functions qualified (Debug::Encode): the member Encode hides them.
  • std::format needs a compile-time format string. A ternary between two literals does not compile.
  • Tests don't inherit defines. Add them with .Define().
  • A consumer's #ifdef'd import Crafter.Debug; is harmless in its plain builds.
  • project.cpp regenerates build/generated/<core variant>/Crafter.Debug.Core-BuildInfo.cpp when the sources or the client's path change. crafter-build clean removes it, and the next load writes it again.
  • The launcher is written by Debug::Use while crafter-build loads the consumer, not by the build. A plain build keeps an existing launcher; a fresh plain build writes the stub.
  • A consumer that depends on Crafter.Network itself must name it exactly as project.cpp here does (same URL, no pin, same args). Otherwise the graph holds two Crafter.Network configurations, and crafter-build refuses with "module … built twice".
  • debug.log's last of 0 means no backlog, as tail -n 0 does. A TimelineQuery's last of 0 means all.
  • Answer before you raise a flag the main loop acts on. The loop may end the Session at once. A reply given before the connections close is delivered; one given after is not. quit replies first, then sets QuitRequested.