A standing brief for any agent editing this repo. First stop. Updates as the truth changes.
Doesn't duplicate README.md or ARCHITECTURE.md: points at them and adds the things an agent needs to act well.
- What this is. Go CLI coding agent, supervisor architecture, JSON-RPC over Unix sockets. See
README.md. - How it's wired. Process model, packages, data flow live in
ARCHITECTURE.md. Treat as authoritative; if you contradict it, update it in the same change. - Personality. The credo shapes the user-facing voice, and it is the
owner's file (
$FIGARO_CONFIG_DIR/credo.md), not the repo's: there is no default credo in this tree. Match lightly; don't parody. Editing it changes every aria on the machine, so say so when you do.
go build ./...
go test ./...
go vet ./...
nix build # flake.nix is a first-class build path- Module:
github.com/jack-work/figaro. Go 1.25. - Stamp the revision when you build a binary to run:
go build -ldflags "-X github.com/jack-work/figaro/internal/cli.commit=$(git rev-parse HEAD)" -o /tmp/figaro ./cmd/figaro. A plaingo buildin a worktree records no VCS revision, so the CLI/daemon build handshake can only warn instead of refusing. See the tmux-testing skill. hushandlargoare local replace directives in active development. Don't pin or vendor without asking.- CGO disabled in nix builds. Keep it that way.
When you change deps (go.mod / go.sum), the flake's vendorHash will need updating: the build will tell you the right hash. Surface flake updates in your summary so the user can audit.
- Comments: default to none. Only when the why is non-obvious, a hidden constraint, a workaround, a subtle invariant. Don't restate the code. No "used by X" or "added for Y": git log handles that. Function-level docs only when the contract isn't obvious.
- Errors: wrap with
%wwhen context matters; raw when the caller has it. Don't swallow. - Concurrency: actor model. Inside a figaro, do not introduce goroutines that touch agent state without going through the inbox.
- Logging:
fmt.Fprintf(os.Stderr, "agent: …")is the runtime trace channel; use sparingly. The per-figaro.jsonlevent log viafanOutis the structured place. - User-facing strings: terse. Italian/Spanish flourish where it doesn't slow comprehension. Code first, aria second.
- No emojis in code or commits unless asked.
- Forward-only refactors in dev mode. No back-compat shims for old on-disk shapes: when the format changes, bump the provider Fingerprint (which clears the translator cache) or document the migration. Exception: when the user explicitly asks.
Load-bearing. Breaking them produces races, lost messages, or silent corruption.
- Single inbox per figaro. All events drain through one goroutine. New goroutines that touch agent state must report results through the inbox.
- Translator stream is a derivable cache.
aria.jsonlis canonical. The translator can be regenerated by walking the figaro timeline throughProvider.Encode. OnProvider.Fingerprint()mismatch the agent clears it. Translator entries are not authoritative: re-derive from the figaro timeline if you need truth. - synchronize owns translator orchestration. Both directions:
catchUpFigaroDelta+condenseLive(translator → figaro) andcatchUpTranslator(figaro → translator).startLLMStreamjust projectstranslator.Durable()and ships. - Translator entries are input-ready. Bytes go into the translator already-shaped for re-splicing. Direct Anthropic captures the sanitized provider-native assistant response so signed/redacted thinking survives; SDK Anthropic stores
Message.ToParam(). The IR fallback encoder omits unsigned thinking because it cannot reconstruct a valid signature. Response-onlystop_reason/model/usagestay on the Figaro IR message and out of the native cache. - Cache prefix is byte-stable. The conversation prefix sent to providers must be byte-identical across requests within an aria's lifetime. The translator cache structurally guarantees this: per-message bytes are written exactly once. Don't add a code path that re-encodes a prior turn's bytes.
- PID binding is 1:1. Shell PID → at most one figaro.
pid.bind/pid.unbind/pid.resolvego through the angelus. - Panic recovery preserves identity.
runWithRecoveryresets the in-memory store and restarts the drain loop. ID, registry entry, PID bindings, socket, and credo survive: invisible to the model. - Interrupt cuts the line.
eventInterruptis selfish; jumps ahead. Stragglers from cancelled goroutines are suppressed bya.interrupted. Keep them suppressed. - Notification ordering is wire-ordered. Notifications come from the drain loop (or
fanOutit owns), not arbitrary goroutines. - Secrets never hit disk in plaintext. Tokens go through
hush. Don't read or log credentials. - One static binary. No new runtime deps (Node, Bun, Python). New tools, providers, frontends reach through the existing socket protocol.
- Harness does not inject overrides. No "ignore previous", no synthetic system speech mid-conversation. State changes flow through the form. Credo persists across panics, model switches, and interrupts.
- XWAL owns cache topology. Parallel channels share the aria tree and its immutable on-disk prefixes; forks inherit parent prefix locations rather than copying or rebuilding them. Keep native hot head/channel snapshots open, serialize writes per lineage, and never reopen/rescan total history per append or block unrelated trunks behind a global tree lock. Prefer Figwal/XWAL snapshots, watermarks, and shared-prefix views over Figaro-side row caches or precautionary locks.
- Translator catch-up is delta work, and the read that follows it holds nothing. In either direction, normal synchronization is O(untranslated messages), usually one or two. O(total history) cache scans, copies, or lookups are a design failure; full walks are reserved for explicit fingerprint invalidation such as a model change. The READ side is bounded separately and differently: assembling a request DOES walk the whole conversation -- eeec2bc0 ruled that the walk replaces the retention, and nothing removes the walk -- but it walks it (
store.Scan,provider.TranslationRows, aprovider.RowSeqinto the streamed body) and never materializes it. A[]json.RawMessageof the whole history on the send path is the same design failure as a whole-history scan, measured: 87% of a 461MB live heap, in one in-flight send, at 0.29.1. - Dormant listing is metadata-only. Agents persist complete
AriaMetaat initialization, turn boundaries, and state-only patches.figaro.listreads that sidecar and a topology snapshot keyed byTrunks.Version(); it never opens IR or folds form history. Repair stale metadata explicitly rather than hiding a history scan in list. - Live forks stay live. Fork coordination enters through the figaro's existing inbox and is serviced between provider/tool stream events. Never kill, interrupt, or restart the addressed actor: its stable trunk ID is the continuation and it resumes appending after the fork.
- Completion metadata is incremental and singular. The actor folds new IR entries once and writes one complete
AriaMetasnapshot throughBackend.SetMeta. Do not add parallel derivation files or history scans at turn completion. - In-progress turn state is memory-only. The IR gets per-message appends (assistant at seal, tool-result tics at completion); interrupt/SIGTERM seals in-memory partial state as an interrupted turn (
sealTurn), and open-timerepairInterruptedTailappends honest interrupted tool results when the IR tail has unresolved tool calls. There is no turn journal. - Assistant IR commits before its native cache. Providers hand
PushFigarothe canonical assistant plus exact input-ready cache bytes/fingerprint. The actor appends IR, then appends+syncs the opaque cache best-effort (a cache failure degrades to re-encoding, never blocks the turn), then acknowledges.
internal/figaro/agent.go: drain loop, turn lifecycle, interrupt handling, panic recovery, in-progress-tic accumulation, set/outfit patch dispatch.internal/figaro/turn_seal.go: in-memory turn state, interrupt/drain sealing, interrupted tool results.internal/figaro/translator.go:synchronizeand its three sub-passes. The bidirectional sync between figStream and the translator stream.internal/figaro/inbox.go: selfish vs. patient semantics, routing subscriber.internal/angelus/protocol.go: supervisor JSON-RPC handlers, dormant-aria handling, lazy restore.internal/store/file_stream.go+mem_stream.go, append/condense/discard semantics. Easy to break.internal/provider/projection.go: shared O(delta) translator watermark, form state, and input-ready cache catch-up.internal/provider/anthropic/anthropic.go: direct HTTP+SSE, no SDK. SSE parser is hand-rolled.Sendassembles the request body internally;Assembleruns the SSE accumulator.Fingerprint()change invalidates the translator cache.github.com/jack-work/jkrpc(extracted frominternal/jsonrpc/): NDJSON framing. Don't switch frame formats without updating both ends.cmd/figaro/main.go: multi-call dispatch, daemon fork, signal handling. Theqandlsymlinks are part of the contract (provided byflake.nixpostInstall).
Take freely-reversible local actions. Pause and ask before:
Always:
- Changing any JSON-RPC method, notification, or wire payload. Frontends in any language are part of the contract.
- Changing the on-disk aria, form, or translator format. Old data must keep loading or migrate explicitly. XWAL stores parallel trees under
arias/{ir,form,translations}, plus_meta/<id>.json. - Anything that mutates the cache prefix mid-session. Invariant #5.
- Touching OAuth / hush flows. Tokens are users' real credentials.
- Adding a new runtime dependency, replace directive, or external service.
- Removing or renaming a CLI subcommand or flag.
- Anything that requires network egress the project doesn't already make.
Surface but don't block:
- Refactors that cross more than two packages.
- Changes to the actor model or items in § Invariants.
- New top-level files in the repo root.
- Changes to
agents.md,README.md,ARCHITECTURE.mdvoice. Facts are fine; tone deserves a sentence.
Just do (and mention in the summary):
- Bug fixes scoped to a single package.
- Tests, including new integration tests.
- Comment cleanup, dead-code removal you're certain is dead.
- Doc fixes that correct stale facts.
plans/*.md are intent documents: historical record once the work lands. Don't rewrite a plan to reflect what shipped; that's what the code and ARCHITECTURE.md are for.
duck/*.md is for rubber-duck brainstorms: captured thinking from design conversations, lower commitment than plans/. The most recent duck doc usually represents in-flight design thinking. Read it before architecturally significant work.
Maintained by agents, audited by the human. Update as the project changes; don't let it drift.
- Just edit for: package added/renamed, build commands changed, a new invariant from a bug fix, a hot-spot file is split, a stale pointer.
- Edit and mention for: changes to disclosure rules, conventions, or this file's structure.
- Propose first for: removing an invariant, narrowing the disclosure rules, replacing a section wholesale.
- Ask the user when uncertain whether a rule still holds.
Surface non-trivial edits in your end-of-turn summary so the user can audit. Compress hard-won knowledge; let it grow when the project teaches you something, prune it when a rule outlives its reason.