luciazero 1.5.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.
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env bash
2
+ # Enforcement-pack statusline (Claude Code only; installed by `install.sh
3
+ # --with-hooks`). Shows: model | git branch | verify status from the state
4
+ # that luciazero-verify.sh maintains — so the doctrine's ground truth is visible
5
+ # at a glance instead of remembered.
6
+ #
7
+ # ✅ verify 3m last verify-ish command succeeded, 3 minutes ago
8
+ # ❌ verify RED last verify-ish command failed
9
+ # ✳ verify ran a verify ran but its result was unreadable
10
+ # ✎ unverified edits happened after the last verify run
11
+ # — no verify yet nothing tracked this session
12
+ #
13
+ # Requires python3. Fails open to a minimal line.
14
+ set -u
15
+
16
+ IN="$(cat 2>/dev/null || true)"
17
+
18
+ LINE="$(printf '%s' "${IN}" | python3 -c '
19
+ import json, os, sys, time, hashlib
20
+
21
+ try:
22
+ d = json.load(sys.stdin)
23
+ except Exception:
24
+ d = {}
25
+
26
+ model = ((d.get("model") or {}).get("display_name")) or "claude"
27
+ cwd = ((d.get("workspace") or {}).get("current_dir")) or d.get("cwd") or os.getcwd()
28
+
29
+ key = hashlib.md5(cwd.encode()).hexdigest()[:12]
30
+ state = os.path.join(os.environ.get("TMPDIR", "/tmp"), "luciazero-verify-state", key)
31
+
32
+ def mtime(name):
33
+ try:
34
+ return os.path.getmtime(os.path.join(state, name))
35
+ except OSError:
36
+ return None
37
+
38
+ def age(ts):
39
+ s = int(time.time() - ts)
40
+ if s < 60:
41
+ return f"{s}s"
42
+ if s < 3600:
43
+ return f"{s // 60}m"
44
+ return f"{s // 3600}h"
45
+
46
+ ve, ed = mtime("last_verify"), mtime("last_edit")
47
+ if ve is None and ed is None:
48
+ verify = "— no verify yet"
49
+ elif ve is None or (ed is not None and ed > ve):
50
+ verify = "✎ unverified"
51
+ else:
52
+ try:
53
+ status = open(os.path.join(state, "last_verify")).read().strip()
54
+ except OSError:
55
+ status = "ran"
56
+ if status == "ok":
57
+ verify = f"✅ verify {age(ve)}"
58
+ elif status == "fail":
59
+ verify = f"❌ verify RED {age(ve)}"
60
+ else:
61
+ verify = f"✳ verify ran {age(ve)}"
62
+
63
+ print(model + "\t" + cwd + "\t" + verify)' 2>/dev/null)" || LINE=""
64
+
65
+ if [ -z "${LINE}" ]; then
66
+ echo "claude"
67
+ exit 0
68
+ fi
69
+
70
+ MODEL="${LINE%%$'\t'*}"
71
+ REST="${LINE#*$'\t'}"
72
+ DIR="${REST%%$'\t'*}"
73
+ VERIFY="${REST#*$'\t'}"
74
+
75
+ BRANCH="$(git -C "${DIR}" branch --show-current 2>/dev/null || true)"
76
+ if [ -n "${BRANCH}" ]; then
77
+ printf '%s | %s | %s\n' "${MODEL}" "${BRANCH}" "${VERIFY}"
78
+ else
79
+ printf '%s | %s\n' "${MODEL}" "${VERIFY}"
80
+ fi
81
+ exit 0
@@ -0,0 +1,252 @@
1
+ #!/usr/bin/env bash
2
+ # Enforcement-pack hook (Claude Code only; installed by `install.sh --with-hooks`).
3
+ # Tracks per-project whether edits have been followed by a verify run, and
4
+ # nudges ONCE at session stop when they have not — mechanizing doctrine rule 1
5
+ # ("done is proven by a command") at the exact moment it is most violated.
6
+ #
7
+ # Subcommands (wired in settings.json):
8
+ # edit — PostToolUse on Edit|Write|NotebookEdit : record "an edit happened"
9
+ # bash — PostToolUse on Bash : record verify runs + status
10
+ # stop — Stop : warn once if edits are unverified
11
+ # session — SessionStart : point at an existing HANDOFF.md
12
+ # doctrine— SessionStart (plugin installs only) : emit the doctrine as context
13
+ # (plugins cannot add a CLAUDE.md import line; this is the same
14
+ # word-ceiling-capped text the classic install imports. Silent when
15
+ # a classic install exists, so the doctrine never loads twice.)
16
+ #
17
+ # Optional strict gate: when LUCIAZERO_STRICT_VERIFY_CMD is set, `stop`
18
+ # actually RUNS that command and refuses the stop (exit 2) while it is red.
19
+ # Set it in your PERSONAL settings (settings.local.json env block, or your
20
+ # shell). LIMITATION: this hook cannot tell which settings scope set the
21
+ # variable — a committed .claude/settings.json env block reaches it too — so
22
+ # never commit it, and treat a repo that ships this variable as hostile.
23
+ # A blocked stop's continuation is never re-blocked (stop_hook_active),
24
+ # so this is a speed bump with evidence attached, not a wall.
25
+ #
26
+ # Requires python3 (for JSON parsing). FAILS OPEN: any internal error exits 0,
27
+ # so a broken hook can never block real work. Per-project state lives under
28
+ # $TMPDIR and never touches the repo. One exception, documented honestly: the
29
+ # stop hook appends one line per stop outcome (stop-clean / nudge /
30
+ # strict-block) to luciazero-stats.log in the harness config dir — local only,
31
+ # capped at ~250 lines, fail-open — so /retro can turn recurring discipline
32
+ # gaps into recorded lessons. Uninstall keeps it (it is learned data).
33
+ set -u
34
+
35
+ MODE="${1:-}"
36
+
37
+ # doctrine mode needs no python3, no state, and no stdin — handled before the
38
+ # shared setup so a machine without python3 still gets the doctrine (everything
39
+ # below fails open to doing nothing there). ${HOME:-} keeps set -u fail-open
40
+ # on hosts with no HOME at all.
41
+ if [ "${MODE}" = "doctrine" ]; then
42
+ # a classic install's CLAUDE.md import already loads this text — never twice
43
+ [ -f "${CLAUDE_CONFIG_DIR:-${HOME:-}/.claude}/luciazero.md" ] && exit 0
44
+ DOCTRINE="$(cd "$(dirname "$0")" 2>/dev/null && cd .. && pwd)/luciazero.md"
45
+ [ -f "${DOCTRINE}" ] || exit 0
46
+ cat "${DOCTRINE}" 2>/dev/null || true
47
+ exit 0
48
+ fi
49
+
50
+ # Plugin-channel dedupe (the plugin's hooks.json invokes every mode with
51
+ # LUCIAZERO_CHANNEL=plugin): when `install.sh --with-hooks` wiring is ALSO
52
+ # present, the classic copy wins and the plugin copy stands down — otherwise
53
+ # the stop nudge double-fires and a strict verify runs twice concurrently.
54
+ if [ "${LUCIAZERO_CHANNEL:-}" = "plugin" ]; then
55
+ CFG="${CLAUDE_CONFIG_DIR:-${HOME:-}/.claude}"
56
+ if [ -x "${CFG}/hooks/luciazero-verify.sh" ] \
57
+ && grep -qF "${CFG}/hooks/luciazero-verify.sh" "${CFG}/settings.json" 2>/dev/null; then
58
+ exit 0
59
+ fi
60
+ fi
61
+
62
+ # Hook stdin is always a pipe; when run by hand from a terminal for debugging,
63
+ # do not hang waiting for EOF that never comes.
64
+ if [ -t 0 ]; then IN=""; else IN="$(cat 2>/dev/null || true)"; fi
65
+
66
+ pyfield() { # pyfield '<python expr over dict d>' — empty string on any error
67
+ printf '%s' "${IN}" | python3 -c "
68
+ import json, sys
69
+ try:
70
+ d = json.load(sys.stdin)
71
+ v = ${1}
72
+ print('' if v is None else v)
73
+ except Exception:
74
+ print('')" 2>/dev/null || true
75
+ }
76
+
77
+ CWD="$(pyfield "d.get('cwd')")"
78
+ [ -n "${CWD}" ] || CWD="${PWD}"
79
+ KEY="$(printf '%s' "${CWD}" | python3 -c 'import sys,hashlib;print(hashlib.md5(sys.stdin.buffer.read()).hexdigest()[:12])' 2>/dev/null)" || exit 0
80
+ [ -n "${KEY}" ] || exit 0
81
+ STATE="${TMPDIR:-/tmp}/luciazero-verify-state/${KEY}"
82
+ mkdir -p "${STATE}" 2>/dev/null || exit 0
83
+
84
+ stat_log() { # stat_log <event> — discipline stats for /retro; fail-open, capped
85
+ SDIR="${CLAUDE_CONFIG_DIR:-${HOME:-}/.claude}"
86
+ SFILE="${SDIR}/luciazero-stats.log"
87
+ {
88
+ mkdir -p "${SDIR}" \
89
+ && printf '%s %s %s\n' "$(date +%Y-%m-%dT%H:%M)" "$1" "$(basename "${CWD}")" >> "${SFILE}"
90
+ if [ "$(wc -l < "${SFILE}")" -gt 500 ]; then
91
+ tail -n 250 "${SFILE}" > "${SFILE}.tmp" && mv "${SFILE}.tmp" "${SFILE}"
92
+ fi
93
+ } 2>/dev/null || true
94
+ }
95
+
96
+ # What counts as a verify run. Deliberately broad; override per-shell with
97
+ # LUCIAZERO_VERIFY_REGEX (extended regex, matched against the Bash tool command).
98
+ # When LUCIAZERO_VERIFY_CMD is set (the repo's exact verify command, e.g.
99
+ # "./test.sh"), only commands that ARE it or START with it count — the broad
100
+ # regex also marks `cat test.sh` or `grep pytest README` as a verify run,
101
+ # flipping the state green without any test having run.
102
+ VERIFY_RE="${LUCIAZERO_VERIFY_REGEX:-verify|test\.sh|pytest|npm (run )?test|pnpm test|yarn test|cargo test|go test|vitest|jest|make (test|check)|tox|rake test|mix test|dotnet test|gradlew? (test|check)}"
103
+ VERIFY_CMD="${LUCIAZERO_VERIFY_CMD:-}"
104
+
105
+ case "${MODE}" in
106
+ edit)
107
+ # Documentation writes do not re-arm the nudge: the closeout skills
108
+ # (/done, /retro, /handoff, /experiment) all write markdown AFTER the
109
+ # final green verify — nudging on those would punish the prescribed flow.
110
+ FP="$(pyfield "d.get('tool_input', {}).get('file_path')")"
111
+ DOC_RE="${LUCIAZERO_DOC_REGEX:-\.(md|markdown|rst|txt)\$}"
112
+ if [ -n "${FP}" ] && printf '%s' "${FP}" | grep -qE "${DOC_RE}"; then
113
+ : # doc-only write; verify state unchanged
114
+ else
115
+ touch "${STATE}/last_edit"
116
+ rm -f "${STATE}/nudged" # new code edits re-arm the one-shot nudge
117
+ fi
118
+ ;;
119
+ bash)
120
+ CMD="$(pyfield "d.get('tool_input', {}).get('command')")"
121
+ IS_VERIFY=no
122
+ if [ -n "${CMD}" ]; then
123
+ if [ -n "${VERIFY_CMD}" ]; then
124
+ # exact mode: the command must BE or START WITH the configured command
125
+ case "${CMD}" in
126
+ "${VERIFY_CMD}"|"${VERIFY_CMD} "*) IS_VERIFY=yes ;;
127
+ esac
128
+ elif printf '%s' "${CMD}" | grep -qE "${VERIFY_RE}"; then
129
+ IS_VERIFY=yes
130
+ fi
131
+ fi
132
+ if [ "${IS_VERIFY}" = yes ]; then
133
+ # Best-effort red/green from the tool response; unknown shape -> "ran"
134
+ 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 {})")"
135
+ printf '%s\n' "${STATUS:-ran}" > "${STATE}/last_verify"
136
+ # remember WHICH command produced the state — the strict gate's fast
137
+ # path trusts a green only when this matches its own command
138
+ printf '%s\n' "${CMD}" > "${STATE}/last_verify_cmd" 2>/dev/null || true
139
+ rm -f "${STATE}/nudged"
140
+ fi
141
+ ;;
142
+ stop)
143
+ # Never re-block a continuation that a stop hook itself caused
144
+ ACTIVE="$(pyfield "d.get('stop_hook_active')")"
145
+ if [ "${ACTIVE}" = "True" ] || [ "${ACTIVE}" = "true" ]; then exit 0; fi
146
+ # Strict gate (opt-in, see header): actually run the user's verify command
147
+ # unless the tracked state is already green-after-last-edit. Any internal
148
+ # error — timeout, missing command, unparseable state — degrades to the
149
+ # ordinary fail-open nudge below, never to a block.
150
+ STRICT_CMD="${LUCIAZERO_STRICT_VERIFY_CMD:-}"
151
+ # Strict gate only on well-formed input: unparseable stdin means we know
152
+ # neither cwd nor stop_hook_active — running a command on guesses would
153
+ # break both the fail-open and the never-re-block guarantees.
154
+ JSON_OK="$(printf '%s' "${IN}" | python3 -c 'import json,sys; json.load(sys.stdin); print("yes")' 2>/dev/null || echo no)"
155
+ if [ -n "${STRICT_CMD}" ] && [ "${JSON_OK}" = yes ]; then
156
+ OUT="$(python3 -c '
157
+ import os, subprocess, sys
158
+ state, cwd, cmd, timeout = sys.argv[1:5]
159
+ def m(name):
160
+ try:
161
+ return os.path.getmtime(os.path.join(state, name))
162
+ except OSError:
163
+ return None
164
+ def read(name):
165
+ try:
166
+ return open(os.path.join(state, name)).read().strip()
167
+ except OSError:
168
+ return ""
169
+ e, v = m("last_edit"), m("last_verify")
170
+ # Fast path only for a green that THIS command (or a longer invocation of
171
+ # it) produced — a broad-regex green from a mere read of the test file must
172
+ # not disarm a gate whose promise is "actually runs the command".
173
+ vcmd = read("last_verify_cmd")
174
+ if (v is not None and read("last_verify") == "ok"
175
+ and (e is None or e <= v)
176
+ and (vcmd == cmd or vcmd.startswith(cmd + " "))):
177
+ print("green"); sys.exit(0)
178
+ try:
179
+ r = subprocess.run(cmd, shell=True, cwd=cwd or None, timeout=float(timeout),
180
+ capture_output=True, text=True)
181
+ except Exception:
182
+ print("error"); sys.exit(0)
183
+ if r.returncode in (126, 127):
184
+ # command not found / not executable: an internal error, not a red verify
185
+ print("error"); sys.exit(0)
186
+ if r.returncode == 0:
187
+ print("ok")
188
+ else:
189
+ tail = ((r.stdout or "") + "\n" + (r.stderr or "")).strip().splitlines()[-8:]
190
+ print("red")
191
+ print("\n".join(tail))
192
+ ' "${STATE}" "${CWD}" "${STRICT_CMD}" "${LUCIAZERO_STRICT_TIMEOUT:-120}" 2>/dev/null || echo error)"
193
+ case "${OUT%%$'\n'*}" in
194
+ green) stat_log stop-clean; exit 0 ;;
195
+ ok)
196
+ printf 'ok\n' > "${STATE}/last_verify" 2>/dev/null || true
197
+ printf '%s\n' "${STRICT_CMD}" > "${STATE}/last_verify_cmd" 2>/dev/null || true
198
+ rm -f "${STATE}/nudged"
199
+ stat_log stop-clean
200
+ exit 0 ;;
201
+ red)
202
+ printf 'fail\n' > "${STATE}/last_verify" 2>/dev/null || true
203
+ printf '%s\n' "${STRICT_CMD}" > "${STATE}/last_verify_cmd" 2>/dev/null || true
204
+ stat_log strict-block
205
+ 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
206
+ echo "${OUT#red}" >&2
207
+ exit 2 ;;
208
+ *) : ;; # error — fall through to the ordinary fail-open nudge
209
+ esac
210
+ fi
211
+ # Sub-second mtime comparison via python: bash 3.2's [ -nt ] compares
212
+ # whole seconds only, silently missing an edit made in the same second
213
+ # as the last verify (the statusline already compares float mtimes).
214
+ NUDGE="$(python3 -c '
215
+ import os, sys
216
+ state = sys.argv[1]
217
+ def m(name):
218
+ try:
219
+ return os.path.getmtime(os.path.join(state, name))
220
+ except OSError:
221
+ return None
222
+ e, v = m("last_edit"), m("last_verify")
223
+ print("yes" if e is not None and (v is None or e > v) else "no")' "${STATE}" 2>/dev/null || echo no)"
224
+ if [ "${NUDGE}" = yes ] && [ ! -f "${STATE}/nudged" ]; then
225
+ touch "${STATE}/nudged"
226
+ stat_log nudge
227
+ echo "Doctrine rule 1: edits were made but no verify command has run since the last edit. Run the repo's verify command and quote its decisive line — or finish anyway and say plainly that the change is unverified. (This nudge fires once.)" >&2
228
+ exit 2
229
+ fi
230
+ # NUDGE=no -> genuinely clean stop; yes-but-already-nudged logs nothing
231
+ # (that nudge was counted when it fired)
232
+ [ "${NUDGE}" = no ] && stat_log stop-clean
233
+ ;;
234
+ session)
235
+ # SessionStart: if a handoff capsule exists, emit ONE pointer line as
236
+ # context (stdout) — never the capsule's contents, which must be read and
237
+ # re-verified, not trusted. Silent (zero context cost) when none exists.
238
+ CAP="${CWD}/HANDOFF.md"
239
+ [ -f "${CAP}" ] || exit 0
240
+ AGE="$(python3 -c 'import os,sys,time;print(int((time.time()-os.path.getmtime(sys.argv[1]))//86400))' "${CAP}" 2>/dev/null || echo '')"
241
+ STALE="${LUCIAZERO_HANDOFF_STALE_DAYS:-7}"
242
+ if [ -n "${AGE}" ] && [ "${AGE}" -ge "${STALE}" ] 2>/dev/null; then
243
+ echo "HANDOFF.md exists but is ${AGE} days old — likely stale; a stale capsule is worse than none. Verify its claims with extra suspicion or delete it."
244
+ else
245
+ echo "HANDOFF.md exists (age: ${AGE:-?}d) — read it before touching code, re-verify its claims against the tree, then delete it (/handoff consume protocol)."
246
+ fi
247
+ ;;
248
+ *)
249
+ ;;
250
+ esac
251
+
252
+ exit 0
@@ -0,0 +1,24 @@
1
+ # Luciazero — default operating mode
2
+
3
+ Applies to every repo, every session. The loop is plan → change → verify → fix, repeated until verify passes or the blocker is real.
4
+
5
+ ## Ground truth
6
+
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: `/luciazero-bootstrap`). Do not silently proceed on vibes.
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
+
11
+ ## Loop
12
+
13
+ 4. **Debugging starts with a hypothesis, not an edit.** State the suspected cause and the command whose output would confirm or refute it — run that command before touching code (procedure: `/debug`). Keep the reproduction as a regression test: red before the fix, green after.
14
+ 5. **Orient before editing an unfamiliar repo:** find how to run, test, and lint it — CI is the most honest source of truth.
15
+ 6. **Smallest reversible step.** Targeted diff over rewrite; extend an existing helper instead of adding a parallel one.
16
+ 7. **Review the diff as a skeptic before saying done.** Tests prove what they cover; the last pass hunts what they do not — edge cases, error paths, changed contracts, files touched by accident. For risky or wide diffs, get an independent adversarial review — the harness's built-in review command when one exists, otherwise the `reviewer` agent — instructed to refute, not approve.
17
+
18
+ ## Memory
19
+
20
+ 8. **Never re-derive a dead end twice.** Non-obvious findings — null results, footguns, "tried X, failed because Y" — go into the project's notes file (`CLAUDE.md` / `AGENTS.md`, extend the one the repo uses), and those notes get read before working in an area they cover (procedure: `/retro`).
21
+
22
+ ## Autonomy
23
+
24
+ 9. **Stop and ask before:** deleting data, deploying or touching production, force-pushing, changing a public API or contract, spending real money, or leaving the agreed scope — make the question decidable in one reply: what, why, options, recommendation. Everything else: proceed, batching unknowns into one sharp question at the right time. Finish the whole scope; name anything left out.
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env bash
2
+ # Install the Luciazero doctrine + skills into OpenAI Codex CLI (~/.codex).
3
+ # Idempotent. Backs up AGENTS.md before editing. Writes nothing outside CODEX_HOME.
4
+ #
5
+ # Mapping (single source of truth stays in claude/):
6
+ # claude/luciazero.md -> marker block in ~/.codex/AGENTS.md
7
+ # skills/<each>/ -> ~/.codex/skills/<each>/
8
+ # (luciazero-bootstrap, retro, debug, done, handoff, experiment)
9
+ # claude/agents/reviewer.md -> ~/.codex/skills/reviewer/SKILL.md
10
+ # (Codex has no subagents; ships as a skill,
11
+ # Claude-only `tools:`/`model:` lines dropped)
12
+ # claude/hooks/ (enforcement pack) -> NOT installed: Codex has no hooks/statusline
13
+ set -euo pipefail
14
+
15
+ for ARG in "$@"; do
16
+ echo "unknown option: ${ARG} (install-codex.sh takes no options)" >&2; exit 1
17
+ done
18
+
19
+ SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
20
+ CODEX_DIR="${CODEX_HOME:-$HOME/.codex}"
21
+ AGENTS_MD="${CODEX_DIR}/AGENTS.md"
22
+ START='<!-- luciazero:start -->'
23
+ END='<!-- luciazero:end -->'
24
+
25
+ # collision-proof backup path for $1 (two runs in the same second must not overwrite)
26
+ bakpath() {
27
+ B="$1.bak.$(date +%Y%m%d%H%M%S)"
28
+ N=1
29
+ while [ -e "${B}" ]; do B="$1.bak.$(date +%Y%m%d%H%M%S).${N}"; N=$((N+1)); done
30
+ printf '%s' "${B}"
31
+ }
32
+
33
+ echo "Installing into ${CODEX_DIR}"
34
+ mkdir -p "${CODEX_DIR}/skills"
35
+
36
+ # 1. doctrine as a marker block in global AGENTS.md (replaced in place on reinstall)
37
+ TMP="$(mktemp)"
38
+ if [ -f "${AGENTS_MD}" ]; then
39
+ cp "${AGENTS_MD}" "$(bakpath "${AGENTS_MD}")"
40
+ # strip the old block AND trailing blank lines, so reinstalls do not
41
+ # accumulate one separator blank line per run
42
+ awk -v s="${START}" -v e="${END}" '
43
+ $0==s {inblock=1; next}
44
+ $0==e {inblock=0; next}
45
+ inblock {next}
46
+ NF {for (i=0; i<blank; i++) print ""; blank=0; print; next}
47
+ {blank++}
48
+ ' "${AGENTS_MD}" > "${TMP}"
49
+ fi
50
+ {
51
+ if [ -s "${TMP}" ]; then cat "${TMP}"; echo; fi
52
+ echo "${START}"
53
+ cat "${SRC}/claude/luciazero.md"
54
+ echo "${END}"
55
+ } > "${AGENTS_MD}"
56
+ rm -f "${TMP}"
57
+ echo " ok AGENTS.md doctrine block"
58
+
59
+ # 2. skills — same SKILL.md format as Claude Code, copied as-is
60
+ for SKILL in luciazero-bootstrap retro debug 'done' handoff experiment; do
61
+ rm -rf "${CODEX_DIR}/skills/${SKILL}"
62
+ cp -r "${SRC}/skills/${SKILL}" "${CODEX_DIR}/skills/${SKILL}"
63
+ echo " ok skills/${SKILL}"
64
+ done
65
+
66
+ # 3. reviewer agent as a Codex skill, minus the Claude-only tools:/model: lines
67
+ rm -rf "${CODEX_DIR}/skills/reviewer"
68
+ mkdir -p "${CODEX_DIR}/skills/reviewer"
69
+ awk 'NR==1 {front=($0=="---")} front && /^(tools|model): / {next} {print} front && NR>1 && $0=="---" {front=0}' \
70
+ "${SRC}/claude/agents/reviewer.md" > "${CODEX_DIR}/skills/reviewer/SKILL.md"
71
+ echo " ok skills/reviewer"
72
+
73
+ # 4. version sidecar (informational; removed by uninstall-codex.sh)
74
+ V_NEW="$(grep -m1 -oE '^## \[[0-9]+\.[0-9]+\.[0-9]+\]' "${SRC}/CHANGELOG.md" 2>/dev/null | tr -d '#[] ' || true)"
75
+ if [ -n "${V_NEW}" ]; then
76
+ printf '%s\n' "${V_NEW}" > "${CODEX_DIR}/.luciazero-version"
77
+ fi
78
+
79
+ echo
80
+ echo "Done. Verify:"
81
+ echo " grep -c 'luciazero:start' ${AGENTS_MD} # expect 1"
82
+ echo " ls ${CODEX_DIR}/skills/"
83
+ echo
84
+ echo "The doctrine applies from the next Codex session."