Commit graph

22 commits

Author SHA1 Message Date
4a0d911e69
feat(examples): Basic, a ball simulation with a bug to find
The example was a placeholder that printed which build it was. It is
now a headless simulation of five balls dropped onto a 10 cm floor
slab at 60 ticks a second, with a bug planted the way such bugs are
written: collision is checked only where each step lands, so the ball
dropped from 60 m, at 34 m/s, steps over the slab and falls forever.

It shows the consumer side end to end:
- Basic.DebugShim forwards Pump, Advance, QuitRequested, Log, Check,
  Emit and Sample to Crafter.Debug in debug builds and does nothing in
  plain ones, so the simulation and main have no #ifdef but the
  session's.
- Basic.DebugCommands, compiled only when Debug::Use returns true,
  registers World_Spawn, World_Reset, World_Clear and Ball_Get, and
  watches for the physics, the floor and the balls.
- The simulation checks each ball stays above the floor, emits Bounce
  and samples the fastest ball, so `pause --on-check`, `wait --event`
  and `trace --history` have something to show.

Driven over the real transport, it shows the whole loop: `log --level
warn` names the ball, `pause --on-check` stops the frame it fails,
`set physics.substeps 4` and `call World_Reset` show the fix holds
before any code changes. The plain build simulates ten seconds and
prints that one ball fell through. Its agent notes describe the app
without the bug.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 03:22:17 +01:00
6da3b274a6
feat(tooling): Debug::Use says whether it added the library
A consumer adds its debug-only sources, the ones that import
Crafter.Debug, under the same condition Use adds the library. Testing
its own flag is not that condition: a build with the flag for a target
other than native Linux gets neither the library nor the define, and
its debug-only sources would then fail to import Crafter.Debug. Use
now returns whether it added them, and the README shows the pattern.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 03:21:41 +01:00
5d34df7afb
fix(cli): print lldb by its path, and report a held app as held
Tried on the example with the app's sandbox, two things were wrong:
- `debugger` printed `flatpak enter <id> lldb -p <pid>`, and flatpak
  enter runs a command without the sandbox's PATH, which /app/bin is
  on: "Failed to execute child process lldb". It now prints the path
  it found, /app/bin/lldb, which attached, listed threads and detached.
- With the app held by that debugger, the handshake times out, and
  the client said "cannot reach basic on port ..." and exited 3, no
  instance, though its lock said it lived. It now says the app is
  alive but not answering, stopped in a debugger or hung, prints how
  to attach, check and end it, and exits 5.

Environment::haveLldb becomes lldb, the path or empty. ShouldServeOverQuic
covers the second with an instance whose lock is held and whose port
is silent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 03:21:41 +01:00
2c77cdcfab
fix(session): report the frame a pause began at
Frames go on counting while paused, since Pump still runs, but status
gave no pause frame and `wait --paused` replied with the current one.
A wait that came minutes after `pause --on-check` caught a failure at
frame 237 said "paused at frame 5475", which an agent takes for when
the check failed.

The session records the frame in PauseNow, and status and the paused
wait report it as pausedAt. The client prints "PAUSED since frame N",
and "paused at frame N (now M)" when the two differ.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 03:21:41 +01:00
7574ad5c4e
fix(cli): take global flags after the command, and call --json again
Only --json was taken after the command, so `wait --paused --timeout
60s`, the order the guide's own synopsis and agents use, failed with
"unknown argument '--timeout'", and `-i` after the command did too.
And since that --json was taken as the output flag, `call X --json
'{...}'`, the documented way to pass arguments as JSON, never reached
call: its object then failed as "not key=value".

--timeout, -i and --json now go anywhere on the line; no command has
options of those names. After `call`, a --json followed by an object
is the arguments, and any other --json the output flag. A trailing
--timeout or -i without its value is a usage error that says so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 03:21:41 +01:00
6987033d6b
fix(core): encode a float as its shortest decimal
A float was widened to double before encoding, so every float watch,
argument default and result read as the double nearest it: gravity
-9.81 showed as -9.8100004196167, a radius of 0.1 as 0.10000000149011612,
in `get`, `help` and call results alike. Agents read these, and copy
them back into `set` and `call`.

