Skip to content

Latest commit

 

History

39 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

monty, the head butler

Monty, the head butler

Monty is a small OCaml launcher for running a head agent session that plans work and spins out Pi or Codex worker sessions. Ghostty tabs, windows, and splits remain the default execution surface. An optional headless workflow can instead use Pi's existing subagent tool or non-interactive codex exec processes without opening Ghostty. The model does the planning. Monty handles the glue around Ghostty, wt, worktrees, manifests, agent harnesses, and startup commands.

Requirements

  • OCaml with Dune 3.20 or newer.
  • Dune package management, with dependencies declared in dune-project.
  • No opam files are used by this repo.
  • pi or codex on PATH, matching the selected harness.
  • The pi-subagents package enabled when using Pi headless chains.
  • wt on PATH.
  • Ghostty on macOS for normal launching.
  • gh on PATH when using GitHub issues as a project task source.

Build

dune build

The repo includes a dune.lock generated by Dune package management. If dependencies change, refresh it with:

dune pkg lock
dune build

Install

Install the monty CLI without opam:

./install.sh

The installer builds with Dune, copies the Monty control room to ~/.local/share/monty, installs the real binary at ~/.local/libexec/monty/monty-real, and writes a wrapper at ~/.local/bin/monty. Control-room, binary, and wrapper activation is rollback-protected, and matching .monty state is preserved across reinstallations. The repository and installer currently use Monty state version 2, stored in .monty/version. Existing unversioned state is adopted as version 2 without deleting it, and version 1 state is migrated in place because singleton records remain readable as one-workspace records. Other version mismatches still require interactive confirmation or the explicit --replace-state option before replacement. It also writes MONTY_HOME=~/.local/share/monty and MONTY_BRANCH_PREFIX=monty to your shell startup file, such as ~/.zshrc. If that file already has a non-Monty-managed MONTY_HOME setting, the installer asks before overriding it. The wrapper pins MONTY_HOME and provides MONTY_BRANCH_PREFIX as an install-time fallback on every invocation, so running monty from any directory uses the installed Monty control room. A persisted branch-prefix setting takes precedence over that fallback. Use another prefix with:

./install.sh --prefix /path/to/prefix

Use another Monty home with:

./install.sh --monty-home /path/to/monty-home

Set your worker branch prefix with:

./install.sh --branch-prefix cto

For development, keep the control room in the current checkout and only install the CLI wrapper and binary:

./install.sh --dev-install --branch-prefix cto

Preview the commands without changing anything:

./install.sh --dry-run

Start the head butler

From this repo, run:

dune exec -- monty

After installation, run:

monty

With the default Pi harness, this changes to the Monty control-room directory and runs:

pi --name "Monty Head Butler"

Select Codex for one invocation with monty --harness codex, or persist it as the Monty default with:

monty settings set harness codex

Inspect settings with monty settings or read just this value with monty settings get harness.

Launch one worker

dune exec -- monty launch \
  --repo /path/to/repo \
  --title "Fix issue 123" \
  --context .monty/runs/run-1/issue-123.md

By default Monty creates or reuses a repo-scoped worktree for the requested branch. Monty asks wt, validates that the returned worktree belongs to the requested repo, and automatically answers wt's repo-selection prompt when multiple repos have the same branch name. The generated worker launch script reruns monty ensure-worktree --repo <repo> --branch <branch> when the terminal starts, so the correct worktree can be recreated if it was deleted after launch. Durable worker memory lives under Monty home, not in the wt worktree. The worker receives both Monty instructions and the context file through the selected harness. Pi uses @file arguments; Codex receives a prompt containing the absolute input and durable-memory paths. Before starting Codex, Monty records the exact worker worktree as trusted in $CODEX_HOME/config.toml (or ~/.codex/config.toml). This answers Codex's directory-trust question for Monty-launched sessions without changing command approval or sandbox settings.

Launch many workers

dune exec -- monty launch-many --manifest .monty/runs/run-1/jobs.json

Example manifest:

