AGENTS.md · git:20261003.be293bd · 2026-10-03 · sha256 d5ebac56bc82eebf

AGENTS.md git:20261003.be293bdA

Immutable. This exact content never changes and is served at /api/v1/blob/d5ebac56bc82eebf.

# AGENTS.md

Conventions for AI agents and contributors in this repository. Keep it
factual: paths, commands, invariants — no essays.

## Project layout

- `lib/` — the `flutter_agent_harness` package (pure Dart core). `test/`
  mirrors it. `prompts/` — all LLM prompts as Markdown (see rules below).
- `lib/src/approval/` — tool approval gate: tiers (read/write/exec),
  session modes (always-ask/write/yolo/autopilot — the CLI label of the
  `unattended` enum), per-tool overrides,
  critical-pattern
  `bash` interceptor, per-turn grants (`grantForTurn(allow:, deny:)` —
  skill `allowed-tools`/`disallowed-tools` manifest keys ride these for one
  turn; explicit deny > turnDeny > per-tool override > turnAllow >
  alwaysAllow > mode (critical bash outranks mode in always-ask/write/yolo,
  but is skipped in unattended so the mode never blocks — for runs with no
  user present); `clearTurnGrants()`
  on every new user
  message). Wired via `attachApproval` into `beforeToolCall` (runs
  first); prompt UI is an injectable `ApprovalPrompt` (null + prompt policy
  = deny).
- `lib/src/tools/ask_tool.dart` — `ask` tool: structured mid-turn questions
  via injectable `AskCallback` (null = error, cancel = plain result).
- `lib/src/tools/request_secret_tool.dart` — `request_secret` tool: the agent
  asks the USER for a missing credential via injectable
  `RequestSecretCallback` (never in chat text); a grant returns
  `RequestSecretResult` (host-adjusted `name`, `value`, `persisted` flag),
  a decline is a plain result. The app wires it in `AgentService` to
  `secretRequestHandler` (chat screen installs
  `ui/widgets/secret_request_sheet.dart`): a grant is persisted into
  `SessionKeysStore`, injected into the live shell env via
  `SecretsExecutionEnv.addSecrets` (the map is runtime-mutable now), and
  registered into the same `SecretRedactor` — so the next run's prompt name
  list, bash `$NAME`, and redaction all pick it up.
- `lib/src/env/session_vars_execution_env.dart` — `SessionVarsExecutionEnv`:
  an `ExecutionEnv` decorator injecting session-correlation env vars
  (`FAH_SESSION_ID`/`FAH_SESSION_FILE`/`FAH_PROVIDER`/`FAH_MODEL`, resolved
  live per `exec`, never secrets) into bash tool executions. Wired around
  `builtinTools` in the CLI (`AgentCli`) and the app (`AgentService`).
  `LocalShell` merges `ShellExecOptions.env` OVER `Platform.environment`
  (never replaces), so injected vars keep the inherited environment.