Codec<float> now encodes the shortest decimal that reads back as the
same float (std::to_chars), as a double. Decoding is unchanged, and so
is every value: 0.1 decodes to the float 0.1f came from. A double is
encoded as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 03:21:41 +01:00
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
fbb5809aea
test(transport): drive a listening session over loopback
ShouldServeOverQuic runs a Session that listens for real and reaches it
with the client's own transport. It covers:
- the discovery record, and its removal when the Session ends;
- status, call, a mistyped command, get and set, and log --follow
  streaming as entries are logged and ending on time;
- six clients at once, and eight streams at once on one connection;
- a wrong token, a request that isn't JSON, and one over 1 MiB, each
  answered with an error;
- ending the Session with a follower waiting, which is told "shutting
  down" and is not sent a backlog it asked none of;
- a client finding the port dead in milliseconds.

The shutdown reply went missing in 4 runs of 30 until Crafter.Network's
~ClientQUIC let stopped streams finish before closing; 50 of 50 pass
since.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 03:07:09 +01:00
f8024b7a43
feat(cli): reach instances over QUIC
crafter-debug had a placeholder transport that reported every instance
unreachable. Cli::ConnectQuic dials 127.0.0.1 at the port in the
discovery record on the first call, keeps the connection for the next,
and dials again after one is lost. Each call opens a stream, sends the
request with a FIN, and reads lines: one with a result or an error is
the reply, anything before it a stream line for onLine, which runs on
the calling thread so a slow terminal never stalls msquic's worker.
Past the deadline the stream is aborted and the call times out.

The handshake gives up after 3 s, and keep-alives every 2 s hold a
quiet follow or a long wait open under the 10 s idle timeout. main
starts a pool with one worker, which Network reports some failures on,
and ends with _Exit once Run has closed its connections, since msquic's
teardown at exit waits on connections.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 03:07:09 +01:00
47f2325664
feat(transport): serve the session over QUIC on loopback
A Session with listen = true (the default) now listens on 127.0.0.1 with
the ALPN crafter-debug/1 and writes its discovery record (port, token,
protocol, source id, client path), which goes first when it ends. One
request per stream the client opens: the request and a FIN in, zero or
more stream lines and then the reply and a FIN out, one JSON object per
line.

It holds no thread while nothing arrives. Crafter.Network delivers a
new stream on the pool, its bytes on msquic's worker through OnRecieve,
and replies leave through SendAsync, which never waits. The handler,
which takes the session's mutex, runs on a pool task rather than inside
the stream's callback, so no msquic worker ever waits on the session,
and the session's threads only send, which Network allows under any
lock. Stream wrappers are let go outside every lock their callbacks
take. The rules head the implementation.

Limits: a request over 1 MiB is answered with an error, not buffered,
and a log follower more than 8 MiB behind is reset. quit --force exits
200 ms after its reply was taken, from a thread of its own, since
"sent" means msquic has the bytes, not that they left. A client killed
without closing is let go at the 10 s idle timeout.

The Session refuses a pool without a worker: Network hands it new
streams there, and with Start(1) nothing would run them. The partition
exports nothing, so an app's view of the module is unchanged and it
does not see Crafter.Network's types.

Crafter.Network comes from forgejo, unpinned, with the args unchanged;
--local takes ../Crafter.Network, and $CRAFTER_DEBUG_NETWORK any
project.cpp, for the branch the callback API is on until it lands.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 03:06:56 +01:00
d75f935bf2
fix(session): answer quit before raising QuitRequested
Quit raised the flag and then replied. A main loop that saw the flag in
between could end the Session, and with it the connections, before the
reply was given, and the agent then saw the app vanish (exit 3) instead
of its quit succeed. The reply now goes first: the transport delivers a
reply given before its connections close. quit --force raises the flag
first as before; it exits from the reply's own thread.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 03:06:38 +01:00
b95c594be8
fix(session): log --last 0 means no backlog
debug.log handed `last` to the timeline query, where 0 means every
entry, so `log --follow --last 0` streamed the whole ring of up to 8192
entries before the new ones, where an agent reads it as tail -n 0 -f:
nothing old, only what comes next. 0 now means no backlog, followed or
not, and the guide says that --follow starts with the last 50.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 03:06:38 +01:00
29f1eed9ab
fix(session): keep the request types out of an anonymous namespace
Session::State has module linkage, and six of its members take Job or
Waiter, which lived in an anonymous namespace: clang warned on each
build that a TU-local entity was exposed (-WTU-local-entity-exposure).
They move to a named namespace, SessionImpl, which this file alone uses
and nothing exports. No behaviour changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 03:06:25 +01:00
2a954b7f1f
docs(tooling): tell consumers' agents how to code with Crafter.Debug
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>
2026-11-01 02:16:05 +01:00
8b995cd76a
docs: the manual, notes for agents, and the notes consumers get
- 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>
2026-11-01 02:12:17 +01:00
34c60dcd34
feat(cli): crafter-debug, the client agents drive an app with
Cli::Run is the whole client but its transport, so tests run it in
process against a real Session; main will hand it the QUIC connection.