{
  "jobs": [
    {
      "id": "issue-123",
      "title": "Fix issue 123",
      "repo": "/Users/cristea/code/example",
      "branch": "monty/issue-123",
      "context": ".monty/runs/run-1/issue-123.md",
      "worker_dir": ".monty/runs/run-1/workers/issue-123",
      "task_key": "local:local-001"
    }
  ]
}

One task can own branches in several repositories. Use workspaces instead of top-level repo and branch fields:

{
  "jobs": [
    {
      "id": "sonnet-5-invoices",
      "title": "Upgrade invoice parsing and admin reprocessing to Claude Sonnet 5",
      "workspaces": [
        {
          "repo": "/Users/cristea/code/django-backend",
          "branch": "cto/invoice-parser-sonnet-5"
        },
        {
          "repo": "/Users/cristea/code/admin",
          "branch": "cto/admin-invoice-sonnet-5"
        }
      ],
      "context": ".monty/runs/run-1/sonnet-5-invoices.md",
      "worker_dir": ".monty/runs/run-1/workers/sonnet-5-invoices",
      "task_key": "local:local-005"
    }
  ]
}

Workspace repositories must be absolute registered project paths. Their order is durable. The first workspace is only the initial launch directory; there is no special primary_project, and all workspaces share one worker, task status, resume history, and completion operation. Monty materializes and validates each repo-plus-branch independently. Pi receives every absolute path in the generated instructions; Codex additionally receives secondary paths with --add-dir.

The manifest can also omit branch. Monty then derives a safe branch name from the title using the branch prefix. The default is monty, so Fix issue 123 becomes monty/fix-issue-123 for one launch or monty/01-fix-issue-123 in launch-many. With monty settings set branch-prefix cto, --branch-prefix cto, or MONTY_BRANCH_PREFIX=cto, the same task becomes cto/fix-issue-123 or cto/01-fix-issue-123. A manifest entry can include task_key to link a worker to a Monty-owned local task. When monty done archives that worker, it also marks the linked local task done. Ordinary launch and reconciliation use only explicit task keys and stable workspace-set-plus-worker identities. Use monty tasks repair-worker <worker> for an explicit, ambiguity-checked repair of a legacy worker that has no stable link.

Monty validates the complete manifest before creating a task, reserving worker memory, invoking wt, writing a launch script, or requesting a terminal. Preflight rejects missing repositories or contexts, unknown projects or tasks, unsafe paths, missing dependencies, existing-state conflicts, and duplicate worker IDs, worker directories, task links, or repo-plus-branch identities anywhere in the workspace sets. Dry-run and real launch use the same preflight. A valid dry-run remains mutation-free.

Real launch reserves every worker under the Monty state lock before external work begins. A runtime failure reports every manifest job as launch-requested, launch-failed, or unattempted/prepared and prints exact recovery commands. Rerunning the printed batch command safely continues prepared and definitely failed jobs. It skips launch-requested jobs because another automatic request could duplicate a session. Use the printed monty resume <worker-id> command when you intentionally want another request for such a worker.

Headless worker chains

Headless execution is an explicit alternative to Ghostty-backed worker sessions. Ordinary launch, launch-many, and resume commands continue to use Ghostty by default. The headless commands resolve the effective harness using the normal precedence: --harness, MONTY_HARNESS, persisted settings, then the Pi default. Once monty settings set harness codex is configured, no --harness codex flag is needed for preparation, execution, or resume.

Start every new batch with the same complete preflight and worktree preparation:

monty headless prepare-many \
  --manifest .monty/runs/run-1/jobs.json \
  --dry-run

monty headless prepare-many \
  --manifest .monty/runs/run-1/jobs.json

Preparation reserves every worker and materializes all of its repo-scoped wt worktrees while leaving it prepared. The output records the effective harness and Codex YOLO setting so the execution mode is visible before a worker is claimed.

Codex headless execution

With Codex selected, run one prepared worker or the entire prepared manifest without opening a terminal:

monty headless run issue-123

monty headless run-many \
  --manifest .monty/runs/run-1/jobs.json

