@vibelord/lanes 1.5.1 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibelord/lanes",
3
- "version": "1.5.1",
3
+ "version": "1.6.1",
4
4
  "description": "Installer for Lanes: shared agent terminals over SSH with Zellij views, registry sync, uploads and a phone interface.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -12,7 +12,7 @@ glyph and command, installation and updates, and the operations reference.
12
12
  Closing a view or losing a connection leaves the terminals running in their workspace containers: a lane ends only
13
13
  when someone closes it explicitly.
14
14
 
15
- **Who uses it.** Operators and teammates who run several Claude Code, Codex, Kimi Code or Grok agents at once use
15
+ **Who uses it.** Operators and teammates who run several Claude Code, Codex, Kimi Code, Grok, Muse Code or ZCode agents at once use
16
16
  Lanes daily to see which lane needs them, switch to it, answer, and move on, from a desk or from a phone (which
17
17
  harness runs where, and what Lanes reads from each: [Harnesses](docs/USERGUIDE.md#harnesses)). An administrator installs the
18
18
  server once on the workspace host. Any member invites a new person with `lanes invite`, and that person joins from
@@ -1 +1 @@
1
- 1.5.1
1
+ 1.6.1
@@ -88,9 +88,10 @@
88
88
  unknown one stops the run before anything is written); --write then applies, in order:
89
89
  the board (`workspaces add`, when --workspace is given), install-container,
90
90
  (--step grok-build first: see below) `hooks install --write` for claude and codex,
91
- then for kimi, grok (Grok Build) and
92
- grok-cli where that harness's home (~/.kimi-code, ~/.grok-build, ~/.grok) is in the
93
- container (else `skipped`); two steps run only when named: --step kimi-notify (Kimi's
91
+ then for kimi, grok (Grok Build), grok-cli, muse (Muse Code) and zcode (ZCode) where
92
+ that harness's home (~/.kimi-code, ~/.grok-build, ~/.grok, ~/.config/muse, ~/.zcode) is
93
+ in the container (else `skipped`; Lanes installs neither Muse nor ZCode); two steps run
94
+ only when named: --step kimi-notify (Kimi's
94
95
  tui.toml notification_condition = "always": the bell on every turn end) and --step
95
96
  grok-build (Grok Build installed under GROK_HOME=~/.grok-build by the stable-channel
96
97
  recipe, never ~/.grok; the channel publishes no checksum, so the receipt records the
@@ -199,9 +200,11 @@
199
200
  (additive; installs no agent hook and no editor: lane-edit runs $LANES_EDITOR or the
200
201
  first of micro hx nvim vim vi there) and add the Lanes MCP server to the agents'
201
202
  configs: mcpServers.lanes in ~/.claude.json and [mcp_servers.lanes] in
202
- ~/.codex/config.toml, and where Kimi Code, Grok Build or grok-cli is installed
203
- ~/.kimi-code/mcp.json, ~/.grok-build/config.toml (a `# >>> lanes mcp >>>` block) or
204
- ~/.grok/user-settings.json (mcp.servers id lanes), all starting
203
+ ~/.codex/config.toml, and where Kimi Code, Grok Build, grok-cli, Muse Code or ZCode is
204
+ installed ~/.kimi-code/mcp.json, ~/.grok-build/config.toml (a `# >>> lanes mcp >>>`
205
+ block), ~/.grok/user-settings.json (mcp.servers id lanes),
206
+ ~/.config/muse/settings.json (mcp_servers.lanes, mode optional) or
207
+ ~/.zcode/cli/config.json (mcp.servers.lanes), all starting
205
208
  /home/coder/.lz/bin/lane-mcp — only where missing,
206
209
  the file kept as <file>.bak-<UTC> first, atomic (a `lanes` entry that starts something
207
210
  else is left alone, exit 1; on a uid split nothing is written there, exit 1). --no-mcp
@@ -218,14 +221,17 @@
218
221
  must start `throttle_` — into its ~/.lz/throttle-token (0600, by rename), read and
219
222
  written through /proc/<init>/root without following a link. Prints `adopted` or the
220
223
  refusal, never the value. Exit 1 when refused
221
- lanes hooks install --agent claude|codex|kimi|grok|grok-cli (--container <name> | --home <dir>) [--dry-run | --write]
224
+ lanes hooks install --agent claude|codex|kimi|grok|grok-cli|muse|zcode (--container <name> | --home <dir>) [--dry-run | --write]
222
225
  merge the lane-signal hooks into that home's ~/.claude/settings.json or ~/.codex/hooks.json
223
226
  (+ a top-level Codex notify when none is set), ~/.kimi-code/config.toml (a `# >>> lanes
224
227
  hooks >>>` block of [[hooks]]), Grok Build's ~/.grok-build/hooks/lanes.json and
225
228
  ~/.grok-build/config.toml (blocks: [compat.claude] hooks/mcps = false, [ui.notifications]
226
229
  bel always, [model."grok-4.6"/"grok-4.5"] env_http_headers from LANES_TRACE) or
227
- grok-cli's ~/.grok/user-settings.json `hooks`; prints the diff (credential values shown
228
- as …), writes only with --write (backup kept; nothing outside a block is rewritten).
230
+ grok-cli's ~/.grok/user-settings.json `hooks`, Muse Code's ~/.config/muse/settings.json
231
+ `hooks` (schema_version 1 required) or ZCode's ~/.zcode/cli/config.json `hooks.events`
232
+ (+ hooks.enabled true; refused when it is false); prints the diff (credential values
233
+ shown as …), writes only with --write (backup kept; nothing outside a block or the one
234
+ JSON member is rewritten).
229
235
  Opt-in, per container; Codex still asks you to trust new hooks.
230
236
  --write into a --container whose home the agent could not read from this account (uid
231
237
  not 1000, or a home volume not owned by 1000) is refused with the reason
@@ -1324,7 +1330,7 @@ def cmd_install_container(args):
1324
1330
  # user (0700), never here.
1325
1331
  install_file(L.REPO / 'container/vimrc', root / 'vimrc')
1326
1332
  # The hook DEFINITIONS travel with the contract for reference; no hook is installed here — that is the opt-in,
1327
- # per-container `lanes hooks install --agent claude|codex|kimi|grok|grok-cli --container <name> --write` (RUNBOOK).
1333
+ # per-container `lanes hooks install --agent claude|codex|kimi|grok|grok-cli|muse|zcode --container <name> --write` (RUNBOOK).
1328
1334
  for source in sorted([*(L.REPO / 'container/hooks').glob('*.json'), *(L.REPO / 'container/hooks').glob('*.toml')]):
1329
1335
  install_file(source, root / 'hooks' / source.name)
1330
1336
  env_file = L.REPO / 'container/env.sh' # TMPDIR off the RAM-backed /tmp; only NEW shells and agents read it
@@ -1462,13 +1468,16 @@ HOOK_TARGETS = {'claude': ('claude-settings.json', '.claude/settings.json'),
1462
1468
  'codex': ('codex-hooks.json', '.codex/hooks.json'),
1463
1469
  'kimi': ('kimi-hooks.toml', '.kimi-code/config.toml'),
1464
1470
  'grok': ('grok-hooks.json', '.grok-build/hooks/lanes.json'),
1465
- 'grok-cli': ('grok-cli-hooks.json', '.grok/user-settings.json')}
1471
+ 'grok-cli': ('grok-cli-hooks.json', '.grok/user-settings.json'),
1472
+ 'muse': ('muse-hooks.json', '.config/muse/settings.json'),
1473
+ 'zcode': ('zcode-hooks.json', '.zcode/cli/config.json')}
1466
1474
  # The other file an install changes: Codex's top-level notify; Grok Build's config.toml blocks (W12).
1467
1475
  HOOK_EXTRA = {'codex': ('.codex/config.toml',), 'grok': ('.grok-build/config.toml',)}
1468
1476
  # W12: a harness whose home is not a directory in the container is not installed there; `lanes onboard` skips it.
1469
- HOOK_HOMES = {'kimi': '.kimi-code', 'grok': '.grok-build', 'grok-cli': '.grok'}
1470
- HOOKS_USAGE = ('usage: lanes hooks install --agent claude|codex|kimi|grok|grok-cli (--container <name> | --home <dir>) '
1471
- '[--dry-run | --write]')
1477
+ HOOK_HOMES = {'kimi': '.kimi-code', 'grok': '.grok-build', 'grok-cli': '.grok', 'muse': '.config/muse',
1478
+ 'zcode': '.zcode'}
1479
+ HOOKS_USAGE = ('usage: lanes hooks install --agent claude|codex|kimi|grok|grok-cli|muse|zcode (--container <name> | '
1480
+ '--home <dir>) [--dry-run | --write]')
1472
1481
 
1473
1482
 
1474
1483
  def hook_files(agent):
@@ -1734,9 +1743,79 @@ def grok_cli_settings(text):
1734
1743
  return after
1735
1744
 
1736
1745
 
1746
+ # ------------------------------------------------------------------- W13: Muse Code and ZCode hooks
1747
+ # Both are JSON files the harness owns, edited without markers: the one member Lanes changes is merged (merge_hooks:
1748
+ # every lane-signal group of an earlier install replaced, every other group, event and key kept) and set in place
1749
+ # (lane_core.set_member), every other byte kept; the result must parse back to the input with that member.
1750
+
1751
+ def muse_settings(text):
1752
+ """Muse Code's ~/.config/muse/settings.json with the lane-signal groups merged into its `hooks` (Claude Code's
1753
+ shape: {Event: [{matcher?, hooks: [{type: "command", command, timeout, async?}]}]}, timeout in seconds; MEASURED
1754
+ 2026-10-16 on muse 1.4.10). A new file is {"schema_version": 1, "hooks": …}; an existing one must already say
1755
+ "schema_version": 1 (Muse refuses a file without it). Never registered: LLMCall, Subagent*, PostToolBatch (they
1756
+ carry model content and fire per call); Interrupt runs `async` (Muse requires it). Muse clears a hook's environment,
1757
+ so lane-signal finds the lane through the agent's own /proc environ (DEF-2031)."""
1758
+ core, what = lane_core(), '~/.config/muse/settings.json'
1759
+ ours = render_hooks('muse')
1760
+ try:
1761
+ doc, why = core.muse_settings_doc(text, what)
1762
+ except core.LaneError as error:
1763
+ raise _lane_error(error) from None
1764
+ if why:
1765
+ raise L.LanesError(why)
1766
+ if text is None or not text.strip():
1767
+ return json.dumps(dict(doc, hooks=ours['hooks']), indent=2, ensure_ascii=False) + '\n'
1768
+ merged = merge_hooks(doc, ours)['hooks']
1769
+ if merged == doc.get('hooks'):
1770
+ return text
1771
+ after = core.set_member(text, [], 'hooks', merged)
1772
+ if core.strict_json(after, what) != dict(doc, hooks=merged):
1773
+ raise L.LanesError(f'{what}: the hooks could not be merged without changing anything else — add them by hand')
1774
+ return after
1775
+
1776
+
1777
+ def zcode_config(text):
1778
+ """ZCode's ~/.zcode/cli/config.json with `hooks.enabled` true and the lane-signal groups merged into
1779
+ `hooks.events` (its README: process hooks {type: "process", command, args, timeoutMs}, argv execution without a
1780
+ shell, so the command is lane-signal's absolute path; hooks are off by default). A file whose `hooks.enabled` is
1781
+ explicitly false refuses: the person turned hooks off, and Lanes does not turn them on behind them. Every other key
1782
+ of `hooks` (timeoutMs, maxOutputBytes) stays."""
1783
+ core, what = lane_core(), '~/.zcode/cli/config.json'
1784
+ ours = render_hooks('zcode')
1785
+ if text is None or not text.strip():
1786
+ return json.dumps({'hooks': {'enabled': True, 'events': ours['hooks']}}, indent=2, ensure_ascii=False) + '\n'
1787
+ try:
1788
+ doc = core.strict_json(text, what)
1789
+ except core.LaneError as error:
1790
+ raise _lane_error(error) from None
1791
+ if not isinstance(doc, dict):
1792
+ raise L.LanesError(f'{what}: not a JSON object — refusing to rewrite it')
1793
+ hooks = doc.get('hooks')
1794
+ if hooks is not None and not isinstance(hooks, dict):
1795
+ raise L.LanesError(f'{what}: "hooks" is not an object — fix it by hand first')
1796
+ if isinstance(hooks, dict) and hooks.get('enabled') is False:
1797
+ raise L.LanesError(f'{what}: hooks.enabled is false — someone turned ZCode hooks off; left alone (set it to '
1798
+ 'true by hand, then run this again)')
1799
+ events = (hooks or {}).get('events', {})
1800
+ merged = merge_hooks({'hooks': events}, ours)['hooks']
1801
+ wanted = dict(hooks or {}, enabled=True, events=merged)
1802
+ if wanted == hooks:
1803
+ return text
1804
+ if hooks is None:
1805
+ after = core.set_member(text, [], 'hooks', wanted)
1806
+ else:
1807
+ after = core.set_member(text, ['hooks'], 'enabled', True)
1808
+ after = core.set_member(after, ['hooks'], 'events', merged)
1809
+ if core.strict_json(after, what) != dict(doc, hooks=wanted):
1810
+ raise L.LanesError(f'{what}: the hooks could not be merged without changing anything else — add them by hand')
1811
+ return after
1812
+
1813
+
1737
1814
  HOOK_EDITORS = {'kimi': (('.kimi-code/config.toml', kimi_config),),
1738
1815
  'grok': (('.grok-build/hooks/lanes.json', grok_hooks_file), ('.grok-build/config.toml', grok_config)),
1739
- 'grok-cli': (('.grok/user-settings.json', grok_cli_settings),)}
1816
+ 'grok-cli': (('.grok/user-settings.json', grok_cli_settings),),
1817
+ 'muse': (('.config/muse/settings.json', muse_settings),),
1818
+ 'zcode': (('.zcode/cli/config.json', zcode_config),)}
1740
1819
 
