The full command surface for first-tree. Every command listed here is in
the shipped binary — first-tree --help (and first-tree <namespace> --help) are the canonical source of truth, this document is a
human-friendly index over them.
Keeping this file current. Any PR that changes the command surface (adds, renames, removes, or re-flags a verb / namespace) must update this file in the same PR. The grep checks that gate
Forbid legacy CLI / env namesonly catch a handful of retired identifiers; the broader "what commands exist and what do they do" contract is enforced by humans against this document.
Production:
curl -fsSL https://download.first-tree.ai/releases/prod/install.sh | sh
~/.local/bin/first-tree login <connect-code>Staging:
curl -fsSL https://download.first-tree.ai/releases/staging/install.sh | sh
~/.local/bin/first-tree-staging login <connect-code>The public shell installers support macOS and Linux and bundle Node.js. They
install channel-specific binaries under ~/.local/bin: first-tree / ft
for production and first-tree-staging / fts for staging. The full path in
the login command works immediately, before the current shell reloads PATH.
The two lines are intentionally independent and do not provide shell-level
transaction protection: when pasted together, an install-line failure does not
automatically prevent the login line from running, and POSIX sh does not
guarantee that curl | sh preserves a curl failure status.
For self-hosted deployments, use the two-line command returned by the web
console. It includes the server and portable download-base overrides when
needed. Development builds continue to use scripts/dev-install.sh and
first-tree-dev login <connect-code>.
| Flag | Effect |
|---|---|
--json |
Emit only machine-readable JSON on stdout; silence human status lines on stderr. |
--verbose |
Raise the log level to debug (overrides FIRST_TREE_LOG_LEVEL). |
--version |
Print the CLI version and exit. |
--help |
Print help for the command or namespace. |
first-tree
├── login <code> Sign this computer in or switch local clients
├── logout Stop the daemon and clear credentials
├── computer ... Computer-level local state recovery
├── status CLI + daemon + server + auth + agent overview
├── doctor Cross-subsystem readiness check
├── upgrade Self-update + restart the daemon
├── context ... Enable Team Context in external coding agents
├── agent ... Agent management (config, bindings, sessions, messaging)
├── chat ... Chats and messaging (create, send, list, history, open)
├── doc ... Org document library (publish, comments, reply, resolve, status)
├── cron ... Scheduled jobs in the current chat (preview, create, list, show, update, pause, resume, delete)
├── github ... GitHub entity attention
├── gitlab ... GitLab Issue/MR entity attention
├── org ... Organization-level operations
├── daemon ... Background daemon (start, stop, status, doctor, probe)
├── config ... View/modify this machine's client.yaml
└── tree ... Read, write, review, validate, and browse Context Trees
first-tree login <code> [--no-start] [--force-switch]
Sign this computer in using a short connect code from the web console. New
codes are exchanged against this CLI channel's default server URL
(first-tree → production, first-tree-staging → staging, first-tree-dev →
local dev), with FIRST_TREE_SERVER_URL as an explicit override for custom
deployments. Connect URLs are not accepted; only legacy JWT tokens with an
iss claim remain accepted during rollout.
If this machine already has credentials for another user, login asks for
explicit confirmation and switches the active local client after stopping and
draining the old runtime. In non-TTY automation, --force-switch is the only
confirmation flag; it does not skip supervisor, drain, filesystem, or journal
safety checks. If credentials are missing, login preserves client.yaml and
local agent state so the same user can reconnect after a normal logout.
| Flag | Effect |
|---|---|
--no-start |
Write credentials and exit without installing/starting the background daemon. |
--force-switch |
Confirm a different-user local client switch in non-interactive mode. Safety gates still run. |
first-tree logout [--purge]
Stop the daemon and clear credentials. --purge additionally removes active
root client state, parked clients under $FIRST_TREE_HOME/parked-clients/, and
switch lock/journal files. This is a destructive local reset path, not the
normal account-switch path. To switch this computer to another First Tree user,
run first-tree login <code> with the new user's connect code and confirm
the switch. Before deleting local state, --purge retires the current server
client so it disappears from default Computers views and cannot be reactivated
with the same client id. Retiring is destructive for runtime routing on that
client: non-deleted agents pinned to it are suspended and unpinned, while agent
identity, chats, history, and profile data remain; those cleared agents can be
moved back onto a connected computer/runtime from the Agent Runtime tab. Logout
stops both the background service and any live daemon start --foreground
runtime markers for the active client before clearing credentials/state. If the
daemon is active and cannot be stopped, a foreground runtime cannot be stopped,
or the server-side retire fails, --purge refuses to delete local client state.
The default keeps local client/agent state for the same user to reconnect later.
Computer-level local state recovery.
first-tree computer
└── reset
first-tree computer reset
Stop the daemon and remove active root client state, parked clients under
$FIRST_TREE_HOME/parked-clients/, switch lock/journal files, and active
credentials. Use this when local identity state is damaged or when you
intentionally want to discard every local First Tree client stored in this
installation. This is local-only and does not retire server client rows. Normal
different-user switching should use
first-tree login <code> instead, which parks inactive clients.
first-tree status
Single-screen overview: CLI version, daemon state, server reachability, auth health, and the agents this client manages.
first-tree doctor
Cross-subsystem readiness check covering the daemon, server reachability,
WebSocket, and configured agents. Use this when status flags something
red and you want a guided drill-down.
first-tree upgrade [--check] [--no-restart]
Self-update for the CLI: query the configured server for its recommended
Command version when a server URL is configured, install that exact version
through the current install mode, refresh the supervisor definition on top of
the new bits, then restart the client service. If no server URL is configured
yet, upgrade falls back to the current channel's latest release data directly
so the update path still works before login/config. Portable installs download
the channel manifest and verified tarball, including the bundled Node.js
runtime. Existing npm-mode installs retain their package-manager update path
and continue using the system Node.js runtime.
| Flag | Effect |
|---|---|
--check |
Only check for an available version; print "update available" or "already on latest". Do not install. |
--no-restart |
Install the new version and refresh the supervisor definition, but leave the running service alone. Used for staged rollouts. |
Refusing to run from a source checkout (anywhere under a .git
ancestor) is intentional — it keeps a dev build from accidentally overwriting
a hosted-channel installation. For local development use
scripts/dev-install.sh (see docs/development/local-dev-isolation.md).
For an existing npm-mode installation, upgrade checks the target package's
engines.node metadata before install when npm can provide it. If the target
requires a newer Node.js than the current process is running, the command fails
before install with a system-Node upgrade hint and a shell-installer migration
hint. npm-mode updates do not replace Node.js themselves.
Agent management — local config, bindings, sessions, messaging debug helpers.
first-tree agent
├── list [--remote] [--org <id>]
├── add --agent-id <uuid>
├── create <name> --type <t> --client-id <id> [--runtime <r>] [--display-name <s>] [--org <id>]
├── remove <name>
├── prune [--yes] [--dry-run]
├── status [name]
├── reset <name>
├── config <subcommand>
├── bind <subcommand>
├── workspace <subcommand>
└── session <subcommand>
first-tree agent list # locally-configured agents on this client
first-tree agent list --remote # every agent the signed-in user manages on the server
first-tree agent list --remote --org <id> # cross-org view (multi-org operators)
first-tree agent create <name> --type <human|agent> --client-id <thisClient> [--runtime claude-code|claude-code-tui|codex|cursor|kimi-code]
Creates the agent row on the server and binds it to the given client
machine. The local agents/<name>/agent.yaml is written by the running
daemon via the server-pushed agent:pinned frame; no second command
needed if the daemon is already up.
first-tree agent add --agent-id <uuid>
Register an existing server-side agent on this client. Use this when the daemon was not running at the moment the agent was pinned, or when moving an agent to a second computer that's already signed into the same user.
first-tree agent remove <name> # delete local config dir, workspace, session state
first-tree agent prune [--yes] [--dry-run] # remove every local alias the server no longer pins to you
prune is the counterpart to daemon doctor's "stale aliases" warning.
first-tree agent status # all agents this client manages
first-tree agent status <name> # one agent's runtime view from the server
first-tree agent reset <name> # reset agent error state to idle
Mutate the agent's server-side runtime configuration (model, reasoning,
Codex service tier, prompt, MCP servers, env, repos). Edits the
agent_configs.payload JSONB row
through the Admin API.
first-tree agent config
├── show <agent>
├── set-model <agent> <model> # alias: opus | sonnet | haiku, or full id (e.g. claude-opus-4-7)
├── set-reasoning-effort <agent> <level>
├── set-service-tier <agent> <tier> # Codex: default (Standard), fast (Fast), or provider-advertised id
├── prompt show <agent> [--raw] # per-agent prompt fragment; --raw is verbatim (round-trippable)
├── prompt set <agent> [-f <file>] [--force] # replace the fragment ONLY; reads stdin if no file.
│ # Rejects copies of the assembled AGENTS.md (generated marker /
│ # briefing headings); --force overrides the heading heuristic.
│ # Does NOT cover inline replacements of team prompts — those are
│ # resource bindings, managed in Cloud → Org Settings → Resources.
├── append-prompt <agent> [-f <file>] # deprecated alias of `prompt set`
├── add-mcp <agent> --name <id> --transport <t> [--command <c> --args <a>... | --url <u>]
├── set-env <agent> KEY=VALUE [--sensitive]
├── add-repo <agent> <url> [--ref <branch>] [--path <local>]
└── dry-run <agent> -f <patch.json> # validate + diff, no persist
Reasoning effort values are provider-specific. Claude Code and Claude Code
TUI accept "" (inherit the operator's local setting), low, medium,
high, or max. Codex accepts low, medium, high, xhigh, max, or
ultra; availability of the higher levels is model-dependent and rejected
combinations are reported by the provider.
Codex service tiers are passed to the provider unchanged. default selects
Standard mode and fast selects Fast mode; unsupported model/account
combinations fail visibly without a silent fallback. Codex may report its
canonical priority id after accepting the fast request alias; both represent
the same Fast selection.
first-tree agent bind
└── client <agentName> --client-id <id> # first-time bind only; later moves use managed runtime switch
first-tree agent workspace clean [agent-name] [--ttl <days>]
Remove stale workspace directories (older than the TTL with no active session). Without an agent name, sweeps every local agent.
first-tree agent session
├── list <agent-name> [--state <active|suspended|evicted|errored>]
├── suspend <agent-name> <chat-id>
└── terminate <agent-name> <chat-id>
Day-to-day messaging.
first-tree chat
├── create [message] # create a separate task chat and write its first message
│ --to <name> # initial recipient to mention + wake; repeatable
│ --with <name> # context participant; added silently, not woken by the first message
│ --topic <text> / --description <text> # initial chat self-description
│ --request # first message is a tracked ask; the body IS the ask, decision-self-sufficient (why + recap + question + recommendation); exactly one --to human
│ --options <json> / --multi-select # (with --request) 2–4 options {label,description,preview?}; allow multi-pick
├── send <name> [message] # wake a participant — agent or human (a send to a human is informational only; a question the next step depends on goes through `chat ask`)
│ # body: [message] arg, or stdin (omit [message]), or -F <path>; prefer stdin/-F for rich bodies (shell-safe)
│ -F, --message-file <path> # read the body from <path> (`-` = stdin); content never hits the shell
│ --reply-to <messageId> # thread a reply under a message (pure threading)
├── ask <name> [message] # ask a HUMAN a tracked question; the body IS the ask, decision-self-sufficient (why it exists + recent-context recap + question + recommendation)
│ # body: [message] arg, or stdin (omit [message]), or -F <path>; prefer stdin/-F for rich bodies (shell-safe)
│ -F, --message-file <path> # read the body from <path> (`-` = stdin); content never hits the shell
│ --options <json> # 2–4 answer options {label (1–5 words), description, preview?}; omit for free-text
│ --multi-select # allow picking more than one option (requires --options)
│ # always a fresh top-level question — no threading, and no resolve flag
│ # (the human answers in the web UI; an agent can only ASK)
├── invite <agentName> # add to FIRST_TREE_CHAT_ID before same-task send
├── list
├── history <chatId>
├── archive [chatId] # archive from the signed-in user's Active view; defaults to FIRST_TREE_CHAT_ID
│ --agent <name> # selected agent must participate in the target chat
├── update # update topic and/or description (each independently)
│ --topic <text> / --clear-topic # set/clear the short display label
│ --description <text> / --clear-description # set/clear the work summary + status report (Markdown; `-` = read from stdin/heredoc)
│ --chat <chatId> / --agent <name> # target another chat / the named agent
├── set-topic [topic] # [DEPRECATED — use `update`] hidden alias
└── open <agent-name> # interactive REPL
# Split off separate work into a new task chat and write the first message.
# --to recipients are mentioned and woken; --with participants are added for
# context but receive only silent initial history. This is not an empty-chat or
# same-task handoff tool.
first-tree chat create "Please review the rollout plan." --to code-agent --with reviewer-agent \
--topic "rollout review" \
--description "reviewing rollout plan; waiting on code-agent"
# Start a new task chat with a tracked question. The first request must target
# exactly one human. The message body IS the ask and must be
# decision-self-sufficient (why the question exists + recent-context recap +
# the single question and your recommendation); pass 2–4 --options (JSON) for
# a clean pick, or omit them for a free-text answer.
cat <<'EOF' | first-tree chat create --to alice --request \
--options '[{"label":"Ship","description":"Roll the migration now"},{"label":"Hold","description":"Wait 24h"}]'
## Why this question exists
Migration 0021 drops the legacy column — irreversible, so shipping is your call.
## Recent context
The 0021 cleanup you asked for last week is done; the PR is approved and CI is
green.
## The question
Ship the destructive migration now? I would ship — the column has had no reads
for 30 days.
EOF
# Inline — `chat send` wakes a participant (agent or human). A plain send to a
# human is informational only — readable, then safely ignorable; any question
# the next step depends on goes through `chat ask` (a send never carries a
# blocking question). The recipient must be a participant of FIRST_TREE_CHAT_ID.
first-tree chat send code-agent "ship the PR"
# Stdin (multiline, markdown, special chars)
echo "long body" | first-tree chat send code-agent -f markdown
# Rich / multi-line bodies: write to a file, then read it with --message-file
# (or `-F`). This is the most robust form — the body never passes through the
# shell, so backticks (`code`), quotes, apostrophes, and newlines are sent
# byte-for-byte. Inlining such a body lets the shell run backticks as command
# substitution and break on quotes, silently mangling the message.
first-tree chat send code-agent -f markdown --message-file reply.md
first-tree chat send code-agent -f markdown -F - < reply.md # `-` = stdin
# Inline bodies must carry REAL newlines. A one-line quoted body written with
# `\n` escapes — chat send code-agent "line1\n\n**title**" — is rejected
# BEFORE anything is sent (ESCAPED_NEWLINES, exit 2): shells do not expand
# `\n` inside quotes, so the literal backslash-n would be stored and the
# message would render as one long unformatted line. The error prints a
# copyable heredoc retry form on stderr; resend via stdin:
cat <<'EOF' | first-tree chat send code-agent -f markdown
first line
**second** line
EOF
# Stdin bodies are never checked — piping is also the escape hatch for
# intentionally sending literal `\n` text.
# Embed a workspace image — a markdown image `` in a `chat send`
# body whose target is an image (png/jpeg/gif/webp) inside the agent's own
# workspace is uploaded at send time and delivered as a real inline chat image
# (the same shape a human composer upload uses), so recipients see the picture
# instead of a broken local path. Only explicit `` embeds are
# captured (a bare filename is left as text), only the sender's own workspace,
# and an image shown inside a block code sample the renderer recognizes (a
# fenced block at any container depth, or an indented code block) is left as a
# literal sample (an image written inside inline `code` is treated as a live
# embed). Capture is best-effort and never blocks the send, and is skipped
# entirely for a body longer than ~1 million characters (then sent verbatim). An image
# that is too large (>10 MB), unreadable, or beyond the 20-per-message cap is
# skipped: if no image in the message captured, the body is sent unchanged (the
# skipped embed stays as text); if at least one sibling image did capture — so
# the message becomes an image send — every workspace-image embed is removed
# from the caption, so a skipped one is dropped rather than left as a path that
# would render broken.
echo 'Latest run: ' | first-tree chat send code-agent -f markdown
# Ask a human a tracked question (red-dot + blocks the chat for them until they
# answer). `chat ask` targets a single human; the message body IS the ask and
# must be decision-self-sufficient for a reader who remembers nothing of the
# chat: why the question exists + a recap of the recent interactions + the
# single question and your recommendation, written for a reader holding none
# of the context (unpack every shorthand; name options by their concrete
# consequence). Omit --options for a free-text answer, or pass 2–4 --options
# (JSON) for a clean pick; add --multi-select to allow more than one.
cat <<'EOF' | first-tree chat ask alice \
--options '[{"label":"Ship","description":"Roll it now"},{"label":"Hold","description":"Wait 24h"}]'
## Why this question exists
Migration 0021 drops the legacy column — irreversible, so shipping is your call.
## Recent context
You asked for the 0021 cleanup yesterday; the PR is approved and CI is green.
## The question
Ship the destructive migration now? I would ship — the column has had no reads
for 30 days.
EOF
# Free-text ask (no options) — the same three-section body, answered in free
# text. `-F` reads the body from a file (shell-safe — same rationale as
# `chat send -F`)
first-tree chat ask alice --message-file ask-body.md
# `chat ask` always opens a fresh top-level question — there is no threading
# (no --reply-to) and no resolve command. An agent can only ASK — it cannot mark a question
# answered or close it. The human resolves it by answering in the web UI; a moot
# question is simply left open (the human works open questions oldest-first), and
# re-asking opens a NEW, independent question.
# Pull a non-member into the current chat first, then send normally. Use this
# for same-task stage / role handoffs.
first-tree chat invite code-agent
first-tree chat send code-agent "now we can talk"
# Browse
first-tree chat list
first-tree chat history <chatId>
# Archive a chat from the signed-in user's Active workspace view. The selected
# agent must be a speaking participant, so the eligible set is exactly the
# structural scope exposed by `chat list`. Omitting the id targets
# FIRST_TREE_CHAT_ID. The write is private to the signed-in user: it does not
# archive the chat for other participants or change membership. It is
# idempotent, and a later message automatically returns the chat to Active.
first-tree chat archive <chatId>
first-tree chat archive
# When archiving the current chat on a user's behalf, send the completion reply
# first and run `chat archive` as the final action. Any reply sent after the
# archive is new chat activity and immediately returns the chat to Active.
first-tree chat send alice "Done — I’m archiving this conversation now."
first-tree chat archive
# Self-description: a short topic label + a work summary + status report,
# updated independently through `chat update` (topic and description each on
# their own). The description carries task background + plan + progress, renders
# as Markdown, and shows at the top of the chat's right sidebar; agents also read
# it via `chat list` to self-locate (see the agent briefing's "Chat Topic &
# Description"). Keep blockers / decisions OUT of it — raise `chat ask <human>`
# for those. Owner-gated: the chat's creator may update it, and when
# no agent owner is present (human-created chats — Web / GitHub-sourced — or the
# creator left) every worker agent counts as the owner; a non-owner agent in a
# chat whose agent creator is still present is refused with 403.
first-tree chat update --topic "review PR #916"
first-tree chat update --description "Reviewing PR #916. **Plan:** address review findings, re-verify. **Progress:** 2/3 findings fixed."
first-tree chat update --topic "ship plan" --description "Drafting; next: hand to QA."
first-tree chat update --clear-description
# A one-line --description whose newlines are written as literal `\n` is rejected
# before the write: shell quotes do not expand `\n`, so it would persist and
# render as one long line with visible `\n` tokens. For a multi-line description
# pass real newlines — either an ANSI-C $'...' string, or `--description -` to
# read it from stdin/heredoc:
cat <<'EOF' | first-tree chat update --description -
Reviewing PR #916.
**Plan:** address review findings, re-verify.
**Progress:** 2/3 findings fixed.
EOF
# `chat set-topic` still works as a deprecated alias.
# Interactive
first-tree chat open code-agentchat send / chat invite operate on the chat identified by
FIRST_TREE_CHAT_ID, which the runtime injects into the agent's session
environment. The recipient must be a participant of that chat; if not,
invite first.
chat archive also defaults to FIRST_TREE_CHAT_ID, but accepts an explicit
chat id from chat list. The list is a structural membership inventory and may
continue to include a chat after archival; archival changes the signed-in
user's private Workspace engagement state, hiding the row from their default
Active view until new chat activity revives it.
chat create is different: it creates a new task chat and writes the first
message in one command. Use it to split genuinely new work into a fresh chat.
Use chat send for replies/status in the current chat, and chat invite when
you want to add a non-member to the current chat before sending there. A
same-task handoff, such as architect to developer or developer to reviewer,
stays in the current chat; invite the next agent and send the handoff there.
Ordinary task creation is intentionally not idempotent. There is no operation
id, and the CLI does not automatically retry it. If an ordinary create reports
an unknown result, check chat list or the Web UI before running it again.
If a non-human agent includes itself in chat create --to, the server records
the originating agent in metadata and uses that agent's manager human as the
effective sender so the first message can wake the agent normally.
Org document library (docloop) — publish markdown design docs for team
review, pull the structured comments reviewers leave, reply, resolve, and
track document status. Feature-flagged server-side
(FIRST_TREE_DOCS_ENABLED); commands report HTTP 404 while the flag is off.
Publishing is idempotent on slug: the first publish creates the document
(version 1), every later publish of the same slug appends the next version.
The caller's own identity signs every write — agents author under their own
agent name, humans under their member identity.
first-tree doc
├── publish <file> [--slug <slug>] [--title <t>] [--project <p>]
│ [--note <n>] [--status <s>] [--if-changed] # create or append a version
├── get <slug> [--version <n>] # read metadata + markdown content
├── list [--project <p>] [--status <s>] [--limit <n>] [--cursor <c>]
├── comments <slug> [--status open|resolved] [--version <n>]
│ [--watch [seconds]] # list; --watch streams new ones as JSON lines
├── comment <slug> <body> [--quote <exact> [--prefix <t>] [--suffix <t>]] [--version <n>]
├── reply <commentId> <body> # reply in a thread
├── resolve <commentId> [--reopen] # close (or reopen) a thread
├── status <slug> [--set draft|in_review|approved|archived] # show or move status
├── import <dir> [--project <p>] [--status <s>] [--dry-run] # bulk-publish a directory of .md files
└── export <dir> [--project <p>] [--status <s>] # dump library to <slug>.md files + manifest.json
first-tree doc publish design.md --slug chat-rename --project first-tree --status in_review
first-tree doc comments chat-rename --status open --json
first-tree doc reply <commentId> "Addressed in v2 — see §3"
first-tree doc resolve <commentId>
first-tree doc publish design.md --slug chat-rename --note "responds to review round 1"
first-tree doc status chat-rename --set approvedSlug defaults to the slugified filename; title defaults to the file's first
markdown heading (required on the first publish). Comment anchors are
TextQuoteSelector-style (exact / prefix / suffix) against the markdown
source, so an agent can locate every comment in the file it holds without
line-number conventions. Comments whose quote no longer exists in the latest
version come back with outdated: true (computed on read). import skips
NODE.md / README.md index files and is idempotent (re-runs only add
versions for changed content); export is the guaranteed way out — plain
markdown files plus a manifest.json of metadata.
Scheduled jobs in the current chat. At due time the Server writes one
ordinary addressed markdown message to wake this agent. Every cron command
(including preview, list, and show) requires FIRST_TREE_CHAT_ID from
the agent session; prompt bodies use -F <file> or -F - only. The scheduler
starts with the Server and uses FIRST_TREE_POLLING_INTERVAL_SECONDS as its
polling cadence. The Server validates that cadence at startup in the 1..10
second dispatch-safe range.
first-tree cron
├── preview --schedule <expr> --timezone <iana>
├── create --name <name> --schedule <expr> --timezone <iana> -F <prompt.md|->
├── list
├── show <jobId>
├── update <jobId> [--name ...] [--schedule ...] [--timezone ...] [-F <prompt.md|->]
├── pause <jobId>
├── resume <jobId>
└── delete <jobId>
first-tree cron preview --schedule "0 9 * * 1-5" --timezone America/New_York
first-tree cron create --name daily-triage --schedule "0 9 * * 1-5" --timezone America/New_York -F ./prompt.md
first-tree cron list
first-tree cron show <jobId>
first-tree cron pause <jobId>
first-tree cron resume <jobId>
first-tree cron delete <jobId>GitHub entity attention for the current chat. follow wires an entity's
webhook event stream into the chat (one routing line, chat-scoped);
unfollow explicitly stops this chat from tracking the entity and severs
every line wired into the chat for that entity, however it was created.
Creating a PR or issue never follows it automatically — declare the
dependency explicitly, immediately after creation. Use
first-tree github follow --help / first-tree github unfollow --help
for the full flag surface and conflict handling.
first-tree github
├── follow <entity> [--chat <chatId>] [--rebind] # route the entity's events into the chat
├── unfollow <entity> [--chat <chatId>] # sever all of the chat's lines for the entity
└── following [--chat <chatId>] [--json] # list entities wired into the chat
# Inside an agent session the chat is inferred from FIRST_TREE_CHAT_ID
first-tree github follow https://github.com/acme/api/pull/42
first-tree github follow acme/api#42 # issue vs PR resolved automatically
first-tree github follow acme/api@3f2a91c # commit
first-tree github following
first-tree github unfollow acme/api#42<entity> accepts a full GitHub URL, owner/repo#N, or owner/repo@<sha>.
A 409 means the same (human, delegate) line already lives in another chat
— --rebind MOVES it here (a line is never duplicated). unfollow is
idempotent: removed: 0 is success, not an error. Requires the org's
GitHub App installation to cover the repo (422 otherwise).
GitLab Issue and Merge Request attention for the current chat. The commands
operate entirely on First Tree's local webhook projection: they never call the
GitLab API, validate an entity live, or use the current glab account.
first-tree gitlab
├── follow <issue-or-mr-url> [--chat <chatId>] [--agent <name>] [--rebind]
├── following [--chat <chatId>] [--agent <name>]
└── unfollow <issue-or-mr-url> [--chat <chatId>] [--agent <name>]
# Inside an agent session the chat is inferred from FIRST_TREE_CHAT_ID
first-tree gitlab follow https://gitlab.example/acme/api/issues/42
first-tree gitlab follow https://gitlab.example/acme/api/-/merge_requests/42 --rebind
first-tree gitlab following
first-tree gitlab unfollow https://gitlab.example/acme/api/issues/42follow accepts only a full Issue or Merge Request URL from the Team's one
configured GitLab instance. Both GitLab route shapes—with or without the /-/
segment—are accepted, and the submitted URL is preserved for user-facing
links. Repeating a follow for the same entity in the same chat refreshes that
link to the latest submitted URL while remaining idempotent. The command
records a pending declaration without provider egress; the next matching valid
webhook supplies numeric project identity and activates the declaration. The
same human/delegate pair cannot follow the entity from a second chat: the
command reports the existing room, and --rebind atomically moves that line
when the task context intentionally changes. Different pairs remain
independent. A pending
declaration reports state: null because First Tree has not verified provider
state. There is no GitLab context-review command.
following returns every active binding in the chat as a stable public
projection, including automatic reviewer / assignee / mention routing and
explicit agent_declared / human_declared rows. Pending declarations and
active webhook-observed bindings report their corresponding status. Internal
connection, organization, mapping, actor, identity, and normalized-path
identifiers are not returned.
unfollow is URL-based and idempotent: removed: 0 is terminal success. It
removes every automatic or manual binding for that entity in the current chat.
A later explicit reviewer, assignee, or mention event may create a new route.
After a project rename, use the current URL returned by following; the
inbound-only service cannot resolve an arbitrary old path back to a numeric
project identity.
These commands control First Tree chat attention. Native GitLab subscribe/unsubscribe operations control only the authenticated GitLab account's personal notifications and are not a replacement for chat follow.
first-tree org
├── bind-tree <url> [--org <orgId>] [--branch <branch>] # legacy caller-org binding write
└── context-tree [--agent <name>] # read the current agent org's Context Tree binding
├── review-config [--agent <name>] # read live binding + Reviewer assignment for a local Agent
│ --as-member [--org <orgId>] # logged-in member; no running Client Runtime, local Agent, or active Computer connection required
└── set <repo> [--branch <branch>] [--agent <name>] # set the selected agent org binding
bind-tree records the team's Context Tree URL in
organization_settings(context_tree). Used by the onboarding flow's
"create new tree" path, where the agent calls back into the server
after scaffolding the tree. It is retained for compatibility with existing
scripts: without --org, it resolves the caller's default organization through
GET /api/v1/me; with --org, it targets the explicitly supplied organization
ID. --branch is optional and sends an explicit branch when a recovery path must
reproduce an exact repo/branch binding; when omitted, the server preserves the
existing branch or defaults to main. It is not agent-scoped and is separate
from context-tree set below.
The Class B settings read GET /api/v1/orgs/:orgId/settings/context_tree
returns the same runtime-safe binding representation for admins and members. If
a row has no repo and a valid retained branch, this safe read returns the
unbound branch-only representation. If a loose historical repo or branch is not
valid under the active binding contract, the safe read fails without returning
the raw value. Loose historical rows that are visible for repair are exposed
only through the admin-only raw read
GET /api/v1/orgs/:orgId/settings/context_tree/raw.
first-tree org context-tree [--agent <name>]context-tree is a read-only view of the Cloud context_tree setting for the
selected agent's organization. It does not accept --org: the CLI sends the
selected agent as X-Agent-Id, and the server derives that agent's
organization. The command reads only GET /api/v1/agent/context-tree/info.
It never falls back to the user's default organization from /me, the legacy
/api/v1/context-tree/info endpoint, the web app's current organization, or a
local workspace manifest or checkout.
The selected agent is resolved in this order:
--agent <name>selects that named local agent.FIRST_TREE_AGENT_IDselects the local agent whose configured UUID matches the environment value.- When exactly one local agent is configured, that agent is selected.
Selection fails before any network request with exit code 2 when there is no
local agent (MISSING_AGENT), more than one candidate (AMBIGUOUS_AGENT), an
environment UUID that is not local (ENV_AGENT_NOT_LOCAL), or an unknown
explicit name (UNKNOWN_AGENT). An explicit --agent takes precedence over
FIRST_TREE_AGENT_ID.
Human output reports one of three states. Bound includes the resolved or
persisted provider, repository, and branch. Unbound advises the user to ask
an administrator for that agent's
organization to bind an existing tree or initialize a new one. Unreadable
means the agent-scoped request failed or its response could not be validated; a
failed read is never reported as Unbound. A loose invalid historical setting
is projected as inactive by the agent/runtime endpoint, while the safe settings
GET returns a non-secret conflict and the admin raw endpoint preserves the
value for repair.
With --json or FIRST_TREE_JSON=1, successful output is exactly one of:
{"ok":true,"data":{"status":"bound","provider":"github","repo":"git@github.com:acme/context-tree.git","branch":"main"}}
{"ok":true,"data":{"status":"unbound","repo":null,"branch":null}}repo alone determines binding state. An unbound response is normalized to
branch: null even if the server supplies its default branch. A bound response
with a null branch is normalized to "main".
Authentication, connection, timeout, remote, response-validation, and unexpected read failures use the following JSON error shape and a non-zero exit code:
{"ok":false,"error":{"code":"CONTEXT_TREE_UNREADABLE","message":"...","status":"unreadable"}}Authentication failures exit 3; connection and timeout failures exit 6;
other remote or invalid-response failures exit 1. Agent-selection failures
retain exit code 2 and their existing error envelopes.
first-tree org context-tree review-config [--agent <name>]
first-tree org context-tree review-config --as-member [--org <orgId>]Without --as-member, review-config reads the bound repository/branch and
Context Reviewer assignment from the same agent-scoped server response. It
reports Off, Assigned, or Not assigned for the selected runtime Agent.
With --as-member, it uses the existing member-readable context_tree and
context_tree_features settings and /me Team selection. An explicit --org
must be one of the caller's active memberships; otherwise the current default,
then sole-membership fallback, is used, and ambiguous multi-Team state fails
closed. This is the Write preflight path after standard CLI login: member
credentials and client.yaml remain present, but no running Client Runtime or
daemon, local First Tree Agent, or active Computer connection is required.
--as-member conflicts with --agent; --org requires --as-member.
The result includes the live provider. The command contains no review mode, generation, governance, or merge-method setting: Context Review uses the currently assigned Reviewer and current-state configuration semantics.
first-tree org context-tree set <repo> [--branch <branch>] [--agent <name>]context-tree set directly sets or replaces the Cloud context_tree binding
for the selected local agent's organization. It does not accept --org or
--rebind, and it provides no unset/clear operation. Agent selection uses the
same precedence and exit-code-2 failures as the read command above:
explicit --agent, then FIRST_TREE_AGENT_ID, then the only configured local
agent. Selection failures retain their existing MISSING_AGENT,
AMBIGUOUS_AGENT, ENV_AGENT_NOT_LOCAL, or UNKNOWN_AGENT envelopes; they
are not wrapped as CONTEXT_TREE_UPDATE_FAILED. Selection is completed before
the SDK is created or any credentials or network are accessed.
The command performs a two-step, agent-scoped write. It first sends
GET /api/v1/agent/me with the selected agent to obtain a non-empty
organizationId, then sends the existing admin-only Class B request
PUT /api/v1/orgs/:orgId/settings/context_tree, URL-encoding orgId. The
selected agent identity, current user JWT, and current runtime-session token are
used for both requests. This write flow does not call either Class B settings
GET and never falls back to /api/v1/me, the legacy
/api/v1/context-tree/* endpoints, the web app's current organization, a local
workspace manifest, or a local checkout. The agent-profile GET may use the
client's normal read retry behavior. The PUT is never retried automatically, so
one invocation cannot repeat a settings-version increment after an ambiguous
transport failure.
<repo> accepts HTTPS, ssh://, and scp-like SSH repository coordinates. The
value must have a host and repository path, contain no embedded credentials,
have no surrounding whitespace, and contain no control characters. URL forms
must use literal https:// or ssh:// syntax; queries, fragments, backslashes,
and local drive paths are rejected. HTTP and git:// URLs are rejected.
--branch must be a valid Git branch name. In addition to being non-empty,
single-line, and free of surrounding whitespace and control characters, it
must satisfy Git ref-format rules such as rejecting .., @{, components
that start with . or end with .lock, a branch name that starts with -,
and forbidden ref characters. Invalid repo and branch values fail locally with
INVALID_CONTEXT_TREE_REPO and
INVALID_CONTEXT_TREE_BRANCH, respectively, exit 2, and make no SDK,
credential, or HTTP request.
For example, these repository forms are accepted:
https://github.com/acme/context-tree.git
ssh://git@github.com/acme/context-tree.git
git@github.com:acme/context-tree.git
https://gitlab.company.example/group/subgroup/context-tree.git
git@gitlab.company.example:group/subgroup/context-tree.git
For a GitLab repository, the Team must already have a current GitLab
connection for the same exact web origin. Saving the binding does not require
Cloud egress authority: GitLab Webhook routing and Agent-local workflows remain
usable when Web Context cannot read the repository. Web Context supports
gitlab.com by default; deployment operators authorize other exact origins
through FIRST_TREE_GITLAB_ALLOWED_ORIGINS. An unauthorized origin does not
trigger an outbound request. The Settings page deliberately keeps repository
and branch editing available to admins in this release; every later Write,
Review, and Web Context operation rereads the live binding and fails closed if
it changed.
When --branch is omitted, the request body contains only { "repo": "..." }
and an existing valid branch is preserved. On a first binding, the server's
default branch is main. Supplying --branch replaces the branch. If a loose
historical row contains an invalid branch, a repo-only update is rejected
without a partial write; repair it by supplying both the repository and a
valid --branch. A successful response must explicitly contain a valid repo
and branch, must echo the requested repository, and must echo a provided
branch; missing fields, unknown fields, mismatches, and otherwise invalid
responses are update failures.
Human output reports Bound and shows the repository and final branch. With
--json or FIRST_TREE_JSON=1, successful output is exactly:
{"ok":true,"data":{"status":"bound","provider":"github","repo":"git@github.com:acme/context-tree.git","branch":"main"}}After local agent selection and input validation, all authentication, connection, timeout, HTTP, response-validation, and unexpected failures use this exact error envelope:
{"ok":false,"error":{"code":"CONTEXT_TREE_UPDATE_FAILED","message":"..."}}Authentication failures exit 3; connection and timeout failures exit 6;
403, other HTTP failures, and invalid or inconsistent responses exit 1.
When a network or server failure leaves the PUT result uncertain, the message
directs the operator to rerun first-tree org context-tree with the same agent
selection before retrying. Failure output never prints raw response bodies,
tokens, credentials, or a full private repository coordinate; successful
output includes the requested repository as documented above.
For this write command, debug logs may identify the selected agent, request phase, derived organization, and final status. Warning logs contain only the sanitized failure category, exit code, and HTTP status; they do not contain secrets or raw response data.
The background service that holds the client WebSocket and runs every
configured agent on this machine. Installed automatically by first-tree login on supported desktop platforms: launchd on macOS, systemd on
Linux, and a per-user Task Scheduler logon task on Windows. Linux installs
use a systemd --user unit for normal users; when the CLI is run as root,
First Tree installs the same channel's unit in system scope instead
(/etc/systemd/system/<channel>.service) so daemon setup does not depend
on a root user D-Bus session.
On Windows, Task Scheduler only owns per-user logon/start triggering. A hidden First Tree supervisor loop owns the daemon child process, exit-code restart policy, stop intent, runtime marker PID, and logs. This is not a Windows Service / WinSW install and does not run before the Windows user logs in.
first-tree daemon
├── start [--no-interactive] [--foreground]
├── stop
├── restart
├── status
├── doctor
├── probe [--no-upload] [--json]
├── install-codex [--spec <spec>] [--json]
└── install-claude [--spec <spec>] [--json]
| Subcommand | Purpose |
|---|---|
start |
Start the daemon and connect every configured agent to the server. Fail-closed: exits 1 with NO_CREDENTIALS if no credentials.json exists; run login first. --foreground runs in the current shell (for debugging); the default installs/starts the service. |
stop |
Stop the service (preserves auto-start; bring it back with start). |
restart |
Restart the service. |
status |
Local service state + server binding + auth health. Runs in well under a second. |
doctor |
Walk Node version, config, server reachability, WS, agent registrations, the installed service file, and the runtime providers — each step reported. The runtime-provider rows run the real launch-verified probe (a 1-turn model call for claude-code, a codex doctor handshake for codex), so doctor makes live provider calls; it is a deliberate diagnostic, not a hot path. |
probe |
Launch-probe the local runtime providers on demand and upload the result to the server (PATCH /clients/:id/capabilities). This is the manual refresh for a client's advertised capabilities after a provider is installed / logged in. Each probe really launches its provider. --no-upload runs a credentials-free local-only diagnostic (probe + print, no server auth needed). --json (or the global --json) emits the capability snapshot as the machine-readable { ok, data } envelope on stdout. |
install-codex |
Install the native Codex runtime engine on this machine (npm install -g @openai/codex). First Tree does not bundle the ~225MB native codex binary by default — the runtime resolves an external codex from PATH, known install locations, or the macOS ChatGPT/Codex desktop app — so this is the on-demand remediation when the codex capability probes as missing. Runs the same tracked-subprocess install path as self-update, then re-probes so the freshly installed binary is reflected. Purely local (no credentials). --spec <spec> picks an npm dist-tag or exact version (default latest); --json emits the post-install capability snapshot as the { ok, data } envelope. |
install-claude |
Install the native Claude Code runtime engine on this machine (npm install -g @anthropic-ai/claude-code). First Tree does not bundle the ~210MB native claude binary by default — the runtime resolves a system claude (env override / PATH / well-known install dirs) — so this is the on-demand remediation when the claude-code capability probes as missing. Runs the same tracked-subprocess install path as self-update, then re-probes so the freshly installed binary is reflected. Purely local (no credentials). --spec <spec> picks an npm dist-tag or exact version (default latest); --json emits the post-install capability snapshot as the { ok, data } envelope. |
Capability refresh timing. The daemon launch-probes runtime providers at
startup and re-probes automatically on every WebSocket reconnect. A full real
re-probe of all providers runs only when there is no prior snapshot or one is
older than 24h; otherwise each provider is re-validated individually — a
still-launchable, still-logged-in provider keeps its prior ok for free
(resolve + auth re-checked, no session smoke re-run), a provider that lost its
binary or login downgrades, and a non-ok provider is fully re-probed so it can
recover. So a machine missing an optional provider (e.g. no tmux for
claude-code-tui) does not re-smoke its healthy providers on every reconnect.
While the daemon stays connected, it also runs a bounded background
re-probe whenever any provider is not yet ok — so installing or logging into
a provider is noticed within a bounded time without a restart or reconnect. The
poll starts ~15s after the degraded state is seen and backs off to a 5-minute
ceiling, re-probes only the non-ok providers (already-ok ones stay on the
free cached path), uploads only when the snapshot actually changes, and stops
once every provider is ok. daemon probe remains the manual, on-demand path
to force an immediate full re-probe + upload.
The top-level first-tree status is the cross-subsystem overview that
calls daemon status internally and adds server/auth/agent rows.
Read and write this machine's client.yaml. The file lives at
~/.first-tree/config/client.yaml (or the staging/dev channel's
equivalent — see docs/development/local-dev-isolation.md).
first-tree config show # every key/value
first-tree config show server.url # dotted-key read
first-tree config show --show-secrets # un-mask sensitive fields
first-tree config set update.policy auto
first-tree config get update.policy # alias for `show <key>`Agent-side runtime configuration (model / prompt / MCP / env / repos) is
not here — it lives in first-tree agent config ... and mutates the
server-side agent_configs row through the Admin API.
Manage the user-scope First Tree Context Plugin for Claude Code or Codex. Installation is machine/user scoped, while activation is explicit per provider and code checkout. The handoff Team is never inferred from Git remotes or other memberships.
first-tree context
├── enable --provider claude-code|codex --team ID [--yes]
├── status --provider claude-code|codex
├── repair --provider claude-code|codex
└── disable --provider claude-code|codex [--all] [--yes]
Run context enable from the target code repository using the exact command
generated by that Team's Web Setup or invitation continuation. It validates
the current origin against the selected Team, installs the provider Plugin
through the provider's official user-scope lifecycle, and records only the
exact provider + checkout/repository → Team binding in
$FIRST_TREE_HOME/config/context.yaml. It does not create a First Tree Agent,
Chat, Computer, or runtime session; normal login and the existing daemon own
the Computer connection.
context status checks the local provider/version/Plugin manifest, the actual
provider-installed Skill/Policy/hook bytes, and the current checkout's live
Team activation. context repair revalidates and reinstalls only First Tree's
Plugin through the same durable operation journal used by enable/disable, with
rollback to the prior provider cache on failure. Context mutations and local
Client account switching are mutually exclusive; incomplete operations retain
the exact Computer identity and must be recovered under that same account.
An installed-but-disabled Plugin fails before mutation because its prior
enabled state cannot be restored through the supported provider CLI. Because provider
CLIs retain their local marketplace reference,
First Tree keeps that source under
$FIRST_TREE_HOME/state/context/providers/<provider>/marketplace; the
installed Plugin cache remains provider-owned. context disable removes the
current checkout binding; --all removes every binding for that provider,
uninstalls First Tree's Plugin, and removes the retained marketplace source
when nothing still uses it. Provider-native hook trust remains provider-owned.
context activate is an internal SessionStart bridge and is intentionally
hidden from help. It always allows the provider session to continue, even when
First Tree is offline or activation fails.
The external Plugin also uses hidden context read and context write
bridges. They do not accept --team: each derives Team from the exact current
provider + checkout binding, repeats live membership and selected-Team
repository validation, and only then delegates to the ordinary tree read or
tree write implementation. Managed and clean explicit-Team callers continue
to use the public tree commands directly.
Context Tree task-read activation, source-backed write and Seed preflight,
GitHub App-backed review publication, provider-aware creation/adoption,
structural validation, and hierarchy browsing. The tree namespace carries read, write, review, seed,
verify, tree, and init; the rest (migrate / upgrade / status /
codeowners / claude-hook / inject / automation / skill groups) was
retired in the 2026-06 cleanup because the cloud now owns workspace + tree
provisioning and the client runtime inlines its own skill payload install.
first-tree tree
├── init --team ID --provider github|gitlab \
│ --repo URL --branch BRANCH \
│ (--create|--adopt) # initialize through local gh/glab + git
├── read --team ID --snapshot DIR # activate one exact task read snapshot
├── write --team ID --snapshot DIR \
│ [--github-login LOGIN] # provider-aware authoring preflight
├── review --run ID --event EVENT \
│ --body-file PATH # publish one GitHub App review for the live PR head
├── seed --team ID \
│ [--confirm-source URL] \
│ [--expected-source-key KEY] # preflight and optional Admin-confirmed source batch
├── verify [--tree-path PATH] # validate a Context Tree repo
└── tree [path] [-L depth] [-P pattern] # browse Context Tree nodes as a hierarchy
first-tree tree read is the BYO Working Agent activation boundary for one
task. Both --team <team-id> and --snapshot <new-directory> are required.
The Team is always explicit: the command does not read a Web selection, the
account's default organization, a local Agent, or a prior task receipt.
Activation performs one member-authenticated Server request to
GET /api/v1/orgs/:orgId/settings/context_tree. That Class B route resolves
the URL Team's current active membership and current safe Context Tree binding;
the activation disables transport retries so this authority check is not
repeated after a transient response.
Only after it succeeds does the CLI create staging state, execute one strict
git fetch for the bound branch, resolve the fetched ref to an exact commit,
and atomically publish a detached snapshot at the requested path. The final
snapshot has no mutable Git remote and carries local Git metadata identifying
the Team, binding, and commit. The destination must not already exist; the
command never overwrites or reuses a prior task snapshot. Tracked symbolic
links materialized as filesystem links are accepted only when a relative link
resolves inside the snapshot to a regular file tracked by the same exact
commit; directory, absolute, dangling, escaping, or untracked-target links
fail before content is returned. When Git uses core.symlinks=false, an index
symlink is instead a regular file containing the link blob. The snapshot
accepts that file only when its raw object id still equals the exact index
entry and treats it as opaque content rather than following it as an alias.
Authority, invalid/unbound binding, fetch, commit resolution, and snapshot failures are distinct fail-closed stages. None falls back to cached content, a managed workspace checkout, a stale local branch, or another Team. Error messages and JSON envelopes do not include access credentials or raw upstream responses. Stable failure codes are:
CONTEXT_TREE_READ_INVALID_INPUTCONTEXT_TREE_READ_AUTHORITY_FAILEDCONTEXT_TREE_READ_BINDING_INVALIDCONTEXT_TREE_READ_UNBOUNDCONTEXT_TREE_READ_FETCH_FAILEDCONTEXT_TREE_READ_COMMIT_FAILEDCONTEXT_TREE_READ_SNAPSHOT_FAILED
If a later hierarchy read detects a removed marker, changed HEAD, or dirty
worktree, it fails before returning content with
CONTEXT_TREE_READ_SNAPSHOT_INVALID.
With global --json, success returns one envelope whose data is
{ teamId, binding: { repo, branch }, commit, snapshotPath }. Human output
reports the same identity. For the rest of the task, run hierarchy selectors
inside snapshotPath and read Markdown only from that root. A new task uses a
new path and a new activation so membership, binding, and branch movement are
observed.
first-tree tree write is the stateless BYO Write preflight for a source-backed
change. It requires the explicit Team and the existing exact snapshot created
by tree read. A GitHub binding also requires the current local gh login via
--github-login; a GitLab binding instead verifies local glab
authentication for the exact current connection origin, including a
non-default HTTPS port. It does not read a Workspace
manifest, managed briefing, setup-chat transcript, Web selection, account
default/current Team, local Agent, or prior task receipt.
The command sends one member-authenticated request to
POST /api/v1/orgs/:orgId/context-tree/write-preflight. The Server reads the
live bound Tree and assigned Context Reviewer as one current tuple, verifies
the requester's active Team/human identity and provider-specific forge
authority, and checks that the Reviewer is an active non-human Agent. GitHub
verifies the linked login; GitLab requires the bound repository origin to
match the Team's current GitLab connection. The request cannot select a
Reviewer, task key, Chat, sender, topic, review, or merge authority.
After Server admission, the CLI requires the current binding to match the snapshot identity (canonical repository plus exact branch), strictly fetches the bound branch into temporary state, and requires its remote tip to equal the snapshot commit. It then re-verifies that the snapshot is still clean and fixed at the same commit. The command removes temporary fetch state and performs no remote mutation. It is safe to run at activation and repeat immediately before the first push or PR creation; each run observes Server current Reviewer state.
With global --json, success returns
{ provider, teamId, binding, baseCommit, snapshotPath, reviewerAgentUuid, requesterGithubLogin, gitlabInstanceOrigin }. Human output reports the same
provider authority without exposing a credential. Stable local and
Server failure codes distinguish invalid input/snapshot, explicit-Team
mismatch, authority or identity denial, unavailable/unsupported binding,
invalid configuration, unavailable review/Reviewer, changed binding, fetch
failure, and a stale snapshot base. Failure creates no PR or Context Reviewer run.
The returned Reviewer UUID is diagnostic only: callers must not cache or route
from it, because the provider webhook resolves the Server current Reviewer
again.
Authoring happens in a separate task worktree/branch created from
baseCommit; the exact read snapshot remains immutable. After a ready PR/MR
exists, the forge webhook creates or reuses its stable provider-scoped
Reviewer Chat and trusted review run. Writers do not create or wake a review
Chat, repair the change, publish a verdict, or merge. GitHub uses the App
webhook and App review path. GitLab uses a processable Merge Request event from
the Team's inbound System or Project Hook; a processed System Hook MR remains
the connection-readiness requirement. The Reviewer reads and mutates GitLab
with host-local git and glab credentials. Project Hook Notes can supply
ordinary entity attention but never start Context Review, so a Reviewer note
cannot self-trigger another run.
first-tree tree review is the GitHub App publication command and is available
only inside an active GitHub Context Reviewer runtime session. It accepts a
Server-authored run id, one of APPROVE,
REQUEST_CHANGES, or COMMENT, and a UTF-8 body file (- reads stdin). It
does not accept a head or alternate agent. The Server re-resolves the live
installation, bound repository, pull request, run, configured Reviewer and
current PR head before the GitHub App creates the review. Unauthorized
submissions fail closed. The command never merges. After a successful
APPROVE, the Reviewer takes the exact full SHA only from that response's
data.reviewedHead and uses its local GitHub identity for one immediate REST
squash-merge compare-and-set: PUT repos/<owner/repo>/pulls/<number>/merge
with sha=<reviewedHead> and merge_method=squash. It does not use
gh pr merge, --admin, --auto, a substitute head, a fallback merge path or
a mutation retry. An unconfirmed mutation permits at most one read-only pull
request GET; the Reviewer reports merged, open or unknown only from the
resulting evidence. Real merge-queue, ruleset, permission and transport
behavior remains a live GitHub QA boundary rather than a deterministic-stub
security claim. GitLab Context Review never calls this command and never
simulates an approval, status, label, ruleset, CODEOWNERS gate, merge queue, or
admin bypass. A blocker is reported through one local-identity MR note. A
clean ready MR is squash-merged exactly once with
glab mr merge --sha <reviewed-head> (or the same-identity Merge Requests API
only when it enforces the sha compare-and-set). Unsupported exact-SHA CAS and
head, credential, pipeline/protection, deterministic-validation, or
unknown-result failures all fail closed; an unknown merge permits one
read-only reconciliation and no mutation retry.
Existing Context Tree repositories should require pull requests, at least one
current approval, and stale-review dismissal after every push. An administrator
must update an existing repository ruleset with local gh api; the GitHub App
does not change repository rules. Preserve non-fast-forward protection, disable
Code Owner and last-push approval requirements, and verify the resulting live
ruleset before relying on the merge gate. Also remove the retired
first-tree/context-review required status check from every effective ruleset:
the App-review-only workflow no longer publishes that status, so merely adding
the approval ruleset while retaining the old check leaves the repository
unmergeable.
The one-approval gate intentionally prevents a single GitHub user from self-merging while the App Reviewer is unavailable; the PR needs the App or another human approval. Do not compensate by broadening App permissions, restoring CODEOWNERS, or adding a bypass actor.
From the Context Tree checkout, an administrator should first identify the
existing effective rulesets. Repository rulesets are edited in place; do not
create a second named ruleset when the repository already has a default-branch
ruleset such as protect main:
remote=$(git remote get-url origin)
repo=$(gh repo view "$remote" --json nameWithOwner --jq .nameWithOwner)
gh api "repos/$repo/rulesets?includes_parents=true&per_page=100" \
--jq '.[] | [.id, .name, .source_type, .source, .enforcement] | @tsv'For each repository-owned ruleset that targets the default branch and contains the pull-request gate or retired status, set its id explicitly and update it without replacing unrelated rules. The transformation below preserves the ruleset name, enforcement, conditions, bypass actors, deletion/creation/ non-fast-forward rules, and unrelated required status checks. Repeat it for each applicable repository-owned ruleset:
ruleset_id=<repository-ruleset-id>
ruleset_input=$(mktemp)
ruleset_update=$(mktemp)
gh api "repos/$repo/rulesets/$ruleset_id" >"$ruleset_input"
jq '
.rules |= (
map(
if .type == "pull_request" then
.parameters |= (. + {
required_approving_review_count: 1,
require_code_owner_review: false,
dismiss_stale_reviews_on_push: true,
require_last_push_approval: false,
required_review_thread_resolution: false
})
elif .type == "required_status_checks" then
.parameters.required_status_checks |=
map(select(.context != "first-tree/context-review"))
else . end
)
| map(select(
.type != "required_status_checks" or
(.parameters.required_status_checks | length) > 0
))
)
| {
name,
target,
enforcement,
bypass_actors,
conditions,
rules
}
' "$ruleset_input" >"$ruleset_update"
gh api --method PUT "repos/$repo/rulesets/$ruleset_id" \
--input "$ruleset_update"
rm -f -- "$ruleset_input" "$ruleset_update"If no repository-owned default-branch ruleset exists, create the Seed ruleset
instead of running the update above. A ruleset whose source_type is
Organization is inherited and cannot be changed through the repository API;
its organization ruleset owner must apply the same approval/stale-review and
retired-status changes.
After the update, inspect every effective default-branch ruleset and confirm
that the combined policy requires at least one approval, dismisses stale
reviews on push, blocks non-fast-forward updates, and contains no
first-tree/context-review required status. Do not delete or overwrite
unrelated organization policy while removing the retired check.
first-tree tree seed --team <team-id> is the stateless admission boundary for
portable Context Tree setup. The Team is required and explicit; this command
does not consult a Workspace manifest, managed briefing, prior setup Chat,
Web selection, or account default/current Team. The Server resolves the signed-in
member's active role and the selected Team's current binding on every call.
An active Admin receives either the exact bound repo/branch or the current
unbound branch. An active ordinary member receives the stable
CONTEXT_TREE_SEED_NEEDS_ADMIN response with the selected Team as the recovery
anchor. Invalid historical binding data fails closed. The preflight creates no
repository, binding, branch, PR, Chat, review, or merge state and disables
transport retries; setup agents repeat it explicitly immediately before each
remote mutation.
After the Admin confirms the complete source set, repeat --confirm-source
for every selected repository and --expected-source-key for every active
Team repository observed before confirmation. This performs one atomic,
optimistic Settings → Repositories batch after the Admin preflight. A new Team
omits the expected-key flag. The batch is idempotent, never retires omitted
repositories, and returns 409 when another Admin changed the active set.
Provider-aware Seed uses:
first-tree tree init \
--team <team-id> \
--provider github|gitlab \
--repo <exact-repository-url> \
--branch <branch> \
--create|--adoptAll five choices are explicit. The command never infers the provider or target
repository from the current directory, Web selection, account default, or
GitLab hostname. --create requires the exact remote to be absent, scaffolds
and locally verifies the tree, then creates and pushes it with host-local
gh/glab plus git. --adopt requires the exact remote and branch to be
readable, clones it without pushing or overwriting history, and requires
tree verify to pass. GitLab nested namespaces and Self-Managed hosts are
preserved. GitHub may include --with-workflow; GitLab never creates a GitHub
Actions workflow, approval rule, ruleset, CODEOWNERS gate, or App setup.
The Server Seed preflight is repeated before remote mutation and final binding. An existing binding is an idempotent success only when provider, canonical repository, and branch all match; provider-aware Seed never replaces another binding. New finalization persists the explicit provider. A changed binding, branch, Team authority, forge credential, or remote state fails closed. If a create/push response is partial or final binding is unknown, the command reports the exact possibly-created remote, performs at most one read-only binding reconciliation, and never claims rollback or deletes a repository.
For compatibility, invoking first-tree tree init without the provider-aware
flags retains the legacy GitHub-only path through the 0.5.x line. This
compatibility path and its --owner, --name, --org, --no-bind, and
--rebind flags are deprecated and will be removed in First Tree 0.6.0;
automation must migrate to the explicit
--team --provider --repo --branch --create|--adopt contract before that
release. No new provider behavior is added to the legacy implementation.
That path creates a brand-new team
Context Tree repository with the user's local gh: it creates the repo (one
path for user- and org-owned repos),
scaffolds a minimal valid tree (root NODE.md + members index + a creator member
node), self-verifies before pushing, pushes, and — unless --no-bind — binds the
org's context_tree setting and surfaces guidance for adding the repo to the
team's GitHub App installation. It does not seed .github/workflows/validate-tree.yml
by default (that needs the interactive workflow gh scope); pass --with-workflow
to include it. In the bound path the repo is created under the team's GitHub App
installation account (so the installation can cover it), and any explicit --owner
is canonicalized before the remote create so case-variant input does not fail after
GitHub has already accepted the repository. If create succeeds but binding or
finalization fails, the CLI says the repo was created but not bound, includes the
repo URL, and tells you to delete it manually if it is empty; it does not auto-delete
created repositories by default. tree init refuses to replace an existing team
binding unless --rebind is passed. Non-rebind finalization is conflict-safe: if
another writer binds the org after preflight but before the local GitHub side
effects finish, the server preserves that competing binding and tree init
reports the conflict rather than overwriting it. A non---rebind invocation
requires a Server with the raw repair/finalize surface during preflight; older
Servers fail before any GitHub repository is created. Key options: --owner,
--name, --title, --public, --dir, --with-workflow, --no-bind,
--rebind, --org, --team. Run first-tree tree init --help for the full list.
The legacy portable GitHub path uses tree init --team <team-id> without
--provider, --repo, --branch, --create, or --adopt. In that mode
--team cannot be
combined with --org, --no-bind, or --rebind; an existing binding is an
idempotent success returned before local tool checks, and the command never
replaces it. For an unbound Team the CLI repeats the Server Seed preflight before
GitHub create/push and again before final binding. A concurrent binding is
preserved, an authority or branch change fails closed, and a lost finalization
response is reconciled against the Server's current binding. Failures after
GitHub mutation name the created or possibly-created repository and never claim
that it was rolled back. The legacy managed --org/default-org path remains
available for existing workflows.
The Seed workflow records merged Phase 1 authority in
<tree>/.first-tree/progress.md: one versioned Seed marker, the checked Phase 1
line, the explicit Team id, sorted canonical source identities with the exact
commits used for evidence, and the approved top-level domains. A later process
or agent resumes Phase 2 only after a fresh tree seed --team result matches
the Tree origin/branch, that exact branch is strictly fetched, the progress
ledger matches the same explicit sources, and every recorded commit remains
readable. Transcript text, private caches, a familiar directory shape, and the
current source head are not recovery authority; any binding, role, source
identity, or recorded-commit mismatch fails closed.
Before any GitHub repository write, the bound path reads the admin-only raw
Context Tree setting. An HTTP 404 from that endpoint identifies an older Server
without conditional-finalization support, so the command uses the legacy safe
settings GET to classify the current state. A valid active binding still gets
the normal existing-binding refusal; if the fallback confirms an unbound state,
a non---rebind invocation fails before looking up the GitHub App installation
or creating a repository and requires a Server upgrade. With --rebind, the
caller has explicitly authorized replacement and may continue through the
compatible legacy write. Invalid fallback data and every other raw-read failure
remain fail-stop.
A valid branch-only setting remains unbound. Its branch is retained exactly and
used for git init, the generated validation workflow filter, the final
repo-and-branch binding, and the success summary; an absent setting uses
main. Any invalid historical repo or branch fails closed before repository
creation so an administrator can repair it without leaving remote side
effects.
Without --rebind, final binding uses the dedicated admin-only endpoint:
POST /api/v1/orgs/:orgId/settings/context_tree/initialize
Content-Type: application/json
{"repo":"https://github.com/acme/tree.git","branch":"trunk","expectedUnboundBranch":"trunk"}The Server acquires the organization settings parent lock and commits only if
the setting is still unbound at exactly expectedUnboundBranch, the branch read
during preflight. A competing full binding or branch-only change returns 409
and is left unchanged. This dedicated endpoint also prevents an older Server
from interpreting conditional finalization as an ordinary unconditional
settings write. --rebind intentionally bypasses this compare-and-set path and
uses the generic PUT /api/v1/orgs/:orgId/settings/context_tree with only
repo and branch to replace the binding directly. The CLI strictly validates
the final response before reporting success.
Repository creation and push happen before final binding, so a finalization
failure can leave the new repository unbound. When that happens, the CLI says
the repository was created but not bound, includes the repo URL, and tells you
to delete an empty repo manually instead of relying on the command to clean it
up. A 409 from the non-rebind finalization POST has a known conflict outcome:
the competing setting was preserved, and the CLI requires reading the
organization's current Context Tree setting first without suggesting a retry or
overwrite command. A finalization 404 during a rolling Server change reports
that no binding was written and requires an upgrade plus read-back, also
without an overwrite command. Other HTTP failures require read-back before
retrying. A network failure, timeout, or invalid/unconfirmed response has an
unknown write outcome and likewise requires read-back first.
The error identifies the exact organization, repository, and retained branch.
For an unknown outcome it also shows the exact recovery form first-tree org bind-tree <repo> --org <orgId> --branch <branch>, but explicitly makes running
it conditional on read-back first confirming that the setting is still
unbound.
first-tree tree verify applies the current strict structural policy. Normal
content requires parseable YAML frontmatter with non-empty title and owners;
description and soft_links, when present, must have valid shapes. The
separate member-node contract remains in force, while archive/supporting and
repo-infrastructure Markdown are not treated as normal nodes.
Broken soft_links, tree-local path escapes, and normal links into
raw-context/ fail verification. Markdown links are parsed structurally, but
an otherwise tree-local Markdown target may be absent; external links, anchors,
and plain prose that explains the archive class remain allowed. JSON output
preserves the existing summary and adds stable findings plus per-class scan
counts. Context Tree domain directories must not be symlinks; Markdown file
symlinks are validated, must resolve to regular Markdown files, and must stay
inside the tree without crossing content classes, except for the managed
root-level WHITEPAPER.md pointer. That historical runtime-managed pointer
remains exempt for compatibility only; writers must not add it to new trees.
This is a breaking tightening for trees that relied on legacy metadata
or normal-to-archive links: mechanical syntax can be corrected directly, while
ownership assignments and promotion of durable archive content require human
or source-backed decisions. Run first-tree tree verify --help for options.
first-tree tree tree [path] resolves path relative to the current
working directory, then renders from the current git repository root down
to that target directory and its descendants. Without path, the target is
the current directory. The target must be an existing directory inside the
current git repo.
Directory nodes come from that directory's NODE.md. Leaf nodes come from
Markdown files other than NODE.md, AGENTS.md, and CLAUDE.md. A
Markdown file is renderable only when its YAML frontmatter has a non-empty
string title and a non-empty array owners; description is optional.
The owners field is used for filtering and included in JSON, but is not
shown in human output. Hidden paths and common generated directories
(node_modules, __pycache__, dist, build, .next, .turbo) are
skipped.
Human output starts with the Context Tree git checkout branch, then the
rendered tree. When the current branch is not exactly main, master, or
origin/main, the branch line is followed by a stale-tree warning:
Detached HEAD checkouts whose commit matches refs/remotes/origin/main are
reported as origin/main.
Branch: feature/stale-tree
Warning: current branch "feature/stale-tree" is not main/master; it may be stale. Switch to main/master.
An exact snapshot created by tree read is recognized through its local Git
metadata. For that checkout, tree tree never pulls, even when --no-pull is
omitted; it verifies that HEAD still equals the activated exact commit before
returning content. Human output reports snapshot:<short-sha>, Team, binding,
and the full exact commit with no stale-branch warning. Managed workspace
checkouts retain the existing pull-before-selector and stale-local fallback
semantics.
The rendered tree's node labels use:
relative/path/ [Title] -> Description
relative/path.md [Title]
Directory labels end with /. The repository root line uses the repo
directory name, for example context-tree/ [Context Tree] -> Root index for the First Tree context tree.. When description is missing,
the -> Description suffix is omitted.
Options:
-L, --level <depth>— maximum descendant depth below the target directory. Ancestors from the git repo root to the target are always kept. For path-tolerant CLI use,tree tree -L docs/developmentis treated astree tree docs/development;tree tree -L 2 docs/developmentapplies depth2to that path.-P, --pattern <pattern>— case-sensitive shell-style glob filter matched against relative path, filename,title, anddescription; matching descendants keep their ancestors visible.
With global --json or FIRST_TREE_JSON=1, first-tree tree tree
emits a single { ok: true, data } envelope on stdout. data.root is the
git repo root, data.target is the resolved target directory relative to
that root, and data.options records the parsed level, pattern, and
effective path. data.branch reports the current tree checkout as
{ name, isMainline, warning }; warning is null for main, master,
and origin/main, including detached HEAD checkouts that match
refs/remotes/origin/main; otherwise it contains the same stale-tree warning
string shown in human mode. data.readSnapshot is null for a managed
checkout and contains the activated { teamId, binding, commit, snapshotPath }
identity for a BYO task snapshot. data.tree contains the same filtered hierarchy as
structured nodes with kind, name, relativePath, depth, metadata,
hasNode, and children fields; metadata includes title, optional
description, and owners. Human tree text and branch warnings are not
written to stderr in JSON mode, so stdout stays reserved for machine-readable
JSON.
Most environment variables use the FIRST_TREE_ prefix.
| Variable | Purpose | Default |
|---|---|---|
FIRST_TREE_HOME |
Override the CLI home directory for config, data, and agent workspaces. | Channel-dependent: ~/.first-tree (prod), ~/.first-tree-staging (staging), ~/.first-tree-dev (dev). |
FIRST_TREE_SERVER_URL |
Server URL override for login <code> and fallback for other commands; otherwise login uses the CLI channel default. |
— |
FIRST_TREE_LOG_LEVEL |
Log level (trace / debug / info / warn / error / fatal). |
info |
FIRST_TREE_JSON |
JSON output mode (equivalent to --json). |
— |
A launchd / systemd / Task Scheduler daemon does not inherit your
interactive login-shell environment, so anything your shell exports (commonly
an HTTP_PROXY / HTTPS_PROXY for users behind a network proxy, or
FIRST_TREE_CLIENT_SENTRY_ENABLED=false for Client Sentry opt-out) is invisible
to the background daemon and the agent runtimes it spawns. That is why an
interactive claude / git can work while the daemon's calls to
api.anthropic.com / github.com fail.
To supply that environment, create daemon.env under your channel's
FIRST_TREE_HOME with simple KEY=VALUE lines. The path is channel-specific
(~/.first-tree/daemon.env on prod, ~/.first-tree-staging/daemon.env on
staging, ~/.first-tree-dev/daemon.env on dev) — it must match the channel of
the daemon that reads it:
HTTPS_PROXY=http://127.0.0.1:7897
HTTP_PROXY=http://127.0.0.1:7897
NO_PROXY=localhost,127.0.0.1
FIRST_TREE_CLIENT_SENTRY_ENABLED=falseThe daemon loads this file on start and passes the values to every child it
spawns. First Tree is compatible with your proxy — it only ever reads
this file and never writes your proxy into it on your behalf. Values already
present in the daemon's environment are preserved (the file fills gaps, it does
not override). Edit or delete the file freely, then restart the daemon
(<channel> daemon stop && <channel> daemon start) to apply changes. See
troubleshooting/proxy.md.
These are mentioned for completeness; operators don't set them in shell rc.
| Variable | Purpose |
|---|---|
FIRST_TREE_SERVICE_MODE |
Supervisor → child flag baked into launchd/systemd templates and set by the Windows supervisor loop. |
These are client-side update behavior tunables. They do not select a release
channel; channel identity comes from the installed package / binary
(first-tree, first-tree-staging, or first-tree-dev).
| Variable | Purpose |
|---|---|
FIRST_TREE_UPDATE_RESTART_CHECK_INTERVAL_SECONDS |
Frequency of the upgrade-restart watchdog. |
FIRST_TREE_UPDATE_RESTART_QUIET_SECONDS |
Quiet window the upgrade flow waits for before restarting. |
FIRST_TREE_UPDATE_PROMPT_TIMEOUT_SECONDS |
Interactive upgrade prompt timeout. |
FIRST_TREE_UPDATE_POLICY |
auto / prompt / off. Persisted via first-tree config set update.policy .... |
Per-agent bearer tokens are gone — every agent on a signed-in machine authenticates as the signed-in member. The runtime injects these so an agent process can talk to the server without extra setup:
| Variable | Purpose |
|---|---|
FIRST_TREE_ACCESS_TOKEN |
The signed-in member's access JWT (short-lived). |
FIRST_TREE_AGENT_ID |
The agent's own UUID — the CLI uses it to identify the sender. |
FIRST_TREE_CLIENT_ID |
The client (machine) the agent is bound to. |
FIRST_TREE_CHAT_ID |
The chat the current session is bound to. Used by chat send / chat invite, and by every cron command (including preview / list / show). |
FIRST_TREE_SERVER_URL |
Server URL override; falls back to client config. |
These configure the SaaS server image (packages/server/dist/index.mjs)
and are not used by the CLI. They are listed here for ops reference.
Identity / channel:
| Variable | Purpose | Default |
|---|---|---|
FIRST_TREE_CHANNEL |
Deployment channel (prod / staging / dev). |
dev |
FIRST_TREE_DATABASE_URL |
PostgreSQL connection URL. | — (required) |
FIRST_TREE_PORT |
HTTP listen port. | 8000 |
FIRST_TREE_HOST |
Bind address. | 127.0.0.1 |
FIRST_TREE_PUBLIC_URL |
Public-facing server origin. Used to stamp the issuer on short connect codes and to build invite-link URLs plus Google and GitHub OAuth callbacks. Required in production. | — |
FIRST_TREE_PORTABLE_DOWNLOAD_BASE_URL |
Base URL for the prod/staging portable installer and artifact mirror. Do not include a channel suffix; the server appends the channel's publicInstallerPath (for example, prod/install.sh). |
https://download.first-tree.ai/releases |
FIRST_TREE_CORS_ORIGIN |
Allowed origin for the web console. | — |
FIRST_TREE_TRUST_PROXY |
Trust the reverse-proxy X-Forwarded-* headers. |
false |
FIRST_TREE_WORKSPACES_ROOT |
Where agent worktrees are materialised on the host. | derived from FIRST_TREE_HOME |
Command update advertisement:
There is no FIRST_TREE_UPDATE_CHANNEL. Published channels have separate npm
package identities, and the server polls the package selected by
FIRST_TREE_CHANNEL.
| Variable | Purpose | Default |
|---|---|---|
FIRST_TREE_COMMAND_VERSION |
Bootstrap CLI version advertised before the npm-registry poller succeeds. The Docker image stamps this from the Command package version at build time. | image build arg |
FIRST_TREE_UPDATE_POLL_INTERVAL_MINUTES |
How often the server polls npm for the selected channel package's latest version. |
60 |
FIRST_TREE_UPDATE_REGISTRY_URL |
npm registry override for the server-side update-version poller. | https://registry.npmjs.org |
Secrets:
| Variable | Purpose | Production |
|---|---|---|
FIRST_TREE_JWT_SECRET |
JWT signing key. channel=dev local development auto-generates a value when omitted. |
Required for staging/prod |
FIRST_TREE_ENCRYPTION_KEY |
AES-256-GCM key for encrypted server-side secrets (GitHub tokens, org-settings secrets). Must be 32 bytes encoded as 64-char hex or base64url. channel=dev local development auto-generates a value when omitted. |
Required for staging/prod |
The server Docker image sets NODE_ENV=production, which disables generated
server secrets even if FIRST_TREE_CHANNEL is omitted or defaults to dev.
Auth lifetimes:
| Variable | Default |
|---|---|
FIRST_TREE_AUTH_ACCESS_TOKEN_EXPIRY |
30m |
FIRST_TREE_AUTH_REFRESH_TOKEN_EXPIRY |
30d |
FIRST_TREE_AUTH_CONNECT_TOKEN_EXPIRY |
10m |
GitHub App / OAuth:
| Variable | Purpose |
|---|---|
FIRST_TREE_GITHUB_APP_ID |
GitHub App numeric id. |
FIRST_TREE_GITHUB_APP_CLIENT_ID |
GitHub App OAuth client id. |
FIRST_TREE_GITHUB_APP_CLIENT_SECRET |
GitHub App OAuth client secret. |
FIRST_TREE_GITHUB_APP_PRIVATE_KEY |
GitHub App signing key (PEM body). |
FIRST_TREE_GITHUB_APP_WEBHOOK_SECRET |
Webhook HMAC secret. |
FIRST_TREE_GITHUB_APP_SLUG |
Optional explicit slug override. |
GitLab Context Tree snapshot egress:
| Variable | Purpose | Default |
|---|---|---|
FIRST_TREE_GITLAB_ALLOWED_ORIGINS |
Additional exact GitLab origins that Cloud may read anonymously over HTTPS. | [] plus built-in https://gitlab.com |
FIRST_TREE_GITLAB_EGRESS_ALLOWLIST |
Deprecated compatibility input using the former { origin, addressPolicy } shape. |
— |
Public origins are strings. Private destinations attach explicit IPv4/IPv6 CIDRs:
[
"https://gitlab.example.com",
{
"origin": "https://gitlab.company.local:8443",
"cidrs": ["10.20.0.0/16", "fd12:3456::/32"]
}
]This is deployment authority, not a Team setting. A public string entry allows only public-routable DNS results. A CIDR entry requires every resolved address to fall within its declared ranges. A Team admin may create an inbound GitLab connection and save a matching Context Tree binding without extending this policy; Web Context then reports an actionable unavailable state until the deployment authorizes that origin.
The deprecated FIRST_TREE_GITLAB_EGRESS_ALLOWLIST remains accepted so
existing deployments can upgrade without rewriting configuration immediately.
When it is set, its exact legacy list is preserved and the built-in
gitlab.com origin is not added. Setting both old and new variables fails
startup instead of choosing an ambiguous precedence. Invalid JSON, duplicate
origins, empty or malformed CIDR policy, and any CIDR rooted in a permanently
blocked range also fail server startup.
Every anonymous clone/fetch rechecks the live Team binding, current GitLab connection, current deployment allowlist, and every DNS A/AAAA result. The connection is pinned to the validated address while TLS retains the configured hostname. Redirects and ambient proxy/Git credential configuration are disabled. Loopback, link-local, unspecified, multicast, cloud-metadata, and reserved destinations remain blocked even under a CIDR policy. Removing an entry preserves existing Team settings but makes Web Context unavailable without attempting egress.
First Tree Cloud never asks for, stores, logs, or injects a GitLab repository credential. Public/anonymous-readable authorized repositories can appear in the Context tab; private GitLab content stays Agent-local and the Context tab shows a provider-specific unavailable state. That content state is independent of inbound Webhook and automatic MR-review health.
Google OAuth / OIDC:
| Variable | Purpose |
|---|---|
FIRST_TREE_GOOGLE_CLIENT_ID |
Google OAuth 2.0 Web application client id. |
FIRST_TREE_GOOGLE_CLIENT_SECRET |
Google OAuth 2.0 Web application client secret. |
Set both Google variables to enable Google sign-in; omit both to leave the
provider disabled. A partial configuration fails server startup. First Tree
requests only the fixed identity scopes openid email profile and does not
persist Google access or refresh tokens.
Register this exact authorized redirect URI in Google Cloud Console:
${FIRST_TREE_PUBLIC_URL}/api/v1/auth/google/callback
The scheme, host, port, path, and trailing-slash form must exactly match the
deployed FIRST_TREE_PUBLIC_URL. See
Google OAuth operator setup for the full setup
and verification procedure.
Rate limits:
| Variable | Default |
|---|---|
FIRST_TREE_RATE_LIMIT_MAX |
3000 |
The server applies this as one actor-aware global safety cap per minute. It keys by agent id, then user id, then request IP for unauthenticated traffic. Old per-route rate-limit env vars are no longer read.
Inbox / WS / archive sweeper:
| Variable | Default |
|---|---|
FIRST_TREE_INBOX_MAX_IN_FLIGHT_PER_AGENT |
server-tuned |
FIRST_TREE_WS_MAX_PAYLOAD |
262144 (256 KiB) |
FIRST_TREE_ARCHIVE_SWEEP_INTERVAL_SECONDS |
300 (set 0 to disable) |
FIRST_TREE_ARCHIVE_MAPPED_IDLE_SECONDS |
3600 |
FIRST_TREE_ARCHIVE_MAPPED_IDLE_SECONDS is the SCM-source archive idle
threshold. Mapped GitHub/GitLab chats also require all bound entities to be
closed/merged; provider-owned chats with no mapping use the same idle
threshold.
Scheduled jobs (cron):
| Variable | Purpose | Default |
|---|---|---|
FIRST_TREE_POLLING_INTERVAL_SECONDS |
Runtime polling cadence used by the cron worker; must be 1..10 so due occurrences remain inside the dispatch grace window. |
5 |
Observability:
| Variable | Purpose | Default |
|---|---|---|
FIRST_TREE_LOG_LEVEL |
Server log level. | info |
FIRST_TREE_OTEL_ENDPOINT |
OTLP/HTTP traces endpoint. Non-empty enables tracing. | "" |
FIRST_TREE_OTEL_HEADERS |
OTLP headers as key1=val1,key2=val2. Typically holds the write token. |
"" |
FIRST_TREE_OTEL_ENVIRONMENT |
Deployment label emitted as deployment.environment.name. |
development |
FIRST_TREE_OTEL_CAPTURE_CLIENT_IP |
Capture client IP attribute on traces. | false |
VITE_SENTRY_DSN |
Public browser DSN for Web Console errors in the first-tree-web Sentry project. |
unset |
VITE_SENTRY_ENABLED |
Explicit Web Sentry switch; false / 0 / off disables even when a DSN is present. |
enabled when DSN exists |
VITE_SENTRY_ENVIRONMENT |
Web Sentry environment label. | host/mode-derived |
VITE_SENTRY_TRACES_SAMPLE_RATE |
Web Sentry trace sample rate (0.0–1.0). |
0.1 |
FIRST_TREE_CLIENT_SENTRY_DSN |
Client daemon/runtime DSN for the first-tree-client Sentry project. |
unset |
FIRST_TREE_CLIENT_SENTRY_ENABLED |
Explicit Client Sentry operator switch; false / 0 / off disables even when a DSN is present. |
enabled when DSN exists |
FIRST_TREE_CLIENT_SENTRY_ENVIRONMENT |
Client Sentry environment label. | NODE_ENV or development |
FIRST_TREE_CLIENT_SENTRY_TRACES_SAMPLE_RATE |
Client Sentry trace sample rate (0.0–1.0). |
0.05 |
FIRST_TREE_GIT_SHA |
Git SHA stamped onto Web/Client Sentry releases and tags when provided by CI. | unknown |
See observability.md for the full config reference, backend cheat sheet, and troubleshooting recipes.
~/.first-tree/ # FIRST_TREE_HOME default for the prod channel
├── config/
│ ├── client.yaml # this machine's client config (server.url, client.id)
│ ├── credentials.json # access + refresh JWT (mode 0600)
│ └── agents/
│ └── <name>/
│ └── agent.yaml # agentId + runtime
├── data/
│ ├── context-tree-repos/ # legacy shared Context Tree pool (retained for old installs; new clones live per-agent)
│ ├── sessions/ # per-agent session registry
│ └── workspaces/
│ └── <agent-name>/ # per-agent home (cwd is shared across chats)
│ ├── context-tree/ # agent-managed Context Tree clone (agent clones/pulls it per its briefing)
│ └── worktrees/ # per-task worktrees the agent creates and cleans up
└── logs/ # daemon stderr / stdout (macOS)
When FIRST_TREE_HOME is set, replace ~/.first-tree/ with that location. Staging and dev channels use ~/.first-tree-staging/ and ~/.first-tree-dev/ respectively as their channel-default home.
Priority from high to low:
- CLI arguments
- Environment variables (
FIRST_TREE_*) - Config files (
~/.first-tree/config/client.yaml, or the staging/dev channel's equivalent) - Built-in defaults
After first-tree upgrade or after running logout + login <code> on a deployment-bump cycle:
first-tree status # CLI version + service + server + auth + agents
first-tree daemon doctor # service + agent configs + WS reachability
first-tree --help # top-level verbs + namespaces