run-many completes a full read-only batch preflight before starting any worker, then runs the independent worker chains concurrently. Each chain uses four non-interactive codex exec processes: one implementer, two parallel reviewers, and one fixer. With Codex YOLO disabled, implementer and fixer processes use workspace-write, while reviewers use read-only. monty settings set codex-yolo true applies the configured unrestricted Codex mode to every phase and must be treated as a high-risk choice.

Codex progress, JSONL events, prompts, and final phase messages are kept under the worker's durable artifacts/headless/<attempt-id>/ directory. Before returning success, Codex also writes the canonical monty:run-handoff:v1 record and a pending finished-run notice. Failures after the worker is claimed write the same record with outcome failed, the last known phase, useful error text, workspace identity, and artifact paths. The CLI returns a compact JSON summary that references the canonical handoff; it does not close the task.

Pi headless execution

Monty does not install a Pi extension. Before a mutating Pi headless command, confirm that the current harness exposes the subagent tool and required agents. After preparation, claim each worker immediately before the tool call:

monty headless begin issue-123

headless begin emits the versioned monty:headless-dispatch:v3 envelope. Its harness_call.tool is subagent, and harness_call.arguments contains the complete asynchronous chain. No prompt needs to reconstruct the chain manually. The envelope also includes a versioned completion contract. After the asynchronous callback, run the exact success_command, or fill the concrete phase and message into the failure_command. monty headless finish normalizes Pi's callback into the same durable handoff and inbox used by Codex and interactive workers.

Each Pi or Codex chain starts in its first supplied Monty worktree and can inspect every declared workspace:

  1. One fresh implementer changes the worktree and runs focused validation.
  2. Two mutually isolated fresh reviewers inspect the same worktree concurrently and write only their separate reports outside it.
  3. One fresh fixer verifies both reports, fixes valid findings, reruns affected validation, and records the final handoff in durable worker memory.

Pi's generated arguments do not request Pi-managed worktrees and explicitly disable progress-file side effects and structured acceptance policy. All step outputs use paths under the worker's durable artifacts/headless/ directory. Child prompts prohibit staging, commits, pushes, pull requests, review comments, other remote writes, worktree management, and automatic monty done calls. Successful chains leave the task open, preserve the worktree, and require the normal explicit monty done <worker-id> lifecycle command later.

Headless dispatch uses a conservative two-phase protocol. Preparation reserves all identities and records their worktrees while every job remains prepared. A preparation or other pre-dispatch failure remains retryable as prepared. Immediately before the Pi harness tool call or the first Codex process, Monty atomically changes only that worker to launch-requested. A failure after that point remains ambiguous and is never replayed automatically. When a fresh successor chain is intentional, run:

monty headless resume issue-123

With Pi selected, the successor envelope contains the complete harness call. With Codex selected, resume directly runs a fresh successor Codex chain. Monty does not persist a backend, Pi run ID, Codex session ID, async status, or runtime state in job.json.

Run handoffs and pending results

A finished run and a done task are deliberately different events. An interactive or headless run can finish successfully while its linked local task remains open, every worktree remains intact, and job.json keeps its existing open launch status. Only the explicit monty done lifecycle closes and archives the task.

Every run handoff uses the documented monty:run-handoff:v1 JSON contract. The complete v1 field and delivery contract is versioned with the repository. Canonical records and deterministic Markdown renderings live outside the movable worker directory:

.monty/handoffs/<worker-run-id>/<worker-id>/<run-id>.json
.monty/handoffs/<worker-run-id>/<worker-id>/<run-id>.md

The record contains the worker and task identity, run outcome, dense summary, per-workspace Git diff statistics, validation and review reports, risks, every repo/branch/worktree, durable evidence, and supported next actions. Artifact evidence also retains a safe path relative to worker memory, so read-only follow-up can remap it after archive or reopen moves the worker directory.

Interactive workers publish without marking the task done:

