@vibelord/lanes 1.2.0 → 1.5.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 +7 -5
- package/payload/lanes/VERSION +1 -1
- package/payload/lanes/bin/lanes +1091 -76
- package/payload/lanes/container/hooks/grok-cli-hooks.json +8 -0
- package/payload/lanes/container/hooks/grok-hooks.json +12 -0
- package/payload/lanes/container/hooks/kimi-hooks.toml +49 -0
- package/payload/lanes/container/lane +55 -8
- package/payload/lanes/container/lane-mcp +120 -22
- package/payload/lanes/container/lane-signal +86 -23
- package/payload/lanes/container/lane_core.py +524 -36
- package/payload/lanes/container/throttle-pulse +456 -0
- package/payload/lanes/docs/ARCHITECTURE.md +357 -34
- package/payload/lanes/docs/DESIGN.md +13 -3
- package/payload/lanes/docs/INSTALL.md +47 -10
- package/payload/lanes/docs/RUNBOOK.md +761 -12
- package/payload/lanes/docs/USERGUIDE.md +421 -46
- package/payload/lanes/holders.lock +5 -0
- package/payload/lanes/install/enable-docs.sh +78 -0
- package/payload/lanes/install/host-install.sh +39 -5
- package/payload/lanes/libexec/agent_state.py +604 -12
- package/payload/lanes/libexec/docs_store.py +995 -0
- package/payload/lanes/libexec/docsd.py +2010 -0
- package/payload/lanes/libexec/economy_poll.py +6 -2
- package/payload/lanes/libexec/harness_grok.py +742 -0
- package/payload/lanes/libexec/lanes_lib.py +143 -20
- package/payload/lanes/libexec/mdlite.py +575 -0
- package/payload/lanes/libexec/members.py +5 -0
- package/payload/lanes/libexec/notify.py +65 -5
- package/payload/lanes/libexec/sidebar.py +626 -25
- package/payload/lanes/libexec/statusd.py +530 -32
- package/payload/lanes/libexec/tokens.py +42 -9
- package/payload/lanes/libexec/update.py +171 -26
- package/payload/lanes/libexec/uploads.py +538 -0
- package/payload/lanes/libexec/users.py +31 -3
- package/payload/lanes/site/site.example.conf +4 -0
- package/payload/lanes/systemd/lanes-docsd.service +29 -0
- package/payload/lanes/web/Caddyfile +24 -1
- package/payload/lanes/web/README.md +86 -8
- package/payload/lanes/web/browser.js +2 -4
- package/payload/lanes/web/docs.css +52 -0
- package/payload/lanes/web/docs.js +36 -0
- package/payload/lanes/web/mobile.js +3 -2
- package/payload/lanes/web/upload.css +17 -0
- package/payload/lanes/web/upload.js +84 -0
- package/payload/manifest.json +5 -3
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""lane-signal <status> [--event NAME] [--harness claude|codex] [--note TEXT]
|
|
3
|
-
target of Claude Code
|
|
4
|
-
|
|
5
|
-
pid>/root and publishes as the
|
|
2
|
+
"""lane-signal <status> [--event NAME] [--harness claude|codex|kimi|grok|grok-cli|unknown] [--note TEXT]
|
|
3
|
+
[--agent-pid PID] [PAYLOAD] — the hook target of Claude Code, Codex, Kimi Code, Grok Build and grok-cli inside a
|
|
4
|
+
workspace container, and the writer behind lane-mcp's `lanes_signal`. Writes ~/.lz/state/hooks/<lane id>.json
|
|
5
|
+
(atomic replace), which the host's lanes-statusd reads through /proc/<container init pid>/root and publishes as the
|
|
6
|
+
lane's `agent{}` (source `hook`).
|
|
6
7
|
|
|
7
8
|
The sidecar has its own directory because `lane-run`, the root process of every zmx-held lane, writes its
|
|
8
9
|
`{phase, exit, at, pid}` record to ~/.lz/state/<zmx name>.json and exports LANE_ID=<zmx name> to the agent: a
|
|
9
10
|
sidecar at state/<lane id>.json would overwrite that record for the agent's whole life (DEF-1572). Nothing but
|
|
10
11
|
this script writes state/hooks/, and lane-run never writes there.
|
|
11
12
|
|
|
12
|
-
{"v":1, "lane":"<id>", "harness":"claude|codex", "status":"<agent{} vocabulary>",
|
|
13
|
-
began>, "at":<epoch of this event>, "event":"<hook event>", "pid":<agent pid>,
|
|
14
|
-
"session":"<session/thread id or ''>", "name":"<the agent's own note>" (only when
|
|
13
|
+
{"v":1, "lane":"<id>", "harness":"claude|codex|kimi|grok|grok-cli|unknown", "status":"<agent{} vocabulary>",
|
|
14
|
+
"since":<epoch the status began>, "at":<epoch of this event>, "event":"<hook event>", "pid":<agent pid>,
|
|
15
|
+
"start":"<its starttime>", "session":"<session/thread id or ''>", "name":"<the agent's own note>" (only when
|
|
16
|
+
there is one)}
|
|
15
17
|
|
|
16
18
|
It never fails the agent and never speaks: every path exits 0 and nothing reaches stdout or stderr (a Claude Code
|
|
17
19
|
UserPromptSubmit/SessionStart hook's stdout becomes model context). An unresolvable lane, a status outside the
|
|
@@ -28,10 +30,17 @@ Agent pid: --agent-pid when that process exists (lane-mcp passes its own parent,
|
|
|
28
30
|
default rule below would name lane-mcp itself); else the parent process, or that parent's parent when the parent is
|
|
29
31
|
a shell (Claude Code runs a hook command through a shell, which may fork it rather than exec it; a pid that dies
|
|
30
32
|
with the shell would read as a dead agent).
|
|
31
|
-
Session id: `session_id` of the hook's JSON on stdin, `thread-id` of a JSON
|
|
32
|
-
its JSON as the last argument), else $CLAUDE_CODE_SESSION_ID / $CODEX_THREAD_ID
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
Session id: `session_id` of the hook's JSON on stdin (Grok Build's camel-case `sessionId` too), `thread-id` of a JSON
|
|
34
|
+
PAYLOAD argument (Codex `notify` passes its JSON as the last argument), else $CLAUDE_CODE_SESSION_ID / $CODEX_THREAD_ID
|
|
35
|
+
/ $GROK_SESSION_ID (lane-mcp passes the first and the last: Claude Code and Grok Build give them to their MCP servers;
|
|
36
|
+
Grok Build also sets GROK_SESSION_ID for every hook it runs).
|
|
37
|
+
Harness (W12, DEF-1990): --harness when it is one of HARNESSES, else $LANE_HARNESS (the same rule), else the first
|
|
38
|
+
process up the parent chain (PARENT_DEPTH steps from this process's parent) whose comm is a harness's
|
|
39
|
+
(HARNESS_COMMS: claude, codex, kimi, kimi-code, grok; a `grok` is Grok Build when /proc/<pid>/exe ends in
|
|
40
|
+
GROK_BUILD_EXE, else grok-cli; any comm whose exe holds CLAUDE_EXE is Claude Code), else `unknown` — never `codex`:
|
|
41
|
+
before W12 every hook without --harness and outside Claude Code was recorded as Codex. $LANE_PROC names another /proc for the chain and the agent's starttime (tests).
|
|
42
|
+
Stdin is read only when it is not a terminal and for at most STDIN_WAIT seconds: a stdin that never closes cannot hang
|
|
43
|
+
the hook.
|
|
35
44
|
The same agent: the same process (pid and starttime: lane-mcp's parent and a hook's are the agent), else the same
|
|
36
45
|
session when both writes name one (lane-mcp under Codex knows no session; the agent's hooks do).
|
|
37
46
|
|
|
@@ -67,7 +76,12 @@ NOTE_MAX = 120
|
|
|
67
76
|
EXPLICIT = 'lanes_signal' # the --event of lane-mcp's lanes_signal: the agent's own word
|
|
68
77
|
REPORT_TOOL = 'lanes_signal' # its tool name; Claude Code names an MCP tool mcp__<server>__<tool>
|
|
69
78
|
RESTING = {('Stop', 'needs_input'), ('Stop', 'done'), ('Notification', 'needs_input')} # the hooks' "turn ended"
|
|
70
|
-
SESSION_VARS = ('CLAUDE_CODE_SESSION_ID', 'CODEX_THREAD_ID')
|
|
79
|
+
SESSION_VARS = ('CLAUDE_CODE_SESSION_ID', 'CODEX_THREAD_ID', 'GROK_SESSION_ID')
|
|
80
|
+
HARNESSES = ('claude', 'codex', 'kimi', 'grok', 'grok-cli', 'unknown') # agent{}.harness, but `shell`
|
|
81
|
+
HARNESS_COMMS = {'claude': 'claude', 'codex': 'codex', 'kimi': 'kimi', 'kimi-code': 'kimi', 'grok': 'grok'}
|
|
82
|
+
GROK_BUILD_EXE = '/downloads/grok-linux-x86_64' # Grok Build's file ($GROK_HOME/downloads/…); grok-cli's: bin/grok
|
|
83
|
+
CLAUDE_EXE = '/claude/versions/' # native Claude Code before 2.1.299 kept its file's name, the version, as comm (MEASURED)
|
|
84
|
+
PARENT_DEPTH = 8
|
|
71
85
|
|
|
72
86
|
|
|
73
87
|
def options(argv):
|
|
@@ -115,15 +129,15 @@ def lane_id(root):
|
|
|
115
129
|
return found if LANE_ID.fullmatch(found or '') else ''
|
|
116
130
|
|
|
117
131
|
|
|
118
|
-
def stat_fields(pid):
|
|
132
|
+
def stat_fields(pid, proc='/proc'):
|
|
119
133
|
"""Fields after `comm` of /proc/<pid>/stat (state is [0], ppid [1], starttime [19]) and the comm itself."""
|
|
120
|
-
with open(f'
|
|
134
|
+
with open(f'{proc}/{pid}/stat') as handle:
|
|
121
135
|
text = handle.read()
|
|
122
136
|
comm = text[text.index('(') + 1:text.rindex(')')]
|
|
123
137
|
return comm, text.rsplit(')', 1)[1].split()
|
|
124
138
|
|
|
125
139
|
|
|
126
|
-
def agent(given=None):
|
|
140
|
+
def agent(given=None, proc='/proc'):
|
|
127
141
|
"""(pid, starttime) of the agent: `given` (--agent-pid) when that process exists, else the parent — or the
|
|
128
142
|
parent's parent when the parent is a shell."""
|
|
129
143
|
try:
|
|
@@ -132,20 +146,69 @@ def agent(given=None):
|
|
|
132
146
|
pid = 0
|
|
133
147
|
if pid > 1:
|
|
134
148
|
try:
|
|
135
|
-
return pid, stat_fields(pid)[1][19]
|
|
149
|
+
return pid, stat_fields(pid, proc)[1][19]
|
|
136
150
|
except (OSError, ValueError, IndexError):
|
|
137
151
|
pass # gone (or never there): the parent rule below names the agent
|
|
138
152
|
pid = os.getppid()
|
|
139
153
|
try:
|
|
140
|
-
comm, fields = stat_fields(pid)
|
|
154
|
+
comm, fields = stat_fields(pid, proc)
|
|
141
155
|
if comm in SHELLS and int(fields[1]) > 1:
|
|
142
156
|
pid = int(fields[1])
|
|
143
|
-
comm, fields = stat_fields(pid)
|
|
157
|
+
comm, fields = stat_fields(pid, proc)
|
|
144
158
|
return pid, fields[19]
|
|
145
159
|
except (OSError, ValueError, IndexError):
|
|
146
160
|
return pid, None
|
|
147
161
|
|
|
148
162
|
|
|
163
|
+
def exe_of(pid, proc='/proc'):
|
|
164
|
+
"""The path /proc/<pid>/exe names ('' when it cannot be read), without the kernel's ` (deleted)`."""
|
|
165
|
+
try:
|
|
166
|
+
return os.readlink(f'{proc}/{pid}/exe').removesuffix(' (deleted)')
|
|
167
|
+
except OSError:
|
|
168
|
+
return ''
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def process_harness(comm, pid, proc='/proc'):
|
|
172
|
+
"""The harness process `pid` (whose comm is `comm`) is, or None. A `grok` is Grok Build when its executable is
|
|
173
|
+
$GROK_HOME/downloads/grok-linux-x86_64 (the installer's file, run through the bin/grok link), anything else grok-cli
|
|
174
|
+
(its own bin/grok, a real file); a process running a native Claude Code file (…/claude/versions/<version>) is
|
|
175
|
+
Claude Code whatever its comm."""
|
|
176
|
+
found = HARNESS_COMMS.get(comm)
|
|
177
|
+
if found == 'grok':
|
|
178
|
+
return 'grok' if exe_of(pid, proc).endswith(GROK_BUILD_EXE) else 'grok-cli'
|
|
179
|
+
if found:
|
|
180
|
+
return found
|
|
181
|
+
return 'claude' if CLAUDE_EXE in exe_of(pid, proc) else None
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def parent_harness(proc='/proc', start=None):
|
|
185
|
+
"""The harness of the first process up the parent chain whose comm is a harness's (HARNESS_COMMS), at most
|
|
186
|
+
PARENT_DEPTH steps from `start` (default: this process's parent), else ''. A hook runs as the harness's child or a
|
|
187
|
+
shell's; lane-mcp's lanes_signal as lane-mcp's child, whose parent is the agent."""
|
|
188
|
+
pid = os.getppid() if start is None else start
|
|
189
|
+
for _step in range(PARENT_DEPTH):
|
|
190
|
+
if pid <= 1:
|
|
191
|
+
break
|
|
192
|
+
try:
|
|
193
|
+
comm, fields = stat_fields(pid, proc)
|
|
194
|
+
parent = int(fields[1])
|
|
195
|
+
except (OSError, ValueError, IndexError):
|
|
196
|
+
break
|
|
197
|
+
found = process_harness(comm, pid, proc)
|
|
198
|
+
if found:
|
|
199
|
+
return found
|
|
200
|
+
pid = parent
|
|
201
|
+
return ''
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def harness_of(given, proc='/proc'):
|
|
205
|
+
"""--harness, else $LANE_HARNESS, each only when it is one of HARNESSES; else the parent chain; else `unknown`."""
|
|
206
|
+
for word in (given, os.environ.get('LANE_HARNESS') or ''):
|
|
207
|
+
if word in HARNESSES:
|
|
208
|
+
return word
|
|
209
|
+
return parent_harness(proc) or 'unknown'
|
|
210
|
+
|
|
211
|
+
|
|
149
212
|
def stdin_doc():
|
|
150
213
|
"""The hook's JSON on stdin as a dict ({} when there is none): Claude Code and Codex pass `session_id`,
|
|
151
214
|
`hook_event_name` and, for a tool event, `tool_name`."""
|
|
@@ -194,7 +257,7 @@ def main(argv):
|
|
|
194
257
|
if not lane:
|
|
195
258
|
return
|
|
196
259
|
hook = stdin_doc()
|
|
197
|
-
session = str(hook.get('session_id') or '')
|
|
260
|
+
session = str(hook.get('session_id') or hook.get('sessionId') or '')
|
|
198
261
|
if not session and payload:
|
|
199
262
|
try:
|
|
200
263
|
doc = json.loads(payload)
|
|
@@ -202,10 +265,10 @@ def main(argv):
|
|
|
202
265
|
except ValueError:
|
|
203
266
|
session = ''
|
|
204
267
|
session = (session or next((os.environ[key] for key in SESSION_VARS if os.environ.get(key)), ''))[:128]
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
pid, start = agent(agent_pid)
|
|
268
|
+
proc = os.environ.get('LANE_PROC') or '/proc'
|
|
269
|
+
given_harness = harness if harness in HARNESSES else ''
|
|
270
|
+
harness = harness_of(harness, proc)
|
|
271
|
+
pid, start = agent(agent_pid, proc)
|
|
209
272
|
now = int(time.time())
|
|
210
273
|
state = os.path.join(root, 'state', 'hooks') # never state/<id>.json: lane-run's phase record (DEF-1572)
|
|
211
274
|
path = os.path.join(state, f'{lane}.json')
|