luciazero 2.5.0 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -17
- package/agents/reviewer.md +4 -2
- package/bin/discipline-report.js +31 -2
- package/bin/global.js +191 -0
- package/bin/luciazero.js +10 -2
- package/claude/agents/reviewer.md +4 -2
- package/claude/hooks/luciazero-verify.sh +244 -120
- package/install.sh +14 -0
- package/package.json +1 -1
- package/skills/done/SKILL.md +12 -11
- package/skills/lucia-relay/SKILL.md +20 -18
- package/skills/lucia-relay/scripts/relay.py +195 -22
- package/skills/ready/references/smart-verification.md +1 -1
package/README.md
CHANGED
|
@@ -26,6 +26,11 @@ Luciazero is the verification and handoff layer for Claude Code, Codex CLI, and
|
|
|
26
26
|
compatible skill runtimes. It helps agents prove tests, preserve scope, and
|
|
27
27
|
move unfinished work with evidence.
|
|
28
28
|
|
|
29
|
+
This checkout is the **2.6.0** tree: the manifests and the top
|
|
30
|
+
[changelog](CHANGELOG.md) entry agree, and this document describes that
|
|
31
|
+
source. Its twelve eval fixtures pass offline under `./test.sh`; no real-model
|
|
32
|
+
skills-ablation pilot has run yet.
|
|
33
|
+
|
|
29
34
|
> Done is proven by a command, not by my judgment. If no verification command
|
|
30
35
|
> exists, that is the first bug.
|
|
31
36
|
|
|
@@ -170,6 +175,17 @@ forged artifact cannot downgrade validation. Detached checkouts are supported;
|
|
|
170
175
|
Relay never executes artifact commands. The receiver runs them in its coding
|
|
171
176
|
harness and passes `consume --verified` only after every result matches.
|
|
172
177
|
|
|
178
|
+
The whole exchange is two commands per side. The producer runs
|
|
179
|
+
`luciazero relay draft --write` (add `--recipient cross-machine --base <base>`
|
|
180
|
+
to publish the transfer tag), fills the JSON, then `luciazero relay finalize`,
|
|
181
|
+
which validates, renders the human view, and for cross-machine prints the
|
|
182
|
+
trusted envelope (`--envelope-out <file>` saves it). The receiver runs
|
|
183
|
+
`luciazero relay inspect --trusted-envelope <file>` and, after re-running the
|
|
184
|
+
evidence, `luciazero relay consume --verified --trusted-envelope <file>`. The
|
|
185
|
+
envelope is only trusted when it arrived through an authenticated channel and
|
|
186
|
+
is named explicitly from outside the clone; one shipped beside the artifact is
|
|
187
|
+
refused.
|
|
188
|
+
|
|
173
189
|
<p align="center">
|
|
174
190
|
<img src="https://cdn.jsdelivr.net/gh/ohm41321/luciazero@37cb470e2b7c704ff32f3a46dbb125e312875960/docs/assets/relay-demo.gif" width="720" alt="One session creates a Lucia Relay; another validates it, detects repository drift, re-runs evidence, and consumes it">
|
|
175
191
|
</p>
|
|
@@ -215,16 +231,33 @@ This installs the 13 skills: no doctrine, reviewer, or hooks.
|
|
|
215
231
|
<details>
|
|
216
232
|
<summary><strong>Classic install · Claude Code or Codex CLI</strong></summary>
|
|
217
233
|
|
|
234
|
+
Install the command globally once, without `sudo`:
|
|
235
|
+
|
|
218
236
|
```bash
|
|
219
|
-
npx luciazero
|
|
220
|
-
|
|
221
|
-
npx luciazero codex # Codex CLI
|
|
237
|
+
npx luciazero@latest global-install
|
|
238
|
+
```
|
|
222
239
|
|
|
223
|
-
|
|
224
|
-
|
|
240
|
+
This installs the CLI under `~/.local/npm` and, after confirmation, adds its
|
|
241
|
+
bin directory to your zsh or bash PATH. Start a new shell, then use the global
|
|
242
|
+
command from any directory:
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
luciazero # Claude Code
|
|
246
|
+
luciazero --with-hooks # Claude Code + hooks/statusline; needs Python 3.9+
|
|
247
|
+
luciazero codex # Codex CLI
|
|
248
|
+
|
|
249
|
+
luciazero uninstall # remove the Claude classic files
|
|
250
|
+
luciazero uninstall-codex # remove the Codex classic files
|
|
251
|
+
luciazero global-status # check the global command and PATH
|
|
252
|
+
luciazero global-uninstall # remove the command and its exact PATH block
|
|
225
253
|
```
|
|
226
254
|
|
|
227
|
-
|
|
255
|
+
For a one-off install without keeping the command, `npx luciazero@latest`
|
|
256
|
+
continues to work. Automation may pass `global-install --yes`; interactive use
|
|
257
|
+
asks before installing the package and changing a shell startup file.
|
|
258
|
+
|
|
259
|
+
Pick either plugin or classic for Claude Code: installing both loads every
|
|
260
|
+
skill and the reviewer twice, even though hooks and doctrine deduplicate.
|
|
228
261
|
Classic installs support `--status`; Codex receives the doctrine and skills but
|
|
229
262
|
not Claude-only hooks/statusline. Installers back up name collisions and remove
|
|
230
263
|
only exact Luciazero-managed copies on uninstall.
|
|
@@ -236,10 +269,13 @@ only exact Luciazero-managed copies on uninstall.
|
|
|
236
269
|
Luciazero never changes classic or Codex files in the background.
|
|
237
270
|
|
|
238
271
|
```bash
|
|
239
|
-
|
|
240
|
-
|
|
272
|
+
luciazero check-update # read-only; contacts npm only now
|
|
273
|
+
luciazero update # updates every detected classic/Codex install
|
|
241
274
|
```
|
|
242
275
|
|
|
276
|
+
If you chose the one-off path, use the same commands through
|
|
277
|
+
`npx luciazero@latest` instead.
|
|
278
|
+
|
|
243
279
|
`update` preserves whether the Claude classic install uses hooks, repairs stale
|
|
244
280
|
managed files, starts no fresh install when it cannot find one, and stops on a
|
|
245
281
|
known newer version or malformed version metadata. Start a new agent session
|
|
@@ -289,8 +325,9 @@ technical evidence plain. Plugin users invoke `/luciazero:imouto-mode focus`;
|
|
|
289
325
|
Codex users invoke `$imouto-mode focus`.
|
|
290
326
|
|
|
291
327
|
Risky diffs also pass through one read-only `reviewer` with `security`,
|
|
292
|
-
`contract`, or `general` focus. Security and contract risk together
|
|
293
|
-
|
|
328
|
+
`contract`, or `general` focus. Security and contract risk together get one
|
|
329
|
+
pass that names both focuses, never two. A small diff with no routed risk gets
|
|
330
|
+
no review.
|
|
294
331
|
|
|
295
332
|
## Evidence & limitations
|
|
296
333
|
|
|
@@ -355,9 +392,15 @@ only one run per arm per task. See the [full benchmark](https://github.com/ohm41
|
|
|
355
392
|
evals invoke a model CLI and consume API credit or subscription quota.
|
|
356
393
|
- Hooks run commands on your machine. Read them before enabling them.
|
|
357
394
|
- Hook telemetry stays local in private per-session state and records aggregate
|
|
358
|
-
turn/Bash wall time
|
|
359
|
-
commands, skill names, or paths.
|
|
395
|
+
turn/Bash/verify wall time, redundant-green counts, and Bash, verify, and
|
|
396
|
+
model/user skill counts—never raw commands, skill names, or paths. Older rows
|
|
397
|
+
without verify timing are reported as not measured, not zero.
|
|
360
398
|
- Set `LUCIAZERO_VERIFY_CMD` to the repo's exact fast verify command.
|
|
399
|
+
- `LUCIAZERO_EDIT_DIAG=1` in your own shell makes the hook keep one line per
|
|
400
|
+
edit event in its state directory (tool name, opaque key, whether
|
|
401
|
+
`file_path` was missing, empty or present, its suffix, under cwd or not,
|
|
402
|
+
counted or not — never the path or the content), for finding out what
|
|
403
|
+
re-armed the nudge when no visible edit did.
|
|
361
404
|
- Put `LUCIAZERO_STRICT_VERIFY_CMD` only in personal settings, never in a
|
|
362
405
|
committed repository config. Strict mode fails open on internal errors.
|
|
363
406
|
- A repository's committed `.claude/settings.json` cannot configure Luciazero
|
|
@@ -374,16 +417,28 @@ See [SECURITY.md](https://github.com/ohm41321/luciazero/blob/main/SECURITY.md) f
|
|
|
374
417
|
## Developing Luciazero
|
|
375
418
|
|
|
376
419
|
```bash
|
|
377
|
-
./test.sh --
|
|
378
|
-
./test.sh
|
|
420
|
+
./test.sh --discipline # editing a hook, the report or a skill prompt: ~30 s
|
|
421
|
+
./test.sh --fast # intermediate loop: adds the agentd suite, Relay, bisect, evidence
|
|
422
|
+
./test.sh # closeout/CI: full eval, packaging, and install coverage
|
|
423
|
+
LZ_TEST_TIMINGS=1 ./test.sh --fast # plus one `TIMING gate=<name> seconds=<n>` per gate on stderr
|
|
424
|
+
scripts/test-timings.sh --fast # same run, and keeps stdout/stderr/meta under .test-timings/
|
|
425
|
+
scripts/test-timings.sh --report # median and p95 per gate over the green samples kept so far,
|
|
426
|
+
# and the commits they came from (a warning when they span more than one)
|
|
379
427
|
```
|
|
380
428
|
|
|
381
|
-
The
|
|
382
|
-
|
|
429
|
+
The discipline tier is the loop command for the enforcement pack, the
|
|
430
|
+
discipline report and the prompts: syntax, bash 3.2 and ShellCheck over every
|
|
431
|
+
shipped script, the prompt and doctrine contracts, and the hook state machine,
|
|
432
|
+
nothing else. The fast tier is the default intermediate check for everything
|
|
433
|
+
else; use a more targeted command when changing a component it does not cover. The default
|
|
383
434
|
full tier (also `./test.sh --full`) covers scripts, hook state, Relay, bisect,
|
|
384
435
|
plugin/npm manifests, self-proving eval graders, and sandboxed install →
|
|
385
436
|
reinstall → uninstall for Claude Code and Codex. CI and `/done` use the full
|
|
386
|
-
tier.
|
|
437
|
+
tier. `test.sh` is the dispatcher; the checks live in `tests/gates/*.sh`, one
|
|
438
|
+
file per subsystem, sourced in order — read the gate a change touches. In the
|
|
439
|
+
full tier the tiers, eval, packaging, install and codex-install gates run at
|
|
440
|
+
once, each in its own subshell, with their output replayed in order so it
|
|
441
|
+
reads as the serial run; `LZ_TEST_PARALLEL=0` runs them one at a time.
|
|
387
442
|
|
|
388
443
|
More detail:
|
|
389
444
|
|
package/agents/reviewer.md
CHANGED
|
@@ -36,8 +36,10 @@ Confirm each suspected defect in source before reporting it. Read direct callers
|
|
|
36
36
|
and consumers when they can prove reachability or compatibility. Use cheap,
|
|
37
37
|
read-only commands when decisive. Never edit, commit, or push.
|
|
38
38
|
|
|
39
|
-
Stay inside the diff's causal scope.
|
|
40
|
-
|
|
39
|
+
Stay inside the diff's causal scope. Read the changed hunks and their direct
|
|
40
|
+
callers and consumers, never a repository sweep or a module the diff does not
|
|
41
|
+
reach, and stop once every routed risk is checked. No style or formatting
|
|
42
|
+
findings unless they change behavior. Do not narrate the search. Report every verified
|
|
41
43
|
`blocker`/`major`; report at most three `minor` findings, ranked by impact.
|
|
42
44
|
|
|
43
45
|
## Output
|
package/bin/discipline-report.js
CHANGED
|
@@ -49,7 +49,10 @@ function parseLine(line) {
|
|
|
49
49
|
if (trimmed.startsWith("{")) {
|
|
50
50
|
let row;
|
|
51
51
|
try { row = JSON.parse(trimmed); } catch (_) { return { malformed: true }; }
|
|
52
|
-
|
|
52
|
+
// Schema 2 rows carry turn/Bash/skill aggregates; schema 3 adds how long
|
|
53
|
+
// verify commands ran and how many greens repeated a green with no edit
|
|
54
|
+
// between. Either is read; anything else is a row this reader cannot vouch for.
|
|
55
|
+
if (![2, 3].includes(row.schema) || !["stop-clean", "nudge", "strict-block"].includes(row.event)) {
|
|
53
56
|
return { malformed: true };
|
|
54
57
|
}
|
|
55
58
|
const timestamp = new Date(row.timestamp);
|
|
@@ -58,9 +61,16 @@ function parseLine(line) {
|
|
|
58
61
|
}
|
|
59
62
|
let telemetry = null;
|
|
60
63
|
if (row.telemetry && typeof row.telemetry === "object") {
|
|
64
|
+
const count = (key) => Number.isSafeInteger(row.telemetry[key]) && row.telemetry[key] >= 0;
|
|
61
65
|
const keys = ["turn_ms", "bash_ms", "bash_count", "verify_count", "skill_count"];
|
|
62
|
-
if (keys.every(
|
|
66
|
+
if (keys.every(count)) {
|
|
63
67
|
telemetry = Object.fromEntries(keys.map((key) => [key, row.telemetry[key]]));
|
|
68
|
+
// The verify aggregates are optional on purpose: a schema-2 row never
|
|
69
|
+
// had them, and a schema-3 row missing or garbling them still counts
|
|
70
|
+
// for everything else. `null` means "not measured", never zero.
|
|
71
|
+
const verifyKeys = ["verify_ms", "redundant_green_count"];
|
|
72
|
+
const measured = verifyKeys.every(count);
|
|
73
|
+
for (const key of verifyKeys) telemetry[key] = measured ? row.telemetry[key] : null;
|
|
64
74
|
}
|
|
65
75
|
}
|
|
66
76
|
return {
|
|
@@ -123,6 +133,7 @@ const modes = { regex: 0, exact: 0, strict: 0, unknown: 0 };
|
|
|
123
133
|
const telemetry = {
|
|
124
134
|
measured_turns: 0, turn_ms: 0, bash_ms: 0, non_bash_ms: 0,
|
|
125
135
|
bash_count: 0, verify_count: 0, skill_count: 0,
|
|
136
|
+
verify_measured_turns: 0, verify_ms: 0, redundant_green_count: 0,
|
|
126
137
|
};
|
|
127
138
|
const projects = new Map();
|
|
128
139
|
for (const row of rows) {
|
|
@@ -136,6 +147,11 @@ for (const row of rows) {
|
|
|
136
147
|
telemetry.bash_count += row.telemetry.bash_count;
|
|
137
148
|
telemetry.verify_count += row.telemetry.verify_count;
|
|
138
149
|
telemetry.skill_count += row.telemetry.skill_count;
|
|
150
|
+
if (row.telemetry.verify_ms !== null) {
|
|
151
|
+
telemetry.verify_measured_turns += 1;
|
|
152
|
+
telemetry.verify_ms += row.telemetry.verify_ms;
|
|
153
|
+
telemetry.redundant_green_count += row.telemetry.redundant_green_count;
|
|
154
|
+
}
|
|
139
155
|
}
|
|
140
156
|
const current = projects.get(row.projectId) || {
|
|
141
157
|
project: row.project,
|
|
@@ -167,6 +183,13 @@ if (counts.nudge > 0) {
|
|
|
167
183
|
if (counts["strict-block"] > 0) {
|
|
168
184
|
recommendations.push(`Investigate the configured strict command: it returned red at ${counts["strict-block"]} stop attempt(s).`);
|
|
169
185
|
}
|
|
186
|
+
if (telemetry.redundant_green_count > 0) {
|
|
187
|
+
recommendations.push(
|
|
188
|
+
`Likely: ${telemetry.redundant_green_count} verify run(s) came back green with no edit since the previous green` +
|
|
189
|
+
` (${Math.round(telemetry.verify_ms / telemetry.verify_measured_turns)} ms of verify per measured turn);` +
|
|
190
|
+
" re-run the verify command after an edit, not to reconfirm a result nothing changed."
|
|
191
|
+
);
|
|
192
|
+
}
|
|
170
193
|
if (total > 0 && recommendations.length === 0) {
|
|
171
194
|
recommendations.push("No recurring gap is supported by the selected records.");
|
|
172
195
|
}
|
|
@@ -215,6 +238,12 @@ if (telemetry.measured_turns === 0) {
|
|
|
215
238
|
console.log(` Average Bash time: ${average(telemetry.bash_ms)} ms`);
|
|
216
239
|
console.log(` Average non-Bash: ${average(telemetry.non_bash_ms)} ms`);
|
|
217
240
|
console.log(` Bash / verify / skill calls: ${telemetry.bash_count} / ${telemetry.verify_count} / ${telemetry.skill_count}`);
|
|
241
|
+
if (telemetry.verify_measured_turns > 0) {
|
|
242
|
+
const perTurn = Math.round(telemetry.verify_ms / telemetry.verify_measured_turns);
|
|
243
|
+
console.log(` Verify time: ${perTurn} ms per turn over ${telemetry.verify_measured_turns} turn(s), ${telemetry.redundant_green_count} redundant green(s)`);
|
|
244
|
+
} else {
|
|
245
|
+
console.log(" Verify time: not measured yet (schema-3 hooks record it)");
|
|
246
|
+
}
|
|
218
247
|
}
|
|
219
248
|
console.log("");
|
|
220
249
|
console.log("Top Nudged Repositories:");
|
package/bin/global.js
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const fs = require("node:fs");
|
|
5
|
+
const os = require("node:os");
|
|
6
|
+
const path = require("node:path");
|
|
7
|
+
const readline = require("node:readline");
|
|
8
|
+
const { spawnSync } = require("node:child_process");
|
|
9
|
+
|
|
10
|
+
const START = "# luciazero:start global-npm-path";
|
|
11
|
+
const END = "# luciazero:end global-npm-path";
|
|
12
|
+
const BODY = `${START}\nexport PATH="$HOME/.local/npm/bin:$PATH"\n${END}\n`;
|
|
13
|
+
|
|
14
|
+
function locations(env = process.env) {
|
|
15
|
+
const home = env.HOME || os.homedir();
|
|
16
|
+
if (!path.isAbsolute(home)) throw new Error("HOME must be an absolute path");
|
|
17
|
+
const shell = path.basename(env.SHELL || "");
|
|
18
|
+
const rcName = shell === "zsh" ? ".zshrc" : shell === "bash" ? ".bashrc" : null;
|
|
19
|
+
if (!rcName) throw new Error("supported shells are zsh and bash; set SHELL to the shell whose PATH should be updated");
|
|
20
|
+
return { home, prefix: path.join(home, ".local", "npm"), rc: path.join(home, rcName) };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function readRc(file) {
|
|
24
|
+
try {
|
|
25
|
+
const stat = fs.lstatSync(file);
|
|
26
|
+
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error(`${file} is not a regular file; left untouched`);
|
|
27
|
+
return { text: fs.readFileSync(file, "utf8"), mode: stat.mode & 0o777 };
|
|
28
|
+
} catch (error) {
|
|
29
|
+
if (error.code === "ENOENT") return { text: "", mode: 0o600 };
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function nextRc(current, remove = false) {
|
|
35
|
+
const starts = current.split(START).length - 1;
|
|
36
|
+
const ends = current.split(END).length - 1;
|
|
37
|
+
if (starts !== ends || starts > 1) throw new Error("shell config has malformed Luciazero PATH markers; left untouched");
|
|
38
|
+
if (starts === 1) {
|
|
39
|
+
const begin = current.indexOf(START);
|
|
40
|
+
const finish = current.indexOf("\n", current.indexOf(END, begin));
|
|
41
|
+
const owned = current.slice(begin, finish < 0 ? current.length : finish + 1);
|
|
42
|
+
if (owned !== BODY) throw new Error("shell config has a customized Luciazero PATH block; left untouched");
|
|
43
|
+
if (remove) return current.slice(0, begin) + current.slice(begin + owned.length);
|
|
44
|
+
return current;
|
|
45
|
+
}
|
|
46
|
+
if (remove) return current;
|
|
47
|
+
const separator = current.length && !current.endsWith("\n") ? "\n" : "";
|
|
48
|
+
return current + separator + BODY;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function writeRc(file, text, mode) {
|
|
52
|
+
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
53
|
+
const tmp = path.join(path.dirname(file), `.${path.basename(file)}.luciazero-${process.pid}-${Date.now()}`);
|
|
54
|
+
const fd = fs.openSync(tmp, "wx", mode);
|
|
55
|
+
try {
|
|
56
|
+
// open(2)'s requested mode is filtered by umask. This is a replacement
|
|
57
|
+
// for an existing user file, so restore its exact permission bits before
|
|
58
|
+
// publishing the temporary file with rename(2).
|
|
59
|
+
fs.fchmodSync(fd, mode);
|
|
60
|
+
fs.writeFileSync(fd, text, "utf8");
|
|
61
|
+
fs.fsyncSync(fd);
|
|
62
|
+
fs.closeSync(fd);
|
|
63
|
+
fs.renameSync(tmp, file);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
try { fs.closeSync(fd); } catch {}
|
|
66
|
+
try { fs.unlinkSync(tmp); } catch {}
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function npm(args, env = process.env) {
|
|
72
|
+
const result = spawnSync("npm", args, { stdio: "inherit", env });
|
|
73
|
+
if (result.error) throw new Error(`could not run npm: ${result.error.message}`);
|
|
74
|
+
if (result.status !== 0) throw new Error(`npm exited ${result.status === null ? "without a status" : result.status}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function confirm(question) {
|
|
78
|
+
if (!process.stdin.isTTY) return Promise.resolve(false);
|
|
79
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
80
|
+
return new Promise((resolve) => rl.question(`${question} [y/N] `, (answer) => {
|
|
81
|
+
rl.close();
|
|
82
|
+
resolve(/^y(?:es)?$/i.test(answer.trim()));
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function install(args) {
|
|
87
|
+
if (args.includes("--help")) {
|
|
88
|
+
console.log("Usage: luciazero global-install [--yes]\nInstalls luciazero@latest under ~/.local/npm and adds its bin to PATH.");
|
|
89
|
+
return 0;
|
|
90
|
+
}
|
|
91
|
+
const unknown = args.filter((arg) => arg !== "--yes");
|
|
92
|
+
if (unknown.length) throw new Error(`unknown option: ${unknown[0]}`);
|
|
93
|
+
const place = locations();
|
|
94
|
+
const before = readRc(place.rc);
|
|
95
|
+
const after = nextRc(before.text);
|
|
96
|
+
if (!args.includes("--yes") && !await confirm(`Install luciazero@latest globally in ${place.prefix}?`)) {
|
|
97
|
+
console.error("global install cancelled; nothing changed");
|
|
98
|
+
return 1;
|
|
99
|
+
}
|
|
100
|
+
npm(["install", "--global", "--prefix", place.prefix, "luciazero@latest"]);
|
|
101
|
+
if (after !== before.text) {
|
|
102
|
+
try {
|
|
103
|
+
writeRc(place.rc, after, before.mode);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
`the package was installed, but PATH was not changed: ${error.message}. ` +
|
|
107
|
+
`Run ${path.join(place.prefix, "bin", "luciazero")} directly or retry global-install`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
console.log(`luciazero installed globally in ${place.prefix}`);
|
|
112
|
+
console.log(`PATH recorded in ${place.rc}; start a new shell or source that file`);
|
|
113
|
+
return 0;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function status(args) {
|
|
117
|
+
if (args.includes("--help")) {
|
|
118
|
+
console.log("Usage: luciazero global-status\nChecks the user-owned global command and its shell PATH block.");
|
|
119
|
+
return 0;
|
|
120
|
+
}
|
|
121
|
+
if (args.length) throw new Error(`unknown option: ${args[0]}`);
|
|
122
|
+
const place = locations();
|
|
123
|
+
const command = path.join(place.prefix, "bin", "luciazero");
|
|
124
|
+
let commandOk = false;
|
|
125
|
+
try {
|
|
126
|
+
const commandStat = fs.statSync(command);
|
|
127
|
+
commandOk = commandStat.isFile() && Boolean(commandStat.mode & 0o111);
|
|
128
|
+
} catch {}
|
|
129
|
+
let pathOk = false;
|
|
130
|
+
try {
|
|
131
|
+
const current = readRc(place.rc).text;
|
|
132
|
+
pathOk = nextRc(current) === current;
|
|
133
|
+
} catch (error) {
|
|
134
|
+
console.error(`luciazero: ${error.message}`);
|
|
135
|
+
return 1;
|
|
136
|
+
}
|
|
137
|
+
if (!commandOk || !pathOk) {
|
|
138
|
+
if (!commandOk) console.error(`MISS ${command}`);
|
|
139
|
+
if (!pathOk) console.error(`MISS Luciazero PATH block in ${place.rc}`);
|
|
140
|
+
return 1;
|
|
141
|
+
}
|
|
142
|
+
console.log(`luciazero is installed globally in ${place.prefix}`);
|
|
143
|
+
console.log(`PATH is recorded in ${place.rc}`);
|
|
144
|
+
return 0;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function uninstall(args) {
|
|
148
|
+
if (args.includes("--help")) {
|
|
149
|
+
console.log("Usage: luciazero global-uninstall [--yes]\nRemoves the global npm package and only Luciazero's exact PATH block.");
|
|
150
|
+
return 0;
|
|
151
|
+
}
|
|
152
|
+
const unknown = args.filter((arg) => arg !== "--yes");
|
|
153
|
+
if (unknown.length) throw new Error(`unknown option: ${unknown[0]}`);
|
|
154
|
+
const place = locations();
|
|
155
|
+
const before = readRc(place.rc);
|
|
156
|
+
const after = nextRc(before.text, true);
|
|
157
|
+
if (!args.includes("--yes") && !await confirm(`Uninstall global luciazero from ${place.prefix}?`)) {
|
|
158
|
+
console.error("global uninstall cancelled; nothing changed");
|
|
159
|
+
return 1;
|
|
160
|
+
}
|
|
161
|
+
npm(["uninstall", "--global", "--prefix", place.prefix, "luciazero"]);
|
|
162
|
+
if (after !== before.text) {
|
|
163
|
+
try {
|
|
164
|
+
writeRc(place.rc, after, before.mode);
|
|
165
|
+
} catch (error) {
|
|
166
|
+
throw new Error(
|
|
167
|
+
`the package was removed, but its PATH block remains in ${place.rc}: ${error.message}. ` +
|
|
168
|
+
`Remove only the lines from '${START}' through '${END}'`
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
console.log(`global luciazero removed from ${place.prefix}`);
|
|
173
|
+
return 0;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function main(argv) {
|
|
177
|
+
const [command, ...args] = argv;
|
|
178
|
+
if (command === "install") return install(args);
|
|
179
|
+
if (command === "status") return status(args);
|
|
180
|
+
if (command === "uninstall") return uninstall(args);
|
|
181
|
+
throw new Error(`${command || "command"} is not implemented`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (require.main === module) {
|
|
185
|
+
main(process.argv.slice(2)).then((code) => { process.exitCode = code; }).catch((error) => {
|
|
186
|
+
console.error(`luciazero: ${error.message}`);
|
|
187
|
+
process.exitCode = 1;
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
module.exports = { BODY, locations, nextRc, readRc, writeRc };
|
package/bin/luciazero.js
CHANGED
|
@@ -9,7 +9,9 @@
|
|
|
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 global-install [--yes] -> persistent user-owned CLI
|
|
12
13
|
// npx luciazero bus status [--json] -> Agent Bus queue summary (beta)
|
|
14
|
+
// npx luciazero relay <subcommand> [...] -> Lucia Relay (draft, finalize, inspect, consume, ...)
|
|
13
15
|
const { spawnSync } = require("node:child_process");
|
|
14
16
|
const path = require("node:path");
|
|
15
17
|
|
|
@@ -21,7 +23,13 @@ const ROUTES = {
|
|
|
21
23
|
discipline: { runtime: process.execPath, script: "bin/discipline-report.js" },
|
|
22
24
|
"check-update": { runtime: process.execPath, script: "bin/update.js", args: ["check"] },
|
|
23
25
|
update: { runtime: process.execPath, script: "bin/update.js", args: ["update"] },
|
|
26
|
+
"global-install": { runtime: process.execPath, script: "bin/global.js", args: ["install"] },
|
|
27
|
+
"global-status": { runtime: process.execPath, script: "bin/global.js", args: ["status"] },
|
|
28
|
+
"global-uninstall": { runtime: process.execPath, script: "bin/global.js", args: ["uninstall"] },
|
|
24
29
|
bus: { runtime: process.execPath, script: "bin/bus.js" },
|
|
30
|
+
// python.org installers on Windows ship python.exe, and python3 there is
|
|
31
|
+
// usually the Store alias, so the wrapper names the interpreter that exists.
|
|
32
|
+
relay: { runtime: process.platform === "win32" ? "python" : "python3", script: "skills/lucia-relay/scripts/relay.py" },
|
|
25
33
|
};
|
|
26
34
|
|
|
27
35
|
const args = process.argv.slice(2);
|
|
@@ -30,7 +38,7 @@ if (args[0] && !args[0].startsWith("-")) {
|
|
|
30
38
|
if (!Object.prototype.hasOwnProperty.call(ROUTES, args[0])) {
|
|
31
39
|
console.error(
|
|
32
40
|
`luciazero: unknown command '${args[0]}' ` +
|
|
33
|
-
"(install, codex, discipline, check-update, update, bus, uninstall, uninstall-codex)"
|
|
41
|
+
"(install, codex, discipline, check-update, update, global-install, global-status, global-uninstall, bus, relay, uninstall, uninstall-codex)"
|
|
34
42
|
);
|
|
35
43
|
process.exit(64);
|
|
36
44
|
}
|
|
@@ -48,7 +56,7 @@ if (process.platform === "win32" && selected.runtime === "bash") {
|
|
|
48
56
|
|
|
49
57
|
const result = spawnSync(selected.runtime, [script, ...(selected.args || []), ...args], { stdio: "inherit" });
|
|
50
58
|
if (result.error) {
|
|
51
|
-
console.error(
|
|
59
|
+
console.error(`luciazero: could not run ${path.basename(selected.runtime)}: ${result.error.message}`);
|
|
52
60
|
process.exit(1);
|
|
53
61
|
}
|
|
54
62
|
process.exit(result.status === null ? 1 : result.status);
|
|
@@ -36,8 +36,10 @@ Confirm each suspected defect in source before reporting it. Read direct callers
|
|
|
36
36
|
and consumers when they can prove reachability or compatibility. Use cheap,
|
|
37
37
|
read-only commands when decisive. Never edit, commit, or push.
|
|
38
38
|
|
|
39
|
-
Stay inside the diff's causal scope.
|
|
40
|
-
|
|
39
|
+
Stay inside the diff's causal scope. Read the changed hunks and their direct
|
|
40
|
+
callers and consumers, never a repository sweep or a module the diff does not
|
|
41
|
+
reach, and stop once every routed risk is checked. No style or formatting
|
|
42
|
+
findings unless they change behavior. Do not narrate the search. Report every verified
|
|
41
43
|
`blocker`/`major`; report at most three `minor` findings, ranked by impact.
|
|
42
44
|
|
|
43
45
|
## Output
|