@vibelord/lanes 1.5.1 → 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.
@@ -49,6 +49,13 @@ then, after the turn's `agent.message.appended` dump, `agent.turn.ended{outcome:
49
49
  `interaction.request{kind: approval}` answered by `interaction.resolved` of the same id (a `question` is the other kind,
50
50
  and a cancelled turn can leave either unanswered); state.json drops `lastTurnReason` when a turn starts and sets it
51
51
  (`completed|failed|cancelled`) when one ends.
52
+
53
+ W13 (spec `docs/superpowers/specs/2026-10-16-lanes-harness-observers-muse-zcode-design.md`): Muse Code and ZCode are
54
+ observed harnesses too (HARNESS_COMMS, MUSE_COMM_PREFIX). Muse Code's own files are read by libexec/harness_muse.py
55
+ through `muse_sessions` (its session log, joined to the pane's Muse pid by the `.session.lock` the process holds open);
56
+ its spinner title `⠙ <workspace>` was published `codex / working` before W13, because the walk did not know Muse and
57
+ the Codex title rule reads a spinner on an `unknown` pane (DEF-2030). ZCode keeps its sessions in SQLite and draws no
58
+ title and no bell: it is observed by its process (`zcode` / `idle`) and its hooks only.
52
59
  """
53
60
  import importlib.util
54
61
  import json
@@ -75,8 +82,15 @@ DWELL = {'needs': 8, 'working': 5, 'done': 5, None: 0}
75
82
  GONE_DWELL = 5
76
83
 
77
84
  # W12 harness identity (spec §2). The comm (/proc/<pid>/comm, at most 15 bytes) of each harness, and what it is called.
78
- HARNESS_COMMS = {'claude': 'claude', 'codex': 'codex', 'kimi': 'kimi', 'kimi-code': 'kimi', 'grok': 'grok'}
79
- AGENT_HARNESSES = ('claude', 'codex', 'kimi', 'grok', 'grok-cli')
85
+ # W13 (spec 2026-10-16 §2): ZCode's CLI sets process.title `zcode-cli` (its comm); Muse Code's launcher is a script named
86
+ # `muse` that execs `muse-bin-<version>` (MEASURED 2026-10-16, muse 1.4.10: comm `muse-bin-1.4.10`, the 15-byte cut of
87
+ # the file `muse-bin-1.4.10-R6427.1`), so Muse is also a comm that starts with MUSE_COMM_PREFIX, or an executable whose
88
+ # file name does (MUSE_EXE_PREFIX). lane-signal's and lane_core's process_harness apply the same rule (pinned equal by
89
+ # test_agent_state_harness.ThreeWalksAgree).
90
+ HARNESS_COMMS = {'claude': 'claude', 'codex': 'codex', 'kimi': 'kimi', 'kimi-code': 'kimi', 'grok': 'grok',
91
+ 'muse': 'muse', 'zcode-cli': 'zcode'}
92
+ MUSE_COMM_PREFIX = MUSE_EXE_PREFIX = 'muse-bin-'
93
+ AGENT_HARNESSES = ('claude', 'codex', 'kimi', 'grok', 'grok-cli', 'muse', 'zcode')
80
94
  HARNESSES = AGENT_HARNESSES + ('shell', 'unknown') # the published `agent.harness` enum (DM-LANES-STATUS)
81
95
  HARNESS_DEPTH = 4 # levels below the pane's root process walked for a harness
82
96
  HARNESS_FANOUT = 64 # processes examined per pane at most: a fork bomb under a pane costs a bounded read
@@ -220,15 +234,22 @@ def _named(proc_root, pid, comm):
220
234
 
221
235
 
222
236
  def _harness_of(proc_root, pid, comm):
223
- """The harness process `pid` (whose comm is `comm`) is, or None: a comm in HARNESS_COMMS (`_named`), else — for a
224
- process that is not a shell — native Claude Code by its executable (CLAUDE_EXE, DEF-1993). A shell's exe is its
237
+ """The harness process `pid` (whose comm is `comm`) is, or None: a comm in HARNESS_COMMS (`_named`), a comm that
238
+ starts with MUSE_COMM_PREFIX (Muse Code, W13), else — for a process that is not a shell — by its executable: native
239
+ Claude Code (CLAUDE_EXE, DEF-1993) or a Muse binary (a file name starting with MUSE_EXE_PREFIX). A shell's exe is its
225
240
  shell, so shells cost no readlink."""
226
241
  if comm in HARNESS_COMMS:
227
242
  return _named(proc_root, pid, comm)
228
243
  if comm is None or comm.lstrip('-') in SHELL_COMMANDS:
229
244
  return None
245
+ if comm.startswith(MUSE_COMM_PREFIX):
246
+ return 'muse'
230
247
  exe = _exe(proc_root, pid)
231
- return 'claude' if exe is not None and CLAUDE_EXE in exe else None
248
+ if exe is None:
249
+ return None
250
+ if CLAUDE_EXE in exe:
251
+ return 'claude'
252
+ return 'muse' if exe.rpartition('/')[2].startswith(MUSE_EXE_PREFIX) else None
232
253
 
233
254
 
234
255
  def harness_walk(proc_root, pid, kids=None):
@@ -776,10 +797,60 @@ def grok_title(pane_row, bell, since):
776
797
  'pid': max(pid, 0), 'since': since, 'source': 'grok-title'}
