@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 +1 -1
- package/payload/lanes/README.md +1 -1
- package/payload/lanes/VERSION +1 -1
- package/payload/lanes/bin/lanes +104 -22
- package/payload/lanes/client/ghostty/common.conf +4 -0
- package/payload/lanes/client/ghostty/lanes-themed-window +83 -0
- package/payload/lanes/client/ghostty/omarchy.conf +4 -2
- package/payload/lanes/client/lanes-launch +21 -4
- package/payload/lanes/client/lanes-window +8 -0
- 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 +41 -11
- package/payload/lanes/docs/INSTALL.md +12 -6
- package/payload/lanes/docs/RUNBOOK.md +115 -15
- package/payload/lanes/docs/USERGUIDE.md +77 -28
- package/payload/lanes/install.sh +7 -1
- package/payload/lanes/libexec/agent_state.py +90 -14
- 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.1
|
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
|
|
@@ -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
|
-
|
|
1471
|
-
|
|
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
|
|
4803
|
-
# `throttle pools:` line. A pool the pulse did not report says `?`: a pulse from before 1.5.0
|
|
4804
|
-
#
|
|
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
|
|
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
|
-
#
|
|
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
|
|
|
@@ -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
|
-
|
|
539
|
-
# Jitter is a second
|
|
540
|
-
if [ -z "$now" ] && [ -n "${2:-}" ]; then
|
|
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])
|
|
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 '
|