luciazero 2.2.0 → 2.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +90 -60
- package/agents/reviewer.md +34 -26
- package/claude/agents/reviewer.md +34 -26
- package/claude/hooks/luciazero-statusline.sh +3 -1
- package/claude/hooks/luciazero-verify.sh +157 -26
- package/install-codex.sh +33 -1
- package/install.sh +41 -6
- package/package.json +10 -6
- package/skills/aliases.txt +1 -2
- package/skills/bisect/SKILL.md +12 -9
- package/skills/debug/SKILL.md +30 -24
- package/skills/discipline-report/SKILL.md +21 -8
- package/skills/done/SKILL.md +36 -30
- package/skills/experiment/SKILL.md +21 -18
- package/skills/imouto-mode/SKILL.md +30 -23
- package/skills/lucia-relay/SKILL.md +47 -33
- package/skills/lucia-relay/scripts/relay.py +599 -85
- package/skills/plan/SKILL.md +15 -11
- package/skills/ready/SKILL.md +83 -67
- package/skills/retro/SKILL.md +47 -40
- package/skills/show/SKILL.md +46 -97
- package/uninstall-codex.sh +14 -2
- package/uninstall.sh +14 -2
- package/CHANGELOG.md +0 -641
- package/README.th.md +0 -252
- /package/{skills/luciazero-bootstrap → migrations/luciazero-bootstrap-v2.2.0}/SKILL.md +0 -0
|
@@ -53,17 +53,11 @@ if [ "${MODE}" = "doctrine" ]; then
|
|
|
53
53
|
exit 0
|
|
54
54
|
fi
|
|
55
55
|
|
|
56
|
-
#
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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,25 +76,156 @@ except Exception:
|
|
|
82
76
|
|
|
83
77
|
CWD="$(pyfield "d.get('cwd')")"
|
|
84
78
|
[ -n "${CWD}" ] || CWD="${PWD}"
|
|
85
|
-
|
|
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.
|
|
89
222
|
# A hostile pre-created symlink or directory makes the hook fail open.
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
|
223
|
+
if [ -e "${BASE}" ] || [ -L "${BASE}" ]; then
|
|
224
|
+
[ -d "${BASE}" ] && [ ! -L "${BASE}" ] && [ -O "${BASE}" ] || exit 0
|
|
225
|
+
else
|
|
226
|
+
(umask 077 && mkdir "${BASE}") 2>/dev/null || exit 0
|
|
227
|
+
fi
|
|
228
|
+
chmod 700 "${BASE}" 2>/dev/null || exit 0
|
|
104
229
|
STATE="${BASE}/${KEY}"
|
|
105
230
|
mkdir -p "${STATE}" 2>/dev/null || exit 0
|
|
106
231
|
chmod 700 "${STATE}" 2>/dev/null || exit 0
|
|
@@ -396,6 +521,12 @@ print("yes" if e is not None and (v is None or e > v) else "no")' "${STATE}" 2>/
|
|
|
396
521
|
[ "${NUDGE}" = no ] && stat_log stop-clean
|
|
397
522
|
;;
|
|
398
523
|
session)
|
|
524
|
+
# A committed settings env block that reconfigures this hook is worth one
|
|
525
|
+
# loud line: the refusal above is silent, and a repository that ships these
|
|
526
|
+
# keys is either mistaken or hostile. Names the keys, never their values.
|
|
527
|
+
if [ -n "${REFUSED_ENV_KEYS}" ]; then
|
|
528
|
+
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."
|
|
529
|
+
fi
|
|
399
530
|
# SessionStart emits ONE pointer, never the relay contents. A legacy
|
|
400
531
|
# HANDOFF.md gets a migration warning but is not silently rewritten.
|
|
401
532
|
CAP="${CWD}/LUCIA_RELAY.json"
|
package/install-codex.sh
CHANGED
|
@@ -27,6 +27,10 @@ skill_inventory() {
|
|
|
27
27
|
catalog "${SRC}/skills/catalog.txt"
|
|
28
28
|
catalog "${SRC}/skills/aliases.txt"
|
|
29
29
|
}
|
|
30
|
+
version_of() {
|
|
31
|
+
awk -F '"' '/^[[:space:]]*"version"[[:space:]]*:/ { print $4; exit }' \
|
|
32
|
+
"${SRC}/package.json" 2>/dev/null || true
|
|
33
|
+
}
|
|
30
34
|
|
|
31
35
|
# collision-proof backup path for $1 (two runs in the same second must not overwrite)
|
|
32
36
|
bakpath() {
|
|
@@ -64,6 +68,28 @@ install_tree() {
|
|
|
64
68
|
cp -R "${IT_SRC}" "${IT_SNAPSHOT}"
|
|
65
69
|
}
|
|
66
70
|
|
|
71
|
+
# Remove a retired Luciazero skill only when its managed snapshot proves
|
|
72
|
+
# ownership. A customized or colliding directory is user data and must survive
|
|
73
|
+
# the migration with an explicit warning. Symlinked skill parents are refused
|
|
74
|
+
# so the deletion cannot escape the configured directory.
|
|
75
|
+
remove_legacy_tree() {
|
|
76
|
+
LT_DST="$1"; LT_SNAPSHOT="$2"; LT_LABEL="$3"
|
|
77
|
+
if [ ! -e "${LT_DST}" ] && [ ! -L "${LT_DST}" ]; then
|
|
78
|
+
if [ ! -L "$(dirname "${LT_SNAPSHOT}")" ]; then
|
|
79
|
+
rm -rf "${LT_SNAPSHOT}"
|
|
80
|
+
fi
|
|
81
|
+
return
|
|
82
|
+
fi
|
|
83
|
+
if [ -L "$(dirname "${LT_DST}")" ] || [ -L "$(dirname "${LT_SNAPSHOT}")" ]; then
|
|
84
|
+
echo " !! ${LT_LABEL} has a symlinked parent; left untouched" >&2
|
|
85
|
+
elif same_tree "${LT_DST}" "${LT_SNAPSHOT}"; then
|
|
86
|
+
rm -rf "${LT_DST}" "${LT_SNAPSHOT}"
|
|
87
|
+
echo " ok migrated ${LT_LABEL}"
|
|
88
|
+
else
|
|
89
|
+
echo " !! ${LT_LABEL} is customized or not Luciazero-owned; left untouched" >&2
|
|
90
|
+
fi
|
|
91
|
+
}
|
|
92
|
+
|
|
67
93
|
echo "Installing into ${CODEX_DIR}"
|
|
68
94
|
mkdir -p "${CODEX_DIR}/skills"
|
|
69
95
|
|
|
@@ -99,6 +125,12 @@ while IFS= read -r SKILL; do
|
|
|
99
125
|
echo " ok skills/${SKILL}"
|
|
100
126
|
done < <(skill_inventory)
|
|
101
127
|
|
|
128
|
+
# v2.3 migration: remove only the untouched /luciazero-bootstrap compatibility
|
|
129
|
+
# alias from older installs. Customized copies remain user data.
|
|
130
|
+
remove_legacy_tree "${CODEX_DIR}/skills/luciazero-bootstrap" \
|
|
131
|
+
"${MANAGED_DIR}/skills/luciazero-bootstrap" \
|
|
132
|
+
"skills/luciazero-bootstrap"
|
|
133
|
+
|
|
102
134
|
LEGACY_HANDOFF="${CODEX_DIR}/skills/handoff"
|
|
103
135
|
if [ -f "${LEGACY_HANDOFF}/SKILL.md" ]; then
|
|
104
136
|
if cmp -s "${SRC}/migrations/handoff-v1.5.0.SKILL.md" "${LEGACY_HANDOFF}/SKILL.md"; then
|
|
@@ -123,7 +155,7 @@ while IFS= read -r AGENT_NAME; do
|
|
|
123
155
|
done < <(catalog "${SRC}/claude/agents/catalog.txt")
|
|
124
156
|
|
|
125
157
|
# 4. version sidecar (informational; removed by uninstall-codex.sh)
|
|
126
|
-
V_NEW="$(
|
|
158
|
+
V_NEW="$(version_of)"
|
|
127
159
|
if [ -n "${V_NEW}" ]; then
|
|
128
160
|
printf '%s\n' "${V_NEW}" > "${CODEX_DIR}/.luciazero-version"
|
|
129
161
|
fi
|
package/install.sh
CHANGED
|
@@ -34,9 +34,10 @@ skill_inventory() {
|
|
|
34
34
|
catalog "${SRC}/skills/aliases.txt"
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
#
|
|
37
|
+
# Package metadata is present in git checkouts and npm payloads alike.
|
|
38
38
|
version_of() {
|
|
39
|
-
|
|
39
|
+
awk -F '"' '/^[[:space:]]*"version"[[:space:]]*:/ { print $4; exit }' \
|
|
40
|
+
"${SRC}/package.json" 2>/dev/null || true
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
if [ "${STATUS_ONLY}" = 1 ]; then
|
|
@@ -100,8 +101,11 @@ if [ "${STATUS_ONLY}" = 1 ]; then
|
|
|
100
101
|
else
|
|
101
102
|
echo " MISS settings.json missing hook entries:${WIRE_MISS} (re-run ./install.sh --with-hooks)"; STATUS_RC=1
|
|
102
103
|
fi
|
|
103
|
-
if command -v python3 >/dev/null 2>&1
|
|
104
|
-
|
|
104
|
+
if command -v python3 >/dev/null 2>&1 \
|
|
105
|
+
&& python3 -c 'import sys; raise SystemExit(0 if sys.version_info >= (3, 9) else 1)' 2>/dev/null; then
|
|
106
|
+
echo " ok python3 >= 3.9 available (the hooks need it)"
|
|
107
|
+
elif command -v python3 >/dev/null 2>&1; then
|
|
108
|
+
echo " MISS python3 is older than 3.9 — the hooks fail open (doing nothing)"; STATUS_RC=1
|
|
105
109
|
else
|
|
106
110
|
# fail-open means a missing python3 breaks the hooks SILENTLY — surface it here
|
|
107
111
|
echo " MISS python3 not found — the installed hooks are failing open (doing nothing)"; STATUS_RC=1
|
|
@@ -158,6 +162,28 @@ install_tree() {
|
|
|
158
162
|
cp -R "${IT_SRC}" "${IT_SNAPSHOT}"
|
|
159
163
|
}
|
|
160
164
|
|
|
165
|
+
# Remove a retired Luciazero skill only when its managed snapshot proves
|
|
166
|
+
# ownership. A customized or colliding directory is user data and must survive
|
|
167
|
+
# the migration with an explicit warning. Symlinked skill parents are refused
|
|
168
|
+
# so the deletion cannot escape the configured directory.
|
|
169
|
+
remove_legacy_tree() {
|
|
170
|
+
LT_DST="$1"; LT_SNAPSHOT="$2"; LT_LABEL="$3"
|
|
171
|
+
if [ ! -e "${LT_DST}" ] && [ ! -L "${LT_DST}" ]; then
|
|
172
|
+
if [ ! -L "$(dirname "${LT_SNAPSHOT}")" ]; then
|
|
173
|
+
rm -rf "${LT_SNAPSHOT}"
|
|
174
|
+
fi
|
|
175
|
+
return
|
|
176
|
+
fi
|
|
177
|
+
if [ -L "$(dirname "${LT_DST}")" ] || [ -L "$(dirname "${LT_SNAPSHOT}")" ]; then
|
|
178
|
+
echo " !! ${LT_LABEL} has a symlinked parent; left untouched" >&2
|
|
179
|
+
elif same_tree "${LT_DST}" "${LT_SNAPSHOT}"; then
|
|
180
|
+
rm -rf "${LT_DST}" "${LT_SNAPSHOT}"
|
|
181
|
+
echo " ok migrated ${LT_LABEL}"
|
|
182
|
+
else
|
|
183
|
+
echo " !! ${LT_LABEL} is customized or not Luciazero-owned; left untouched" >&2
|
|
184
|
+
fi
|
|
185
|
+
}
|
|
186
|
+
|
|
161
187
|
install_file() {
|
|
162
188
|
IF_SRC="$1"; IF_DST="$2"; IF_SNAPSHOT="$3"; IF_LABEL="$4"
|
|
163
189
|
if [ -e "${IF_DST}" ] || [ -L "${IF_DST}" ]; then
|
|
@@ -191,7 +217,7 @@ install_file "${SRC}/claude/${DOCTRINE}" "${CLAUDE_DIR}/${DOCTRINE}" \
|
|
|
191
217
|
"${MANAGED_DIR}/${DOCTRINE}" "${DOCTRINE}"
|
|
192
218
|
echo " ok ${DOCTRINE}"
|
|
193
219
|
|
|
194
|
-
# 2. canonical skills
|
|
220
|
+
# 2. canonical skills
|
|
195
221
|
while IFS= read -r SKILL; do
|
|
196
222
|
install_tree "${SRC}/skills/${SKILL}" \
|
|
197
223
|
"${CLAUDE_DIR}/skills/${SKILL}" \
|
|
@@ -200,6 +226,12 @@ while IFS= read -r SKILL; do
|
|
|
200
226
|
echo " ok skills/${SKILL}"
|
|
201
227
|
done < <(skill_inventory)
|
|
202
228
|
|
|
229
|
+
# v2.3 migration: remove only the untouched /luciazero-bootstrap compatibility
|
|
230
|
+
# alias from older installs. Customized copies remain user data.
|
|
231
|
+
remove_legacy_tree "${CLAUDE_DIR}/skills/luciazero-bootstrap" \
|
|
232
|
+
"${MANAGED_DIR}/skills/luciazero-bootstrap" \
|
|
233
|
+
"skills/luciazero-bootstrap"
|
|
234
|
+
|
|
203
235
|
# v1.5 migration: remove only an untouched Luciazero /handoff. A customized
|
|
204
236
|
# skill is user data and stays in place with an explicit warning.
|
|
205
237
|
LEGACY_HANDOFF="${CLAUDE_DIR}/skills/handoff"
|
|
@@ -250,6 +282,10 @@ fi
|
|
|
250
282
|
# 6. enforcement pack (opt-in): hooks + statusline wired into settings.json
|
|
251
283
|
if [ "${WITH_HOOKS}" = 1 ]; then
|
|
252
284
|
command -v python3 >/dev/null 2>&1 || { echo "FAIL: --with-hooks requires python3" >&2; exit 1; }
|
|
285
|
+
# 3.9 is where hashlib gained usedforsecurity=, which the hooks pass so their
|
|
286
|
+
# md5 state key does not raise under FIPS and silently disable tracking
|
|
287
|
+
python3 -c 'import sys; raise SystemExit(0 if sys.version_info >= (3, 9) else 1)' 2>/dev/null \
|
|
288
|
+
|| { echo "FAIL: --with-hooks requires a working python3 >= 3.9" >&2; exit 1; }
|
|
253
289
|
mkdir -p "${CLAUDE_DIR}/hooks"
|
|
254
290
|
for H in luciazero-verify.sh luciazero-statusline.sh; do
|
|
255
291
|
DST="${CLAUDE_DIR}/hooks/${H}"
|
|
@@ -332,7 +368,6 @@ echo
|
|
|
332
368
|
SKILL_SUMMARY="$(catalog "${SRC}/skills/catalog.txt" | awk 'BEGIN{s=""} {s=s (s ? ", " : "") "/" $0} END{print s}')"
|
|
333
369
|
AGENT_SUMMARY="$(catalog "${SRC}/claude/agents/catalog.txt" | awk 'BEGIN{s=""} {s=s (s ? ", " : "") $0} END{print s}')"
|
|
334
370
|
echo "Skills: ${SKILL_SUMMARY}. Agents: ${AGENT_SUMMARY}."
|
|
335
|
-
echo "Compatibility alias for one release: /luciazero-bootstrap -> /ready."
|
|
336
371
|
if [ "${WITH_HOOKS}" = 1 ]; then
|
|
337
372
|
echo "Enforcement pack installed: verify-tracking hooks + statusline (see settings.json)."
|
|
338
373
|
else
|
package/package.json
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "luciazero",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "Verification-first discipline for coding agents (Claude Code + Codex CLI): 9-rule doctrine, 11 skills
|
|
5
|
-
"repository": {
|
|
3
|
+
"version": "2.4.1",
|
|
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
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/ohm41321/luciazero.git"
|
|
8
|
+
},
|
|
6
9
|
"homepage": "https://github.com/ohm41321/luciazero#readme",
|
|
7
|
-
"bugs": {
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/ohm41321/luciazero/issues"
|
|
12
|
+
},
|
|
8
13
|
"bin": {
|
|
9
14
|
"luciazero": "bin/luciazero.js"
|
|
10
15
|
},
|
|
@@ -17,8 +22,7 @@
|
|
|
17
22
|
"install.sh",
|
|
18
23
|
"uninstall.sh",
|
|
19
24
|
"install-codex.sh",
|
|
20
|
-
"uninstall-codex.sh"
|
|
21
|
-
"CHANGELOG.md"
|
|
25
|
+
"uninstall-codex.sh"
|
|
22
26
|
],
|
|
23
27
|
"engines": {
|
|
24
28
|
"node": ">=18"
|
package/skills/aliases.txt
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
#
|
|
2
|
-
luciazero-bootstrap
|
|
1
|
+
# Compatibility aliases are retired. Keep this file as the empty alias catalog.
|
package/skills/bisect/SKILL.md
CHANGED
|
@@ -1,26 +1,29 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: bisect
|
|
3
|
-
description: Pinpoint the first bad commit for a reproducible regression
|
|
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
|
-
# Bisect
|
|
6
|
+
# Bisect
|
|
7
7
|
|
|
8
8
|
## 1. Freeze the criterion
|
|
9
9
|
|
|
10
|
-
Confirm the bad endpoint fails and
|
|
10
|
+
Confirm the bad endpoint fails and name a good commit or tag. One unattended
|
|
11
|
+
command decides: exit `0` means good, `1–124` means bad, and `125` means
|
|
12
|
+
untestable. A missing executable is an infrastructure error.
|
|
11
13
|
|
|
12
14
|
## 2. Run in a throwaway worktree
|
|
13
15
|
|
|
14
|
-
Locate this skill's installed directory, stay in the repository under investigation, and invoke the bundled script by its absolute path:
|
|
15
|
-
|
|
16
16
|
```bash
|
|
17
17
|
<this-skill-dir>/scripts/safe-bisect.sh --good <good-rev> --bad <bad-rev> -- <verify-command> [args...]
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
The
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
The helper repeats each endpoint twice, uses a detached temporary worktree,
|
|
21
|
+
cleans state, resets bisect, and removes the worktree on every exit path. Use
|
|
22
|
+
`--retries N` only for endpoint samples. Make the reproduction deterministic
|
|
23
|
+
first through `/debug`.
|
|
23
24
|
|
|
24
25
|
## 3. Interpret narrowly
|
|
25
26
|
|
|
26
|
-
Report the
|
|
27
|
+
Report the first bad commit, not automatically the root cause. Read its diff and
|
|
28
|
+
relevant callers; feed `/debug`, keep the regression test, and run the full
|
|
29
|
+
verification tier after fixing.
|
package/skills/debug/SKILL.md
CHANGED
|
@@ -1,51 +1,57 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: debug
|
|
3
|
-
description:
|
|
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
|
-
# Debug
|
|
6
|
+
# Debug
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
For bugs that resist the first obvious look: debugging starts with a hypothesis,
|
|
9
|
+
not an edit. Reality, not a plausible patch, decides.
|
|
9
10
|
|
|
10
11
|
## 1. Reproduce first
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
Find one command that shows the failure deterministically. It is ground truth.
|
|
14
|
+
Do not theorize about causes of a failure you cannot trigger.
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
+
For intermittent failures, fix the seed, pin the time/timezone, run it in a loop,
|
|
17
|
+
and identify the trigger before proceeding.
|
|
16
18
|
|
|
17
19
|
## 2. Minimize
|
|
18
20
|
|
|
19
|
-
Shrink
|
|
21
|
+
Shrink to smaller input, fewer flags, one test instead of the suite. Stop when
|
|
22
|
+
further reduction costs more than it clarifies.
|
|
20
23
|
|
|
21
24
|
## 3. Hypothesis ledger
|
|
22
25
|
|
|
23
|
-
|
|
26
|
+
Before inventing causes, search the symptom in:
|
|
24
27
|
|
|
25
|
-
-
|
|
26
|
-
-
|
|
28
|
+
- repo ledger `docs/lessons.md`;
|
|
29
|
+
- configured harness `luciazero-heuristics.md` under
|
|
30
|
+
`${CLAUDE_CONFIG_DIR:-$HOME/.claude}` or
|
|
31
|
+
`${CODEX_HOME:-$HOME/.codex}`.
|
|
27
32
|
|
|
28
|
-
A match becomes **H1
|
|
29
|
-
|
|
30
|
-
Keep a visible ledger in the conversation. Each entry:
|
|
33
|
+
A match becomes **H1**, but must pass its `proven-by` observation.
|
|
31
34
|
|
|
32
35
|
```
|
|
33
|
-
H<N>: <suspected cause> — refutable by: <command / observation> → <
|
|
36
|
+
H<N>: <suspected cause> — refutable by: <command / observation> → <refuted|confirmed|pending>
|
|
34
37
|
```
|
|
35
38
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
Run the observation, not the edit. Choose the cheapest discriminating log,
|
|
40
|
+
query, debugger stop, grep, or bisect. Trust real state. Dead hypotheses stay in
|
|
41
|
+
the ledger marked refuted. Keep the ledger visible in the conversation.
|
|
39
42
|
|
|
40
43
|
## 4. One variable per iteration
|
|
41
44
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
+
Change one thing, re-run the reproduction, and record the result. A failed fix
|
|
46
|
+
gets **reverted before the next attempt**. Two consecutive failed fixes on the
|
|
47
|
+
same hypothesis means the hypothesis is dead; widen to environment, versions,
|
|
48
|
+
input, concurrency, or a wrong test.
|
|
45
49
|
|
|
46
50
|
## 5. Close out
|
|
47
51
|
|
|
48
|
-
-
|
|
49
|
-
|
|
50
|
-
-
|
|
51
|
-
-
|
|
52
|
+
- Commit a regression test. Run it both ways and quote both results: red before
|
|
53
|
+
the fix, green after. Use the done skill's `revert-probe.sh` when applicable.
|
|
54
|
+
- Remove all instrumentation: prints, sleeps, debug flags.
|
|
55
|
+
- Run the full verify tier.
|
|
56
|
+
- If the cause, footgun, or null result is not obvious from code, run `/retro`;
|
|
57
|
+
debugged failures go to `docs/lessons.md` as symptom → cause → proof → fix.
|
|
@@ -1,20 +1,33 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: discipline-report
|
|
3
|
-
description: Analyze Luciazero
|
|
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
|
-
# Discipline report
|
|
6
|
+
# Discipline report
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Resolve the first available local CLI:
|
|
9
9
|
|
|
10
10
|
```bash
|
|
11
|
-
|
|
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
|
|
|
14
|
-
|
|
15
|
+
Use the first only when on PATH; use the second from a checkout/package. If
|
|
16
|
+
neither local form exists, report unavailable offline. Use `npx` only when
|
|
17
|
+
package resolution is explicitly allowed.
|
|
15
18
|
|
|
16
|
-
|
|
19
|
+
The command reads `luciazero-stats.log`, accepts current
|
|
20
|
+
schema-versioned JSON lines and legacy space-delimited records, ignores malformed
|
|
21
|
+
lines without failing, and never sends data over the network. Telemetry includes
|
|
22
|
+
turn/Bash wall time plus Bash, verify, and skill counts; parallel Bash intervals
|
|
23
|
+
are merged. Raw commands and skill names are never persisted.
|
|
17
24
|
|
|
18
|
-
|
|
25
|
+
Treat recorded outcomes as observations, not causes. `nudge` means no recognized
|
|
26
|
+
later verify; `strict-block` means its strict command was red. Explanations must
|
|
27
|
+
say `likely` unless the log records the cause.
|
|
19
28
|
|
|
20
|
-
|
|
29
|
+
Non-Bash remainder includes tools, hooks, scheduling, and reasoning; do not
|
|
30
|
+
label it as model latency without another measurement.
|
|
31
|
+
|
|
32
|
+
Use `--project .` for this repo or another name/id as needed. Use `--json` for
|
|
33
|
+
dashboards or `/retro`.
|