777
798
 
778
799
 
779
- def merge(pane_row, claude_by_pane, sidecar, kimi_by_pane=None, grok_by_pane=None):
780
- """Precedence: hook sidecar (event-driven, every harness) > Claude session file > Kimi session file > Grok observer
781
- (`grok_sessions`: its session file, else the title with its blink memory) > the Grok title (`grok_title`, a pane
782
- observed as `grok` nothing above claimed) > Codex title > the observed harness itself > the bell.
800
+ def _harness_muse():
801
+ """libexec/harness_muse.py (W13) when it ships, else None: looked up like `_harness_grok`."""
802
+ module = sys.modules.get('harness_muse')
803
+ if module is None:
804
+ if importlib.util.find_spec('harness_muse') is None:
805
+ return None
806
+ import harness_muse as module
807
+ return module
808
+
809
+
810
+ def muse_sessions(root, panes, *, home=CONTAINER_HOME, now=None):
811
+ """The Muse Code observer (W13): `harness_muse.muse_sessions(root, panes, home=home, now=now)` -> {pane key:
812
+ verdict}, or {} when the module does not ship. `root` the container's root as the host sees it, `panes` {pane key:
813
+ the pane's Muse pid} (statusd: the panes whose walk found `muse`). Kept: verdicts whose status is a vocabulary word."""
814
+ if not panes:
815
+ return {}
816
+ module = _harness_muse()
817
+ if module is None:
818
+ return {}
819
+ got = module.muse_sessions(root, panes, home=home, now=now)
820
+ if not isinstance(got, dict):
821
+ return {}
822
+ return {key: dict(verdict) for key, verdict in got.items()
823
+ if isinstance(verdict, dict) and verdict.get('status') in STATUSES}
824
+
825
+
826
+ def muse_title(pane_row, since):
827
+ """merge's Muse title rung for a pane observed running Muse Code that no observer claimed:
828
+ `harness_muse.parse_muse_title` of its title (`⠙ <workspace>` working, any other title idle), shaped as a title
829
+ verdict (`source: muse-title`), or None. Muse rings no bell (MEASURED 2026-10-16), so the bell never makes it done."""
830
+ module = _harness_muse()
831
+ parse = getattr(module, 'parse_muse_title', None) if module is not None else None
832
+ parsed = parse(pane_row.get('pane_title')) if parse is not None else None
833
+ if not isinstance(parsed, dict) or parsed.get('status') not in STATUSES:
834
+ return None
835
+ try:
836
+ pid = int(pane_row.get('harness_pid') or 0)
837
+ except (TypeError, ValueError):
838
+ pid = 0
839
+ return {'harness': 'muse', 'status': parsed['status'], 'name': str(parsed.get('name') or ''), 'session_id': '',
840
+ 'pid': max(pid, 0), 'since': since, 'source': 'muse-title'}
841
+
842
+
843
+ # Harnesses that ring no bell (MEASURED 2026-10-16: Muse Code 1.4.10 none; ZCode has no BEL path in its source): a bell
844
+ # flag on their pane came from something else (a tool's output), so it never turns their verdict into `done`.
845
+ BELL_LESS = ('muse', 'zcode')
846
+
847
+
848
+ def merge(pane_row, claude_by_pane, sidecar, kimi_by_pane=None, grok_by_pane=None, muse_by_pane=None):
849
+ """Precedence: hook sidecar (event-driven, every harness) > Claude session file > Kimi session file > Muse session
850
+ file (`muse_sessions`, W13) > Grok observer (`grok_sessions`: its session file, else the title with its blink
851
+ memory) > the Grok title (`grok_title`, a pane observed as `grok` nothing above claimed) > the Muse title
852
+ (`muse_title`, a pane observed as `muse`) > Codex title > the observed harness itself > the bell (never for a
853
+ BELL_LESS harness).
783
854
  Any Codex title — idle, spinner or Action Required, bell or not — over a bare-shell pane whose shell has no
784
855
  child process (`pane_has_child is False`, statusd's row key) publishes `shell`: the harness ended and tmux kept
785
856
  its last title, so no ghost idle/done/working/needs_approval. Unknown liveness leaves the title in charge.
@@ -794,22 +865,23 @@ def merge(pane_row, claude_by_pane, sidecar, kimi_by_pane=None, grok_by_pane=Non
794
865
  nothing above claimed is that harness, `idle`, `done` with the bell (`source: process`, `pid` the harness pid statusd
795
866
  found). Returns None when nothing says an agent is here — the sidebar then draws a plain shell."""