- Commands: ls, status, cmds, help, call, get, set, pause, resume,
  step, wait, trace, log (with --follow), screenshot, quit, debugger,
  guide and version, with -i, --json and --timeout.
- call types key=value from the command's argument schema (slot=1 is
  a number, name=1 a string, at.x=3 nests), so agents need no JSON
  for simple calls; a value that cannot fit its field is a usage
  error before anything is sent.
- The instance comes from discovery; one running instance needs no -i.
  The record's protocol is checked before any request: a mismatch
  exits 4, says which side is newer and prints the app's own client.
  Other sources only warn.
- A request with no reply is diagnosed: the app has exited, stopped
  in a debugger (with the attach command), or no longer calls Pump
  (with what to change). Exit codes: 2 usage, 3 no instance,
  4 version, 5 timeout, 6 the app's error.
- Output is short by default, arrays cut at 20 with a note; --json
  prints everything. `guide` is the full reference for agents.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 02:03:38 +01:00
37e0d9b9ae
feat(discovery): find live instances across sandboxes; build info
Each app writes $HOME/.cache/crafter-debug/instances/<id>.json (0600,
in a 0700 directory) and holds an flock on <id>.lock while it lives.
Every `flatpak run` is its own pid namespace, so pids collide and can
neither name an instance nor tell whether it lives; a random id names
it and the lock says it lives, since flock works across sandboxes on
the shared filesystem. An app killed without its destructor leaves its
files with the lock free, and the next listing removes them. The lock
is created under a temporary name, locked, then renamed, as in
Crafter.Review's LifetimeLock, so it is never seen unlocked while its
app lives. Inside a Flatpak the record also carries the sandbox's
instance id (for `flatpak enter`) and the host pid of its init (to end
the whole sandbox from the host).

project.cpp now generates :BuildInfo per core variant: a hash of the
library's sources and the path of the client built with it. App and
client share the core, so they report the same id exactly when built
from the same sources; this is the safety net under building the
client in the same pass as the app.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 01:56:31 +01:00
bd64c44fc5
feat(session): serve requests without blocking a thread, pause and step
The app's end of the connection, independent of the transport, which
reaches it through Detail::Exchange: one request's way back, safe from
any thread and never blocking.

- What needs only the session's own state (hello, status, list, log,
  pause, resume) is answered on the transport thread. What touches the
  app (calls, get, set, trace, screenshot) queues for the next Pump and
  runs on the app's thread between frames. Waits and steps reply from
  the Pump that sees them satisfied, events from the thread that emits
  them. No request ever holds a thread while it waits.
- A request whose timeout passes before Pump is answered Timeout and
  is guaranteed not to have run, so an agent never wonders whether a
  command went through; an Async result arrives on the Pump it is
  ready; requests left when the Session ends are answered ShuttingDown.
- Advance() for frame loops and Paused()/TakeSteps() for fixed-step
  loops hold what the app gates on them; pausing an app that gates
  nothing is refused instead of silently doing nothing. pause --when
  stops the frame its condition holds; pause --on-check stops before
  the next Advance in the frame a check fails, optionally raising
  SIGTRAP for an attached debugger.
- A wait that times out says what its condition's paths were; a log
  follower gets matching entries in order, from the backlog on, and is
  dropped when it cannot keep up.
- Request parameters go through Codec, so a mistyped request gets the
  same did-you-mean replies a mistyped command does. The session sets
  PR_SET_PTRACER_ANY so a debugger the agent starts can attach.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 01:53:15 +01:00
