Profile
Back to NewsBack
GitHub Trending 36 min
Reader Mode
Dicklesworthstone/ntm: Named Tmux Manager: a Go control plane to spawn, tile, and coordinate multiple AI coding agents (Claude, Codex, Antigravity, omp, Grok) across tmux panes - graph-aware work triage, safety policy and approvals, Agent Mail coordi

Dicklesworthstone/ntm: Named Tmux Manager: a Go control plane to spawn, tile, and coordinate multiple AI coding agents (Claude, Codex, Antigravity, omp, Grok) across tmux panes - graph-aware work triage, safety policy and approvals, Agent Mail coordi

4 hours ago

NTM - Named Tmux Manager

NTM dashboard

NTM turns tmux into a local control plane for multi-agent software development. It combines session orchestration, graph-aware work triage, safety policy and approvals, Agent Mail coordination, durable state capture, machine-readable robot surfaces, and a local REST/WebSocket API in one Go binary.

curl -fsSL "https://raw.githubusercontent.com/Dicklesworthstone/ntm/main/install.sh?$(date +%s)" | bash -s -- --easy-mode

TL;DR

The Problem

Running several coding agents in parallel is easy to start and annoying to sustain. Plain tmux gives you panes, but it does not give you durable coordination, work selection, safety policy, approvals, history, replayable automation surfaces, or a shared control model that both humans and agents can use.

The Solution

NTM gives you a single local system for:

  • spawning labeled multi-agent sessions in tmux
  • sending work, interrupts, and follow-ups across panes
  • triaging what to do next with br and bv
  • coordinating agents with Agent Mail, file reservations, and assignments
  • protecting dangerous operations with policy, approvals, and guards
  • exposing the whole system through --robot-*, REST, SSE, WebSocket, and OpenAPI
  • capturing state with checkpoints, timelines, audit trails, and pipeline state

Why NTM

| Area | What NTM provides | Typical commands | | --- | --- | --- | | Session orchestration | Spawn, label, inspect, zoom, dashboard, palette | ntm spawn, ntm dashboard, ntm palette | | Work intelligence | Graph-aware triage, next-step selection, impact analysis, assignment | ntm work triage, ntm work next, ntm assign | | Coordination | Human overseer mail, inbox views, file reservations, worktrees | ntm mail, ntm locks, ntm worktrees | | Safety | Destructive-command protection, policy editing, approval workflows | ntm safety, ntm policy, ntm approve, ntm guards | | Durable operations | Checkpoints, timelines, audit logs, saved sessions, pipelines | ntm checkpoint, ntm timeline, ntm audit, ntm pipeline | | Automation surfaces | Robot JSON, REST API, SSE/WebSocket streams, OpenAPI | ntm --robot-snapshot, ntm serve, ntm openapi generate |

Quick Start

Requirements

NTM is a pure Go project, but the runtime experience is intentionally integration-heavy.

  • Required: tmux
  • Required for agent spawning: whichever CLIs you want to run, typically Claude Code, Codex, Antigravity CLI, Oh My Pi (omp), or Grok Build (Gemini CLI is supported as legacy)
  • Optional but powerful: br, bv, Agent Mail, cass, dcg, pt
  • Sanity check everything with ntm deps -v

First Session

# Install
curl -fsSL "https://raw.githubusercontent.com/Dicklesworthstone/ntm/main/install.sh?$(date +%s)" | bash -s -- --easy-mode

Enable shell integration

eval "$(ntm shell zsh)"

Verify tools and integrations

ntm deps -v

Scaffold a project directory

ntm quick api --template=go

Launch a mixed swarm

ntm spawn api --cc=2 --cod=1 --agy=1

Open the live operator surfaces

ntm dashboard api ntm palette api

Dispatch work

ntm send api --cc "Map the auth layer and propose a refactor plan."

If the repo uses br/bv, inspect the work graph

ntm work triage --format=markdown

Save a recoverable checkpoint

ntm checkpoint save api -m "before auth refactor"

Expose local APIs for dashboards, scripts, and agents

ntm serve --port 7337 ntm --robot-snapshot

Core Workflows

1. Multi-Agent Session Orchestration

NTM is built around named tmux sessions with explicit agent panes and a user pane. It handles session naming, pane layout, agent startup, labels, and inspection so you can treat a swarm like a manageable unit instead of a pile of terminals.

ntm quick payments --template=go
ntm spawn payments --cc=3 --cod=2 --agy=1
ntm add payments --cc=1
ntm list
ntm status payments
ntm view payments
ntm zoom payments 3
ntm attach payments