1741
1820
 
1742
1821
  def install_edits(home, edits, write):
@@ -2748,7 +2827,8 @@ def cmd_workspaces(args):
2748
2827
  ONBOARD_USAGE = ('usage: lanes onboard <login> --container <name> [--workspace <id> --repo <path> [--title T]] '
2749
2828
  '[--write] [--no-mcp] [--step kimi-notify|grok-build]...')
2750
2829
  ONBOARD_OPTIONAL = ('kimi-notify', 'grok-build') # steps that run only when named with --step (W12)
2751
- HARNESS_NAMES = {'kimi': 'Kimi Code', 'grok': 'Grok Build', 'grok-cli': 'grok-cli'}
2830
+ HARNESS_NAMES = {'kimi': 'Kimi Code', 'grok': 'Grok Build', 'grok-cli': 'grok-cli', 'muse': 'Muse Code',
2831
+ 'zcode': 'ZCode'}
2752
2832
  # The receipt's status vocabulary. A dry run reports planned, unchanged, skipped, manual or failed, and `ok` for the
2753
2833
  # read-only `inspect` of the container; `manual` names a step a person still has to take (printed with the exact command);
2754
2834
  # `warn` is a step that ran and reported a problem that does not stop onboarding (the doctor's verdict, a board with
@@ -3229,6 +3309,7 @@ def cmd_onboard(args):
3229
3309
  ('lane-mcp', mcp_step, False), ('hooks-claude', hooks_step('claude'), False),