796
867
  observed = pane_row.get('harness')
797
- verdict = _verdict(pane_row, claude_by_pane, sidecar, kimi_by_pane or {}, grok_by_pane or {}, observed)
868
+ verdict = _verdict(pane_row, claude_by_pane, sidecar, kimi_by_pane or {}, grok_by_pane or {}, muse_by_pane or {},
869
+ observed)
798
870
  if verdict is not None and observed in AGENT_HARNESSES:
799
871
  verdict['harness'] = observed
800
872
  return verdict
801
873
 
802
874
 
803
- def _verdict(pane_row, claude_by_pane, sidecar, kimi_by_pane, grok_by_pane, observed):
875
+ def _verdict(pane_row, claude_by_pane, sidecar, kimi_by_pane, grok_by_pane, muse_by_pane, observed):
804
876
  if sidecar and sidecar.get('status') in STATUSES:
805
877
  return dict(sidecar, source=sidecar.get('source') or 'hook')
806
878
  key = pane_key(pane_row)
807
879
  if key in claude_by_pane:
808
880
  return dict(claude_by_pane[key])
809
- bell = pane_row.get('window_bell_flag') == '1'
881
+ bell = pane_row.get('window_bell_flag') == '1' and observed not in BELL_LESS
810
882
  # The Kimi observer leaves the bell to merge; the Grok observer applies it itself (harness_grok: only a completed
811
- # turn or an idle title is `done` — a cancelled or interrupted turn stays needs_input, spec §4).
812
- for index, rings in ((kimi_by_pane, True), (grok_by_pane, False)):
883
+ # turn or an idle title is `done` — a cancelled or interrupted turn stays needs_input, spec §4); Muse rings none.
884
+ for index, rings in ((kimi_by_pane, True), (muse_by_pane, False), (grok_by_pane, False)):
813
885
  found = index.get(key)
814
886
  if isinstance(found, dict) and found.get('status') == GROK_UNKNOWN:
815
887
  return None # seen, not joinable (Grok's leader mode): no guess below
@@ -826,6 +898,10 @@ def _verdict(pane_row, claude_by_pane, sidecar, kimi_by_pane, grok_by_pane, obse
826
898
  found = grok_title(pane_row, bell, activity)
827
899
  if found:
828
900
  return found
901
+ if observed == 'muse':
902
+ found = muse_title(pane_row, activity)
903
+ if found:
904
+ return found
829
905
  title = parse_codex_title(pane_row.get('pane_title'), observed)
830
906
  if title:
831
907
  if _harness_ended(pane_row):