The consumer notes only said how to drive a running app. They now also say how to add to what an agent can reach: where the hook points and registrations go, gating pause and step, registering commands, watches, samples and checks, and the rules that keep that code right (handlers run on the app's thread and must not block; Async for results that take frames; call the app's own code paths instead of re-implementing them, the lesson from 3DForts' drifting verbs; Group for per-object state; which types travel). All of it stays in tooling/AGENTS.consumer.md, which every build merges through the unconditional pragma. Notes beside the root project.cpp would only come with debug builds, when the library is in the graph, and would then duplicate these; AGENTS.template.md now says so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
119 lines
8 KiB
Markdown
119 lines
8 KiB
Markdown
# 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:<br>• `:Value`: JSON value with its own parser and printer<br>• `:Reflect`: aggregate members and scoped-enum names, without `<meta>`<br>• `:Codec`: encode, decode, describe<br>• `:Result`: Error, Result, Async<br>• `:Protocol`: envelope, codes, versions, base64<br>• `:BuildInfo`: source id and client path, generated<br>• `: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:<br>• `:Registry`: commands, watches, samples, Group<br>• `:Timeline`: Log, Check, Emit<br>• `:Condition`: what wait and pause --when evaluate<br>• `: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.
|
|
|
|
`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.
|
|
|
|
## Build and test commands
|
|
|
|
```bash
|
|
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:
|
|
- `coreTests` link the core;
|
|
- `libraryTests` link the library;
|
|
- `ShouldRunClientCommands` links the library and the client.
|
|
|
|
**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.
|
|
|
|
**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.
|
|
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.
|
|
- **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.
|