Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 11 additions & 10 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ flt is a **set of primitives a user can compose however they want.** `spawn`, `k
- **Worktree-per-agent isolation.** `src/worktree.ts` creates a `flt/<name>` branch and worktree under `tmpdir()` so concurrent agents don't trample each other's git state. `--no-worktree` opts out.
- **Per-CLI adapters.** `src/adapters/` defines `CliAdapter` (spawn args, instruction filename, ready-state detection, dialog auto-approval, status detection). Most adapters delegate to `@twaldin/harness-ts` for shared detection logic; flt adds the spawn/lifecycle/dialog-bypass on top.
- **Status by polling pane content.** No agent SDKs are wired up. `src/controller/poller.ts` runs `tmux capture-pane`, feeds the buffer to `adapter.detectStatus`, and writes status transitions back to state. Workflow advancement, ephemeral cleanup, and TUI status colors all hang off this single transition signal.
- **Instructions are projected, not owned.** `src/instructions.ts` (delegating to `@twaldin/harness-ts`) prepends an `` block to the project's native instruction file (`CLAUDE.md`, `AGENTS.md`, `GEMINI.md`, etc.) and restores the original on kill. flt never owns the file outside its markers.
- **Instructions are projected, not owned.** `src/instructions.ts` (delegating to `@twaldin/harness-ts`) prepends a block delimited by `flt:start` / `flt:end` to the adapter's instruction file (`CLAUDE.md`, `AGENTS.md`, `GEMINI.md`, etc.) and restores the original on kill. flt never owns the file outside its markers.

## Repo map

Expand All @@ -24,6 +24,7 @@ src/
tmux.ts tmux session lifecycle + key delivery
worktree.ts git worktree creation under tmpdir()
instructions.ts project instruction file projection (flt:start/end)
flt-skill.ts synthetic flt protocol skill builder
presets.ts preset CRUD (~/.flt/presets.json)
skills.ts skill discovery + projection per CLI
harness.ts @twaldin/harness-ts wrapper bits
Expand All @@ -40,19 +41,19 @@ src/

adapters/ per-CLI adapters — see src/adapters/CLAUDE.md
commands/ one file per `flt <cmd>` (init/spawn/send/kill/…)
controller/ daemon: server.ts + client.ts + poller.ts
controller/ daemon: server.ts + client.ts + poller.ts + reaper.ts
workflow/ YAML workflow engine — see src/workflow/CLAUDE.md
tui/ raw-ANSI TUI — see src/tui/CLAUDE.md
pr-adapters/ gh / gt / manual PR creation backends
hooks/ git hook installers
types/ ambient module declarations (e.g. harness-ts.d.ts)
utils/ stripAnsi etc.

templates/ instruction-file templates (system-block-*.md, workflow-block.md)
docs/ design docs (droid-oauth-proxy.md, ssh-sandbox-design.md)
templates/ instruction blocks, flt protocol skill, roles, skills, workflows
docs/ isolated TUI testing guide (testing-tui.md)
demo/ demo gif assets and scripts
examples/ example workflows + presets
scripts/ release + smoke-test scripts
examples/ example workflows
scripts/ diagnostic, fixture, and smoke-test scripts
tests/
unit/ bun test — pure functions, parsers
integration/ bun test — end-to-end flows (real tmux/git)
Expand All @@ -66,18 +67,18 @@ tests/
- **Bun-only.** Runtime is Bun (≥1.0.0). Tests use `bun test`. Don't introduce node-only APIs without checking; many `Bun.*` calls (`Bun.spawn`, `Bun.file`, fetch-over-unix-socket) appear by design.
- **TypeScript with no `any` casts.** No `as any` / `as unknown as` shortcuts. Add narrow types or fix the source type.
- **One command per file in `src/commands/`.** Each file exports a `<cmd>Direct(...)` function (callable in-process by tests and the controller) and a thin Commander wrapper in `cli.ts`. Don't fold multiple subcommands into one file.
- **One adapter per file in `src/adapters/`** following the `CliAdapter` interface (`src/adapters/types.ts`). Register the new adapter in `src/adapters/registry.ts` and add it to the `Record<string, CliAdapter>` map. See `src/adapters/CLAUDE.md`.
- **One adapter per file in `src/adapters/`** following the `CliAdapter` interface (`src/adapters/types.ts`). Register the new adapter in `src/adapters/registry.ts`: import it and update `knownAdapters`, `adapterCommands`, and `adapterFactories`. See `src/adapters/CLAUDE.md`.
- **Workflow steps are typed by `type` field.** New step kinds add a discriminated union member in `src/workflow/types.ts` and a handler in `src/workflow/engine.ts`. See `src/workflow/CLAUDE.md`.
- **`flt:start`/`flt:end` markers are sacred.** Anything inside is regenerated on spawn and cleaned on kill. Never hand-edit content between those markers — edit the templates in `templates/` or the projection logic in `src/instructions.ts` instead.
- **Filenames for instructions: CLAUDE.md is canonical, AGENTS.md is a symlink.** Where a CLAUDE.md exists in this repo, an `AGENTS.md` symlink points to it so OpenCode/Codex/etc. pick up the same content. Don't divergently edit AGENTS.md.

## Gotchas

- **Adapter parity isn't perfect.** Each CLI has its own dialog phrasing, ready signal, and submit-key sequence. When you add behavior, exercise it on at least claude-code + codex + opencode; they cover the three main quirks (multi-line paste, slow startup, custom instruction file).
- **Aider was removed** from the registry — it's REPL-driven (`/run`, `/add`, `/edit`) with no autonomous shell tool, so it doesn't fit the autonomous-agent-with-tools model. The README still mentions it; don't re-add an aider adapter without revisiting that decision.
- **Aider was removed** from the registry — it's REPL-driven (`/run`, `/add`, `/edit`) with no autonomous shell tool, so it doesn't fit the autonomous-agent-with-tools model. Don't re-add an aider adapter without revisiting that decision.
- **OpenCode uses a custom agent file** (`.opencode/agents/flt.md`), not the project's `AGENTS.md`. See `src/adapters/opencode.ts`.
- **Status polling, not push.** A change in the running agent's pane is observed at most ~1s late. Tests that race against status transitions need to wait, not assume.
- **`flt kill` nukes the worktree.** If you used the helper worktree to stash diffs, capture them before kill.
- **`flt kill` removes the worktree by default.** Capture any needed diffs first, or use `flt kill <name> --preserve-worktree` to retain it for recovery.
- **Workflow advancement fires on `running → idle`.** Steps that exit before going idle (instant errors, refusals to start) won't trigger `advanceWorkflow`. Add an explicit failure path if you introduce a new such case.
- **TUI is a separate process from the controller.** `flt controller stop && flt controller start` does NOT restart the TUI — the TUI process loaded `panels.ts` and other rendering code ONCE at launch and keeps using that snapshot. After syncing source changes into the install directory (or merging a fix that touches sidebar/render code), you have to quit the TUI (`q` or kill the `flt tui` process) AND restart it for the change to take effect. The controller restart only matters for spawn/kill/poller logic, not for what's painted on screen.
- **Two install paths exist for `flt`.** The `flt` binary on `$PATH` is typically `~/.bun/bin/flt`, which symlinks to `~/.bun/install/global/node_modules/@twaldin/flt-cli/src/cli.ts`. There is ALSO an `~/.nvm/versions/node/<v>/lib/node_modules/@twaldin/flt-cli/` install if `npm i -g` was used at any point. When syncing local source changes into the install directory for testing, copy into BOTH paths (or at least the one that backs `$(readlink "$(which flt)")`) — copying to only one means the running TUI/controller might still be on the stale code. `md5 ~/flt/src/foo.ts <each-install-path>/src/foo.ts` is the quickest verifier.
Expand All @@ -86,6 +87,6 @@ tests/

- Tests: `bun test`, `bun test:unit`, `bun test:integration`, or `bun test <path>` for one file.
- Local CLI run: `bun src/cli.ts <cmd>` (the `flt` bin in `package.json` points at the same).
- Don't `git add -A` blindly — untracked session artifacts (`AUDIT.md`, `HANDOFF.md`, `tree.md`, `plan.json`, `handoffs/`, etc.) live at the root and are gitignored; only `AGENTS.md` is a symlink (→ CLAUDE.md).
- Don't `git add -A` blindly — untracked session artifacts (`AUDIT.md`, `HANDOFF.md`, `tree.md`, `plan.json`, `handoffs/`, etc.) live at the root and are gitignored; preserve them and the `AGENTS.md` → `CLAUDE.md` symlinks.
- For a feature that touches adapter behavior, read the corresponding `harness-ts` adapter first; many fields are inherited and shouldn't be overridden in flt.
- `scripts/tui-pilot.sh smoke` spins up a fully isolated flt (private HOME + tmux server) and pilots the TUI via tuistory — use it to verify TUI/skill changes without touching the live fleet; see `docs/testing-tui.md`.
21 changes: 20 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,29 @@ Thanks for the interest. flt is a solo project; I merge PRs when I have time.

```bash
bun install
bunx tsc --noEmit
bun test
```

All tests must pass (`bun test`). If your change needs new tests, add them.
All tests and the typecheck must pass. CI runs `bunx tsc --noEmit`, then the unit and integration suites separately. `bun test` also includes adapter telemetry tests.

Integration tests create real tmux sessions. On a machine with a live fleet, run tests with a temporary HOME and private tmux socket directory, with `TMUX` unset so it cannot select your live server:

```bash
(
sandbox=$(mktemp -d /tmp/flt-tests.XXXXXX)
mkdir -p "$sandbox/tmux"
trap 'env -u TMUX TMUX_TMPDIR="$sandbox/tmux" tmux kill-server 2>/dev/null || true; rm -rf "$sandbox"' EXIT
git config --file "$sandbox/.gitconfig" init.defaultBranch main
git config --file "$sandbox/.gitconfig" user.name "flt tests"
git config --file "$sandbox/.gitconfig" user.email "flt-tests@example.invalid"
env -u TMUX -u FLT_AGENT_NAME -u FLT_PARENT_NAME -u FLT_PARENT_SESSION \
-u FLT_DEPTH -u FLT_SKILLS_DIR -u FLT_ALLOW_NO_WORKTREE \
HOME="$sandbox" TMUX_TMPDIR="$sandbox/tmux" bun test
)
```

Install dependencies before entering the sandbox. The temporary Git configuration is only for fixture repositories; your configured identity is unchanged. The tool-gated TUI pilot test skips when `tctl` is absent from the temporary HOME. See [the TUI testing guide](docs/testing-tui.md) for separate TUI verification.

## Style

Expand Down
38 changes: 21 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ Requires [Bun](https://bun.sh) and [tmux](https://github.com/tmux/tmux). You nee
## Quick start

```bash
flt init # initialize fleet + start controller
flt tui # open the terminal UI
flt init # run inside tmux: initialize fleet, start controller, open TUI
# After quitting the TUI with q, reconnect with: flt tui

