- README: how the pieces fit, adding Crafter.Debug to a project (Debug::Use, the shared-args rule, Session and Pump, the AGENTS.md checklist), the app-side API with examples, the fixed-step and Crafter.Graphics patterns, a shim module for calls without #ifdefs, and the agent side: commands, exit codes, stopping apps, attaching a debugger, version skew. - AGENTS.template.md: the configurations and partitions, commands, test conventions (including the dangling Value::Find pointer two tests hit), style, design rules, security and gotchas. - tooling/AGENTS.consumer.md: what every consumer's agents read in each session, kept short: the loop, exit codes and the gotchas, pointing at `crafter-debug guide` for the rest. The QUIC transport the README describes lands next. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7.5 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: Cli::Run, with the transport injected. |
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 |
The client is one of the library's dependencies only so that a consumer's debug build compiles it in the same pass.
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.
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:
/tmpis private;- the toolchain is clang 23 with libc++;
/.flatpak-infoexists.
Every flatpak run is its own sandbox with its own pid namespace.
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*'
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:
coreTestslink the core;libraryTestslink the library;ShouldRunClientCommandslinks the library and the client.
Shape of a test:
- Name it
Should<Verb><What>. - Open
main.cppwith 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.
Files and discovery:
- Temporary files go under
fs::temp_directory_path() / "crafter-debug-<Test>". - Discovery tests set
CRAFTER_DEBUG_DIRthere.
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.
PascalCasefor types and functions,camelCasefor variables, members and constants.std::printlnandstd::format.- Fixed-width integers.
enum class.
Comments say why: the failure mode, the race, the rejected alternative. Long comments are deliberate.
Design rules
- Release builds never link Crafter.Debug. Consumers gate it through
Debug::Useand their own define. The library has no no-op mode. - 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::ReplyandStreamnever block. The thread pool belongs to the host app. - 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.
- 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. - The client is built with the app.
debug.hello, the ALPNcrafter-debug/1and the discovery record'sprotocolandsourceIdare frozen. BumpprotocolVersionon any wire change. - Errors name the next step. Decoding never throws. A request that timed out before
Pumpnever runs. - Discovery names instances by random ids and tells liveness by flock, never by pid. Pids collide across
flatpak runsandboxes.
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/orAGENTS.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.
- 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
Groupis 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 memberEncodehides them. std::formatneeds 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'dimport Crafter.Debug;is harmless in its plain builds. project.cppregeneratesbuild/generated/<core variant>/Crafter.Debug.Core-BuildInfo.cppwhen the sources or the client's path change.crafter-build cleanremoves it, and the next load writes it again.- The launcher is written by
Debug::Usewhile crafter-build loads the consumer, not by the build. A plain build keeps an existing launcher; a fresh plain build writes the stub.