@vibelord/lanes 1.5.0 → 1.6.0
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 +1 -1
- package/payload/lanes/README.md +1 -1
- package/payload/lanes/VERSION +1 -1
- package/payload/lanes/bin/lanes +111 -25
- package/payload/lanes/client/ghostty/common.conf +4 -0
- package/payload/lanes/client/ghostty/lanes-themed-window +73 -0
- package/payload/lanes/client/ghostty/omarchy.conf +4 -2
- package/payload/lanes/container/hooks/muse-hooks.json +13 -0
- package/payload/lanes/container/hooks/zcode-hooks.json +9 -0
- package/payload/lanes/container/lane +7 -4
- package/payload/lanes/container/lane-mcp +13 -5
- package/payload/lanes/container/lane-signal +88 -21
- package/payload/lanes/container/lane_core.py +143 -19
- package/payload/lanes/container/throttle-pulse +9 -6
- package/payload/lanes/docs/ARCHITECTURE.md +58 -11
- package/payload/lanes/docs/INSTALL.md +10 -6
- package/payload/lanes/docs/RUNBOOK.md +125 -16
- package/payload/lanes/docs/USERGUIDE.md +77 -28
- package/payload/lanes/install.sh +2 -1
- package/payload/lanes/libexec/agent_state.py +90 -14
- package/payload/lanes/libexec/attach.py +181 -0
- package/payload/lanes/libexec/harness_muse.py +531 -0
- package/payload/lanes/libexec/lanes_lib.py +3 -2
- package/payload/lanes/libexec/sidebar.py +9 -6
- package/payload/lanes/libexec/statusd.py +16 -6
- package/payload/manifest.json +4 -3
package/package.json
CHANGED
package/payload/lanes/README.md
CHANGED
|
@@ -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
|
|
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
|
package/payload/lanes/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.
|
|
1
|
+
1.6.0
|
package/payload/lanes/bin/lanes
CHANGED
|
@@ -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
|
-
|
|
93
|
-
container (else `skipped
|
|
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
|
|
203
|
-
~/.kimi-code/mcp.json, ~/.grok-build/config.toml (a `# >>> lanes mcp >>>`
|
|
204
|
-
~/.grok/user-settings.json (mcp.servers id lanes),
|
|
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
|
|
228
|
-
|
|
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
|
|
@@ -1127,8 +1133,8 @@ def cmd_attach(args):
|
|
|
1127
1133
|
# Record the base and bring the owner (W8 A2) in line, also for sessions that existed before either was known.
|
|
1128
1134
|
record_base(device, args)
|
|
1129
1135
|
record_look(device, args)
|
|
1130
|
-
# Views idle until a real client (with a real size) has attached once. We exec into the
|
|
1131
|
-
#
|
|
1136
|
+
# Views idle until a real client (with a real size) has attached once. We exec into the display
|
|
1137
|
+
# supervisor below, so a detached helper raises the flag shortly after its client has sized the session.
|
|
1132
1138
|
ready = L.STATE / 'clients' / f'{device}.ready'
|
|
1133
1139
|
if not ready.exists() and os.fork() == 0:
|
|
1134
1140
|
os.setsid()
|
|
@@ -1163,7 +1169,11 @@ def cmd_attach(args):
|
|
|
1163
1169
|
'COLORTERM': os.environ.get('COLORTERM', 'truecolor')})
|
|
1164
1170
|
if L.run_bounded(['infocmp', env['TERM']], 5)[0] != 0: # unknown here — or no answer: the safe default
|
|
1165
1171
|
env['TERM'] = 'xterm-256color'
|
|
1166
|
-
|
|
1172
|
+
# Own the terminal backlog as well as SSH's transport. A slow Zellij 0.45.1
|
|
1173
|
+
# client can stay alive after its server has dropped it (Exit is behind the
|
|
1174
|
+
# queued frames); keepalives alone cannot make lanes-launch reconnect it.
|
|
1175
|
+
os.execve(sys.executable, [sys.executable, str(L.REPO / 'libexec/attach.py'),
|
|
1176
|
+
*ZELLIJ_BASE, 'attach', device], env)
|
|
1167
1177
|
|
|
1168
1178
|
|
|
1169
1179
|
def cmd_open(args):
|
|
@@ -1320,7 +1330,7 @@ def cmd_install_container(args):
|
|
|
1320
1330
|
# user (0700), never here.
|
|
1321
1331
|
install_file(L.REPO / 'container/vimrc', root / 'vimrc')
|
|
1322
1332
|
# The hook DEFINITIONS travel with the contract for reference; no hook is installed here — that is the opt-in,
|
|
1323
|
-
# 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).
|
|
1324
1334
|
for source in sorted([*(L.REPO / 'container/hooks').glob('*.json'), *(L.REPO / 'container/hooks').glob('*.toml')]):
|
|
1325
1335
|
install_file(source, root / 'hooks' / source.name)
|
|
1326
1336
|
env_file = L.REPO / 'container/env.sh' # TMPDIR off the RAM-backed /tmp; only NEW shells and agents read it
|
|
@@ -1458,13 +1468,16 @@ HOOK_TARGETS = {'claude': ('claude-settings.json', '.claude/settings.json'),
|
|
|
1458
1468
|
'codex': ('codex-hooks.json', '.codex/hooks.json'),
|
|
1459
1469
|
'kimi': ('kimi-hooks.toml', '.kimi-code/config.toml'),
|
|
1460
1470
|
'grok': ('grok-hooks.json', '.grok-build/hooks/lanes.json'),
|
|
1461
|
-
'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')}
|
|
1462
1474
|
# The other file an install changes: Codex's top-level notify; Grok Build's config.toml blocks (W12).
|
|
1463
1475
|
HOOK_EXTRA = {'codex': ('.codex/config.toml',), 'grok': ('.grok-build/config.toml',)}
|
|
1464
1476
|
# W12: a harness whose home is not a directory in the container is not installed there; `lanes onboard` skips it.
|
|
1465
|
-
HOOK_HOMES = {'kimi': '.kimi-code', 'grok': '.grok-build', 'grok-cli': '.grok'
|
|
1466
|
-
|
|
1467
|
-
|
|
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]')
|
|
1468
1481
|
|
|
1469
1482
|
|
|
1470
1483
|
def hook_files(agent):
|
|
@@ -1730,9 +1743,79 @@ def grok_cli_settings(text):
|
|
|
1730
1743
|
return after
|
|
1731
1744
|
|
|
1732
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
|
+
|
|
1733
1814
|
HOOK_EDITORS = {'kimi': (('.kimi-code/config.toml', kimi_config),),
|
|
1734
1815
|
'grok': (('.grok-build/hooks/lanes.json', grok_hooks_file), ('.grok-build/config.toml', grok_config)),
|
|
1735
|
-
'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),)}
|
|
1736
1819
|
|
|
1737
1820
|
|
|
1738
1821
|
def install_edits(home, edits, write):
|
|
@@ -2744,7 +2827,8 @@ def cmd_workspaces(args):
|
|
|
2744
2827
|
ONBOARD_USAGE = ('usage: lanes onboard <login> --container <name> [--workspace <id> --repo <path> [--title T]] '
|
|
2745
2828
|
'[--write] [--no-mcp] [--step kimi-notify|grok-build]...')
|
|
2746
2829
|
ONBOARD_OPTIONAL = ('kimi-notify', 'grok-build') # steps that run only when named with --step (W12)
|
|
2747
|
-
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'}
|
|
2748
2832
|
# The receipt's status vocabulary. A dry run reports planned, unchanged, skipped, manual or failed, and `ok` for the
|
|
2749
2833
|
# read-only `inspect` of the container; `manual` names a step a person still has to take (printed with the exact command);
|
|
2750
2834
|
# `warn` is a step that ran and reported a problem that does not stop onboarding (the doctor's verdict, a board with
|
|
@@ -3225,6 +3309,7 @@ def cmd_onboard(args):
|
|
|
3225
3309
|
('lane-mcp', mcp_step, False), ('hooks-claude', hooks_step('claude'), False),
|
|
3226
3310
|
('hooks-codex', hooks_step('codex'), False), ('hooks-kimi', hooks_step('kimi'), False),
|
|
3227
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),
|
|
3228
3313
|
('throttle', lambda: provision_throttle(container, site, write), False),
|
|
3229
3314
|
('sync', sync_step, False), ('cursor-sync', cursor_step, False), ('resync', resync_step, False),
|
|
3230
3315
|
('doctor', doctor_step, False)]
|
|
@@ -4795,10 +4880,10 @@ def docs_doctor(home=None, probe=None):
|
|
|
4795
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
|
|
4796
4881
|
# two never disagree about a healthy pulse.
|
|
4797
4882
|
PULSE_FRESH_S = 90
|
|
4798
|
-
# The pulse's pools in its order (statusd.PULSE_POOLS, the throttle-pulse POOLS; W12 added kimi
|
|
4799
|
-
# `throttle pools:` line. A pool the pulse did not report says `?`: a pulse from before 1.5.0
|
|
4800
|
-
#
|
|
4801
|
-
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')
|
|
4802
4887
|
FLEET_FRESH_S = 60 # statusd republishes `fleet` every FLEET_INTERVAL (10 s) unless the host is critical
|
|
4803
4888
|
THROTTLE_MONITOR = 'tools--throttle-monitor'
|
|
4804
4889
|
FLEET_DASHBOARD_KEY = 'LANES_FLEET_DASHBOARD_URL'
|
|
@@ -4969,11 +5054,12 @@ def fleet_doctor(status, registry, home=None, now=None):
|
|
|
4969
5054
|
# value of the spec's enum, `unknown` (and any value outside the enum, such as an empty harness from a statusd before
|
|
4970
5055
|
# W12) only when some lane has it. A row without `agent` (no signal names an agent there) is not counted. Information
|
|
4971
5056
|
# only, never a ✗.
|
|
4972
|
-
HARNESSES = ('claude', 'codex', 'kimi', 'grok', 'grok-cli', 'shell')
|
|
5057
|
+
HARNESSES = ('claude', 'codex', 'kimi', 'grok', 'grok-cli', 'muse', 'zcode', 'shell') # W13: + muse, zcode
|
|
4973
5058
|
|
|
4974
5059
|
|
|
4975
5060
|
def harness_line(status):
|
|
4976
|
-
"""`lanes: claude 50 · codex 20 · kimi 3 · grok 0 · grok-cli 0 · shell 0` from status.json's
|
|
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`
|
|
4977
5063
|
counts an agent{} that names it, which statusd's merge never writes (a pane whose harness ended is published under
|
|
4978
5064
|
that harness's name with status `shell`, DM-LANES-STATUS): the slot reads 0 unless a foreign sidecar says so."""
|
|
4979
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,73 @@
|
|
|
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
|
+
lines = [line for line in target.read_text().splitlines() if key_of(line) != "config-file"]
|
|
50
|
+
lines.extend((f"config-file = {base}",))
|
|
51
|
+
appearance = lanes / "appearance.ghostty"
|
|
52
|
+
if appearance.is_file():
|
|
53
|
+
lines.append(f"config-file = {appearance}")
|
|
54
|
+
rendered = "\n".join(lines) + "\n"
|
|
55
|
+
candidate = lanes / f".ghostty-appearance.{os.getpid()}.conf"
|
|
56
|
+
candidate.write_text(rendered)
|
|
57
|
+
try:
|
|
58
|
+
ghostty = subprocess.run(["ghostty", "+validate-config", f"--config-file={candidate}"], check=False)
|
|
59
|
+
if ghostty.returncode:
|
|
60
|
+
raise SystemExit(ghostty.returncode)
|
|
61
|
+
target.write_text(rendered)
|
|
62
|
+
finally:
|
|
63
|
+
candidate.unlink(missing_ok=True)
|
|
64
|
+
|
|
65
|
+
if sys.argv[1:] == ["--prepare-only"]:
|
|
66
|
+
print(target)
|
|
67
|
+
return
|
|
68
|
+
launcher = home / ".local" / "bin" / "lanes-window"
|
|
69
|
+
os.execv(str(launcher), [str(launcher), *sys.argv[1:]])
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
if __name__ == "__main__":
|
|
73
|
+
main()
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# Omarchy / Hyprland specifics for the lanes window (Ghostty GTK, dedicated instance).
|
|
2
|
-
#
|
|
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),
|
|
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
|
|
|
@@ -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])
|
|
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
|
|
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'
|
|
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
|
|
87
|
-
|
|
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 '
|