monty handoff publish issue-123 \
  --summary "Added the shared handoff contract and pending inbox." \
  --check "dune runtest --force: passed" \
  --fixed "Rejected forged inbox paths before reading evidence" \
  --risk "Draft PR still requires explicit approval"

The compact TL;DR is printed directly in that worker's Ghostty session. Because the user owns an interactive terminal run, it is not also queued for the head butler. Its canonical record remains available for later inspection.

Headless completion uses Monty's home-level .monty/inbox/run-handoffs/, whose small monty:run-handoff-notice:v1 records reference canonical handoffs. Reading is at-least-once: pending never acknowledges implicitly, and acknowledgement is idempotent. If publication stopped after the canonical record was written, pending repairs its rendering and delivery receipt without turning an interactive result into a head-butler notification. If a Pi chain wrote its final artifact after the original head-butler callback became unavailable, pending publishes a needs-attention receipt without inferring success from final.md; it never launches or resumes a run and never changes task status.

monty handoff pending
monty handoff pending --format plain
monty handoff acknowledge <notice-id>
monty handoff show issue-123 --format json

The head butler displays pending cards only at a safe conversation boundary, then acknowledges the notices it actually displayed. If a native runner is no longer addressable, a fresh agent or the head butler can prepare a durable, read-only drill-down without resuming implementation:

monty handoff follow-up issue-123 \
  --question "Which review findings were rejected and why?"

This emits monty:run-handoff-follow-up:v1 with the current workspace map and verified paths to the canonical handoff, task context, worker memory, and attempt artifacts. It never modifies code; continuing work still requires an explicit monty resume or monty headless resume.

Worker memory and resume

Each launched worker gets a durable memory directory. For launch-many, the default is:

.monty/runs/<run-id>/workers/<worker-id>/

Monty writes:

job.json
MONTY.md
memory.md
artifacts/

Workers are instructed to write important discoveries, blockers, and handoff notes back to this folder. The wt worktrees are treated as ephemeral and can be recreated from the durable workspace array in job.json.

Resume a worker by id, branch leaf, branch, or title slug:

dune exec -- monty resume issue-123

resume reads job.json, recreates or reuses every repo-scoped worktree, and opens a new session in the selected harness with the same durable worker memory. Resume always derives worktree mode from the durable record rather than a current CLI default, so a never worker cannot accidentally create an unmanaged worktree. Open jobs are found from worker job.json files. The original jobs.json manifest is launch input and a safe batch-retry contract. Durable job.json remains the lifecycle source of truth. done archives the worker and closes the linked Monty-owned local task in the same command.

Open launch states have conservative meanings.

  • prepared means the durable identity is reserved and no terminal request has been made.
  • launch-requested means a terminal request was persisted before the external call, so automatic replay must not risk a duplicate.
  • launch-failed means launch preparation definitely failed before a terminal request and is safe to retry.

A nonzero terminal command after launch-requested remains ambiguous because Ghostty may have created a surface before a later AppleScript step failed. Monty preserves launch-requested in that case and requires explicit resume.

  • Legacy active records remain open and resumable, but the value is not proof of process liveness.

The active path is always .monty/runs/<run-id>/workers/<worker-id>/job.json. The archive path is always .monty/runs/<run-id>/archive/<worker-id>/job.json. Physical location determines active versus archived identity. Persisted path fields cannot override the canonical physical location.

Resume an archived worker and move it back to active memory with:

dune exec -- monty resume --archived issue-123

List and archive jobs

List active jobs from durable job.json files:

dune exec -- monty list

List archived or all jobs with:

dune exec -- monty list --archived
dune exec -- monty list --all

Filter a run by directory name or path with:

dune exec -- monty list --run run-1

Mark a job done with:

dune exec -- monty done issue-123

Inside a worker session, monty done uses MONTY_WORKER_DIR and does not need an argument. done refuses to archive dirty worktrees by default. Use --force to discard local worktree changes while archiving:

dune exec -- monty done issue-123 --force

Completing a job first verifies every workspace, then deletes all of its worktrees and branches, writes status: done to job.json, and moves durable memory to:

.monty/runs/<run-id>/archive/<worker-id>/

Completion and archived resume are recoverable phase workflows. Monty persists completing or reopening intent before moving state, updating the linked local task, and finalizing status. A retry can continue from either canonical physical location. The original --force decision is persisted for the whole completion retry. monty doctor reports an exact recovery command for every incomplete lifecycle transition.

Project overview and local tasks

Monty can keep a small overview of projects and task sources. Project IDs are stable persisted identities derived from repo names, with deterministic disambiguation when needed. External task sources, such as GitHub issues, refresh local external metadata such as title and URL. The local task registry always owns user-facing open or done status, even when a remote issue changes state. Monty-owned local tasks are the task inventory for both external and local work.

Add a project with optional GitHub issues as its task source:

dune exec -- monty projects add \
  --repo /path/to/repo \
  --github owner/repo \
  --query "is:open"

List or show project memory:

dune exec -- monty projects list
dune exec -- monty projects show monty

Show a cross-project overview:

dune exec -- monty overview

List tasks:

dune exec -- monty list
dune exec -- monty tasks list
dune exec -- monty tasks list --project monty

list and tasks list are equivalent task-listing views and show ID, PROJECT, STATUS, TITLE, and BRANCH. They automatically reconcile worker jobs into the local task source of truth before rendering. Worker jobs are linked back to local tasks with exact task_key values and stable workspace-set-plus-worker identities. Reconciliation is sorted and replay-safe. It writes tasks before patching job links, so interruption can be retried without creating duplicate tasks. Corrupt or unknown neighboring workers produce diagnostics while healthy records continue. Use --no-sync with either inventory command for a read-only view with no reconciliation writes or external metadata fetches. You can run dune exec -- monty tasks sync explicitly when repairing or inspecting sync behavior.

Add or complete a local task:

dune exec -- monty task add --project monty --title "Design overview"
dune exec -- monty task done local-001

Attach planned repository work to an open task and inspect its absolute paths:

monty task workspace add local-005 \
  --repo /absolute/path/to/django-backend \
  --branch cto/invoice-parser-sonnet-5

monty task workspace add local-007 \
  --repo /absolute/path/to/admin \
  --branch cto/admin-invoice-sonnet-5

monty task show local-005

After a worker is linked, materialize or rehydrate all of its workspaces—or one selected absolute repo—and persist the returned worktree paths with:

monty task workspace ensure local-005
monty task workspace ensure local-005 --repo /absolute/path/to/admin

Normal launch and resume do this automatically. Planning-only tasks deliberately cannot ensure worktrees until their one durable worker record exists.

If local-005 and local-007 were created for the same feature and neither has launched, combine them explicitly:

monty task merge local-007 --into local-005
monty task show local-005

The merge closes local-007 with durable provenance and gives local-005 the ordered workspace set. It does not create branches or worktrees; launch or headless preparation materializes them later.

Monty stores project overview state under:

.monty/projects.json
.monty/projects/<project-id>.md
.monty/tasks.local.json

Dry run

Use dry-run mode to inspect what Monty would do without creating tasks, worker reservations, launch scripts, worktrees, or Ghostty requests. Dry-run performs the same complete path, identity, project, task, state-conflict, and dependency validation as real launch.

dune exec -- monty launch-many \
  --terminal dry-run \
  --manifest .monty/runs/run-1/jobs.json

Configuration

The most important options are available as CLI flags.

--terminal ghostty|dry-run
--target tab|window|split
--worktree always|never
--branch-prefix PREFIX
--harness pi|codex
--pi-command COMMAND
--codex-command COMMAND
--wt-command COMMAND
--fork SESSION
--home DIR

Environment defaults are also supported.

MONTY_TERMINAL=ghostty
MONTY_TARGET=tab
MONTY_WORKTREE=always
MONTY_BRANCH_PREFIX=cto
MONTY_HARNESS=codex
MONTY_PI_COMMAND=pi
MONTY_CODEX_COMMAND=codex
MONTY_CODEX_YOLO=false
MONTY_WT_COMMAND=wt
MONTY_HOME=/path/to/monty