# spawn agents in any CLI from the same interface
flt spawn coder -c claude-code -m sonnet -d ~/project "fix the login bug"
Expand All @@ -46,7 +46,7 @@ flt presets add coder -c codex -m gpt-5.3-codex
flt spawn coder -d ~/project "fix the parser bug" # name == preset, auto-resolved
```

Presets can store everything — `cli`, `model`, `dir`, `parent`, `worktree`, `persistent`, `soul`. A fully configured preset means spawn is just name + task:
Presets can also store `dir`, `parent`, `worktree`, `persistent`, and `soul` in `~/.flt/presets.json`; `flt presets add` exposes CLI, model, and description. A fully configured preset means spawn is just name + task:

```bash
*/30 * * * * flt spawn monitor "run health checks" # dir, parent, cli, model all in preset
Expand Down Expand Up @@ -79,7 +79,8 @@ DEC 2026 synchronized output for zero-flicker rendering on modern terminals (Gho
| Normal | `s` | Spawn |
| Normal | `r` | Reply to selected agent |
| Normal | `m` | Inbox |
| Normal | `t` | Shell |
| Normal | `t` | Metrics |
| Normal | `T` | Shell |
| Normal | `K` | Kill agent |
| Normal | `q` | Quit |
| Log focus | `j/k` | Scroll |
Expand Down Expand Up @@ -111,15 +112,15 @@ Each adapter handles the messy per-CLI differences so you don't have to:
| SWE-agent | `swe-agent` | Prompt template injection, no instruction file |
| pi | `pi` | OpenAI subscription OAuth flow, slash-command prompt detection |

Dialog auto-approval means agents spawned from cron never block on permission prompts. This is what makes unattended operation work.
Adapters auto-approve recognized dialogs to support unattended operation. Unknown prompts or missing authentication can still block an agent; inspect its output with `flt logs <name>`.

## Agent identity

Agents get their identity from two sources:

**SOUL.md** — who the agent is. Lives at `~/.flt/agents/<name>/SOUL.md` or referenced via preset. Defines role, behavior, domain knowledge. Injected into the CLI's native instruction file on spawn.

**Skills** — what the agent can do. Markdown files in `~/.flt/skills/` (global) or `~/.flt/agents/<name>/skills/` (per-agent). For Claude Code, skills become slash commands. For other CLIs, skills are embedded in the instruction file.
**Skills** — what the agent can do. Each skill is a directory containing `SKILL.md` under `~/.flt/skills/`. Enable skills with a preset's `skills` list, repeatable `flt spawn --skill <name>`, or `--all-skills`. flt copies selected skill directories into the adapter's project-local skill area; discovery and instruction-file indexing vary by CLI.

The project's own instructions (CLAUDE.md, AGENTS.md, GEMINI.md) stay untouched — flt prepends its block with markers and removes it on kill.

Expand All @@ -146,31 +147,34 @@ Messaging is simple: `flt send parent` routes to whoever spawned you. `flt send

## Workflows

YAML state machines that chain agents together:
YAML state machines that chain agents together. This example uses the `cc-coder` and `cc-reviewer` presets seeded by `flt init` (both use Claude Code):

```yaml
# ~/.flt/workflows/code-review.yaml
name: code-review
steps:
- id: implement
preset: coder
task: "Implement {task}"
preset: cc-coder
task: "Implement {task}. Address previous review feedback: {fail_reason}"
max_retries: 2
on_complete: review

- id: review
preset: reviewer
preset: cc-reviewer
dir: "{steps.implement.worktree}"
worktree: false
task: "Review PR {pr}. Branch: {steps.implement.branch}"
on_complete: done
on_fail: implement
max_retries: 2
```

```bash
flt workflow run code-review -t "add OAuth login"
flt workflow status code-review
flt workflow run code-review -t "add OAuth login" -d ~/project
flt workflow status # show active runs
# For one run, use the ID printed by run/list: flt workflow status <run-id>
```

Later steps can reference earlier agents' worktrees, branches, and PRs via template variables. Agents signal transitions with `flt workflow pass` or `flt workflow fail`.
Later steps can reference earlier agents' worktrees and branches; `{pr}` holds the auto-created PR URL when available. The reviewer uses the implementer's worktree rather than a new checkout. `max_retries` belongs on the retry target (`implement` here) to bound the review loop. Agents signal transitions with `flt workflow pass` or `flt workflow fail`.

## Architecture

Expand All @@ -182,12 +186,12 @@ Later steps can reference earlier agents' worktrees, branches, and PRs via templ
config.json # settings, theme
inbox.log # agent messages
activity.log # JSONL event stream
workflows/ # YAML definitions + run state
workflows/ # YAML definitions
runs/<run-id>/ # run.json, results, handoffs, and other run artifacts
skills/ # global skills
agents/<name>/
SOUL.md # identity
state.md # agent state (compaction/resume)
skills/ # per-agent skills
```

```
Expand Down Expand Up @@ -218,7 +222,7 @@ flt activity # fleet event log
flt exit # shut down fleet
flt presets list|add|remove # manage presets
flt workflow run|status|list|cancel # manage workflows
flt skills list # list available skills
flt skill list # list available skills
flt controller start|stop|status # manage controller daemon
```

Expand Down
23 changes: 15 additions & 8 deletions docs/testing-tui.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,23 +152,27 @@ reviewer's job is to judge it.

## Cross-CLI skill discovery verification (plan 007 worked example)

Use the harness to confirm each adapter finds `.flt/skills/flt/SKILL.md` at
spawn time. For each CLI adapter to test (here: `pi` as a cheap proxy):
Use the harness to check projected skill files and confirm that a CLI can
discover them at spawn time. Projection paths vary by adapter; see
`projectSkills` in `src/skills.ts`. The example below uses `pi`, which currently
projects into `.flt/skills/`. Running it invokes a real provider-backed agent;
it is not part of the TUI-chrome smoke test.

```bash
ISO=$(bash scripts/tui-pilot.sh up --link ~/.claude --link ~/.codex)
export FLT_PILOT_HOME=$ISO
WORKDIR=$(mktemp -d "$ISO/skill-check.XXXXXX")

# Spawn a cheap agent with skill projection enabled.
# Spawn an agent in a private throwaway project directory.
# FLT_ALLOW_NO_WORKTREE=1 is an env var (read via process.env), not a CLI arg —
# it must be passed as a prefix or via the env command.
FLT_ALLOW_NO_WORKTREE=1 \
env -u TMUX HOME=$ISO TMUX_TMPDIR=$ISO/tmux \
bun src/cli.ts spawn skill-check --cli pi --no-worktree --dir /tmp
bun src/cli.ts spawn skill-check --cli pi --no-worktree --dir "$WORKDIR"

# Check that the flt skill was projected into the agent's instruction file area.
# The skill projection lands in the project dir supplied via --dir:
ls /tmp/.flt/skills/flt/SKILL.md
ls "$WORKDIR/.flt/skills/flt/SKILL.md"

# Or grep the agent logs to confirm skill injection:
env -u TMUX HOME=$ISO TMUX_TMPDIR=$ISO/tmux \
Expand All @@ -178,6 +182,9 @@ env -u TMUX HOME=$ISO TMUX_TMPDIR=$ISO/tmux \
bash scripts/tui-pilot.sh down --rm
```

For a full cross-adapter sweep, repeat for each adapter (`claude-code`, `codex`,
`opencode`, etc.) and assert that `.flt/skills/flt/SKILL.md` is present in the
worktree after spawn and absent after kill.
For a cross-adapter sweep, check the destination selected by `projectSkills`
for each adapter, not a universal `.flt/skills/` path. Claude Code uses
`.claude/skills/`, OpenCode uses `.opencode/skills/`, and Droid uses
`.factory/skills/`. Check file projection separately from actual CLI discovery;
the latter requires an installed, authenticated CLI. Use a fresh private
project directory per probe and verify managed files are removed after kill.
4 changes: 2 additions & 2 deletions src/adapters/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,9 @@ When changing detection behavior, **fix it in `harness-ts` first** — flt and t
1. Confirm the CLI has an autonomous shell tool (not REPL-only). aider was removed for failing this test.
2. Add (or reuse) a harness-ts adapter for shared detection.
3. Create `src/adapters/<name>.ts` exporting `<name>Adapter: CliAdapter`.
4. Register it in `registry.ts` (import + add to the `adapters` map).
4. Register it in `registry.ts`: import it and update `knownAdapters`, `adapterCommands`, and `adapterFactories`.
5. Add fixtures under `tests/adapters/` covering ready detection, dialog auto-approval, idle/running transitions.
6. Update `docs/adapters.md` (end-user table) and any README adapter listing.
6. Update the end-user adapter table in `README.md`.

## Currently registered

Expand Down
Loading
Loading