3230
3310
  ('hooks-codex', hooks_step('codex'), False), ('hooks-kimi', hooks_step('kimi'), False),
3231
3311
  ('kimi-notify', kimi_notify_step, False), ('hooks-grok', hooks_step('grok'), False), ('hooks-grok-cli', hooks_step('grok-cli'), False),
3312
+ ('hooks-muse', hooks_step('muse'), False), ('hooks-zcode', hooks_step('zcode'), False),
3232
3313
  ('throttle', lambda: provision_throttle(container, site, write), False),
3233
3314
  ('sync', sync_step, False), ('cursor-sync', cursor_step, False), ('resync', resync_step, False),
3234
3315
  ('doctor', doctor_step, False)]
@@ -4799,10 +4880,10 @@ def docs_doctor(home=None, probe=None):
4799
4880
  # statusd calls it stale past 90 s (statusd.PULSE_STALE_S, DEF-1941), and doctor's own clock uses the same bound, so the
4800
4881
  # two never disagree about a healthy pulse.
4801
4882
  PULSE_FRESH_S = 90
4802
- # The pulse's pools in its order (statusd.PULSE_POOLS, the throttle-pulse POOLS; W12 added kimi), named by the
4803
- # `throttle pools:` line. A pool the pulse did not report says `?`: a pulse from before 1.5.0, or a daemon from before
4804
- # its Kimi pool (`/pools/kimi/ready` answers 404 there).
4805
- PULSE_POOLS = ('claude', 'codex', 'kimi')
4883
+ # The pulse's pools in its order (statusd.PULSE_POOLS, the throttle-pulse POOLS; W12 added kimi, W13 muse-code and zai),
4884
+ # named by the `throttle pools:` line. A pool the pulse did not report says `?`: a pulse from before 1.5.0 (1.6.0 for
4885
+ # muse-code and zai), or a daemon from before that pool (`/pools/<pool>/ready` answers 404 there).
4886
+ PULSE_POOLS = ('claude', 'codex', 'kimi', 'muse-code', 'zai')
4806
4887
  FLEET_FRESH_S = 60 # statusd republishes `fleet` every FLEET_INTERVAL (10 s) unless the host is critical