Doctor

dune exec -- monty doctor

Doctor prints a stable table with PASS, WARN, and FAIL levels plus exact recovery commands. The configured command for the selected Pi or Codex harness is required. The configured wt command is required only when worktree mode is always. Ghostty and osascript are required only for the Ghostty backend. Optional integrations such as gh and sdef produce warnings when unavailable. Doctor also inspects durable worker state for corruption, incomplete lifecycle transitions, and pending launch states. It exits zero when checks contain only PASS and WARN, and exits nonzero when any FAIL is present.

Settings

Monty stores user-controlled defaults in .monty/settings.json under MONTY_HOME. Show the effective persisted settings with:

monty settings

Read or change the default harness with:

monty settings get harness
monty settings set harness codex
monty settings set harness pi

Set the prefix for automatically generated worker branches with:

monty settings get branch-prefix
monty settings set branch-prefix cto

Enable Codex YOLO mode for Monty-launched Codex sessions with:

monty settings set codex-yolo true

Disable it again with:

monty settings set codex-yolo false

Monty also trusts the exact head-butler directory or worker worktree before starting Codex. This only suppresses Codex's initial directory-trust question; it does not enable YOLO mode.

When enabled, Monty adds --dangerously-bypass-approvals-and-sandbox to Codex head-butler and worker commands. This disables both approval prompts and the Codex sandbox, so only enable it when the surrounding environment provides the isolation you need.

Monty also pins model_reasoning_effort="xhigh" with a Codex -c override for every Codex head-butler, launch, and resume command. This takes precedence over a lower effort configured in the user's global Codex configuration. Monty likewise sets tui.vim_mode_default=true, so each Codex composer starts in Vim normal mode. Use /vim inside Codex to toggle it for the current session.

Harness selection uses this precedence:

  1. --harness pi|codex
  2. MONTY_HARNESS
  3. The persisted harness setting
  4. The Pi compatibility default

For Codex YOLO mode, --codex-yolo overrides MONTY_CODEX_YOLO, which overrides the persisted codex-yolo setting. It defaults to false.

Branch-prefix selection uses this precedence:

  1. --branch-prefix PREFIX
  2. The persisted branch-prefix setting
  3. MONTY_BRANCH_PREFIX
  4. The monty default

The persisted setting precedes the environment fallback because installed wrappers export their install-time branch prefix on every invocation.

Settings mutations use Monty's state lock and atomic JSON replacement.

Durable state safety

Monty stores durable state as plain JSON and Markdown under the configured home. Every mutation uses one advisory lock at .monty/state.lock. The lock protects only short local read-plan-write phases and is never held around gh, wt, Ghostty, pi, osascript, git, or other slow external commands.

JSON updates use a same-directory temporary file, file fsync, atomic rename, and parent-directory fsync. Canonical path validation rejects traversal, unsafe components, symlink escapes, and persisted path metadata that disagrees with physical state. Unsafe legacy records require explicit repair and are never silently migrated. Launch-state changes use locked compare-and-update and refuse to overwrite a concurrent completion or reopening transition. Persisted launch-script paths are accepted only when the complete script bytes prove ownership or an absent destination remains under an explicitly trusted script root. Scripts are published through same-directory atomic replacement so a destination symlink swap cannot redirect the write.

Tests

The checkout-binary E2E suite uses a unique temporary MONTY_HOME for every scenario. It installs fake wt, gh, pi, Ghostty, osascript, and related tools, and uses real temporary Git repositories when repository identity matters. The suite covers concurrent mutation, atomic faults, canonical paths, lifecycle recovery, deterministic reconciliation, whole-batch preflight, partial launch recovery, the headless two-phase protocol, generated harness tool arguments, shared run handoffs, pending delivery and acknowledgement, read-only follow-up construction, clean-context chain construction, parser behavior, and doctor exit contracts.

Run the complete validation with:

dune build @all
dune runtest --force
git diff --check

About

The head butler

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages