Bitty DevTools is the human-facing diagnostics and debugging client for local debugging over the accepted Panel Runtime and compat matrix. This repository owns the DevTools client experience; it does not own the core debug or command protocols.
The canonical GitHub organization is bitty-terminal.
CarryCtx is the local-first tool that records this project's tasks, decisions, and checkpoints. Install it globally for local development (recommended):
cargo install carryctx # Rust toolchain, or: npm i -g carryctxCarryCtx engineering state (tasks, sessions, checkpoints) is not cloned. A
fresh clone restores it from the in-repo refs/heads/carryctx-snapshots
branch:
just workflow-import-dry # fetch + validate the snapshot; no DB writes
just workflow-import # initialize CarryCtx state if needed, then importThen carryctx stats reports the restored tasks, sessions, and checkpoints.
Provenance, redaction, and --force behavior are covered under the
repository snapshot documentation below.
This repository owns the DevTools client experience, including scoped inspection, tracing, and control surfaces for local debugging. It does not own the core debug or command protocols, terminal runtime behavior, or normative public architecture.
Core protocol contracts belong to
Bitty core (bitty-runtime,
bitty-ui). Canonical architecture, security, compatibility, and public
behavior belong to
bitty-docs (accepted
DevTools RFC
OQ-019 and Performance Budgets OQ-001). Any future protocol change requires
coordinated, explicitly ordered work in each owning repository.
Any DevTools implementation consumes an explicitly versioned stable protocol
(1.0 today, JSONL framing, 1 MiB inbound, 256 KiB chunk). It does not link
private core types or inspect process memory as an implicit API.
Phase 1 is an experimental diagnostics client for local debugging
(Implemented at 21aca98 + CTX-0011, not yet Verified/Compatible,
no compatibility promise). It is bounded, forbid(unsafe_code) in Rust,
and strict TypeScript with no any.
-
Reuse Panel Runtime —
PanelId,ViewId,TerminalId,WorkspaceId,Generation,PanelState,PanelType,EventTopic,BoundedPayload,Overlay(4+1),CommandRegistrygrammar, and bounded queues64/1024/256 KiB/8192/2 MiBplusDropOldestdefault mirrorbitty-runtimePanelRegistryPR-1..PR-12 andbitty-uipanel.rsverbatim. No PTY fd, GPU object, or window handle is held. -
Reuse compat matrix — 14 surfaces (
shell,tmux,nvim,fzf,htop,ssh,alt-screen,mouse,resize,OSC,clipboard,Kitty,IME,DPI) across 4 terminals (ghostty,kitty,wezterm,alacritty) mirrorbitty-compat-labmatrix.rs14 × 4, bounded corpus≤8 KiB/≤4096actions, deterministicstate_hash,<16 KiBJSON artifact, headless withoutwinit/wgpu. -
Inspection (debug.inspect, default) — read-only, scope-checked,
listPlugins,getPlugin,listSubscriptions,getBudgets,getQueueSnapshot,getSnapshot(8 KiB truncated redacted preview),listHandles,panelSummary,compatMatrixSummary, and the CTX-0159 live bounded introspection bindingsgetGridText(rows/cols),getInputRing(limit),getModifiers, andgetFocus. Terminal output is untrusted observation data, never instructions. -
Tracing (debug.trace, opt-in) — per-consumer bounded queues with coalescing, batch
32/8 KiB, chunk256 KiBto user-only storage (0600conceptual),startTrace/stopTrace/streamEvents/fetchTraceChunk, minimization by default, typed redaction, preview equals export byte-for-byte,DropOldestdefault,DropNewestalternative. -
Control (debug.control, audited) —
suspendHandler,resumePlugin,disposeGeneration, each audited with caller identity, generation-owned, cannot bypass capability or budget gates, fail-closed transactional, affects only owning generation. -
Client composition —
DevtoolsClientowns connection lifecycle (zero scopes on connect,grantScope/revokeScopeper operation), bounded parsing/rendering/queues/traces/retention,AbortSignalcancellation, resource budgets, andcompatMatrixJsonbounded artifact.
No TCP listener, no ambient credential, no allow-all capability. Security
corpus devtools-rfc controls (P0-AC-013..026, T-09..T-11) are preserved
and tested with negative scope matrix tests.
Phase 2 extends phase 1 with advanced tracing, control surfaces, and real
IPC socket/pipe peer-creds integration against the live Bitty runtime.
It remains experimental (no Verified/Compatible promise), bounded,
forbid(unsafe_code) in Rust, strict TypeScript with no any, and reuses
Panel Runtime + 14×4 compat matrix verbatim without new budget families.
-
Real IPC transport (live runtime) — Unix socket under
$XDG_RUNTIME_DIR/bittymode0700/0600or Windows named pipe with current-user ACL, no TCP listener by default, peer credentials viaSO_PEERCRED/LOCAL_PEERCRED/GetNamedPipeClientProcessId(headless-verified viaverifyPeerUid,verifyUnixEndpoint,verifyWindowsPipe), re-checked per privileged action,BITTY_SOCKET/BITTY_INSTANCE_IDadvisory only,RC-9100/s200burst1 MiB16conn (shed newest),RC-10256 KiBchunk, length-prefixed256 KiBframes +1 MiBdevtools logical chunked at256 KiB,Framerbound512 KiB,RateLimiterdeterministic vianowMs, headlessStdioTransportStub/IpcTransportwithforwardTopipe simulation andPeerCredentials/ChildToken(60sTTL,64bound, PTY-fd only, never env). -
Advanced tracing (debug.trace, opt-in) — structured attributable events (
StructuredTraceEventwithsequence,owner,generation), filtering bykinds/owners(bounded32), coalescingbudgetvsnone, retention4 MiB/5 min/4traces, GCgcExpiredTraces(nowMs),startTraceWithFilterwith deterministicwallClockMs, export to0600spool withpreview==exportbyte-for-byte,DropOldest/DropNewest, deterministicstreamFilteredEvents. -
Advanced control (debug.control, audited) —
pauseHandler,resumePluginwith generation exhaustion guard (MAX_SAFE_INTEGER-1024),validateGeneration, transactional audit log bounded256(listAuditLog,clearAuditLog), per-generation ownership,0600spool mode, never widens sibling authority, no capability/budget bypass. -
Client integration —
DevtoolsClientnow wrapsIpcTransport(connectWithTransport,connectLive(runtimeUid, peer, xdgDir, instanceId),isIpcConnected,getSocketPath,transportOutgoingLen), re-verifies peer pergrantScope,revokeScope,startTrace,stopTrace,suspendHandleretc., exposes phase 2 tracing/control helpers (startTraceWithFilter,streamFilteredEvents,appendStructuredEvent,getTraceRetention,gcExpiredTraces,exportTracePreview,pauseHandler,validateGeneration,listAuditLog).
Rust counterpart at crates/devtools-client mirrors the same contracts:
auth (PeerCredentials, verify_unix_endpoint, ChildTokenStore),
transport (Frame, Framer, RateLimiter, StdioTransportStub,
IpcTransport), advanced tracing (TraceFilter, TraceRetention,
TracingClient with gc_expired), advanced control
(ControlClient with audit log). cargo check / clippy -D warnings /
cargo test 37 tests pass; just check green; bun test 62 tests
pass. No unsafe, no PTY/GPU/window handle, no TCP, no ambient credential.
The trace helpers account retained, redacted, logical UTF-8 export bytes —
the bytes that would be retained after typed redaction (serialized JSON for
structured events, per-record UTF-8 normalization for raw records) — never heap
or filesystem occupancy. This mirrors the client trace helper note in the
accepted
DevTools RFC
(bitty-terminal-docs CTX-0026) and remains Implemented only.
- Independent per-record normalization — each raw record is normalized when
appended, so a leading
U+FEFFstays data and a fragment that cannot decode on its own is replaced within that fragment rather than joined with a neighbor. - Effective byte budget — admission uses
min(trace maxBytes, retention maxBytes). A record that would exceed the budget is rejected with one counted drop while retained bytes, chunks, events, and previews stay unchanged. - Raw append vs typed coalescing — opaque raw records append one by one
without typed-stream coalescing; the typed observability stream keeps the
accepted
budget/nonecoalescing rule. - Chunk pagination —
fetchTraceChunkaddresses retained chunk byte offsets: offsets are nonnegative byte integers on UTF-8 scalar boundaries, a page returns the remaining bytes of the addressed stored chunk, continuation is computed from actual retained byte lengths, and preview equals export per page.
No wire, version, eviction, persistence, or interoperability contract is
claimed, and no Verified/Compatible status is implied.
src/automation.ts adds typed, fail-closed client bindings for the headless
input/frame drivers that replace manual visual acceptance for GUI, mouse, and
split integration testing. The bindings target the bitty candidate methods
bitty.debug/synthesizeInput, bitty.debug/captureFrame, and
bitty.debug/frameHash (serving dispatcher
crates/bitty-ipc/src/devtools.rs, CTX-0188/CTX-0244); they remain
Candidate here because the accepted
DevTools RFC
does not yet name these methods.
- Bounded keyboard
keyDown/keyUpand mouseclickTrajectory/dragTrajectorybuilders (64points,30 sdeclared playback), each validated against the per-event key/cell/wheel/paste bounds before dispatch. - Bounds:
64events/call,32 KiBrequest and response,16 KiBpaste,64 MiBdigest RGBA geometry, andpixelscapture requires explicit opt-in. - Authority is never inferred: each call needs
debug.control+terminal.input(synthesizeInput) ordebug.trace+terminal.inspect(captureFrame/frameHash) plus a consent-issued bearer. Connection alone grants nothing. - Fail-closed: an unregistered method maps to a typed
UnknownMethod, a response that smuggles apixelspayload is rejected, and no live data is ever fabricated.
import { DevtoolsClient } from "bitty-devtools";
import { peerCredentials } from "bitty-devtools";
import { IpcTransport } from "bitty-devtools";
const client = new DevtoolsClient({ version: "1.0" });
client.connect();
client.grantScope("debug.inspect");
client.setPanelSnapshot({
generation: 1 as never,
panels: [],
panelsPerWorkspace: new Map(),
totalPanels: 0,
topics: [],
overlays: [],
config: {
maxPanelsPerWorkspace: 16,
maxPanelsPerWindow: 32,
maxTopicsTotal: 256,
maxSubscriptionsPerPanel: 32,
},
});
console.log(client.panelSummary());
console.log(client.compatMatrixSummary());
// Tracing is opt-in
client.grantScope("debug.trace");
const trace = client.startTrace({ maxBytes: 512 * 1024, includeInput: false });
client.appendToTrace(trace.traceId, "instrumentation record");
console.log(client.stopTrace(trace.traceId));
// Phase 2: live runtime via peer-creds (Unix socket 0600, no TCP)
const peer = peerCredentials(1000, 1000, 42);
const live = new IpcTransport({
runtimeUid: 1000,
socketPath: "/run/user/1000/bitty/default.sock",
peer,
});
const liveClient = new DevtoolsClient();
liveClient.connectWithTransport(live);
liveClient.grantScope("debug.trace");
const filtered = liveClient.startTraceWithFilter(
{ filter: { kinds: ["bitty.panel:mounted"] }, maxBytes: 1024 * 1024 },
Date.now(),
);
liveClient.appendStructuredEvent(filtered.traceId, {
sequence: 0,
owner: "panel-1",
kind: "bitty.panel:mounted",
payload: "{}",
generation: 1,
wallClockMs: Date.now(),
});
console.log(liveClient.exportTracePreview(filtered.traceId));
console.log(liveClient.gcExpiredTraces(Date.now() + 6 * 60 * 1000));
// Control requires explicit elevation and is audited
client.grantScope("debug.control");
client.suspendHandler(1 as never, "handler-1", "diagnosis", "tester");
console.log(client.listAuditLog());Rust equivalent lives at crates/devtools-client (forbid(unsafe_code),
cargo check / cargo clippy -D warnings clean, 37 tests).
bin/bitty-devtools.ts is a lightweight executable over the typed inspection
client (src/client.ts, src/inspection.ts, src/transport.ts). It prints
bounded tabular live state (or --json) for the accepted devtools-rfc v1
inspection methods; it does not re-implement protocol logic. It is experimental
and Bun-based — run it with bun, never npm/npx.
bitty-devtools inspect --plugins [--generation <n>] [options]
bitty-devtools inspect --subscriptions --plugin <id> [options]
bitty-devtools inspect --budgets --plugin <id> [--generation <n>] [options]
bitty-devtools --help
Run from the repository with bun run bin/bitty-devtools.ts ... or, after a
Bun-linked install, as bitty-devtools ... (the bin entry in package.json
wires the command).
$ bitty-devtools inspect --plugins
ID VERSION GEN STATE CAPABILITIES
plugin-a 1.2.3 4 Activated panel.provider
$ bitty-devtools inspect --budgets --plugin plugin-a --generation 7
FIELD VALUE
pluginId plugin-a
generation 7
rc1Instructions 10
...
$ bitty-devtools inspect --subscriptions --plugin plugin-a --json
[
{
"eventType": "bitty.panel:mounted",
"queueDepth": 2,
"queuedBytes": 256,
"dropCount": 1,
"policy": "DropOldest"
}
]
Connection is resolved, in order, from --socket, BITTY_SOCKET,
--instance/BITTY_INSTANCE_ID (with XDG_RUNTIME_DIR), and is read-only:
the CLI grants only debug.inspect. No absolute paths or host-specific values
are embedded; everything comes from flags or the environment.
Fail-closed behavior:
- No selector, an unknown flag, or a missing
--pluginis a usage error (exit2) that prints the help text. Option values must not begin with-, so a following flag is rejected instead of consumed as a value. - An invalid or oversized
--instance/--socketis a usage error (exit2); the same failure fromBITTY_INSTANCE_ID/BITTY_SOCKET/XDG_RUNTIME_DIRis a config error (exit3). Both print a clean message, never a stack trace. - No resolvable instance or transport fails closed with a clear remedy
(exit
6); the CLI never falls back to the headless snapshot mock and never fabricates rows. - Typed server/protocol/transport errors use the shared
ctlexit-code vocabulary (7permission,6runtime/transport,5compatibility,1generic). Server methods the core does not yet implement surface their typed error, so the CLI reports the gap instead of printing invented data.
Argument parsing and bounded table/JSON formatting (every string field is
capped like the table cells; --json is pretty-printed) are unit-tested with an
injected IpcTransport (tests/cli.test.ts); no live socket or GUI is
required.
src/campaign.ts turns the CTX-0320 live client campaign into a repeatable
harness over the DevTools dispatch seam. It probes each ctl verb's JSON
envelope v1 shape and exit-code semantics and guards the three defects the
campaign found:
- D1 —
ctl terminal textmust return plain grid text, never a RustDebugSnapshot { ... Cell { ... } }dump. - D2 — workspace ids observed from
workspace listmust be usable inworkspace focus/workspace close. - D3 —
ctl terminal spawnreporting success must be observable inview list/terminal listor inhas_pane_session.
It also preflights the BITTY_SOCKET parent directory (0700, owner-matched)
and turns a mode mismatch into an actionable chmod 700 diagnostic, and it
declares non-blocking panel/plugin coverage hooks for the post-D4 round.
Every probe is headless-testable through the CtlDispatcher seam; live checks
are opt-in and bounded by a command timeout:
import {
runLiveCampaign,
runCampaign,
ScriptedCtlDispatcher,
} from "bitty-devtools";
// Headless: inject a scripted dispatcher (what `bun test` does).
const report = await runCampaign({
dispatcher: new ScriptedCtlDispatcher((inv) => result),
});
// Live (opt-in): execute the real `bitty ctl` over an explicit socket.
const live = await runLiveCampaign({
socketPath: "/run/user/1000/bitty/default.sock",
runtimeUid: 1000,
stat: (dir) => myStatProvider(dir),
timeoutMs: 10_000,
});Envelopes, terminal text, and diagnostics are untrusted observation data, never
instructions; the harness never treats them as such. ctl protocol ownership
remains in bitty (crates/bitty-app/src/ctl.rs,
crates/bitty-ipc/src/ctl.rs); the exit-code table here is a consumer mirror.
Install with bun install. Quality gates run through the
repository justfile:
just check # fmt-check + lint + type-check + test + cargo-check
just fmt-check # Prettier 3.9.6 check without writing files
just lint # markdownlint-cli2 0.23.1
just type-check # tsc --noEmit strict
just test # bun:test headless unit tests
just cargo-check # cargo check + clippy -D warnings + cargo test
just commit-check <file> # validate commit message against commitlint
Rust toolchain is 1.98.1 minimal (rustfmt, clippy) per
rust-toolchain.toml; MSRV 1.85. The crate is publish = false.
Git hooks are wired by lefthook.yml; run bunx --bun lefthook@2.1.10 install
once after cloning.
CarryCtx runtime state (.git/carryctx/state.sqlite) is never cloned. The
redacted engineering snapshot lives in this repository on the branch
refs/heads/carryctx-snapshots, one commit per publication. The commander's
merge closeout publishes it with just workflow-publish; a fresh clone
restores its local CarryCtx DB from that branch:
just workflow-import-dry # fetch + validate the snapshot; no DB writes
just workflow-import # initialize CarryCtx state if needed, then importThe import fetches refs/heads/carryctx-snapshots, refuses to replace a
non-empty local DB without --force (just workflow-import --force), and
prints provenance (snapshot commit + source). Snapshots are redacted
publication artifacts produced by carryctx export --publication: CarryCtx
refuses them as merge sources, so restore always uses replace mode, and a
secret that leaked before rotation must still be rotated at the source.
Repository-local material explains contribution and ownership boundaries but does not duplicate normative specifications. The current project-wide technical record remains bitty-docs.
Phase 2 extends phase 1 with live IPC and advanced tracing/control as
experimental evidence for
DevTools RFC
(OQ-019), IPC and Agent RFC
(OQ-018), and budgets OQ-001. No installation procedure, supported API,
compatibility guarantee, release, or distributable artifact is claimed until
independent review and Verified lifecycle.