# Thrum v0.10.5 -- Complete Agent Reference > Git-backed messaging for AI agent coordination. Real-time. Cross-machine. Cross-repo. Persistent. ## What's New New in v0.7.0: single-agent mode (default for new repos), cross-repo peer communication via Tailscale, PID-based identity resolution, three-tier context model for session persistence across compaction. New in v0.7.1: tmux-managed sessions replace background listeners with instant daemon-driven nudge delivery. Session restart extracts JSONL conversation history and reloads it in new sessions. New in v0.8.0: - Tmux command queue (`thrum tmux queue/queue-status/cancel`) — daemon-managed FIFO dispatch with `@system` completion notifications and restart recovery. Backed by SQLite (schema v18/v19) with per-command `silence_ms` and `notify_on_complete` configuration. - Worktree management (`thrum worktree create/teardown/list`) — git worktrees with automatic thrum and beads redirect wiring. - Orchestrator role — plan execution with review gates, agent lifecycle, and worktree management. Includes `thrum:orchestrate` skill and orchestration config block. - Multi-runtime tmux — `ClaudePID` renamed to `AgentPID` (schema v17). `PreferredRuntime` field on identity file (v5). `--runtime` flag on `thrum quickstart`. OpenCode, Codex, Cursor, Gemini, Auggie all launch via tmux alongside Claude. - Agent status (`thrum agent set-status working|idle|blocked`) — auto-nudge detection when working status mismatches silent pane. - Daemon logging — slog + lumberjack rotation (10 MB, 4 backups, 28 days). `thrum daemon logs` command. Configurable `daemon.log_level` in config.json. - Graceful restart flow — `thrum tmux restart` (without `--force`) asks the agent to save its own snapshot before killing, falling back to JSONL extraction on timeout. - `who-has` live git extraction — results reflect live worktree state rather than stale cached data from heartbeats. - `thrum prime` identity refresh — now writes back `preferred_runtime` and `branch` (in addition to `tmux_session`) when detected values differ. New in v0.8.1: CI fix for npm publish packaging (no user-facing changes). New in v0.8.2: - Cursor plugin — full Cursor integration with local-install.sh deploying rules, hooks (5: sessionStart, beforeShellExecution, stop, preCompact, postCompact), 2 `.mdc` rules, 4 skills, 11 commands, MCP config, and message-listener agent into `.cursor/`. Skills and commands synced from claude-plugin source via `scripts/sync-skills.sh`. - Test infrastructure scripts (`scripts/test-setup.sh`, `scripts/test-teardown.sh`) for automated agent test environments with daemon, agent registration, and cleanup. - Claude Code plugin docs updates: removed stale GROUPS.md resource reference, added TMUX_SESSIONS.md, added /thrum:load-context command, updated version references. - Worktree create quickstart fix — `thrum worktree create` with quickstart flags now creates a real tmux session (kept, not destroyed) and runs quickstart inside it via SendKeys. The old path created a temp `--no-agent` session and tried to send quickstart through the queue RPC, which rejects agentless sessions. New flow: create real session → SendKeys quickstart → daemon-side retry at 5s if shell init swallows the command → CLI-side pane capture at 12s if the identity file still hasn't appeared. Repo-root guard: errors out if the resolved worktree path or `worktrees.base_path` is the repo root itself. - Two-step create + launch pattern — `thrum worktree create` and `thrum tmux create` now print explicit next-step hints from the CLI: "Agent is NOT running yet. Start it with: thrum tmux launch ". The agent identity is registered, but the runtime is not started until `tmux launch` runs. Worktree create with no agent flags shows the full `tmux create` command needed to register an agent later. - `thrum tmux launch` hard-errors on missing identity — sessions created with `--no-agent` (or worktrees with no identity file) cannot be launched. Returns an error telling you to run `thrum quickstart` first. Launching a runtime without an identity is a no-op — the agent has no way to register. - Next-step guard messages across CLI commands — `agent register` wires up the existing hint system (was dead code in `hints.go`), `worktree create` with no agent flags hints about `tmux create` as the next step, `purge --confirm` notes when sessions were deleted, `daemon stop` warns that messaging commands will fail until the daemon restarts, `tmux restart` warns when the snapshot has 0 lines, `tmux launch` warns (then hard-errors) when no agent identity is registered. - Purge auto-re-register message corrected — agents auto-re-register via `RefreshLocalIdentity` wired into `getClient()`, so no manual action is needed after `thrum purge`. The previous message implied the user had to re-register manually. - Plugin docs updates — claude/codex/cursor/opencode plugin SKILL.md and resources updated: WORKTREES.md replaces stale shell-script setup with the `thrum worktree create` workflow; TMUX_SESSIONS.md warns that `tmux launch` hard-errors on `--no-agent` sessions; CLI_REFERENCE.md corrected to drop the old broken temp-session description. New in v0.10.5: - Daemon-side backstop nudger for stale-unread messages — the daemon polls for unread messages older than the nudge threshold and re-emits delivery notifications itself, replacing the user-side `thrum-inbox-poll.sh` cron pattern. More reliable (no missed cron windows when an agent is mid-restart) and lower-overhead than a per-agent schedule. Existing cron installs continue to function alongside the daemon backstop as no-ops; cron removal queued for a future release. - `thrum inbox --from @agent` filter — scope unread inbox to messages from a single sender. Useful for catching up on one agent's traffic without paging through the rest of the queue. Composes with the existing `--unread`, `--scope`, `--mentions`, `--all` filters. - `thrum worktree teardown --delete-branch` flag — tear down a worktree and delete its branch in one step. Previously the branch was kept and required a separate `git branch -D` after teardown. - `thrum prime` first-turn ack instruction — the prime briefing now asks the runtime to emit a short acknowledgment line on receipt, giving the agent visible scrollback signal that context loaded. Previously some runtimes (notably Claude Code's SessionStart hook) silently absorbed the prime briefing with no observable first-turn anchor, making it hard to diagnose "didn't get prime" without reading transcripts. Backticks in identity fields are explicitly stripped from the interpolation path (thrum-x7rb) so they don't leak literal markdown/shell interpretation into the rendered ack. - Headless `worktree.Create` / `worktree.Destroy` Go API (`internal/worktree`) — worktree lifecycle moves into a single shared package used by both the cobra commands and future programmatic callers (the v0.11 substrate epics in particular). Behavior-equivalent to the previous cobra-only path; opens the door to ephemeral-worktree flows. Cobra `thrum worktree create` / `teardown` are now thin shims over the shared package. - `runtime-init` overwrites stale daemon-managed scripts (thrum-akqv P1) — the runtime-init template logic now distinguishes daemon-managed scripts (`scripts/thrum-startup.sh`, `scripts/thrum-check-inbox.sh`, `scripts/thrum-polling.sh`, `.codex/hooks/session-start`) from user-customized configs (`.claude/settings.json`, `AGENTS.md`, `.gemini/instructions.md`, `.cursorrules`, `opencode.json`, `.augment/settings.json`, `.augment/rules/thrum.md`). Daemon-managed scripts overwrite on init so long-running agent worktrees stop drifting from the embedded template; user configs preserve on init as before. Pass `--force` to overwrite user configs too if needed. Fixes the silent drift class where substrate-side template changes (e.g., backstop polling moving from a per-agent cron to the daemon-side ticker) never propagated into already-registered worktrees. - `project-setup` skill follows `.thrum/redirect` when checking `philosophy.md` — previously failed in redirected worktrees because the skill resolved the worktree-local `philosophy.md` path directly without honoring the redirect. The check now resolves the redirect first. - Inbox backstop spool envelope durability — the janitor was prematurely deleting backstop-pending envelopes, causing stale-unread messages to disappear from inbox before the daemon backstop nudger could re-deliver them. Janitor now preserves envelopes flagged backstop-pending. - Self-mention semantic fix — messages with an explicit self-mention now route correctly to the author's own inbox without being filtered out by the recipient-set construction. `read_at` is stamped at insert for the self-delivery row, so the message doesn't appear as unread to its own author the first time the inbox listing loads. - URLs migrated from `leonletto.github.io/thrum` to `thrum.team` (Phase 6.3) across README, website content, docs, plugin manifests, agent definitions, and SEO references. Old GitHub Pages URLs continue to resolve via redirect; canonical now points at `thrum.team`. New in v0.9.1: - Peercred resolver error taxonomy (thrum-ndtw) — introspection failures no longer masquerade as anonymous. Step 1 (kernel peer credentials via tailscale/peercred) and Step 2 (gopsutil CWD lookup) now return raw errors instead of wrapping `ErrAnonymous`. `server.go` treats `ErrAnonymous` as "provably anonymous" and enforces the allowlist; raw errors fall through to legacy client-asserted identity (pre-v0.9.0 behavior). Only Step 3 (no git root above CWD) and Step 5 (git root doesn't match any registered worktree) are provable evidence that the caller is outside every registered worktree — those still wrap `ErrAnonymous`. slog.Warn fires at both introspection-failure paths: step=pid failed — tailscale/peercred.Get errored or returned PID=0 step=cwd failed — gopsutil Cwd errored for the peercred PID The rejection error "anonymous caller cannot invoke X: cd into a registered agent worktree and retry" now fires only for cases where the daemon could inspect the caller and determine they were outside every registered worktree. It no longer fires when peercred or gopsutil couldn't classify the caller at all — those now fall through. Fixes 2026-04-24 failure mode where claude-code Bash subprocesses on macOS hit gopsutil.Cwd races (subprocess exits before introspection completes) and interactive zsh callers hit the same path. Pre-fix symptom: "anonymous caller cannot invoke X" even when the caller was CD'd into the correct worktree and whoami resolved correctly. Net-zero security regression: only restores pre-v0.9.0 behavior on the "don't know" path. Provably anonymous callers still hit the allowlist. - thrum setup claude-md --apply (issue #8) — implemented the documented-but-missing CLI subcommand. External user mjn298 followed the quickstart and hit `Error: unknown flag: --apply` because the setupCmd stub at cmd/thrum/main.go just suggested `thrum worktree setup`. The command now installs a Thrum-managed block (wrapped in `...` markers) into ./CLAUDE.md. Three forms: bare: print template to stdout, write nothing --apply: create CLAUDE.md if missing, or append blank-line + template to end of existing file. Errors with exit 1 + stderr if a Thrum block already exists ("use --force to replace"). --apply --force: replace existing Thrum block in place; idempotent. Template is static (go:embed at internal/cli/templates/claude-md/ thrum-block.md), intentionally minimal — ~30-40 lines covering what Thrum is, the 5 essential commands (whoami, team, inbox, send, reply), and a pointer to the full docs. Designed as the standalone messaging-instruction path for environments without the Claude Code Thrum plugin (other runtimes: codex, cursor, opencode, kiro, auggie; or Claude Code where the plugin isn't installed). Plugin users should NOT also run this — it would duplicate messaging instructions the plugin already provides via SessionStart hook + skills + slash commands. Use one approach or the other. New in v0.9.0: - Safe agent registration — `thrum tmux create` now requires `--name`, `--role`, `--module` (or `--no-agent`); runs quickstart inside the pane automatically. New alias: `thrum tmux quickstart`. `thrum worktree create` gains optional quickstart flags; when all three are provided it creates a real tmux session and runs quickstart inside it via SendKeys (PID-isolated, with retry). New alias: `thrum worktree setup`. Single-identity-per-worktree enforcement. Auto-creates tmux sessions when stored cwd is available. - Monitor Jobs — `thrum monitor start/list/show/stop/logs/restart`. Spawn a child process (explicit argv, no shell), watch stdout+stderr with a Go regex, emit matching lines as synthetic Thrum messages. Sender: `monitor:`. Leading-edge debounce (default 60s, min 30s). Auto-persist. Local Unix socket only. (`thrum monitor add` is an alias for `thrum monitor start`.) Schema v20 adds `monitors` table. Package: `internal/daemon/monitor/`. ## Architecture Thrum uses event sourcing with CQRS: Source of truth: Append-only JSONL event logs (sharded by agent), tracked on the `a-sync` orphan branch via a `.git/thrum-sync/a-sync` worktree - events.jsonl (agent lifecycle events) - messages/{agent_name}.jsonl (per-agent message logs) Query model: SQLite projection (.thrum/var/messages.db), gitignored, rebuildable Daemon: Background service with JSON-RPC 2.0 over Unix socket + WebSocket Sync: Automatic fetch/merge/push in `.git/thrum-sync/a-sync` worktree every 60 seconds No branch switching -- all operations happen within the worktree All CLI commands communicate with the daemon via the Unix socket. The daemon manages the JSONL log, SQLite projection, sync loop, and subscription notifications. SQLite can be rebuilt from JSONL at any time. ### Directory Structure .thrum/ Gitignored entirely sync/ Git worktree on `a-sync` orphan branch events.jsonl Agent lifecycle events (tracked on a-sync) messages/ Per-agent message logs (tracked on a-sync) {agent_name}.jsonl One JSONL file per agent context/ Agent context files (tracked on a-sync) {agent_name}.md Saved context per agent var/ Runtime files messages.db SQLite query cache thrum.sock Unix domain socket thrum.pid Daemon process ID ws.port Actual WebSocket port sync.lock Sync lock file identities/ Agent identity files (per-worktree) {agent_name}.json One JSON file per agent context/ Local agent context files {agent_name}.md Saved context per agent {agent_name}.preamble.md Agent preamble file config.json Local configuration (local_only, etc.) redirect (feature worktrees only) points to main .thrum/ ### Transport Unix Socket: .thrum/var/thrum.sock (CLI, scripts, local tools) WebSocket: ws://localhost:9999/ (Web UI, real-time apps) Protocol: JSON-RPC 2.0 over both transports Both transports expose the same set of RPC methods. ## Data Model ### Message message_id string Unique ID (format: msg_ + ULID) thread_id string Optional thread association agent_id string Author agent ID session_id string Author session ID body.format string "markdown" | "plain" | "json" body.content string Message text content body.structured string Optional JSON payload scopes []Scope Routing metadata (type:value pairs) refs []Ref References (type:value pairs, includes mentions) created_at string ISO 8601 timestamp updated_at string ISO 8601 timestamp (set on edit) deleted bool Soft-delete flag deleted_at string Deletion timestamp authored_by string Actual author if impersonating (users only) disclosed bool Whether to show impersonation in UI ### Agent agent_id string Deterministic ID (format: agent:role:hash) kind string "agent" or "user" role string Agent function (e.g., implementer, reviewer) module string Component responsibility (e.g., auth, db) display string Human-readable display name registered_at string ISO 8601 timestamp last_seen_at string ISO 8601 timestamp (updated by heartbeat) ### Session session_id string Unique ID (format: ses_ + ULID) agent_id string Owning agent started_at string ISO 8601 timestamp ended_at string ISO 8601 timestamp (null if active) end_reason string "normal" | "crash" | "crash_recovered" | "superseded" last_seen_at string Updated by heartbeat ### Work Context (per session) session_id string Session this context belongs to agent_id string Agent this context belongs to branch string Current git branch worktree_path string Path to git worktree unmerged_commits []CommitSummary Commits not merged to main uncommitted_files []string Files with uncommitted changes changed_files []string All changed files git_updated_at string When git context was last extracted current_task string Task identifier (e.g., beads:thrum-xyz) task_updated_at string When task was last set intent string Free-text work intent intent_updated_at string When intent was last set ### CommitSummary sha string Short commit hash message string Commit message files []string Files changed in commit ### Scope type string Category (e.g., "module", "team", "project") value string Specific value (e.g., "auth", "backend") ### Ref type string Reference type (e.g., "pr", "issue", "mention", "file") value string Reference value (e.g., "42", "reviewer", "auth.go") ### Subscription id int Auto-increment ID session_id string Owning session scope_type string Optional scope filter type scope_value string Optional scope filter value mention_role string Optional mention filter role all bool True if firehose subscription created_at string ISO 8601 timestamp ### Group name string Group name (e.g., "backend-team") description string Optional description created_by string Agent ID of creator created_at string ISO 8601 timestamp ### Group Member group_name string Parent group name member_type string "agent" | "role" | "group" member_value string Agent name, role name, or nested group name ### ID Formats Repo: r_ + base32(sha256(normalized_origin_url))[:12] Agent: agent: + role + : + base32(sha256(repo_id|role|module))[:10] User: user: + username Session: ses_ + ULID Message: msg_ + ULID Repo and Agent IDs are deterministic (same inputs = same ID). Session and Message IDs use ULID (time-ordered, unique). ## Global Flags All commands accept: --role VALUE Agent role (or THRUM_ROLE env var) --module VALUE Agent module (or THRUM_MODULE env var) --json JSON output for scripting --quiet Suppress non-essential output --verbose Debug output ## All Commands (Detailed) ### thrum init Usage: thrum init [flags] Flags: --force Force reinitialization / overwrite existing files --stealth Write exclusions to .git/info/exclude instead of .gitignore --runtime STRING Generate runtime-specific configs (claude|codex|cursor|gemini|opencode|auggie|cli-only|all) --dry-run Preview changes without writing files --skills v0.8.0: install just the thrum skill, no full runtime config --agent-name STRING Agent name for templates (default: default_agent) --agent-role STRING Agent role for templates (default: implementer) --agent-module STRING Agent module for templates (default: main) Action: Creates .thrum/ directory structure, sets up .git/thrum-sync/a-sync as a git worktree on the `a-sync` orphan branch, creates identities/ directory, updates .gitignore (or .git/info/exclude with --stealth) to ignore .thrum/. Writes config.json with local_only: true and single_agent_mode: true by default. Also writes default `worktrees` (base_path inferred to ~/.workspaces/) and `orchestration` (merge_target: "main", default_autonomy: "end_only") config blocks. Optionally detects installed AI runtimes and generates runtime-specific config files. Errors: Fails if .thrum/ exists (unless --force). Fails if not a git repo. Related: `thrum worktree setup` configures .thrum/ in feature worktrees (creates redirect). ### thrum quickstart Usage: thrum quickstart --name --role ROLE --module MODULE [flags] Flags: --name STRING Human-readable agent name (optional, defaults to role_hash) --role STRING Agent role (or THRUM_ROLE env var) --module STRING Agent module (or THRUM_MODULE env var) --display STRING Display name for the agent --intent STRING Initial work intent --runtime STRING Runtime preset (claude, codex, cursor, gemini, opencode, auggie, cli-only) v0.8.0: writes `preferred_runtime` to the identity file, used by `thrum tmux launch` at session start. --dry-run Preview changes without writing files or registering --force Overwrite existing runtime config files --no-init Skip runtime config generation, just register agent --preamble-file STRING Custom preamble file to compose with default preamble Action: Chains: runtime detect -> config generate -> agent register -> session start -> set intent (if provided). Auto-retries with re-register on conflict. Requires: Daemon running. --role and --module required. Output: Agent ID, session ID, intent confirmation. ### thrum overview Usage: thrum overview [--json] Action: Fetches health, identity (whoami), work context, team contexts, inbox counts, and sync state. Combines into single orientation view. Requires: Daemon running. Shows "Not registered" if no agent identity. ### thrum whoami Usage: thrum whoami [--json] Action: Shows current agent identity. Lightweight alternative to `thrum overview` -- reads directly from .thrum/identities/*.json, no daemon required. Output: Agent ID, name, role, module, display, source. ### thrum version Usage: thrum version Action: Shows version number, build hash, repository URL, and documentation URL. ### thrum completion Usage: thrum completion [bash|fish|powershell|zsh] Action: Generates shell autocompletion scripts for the specified shell. ### thrum send Usage: thrum send MESSAGE [flags] Flags: --scope type:value Add scope (repeatable) --ref type:value Add reference (repeatable) --mention @role Mention a role (repeatable, stored as ref type "mention") --thread ID Reply to thread --to @agent_name|@everyone Recipient — @agent_name or @everyone --format markdown|plain|json Message format (default: markdown) --structured JSON Structured payload Requires: Daemon running, active session. Output: Message ID, thread ID (if set), created timestamp. Errors: No active session. Empty content. ### thrum reply Usage: thrum reply MSG_ID TEXT [--format markdown|plain|json] Flags: --format Message format (default: markdown) Action: Replies to a message. Copies the parent message's audience (mentions/scopes). Includes a reply_to reference to the parent message. Creates a thread if the message has no thread. Auto-marks the replied-to message as read. Requires: Daemon running, active session. Output: Reply message ID, thread ID. ### thrum inbox Usage: thrum inbox [flags] Flags: --scope type:value Filter by scope --mentions Only messages mentioning current agent --unread Only unread messages --all, -a Show all messages (disable auto-filtering) --from @agent_name (v0.10.5) Filter to messages from a single sender --page-size N Results per page (default: 10) --limit N Alias for --page-size --page N Page number (default: 1) Action: Lists messages. By default auto-filters to messages addressed to you (via --to) plus broadcasts and general messages. Use --all to see everything. Auto-marks displayed messages as read (unless --unread). Requires: Daemon running, active session. Output: Paginated message list with sender, content preview, timestamps. ### thrum sent Usage: thrum sent [flags] Flags: --to @agent|@everyone Filter by recipient or audience --unread Only messages with unread recipients --page-size N, --limit N Results per page --page N Page number Action: Lists messages you authored, including resolved recipients and durable delivery/read state. Use 'thrum message get ' for full details. Requires: Daemon running, active session. Output: Paginated sent message list with recipients and receipt status. ### thrum wait Usage: thrum wait [flags] Flags: --timeout DURATION Max wait time (default: 30s, e.g., 30s, 5m) --scope type:value Filter by scope --mention @role Wait for mentions of role --all Subscribe to all messages (broadcasts + directed) --after OFFSET Relative time offset (sign convention: negative e.g. -30s = include messages sent up to N ago; positive e.g. +60s = only messages N in the future; omit = "now") Action: Blocks until a matching message arrives or timeout. When --all is used without --after, defaults to "now" (only new messages). Exit codes: 0 = message received, 1 = timeout, 2 = error. Requires: Daemon running, active session. ### thrum message get Usage: thrum message get MSG_ID Action: Retrieves full message details including scopes, refs, metadata. Auto-marks message as read. Requires: Daemon running. Output: Full message with author, body, scopes, refs, timestamps, edit/delete info. ### thrum message edit Usage: thrum message edit MSG_ID TEXT Action: Replaces message content entirely. Only author can edit. Requires: Daemon running, active session. Errors: Not found, already deleted, not the author. Output: Message ID, updated timestamp, edit version number. ### thrum message delete Usage: thrum message delete MSG_ID --force Flags: --force Required to confirm deletion Action: Soft-deletes a message. Requires: Daemon running. Errors: Not found, already deleted, --force not specified. ### thrum message read Usage: thrum message read [MSG_ID...] [--all] Flags: --all Mark all unread messages as read Action: Marks one or more messages as read for the current agent/session. Use --all to mark every unread message as read. Requires: Daemon running, active session. Output: Count of marked messages. Also shows other agents who read the same messages. ### thrum purge Usage: thrum purge --before DURATION|DATE [--confirm] thrum purge --all [--confirm] Flags: --before string Cutoff: duration (2d, 24h), date (2026-03-15), or RFC 3339 --all Purge all messages, sessions, and events --confirm Execute the purge (without this, only shows a preview) Action: Removes messages, sessions, and events before the cutoff date from both SQLite and sync JSONL files. Agents, groups, and subscriptions are not touched. --before and --all are mutually exclusive. Requires: Daemon running. Output: Preview (counts) without --confirm. Deletion summary with --confirm. ### thrum agent register Usage: thrum agent register --role ROLE --module MODULE [--name NAME] [flags] Flags: --name NAME Agent name (e.g., "furiosa") --display NAME Display name --force Force override existing agent --re-register Re-register same agent (update) Action: Registers agent identity. Saves to .thrum/identities/{name}.json. Identity: Priority: THRUM_NAME env var (highest) > --name flag > THRUM_ROLE/THRUM_MODULE env > identity file. Errors: Conflict if different agent exists with same role+module (use --force). Output: Agent ID, status (registered | updated | conflict). ### thrum agent whoami Usage: thrum agent whoami Action: Shows current agent identity and active session. Identity resolution: 1. CLI flags (--name, --role, --module) 2. Environment variables (THRUM_NAME, THRUM_ROLE, THRUM_MODULE) 3. Identity files in .thrum/identities/ directory Output: Agent ID, role, module, display, source, session ID, session start. ### thrum agent list Usage: thrum agent list [--role ROLE] [--module MODULE] [--context] Flags: --role ROLE Filter by role --module MODULE Filter by module --context Show work context (branch, commits, intent) Action: Lists all registered agents. With --context, shows work context table. Requires: Daemon running. ### thrum agent delete Usage: thrum agent delete Action: Deletes an agent and all associated data: identity file (identities/.json), message file (messages/.jsonl), and database record. Requires: Daemon running. ### thrum agent cleanup Usage: thrum agent cleanup [--dry-run] [--force] [--threshold DAYS] Flags: --dry-run List orphans without deleting --force Delete all orphans without prompting --threshold DAYS Days since last seen to consider stale (default: 30) Action: Scans registered agents and identifies orphans (missing worktree, missing branch, or stale). Interactive by default. Requires: Daemon running. ### thrum agent start | end | set-intent | set-task | heartbeat These are aliases for the corresponding `thrum session` commands: thrum agent start -> thrum session start thrum agent end [--reason] [--session-id] -> thrum session end thrum agent set-intent TEXT -> thrum session set-intent thrum agent set-task TASK -> thrum session set-task thrum agent heartbeat [--add-scope] [--remove-scope] [--add-ref] [--remove-ref] -> thrum session heartbeat ### thrum agent set-status (v0.8.0) Usage: thrum agent set-status [--agent NAME] Flags: --agent NAME Target agent name (uses daemon RPC to update a remote agent) Action: Without --agent, updates the local identity file directly (setting `agent_status` and `agent_status_updated_at`). With --agent, calls the `agent.set-status` RPC; the daemon locates the agent's identity file across worktrees and writes the status. Values: working | idle | blocked (rejected otherwise) Auto-nudge: if a tmux agent's pane is silent but `agent_status` is "working", the daemon fires a nudge on the next silence event. ### thrum session start Usage: thrum session start Action: Starts a new work session. Auto-recovers orphaned sessions (marks as crash_recovered). Agent must be registered. Output: Session ID, agent ID, start timestamp. ### thrum session end Usage: thrum session end [--reason normal|crash] [--session-id ID] Action: Ends the session. Syncs work contexts to JSONL. Defaults: --reason normal, --session-id from whoami. ### thrum session list Usage: thrum session list [--active] [--agent AGENT_ID] Flags: --active Show only active sessions --agent STRING Filter by agent ID Action: Lists all sessions (active and ended). Requires: Daemon running. ### thrum session heartbeat Usage: thrum session heartbeat [--add-scope type:value] [--remove-scope type:value] [--add-ref type:value] [--remove-ref type:value] Action: Updates last_seen_at. Extracts git work context (branch, commits, changed files). Manages session scopes and refs. ### thrum session set-intent Usage: thrum session set-intent TEXT Action: Sets work intent (free text). Pass "" to clear. ### thrum session set-task Usage: thrum session set-task TASK Action: Sets current task identifier (e.g., beads:thrum-xyz). Pass "" to clear. ### thrum team Usage: thrum team [--all] [--system] [--json] Flags: --all Include offline agents --system Surface reserved pseudo-agents (displayed with ⊙ glyph) Action: Shows rich, multi-line status for every active agent. Displays session info, work context, inbox counts, branch status, and per-file change details. Status glyphs: ● active ○ offline ⊙ reserved (pseudo-agent) Requires: Daemon running. ### thrum context save Usage: thrum context save [--file PATH] [--agent NAME] Flags: --file STRING Read context from file (default: stdin) --agent STRING Override agent name Action: Saves context for the current agent (or specified agent). ### thrum context show Usage: thrum context show [--agent NAME] [--raw] [--no-preamble] Aliases: thrum context load Flags: --agent STRING Override agent name --raw Raw output with file boundary markers, no header --no-preamble Exclude preamble from output Action: Displays saved context for the current agent. ### thrum context clear Usage: thrum context clear [--agent NAME] Flags: --agent STRING Override agent name Action: Clears saved context for the current agent. ### thrum context preamble Usage: thrum context preamble [--init] [--file PATH] [--agent NAME] Flags: --init Create or reset to default preamble --file STRING Set preamble from file --agent STRING Override agent name Action: Shows current preamble (no flags), creates/resets preamble (--init), or sets preamble from file (--file). The preamble is a stable, user-editable header prepended when showing context. ### thrum context sync Usage: thrum context sync [--agent NAME] Flags: --agent STRING Override agent name Action: Copies context file to the a-sync branch for sharing across worktrees. Commits and pushes. No-op when no remote is configured (local-only mode). ### thrum who-has Usage: thrum who-has FILE Action: Shows which agents have the file in their uncommitted or changed files. v0.8.0: the daemon calls `gitctx.ExtractWorkContext` on each result's worktree path on every call (~500ms for a handful of worktrees), replacing stale cached heartbeat data with live git state. ### thrum worktree create | setup | teardown | list (v0.8.0) thrum worktree create [-b BRANCH] [--detach] [--name --role ROLE --module MODULE] [--intent TEXT] [--runtime PRESET] [--force] Create a git worktree at `worktrees.base_path/` with thrum/beads redirect wiring. Flags: -b / --branch STRING Branch name (default: feature/) --detach Create detached HEAD worktree --name, --role, --module (all optional, v0.9.0): if all three provided, creates a real tmux session with the worktree as cwd and runs quickstart inside the pane via SendKeys (PID-isolated). Daemon-side retry at 5s if shell init swallowed the command, CLI-side pane capture at 12s if the identity file still hasn't appeared. The session is kept (NOT torn down) — the agent runtime is started later with `thrum tmux launch `. The CLI prints the next-step launch command in its output. --intent, --runtime, --force: forwarded to quickstart (require --name/--role/--module) Behavior: validates name (no /, \, ..), creates worktree, sets up .thrum/redirect + .thrum/identities/ (if worktrees.thrum_enabled), sets up .beads/redirect (if worktrees.beads_enabled and .beads/ exists in main repo). Errors out if the resolved worktree path or worktrees.base_path is the repo root itself (repo-root guard, v0.8.2). Base path defaults to ~/.workspaces/ when not configured. thrum worktree setup [-b BRANCH] [--detach] [...] Alias for `thrum worktree create` (v0.9.0). thrum worktree teardown [--delete-branch] Remove a worktree and clean up thrum identity files. Deletes .thrum/identities/*.json from the worktree, then runs `git worktree remove --force `. --delete-branch (v0.10.5): also delete the worktree's branch in the same step (previously required a separate `git branch -D` after teardown). thrum worktree list List git worktrees with thrum agent info (reads identity files from each worktree's .thrum/identities/ to show which agent is active where). Columns: WORKTREE | BRANCH | HEAD | AGENT | STATUS ### thrum setup claude-md (v0.9.1) Usage: thrum setup claude-md [--apply] [--force] Action: Install or manage a Thrum-managed block in ./CLAUDE.md. Behavior: Default (no flags): Print template to stdout. Writes nothing. --apply: Write template to ./CLAUDE.md. - File missing: create it with template-only content (the BEGIN/END THRUM block + nothing else). - File exists, no Thrum block: append blank line + template at end. Existing content preserved byte-for-byte. - File exists, Thrum block present: exit 1 with stderr "Error: thrum block already present in CLAUDE.md — use --force to replace". (Lowercase "thrum" matches the underlying sentinel error per Go staticcheck ST1005.) --apply --force: Replace the existing Thrum block in place. Idempotent — re-runs produce the same result. Useful for refreshing the block after upgrading thrum. Block markers: and (single space, no version, no trailing dot — exact byte match is what the command uses for detection). Template: Static, embedded at build time via go:embed from internal/cli/templates/claude-md/thrum-block.md. Intentionally minimal (~30-40 lines): what Thrum is, the 5 essential commands (whoami, team, inbox, send, reply), pointer to full docs at https://leonletto.github.io/thrum. When to use: Claude Code WITHOUT the Thrum plugin, or other runtimes (codex, cursor, opencode, kiro, auggie) that don't have a first-class plugin. The CLAUDE.md block is the standalone messaging path. When NOT to use: Claude Code WITH the Thrum plugin installed. The plugin's SessionStart hook + skills + slash commands already provide messaging instructions; running setup claude-md on top of the plugin would duplicate what the plugin injects. Use one approach or the other, not both. Issue context: GitHub issue #8 — external user hit "Error: unknown flag: --apply" because the command was documented but the setupCmd stub at cmd/thrum/main.go just suggested "worktree setup". v0.9.1 implements the documented behavior. ### thrum ping Usage: thrum ping AGENT Action: Checks agent presence (with or without @ prefix). Shows intent, task, branch. ### thrum sync status Usage: thrum sync status Action: Shows sync loop state, last sync time, mode (local-only or normal), errors. ### thrum sync force Usage: thrum sync force Action: Triggers immediate sync (non-blocking). ### thrum peer add Usage: thrum peer add --type TYPE [flags] Flags: --type STRING Transport type: tailscale | local | network | repair MANDATORY in v0.9.0 (BREAKING: no longer defaults to tailscale) Action: Starts a pairing session for the given transport type and displays a code. Blocks until a peer connects or the session times out (5 minutes). Types: tailscale — cross-machine via Tailscale DERP/direct local — same machine, different repo/worktree (loopback) network — same LAN (same /24 subnet) ### thrum peer join Usage: thrum peer join --type TYPE
[flags] Flags: --type STRING Transport type: tailscale | local | network | repair MANDATORY in v0.9.0 (BREAKING: no longer defaults to tailscale) Action: Connects to a remote daemon at the given address. Prompts for the 4-digit pairing code displayed on the other machine. Types: tailscale — cross-machine via Tailscale local — same machine, different path network — same LAN repair — re-establish drifted peer without re-pairing (no new peercode needed) ### thrum peer list Usage: thrum peer list Action: Lists all paired Tailscale sync peers. ### thrum peer remove Usage: thrum peer remove Action: Removes a paired peer. ### thrum peer status Usage: thrum peer status Action: Shows detailed sync status for all peers. ### thrum runtime list Usage: thrum runtime list Action: Lists all available runtime presets (claude, codex, cursor, gemini, auggie, amp, cli-only). ### thrum runtime show Usage: thrum runtime show Action: Shows details for a specific runtime preset including config files it generates. ### thrum runtime set-default Usage: thrum runtime set-default Action: Sets the default runtime preset for the repository. ### thrum roles list Usage: thrum roles list Action: Lists all role templates in .thrum/role_templates/ and shows which registered agents match each template. Output: coordinator.md (2 agents: coord_main, coord_api) implementer.md (3 agents: impl_auth, impl_payments, impl_ui) ### thrum roles deploy Usage: thrum roles deploy [--agent NAME] [--dry-run] Action: Re-renders preambles for all registered agents from role templates. Templates in .thrum/role_templates/ are rendered with each agent's identity data and written to .thrum/context/{agent}_preamble.md. Full overwrite — templates are the source of truth. Flags: --agent NAME Deploy for a specific agent only --dry-run Preview changes without writing files ### thrum config show Usage: thrum config show [--json] Action: Shows effective Thrum configuration from all sources (config.json, environment variables, defaults, auto-detected values). Shows the source of each config value. Works whether the daemon is running or not. ### thrum tmux create | quickstart | launch | status | kill | send | capture | restart (v0.7.1+) thrum tmux create --cwd --name --role ROLE --module MODULE [--intent TEXT] [--runtime PRESET] [--force] [--no-agent] Create tmux session for an agent with clean env and monitor-silence hooks. v0.8.0: writes `tmux_session` to the identity file at so HandleLaunch can match by session name. v0.9.0: --name, --role, --module are required (or --no-agent to skip). Runs `thrum quickstart` inside the new pane automatically. --intent, --runtime, --force are forwarded to quickstart. Single-identity-per-worktree is enforced: fails if a different agent is already registered in the worktree's identity directory. thrum tmux quickstart --cwd --name --role ROLE --module MODULE [...] Alias for `thrum tmux create` (v0.9.0). thrum tmux launch [--runtime claude|codex|cursor|gemini|opencode|auggie|shell] Start AI tool inside tmux session. Default: reads from config.json/identity file. v0.8.0: opens to all runtimes (multi-runtime tmux support). v0.8.2: hard-errors if no agent identity is registered in the target worktree (sessions created with --no-agent, or worktrees with no identity file). Returns an error telling the caller to run `thrum quickstart` first. Launching a runtime without an identity is a no-op. thrum tmux status Show tmux-managed sessions with state (alive/stale/dead), runtime, branch. v0.8.0: read-only — no longer clears tmux_session from stale sessions (that's the responsibility of HandleKill and HandleRestart). thrum tmux list Alias for status. thrum tmux kill Tear down session, clear tmux_session from identity. thrum tmux send "text" Send text into session via send-keys. thrum tmux capture [--lines N] Capture visible pane content. Default: 50 lines. thrum tmux restart [--force] [--runtime R] v0.8.0: two flows — graceful (default) and force. - Graceful: daemon sends @system save request, nudges pane, polls for snapshot file up to `restart.graceful_timeout` (default 30s), falls back to JSONL extraction on timeout. Agent-authored snapshots tend to be more useful than raw JSONL dumps. - Force (--force): skip the graceful prompt, extract directly from JSONL conversation transcript. Only works for Claude Code (other runtimes don't use the same JSONL format). Either way: kills session, creates new one, relaunches with snapshot via prime. ### thrum tmux queue | queue-status | cancel (v0.8.0) Per-session FIFO command dispatch. Daemon serializes commands, detects completion via silence events, captures output, and sends `@system` inbox notifications. Commands survive daemon restart (interrupted in-flight commands get notification messages; queued commands reload into memory). thrum tmux queue [--timeout SECONDS] [--wait] [--silence FLOAT] Submit a command to the session's queue. Flags: --timeout INT Command timeout in seconds (default: 120) --wait Block until terminal state; suppresses @system notification (CLI gets result directly) --silence FLOAT Silence threshold (server default: 5.0 seconds) Output without --wait: "Queued (position )" Output with --wait: "State: \nElapsed: ms\n\n" Command states: queued -> sent -> completed | timeout_waiting | cancelled | interrupted Terminal states: completed, cancelled, interrupted. thrum tmux queue-status [--json] Show active command (if any) and queued commands list for a session. thrum tmux cancel Cancel a queued or active command. Returns final state and partial output. @system messages: Unless suppressed via --wait, the daemon emits inbox messages from agent "system" to the requester on completion, timeout (with cancel hint), cancellation (with partial output), and interruption. Restart recovery always sends interruption notifications regardless of notify_on_complete. ### thrum monitor start | list | show | stop | logs | restart (v0.9.0) Monitor Jobs spawn a long-running child process and watch its output for regex matches. Each match emits a synthetic Thrum message to a target agent. Monitors are local-only (Unix socket), auto-persist across daemon restarts, and are capped at 100 per daemon. thrum monitor start --name --match --to @ [--debounce DURATION] [--env KEY=VALUE] -- Create and start a monitor. (`thrum monitor add` is an alias.) Flags: --name STRING Unique monitor name (required) --match STRING Go regex applied to each stdout/stderr line (required) --to STRING Recipient agent, e.g. @reviewer (required) --debounce DURATION Leading-edge debounce window (default: 60s, min: 30s) --env KEY=VALUE Extra env var for the child process (repeatable) Env var names/values are redacted in log output. Command + args after `--` (no shell; explicit argv array) Behavior: daemon spawns the process. stdout+stderr are captured line-by-line (max 2KB per line; longer lines are truncated). Matching lines trigger a message send. Debounce is leading-edge: first match fires immediately, subsequent matches within the window are suppressed. Sender: `monitor:`. Message body contains the matched line. On process exit, daemon sends a notify-only message (no match required). thrum monitor list [--all] List monitors. Without --all, shows only running monitors. Columns: ID | NAME | STATUS | MATCH | TO | PID | STARTED thrum monitor show Show full monitor config: name, argv, match regex, target agent, debounce, env vars (redacted), pid, status, start time, exit code (if stopped). thrum monitor stop Send SIGTERM to the child process. Monitor moves to "stopped" state. thrum monitor logs Show recent captured output lines from the monitor's ring buffer. thrum monitor restart Restart a stopped or failed monitor using the same config. ### thrum tmux snapshot save | restore | check (v0.7.1) thrum tmux snapshot save [--reason self-initiated|external|context-threshold] Save conversation snapshot from JSONL transcript. thrum tmux snapshot restore Output snapshot to stdout and delete file. thrum tmux snapshot check Exit 0 if snapshot exists, 1 if not. No stdout. ### thrum daemon start | stop | status | restart | logs thrum daemon start [--local] Start daemon in background (10s socket wait) --local disables remote git sync (local-only mode) thrum daemon stop SIGTERM graceful shutdown (10s wait) thrum daemon status Running state, PID, uptime, version, sync state thrum daemon restart Stop + 500ms + start thrum daemon logs [--follow|-f] [--lines|-n N] [--since DURATION|DATE] (v0.8.0) View the daemon log file (.thrum/var/daemon.log). Default: last 50 lines. Flags: --follow / -f Stream new lines as they're written --lines / -n INT Number of lines to show (0 = all, default 50) --since STRING Time filter: "1h", "7d", "2026-04-09", or RFC 3339 The daemon uses lumberjack for log rotation: 10 MB max file size, 4 rotated backups, 28-day retention, gzip compression. Log level is controlled by `daemon.log_level` in config.json (debug|info|warn|error). ## RPC Methods All methods use JSON-RPC 2.0 protocol. ### health Request: {} (no params) Response: { status, uptime_ms, version, repo_id, sync_state, identity? } Notes: sync_state is "ok", "synced", "pending", or "error". identity sub-object (v0.9.0): { daemon_id, repo_name, hostname, repo_path, git_origin_url, init_at } ### agent.register Request: { role, module, display?, force?, re_register? } Response: { agent_id, status, conflict? } Status: "registered" | "updated" | "conflict" Conflict: { existing_agent_id, registered_at, last_seen_at } ### agent.list Request: { role?, module? } Response: { agents: [{ agent_id, kind, role, module, display, registered_at, last_seen_at }] } ### agent.whoami Request: {} (identity from env/config) Response: { agent_id, role, module, display, source, session_id?, session_start? } Source: "environment" | "identity_file" ### agent.listContext Request: { agent_id?, branch?, file? } Response: { contexts: [{ session_id, agent_id, branch, worktree_path, unmerged_commits, uncommitted_files, changed_files, git_updated_at, current_task, task_updated_at, intent, intent_updated_at }] } ### agent.delete Request: { name } Response: { agent_id, deleted } Notes: Removes identity file, message JSONL file, and database record. ### agent.cleanup Request: { force?, dry_run?, threshold? } Response: { orphans: [{ agent_id, name, reason }], deleted_count } Notes: threshold is days since last seen (default: 30). ### agent.set-status (v0.8.0) Request: { agent, status } Response: { agent, status } Fields: agent (string, required) — Agent name status (string, required) — "working" | "idle" | "blocked" Errors: `invalid status "": must be working, idle, or blocked` Notes: Daemon locates the agent's identity file (searches .thrum/identities/.json first, then scans all git worktrees via `git worktree list --porcelain`). Writes `agent_status` and `agent_status_updated_at`. ### team.list Request: { all? } Response: { agents: [{ agent_id, role, module, display, session, context, inbox }] } Notes: Returns rich status for each agent. all=true includes offline agents. ### context.save Request: { agent_name?, content } Response: { agent_name, saved_at } ### context.show Request: { agent_name?, raw?, no_preamble? } Response: { agent_name, content, preamble? } ### context.clear Request: { agent_name? } Response: { agent_name, cleared } ### context.preamble.show Request: { agent_name? } Response: { agent_name, content } ### context.preamble.save Request: { agent_name?, content } Response: { agent_name, saved_at } ### session.start Request: { agent_id, scopes?, refs? } Response: { session_id, agent_id, started_at } Notes: Auto-recovers orphaned sessions (marks as crash_recovered). ### session.end Request: { session_id, reason? } Response: { session_id, ended_at, duration_ms } Reason: "normal" (default) | "crash" | "superseded" ### session.list Request: { active?, agent_id? } Response: { sessions: [{ session_id, agent_id, started_at, ended_at, end_reason, last_seen_at }] } ### session.heartbeat Request: { session_id, add_scopes?, remove_scopes?, add_refs?, remove_refs? } Response: { session_id, last_seen_at } Notes: Extracts git work context. Updates last_seen_at in sessions table. ### session.setIntent Request: { session_id, intent } Response: { session_id, intent, intent_updated_at } ### session.setTask Request: { session_id, current_task } Response: { session_id, current_task, task_updated_at } ### message.send Request: { content, format?, structured?, thread_id?, scopes?, refs?, mentions?, tags?, acting_as?, disclose? } Response: { message_id, thread_id?, created_at } Format: "markdown" (default) | "plain" | "json" Mentions: Converted to refs with type "mention". acting_as: For user impersonation of agents (WebSocket users only). Notes: Triggers subscription notifications for matching subscriptions. ### message.get Request: { message_id } Response: { message: { message_id, thread_id?, author: { agent_id, session_id }, body: { format, content, structured? }, scopes, refs, metadata: { deleted_at?, delete_reason? }, created_at, updated_at?, deleted } } ### message.list Request: { scope?, ref?, thread_id?, author_id?, mentions?, unread?, page_size?, page?, sort_by?, sort_order? } Response: { messages: [{ message_id, thread_id?, agent_id, body, created_at, deleted, is_read }], total, unread, page, page_size, total_pages } sort_by: "created_at" (default) | "updated_at" sort_order: "asc" | "desc" (default) Page size: Default 10, max 100. ### message.edit Request: { message_id, content?, structured? } Response: { message_id, updated_at, version } Notes: Only the message author can edit. Triggers subscription notifications. ### message.delete Request: { message_id, reason? } Response: { message_id, deleted_at } Notes: Soft-delete. Message must exist and not already be deleted. ### message.markRead Request: { message_ids: [string] } Response: { marked_count, also_read_by? } Notes: Batch support. also_read_by maps message_id to list of other agent_ids that also read the message. ### sync.force Request: {} (no params) Response: { triggered, last_sync_at, sync_state } sync_state: "synced" | "idle" | "error" | "stopped" ### sync.status Request: {} (no params) Response: { running, last_sync_at, last_error?, sync_state } ### Peer RPC Methods (v0.7.0) ### peer.start_pairing Request: { timeout_seconds?, auth_key? } Response: { code, address } Notes: Generates a 16-digit pairing code and starts a Tailscale listener. auth_key is the Tailscale auth key if tsnet needs initializing. ### peer.wait_pairing (long-poll) Request: {} (no params) Response: { status, peer_name, peer_address, peer_daemon_id, message } Notes: Blocks until a remote peer completes pairing or timeout. status: "paired", "timeout", or "error". ### peer.join Request: { address, code, type, repo_path? } Response: { status, peer_name, peer_daemon_id, message, repo_name?, hostname?, repo_path?, git_origin_url? } Notes: Connects to a remote peer using the peercode. type is required (v0.9.0). repo_path sets transport to "local" for same-machine peers. 4 identity fields (omitempty, v0.9.0): exchanged at handshake; stored in peers.json as remote_repo_name, remote_hostname, remote_repo_path, remote_git_origin_url. ### peer.repair Request: { token, daemon_id, address, repo_name?, hostname?, repo_path?, git_origin_url? } Response: { status, peer_name, peer_daemon_id, reconcile_status } Auth: Bearer token (distinct from pairing peercode — issued at original pair time) Notes: Re-establishes a peer after address drift without a new peercode. Use when `peer list` shows drift_reconcile_failed status. Manual trigger: `thrum peer join --type repair ` Errors: CatTokenRejected — token invalid or expired CatOther — general failure ### peer.list Request: {} (no params) Response: [{ daemon_id, name, address, last_sync, last_synced_seq }] ### peer.remove Request: { name? | daemon_id? } Response: { status: "ok" } ### peer.status Request: {} (no params) Response: [{ daemon_id, name, address, has_token, paired_at, last_sync, last_synced_seq }] ### peer.configure Request: { peer_name, action, agent_name } Response: { ok, action, agent } Notes: action: "add-agent" or "remove-agent". Manages proxy agents for cross-repo routing. ### peer.address_changed Request: { peer_token, new_ip, new_port } Response: { ok } Notes: Called by remote peer to notify address change. Validates address per transport (loopback for local, CGNAT for Tailscale, same /24 for network). ### user.register (WebSocket only) Request: { username, display? } Response: { user_id, token, status } Notes: Only available via WebSocket transport. Username must be alphanumeric, underscore, or hyphen (1-32 chars). Cannot start with "agent:". ### user.identify (WebSocket only) Request: {} (no params) Response: { username, email, display } Notes: Returns the git user.name and user.email from the repo's git config. Username is sanitized (lowercase, alphanumeric + hyphens). ### Tmux RPC Methods (v0.7.1) ### tmux.create Request: { name, cwd, agent_name?, role?, module?, intent?, runtime?, force?, no_agent? } Response: { session, identity? } Fields: name (string, required) — tmux session name cwd (string, required) — working directory for the session agent_name (string) — v0.9.0: agent name for quickstart (required unless no_agent=true) role (string) — v0.9.0: agent role (required unless no_agent=true) module (string) — v0.9.0: agent module (required unless no_agent=true) intent (string) — v0.9.0: forwarded to quickstart runtime (string) — v0.9.0: runtime preset, forwarded to quickstart force (bool) — v0.9.0: overwrite existing runtime config no_agent (bool) — v0.9.0: skip quickstart, create bare session only Notes: v0.9.0: after creating the session, daemon runs `thrum quickstart` inside the pane with the provided flags. Single-identity-per-worktree enforced. ### tmux.launch Request: { name, runtime? } Response: { session, runtime } Default: runtime = "claude" ### tmux.status Request: {} (no params) Response: { sessions: [{ name, agent, role, module, state, runtime, branch }] } States: "alive" | "stale" | "dead" ### tmux.kill Request: { name } Response: null ### tmux.send Request: { name, text } Response: null ### tmux.capture Request: { name, lines? } Response: { content } Default: lines = 50 ### tmux.check-pane Request: { session, reason?, content? } Response: { session: string, state: string, reason: string } States: idle | permission | working | command_completed | working_but_idle Notes: Synthesized by daemon SessionPoller on stable pane content (10s poll, 2-hash stability, ~20s detection latency); rarely invoked directly. v0.9.0: pattern match and stability hash both run against the BOTTOM 15 lines of the capture (not the full 30-line window). An active prompt sits at the bottom; scoping to the tail stops re-firing on resolved prompts still visible in upper scrollback. ### tmux.restart Request: { name, force?, runtime? } Response: { session, snapshot_lines } Notes: v0.8.0: two flows selected by `force`. - Graceful (force=false, default): daemon sends @system save request to the agent, nudges the pane, polls for the snapshot file up to `restart.graceful_timeout` seconds. Falls back to JSONL extraction on timeout. - Force (force=true): skip graceful prompt, extract directly from JSONL conversation transcript. Only works for Claude Code (other runtimes don't use the same JSONL format). Either way, the session is killed, a new one is created, and the new session loads the snapshot via `thrum prime`. ### Queue RPC Methods (v0.8.0) ### tmux.queue Request: { session, text, requester, timeout_ms?, silence_ms?, notify_on_complete? } Response: { command_id, position } Fields: session (string, required) — Tmux session name text (string, required) — Command text to send requester (string, required) — Agent name (for @system notifications) timeout_ms (int64, default 120000) — Command timeout in ms silence_ms (int64, default 5000) — Silence threshold in ms notify_on_complete (*bool, default true) — Send @system on completion CommandID: `cmd_` format Notes: Enqueues a command for the session. Daemon dispatches FIFO when the pane goes silent and detects completion via the next silence event. Completion, timeout, cancellation, and interruption send `@system` messages to the requester unless notify_on_complete=false. Restart recovery always notifies regardless of notify_on_complete. ### tmux.queue-wait (long-poll) Request: { command_id, timeout_ms? } Response: { command_id, state, output?, elapsed_ms? } Notes: Polls the database every 500ms until the command reaches a terminal state (completed, cancelled, interrupted) or the timeout expires. Output is populated on terminal states. `elapsed_ms` is measured from SentAt (preferred) or SubmittedAt. ### tmux.queue-status Request: { session } Response: { session, active?, queued[] } Active: { id, text, requester_agent, state, silence_ms, notify_on_complete, submitted_at, sent_at?, completed_at?, captured_output?, session_name? } Notes: `active` is nullable (no active command). `queued` is an array of the same shape. States: queued | sent | completed | timeout_waiting | cancelled | interrupted. Terminal states: completed, cancelled, interrupted. ### tmux.cancel Request: { command_id } Response: { command_id, state, output? } Notes: Cancels a queued or active command. Partial captured output (last 500 lines) is included in the response. ### Monitor RPC Methods (v0.9.0) ### monitor.start Request: { name, argv, match, to, debounce_ms?, env? } Response: { id, name, status } Fields: name (string, required) — unique monitor name argv ([]string, required) — command + args (no shell) match (string, required) — Go regex pattern to (string, required) — recipient agent name (e.g., "reviewer") debounce_ms (int64, default 60000) — leading-edge debounce in ms (min 30000) env (map[string]string) — additional env vars for the child Errors: duplicate name, max 100 monitors exceeded, invalid regex, empty argv. ### monitor.list Request: { all? } Response: { monitors: [{ id, name, status, match, to, pid?, started_at?, exit_code? }] } Notes: Without all=true, returns only monitors with status "running". ### monitor.show Request: { id } Response: { id, name, argv, match, to, debounce_ms, env, status, pid?, started_at?, stopped_at?, exit_code?, last_match_at? } Notes: env values are redacted in the response. ### monitor.stop Request: { id } Response: { id, status } Notes: Sends SIGTERM to the child process. Status transitions to "stopped". ### monitor.logs Request: { id, lines? } Response: { id, lines: [string] } Notes: Returns up to `lines` recent output lines from the ring buffer (default: 50). ### monitor.restart Request: { id } Response: { id, name, status } Notes: Re-spawns the child using the same config. Only valid for stopped/failed monitors. ## Event Types (JSONL) Events stored in sharded JSONL files within `.git/thrum-sync/a-sync` worktree: sync/events.jsonl (agent lifecycle): agent.register Agent/user registration agent.session.start Session begins agent.session.end Session ends agent.cleanup Agent deletion sync/messages/{agent_name}.jsonl (per-agent): message.create New message message.edit Message content update message.delete Message soft-deletion agent.update Work context snapshot group.create Group created group.delete Group deleted group.member.add Member added to group group.member.remove Member removed from group All events have: { type, timestamp, ...event-specific-fields } Unknown event types are silently ignored (forward compatibility). ## Extended Workflows ### Multi-agent project coordination # Agent 1: Planner thrum quickstart --name --role planner --module core --intent "Planning sprint" thrum send "Sprint plan: @implementer handles auth, @reviewer reviews" \ --scope module:core --mention @implementer --mention @reviewer # Agent 2: Implementer thrum quickstart --name --role implementer --module auth --intent "Building auth flow" thrum inbox --mentions thrum sent --to @planner --unread thrum reply msg_01HXE... "Acknowledged, starting on auth module" thrum agent set-intent "Implementing OAuth2 flow" # Agent 3: Reviewer thrum quickstart --name --role reviewer --module auth thrum wait --mention @reviewer --timeout 10m ### Cross-machine messaging via Git sync # Machine A (has remote configured) thrum init thrum daemon start thrum quickstart --name --role agent-a --module backend thrum send "Backend API ready" --scope module:backend # Machine B (same remote) thrum init thrum daemon start thrum quickstart --name --role agent-b --module frontend # After sync (auto every 60s, or force): thrum sync force thrum inbox thrum sent # Sees message from agent-a ### Cross-machine messaging via Tailscale # Machine A thrum init && thrum daemon start thrum quickstart --name --role agent-a --module backend thrum peer add # Displays pairing code, e.g., 4821 # Machine B (same Tailscale network) thrum init && thrum daemon start thrum quickstart --name --role agent-b --module frontend thrum peer join 100.x.y.z # Enter code 4821 # Direct sync established, no Git remote needed thrum send "Ready for integration" --to @everyone # Message arrives on Machine A within seconds ### Session lifecycle management # Start thrum agent register --role impl --module auth thrum session start thrum agent set-intent "Working on OAuth" thrum agent set-task "beads:auth-oauth" # During work (periodically) thrum agent heartbeat thrum agent heartbeat --add-scope module:auth thrum agent set-intent "Debugging token refresh" # End thrum session end --reason normal # Crash recovery (automatic on next session start) thrum session start # Previous orphaned session auto-closed as crash_recovered ### Context save and recovery # Save context before ending session thrum context save --file session-notes.md # In new session, recover context thrum prime # Gather all context for recovery thrum context show # Show saved context # Manage preamble (stable header) thrum context preamble --init thrum context preamble --file custom-header.md # Sync context across worktrees thrum context sync ### File coordination with who-has # Before editing a shared file thrum who-has internal/db/schema.go # Output shows which agents have that file in their changes # Check overall team activity thrum team thrum agent list --context ### Runtime configuration # List available runtimes thrum runtime list # Generate config for a specific runtime thrum init --runtime claude # Quickstart with runtime thrum quickstart --name --role impl --module auth --runtime codex # Set default runtime for the repo thrum runtime set-default claude ## Daemon Details ### Socket Path Resolution Default: .thrum/var/thrum.sock (relative to --repo flag, default ".") Absolute: Resolved from repo path at daemon start. ### Auto-start The daemon does not auto-start. You must run `thrum daemon start` explicitly. Most commands that require the daemon will fail with a connection error if it is not running. ### WebSocket Server Default port: 9999 (override with THRUM_WS_PORT env var) Address: ws://localhost:{port}/ Actual port written to .thrum/var/ws.port Supports same RPC methods as Unix socket. ### WebSocket Events (Push Notifications) notification.message Sent when a message matches a subscription { message_id, preview, matched_subscription: { match_type } } ### Health Endpoint Method: "health" (via RPC) Returns: status, uptime_ms, version, repo_id, sync_state ### Process Management PID file: .thrum/var/thrum.pid Start: Forks background process, waits for socket (10s timeout) Stop: SIGTERM, waits for exit (10s timeout) Restart: Stop (best-effort) + 500ms delay + Start ### Sync Loop Interval: 60 seconds Worktree: .git/thrum-sync/a-sync (checked out on `a-sync` orphan branch) Process: 1. Acquire lock (.thrum/var/sync.lock) 2. Fetch remote in worktree (git -C .git/thrum-sync/a-sync fetch origin a-sync) 3. Merge remote into worktree (fast-forward or JSONL dedup merge) 4. Project new events into SQLite 5. Notify subscribers of new events 6. Commit local changes in worktree 7. Push worktree to remote 8. Release lock No branch switching -- all git operations happen in .git/thrum-sync/a-sync worktree Force sync: thrum sync force (non-blocking trigger) ### Stale Context Cleanup On daemon start, stale work contexts are cleaned up automatically. Contexts are considered stale when their session has ended or the git_updated_at timestamp is too old. ## Configuration ### config.json Schema (.thrum/config.json) Full schema for .thrum/config.json. Every field is optional — omit a block to use defaults. View resolved config with `thrum config show`. { "runtime": { "primary": "claude" // claude | codex | cursor | gemini | opencode | auggie | cli-only }, "daemon": { "local_only": true, // disable remote git sync "sync_interval": 60, // seconds between syncs (default 60) "ws_port": "auto", // "auto" or specific port like "9999" "peer_port": "auto", // Tailscale peer-to-peer listener port "single_agent_mode": true, // skip messaging infrastructure "log_level": "info" // v0.8.0: debug | info | warn | error (default: info) }, "peers": { "auto_connect": true, // auto-connect to known peers on daemon start "pairing_code_length": 16 // length of generated pairing codes }, "worktrees": { // v0.8.0: used by `thrum worktree create/teardown/list` "base_path": "~/.workspaces/myproj", // absolute path where new worktrees are created "beads_enabled": true, // create .beads/redirect in new worktrees "thrum_enabled": true // create .thrum/redirect + identities/ in new worktrees }, "orchestration": { // v0.8.0: used by the orchestrator role "merge_target": "main", // branch for final merge after all epics complete "default_autonomy": "end_only" // "per_epic" (approve each epic) | "end_only" }, "restart": { "max_lines": 200, // max lines in restart snapshot (default: 200; appends git/bd/inbox hint) "auto_threshold": 0, // 0-100 context % trigger for auto-restart; 0 = off "graceful_timeout": 30 // seconds to wait for graceful save before JSONL fallback }, "backup": { "dir": ".thrum/backup", // backup directory (relative or absolute) "schedule": "24h", // Go duration format; omit or "off" to disable "retention": { "daily": 5, // -1 keeps all; 0 keeps none "weekly": 4, "monthly": -1 }, "post_backup": "", // shell command to run after each backup "plugins": [] // third-party backup plugin definitions }, "permission_supervisors": ["coordinator"], // v0.9.0: agents to nudge on permission prompts // values: role names, @name, @user:name "project_name": "", // v0.9.0: owner ID for @supervisor_ pseudo-agent // defaults to filepath.Base(repo_root) "identity": { // v0.9.0: daemon identity (managed automatically) "daemon_id": "d_01HYTESTULID...", // set once; rotated only on legacy migration "repo_name": "thrum", // refreshed each Bootstrap "hostname": "mymachine", // refreshed each Bootstrap "repo_path": "/path/to/repo", // refreshed each Bootstrap "git_origin_url": "https://github.com/...", // set once "init_at": "2026-04-17T06:30:00Z" // set on creation/rotation; not refreshed }, "identity_guard": { // v0.9.0: enforcement checkpoints (strict|warn|off) "cross_worktree": "strict", // caller PID in wrong worktree "quickstart_self_rename": "strict", // caller already owns an identity here "quickstart_name_collision": "strict", // requested name held by live foreign PID "non_git_bootstrap": "strict", // thrum init/daemon start outside git repo "unauthenticated_rpc": "strict", // forged caller_agent_id or missing identity "daemon_writer_liveness": "strict", // daemon write to dead agent's identity file "prime_ownership": "strict", // thrum prime called from sub-agent "dead_pid_auto_reclaim": "warn" // stale PID auto-reclaimed (informational) } // daemon-level override: .thrum/var/guard-daemon.json // (same keys; daemon-level wins over repo-level wins over strict default) // unauthenticated_rpc/identity_mismatch (forgery rejection) // ignores mode config. Narrow v0.9.0 exception: // shared-worktree claim trust — if peercred picks Y but // client claims X and state.IsAgentInWorktree(X, peercred_wt) // is true, the claim is honored. Cross-worktree claims fail. // v0.9.1 (thrum-ndtw): anonymous-rejection scope tightened — // only PROVABLY anonymous (no git root above CWD, or no // registered-worktree match) triggers the allowlist. Peercred // introspection failures (kernel peer-creds or gopsutil.Cwd) // fall through to legacy client-asserted identity. slog.Warn // with step=pid failed / step=cwd failed is the diagnostic. } Config key for worktree base directory: `worktrees.base_path` (NOT `worktree_dir`). If you set `worktrees.base_path`, `thrum worktree create ` places new worktrees at `/`. Otherwise it defaults to `~/.workspaces//`. ### Environment Variables THRUM_NAME Agent name (highest priority for identity file selection) THRUM_ROLE Agent role (e.g., implementer, reviewer, planner) THRUM_MODULE Agent module/component (e.g., auth, db, ui) THRUM_DISPLAY Human-readable display name THRUM_LOCAL Enable local-only mode (disables remote sync) -- set to "1" THRUM_WS_PORT WebSocket port (default: auto) THRUM_HOME Pin repo path for all commands THRUM_AGENT_ID Pin caller identity for daemon RPC THRUM_SOCKET Unix socket path override THRUM_TS_AUTHKEY Tailscale auth key for peering THRUM_TS_PORT Tailscale listener port (default: auto) THRUM_TS_HOSTNAME tsnet hostname override THRUM_TS_STATE_DIR tsnet state directory THRUM_TS_ENABLED (deprecated) Tailscale auto-starts when peers are configured THRUM_NO_HINTS v0.9.0: suppress CLI hint output (Shape B stderr trailers) Truthy: any non-empty value except "0" or "false" Also suppressed by --quiet or --json flags ### Identity Resolution Chain (v0.7.0+) File selection (which .thrum/identities/*.json gets loaded): 1. THRUM_NAME env var → load {name}.json directly 2. Solo-agent auto-select → only one .json file exists 3. PID match → walk process tree to find runtime PID, match `agent_pid` field (v0.8.0: renamed from `claude_pid` for multi-runtime support) 4. Worktree match → filter by current git worktree name 5. Error if no unambiguous selection Field value overrides (after file is loaded): CLI flags (--role, --module) > env vars (THRUM_ROLE, THRUM_MODULE) > identity file PID adoption: after picking an identity, if its agent_pid is zero or dead, the current runtime PID gets written in. Skipped if agent_pid points to a different live process (genuine conflict). Single-identity-per-worktree (v0.9.0): `thrum tmux create` enforces that only one agent identity exists per worktree. If a different agent is already registered in the worktree's .thrum/identities/ directory, the command fails with an error rather than creating a second identity file. ensureWorktreeRedirects: when `thrum tmux create` detects that the target cwd is a git worktree without a .thrum/redirect file, it creates the redirect automatically before running quickstart, so the new agent shares the main repo's daemon and DB. ### Identity File (.thrum/identities/{name}.json) { "version": 5, "repo_id": "r_7K2Q1X9M3P0B", "agent": { "kind": "agent", "name": "furiosa", "role": "implementer", "module": "auth", "display": "Auth Implementer" }, "worktree": "auth", "agent_pid": 12345, "preferred_runtime": "claude", "runtime": "claude", "tmux_session": "implementer-auth:0.0", "agent_status": "working", "agent_status_updated_at": "2026-04-10T18:05:00.000Z", "reserved": false, "confirmed_by": "human:leon", "updated_at": "2026-04-10T18:02:10.000Z" } Created/updated by `thrum agent register` and `thrum quickstart`. Each agent gets a separate file named after the agent name (e.g., furiosa.json). Unnamed agents use {role}_{hash}.json. Field reference: agent_pid v0.8.0 (renamed from claude_pid): PID of runtime process preferred_runtime v0.8.0: set by `thrum quickstart --runtime`; used at launch runtime v0.7.1: auto-populated from preferred_runtime; backfilled at boot for legacy files that predate this field tmux_session v0.7.1: full pane target for daemon nudge delivery agent_status v0.8.0: working | idle | blocked | stuck stuck: set after 6 unanswered permission nudges; cleared on pane recovery (set via `thrum agent set-status`; stuck is daemon-only) agent_status_updated_at v0.8.0: UTC timestamp of last status change reserved v0.9.0: bool (omitempty); true for daemon-internal pseudo-agents (e.g., @supervisor_); hidden from `thrum team` unless --system `thrum prime` refreshes `tmux_session`, `preferred_runtime`, and `branch` on every run — matters when agents relaunch under a different runtime or move between branches. Auto-nudge: if a pane is silent but `agent_status` is "working", the daemon fires a nudge on the next silence event. ### Init Options thrum init creates: .thrum/ Top-level directory (gitignored entirely) .git/thrum-sync/a-sync/ Git worktree on `a-sync` orphan branch .git/thrum-sync/a-sync/events.jsonl Empty agent lifecycle event log .git/thrum-sync/a-sync/messages/ Per-agent message logs directory .thrum/identities/ Agent identity files directory .thrum/var/ Runtime directory a-sync branch Orphan branch for message sync Updates .gitignore with: .thrum/ Related commands: thrum worktree setup Configures .thrum/ in feature worktrees (creates redirect) ## MCP Server thrum mcp serve [--agent-id NAME] Starts an MCP server on stdin/stdout for native tool-based messaging. Requires the Thrum daemon to be running. Use --agent-id to select an identity file from .thrum/identities/. Configure in Claude Code's .claude/settings.json: { "mcpServers": { "thrum": { "type": "stdio", "command": "thrum", "args": ["mcp", "serve"] } } } MCP Tools: send_message Send a message to another agent (to: "@agent_name" or "@everyone") check_messages Poll for unread messages mentioning this agent, auto-marks read wait_for_message Block until a message arrives (WebSocket push) or timeout list_agents List registered agents with active/offline status broadcast_message Send to all agents (convenience wrapper around send_message to @everyone) ## Local-Only Mode Disables all git push/fetch in the sync loop. Use for public repos where you don't want to expose agent messages to origin. Enable via: thrum daemon start --local CLI flag (highest priority) THRUM_LOCAL=1 thrum daemon start Environment variable .thrum/config.json: { "local_only": true } Config file (auto-persisted) Priority: CLI flag > env var > config file > default (false) Check status: thrum sync status (shows "Mode: local-only" or "Mode: normal") When enabled: - Sync loop skips fetch/push to origin - Messages stay local to the machine - SQLite projection and JSONL logs still work normally - Setting persists across daemon restarts via .thrum/config.json ## Worktree Setup Scripts Scripts for batch-configuring redirect files across all git worktrees: ./scripts/setup-worktree-thrum.sh Auto-detect all worktrees ./scripts/setup-worktree-thrum.sh PATH Single worktree ./scripts/setup-worktree-beads.sh Auto-detect all worktrees ./scripts/setup-worktree-beads.sh PATH Single worktree Creates .thrum/redirect and .beads/redirect pointing to the main repo's .thrum/ and .beads/ directories. Idempotent -- skips already-configured worktrees. Redirect files allow feature worktrees to share the same daemon, message database, and issue tracker as the main repo without separate initialization. ## Toolkit Ready-to-use agent configurations and workflow templates in toolkit/: toolkit/agents/ Claude Code agent definitions thrum-agent.md Thrum messaging coordination guide (Beads plugin) Install via /install-plugin beads (recommended over agent file) message-listener.md Background message polling agent (Haiku model) README.md Installation and usage instructions toolkit/templates/agent-dev-workflow/ Four-phase skill pipeline templates implementation-agent.md Active: prompt template filled by project-setup, given to agents philosophy-template.md Active: reusable anti-patterns template for implementation standards planning-agent.md Reference: superseded by brainstorming + writing-plans skills worktree-setup.md Reference: superseded by project-setup Phase 3 CLAUDE.md Overview of the 4-phase Design/Plan/Setup/Implement workflow README.md Template customization guide Install agents: cp toolkit/agents/thrum-agent.md toolkit/agents/message-listener.md your-project/.claude/agents/ Install templates: cp toolkit/templates/agent-dev-workflow/*.md your-project/docs/templates/ ## Beads + Thrum Integration Beads (task tracking) and Thrum (messaging) together provide complete agent memory and coordination: Beads: what agents were doing -- tasks, status, dependencies, context recovery Thrum: what agents said -- messages, presence, notifications, session tracking Both use Git as persistence layer. State survives session boundaries, context compaction, and machine changes. Unified workflow: 1. bd create --title "Feature X" Create task in Beads 2. thrum send "Starting Feature X" Announce via Thrum 3. thrum agent set-task "beads:thrum-xyz" Link session to task 4. bd close thrum-xyz Complete task 5. thrum send "Feature X done" Notify team ## Database Tables (SQLite) messages All messages with body, author, timestamps, soft-delete message_scopes Message scope associations (many-to-many) message_refs Message reference associations (many-to-many) message_reads Read receipts (per message, per session/agent) message_edits Edit history message_deliveries Durable delivery/seen/read tracking per recipient (v14) agents Registered agents and users (v17: `claude_pid` renamed to `agent_pid` for multi-runtime) sessions Agent work sessions session_scopes Session scope associations session_refs Session reference associations subscriptions Push notification subscriptions agent_work_contexts Git work context per session (branch, files, commits, intent, task) groups Named messaging groups group_members Group membership (agent, role, or nested group) purge_metadata Latest purge cutoff for sync-aware filtering (v15) command_queue v18: per-session command queue (FIFO dispatch to tmux panes) v19: added `silence_ms` and `notify_on_complete` columns monitors v20: monitor job definitions (name, argv, match regex, target agent, debounce_ms, env, status, pid, started_at, stopped_at, exit_code) events Sequence-ordered deduplicated event log for daemon-to-daemon sync sync_checkpoints Per-peer sync progress tracking permission_nudges Pending permission-prompt nudges (v21) daemon_identity Local daemon identity cache (v23) telegram_msg_map Telegram ↔ Thrum message ID map (v24) schema_version Migration tracking Current schema version: 24 (as of v0.9.0) Key migrations since v0.7.0: v15 -> v16 `claude_pid INTEGER NOT NULL DEFAULT 0` added to `agents` v16 -> v17 `claude_pid` renamed to `agent_pid` (multi-runtime support) v17 -> v18 `command_queue` table added v18 -> v19 `silence_ms` and `notify_on_complete` columns added to command_queue v19 -> v20 `monitors` table added v20 -> v21 `permission_nudges` table added (restart-resilient permission-prompt nudges) v21 -> v22 `origin_daemon TEXT` column added to `agents` (cross-daemon registration scoping) v22 -> v23 `daemon_identity` table added (single-row local cache mirrored from config.json) v23 -> v24 `telegram_msg_map` table added (durable Telegram ↔ Thrum message ID map) ## Daemon RPC Flow (v0.9.0) Every unix-socket RPC goes through a 3-step pipeline before the handler runs: Step 1: Peercred PID extraction Linux: SO_PEERCRED syscall macOS: LOCAL_PEERPID getsockopt Result: caller OS PID (kernel-verified; cannot be spoofed) v0.9.1: introspection failure here is UNKNOWN STATE, not anonymous — returns raw error, slog.Warn "peercred.resolve step=pid failed", and server.go falls through to legacy client-asserted identity. Step 2: DaemonResolve — 3-priority identity chain 1. PID match: caller PID matches agent_pid in an identity file (highest trust) 2. Worktree match: caller PID's CWD maps to a known worktree path 3. caller_agent_id: client-asserted fallback (honored only when peercred absent, e.g., tests or WebSocket callers) On unix socket: client-asserted caller_agent_id is cross-checked against peercred resolution; mismatch fires unauthenticated_rpc/identity_mismatch (not guard-mode-gated). Shared-worktree claim trust (v0.9.0): when peercred picks agent Y but client claims X, DaemonResolve consults state.IsAgentInWorktree(X, peercred_wt). If X is also registered in the peercred-resolved worktree (session_refs match or identity-file fallback), the claim is honored — this covers multi-agent worktrees (E2E harnesses, peer proxies) where peercred's pick is arbitrary. Cross-worktree claims still fail. CLI now forwards caller_agent_id on message.delete / message.get plumbing paths so THRUM_NAME claims reach the resolver. Error taxonomy (v0.9.1, thrum-ndtw): peercred resolver distinguishes UNKNOWN STATE from PROVABLY ANONYMOUS. Only these two resolver outcomes wrap ErrAnonymous and trigger server.go's anonymous-allowlist enforcement: - CWD has no git root in its ancestor chain - CWD's git root matches no registered worktree Introspection failures (Step 1 kernel peercred, Step 2 gopsutil.Cwd) return raw errors so server.go falls through to the legacy client- asserted-identity path (pre-v0.9.0 behavior). Diagnostic: slog.Warn with step=pid failed or step=cwd failed captures the failing step. Step 3: Identity guard enforcement guard.Check() runs the applicable guard for the RPC type. In strict mode: returns error, blocks handler. In warn mode: logs identity_guard_fire slog event, allows through. In off mode: skips check entirely. 30 methods are on the anonymous allowlist (observability, bootstrap, etc.) and skip identity resolution entirely. ## Identity Guard Fire Event (v0.9.0) Slog event key: `identity_guard_fire` Emitted at: warn level (strict mode also returns error to caller) Fields: guard string Guard key (e.g., "cross_worktree") mode string "strict" | "warn" | "off" outcome string "denied" | "allowed" | "auto_reclaimed" | "skipped" reason string Error sub-code (see Guard Error Vocabulary below) caller_pid int OS PID of the calling process target_pid int PID from the identity file (if applicable) Operators can subscribe to this log event for identity-violation alerts. ## Guard Error Vocabulary (v0.9.0) Guard key Reason code Trigger --- --- --- cross_worktree pid_mismatch Caller PID owner ≠ identity owner unauthenticated_rpc identity_mismatch Forged caller_agent_id (mode-independent; exception: shared-worktree claim trust) unauthenticated_rpc anonymous_mutating_rpc Mutating RPC outside any worktree unauthenticated_rpc no_caller_agent_id Non-peercred transport missing CallerAgentID non_git_bootstrap not_a_git_repo thrum init/daemon start outside git repo daemon_writer_liveness subject_pid_dead Daemon refused write to dead agent identity prime_ownership caller_not_topmost_runtime thrum prime called from sub-agent quickstart_self_rename caller_already_owns_identity Caller already registered in this directory quickstart_name_collision name_held_by_live_foreign_pid Requested name held by different live PID Note: identity_mismatch ignores guard mode setting. Narrow runtime exception (not a config knob): shared-worktree claim trust — when peercred picks Y but client claims X AND X is co-located in the peercred-resolved worktree, the claim is honored. Cross-worktree claims still hit strict deny. All other guards respect strict/warn/off from identity_guard config block. ## CLI Hints Architecture (v0.9.0) Pre/post-action contextual hints on wired commands. Phase B pilot: `thrum init`, `thrum send`, `thrum tmux create`. Two output shapes: Shape B: stderr trailer text (human-readable; default on wired commands) Shape C: JSON `hints` array in --json output (scripting-friendly) Suppression: --quiet, --json (converts B→C), or THRUM_NO_HINTS=1 8 stable hint codes (Phase B): Code Severity AllowForce Command --- --- --- --- tmux.create.not-a-worktree warn false tmux create tmux.create.session-exists warn true tmux create tmux.create.identity-exists-alive warn false tmux create tmux.create.identity-exists-stale warn true tmux create tmux.create.next-launch info n/a tmux create tmux.create.identity-replaced info n/a tmux create send.recipient-stale info n/a send init.next-quickstart info n/a init AllowForce semantics: warn hints with AllowForce=true accept --force to proceed. AllowForce=false is a hard refusal — --force has no effect. Pre-action blocking: when a warn hint fires with AllowForce=false, execution is aborted before the RPC is dispatched. ## Snapshot Resume Plan Format (v0.9.0) `/thrum:restart` skill writes a structured `## Resume Plan` block as the tail anchor of the snapshot file. `thrum prime` reads back from this heading. mktemp single-source-of-truth: write to tempfile → cat to terminal → cat appended to snapshot → remove tempfile. Printed and appended copies cannot drift. Format (appended to snapshot file): ## Resume Plan **In flight:** **Next step:** **Open questions / blockers:** - ## Telegram Permission Routing (v0.9.0) Permission nudges auto-forward to Telegram when: - The Telegram bridge is configured - The bridge user is listed in `permission_supervisors` Reply paths: Path 1 — Threaded reply: tap Reply on the nudge message → type a token Accepted tokens (TryResolve set): y, yes, approve, a (approve) | n, no, deny, d (deny) Path 2 — Fresh DM (v0.9.0): send a new DM containing only a token Accepted tokens (fresh-DM set): y, yes, allow, n, no, deny Note: approve and a do NOT trigger fresh-DM resolution (threaded only) Resolves the most-recent pending nudge for that sender's Telegram user ID Routing table (additions): Source Action --- --- Threaded reply with TryResolve token Resolves the linked nudge Fresh DM with y/n/yes/no/allow/deny Resolves most-recent pending nudge for sender Restart resilience: `telegram_msg_map` SQLite table (schema v24) persists the Telegram ↔ Thrum message ID mapping across daemon restarts. In-flight approvals are not lost when the daemon restarts between nudge send and reply. ## Permission Prompt Detection Details (v0.9.0) Claude per-shape deny key (`DisambiguateClaudeDeny`): claude.tool_confirmation matches 3 observable prompt shapes under one regex anchor. At first detection, the scheduler picks the deny key based on the captured pane (ANSI-stripped): Shape Deny key stored on NudgeRow --- --- Variant A — 3-option Write/Exec (3. No, and tell…) "3" Variant B-Bash — 2-option (option 2 starts with "No") "2" Variant B-Read — 2-option (option 2 = "don't ask…") "Escape" (defensive) No 1/2/3 option lines detected "Escape" Variant B-Read falls back to Escape because option 2 is session-scoped forever-allow — sending "2" would grant a forever-allow, not a deny. The disambiguated key is written to NudgeRow.DenyKey at firstDetect so every reminder in the thread carries the same keystroke. Snippet truncation is TAIL-biased: `truncatePaneTail` keeps the last 5-6 lines of the captured pane for the nudge body (claude tool_confirmation prompts are 5-6 lines; cap=6). Pane capture is ANSI-stripped before option-shape matching to prevent inline SGR escapes (e.g., `\x1b[32m3. No, …\x1b[0m`) from defeating start-of-line anchors. Non-claude runtimes use their library DenyKey verbatim; DisambiguateClaudeDeny is gated to `runtime == "claude"`. Detection scoping (k4wf): pattern regex AND OnDetection hash both run against the bottom 15 lines of the capture window (not the full 30-line tail). Prevents re-firing firstDetect on resolved prompts still in upper scrollback. `bottomLines()` handles trailing-newline and CRLF normalization. Nudge dispatch observability (`[nudge]` slog tag): structured slog.Info events at four points in the fan-out for routing visibility: Site Fields --- --- SetOnEventWrite entry msg_id, sender, recipients, origin_daemon, session_id SetOnEventWrite per-recipient msg_id, recipient, target (or skip reason) nudge.DispatchTmux msg_id, recipient, target, session, self_echo telegram outbound.handleNotify msg_id, bridge_user, self_echo_candidate self_echo=true means the dispatch target resolved to the sender's own pane; the daemon refuses the write and logs the skip. Used by the kfn3/l9s1 investigation to diagnose phantom-pane misroutes.