4807
4888
  THROTTLE_MONITOR = 'tools--throttle-monitor'
4808
4889
  FLEET_DASHBOARD_KEY = 'LANES_FLEET_DASHBOARD_URL'
@@ -4973,11 +5054,12 @@ def fleet_doctor(status, registry, home=None, now=None):
4973
5054
  # value of the spec's enum, `unknown` (and any value outside the enum, such as an empty harness from a statusd before
4974
5055
  # W12) only when some lane has it. A row without `agent` (no signal names an agent there) is not counted. Information
4975
5056
  # only, never a ✗.
4976
- HARNESSES = ('claude', 'codex', 'kimi', 'grok', 'grok-cli', 'shell')
5057
+ HARNESSES = ('claude', 'codex', 'kimi', 'grok', 'grok-cli', 'muse', 'zcode', 'shell') # W13: + muse, zcode
4977
5058
 
4978
5059
 
4979
5060
  def harness_line(status):
4980
- """`lanes: claude 50 · codex 20 · kimi 3 · grok 0 · grok-cli 0 · shell 0` from status.json's lane rows. `shell`
5061
+ """`lanes: claude 50 · codex 20 · kimi 3 · grok 0 · grok-cli 0 · muse 0 · zcode 0 · shell 0` from status.json's
5062
+ lane rows. `shell`
4981
5063
  counts an agent{} that names it, which statusd's merge never writes (a pane whose harness ended is published under
4982
5064
  that harness's name with status `shell`, DM-LANES-STATUS): the slot reads 0 unless a foreign sidecar says so."""
4983
5065
  counts, unknown = dict.fromkeys(HARNESSES, 0), 0
@@ -1,6 +1,10 @@
1
1
  # lanes window — shared by macOS and Omarchy. Loaded by a SEPARATE Ghostty instance
2
2
  # (`--config-default-files=false --config-file=…`), so the everyday Ghostty config is never modified.
3
3
  # The everyday config is inherited first (fonts, theme, padding); `?` = optional include.
4
+ # Lanes' default theme follows the desktop light/dark preference. On Omarchy the launcher keeps non-color settings
5
+ # from the everyday config while filtering its explicit theme colors, which otherwise override this pair.
6
+ theme = light:Catppuccin Latte,dark:Dracula
7
+ window-theme = system
4
8
  config-file = ?~/.config/ghostty/config.ghostty
5
9
  config-file = ?~/.config/ghostty/config
