claustre claustre

Architecture

claustre is a single-binary Rust application with seven modules, each with one responsibility. This page describes how the pieces fit together: the module layout, entity model, hook-based communication, and the session lifecycle.

Module Overview

Each module owns a single concern. The binary compiles to one executable with no runtime dependencies beyond SQLite (bundled), gh CLI, and optionally npx for skills.

Module Purpose
main.rs CLI entry point (clap). Dispatches to TUI or subcommands
config/ Config loading (config.toml), CLAUDE.md merge, path helpers
store/ SQLite database: schema, models, CRUD queries
tui/ ratatui terminal UI: app state, event loop, rendering
session/ Git worktree lifecycle + session setup
pty/ Native PTY embedding via portable-pty + vt100
skills/ skills.sh CLI wrapper, ANSI parser

Entity Model

Relationships

Project 1──* Task 1──* Subtask
Project 1──* Session
Task *──0..1 Session (assigned via session_id FK)

Entities

  • Project — a git repository registered in claustre. Has a name and repo_path.
  • Task — a unit of work belonging to a project. Has a title, description, status, mode (autonomous/supervised), and an optional session_id linking it to the session executing it. Tracks token usage (input_tokens, output_tokens) and timing (started_at, completed_at).
  • Subtask — an optional breakdown of a task into steps. When a task has subtasks, they are all included in the prompt as an ordered list. Claude works through them sequentially in a single session.
  • Session — a running Claude Code instance tied to a project. Maps 1:1 to a git worktree + embedded terminal tab. Tracks claude_status (idle/working/done/error) and git diff stats (files_changed, lines_added, lines_removed).

Communication Architecture

claustre uses Claude Code's hook system and CLI subcommands instead of an MCP server. Hooks fire after each Claude turn and call back into the claustre binary to update state in SQLite. The TUI polls the database every second to pick up changes.

┌─────────┐   hooks    ┌──────────────────┐  writes   ┌──────────┐  reads    ┌─────────┐
│ Claude   │ ──fires──> │ claustre         │ ────────> │  SQLite  │ <──poll── │   TUI   │
│ Session  │            │ session-update   │           │   (WAL)  │           │  (1s)   │
└─────────┘            └──────────────────┘           └──────────┘           └─────────┘

Hooks

Each worktree gets three hooks registered in .claude/settings.local.json. The TaskCompleted and Stop hooks source a shared _claustre-common.sh helper.

TaskCompleted hook

Progress sync

  1. Reads Claude's internal task progress from ~/.claude/tasks/<session_id>/ and writes progress.json to ~/.claustre/tmp/<session_id>/
  2. Calls claustre session-update --session-id <ID> (no token extraction — deferred to Stop hook)

Stop hook

Final validation + usage

  1. Final sweep of task progress and writes progress.json
  2. Extracts cumulative token usage from Claude's JSONL conversation log
  3. Checks for an open PR on the current branch via gh pr view
  4. Calls claustre session-update --session-id <ID> [--pr-url <URL>] [--input-tokens N --output-tokens N --cost F]

UserPromptSubmit hook

Resume signal

  1. Reads session ID from .claustre_session_id
  2. Calls claustre session-update --session-id <ID> --resumed
  3. If the session has an in_review task, transitions it back to working

Task Status Lifecycle

pending ──[launch]──> working ──[PR detected]──> in_review ──[PR merged]──> done
                         ↑                            │
                         └───[user resumes]────────────┘
Transition Trigger Where
pending → working User presses l (launch), or feed-next picks up next task session::create_session(), main::run_feed_next()
working → in_review Stop hook detects a PR via gh pr view and calls claustre session-update --pr-url main.rs SessionUpdate handler
in_review → working UserPromptSubmit hook detects user activity and calls session-update --resumed main.rs SessionUpdate handler
in_review → done PR merge poller detects merge (auto), or user presses r (manual) tui/app.rs poll + key handler

Session Creation Flow

When a user presses l on a pending task, claustre executes the following sequence to stand up an isolated environment and launch Claude:

  1. create_worktree() — runs git worktree add from the project repo to create an isolated working copy
  2. write_merged_config() — merges global + project CLAUDE.md, copies hooks into the worktree
  3. store.create_session() — inserts a session row in the database
  4. Write session marker — writes .claustre_session_id and hook scripts into the worktree
  5. pre_trust_worktree() — seeds ~/.claude.json to skip the trust dialog
  6. Return SessionSetup — contains session, claude command, worktree path, and tab label
  7. TUI spawns terminals — creates SessionTerminals (shell + Claude PTYs) and adds a session tab

Key Patterns

State refresh via polling

The TUI runs a 1-second tick. On each tick, refresh_data() re-queries the database to pick up any changes from the Stop hook, session-update, or feed-next. This is simpler than cross-thread channels and provides acceptable dashboard latency.

Pre-fetched sidebar summaries

build_project_summaries() queries session and task data for all projects up front and stores results in a HashMap<String, ProjectSummary>. This avoids N+1 queries during rendering.

Config inheritance

Worktree config is assembled at session creation time in session::write_merged_config(). The merge order is:

  1. Global CLAUDE.md (~/.claustre/CLAUDE.md)
  2. Project-level CLAUDE.md (~/.claustre/projects/<name>/CLAUDE.md)
  3. Repository CLAUDE.md (checked into the repo)

Hooks follow the same pattern: global hooks are copied first, then project-specific hooks override by filename.