3500b73ecf
feat(registry): commands, watches, samples, the timeline and conditions
The app side of Crafter.Debug, minus the session:

- Command(name, description, handler): the handler takes nothing or
  one argument struct and returns void, a value, Result<T> or
  Async<T>. Arguments are decoded before it runs, so a mistyped call
  is answered with every mistake and never reaches it; a throwing
  handler becomes an Exception error instead of unwinding into the
  frame loop. This replaces 3DForts' three-file pattern per verb.
- Watch(path, variable | getter [, setter]) with bounds, help and
  read-only, and Sample(path, value) from any thread with a
  frame-stamped history. Paths resolve into values ("bodies[1].pos.y")
  and set the same way, writing a member writes the whole value back.
  Unknown paths and commands get a suggestion.
- Group: registrations under a prefix that end with their owner. Not
  movable, so neither is its owner: its watches refer to the owner's
  members, and a move would leave them pointing at the old object.
- Log, LogWarn, LogError, Check and Emit on one frame-stamped
  timeline, with source locations captured without macros. A check
  failing every frame is counted every time but stored sparsely, and
  the session's listener hears every failure.
- Condition: what `wait` and `pause --when` evaluate each frame. A
  condition that cannot be decided reports why instead of being false,
  which would make a wait hang.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 01:40:59 +01:00
89a12bcd61
feat(core): Value, reflection, codecs and results, shared by app and client
Crafter.Debug.Core is what the app side and the crafter-debug client
both need, without the session:

- Value: a JSON value with its own parser and printer, so no consumer
  interface sees a JSON library. Integers stay integers and whole
  doubles stay doubles; NaN and the infinities, which JSON cannot
  spell, are written as strings rather than hidden, because a debugger
  must not lose a NaN; invalid UTF-8 is replaced so output always
  parses. Objects keep insertion order, the order an agent reads.
- Reflect: aggregate members and names through structured-binding
  packs and source_location, taken from Crafter.DB, plus scoped-enum
  names by probing a range of values. Self-tested with static_asserts
  so a compiler that changes function_name()'s shape fails the build.
- Codec<T>: encode, decode and describe for bool, integers, floating
  point, strings, enums, optional, vector, array, string-keyed maps,
  Value, Required<T> and every aggregate. Decode reports every
  problem in a call with its path and, for a typo, the likely intended
  name, and never throws: a bad call cannot take an app down, which a
  wrongly typed parameter could in 3DForts' hand-written from_json.
- Result, Error and Async: handler results, including ones that take
  frames to arrive, so callers need no ready:false polling.

project.cpp now builds Crafter.Debug.Core as its own library, used by
both the app library and the client.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 01:31:58 +01:00
fb2c06c32e
feat(tooling): wire consumers in through Debug::Use and a launcher
A consumer loads Crafter.Debug.Build through
  #pragma crafter dependency "<Crafter.Debug>" project "tooling/project.cpp"
and calls Debug::Use in its own CrafterBuildProject, the way
Crafter.LintRules gets in. With the consumer's flag on, Use adds the
library from the checkout the pragma loaded, and writes
build/crafter-debug: a POSIX sh launcher for the client that library
builds. The client is one of the library's dependencies, so a debug
build compiles it in the same pass as the app, from the same sources.

Why a launcher and not a script: every `crafter-build run` loads the
whole project first, which costs 13-28 s per call on 3DForts (a git
pull per unpinned dependency plus rescanning sources), while the
launcher is one sandbox start, 0.08 s. A client built in the Flatpak
links the Flatpak's libc++ against the runtime's glibc, so the launcher
starts it through `flatpak run` and never on the host itself; inside a
CrafterBuild sandbox it runs it directly.

The library, the client and examples/Basic are placeholders that prove
the wiring: the debug build writes a launcher whose client exists, a
plain build has no Crafter::Debug symbols and keeps the notes from
tooling/AGENTS.consumer.md, and a fresh plain build writes a stub that
names the flag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 01:18:07 +01:00
d693d7b645
chore: start the repository
LGPL-3.0-only like the other Crafter libraries, and ignore what builds
write: bin/, build/ and the AGENTS.md crafter-build generates, in the
root and in examples.

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