6
10
 
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env python3
2
+ """Prepare the Omarchy Lanes-only Ghostty config and launch its dedicated window."""
3
+ import os
4
+ from pathlib import Path
5
+ import subprocess
6
+ import sys
7
+
8
+
9
+ COLORS = {
10
+ "theme", "window-theme", "command", "initial-command", "background", "foreground", "palette",
11
+ "cursor-color", "cursor-text", "selection-background", "selection-foreground",
12
+ }
13
+
14
+
15
+ def key_of(line):
16
+ text = line.strip()
17
+ return text.split("=", 1)[0].strip() if text and not text.startswith("#") and "=" in text else None
18
+
19
+
20
+ def main():
21
+ home = Path.home()
22
+ config_home = Path(os.environ.get("XDG_CONFIG_HOME", home / ".config"))
23
+ lanes = config_home / "lanes"
24
+ target = lanes / "ghostty.conf"
25
+ base = lanes / "appearance-base.ghostty"
26
+ if not target.is_file():
27
+ raise SystemExit("Lanes Ghostty config is missing; run the Lanes installer first.")
28
+
29
+ queue = [config_home / "ghostty" / name for name in ("config.ghostty", "config")]
30
+ seen = set()
31
+ inherited = ["# Generated for Lanes from everyday Ghostty settings; theme colors are omitted."]
32
+ while queue:
33
+ path = queue.pop(0).expanduser().resolve()
34
+ if path in seen or not path.is_file():
35
+ continue
36
+ seen.add(path)
37
+ if len(seen) > 64:
38
+ raise SystemExit("Too many nested Ghostty config files.")
39
+ for line in path.read_text().splitlines():
40
+ key = key_of(line)
41
+ if key == "config-file":
42
+ value = line.split("=", 1)[1].strip().removeprefix("?").strip().strip('"')
43
+ child = Path(value).expanduser()
44
+ queue.append(child if child.is_absolute() else path.parent / child)
45
+ elif key not in COLORS:
46
+ inherited.append(line)
47
+ base.write_text("\n".join(inherited) + "\n")
48
+
49
+ appearance = lanes / "appearance.ghostty"
50
+ generated = {base.resolve(), appearance.resolve()}
51
+
52
+ def own(line):
53
+ """An include of Lanes' own (the palette chain under ~/.config/lanes) stays; the everyday Ghostty ones are
54
+ replaced by the filtered base below; an earlier run's base/appearance lines are re-added, not kept."""
55
+ value = line.split("=", 1)[1].strip().removeprefix("?").strip().strip('"')
56
+ path = Path(value).expanduser()
57
+ path = (path if path.is_absolute() else lanes / path).resolve()
58
+ return path not in generated and path.is_relative_to(lanes.resolve())
59
+
60
+ lines = [line for line in target.read_text().splitlines() if key_of(line) != "config-file" or own(line)]
61
+ lines.extend((f"config-file = {base}",))
62
+ if appearance.is_file():
63
+ lines.append(f"config-file = {appearance}")
64
+ rendered = "\n".join(lines) + "\n"
65
+ candidate = lanes / f".ghostty-appearance.{os.getpid()}.conf"
66
+ candidate.write_text(rendered)
67
+ try:
68
+ ghostty = subprocess.run(["ghostty", "+validate-config", f"--config-file={candidate}"], check=False)
69
+ if ghostty.returncode:
70
+ raise SystemExit(ghostty.returncode)
71
+ target.write_text(rendered)
72
+ finally:
73
+ candidate.unlink(missing_ok=True)
74
+
75
+ if sys.argv[1:] == ["--prepare-only"]:
76
+ print(target)
77
+ return
78
+ launcher = home / ".local" / "bin" / "lanes-window"
79
+ os.execv(str(launcher), [str(launcher), *sys.argv[1:]])
80
+
81
+
82
+ if __name__ == "__main__":
83
+ main()
@@ -1,7 +1,9 @@
1
1
  # Omarchy / Hyprland specifics for the lanes window (Ghostty GTK, dedicated instance).
2
- # Look & feel is inherited from ~/.config/ghostty/config (Omarchy's default: theme include, the Omarchy default font —
2
+ # Fonts, padding and shortcuts are inherited from ~/.config/ghostty/config. The Lanes launcher filters its explicit
3
+ # theme colors so the light/dark pair in common.conf can take effect; the everyday Ghostty config is never changed.
4
+ # Omarchy's default font —
3
5
  # a Nerd Font is not required by lanes, every glyph is plain Unicode; shift+enter as CSI-u, control+insert /
4
- # shift+insert copy & paste), so Omarchy theme switches follow.
6
+ # shift+insert copy & paste), while the Lanes palette follows the desktop light/dark preference.
5
7
  # Do NOT include the theme file here as well: Ghostty reports a second include of the same file as
6
8
  # "config-file …: cycle detected" and opens its configuration-errors window.
7
9
 
@@ -384,6 +384,7 @@ dog_ready=
384
384
  reader=
385
385
  holding=
386
386
  now=
387
+ hold_enter=
387
388
  limit=
388
389
  tree= # set: a timeout or a signal ends the child's whole process tree (kill_tree), not only the child
389
390
  helper_pid=
@@ -528,17 +529,33 @@ kill_tree() {
528
529
  }
