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.
@@ -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,25 +76,156 @@ 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.
89
222
  # 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
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="$(grep -m1 -oE '^## \[[0-9]+\.[0-9]+\.[0-9]+\]' "${SRC}/CHANGELOG.md" 2>/dev/null | tr -d '#[] ' || true)"
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
- # newest released version in this checkout's CHANGELOG (informational)
37
+ # Package metadata is present in git checkouts and npm payloads alike.
38
38
  version_of() {
39
- grep -m1 -oE '^## \[[0-9]+\.[0-9]+\.[0-9]+\]' "${SRC}/CHANGELOG.md" 2>/dev/null | tr -d '#[] ' || true
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; then
104
- echo " ok python3 available (the hooks need it)"
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 plus temporary compatibility aliases
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.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.",
5
- "repository": { "type": "git", "url": "git+https://github.com/ohm41321/luciazero.git" },
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": { "url": "https://github.com/ohm41321/luciazero/issues" },
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"
@@ -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,26 +1,29 @@
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
- # Bisect — isolate the first bad commit safely
6
+ # Bisect
7
7
 
8
8
  ## 1. Freeze the criterion
9
9
 
10
- Confirm the bad endpoint fails and identify a good commit or tag. Use one unattended command whose exit `0` means good, `1–124` means bad, and `125` means the revision cannot be tested. A missing executable is an infrastructure error, not evidence that a revision is bad.
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 script resolves both endpoints, repeats each endpoint twice to catch instability, creates a detached temporary worktree, cleans generated state between revisions, runs `git bisect`, resets it, and removes the worktree on every exit path. Use `--retries N` only when the reproduction needs more endpoint samples.
21
-
22
- Do not run it for a nondeterministic symptom. Make the reproduction deterministic first through `/debug`.
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 result as the **first bad commit**, not automatically the root cause. Read its diff and relevant callers, then feed that evidence into `/debug` as a hypothesis. Keep the reproduction as a regression test and run the full verification tier after the fix.
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.
@@ -1,51 +1,57 @@
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
- # Debug — hypothesis before edit
6
+ # Debug
7
7
 
8
- The doctrine says: *debugging starts with a hypothesis, not an edit.* Mutating code until the test goes green is not debugging — it is how plausible-but-wrong fixes ship. This is the procedure for bugs that resist the first obvious look.
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
- One command that shows the failure deterministically. This command is the ground truth for the whole session — every hypothesis is judged against it.
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
- - If it cannot be reproduced yet, that is the entire current task. Do not theorize about causes of a failure you cannot trigger.
15
- - If it is intermittent, make it deterministic before proceeding: fix the seed, pin the time/timezone, run it in a loop (`for i in $(seq 20)`) until the trigger condition is understood. An intermittent repro means the hypothesis space still contains "timing/state you have not seen".
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 the reproduction — smaller input, fewer flags, one test instead of the suite — until the failure is small enough to reason about. Every element removed eliminates a family of hypotheses for free. Stop minimizing when shrinking stops being cheap.
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
- **Seed it from recorded experience first.** Before inventing hypotheses, grep the symptom's keywords (error strings, subsystem names) against two files, if they exist:
26
+ Before inventing causes, search the symptom in:
24
27
 
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.
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** — 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
-
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> → <result: refuted | confirmed | pending>
36
+ H<N>: <suspected cause> — refutable by: <command / observation> → <refuted|confirmed|pending>
34
37
  ```
35
38
 
36
- - **Run the observation, not the edit.** Choose the cheapest command whose output discriminates between this hypothesis and the alternatives — a log line, a targeted print, a debugger break, one `grep`, `git bisect run <verify-cmd>` when a known-good commit exists.
37
- - Prefer reading real state over reasoning about imagined state. The bug exists precisely because the mental model and reality differ — trust output.
38
- - Dead hypotheses stay in the ledger marked refuted, so they are not silently retried an hour later.
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
- - Change one thing, re-run the reproduction, record the result in the ledger.
43
- - A fix attempt that failed gets **reverted before the next attempt** — stacked failed fixes create a second bug on top of the first.
44
- - Two consecutive failed fixes on the same hypothesis means the hypothesis is dead, not unlucky. Widen the search: environment, dependency versions, input data, concurrency, or the test itself being wrong.
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
- - 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).
49
- - Remove all instrumentation (prints, sleeps, debug flags) — check the diff for it explicitly.
50
- - Run the full verify tier, not just the one test.
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.
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'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
- # Discipline report — turn local outcomes into evidence
6
+ # Discipline report
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
 
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
+ 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
- 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.
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
- 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.
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
- 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`.
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`.