luciazero 2.0.2 → 2.1.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 +30 -0
- package/README.md +20 -11
- package/README.th.md +19 -11
- package/agents/reviewer.md +49 -0
- package/bin/discipline-report.js +35 -0
- package/claude/hooks/hooks.json +36 -0
- package/claude/hooks/luciazero-statusline.sh +17 -2
- package/claude/hooks/luciazero-verify.sh +157 -18
- package/claude/luciazero.md +2 -2
- package/install-codex.sh +6 -2
- package/install.sh +17 -7
- package/package.json +3 -2
- package/skills/aliases.txt +2 -0
- package/skills/catalog.txt +2 -1
- package/skills/debug/SKILL.md +1 -1
- package/skills/discipline-report/SKILL.md +3 -1
- package/skills/done/SKILL.md +1 -1
- package/skills/done/scripts/revert-probe.sh +1 -1
- package/skills/luciazero-bootstrap/SKILL.md +5 -104
- package/skills/plan/SKILL.md +1 -1
- package/skills/ready/SKILL.md +109 -0
- package/skills/{luciazero-bootstrap → ready}/scripts/detect.sh +1 -1
- package/skills/show/SKILL.md +133 -0
- package/skills/show/agents/openai.yaml +4 -0
- package/uninstall-codex.sh +5 -1
- package/uninstall.sh +6 -2
- /package/skills/{luciazero-bootstrap → ready}/references/smart-verification.md +0 -0
|
@@ -5,8 +5,13 @@
|
|
|
5
5
|
# ("done is proven by a command") at the exact moment it is most violated.
|
|
6
6
|
#
|
|
7
7
|
# Subcommands (wired in settings.json):
|
|
8
|
+
# prompt — UserPromptSubmit: start privacy-preserving turn telemetry
|
|
9
|
+
# bash-start — PreToolUse on Bash: start shell-command timing
|
|
8
10
|
# edit — PostToolUse on Edit|Write|NotebookEdit : record "an edit happened"
|
|
9
|
-
# bash — PostToolUse on Bash
|
|
11
|
+
# bash — PostToolUse on Bash: record duration, verify runs, and status
|
|
12
|
+
# bash-failure — PostToolUseFailure on Bash: record failed commands
|
|
13
|
+
# skill — PostToolUse on Skill: count model-invoked skills
|
|
14
|
+
# skill-prompt — UserPromptExpansion: count user-invoked slash skills
|
|
10
15
|
# stop — Stop : warn once if edits are unverified
|
|
11
16
|
# session — SessionStart : point at an existing Lucia Relay
|
|
12
17
|
# doctrine— SessionStart (plugin installs only) : emit the doctrine as context
|
|
@@ -29,8 +34,8 @@
|
|
|
29
34
|
# stop hook appends one schema-versioned JSON line per stop outcome
|
|
30
35
|
# (stop-clean / nudge / strict-block) to luciazero-stats.log in the harness
|
|
31
36
|
# config dir — local only, capped at ~250 lines, fail-open. It records a
|
|
32
|
-
# privacy-preserving project hash
|
|
33
|
-
# command. Uninstall keeps it
|
|
37
|
+
# privacy-preserving project hash, verify mode, and aggregate latency/counts;
|
|
38
|
+
# never the project path, command, or skill name. Uninstall keeps it.
|
|
34
39
|
set -u
|
|
35
40
|
|
|
36
41
|
MODE="${1:-}"
|
|
@@ -79,8 +84,54 @@ CWD="$(pyfield "d.get('cwd')")"
|
|
|
79
84
|
[ -n "${CWD}" ] || CWD="${PWD}"
|
|
80
85
|
KEY="$(printf '%s' "${CWD}" | python3 -c 'import sys,hashlib;print(hashlib.md5(sys.stdin.buffer.read()).hexdigest()[:12])' 2>/dev/null)" || exit 0
|
|
81
86
|
[ -n "${KEY}" ] || exit 0
|
|
82
|
-
|
|
87
|
+
BASE="${TMPDIR:-/tmp}/luciazero-verify-state-$(id -u 2>/dev/null || echo unknown)"
|
|
88
|
+
# The base name is predictable, so validate ownership/type before touching it.
|
|
89
|
+
# A hostile pre-created symlink or directory makes the hook fail open.
|
|
90
|
+
python3 - "${BASE}" <<'PY' 2>/dev/null || exit 0
|
|
91
|
+
import os, stat, sys
|
|
92
|
+
path = sys.argv[1]
|
|
93
|
+
try:
|
|
94
|
+
info = os.lstat(path)
|
|
95
|
+
except FileNotFoundError:
|
|
96
|
+
os.mkdir(path, 0o700)
|
|
97
|
+
info = os.lstat(path)
|
|
98
|
+
if not stat.S_ISDIR(info.st_mode) or stat.S_ISLNK(info.st_mode):
|
|
99
|
+
raise SystemExit(1)
|
|
100
|
+
if hasattr(os, "getuid") and info.st_uid != os.getuid():
|
|
101
|
+
raise SystemExit(1)
|
|
102
|
+
os.chmod(path, 0o700)
|
|
103
|
+
PY
|
|
104
|
+
STATE="${BASE}/${KEY}"
|
|
83
105
|
mkdir -p "${STATE}" 2>/dev/null || exit 0
|
|
106
|
+
chmod 700 "${STATE}" 2>/dev/null || exit 0
|
|
107
|
+
SESSION_RAW="$(pyfield "d.get('session_id')")"
|
|
108
|
+
[ -n "${SESSION_RAW}" ] || SESSION_RAW="parent-${PPID}"
|
|
109
|
+
SESSION_KEY="$(printf '%s' "${SESSION_RAW}" | python3 -c 'import hashlib,sys; print(hashlib.sha256(sys.stdin.buffer.read()).hexdigest()[:16])' 2>/dev/null)" || exit 0
|
|
110
|
+
TELEMETRY="${STATE}/telemetry/${SESSION_KEY}"
|
|
111
|
+
|
|
112
|
+
tool_key() { # stable opaque key; raw tool input never leaves temporary state
|
|
113
|
+
RAW="$(pyfield "d.get('tool_use_id') or d.get('tool_input', {}).get('command') or d.get('tool_input', {}).get('skill') or d.get('command_name') or d.get('prompt') or d.get('command')")"
|
|
114
|
+
[ -n "${RAW}" ] || RAW=unknown
|
|
115
|
+
printf '%s' "${RAW}" | python3 -c 'import hashlib,sys; print(hashlib.sha256(sys.stdin.buffer.read()).hexdigest()[:16])' 2>/dev/null
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
now_ms() {
|
|
119
|
+
python3 -c 'import time; print(int(time.time() * 1000))' 2>/dev/null
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
record_strict_telemetry() { # record_strict_telemetry <start-ms>
|
|
123
|
+
STRICT_END_MS="$(now_ms || true)"
|
|
124
|
+
case "${1:-}:${STRICT_END_MS}" in
|
|
125
|
+
*[!0-9:]*|:|*:|*::* ) return ;;
|
|
126
|
+
esac
|
|
127
|
+
[ "${STRICT_END_MS}" -ge "$1" ] 2>/dev/null || return
|
|
128
|
+
mkdir -p "${TELEMETRY}/bash_count" "${TELEMETRY}/bash_intervals" \
|
|
129
|
+
"${TELEMETRY}/verify_count" 2>/dev/null || return
|
|
130
|
+
: > "${TELEMETRY}/bash_count/strict-gate" 2>/dev/null || true
|
|
131
|
+
: > "${TELEMETRY}/verify_count/strict-gate" 2>/dev/null || true
|
|
132
|
+
printf '%s %s\n' "$1" "${STRICT_END_MS}" \
|
|
133
|
+
> "${TELEMETRY}/bash_intervals/strict-gate" 2>/dev/null || true
|
|
134
|
+
}
|
|
84
135
|
|
|
85
136
|
stat_log() { # stat_log <event> — discipline stats; fail-open, capped
|
|
86
137
|
SDIR="${CLAUDE_CONFIG_DIR:-${HOME:-}/.claude}"
|
|
@@ -88,9 +139,9 @@ stat_log() { # stat_log <event> — discipline stats; fail-open, capped
|
|
|
88
139
|
VMODE=regex
|
|
89
140
|
[ -n "${VERIFY_CMD:-}" ] && VMODE=exact
|
|
90
141
|
[ -n "${LUCIAZERO_STRICT_VERIFY_CMD:-}" ] && VMODE=strict
|
|
91
|
-
python3 - "${SFILE}" "${CWD}" "$1" "${VMODE}" <<'PY' 2>/dev/null || true
|
|
142
|
+
python3 - "${SFILE}" "${CWD}" "$1" "${VMODE}" "${TELEMETRY}" <<'PY' 2>/dev/null || true
|
|
92
143
|
import datetime, hashlib, json, os, sys
|
|
93
|
-
path, cwd, event, mode = sys.argv[1:]
|
|
144
|
+
path, cwd, event, mode, telemetry_dir = sys.argv[1:]
|
|
94
145
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
95
146
|
real = os.path.realpath(cwd)
|
|
96
147
|
row = {
|
|
@@ -101,6 +152,47 @@ row = {
|
|
|
101
152
|
"project": os.path.basename(real) or "(root)",
|
|
102
153
|
"verify_mode": mode,
|
|
103
154
|
}
|
|
155
|
+
def read_int(path):
|
|
156
|
+
try:
|
|
157
|
+
value = int(open(path, encoding="utf-8").read().strip())
|
|
158
|
+
return value if value >= 0 else None
|
|
159
|
+
except (OSError, ValueError):
|
|
160
|
+
return None
|
|
161
|
+
def count_files(name):
|
|
162
|
+
try:
|
|
163
|
+
return sum(os.path.isfile(os.path.join(telemetry_dir, name, item))
|
|
164
|
+
for item in os.listdir(os.path.join(telemetry_dir, name)))
|
|
165
|
+
except OSError:
|
|
166
|
+
return 0
|
|
167
|
+
start = read_int(os.path.join(telemetry_dir, "turn_start_ms"))
|
|
168
|
+
if start is not None:
|
|
169
|
+
now = int(datetime.datetime.now(datetime.timezone.utc).timestamp() * 1000)
|
|
170
|
+
intervals = []
|
|
171
|
+
try:
|
|
172
|
+
interval_dir = os.path.join(telemetry_dir, "bash_intervals")
|
|
173
|
+
for item in os.listdir(interval_dir):
|
|
174
|
+
try:
|
|
175
|
+
a, b = map(int, open(os.path.join(interval_dir, item), encoding="utf-8").read().split())
|
|
176
|
+
except (OSError, ValueError):
|
|
177
|
+
continue
|
|
178
|
+
if 0 <= a <= b:
|
|
179
|
+
intervals.append((max(start, a), min(now, b)))
|
|
180
|
+
except OSError:
|
|
181
|
+
pass
|
|
182
|
+
merged = []
|
|
183
|
+
for a, b in sorted((a, b) for a, b in intervals if a <= b):
|
|
184
|
+
if not merged or a > merged[-1][1]:
|
|
185
|
+
merged.append([a, b])
|
|
186
|
+
else:
|
|
187
|
+
merged[-1][1] = max(merged[-1][1], b)
|
|
188
|
+
bash_ms = sum(b - a for a, b in merged)
|
|
189
|
+
row["telemetry"] = {
|
|
190
|
+
"turn_ms": max(0, now - start),
|
|
191
|
+
"bash_ms": bash_ms,
|
|
192
|
+
"bash_count": count_files("bash_count"),
|
|
193
|
+
"verify_count": count_files("verify_count"),
|
|
194
|
+
"skill_count": count_files("skill_count"),
|
|
195
|
+
}
|
|
104
196
|
with open(path, "a", encoding="utf-8") as handle:
|
|
105
197
|
handle.write(json.dumps(row, separators=(",", ":")) + "\n")
|
|
106
198
|
with open(path, encoding="utf-8", errors="replace") as handle:
|
|
@@ -123,6 +215,18 @@ VERIFY_RE="${LUCIAZERO_VERIFY_REGEX:-verify|test\.sh|pytest|npm (run )?test|pnpm
|
|
|
123
215
|
VERIFY_CMD="${LUCIAZERO_VERIFY_CMD:-}"
|
|
124
216
|
|
|
125
217
|
case "${MODE}" in
|
|
218
|
+
prompt)
|
|
219
|
+
# Per-turn scratch data is ephemeral. Persistent rows keep aggregates only.
|
|
220
|
+
rm -rf "${TELEMETRY}" 2>/dev/null || exit 0
|
|
221
|
+
mkdir -p "${TELEMETRY}" 2>/dev/null || exit 0
|
|
222
|
+
now_ms > "${TELEMETRY}/turn_start_ms" 2>/dev/null || true
|
|
223
|
+
;;
|
|
224
|
+
bash-start)
|
|
225
|
+
TK="$(tool_key)" || exit 0
|
|
226
|
+
mkdir -p "${TELEMETRY}/bash_start_ms" "${TELEMETRY}/bash_count" 2>/dev/null || exit 0
|
|
227
|
+
now_ms > "${TELEMETRY}/bash_start_ms/${TK}" 2>/dev/null || true
|
|
228
|
+
: > "${TELEMETRY}/bash_count/${TK}" 2>/dev/null || true
|
|
229
|
+
;;
|
|
126
230
|
edit)
|
|
127
231
|
# Documentation writes do not re-arm the nudge: the closeout skills
|
|
128
232
|
# Closeout skills write docs AFTER the final green verify. Relay's JSON is
|
|
@@ -141,7 +245,20 @@ case "${MODE}" in
|
|
|
141
245
|
;;
|
|
142
246
|
esac
|
|
143
247
|
;;
|
|
144
|
-
bash)
|
|
248
|
+
bash|bash-failure)
|
|
249
|
+
TK="$(tool_key)" || TK=unknown
|
|
250
|
+
mkdir -p "${TELEMETRY}/bash_count" "${TELEMETRY}/bash_intervals" 2>/dev/null || true
|
|
251
|
+
: > "${TELEMETRY}/bash_count/${TK}" 2>/dev/null || true
|
|
252
|
+
START_MS="$(cat "${TELEMETRY}/bash_start_ms/${TK}" 2>/dev/null || true)"
|
|
253
|
+
END_MS="$(now_ms || true)"
|
|
254
|
+
case "${START_MS}:${END_MS}" in
|
|
255
|
+
*[!0-9:]*|:|*:|*::* ) : ;;
|
|
256
|
+
*)
|
|
257
|
+
if [ "${END_MS}" -ge "${START_MS}" ] 2>/dev/null; then
|
|
258
|
+
printf '%s %s\n' "${START_MS}" "${END_MS}" > "${TELEMETRY}/bash_intervals/${TK}" 2>/dev/null || true
|
|
259
|
+
fi
|
|
260
|
+
;;
|
|
261
|
+
esac
|
|
145
262
|
CMD="$(pyfield "d.get('tool_input', {}).get('command')")"
|
|
146
263
|
IS_VERIFY=no
|
|
147
264
|
if [ -n "${CMD}" ]; then
|
|
@@ -155,15 +272,31 @@ case "${MODE}" in
|
|
|
155
272
|
fi
|
|
156
273
|
fi
|
|
157
274
|
if [ "${IS_VERIFY}" = yes ]; then
|
|
158
|
-
|
|
159
|
-
|
|
275
|
+
mkdir -p "${TELEMETRY}/verify_count" 2>/dev/null || true
|
|
276
|
+
: > "${TELEMETRY}/verify_count/${TK}" 2>/dev/null || true
|
|
277
|
+
# Best-effort red/green from the tool response; failure hooks are red.
|
|
278
|
+
if [ "${MODE}" = bash-failure ]; then
|
|
279
|
+
STATUS=fail
|
|
280
|
+
else
|
|
281
|
+
STATUS="$(pyfield "(lambda r, c=None: (lambda c: 'ok' if c == 0 else ('fail' if isinstance(c, int) else ('fail' if r.get('is_error') is True else 'ran')))(r.get('exit_code', r.get('exitCode'))))(d.get('tool_response') or {})")"
|
|
282
|
+
fi
|
|
160
283
|
printf '%s\n' "${STATUS:-ran}" > "${STATE}/last_verify"
|
|
161
|
-
#
|
|
162
|
-
#
|
|
163
|
-
printf '%s
|
|
284
|
+
# Keep only an opaque digest for strict-gate equality; raw commands may
|
|
285
|
+
# contain paths or secrets and must never persist in shared state.
|
|
286
|
+
printf '%s' "${CMD}" | python3 -c 'import hashlib,sys; print(hashlib.sha256(sys.stdin.buffer.read()).hexdigest())' \
|
|
287
|
+
> "${STATE}/last_verify_cmd_hash" 2>/dev/null || true
|
|
164
288
|
rm -f "${STATE}/nudged"
|
|
165
289
|
fi
|
|
166
290
|
;;
|
|
291
|
+
skill|skill-prompt)
|
|
292
|
+
if [ "${MODE}" = skill-prompt ]; then
|
|
293
|
+
EXPANSION_TYPE="$(pyfield "d.get('expansion_type')")"
|
|
294
|
+
[ "${EXPANSION_TYPE}" = slash_command ] || exit 0
|
|
295
|
+
fi
|
|
296
|
+
TK="$(tool_key)" || TK=unknown
|
|
297
|
+
mkdir -p "${TELEMETRY}/skill_count" 2>/dev/null || true
|
|
298
|
+
: > "${TELEMETRY}/skill_count/${TK}" 2>/dev/null || true
|
|
299
|
+
;;
|
|
167
300
|
stop)
|
|
168
301
|
# Never re-block a continuation that a stop hook itself caused
|
|
169
302
|
ACTIVE="$(pyfield "d.get('stop_hook_active')")"
|
|
@@ -178,6 +311,7 @@ case "${MODE}" in
|
|
|
178
311
|
# break both the fail-open and the never-re-block guarantees.
|
|
179
312
|
JSON_OK="$(printf '%s' "${IN}" | python3 -c 'import json,sys; json.load(sys.stdin); print("yes")' 2>/dev/null || echo no)"
|
|
180
313
|
if [ -n "${STRICT_CMD}" ] && [ "${JSON_OK}" = yes ]; then
|
|
314
|
+
STRICT_START_MS="$(now_ms || true)"
|
|
181
315
|
OUT="$(python3 -c '
|
|
182
316
|
import os, subprocess, sys
|
|
183
317
|
state, cwd, cmd, timeout = sys.argv[1:5]
|
|
@@ -192,13 +326,14 @@ def read(name):
|
|
|
192
326
|
except OSError:
|
|
193
327
|
return ""
|
|
194
328
|
e, v = m("last_edit"), m("last_verify")
|
|
195
|
-
# Fast path only for a green
|
|
196
|
-
#
|
|
329
|
+
# Fast path only for a green whose command digest exactly matches. A
|
|
330
|
+
# broad-regex green from a mere read of the test file must
|
|
197
331
|
# not disarm a gate whose promise is "actually runs the command".
|
|
198
|
-
vcmd = read("
|
|
332
|
+
vcmd = read("last_verify_cmd_hash")
|
|
333
|
+
cmd_hash = __import__("hashlib").sha256(cmd.encode()).hexdigest()
|
|
199
334
|
if (v is not None and read("last_verify") == "ok"
|
|
200
335
|
and (e is None or e <= v)
|
|
201
|
-
and
|
|
336
|
+
and vcmd == cmd_hash):
|
|
202
337
|
print("green"); sys.exit(0)
|
|
203
338
|
try:
|
|
204
339
|
r = subprocess.run(cmd, shell=True, cwd=cwd or None, timeout=float(timeout),
|
|
@@ -218,14 +353,18 @@ else:
|
|
|
218
353
|
case "${OUT%%$'\n'*}" in
|
|
219
354
|
green) stat_log stop-clean; exit 0 ;;
|
|
220
355
|
ok)
|
|
356
|
+
record_strict_telemetry "${STRICT_START_MS}"
|
|
221
357
|
printf 'ok\n' > "${STATE}/last_verify" 2>/dev/null || true
|
|
222
|
-
printf '%s
|
|
358
|
+
printf '%s' "${STRICT_CMD}" | python3 -c 'import hashlib,sys; print(hashlib.sha256(sys.stdin.buffer.read()).hexdigest())' \
|
|
359
|
+
> "${STATE}/last_verify_cmd_hash" 2>/dev/null || true
|
|
223
360
|
rm -f "${STATE}/nudged"
|
|
224
361
|
stat_log stop-clean
|
|
225
362
|
exit 0 ;;
|
|
226
363
|
red)
|
|
364
|
+
record_strict_telemetry "${STRICT_START_MS}"
|
|
227
365
|
printf 'fail\n' > "${STATE}/last_verify" 2>/dev/null || true
|
|
228
|
-
printf '%s
|
|
366
|
+
printf '%s' "${STRICT_CMD}" | python3 -c 'import hashlib,sys; print(hashlib.sha256(sys.stdin.buffer.read()).hexdigest())' \
|
|
367
|
+
> "${STATE}/last_verify_cmd_hash" 2>/dev/null || true
|
|
229
368
|
stat_log strict-block
|
|
230
369
|
echo "Strict verify gate: '${STRICT_CMD}' is RED. Fix it before finishing — or say plainly that you are handing back a red state. Failing output:" >&2
|
|
231
370
|
echo "${OUT#red}" >&2
|
package/claude/luciazero.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# Luciazero — default operating mode
|
|
2
2
|
|
|
3
|
-
Applies to every repo, every session. The loop is plan → change →
|
|
3
|
+
Applies to every repo, every session. The loop is plan → change → fastest relevant check → fix; run full verification once at closeout.
|
|
4
4
|
|
|
5
5
|
## Ground truth
|
|
6
6
|
|
|
7
7
|
1. **Done is proven by a command, not by my judgment.** Before saying a change works, run something that returns an exit code — test, lint, type-check, build, or a real invocation — and quote the shortest decisive line of its output. A run that did not happen is reported as exactly that. (closeout procedure: `/done`)
|
|
8
|
-
2. **If no verification command exists, that is the first bug.** Say so and offer to create the smallest one that covers the change (procedure: `/
|
|
8
|
+
2. **If no verification command exists, that is the first bug.** Say so and offer to create the smallest one that covers the change (procedure: `/ready`). Do not silently proceed on vibes.
|
|
9
9
|
3. **Failing test/lint means not done.** Fix the cause. Never delete, skip, weaken, or suppress a check to reach green — if a check is genuinely wrong, say why and ask.
|
|
10
10
|
|
|
11
11
|
## Loop
|
package/install-codex.sh
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
#
|
|
5
5
|
# Mapping (single source of truth stays in claude/):
|
|
6
6
|
# claude/luciazero.md -> marker block in ~/.codex/AGENTS.md
|
|
7
|
-
# skills/catalog.txt
|
|
7
|
+
# skills/catalog.txt + aliases.txt -> ~/.codex/skills/<each>/
|
|
8
8
|
# claude/agents/catalog.txt entries -> ~/.codex/skills/<agent>/SKILL.md
|
|
9
9
|
# (Claude-only `tools:`/`model:` lines dropped)
|
|
10
10
|
# claude/hooks/ (enforcement pack) -> NOT installed: Codex has no hooks/statusline
|
|
@@ -23,6 +23,10 @@ MANAGED_DIR="${CODEX_DIR}/.luciazero-managed"
|
|
|
23
23
|
BACKUP_DIR="${CODEX_DIR}/.luciazero-backups"
|
|
24
24
|
|
|
25
25
|
catalog() { sed '/^[[:space:]]*#/d; /^[[:space:]]*$/d' "$1"; }
|
|
26
|
+
skill_inventory() {
|
|
27
|
+
catalog "${SRC}/skills/catalog.txt"
|
|
28
|
+
catalog "${SRC}/skills/aliases.txt"
|
|
29
|
+
}
|
|
26
30
|
|
|
27
31
|
# collision-proof backup path for $1 (two runs in the same second must not overwrite)
|
|
28
32
|
bakpath() {
|
|
@@ -93,7 +97,7 @@ while IFS= read -r SKILL; do
|
|
|
93
97
|
"${MANAGED_DIR}/skills/${SKILL}" \
|
|
94
98
|
"skills/${SKILL}"
|
|
95
99
|
echo " ok skills/${SKILL}"
|
|
96
|
-
done < <(
|
|
100
|
+
done < <(skill_inventory)
|
|
97
101
|
|
|
98
102
|
LEGACY_HANDOFF="${CODEX_DIR}/skills/handoff"
|
|
99
103
|
if [ -f "${LEGACY_HANDOFF}/SKILL.md" ]; then
|
package/install.sh
CHANGED
|
@@ -29,6 +29,10 @@ MANAGED_DIR="${CLAUDE_DIR}/.luciazero-managed"
|
|
|
29
29
|
BACKUP_DIR="${CLAUDE_DIR}/.luciazero-backups"
|
|
30
30
|
|
|
31
31
|
catalog() { sed '/^[[:space:]]*#/d; /^[[:space:]]*$/d' "$1"; }
|
|
32
|
+
skill_inventory() {
|
|
33
|
+
catalog "${SRC}/skills/catalog.txt"
|
|
34
|
+
catalog "${SRC}/skills/aliases.txt"
|
|
35
|
+
}
|
|
32
36
|
|
|
33
37
|
# newest released version in this checkout's CHANGELOG (informational)
|
|
34
38
|
version_of() {
|
|
@@ -50,8 +54,8 @@ if [ "${STATUS_ONLY}" = 1 ]; then
|
|
|
50
54
|
check -f "${CLAUDE_DIR}/${DOCTRINE}" "doctrine ${DOCTRINE}"
|
|
51
55
|
while IFS= read -r SKILL; do
|
|
52
56
|
check -f "${CLAUDE_DIR}/skills/${SKILL}/SKILL.md" "skill ${SKILL}"
|
|
53
|
-
done < <(
|
|
54
|
-
check -x "${CLAUDE_DIR}/skills/
|
|
57
|
+
done < <(skill_inventory)
|
|
58
|
+
check -x "${CLAUDE_DIR}/skills/ready/scripts/detect.sh" "detect.sh executable"
|
|
55
59
|
check -x "${CLAUDE_DIR}/skills/done/scripts/revert-probe.sh" "revert-probe.sh executable"
|
|
56
60
|
check -x "${CLAUDE_DIR}/skills/bisect/scripts/safe-bisect.sh" "safe-bisect.sh executable"
|
|
57
61
|
check -x "${CLAUDE_DIR}/skills/lucia-relay/scripts/relay.py" "relay.py executable"
|
|
@@ -87,12 +91,12 @@ if [ "${STATUS_ONLY}" = 1 ]; then
|
|
|
87
91
|
fi
|
|
88
92
|
done
|
|
89
93
|
WIRE_MISS=""
|
|
90
|
-
for SUB in edit bash stop session; do
|
|
91
|
-
grep -qF "${CLAUDE_DIR}/hooks/luciazero-verify.sh ${SUB}" "${CLAUDE_DIR}/settings.json" 2>/dev/null \
|
|
94
|
+
for SUB in prompt skill-prompt bash-start edit bash bash-failure skill stop session; do
|
|
95
|
+
grep -qF "${CLAUDE_DIR}/hooks/luciazero-verify.sh ${SUB}\"" "${CLAUDE_DIR}/settings.json" 2>/dev/null \
|
|
92
96
|
|| WIRE_MISS="${WIRE_MISS} ${SUB}"
|
|
93
97
|
done
|
|
94
98
|
if [ -z "${WIRE_MISS}" ]; then
|
|
95
|
-
echo " ok hooks wired in settings.json (edit/bash/stop/session)"
|
|
99
|
+
echo " ok hooks wired in settings.json (prompt/skill-prompt/bash-start/edit/bash/bash-failure/skill/stop/session)"
|
|
96
100
|
else
|
|
97
101
|
echo " MISS settings.json missing hook entries:${WIRE_MISS} (re-run ./install.sh --with-hooks)"; STATUS_RC=1
|
|
98
102
|
fi
|
|
@@ -187,14 +191,14 @@ install_file "${SRC}/claude/${DOCTRINE}" "${CLAUDE_DIR}/${DOCTRINE}" \
|
|
|
187
191
|
"${MANAGED_DIR}/${DOCTRINE}" "${DOCTRINE}"
|
|
188
192
|
echo " ok ${DOCTRINE}"
|
|
189
193
|
|
|
190
|
-
# 2. skills
|
|
194
|
+
# 2. canonical skills plus temporary compatibility aliases
|
|
191
195
|
while IFS= read -r SKILL; do
|
|
192
196
|
install_tree "${SRC}/skills/${SKILL}" \
|
|
193
197
|
"${CLAUDE_DIR}/skills/${SKILL}" \
|
|
194
198
|
"${MANAGED_DIR}/skills/${SKILL}" \
|
|
195
199
|
"skills/${SKILL}"
|
|
196
200
|
echo " ok skills/${SKILL}"
|
|
197
|
-
done < <(
|
|
201
|
+
done < <(skill_inventory)
|
|
198
202
|
|
|
199
203
|
# v1.5 migration: remove only an untouched Luciazero /handoff. A customized
|
|
200
204
|
# skill is user data and stays in place with an explicit warning.
|
|
@@ -290,6 +294,11 @@ def ensure(event, matcher, command):
|
|
|
290
294
|
|
|
291
295
|
ensure("PostToolUse", "Edit|Write|NotebookEdit", verify_cmd + " edit")
|
|
292
296
|
ensure("PostToolUse", "Bash", verify_cmd + " bash")
|
|
297
|
+
ensure("PostToolUse", "Skill", verify_cmd + " skill")
|
|
298
|
+
ensure("PostToolUseFailure", "Bash", verify_cmd + " bash-failure")
|
|
299
|
+
ensure("PreToolUse", "Bash", verify_cmd + " bash-start")
|
|
300
|
+
ensure("UserPromptSubmit", None, verify_cmd + " prompt")
|
|
301
|
+
ensure("UserPromptExpansion", None, verify_cmd + " skill-prompt")
|
|
293
302
|
ensure("Stop", None, verify_cmd + " stop")
|
|
294
303
|
ensure("SessionStart", None, verify_cmd + " session")
|
|
295
304
|
|
|
@@ -323,6 +332,7 @@ echo
|
|
|
323
332
|
SKILL_SUMMARY="$(catalog "${SRC}/skills/catalog.txt" | awk 'BEGIN{s=""} {s=s (s ? ", " : "") "/" $0} END{print s}')"
|
|
324
333
|
AGENT_SUMMARY="$(catalog "${SRC}/claude/agents/catalog.txt" | awk 'BEGIN{s=""} {s=s (s ? ", " : "") $0} END{print s}')"
|
|
325
334
|
echo "Skills: ${SKILL_SUMMARY}. Agents: ${AGENT_SUMMARY}."
|
|
335
|
+
echo "Compatibility alias for one release: /luciazero-bootstrap -> /ready."
|
|
326
336
|
if [ "${WITH_HOOKS}" = 1 ]; then
|
|
327
337
|
echo "Enforcement pack installed: verify-tracking hooks + statusline (see settings.json)."
|
|
328
338
|
else
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "luciazero",
|
|
3
|
-
"version": "2.0
|
|
4
|
-
"description": "Verification-first discipline for coding agents (Claude Code + Codex CLI): 9-rule doctrine,
|
|
3
|
+
"version": "2.1.0",
|
|
4
|
+
"description": "Verification-first discipline for coding agents (Claude Code + Codex CLI): 9-rule doctrine, 10 skills plus a temporary command alias, 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" },
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
},
|
|
11
11
|
"files": [
|
|
12
12
|
"bin",
|
|
13
|
+
"agents",
|
|
13
14
|
"claude",
|
|
14
15
|
"skills",
|
|
15
16
|
"migrations",
|
package/skills/catalog.txt
CHANGED
package/skills/debug/SKILL.md
CHANGED
|
@@ -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,
|
|
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.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Debug — hypothesis before edit
|
|
@@ -11,8 +11,10 @@ Run:
|
|
|
11
11
|
npx luciazero discipline [--days N] [--project PATH_OR_ID] [--json]
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
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.
|
|
14
|
+
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
15
|
|
|
16
16
|
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.
|
|
17
17
|
|
|
18
|
+
Latency telemetry separates observed Bash time from the rest of the measured turn. The non-Bash remainder can include model reasoning, non-Bash tools, hook overhead, and harness scheduling, so do not label it as model latency without another measurement.
|
|
19
|
+
|
|
18
20
|
Use `--project .` to filter by the current repository's privacy-preserving project hash, or `--project <display-name-or-id>` for another entry. Use `--json` when feeding a dashboard or `/retro`.
|
package/skills/done/SKILL.md
CHANGED
|
@@ -12,7 +12,7 @@ The doctrine says: *done is proven by a command, not by my judgment.* This is th
|
|
|
12
12
|
Run the **full** tier (`verify-full` if the repo has two tiers, else the verify command). Quote the shortest decisive line of real output.
|
|
13
13
|
|
|
14
14
|
- Red → you are not here yet. Go back to the loop; do not continue this ritual.
|
|
15
|
-
- No verify command exists → that is the first bug (`/
|
|
15
|
+
- No verify command exists → that is the first bug (`/ready`). Say so instead of declaring done.
|
|
16
16
|
- The command must actually have run **now**, in this session — a green from an hour ago proves the past, not the present.
|
|
17
17
|
|
|
18
18
|
## 2. Skeptic diff pass
|
|
@@ -28,7 +28,7 @@ git rev-parse --verify --quiet "${BASE}^{commit}" >/dev/null 2>&1 \
|
|
|
28
28
|
TOP="$(git rev-parse --show-toplevel 2>/dev/null)" || unassessable "no working tree (bare repo?)"
|
|
29
29
|
cd "${TOP}"
|
|
30
30
|
|
|
31
|
-
# test-file patterns mirror
|
|
31
|
+
# test-file patterns mirror ready's detect.sh: tests-style dirs
|
|
32
32
|
# plus the common root `test.sh` entrypoint and test_*.*, *_test.*, *.test.*,
|
|
33
33
|
# *.spec.* file names
|
|
34
34
|
is_test_file() {
|
|
@@ -1,109 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: luciazero-bootstrap
|
|
3
|
-
description:
|
|
3
|
+
description: Compatibility alias for /ready. Use only when the user explicitly invokes /luciazero-bootstrap; tell them it was renamed to /ready, then follow the canonical ready procedure completely.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
#
|
|
6
|
+
# Renamed to Ready
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
## Phase 1 — Detect (never assume)
|
|
13
|
-
|
|
14
|
-
Run the bundled evidence scan first — it replaces a dozen manual reads with one call:
|
|
15
|
-
|
|
16
|
-
```
|
|
17
|
-
<this-skill-dir>/scripts/detect.sh <repo-root>
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
(The skill directory is wherever this SKILL.md lives, e.g. `~/.claude/skills/luciazero-bootstrap/` or `~/.codex/skills/luciazero-bootstrap/`.) The script surfaces candidates — **you still decide**. It cannot parse CI matrices or exotic build systems; open anything it flags and read the CI config yourself.
|
|
21
|
-
|
|
22
|
-
Sources, in order of trust:
|
|
23
|
-
|
|
24
|
-
1. CI config — the most honest source of truth: `.github/workflows/*`, `.gitlab-ci.yml`, `.circleci/`. **Whatever CI runs is the verify command.**
|
|
25
|
-
2. Manifests: `package.json` scripts, `pyproject.toml` / `tox.ini` / `noxfile.py`, `Makefile`, `justfile`, `Cargo.toml`, `go.mod`, `build.gradle`, `composer.json`
|
|
26
|
-
3. Repo docs: `README*`, `CONTRIBUTING*`, `AGENTS.md`, `CLAUDE.md`, `docs/` — docs go stale; cross-check any doc-claimed command against CI when CI exists. A docs/CI mismatch is itself a finding to record in Phase 5.
|
|
27
|
-
4. Existing test dirs: `tests/`, `test/`, `spec/`, `__tests__/`, `*_test.*`, `test_*.*`
|
|
28
|
-
|
|
29
|
-
Report what was found as a short table: run / test / lint / typecheck / build / git repo — command or `MISSING`.
|
|
30
|
-
|
|
31
|
-
**If the directory is not under version control**, propose `git init` early (ask first — some dirs are deliberately not repos): without git there is no smallest reversible step, no safe break-and-restore in Phase 6, and no bisect.
|
|
32
|
-
|
|
33
|
-
## Phase 2 — Establish the verify command
|
|
34
|
-
|
|
35
|
-
If a verify path exists, **use it** — do not invent a parallel one.
|
|
36
|
-
|
|
37
|
-
If none exists, create the smallest real one. Order of preference:
|
|
38
|
-
|
|
39
|
-
1. The project's native runner, already installed (`pytest`, `vitest`, `go test`, `cargo test`, `dotnet test`)
|
|
40
|
-
2. A single entrypoint that chains them, matching the repo's existing convention (`Makefile` target, `package.json` script, `justfile` recipe) — e.g. `make verify` running lint then tests
|
|
41
|
-
|
|
42
|
-
Rules:
|
|
43
|
-
- Must exit non-zero on failure. A script that always exits 0 is worse than nothing.
|
|
44
|
-
- Must run to completion unattended: disable watch/interactive modes (e.g. `CI=1`, `--run`, `--watch=false`) — a command that waits for input or watches files hangs the loop.
|
|
45
|
-
- Must run offline, with no credentials. Anything needing GPU/network/secrets belongs in a separate slow target.
|
|
46
|
-
- Time the suite once (`time <cmd>`); the measurement, not a guess, decides one tier or two.
|
|
47
|
-
- On success, output should be near-silent — prefer quiet flags in the fast tier so failures, not progress spam, fill the context.
|
|
48
|
-
- Add it to the repo's own docs so humans find it too.
|
|
49
|
-
|
|
50
|
-
**Two tiers when the repo has slow checks.** One `verify` command forces a bad trade: either the loop crawls or coverage gets cut. Split it:
|
|
51
|
-
|
|
52
|
-
- `verify` — fast (<~60s), offline: lint, typecheck, unit/smoke tests. Run on **every** loop iteration.
|
|
53
|
-
- `verify-full` — everything else: full suite, integration, build, slow checks. Run **before declaring done** and before a PR — "done" means `verify-full` green, not just `verify`.
|
|
54
|
-
|
|
55
|
-
Name them by the repo's convention (`make verify` / `make verify-full`, npm scripts, just recipes). A small repo whose whole suite runs in seconds needs only the single tier — do not add ceremony it does not need.
|
|
56
|
-
|
|
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
|
-
|
|
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.
|
|
60
|
-
|
|
61
|
-
## Phase 3 — Smoke tests, if there are none
|
|
62
|
-
|
|
63
|
-
Do **not** attempt coverage. Write 3–6 tests that would catch a catastrophic break. Pick by this heuristic:
|
|
64
|
-
|
|
65
|
-
- **Contract shape** — the core data structure in/out: dimensions, keys, types, no NaN/null where impossible
|
|
66
|
-
- **Round trip** — serialize→deserialize, encode→decode, save→load returns equal
|
|
67
|
-
- **Import/boot** — every package imports, the app answers one request, the CLI runs `--help`. Prefer the framework's test client over binding a real port; any test that starts a process needs a hard timeout and must kill what it started.
|
|
68
|
-
- **Artifact loads** — trained model / migration / config parses and does one forward pass or one query
|
|
69
|
-
- **The bug you were sent to fix** — a regression test reproducing it, written *before* the fix
|
|
70
|
-
|
|
71
|
-
Use fixtures small enough to commit. Never depend on the user's real data paths.
|
|
72
|
-
|
|
73
|
-
State plainly that these are smoke tests, not a suite.
|
|
74
|
-
|
|
75
|
-
## Phase 4 — Guardrails (only ones that pay for themselves)
|
|
76
|
-
|
|
77
|
-
Hooks, `.claude/settings.json`, and `/fewer-permission-prompts` are **Claude Code mechanisms**. On a harness without them (Codex CLI), skip the hook items and encode the same guardrails as instructions in the project's `AGENTS.md` instead: which files are untouchable, which derived file must be regenerated after editing which source.
|
|
78
|
-
|
|
79
|
-
Prefer few and deterministic. Candidates, in value order:
|
|
80
|
-
|
|
81
|
-
- **Auto-format/lint on write** — `PostToolUse` hook matching `Edit|Write`, running the repo's own formatter. Only if the repo already has one configured.
|
|
82
|
-
- **Regenerate derived files** — if editing source X requires regenerating Y (protobuf, OpenAPI clients, migrations, lockfiles), hook it, scoped inside the command to the relevant paths. This is the highest-value hook in most repos because humans forget it.
|
|
83
|
-
- **Protect the untouchables** — `PreToolUse` deny on production config, secrets, live model/deploy pointers.
|
|
84
|
-
- **Permission allowlist** — put the repo's read-only and verify commands into `.claude/settings.json` so the loop is not interrupted. `/fewer-permission-prompts` derives this from real transcripts.
|
|
85
|
-
|
|
86
|
-
Put project-scoped settings in the repo's `.claude/settings.json` (shared) or `.claude/settings.local.json` (personal, gitignored) — **not** in global settings.
|
|
87
|
-
|
|
88
|
-
Hooks execute automatically on the user's machine. Show the exact command before installing it, and never install one that pushes, deploys, deletes, or writes outside the repo.
|
|
89
|
-
|
|
90
|
-
## Phase 5 — Project notes file (`CLAUDE.md` / `AGENTS.md`)
|
|
91
|
-
|
|
92
|
-
Extend the notes file the repo already uses; if neither exists, create the one matching the current harness and add a one-line pointer from the other name so both find it. Write only what reading the code cannot tell you:
|
|
93
|
-
|
|
94
|
-
- How to run / test / verify — the commands from Phase 2
|
|
95
|
-
- Architecture facts that are load-bearing and non-obvious (what serves what, which file is source of truth)
|
|
96
|
-
- **Footguns and null results**: "X looks right but breaks Y", "tried A, measured no gain, do not retry", "always rebuild Z after W"
|
|
97
|
-
- Where the real docs live
|
|
98
|
-
|
|
99
|
-
Do not restate the directory tree, git history, or anything a `grep` answers. Keep it dense; every line costs context on every future session.
|
|
100
|
-
|
|
101
|
-
## Phase 6 — Prove it and report
|
|
102
|
-
|
|
103
|
-
1. **Flake check** — run the fast verify tier twice. A green that does not repeat is a flake, and a flaky verify makes every future red ambiguous; fixing or quarantining the flake comes before relying on the loop. (Skip the double run only when the repo has a single slow tier — say so.)
|
|
104
|
-
2. **Red check** — break a line a smoke test actually covers (flip an expected value or a return), confirm verify goes red, then restore. The break is one deliberate edit: **record file, line, and original text before making it, and restore by reverting exactly that edit.** Only use `git checkout -- <file>` if the file was committed before the break — on a file carrying uncommitted work it silently discards that work too, and it cannot restore the untracked test files this skill just wrote. Never use bare `git stash` here (it sweeps the whole tree and skips untracked files). Breaking an uncovered line and staying green proves nothing. A verify command that cannot fail is not a verify command.
|
|
105
|
-
|
|
106
|
-
Report:
|
|
107
|
-
- The one command to run (both tiers if split)
|
|
108
|
-
- What it does and does not cover
|
|
109
|
-
- What was added, and what was deliberately left out
|
|
8
|
+
Tell the user `/luciazero-bootstrap` is deprecated and renamed to `/ready`.
|
|
9
|
+
Then read `../ready/SKILL.md` and follow that procedure completely, resolving
|
|
10
|
+
its relative resource paths from `../ready/`.
|
package/skills/plan/SKILL.md
CHANGED
|
@@ -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
|
|
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.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Plan — make the change falsifiable
|