529
530
  # hold <seconds> [jitter]: wait, but Enter continues at once. No raw mode, so there is no terminal state to restore
530
531
  # when the window closes mid-wait: a line reader signals this shell instead. End of input (no terminal) is no Enter.
532
+ # The wait is a poll in tenth-second steps, not one `run sleep`: an Enter that landed before a sleep's pid was on
533
+ # record went unserved for the whole wait (DEF-2176), and dash keeps a trap that lands during its blocking `wait`
534
+ # pending until that wait ends — after a foreground step every shell runs it. The reader also leaves a marker, read
535
+ # by the poll, so even a trap not yet run ends the wait. No child is ended by another process: no job notice leaks.
531
536
  trap 'if [ -n "$holding" ]; then now=1; [ -z "$child" ] || { kill -TERM "$child"; wait "$child"; } 2>/dev/null; fi' USR1
537
+ hold_poll() { # hold_poll SECONDS: whole seconds in tenth-second steps; Enter (now, or the reader's marker) ends it
538
+ steps=$(( ${1%%.*} * 10 )) 2>/dev/null || steps=0
539
+ while [ "$steps" -gt 0 ]; do
540
+ if [ -n "$now" ] || [ -e "$hold_enter" ]; then return 0; fi
541
+ sleep .1
542
+ steps=$((steps - 1))
543
+ done
544
+ }
532
545
  hold() {
533
546
  now=; holding=1
547
+ hold_enter=${TMPDIR:-/tmp}/lanes-hold-$$.enter
548
+ rm -f "$hold_enter" 2>/dev/null || :
534
549
  exec 9<&0
535
- ( IFS= read -r line <&9 && kill -USR1 "$$" ) 2>/dev/null &
550
+ ( IFS= read -r line <&9 && { : >"$hold_enter" 2>/dev/null || :; kill -USR1 "$$"; } ) 2>/dev/null &
536
551
  reader=$!
537
552
  exec 9<&-
538
- run sleep "$1" || :
539
- # Jitter is a second sleep: should its number ever be malformed, the whole wait is still served — no tight loop.
540
- if [ -z "$now" ] && [ -n "${2:-}" ]; then run sleep "$2" || :; fi
553
+ hold_poll "$1"
554
+ # Jitter is a second wait: should its number ever be malformed, the whole wait is still served — no tight loop.
555
+ if [ -z "$now" ] && [ ! -e "$hold_enter" ] && [ -n "${2:-}" ]; then hold_poll "$2"; fi
541
556
  holding=
557
+ rm -f "$hold_enter" 2>/dev/null || :
558
+ hold_enter=
542
559
  kill -TERM "$reader" 2>/dev/null || :
543
560
  wait "$reader" 2>/dev/null || :
544
561
  reader=
@@ -40,6 +40,14 @@ elif [ -n "$look" ]; then
40
40
  device=$(head -n 1 "$config/device")
41
41
  file=$config/ghostty.look.conf
42
42
  fi
43
+ # Linux Ghostty: Omarchy's everyday theme sets colours explicitly, which would override the theme pair. The themed
44
+ # launcher rewrites ghostty.conf with those filtered out of the inherited settings; every way in (key binding, desktop
45
+ # entry, this command) prepares it here, before a variant is derived from it (DEF-2175). Without it, the plain config.
46
+ if [ "$(uname -s)" != Darwin ] && command -v ghostty >/dev/null 2>&1 && command -v python3 >/dev/null 2>&1 \
47
+ && [ -x "$HOME/.local/bin/lanes-themed-window" ]; then
48
+ "$HOME/.local/bin/lanes-themed-window" --prepare-only >/dev/null \
49
+ || echo 'lanes-window: the themed Ghostty config did not validate; opening with the plain one' >&2
50
+ fi
43
51
  if [ -n "$device" ]; then
44
52
  device="$device$look"
45
53
  sed "s|^command = .*|command = $launch $device|" "$config/ghostty.conf" > "$file"
@@ -0,0 +1,13 @@
1
+ {"hooks": {
2
+ "SessionStart": [{"hooks": [{"type": "command", "command": "~/.lz/bin/lane-signal idle --event SessionStart --harness muse", "timeout": 2}]}],
3
+ "UserPromptSubmit": [{"hooks": [{"type": "command", "command": "~/.lz/bin/lane-signal working --event UserPromptSubmit --harness muse", "timeout": 2}]}],
4
+ "PreToolUse": [{"hooks": [{"type": "command", "command": "~/.lz/bin/lane-signal working --event PreToolUse --harness muse", "timeout": 2}]}],
5
+ "PostToolUse": [{"hooks": [{"type": "command", "command": "~/.lz/bin/lane-signal working --event PostToolUse --harness muse", "timeout": 2}]}],
6
+ "PostToolUseFailure": [{"hooks": [{"type": "command", "command": "~/.lz/bin/lane-signal working --event PostToolUseFailure --harness muse", "timeout": 2}]}],
7
+ "PermissionRequest": [{"hooks": [{"type": "command", "command": "~/.lz/bin/lane-signal needs_approval --event PermissionRequest --harness muse", "timeout": 2}]}],
8
+ "Notification": [{"hooks": [{"type": "command", "command": "~/.lz/bin/lane-signal needs_approval --event Notification --harness muse", "timeout": 2}]}],
9
+ "Stop": [{"hooks": [{"type": "command", "command": "~/.lz/bin/lane-signal needs_input --event Stop --harness muse", "timeout": 2}]}],
10
+ "StopFailure": [{"hooks": [{"type": "command", "command": "~/.lz/bin/lane-signal errored --event StopFailure --harness muse", "timeout": 2}]}],
11
+ "Interrupt": [{"hooks": [{"type": "command", "command": "~/.lz/bin/lane-signal needs_input --event Interrupt --harness muse", "timeout": 2, "async": true}]}],
12
+ "SessionEnd": [{"hooks": [{"type": "command", "command": "~/.lz/bin/lane-signal shell --event SessionEnd --harness muse", "timeout": 2}]}]
13
+ }}
@@ -0,0 +1,9 @@
1
+ {"hooks": {
2
+ "SessionStart": [{"hooks": [{"type": "process", "command": "~/.lz/bin/lane-signal", "args": ["idle", "--event", "SessionStart", "--harness", "zcode"], "timeoutMs": 2000}]}],
3
+ "UserPromptSubmit": [{"hooks": [{"type": "process", "command": "~/.lz/bin/lane-signal", "args": ["working", "--event", "UserPromptSubmit", "--harness", "zcode"], "timeoutMs": 2000}]}],
4
+ "PreToolUse": [{"hooks": [{"type": "process", "command": "~/.lz/bin/lane-signal", "args": ["working", "--event", "PreToolUse", "--harness", "zcode"], "timeoutMs": 2000}]}],
5
+ "PostToolUse": [{"hooks": [{"type": "process", "command": "~/.lz/bin/lane-signal", "args": ["working", "--event", "PostToolUse", "--harness", "zcode"], "timeoutMs": 2000}]}],
6
+ "PostToolUseFailure": [{"hooks": [{"type": "process", "command": "~/.lz/bin/lane-signal", "args": ["working", "--event", "PostToolUseFailure", "--harness", "zcode"], "timeoutMs": 2000}]}],
7
+ "PermissionRequest": [{"hooks": [{"type": "process", "command": "~/.lz/bin/lane-signal", "args": ["needs_approval", "--event", "PermissionRequest", "--harness", "zcode"], "timeoutMs": 2000}]}],
8
+ "Stop": [{"hooks": [{"type": "process", "command": "~/.lz/bin/lane-signal", "args": ["needs_input", "--event", "Stop", "--harness", "zcode"], "timeoutMs": 2000}]}]
9
+ }}
@@ -29,13 +29,15 @@ a `holder: tmux` lane through its exact socket and session; any other id is a zm
29
29
  once) and prints {id, …, picked_up_by, expected_within_s} as JSON; the host's lanes-docsd
