luciazero 2.2.0 → 2.3.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/CHANGELOG.md CHANGED
@@ -7,6 +7,77 @@ Versioning: [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.3.0] - 2026-08-16
11
+
12
+ ### Fixed
13
+
14
+ - `test.sh` clears ambient `LUCIAZERO_*` variables before running. An exported
15
+ `LUCIAZERO_VERIFY_CMD` flipped the hook fixtures into exact-match mode, so
16
+ the suite went red on exactly the machines that dogfood the pack
17
+ (`FAIL: stop hook nudged despite verify after edit`) while CI stayed green.
18
+ A new self-test re-runs the fast tier in a child poisoned with every knob the
19
+ hooks read, and quotes the child's own failing line.
20
+ - The verify hook parses under bash 3.2 again — the `/bin/bash` on stock macOS.
21
+ A here-document inside a command substitution (with a quoted expansion and a
22
+ trailing redirection on the same line) breaks that parser, and it rejects the
23
+ **whole file** at load time while pointing at an unrelated later line, so the
24
+ enforcement pack silently did nothing there. The scanner program now lives in
25
+ a variable. `test.sh` rejects the construct in the hooks and, with
26
+ `LZ_BASH32=/path/to/bash-3.2`, parses every script with the real thing.
27
+ - Both hooks call `hashlib.md5(..., usedforsecurity=False)` for their state
28
+ directory name. On a FIPS-enforcing python3 the bare call raised and the
29
+ tracker failed open — silently doing nothing. The digest is unchanged, so
30
+ existing state keys still resolve.
31
+ - CI now parses every repository shell script with Bash 3.2.
32
+
33
+ ### Security
34
+
35
+ - A repository's **committed** `.claude/settings.json` can no longer configure
36
+ Luciazero at all: every `LUCIAZERO_*` key declared there is dropped and the
37
+ hook falls back to its own defaults. `LUCIAZERO_VERIFY_REGEX` and
38
+ `LUCIAZERO_VERIFY_CMD` could make any command count as a verify run,
39
+ `LUCIAZERO_DOC_REGEX='.*'` made every edit look like documentation so nothing
40
+ was ever unverified, and `LUCIAZERO_STRICT_VERIFY_CMD` was a command the stop
41
+ hook would run. A committed `CLAUDE_CONFIG_DIR` is refused for the same
42
+ reason: it could point at a repository-controlled "wired classic install" and
43
+ make every hook copy stand down. Only the default `~/.claude` is treated as
44
+ the user's config directory during the search — honouring `CLAUDE_CONFIG_DIR`
45
+ there let a repository point it at its own `.claude` so the scanner skipped
46
+ the file declaring the key. The search covers the session directory and
47
+ its ancestors — Claude Code merges project settings from the repository root
48
+ and a session's cwd is often a subdirectory — but it is **project scope
49
+ only**: it stops at the repository root, at `CLAUDE_PROJECT_DIR`, and at
50
+ `$HOME`, and never reads the user's own config directory, so a global
51
+ `~/.claude/settings.json` keeps configuring the hook.
52
+ `SessionStart` names the refused keys once. The
53
+ personal, gitignored `.claude/settings.local.json` is untouched, a parse error
54
+ leaves values alone (still fail-open), the lookup runs only in the modes that
55
+ consume a knob, skips a non-regular file (a planted fifo would hang the hook),
56
+ and refuses everything outright on an absurdly large settings file instead of
57
+ parsing it.
58
+ - Channel dedupe is decided from the running copy's own path instead of
59
+ `LUCIAZERO_CHANNEL`. A committed `env` block could set that variable, hand the
60
+ **classic** hook a plugin label, and make it stand itself down — disabling
61
+ enforcement with one line.
62
+ - `install.sh --with-hooks` refuses a python3 older than 3.9 (where hashlib
63
+ gained `usedforsecurity=`) instead of installing hooks that fail open, and
64
+ `--status` reports the version. README states the requirement.
65
+ - CI runs with `permissions: contents: read`, and both workflows pin every
66
+ action to a commit SHA.
67
+
68
+ ### Changed
69
+
70
+ - `shellcheck` is required, not silently skipped, when `CI` or
71
+ `LZ_REQUIRE_LINT` is set — a local green must not disagree with the CI that
72
+ gates the release.
73
+ - README and README.th document the committed-settings refusal and the
74
+ Windows/WSL requirement.
75
+ - Retired the `/luciazero-bootstrap` compatibility alias after the `/ready`
76
+ migration window. Installers remove only untouched Luciazero-owned copies and
77
+ preserve customized user directories.
78
+ - Capped skill discovery descriptions at 40 words and tightened path/offline
79
+ guidance for the closeout, debug, ready, retro, and discipline skills.
80
+
10
81
  ## [2.2.0] - 2026-08-15
11
82
 
12
83
  ### Added
