The canonical reference for how fstack is wired. Read once, refer back forever.
┌─────────────────────────────────────────────────────────────────┐
│ LAYER 3 — SKILLS (Markdown, read by Claude Code) │
│ ─────────────────────────────────────────────────────────────── │
│ 10 fstack-original + 7 brain-aware (modified gstack) │
│ + 10+ untouched gstack production skills │
│ + browser stack (/browse, /qa, /qa-only, /scrape, …) │
│ Skills shell out to `fstack-brain` via Bash for DB ops. │
└─────────────────────────────────────────────────────────────────┘
▲
┌─────────────────────────────────────────────────────────────────┐
│ LAYER 2 — RUNTIME (Bun + Claude Code hooks) │
│ ─────────────────────────────────────────────────────────────── │
│ `fstack-brain` CLI binary — single-shot subcommand dispatch │
│ Claude Code hooks — auto-fire CLI at lifecycle events │
│ Chromium daemon (inherited from gstack/browse) — for /qa /browse│
└─────────────────────────────────────────────────────────────────┘
▲
┌─────────────────────────────────────────────────────────────────┐
│ LAYER 1 — DATA (Supabase Postgres + Realtime) │
│ ─────────────────────────────────────────────────────────────── │
│ Graph-shaped schema. First-class entities + junction tables. │
│ Realtime publication on presence, intents, handoffs. │
└─────────────────────────────────────────────────────────────────┘
- Graph-shaped, not flat. First-class entities (
agents,repos,features,files,intents,decisions,branches) connected via FKs and junction tables. No JSON-blob dumping for relationships. - One brain, two writers. Every row tagged with
agent_id. No per-user partitioning; queries filter byagent_idfor attribution. - Append-only edits log.
editsis the single-source-of-truth for "who touched what when" — feeds/why,/resolve, conflict precheck. - Compiled-truth + timeline pattern (borrowed from gbrain).
decisions.bodyholds current understanding;decisions.timelineis an append-only JSONB array of events. Lets you see "this decision evolved over time" without losing the current state.
| Table | What | Why first-class |
|---|---|---|
agents |
one row per teammate (e.g. alice, bob) |
Attribution everywhere; permissioning hook for future |
repos |
One row per repo (canonical github.com/org/name form) |
Multi-repo support from day 1 |
features |
Domain tags per repo (auth, billing, matching) | Cross-cutting query axis |
files |
Every file we've ever touched | Attach features + ownership; referenced by edits |
branches |
Branch state per repo | Foreign-key target for intents |
intents |
What an agent is doing on a branch | The conflict-resolution payload |
edits |
Append-only edit log | Powers /why, regression precheck |
decisions |
ADRs with timeline | Long-lived knowledge |
handoffs |
Session-end notes | Cross-session continuity |
presence |
LIVE state, 5-min TTL | Real-time awareness |
| Junction | Lets you ask |
|---|---|
file_features |
"all files in the auth feature" |
decision_features |
"all decisions affecting billing" |
decision_files |
"all decisions about this file" |
intent_features |
"active intents in the matching feature" |
intent_decisions |
"what past decisions inform this intent" |
next_decision_number(repo_id)— atomic ADR numbering per repo.expire_stale_presence(threshold)— sweep heartbeats older than N minutes.upsert_file/branch/feature(...)— get-or-create with read-after-write semantics.
live_presence— presence + agent + intent + branch joined; only rows < 5min old.active_intents— non-shipped, non-abandoned intents joined to agent + branch.file_intents— distinct active intents that have edited a given file.decisions_by_feature— decisions grouped by feature, recent first.
fstack_realtime publishes inserts/updates/deletes on presence, intents, handoffs. Other agents' subscriptions get push updates within ~250ms.
Disabled in v1. Trusted two-person team, single shared anon key. agent_id is for attribution, not access control. Replace with proper policies if scaling beyond the team.
Single Bun binary (bun build --compile) compiled to ~/.local/bin/fstack-brain. Subcommand dispatch via process.argv. Each subcommand:
- Calls
buildCtx()— loads config, connects to Supabase, resolves repo + branch. - Performs its DB ops via the typed helpers in
brain/cli/src/client.ts. - Emits human-readable text on TTY, JSON on non-TTY (subagent consumption).
Subcommands:
| Command | Purpose | Called by |
|---|---|---|
doctor |
Health check (config + schema + agent registered) | User, setup |
sync |
Pull team digest | SessionStart hook + /sync skill |
heartbeat |
Write presence row | Background loop + /freeze etc. |
log-edit |
Append edits row + refresh heartbeat | PostToolUse Edit/Write/MultiEdit |
intent get/write/infer/ship |
CRUD on intents | /intent, UserPromptSubmit, /ship |
handoff write/auto/list |
Handoff CRUD | /handoff, SessionEnd |
conflict-precheck |
Diff vs other agents' intents | PreToolUse on git push |
presence |
List live other agents | /presence |
decide write/search |
ADR write + ILIKE search | /decide, /office-hours prefetch |
standup |
Activity digest | /standup, /retro, /queue |
why --target |
Look up decisions + edits for a file | /why, /resolve |
Wired by hooks/install.ts into ~/.claude/settings.json:
| Event | Matcher | Command |
|---|---|---|
SessionStart |
— | fstack-brain sync |
SessionEnd |
— | fstack-brain handoff auto |
UserPromptSubmit |
— | fstack-brain intent infer --prompt "$CLAUDE_USER_PROMPT" |
PostToolUse |
Edit |
fstack-brain log-edit --op edit --file "$CLAUDE_TOOL_INPUT_FILE_PATH" |
PostToolUse |
Write |
fstack-brain log-edit --op write --file "..." |
PostToolUse |
MultiEdit |
fstack-brain log-edit --op edit --file "..." |
PreToolUse |
Bash:git push* |
fstack-brain conflict-precheck |
All hooks are best-effort — fstack-brain exits 0 on missing config / unreachable Supabase. Hooks NEVER block Claude Code's main flow.
Untouched. Persistent headless Chromium + Playwright, ~100-200ms latency, accessibility-tree refs (@e1, @e2), 6-layer prompt-injection defense, dual-listener security model, macOS Keychain cookie import. Used by /qa, /qa-only, /browse, /scrape.
See BROWSER.md and browse/ for full details.
Same as gstack. YAML frontmatter (name, version, description, allowed-tools, triggers) + markdown instructions. Each skill is a top-level directory with SKILL.md. Claude Code auto-discovers them.
Three patterns for how skills interact with the brain:
- Pull on entry —
/office-hours,/office-review,/syncshell out tofstack-brain syncandfstack-brain decide searchto load context before reasoning. - Write on completion —
/ship,/decide,/intent,/handoffwrite rows after the user-visible action completes. - Pre-check —
/shiprunsfstack-brain conflict-precheckbefore the push to surface regression risk.
Skills NEVER directly query Supabase. They always go through fstack-brain. This keeps the data layer a single seam — change the schema, only the CLI changes.
Skills assume fstack-brain is on PATH. The setup script symlinks brain/cli/dist/fstack-brain → ~/.local/bin/fstack-brain. If ~/.local/bin is not on PATH, the user gets a setup-time warning.
agent_id: alice # your handle
brain_url: https://xxx.supabase.co
brain_anon_key: eyJ...
machine: alice-mbp
auto_upgrade: false # hardcoded false (we manage versions)
telemetry: off # hardcoded off (no outbound)Mode 0600. Loaded by brain/cli/src/config.ts. Required fields validated on every CLI invocation.
Optional. If a repo wants different hook behavior (eg. disable certain hooks for a sensitive area), repo-local settings override ~/.claude/settings.json. Generated by hooks/install.ts.
~/.fstack/
├── config.yaml # per-machine config (mode 600)
├── analytics/skill-usage.jsonl # local-only usage log (gstack-inherited)
├── projects/<repo-slug>/ # gstack-inherited project state
│ ├── retros/ # legacy retros
│ ├── timeline.jsonl # legacy event log
│ └── learnings.jsonl # legacy learnings
└── (state cache files)
~/.claude/
├── skills/fstack/ # if installed at this canonical path
└── settings.json # Claude Code hooks
The brain owns the new coordination state in Supabase. Inherited gstack skills (/retro, /review, etc.) still write some legacy data to ~/.fstack/projects/ (timeline.jsonl, learnings.jsonl) — harmless local-only logs. Eventually consolidate into the brain.
- Supabase unreachable — every CLI subcommand prints a one-line warning to stderr and exits 0. Hooks see this as success and don't block. Skills should detect missing brain data gracefully ("no brain context available, proceeding without").
- Config missing —
fstack-brain doctoremits actionable error. Hooks degrade silently (the user just doesn't get auto-/sync). - Schema mismatch —
doctorcatches it. CLI ops to missing tables return Supabase errors; surfaced via stderr. - Stale presence —
expire_stale_presencesweeps rows older than 5 minutes. Called insidesync. No background daemon required.
Considered: a long-running HTTP+MCP server that skills call as tools. Rejected: adds infra (a daemon to keep alive), and skills shelling out to a CLI gives identical functionality with zero ops cost. We're two trusted users on local machines, not exposing a public API.
Reconsider if we ever expose the brain to remote agents.
Considered: pgvector + OpenAI embeddings for hybrid search over decisions and intents. Rejected for v1: we'll have ~50 decisions and ~50 active intents at peak. Postgres ILIKE is plenty. Adding pgvector means another extension, embedding pipeline, staleness tracking — all overhead with no measurable retrieval benefit at our scale.
Reconsider when we have 1000+ decisions or notice ILIKE missing relevant results.
Considered: Supabase auth with auth.uid()-keyed RLS policies.
Rejected for v1: a small trusted team sharing one anon key. RLS adds operational overhead (Supabase auth users, JWT management) for zero security benefit when everyone with the key is already a trusted teammate.
Reconsider when we add a third person or take on client work.
Considered: a simple events(agent_id, type, payload jsonb) event log.
Rejected: the queries we actually want — "all decisions affecting auth", "intents that touched this file", "what's the other agent's current intent" — require join semantics. Flat JSON forces application-level joins in TypeScript. Junction tables make these one SQL query.
Considered: install gstack unchanged, build fstack as a separate skill pack alongside. Rejected: gstack ships 13 greenfield skills we'll never use, plus team-mode auto-update we explicitly want off. Living with the noise creates cognitive friction every session. Fork once, get a clean tree, sync via cherry-pick when there's actually something worth pulling.
- gen-skill-docs.ts overlay support — currently SKILL.md.tmpl regen would blow away our brain-aware overlays. Either teach the codegen about fstack overlays, or accept SKILL.md as hand-edited.
- Codebase index for
/office-hours— current heuristic (ripgrep + import follow) works up to ~50K LOC. Add a vector index when we hit that. - Symbol-level
/why— currently file-only. When the codebase is too big to grep for symbols, add a tree-sitter or LSP-driven symbol graph. - MCP server — if we expose the brain to remote agents (eg. cloud-based runs), wrap the CLI ops as MCP tools.
- RLS — when third user joins.