30
30
  takes it within ~5 s and writes <id>.receipt.json beside it (kind from the extension when
31
31
  --kind is left out; the file must be under /home/coder, at most 32 MiB)
32
- lane mcp install [--agent claude|codex|kimi|grok|grok-cli|all] [--write]
32
+ lane mcp install [--agent claude|codex|kimi|grok|grok-cli|muse|zcode|all] [--write]
33
33
  add the Lanes MCP server (~/.lz/bin/lane-mcp) to ~/.claude.json (mcpServers.lanes) and
34
34
  ~/.codex/config.toml ([mcp_servers.lanes], with env_vars passing LANE_ID, LANE_WS, TMUX
35
35
  and TMUX_PANE: Codex clears a server's environment), and — where that harness's home is
36
36
  a directory here, or when named — ~/.kimi-code/mcp.json (mcpServers.lanes),
37
37
  ~/.grok-build/config.toml (Grok Build's GROK_HOME: a `# >>> lanes mcp >>>` block with
38
- [mcp_servers.lanes]) and ~/.grok/user-settings.json (grok-cli: mcp.servers id `lanes`);
38
+ [mcp_servers.lanes]), ~/.grok/user-settings.json (grok-cli: mcp.servers id `lanes`),
39
+ ~/.config/muse/settings.json (Muse Code: mcp_servers.lanes, mode optional, env passing
40
+ the lane identity) and ~/.zcode/cli/config.json (ZCode: mcp.servers.lanes);
39
41
  `all` is that default set, a named harness is written even where its home is
40
42
  missing: prints the diff; --write applies