package/README.md CHANGED
@@ -1,26 +1,32 @@
1
- **English** | [ภาษาไทย](README.th.md)
2
-
3
- # Luciazero for Claude Code & Codex CLI
4
-
5
- [![npm](https://img.shields.io/npm/v/luciazero)](https://www.npmjs.com/package/luciazero)
6
- [![CI](https://github.com/ohm41321/luciazero/actions/workflows/ci.yml/badge.svg)](https://github.com/ohm41321/luciazero/actions/workflows/ci.yml)
7
- [![license](https://img.shields.io/github/license/ohm41321/luciazero)](LICENSE)
8
-
9
- <p align="center">
10
- <img src="https://raw.githubusercontent.com/ohm41321/luciazero/main/docs/assets/lucia.png" width="280" alt="Lucia — Luciazero's mascot">
11
- </p>
12
-
13
- Luciazero makes coding agents run a `plan → change → verify → fix` loop instead
14
- of handing back work they merely believe is finished.
1
+ <div align="center">
2
+ <img src="https://raw.githubusercontent.com/ohm41321/luciazero/main/docs/assets/lucia.png" width="220" alt="Lucia — Luciazero's mascot">
3
+ <h1>Luciazero</h1>
4
+ <p>
5
+ <strong>Evidence-first discipline for coding agents.</strong><br>
6
+ <code>plan → change → verify → fix</code>
7
+ </p>
8
+ <p>
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://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>
11
+ <a href="LICENSE"><img src="https://img.shields.io/github/license/ohm41321/luciazero" alt="MIT license"></a>
12
+ </p>
13
+ </div>
14
+
15
+ **English** · [ภาษาไทย](README.th.md)
16
+
17
+ Luciazero is a lightweight discipline layer for coding agents. It works with
18
+ Claude Code, Codex CLI, and compatible skill runtimes through plugin, CLI, or
19
+ skills-only installs.
15
20
 
16
21
  > Done is proven by a command, not by my judgment. If no verification command
17
22
  > exists, that is the first bug.
18
23
 
19
24
  It ships a compact [9-rule doctrine](claude/luciazero.md), 11 on-demand skills,
20
- verification hooks, a risk-routed reviewer, and an eval harness. It is a
21
- discipline layer, not an agent runtime or overnight orchestrator.
25
+ verification hooks, a risk-routed reviewer, and an eval harness. It is designed
26
+ to make completion claims auditable—not to replace an agent runtime or become an
27
+ overnight orchestrator.
22
28
 
23
- ## See it in 15 seconds
29
+ ## See the loop
24
30
 
25
31
  This GIF is driven by the shipped hooks, not a mockup:
26
32
 
@@ -34,7 +40,7 @@ This GIF is driven by the shipped hooks, not a mockup:
34
40
  ✅ verify 3m → the latest check passed three minutes ago
35
41
  ```
36
42
 
37
- ## What it prevents
43
+ ## What it protects
38
44
 
39
45
  | Failure mode | Mechanism |
40
46
  |---|---|
@@ -49,7 +55,7 @@ This GIF is driven by the shipped hooks, not a mockup:
49
55
  Mechanical guarantees run in `test.sh`; behavioral claims are measured by the
50
56
  [eval harness](eval/README.md).
51
57
 
52
- ## Carry unfinished work across agents
58
+ ## Keep work portable
53
59
 
54
60
  `/lucia-relay` transfers decisions and evidence instead of dumping a chat
55
61
  transcript. Session A writes canonical `LUCIA_RELAY.json` plus a generated
@@ -74,7 +80,11 @@ checks—not model-uplift results. See the [method and limits](docs/benchmark.md
74
80
 
75
81
  ## Install
76
82
 
77
- ### Claude Code plugin recommended
83
+ Luciazero supports Claude Code, Codex CLI, and compatible agents. Choose the
84
+ installation path that matches your workflow.
85
+
86
+ <details>
87
+ <summary><strong>Recommended · Claude Code plugin</strong></summary>
78
88
 
79
89
  Carries the doctrine, all skills, reviewer, and verify-tracking hooks:
80
90
 
@@ -87,20 +97,25 @@ Start a repository with `/luciazero:ready`. Plugin skills use the
87
97
  `/luciazero:` prefix. The plugin has no statusline because Claude Code plugins
88
98
  cannot set one.
89
99
 
90
- ### Skills only — any compatible agent
100
+ </details>
101
+
102
+ <details>
103
+ <summary><strong>Skills only · any compatible agent</strong></summary>
91
104
 
92
105
  ```bash
93
106
  npx skills add ohm41321/luciazero
94
107
  ```
95
108
 
96
- This installs the 11 skills plus the temporary `/luciazero-bootstrap`
97
- compatibility alias: no doctrine, reviewer, or hooks.
109
+ This installs the 11 skills: no doctrine, reviewer, or hooks.
98
110
 
99
- ### Classic Claude Code and Codex
111
+ </details>
112
+
113
+ <details>
114
+ <summary><strong>Classic install · Claude Code or Codex CLI</strong></summary>
100
115
 
101
116
  ```bash
102
117
  npx luciazero # Claude Code
103
- npx luciazero --with-hooks # Claude Code + hooks/statusline; needs Python 3
118
+ npx luciazero --with-hooks # Claude Code + hooks/statusline; needs Python 3.9+
104
119
  npx luciazero codex # Codex CLI
105
120
 
106
121
  npx luciazero uninstall
@@ -112,7 +127,9 @@ Classic installs support `--status`; Codex receives the doctrine and skills but
112
127
  not Claude-only hooks/statusline. Installers back up name collisions and remove
113
128
  only exact Luciazero-managed copies on uninstall.
114
129
 
115
- ## Update
130
+ </details>
131
+
132
+ ## Update safely
116
133
 
117
134
  Luciazero never changes classic or Codex files in the background.
118
135
 
@@ -141,7 +158,7 @@ Claude Code can auto-update the plugin at startup: open `/plugin` →
141
158
  marketplaces leave this off by default. For release-only notifications, use
142
159
  GitHub **Watch → Custom → Releases**.
143
160
 
144
- ## The 11 skills
161
+ ## Skills at a glance
145
162
 
146
163
  Run `/ready` first; the rest activate when their moment arrives.
147
164
 
@@ -168,7 +185,7 @@ Risky diffs also pass through one read-only `reviewer` with `security`,
168
185
  `contract`, or `general` focus. Security and contract risk together receive two
169
186
  separate passes.
170
187
 
171
- ## Evidence
188
+ ## Evidence & limitations
172
189
 
173
190
  <!-- BEGIN GENERATED: benchmark-evidence -->
174
191
 
@@ -206,10 +223,11 @@ only one run per arm per task. See the [full benchmark](docs/benchmark.md),
206
223
 
207
224
  <!-- END GENERATED: benchmark-evidence -->
208
225
 
209
- ## Requirements and safety
226
+ ## Security & requirements
210
227
 
211
228
  - Node.js 18+ for the CLI and discipline report.
212
- - Bash for classic installers; Python 3 for hooks and Lucia Relay.
229
+ - Bash for classic installers; Python 3.9+ for hooks and Lucia Relay
230
+ (`install.sh --with-hooks` refuses anything older).
213
231
  - Core installers, hooks, helpers, and graders are offline. Real behavioral
214
232
  evals invoke a model CLI and consume API credit or subscription quota.
215
233
  - Hooks run commands on your machine. Read them before enabling them.
@@ -219,10 +237,18 @@ only one run per arm per task. See the [full benchmark](docs/benchmark.md),
219
237
  - Set `LUCIAZERO_VERIFY_CMD` to the repo's exact fast verify command.
220
238
  - Put `LUCIAZERO_STRICT_VERIFY_CMD` only in personal settings, never in a
221
239
  committed repository config. Strict mode fails open on internal errors.
240
+ - A repository's committed `.claude/settings.json` cannot configure Luciazero
241
+ at all: every `LUCIAZERO_*` key (and `CLAUDE_CONFIG_DIR`) declared there — in
242
+ the session directory or any ancestor up to the repository root — is refused
243
+ and named once at `SessionStart`. Your own settings still configure it: the
244
+ search stops at the repo root and at `$HOME`, and never reads your global
245
+ `~/.claude/settings.json` or gitignored `.claude/settings.local.json`.
246
+ - Windows: the installers and hooks are Bash scripts — run them under WSL.
247
+ `npx luciazero discipline` works in native Node.
222
248
 
223
249
  See [SECURITY.md](SECURITY.md) for the complete trust boundary.
224
250
 
225
- ## Development
251
+ ## Developing Luciazero
226
252
 
227
253
  ```bash
228
254
  ./test.sh --fast # intermediate loop: core doctrine/hooks/report/Relay checks
@@ -247,7 +273,7 @@ More detail:
247
273
  - [Publishing](docs/publishing.md)
248
274
  - [Changelog](CHANGELOG.md)
249
275
 
250
- ## Lucia family & support
276
+ ## Support the project
251
277
 
252
278
  Luciazero shares its mascot with [Lucia](https://lucia-discord-bot.vercel.app),
253
279
  a Thai-language Discord bot. If Luciazero saves you review cycles, you can
package/README.th.md CHANGED
@@ -1,26 +1,32 @@
1
- [English](README.md) | **ภาษาไทย**
2
-
3
- # Luciazero สำหรับ Claude Code และ Codex CLI
4
-
5
- [![npm](https://img.shields.io/npm/v/luciazero)](https://www.npmjs.com/package/luciazero)
6
- [![CI](https://github.com/ohm41321/luciazero/actions/workflows/ci.yml/badge.svg)](https://github.com/ohm41321/luciazero/actions/workflows/ci.yml)
7
- [![license](https://img.shields.io/github/license/ohm41321/luciazero)](LICENSE)
8
-
9
- <p align="center">
10
- <img src="https://raw.githubusercontent.com/ohm41321/luciazero/main/docs/assets/lucia.png" width="280" alt="Lucia — มาสคอตของ Luciazero">
11
- </p>
12
-
13
- Luciazero ทำให้ coding agent รันลูป `วางแผน → แก้ → ตรวจ → แก้ซ้ำ`
14
- แทนการส่งงานกลับมาเพราะคิดเองว่าน่าจะเสร็จแล้ว
1
+ <div align="center">
2
+ <img src="https://raw.githubusercontent.com/ohm41321/luciazero/main/docs/assets/lucia.png" width="220" alt="Lucia — มาสคอตของ Luciazero">
3
+ <h1>Luciazero</h1>
4
+ <p>
5
+ <strong>ชั้นวินัยที่พิสูจน์ได้ด้วยหลักฐานสำหรับ coding agent</strong><br>
6
+ <code>วางแผน → แก้ → ตรวจ → แก้ซ้ำ</code>
7
+ </p>
8
+ <p>
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://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>
11
+ <a href="LICENSE"><img src="https://img.shields.io/github/license/ohm41321/luciazero" alt="MIT license"></a>
12
+ </p>
13
+ </div>
14
+
15
+ [English](README.md) · **ภาษาไทย**
16
+
17
+ Luciazero เป็นชั้นวินัยขนาดเล็กสำหรับ coding agent รองรับ Claude Code,
18
+ Codex CLI และ runtime ที่ใช้ skill ได้ ผ่านการติดตั้งแบบ plugin, CLI
19
+ หรือเฉพาะ skill
15
20
 
16
21
  > งานเสร็จต้องพิสูจน์ด้วยคำสั่ง ไม่ใช่คำตัดสินของ agent
17
22
  > ถ้ายังไม่มีคำสั่งตรวจ นั่นคือบั๊กแรก
18
23
 
19
24
  ภายในมี [doctrine 9 ข้อ](claude/luciazero.md) ที่สั้น, skill แบบเรียกเมื่อจำเป็น
20
25
  11 ตัว, hook ติดตามการ verify, reviewer ที่ route ตามความเสี่ยง และ eval harness
21
- นี่คือชั้นวินัย ไม่ใช่ agent runtime หรือระบบ orchestration สำหรับรันงานข้ามคืน
26
+ นี่คือชั้นวินัยที่ทำให้คำกล่าวว่าเสร็จตรวจสอบได้ ไม่ใช่ agent runtime หรือระบบ
27
+ orchestration สำหรับรันงานข้ามคืน
22
28
 
23
- ## ดูการทำงานใน 15 วินาที
29
+ ## ดูลูปการทำงาน
24
30
 
25
31
  GIF นี้ขับด้วย hook ที่ ship จริง ไม่ใช่ mockup:
26
32
 
@@ -34,7 +40,7 @@ GIF นี้ขับด้วย hook ที่ ship จริง ไม่ใ
34
40
  ✅ verify 3m → การตรวจผ่านเมื่อสามนาทีก่อน
35
41
  ```
36
42
 
37
- ## ป้องกันอะไร
43
+ ## ปกป้องอะไร
38
44
 
39
45
  | ความพัง | กลไกที่จับ |
40
46
  |---|---|
@@ -49,7 +55,7 @@ GIF นี้ขับด้วย hook ที่ ship จริง ไม่ใ
49
55
  กลไกที่ตรวจด้วยเครื่องรันใน `test.sh`; ข้ออ้างด้านพฤติกรรมวัดด้วย
50
56
  [eval harness](eval/README.md)
51
57
 
52
- ## ส่งงานที่ยังไม่เสร็จข้าม agent
58
+ ## ส่งต่องานข้าม agent
53
59
 
54
60
  `/lucia-relay` ส่งต่อการตัดสินใจและหลักฐาน แทนการเท transcript ทั้งแชต
55
61
  Session A สร้าง `LUCIA_RELAY.json` ที่เป็น canonical พร้อม human view
@@ -72,7 +78,11 @@ fingerprint เก่า (5/6) ตัวเลขเหล่านี้เป
72
78
 
73
79
  ## ติดตั้ง
74
80
 
75
- ### Claude Code plugin แนะนำ
81
+ Luciazero รองรับ Claude Code, Codex CLI และ agent ที่ใช้ skill ได้ เลือกช่องทาง
82
+ ที่เหมาะกับ workflow ของคุณ
83
+
84
+ <details>
85
+ <summary><strong>แนะนำ · Claude Code plugin</strong></summary>
76
86
 
77
87
  ได้ doctrine, skill ทั้งหมด, reviewer และ hook ติดตาม verify:
78
88
 
@@ -84,20 +94,25 @@ fingerprint เก่า (5/6) ตัวเลขเหล่านี้เป
84
94
  เริ่ม repo ด้วย `/luciazero:ready` ชื่อ skill แบบ plugin มี prefix
85
95
  `/luciazero:` และไม่มี statusline เพราะ Claude Code plugin ตั้งค่านี้ไม่ได้
86
96
 
87
- ### เฉพาะ skill — agent ที่รองรับ
97
+ </details>
98
+
99
+ <details>
100
+ <summary><strong>เฉพาะ skill · agent ที่รองรับ</strong></summary>
88
101
 
89
102
  ```bash
90
103
  npx skills add ohm41321/luciazero
91
104
  ```
92
105
 
93
- ช่องทางนี้ติดตั้งเฉพาะ skill 11 ตัว พร้อม alias `/luciazero-bootstrap`
94
- ชั่วคราว ไม่มี doctrine, reviewer หรือ hook
106
+ ช่องทางนี้ติดตั้งเฉพาะ skill 11 ตัว ไม่มี doctrine, reviewer หรือ hook
95
107
 
96
- ### Classic Claude Code และ Codex
108
+ </details>
109
+
110
+ <details>
111
+ <summary><strong>Classic install · Claude Code หรือ Codex CLI</strong></summary>
97
112
 
98
113
  ```bash
99
114
  npx luciazero # Claude Code
100
- npx luciazero --with-hooks # Claude Code + hook/statusline; ต้องมี Python 3
115
+ npx luciazero --with-hooks # Claude Code + hook/statusline; ต้องมี Python 3.9+
101
116
  npx luciazero codex # Codex CLI
102
117
 
103
118
  npx luciazero uninstall
@@ -109,7 +124,9 @@ npx luciazero uninstall-codex
109
124
  เฉพาะ Claude Installer สำรองชื่อที่ชน และตอนถอนจะลบเฉพาะสำเนาที่ Luciazero
110
125
  ยืนยันความเป็นเจ้าของได้
111
126
 
112
- ## อัปเดต
127
+ </details>
128
+
129
+ ## อัปเดตอย่างปลอดภัย
113
130
 
114
131
  Luciazero จะไม่แก้ไฟล์ของ classic หรือ Codex อยู่เบื้องหลัง
115
132
 
@@ -137,7 +154,7 @@ Claude Code อัปเดต plugin ตอนเริ่มโปรแกร
137
154
  ภายนอกจะปิดตัวเลือกนี้เป็นค่าเริ่มต้น ถ้าต้องการเพียงการแจ้งเตือน release ให้ใช้
138
155
  GitHub **Watch → Custom → Releases**
139
156
 
140
- ## Skill ทั้ง 11 ตัว
157
+ ## ภาพรวม skill ทั้ง 11 ตัว
141
158
 
142
159
  รัน `/ready` ก่อนหนึ่งครั้ง ที่เหลือใช้เมื่อถึงจังหวะของมัน
143
160
 
@@ -163,7 +180,7 @@ GitHub **Watch → Custom → Releases**
163
180
  Diff เสี่ยงจะผ่าน `reviewer` แบบอ่านอย่างเดียวใน focus `security`, `contract`
164
181
  หรือ `general` ถ้าเสี่ยงทั้ง security และ contract จะตรวจแยกสองรอบ
165
182
 
166
- ## หลักฐาน
183
+ ## หลักฐานและข้อจำกัด
167
184
 
168
185
  <!-- BEGIN GENERATED: benchmark-evidence -->
169
186
 
@@ -201,10 +218,10 @@ pilot มีเพียง 1 run ต่อ arm ต่อ task ดู [ผลเ
201
218
 
202
219
  <!-- END GENERATED: benchmark-evidence -->
203
220
 
204
- ## Requirement และความปลอดภัย
221
+ ## ความปลอดภัยและ requirement
205
222
 
206
223
  - Node.js 18+ สำหรับ CLI และ discipline report
207
- - Bash สำหรับ classic installer; Python 3 สำหรับ hook และ Lucia Relay
224
+ - Bash สำหรับ classic installer; Python 3.9+ สำหรับ hook และ Lucia Relay (`install.sh --with-hooks` ปฏิเสธเวอร์ชันเก่ากว่านี้)
208
225
  - Installer, hook, helper และ grader หลักรัน offline ส่วน behavioral eval จริง
209
226
  เรียก model CLI และใช้เครดิต API หรือโควตา subscription
210
227
  - Hook รันคำสั่งบนเครื่อง ควรอ่านก่อนเปิดใช้
@@ -214,10 +231,18 @@ pilot มีเพียง 1 run ต่อ arm ต่อ task ดู [ผลเ
214
231
  - ตั้ง `LUCIAZERO_VERIFY_CMD` เป็นคำสั่ง verify ระดับเร็วที่ exact ของ repo
215
232
  - ใส่ `LUCIAZERO_STRICT_VERIFY_CMD` ใน personal settings เท่านั้น ห้าม commit ลง
216
233
  config ของ repository; strict mode จะ fail open เมื่อเกิด internal error
234
+ - `.claude/settings.json` ที่ commit ไว้ใน repository ตั้งค่า Luciazero ไม่ได้เลย:
235
+ คีย์ `LUCIAZERO_*` ทุกตัว (รวม `CLAUDE_CONFIG_DIR`) ที่ประกาศไว้ที่นั่น — ทั้งใน
236
+ ไดเรกทอรีที่เปิด session และไดเรกทอรีแม่จนถึง root ของ repo — จะถูกปฏิเสธ
237
+ และแจ้งชื่อคีย์หนึ่งครั้งตอน `SessionStart` ส่วน settings ของคุณเองยังใช้ได้:
238
+ การค้นหยุดที่ root ของ repo และที่ `$HOME` ไม่เคยอ่าน `~/.claude/settings.json`
239
+ หรือ `.claude/settings.local.json` ของคุณ
240
+ - Windows: installer และ hook เป็นสคริปต์ Bash ให้รันใน WSL;
241
+ `npx luciazero discipline` ใช้ได้บน Node ปกติ
217
242
 
218
243
  อ่าน trust boundary ฉบับเต็มใน [SECURITY.md](SECURITY.md)
219
244
 
220
- ## พัฒนา repo นี้
245
+ ## พัฒนา Luciazero
221
246
 
222
247
  ```bash
223
248
  ./test.sh --fast # loop ระหว่างทำ: ตรวจ doctrine/hook/report/Relay ส่วนหลัก
@@ -241,7 +266,7 @@ plugin/npm, eval grader ที่พิสูจน์ตัวเองได
241
266
  - [การ publish](docs/publishing.md)
242
267
  - [Changelog](CHANGELOG.md)
243
268
 
244
- ## ผลิตภัณฑ์ในเครือและสนับสนุน
269
+ ## สนับสนุนโปรเจกต์
245
270
 
246
271
  Luciazero ใช้มาสคอตร่วมกับ [Lucia](https://lucia-discord-bot.vercel.app)
247
272
  Discord bot ภาษาไทย ถ้า Luciazero ช่วยลดรอบ review ได้
@@ -26,7 +26,9 @@ except Exception:
26
26
  model = ((d.get("model") or {}).get("display_name")) or "claude"
27
27
  cwd = ((d.get("workspace") or {}).get("current_dir")) or d.get("cwd") or os.getcwd()
28
28
 
29
- key = hashlib.md5(cwd.encode()).hexdigest()[:12]
29
+ # state-directory name, not a security decision — explicit so a FIPS python3
30
+ # does not raise here and drop the statusline to its minimal fallback line
31
+ key = hashlib.md5(cwd.encode(), usedforsecurity=False).hexdigest()[:12]
30
32
  uid = os.getuid() if hasattr(os, "getuid") else "unknown"
31
33
  base = os.path.join(os.environ.get("TMPDIR", "/tmp"), f"luciazero-verify-state-{uid}")
32
34
  try:
@@ -53,17 +53,11 @@ if [ "${MODE}" = "doctrine" ]; then
53
53
  exit 0
54
54
  fi
55
55
 
56
- # Plugin-channel dedupe (the plugin's hooks.json invokes every mode with
57
- # LUCIAZERO_CHANNEL=plugin): when `install.sh --with-hooks` wiring is ALSO
58
- # present, the classic copy wins and the plugin copy stands down — otherwise
59
- # the stop nudge double-fires and a strict verify runs twice concurrently.
60
- if [ "${LUCIAZERO_CHANNEL:-}" = "plugin" ]; then
61
- CFG="${CLAUDE_CONFIG_DIR:-${HOME:-}/.claude}"
62
- if [ -x "${CFG}/hooks/luciazero-verify.sh" ] \
63
- && grep -qF "${CFG}/hooks/luciazero-verify.sh" "${CFG}/settings.json" 2>/dev/null; then
64
- exit 0
65
- fi
66
- fi
56
+ hook_path() { # canonical path of $1; empty when its directory does not exist
57
+ HP_DIR="$(cd "$(dirname "$1")" 2>/dev/null && pwd -P)" || return 0
58
+ [ -n "${HP_DIR}" ] || return 0
59
+ printf '%s/%s' "${HP_DIR}" "$(basename "$1")"
60
+ }
67
61
 
68
62
  # Hook stdin is always a pipe; when run by hand from a terminal for debugging,
69
63
  # do not hang waiting for EOF that never comes.
@@ -82,7 +76,146 @@ except Exception:
82
76
 
83
77
  CWD="$(pyfield "d.get('cwd')")"
84
78
  [ -n "${CWD}" ] || CWD="${PWD}"
85
- KEY="$(printf '%s' "${CWD}" | python3 -c 'import sys,hashlib;print(hashlib.md5(sys.stdin.buffer.read()).hexdigest()[:12])' 2>/dev/null)" || exit 0
79
+
80
+ # A repository's COMMITTED .claude/settings.json can put anything in its `env`
81
+ # block, and that env reaches this hook — so NO LUCIAZERO_* knob is accepted
82
+ # from that scope. Each one is a way to disable enforcement while the
83
+ # statusline stays green: a widened LUCIAZERO_VERIFY_REGEX (or a
84
+ # LUCIAZERO_VERIFY_CMD pointing at `echo`) makes any command count as a verify
85
+ # run, LUCIAZERO_DOC_REGEX='.*' makes every edit look like documentation so
86
+ # nothing is ever unverified, and LUCIAZERO_STRICT_VERIFY_CMD is a command this
87
+ # hook would RUN at stop. CLAUDE_CONFIG_DIR is refused from that scope too: it
88
+ # moves the config directory the dedupe below trusts.
89
+ #
90
+ # PROJECT scope only. The walk covers the session directory and its ancestors —
91
+ # Claude Code merges project settings from the repository root and a session's
92
+ # cwd is often a subdirectory — but it stops at the repository root, at
93
+ # CLAUDE_PROJECT_DIR, and at $HOME, and it never reads the user's own config
94
+ # directory. Personal settings (global `~/.claude/settings.json`, gitignored
95
+ # `.claude/settings.local.json`) are the user's scope and keep working.
96
+ #
97
+ # Refusal only ever falls back to this file's own defaults, never to a block,
98
+ # and a parse error leaves the configured values untouched. Only the modes that
99
+ # consume a knob pay for the lookup.
100
+ # The scanner program lives in a variable, not a here-document inside
101
+ # $( ): bash 3.2 (still the /bin/bash on macOS) cannot parse that
102
+ # combination and fails the whole file at load time.
103
+ REFUSED_SCAN_PY='import json, os, stat, sys
104
+ LIMIT = 1000000 # a settings file is kilobytes; this runs on every tool call
105
+ MAX_DEPTH = 40 # ancestor walk is bounded, never unbounded I/O
106
+
107
+ def refused(key):
108
+ return isinstance(key, str) and (key.startswith("LUCIAZERO_")
109
+ or key == "CLAUDE_CONFIG_DIR")
110
+
111
+ def keys_in(path):
112
+ try:
113
+ info = os.stat(path)
114
+ except OSError:
115
+ return ()
116
+ # never read a fifo or device planted here: that would hang the hook
117
+ # instead of failing open
118
+ if not stat.S_ISREG(info.st_mode):
119
+ return ()
120
+ if info.st_size > LIMIT:
121
+ # absurd for a settings file: refuse everything rather than parse it
122
+ return ("LUCIAZERO_*",)
123
+ try:
124
+ with open(path, encoding="utf-8") as handle:
125
+ env = json.loads(handle.read(LIMIT)).get("env")
126
+ except Exception:
127
+ return ()
128
+ if not isinstance(env, dict):
129
+ return ()
130
+ return tuple(k for k in env if refused(k))
131
+
132
+ def real(path):
133
+ try:
134
+ return os.path.realpath(path)
135
+ except OSError:
136
+ return path
137
+
138
+ home = real(os.path.expanduser("~"))
139
+ # Only the DEFAULT config directory counts as user scope. CLAUDE_CONFIG_DIR is
140
+ # attacker-reachable: pointed at the project itself, it would mark the
141
+ # repository settings file as user scope and skip the very file that declares
142
+ # it, and the dedupe below would then trust a classic install inside the repo.
143
+ user_config = real(os.path.join(home, ".claude"))
144
+ project_dir = os.environ.get("CLAUDE_PROJECT_DIR")
145
+ project_dir = real(project_dir) if project_dir else None
146
+
147
+ found, seen = [], set()
148
+ directory = real(sys.argv[1] or ".")
149
+ for _ in range(MAX_DEPTH):
150
+ claude_dir = os.path.join(directory, ".claude")
151
+ if directory != home and real(claude_dir) != user_config:
152
+ for key in keys_in(os.path.join(claude_dir, "settings.json")):
153
+ if key not in seen:
154
+ seen.add(key)
155
+ found.append(key)
156
+ if directory == home:
157
+ break
158
+ if os.path.exists(os.path.join(directory, ".git")):
159
+ break # repository root: project scope ends here
160
+ if project_dir is not None and directory == project_dir:
161
+ break
162
+ parent = os.path.dirname(directory)
163
+ if parent == directory:
164
+ break
165
+ directory = parent
166
+ print("\n".join(found))
167
+ '
168
+ REFUSED_ENV_KEYS=""
169
+ case "${MODE}" in
170
+ edit|bash|bash-failure|stop|session)
171
+ REFUSED_ENV_KEYS="$(printf '%s' "${REFUSED_SCAN_PY}" \
172
+ | python3 - "${CWD}" 2>/dev/null || true)"
173
+ ;;
174
+ esac
175
+ if [ -n "${REFUSED_ENV_KEYS}" ]; then
176
+ # `LUCIAZERO_*` is the oversized-file marker: drop every knob this hook reads
177
+ case "${REFUSED_ENV_KEYS}" in
178
+ *'LUCIAZERO_*'*)
179
+ REFUSED_ENV_KEYS='LUCIAZERO_VERIFY_CMD
180
+ LUCIAZERO_VERIFY_REGEX
181
+ LUCIAZERO_DOC_REGEX
182
+ LUCIAZERO_STRICT_VERIFY_CMD
183
+ LUCIAZERO_STRICT_TIMEOUT
184
+ LUCIAZERO_RELAY_STALE_DAYS
185
+ LUCIAZERO_HANDOFF_STALE_DAYS
186
+ CLAUDE_CONFIG_DIR' ;;
187
+ esac
188
+ while IFS= read -r RK; do
189
+ case "${RK}" in
190
+ LUCIAZERO_[A-Z_]*|CLAUDE_CONFIG_DIR) unset "${RK}" 2>/dev/null || true ;;
191
+ esac
192
+ done <<EOF
193
+ ${REFUSED_ENV_KEYS}
194
+ EOF
195
+ fi
196
+
197
+ # Channel dedupe: when `install.sh --with-hooks` wiring is ALSO present, the
198
+ # classic copy wins and every other copy (the plugin's) stands down — otherwise
199
+ # the stop nudge double-fires and a strict verify runs twice concurrently.
200
+ #
201
+ # Decided from this script's own path, never from LUCIAZERO_CHANNEL: an
202
+ # env-driven dedupe let a repository hand the CLASSIC hook a plugin label so it
203
+ # stood itself down. It runs after the refusal above for the same reason — a
204
+ # committed CLAUDE_CONFIG_DIR could otherwise point at a repository-controlled
205
+ # directory holding a "wired classic install", and every copy would stand down.
206
+ CFG="${CLAUDE_CONFIG_DIR:-${HOME:-}/.claude}"
207
+ CLASSIC_HOOK="$(hook_path "${CFG}/hooks/luciazero-verify.sh")"
208
+ SELF_HOOK="$(hook_path "$0")"
209
+ if [ -n "${CLASSIC_HOOK}" ] && [ "${SELF_HOOK}" != "${CLASSIC_HOOK}" ] \
210
+ && [ -x "${CLASSIC_HOOK}" ] \
211
+ && grep -qF "${CFG}/hooks/luciazero-verify.sh" "${CFG}/settings.json" 2>/dev/null; then
212
+ exit 0
213
+ fi
214
+
215
+ # md5 here names a state directory; it is never a security decision. Saying so
216
+ # explicitly keeps the hook alive on a FIPS-enforcing python3, where a bare
217
+ # md5() call raises and the tracker would fail open (silently doing nothing).
218
+ KEY="$(printf '%s' "${CWD}" | python3 -c 'import sys,hashlib;print(hashlib.md5(sys.stdin.buffer.read(), usedforsecurity=False).hexdigest()[:12])' 2>/dev/null)" || exit 0
86
219
  [ -n "${KEY}" ] || exit 0
87
220
  BASE="${TMPDIR:-/tmp}/luciazero-verify-state-$(id -u 2>/dev/null || echo unknown)"
88
221
  # The base name is predictable, so validate ownership/type before touching it.
@@ -396,6 +529,12 @@ print("yes" if e is not None and (v is None or e > v) else "no")' "${STATE}" 2>/
396
529
  [ "${NUDGE}" = no ] && stat_log stop-clean
397
530
  ;;
398
531
  session)
532
+ # A committed settings env block that reconfigures this hook is worth one
533
+ # loud line: the refusal above is silent, and a repository that ships these
534
+ # keys is either mistaken or hostile. Names the keys, never their values.
535
+ if [ -n "${REFUSED_ENV_KEYS}" ]; then
536
+ echo "This repository's committed .claude/settings.json sets $(printf '%s' "${REFUSED_ENV_KEYS}" | tr '\n' ' ')— Luciazero refuses those keys from project scope (they can disable verify tracking or run a command at every stop). Review that env block before trusting this repo."
537
+ fi
399
538
  # SessionStart emits ONE pointer, never the relay contents. A legacy
400
539
  # HANDOFF.md gets a migration warning but is not silently rewritten.
401
540
  CAP="${CWD}/LUCIA_RELAY.json"
package/install-codex.sh CHANGED
@@ -64,6 +64,28 @@ install_tree() {
64
64
  cp -R "${IT_SRC}" "${IT_SNAPSHOT}"
65
65
  }
66
66
 
67
+ # Remove a retired Luciazero skill only when its managed snapshot proves
68
+ # ownership. A customized or colliding directory is user data and must survive
69
+ # the migration with an explicit warning. Symlinked skill parents are refused
70
+ # so the deletion cannot escape the configured directory.
71
+ remove_legacy_tree() {
72
+ LT_DST="$1"; LT_SNAPSHOT="$2"; LT_LABEL="$3"
73
+ if [ ! -e "${LT_DST}" ] && [ ! -L "${LT_DST}" ]; then
74
+ if [ ! -L "$(dirname "${LT_SNAPSHOT}")" ]; then
75
+ rm -rf "${LT_SNAPSHOT}"
76
+ fi
77
+ return
78
+ fi
79
+ if [ -L "$(dirname "${LT_DST}")" ] || [ -L "$(dirname "${LT_SNAPSHOT}")" ]; then
80
+ echo " !! ${LT_LABEL} has a symlinked parent; left untouched" >&2
81
+ elif same_tree "${LT_DST}" "${LT_SNAPSHOT}"; then
82
+ rm -rf "${LT_DST}" "${LT_SNAPSHOT}"
83
+ echo " ok migrated ${LT_LABEL}"
84
+ else
85
+ echo " !! ${LT_LABEL} is customized or not Luciazero-owned; left untouched" >&2
86
+ fi
87
+ }
88
+
67
89
  echo "Installing into ${CODEX_DIR}"
68
90
  mkdir -p "${CODEX_DIR}/skills"
69
91
 
@@ -99,6 +121,12 @@ while IFS= read -r SKILL; do
99
121
  echo " ok skills/${SKILL}"
100
122
  done < <(skill_inventory)
101
123
 
124
+ # v2.3 migration: remove only the untouched /luciazero-bootstrap compatibility
125
+ # alias from older installs. Customized copies remain user data.
126
+ remove_legacy_tree "${CODEX_DIR}/skills/luciazero-bootstrap" \
127
+ "${MANAGED_DIR}/skills/luciazero-bootstrap" \
128
+ "skills/luciazero-bootstrap"
129
+
102
130
  LEGACY_HANDOFF="${CODEX_DIR}/skills/handoff"
103
131
  if [ -f "${LEGACY_HANDOFF}/SKILL.md" ]; then
104
132
  if cmp -s "${SRC}/migrations/handoff-v1.5.0.SKILL.md" "${LEGACY_HANDOFF}/SKILL.md"; then
package/install.sh CHANGED
@@ -100,8 +100,11 @@ if [ "${STATUS_ONLY}" = 1 ]; then
100
100
  else
101
101
  echo " MISS settings.json missing hook entries:${WIRE_MISS} (re-run ./install.sh --with-hooks)"; STATUS_RC=1
102
102
  fi
103
- if command -v python3 >/dev/null 2>&1; then
104
- echo " ok python3 available (the hooks need it)"
103
+ if command -v python3 >/dev/null 2>&1 \
104
+ && python3 -c 'import sys; raise SystemExit(0 if sys.version_info >= (3, 9) else 1)' 2>/dev/null; then
105
+ echo " ok python3 >= 3.9 available (the hooks need it)"
106
+ elif command -v python3 >/dev/null 2>&1; then
107
+ echo " MISS python3 is older than 3.9 — the hooks fail open (doing nothing)"; STATUS_RC=1
105
108
  else
106
109
  # fail-open means a missing python3 breaks the hooks SILENTLY — surface it here
107
110
  echo " MISS python3 not found — the installed hooks are failing open (doing nothing)"; STATUS_RC=1
@@ -158,6 +161,28 @@ install_tree() {
158
161
  cp -R "${IT_SRC}" "${IT_SNAPSHOT}"
159
162
  }
160
163
 
164
+ # Remove a retired Luciazero skill only when its managed snapshot proves
165
+ # ownership. A customized or colliding directory is user data and must survive
166
+ # the migration with an explicit warning. Symlinked skill parents are refused
167
+ # so the deletion cannot escape the configured directory.
168
+ remove_legacy_tree() {
169
+ LT_DST="$1"; LT_SNAPSHOT="$2"; LT_LABEL="$3"
170
+ if [ ! -e "${LT_DST}" ] && [ ! -L "${LT_DST}" ]; then
171
+ if [ ! -L "$(dirname "${LT_SNAPSHOT}")" ]; then
172
+ rm -rf "${LT_SNAPSHOT}"
173
+ fi
174
+ return
175
+ fi
176
+ if [ -L "$(dirname "${LT_DST}")" ] || [ -L "$(dirname "${LT_SNAPSHOT}")" ]; then
177
+ echo " !! ${LT_LABEL} has a symlinked parent; left untouched" >&2
178
+ elif same_tree "${LT_DST}" "${LT_SNAPSHOT}"; then
179
+ rm -rf "${LT_DST}" "${LT_SNAPSHOT}"
180
+ echo " ok migrated ${LT_LABEL}"
181
+ else
182
+ echo " !! ${LT_LABEL} is customized or not Luciazero-owned; left untouched" >&2
183
+ fi
184
+ }
185
+
161
186
  install_file() {
162
187
  IF_SRC="$1"; IF_DST="$2"; IF_SNAPSHOT="$3"; IF_LABEL="$4"
163
188
  if [ -e "${IF_DST}" ] || [ -L "${IF_DST}" ]; then
@@ -191,7 +216,7 @@ install_file "${SRC}/claude/${DOCTRINE}" "${CLAUDE_DIR}/${DOCTRINE}" \
191
216
  "${MANAGED_DIR}/${DOCTRINE}" "${DOCTRINE}"
192
217
  echo " ok ${DOCTRINE}"
193
218
 
194
- # 2. canonical skills plus temporary compatibility aliases
219
+ # 2. canonical skills
195
220
  while IFS= read -r SKILL; do
196
221
  install_tree "${SRC}/skills/${SKILL}" \
197
222
  "${CLAUDE_DIR}/skills/${SKILL}" \
@@ -200,6 +225,12 @@ while IFS= read -r SKILL; do
200
225
  echo " ok skills/${SKILL}"
201
226
  done < <(skill_inventory)
202
227
 
228
+ # v2.3 migration: remove only the untouched /luciazero-bootstrap compatibility
229
+ # alias from older installs. Customized copies remain user data.
230
+ remove_legacy_tree "${CLAUDE_DIR}/skills/luciazero-bootstrap" \
231
+ "${MANAGED_DIR}/skills/luciazero-bootstrap" \
232
+ "skills/luciazero-bootstrap"
233
+
203
234
  # v1.5 migration: remove only an untouched Luciazero /handoff. A customized
204
235
  # skill is user data and stays in place with an explicit warning.
205
236
  LEGACY_HANDOFF="${CLAUDE_DIR}/skills/handoff"
@@ -250,6 +281,10 @@ fi
250
281
  # 6. enforcement pack (opt-in): hooks + statusline wired into settings.json
251
282
  if [ "${WITH_HOOKS}" = 1 ]; then
252
283
  command -v python3 >/dev/null 2>&1 || { echo "FAIL: --with-hooks requires python3" >&2; exit 1; }
284
+ # 3.9 is where hashlib gained usedforsecurity=, which the hooks pass so their
285
+ # md5 state key does not raise under FIPS and silently disable tracking
286
+ python3 -c 'import sys; raise SystemExit(0 if sys.version_info >= (3, 9) else 1)' 2>/dev/null \
287
+ || { echo "FAIL: --with-hooks requires a working python3 >= 3.9" >&2; exit 1; }
253
288
  mkdir -p "${CLAUDE_DIR}/hooks"
254
289
  for H in luciazero-verify.sh luciazero-statusline.sh; do
255
290
  DST="${CLAUDE_DIR}/hooks/${H}"
@@ -332,7 +367,6 @@ echo
332
367
  SKILL_SUMMARY="$(catalog "${SRC}/skills/catalog.txt" | awk 'BEGIN{s=""} {s=s (s ? ", " : "") "/" $0} END{print s}')"
333
368
  AGENT_SUMMARY="$(catalog "${SRC}/claude/agents/catalog.txt" | awk 'BEGIN{s=""} {s=s (s ? ", " : "") $0} END{print s}')"
334
369
  echo "Skills: ${SKILL_SUMMARY}. Agents: ${AGENT_SUMMARY}."
335
- echo "Compatibility alias for one release: /luciazero-bootstrap -> /ready."
336
370
  if [ "${WITH_HOOKS}" = 1 ]; then
337
371
  echo "Enforcement pack installed: verify-tracking hooks + statusline (see settings.json)."
338
372
  else
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "luciazero",
3
- "version": "2.2.0",
4
- "description": "Verification-first discipline for coding agents (Claude Code + Codex CLI): 9-rule doctrine, 11 skills plus a temporary command alias, risk-routed reviewer, fail-open enforcement hooks. npx luciazero installs it.",
3
+ "version": "2.3.0",
4
+ "description": "Verification-first discipline for coding agents (Claude Code + Codex CLI): 9-rule doctrine, 11 skills, risk-routed reviewer, fail-open enforcement hooks. npx luciazero installs it.",
5
5
  "repository": { "type": "git", "url": "git+https://github.com/ohm41321/luciazero.git" },
6
6
  "homepage": "https://github.com/ohm41321/luciazero#readme",
7
7
  "bugs": { "url": "https://github.com/ohm41321/luciazero/issues" },
@@ -1,2 +1 @@
1
- # Temporary compatibility aliases. Remove luciazero-bootstrap after one release.
2
- luciazero-bootstrap
1
+ # Compatibility aliases are retired. Keep this file as the empty alias catalog.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: bisect
3
- description: Pinpoint the first bad commit for a reproducible regression without disturbing the caller's working tree. Use when HEAD is bad, a known revision is good, and one unattended command distinguishes them; supports flaky-endpoint detection and git-bisect skip exit 125.
3
+ description: Pinpoint the first bad commit for a reproducible regression in a safe temporary worktree. Use when HEAD is bad, a known revision is good, and one unattended command distinguishes them; handles flaky endpoints and git-bisect skip exit 125.
4
4
  ---
5
5
 
6
6
  # Bisect — isolate the first bad commit safely
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: debug
3
- description: Hypothesis-driven debugging procedure. Use when a bug is not yet reliably reproduced, a fix attempt failed, debugging has gone two or more iterations without progress, or the user asks "debug this properly" or "ไล่บั๊ก". Not for a first obvious failure whose cause is already visible; reproduce and fix it directly.
3
+ description: Debug a stubborn bug with a deterministic reproduction, hypothesis ledger, one-variable fixes, and a regression test. Use after the first obvious look fails, reproduction is unclear, or a fix attempt failed. Not for routine obvious failures; use for "ไล่บั๊ก".
4
4
  ---
5
5
 
6
6
  # Debug — hypothesis before edit
@@ -23,7 +23,7 @@ Shrink the reproduction — smaller input, fewer flags, one test instead of the
23
23
  **Seed it from recorded experience first.** Before inventing hypotheses, grep the symptom's keywords (error strings, subsystem names) against two files, if they exist:
24
24
 
25
25
  - the repo's lesson ledger `docs/lessons.md` — this project's previously debugged failures;
26
- - the global heuristics file `luciazero-heuristics.md` in the harness config dir (`~/.claude` / `~/.codex`) — cross-repo lessons.
26
+ - the global heuristics file `luciazero-heuristics.md` in the configured harness directory (`${CLAUDE_CONFIG_DIR:-$HOME/.claude}` or `${CODEX_HOME:-$HOME/.codex}`) — cross-repo lessons. Use the configured path instead of assuming `~/.claude` or `~/.codex`.
27
27
 
28
28
  A match becomes **H1** — still verify it with its `proven-by` command; a ledger match is a hypothesis with a head start, not a conclusion. No match, or no files: proceed normally.
29
29
 
@@ -45,7 +45,7 @@ H<N>: <suspected cause> — refutable by: <command / observation> → <result: r
45
45
 
46
46
  ## 5. Close out
47
47
 
48
- - The reproduction becomes a committed regression test: **red before the fix, green after** — run it both ways and quote both results. This proves the fix touched the actual cause. The mechanical form lives in the done skill's `scripts/` dir — run its `scripts/revert-probe.sh "<verify-cmd>"` from wherever that skill is installed (classic: `~/.claude/skills/done/`; plugin and `npx skills` installs keep it next to the done SKILL.md).
48
+ - The reproduction becomes a committed regression test: **red before the fix, green after** — run it both ways and quote both results. This proves the fix touched the actual cause. The mechanical form lives in the done skill's `scripts/` dir — locate that installed skill and run `<this-skill-dir>/scripts/revert-probe.sh "<verify-cmd>"`.
49
49
  - Remove all instrumentation (prints, sleeps, debug flags) — check the diff for it explicitly.
50
50
  - Run the full verify tier, not just the one test.
51
51
  - If the session surfaced something reading the code cannot teach (a footgun, an environment quirk, a disproven approach), run `/retro` so the next session does not pay for this one's dead ends — for a debugged failure specifically, `/retro` records it in `docs/lessons.md` (symptom → cause → proven-by → fix), which is exactly what step 3 reads next time.
@@ -1,16 +1,19 @@
1
1
  ---
2
2
  name: discipline-report
3
- description: Analyze Luciazero's local stop-outcome log for evidence-backed verification habits. Use when the user asks for discipline stats, recurring nudge or strict-block patterns, a local behavior report, or runs `npx luciazero discipline`; supports time/project filters and machine-readable JSON.
3
+ description: Analyze Luciazero stop-outcome logs for evidence-backed verification habits. Use for discipline stats, recurring nudge or strict-block patterns, local behavior reports, or machine-readable JSON.
4
4
  ---
5
5
 
6
6
  # Discipline report — turn local outcomes into evidence
7
7
 
8
- Run:
8
+ Resolve the first available local CLI:
9
9
 
10
10
  ```bash
11
- npx luciazero discipline [--days N] [--project PATH_OR_ID] [--json]
11
+ luciazero discipline [--days N] [--project PATH_OR_ID] [--json]
12
+ node <this-skill-dir>/../../bin/luciazero.js discipline [--days N] [--project PATH_OR_ID] [--json]
12
13
  ```
13
14
 
15
+ Use the first command only when `luciazero` is on PATH; use the second from a source checkout or npm package. If neither local form exists, report that the discipline report is unavailable offline instead of silently invoking `npx`. Use `npx` only when package resolution is explicitly allowed.
16
+
14
17
  The report reads `luciazero-stats.log` from the Claude config directory by default. It accepts current schema-versioned JSON lines and legacy space-delimited records, ignores malformed lines without failing, and never sends data over the network. New enforcement-pack installs also summarize measured turn/Bash wall-clock milliseconds and Bash, verify, and model/user skill invocation counts. Parallel Bash intervals are merged before subtraction. These are aggregates: raw commands and skill names are never persisted.
15
18
 
16
19
  Treat recorded outcomes as observations, not causes. A `nudge` proves an edit lacked a recognized later verify run; it does not prove why. A `strict-block` proves the configured strict command was red. Recommendations derived from patterns must say `likely` unless the log directly records the cause.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: done
3
- description: Closeout ritual before declaring any non-trivial task complete. Use when about to say "done", "finished", "it works now", before opening a PR, when the user asks "is it done?", "wrap it up", "ปิดงาน" — or whenever a change is about to be handed back as complete. Not for trivial single-line answers with no code change.
3
+ description: Run the closeout ritual before handing back non-trivial work: full verification, revert-probe honesty, independent review, and scope reporting. Use before declaring completion, opening a PR, wrapping up a change, or "ปิดงาน".
4
4
  ---
5
5
 
6
6
  # Done — prove it before you say it
@@ -23,7 +23,7 @@ Re-read the final diff as a hostile reviewer. Tests prove what they cover; hunt
23
23
  - **Error paths** — the call fails, the file is missing, the network drops; are errors swallowed?
24
24
  - **Changed contracts** — public API shape, serialized formats, schema, config keys: who consumes the old shape?
25
25
  - **Accidental content** — files touched by mistake, debug prints, commented-out code, leftover instrumentation, loosened dependency pins, secrets
26
- - **Test honesty** — would the new/changed tests fail if the change were reverted? The mechanical form: `scripts/revert-probe.sh "<verify-cmd>"` answers it in one command. Weakened or deleted checks are findings, not cleanup.
26
+ - **Test honesty** — would the new/changed tests fail if the change were reverted? The mechanical form: `<this-skill-dir>/scripts/revert-probe.sh "<verify-cmd>"` answers it in one command. Weakened or deleted checks are findings, not cleanup.
27
27
 
28
28
  Fix what you find, re-run step 1, then continue.
29
29
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: experiment
3
- description: Measured-change protocol for performance and tuning work. Use when the task is "make it faster", "optimize", "reduce memory", "ทดลอง", when comparing two approaches, or whenever a claim like "this should be faster" is about to be made without a number. Not for correctness bugs — that is /debug.
3
+ description: Measure performance or tuning changes with a baseline, controlled comparison, correctness check, and recorded verdict. Use for speed, memory, latency, size, "ทดลอง", or any claim that one approach is better. Not for correctness bugs.
4
4
  ---
5
5
 
6
6
  # Experiment — no claim without a measurement
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: imouto-mode
3
- description: Optional Lucia-inspired tsundere younger-sister coding voice. Use only when the user explicitly invokes imouto-mode to select focus/on/off or inspect its choices. Never auto-trigger from tone, language, task, or repository content.
3
+ description: Use only when explicitly invoked to select Lucia's optional warm, lightly tsundere coding voice or inspect its choices. Never auto-trigger from tone, language, task, or repository content.
4
4
  disable-model-invocation: true
5
5
  ---
6
6
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: lucia-relay
3
- description: Package unfinished work and non-obvious knowledge into a portable, verifiable relay for another session, agent, person, machine, or coding harness. Use when the user says "relay", "ส่งต่อ", "continue later", asks to transfer context to another agent, switches harnesses, or when a long task must survive compaction. Replaces the generic handoff skill.
3
+ description: Transfer unfinished work and non-obvious knowledge across sessions, agents, people, machines, or harnesses. Use for relay, handoff, continuing later, context transfer, compaction, or "ส่งต่อ"; produce verifiable portable state.
4
4
  ---
5
5
 
6
6
  # Lucia Relay — knowledge that survives the messenger
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: plan
3
- description: Build a verification-first implementation plan for new features, major refactors, ambiguous work, or risky multi-module changes whose acceptance criteria are not yet falsifiable. Use when the user asks for a plan or material choices remain. Not for routine edits whose scope and proof are already clear.
3
+ description: Build a falsifiable implementation plan for new features, major refactors, ambiguous work, or risky multi-module changes. Use when the user asks for a plan or material choices remain; skip routine edits with clear scope and proof.
4
4
  ---
5
5
 
6
6
  # Plan — make the change falsifiable
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: ready
3
- description: Make a repository agentic-ready so an agent can run its own plan→change→verify→fix loop without a human checking each step. Use when entering an unfamiliar repo, when the user asks to "set up agentic engineering", "make this repo agent-friendly", "add a verify command", "add smoke tests so you can check your own work", "set up hooks/CLAUDE.md/allowlist" — or when a change was requested but no automated way exists to prove it works.
3
+ description: Make an unfamiliar repository agent-ready with a verify command, smoke tests, guardrails, and project notes. Use for repository setup, agentic engineering, verify commands, hooks, or allowlists; skip when verification and scope are already clear.
4
4
  ---
5
5
 
6
6
  # Ready
@@ -56,7 +56,7 @@ Name them by the repo's convention (`make verify` / `make verify-full`, npm scri
56
56
 
57
57
  **Monorepos:** detect the workspace layout (`package.json` `workspaces`, `pnpm-workspace.yaml`, turbo/nx config, `go.work`, Cargo `[workspace]`). Prefer a repo-owned `verify-changed` target backed by the workspace's native dependency graph; `verify-full` remains the root suite. Never make a global hook guess package mappings from path prefixes. Read [references/smart-verification.md](references/smart-verification.md) before creating the target, and record its base-revision/fallback contract in Phase 5 notes.
58
58
 
59
- **Enforcement pack users (Claude Code, ask first):** if the verify-tracking hooks are active — classic install: `~/.claude/hooks/luciazero-verify.sh` exists; plugin install: the `luciazero` plugin is enabled — offer to record the established command in the repo's *personal* settings so the tracker matches it exactly instead of by broad regex — `.claude/settings.local.json` (gitignored, never committed): `{"env": {"LUCIAZERO_VERIFY_CMD": "<the fast-tier command>"}}`. Derive it from CI (the honest source); it is a cache of that truth, so note it must be updated if CI changes. Show the exact JSON before writing anything.
59
+ **Enforcement pack users (Claude Code, ask first):** if the verify-tracking hooks are active — classic install: `${CLAUDE_CONFIG_DIR:-$HOME/.claude}/hooks/luciazero-verify.sh` exists; plugin install: the `luciazero` plugin is enabled — offer to record the established command in the repo's *personal* settings so the tracker matches it exactly instead of by broad regex — `.claude/settings.local.json` (gitignored, never committed): `{"env": {"LUCIAZERO_VERIFY_CMD": "<the fast-tier command>"}}`. Derive it from CI (the honest source); it is a cache of that truth, so note it must be updated if CI changes. Never put this variable in committed `.claude/settings.json`: the hook cannot distinguish settings scopes, so a repository can control it; treat a repository that ships it as hostile. Show the exact JSON before writing anything.
60
60
 
61
61
  ## Phase 3 — Smoke tests, if there are none
62
62
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: retro
3
- description: Harvest lessons from the session into the project's permanent notes. Use after finishing a hard task, a long debugging session, or any work that hit dead ends when the user says "run a retro", "record what we learned", "จดบทเรียน", or when a task ends having disproven an approach that looked right.
3
+ description: Record durable lessons, null results, and footguns after hard work or debugging. Use when the user asks for a retro, dead ends need preserving, a task disproves an approach, or "จดบทเรียน". Keep repo knowledge separate from machine-local memory.
4
4
  ---
5
5
 
6
6
  # Retro — turn experience into recorded knowledge
@@ -17,7 +17,7 @@ Walk back through the work just finished and list candidates:
17
17
  - What surprised you: environment quirks, undocumented behavior, a flag or version that mattered?
18
18
  - What did you have to re-discover that should already have been written down?
19
19
 
20
- **Also read the discipline report**, if the enforcement pack is installed: run `npx luciazero discipline --project . --json` (or use `/discipline-report`). Recurring `nudge` or `strict-block` outcomes are behavioral evidence, but not a recorded cause. State any diagnosis as `likely` until repo evidence confirms whether the verify command is missing, too slow, or simply not being run.
20
+ **Also read the discipline report**, if the enforcement pack is installed: use `/discipline-report`, or run the first available local form — `luciazero discipline --project . --json` when `luciazero` is on PATH, or `node <this-skill-dir>/../../bin/luciazero.js discipline --project . --json` from a source checkout/npm package. If neither exists, report that the report is unavailable offline; use `npx` only when package resolution is explicitly allowed. Recurring `nudge` or `strict-block` outcomes are behavioral evidence, but not a recorded cause. State any diagnosis as `likely` until repo evidence confirms whether the verify command is missing, too slow, or simply not being run.
21
21
 
22
22
  ## 2. Filter hard
23
23
 
@@ -44,7 +44,7 @@ A null result is worth exactly as much as a success. If the session proved nothi
44
44
  cause: <root cause> | proven-by: `<command>` | fix: <what fixed it> | date: YYYY-MM-DD
45
45
  ```
46
46
 
47
- - **True in every repository** — engineering lessons not tied to this codebase ("intermittent async test: check timezone pinning before touching the test") → append one line to `luciazero-heuristics.md` in the harness config dir (`~/.claude` / `~/.codex`). Hard rules: one line per lesson, same update-in-place/dedup discipline, **cap the file at 100 lines** — when full, drop the weakest entry rather than growing (an unbounded heuristics file becomes context tax, the exact failure this pack exists to prevent). Never personal paths or secrets, even here.
47
+ - **True in every repository** — engineering lessons not tied to this codebase ("intermittent async test: check timezone pinning before touching the test") → append one line to `luciazero-heuristics.md` in the configured harness directory (`${CLAUDE_CONFIG_DIR:-$HOME/.claude}` or `${CODEX_HOME:-$HOME/.codex}`). Hard rules: one line per lesson, same update-in-place/dedup discipline, **cap the file at 100 lines** — when full, drop the weakest entry rather than growing (an unbounded heuristics file becomes context tax, the exact failure this pack exists to prevent). Never personal paths or secrets, even here.
48
48
  - **Only this machine or this user** — local paths, installed tool versions, personal preferences, credential locations → must **never** be committed. If the harness provides a persistent memory directory (Claude Code announces its per-project `memory/` dir and `MEMORY.md` index in context when enabled), write it there and update the index, applying the same format, dedup, and prune rules. If no memory system exists (Codex CLI, or memory disabled), keep only the generalization that is true for anyone who clones the repo — never personal preferences or credential locations, even generalized; if nothing repo-true remains, state the lesson in the retro report instead of writing it anywhere — an honest gap beats a note no harness will ever load.
49
49
 
50
50
  Format, one entry per lesson:
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: show
3
- description: Turn code structure, changes, and verification evidence into the smallest useful visual. Use when the user invokes /show, asks what connects to what, what changed, how a flow works, or what proves a result; use for compact pseudocode, call or component trees, file maps, structural diffs, Mermaid diagrams, and evidence maps, with focused HTML only when simpler forms cannot carry the information.
3
+ description: Visualize code structure, changes, and verification evidence in the smallest useful view. Use for connections, flows, diffs, file maps, Mermaid diagrams, evidence maps, or focused HTML; show facts and label unknowns.
4
4
  ---
5
5
 
6
6
  # Show — make the evidence visible
@@ -32,12 +32,18 @@ same_tree() {
32
32
  && diff -qr "$1" "$2" >/dev/null 2>&1
33
33
  }
34
34
 
35
+ tree_parents_safe() {
36
+ [ ! -L "$(dirname "$1")" ] && [ ! -L "$(dirname "$2")" ]
37
+ }
38
+
35
39
  remove_managed_tree() {
36
- RT_DST="$1"; RT_SNAPSHOT="$2"; RT_SHIPPED="$3"; RT_LABEL="$4"
40
+ RT_DST="$1"; RT_SNAPSHOT="$2"; RT_SHIPPED="$3"; RT_LABEL="$4"; RT_ALLOW_SHIPPED="${5:-1}"
37
41
  if [ ! -e "${RT_DST}" ] && [ ! -L "${RT_DST}" ]; then
38
42
  echo " ok ${RT_LABEL} (already absent)"
43
+ elif ! tree_parents_safe "${RT_DST}" "${RT_SNAPSHOT}"; then
44
+ echo " !! ${RT_LABEL} has a symlinked parent; left untouched" >&2
39
45
  elif same_tree "${RT_DST}" "${RT_SNAPSHOT}" \
40
- || { [ ! -e "${RT_SNAPSHOT}" ] && same_tree "${RT_DST}" "${RT_SHIPPED}"; }; then
46
+ || { [ "${RT_ALLOW_SHIPPED}" = 1 ] && [ ! -e "${RT_SNAPSHOT}" ] && same_tree "${RT_DST}" "${RT_SHIPPED}"; }; then
41
47
  rm -rf "${RT_DST}"
42
48
  echo " ok ${RT_LABEL}"
43
49
  else
@@ -54,6 +60,12 @@ while IFS= read -r SKILL; do
54
60
  "${MANAGED_DIR}/skills/${SKILL}" "${SRC}/skills/${SKILL}" "skills/${SKILL}"
55
61
  done < <(skill_inventory)
56
62
 
63
+ # v2.3 migration: also remove an untouched alias left by older installs.
64
+ remove_managed_tree "${CODEX_DIR}/skills/luciazero-bootstrap" \
65
+ "${MANAGED_DIR}/skills/luciazero-bootstrap" \
66
+ "${SRC}/migrations/luciazero-bootstrap-v2.2.0" \
67
+ "skills/luciazero-bootstrap (retired alias)" 0
68
+
57
69
  AGENT_STAGE_ROOT="$(mktemp -d)"
58
70
  trap 'rm -rf "${AGENT_STAGE_ROOT}"' EXIT
59
71
  while IFS= read -r AGENT_NAME; do
package/uninstall.sh CHANGED
@@ -32,12 +32,18 @@ same_tree() {
32
32
  && diff -qr "$1" "$2" >/dev/null 2>&1
33
33
  }
34
34
 
35
+ tree_parents_safe() {
36
+ [ ! -L "$(dirname "$1")" ] && [ ! -L "$(dirname "$2")" ]
37
+ }
38
+
35
39
  remove_managed_tree() {
36
- RT_DST="$1"; RT_SNAPSHOT="$2"; RT_SHIPPED="$3"; RT_LABEL="$4"
40
+ RT_DST="$1"; RT_SNAPSHOT="$2"; RT_SHIPPED="$3"; RT_LABEL="$4"; RT_ALLOW_SHIPPED="${5:-1}"
37
41
  if [ ! -e "${RT_DST}" ] && [ ! -L "${RT_DST}" ]; then
38
42
  echo " ok ${RT_LABEL} (already absent)"
43
+ elif ! tree_parents_safe "${RT_DST}" "${RT_SNAPSHOT}"; then
44
+ echo " !! ${RT_LABEL} has a symlinked parent; left untouched" >&2
39
45
  elif same_tree "${RT_DST}" "${RT_SNAPSHOT}" \
40
- || { [ ! -e "${RT_SNAPSHOT}" ] && same_tree "${RT_DST}" "${RT_SHIPPED}"; }; then
46
+ || { [ "${RT_ALLOW_SHIPPED}" = 1 ] && [ ! -e "${RT_SNAPSHOT}" ] && same_tree "${RT_DST}" "${RT_SHIPPED}"; }; then
41
47
  rm -rf "${RT_DST}"
42
48
  echo " ok ${RT_LABEL}"
43
49
  else
@@ -72,6 +78,12 @@ while IFS= read -r SKILL; do
72
78
  "${MANAGED_DIR}/skills/${SKILL}" "${SRC}/skills/${SKILL}" "skills/${SKILL}"
73
79
  done < <(skill_inventory)
74
80
 
81
+ # v2.3 migration: also remove an untouched alias left by older installs.
82
+ remove_managed_tree "${CLAUDE_DIR}/skills/luciazero-bootstrap" \
83
+ "${MANAGED_DIR}/skills/luciazero-bootstrap" \
84
+ "${SRC}/migrations/luciazero-bootstrap-v2.2.0" \
85
+ "skills/luciazero-bootstrap (retired alias)" 0
86
+
75
87
  while IFS= read -r AGENT_NAME; do
76
88
  remove_managed_file "${CLAUDE_DIR}/agents/${AGENT_NAME}.md" \
77
89
  "${MANAGED_DIR}/agents/${AGENT_NAME}.md" \