Skip to content

Latest commit

 

History

History
120 lines (92 loc) · 11.8 KB

agents.md

File metadata and controls

120 lines (92 loc) · 11.8 KB

agents.md

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.

Orientation

  • 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.

Build, run, test

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 plain go build in a worktree records no VCS revision, so the CLI/daemon build handshake can only warn instead of refusing. See the tmux-testing skill.
  • hush and largo are 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.

Conventions

  • 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 %w when 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 .jsonl event log via fanOut is 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.

Invariants

Load-bearing. Breaking them produces races, lost messages, or silent corruption.

  1. Single inbox per figaro. All events drain through one goroutine. New goroutines that touch agent state must report results through the inbox.
  2. Translator stream is a derivable cache. aria.jsonl is canonical. The translator can be regenerated by walking the figaro timeline through Provider.Encode. On Provider.Fingerprint() mismatch the agent clears it. Translator entries are not authoritative: re-derive from the figaro timeline if you need truth.
  3. synchronize owns translator orchestration. Both directions: catchUpFigaroDelta + condenseLive (translator → figaro) and catchUpTranslator (figaro → translator). startLLMStream just projects translator.Durable() and ships.
  4. 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-only stop_reason / model / usage stay on the Figaro IR message and out of the native cache.
  5. 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.
  6. PID binding is 1:1. Shell PID → at most one figaro. pid.bind / pid.unbind / pid.resolve go through the angelus.
  7. Panic recovery preserves identity. runWithRecovery resets the in-memory store and restarts the drain loop. ID, registry entry, PID bindings, socket, and credo survive: invisible to the model.
  8. Interrupt cuts the line. eventInterrupt is selfish; jumps ahead. Stragglers from cancelled goroutines are suppressed by a.interrupted. Keep them suppressed.
  9. Notification ordering is wire-ordered. Notifications come from the drain loop (or fanOut it owns), not arbitrary goroutines.
  10. Secrets never hit disk in plaintext. Tokens go through hush. Don't read or log credentials.
  11. One static binary. No new runtime deps (Node, Bun, Python). New tools, providers, frontends reach through the existing socket protocol.
  12. 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.
  13. 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.
  14. 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, a provider.RowSeq into the streamed body) and never materializes it. A []json.RawMessage of 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.
  15. Dormant listing is metadata-only. Agents persist complete AriaMeta at initialization, turn boundaries, and state-only patches. figaro.list reads that sidecar and a topology snapshot keyed by Trunks.Version(); it never opens IR or folds form history. Repair stale metadata explicitly rather than hiding a history scan in list.
  16. 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.
  17. Completion metadata is incremental and singular. The actor folds new IR entries once and writes one complete AriaMeta snapshot through Backend.SetMeta. Do not add parallel derivation files or history scans at turn completion.
  18. 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-time repairInterruptedTail appends honest interrupted tool results when the IR tail has unresolved tool calls. There is no turn journal.
  19. Assistant IR commits before its native cache. Providers hand PushFigaro the 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.

Hot spots

  • 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: synchronize and 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. Send assembles the request body internally; Assemble runs the SSE accumulator. Fingerprint() change invalidates the translator cache.
  • github.com/jack-work/jkrpc (extracted from internal/jsonrpc/): NDJSON framing. Don't switch frame formats without updating both ends.
  • cmd/figaro/main.go: multi-call dispatch, daemon fork, signal handling. The q and l symlinks are part of the contract (provided by flake.nix postInstall).

Disclosure rules

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.md voice. 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 and duck

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.

Living document

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.