luciazero 2.4.2 → 2.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/README.md +140 -20
- package/bin/bus.js +121 -0
- package/bin/luciazero-agentd +107 -0
- package/bin/luciazero.js +3 -1
- package/install-codex.sh +104 -12
- package/install.sh +283 -25
- package/package.json +10 -3
- package/skills/catalog.txt +2 -0
- package/skills/done/SKILL.md +6 -3
- package/skills/done/scripts/revert-probe.sh +137 -26
- package/skills/imouto-mode/SKILL.md +15 -1
- package/skills/lucia-bus/SKILL.md +75 -0
- package/skills/lucia-chat/SKILL.md +152 -0
- package/skills/lucia-relay/SKILL.md +5 -5
- package/uninstall-codex.sh +129 -15
- package/uninstall.sh +308 -27
package/README.md
CHANGED
|
@@ -2,29 +2,68 @@
|
|
|
2
2
|
<img src="https://cdn.jsdelivr.net/gh/ohm41321/luciazero@37cb470e2b7c704ff32f3a46dbb125e312875960/docs/assets/lucia.png" width="220" alt="Lucia — Luciazero's mascot">
|
|
3
3
|
<h1>Luciazero</h1>
|
|
4
4
|
<p>
|
|
5
|
-
<strong>
|
|
5
|
+
<strong>Make coding agents prove their work.</strong><br>
|
|
6
6
|
<code>plan → change → verify → fix</code>
|
|
7
7
|
</p>
|
|
8
8
|
<p>
|
|
9
9
|
<a href="https://www.npmjs.com/package/luciazero"><img src="https://img.shields.io/npm/v/luciazero" alt="npm version"></a>
|
|
10
|
+
<a href="https://www.npmjs.com/package/luciazero"><img src="https://img.shields.io/npm/dw/luciazero" alt="npm weekly downloads"></a>
|
|
10
11
|
<a href="https://github.com/ohm41321/luciazero/actions/workflows/ci.yml"><img src="https://github.com/ohm41321/luciazero/actions/workflows/ci.yml/badge.svg" alt="CI status"></a>
|
|
12
|
+
<a href="https://github.com/ohm41321/luciazero/stargazers"><img src="https://img.shields.io/github/stars/ohm41321/luciazero?style=flat" alt="GitHub stars"></a>
|
|
13
|
+
<a href="https://github.com/ohm41321/luciazero/network/members"><img src="https://img.shields.io/github/forks/ohm41321/luciazero?style=flat" alt="GitHub forks"></a>
|
|
11
14
|
<a href="https://github.com/ohm41321/luciazero/blob/main/LICENSE"><img src="https://img.shields.io/github/license/ohm41321/luciazero" alt="MIT license"></a>
|
|
12
15
|
</p>
|
|
13
16
|
</div>
|
|
14
17
|
|
|
15
18
|
**English** · [ภาษาไทย](https://github.com/ohm41321/luciazero/blob/main/README.th.md)
|
|
16
19
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
<p align="center">
|
|
21
|
+
<strong>13 skills</strong> · <strong>Relay fixture 6/6</strong> · <strong>Claude + Codex</strong> · <strong>MIT</strong>
|
|
22
|
+
</p>
|
|
23
|
+
<p align="center"><sub>Relay 6/6 is a mechanical protocol check; behavioral results are reported separately.</sub></p>
|
|
24
|
+
|
|
25
|
+
Luciazero is the verification and handoff layer for Claude Code, Codex CLI, and
|
|
26
|
+
compatible skill runtimes. It helps agents prove tests, preserve scope, and
|
|
27
|
+
move unfinished work with evidence.
|
|
20
28
|
|
|
21
29
|
> Done is proven by a command, not by my judgment. If no verification command
|
|
22
30
|
> exists, that is the first bug.
|
|
23
31
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
32
|
+
Luciazero is for teams that want coding agents to prove work instead of merely
|
|
33
|
+
report it. It helps prevent false-green verification and dropped scope,
|
|
34
|
+
preserves lessons from repeated dead ends, and carries context across handoffs
|
|
35
|
+
when `/lucia-relay` is used. It is a discipline layer, not an agent runtime.
|
|
36
|
+
|
|
37
|
+
## Start in 30 seconds
|
|
38
|
+
|
|
39
|
+
Choose the path that matches your agent, then start a new session:
|
|
40
|
+
|
|
41
|
+
**Claude Code · full pack**
|
|
42
|
+
|
|
43
|
+
```text
|
|
44
|
+
/plugin marketplace add ohm41321/luciazero
|
|
45
|
+
/plugin install luciazero@luciazero
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Run `/luciazero:ready` in the new session.
|
|
49
|
+
|
|
50
|
+
**Codex CLI · doctrine + skills**
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
npx luciazero codex
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Run `$ready` in the new session.
|
|
57
|
+
|
|
58
|
+
**Skills-only · any compatible agent**
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
npx skills add ohm41321/luciazero
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Invoke the installed `ready` skill using your agent's syntax (for Codex CLI,
|
|
65
|
+
use `$ready`). This path intentionally does not install the doctrine, reviewer,
|
|
66
|
+
or hooks.
|
|
28
67
|
|
|
29
68
|
## See the loop
|
|
30
69
|
|
|
@@ -40,6 +79,65 @@ This GIF is driven by the shipped hooks, not a mockup:
|
|
|
40
79
|
✅ verify 3m → the latest check passed three minutes ago
|
|
41
80
|
```
|
|
42
81
|
|
|
82
|
+
Run the same shipped hook driver locally—no model or API is required:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
bash docs/assets/statusline-demo.sh
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
For the cross-session handoff demo:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
bash docs/assets/relay-demo.sh
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Both scripts use throwaway directories and exercise the real implementation.
|
|
95
|
+
|
|
96
|
+
## Let two agents hand work to each other
|
|
97
|
+
|
|
98
|
+
**Agent Bus is beta, opt-in, and checkout only.** It does not come with
|
|
99
|
+
`npx luciazero` and never starts during a normal npm, plugin, or skills-only
|
|
100
|
+
install. After the one-time checkout setup, the ordinary path is one command
|
|
101
|
+
in each agent window:
|
|
102
|
+
|
|
103
|
+
```text
|
|
104
|
+
window A window B
|
|
105
|
+
$ lucia claude $ lucia codex
|
|
106
|
+
|
|
107
|
+
task_create + message_send ───────► queued durably in the local bus
|
|
108
|
+
nudge ─► check your bus inbox (1 new task from claude)
|
|
109
|
+
message_ack + task_claim
|
|
110
|
+
work + verify
|
|
111
|
+
artifact_publish + task_complete
|
|
112
|
+
result in the inbox ◄─────── message_send
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
The daemon starts when the first `lucia` session needs it, each CLI receives
|
|
116
|
+
an MCP configuration for that run only, and the user's central Claude or
|
|
117
|
+
Codex configuration is not edited. Messages and task state stay in local
|
|
118
|
+
SQLite; peer message text is never typed into another session's prompt. The
|
|
119
|
+
proxy types only the daemon-built inbox notice after the provider is quiet.
|
|
120
|
+
|
|
121
|
+
Try the shipped fake-provider demo—no model, login, or quota required:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
bash docs/assets/agent-bus-demo.sh
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
To open real Claude Code and Codex sessions, first follow the one-time
|
|
128
|
+
[checkout setup](docs/agent-bus.md#start-here), then run `lucia claude` and
|
|
129
|
+
`lucia codex`. See the [Agent Bus guide](docs/agent-bus.md) for worktree
|
|
130
|
+
ownership, `--no-nudge`, security boundaries, and cleanup.
|
|
131
|
+
|
|
132
|
+
Inside the Bus workflow, the skill stops before risky work: delete, deploy,
|
|
133
|
+
production access, spending, force-push, public-contract changes, and scope
|
|
134
|
+
expansion need a single-use nonce you mint in your own terminal. No bus tool
|
|
135
|
+
can create one, and a nonce put into a bus message, task, or artifact is
|
|
136
|
+
scrubbed or refused ([approvals](docs/agent-bus.md#approvals)). This records
|
|
137
|
+
approval provenance; it is not a host sandbox against another process running
|
|
138
|
+
as you. The project trust boundary is in
|
|
139
|
+
[SECURITY.md](https://github.com/ohm41321/luciazero/blob/main/SECURITY.md).
|
|
140
|
+
|
|
43
141
|
## What it protects
|
|
44
142
|
|
|
45
143
|
| Failure mode | Mechanism |
|
|
@@ -110,7 +208,7 @@ cannot set one.
|
|
|
110
208
|
npx skills add ohm41321/luciazero
|
|
111
209
|
```
|
|
112
210
|
|
|
113
|
-
This installs the
|
|
211
|
+
This installs the 13 skills: no doctrine, reviewer, or hooks.
|
|
114
212
|
|
|
115
213
|
</details>
|
|
116
214
|
|
|
@@ -151,11 +249,13 @@ Other install channels use their own updater:
|
|
|
151
249
|
|
|
152
250
|
```bash
|
|
153
251
|
claude plugin update luciazero@luciazero # then run /reload-plugins
|
|
154
|
-
npx skills update #
|
|
252
|
+
npx skills update # every installed skill in the selected scope
|
|
253
|
+
npx skills update done -g # only the global "done" skill
|
|
155
254
|
```
|
|
156
255
|
|
|
157
256
|
The skills command updates every installed skill in the selected scope, not
|
|
158
|
-
only Luciazero; review its prompt before confirming.
|
|
257
|
+
only Luciazero; review its prompt before confirming. Use the targeted form when
|
|
258
|
+
you only want to refresh one skill.
|
|
159
259
|
|
|
160
260
|
Claude Code can auto-update the plugin at startup: open `/plugin` →
|
|
161
261
|
**Marketplaces** → **luciazero** → **Enable auto-update**. Third-party
|
|
@@ -164,7 +264,8 @@ GitHub **Watch → Custom → Releases**.
|
|
|
164
264
|
|
|
165
265
|
## Skills at a glance
|
|
166
266
|
|
|
167
|
-
|
|
267
|
+
Invoke `ready` first (`/ready` for slash-based agents, `$ready` in Codex); the
|
|
268
|
+
rest activate when their moment arrives.
|
|
168
269
|
|
|
169
270
|
| Moment | Skill | Result |
|
|
170
271
|
|---|---|---|
|
|
@@ -176,6 +277,8 @@ Run `/ready` first; the rest activate when their moment arrives.
|
|
|
176
277
|
| Good and bad revisions are known | `/bisect` | Finds the first bad commit in a temporary worktree |
|
|
177
278
|
| Before claiming completion | `/done` | Full verify, skeptic review, scope report |
|
|
178
279
|
| Work must move elsewhere | `/lucia-relay` | Portable JSON + Markdown state with drift inspection |
|
|
280
|
+
| Another agent's work is queued for you (beta) | `/lucia-bus` | Register, read the inbox, claim, work, publish the result through the local Agent Bus ([setup and demo](docs/agent-bus.md)) |
|
|
281
|
+
| Want two agent sessions talking (beta) | `/lucia-chat` | Shows what is waiting, opens one window per agent, and optionally watches the conversation |
|
|
179
282
|
| Optimizing performance | `/experiment` | Baseline, threshold, controlled measurement |
|
|
180
283
|
| Reviewing local verify habits | `/discipline-report` | Time/project-filtered local outcome report |
|
|
181
284
|
| After difficult work | `/retro` | Stores reusable lessons and disproved approaches |
|
|
@@ -191,21 +294,31 @@ separate passes.
|
|
|
191
294
|
|
|
192
295
|
## Evidence & limitations
|
|
193
296
|
|
|
297
|
+
The measurements below are early and model/task-specific. The linked raw rows
|
|
298
|
+
and methodology are the source of truth; they are not a promise of uplift on
|
|
299
|
+
every repository or model.
|
|
300
|
+
|
|
194
301
|
<!-- BEGIN GENERATED: benchmark-evidence -->
|
|
195
302
|
|
|
196
303
|
### Claude results
|
|
197
304
|
|
|
198
|
-
|
|
305
|
+
Snapshots: 2026-08-11 for Haiku and the Sonnet pilot, 2026-09-02 for Sonnet.
|
|
306
|
+
All-criteria pass rate generated from checked-in raw rows:
|
|
199
307
|
|
|
200
|
-
| Claude model | Luciazero | Bare | Difference |
|
|
201
|
-
|
|
202
|
-
| Haiku†, 10 valid/task | 36/60 (60%) | 27/60 (45%) | +15pp |
|
|
203
|
-
| Sonnet, 4–5 valid/task* | 25/27 (93%) | 16/26 (62%) | +31pp |
|
|
308
|
+
| Claude model | Tasks | Luciazero | Bare | Difference |
|
|
309
|
+
|---|---:|---:|---:|---:|
|
|
310
|
+
| Haiku†, 10 valid/task | 6 | 36/60 (60%) | 27/60 (45%) | +15pp |
|
|
311
|
+
| Sonnet (2026-08-11 pilot), 4–5 valid/task* | 6 | 25/27 (93%) | 16/26 (62%) | +31pp |
|
|
312
|
+
| Sonnet, 5 valid/task | 10 | 39/50 (78%) | 23/50 (46%) | +32pp |
|
|
204
313
|
|
|
205
314
|
The `Luciazero` arm installs the classic pack without hooks; it is not a clean
|
|
206
|
-
doctrine-only ablation.
|
|
207
|
-
|
|
208
|
-
|
|
315
|
+
doctrine-only ablation. Rows are not like-for-like: the 2026-09-02 Sonnet
|
|
316
|
+
campaign adds four harder tasks that the 2026-08-11 campaigns never ran, so
|
|
317
|
+
compare each row against its own bare arm rather than across models.
|
|
318
|
+
*The Sonnet pilot stays preliminary because eight invalid rows leave several
|
|
319
|
+
arms at four valid runs; the 2026-09-02 campaign supersedes it with five valid
|
|
320
|
+
runs in every cell and no invalid rows. The previously stated `+37pp` top-up
|
|
321
|
+
remains retired because its replacement raw rows could not be recovered.
|
|
209
322
|
|
|
210
323
|
†Model provenance is incomplete for Haiku: only 70/140 rows encode model
|
|
211
324
|
identity. The other 70 are attributed at campaign-file/report level and
|
|
@@ -232,6 +345,12 @@ only one run per arm per task. See the [full benchmark](https://github.com/ohm41
|
|
|
232
345
|
- Node.js 18+ for the CLI and discipline report.
|
|
233
346
|
- Bash for classic installers; Python 3.9+ for hooks and Lucia Relay
|
|
234
347
|
(`install.sh --with-hooks` refuses anything older).
|
|
348
|
+
- The Agent Bus daemon (beta, opt-in) needs Python 3.10+ and a checkout: it is
|
|
349
|
+
not in the npm payload and `npx luciazero` never starts it. From a checkout,
|
|
350
|
+
`./install.sh` adds the `luciazero-agentd` launcher to `~/.claude/bin`
|
|
351
|
+
(`LUCIAZERO_BIN_DIR` chooses another directory), and
|
|
352
|
+
`luciazero-agentd service install` runs the daemon under launchd or systemd
|
|
353
|
+
`--user`. macOS, Linux and WSL2 only. See [docs/agent-bus.md](docs/agent-bus.md).
|
|
235
354
|
- Core installers, hooks, helpers, and graders are offline. Real behavioral
|
|
236
355
|
evals invoke a model CLI and consume API credit or subscription quota.
|
|
237
356
|
- Hooks run commands on your machine. Read them before enabling them.
|
|
@@ -273,6 +392,7 @@ More detail:
|
|
|
273
392
|
- [Benchmark results and GPT plan](https://github.com/ohm41321/luciazero/blob/main/docs/benchmark.md)
|
|
274
393
|
- [Raw campaign registry](https://github.com/ohm41321/luciazero/blob/main/eval/results/campaigns.json)
|
|
275
394
|
- [Experiment log](https://github.com/ohm41321/luciazero/blob/main/docs/experiments.md)
|
|
395
|
+
- [Launch kit](https://github.com/ohm41321/luciazero/blob/main/docs/launch-kit.md)
|
|
276
396
|
- [Contributing](https://github.com/ohm41321/luciazero/blob/main/CONTRIBUTING.md)
|
|
277
397
|
- [Publishing](https://github.com/ohm41321/luciazero/blob/main/docs/publishing.md)
|
|
278
398
|
- [Changelog](https://github.com/ohm41321/luciazero/blob/main/CHANGELOG.md)
|
package/bin/bus.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// `npx luciazero bus status [--json]`: show what is waiting on whom in the
|
|
3
|
+
// local Agent Bus. Talks to the running luciazero-agentd over loopback with
|
|
4
|
+
// the capability token; never starts a daemon and works without one
|
|
5
|
+
// installed (it just reports that none is running).
|
|
6
|
+
const fs = require("node:fs");
|
|
7
|
+
const os = require("node:os");
|
|
8
|
+
const path = require("node:path");
|
|
9
|
+
|
|
10
|
+
function stateDir() {
|
|
11
|
+
const env = process.env.LUCIAZERO_AGENT_BUS_HOME;
|
|
12
|
+
return env ? env : path.join(os.homedir(), ".luciazero", "agent-bus");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Peer-supplied strings never reach the terminal unfiltered.
|
|
16
|
+
const clean = (value) => String(value).replace(/[\x00-\x1f\x7f-\x9f]/g, "?");
|
|
17
|
+
|
|
18
|
+
function usage(code) {
|
|
19
|
+
const out = code === 0 ? console.log : console.error;
|
|
20
|
+
out("usage: luciazero bus status [--json]");
|
|
21
|
+
out(" Reads endpoint.json and token from " + stateDir() + " (LUCIAZERO_AGENT_BUS_HOME).");
|
|
22
|
+
process.exit(code);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function status(json) {
|
|
26
|
+
const dir = stateDir();
|
|
27
|
+
let endpoint;
|
|
28
|
+
try {
|
|
29
|
+
endpoint = JSON.parse(fs.readFileSync(path.join(dir, "endpoint.json"), "utf8"));
|
|
30
|
+
} catch (err) {
|
|
31
|
+
console.error(`luciazero bus: no running daemon recorded in ${dir}`);
|
|
32
|
+
console.error(" the bus is an opt-in beta that ships only in a checkout, not with npx luciazero:");
|
|
33
|
+
console.error(" clone https://github.com/ohm41321/luciazero, run ./install.sh, then: lucia claude");
|
|
34
|
+
process.exit(2);
|
|
35
|
+
}
|
|
36
|
+
let token;
|
|
37
|
+
try {
|
|
38
|
+
token = fs.readFileSync(path.join(dir, "token"), "utf8").trim();
|
|
39
|
+
} catch (err) {
|
|
40
|
+
console.error(`luciazero bus: cannot read the capability token in ${dir}`);
|
|
41
|
+
process.exit(2);
|
|
42
|
+
}
|
|
43
|
+
const base = endpoint.url.replace(/\/mcp$/, "");
|
|
44
|
+
let response;
|
|
45
|
+
try {
|
|
46
|
+
response = await fetch(base + "/status", { headers: { Authorization: `Bearer ${token}` } });
|
|
47
|
+
} catch (err) {
|
|
48
|
+
console.error(`luciazero bus: daemon at ${endpoint.url} is not answering (${err.message})`);
|
|
49
|
+
process.exit(2);
|
|
50
|
+
}
|
|
51
|
+
if (!response.ok) {
|
|
52
|
+
console.error(`luciazero bus: daemon answered HTTP ${response.status}`);
|
|
53
|
+
process.exit(2);
|
|
54
|
+
}
|
|
55
|
+
const body = await response.json();
|
|
56
|
+
if (json) {
|
|
57
|
+
console.log(JSON.stringify(body, null, 2));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const tasks = Object.entries(body.tasks).map(([k, v]) => `${clean(k)} ${Number(v)}`).join(", ");
|
|
61
|
+
console.log(`agent bus: ${clean(body.server.name)} ${clean(body.server.version)} since ${clean(body.server.started_at)}`);
|
|
62
|
+
console.log(`queued deliveries: ${Number(body.queued_deliveries)} tasks: ${tasks}`);
|
|
63
|
+
for (const agent of body.agents) {
|
|
64
|
+
const wt = agent.worktree;
|
|
65
|
+
const where = wt ? ` on ${clean(wt.branch)}${wt.dirty ? " (dirty)" : ""}` : "";
|
|
66
|
+
// ADR 0004: an agent with no terminal binding says so on its own line.
|
|
67
|
+
const who = agent.binding ? ` ${clean(agent.binding.tty || "no tty")}` : " unverified";
|
|
68
|
+
console.log(
|
|
69
|
+
` ${clean(agent.id).padEnd(24)} ${clean(agent.provider).padEnd(7)} ${clean(agent.role).padEnd(14)} ` +
|
|
70
|
+
`inbox ${String(Number(agent.queued_deliveries)).padStart(3)} claimed ${String(Number(agent.claimed_tasks)).padStart(3)} seen ${clean(agent.last_seen_at)}${where}${who}`
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
for (const task of body.open_tasks) {
|
|
74
|
+
const needs = task.requires_worktree ? " needs worktree" : "";
|
|
75
|
+
console.log(` open task ${clean(task.id)} p${Number(task.priority)} ${clean(task.assigned_to || "unassigned")}: ${clean(task.title)}${needs}`);
|
|
76
|
+
}
|
|
77
|
+
const workers = Array.isArray(body.workers) ? body.workers : [];
|
|
78
|
+
for (const worker of workers) {
|
|
79
|
+
console.log(` managed worker ${clean(worker.agent_id).padEnd(20)} ${clean(worker.provider).padEnd(7)} ${worker.enabled ? "enabled" : "paused"}`);
|
|
80
|
+
}
|
|
81
|
+
const running = Array.isArray(body.running_runs) ? body.running_runs : [];
|
|
82
|
+
for (const run of running) {
|
|
83
|
+
console.log(` turn running ${clean(run.agent_id).padEnd(20)} attempt ${Number(run.attempt)} since ${clean(run.started_at)}`);
|
|
84
|
+
}
|
|
85
|
+
const stopped = Array.isArray(body.stopped_tasks) ? body.stopped_tasks : [];
|
|
86
|
+
for (const task of stopped) {
|
|
87
|
+
console.log(` stopped task ${clean(task.id)} spent its ${clean(task.dimension || "budget")} budget: ${clean(task.title)}`);
|
|
88
|
+
}
|
|
89
|
+
const unverified = Array.isArray(body.unverified_agents) ? body.unverified_agents : [];
|
|
90
|
+
if (unverified.length > 0) {
|
|
91
|
+
console.log(
|
|
92
|
+
`unverified: ${unverified.map(clean).join(", ")} (no terminal binding; these sessions act as whoever they claim to be)`
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
if (Number(body.approvals_pending) > 0) {
|
|
96
|
+
console.log(`approvals pending: ${Number(body.approvals_pending)} (unused nonces; each is bound to one task and operation)`);
|
|
97
|
+
}
|
|
98
|
+
if (body.queued_deliveries > 0 || body.tasks.open > 0) {
|
|
99
|
+
console.log("next: start the agent's session and run /lucia-bus (Codex: $lucia-bus)");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const args = process.argv.slice(2);
|
|
104
|
+
if (args.length === 0 || args[0] === "--help" || args[0] === "-h") usage(args.length === 0 ? 64 : 0);
|
|
105
|
+
if (args[0] !== "status") {
|
|
106
|
+
console.error(`luciazero bus: unknown subcommand '${args[0]}'`);
|
|
107
|
+
usage(64);
|
|
108
|
+
}
|
|
109
|
+
const extra = args.slice(1).filter((a) => a !== "--json");
|
|
110
|
+
if (extra.length > 0) {
|
|
111
|
+
console.error(`luciazero bus: unknown option '${extra[0]}'`);
|
|
112
|
+
usage(64);
|
|
113
|
+
}
|
|
114
|
+
if (typeof fetch !== "function") {
|
|
115
|
+
console.error("luciazero bus: needs Node 18+ (global fetch)");
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
status(args.includes("--json")).catch((err) => {
|
|
119
|
+
console.error("luciazero bus: " + err.message);
|
|
120
|
+
process.exit(1);
|
|
121
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# luciazero-agentd -- run the Agent Bus daemon from anywhere.
|
|
3
|
+
# lucia -- the same program under a shorter name, installed as a second copy.
|
|
4
|
+
#
|
|
5
|
+
# luciazero-managed: agentd-launcher
|
|
6
|
+
#
|
|
7
|
+
# The daemon is a Python package that is neither on PATH nor pip-installed: it
|
|
8
|
+
# lives in `agentd/` inside a checkout (ADR 0002 keeps it out of the npm
|
|
9
|
+
# payload). This shim finds it from where the shim itself is, never from the
|
|
10
|
+
# caller's working directory -- the working directory is meaningful to the
|
|
11
|
+
# daemon, since `attach` records it as the session's, so this must not change
|
|
12
|
+
# it the way `cd agentd && python3 -m luciazero_agentd` did.
|
|
13
|
+
set -eu
|
|
14
|
+
|
|
15
|
+
fail() { printf 'luciazero-agentd: %s\n' "$*" >&2; exit 127; }
|
|
16
|
+
|
|
17
|
+
# Resolve this script through any symlinks. `readlink -f` is GNU-only, so the
|
|
18
|
+
# loop is hand-rolled; every expansion is quoted, so a path with spaces (and
|
|
19
|
+
# an installed copy reached through a symlink) resolves correctly.
|
|
20
|
+
# The name typed, before any symlink is followed: `lucia` and
|
|
21
|
+
# `luciazero-agentd` are one program, and only the messages that tell the user
|
|
22
|
+
# what to type next need to know which name they are speaking under.
|
|
23
|
+
argv0="$(basename "$0")"
|
|
24
|
+
self="$0"
|
|
25
|
+
hops=0
|
|
26
|
+
while [ -L "$self" ]; do
|
|
27
|
+
hops=$((hops + 1))
|
|
28
|
+
[ "$hops" -le 40 ] || fail "symlink loop while resolving $0"
|
|
29
|
+
target="$(readlink "$self")"
|
|
30
|
+
case "$target" in
|
|
31
|
+
/*) self="$target" ;;
|
|
32
|
+
*) self="$(dirname "$self")/$target" ;;
|
|
33
|
+
esac
|
|
34
|
+
done
|
|
35
|
+
here="$(cd "$(dirname "$self")" && pwd -P)"
|
|
36
|
+
|
|
37
|
+
config_dir="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
|
|
38
|
+
home_file="${config_dir}/.luciazero-agentd-home"
|
|
39
|
+
|
|
40
|
+
# Checkout layout wins over the recorded one: running `bin/luciazero-agentd`
|
|
41
|
+
# out of a second clone must use that clone, not whichever one was installed.
|
|
42
|
+
package_home=""
|
|
43
|
+
if [ -n "${LUCIAZERO_AGENTD_HOME:-}" ]; then
|
|
44
|
+
package_home="${LUCIAZERO_AGENTD_HOME}"
|
|
45
|
+
elif [ -d "${here}/../agentd/luciazero_agentd" ]; then
|
|
46
|
+
package_home="$(cd "${here}/.." && pwd -P)/agentd"
|
|
47
|
+
elif [ -f "${home_file}" ]; then
|
|
48
|
+
package_home="$(cat "${home_file}")"
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
if [ -z "${package_home}" ]; then
|
|
52
|
+
fail "cannot find the luciazero_agentd package.
|
|
53
|
+
It ships in a checkout of https://github.com/ohm41321/luciazero, not in the
|
|
54
|
+
npm package. Clone it and run ./install.sh from the checkout, or point
|
|
55
|
+
LUCIAZERO_AGENTD_HOME at its agentd/ directory."
|
|
56
|
+
fi
|
|
57
|
+
case "${package_home}" in
|
|
58
|
+
*"
|
|
59
|
+
"*) fail "the package path contains a newline: ${package_home}" ;;
|
|
60
|
+
esac
|
|
61
|
+
if [ ! -d "${package_home}/luciazero_agentd" ]; then
|
|
62
|
+
fail "no luciazero_agentd package in ${package_home} (moved or deleted checkout?).
|
|
63
|
+
Re-run ./install.sh from the checkout, or set LUCIAZERO_AGENTD_HOME."
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
# ADR 0002: try python3, then python, then the Windows launcher, and take the
|
|
67
|
+
# first one that reports 3.10 or newer. A wrong pick here is worse than none:
|
|
68
|
+
# the daemon would fail deep inside the package instead of at the door.
|
|
69
|
+
usable() {
|
|
70
|
+
"$@" -c 'import sys; raise SystemExit(0 if sys.version_info >= (3, 10) else 1)' >/dev/null 2>&1
|
|
71
|
+
}
|
|
72
|
+
python=""
|
|
73
|
+
for candidate in python3 python; do
|
|
74
|
+
if command -v "${candidate}" >/dev/null 2>&1 && usable "${candidate}"; then
|
|
75
|
+
python="${candidate}"
|
|
76
|
+
break
|
|
77
|
+
fi
|
|
78
|
+
done
|
|
79
|
+
if [ -z "${python}" ] && command -v py >/dev/null 2>&1 && usable py -3; then
|
|
80
|
+
python="py -3"
|
|
81
|
+
fi
|
|
82
|
+
[ -n "${python}" ] || fail "needs Python 3.10 or newer; tried python3, python and py -3"
|
|
83
|
+
|
|
84
|
+
# The package path travels in an environment variable and is put on sys.path
|
|
85
|
+
# by the interpreter itself. Two things this avoids, both of which let code
|
|
86
|
+
# from the caller's working directory run instead of the daemon:
|
|
87
|
+
#
|
|
88
|
+
# * PYTHONPATH is colon-separated, so a checkout at a path containing ":"
|
|
89
|
+
# would split into two entries, the tail of which resolves relative to the
|
|
90
|
+
# caller's directory.
|
|
91
|
+
# * `python -m pkg` puts the working directory FIRST on sys.path, ahead of
|
|
92
|
+
# PYTHONPATH, so a directory named luciazero_agentd next to the caller
|
|
93
|
+
# would shadow the real package.
|
|
94
|
+
#
|
|
95
|
+
# The variable is popped before the daemon does anything, so no provider
|
|
96
|
+
# subprocess inherits it, and the -c program below is a fixed literal: nothing
|
|
97
|
+
# a caller supplies is ever parsed as code.
|
|
98
|
+
LUCIAZERO_AGENTD_PACKAGE="${package_home}"
|
|
99
|
+
export LUCIAZERO_AGENTD_PACKAGE
|
|
100
|
+
LUCIAZERO_ARGV0="${argv0}"
|
|
101
|
+
export LUCIAZERO_ARGV0
|
|
102
|
+
# ${python} is unquoted on purpose -- "py -3" is two words. The value is one of
|
|
103
|
+
# this script's own literals, never anything a caller supplied.
|
|
104
|
+
exec ${python} -c 'import os, sys, runpy
|
|
105
|
+
sys.path = [entry for entry in sys.path if entry not in ("", ".", os.getcwd())]
|
|
106
|
+
sys.path.insert(0, os.environ.pop("LUCIAZERO_AGENTD_PACKAGE"))
|
|
107
|
+
runpy.run_module("luciazero_agentd", run_name="__main__", alter_sys=True)' "$@"
|
package/bin/luciazero.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
// npx luciazero discipline [options] -> local stats report
|
|
10
10
|
// npx luciazero check-update [--json] -> explicit npm version check
|
|
11
11
|
// npx luciazero update -> update detected classic installs
|
|
12
|
+
// npx luciazero bus status [--json] -> Agent Bus queue summary (beta)
|
|
12
13
|
const { spawnSync } = require("node:child_process");
|
|
13
14
|
const path = require("node:path");
|
|
14
15
|
|
|
@@ -20,6 +21,7 @@ const ROUTES = {
|
|
|
20
21
|
discipline: { runtime: process.execPath, script: "bin/discipline-report.js" },
|
|
21
22
|
"check-update": { runtime: process.execPath, script: "bin/update.js", args: ["check"] },
|
|
22
23
|
update: { runtime: process.execPath, script: "bin/update.js", args: ["update"] },
|
|
24
|
+
bus: { runtime: process.execPath, script: "bin/bus.js" },
|
|
23
25
|
};
|
|
24
26
|
|
|
25
27
|
const args = process.argv.slice(2);
|
|
@@ -28,7 +30,7 @@ if (args[0] && !args[0].startsWith("-")) {
|
|
|
28
30
|
if (!Object.prototype.hasOwnProperty.call(ROUTES, args[0])) {
|
|
29
31
|
console.error(
|
|
30
32
|
`luciazero: unknown command '${args[0]}' ` +
|
|
31
|
-
"(install, codex, discipline, check-update, update, uninstall, uninstall-codex)"
|
|
33
|
+
"(install, codex, discipline, check-update, update, bus, uninstall, uninstall-codex)"
|
|
32
34
|
);
|
|
33
35
|
process.exit(64);
|
|
34
36
|
}
|
package/install-codex.sh
CHANGED
|
@@ -19,6 +19,9 @@ CODEX_DIR="${CODEX_HOME:-$HOME/.codex}"
|
|
|
19
19
|
AGENTS_MD="${CODEX_DIR}/AGENTS.md"
|
|
20
20
|
START='<!-- luciazero:start -->'
|
|
21
21
|
END='<!-- luciazero:end -->'
|
|
22
|
+
# written inside the block, under the start marker, when the install had to add
|
|
23
|
+
# a final newline to the user's content to make room for that marker
|
|
24
|
+
ADDED_NL_MARK='<!-- luciazero:added-final-newline -->'
|
|
22
25
|
MANAGED_DIR="${CODEX_DIR}/.luciazero-managed"
|
|
23
26
|
BACKUP_DIR="${CODEX_DIR}/.luciazero-backups"
|
|
24
27
|
|
|
@@ -32,11 +35,18 @@ version_of() {
|
|
|
32
35
|
"${SRC}/package.json" 2>/dev/null || true
|
|
33
36
|
}
|
|
34
37
|
|
|
35
|
-
#
|
|
38
|
+
# A free backup name for $1. Two runs in the same second must not overwrite
|
|
39
|
+
# each other, and a name a symlink already holds is taken too: `-e` follows
|
|
40
|
+
# the name and answers false for a symlink whose target is missing, which
|
|
41
|
+
# would send the `cp` below straight through that symlink and out of the
|
|
42
|
+
# config directory. This is a check, not a reservation -- the name is still
|
|
43
|
+
# free to be taken between the test and the `cp` (roadmap R24). The
|
|
44
|
+
# uninstaller's settings backup reserves its name with `O_CREAT | O_EXCL`
|
|
45
|
+
# instead, which the shell has no portable equivalent for.
|
|
36
46
|
bakpath() {
|
|
37
47
|
B="$1.bak.$(date +%Y%m%d%H%M%S)"
|
|
38
48
|
N=1
|
|
39
|
-
while [ -e "${B}" ]; do B="$1.bak.$(date +%Y%m%d%H%M%S).${N}"; N=$((N+1)); done
|
|
49
|
+
while [ -e "${B}" ] || [ -L "${B}" ]; do B="$1.bak.$(date +%Y%m%d%H%M%S).${N}"; N=$((N+1)); done
|
|
40
50
|
printf '%s' "${B}"
|
|
41
51
|
}
|
|
42
52
|
|
|
@@ -90,26 +100,108 @@ remove_legacy_tree() {
|
|
|
90
100
|
fi
|
|
91
101
|
}
|
|
92
102
|
|
|
103
|
+
# Does $1 end in a newline? A last line without one is content like any other,
|
|
104
|
+
# and `awk` cannot pass it through: print terminates every record it writes, so
|
|
105
|
+
# a rewrite that goes through awk hands such a file back one byte longer.
|
|
106
|
+
ends_with_newline() {
|
|
107
|
+
[ -s "$1" ] && [ -z "$(tail -c 1 "$1")" ]
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
# Write $1 with its marker block removed and every other byte kept, including a
|
|
111
|
+
# last line that carries no newline.
|
|
112
|
+
#
|
|
113
|
+
# The newline directly above the start marker is removed with the block when
|
|
114
|
+
# the block says the installer put it there. The install has to: a start marker
|
|
115
|
+
# only counts on a line of its own, so a file whose last line was unterminated
|
|
116
|
+
# needs one before the block can be appended. That newline is the installer's,
|
|
117
|
+
# not the user's, and nothing in the finished file distinguishes it from a
|
|
118
|
+
# newline the user typed -- so the installer records it, on the line under the
|
|
119
|
+
# start marker, where the markers are its provenance exactly as they are the
|
|
120
|
+
# blank line's. The record is honoured only while the block is still the last
|
|
121
|
+
# thing in the file, which is where the install put it; a user who has moved
|
|
122
|
+
# the block since has moved that newline into the middle of their own text,
|
|
123
|
+
# where it is no longer provably ours and stays.
|
|
124
|
+
strip_marker_block() {
|
|
125
|
+
if ends_with_newline "$1"; then SMB_SRC_NL=1; else SMB_SRC_NL=0; fi
|
|
126
|
+
awk -v s="${START}" -v e="${END}" -v mark="${ADDED_NL_MARK}" -v srcnl="${SMB_SRC_NL}" '
|
|
127
|
+
$0==s {inblock=1; head=1; blockend=NR; next}
|
|
128
|
+
$0==e {inblock=0; blockend=NR; next}
|
|
129
|
+
inblock {if (head && $0==mark) added=1; head=0; blockend=NR; next}
|
|
130
|
+
{n++; keep[n]=$0; lastkept=NR}
|
|
131
|
+
END {
|
|
132
|
+
chop = (added && n > 0 && blockend == NR)
|
|
133
|
+
for (i = 1; i <= n; i++) {
|
|
134
|
+
printf "%s", keep[i]
|
|
135
|
+
if (i < n) printf "\n"
|
|
136
|
+
}
|
|
137
|
+
if (n > 0 && !chop && !(lastkept == NR && srcnl == 0)) printf "\n"
|
|
138
|
+
}
|
|
139
|
+
' "$1"
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
# Exactly one well-formed marker pair, or none at all. Anything else — a start
|
|
143
|
+
# with no end, a second pair, a pair nested inside another — has no defined
|
|
144
|
+
# meaning, and the awk rewrites below would answer it by dropping whatever
|
|
145
|
+
# follows the opening marker. AGENTS.md is the user's file; an ambiguous one is
|
|
146
|
+
# left exactly as it is, down to the byte, rather than repaired by guesswork.
|
|
147
|
+
# Markers count only on a line of their own, which is what the rewrites match.
|
|
148
|
+
marker_block_ok() {
|
|
149
|
+
MB_FILE="$1"
|
|
150
|
+
[ -f "${MB_FILE}" ] || return 0
|
|
151
|
+
MB_S="$(grep -cxF "${START}" "${MB_FILE}" || true)"
|
|
152
|
+
MB_E="$(grep -cxF "${END}" "${MB_FILE}" || true)"
|
|
153
|
+
[ "${MB_S}" = 0 ] && [ "${MB_E}" = 0 ] && return 0
|
|
154
|
+
if [ "${MB_S}" = 1 ] && [ "${MB_E}" = 1 ]; then
|
|
155
|
+
MB_SL="$(grep -nxF "${START}" "${MB_FILE}" | cut -d: -f1)"
|
|
156
|
+
MB_EL="$(grep -nxF "${END}" "${MB_FILE}" | cut -d: -f1)"
|
|
157
|
+
[ "${MB_SL}" -lt "${MB_EL}" ] && return 0
|
|
158
|
+
fi
|
|
159
|
+
return 1
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if ! marker_block_ok "${AGENTS_MD}"; then
|
|
163
|
+
echo "AGENTS.md carries ambiguous Luciazero markers; nothing was installed" >&2
|
|
164
|
+
echo " expected exactly one '${START}' ... '${END}' pair, on their own lines" >&2
|
|
165
|
+
echo " fix ${AGENTS_MD} and run this again" >&2
|
|
166
|
+
exit 1
|
|
167
|
+
fi
|
|
168
|
+
|
|
93
169
|
echo "Installing into ${CODEX_DIR}"
|
|
94
170
|
mkdir -p "${CODEX_DIR}/skills"
|
|
95
171
|
|
|
96
172
|
# 1. doctrine as a marker block in global AGENTS.md (replaced in place on reinstall)
|
|
173
|
+
#
|
|
174
|
+
# This rewrite and the uninstaller's are exact inverses: it strips the marker
|
|
175
|
+
# block and nothing else, and appends the block back with no separator of its
|
|
176
|
+
# own. That is the whole of what makes a full cycle return AGENTS.md to its
|
|
177
|
+
# original bytes.
|
|
178
|
+
#
|
|
179
|
+
# It used to write a blank separator above the start marker, and then trim
|
|
180
|
+
# trailing blank lines here so that separator would not accumulate one line
|
|
181
|
+
# per reinstall. The trim could not tell a blank line this installer had added
|
|
182
|
+
# from one the user wrote, so it spent theirs to pay for ours: a file ending in
|
|
183
|
+
# no blank line came back from a cycle one line longer, and one ending in
|
|
184
|
+
# several came back shorter. The blank line that keeps the doctrine readable
|
|
185
|
+
# now lives INSIDE the block, under the start marker, where the markers are its
|
|
186
|
+
# provenance and the uninstaller takes it away without having to guess. The
|
|
187
|
+
# newline this installer has to add to an unterminated last line is recorded in
|
|
188
|
+
# the same place, for the same reason.
|
|
97
189
|
TMP="$(mktemp)"
|
|
98
190
|
if [ -f "${AGENTS_MD}" ]; then
|
|
99
191
|
cp "${AGENTS_MD}" "$(bakpath "${AGENTS_MD}")"
|
|
100
|
-
|
|
101
|
-
# accumulate one separator blank line per run
|
|
102
|
-
awk -v s="${START}" -v e="${END}" '
|
|
103
|
-
$0==s {inblock=1; next}
|
|
104
|
-
$0==e {inblock=0; next}
|
|
105
|
-
inblock {next}
|
|
106
|
-
NF {for (i=0; i<blank; i++) print ""; blank=0; print; next}
|
|
107
|
-
{blank++}
|
|
108
|
-
' "${AGENTS_MD}" > "${TMP}"
|
|
192
|
+
strip_marker_block "${AGENTS_MD}" > "${TMP}"
|
|
109
193
|
fi
|
|
194
|
+
# A start marker counts only on a line of its own, so content whose last line
|
|
195
|
+
# has no newline needs one before the block can follow it. That newline is the
|
|
196
|
+
# only byte of the user's file this installer changes, and it is recorded under
|
|
197
|
+
# the start marker so the uninstall takes it back with the block.
|
|
198
|
+
if [ -s "${TMP}" ] && ! ends_with_newline "${TMP}"; then ADDED_NL=1; else ADDED_NL=0; fi
|
|
110
199
|
{
|
|
111
|
-
if [ -s "${TMP}" ]; then cat "${TMP}";
|
|
200
|
+
if [ -s "${TMP}" ]; then cat "${TMP}"; fi
|
|
201
|
+
if [ "${ADDED_NL}" = 1 ]; then printf '\n'; fi
|
|
112
202
|
echo "${START}"
|
|
203
|
+
if [ "${ADDED_NL}" = 1 ]; then echo "${ADDED_NL_MARK}"; fi
|
|
204
|
+
echo
|
|
113
205
|
cat "${SRC}/claude/luciazero.md"
|
|
114
206
|
echo "${END}"
|
|
115
207
|
} > "${AGENTS_MD}"
|