41
43
  it, keeping <file>.bak-<UTC> first; an entry that is already there is left alone (a
@@ -532,7 +534,7 @@ def cmd_gc(args):
532
534
  print(f'lane gc --sockets: {removed} unlinked, {kept} kept in {directory}')
533
535
 
534
536
 
535
- MCP_USAGE = 'usage: lane mcp install [--agent claude|codex|kimi|grok|grok-cli|all] [--write]'
537
+ MCP_USAGE = 'usage: lane mcp install [--agent claude|codex|kimi|grok|grok-cli|muse|zcode|all] [--write]'
536
538
 
537
539
 
538
540
  def cmd_mcp(args):
@@ -581,7 +583,8 @@ def cmd_mcp(args):
581
583
  print(f"backup {result['backup']}")
582
584
  print(f"wrote {result['path']}")
583
585
  if added:
584
- print('new Claude Code and Codex sessions load it, and new Kimi Code, Grok Build and grok-cli ones where '
586
+ print('new Claude Code and Codex sessions load it, and new Kimi Code, Grok Build, grok-cli, Muse Code and '
587
+ 'ZCode ones where '
585
588
  'their entry was written (a running session keeps its tools)')
586
589
  sys.exit(1 if refused else 0)
587
590
 
@@ -3,7 +3,8 @@
3
3
  Installed at /home/coder/.lz/bin/lane-mcp by `lanes install-container`; Claude Code and Codex start it from
4
4
  `mcpServers.lanes` in ~/.claude.json and `[mcp_servers.lanes]` in ~/.codex/config.toml, Kimi Code from
5
5
  ~/.kimi-code/mcp.json, Grok Build from `[mcp_servers.lanes]` in $GROK_HOME/config.toml and grok-cli from `mcp.servers`
6
- in ~/.grok/user-settings.json (`lane mcp install`, W12).
6
+ in ~/.grok/user-settings.json (`lane mcp install`, W12), Muse Code from `mcp_servers.lanes` in ~/.config/muse/settings.json
7
+ and ZCode from `mcp.servers.lanes` in ~/.zcode/cli/config.json (W13).
7
8
 
8
9
  usage: lane-mcp (speaks MCP on stdin/stdout; nothing else to run by hand)
9
10
 
@@ -80,11 +81,14 @@ SYNC_PICKUP = 'lanes sync'
80
81
  STATUS_PICKUP = 'lanes-statusd'
81
82
  # A publish request: lanes-docsd's own pick-up cycle (5 s, plan W10 W4) takes it and writes the receipt back.
82
83
  PUBLISH_PICKUP, PUBLISH_LATENCY_S = C.PUBLISH_PICKUP, C.PUBLISH_LATENCY_S
83
- SESSION_VARS = ('CLAUDE_CODE_SESSION_ID', 'CODEX_THREAD_ID', 'GROK_SESSION_ID') # the agent's session id, when known
84
+ SESSION_VARS = ('CLAUDE_CODE_SESSION_ID', 'CODEX_THREAD_ID', 'GROK_SESSION_ID', 'MUSE_SESSION_ID',
85
+ 'ZCODE_SESSION_ID') # the agent's session id, when known
84
86
  # The harness of the client that started this server (W12, DM-LANES-LANE-MCP), from its `clientInfo.name` (MEASURED:
85
87
  # Claude Code `claude-code`, Codex `codex-mcp-client`, Grok Build `grok-shell-<server>`, grok-cli `grok-cli-<id>`;
86
- # Kimi Code's names `kimi`), else the agent process above this one (lane_core.parent_harness), else `unknown`.
87
- HARNESS_FILTERS = ('claude', 'codex', 'kimi', 'grok', 'grok-cli') # lanes_list `agent`
88
+ # Kimi Code's names `kimi`; W13: Muse Code `tbh` MEASURED 2026-10-16 on 1.4.10, ZCode `zcode…` INFERRED from its
89
+ # source), else the agent process above this one (lane_core.parent_harness), else `unknown`.
90
+ HARNESS_FILTERS = ('claude', 'codex', 'kimi', 'grok', 'grok-cli', 'muse', 'zcode') # lanes_list `agent`
91
+ MUSE_CLIENT = 'tbh' # Muse Code's MCP clientInfo.name (MEASURED, muse 1.4.10: {"name": "tbh", "version": "0.1.0"})
88
92
 
89
93
 
90
94
  def client_harness(client):
@@ -100,6 +104,10 @@ def client_harness(client):
100
104
  return 'grok'
101
105
  if name.startswith('grok-cli'):
102
106
  return 'grok-cli'
107
+ if name == MUSE_CLIENT:
108
+ return 'muse'
109
+ if name.startswith('zcode'):
110
+ return 'zcode'
103
111
  return None
104
112
  # How long a lanes_signal report holds against the agent's own hooks (lane-signal's EXPLICIT rule, DEF-1801).
105
113
  SIGNAL_STANDS = ('your next tool call, a new prompt, a permission prompt, a failed turn or the session\'s end; the end '
@@ -167,7 +175,7 @@ TOOLS = (
167
175
  'needs_input — the boards\' "Needs you" section).'},
168
176
  'agent': {'type': 'string', 'enum': ['any', 'none', *HARNESS_FILTERS],
169
177
  'description': 'any: lanes with an agent; none: lanes without one; claude / codex / kimi / grok '
170
- '(Grok Build) / grok-cli: that harness.'}}),
178
+ '(Grok Build) / grok-cli / muse (Muse Code) / zcode (ZCode): that harness.'}}),
171
179
  _tool('lanes_status', 'Show one lane',
172
180
  'One lane in full: the registry facts (board, display name, kind, tmux socket and session, the declaring '
173
181
  'terminals.json and repository), the host\'s observation (state, agent, attached clients, viewers, last '