mfbt CLI is a Python-based CLI tool for the mfbt platform, targeting publication to PyPI. It provides both an interactive TUI mode (K9S-style) and traditional subcommands for managing mfbt projects.
- Language: Python 3.10+ (uses modern features: pattern matching, improved type hints)
- Backend: Integrates with mfbt REST API (
{BASE_URL}/api/v1/) - Auth: OAuth 2.1 with PKCE (1hr access tokens, 30-day refresh tokens), plus API key management (
mfbtsk-{uuid}, passed asAuthorization: Bearer {key}) - Config: Global
~/.mfbt/directory stores auth tokens and configuration (no per-project config)
The file openapi.json in the project root contains the full OpenAPI spec for the mfbt backend. Do NOT read this file in full — it is very large. Instead, use Grep or search tools to find specific endpoints, schemas, or parameters as needed.
- Authentication & Configuration — Browser-based OAuth flow with PKCE, auto token refresh + interactive re-auth on 401, API key support. Shared auth flow in
auth_flow.py. Config/auth in~/.mfbt/(schema v2, no per-projectproject_id). - Interactive TUI Mode — Launched when CLI runs without subcommands; K9S-style keyboard-driven navigation: Projects > Phases > Modules > Features. Ralph orchestration integrated via
rkey. - Subcommands —
auth,projects(list, show, create, archive, delete),ralph,tui. Consistent output formatting (table, JSON, etc.) - API Integration Layer — REST client with
on_auth_requiredcallback for auto re-auth on 401. Paginated responses ({items, total, page, page_size, total_pages}), error handling (402 for token limits), job polling, WebSocket support. - API Key Management — CRUD via
/api/v1/users/me/api-keys - Coding Agent Checks —
coding_agents.pyprovides pre-flight checks (agent installed, MCP configured) before Ralph orchestration.
- Projects CRUD
- Brainstorming phases
- Modules, features, and implementations
- Job monitoring:
GET /api/v1/jobs/{job_id} - Thread comments
- MCP config retrieval:
GET /api/v1/projects/{id}/mcp-config - API key management:
/api/v1/users/me/api-keys
- Must work with existing mfbt FastAPI backend
- Support both UUIDs and short URL identifiers
- Handle ISO 8601 UTC timestamps
- Graceful degradation when token limits reached (HTTP 402)
This project uses the mfbt MCP server, which exposes a virtual filesystem (VFS) for navigating mfbt project data. Always call readMeFirst at the start of a session to get the full VFS guide, available tools, and recommended workflow.
- The VFS exposes project phases, modules, features, and implementations as a navigable filesystem
- Use UNIX-like commands:
ls,cat,tree,find,grep,head,tail - Smart metadata on directories guides what to work on next (progress %, next feature, completion status)
- Two feature sources:
system-generated/(AI-created from brainstorming) anduser-defined/(manually created or imported)
/
├── phases/
│ ├── system-generated/{phase}/
│ │ ├── phase-spec/ # full.md, summary.md, by-section/
│ │ ├── phase-prompt-plan/ # full.md, by-section/
│ │ └── features/{module}/{feature}/
│ │ ├── implementations/{impl}/
│ │ │ ├── spec.md # WHAT to build
│ │ │ ├── prompt_plan.md # HOW to build
│ │ │ └── notes.md # Writable learnings
│ │ └── conversations/conversations.md
│ └── user-defined/features/{module}/{feature}/
│ └── (same structure as above)
├── project-info/team-info.json
├── system-info/users/available-users-list.csv
└── for-coding-agents/
├── agents.md # Grounding context (read at session start)
└── mfbt-usage-guide/ # Workflow guides
cat /for-coding-agents/agents.md --branch_name='your-branch'— grounding contextls /→ls /phases/→ drill into phases, modules, features- Read
spec.md(what to build) andprompt_plan.md(how to build) setMetadataValueForKey .../features/{module}/{feature}/ in_progress true— mark as started- Implement the feature
write .../implementations/{impl}/notes.md '<learnings>'— document what you learnedsetMetadataValueForKey .../implementations/{impl}/ is_complete true— mark done (auto-syncs feature status)
- Include
coding_agent_name: "claude_code"andbranch_namein tool calls - Use
summary.mdfor quick spec overviews;by-section/for large specs - Focus on
must_havefeatures first, thenimportant, thenoptional notes.mdauto-feeds intoagents.mdgrounding — document architectural decisions and gotchas- Use
grepto search across specs and prompt plans - For team communication (posting comments), see
/for-coding-agents/mfbt-usage-guide/ - To update status:
in_progresson features when starting,is_completeon implementations when done
- Venv:
.venv/bin/python,.venv/bin/pytest - Run tests:
.venv/bin/pytest tests/unit/ -v - Editable install with uv:
uv sync && uv pip install -e . && uv pip install -r requirements-dev.txt— then.venv/bin/mfbt ...runs the in-development CLI with edits picked up live. Note:uv synconly installs runtime deps frompyproject.tomland will uninstall anything not declared there, so re-run therequirements-dev.txtstep after a sync to restore pytest/mypy/ruff/etc. - Config functions no longer take
project_root—load_config(),save_config(),load_auth(),save_auth(),init_mfbt_dir(),resolve_config()all operate on~/.mfbt/viaget_mfbt_dir().TokenManager.__init__takes onlybase_url. - API response formats: List endpoints (
/features,/implementations) return paginated{"items": [...], "total": N, ...}— always extract viabody["items"]. Some endpoints (/brainstorming-phases) return plain lists. Seesrc/mfbt/tui/data_provider.pyfor the canonical parsing pattern. - TUI navigation: Projects > Phases > Modules > Features (4 levels).
NavigationStatestack depth: 0=projects, 1=phases, 2=modules, 3=features. Phase list shows brainstorming phases + virtual "Orphan modules" entry. - Ralph in TUI: Integrated into main TUI (
rkey), not a standalone app. Ralph runs in place inside the unifiedFeatureListPanel(tui/screens/feature_list.py) in#main-content— there is no separate Ralph panel — withPreflightModalfor agent checks. Standalonetui_app.pyand the oldralph_panel.pyare deleted. - Ralph subcommand:
src/mfbt/commands/ralph/— orchestrator, display (console), tui_display (Textual adapter), ralph_widgets (TUI widgets), agent runner, progress (API), prompt builder, types. - Display duck typing:
RalphOrchestrator.displayis typed asAny— bothRalphDisplayandRalphTUIDisplayare structurally compatible (same display protocol). - Ralph sandboxing (two-mechanism, hard-fail — current design): Exactly two sandbox mechanisms, each a hard requirement on its platform:
sandbox-execon macOS,bwrap(bubblewrap) on Linux. There is no unsandboxed mode, nodocker/firejail, no config override, no--sandbox/--yesflag, no consent/feedback layer. If the platform's mechanism is unavailable (or the platform is neither macOS nor Linux), Ralph aborts before any agent subprocess with an actionable message.sandbox/package:models.py(SandboxMechanism={SANDBOX_EXEC,BWRAP} only;SandboxConfig,SandboxPermissions,resolve_paths),detect.py,exceptions.py,wrapper.py,adapters/(sandbox_exec.py,bwrap.py,__init__.py). NomodelsSandboxResult/SandboxType/CandidateOutcome/SandboxDetectionReport; nofeedback.py; nodocker.py/firejail.py.models.pyimports the sibling leafexceptions(acyclic, still no external deps / no import-time side effects) and does fail-loud, filesystem-free__post_init__validation:SandboxPermissionsrejects a writable filesystem-root or bare$HOME(incl.project_dir) →SandboxSetupError;SandboxConfigrejectsproject_dir != permissions.project_dir(the boundpermissionsis stored as-is, no silent replace).SandboxConfig.base_urlis inert (no adapter consumes it; never a confinement input).- Detection: one function
detect_sandbox(which_fn=shutil.which, system_fn=platform.system, *, run_fn=subprocess.run) -> SandboxMechanism. macOS→SANDBOX_EXEC(binary-presence check), Linux→BWRAP(functional liveness probebwrap --ro-bind / / true), else→raiseSandboxDetectionError. Nodetect_sandbox_with_report, noexplicit=, no config loader.detect.pydoes not importmfbt.config(cycle severed;config.validate_confighas no sandbox block; no_VALID_SANDBOX_TYPES/_CONFIG_OVERRIDE_KEY). - Adapters: each is pure
build_prefix(config: SandboxConfig) -> list[str]+classify_exit(rc, sandbox_stderr) -> SandboxError|None.adapters/__init__.pyget_adapter/get_classifierare strict 2-entry dispatch (unknown mechanism →SandboxError, no identity/no-op). SBPL assetcommands/ralph/profiles/claude.sb(MCLI-126 write-confine:(allow default)+ global write-deny + per-writable re-allow + a curated/devdevice-node allow-list —/dev/null|zero|random|urandom|tty|dtracehelper|ptmx,^/dev/ttys[0-9]+$,(subpath "/dev/fd")— not a blanket(subpath "/dev"); kept a single line so the comment-breakout test's balanced-sexpr invariant holds; network open) loaded viaimportlib.resources; hatchling auto-bundles non-.pypackage files.classify_exitre-attributes a non-zero exit to the sandbox only on the sandbox's own line-anchored stderr signature ([Sandbox]wrapper prefix stripped first; sandbox_exec:deny(\d+)regex anywhere on a line OR a line startingsandbox-exec:; bwrap: a line startingbwrap:); elseNone= Claude's own exit, returned asAgentResult. SandboxWrapper:wrap_command(cmd)canonical (wrap(cmd)alias);prepare()is a no-arg no-op lifecycle hook;cleanup()removes the sandbox-exec SBPL temp profile inrun()'sfinally.mechanism=None→detect_sandbox()opt-in (orchestrator passes a concrete mechanism; bad value→SandboxError). Sandbox stderr: pureformat_sandbox_stderr([Sandbox]prefix) +note_stderr_line(always attributes) /get_sandbox_stderr; sandbox tool +claudeshare ONE stderr fd by design (attribution by[Sandbox]marker, not OS-fd — do not split processes).AgentResult.sandbox_stderrdefaulted.AgentRunner:__init__(*, project_dir=None, base_url=None)(noassume_yes/presented).sandbox_wrapper is None→auto-detect once; injected wrapper used as-is. The no-wrapper defaultbase_urlis sourced frommfbt.config.DEFAULT_CONFIG["base_url"](single source — no hand-synced_DEFAULT_BASE_URLconstant).build_commandalways inserts--dangerously-skip-permissionsat argv[1] (a real OS sandbox is always the boundary; headlessclaude -p/stdin=DEVNULLcan't answer tool prompts). The subprocessPopenis launched withcwd=pinned to the sandbox's own realpath'dproject_dir(single source = the wrapper'sSandboxConfig; resolved exactly as the adapters resolve it) so the run location is always inside the confined/bound area, never the inherited parent CWD —cwdis part of the frozen Popen signature. After the timedstderrjoin,run()logs (WARNING) if the drain was truncated, and on a non-zero exit not attributed to a denial logs INFO (WARNING if truncated) so a possibly-missed denial is never fully silent.run()= atry/except SandboxErrorsetup phase (build→prepare()→wrap_command) + the subprocesstry/…/finally cleanup()phase; orderedexceptclauses (baseSandboxErrorlast) each_emit_sandbox_error(exc); raise(re-raise, notSystemExit). PopenOSErrorEPERM/EACCES→SandboxDeniedError. No_handle_none_sandbox_fallback/acknowledge_unsandboxed/_emit_sandbox_note/_NONE_SANDBOX_WARNING.- Exceptions:
SandboxError(message, *, mechanism=None)(__str__="[<mechanism>] <message>"); subclassesSandboxDetectionError,SandboxSetupError,SandboxDeniedError,SandboxCompatibilityError(reserved, defensively handled, never raised by current paths). All 5 re-exported fromsandbox/__init__. - Hard-fail wiring (TWO chokepoints — construction AND run): (1) construction:
RalphOrchestrator.__init__callsdetect_sandbox(), logs+re-raisesSandboxDetectionError. (2) post-construction:orchestrator._implement_featurehas anexcept SandboxError: logger.error(...); raisebefore its broadexcept Exceptionso aSandboxSetupError/SandboxDeniedErrorfromagent.run()is never downgraded to a per-featureFAILED(which would loop on a broken sandbox and mask a denial) — it propagates out ofrun(). CLIcommands/ralph/__init__.py:except SandboxDetectionError→display.error+typer.Exit(1);display.sandbox_status(...)beforeorchestrator.run();except SandboxError→display.error("sandbox failure — Ralph aborted: …")+typer.Exit(1);summary.failed>0→typer.Exit(1). TUIfeature_list.py::_run_orchestrator:except SandboxDetectionError(nowlogger.error(exc_info=True)— parity with CLI) wrapping theRalphOrchestrator(...)construction → notify(error) +_return_to_browsing; a separateexcept SandboxErroraroundrun()→ notify("Sandbox failure — Ralph aborted: …", error) +_return_to_browsing(both fail closed visibly).RalphConfighas noassume_yes/sandbox; the pre-runtyper.confirmprompt is gone;--statusonly conflicts with--quiet.RalphHeader._sandbox_label()always shows the mechanism (no UNSANDBOXED branch).RalphDisplay/RalphTUIDisplaykeepsandbox_status(stdout/log, quiet-suppressed, enum.valueverbatim);sandbox_warningremoved. - Debug: module-scope
_debug_enabled = Falseinwrapper.pyANDdetect.py(literal# TODO: decision needed — wire to --<flag-name> and <ENV_VAR_NAME>, intentionally unwired; testsmonkeypatch.setattr(<mod>, "_debug_enabled", True)).[sandbox-debug]viawrapper.format_sandbox_debug/emit_debug_sandbox_stderr()/detect._probe_liveness. - Test gotchas:
__init__'sloggerparam shadows the module logger — uselogging.getLogger(__name__).test_orchestrator.pyautouse fixture patchesorchestrator.detect_sandbox(NOT_with_report) returning a bareSandboxMechanism.test_agent.pyautouse fixture patchesagent.detect_sandbox+agent.SandboxWrapper;_stub_wrapperdefaults toSANDBOX_EXEC(a real sandbox) and setsclassify_exit.return_value=None, sobuild_commandincludes--dangerously-skip-permissionsat index 1 — assert presence, never absence; mock wrappers needwrap_command.side_effect(not.wrap).test_detect.py::test_import_has_no_side_effectsmust keep the parent-packagedetectattr save/restore infinally(full-suite-order hermeticity). Real-wrapper integration tests intest_agent.pyexercise BWRAP/SANDBOX_EXEC throughrun()mocking onlyPopen/which.
- Key files:
auth_flow.py(shared OAuth),coding_agents.py(agent pre-flight checks),tui/screens/phase_list.py,tui/screens/preflight_modal.py,tui/screens/feature_list.py(unified browse + in-place Ralph panel),commands/ralph/ralph_widgets.py. - TUI shortcuts:
r= Ralph,ctrl+r= Refresh,d= Describe,enter= Open/Detail,esc= Back,?= Help,q= Quit.