A bare --cc=N passes no --model, so Claude Code starts with its own default or saved model and context setting. Pin one with --cc=N:MODEL (the built-in aliases opus, sonnet, and haiku pass Claude Code's own family aliases, which track the newest model) or set [models] default_claude in ~/.config/ntm/config.toml; default_claude = "" restores the delegate behaviour.

Grok Build

NTM recognizes the official xAI Grok Build CLI as the canonical grok agent type. Install it using xAI's current instructions, then authenticate without putting credentials in NTM configuration:

curl -fsSL https://x.ai/cli/install.sh | bash
grok login                   # browser authentication
grok login --device-auth     # SSH/headless alternative
ntm deps -v

A bare spec delegates model selection to Grok Build. Use grok models to inspect the models available to the authenticated account, then pass an exact model ID only when an override is needed:

ntm spawn research --grok=1
ntm spawn research --grok=1:MODEL_ID
ntm spawn research --grok=1:MODEL_ID:EFFORT
ntm --robot-spawn=research --spawn-grok=1

NTM launches Grok Build with its official --always-approve automation flag. Phase 2 (GH#251) implements the interactive TUI protocol against live Grok Build captures: readiness and working-state detection, composer-gated and submission-verified prompt delivery (send, assign, --robot-spawn --spawn-wait and --spawn-assign-work), interrupt, restart, and restore-time relaunch. The one deliberate refusal is personas: the Grok Build CLI has no system-prompt flag or environment variable, so a persona on a Grok agent fails the launch instead of being silently dropped.

Oh My Pi (omp)

NTM operates Oh My Pi as the built-in omp agent type (panes are named session__omp_N; oh-my-pi is accepted as an alias). Finish omp's own setup once before the first spawn, otherwise the first prompt lands in the setup wizard:

omp setup
ntm deps -v                               # probes the omp binary
ntm spawn swarm --omp=8                   # omp picks its default model
ntm spawn swarm --omp=2:MODEL             # pins MODEL (omp fuzzy-matches it)
ntm spawn swarm --omp=2:MODEL:high        # also sets the thinking level (or MODEL@high)
ntm add swarm --omp=1
ntm adopt swarm --omp=1,2,3               # adopt existing omp panes
ntm --robot-spawn=swarm --spawn-omp=8 --spawn-wait
ntm send swarm --omp "Run the test suite and report failures"

The launch command is omp --auto-approve, plus --model, --thinking, and --append-system-prompt (personas) when requested. Override it with [agents] omp = "..." (a template; keep {{.ReasoningEffort}} if you want effort specs honoured, otherwise NTM rejects them rather than dropping them), set [models] default_omp or [models.omp] aliases, and [prompts] omp_default. [spawn_pacing.agent_caps] omp_max_concurrent (default 8) is omp's share of the host-wide agent budget robot spawn admission enforces (running plus requested agents may not exceed the sum of all agent caps); launches are never serialized per type, and the default admits a full --spawn-omp=8 swarm alongside the default Claude/Codex/Gemini budget.

Everything NTM does to an omp pane is derived from omp's live TUI (v18.2.3, across its nerd, unicode and ascii symbol presets):

  • Ready / working / idle. omp keeps a bordered composer at the bottom of the
pane. A turn in flight shows a spinner and elapsed timer in the composer's top border (╭── ⠧ 1s), an Esc-hint activity line above it (⎋ Working… or an intent label), a queued Steering · N block, or a Subagents panel listing running subagents. A TODO tree alone is not read as working, since a plan can outlive its turn. A quiet composer with nothing below it is idle.
  • Delivery. Enter submits. A message sent to a busy pane is queued by omp
as steering; delivery is confirmed when the payload leaves the composer. A staged large paste (#1 token) is reported by --robot-dialogs as paste limbo and cleared with Ctrl+C.
  • Interrupt and exit. omp interrupts on Escape (Ctrl+C only clears the
draft, and a double Ctrl+C quits), so ntm interrupt, --robot-interrupt, restart, and the REST interrupt send Escape to omp panes. Graceful exit is Escape, Ctrl+C, then Ctrl+D.
  • Context. --robot-context, --robot-snapshot, ntm status, and the
coordinator's context-rotation trigger read omp's own context gauge from the composer border (7% … 262K), reported as source: "status_bar", so the reading stays per-pane even when many omp panes share one directory. Compaction uses native /compact and verifies a fresh reduction in the context gauge.
  • Sessions. ntm sessions save binds each pane to its omp session (omp
holds its transcript under ~/.omp/agent/sessions/ open, so the binding is exact per pane; profile, XDG and PI_CODING_AGENT_* stores are honoured), and resume relaunches with omp --auto-approve --resume .
  • Errors. A failed turn leaves a provider-error block framed by rules
directly above the quiet composer (server_error: ERROR, 401 Invalid API Key, then Dismissed when you send your next message.). NTM reports it as an error, never as idle-after-completion: --robot-is-working returns ERROR_STATE with indicator_basis: "provider_error" and a retry hint, the status observation and coordinator report an error (auth for 401/403, otherwise generic), and assignment will not feed the pane new work. An F5 to Retry hint may sit between the block and the input box. Press F5 (omp's retry key) to re-run the failed turn, or send any message, such as a continue prompt, which dismisses the block and retries. Error: No model selected. is an error too. No omp rate-limit screen has been captured yet, so omp panes are never marked rate-limited from transcript text; a provider 429 surfaces as a provider error.
  • Quota. omp has no quota or usage command. ntm quota lists omp panes as
"no quota API (provider-error classification)", ntm rotate all-limited skips them with that reason, and --robot-quota-check --provider=omp answers NOT_IMPLEMENTED (exit 2) naming the same fallback.
  • Palette, assignment, boot checks. In ntm palette, key 7 targets omp
panes (6 still picks individual agents). --assign scores omp with the per-task midpoint of the Claude and Codex capability profiles, since omp's strengths follow whichever model it runs. --verify-boot counts a pane as booted when it is idle, or when it received a spawn prompt (recovery context, CASS context, or your prompt) and is working on it.

OpenCode (oc)

OpenCode panes are named session__oc_N. The model is OpenCode's provider/model form; with no model, OpenCode uses its own default.

ntm spawn swarm --oc=2                          # OpenCode picks its default model
ntm spawn swarm --oc=2:opencode/big-pickle      # pins the model
ntm add swarm --oc=1
ntm --robot-spawn=swarm --spawn-oc=2:opencode/big-pickle --spawn-wait
ntm send swarm --oc "Run the test suite and report failures"

The launch command is opencode --model . There is no effort knob: the TUI has no --variant flag (only opencode run does), so an effort in the spec is ignored. Override the command with [agents] oc = "..." (a template; keep {{if .Model}} --model {{shellQuote .Model}}{{end}} if you want model specs honoured), and set [models] default_opencode or [models.opencode] aliases. Prompts are delivered with bracketed paste and verified against the composer: a prompt the TUI drops fails the send instead of reporting success.

Unattended panes and permissions. OpenCode asks before actions its permission config marks ask (by default this includes paths outside the project, external_directory). With nobody at the pane, the request is auto-rejected and the turn stops. Two ways to pre-approve:

  • Launch with --auto (OpenCode 1.18.0+), which approves every request that
is not explicitly denied, for that pane only:
[agents]
  oc = "opencode --auto{{if .Model}} --model {{shellQuote .Model}}{{end}}"

deny rules still apply, so you can pair --auto with deny rules for anything a pane must never do.

  • Set OpenCode's own "permission" config. It is global for every OpenCode
session that reads it, not per pane: ~/.config/opencode/opencode.json, a project opencode.json, or the OPENCODE_CONFIG / OPENCODE_CONFIG_CONTENT environment variables. Each key takes allow, ask or deny, and "permission": "allow" sets them all at once:

{
    "$schema": "https://opencode.ai/config.json",
    "permission": {
      "edit": "allow",
      "bash": "allow",
      "external_directory": { "~/shared/fixtures/**": "allow" }
    }
  }

Path patterns support , ? and a leading ~. Use * to cover a directory tree: OpenCode may check the parent directory's glob rather than the exact path it touches, so a pattern for one leaf directory can still be rejected.

Use labels when you want multiple coordinated swarms on the same project while keeping a shared project directory:

ntm quick payments --template=go
ntm spawn payments --label backend --cc=2 --cod=1
ntm spawn payments --label frontend --cc=2
ntm add payments --label frontend --cc=1

Staggered prompts (thundering-herd prevention)

When several agents boot together and all pick work from bv/br at once, they race for the same bead. Stagger their initial prompts so each agent has claimed something before the next one starts looking:

ntm spawn payments --cc=3 --prompt="Read AGENTS.md, then pick ready work" --stagger-mode=fixed --stagger-delay=90s
ntm spawn payments --cc=5 --prompt="..." --stagger-mode=smart   # learned rate-limit spacing
ntm --robot-spawn=payments --spawn-cc=3 --spawn-prompt="Read AGENTS.md, then pick ready work" \
    --spawn-stagger-mode=fixed --spawn-stagger-delay=90s

The agent at position i receives its prompt i × interval after the first. --robot-spawn --spawn-prompt (or --spawn-prompt-file, - for stdin) waits for readiness, delivers through robot send's dispatch path, and reports stagger.schedule[] and per-agent prompt_deliveries[]; the same --spawn-stagger-* flags pace --spawn-assign-work prompts. Set defaults for both commands in ~/.config/ntm/config.toml (flags still win):

[spawn]
stagger_mode = "fixed"   # none (default), fixed, or smart
stagger_delay = "90s"    # fixed-mode interval, 0-5m (default 30s)

Worktree isolation and reservations

Use --worktrees when agents need independent Git checkouts as well as separate tmux panes. NTM creates one ntm// branch and worktree per agent, so filesystem changes and destructive Git operations stay isolated until you deliberately merge them:

ntm spawn payments --cc=3 --worktrees
ntm worktrees list
ntm worktrees merge cc_1
ntm worktrees clean --session payments

Worktrees do not replace Agent Mail reservations. Continue to reserve the files or areas an agent owns: reservations communicate intent, reduce merge conflicts, and give operators an auditable ownership record; worktrees provide the separate checkout boundary. Merge reviewed agent branches into main, then clean the session's worktrees when that session is finished.

Reservations are enforced at commit time: the pre-commit hook ntm init installs runs ntm guards check --staged, which refuses a commit touching files another agent holds an exclusive reservation on and names the holder (when Agent Mail is unreachable it warns and records a degraded run for ntm doctor; set NTM_GUARD_STRICT=1 to fail closed). ntm guards install adds the check to a repository without that hook, or updates an older ntm init hook in place.

2. Dispatch, Monitoring, and Recovery

Humans can broadcast prompts, interrupt panes, stream output, inspect health, compare responses, search pane history, and keep an eye on activity without dropping to raw tmux commands.

ntm send payments --all "Checkpoint and summarize current progress."
ntm interrupt payments
ntm activity payments --watch
ntm health payments
ntm watch payments --cc
ntm extract payments --lang=go
ntm diff payments cc_1 cod_1
ntm grep "timeout" payments -C 3
ntm analytics --days 7

Pending context rotations can be acted on directly:

ntm rotate context pending
ntm rotate context confirm payments__cc_1 --action=rotate
ntm rotate context confirm payments__cc_1 --action=compact

CLI and dashboard confirmation execute the selected action and report its actual result. Rotation verifies the original process, requests a fresh handoff, and delivers it to a ready replacement before retiring the old pane. Confirmation ownership and results survive process interruption: repeating a completed choice returns its saved result, while a failed or interrupted choice remains visible in pending. The pending output identifies the execution, both physical panes, the handoff phase, and the available recovery actions. Inspect the affected panes before using the indicated retry or recovery action. Rotation transfers the existing task, Agent Mail identity, file reservations, and restart configuration to the replacement. Both panes are fenced against new task dispatch and automatic restarts while ownership is being transferred; existing reservation leases continue to renew. If an accepted handoff is interrupted during ownership transfer, --retry finishes that transfer without launching another agent or resending the handoff. When handoff delivery is uncertain, the pending record and pane fences remain in place for inspection, and retry or ignore cannot silently discard that unresolved state.

Resolve an interrupted rotation with an explicit operator decision:

ntm rotate context pending --json
ntm rotate context recover payments__cc_1 --execution-id=EXECUTION_ID \
  --action=accept --reason="Replacement is working on the handed-off task"

Alternatively, stop the replacement and keep the original agent:

ntm rotate context recover payments__cc_1 --execution-id=EXECUTION_ID \ --action=abort --reason="Continue the task in the original pane"

accept records the operator's observation of an uncertain handoff and finishes the existing ownership transfer without spawning or sending again. abort verifies the original ownership, stops only the recorded replacement, and verifies its absence before releasing the original task's rotation barrier. Work already performed by the replacement is preserved. An accepted ownership transfer cannot be aborted. Both actions require the exact execution ID and a nonempty reason; the decision is recorded durably, conflicting decisions are rejected, and the same command can resume recovery after another interruption. Changed process identities, foreign ownership, and unaccounted-for panes keep the recovery fenced for further inspection.

Native compaction preserves the conversation and requires fresh provider context accounting to prove a reduction; it does not automatically clear history. Claude compaction reads the transcript the pane's own Claude process names in its session record, so it works when many Claude panes share one project directory. Without that binding it falls back to the directory's newest transcript and refuses ambiguous scope, including another Claude pane in a different session using the same transcript directory.

3. Work Graph Triage and Assignment

NTM integrates with br and bv so the operator loop is not just "send prompts and hope." It can surface the best next task, highlight blockers, analyze impact, forecast work, and push assignments to specific panes or agent types.

ntm work triage
ntm work triage --by-track
ntm work alerts
ntm work search "JWT auth"
ntm work impact internal/api/auth.go
ntm work next
ntm work graph
ntm assign payments --auto --strategy=dependency
ntm assign payments --beads=br-123,br-124 --agent=codex

Automated assignment treats tracker labels as an authorization boundary. Add organization-specific approval labels globally or in .ntm/config.toml; project labels extend the global list and cannot remove built-in gates:

[assign]
operator_gated_labels = ["security-review", "legal-approval"]

Matching is case-insensitive. NTM requires a structurally valid full actionable plan, uses scored triage only to rank IDs authorized by that plan, and restores every candidate's labels from both br ready and br list --status open so epics are covered. Any plan command, parse, structure, ID, label lookup, or coverage failure stops automated assignment before dispatch.

Executing assignment paths can recover a corrupt local Beads SQLite index before reading the evidence needed to claim work. This includes automatic and watch assignment, coordinator assignment, and robot assignment preparation. NTM retries the failed read once after rebuilding from authoritative JSONL. If br refuses that rebuild because corruption prevents tombstone enumeration, NTM independently checks that the tombstone indexes agree and JSONL preserves every known tombstone before running br doctor --repair --json. It requires a verified database, validates the structured preservation evidence, and checks the known tombstones again after repair. An accepted partial repair retains its warnings, verified backups, and recovery marker. Unreadable indexes, missing tombstones, invalid JSONL, or failed verification stop assignment. Preview and dry-run commands do not request this repair. Recovery never replays a claim, reservation, or dispatch, and the normal eligibility and ownership checks still run against freshly read evidence.

When br and bv report that no ready work exists, use the queue-dry flow to distinguish a genuinely empty queue from stale coordination state:

# Confirm the work queue first. Do not run bare bv; use robot output.
br ready --json
bv --robot-triage | jq '.triage.quick_ref'

Diagnose why the queue appears dry.

ntm work queue-dry --format=json | jq '{queue_dry, evidence, recommendations}'

Render an advisory roadmap only after the dry queue is confirmed.

ntm work queue-dry --ideate --format=json | jq '{ queue_dry, ideation: { status: .ideation.status, guard: .ideation.guard.recommendation, rendered: .ideation.roadmap.rendered_count, preview: .ideation.creation.remaining_commands }, warnings }'

The same plan is available as markdown for human review.

ntm work queue-dry --ideate --format=markdown

Review the duplicate and novelty evidence before creating anything. If br ready has work, or bv --robot-triage shows actionable recommendations, claim that work instead of ideating. --force is only for an explicit preview when an operator wants to inspect the plan despite ready work or degraded tracker state.

Gated creation is opt-in and still uses Beads as the source of truth:

# Re-check the preview and guard before mutating Beads.
ntm work queue-dry --ideate --format=json | jq '.ideation.creation.remaining_commands'

Create proposed beads only after review. The plan version is an audit token.

ntm work queue-dry --ideate --create-beads --yes --plan-version="$(git rev-parse --short HEAD)"

Validate the graph and export Beads state after any mutation.

br dep cycles --json bv --robot-triage | jq '.triage.quick_ref' br sync --flush-only git add .beads/issues.jsonl

If Agent Mail, CASS, or CM are unavailable, queue-dry --ideate keeps running and marks those sources as degraded in warnings. Treat degraded Agent Mail reservation visibility as a coordination stop sign for mutating creation; fix coordination or use the non-mutating preview. Never edit .beads/*.jsonl directly, and use ntm work queue-dry --help for the current flag surface.

4. Coordination, Reservations, and Human Oversight

NTM exposes Agent Mail and reservation workflows directly from the CLI. You can act as Human Overseer, inspect inbox state, review reservations, renew or force-release stale locks, and coordinate work without inventing an ad hoc protocol.

ntm mail send payments --all "Sync to main and report blockers."
ntm mail inbox payments
ntm locks list payments --all-agents
ntm locks renew payments
ntm locks force-release payments 42 --note "agent inactive"
ntm coordinator status payments
ntm coordinator digest payments
ntm coordinator conflicts payments
ntm coordinator enable auto-assign
ntm coordinator enable digest --interval=30m
ntm coordinator disable conflict-negotiate

ntm locks force-release is approval-gated by default (automation.force_release: approval): the first invocation files a durable approval request, a second operator grants it with ntm approve (self-approval is rejected), and re-running the command consumes that approval and executes exactly once. Use ntm policy automation --force-release auto to allow unattended force-release, or never to disable it entirely; the serve HTTP endpoint and the dashboard conflict action honor the same policy setting.

Pane identity badges

In a mixed session, pane titles name the harness (payments__cc_1) while coordination happens under Agent Mail names (BlueLake). NTM can show each managed pane's assigned name in its tmux pane border next to the existing title:

[agent_mail]
enabled = true
pane_badges = true                        # default false (opt-in)

pane_badge_format = "[{name}{drift}]{lifecycle}"

The badge is NTM's assigned identity (the session agent registry, keyed by stable pane id), never whatever a later process wrote to the pane identity file. At every reconciliation NTM compares the assignment with the canonical Agent Mail identity file (legacy plain-text name or the structured record Agent Mail >= 0.3.31 writes) and caches the outcome in per-pane tmux user options (@ntm_agent_mail_name, @ntm_agent_mail_state, @ntm_agent_mail_lifecycle, @ntm_agent_mail_label); the window's pane-border-format gains a fragment that renders only the cached label, so tmux never calls Agent Mail while drawing.

| Border | Meaning | |---|---| | [BlueLake] | current assignment; names agree (binding verified, or a legacy file that cannot be verified) | | [BlueLake!] | current assignment; the identity file disagrees (name-disagreement), is missing-file / unreadable-file / invalid-file, or its binding is binding-unverifiable / binding-stale — the assigned name is retained | | [?!] | no current assignment: assignment-stale (pane generation changed), assignment-unobservable (tmux unreadable) or assignment-unregistered | | … (starting), … (exited), … (unknown) | lifecycle observed at the last reconciliation; running agents carry no marker |

Badges are written alongside identity assignment before the agent launches ((starting)) and reconciled on ntm spawn, ntm add, ntm adopt, restart/respawn and on explicit refresh with ntm mapping --session , which also prints the discrepancy report (assigned vs resolved name, source path, assignment/observation state, last_attempt_at / last_success_at) and carries the same data under identity and discrepancies in --json. They describe the last reconciliation, not continuous liveness. Badge failures warn and never block a launch or change an assignment.

Set pane_badges = false and run ntm mapping --session to withdraw the pane options and restore the window's previous pane-border-format (including inheritance). Opt a single session out with tmux set-option -t =: @ntm_agent_mail_badges off. A pane-border-format you wrote yourself that references @ntm_agent_mail_* is left untouched, and windows linked into several sessions are skipped with a diagnostic.

Every session started by ntm spawn runs the coordinator inside its session monitor; ntm coordinator status shows it under Runtime (runtime in --json). The monitor always maintains assignment leases: it renews the file reservations of in-progress assignments, including those taken by a one-shot ntm assign, before their one-hour leases lapse, and releases them when the assigned bead closes. Every other coordinator action (auto-assign, digest, conflict-negotiate, mail-nudge) is off until enabled. conflict-notify never runs in the session monitor, only under ntm coordinator run: older ntm config init files set it to true, and it mails every holder of overlapping reservations in the project (ntm coordinator status notes when it is set). A foreground ntm coordinator run or a reservation-maintaining ntm assign --watch pauses the monitor's coordinator while it runs, so a session is never coordinated twice.

coordinator enable and disable persist the selected --config file, or the global config by default, without replacing unrelated settings or comments. Session monitors apply a toggle within 15 seconds; restart an already running foreground ntm coordinator run to apply it there. The selected file may use a [coordinator] table or root dotted assignments such as coordinator.auto_assign = false. A whole-section inline assignment such as coordinator = { auto_assign = false } is rejected without changing the file; convert it to either supported form before toggling a feature.

ntm assign --watch --reserve-files maintains the saved file reservations for active assignments immediately on startup and every 30 seconds, including while scans or completion handlers are busy. Each maintenance pass is bounded to 30 seconds. The watcher renews the exact recorded leases only after verifying the current task claim and pane owner, and releases recorded leases when the task is confirmed terminal. It does not recreate expired or missing leases; failures remain visible, including under --quiet, and are retried.

The monitor-hosted coordinator and ntm coordinator run use the same maintenance path before admitting new work. If one pane cannot be observed, independently verified healthy assignments can still renew and confirmed terminal work can still be cleaned up. Unverified assignments remain in the ledger, and a degraded coordinator cycle reports an error without dispatching new work. Watch mode checks the current saved ledger before exiting with --stop-when-done.

The coordinator also recovers a delivered assignment when its original physical pane disappears. It verifies absence across the entire tmux server, rereads agent registries to rule out a moved or rebound owner, and checks the exact current Beads claim and recorded reservations. Only then does it persist task failure, release that assignment's leases, and reopen its claim for ordinary reassignment. Incomplete observations, a remaining pane, changed ownership, or failed cleanup keep the assignment protected. Interrupted cleanup resumes from its durable record; the previous prompt remains recorded as sent and is not replayed.

5. Safety Policy and Approvals

NTM includes a first-class safety system for destructive or sensitive actions. Policy rules define what is allowed, blocked, or approval-gated. Approvals are durable, auditable, and support SLB-style two-person workflows for high-risk operations.

ntm safety status
ntm safety check -- git reset --hard
ntm safety blocked --hours 24
ntm safety install

ntm policy show --all ntm policy validate ntm policy edit ntm policy automation

ntm approve list ntm approve show abc123 ntm approve abc123 ntm approve deny abc123 --reason "wrong target branch"

Every Claude agent NTM launches (spawn, add, robot spawn, restart, restore, rotation, swarm) gets a Claude Code PreToolUse hook through --settings that checks each Bash command against this policy and refuses blocked ones ([safety] claude_policy_hook, default on). Claude Code runs PreToolUse hooks even under --dangerously-skip-permissions, and runs only hooks a settings file or --settings lists, so ntm safety install also registers its hook script in ~/.claude/settings.json for Claude sessions started outside NTM, and ntm safety status reports the hook as installed only when it is registered. dcg and rch hooks ride on the same --settings when those integrations are enabled and installed. A custom [agents] claude command whose last shell command is not the claude invocation (a pipe, a subshell, a wrapper script) must place the hooks itself with --settings {{shellQuote .ClaudeSettings}}; otherwise NTM launches it without them and warns at every launch.

With [integrations.dcg] enabled = true, command lines in messages bound for non-Claude agents are checked by dcg before delivery. ntm send, --robot-send (including --track) and REST sends all refuse a blocked command — robot callers get DESTRUCTIVE_COMMAND_BLOCKED — and each refusal counts toward ntm metrics destructive-command incidents. A missing dcg binary skips the check; a check that errors or is interrupted refuses the send instead.

6. Pipelines, Templates, Recipes, and Workflow Assets

NTM supports several layers of reusable automation:

  • recipes: reusable session presets
  • workflows: runnable orchestration patterns such as pipeline, ping-pong, and review-gate — ntm workflow run executes a template's coordination loop against a live session (ntm spawn -t only uses a template's agent counts to size a session)
  • template: prompt templates and substitutions
  • pipeline: executable multi-step agent workflows with variables, dependencies, resume, and cleanup
  • session-templates: higher-level session layouts
ntm recipes list
ntm recipes show full-stack
ntm workflows list
ntm workflows show red-green
ntm workflow run red-green --var feature="parser rewrite"
ntm workflow run ./my-flow.toml --session payments
ntm template list
ntm template show fix-bug
ntm send payments --cc -t marching_orders --bead bd-123   # rendered per pane: own {{agent_num}}/{{agent_type}}

ntm pipeline run .ntm/pipelines/review.yaml --session payments ntm pipeline status run-20241230-123456-abcd ntm pipeline list ntm pipeline resume run-20241230-123456-abcd --mode=continue ntm pipeline cleanup --older=7d

Pipeline resume preserves completed step outputs. Prompt and template steps also save their delivery and response evidence: --mode=continue resumes observing a confirmed request on its original tmux host and physical pane without submitting it again. Partial output survives interruption, and a completed response can be restored even if the process stopped before saving the enclosing step result. Recognized composer and footer redraws are excluded from response evidence; an idle pane showing only the submitted prompt is not counted as completed work.

Unknown submission outcomes, replaced pane processes or hosts, and missing output boundaries stop recovery with the receipt preserved. If you verify that the original agent received an uncertain submission, record that observation and continue without resending:

# Inspect saved identities and obtain the exact receipt token.
ntm pipeline recover run-20241230-123456-abcd --json

Accept only the inspected delivery, after checking the original agent.

ntm pipeline recover run-20241230-123456-abcd --step-id review \ --receipt <token-from-inspection> --accept-delivery \ --reason "The original agent is working on the submitted review"

Collect the original response and continue dependent steps.

ntm pipeline resume run-20241230-123456-abcd --mode=continue

Recovery runs from the project containing the saved pipeline state. Inspection shows receipt identities without prompt or transcript contents. Acceptance requires the original tmux endpoint, physical pane and process, an intact output boundary, and the frozen workflow. It records the reason and time without sending input, completing the task, or starting dependent steps. Resume keeps its existing step-definition checks. A live run owner, stale receipt, or conflicting decision is refused; repeating the same receipt and reason returns the saved decision, including after the run completes.

REST clients can perform the same recovery through the server's configured project. GET /api/v1/pipelines/{id}/recover requires pipelines:read and returns the inspectable delivery identities and their receipt_token values. To record an observation, send POST /api/v1/pipelines/{id}/recover with pipelines:write permission, Content-Type: application/json, and this JSON body:

{
  "step_id": "review",
  "receipt": "<receipt_token from GET>",
  "accept_delivery": true,
  "reason": "The original agent is working on the submitted review"
}

Acceptance records the same durable decision as the CLI. Continue with POST /api/v1/pipelines/{id}/resume, or submit a pipeline_resume job for pollable execution. Inspection and acceptance expose receipt metadata without prompt or transcript contents. Recovery requests cannot select another project or replace the saved session. Stale receipts, conflicting observations, and a live run owner produce a conflict response without changing the checkpoint.

Each REST pipeline request retains the project selected when it was admitted. Changing project_dir through the configuration API affects subsequent requests; it cannot redirect an admitted request's workflow lookup, run ownership, execution, or cancellation. HTTP idempotency keys use that same project scope, so retrying a key after switching projects cannot replay the previous project's success. Clearing the configured project also cannot replay a cached file-based operation that required it. File-based run, validation, and resume still require a configured project; recovery additionally requires an accessible absolute project directory.

Foreground command steps in the main workflow have a separate CLI recovery path for the case where the command finished but its final checkpoint was lost. pipeline recover includes these command receipts in its commands JSON array, with eligibility and refusal information. Before accepting one, independently verify that the original command and all its descendants have finished, then supply its observed exit code and complete captured output.

On Linux, a failed process-group observation keeps the command under supervision and is retried within its existing execution deadline. It does not request cancellation or shorten the five-second SIGTERM grace period. After the final group signal, NTM retries observation for up to one second while retaining the original group leader's identity. A complete quiet-group observation and complete output capture are both required for a settled receipt.

If cleanup returns without that proof, the receipt becomes unresolved, with the failed or cancelled outcome and cleanup details preserved, rather than remaining running after supervision ended. Ordinary resume and automatic retry refuse these receipts. A saved PID disappearing later is not sufficient evidence for automatic reconciliation: its original group identity is no longer pinned. Use the explicit recovery procedure below only after independently verifying the original command and its effects.

For example:

ntm pipeline recover run-20241230-123456-abcd --json

ntm pipeline recover run-20241230-123456-abcd --step-id migrate \ --receipt <command-receipt-from-inspection> --accept-command \ --exit-code 0 --output-file migration-output.txt \ --reason "Verified the migration and all child processes finished successfully"

ntm pipeline resume run-20241230-123456-abcd --mode=continue

Both --exit-code and --output-file must be supplied explicitly; an empty regular file records an observed empty output. Use complete UTF-8 stdout for a step with output_parse, or captured combined stdout and stderr otherwise. Recovery enforces a 16 MiB bound and the frozen workflow's stdout limit. Inspection and acceptance responses contain identities and output fingerprints, without command text, environment values, or captured output.

Command acceptance records an immutable observation without starting a process, parsing output, or executing dependent work. Ordinary resume checks the exact materialized command definition and applies the existing output parser and failure policy, including parsed output and named variables used by dependents. A nonzero exit code does not authorize an automatic rerun. Repeating the same receipt, reason, exit code, and exact output returns the saved decision, including after completion; a conflicting observation is refused. A live run owner, background command, cleanup or post-pipeline command, or command whose enclosing work already completed prevents acceptance. Recorded PIDs are diagnostic only: recovery neither infers completion from PID disappearance nor signals a saved process ID.

Pipeline mail_send steps also checkpoint their send intent and delivery receipt. If a response is lost or the run stops while a send is unresolved, automatic retries and ordinary resume refuse to send the message again: the recipient may already have received it. Inspect the recipient's inbox and original thread before deliberately choosing --mode=restart-failed to resend. A saved successful receipt restores the original output and parsed variables without another send, even if the scheduler's final step checkpoint was lost. A local output-parsing failure does not authorize resending delivered mail. Explicit server refusals remain eligible for the step's normal retry policy. Interrupted sends from older checkpoints without a receipt require the same deliberate replay decision.

Pipeline file reservations retain their resolved request, exact lease identities, and partial grant evidence. A lost acquisition response or unverified grant never triggers automatic reacquisition. Resume checks that retained protection still has the original IDs, owner, paths, mode, and creation identity, with live expiry, before dispatching further work. A same-ID renewal is accepted; a replacement lease on the same path is not adopted. Release steps for this run's acquisitions select only the recorded IDs and verify their disappearance, so another run's same-path reservations remain intact. Standalone release steps without acquisition evidence retain their explicit path behavior.

Verified partial grants remain available for exact-ID cleanup. Unverified handles are inspection evidence, not authority to release somebody else's leases. Finish releasing known active leases before resetting acquisition history. If an authored cancellation or finalization hook already released protection, resuming unfinished protected work requires a whole-workflow reset to acquire it again; iteration replay cannot retain those released locks. Cleanup-only resume can finish when the original IDs have already expired or disappeared.

While a pipeline is running, a heartbeat renews its verified reservation IDs before expiry, including during long commands, agent waits, and post-pipeline work. Independent readback must confirm the original lease identities and an extended expiry; a lost renewal reply is accepted only when that readback proves protection. Dispatch also checks ownership immediately before sending more work. Lost, replaced, expired, or unverifiable protection fails the run and cancels active waits and commands. Input already delivered to an external agent cannot be recalled. Authored on_cancel cleanup still runs; reservations are never automatically reacquired or released. Use explicit workflow release steps.

If you instead intend to repeat failed or interrupted agent work, inspect the original pane before using --mode=restart-failed; that mode still preserves completed delivery receipts. Use --keep-state=false or --mode=force-iter --step-id= --iteration= for an intentional reset or iteration replay. Commands with a normally settled outcome retain ordinary retry behavior; operator-accepted outcomes require an explicit reset before repeating them. An unresolved command is never automatically repeated. Before deliberately restarting one, inspect its side effects and stop any surviving command processes. These restart choices can repeat side effects.

7. Durable State, Audit, and Recovery

NTM treats recoverability as a core feature. Sessions can be checkpointed, timelines can be replayed, audit records can be exported, and prompt/session history remains available for analysis or resumption.

ntm checkpoint save payments -m "pre-migration"
ntm checkpoint list payments
ntm checkpoint restore payments
ntm checkpoint restore payments last --as payments-recovered --directory /path/to/recovery-worktree

ntm sessions save payments ntm sessions restore payments --name payments-restored --launch ntm sessions resume payments --name payments-resumed

ntm timeline list ntm timeline show <session-id> ntm history search "authentication error" ntm audit show payments ntm conflicts payments ntm resume payments

Ordinary ntm spawn payments --cc=2 also restores the latest readable local handoff for that session from the resolved project directory. The previous goal, immediate task, decisions, and next steps are delivered before the new user prompt through the same readiness and submission checks as other spawn prompts. This works without Beads, Agent Mail, or CM being installed. The handoff uses a bounded recovery budget, and JSON output reports its source as recovery.handoff_path. --no-recovery, recovery.enabled = false, or recovery.auto_inject_on_spawn = false disables this injection. The shared handoff reader can fall back to an older readable file if the newest is corrupt; the reported source identifies the file actually used. If no handoff can be read, or its session or project does not match, spawn reports partial recovery and can proceed with its other available context. Remote tmux sessions skip local handoffs and report that source as unavailable; NTM does not treat a matching local path as proof of the remote project's identity.

Checkpoint restoration accepts --as to recover into a different tmux session without replacing the source session or rewriting its checkpoint. The REST restore endpoint accepts the same option as target_session. A separate session still uses the checkpoint's working directory unless --directory (REST: custom_directory) selects another working tree. Restores honor cancellation; JSON responses retain the last restore stage, pane count, warnings, and interruption status so partially restored sessions can be inspected before retrying.

Newly spawned agents also record their rendered launch command independently of the pane title. Checkpoints preserve that command and its model, persona, reasoning effort, and account-profile reference. Restore validates these inputs before replacing an existing session, then recreates any configured Claude credential isolation for the destination pane. Prepared persona prompt files remain external references: a missing file or changed contents stops automatic replay. Explicit pane or plugin environment values are not copied into this record; recovery reports missing inputs instead of silently substituting the current environment. Older panes without a launch record retain the existing command-reconstruction path.

Saved sessions preserve each pane's working directory and the same recorded launch settings, so panes in separate worktrees return to their own directories. sessions restore --launch replays those settings; sessions resume also replaces the native conversation selector when the save contains a fresh provider binding. Recorded commands keep their model, reasoning, persona, and account settings instead of taking current defaults. Native resume refuses ambiguous shell commands or conflicting conversation selectors before --force replaces a live session. Older saves without a recorded command retain the existing CASR preference. Missing worktrees, changed persona files, unavailable profiles, and omitted environment inputs prevent automatic relaunch. Agent counts report successful command dispatch; they do not assert that an agent has finished starting.

8. Multi-Project Swarm Execution

ntm swarm --json launches the requested swarm and returns an execution receipt. Use swarm plan or --dry-run to preview allocations without creating sessions.

ntm swarm plan --scan-dir=/projects --json
ntm swarm --scan-dir=/projects --json --wait-ready
ntm swarm --scan-dir=/projects --json --prompt="Run the tests and report failures"

The receipt distinguishes planned allocations, created sessions and physical panes, agent launches, initial prompt delivery, and optional readiness results. Partial failures and cancellation return a nonzero exit status while retaining the actual outcomes. Prompts go only to successfully launched agents that pass readiness and identity checks; surviving sessions remain available for inspection.

--auto-rotate-accounts starts an acknowledged resident monitor for eligible local Claude and Codex panes. It checks CAAM account ava

... (README truncated for length)

Chat with me