-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathconfig.example.toml
More file actions
566 lines (521 loc) · 27.7 KB
/
Copy pathconfig.example.toml
File metadata and controls
566 lines (521 loc) · 27.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
# muxa daemon configuration — copy to ~/.config/muxa/config.toml
# Unix socket the daemon listens on.
# Defaults to $XDG_RUNTIME_DIR/muxa.sock (or /tmp/muxa-$UID.sock as fallback).
# socket = "/run/user/1000/muxa.sock"
# tmux socket to attach to for control mode. Default: auto-detect from $TMUX.
# tmux_socket = "default"
# Env-only: MUXA_TMUX_SOCKET scopes muxa to a single tmux socket path instead
# of enumerating every server under /tmp/tmux-$UID. It scopes BOTH the
# multi-server pane scan AND hook ingest: events whose captured $TMUX socket
# is a different server are dropped. Opt-in — unset keeps the global view.
# Set it on the daemon (e.g. in the launchd/systemd unit) to isolate muxa to
# one server: handy for single-server users, tests, isolated demo/CI
# recordings, or excluding agents another multiplexer (e.g. cmux) runs on its
# own server but which share muxa's globally-installed agent hooks:
# MUXA_TMUX_SOCKET=/tmp/tmux-1000/work muxad
[ui]
# theme = "classic" # classic (default), oh-my-muxa, focus, ops, mono, high-contrast, minimal
[notifier]
# enabled = true
# backend = "libnotify" # libnotify | none (more backends TBD)
# Backfill discovery: when muxad starts, scan tmux panes for running agents
# (matched against `pane_current_command`) and synthesize Started events so
# they appear in `muxa status` without waiting for a fresh hook. The same
# routine powers `muxa sync`. Default on.
[discovery]
# enabled = true
# Periodic reconciler: a control-loop pass that converges the in-memory
# agent registry against tmux ground truth. Reaps records whose pane is
# gone, drops synthetic placeholders that lost to a real session, and
# collapses duplicate rows for the same pane. Idempotent — `interval_secs`
# is a tuning knob, not a correctness one.
[reconciler]
# enabled = true
# interval_secs = 30 # 30s default — closed panes disappear from `muxa watch` within seconds
# paneless_stale_timeout_secs = 86400 # 24h. Age out fully orphaned rows (no pane, surface, or pid) to
# # Stopped so the GC reaps them — e.g. codex sessions driven through a
# # detached app-server/remote bridge that fire paneless hooks and never
# # stop on their own. Only the registry row is removed; the tmux session
# # (if any) is untouched. `muxa prune` does this on demand. Set 0 to disable.
# Screen-manifest fallback detection: for agent CLIs muxa has NO hooks for
# (cursor-agent, amp, copilot, aider, goose, and any you declare), the daemon
# periodically captures the pane and matches TOML manifest rules against the
# visible tail to infer Working / WaitingInput / Idle. Last-resort only: hooks
# stay authoritative, herdr hosts are covered by herdr's own detection, and the
# synthetic rows are evicted the instant a real hook claims the pane. Bundled
# Codex panes are the metadata-only exception: their visible Conversation recap
# is retained for watch/peek summaries without changing hook-owned state.
# manifests ship in the binary; drop overrides at
# $XDG_CONFIG_HOME/muxa/agents/<name>.toml (same name replaces a bundled one).
# See docs/SCREEN_DETECTION.md.
[screen_detect]
# enabled = true
# interval_secs = 3 # 3s default — capture/classify cadence; a tick is skipped
# # if the previous is still running. Hook-owned Codex panes
# # add one recap capture per matching pane on each tick.
# Defaults and conventions advertised to MCP-connected agents. These values
# are included in the MCP initialization guide, returned by `muxa_guide`, and
# used by `muxa_start_agent` when the matching argument is omitted. Without an
# `agent`, callers still have to choose one. Options are additional arguments
# after Muxa's built-in provider profile (for example, codex already gets
# `--yolo`) and before the initial prompt. Restart an MCP-connected agent after
# changing this section because its stdio server loaded the old config.
[mcp.guide]
# placement = "pane" # pane (default) | window | session
# agent = "codex" # claude | codex | gemini | agy | opencode
# options = ["--model", "preferred-model"]
# # superseded by [agent.<program>] below, which is
# # per provider and applies to every launch path.
# # Kept working for configs that predate it.
# direction = "right" # right (default) | down; pane splits only
# instructions = "Keep one task per window and use a new session for unrelated projects."
# How each agent CLI is launched. Keyed by provider so a flag can never reach
# the wrong one: `--model` is spelled differently per CLI, and one shared list
# would either fit a single provider or have to be guarded at every launch.
#
# These apply to EVERY path that starts an agent — `muxa agent start`, `muxa
# work start`, a `muxa work up` pipeline, `muxa_start_agent`, and an automatic
# `muxa_call_peer` spawn. Options land after Muxa's built-in provider profile
# (codex already gets `--yolo`) and before the initial prompt.
#
# Anything named explicitly REPLACES these rather than adding to them —
# `muxa agent start --option`, an MCP `options` array, or a pipeline agent's
# own `options` — so an override never puts `--model` on the line twice.
# An unknown provider key fails the load rather than sitting inert.
# [agent.claude]
# options = ["--model", "claude-opus-5"]
# [agent.codex]
# options = ["--model", "gpt-5-codex", "--search"]
# Durable request/reply collaboration between top-level agents in the same
# tmux window. Opt-in: idle_only delivery types only at a hook-authoritative
# agent's top-level Idle state. Working / waiting / choice / error panes and
# synthetic screen-detected agents are never auto-woken. `wake_payload`
# controls whether the body or a short mailbox notification is delivered.
# Headless one-shot questions from `muxa watch` (`a` asks, `A` browses).
# Off by default: enabling it lets muxad spawn an agent CLI that bills
# your account.
# [ask]
# enabled = true
# agent = "claude" # claude | codex; ←/→ switches at runtime
# cwd = "~" # where the headless process runs; defaults to $HOME
# timeout_secs = 180
# keep = 200 # answers retained before the oldest are dropped
[collaboration]
# enabled = true
# wake = "idle_only" # idle_only | never (pull-only)
# wake_payload = "operator_full" # notice | operator_full | full
# Historical JSON paths map to an indexed sibling collaboration.sqlite3.
# Existing JSON is imported once and retained as a migration backup; its
# duplicate message bodies are not covered by retention. A .sqlite/.sqlite3/.db
# path is used directly.
# path = "/home/$USER/.local/share/muxa/collaboration.json"
# max_message_bytes = 16384
# retention_days = 90 # omit to retain indefinitely; prunes eligible whole
# # terminal threads at daemon startup
# Prompt audit log: every PromptSubmitted event is appended to a bounded
# NDJSON file plus an in-memory ring per pane. Powers `muxa recap --all`
# even after the live agent record has been reaped (daemon restart, pane
# close, etc.). Default path lives in $XDG_DATA_HOME.
# window (default): requests stay inside the sender's tmux window.
# host: an explicit pane target may address any tracked agent on this host
# — this is what lets `muxa watch`'s composer send to the row under the
# cursor. peer/@alias/role: selectors stay window-scoped either way.
# scope = "window"
# Central physical-host management over persistent, outbound OpenSSH stdio
# relays. The remote side needs the same muxa version and an owner-local muxad;
# no TCP listener is opened on either side. SSH identity, ProxyJump, port, and
# host-key policy belong in ~/.ssh/config. See docs/FLEET.md.
[fleet]
# enabled = false # outbound SSH hosts; local is always visible
# refresh_secs = 15
# keepalive_secs = 10
# offline_after_secs = 30 # must be at least 2 × keepalive_secs
# connect_timeout_secs = 10
# command_timeout_secs = 10
# max_parallel_connects = 6
# capture_policy = "selected" # selected | never; pane text is fetched on demand only
# Optional metadata for the built-in controller node. `muxa host label local`
# and `muxa host annotate local` edit these tables. muxad supplies immutable
# muxa.io/local, muxa.io/transport, and kubernetes.io/* identity labels.
# [fleet.local.labels]
# environment = "development"
# [fleet.local.annotations]
# "muxa.dev/owner" = "platform-team"
# [fleet.hosts.devbox]
# ssh = "muxa-devbox" # OpenSSH Host alias; no embedded flags
# muxa_path = "muxa"
# enabled = true
# connect = "auto" # auto | on_demand
# mode = "observe" # observe | control
# remote_socket = "/run/user/1000/muxa.sock" # optional
# Kubernetes-style labels are selector/indexing metadata. Values are short
# identifiers; use annotations for descriptive or URL-like data.
# [fleet.hosts.devbox.labels]
# environment = "development"
# region = "icn"
# accelerator = "gpu"
# [fleet.hosts.devbox.annotations]
# "muxa.dev/owner" = "platform-team"
# "muxa.dev/runbook" = "https://example.invalid/runbooks/devbox"
[history]
# enabled = true
# path = "/home/$USER/.local/share/muxa/prompts.ndjson"
# max_per_pane = 200 # cap entries kept per pane (memory + disk)
# max_age_days = 30 # compaction drops anything older
# compact_interval_secs = 3600 # rewrites the file every hour
# Activity ledger: append-only duration events used by `muxa stats` and
# `muxa report`. Records closed agent-state intervals plus closed tmux
# foreground intervals, so duration survives pane/session removal.
[activity]
# enabled = true
# path = "/home/$USER/.local/share/muxa/activity.ndjson"
# max_age_days = 30
# compact_interval_secs = 3600
# Agent registry snapshot: mirrors the live in-memory registry to a single
# JSON file so a daemon restart rehydrates real `session_id`s, last
# prompt/response, and full state/metadata instead of falling back to
# discovery's synthetic-%X placeholders. Writes are event-driven (debounced
# to coalesce bursts), so idle daemons make zero disk traffic.
[state]
# enabled = true
# path = "/home/$USER/.local/share/muxa/state.json"
# debounce_ms = 200 # coalesce window between dirty signal and write
# tmux session foreground-time tracker. A session counts as active while an
# interactive tmux client has that session foregrounded. `muxa watch`
# (work view — the default) shows the workspace cumulative total in its DUR column.
[session_activity]
# enabled = true
# path = "/home/$USER/.local/share/muxa/session-activity.json"
# interval_secs = 5
# `muxa stats` / `muxa report` ACTIVE (engaged time) estimate. Each human action
# (a prompt or a tmux keypress/scroll) is padded into a window; these tune it.
[stats]
# active_lookback_secs = 60 # seconds credited before each action
# active_timeout_secs = 300 # idle timeout after each prompt (5m); larger = more generous
# active_tick_timeout_secs = 90 # idle timeout after a tmux input tick (keypress/scroll);
# # shorter than prompts so watching/scrolling an agent does
# # not chain into hours of "active" time
# count_tmux_input = true # whether tmux input ticks (keypress/scroll) count toward
# # ACTIVE/WORK_ACTIVE at all. tmux advances client_activity
# # for any client input, and with `mouse on` that includes
# # mouse motion/wheel/clicks — indistinguishable from a
# # keypress — so an attached idle session keeps accruing
# # ACTIVE just from the mouse. Set false to anchor ACTIVE
# # only on submitted prompts and thinking.
# Logging is controlled via the standard RUST_LOG env var, e.g.:
# RUST_LOG=muxa=debug,tokio=warn muxad
# Default when unset: "muxa=info".
# `muxa watch` TUI columns. Default keeps the table prompt-forward; uncomment
# to opt back into model / ctx / cost, always-visible workload, or to reorder.
#
# Valid column keys: pane, kind, state, state_age, model, ctx, cost, limits, workload,
# prompt, activity, workspace_time.
# Omit a key to hide the column. Unknown keys log a warning and are skipped.
[watch]
# theme = "classic" # optional watch-only override for [ui].theme
# view = "window" # session + window depth (default)
# view = "session" # session roots only
# view = "pane" # full session → window → pane depth
# layout = "tree" # selectable hierarchy (default)
# layout = "swarm" # dense collaboration-room clusters
# layout = "work" # flat one row per Work, like `muxa work list` with a
# # live state gauge
# Collaboration history has its own presentation axis, independent of the
# topology layout above. `v` toggles it at runtime.
# collab_layout = "table" # table (newest-first) | sequence (chronological)
# Which list watch opens on. `topology` is everything view/layout describe;
# `collab` lists collaboration requests across every room the daemon holds.
# screen = "topology" # (default)
# screen = "collab"
# How tree children open:
# focus — selected session reveals windows; selected window reveals panes
# in pane view, while sibling paths stay folded (default)
# always — show every node through the configured view depth
# manual — start collapsed and change expansion only with h/l or arrows
# tree_expansion = "focus"
# columns = ["pane", "state_age", "model", "ctx", "cost", "workload", "prompt", "activity"]
# What the summary ("prompt") column leads with. Each value names the highest
# tier it will show and then degrades, so the column is never blank when a
# last prompt exists.
# summary = "recap" # response → recap → session title → last prompt (default)
# summary = "title" # session title → last prompt; skips the sparse recap
# summary = "prompt" # last prompt only (pre-recap behavior)
# "recap" is the agent's own summary: Claude Code's `※ recap: …` is read
# from its transcript, while Codex's `Conversation recap` is observed from a
# capture-capable pane when context compaction renders it. Neither is in the
# normal hook payload and both are sparse. The latest captured response takes
# precedence; the title/prompt tiers keep the
# column useful between recaps. Gemini currently falls through to last prompt.
# The header reads SUMMARY unless set to "prompt".
# summary = "recap"
# Hide agents that aren't bound to a tmux pane (Claude SDK sub-processes,
# agents launched outside tmux). Default true because picker actions
# can't target them anyway; the footer surfaces a `+N paneless` count so
# they remain discoverable. `muxa watch --include-paneless` reveals them
# for one invocation without flipping this flag.
# hide_paneless = true
# Animate the state glyph in the `muxa watch` TUI: a spinning braille dot for
# `working` and a rotating half-circle for `starting`. Purely a watch-TUI
# affordance — `muxa status` and the tmux status-line always render the static
# `[ui] icons` glyph. Default true; set false for calm static icons (or a
# terminal without braille support).
# spinner = true
# Preview overlay (`p` key) — first-paint shape. Geometry (`f`, popup ↔
# fullscreen) and content (`c`, prompt/response ↔ live pane) remain
# toggleable at runtime regardless of the default.
[watch.preview]
# default_content = "live_pane" # tmux choose-tree-style live snapshot (default)
# default_content = "prompt_response" # text-only: agent's last prompt + last response
# Sort order for agent rows. Keys are evaluated left-to-right with
# `pane_id` as a final stable tiebreaker. Stale agents (pane closed)
# always sink to the bottom regardless of what's listed here.
#
# Default: ["state", "workspace", "latest"] — floats attention states first,
# then groups by workspace and recency.
#
# Available keys:
# workspace — tmux workspace/session name asc
# workspace_time — workspace foreground duration desc
# latest — last_activity_at desc (most recently updated first)
# activity — alias for latest
# pane — window then pane index, parsed numerically
# pane_id — raw pane id lex asc (predictable; useful for screenshots)
#
# Examples:
# sort = ["latest"] # global newest-first, no grouping
# sort = ["workspace", "pane"] # tmux-native order within workspace
# sort = ["workspace", "latest"] # workspace grouping + recency
# sort = ["workspace", "latest"]
# Per-column widths. Three accepted forms:
# integer -> fixed length (Constraint::Length)
# "min:N" -> minimum length (Constraint::Min, takes leftover space)
# "pct:N" -> percentage 0..100 (Constraint::Percentage)
# Missing keys fall back to a per-column default.
# [watch.widths]
# pane = 22
# state = 3
# workload = 8
# prompt = "min:20"
# activity = 6
# workspace_time = 6
# Selected-row detail: a dim "↳ <text>" line rendered under the highlighted
# row. Visible child shell/subagent work is shown as `tree ◇1 ▸1 +2` in the
# session/name column; otherwise this falls back to the template below.
#
# Default `template = "{last_response|last_prompt}"` — pipe-separated
# alternatives resolve left-to-right and pick the first non-dash value.
# When the agent has captured a response that wins; otherwise the user's
# last prompt is shown so the detail row stays useful for older agents,
# agents mid-turn, and adapters that don't read transcripts yet
# (Codex / Gemini today). When BOTH are empty the detail line suppresses
# itself — that's normal for a freshly-discovered pane, not a bug.
#
# Available variables: pane, kind, state, model, ctx, cost, activity,
# workload, last_prompt, last_response, last_notification, cwd. Unknown
# placeholders are preserved verbatim so typos surface visually.
# [watch.detail]
# enabled = true
# template = "{last_response|last_prompt}"
# # Combined view (heavily truncated since each side gets half the room):
# # template = "{last_prompt} → {last_response}"
# # Just the user's prompt:
# # template = "{last_prompt}"
# HTTP dashboard. Off by default. When enabled, muxad serves a small
# read-only web UI + JSON/SSE API alongside its unix-socket IPC.
#
# Security model:
# - Token auth is the default. When the dashboard is enabled, set an
# explicit `token` or explicitly opt out with `auth = "none"`.
# - Bound to 127.0.0.1 by default; non-loopback `bind` also requires
# `allow_public = true`.
# - Static HTML/CSS/JS load without auth so a browser can bootstrap
# a token via `#token=...` (which is then stored in localStorage
# and stripped from the URL bar). All `/api/*` routes are gated.
# Legacy `?token=...` parsing remains for compatibility only.
# - TLS is intentionally out of scope — front with nginx/Caddy if you
# expose this beyond your machine.
#
# Generate a token with `openssl rand -hex 32` (or any 32+ random bytes).
# CLI flags / env vars override these values per-field:
# --dashboard / --no-dashboard
# --dashboard-bind / MUXA_DASHBOARD_BIND
# --dashboard-auth / MUXA_DASHBOARD_AUTH
# --dashboard-token / MUXA_DASHBOARD_TOKEN
# --allow-public / MUXA_DASHBOARD_ALLOW_PUBLIC
[dashboard]
# enabled = true
# bind = "127.0.0.1:7878"
# auth = "token" # or "none" to explicitly disable authentication
# token = "<paste 32+ random hex bytes here>"
# allow_public = false
# pane_cache_ttl_ms = 2000
#
# Let the dashboard stand a work pipeline up — that is, launch agent
# processes — through `POST /api/work-control/up` and the board's
# "start work" control. Off by default, and deliberately its own switch
# rather than riding on the control token: every other write route steers
# a process you already started, while this one starts new ones with
# permissions bypassed. Requires [[route]] and [pipeline.*] to be
# configured; see docs/PIPELINE.md.
# allow_work_start = false
# External sinks. Default off — none of these forward data anywhere
# unless you explicitly opt in.
#
# [sinks.oh_my_prompt] forwards every PromptSubmitted event to an
# oh-my-prompt (omp) instance via its HTTP ingestion endpoint
# (POST /api/sync/upload). The X-User-Token UUID lives in an env var
# (default OMP_SERVER_TOKEN) — never in this file. The endpoint must be
# explicitly set; there is no default to keep "send your data to a
# stranger's server" out of the footgun zone.
#
# What gets sent: prompt text, char/word/token counts, agent kind,
# session id, working directory + project basename (when known),
# model (when known), and a deterministic event_id derived from
# session id + timestamp so retries dedup cleanly.
#
# See docs/SINKS.md for setup, troubleshooting, and an exhaustive
# field-by-field mapping.
[sinks.oh_my_prompt]
# enabled = true
# endpoint = "https://prompt.example.dev"
# token_env = "OMP_SERVER_TOKEN"
# device_id = "laptop-01"
# batch_size = 50
# flush_interval_ms = 5000
# [sinks.webhook] forwards `Transition` events to a Slack or Discord
# webhook so you can be paged on your phone the moment an agent flips
# to `WaitingInput` or `Error` while you're AFK from `muxa watch`.
# Auto-detects the wire format from the URL: hooks.slack.com →
# `{"text": "..."}`, discord.com/api/webhooks → `{"content": "..."}`,
# anything else → the full `Transition` JSON.
#
# The webhook URL is itself the secret for Slack/Discord; prefer
# `endpoint_env` over inline `endpoint` so it never lives in TOML.
# `endpoint_env` wins when both are set.
#
# Best-effort by design: a failed POST logs and drops. No queue, no
# backoff — Slack/Discord's own rate-limit handling is good enough,
# and we'd rather lose a single page than thunder-herd 1000 buffered
# alerts when an outage clears.
[sinks.webhook]
# enabled = true
# endpoint = "https://hooks.slack.com/services/T0/B0/abc"
# endpoint_env = "MUXA_SLACK_URL"
# flavor = "slack" # slack | discord | generic
# on_states = ["WaitingInput", "Error"]
# rate_limit_secs = 60 # per (kind, session, state)
# ---------------------------------------------------------------------------
# `muxa work up` — declarative work pipelines
#
# One command takes a ticket id and leaves you with a staffed tmux window:
#
# muxa work up cal-1234
#
# It resolves the ticket, routes it to a workspace/session, and creates
# whichever agent panes the pipeline declares but the window does not have
# yet. Re-running converges — it fills gaps, it does not duplicate a team.
# Add `--dry-run` to see the plan without touching tmux. See docs/PIPELINE.md.
#
# What the work *is* rides in as a request, phrased the same way
# `muxa_call_peer` takes one — `--skill` expands a [message.skills] entry,
# then `--body`, then `--context`:
#
# muxa work up cal-1234 --body "fix the double reap"
#
# and the same request is delivered two ways depending on what exists: into
# the launch prompt of an agent that has no pane yet, and typed into the
# pane of one that is already running. Agents can call this as
# `muxa_start_work` over MCP.
# ---------------------------------------------------------------------------
# How a work id becomes ticket context.
#
# muxa does not speak Linear, Jira, or GitHub. It spends one headless agent
# turn asking an agent to fetch the ticket, because you already taught your
# agent CLI how — a skill, an MCP server, `gh`, a token in the environment.
# Adding a provider here is a prompt, not a muxa release.
#
# With no [ticket.source] entries nothing is ever spawned and `muxa work up`
# runs on the work id alone.
[ticket]
# agent = "claude" # resolver CLI: claude | codex (print mode)
# cwd = "~" # neutral cwd; also where user-scoped skills live
# permission_mode = "bypass" # default | edit | bypass — nobody is at the keyboard
# additional_dirs = []
# timeout_secs = 180
# cache_secs = 900 # serve a cached ticket this long; 0 disables
# Sources are tried in sorted-key order; the first `match` wins. `{{id}}` is
# the work id. The reply is scanned for a JSON object, so ask for one — prose
# around it is fine, and `{"like":"this"}` in the prompt is left alone because
# only `{{double}}` braces are substituted.
# [ticket.source.linear]
# match = '^cal-\d+$'
# prompt = '''
# Look up Linear issue {{id}} using the linear skill and answer with one JSON
# object and nothing else:
# {"id": "...", "title": "...", "body": "...", "url": "...", "state": "..."}
# '''
# [ticket.source.github]
# match = '^\d+$'
# prompt = 'Run `gh issue view {{id}} --json number,title,body,url,state` and return its JSON.'
# Routing: work id → where its tmux surfaces live and which pipeline staffs
# them. Ordered, first match wins, so put the catch-all last. Without any
# route, `muxa work up --pipeline <name>` still works and uses the current
# directory.
# [[route]]
# match = '^cal-'
# workspace = 'callabo' # the tmux session
# pipeline = 'triad'
# # Give each work item its own git worktree so three agents in one window
# # cannot trip over each other in a shared checkout. Omit to share a
# # checkout and use `cwd` instead.
# [route.worktree]
# repo = '~/workspace/callabo'
# branch = '{{id}}' # default: {{id}}
# # path = '~/worktrees/{{id}}' # default: <repo>/../<repo-name>-worktrees/{{id}}
# # base = 'origin/main' # default: the repo's origin/HEAD
# [[route]]
# match = '.*'
# cwd = '{{cwd}}'
# pipeline = 'solo'
# A pipeline is a desired set of panes, not a script. `alias` is the key the
# desired-vs-actual diff runs on: it is recorded on the pane, so it survives
# muxad, the CLI, and the agent restarting. Keep it unique and stable.
#
# Placeholders: {{id}} (lowercase work id, for branches and paths), {{work}}
# (as muxa stores it), {{workspace}}, {{cwd}}, {{alias}}, {{role}},
# {{ticket.title|body|url|state|id|branch}}, and {{request}} — the composed
# --skill/--body/--context. If no template here places {{request}}, it is
# prepended to every agent's prompt, so a pipeline written before you passed
# a --body cannot silently swallow one.
# [pipeline.triad]
# description = 'planner → implementer → reviewer'
# layout = 'main-vertical' # tmux layout, applied once every pane exists
# # Context every agent in this pipeline needs, stated once. Each agent's own
# # prompt is appended to it.
# prompt = '''
# {{work}} — {{ticket.title}}
# {{ticket.url}}
#
# {{ticket.body}}
# '''
# [[pipeline.triad.agent]]
# alias = 'plan'
# program = 'codex' # claude | codex | gemini | agy | opencode
# role = 'planner' # peers can address it as role:planner
# prompt = 'You own the approach. Write the plan first; do not edit code.'
# [[pipeline.triad.agent]]
# alias = 'impl'
# program = 'codex'
# role = 'implementer'
# prompt = 'You own the implementation. Follow the planner; ask before scope changes.'
# direction = 'down' # right (default) | down
# [[pipeline.triad.agent]]
# alias = 'review'
# program = 'claude'
# role = 'reviewer'
# prompt = 'You own review. Critique the implementer; do not edit.'
# options = ['--model', 'claude-sonnet-5']
# # optional: this pane's provider arguments,
# # replacing [agent.claude].options. Panes in one
# # line-up are not doing the same job — a reviewer
# # reading a diff need not run what wrote it.