- `lib/src/env/io_execution_env.dart` — `LocalShell`: foreground `exec`
  runs under `setsid` when the host has it (probe
  `ownProcessGroupAvailable`, seam `ownProcessGroupOverride`) and the
  timeout/cancel paths reap the whole tree (`killTree` + direct-kill
  backstop) with a capped pipe-drain (`_drainGrace`) — timeout/cancel'd
  calls complete in ≤ timeout + kill grace + drain grace; a no-timeout
  call within `_drainGrace` of the direct child's exit (the drain is
  capped unconditionally: after the child is reaped every remaining pipe
  byte comes from an orphan). Killed calls are marked `timeout`/`aborted`
  and carry the captured partial output on `ExecutionError.stdout`/
  `stderr` — tail-capped at the source to the last 64 KiB (`_captureTail`)
  — rendered by the bash tool and the skill renderer's `!cmd` failure
  branch (every string that branch embeds is tail-capped to ~2 KB)
  (gh-1053; the timeout timer stays
  armed after the child exits because an orphaned descendant can hold
  the pipes — a drain that outlives the deadline marks the call
  `timedOut` even when the child exited 0). Background jobs (issue #517)
  keep their own group + `stop()` semantics.
- `lib/src/tools/checkpoint_tool.dart` — `checkpoint`/`rewind` tools:
  context hygiene for detours. `CheckpointRewindController` wraps
  `Agent.prepareNextTurn`, persists via host `CheckpointSessionSink`.
  Lifecycle (issue #286): a checkpoint is scoped to its detour — when the
  next `checkpoint` call finds it stale (a user turn arrived inside the
  detour, or the anchored span was rebuilt away by reload/compaction), it
  is auto-closed with a note + `checkpoint_auto_closed` session record
  instead of refusing; `rewind` semantics are unchanged.
- `lib/src/compaction/branch_summarization.dart` — `generateBranchSummary` +
  `navigateSessionTree` (use instead of `Session.moveTo` for tree
  navigation); summary is a `branch_summary` record on the entered branch.
- `lib/src/agent/tool_pairing.dart` — context pair-integrity (issue #85):
  `validateToolPairing` checks the wire-equivalent message sequence (orphan
  results, unanswered calls, duplicate ids, displaced results after
  same-role merges); `repairToolPairing` fixes the outbound payload only
  (drop orphan + note, synthesize interrupted results, hoist steering text
  after results, uniquify duplicate ids symmetrically on call+result).
  `agent_loop.dart` repairs before every request and retries once on a
  pairing-shaped provider 400 (`ToolPairingRepairEvent` is the audit
  trail); `_finishStreamed` stamps per-run position-counter ids
  session-unique; `auto_compactor.dart`'s `_localTrimFallback` routes the
  kept region through the same repairer.
- `lib/src/agent/image_registry.dart` — session image registry (issue
  #171): unique images ride a provider request exactly once; every other
  occurrence becomes a `[Image N]` text ref. `rewriteHistoryImages`
  rewrites the OUTBOUND payload only (session JSONL byte-identical);
  carriers `[{text:"[Image N]"},{image}]` anchor before the first
  referencing user message or after the tool-result run (never inside
  call/result pairs); the current user message rides images in place
  (I3), each in-place image carrying its `[Image N]` label (issue #195
  F3 — the model can cite what it sees); the per-request cap
  (`images.maxPerRequest`, default 20) drops current-first-then-newest
  with a drop notice (never silent; the app logs drops via `AppLog`,
  issue #195 F4); dangling refs (compaction, cap) resolve to `(image no
  longer available)`, and once a renumbering boundary exists (compaction
  summary, structured marker, local-trim note) AUTHORED history
  citations degrade to that note too — after renumbering an `[Image N]`
  may name a different image, so silent rebinding is never allowed
  (issue #195 F2; generated refs/labels stay content-keyed and fresh).
  Stateless per-request rebuild — determinism, compaction eviction and
  resume come free. Kill switch `images.registry: false` → byte-for-byte
  legacy shape. `agent_loop.dart` applies it in `_buildRequestContext`
  after `transformContext`, before tool-pairing repair.
- `lib/src/trajectory/` — the trajectory ledger core (issue #10): finalized
  session records project into an immutable `TrajectorySnapshot` through
  `TrajectorySnapshotBuilder.append`/`applyEvent` — streamed agent events
  land as synthetic rows keyed `turn\0step` (`u\0turn` for prompts) and are
  replaced when the real record lands; revision grows per append. Record
  kinds (closed set, `TrajectoryCellKind`): system, user, context,
  compacted, message, tool, subtool — a subtool is a
  `TrajectoryToolRecord.parentCallId != null`, which only hosts build
  directly (the builder never nests). Derived state folds once per
  snapshot: `deriveTrajectoryLayout` (turns of Message/Step groups +
  collapsible turn/assistant-run summaries), `deriveTrajectoryTimeline`
  (four modes — `sequence` equal-width slots, `duration` with idle gaps
  compressed out, `time` zero-width wall-clock markers, `actual` real gaps;
  three fixed lanes: 0 system/user/context, 1 message/compacted, 2 tools),
  `trajectoryTimelineFocusIndexes` (selection → visible ledger indexes),
  and `ThrottledTrajectorySearchIndex` (full-text; matches are stable
  record ids). `formatters.dart` holds the shared duration/token/throughput
  formatting; `trajectory_preview.dart` the bounded single-line previews;
  `trajectory_export.dart` the whole-snapshot JSON/Markdown exporters the
  header's copy menu uses. Tool records carry `startedAt` (real launch
  time, distinct from the projected position) so the Gantt/actual
  projections plot true spans. Outbound model requests persist for the
  ledger as hidden `model_request_summary` `CustomRecord`s — the CLI
  event handler (`agent_event_handler.dart`, emitted through
  `agent_cli.dart`) and the app's `AgentService` both buffer a
  `TrajectoryRequestDetail` per call and flush it onto the session
  record chain right before the assistant message, so replays see it
  ahead of the reply — and `TrajectorySnapshotBuilder` folds them into
  the message record's request detail (the Request tab).
  Consumers render, never re-derive: `packages/fa_ui`'s
  `lib/src/trajectory/` widgets and the CLI `/trajectory` family.
- `lib/src/cli/trajectory_commands.dart` (a `part of` `agent_cli.dart`) +
  `lib/src/cli/trajectory_tui.dart` — the read-only REPL family
  `/trajectory [view|cost|tail|inspect <n>]` (bare = `view`, capped at 200
  rows; `tail` follows the live session every 500ms) and the headless
  `fa trajectory <view|tail|cost|inspect> [sessionId] [--json] [--at N]`
  subcommand (`--at N` replays the snapshot as it stood at record N, view
  only). Session resolution: exact id > session-name match > most recent.
  Rendering is pure Dart in `trajectory_tui.dart` (no `dart:io`), shared by
  both entries; `bin/fah.dart` routes the subcommand.
- `lib/src/redact/` — layered secret redaction (issue #24): ten pure layer
  functions (registered/path/vendor/prefix/pem/asn1/connection/context/
  entropy/pii) merged priority-first into `RedactionPipeline.scan/redact`
  with `[REDACTED:<kind>]` idempotent markers, allowlist + data-URL
  pass-through, live-mutable `RedactionConfig`, and `RedactionStats`
  (per layer + per tool). `redaction_hooks.dart` attaches it to the agent:
  afterToolCall masks tool results BEFORE session persist, transformContext
  masks the outgoing context, beforeToolCall denies credential-file
  read/bash in `blockMode`, `redactPrompt` masks user input — write-side
  tools (`write`/`edit`/`checkpoint`/MCP write-ish) are never filtered
  (AC7). Config: `redact:` in `~/.fah/config.yaml` or project
  `.fah/config.yaml` (tolerant parse). CLI `/redact`; docs/redaction.md.
- `lib/src/hashline/` — hashline patch language: `[path#TAG]` headers
  (4-hex xxHash32 of whole file), `SWAP`/`DEL`/`INS.*` ops, all-or-nothing
  `HashlinePatcher`, stale tags reject before any write. Wired in
  `builtin_tools.dart`: `edit` takes `patch`, `read` takes `hashline` flag.
- `lib/src/tools/read_selector.dart` — `read` trailing selectors:
  `:N`/`:A-B`/`:A+C` (`..` alias), comma multi-ranges, `:raw`. A literal
  file named `x:1-2` wins over the selector. `offset`/`limit` must not be
  combined with a selector.
- `lib/src/tools/archive_reader.dart` — `read` inside archives
  (`a.zip:inner/entry`, `.tar`, `.tgz`), 256 MiB cap.
- `lib/src/tools/sqlite/` — `read` SQLite targets (`db.sqlite`,
  `:table[:key][?params]`, `?q=SELECT`). FFI engine (`package:sqlite3`)
  exported only from `lib/io.dart`, passed via `builtinTools(env, sqlite:)`;
  without it a clean "not supported" note.
- `lib/src/tools/shell_jobs.dart` — background shell jobs:
  `ShellJobRegistry` over the `BackgroundShell` capability
  (`execution_env.dart`; `LocalShell`/`LocalExecutionEnv` implement it,
  `SessionVarsExecutionEnv`/`SecretsExecutionEnv`/`MemoryExecutionEnv` and
  the app's `ProjectMountEnv`/`SandboxedExecutionEnv`/
  `PersistentWebExecutionEnv` forward it — the desktop app runs the host
  shell; mobile's `WasiSandboxShell` and web's `MemoryShell` run jobs as
  detached script Futures on job-local interpreter clones via
  `flutter_app/lib/sandbox/shell_job.dart` — own cwd/vars/capture, shared
  fs). `bash background: true` runs detached with output
  streaming into `.fah/bash_jobs/<id>.log`; `bash_job {status|output|stop}`
  (write tier) manages them. A settle fires `onSettled` on every exit —
  the CLI's terminal bookkeeping (`_onShellJobSettled`: job-board Running
  count, waiting row) hangs off it (issue #562). The model-facing notice
  (steered mid-run or a fresh idle turn; app `sendText` system-notice)
  is skipped when a foreground call consumed the result inline
  (`suppressSettleNotification`). Job logs on disk are NOT secret-redacted.
- `lib/src/tools/availability.dart` + `availability_gate.dart` —
  capability-gated tool availability (issue #19): `resolveToolAvailability`
  merges the `tools:` scope stack (global `~/.fah/config.yaml` < project
  `.fah/config.yaml` < session `.tools/<sessionId>.yaml` < runtime
  `--tools`/`FA_TOOLS`; deepest scope wins per key) over the host's hard
  capability floor — absent tools can never be force-enabled, unknown ids
  warn. The gate applies a resolution to the live registry idempotently
  (hide/restore + prompt rebuild) and tombstones calls to disabled tools.
  CLI surface in `lib/src/cli/agent_cli_tools.dart` (`/tools`, live
  rebuild); docs/tool-availability.md is the user-facing page.
- `lib/src/tools/load_modes.dart` + `discover_tools_tool.dart` — agent
  load modes (issue #680): `--omp` > `FA_AGENT_MODE` > `agent.mode`
  (`default|pi|omp`) pick the tool-LOAD preset — `default` loads every
  enabled tool; `pi` the 4-tool benchmark base (issue #679, discovery
  off); `omp` the essential base with everything else discoverable,
  mounted on demand through the omp-only `discover_tools` meta tool (an
  explicit `tools: {id: on}` is a standing mount). The gate's
  `discoveryEnabled` flag keeps the discoverable tombstone from
  dead-ending on `discover_tools` in modes that don't register it. CLI
  sync + `/settings` switch in `agent_cli_tools.dart`/`settings_flow.dart`;
  user-facing docs: docs/tool-availability.md §Load modes.
- `lib/src/lsp/` — `lsp` tool (diagnostics/definition/references/rename):
  pure-Dart JSON-RPC client over `LspTransport`; `.dart` →
  `dart language-server --protocol=lsp`, projects merge `.fah/lsp.json`;
  lazy start, 5-min idle shutdown, crash respawn with backoff; renames apply
  all-or-nothing through the env. IO transport only via `lib/io.dart` +
  `builtinTools(env, lsp:)`; 1-indexed line/character; missing server =
  clean note, never a crash.
- `lib/src/mcp/` — MCP (Model Context Protocol) servers from the `mcp:`
  section of `~/.fah/config.yaml` (strict `ConfigException` parsing; config
  file only, no CLI commands). Stdio (`command`/`args`/`env`,
  newline-delimited JSON-RPC — NOT LSP's Content-Length framing) and remote
  (`url`, streamable-http default or legacy sse, `headers`). Message-level
  `McpTransport`; stdio framing glue is pure Dart over `McpByteChannel`
  (process impl only via `lib/io.dart`), both HTTP transports are pure Dart
  over injectable `package:http` (web gets remote servers; stdio = clean
  "not supported" status). `McpManager` connects lazily in the background
  (boot never blocks), per-server connecting/connected/failed status,
  reconnect with capped backoff; tools register as `mcp__<server>__<tool>`
  (exec approval tier, description prefixed with the origin, inputSchema
  verbatim) through the manager's `onChanged` (AgentCli re-registers +
  rebuilds the prompt's tiny MCP section). Results map MCP content blocks
  onto ours (text as-is, images as ImageContent, resources/links as text
  placeholders) under a shared 100k-char budget; `isError` throws so the
  loop records an error result; timeouts name `mcp.toolCallTimeoutMs`.
  Wired via `builtinTools(env, mcp:)`; resources/prompts out of scope.
- `lib/src/model_roles/` — model roles (`default`/`smol`/`slow`/`plan`) with
  fallback chains, key rotation (`ApiKeyRing` over `NAME`/`NAME_2`/…), 429
  mid-turn take-over (`FallbackStreamFunction`, never silent). Provider
  streams carry watchdogs (`provider_common.dart`:
  `providerConnectTimeout` 180s for the request, `providerStreamIdleTimeout`
  5min between bytes — overridable via the `providerTimeouts:` config
  section, `connectTimeoutMs`/`streamIdleTimeoutMs`, published to
  `providerTimeoutsOverride` at startup) so a wedged endpoint errors out — and the resolver
  can fail over — instead of hanging the turn forever. Streaming adapters
  share ONE keep-alive `http.Client` (`sharedProviderHttpClient`) instead of
  a fresh client per call: per-turn TCP churn piled TIME_WAIT sockets into
  connect stalls. Config:
  `roles:`/`modelOverrides:`/`retry:` in `~/.fah/config.yaml` (invalid
  schema = `ConfigException`). Also the shared models config:
  `media_model_slots.dart` (media slot names/fields shared with the app's
  `MediaModelsStore` + strict yaml slot entry) and `models_config.dart`
  (the `models:` section — per-slot media overrides + named custom model
  definitions `/model <name>` resolves; mutable like the custom-provider
  registry, persisted by the host). Owner context override (issues
  #273/#729): `agent:`/`contextWindowCap` in `~/.fah/config.yaml` (strict
  section, minimum 16384 = the compaction reserve) SETS the EFFECTIVE
  context window via `effectiveContextWindow` (model.dart) for the
  compaction thresholds, the CLI ctx meter/footer and the loop's over-window
  guard — clamped down below the catalog window; raised above it to the
  served truth (a custom provider whose catalog entry under-reports, e.g. a
  glm endpoint serving ~1M under a 200k catalog id). Uncapped runs are
  byte-identical. Threaded: CliConfig → AgentCliConfig (bin/fah.dart) →
  Agent → AgentLoopConfig. Compaction payloads (#729): every summarization
  request is bounded to `summarizationPayloadBudget(summarizerWindow)` —
  chunked summarization (chunk → `<previous-checkpoint>`/`<folded-checkpoint>`
  fold) when the dropped region exceeds it, explicit truncation note for a
  single oversized message; the smol summarizer's own window governs (never
  the main model's); overflow-classified compaction failures route straight
  to the next summarizer/local trim with zero backoff retries.
- `lib/src/ttsr/` — time-traveling stream rules: regex matched against
  streaming deltas; on match abort, inject rule bodies as hidden
  `<system-interrupt>` message, retry after 50ms. Persisted via
  `TtsrSessionSink`; guards: once-per-session, `maxInjectionsPerTurn`.
  Config: `ttsr:` in `~/.fah/config.yaml` + project `.fah/rules.yaml`.
- `lib/src/skills/skills.dart` — agent skills: `<root>/<name>/SKILL.md` plus
  Claude/Copilot/Codex layouts (`.claude/skills` + `.claude/commands`,
  `.github/skills`, `.codex/skills`, user-level equivalents incl.
  `~/.copilot/skills`), each root tagged with a `SkillSource`; project >
  user, first-name-wins. Third-party roots are discovered BY DEFAULT
  (opt-out): `skills_access.dart` `SkillsAccess` ask/granted/denied with
  `granted` as the zero-config default — `allowedSources` on
  `discoverSkills`/`discoverTaskAgents`; CLI `skills:` config section,
  the TUI `/skills` management menu + `/skills access`, startup dialog
  only for an explicit `ask`; app `SkillsAccessStore` + settings/boot
  dialog). Typed frontmatter lives in
  `skill_manifest.dart` (`SkillManifest` — allowed-tools, context: fork,
  paths, user/model-invocable …; unknown keys = notes). Only metadata
  enters the system prompt (path-gated `paths:` skills appear once a
  matching file was touched); bodies loaded with `read` or rendered for
  invocation by `skill_renderer.dart` (`$ARGUMENTS`/`$N`/`${CLAUDE_*}`,
  `` !`cmd` `` shell injections through the env, `allowed-tools` →
  per-turn approval grants, `context: fork` → the task tool). Migration
  guide: `docs/migrating-from-claude-copilot-codex.md`.
- `lib/src/prompts/project_context.dart` — `AGENTS.md`/`CLAUDE.md`/`GEMINI.md`/
  `GOAL.md`/`DESIGN.md` plus per-directory GitHub Copilot files
  (`.github/copilot-instructions.md` and `.github/instructions/
  *.instructions.md` — `applyTo` globs not evaluated, scoped files get an
  `<!-- applies to: ... -->` marker, `excludeAgent` ignored) auto-merged into
  the system prompt: cwd → git root, farthest-first, `<!-- From: -->`
  annotations, 32 KiB leaf-first budget; optional `~/.fah/AGENTS.md` first.
  CLI: `/skill:<name> [args]`, the `/<name>` alias, `/skills` (TUI: a
  management menu — picking a skill prefills `/skill:<name> ` into the
  composer via `FaTuiController.sendInputText`, plus access/import rows;
  line mode: plain list) with `[reload|access [ask|granted|denied]|import]`
  subcommands (bare `/skills access` opens an interactive picker, run
  detached from the sequential line REPL like the guided provider flows).
- `lib/src/task/` — `task` tool: parallel subagents, batch form
  `{context, tasks[]}` + `background` flag; children never get `task` (no
  nesting); roles: `explore`→`smol`, `review`→`slow`, `plan`→`plan` — a
  role resolves through the roles resolver only when explicitly pinned
  (`ModelRolesConfig.pinsRole`); an unpinned role keeps the parent's live
  wiring, so the child inherits the parent's runtime-resolved output cap
  instead of the default chain's rebuilt catalog default (issue #302);
  `outputSchema` with
  ONE fix retry; child failure = per-item error, never batch failure.
  Agent types: built-ins (`task`/`explore`/`review`/`plan`) plus discovered
  `.md`/`.agent.md` files (`agent_discovery.dart` — roots `.fah/agents`,
  `.agents/agents`, `.claude/agents`, `.github/agents`, `.codex/agents` +
  user-level, gated by the skills access setting; `canonicalTaskAgentName` folds
  Claude/Copilot names like `general-purpose`/`Explore`/`Reviewer` onto the
  built-ins).
  `/tasks` lists jobs (background agents AND background shell jobs),
  `/tasks cancel <id>` routes by id; completions re-enter the parent
  as async-result messages (`agent://<id>` refs). Subagents are retained &
  addressable: real JSONL child sessions, `task_status`/`task_observe`/
  `task_send`/`task_cancel` (model-facing job abort), child-only `reply` +
  sibling `agent_message` (pending-queue + hop-capped),
  `completed_without_reply` notice. Child IDENTITY for tool calls is
  zone-scoped (`subagent_scope.dart`, gh-970): `runSpawn`/`resumeChild`
  publish the running child's id via `runWithSubagentScope`, and
  `TaskExecutor.currentSubagentId` reads the zone FIRST — the executor's
  in-flight id stack is shared by all concurrent children, so its head is
  "started last", not "executing" (that shift mislabeled every sender
  envelope and recorded replies on sibling handles). Hosts also resolve
  shared tools' self-mailbox semantics through the zone (`activeSubagentId`
  → `schedule_message`'s `senderMailbox`), and the CLI wires
  `SubagentManager.wakeChild` so mail that fires into a finished child's
  inbox (a self-scheduled reminder, sibling mail) resumes it in its own
  session — the child-side analog of the idle inbox wake (the sweep is
  `wakeChildrenWithPendingMail`, status-gated to completed/idle, one wake
  per child per settle, a throwing wake retires the child).
- Steer soft-yield: a steering message (user `Ctrl+S`, subagent completion,
  inbox mail) arriving DURING a tool-call phase cancels the phase's
  zone-scoped yield token — `currentYieldToken()` in `cancel_token.dart`,
  published by `_runToolCallPhase` (`agent_loop.dart`), fed by
  `AgentLoopConfig.steeringNotifications` (in-process queue) and the
  non-draining `hasPendingSteering` probe (external inbox, 2s poll).
  Yield-aware tools (`bash`, blocking `task`) finish the tool call early
  WITHOUT stopping the work — it continues as a background job and the user
  message is delivered at the step boundary. Tools ignoring the token keep
  the classic behavior (message waits for the phase).
- `lib/src/messaging/` — the agent messaging fabric: every agent (main,
  subagents, other Fa instances sharing the root) owns a file inbox behind
  the isolated `MessagingRepository` interface (send/peek/drain/directory —
  future DB/network impls drop in without caller changes).
  `AgentMessage.kind` distinguishes agent chat from `user` input handed
  over by an attached client (the Fa app's attach view; the CLI delivers
  it as `[from app] …`, its own words, not agent chat).
  `FileMessagingRepository` over `ExecutionEnv`:
  `<root>/<agent>/inbox|read/<id>.json`, timestamp-ordered names, sanitized
  agent dirs, torn writes skipped. `SubagentManager.messaging` routes
  `enqueueMessage`/`drainMessages` through the fabric (in-memory queue is
  the no-fabric fallback); `mailboxPrefix` (the session id, set by the host
  on every session init/switch) namespaces mailboxes so two instances never
  drain each other — ids with `/` are absolute cross-instance addresses
  (`<sessionId>/main`). Sessions also carry their display NAME
  (`--session goal_builder`): `register(agentId, {sessionName})` persists
  it in a `.name` marker + messages-registry.json, `agent_directory`
  renders `name (id)`, and `agent_message` resolves `goal_builder` /
  `goal_builder/main` through the directory (exact ids/siblings/`main`
  pass through untouched; ambiguous names error with candidates).
  `agent_message` targets siblings, `main`, a session name, or an
  absolute mailbox; `agent_directory` lists LIVE fabric mailboxes (recent
  activity via the heartbeat hosts touch on their inbox-watch timers) plus
  anything with pending mail — stale mailboxes from finished sessions are
  hidden behind the tool's `all: true` (`.id` markers keep real ids despite
  dir sanitization; `_scheduled`/dot-dirs are not mailboxes).
  `schedule_message` (issue #59): records under `<messagesRoot>/_scheduled/`;
  self-addressed records deliver to the host's LIVE mailbox at fire time
  (the address pinned at schedule time goes stale on session switch/restart
  and would strand the reminder) — EXCEPT self-addressed records naming a
  subagent mailbox of this session (`<prefix>/<childId>`, gh-970): those
  deliver as recorded (the address rides the same owner prefix and is as
  stable; re-addressing stole the child's reminder into main), and the
  tool's `senderMailbox` override (host-wired from `activeSubagentId()`)
  makes a subagent's no-`to` schedule default to the CHILD's mailbox, not
  main's — hosts re-arm on every mailbox change
  (CLI `_syncMailboxPrefix`, app `_setMailboxPrefix`) and sweep due records
  on their inbox ticks; `dispose()` cancels only the timer, the files stay.
  Sleep resilience (issue #259): all due math rides an injectable wall
  clock (`ScheduledMessageQueue(clock:)`), long waits are split into ≤60s
  timer legs (`maxTimerLeg`) that recompute the remaining delay from the
  wall clock on every fire — never accumulating duration drift — and
  hosts run a catch-up sweep at every TURN START (CLI `_beginUserPrompt`,
  app `sendText`) in addition to the idle inbox ticks, so the first
  post-sleep turn delivers every overdue record immediately. Catch-up is
  exactly-once per record (file removed on send + the `_delivering`
  in-flight guard) — missed cycles of a recurring self-re-arm are NOT
  replayed; the re-armed cycle simply resumes from "now". For true
  sleep-proofing (delivery DURING lid sleep) no in-process timer can
  help: add an external pinger, e.g. a cron/launchd entry that messages
  the session's mailbox — mail to an asleep session already launches a
  headless wake run that drains the inbox and sweeps due records.
 Failure isolation (issue #270): a throwing `send` is contained per
 record (logged via `onError` — CLI `[sched]` line, app `AppLog`), the
 record stays for the next sweep, and the post-failure re-arm floors the
 leg at `failureBackoff` (60s) so a poison record cannot spin a
 zero-delay timer; sweeps deliver in due-time order, and the app's
 turn-start sweep is awaited so the fresh turn sees the fired reminder.
 Records carry the scheduling instance's `owner` (mailbox prefix): a
 sweeper re-addresses a self-addressed record only when the stored owner
 matches its own prefix, and never deletes another instance's record -
 foreign-owned records are left where their owner sweeps (also across a
 root change: only own records migrate to an adopted folder's root).
 Legacy untagged records drain via the old any-sweeper path; records
 whose owner session is deleted (`/reset` mints a new id) stay on disk -
 there is no reaper, cleanup is manual. `agent_fabric` resolves the
 messages root lazily per sweep (was: pinned to the launch cwd at
 construction), and pending records migrate across a root change, so a
 session-folder adoption carries the reminders along instead of
 stranding them under the launch cwd.


  The `## Agent messaging` prompt section (prompts messaging_section.md, CLI +
  app variants) tells the model its own mailbox address. Turn-boundary
  delivery: `Agent.externalSteeringSource`
  merges inbox drains into the steering poll (main in AgentCli/AgentService
  as `_mainInboxMessages`, children via the executor) — messages land as
  sender-attributed user messages, so they persist in the session and read
  like a chat. Idle wake: an inbox watcher (2s CLI / 3s app) starts a turn
  when mail arrives while idle — two Fa instances chat live; a 10-run
  streak cap without user input breaks ping-pong loops.
  UI: `/agents` rows show a `mail:N` pending marker (the CLI
  font has no ✉ glyph), the app's AgentsSection shows `✉N`; observe/detail
  views list the pending inbox.
  Issue #27 adds the hub-first composition: with `DAP_MASTER_SECRET` set
  and the hub plugin enabled, `bin/fah.dart` wraps the hub as
  `HubFabricRepository` (bin/, IO-bound) and routes the fabric through
  `FallbackMessagingRepository` (lib) — hub-first for hub-resolvable
  targets, file inboxes as offline fallback, queued mail forwarded on
  reconnect, merged drains deduped by id. `AgentCliConfig.hubFabric`
  hosts inject it; the plugin host skips its separate inbox when the
  fabric owns delivery (one hub-mail consumer). Subagent drains never
  touch the hub. Issue #304 adds the one-step CLI surface over it:
  `fa dap start|stop|status` (bin/fah_dap_command.dart —
  `DapHubController`, every IO a seam) probes the port (`/healthz` + a
  `/ws` presence handshake; foreign server = clear error naming the
  port, no enrollment), spawns `fa hub serve` detached, prompts the
  master key ONCE (hidden; empty = open hub, the zero-config extension
  default; non-interactive starts prompt nothing), enrolls per the
  DAP_MASTER_SECRET flow and persists only the hub-issued clientSecret
  (0600 `~/.dap/config.json`; wrong key re-prompts 3x then a manual
  hint — a rejected secret is never persisted). The hub pid/state file
  `~/.dap/hub.pid` (parse in lib/src/hub/dap_local_hub_state.dart) lets
  a second CLI attach (no double-spawn) and `fa dap stop` work from ANY
  instance exactly once — stop names connected peers (e.g. "Browser")
  and SIGTERMs the owning pid; the fabric falls back to file inboxes.
  The `/dap` menu's leading row is state-dependent (AC7): "Start DAP
  locally (one step)" stopped, "Stop DAP" running
  (lib/src/cli/dap_menu_options.dart `dapMenuOptions(hubRunning:)`).
  `agent_directory` renders hub roster entries (MailboxEntry.source ==
  'hub') with a `[hub]` marker; the `fabric.hub: false` kill switch
  (FabricConfig.hub, gate `hubFabricWired` in bin/hub_fabric_
  repository.dart, mirrors images.registry) leaves the fabric the bare
  file layer — byte-identical legacy listing (REG test
  test/messaging/fabric_kill_switch_reg_test.dart).
  Issue #27 phase 3 adds the A2A boundary gateway: `agent_message` accepts
  `name@machine` for OTHER machines — `A2aMailGateway` (lib/src/a2a/
  a2a_mail_gateway.dart) resolves the machine against the `a2a:` config
  section (the server named like the machine) and sends the mail as an A2A
  `message/send` with a `faMail` metadata envelope (id/from/to/text/sentAt/
  hops, sender address stamped `mailbox@machine` for replies); a failed
  remote task errors instead of dead-dropping. Inbound: `fa serve --a2a`
  mounts a mail sink — envelope-carrying sends deposit into the project's
  file inboxes (resolved by mailbox id or session display name) instead of
  running a turn; endpoints without a sink fail such sends honestly. The
  hub stays the intra-machine transport; A2A stays the interop boundary.
- `lib/src/session/attach/` — attached-session infrastructure (the Fa app
  watching a live `fa` CLI session 1:1 and handing it input): three
  INTERFACES so a later network impl (`fa serve --attach`, remote headless)
  drops in without touching consumers — `SessionPresenceStore`
  (register/touch/unregister/list; stale heartbeats auto-drop, crash
  coverage), `SessionEventSource.watch` (transcript tail as
  `AttachedMessage` rows: user/assistant/tool), `SessionInputChannel.send`
  (user input to the owning process). File impls:
  `FileSessionPresenceStore` (`<sessionsRoot>/.presence/<id>.json`,
  15s staleness) and `FileSessionEventSource` (JSONL tail via
  `readTextLines` deltas) + `FileSessionInputChannel` (fabric mail,
  `AgentMessageKind.user`, to `<sessionId>/main`). CLI wiring: `run()`
  registers presence, the 2s inbox timer touches it every other tick,
  `finally` unregisters; `AgentCliConfig.presenceStore` is injectable
  (bin/fah.dart builds the file store; tests inject fakes). CLI sessions
  carry `agent: cli` metadata (the app writes `fa`). App wiring:
  `CliSessionPresence` service (3s poll) + green-dot `live` on
  `SessionTile`; a live tap in the chat-sheet drawer opens
  `AttachedSessionScreen` (controller `attached_session_controller.dart`)
  instead of a second writer on the same JSONL.
- `lib/src/memory/` — long-term memory: `MemoryController` over the
  `flutter_agent_memory` package (hosted pub.dev dep),
  `execution_env_kb_storage` (KbStorage → ExecutionEnv), `memory_add`/
  `memory_search`/`memory_list` tools (`memoryTools(controller, onChanged:)`),
  and the `<memory>` prompt section: `formatPromptSection()` (BOTH scopes,
  entities parsed via `KBFileParser` — the raw file's first line is the
  `---` delimiter, not the fact) is cached by the host and recomposed into
  the system prompt asynchronously after startup + on every `memory_add`
  (CLI `_refreshMemorySection` in agent_cli.dart, app in AgentService);
  phase 2:
  `compaction_memory_hook` (compaction-time durable-fact extraction on the
  smol role, non-blocking), `maintain()` (levels + consolidate, running
  guard, 24h stamp), triggers on session start + `/memory maintain`;
  `/memory` shows stats.
- Git-backed memory (the repo IS the memory): storage paths come from the
  `memory:` config section — project `.fah/config.yaml` wins over
  `~/.fah/config.yaml` (`loadProjectMemoryConfig`); a relative
  `projectPath` resolves against the project root. This repo dogfoods it:
  `.fah/config.yaml` points at the COMMITTED `memory/` directory — clone
  the repo, get its memory. Inside the store dir: `MemoryRepoInit.
  ensureGitSupport()` (flutter_agent_memory 0.2.0) keeps `.gitignore`
  (derived artifacts: GRAPH.md, MEMORY.revision, INDEX.md,
  .last_maintenance — rebuilt on load, never committed) and
  `.gitattributes` (`DELETIONS.md merge=union` — the append-only
  tombstone ledger merges by union). Note ids are merge-friendly since
  0.2.0: `n_0447_a1b2` = sequential index + 4-hex md5 of the normalized
  text (parallel branches union-merge; legacy `n_0001` ids stay valid
  forever). The `memory_add` description IS the package's
  `MemoryPolicy.memoryAddPolicy` (docs/memory/memory_add_policy.md in
  the memory repo): durable facts only, solved problems are superseded
  via delete+add (never left to rot), project scope is PUBLIC (git) —
  no secrets/personal data there, user scope stays machine-local.
  `.gitignore` whitelists `.fah/config.yaml` (+rules/lsp/mcp/agents/
  skills/packages); logs/sessions/bash_jobs stay local. Commit memory/
  changes with the task that produced them (`memory:` prefix for
  memory-only commits).
- `lib/src/a2a/` — A2A (Agent2Agent) interop: `a2a_client.dart` (Agent Card
  + JSON-RPC + SSE), `a2a_config.dart` (`a2a:` yaml section, `${NAME}`
  env tokens), `a2a_manager.dart` (lazy per-server connect, A2A task state
  → subagent lifecycle mapping), `a2a_server.dart` (request handler).
  `task` tool agent type `a2a:<name>` runs remote agents as subagents
  (uniform `task_status`/`task_send`); `/a2a` shows server status;
  `fa serve --a2a [--port N] [--token T]` mounts this agent as an endpoint
  (`bin/serve_a2a.dart`).
- `lib/src/cli/hep.dart` + `agent_cli_hep_io.dart` — HEP v1, the Harness
  Event Protocol (`fa --output events[=full]`, issue #155): strict JSONL
  on stdout for server-side supervisors, one JSON object per line,
  flushed per line. A TURN is one assistant LLM ROUND, not one user
  message — a user turn that runs tools yields SEVERAL terminal frames
  (`turn_done` per round, ids incrementing). `HepWriter` is
  `AgentListener`-shaped (`agent.subscribe(hep.handleEvent)`); the
  `HepEventsIO` decorator (a `part of agent_cli.dart`) drops
  `CliIO.write` so stdout stays pure while diagnostics keep stderr.
  Frame catalog, ordering/flush guarantees and versioning policy:
  docs/hep.md; byte shape pinned by golden tests (test/cli/hep_test.dart).
  `--attach` sniffs image mimes by magic bytes (png/jpeg/gif/webp ride
  as base64 blocks); any other file passes through as an
  `[attached file: …]` path reference appended to the prompt (issue
  #196) — never an octet-stream image block.
- `bin/fah.dart` — the `fah`/`fa` CLI. REPL (no args) or headless
  (`fa "prompt"` / `-p` / `--prompt-file <path>` (alias `-f`), mutually
  exclusive). First positional naming an EXISTING file is the prompt source
  (`.md`/`.txt` inlined, others attached by reference; `-p` is verbatim;
  `--prompt-file` reads ANY path verbatim, missing/unreadable = usage
  error exit 64). Args parsed in `lib/src/cli/cli_args.dart`
  (pure Dart). Headless: exit 0/1/130; `CliIO` contract — `write` = primary
  stream, `writeln` = diagnostics (stderr headless). The TUI captures the
  mouse by default (wheel scrolling + click routing — the alternate screen
  has no native scrollback): SGR 1002+1006 with a per-frame hit-region
  registry (`tui_hit_regions.dart`, topmost-region click/drag/release
  routing in `fa_tui_mouse.dart` — composer caret on mousedown, model
  picker rows, queue-row drop); legacy X10 payloads (modes 9/1000) decode
  too; `/mouse off` degrades to keyboard with a one-time hint. Selection
  works via the terminal's bypass modifier (Shift), and `FA_TUI_MOUSE=0`
  (`AgentCliConfig.tuiMouseCapture`) hands the mouse back for always-on
  native select-to-copy.
  Shift+Enter's HID poll is resolved once at startup
  (`lib/src/cli/shift_hid.dart`, issue #355): disabled when
  `SSH_CONNECTION`/`SSH_TTY` is set, under `FA_TUI_SHIFT_HID=0`, or when
  the one-shot CoreGraphics probe (sacrificial isolate, ~300 ms timeout)
  hangs — the returned callback is null and a bare CR stays a submit.
- `lib/src/cli/` — REPL machinery: `/provider [name] [baseUrl] [token] |
  custom` (guided wizard in `provider_flow.dart` + `provider_commands.dart`;
  `/models` fetched for openai-like endpoints), custom providers in the
  `customProviders:` section of `~/.fah/config.yaml`. Editing a saved
  OAuth-connected entry (OpenRouter) offers the connect-time sign-in choice
  at the key step — keep / Browser OAuth re-auth / paste a key
  (`CustomProviderFlowConfig.reauth`). The same picker appears for
  CodeMie entries in edit mode (matched by the `/code-assistant-api/`
  URL marker — works for legacy entries whose `authMethod` was never
  recorded): `Browser SSO (CodeMie)` (`_mintCodeMieSsoCookie`); the
  wizard applies the minted value through format-aware switch routing
  (`isCodeMieJwtToken` decides SSO-cookie vs JWT-Bearer) so the cookie
  stays a `cookie:` header (not Bearer) and the JWT keeps its Bearer
  slot. Every add/connect flow that saves a
  registry entry offers a provider-name step (`_askConnectProviderName`:
  typed value, else the endpoint host; a clash with a DIFFERENT endpoint
  retries, and so does a name matching a CATALOG provider — `/provider
  kimi` routes to the catalog before the registry lookup, so such an
  entry would be unreachable) — the custom wizard, DIAL (custom names
  scope the store key; the switch stores under the entry's slot via
  `tokenKeyName`), Kimi (a typed key becomes a named
  entry with a name-scoped key; a resolved key offers "use it / add a
  second account"), OpenRouter OAuth + API-key, ChatGPT OAuth (lands a
  registry entry too, `apiType: chatgpt`), CodeMie SSO/JWT (the
  SSO name step only from the explicit add flows: `offerName` — automatic
  re-authorizations on startup/switch/mid-stream expiry stay silent; a
  NEW account name re-runs the guided project → model pick, a re-login
  to the same entry keeps its model).
  Saved-entry lookups are by base URL (`_entryForBaseUrl`), so a renamed
  entry is not duplicated on re-connect. Catalog switches RESET
  `_activeCustomName` (kimi/openrouter stored-key branches included) —
  a leaked marker made `/model` rewrite the previous custom entry's
  model memory. `/models` also manages
  the `models:` section: `/models config`/`set <slot> <model> [baseUrl]`/
  `remove <slot>` for media slot overrides (persisted via
  `onModelsConfigChanged`), and `/model <name>` resolves `models.custom`
  definitions. Keys: env first, then
  secure store (`lib/src/secrets/secure_key_store*.dart` — Keychain /
  Secret Service / PasswordVault, IO backends only in `lib/io.dart`),
  preloaded into `SecureKeyCache`. Resolution: the spec's DEFAULT endpoint
  gets env → `FA_KEY_<HOST>` → `FA_KEY_<HOST>_<NAME>` → legacy; any other
  endpoint resolves ONLY its scoped store keys (env names never hijack a
  custom endpoint); `/key set` writes store-only, never config.
  `/settings` is the interactive settings hub: a TUI picker whose entries
  (Provider, Edit/delete provider, Chat model, Model parameters, Media
  models, Agent models, Approval mode, Agent mode, API keys, MCP servers)
  launch the same flows the dedicated slash commands open; line mode prints
  a summary. ALL TUI pickers (generic host pickers included) have
  type-to-filter + backspace (generic ones filter their static item list
  locally via `menuAllItems`, the models picker rebuilds through
  `buildModelMenu`; a no-match filter keeps the title + `(no matches)`).
  The `/model` TUI menu is TWO-STEP when several providers exist:
  `_buildModelMenu` returns provider rows (`@<name>` keys, the filter
  matches provider names AND their model ids) first, and selecting one
  opens that provider's model list (generic `modelProvider` picker whose
  `provider|model` rows route back through `_tuiSelectModel`'s
  cross-provider switch). A single provider goes straight to its models.
  Model rows render as a TABLE (`buildModelPickerTable` in
  `model_picker_table.dart`): `● id  provider  ctx  cost` with the
  context window compacted (1048576 → `1M`) and `$/Mtok` from the catalog
  (`—` when pricing is unknown, never 0.00); rows sort recency → cheaper
  total → unpriced last; narrow widths elide cost first, then provider,
  keeping id + ctx. The rows are click targets (hit-region routing).
  The provider→model pick flows live in `lib/src/cli/settings_flow.dart` in the
  NAMED `SettingsFlow` extension (public `runProviderModelFlow`/
  `startChatModelFlow`/`startMediaSlotFlow`/`startAgentModelFlow`), so
  tests can drive them line-mode (see `test/cli/settings_flow_test.dart`).
  The model-list fetch dispatches per dialect
  (`_fetchProviderModelIds`: CodeMie `/llm_models` by URL marker, DIAL
  deployments when the spec is `dial`, else OpenAI `/models`; the saved
  entry's own key authenticates; failures fall back to manual entry).
  `startAgentModelFlow` pins the `smol`/`subagent` role chains through
  `ModelRolesResolver.setRoleChain`/`clearRoleChain` (the resolver is
  created on demand when the config had no `roles:` section —
  `AgentCliConfig.modelRolesResolver` is mutable) and persists via
  `onModelsConfigChanged` (bin/fah.dart's persistConfig reads the live
  resolver config).
- `lib/src/cube/` — fa_cube sandbox profiles (issue #8): strict `CubeSpec`
  YAML (`.fah/cubes/<name>.yaml`, apiVersion fa/v1 kind Cube) with
  tool/network/fs/env/resource/cache policies (deny-wins, empty allowlist =
  deny-all, lexical path normalization as traversal guard), `CubeResolver`
  (path > name lookup), Dart-layer enforcement (`SandboxedExecutionEnv`
  decorator: `CubeFsGuard` gates the agent's own read/edit/write tools,
  `CubePolicyEngine` gates bash incl. pipes/`$(...)` subshells and shell
  redirect targets (`>`/`>>`/`<>`/`&>`/`2>` writes, `<` reads, `2>&1`
  exempt; arguments stay name-gated only), exit 127
  denials with per-exec reason), content-addressed `CubeCacheManager` under
  `.fah/cube-cache/<md5>/` (manifest + ttl). `spec.backend:` = `policy`
  (default, Dart-only) | `kernel` opt-in: every command wrapped in the OS
  sandbox via `CubeSandboxBackend.wrapCommand` — macOS `sandbox-exec -f
  <profile>`, Linux `unshare --user --map-root-user --mount [--net iff the
  spec allows no network]` + `ulimit -v/-t` ceilings, both over a clean
  `env -i` environment; profiles staged content-verified under
  `<home>/.fah/cube-profiles/<md5>.sb` (outside the guest-writable
  workspace; re-verified against the recomputed profile before every
  wrapped exec — #790); kernel mode fail-closes (`fa_cube[<name>]:`
  error, no exec) when the staging location is not provably
  guest-unwritable — a `homeDir` inside/relative-to the workspace or a
  spec mount granting rw over it (the refusal names
  `spec.allowDegrade: true` as the explicit policy-mode escape hatch;
  with it set, the bind degrades and `onDegrade` fires); the staging
  dir is swept once per
  binding for this profile's own `.tmp` orphans (other bindings' files
  untouched); the macOS SBPL profile denies writes blanket outside the
  workspace + `rw` mounts (`/dev/null`, `/dev/fd` exempt) and denies reads
  of `/etc` + user homes when no root mount grants reads (a blanket read
  deny would abort exec — dyld); Windows = Job Object descriptor only
  (kernel on a platform without an enforcing backend is REFUSED — clean
  `fa_cube[<name>]:` error naming `spec.allowDegrade`, zero commands —
  unless the spec opts in via `allowDegrade: true`, which degrades to
  policy mode with the loud `fa_cube[<name>]: kernel backend unavailable,
  running in policy mode` warning; `effectiveBackend` reports what
  actually ran — #793); a wrapper that is missing from
  PATH or refuses the sandbox (EPERM, SBPL
  rejection) surfaces as a clean `fa_cube[<name>]: kernel backend ...`
  spawn error in foreground execs and background jobs alike. CLI:
  startup precedence flags > project `.fah/config.yaml` `cube:` > user
  config (`resolveStartupCubeSource`), `/settings` Cube sandbox picker
  (shared switch logic in `agent_cli_cube.dart`, persists the `cube:`
  startup default), `/cube [use <name>|off|reload|list|cache status|cache
  clear]`; cache restore at run start + save at end, headless included.
  `SharedSetting.cubeSandbox` is cli-only (host process sandboxing; app
  shells already confined). Contract + reference: `schema/cube_schema.json`
  and `docs/cubes.md`; mock-LLM integration suite
  `test/integration/fa_cube_integration_test.dart` (8 scenarios,
  `--tags integration`).
- `lib/src/prompts/prompt_overrides.dart` — `prompts:` config section maps
  prompt names to file path or inline text; strict validation; flags
  `--system-prompt(-file)` > config > built-in.
- `lib/src/cli/cli_help.dart` — full `fah --help` text, guarded by
  `test/cli/cli_help_test.dart` (update BOTH).
- `lib/src/web_search/` — `web_search` (DDG keyless → Brave/Tavily keyed)
  and `web_fetch` (HTML→markdown, pub.dev handler) via
  `builtinTools(env, webSearch:)`.
- `lib/src/model_roles/provider_catalog.dart` — provider table (incl. the
  `chatgpt` Codex-backend entry, kind `chatgpt-codex`); specs
  default `input: ['text','image']` (vision). The `FA_PROVIDERS`
  dart-define / runtime env (`providerFilterEnvOverride`, wired from the
  process env in `bin/fah.dart`; the define wins) allowlists the catalog
  per build — `enabledProviders`/`providerEnabledInBuild`/`catalogProvider`
  all honor it, default is everything on
  (`test/build_filter/provider_filter_test.dart`).
  Issue #273 token architecture: `buildCatalogModel`/`buildCliDefaultModel`
  resolve `maxTokens` as config override > `resolveModelMaxOutputTokens`
  (the kimi-code per-family Claude OUTPUT ceiling table with
  nearest-lower-minor fallback, 128000 conservative unknown fallback —
  scoped to the `anthropic-messages` api family, `anthropicMessagesApi`;
  claude ids on other dialects keep the provider default, issue #302) >
  provider spec default;
  `lib/src/providers/thinking.dart` ports pi's
  thinking ladder (budgets 1024/2048/8192/16384, `minAnswerTokens` 1024,
  `adjustMaxTokensForThinking` — thinking fits INSIDE `max_tokens`),
  wired into the anthropic adapter via `AnthropicOptions.thinkingLevel`.
  `lib/src/providers/chatgpt_oauth.dart` + `chatgpt_codex.dart` — ChatGPT
  account sign-in (PKCE against auth.openai.com, Codex CLI client id) and
  the Responses-API SSE adapter (`store: false` — the backend rejects
  server-side storage; `ChatGPT-Account-ID` header; 401 → refresh →
  re-persist via `ChatGptCredentialsPersist`). CLI:
  `/provider chatgpt oauth [headless]` (callback server in
  `lib/src/cli/chatgpt_oauth_server.dart`, exported from `lib/io.dart`);
  the credentials blob lives in the secure store as
  `CHATGPT_OAUTH_CREDENTIALS`.
  `lib/src/providers/codemie_sso.dart` + `lib/src/cli/codemie_sso_server.dart`
  (io.dart export) — CodeMie organization sign-in: `/provider codemie sso
  [orgUrl]` runs the browser SSO (login URL embeds the RANDOM callback port;
  the base64 `token` callback carries session cookies); the
  `codemie_access_token` JWT authenticates as Bearer against
  `<org>/code-assistant-api/v1` — the org is saved as a custom provider
  entry (re-login refreshes the key, keeps the last-used model). Models come
  from `/llm_models?include_all=true` (LiteLLM shape), not `/models`;
  `_refreshModelCache` branches on the `code-assistant-api` marker. The
  catalog also has a `codemie` entry for the manual
  `/provider codemie [url] [token]` path (env `CODEMIE_API_KEY`).
  `lib/src/providers/dial.dart` — the `dial` catalog entry (EPAM DIAL Core):
  OpenAI-completions payload, but chat at `{baseUrl}/openai/deployments/
  {model}/chat/completions` (the deployment name lives in the PATH — via the
  `OpenAICompletionsOptions.urlBuilder` extension point) with `Api-Key`
  header auth (the adapter gets a null key so no `Authorization: Bearer` is
  sent); optional `?api-version=` from the `DIAL_API_VERSION` env
  (`_catalogStreamFunction` reads it via `config.envVarValue`). Models come
  from `{baseUrl}/openai/models` (`fetchDialModels`, the
  `_refreshModelCache` `provider == 'dial'` branch). Headless:
  `--provider dial --model <deployment> [--base-url …]` (dial has no
  default model id — `buildCliDefaultModel` throws without `--model`).
  `lib/src/model_roles/vision_models.dart` — the shared vision heuristic
  (`modelIdSuggestsVision`, `visionMarker` picker checkmark,
  `inputModalitiesFor`): CLI model switches recompute `Model.input` from it
  and the model pickers show the ✓/✗ marker; `packages/fa_ui` re-exports it
  (one marker list for CLI and app).
  `lib/src/providers/models_for_endpoint.dart` — the shared "list this
  endpoint's models" dispatch (`fetchModelsForEndpoint`): CodeMie
  `/llm_models` by URL marker, DIAL deployments by `provider: 'dial'`,
  else OpenAI `/models`; every model picker (CLI flows, app pages) routes
  through it so no dialect silently degrades to manual entry.
- `packages/fa_ui/` — reusable Flutter package for hosts embedding the Fa
  agent: the Fa theme (`FahPalette`/`FahLightPalette`/`FahColors.of(context)`,
  `buildFahTheme()`/`buildFahThemeLight()` + chat themes) with the
  `FaUiTheme`/`FaUiThemeProvider` customization layer (accent colors, font
  family, radii), the provider/model settings widgets (`ProvidersSection`,
  `ProviderEditorPage`, `DefaultChatModelSection` + pickers,
  `MediaModelsSection` (the settings media-models section, moved from the
  app — store/`mainBaseUrl`/`modelsFetcher` in, host analytics via the
  `onSlotEditorOpened`/`onSlotOverrideSaved` hooks),
  `MediaSlotProviderPickerPage`/`MediaSlotModelPage`, `ProviderPreset` +
  helpers (hosted presets: OpenRouter, Ollama Cloud, Google Gemini), `ModelIdAutocompleteField`, `FaVoicePresetPicker` +
  `faVoicePresetsFor` (per-(baseUrl, modelId) TTS voice presets — Gemini /
  Kokoro / OpenAI — with inline sample previews), and the stores (`ProviderRegistry`,
  `MediaModelsStore`, `SessionKeysStore`, `KeychainStore`,
  `modelIdSuggestsVision`). Every post-provider model choice uses ONE
  pattern: the fetched list renders immediately with the field text as the
  live quick-search, and a `Use "<query>"` row keeps manual entry —
  `FaModelListPicker` (`src/widgets/model_list_picker.dart`, the initial /
  tap-picked value shows the FULL list with a check, only user typing
  filters) for the form pages — media slots, agent roles, and the CodeMie
  SSO model step
  (`flutter_app/lib/services/codemie_sso_flow.dart`); the same
  `Use "<filter>"` row lives inside
  `UnifiedModelPickerPage` (applies on the ACTIVE provider, key resolved
  via the registry). The `ProvidersSection` rows carry the SAME brand mark
  the add-provider picker shows — `providerMarkKey` (preset) /
  `providerMarkKeyForBaseUrl` (custom provider, URL-matched: codemie
  marker, openrouter.ai, ollama.com, generativelanguage…, else `custom`) in
  `provider_marks.dart` — and never a current-provider check; every row
  trails a chevron into the editor. The provider editor
  (`ProviderEditorPage`) owns
  name/URL/key; the model id is a button-style row (icon + current model +
  chevron, mirroring the `TaskModelsSection` rows) opening
  `MediaSlotModelPage` with `slot: null` DIRECTLY (no provider-picker step)
  for a transient provider entry built from the CURRENT form values — the
  edited provider keeps its id so the stored key resolves, and a freshly
  typed (unsaved) key rides the page's `apiKeyOverride` (preset mode falls
  back to the stored preset key). The editor's `modelsFetcher` is a test
  seam threaded through `ProvidersSection`/`AddProviderPresetPickerPage`/
  `pushProviderEditor`. The media slots AND the agent-role rows
  (`TaskModelsSection`: Quick model / Subagents model) share the ONE
  two-step flow — `MediaSlotProviderPickerPage` → `MediaSlotModelPage`
  with `slot: null` for roles (no voice field, no capability chips,
  dial-aware provider kind) — so provider→model picking can never drift
  between settings surfaces. The endpoint fetch
  goes through the core `fetchModelsForEndpoint` dispatch (DIAL deployments
  / CodeMie marker / OpenAI `/models`); the legacy two-step
  `DefaultModelProviderPickerPage`/`DefaultModelPickerPage` and the
  text-field `TaskRoleConfigPage` are gone.
  The chat leaf widgets live there too (`lib/src/chat/`): the Markdown
  style/sandbox image resolver (`markdown_style.dart`), the inline
  audio/video players (`media_player.dart`), the approval/ask/
  secret-request sheets, the
  transcript message tile (`chat_message_tile.dart`), the composer
  (`chat_composer.dart` — file/gallery/camera picking through the
  `FaChatHost.uploadPicker`/`galleryPicker`/`cameraPicker` hooks, voice
  input through `FaChatHost.voiceInput`; desktop keys: Enter sends and
  Shift+Enter inserts a newline (issue #973 — the Focus ancestor swallows
  plain Enter before the IME turns it into a break; the touch return key
  keeps its IME newline path), Cmd/Ctrl+V is smart paste — a clipboard image
  (via the `FaChatHost.clipboardImageReader` hook) or long/multi-line text
  is staged as an `uploads/` attachment chip, short single-line text pastes
  inline), and the single-service chat
  screen (`fa_chat_screen.dart` — `FaChatScreen(service:, features:,
  title:, settingsBuilder:, fileBrowserBuilder:, composerBuilder:)`),
  plus the upload helpers
  (`upload_utils.dart`), the media tool-name constants
  (`media_tool_names.dart`), and the brand glyphs
  (`fa_glyphs.dart` — `FaAttachGlyph`, the SVG attach icon used by the
  composer, `FaModelGlyph`, the AI-chip icon leading the provider editor's
  model row, and `FaAiAvatar`, the `>_` squircle used as the assistant
  avatar in `chat_message_tile.dart`) —
  localized via `FaChatStrings` (en/ru defaults, `FaChatStringsScope`
  override), analytics via the `FaChatHost.track` hook, backend surface is
  the `FaChatService` interface (which `AgentService` implements;
  `ApprovalModeSelector` needs only the
  `FaApprovalModeController` slice).
  The trajectory ledger UI lives in `lib/src/trajectory/` (issue #25
  redesign): `TrajectoryController` (view-state owner — debounced
  snapshot updates, turn/assistant folds, the four timeline projections
  with selection, record selection, throttled search),
  `TrajectoryScreen` (the full-screen trajectory page: wide canvases
  `>= kWideLayoutBreakpoint` push it as a master-detail route from the
  chat app bar; narrow canvases swap it in as the chat screen's body via
  the Chat | Trajectory segmented switcher — controller state lives at
  screen level so the swap never loses scroll, selection, or filters),
  `TrajectoryHeader` (title + session stamp, stat pills, the search
  field with match position and prev/next navigation, the ledger filter
  chips, and the export overflow menu that copies the session as
  JSON/Markdown to the clipboard), `TrajectoryBody` (header above the
  ledger; wide splits ledger ~55% / details ~45% into
  `TrajectoryDetailsPane`, narrow keeps the tap-for-details sheet),
  `TrajectoryView` (control strip + Gantt timeline strip + ledger of
  two-line expandable selectable rows with per-row copy and injectable
  table/timeline builder seams), the details surface (`showTrajectoryDetails`
  sheet narrow / persistent pane wide: tab set per record kind with
  empty tabs hidden, a Request tab for outbound model payloads, per-tab
  copy, highlighted Raw view; session-scoped last-tab restore,
  `resetTrajectoryTabHistory()` test seam), and `FaTrajectoryPanel`
  for hosts that feed their own stream via `TrajectoryServiceFeed`
  (broadcast snapshot stream that replays `latest` to new listeners).
  `TrajectoryTimeline` draws the Gantt: time ruler + lane labels, three
  fixed lanes (0 system/user/context, 1 message/compacted, 2 tools), a
  live pulse on the in-progress span, and the whole strip hidden when
  the session is untimed.
  Strings: `TrajectoryStrings` en/ru + `TrajectoryStringsScope`. Golden
  baselines: `test/trajectory/golden/` (regenerate with
  `flutter test test/trajectory/golden/ --update-goldens`, verify without
  the flag) — `golden_test_setup.dart` loads the real `Inter` and
  `JetBrainsMono` families from `test/assets/fonts/` (test-only copies
  of flutter_app's bundled TTFs; the package itself ships no font
  assets) plus the bundled MaterialIcons font, so goldens render real
  glyphs; review every changed PNG after regenerating.
  Package strings live in `FaUiStrings`
  (en/ru defaults resolved from the locale, host-overridable via
  `FaUiStringsScope`) — never the app's gen-l10n. App-level concerns are
  injected: the active connection is the `FaChatConnection` interface,
  on-device engine routes are `FaOnDeviceRoute` builders, the apply
  callback carries `FaChatModelConfig`, and named-key resolution goes
  through the `FaUiHost.keyResolver` hook. `flutter_app` consumes it via
  path dep + thin `export` shims at the old paths; the app's
  `lib/ui/screens/providers_section.dart` additionally keeps a
  `DefaultChatModelSection` ADAPTER (old constructor) wiring `AgentService`,
  `LastConnectionStore`, and the on-device engines into the package flow.
- `flutter_app/` — Flutter chat example. Layout: `lib/main.dart`
  (entrypoint + BootstrapScreen: auto-connects the restored last connection
  when its key resolves, else SetupScreen), `lib/ui/` (`app_theme.dart` —
  shim re-exporting the fa_ui theme; `markdown_style.dart`, `screens/`,
  `widgets/`),
  `lib/services/` (agent service, stores, upload, secrets, project mount,
  vision, `theme_controller.dart` theme-mode persistence + `FahThemeScope`;
  `session_keys_store.dart`/`keychain_store.dart`/`provider_registry.dart`/
  `media_models_store.dart`/`vision_models.dart` are shim re-exports of the
  fa_ui stores), `lib/sandbox/` (env,
  shells, wasm, git, fs persistence), feature dirs
  `lib/apps|gemma|webllm|transformers_js|l10n/`. All lib-internal imports
  are absolute `package:fa/...` — no relative imports.
- `flutter_app/lib/ui/markdown_style.dart` (shim re-export of
  `packages/fa_ui/lib/src/chat/markdown_style.dart`) — beyond
  `fahMarkdownStyleSheet`: `SandboxImageResolver`/`fahSandboxImageBuilder`
  render markdown images with sandbox paths (`![alt](generated/x.png)`,
  leading `/` stripped) by loading bytes via `env.readBinaryFile` (memoized
  per surface, dim placeholder on failure, tap → `showFahImagePreview`
  fullscreen dialog); wired into the chat screen and the Fa chat overlay
  markdown. `generate_image` tool tiles also render the saved image inline
  (path parsed from the result text, which itself teaches the model the
  `![image](<path>)` convention).
- `flutter_app/lib/ui/widgets/media_player.dart` (shim re-export of
  `packages/fa_ui/lib/src/chat/media_player.dart`) — inline audio/video
  playback of sandbox media: `SandboxAudioPlayer` (play/pause, seek slider,
  `m:ss / m:ss`) and `SandboxVideoPlayer` (bounded tap-to-toggle surface,
  progress bar, mute) behind injectable `SandboxAudioController`/
  `SandboxVideoController` abstractions (tests/goldens inject fakes via
  `ChatScreen(audioControllerFactory:/videoControllerFactory:)`, real
  defaults wrap `audioplayers` (`BytesSource` — no file needed) and
  `video_player` (bytes staged to a temp file; web build shows an honest
  "not supported" note). Wired in `chat_screen.dart` for `speak`/
  `generate_music` results plus a `.mp3/.wav/.m4a` (audio) / `.mp4/.mov/
  .webm` (video) extension fallback for any NON-`read`, non-error tool
  result (which is how `generate_video` tiles render); markdown media links open a `showFahMediaDialog` player dialog
  (`onTapLink` — flutter_markdown has no custom link renderer) in the chat
  screen and the Fa chat overlay. Bytes load through the same memoized
  `SandboxImageResolver.load`.
- `flutter_app/lib/ui/widgets/chat_message_tile.dart` (shim re-export of
  `packages/fa_ui/lib/src/chat/chat_message_tile.dart`) — the ONE transcript
  message renderer, shared by the chat screen (its `flutter_chat_ui`
  builders delegate) and the in-app Fa chat overlay (`compact: true` for
  tighter panel padding): user/assistant Markdown bubbles (sandbox images
  via `SandboxImageResolver`, selectable), tail-collapsed thinking bubble,
  styled `[ tool ]` tiles with the private collapsible output block,
  `$`-prompt system lines, and inline generated-image/audio/video under
  tool tiles. Never fork message rendering — extend this widget.
- `flutter_app/lib/ui/widgets/chat_composer.dart` (ADAPTER over
  `packages/fa_ui/lib/src/chat/chat_composer.dart`) — keeps the app's
  constructor surface (`AgentService` + injectable `uploadPicker`/`asr`/
  `asrTranscriber`/`clipboardImageReader` test fakes) and bridges it to the
  shared composer's hooks: the `UploadPicker` becomes a `FaChatUploadPicker`,
  gallery/camera come from `image_picker`, the ASR stack is wrapped in a
  `FaChatVoiceInput` (transcriber resolved per take via the session's
  media gateway, falling back to the active provider), and the smart-paste
  clipboard image probe rides `pasteboard` (`Pasteboard.image` → PNG bytes;
  native plugin — the desktop/mobile apps need a REBUILD, not a hot reload,
  to register it).
- `flutter_app/lib/ui/screens/app_launcher_screen.dart` — THE app home on
  every layout (`faHomeScreen` in main.dart always returns it; the classic
  sidebar chat home is legacy and `session_sidebar.dart` no longer exists —
  sessions are managed by the chat sheet's pager/menus).
  iOS-home-screen grid of the JS apps (fsRevision-refreshed like
  `AppsGridView`) plus Settings/Files system tiles, laid out on the
  icon-unit geometry (`LauncherGridSpec` in
  `lib/ui/widgets/span_grid_delegate.dart`: 56px icon square + 20px label,
  16px gaps; default 4 columns < 600px, 6 above, clamped 3–8) as a
  `SingleChildScrollView` + `Stack` of `AnimatedPositioned` tiles
  (`packTileSpans`/`layOutTileRects`; issue #166 made the packer
  order-preserving row-wrap: tiles place strictly in list order, a span
  that doesn't fit the remaining row cells wraps to the next row and the
  trailing cells stay blank — no backfilling earlier rows, reading order
  == tile order; spans clamp to the column count), so reorders animate
  live while dragging (center-band hover on an app = folder intent, edge
  halves = insertion preview; drop persists; hold-release without
  movement opens the tile-size menu). Folder tap opens a floating panel
  (rename/dissolve buttons, drag-out-to-ungroup onto the barrier). Tile
  layout persists via
  `LauncherLayoutStore` (`lib/services/launcher_layout_store.dart`,
  `launcher_layout.json` v2: ordered keys `app:<id>`/`system:*`/`folder:<id>`
  + `grid.columns` + `tileSizes` {appId: "WxH"} overrides — agent-editable,
  re-read live on fsRevision; v1 migrates, corrupt file → defaults;
  `syncApps` reconciles with installed apps). Its Stack hosts the
  `SessionChatSheet`.
- `flutter_app/lib/apps/session_chat_sheet.dart` — the iMessage-style
  session chat overlay over the launcher, three layers above the app grid:
  the always-visible docked input bar (the shared `ChatComposer` with
  `autofocus: false` — the bar never pops the keyboard at app start;
  leading slot = sessions-drawer toggle with the custom `SessionsGlyph`
  stacked-bubbles icon, turning into the attach button once a session is
  open; trailing = exactly ONE action — the mic while the field is empty
  and idle, stop while streaming-empty, send once text is entered, swapped
  via AnimatedSwitcher scale+fade (the composer's `hideMicWhenNotEmpty`;
  the idle mic sits in the same pill-colored circle the send/stop button
  occupies); the composer carries a stable key so the status-row slot
  toggling never recreates it (that killed the field's focus after send),
  and `_send` restores focus past the IME send action's own unfocus —
  the keyboard stays up for the follow-up message. Focusing the field
  opens the panel right away via
  `onFocusChanged`, `onSent` does too), the sessions drawer sliding in from
  the LEFT (New-session tile + live then persisted sessions rendered with
  the SAME `SessionTile`/`sessionTileSubtitle`/`sessionTileCwdLabel`/
  `showSessionActionsMenu` the wide sidebar exports from
  `sidebar_sessions_list.dart`, persisted open lazily via
  `manager.openSession`; a tile's folder label always comes from the
  session's OWN on-disk metadata cwd — a live session's env reports the
  app's CURRENT mount for every session, so reading
  `service.env.sessionCwd` would flip the label the moment a persisted
  session opens; its header clears the floating macOS traffic
  lights via `faIsMacOSDesktop`; a scrim tap on the exposed part dismisses
  the top layer, and the drawer carries its OWN scrim ABOVE the panel so an
  outside tap still closes it while a session is open), and the session
  panel sliding up UNDER
  the bar to 92% (drag-handle header: sessions-drawer button with the
  `SessionsGlyph`, title via `SessionNamesStore`, 3-dots menu New / Rename /
  Open full chat / Copy / Close — Rename opens the shared
  `showRenameSessionDialog`; the shared `ChatMessageTile` transcript padded
  above the bar by its measured height; pull-down on the header zone or a
  scrim tap closes). While streaming with the panel closed a slim
  `FaWorkBar(embedded:)` status row sits above the composer. There is no
  collapsed/mini state and no pager — session switching is the drawer.
  Sizing: the expanded panel height and all drag math come from the sheet's
  own `LayoutBuilder` constraints (`panelFraction`, default
  `defaultPanelFraction` 0.92 — hosts like the store-inapp shot pass a
  smaller fraction to park the panel lower), NEVER from
  `MediaQueryData.fromView(View.of(...))` — `View.of` subscribes to
  `_ViewScope`, whose `updateShouldNotify` compares only the view instance,
  so it never rebuilds on viewInsets changes and the keyboard opening used
  to push the panel header off the top edge; the host Scaffold's
  `resizeToAvoidBottomInset` shrinks the body constraints around the
  keyboard, which DOES rebuild the LayoutBuilder.
- `flutter_app/lib/ui/screens/chat_screen.dart` (ADAPTER over
  `packages/fa_ui/lib/src/chat/fa_chat_screen.dart`; also re-exports
  `chatImageMessageSource`/`kWideLayoutBreakpoint`) — keeps the app's
  multi-session surface: the `FlutterSessionManager` subscription (session
  switch hands the shared screen the new active service, which
  re-subscribes and re-syncs in place; closing the last session clones a
  fresh one via `ensureActiveSession`) plus the fa-specific affordances the
  package screen takes as hooks — the settings route (`settingsBuilder` →
  `SettingsScreen`), the files panel (`fileBrowserBuilder` → `FileBrowser`
  with env/fsRevision), the `open_app` launcher (`service.appLauncher` →
  `FaChatHost.appLauncher` when set, else `pushJsApp`), and the composer
  test fakes (`composerBuilder` → the app's `ChatComposer` adapter).
  Composer changes must keep the chat goldens pixel-identical.
- `flutter_app/lib/l10n/` — gen-l10n: `app_en.arb` + `app_ru.arb` →
  `AppLocalizations` (generated, never edit; `flutter gen-l10n`). UI copy
  via `context.l10n.<key>` (`l10n_ext.dart`); locale follows system.
  `test/l10n_guard_test.dart` hard-fails on hardcoded widget strings, en/ru
  key drift, placeholder mismatches, missing keys — opt out per line with
  `// l10n:ignore`, per file with `// l10n:ignore-file` (agent-facing/log
  strings stay literal).
- `flutter_app/test/golden/` — golden tests (see MANDATORY section below).
- `flutter_app/test/cli_visual/` — CLI visual integration tests
  (integration-tagged, excluded from the pre-commit `flutter test`): the
  real `dart bin/fah.dart` runs in a PTY (package:pty2) and every step is
  screenshotted through the real Flutter `TerminalView` (JetBrainsMono +
  Fa palette, `RepaintBoundary.toImage` at 2x) into the repo-root
  `test/integration/screenshots/NN_name.png` + a `.txt` twin with the exact
  xterm screen text. Run: `flutter test test/cli_visual --tags
  integration`. The pure-Dart counterpart harness lives in
  `test/integration/pty_harness.dart` (see its README for the PTY/pty2
  pitfalls: spawn `dart bin/fah.dart` NOT `dart run`, always cancel the
  output subscription in `close()`).
- `flutter_app/lib/ui/screens/model_presets.dart` — settings "Model presets"
  section: a swipeable `PageView` of `kModelPresets` cards applying a whole
  model combo in one tap (`applyModelPreset` — per-slot `MediaModelsStore`
  overrides, unmapped slots cleared, then `service.reconfigure` +
  `LastConnectionStore.saveFromConfig`; missing provider key = inline hint +
  jump to `ProviderEditorPage`, nothing applied). Add a preset by appending a
  `ModelPreset` to `kModelPresets` (doc comment there) plus its
  `modelPreset<Id>Name`/`...Description` arb keys; `ModelPresetTarget` is
  sealed for future custom/on-device targets. The presets headline the
  dedicated **Models** settings page
  (`flutter_app/lib/ui/screens/models_settings_page.dart` —
  `ModelsSettingsPage`: presets → `DefaultChatModelSection` →
  `TaskModelsSection` → `MediaModelsSection`), opened from the "Models"
  row on `SettingsScreen` (chip icon + chevron, ABOVE the Agents section)
  via `pushFaPage` — a centered dialog on wide canvases, full-screen on
  the phone — so the settings top level stays provider-focused; same
  layout on phone and macOS. The sections need the `MediaModelsScope`/`TaskModelsScope`
  from the app shell (tests: wrap ABOVE the MaterialApp — pushed routes
  build inside the Navigator, above `home:`).
- `flutter_app/lib/sandbox/sandbox_registry.dart` — central registry of
  sandbox shell commands per platform; the Fa system prompt's `{{commands}}`
  renders from it. Never list commands in prompt text or UI by hand.
- `flutter_app/lib/sandbox/python_http_bridge.dart` — the mobile WASM
  python HTTP bridge (issue #337): the WASI CPython build has no sockets and
  no `ssl` module, so `fa_http.py` + `sitecustomize.py` (materialized into
  site-packages by `WasiSandboxShell._ensurePythonBridge`) patch
  `http.client`/`urllib`/`urllib3` to print each request to stdout as a
  `\x01FAHTTP1` control line and poll `/dev/.fahttp/<id>` for the raw
  response. `FaHttpBridge` (wired into `WasiSandboxShell._runStage` for
  python stages) strips those lines from captured stdout, performs the
  request with the shell's real-TLS HTTP client and writes the response
  back; the authority in the control line carries the scheme
  (`https://host:port`). Curl `-d` follows real-curl semantics (`@file`,
  `@-`, multiple flags join with `&`, 1 MiB cap, `-d` implies POST) and the
  mobile prompt advertises the TLS path via `sandbox_registry.dart`.
- `flutter_app/lib/services/project_mount_env.dart` — macOS project-folder
  mount (`/project` → user-picked host dir; security-scoped bookmarks in
  `project_mount.json`; stale bookmark = "pick again" warning). Sessions on
  macOS are stored in the shared App Group container
  (`~/Library/Group Containers/group.dev.fa1.shared/fa/sessions`) so the Fa
  CLI and the sandboxed Fa macOS app see the same workspace-scoped sessions.
- `flutter_app/lib/apps/` — JS apps platform on `package:js_widget_runtime`
  (hosted `^0.4.79` — ships the queued-callEvent-after-dispose guard +
  restart-safe bridge channels that the old git pin carried, plus the M3
  nodes/overlays/flChart/pickers/drawer catalog and M3 motion tokens; the
  use-after-free SIGSEGV is owned by `js_app_engine.dart`'s process-wide
  lifecycle serialization — releases must stay immediate, never deferred;
  the `map` node: center/zoom/markers/polylines/
  fitBounds, onTap/onMarkerTap). `flutter_js` itself is overridden in
  `flutter_app/pubspec.yaml` to IstiN/flutter_js@74a11bf
  (fix-jscore-multi-instance: the shared native sendMessage callback routed
  to the LAST created runtime, so coexisting engines converted each other's
  JSValues with the wrong JSContext — SIGSEGV in JSC::JSLock::lock; the fork
  routes by executing context and refuses post-dispose evaluate): apps live in env-shared `apps/<id>/
  {manifest.json, widget.js}`; permissions in `apps_permissions.json` (network/
  allowedCommands/llm/homekit/health/contacts/calendar/microphone/
  notifications/media/keys/theme — default denied);
  `jsr.fa.*` bridge over exec (`fa.llm`, `fa.calendar`, `fa.home.*`,
  `fa.health.*`, `fa.asr.*`, `fa.notify.*`, `fa.theme` — declarative-only
  list/current/apply over installed theme packs, consent dialog per apply,
  no install/delete surface (issue #169); `fa.keys` — list/get/request the
  host's merged secrets (AgentService.hostSecrets); `request` renders the
  shared secret_request sheet from JsAppView and persists via
  AgentService.acceptSecretGrant; contacts is a gated "not
  available yet" stub); the `js-apps` skill seeds
  into `.fah/skills/` on startup. Bundled demos (seeded by
  `AppsStore.demoAppIds`, assets in `flutter_app/assets/apps/` — each id
  MUST also have its `- assets/apps/<id>/` entry in pubspec.yaml, gated by
  `test/apps/demo_assets_declared_test.dart`): calculator,
  weather, stocks, crypto, animation-showcase, yolo-hello, calendar
  (`jsr.fa.calendar`), map (`map` node), health + homekit (real bridge on
  iOS, honest demo-panel fallback elsewhere), fitness-trainer — guided
  workout with a 3D animated coach (a realistic human baked from NAVER's
  anny body model — Apache-2.0 — into
  `assets/apps/fitness-trainer/models/coach_anny.glb` with 10
  hand-authored skeletal clips; baker script in
  `references/anny/tools/bake_coach_glb.py` — anny `local-bone` deltas in
  world axes → glTF: IBMs are column-major, mesh split into ≤16-joint
  surfaces for flame_3d, and the skinned node must NOT be parented to a
  joint (flame_3d's dependency count then never settles); rendered via
  the `scene3d` node + flame_3d; START-driven exercise/rest steps with
  per-clip mapping, pause/skip/quit, sessions persisted via jsr.storage;
  `integration_test/fitness_coach_screenshot_test.dart` screenshot-verifies
  it on the macOS host), english-teacher — the "Language Tutor": Duolingo-style
  quiz sessions (hearts/XP/streak, choice + typing modes), a per-language
  offline word bank (en/de/es/fr/pl picker persisted via jsr.storage),
  LLM-generated extra words via `jsr.fa.llm.chat` (manifest `llm: true`),
  3d-game (`scene3d` node +
  `jsr.scene3d.*` bridge on the runtime's flutter_cube/flame_3d dispatcher
  — the engine's `JsRuntimeConfig` and both renderers (JsAppView,
  AppTileHost) wire `js3dHost: createJs3dHost()`, tap picking flows back
  via `dispatchHostEvent('scene3d.tap:<id>')`). Demo seeding is
  ownership-aware: `apps/.demo_seeds.json` records sha256 of each file as
  last seeded — a file whose content no longer matches is user/agent-owned
  and never overwritten, UNLESS the on-disk manifest is unparseable (a
  half-written skeleton is re-seeded, not protected: it bricks the tile), (`resetDemoApp(id)` force-restores the reference
  version, `storage.json` untouched). A demo id whose seeding FAILS
  (missing/corrupt asset) never kills the rest: it lands in
  `AppsStore.failedSeeds` (id → error text) and gets an error badge on its
  launcher tile; tapping the tile shows a dialog with the copyable error
  (hand it to Fa for a fix). `open_app_tool.dart` registers
  the agent tool `open_app`
  (host callback navigates via `js_app_navigation.dart` `pushJsApp`).
  Live launcher tiles: a manifest `"widget"` section
  (`{entry: 'widget_tile.js', size: 'WxH', refreshSeconds?}` →
  `JsAppInfo.tileWidget`; size in icon-slot cells, W 1–4 × H 1–4, default
  2x2 — W 1 = icon-only; out-of-range values clamp to the range with an
  AppLog note, never a crash) makes the launcher
  grid render `app_tile_host.dart`
  (a JsAppEngine on the tile entry, display-only — any tap opens the app)
  instead of the static icon tile; a WxH tile's edges align exactly with
  the WxH block of icon slots it replaces. Users resize tiles via the
  hold-release menu (issue #166 preset list: 1×1/1×2/2×1/2×2/1×3/3×1
  plus the iOS 4×2/4×4; writes `tileSizes` into `launcher_layout.json`);
  the same menu offers demo apps "Restore reference version"
  (`AppsStore.resetDemoApp` — force-reseeds bundled code when
  ownership-aware seeding skips modified files, `storage.json` untouched).
- `flutter_app/lib/services/home_service.dart` — smart home: `HomeApi` over
  the `fah/home` MethodChannel (HomeKit in `AppDelegate.swift`, iOS only;
  the macOS channel answers unsupported): the HMHomeManager is created
  LAZILY on the first home call — instantiating it is what triggers the OS
  home-data prompt, so the channel registration never touches it (the prompt
  waits for the home JS app / a home tool, never app start). `listHomes`/
  `listRooms`/
  `listAccessories` (with the full services/characteristics breakdown +
  isOn/brightness/targetTemperature conveniences), `readAccessory`,
  `writeCharacteristic` (ANY writable characteristic by HomeKit type
  string), `listScenes`/`executeScene`, and the setPower/setBrightness/
  setTargetTemperature aliases. The delegate waits for the first
  `homeManagerDidUpdateHomes` (5 s cap) when access was granted but homes
  have not loaded yet, and polls the authorization status so a denied
  prompt answers `false` instead of hanging. All four writes take optional
  `name`/`room`: bridge sub-devices (Aqara/Mi) can share ONE
  `uniqueIdentifier`, so the native write routing narrows id matches by
  name+room (case-insensitive, exact > partial) and falls back to a
  name+room match when the id matches nothing; still-ambiguous = clean
  error, never a first-match write. JS surface `jsr.fa.home.*`
  mirrors it (`docs/js-system-apis.md`); agent tools in `home_tool.dart`
  (`home_devices`, `home_turn_on`/`home_turn_off`, `home_set`) — `match`
  accepts a name or a full UUID, optional `room`/`home` args narrow
  duplicate names (the ambiguity error teaches both escape hatches), and
  `home_devices` shows each accessory's short id (first 8 chars).
- `flutter_app/lib/services/app_log.dart` — process-wide debug log: ring
  buffer (2000 lines) + best-effort persistence to `logs/app.log` under
  `ExecutionEnv.cwd` (rewritten with its tail past 1 MB). `main.dart` tees
  `debugPrint` into it; settings has a "Copy debug logs" row.
- `flutter_app/lib/services/background_execution.dart` — iOS extended
  background execution (`fah/background` channel →
  `UIApplication.beginBackgroundTask`, io/stub pair): the
  `AgentService.isStreaming` setter brackets every run so the OS grants
  ~30 s of execution when the user backgrounds mid-stream instead of
  suspending instantly.
- `flutter_app/lib/services/live_activity.dart` — iOS Live Activity
  (`fah/live_activity` channel, ActivityKit ≥16.2, io/stub pair): starts
  on run start, updates with the FaWorkBar-style status text on tool
  start/end and first deltas, final done/error update then `end` after
  4 s. The widget extension is `ios/FaLiveActivity/` (bundle id
  `dev.fa1.app.FaLiveActivity`, compiled into both targets via
  `FaLiveActivityAttributes.swift`); `Info.plist` has
  `NSSupportsLiveActivities`. Device/release signing needs an App ID +
  provisioning profiles for the extension in the portal.
- `flutter_app/lib/services/calendar_service.dart` — system calendar:
  `CalendarApi` over the `fah/calendar` MethodChannel (EventKit in
  `MainFlutterWindow.swift`/`AppDelegate.swift` — MIRRORED, edit both;
  entitlement `com.apple.security.personal-information.calendars`, both
  NSCalendars*UsageDescription plist keys); stub = not-available on web.
  Agent tools in `calendar_tool.dart` (registered when
  `calendarPlatformSupported`): `calendar_events {date?, days?}` (rows carry
  recurrence/alarm/url hints), `calendar_calendars` (title, source account,
  writable), and the write-tier `calendar_add` / `calendar_update` /
  `calendar_delete`. Writes support `recurrence`
  ({frequency, interval?, daysOfWeek? MO..SU weekly-only, daysOfMonth?
  monthly-only, until|count — at most one end; validated by
  `parseCalendarRecurrence` in calendar_service.dart, removed on update via
  `'none'`/`{}`), `alarms` (minutes before start; replace-on-update),
  `calendar` (target calendar title), `span` (`this`/`future` → `EKSpan`)
  on update/delete, and `url`. Denial result points to System Settings →
  Privacy → Calendars. The `jsr.fa.calendar` bridge
  (`js_app_engine.dart`) passes the same fields through.
- macOS window chrome: `MainFlutterWindow.swift` uses the modern unified
  titlebar (`titlebarAppearsTransparent`, hidden title,
  `fullSizeContentView`, `toolbarStyle = .unifiedCompact`, window
  background `#070A10`; deployment target 14.0 in `project.pbxproj`), so the
  compact traffic lights float over Flutter content; `MaterialApp.builder`
  in `main.dart` reserves a 28px top strip on macOS so they never overlap
  the app header.
- CodeMie SSO in the app: `lib/services/codemie_sso_flow.dart` — macOS uses
  the CLI flow (local callback server + system browser), iOS drives
  `ASWebAuthenticationSession` through the `fah/web_auth_session` channel in
  `ios/Runner/AppDelegate.swift` (Safari-grade WebAuthn/passkey sign-in —
  an embedded WKWebView cannot offer Face ID without a `webcredentials`
  associated-domain relationship with the IdP; the session intercepts the
  `http://localhost:<port>/?token=` redirect by its `http` scheme, every
  flow page being https). When the session cannot start, the flow falls back
  to `ui/screens/codemie_sso_webview.dart` (in-app `webview_flutter` page
  intercepting the same redirect via its `NavigationDelegate`, password
  login only).
- `flutter_app/lib/services/theme_controller.dart` — ThemeMode
  (system/light/dark) persisted as `theme.json`; the theme itself lives in
  `packages/fa_ui` (`buildFahTheme()` dark + `buildFahThemeLight()`,
  re-exported by the `lib/ui/app_theme.dart` shim), widgets read colors via
  `FahColors.of(context)` (never `FahPalette` directly in widgets).
- `flutter_app/lib/ui/screens/onboarding_screen.dart` — first-launch
  onboarding: 4 pages on every platform (third-party skills are discovered
  by default, so there is no consent page). Pages:
  welcome + AI disclaimer, permissions explainer, privacy
  + policy link via `url_launcher`), page dots, Skip on every page.
  `BootstrapScreen` shows it once only when there is NO restorable
  connection; the seen flag lives in
  `lib/services/onboarding_store.dart` (`onboarding_seen.json`, same
  tiny-store pattern as `theme.json`).
- `flutter_app/lib/services/session_keys_store.dart` — shim re-exporting
  the fa_ui store; in-app secrets:
  on iOS/macOS persisted in the platform Keychain via `keychain_store.dart`
  (the `fah/keychain` MethodChannel, service `fa.app`; file-persisted keys
  migrate once), elsewhere `session_keys.json` via the env (set/delete,
  never displays values); the settings Keys section manages them.
  `provider_registry.dart` custom-provider keys ride the same Keychain
  backend (host-scoped `FA_KEY_<HOST>` names, like the CLI). The Keys
  section's "Add key" dialog saves arbitrary names
  (`^[A-Z][A-Z0-9_]*$`, uppercase-normalized, duplicates rejected);
  `AgentService.create` merges saved keys into the agent secrets — dotenv
  first, saved keys OVERRIDE on conflict (bash env, redactor, system-prompt
  name list all flow from the merged map).
- `flutter_app/lib/services/session_names_store.dart` — user-given session
  titles (`session_names.json` envelope in `ExecutionEnv.cwd`, keyed by
  session id; the session repo has no header-update API, so renames are an
  app-side overlay) plus `derivedSessionTitle(context, id:, createdAt:)` —
  the fallback title the chat sheet uses: a localized
  intl `DateFormat.MMMd(locale).add_Hm()` date+time from the session's
  creation time ("Jul 31 12:30" en / "31 июл. 12:30" ru; `main.dart` calls
  `initializeDateFormatting` for the app locales), `session <id8>` when the
  creation time is unreachable. The rename dialog
  (`flutter_app/lib/ui/widgets/rename_session_dialog.dart`,
  `showRenameSessionDialog`) is opened from the chat sheet's menu
  (Save/Clear; empty clears → the derived name).
- `flutter_app/lib/services/approval_mode_store.dart` — the tool-approval
  mode persisted as `approval_mode.json`; `AgentService.create` seeds
  `approval` from it, `setApprovalMode` writes through (fire-and-forget),
  `clone()` inherits the CURRENT mode (never a fresh read).
- `flutter_app/lib/services/skills_access_store.dart` — the third-party
  skills access setting persisted as `skills_access.json` (same tiny-store
  pattern); `null` = never chose = **granted by default** (opt-out
  discovery — a fresh install reads `.claude`/`.github/skills`/`.codex`
  without asking); a persisted `ask` round-trips verbatim (it is the
  boot-dialog state), `denied` opts out. `AgentService.create` gates
  `discoverSkills` on it
  (`allowedSources: {SkillSource.fah, SkillSource.agents}` unless granted),
  `setSkillsAccess` writes through (fire-and-forget) AND re-discovers the
  prompt's skills section live (no reconfigure needed), `clone()` inherits
  the CURRENT setting. UI: the settings `SkillsAccessSection` dropdown
  (ask/allowed/denied), and the one-time boot dialog in `main.dart`'s
  `BootstrapScreen` (seen-onboarding + explicit `ask` only; Allow persists
  granted, Not now persists DENIED so it never re-asks, a dismissed dialog
  stays `ask`). Both surfaces are desktop-only:
  `skillsConsentSurfacesVisible` (same file, off `defaultTargetPlatform` so
  widget tests can flip it) hides them on Android/iOS — the third-party
  roots don't exist there; discovery gating itself is unaffected.
- `flutter_app/lib/services/analytics.dart` — `AppAnalytics` facade over
  Firebase Analytics (global instance, noop without Firebase; tests install
  a recorder sink): app start, bootstrap outcome, setup shown, connect
  result (provider kind/custom/on-device, success only), provider
  add/edit/delete, models fetch count bucket, suggestion-vs-free-text model
  pick, message sent (attachment flag + length bucket — never content),
  session new/switch/delete, settings opened, key set/delete (names only),
  upload count, screen_opened per screen (the user-path backbone), chat
  sheet state, JS app open/reload, launcher folders/tiles/grid, theme +
  approval mode, model presets, media slot set/generated, voice input,
  secret request outcome, files opened. Privacy rule: never keys, message
  text, or file contents. `test/analytics_guard_test.dart` hard-fails on a
  screen/composer/sheet file without an `AppAnalytics.instance` call (or a
  documented exemption) and on facade events never called from lib/ —
  keep both sides wired.
  Crashlytics (`firebase_crashlytics`, wired in `main.dart`: fatal Flutter
  errors + uncaught async + debugPrint breadcrumbs) NEEDS
  `GoogleService-Info.plist` bundled in the Runner target — both
  pbxproj files carry the reference (gitignored file, CI writes it from
  `GOOGLE_SERVICE_INFO_PLIST_BASE64`) plus a "Crashlytics: upload dSYMs"
  build phase and `dwarf-with-dsym` in Release; settings has a
  "Send test crash report" row (non-fatal recordError) to verify the
  pipeline from a device.
- `flutter_app/lib/services/last_connection.dart` — persists last connection
  (never API keys) as `last_connection.json`; at boot `restorableBootConfig`
  (main.dart) rebuilds the AgentConfig (custom-provider key → saved hosted
  key) for the auto-connect, else it pre-selects the settings form.
- `flutter_app/lib/services/vision_models.dart` — shim re-exporting the
  `modelIdSuggestsVision` heuristic (fa_ui shim → the pure-Dart core
  `lib/src/model_roles/vision_models.dart`), which fills `Model.input`;
  `AgentConfig.supportsImages` overrides;
  without `image` the `read` tool notes non-vision and adapters drop image
  blocks.
- `flutter_app/lib/services/media_tools.dart` — `MediaGateway` over the
  `media_models.json` slots (+ main-connection fallback) backing the
  `generate_image`/`speak`/`generate_music`/`generate_video` tools and the
  `jsr.fa.media.*` bridge; files land in `generated/`. `generate_video`
  rides the `videoGeneration` slot (required — no fallback): async
  OpenAI/OpenRouter `/videos` contract (POST job → poll `GET /videos/{id}`
  every 3s, 4-min cap, cancel-token aware → `unsigned_urls` or
  `GET /videos/{id}/content` mp4). Google (`generativelanguage`) endpoints
  switch to the native Gemini protocol by baseUrl: `speak` posts
  `/models/{model}:generateContent` (`x-goog-api-key` auth, LINEAR16 PCM
  24 kHz mono wrapped into a `.wav`), `generate_music` posts
  `/interactions` with `{model, input}` and deep-searches the response for
  base64 audio; the image/video slots answer an honest "not supported for
  the Google provider yet" error.
- `docs/subagents/` — the subagents-2.0 + long-term-memory master plan
  (phased: memory package publish, memory foundation, memory-aware
  compaction, session-backed retained subagents, agent-type menu); update
  the checklists there as work lands.
- `site/` — static GitHub Pages landing; `.github/workflows/pages.yml`
  builds the web demo into `app/` (never committed). `site/privacy.html`
  is the published privacy policy (`PRIVACY.md` in the repo root is the
  source text — keep both in sync; the onboarding privacy page links to
  `https://fa1.dev/privacy.html`).
- `scripts/` — codegen and quality-gate scripts.
- `browser_ext/` — Chrome MV3 extension pairing fa with the browser
  (issue #23): `sw/main.js` (entry, pairing storage, panel API,
  optional embedded-agent boot), `sw/bridge.js` (wire protocol v1
  client), `sw/ops.js` (browserReq op table), `sw/tabs.js` (task tab
  groups), `sw/cdp.js` (chrome.debugger trusted path), `sw/agent.js`
  (dart2js build artifact, gitignored, absent → bridge-only scaffold),
  `content/content.js` (isolated-world DOM ops), `panel/` (side panel).
  `dart/` is the `fa_browser_agent` package (path dep) compiled into
  `sw/agent.js` by `scripts/build_browser_ext.sh` — agent host,
  chrome.storage ExecutionEnv, provider streams (incl. deterministic
  `fake:` provider), DAP hub client. Tests: `browser_ext/dart/test`,
  `browser_ext/test` (node --test), `test/browser_ext/` (headless
  Chrome, integration tag). Docs: docs/browser-extension.md.
- `lib/src/browser/` — pure-Dart browser half: `bridge_protocol.dart`
  (wire protocol v1 — frame codec, ops/error codes, one-time
  `pairingToken()`, `browser-ext/` mailbox ids, reconnect backoff, mail
  dedupe) and `browser_tools.dart` (the eleven exec-tier `browser_*`
  tools over the injectable `BrowserController`; failures throw
  `BrowserToolException` carrying the wire code). Availability family
  `browser` + `browser_eval` in `agent_cli_tools.dart`
  (docs/tool-availability.md); CLI surface in
  `lib/src/cli/browser_bridge_commands.dart` (`/browser connect|status`).
- `bin/serve_bridge.dart` — `fa serve --bridge [--port N] [--token T]`
  loopback WebSocket bridge (binds 127.0.0.1 only, AC15; non-
  `chrome-extension://` origins refused): one-time token pairing
  (`.fah/bridge/token`, 0600, minted fresh per `/browser connect`),
  two-way mail relay over the messaging fabric, ping/pong keepalive,
  `browserReq`/`browserRes` correlation for the `browser_*` tools. The
  CLI mounts the same server in-process via `bin/fah.dart`'s
  `_FaBrowserBridgeHandle` (also the `BrowserController` seam).

## Hard architecture rules

- `lib/` is pure Dart: **no `dart:io`** (must compile for web). The only
  `dart:io` entry points are `bin/` and `lib/io.dart`; file/process/network
  behind tools goes through the `ExecutionEnv` abstraction.

## Working in parallel: shared checkout + worktrees

The user's primary working directory is this checkout — they may be
running an LLM loop here at the same time as any agent. To avoid
contention and stale-context confusion, agents follow this policy:

- **Do not modify files in this shared checkout unless the user asked for
  it.** For review, `gh api` + `git fetch` + `git show` /
  `git diff origin/<branch>..HEAD` is enough — no local checkout needed.
- **Do not run heavy commands in the shared checkout** (`dart test` for
  the whole suite, full `dart analyze`). They compete with the user's
  own LLM loop for CPU/IO and can flip its context mid-task.
- **Do not `git stash` the user's WIP.** Their uncommitted changes are
  sacred; never pop, drop, or apply them. If a checkout conflict blocks
  you, ask the user before touching `git stash`.
- **If a worktree is genuinely required** (running `dart test`/`analyze`
  locally against a non-current branch, exploring a fresh PR head):
  - **Ask the user first** whether to proceed — by default, review
    should not need a worktree at all.
  - If the user agrees, create under `flutter_agent/.worktrees/<name>/`
    (the in-repo location they picked — dot-prefix keeps it visually
    separate in `ls`, and `rm -rf .worktrees/` cleans everything in one
    shot). Never in the parent directory `../<name>/`, which has been
    the source of orphan pollution before.
  - Use `git worktree add --detach .worktrees/<name> origin/<branch>`
    against the remote tracking ref — never the local branch ref, which
    the user may have moved on.
  - **`git worktree remove` the moment you're done.** Don't leave stale
    worktrees behind.
  - The `.worktrees/` directory should be in `.gitignore` (so worktrees
    never get committed by accident). Add it there **only after
    confirming with the user** — silent `.gitignore` edits conflict
    with parallel agents who may already have it dirty.
- **Communicate with peer agents through the inbox fabric**, not by
  reading or writing their working files. `agent_directory` lists
  siblings; `agent_message` to the relevant mailbox id. Their
  `memory/note/` files are theirs — leave them alone.

## Memory is part of the system prompt

Before starting any non-trivial task, **search memory first** to pick up
durable facts the user has accumulated across sessions — preferences,
gotchas, conventions, prior decisions. Cheap to query, often the
difference between re-deriving a rule and silently violating it:

- `memory_search <topic>` for keyword search across both scopes
  (project = this repo, committed; user = global, private). Read the
  top results before acting.
- `memory_list` to skim recent entries when the topic is fuzzy or the
  user asks "what do you know about X".
- Project-scope notes under `memory/note/` are **the user's** — read
  freely, write only via `memory_add` (which goes through the package's
  policy: durable facts, supersede via delete+add, never leave solved
  problems to rot).
- User-scope notes are global across projects — facts about the user,
  their environment, their habits. Always relevant.

Common things memory already covers for this repo: worktree placement,
shared-checkout rules, agent messaging fabric, known fragile files,
prior false-blame incidents. If the task touches one of those, search
before you start.

## Golden tests are MANDATORY for UI work

Any change to `flutter_app/lib/` UI code is INCOMPLETE until its golden
tests are done right:

1. **Coverage first.** New widget file → tests in
   `test/golden/<area>_golden_test.dart` + map entry in
   `golden_guard_test.dart` (hard-fails otherwise). Changed visuals →
   regenerate affected snapshots.
2. **Real fonts.** `setUpAll(ensureGoldenFonts)` in every golden file;
   placeholder-box glyphs = garbage, redo.
3. **Full frames.** Snapshots are marketing material: full app frames
   (`goldenSizeDesktop` 1280x800, `goldenSizePhone` 390x844) with realistic
   content — never a widget floating on black void.
4. **Determinism.** Fixed data/timestamps, existing-test fakes, no
   network/engines/infinite animations.
5. **Eyes on pixels.** After `--update-goldens`, OPEN every changed PNG
   (no tofu, no overflow, legible buttons/icons) before committing.
6. **Green gate.** `flutter test test/golden` passes; pre-commit runs it +
   `scripts/check_goldens.py` (which also hard-fails on ORPHAN snapshots —
   committed PNGs no test references; delete stale files, never let them
   rot in git history).

## Prompts live outside Dart code

- Every LLM prompt is a Markdown file under `prompts/**` (example app:
  `flutter_app/prompts/`). Never prompt string literals in `.dart` files.
- Format: YAML frontmatter (`name`, `description`) between `---` lines,
  then body; runtime placeholders are `{{name}}` tokens.
- After editing a prompt: `dart run scripts/gen_prompts.dart` rewrites
  `lib/src/prompts/prompts.g.dart` + `flutter_app/lib/prompts.g.dart`
  (generated, never edit by hand); `test/prompts/prompts_sync_test.dart`
  gates drift.
- Bundled Codex model catalog
  (`lib/src/providers/chatgpt_codex_models_data.dart`) is generated from
  `codex-rs/models-manager/models.json` via
  `dart run scripts/sync_codex_models.dart`. Re-run the script when
  codex-rs ships a new catalog so the picker / OAuth default stay current
  without a manual edit.

## Quality gates (pre-commit hook: `scripts/pre-commit`)

Issue #177 restructured CI so a PR pays only for what it touched. The hook
is a thin wrapper over `scripts/ci_fast_gate.sh`; CI is NOT the same
process — it implements the same stages as parallel jobs, and since
issue #194 its PR path classification runs through the SAME script
(`ci_fast_gate.sh --dry-run` in the `changes` job). One classifier, one
set of thresholds; parity is checkable by diffing the hook's
`GATE_STAGE <name> OK|SKIP` markers against the stages the CI changes job
logs for the same diff. Path filters: docs/markdown/prompts-only →
size+analyze; lib/bin/test/example/pubspec → +tests, coverage, CRAP,
jscpd + cross-module dupx; `test/integration/**` → + `integration-mock`
(issue #551: the no-key legs run per-PR); flutter_app/packages → +dupx,
flutter analyze+tests;
scripts/.github/crap4dart.yaml/unknown → everything (safe default, E2 —
holds in CI too). Hook-only extras: dart format self-heal of staged files
and `scripts/check_goldens.py --quick` (skipped for docs-only commits).

- `dart analyze` + dart format clean (explicit dirs — `yoclip/` is a
  standalone video workspace with its own toolchain); example app also
  `flutter analyze --no-fatal-infos --no-fatal-warnings`.
- `dart test` green (integration-tagged excluded from test-core — issue
  #551: `test/integration/**` changes also run the `integration-mock` leg
  per-PR: no-key legs via `MockLlmServer`, `--exclude-tags llm,browser-ext,
  perf,pty`; real-provider files carry `@Tags(['integration', 'llm'])` and
  run ONLY in the tag-only provider-smoke job and nightly with secrets;
  child-agent-spawn PTY tests carry `pty` and stay in nightly (issue #553).
  Tag-only CI = llm smoke + publish + binaries; nightly keeps the full
  real-provider suite (pty-integration).
- `cd flutter_app && flutter test --exclude-tags integration` green
  (includes golden suite; integration-tagged `test/cli_visual` runs in the
  nightly workflow + on demand).
- Line coverage of `lib/` ≥ 80% (full ratchet on main/nightly; PRs ratchet
  CHANGED lines via `scripts/diff_coverage.py`); jscpd duplication < 1%
  core `lib/`, < 3.7% `flutter_app/lib/` (ratchet — only tighten).
- Cross-module duplication (issue #487): clones spanning module roots
  (`lib`, `bin`, `flutter_app/lib`, `packages/*/lib`, `browser_ext/dart`)
  via `scripts/check_dup_cross_module.sh` — stage `dupx`, ONE jscpd
  invocation, marker-parity with the CI `guards` job (which also runs it
  for app-only PRs — cross-module clones are usually born app-side).
  Baseline 0.3013% of the combined ~283k lines, pinned 0.31 in
  `ci_fast_gate.sh` (ratchet — only tighten; the per-package gates above
  cannot see across module boundaries).
- CRAP ratchet (`crap4dart analyze`, tool pinned as
  `dart pub global activate crap4dart ">=0.10.0 <0.11.0"` — gh-1061;
  bounded so a future 0.11.0+ is adopted deliberately, never
  auto-floating), one config per package, thresholds are the current
  per-package max — only down from here:
  - core (`crap4dart.yaml`, sources `[lib, bin]`): **12.0** — three
    TUI-only dispatchers at CC 3 / 0% cov pending PTY tests (documented
    exception).
  - flutter_app (`flutter_app/crap4dart.yaml`, sources `[lib]`,
    issue #433/#475): **210.0** — measured by the SAME pipeline the CI gate
    uses (the two `flutter-tests` shards emit `--coverage`; the
    `app-crap-gate` job merges the lcovs and runs the pinned analyzer —
    widget-test coverage differs from the core's dart lcov, E2).
  - The app ladder (follow-up cards, NOT this one): 2450 → 870 → 702 → 650
    → 380 → 272 → 210 (done: `JsAppEngine._faCall` 2450→6 via map
    dispatch, `patch` 94→19, `_tokenize` 70→18; issue #475:
    `wasm_shell_git.run` route-table + 51 golden tests, app `main()`
    boot-phase helpers, `wasm_shell` beasts `_grepBuiltin` 812→covered
    parse helpers, `_runPipeline` 702→210, `_runStage` 600→380,
    `_parsePrimary` 506→210; issue #558: the wasm_shell pipeline engine
    (`_runStage` 380→4, `_runPipeline` 210→4, `_evalExpr` 182→3); issue
    #559: the wasm_shell builtins (`_xargsBuiltin` 272→7,
    `_TestEvaluator._parsePrimary` 210→3, `_trBuiltin` 210→4,
    `_testBuiltin` 132→9, `_duBuiltin` 132→3, `_expandTrSet` 110→gone,
    `_tacBuiltin` 90→6, `_pipBuiltin` 72→3 — spec/table-driven parse
    helpers in `wasm_shell_builtins.dart` + tested exec halves) → next
    milestone 210-class (`GemmaService.chatStream`, `_statBuiltin` 156,
    wide_layout 156 pair) → … → **40**, each step "fix the code, then
    lower the threshold" — never lower first.
  - Guards (`scripts/check_crap_guards.py`): README badge == config
    threshold for BOTH packages, generated-exclude parity between the
    two configs, and `--only-down` (a PR that raises any threshold vs
    the merge base is rejected in CI).
- Max 2800 lines per `.dart` file (`*.g.dart` exempt).
- CI layout: `changes` (path filter) → parallel `static`, `test-core`
  (3 duration-balanced shards, `scripts/test_shards.json`, rebalanced
  weekly from junit durations; shard jobs emit junit-shard-N artifacts and
  `shard_files.py` bin-packs manifest-missing new test dirs at runtime),
  `coverage-gate`, `guards`, `flutter-tests` (ubuntu; goldens stay
  host-locked in build-macos/nightly) → one aggregate `Quality gate`
  required check, plus `step-timings` (per-step duration telemetry) and a
  `watchdog` (>2x-baseline timeouts; a timed-out PR leg gets ONE
  empty-commit retrigger — never `gh run rerun`, which reallocates into
  the same degraded runner pool). `perf-gate` (issue #303) folds the #262
  DoD `dart test --tags perf` trajectory gate into the aggregate for
  trajectory-touching PRs. Issue #551 adds the `integration-mock` leg to
  the aggregate for `test/integration/**` PRs (no-key legs via
  MockLlmServer); the tag-only `integration` job is just the llm provider
  smoke and `publish` waits on it. `nightly.yml` runs the full monolith +
  PTY/CLI integration (full real-provider suite with secrets) +
  terminal-visual suites; `coverage-gardener.yml` measures the only-up CLI
  coverage weekly and files an ai-teammate "raise coverage to X" issue —
  the baseline itself only moves through a reviewed PR (issue #927).

## Cross-platform parity

Every shared setting and interactive prompt type must exist on BOTH the CLI
(`lib/src/cli/`) and the Flutter app (`flutter_app/lib/`) unless it is
**fundamentally impossible** on the target platform. When a setting or prompt
type is platform-only (e.g. MCP servers need process spawning — impossible on
web), it MUST be explicitly listed in `cliOnlySettings` or `appOnlySettings`
in `lib/src/parity/settings_registry.dart` with a comment explaining WHY.

**Workflow when adding a new setting or interactive type:**
1. Add it to `SharedSetting` enum in `settings_registry.dart` FIRST
2. Implement on BOTH platforms (CLI + Flutter app)
3. If one platform genuinely cannot support it: add to the exemption set
   with a one-line reason comment
4. Run `dart test test/parity/` — the parity guard must pass

**Capability rules (issue #1079, host-wiring SDK layer):** every NEW
host-wiring capability declares its matrix state AT BIRTH — for all seven
hosts (`cli`, `macos`, `ios`, `android`, `web`, `extension`, `outlook`) a
state: `on`, `off(reason)`, or `transport(choice, reason)` for a
partial/different transport. The record types, validation and transport
vocabularies live in `lib/src/hosts/host_capability_profile.dart`; the
seven built-in profiles — where those per-host cells are actually
declared — live in `lib/src/hosts/host_wiring_builder.dart`. The
matrix-completeness test rejects undeclared capabilities, so "CLI got X,
app didn't" cannot happen silently. A capability that is
`off` is INVISIBLE: hidden from the host's UI (menus, settings, slash
commands, tool palettes) AND from everything model-facing (tool schemas,
system-prompt sections, skills listings, help) — the agent never sees
what it cannot use and the user never sees a button that errors; on a
`transport` cell only the available transports surface. Profiles can only
NARROW what the platform floor allows: force-enabling a floored
capability throws `HostProfileViolation` at construction, never a runtime
surprise.

## Commits and releases

- Commit identity: human/AI contributors commit as `ai.teammate
  <agent.ai.native@gmail.com>` (history was rewritten to it — set
  `git config user.name ai.teammate` + `git config user.email
  agent.ai.native@gmail.com` repo-locally). Release commits from
  `scripts/auto_release.sh` are authored `fa-release-bot[bot]
  <fa-release-bot[bot]@users.noreply.github.com>` — the gh-1172 ruleset-
  bypass App pushes the bump straight to protected main, so the App identity
  is what auditability hangs on.
- Commit subjects: `type(scope): ...` (`feat:`, `fix:`, `fix(example):`,
  `ci:`, `test(providers):`, `refactor(prompts):`).
- Every push to `main` auto-releases a patch to pub.dev
  (`scripts/auto_release.sh` via `ci.yml`) — intended. The version scheme is
  `1.0.<counter>` (gh-785: App Store Connect approved 1.0.0 and enforces a
  monotonic `CFBundleShortVersionString`, so the pre-promotion `0.1.<counter>`
  train can never upload again); the core and app pubspecs bump in lockstep.
- CLI binaries build per tag (`ci.yml` `binaries` job), attach to the
  GitHub Release (`fa-<os>-<arch>[.exe]`); `installer-smoke` verifies
  installers.
- App builds (`build-mobile.yml`, `build-macos.yml`, manual dispatch):
  Android AAB + iOS IPA on `[self-hosted, macOS, ARM64]`, macOS DMG/ZIP
  signed + notarized, TestFlight via `flutter_app/fastlane`. Optional
  secrets: `APP_STORE_CONNECT_KEY_ID/_ISSUER_ID/_KEY_CONTENT`,
  `MACOS_INSTALLER_P12_BASE64/_PASSWORD/MACOS_INSTALLER_CERT_ID`,
  `GOOGLE_SERVICE_INFO_PLIST_BASE64`. Both workflows hard-gate the binary
  on wasm_run FFI exports (`xcrun dyld_info -exports` must list
  `_wire_compile_wasm` — Podfiles force-load + `-exported_symbol` +
  `STRIP_STYLE=non-global`, else white screen on TestFlight). Pods cached
  keyed by `Podfile.lock`. The gate proves FFI exports ONLY — a Dart boot
  crash before the first frame white-screens TestFlight with a green gate
  (issue #544: build 160, boot refactor created the platform env before
  `WidgetsFlutterBinding.ensureInitialized`); boot stage order is pinned by
  `flutter_app/test/boot/app_boot_test.dart`.
- TestFlight external distribution (issue #239): the `submit_only` lanes
  (driven by build-mobile.yml / build-macos.yml) distribute every build
  straight to the EXTERNAL group — REQUIRED repo variables
  `TESTFLIGHT_EXTERNAL_GROUP`, `BETA_REVIEW_CONTACT_EMAIL`,
  `BETA_REVIEW_CONTACT_PHONE`, `BETA_APP_FEEDBACK_EMAIL` (lanes fail loudly
  at start without them). Pilot must WAIT for processing (skipping the wait
  silently skips distribution); `verify_external_distribution!` asserts
  group membership bounded — beta-review-pending is a neutral notice, a
  build never reaching the group fails the leg (self-filed issue).
- Manual App Store review: `release-appstore.yml` (workflow_dispatch:
  `version` + `platforms` ios/macos/both + `confirm` repeating the version)
  runs the `submit_for_review` lanes — pure pre-flight in
  `flutter_app/fastlane/appstore_preflight.rb` (plain-ruby tested) fails
  BEFORE any mutation; already-submitted versions are a green no-op;
  release-after-approval stays a manual ASC click
  (`APP_STORE_AUTOMATIC_RELEASE` repo variable flips it, E5). Grep-guards:
  `test/store_automation_guard_test.dart`.
- App Store content pipeline (no binary): store screenshots are COMMITTED
  goldens from `flutter_app/test/golden/store_screenshots_test.dart` (frame +
  inline en/ru copy in `store_marketing_frame.dart`) at
  `flutter_app/test/goldens/store/{en,ru}/{ios,ipad,mac}/` — regenerate with
  `flutter test test/golden/store_screenshots_test.dart --update-goldens`,
  then open every PNG. Store copy lives in
  `flutter_app/fastlane/metadata/{ios,macos}/{en-US,ru-RU}/` (the lanes strip
  name.txt/subtitle.txt — App Info is not editable post-release); release
  notes come from the latest `## […]` section of
  `store_artefacts/metadata/{en,ru}/changelog.md`. Upload: `fastlane ios
  app_store` / `fastlane mac app_store` in `flutter_app` (env gates
  `IOS_DEPLOY_METADATA`/`IOS_DEPLOY_SCREENSHOTS`/`MACOS_DEPLOY_*`; ASC API key
  env secrets) or the `store-metadata.yml` workflow (`ios_content`/
  `macos_content` inputs: none/metadata_only/screenshots_only/all).
- Google Play listing pipeline (issue #289, no binary): the Play Console
  image rules are repo tests. Listing screenshots are the same story frames
  as COMMITTED GOLDENS straight into the supply tree
  (`fastlane/metadata/android/{en-US,ru-RU}/images/`): phone 1080×1920
  (exact 9:16, en+ru), 10-inch 1440×2560 (exact 9:16 — 1600×2560 is 10:16
  and Play rejects it; en only, ru falls back to phone shots), regenerated
  with `flutter test test/golden/store_screenshots_test.dart
  --update-goldens` then `git checkout -- test/goldens/store` (the run also
  refreshes the App Store goldens on a non-canonical host — restore them);
  the feature graphic (1024×500) is a widget golden in
  `test/golden/play_store_assets_test.dart`; the icon is the iOS master
  resized by `tool/generate_android_adaptive_icons.dart` (512×512, mirrored
  byte-identical to ru-RU). Sizes/aspects/file caps are enforced by
  `flutter_app/test/play_store_listing_guard_test.dart`. Listing texts
  (title ≤30, short_description ≤80, full_description) live in
  `fastlane/metadata/android/{en-US,ru-RU}/`. Upload: `fastlane android
  play_store` in `flutter_app` (env gates `PLAY_DEPLOY_METADATA`/
  `PLAY_DEPLOY_IMAGES`, `PLAY_VALIDATE_ONLY=1` dry-run; Play service-account
  secret `PLAY_STORE_SERVICE_ACCOUNT_JSON`) or the `store-metadata.yml`
  android leg (`android_content`: none/metadata_only/images_only/all). Images
  never ride supply (it only APPENDS uploads — issue #947): the `play_store`
  lane pushes them through `play_listing_sync.rb`, which replaces the whole
  image set per locale + device type (dropped sets are cleared) and verifies
  the committed goldens against `edits.images.list`. The daily
  `daily-publish.yml` play leg dispatches `build-mobile.yml` with
  `android_content=all`, so every release train redeploys the listing.