overcodex 0.1.0__py3-none-any.whl

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.
overcodex/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ """overcodex — Codex CLI, overclocked: cold account switching, handoff prompts,
2
+ statusline config, and AGENTS.md routing policy for multi-agent workflows."""
3
+
4
+ __all__ = []
overcodex/cli.py ADDED
@@ -0,0 +1,53 @@
1
+ """overcodex CLI — runs the bundled kit installer/uninstaller.
2
+
3
+ The package wheel carries the same payload a git clone has (bin/, hooks/,
4
+ config/, codex/, prompts/, shell/, and the install/uninstall scripts). This
5
+ CLI just locates that payload and runs the battle-tested bash scripts
6
+ against it.
7
+ """
8
+
9
+ import argparse
10
+ import os
11
+ import subprocess
12
+ import sys
13
+ from importlib.metadata import version as pkg_version
14
+ from importlib.resources import files
15
+
16
+
17
+ def _payload_dir():
18
+ p = files("overcodex").joinpath("payload")
19
+ path = str(p)
20
+ if not os.path.isdir(path) or not os.path.isfile(os.path.join(path, "install.sh")):
21
+ sys.exit("overcodex: bundled payload is missing — broken install; reinstall the package")
22
+ return path
23
+
24
+
25
+ def _run_script(name):
26
+ return subprocess.call(["bash", os.path.join(_payload_dir(), name)])
27
+
28
+
29
+ def main():
30
+ ap = argparse.ArgumentParser(
31
+ prog="overcodex",
32
+ description="Codex CLI, overclocked — codex-swap, hooks, statusline, AGENTS.md routing.",
33
+ )
34
+ sub = ap.add_subparsers(dest="cmd", required=True)
35
+ sub.add_parser("install", help="install/refresh the kit into $CODEX_HOME (idempotent, backs everything up)")
36
+ sub.add_parser("uninstall", help="remove exactly what install added")
37
+ sub.add_parser("path", help="print the bundled payload directory")
38
+ sub.add_parser("version", help="print the overcodex version")
39
+ args = ap.parse_args()
40
+
41
+ if args.cmd == "install":
42
+ sys.exit(_run_script("install.sh"))
43
+ if args.cmd == "uninstall":
44
+ sys.exit(_run_script("uninstall.sh"))
45
+ if args.cmd == "path":
46
+ print(_payload_dir())
47
+ return
48
+ if args.cmd == "version":
49
+ print(pkg_version("overcodex"))
50
+
51
+
52
+ if __name__ == "__main__":
53
+ main()
@@ -0,0 +1,320 @@
1
+ #!/usr/bin/env bash
2
+ # codex-swap — cold multi-account manager for the Codex CLI.
3
+ #
4
+ # Design: CODEX_HOME-per-account isolation. Codex refresh tokens can be
5
+ # single-use across COPIES of auth.json, so accounts are never created by
6
+ # copying credentials — each account gets its own real $CODEX_HOME directory
7
+ # under ~/.codex-accounts/<name>/ and logs in there directly. This is a COLD
8
+ # switch by design: a running `codex` process reads its credentials once at
9
+ # startup, so switching the active account never affects sessions already
10
+ # running — only new `codex` invocations (via the codex() shell wrapper) pick
11
+ # it up. There is no hot-swap here; that's a different kit's trick.
12
+ #
13
+ # Layout:
14
+ # ~/.codex the PRIMARY account (name: "primary", implicit,
15
+ # never created/removed by this tool)
16
+ # ~/.codex-accounts/<name>/ one directory per additional account
17
+ # ~/.codex-accounts/.active one line: the active account name (or absent /
18
+ # "primary" for the default)
19
+ #
20
+ # Per account, "add" symlinks the SHARED config in from the primary home
21
+ # (AGENTS.md, prompts/, hooks/, hooks.json, config.toml) so every account
22
+ # runs the same routing/hooks setup. auth.json and all session/thread state
23
+ # are always per-account: real files, never symlinked, never copied.
24
+ #
25
+ # Subcommands:
26
+ # add <name> create (or repair) an account, linking shared config
27
+ # list [--json] accounts, which is active, login state
28
+ # use <name|primary> set the active-account marker (cold switch)
29
+ # which print the active account name + its CODEX_HOME
30
+ # remove <name> [--yes] delete an account (never removes primary)
31
+ set -u
32
+
33
+ HOME_DIR="${HOME:?codex-swap: \$HOME is not set}"
34
+ PRIMARY_HOME="$HOME_DIR/.codex"
35
+ ACCOUNTS_ROOT="$HOME_DIR/.codex-accounts"
36
+ ACTIVE_FILE="$ACCOUNTS_ROOT/.active"
37
+ PRIMARY_LABEL="primary"
38
+
39
+ # Shared across every account (symlinked from the primary home by `add`).
40
+ SHARED_ITEMS="AGENTS.md prompts hooks hooks.json config.toml"
41
+
42
+ usage() {
43
+ cat >&2 <<'EOF'
44
+ usage: codex-swap <subcommand> [args]
45
+ add <name> create/repair an account under ~/.codex-accounts/<name>
46
+ (symlinks shared config from primary; prints login steps)
47
+ list [--json] accounts + active marker + login state
48
+ use <name|primary> set the active account marker (cold switch — restart
49
+ running codex sessions to adopt it)
50
+ which print the active account name and its CODEX_HOME
51
+ remove <name> [--yes] delete an account's directory (never removes primary)
52
+
53
+ "primary" always refers to the default $HOME/.codex and is never created,
54
+ listed as a file, or removable — it's the account you get with no marker set.
55
+ EOF
56
+ exit 2
57
+ }
58
+
59
+ die() { echo "codex-swap: $1" >&2; exit 1; }
60
+
61
+ charset_ok() {
62
+ case "$1" in
63
+ '') return 1 ;;
64
+ *[!A-Za-z0-9_-]*) return 1 ;;
65
+ *) return 0 ;;
66
+ esac
67
+ }
68
+
69
+ home_for() {
70
+ if [ "$1" = "$PRIMARY_LABEL" ]; then
71
+ printf '%s\n' "$PRIMARY_HOME"
72
+ else
73
+ printf '%s\n' "$ACCOUNTS_ROOT/$1"
74
+ fi
75
+ }
76
+
77
+ active_name() {
78
+ local n=""
79
+ if [ -f "$ACTIVE_FILE" ]; then
80
+ n="$(tr -d '[:space:]' <"$ACTIVE_FILE" 2>/dev/null)"
81
+ fi
82
+ [ -n "$n" ] || n="$PRIMARY_LABEL"
83
+ printf '%s\n' "$n"
84
+ }
85
+
86
+ # human_age <epoch-seconds> — coarse relative-time string.
87
+ human_age() {
88
+ local mtime="$1" now diff
89
+ case "$mtime" in ''|*[!0-9]*) printf 'unknown age\n'; return 0 ;; esac
90
+ now="$(date +%s)"
91
+ diff=$(( now - mtime ))
92
+ [ "$diff" -ge 0 ] || diff=0
93
+ if [ "$diff" -lt 60 ]; then printf '%ss ago\n' "$diff"
94
+ elif [ "$diff" -lt 3600 ]; then printf '%sm ago\n' $(( diff / 60 ))
95
+ elif [ "$diff" -lt 86400 ]; then printf '%sh ago\n' $(( diff / 3600 ))
96
+ else printf '%sd ago\n' $(( diff / 86400 ))
97
+ fi
98
+ }
99
+
100
+ # auth_status <codex_home> — one-line login state for the account.
101
+ auth_status() {
102
+ local ch="$1" f mtime
103
+ f="$ch/auth.json"
104
+ if [ -f "$f" ]; then
105
+ mtime="$(stat -f %m "$f" 2>/dev/null)" || mtime=""
106
+ printf 'logged in (auth.json %s)\n' "$(human_age "$mtime")"
107
+ else
108
+ printf 'not logged in (no auth.json)\n'
109
+ fi
110
+ }
111
+
112
+ # all_account_names — "primary" then every dir under ACCOUNTS_ROOT, one per line.
113
+ all_account_names() {
114
+ printf '%s\n' "$PRIMARY_LABEL"
115
+ [ -d "$ACCOUNTS_ROOT" ] || return 0
116
+ local d n
117
+ for d in "$ACCOUNTS_ROOT"/*/; do
118
+ [ -d "$d" ] || continue
119
+ n="$(basename "$d")"
120
+ printf '%s\n' "$n"
121
+ done
122
+ }
123
+
124
+ cmd_add() {
125
+ local name="${1:-}" acc_dir is_new item src dst
126
+ [ -n "$name" ] || { echo "usage: codex-swap add <name>" >&2; exit 2; }
127
+ charset_ok "$name" || die "invalid name '$name' — use letters, digits, - and _ only"
128
+ [ "$name" != "$PRIMARY_LABEL" ] || die "'$PRIMARY_LABEL' is reserved for \$CODEX_HOME ($PRIMARY_HOME) — pick another name"
129
+
130
+ [ -d "$PRIMARY_HOME" ] || echo "codex-swap: warning: primary CODEX_HOME ($PRIMARY_HOME) does not exist yet — shared items will be skipped" >&2
131
+
132
+ acc_dir="$ACCOUNTS_ROOT/$name"
133
+ is_new=1
134
+ [ -d "$acc_dir" ] && is_new=0
135
+ mkdir -p "$acc_dir" || die "could not create $acc_dir"
136
+
137
+ echo "== codex-swap add $name =="
138
+ echo " account home: $acc_dir"
139
+ for item in $SHARED_ITEMS; do
140
+ src="$PRIMARY_HOME/$item"
141
+ dst="$acc_dir/$item"
142
+ if [ -e "$dst" ] || [ -L "$dst" ]; then
143
+ if [ -L "$dst" ] && [ "$(readlink "$dst")" = "$src" ]; then
144
+ echo " [=] already linked: $item"
145
+ else
146
+ echo " [!] leaving as-is (exists, not our symlink): $item" >&2
147
+ fi
148
+ continue
149
+ fi
150
+ if [ -e "$src" ]; then
151
+ if ln -s "$src" "$dst" 2>/dev/null; then
152
+ echo " [+] linked: $item -> $src"
153
+ else
154
+ echo " [!] failed to link: $item" >&2
155
+ fi
156
+ else
157
+ echo " [-] skip (not present in primary yet): $item"
158
+ fi
159
+ done
160
+
161
+ echo
162
+ if [ "$is_new" -eq 1 ]; then
163
+ echo "Created account '$name'. auth.json and session state are per-account (never shared, never copied)."
164
+ else
165
+ echo "Account '$name' already existed — repaired missing shared symlinks only (auth.json/session state untouched)."
166
+ fi
167
+ echo
168
+ echo "Next steps:"
169
+ echo " 1. One-off login for this account (does not touch primary or the active marker):"
170
+ echo " CODEX_HOME=\"$acc_dir\" codex login"
171
+ echo " 2. Make it the active account for the codex() shell wrapper:"
172
+ echo " codex-swap use $name"
173
+ echo " Then open a NEW terminal/tab (or: exec zsh) — this is a cold switch, running"
174
+ echo " codex sessions keep whatever account they started with."
175
+ echo
176
+ echo "Caveat: config.toml is shared by symlink. If it sets an ABSOLUTE sqlite_home or"
177
+ echo "log path, that storage would be shared too — keep such paths relative (the"
178
+ echo "default) so each account's session state actually stays separate."
179
+ exit 0
180
+ }
181
+
182
+ cmd_list() {
183
+ local as_json=no n ch mark login
184
+ case "${1:-}" in
185
+ --json) as_json=yes ;;
186
+ "") ;;
187
+ *) echo "usage: codex-swap list [--json]" >&2; exit 2 ;;
188
+ esac
189
+
190
+ local active; active="$(active_name)"
191
+
192
+ if [ "$as_json" = yes ]; then
193
+ printf '['
194
+ local first=1
195
+ while IFS= read -r n; do
196
+ [ -n "$n" ] || continue
197
+ ch="$(home_for "$n")"
198
+ login=false
199
+ [ -f "$ch/auth.json" ] && login=true
200
+ [ "$first" -eq 1 ] || printf ','
201
+ printf '{"name":"%s","codexHome":"%s","active":%s,"loggedIn":%s}' \
202
+ "$n" "$ch" "$([ "$n" = "$active" ] && echo true || echo false)" "$login"
203
+ first=0
204
+ done <<EOF
205
+ $(all_account_names)
206
+ EOF
207
+ printf ']\n'
208
+ exit 0
209
+ fi
210
+
211
+ printf '%-2s %-20s %-10s\n' "" "NAME" "STATE"
212
+ while IFS= read -r n; do
213
+ [ -n "$n" ] || continue
214
+ ch="$(home_for "$n")"
215
+ mark=" "
216
+ [ "$n" = "$active" ] && mark="*"
217
+ printf '%-2s %-20s %s\n' "$mark" "$n" "$(auth_status "$ch")"
218
+ printf ' %-20s %s\n' "" "$ch"
219
+ done <<EOF
220
+ $(all_account_names)
221
+ EOF
222
+ echo
223
+ echo "* = active for the codex() shell wrapper (cold switch — see 'codex-swap which')"
224
+ exit 0
225
+ }
226
+
227
+ cmd_use() {
228
+ local name="${1:-}"
229
+ [ -n "$name" ] || { echo "usage: codex-swap use <name|primary>" >&2; exit 2; }
230
+ if [ "$name" != "$PRIMARY_LABEL" ]; then
231
+ charset_ok "$name" || die "invalid name '$name'"
232
+ [ -d "$ACCOUNTS_ROOT/$name" ] || die "no such account '$name' (see: codex-swap list)"
233
+ fi
234
+
235
+ mkdir -p "$ACCOUNTS_ROOT" || die "could not create $ACCOUNTS_ROOT"
236
+ local tmp="$ACTIVE_FILE.tmp.$$"
237
+ if printf '%s\n' "$name" >"$tmp" 2>/dev/null && mv "$tmp" "$ACTIVE_FILE" 2>/dev/null; then
238
+ echo "Active account set to '$name' ($(home_for "$name"))."
239
+ echo "Cold switch: any ALREADY-RUNNING codex session keeps its old account until"
240
+ echo "restarted. Open a new terminal/tab (or: exec zsh) and start a fresh 'codex'."
241
+ exit 0
242
+ fi
243
+ rm -f "$tmp" 2>/dev/null
244
+ die "failed to write active marker $ACTIVE_FILE"
245
+ }
246
+
247
+ cmd_which() {
248
+ local n ch
249
+ n="$(active_name)"
250
+ ch="$(home_for "$n")"
251
+ echo "$n"
252
+ echo " CODEX_HOME: $ch"
253
+ echo " $(auth_status "$ch")"
254
+ exit 0
255
+ }
256
+
257
+ cmd_remove() {
258
+ local name="${1:-}" confirm_yes=no acc_dir reply
259
+ [ -n "$name" ] || { echo "usage: codex-swap remove <name> [--yes]" >&2; exit 2; }
260
+ shift 2>/dev/null || true
261
+ while [ $# -gt 0 ]; do
262
+ case "$1" in --yes|-y) confirm_yes=yes ;; esac
263
+ shift
264
+ done
265
+
266
+ [ "$name" != "$PRIMARY_LABEL" ] || die "refusing to remove '$PRIMARY_LABEL' (\$CODEX_HOME, $PRIMARY_HOME)"
267
+ charset_ok "$name" || die "invalid name '$name'"
268
+ acc_dir="$ACCOUNTS_ROOT/$name"
269
+ [ -d "$acc_dir" ] || die "no such account '$name' (see: codex-swap list)"
270
+ # Defense in depth: never rm -rf outside ACCOUNTS_ROOT even though the
271
+ # charset check above already makes escaping the directory impossible.
272
+ case "$acc_dir" in
273
+ "$ACCOUNTS_ROOT"/*) : ;;
274
+ *) die "refusing to remove path outside $ACCOUNTS_ROOT: $acc_dir" ;;
275
+ esac
276
+
277
+ if [ "$confirm_yes" != yes ]; then
278
+ if [ -t 0 ]; then
279
+ printf "This deletes %s — including its auth.json and session state. Type the account name to confirm: " "$acc_dir"
280
+ read -r reply
281
+ [ "$reply" = "$name" ] || die "confirmation did not match — aborted, nothing removed"
282
+ else
283
+ die "remove requires --yes when not run interactively"
284
+ fi
285
+ fi
286
+
287
+ rm -rf "$acc_dir" || die "failed to remove $acc_dir"
288
+ echo "Removed account '$name' ($acc_dir)."
289
+
290
+ if [ "$(active_name)" = "$name" ]; then
291
+ rm -f "$ACTIVE_FILE" 2>/dev/null
292
+ echo "It was the active account — active account reset to '$PRIMARY_LABEL'."
293
+ echo "Cold switch: restart running codex sessions to adopt this."
294
+ fi
295
+ exit 0
296
+ }
297
+
298
+ main() {
299
+ local sub="${1:-}"
300
+ [ $# -gt 0 ] && shift
301
+ case "$sub" in
302
+ add) cmd_add "$@" ;;
303
+ list) cmd_list "$@" ;;
304
+ use) cmd_use "$@" ;;
305
+ which) cmd_which "$@" ;;
306
+ remove) cmd_remove "$@" ;;
307
+ -h|--help|help|"") usage ;;
308
+ *)
309
+ # Bare account name convenience: `codex-swap work` == `codex-swap use work`.
310
+ if [ -d "$ACCOUNTS_ROOT/$sub" ]; then
311
+ cmd_use "$sub"
312
+ else
313
+ echo "codex-swap: unknown subcommand or account '$sub'" >&2; usage
314
+ fi ;;
315
+ esac
316
+ }
317
+
318
+ if [ "${BASH_SOURCE[0]:-}" = "$0" ]; then
319
+ main "$@"
320
+ fi
@@ -0,0 +1,54 @@
1
+ # --- overcodex ultracode (begin) ---
2
+ # ULTRACODE.md — Model & Effort Routing for Codex (v2-codex, ported 2026-07 from the Claude Code ULTRACODE v2)
3
+
4
+ ## 1. Core principle
5
+ Spend the flagship tier only where judgment is the bottleneck, never where volume is. `model_reasoning_effort` and any per-role `model` override that you omit silently inherits the top-level `model` (currently `gpt-5.6-sol` — the flagship). Explicit down-routing is your default posture, not an optimization.
6
+ Route each subagent/role by one question: "if this is quietly wrong, who catches it?" — a downstream check means you may downgrade; nobody means top tier.
7
+
8
+ ## 2. Tier map (GPT-5.6 family, mid-2026)
9
+ Codex's current lineup is three permanent price/capability tiers, not a Claude-style haiku/sonnet/opus/apex ladder — treat them as the equivalent rungs:
10
+
11
+ | Rung | Codex tier | Claude-side equivalent | Use for |
12
+ |---|---|---|---|
13
+ | 1 (cheap/fast) | **Luna** | haiku | scouting, file listing, mechanical transforms, fixed-schema extraction |
14
+ | 2 (mid) | **Terra** | sonnet | finder sweeps, well-scoped implementation edits, bulk reading of dense code |
15
+ | 3 (flagship) | **Sol** | opus | security sweeps, cross-cutting edits, adversarial verification, judge panels, completeness critics |
16
+ | 3 + effort ceiling | **Sol @ high** (or the Max reasoning-effort / Ultra sub-agent mode where the deployment exposes it) | fable/apex | terminal judge, final synthesis, subtle-correctness verdicts |
17
+
18
+ Effort is the second axis: `model_reasoning_effort = low \| medium \| high` (config.toml or `--config`). Pair rung × effort the same way ULTRACODE always has:
19
+ - Rung 1 → low. Rung 2 → medium. Rung 3 → high. Terminal/apex → Sol @ high, plus Max/Ultra if the account has it — never below Sol.
20
+ - Effort amplifies capability, it never substitutes: Luna@high loses to Terra@medium on judgment work. Never pair a high-effort setting with a fan-out stage.
21
+
22
+ ## 3. Routing surface (how to actually pin a tier per subagent)
23
+ Codex's per-subagent override surface is younger than Claude Code's Task-tool `model` param — there is no first-class "one dispatch call, one model" primitive yet. Use what exists, and state the gap honestly where it doesn't:
24
+ - **`[agents]` table (`AgentRoleToml` per role)** in config.toml — the closest analog to Claude's per-agent `model`. Give each role you define (scout, finder, verifier, judge) its own `model` + `model_reasoning_effort` entry. This is the primary lever; use it whenever the orchestrator supports role-scoped agents.
25
+ - **`profiles`** (`codex --profile <name>`) — a named bundle of `model` + `model_reasoning_effort` (+ sandbox/approval settings). Define one profile per rung (e.g. `scout`, `implement`, `verify`, `apex`) and invoke the right profile per stage instead of editing global config mid-session.
26
+ - **`orchestrator.max_threads` / `max_depth`** — the fan-out width and recursion-depth knobs. Set `max_threads` to match the routing-lint width rule below (wide stages get width, not tier); use `max_depth` to cap runaway recursive sub-agent spawning, which is the Codex-side proxy for "never two agents editing the same files."
27
+ - **Where enforcement is weak** (no live per-call model override mid-session, no schema-typed subagent handoff): the orchestrating model itself must apply the routing table by discipline — choosing the right profile/role before dispatch — because the harness will not silently downgrade or refuse an unrouted call the way a stricter per-call API might. Say so in-session rather than assuming the harness caught it.
28
+
29
+ ## 4. Hard guardrails (unchanged from v2, ported verbatim)
30
+ Never downgrade below floor:
31
+ - Final synthesis and any output the user sees with no downstream check: Sol, never below. Terminal stage = apex profile, or Sol@high with Max/Ultra if available.
32
+ - Adversarial verification, judge panels, security verdicts, completeness critics: Sol floor. A false CONFIRM ends scrutiny.
33
+ - Subtle-correctness verdicts (concurrency, auth/crypto, money math, migrations): apex — "looks correct" and "is correct" diverge most here.
34
+
35
+ Verification order: where an OBJECTIVE check exists (tests, typecheck, lint, a numeric answer), gate on it via `codex exec` + shell BEFORE spending an LLM verifier. A passing test outranks an LLM CONFIRM. For fuzzy deliverables with no automatic check, buy a stronger generator, not a weak-generator-plus-judge pipeline.
36
+
37
+ Escalation rule: every finder/verifier role emits `verdict`/`confidence`/`evidence` (approximate via prompt contract — Codex has no first-class typed `schema` param yet, so state the required shape in the prompt and treat a response missing any field as low confidence). Re-run once at the next rung up on confidence < 0.7, UNSURE, empty/malformed output, or contradiction between parallel roles. Escalation target must be ≥ generator's rung. Escalation has a ceiling too: >1-in-4 downgraded stages escalating means the routing was miscalibrated — stop and re-profile, don't silently run everything on Sol.
38
+
39
+ Panels: 2–3 voters with distinct lenses (correctness / security / reproduces-it), never N identical-role repeats. Unanimous → accept; split → one apex adjudicator; never majority-vote or average.
40
+
41
+ Budget pressure: cut `max_threads` / batch more files per role first; floors are the last thing to fall. Treat the apex rung as a read-only reserve (usually synthesis only). If budget can't cover the apex/Sol terminal stages, say so and propose the cut — never silently ship a downgraded final answer.
42
+
43
+ ## 5. Routing lint (pre-flight, fix before dispatch)
44
+ 1. Every role/profile has an explicit `model` + `model_reasoning_effort`, or the omission is a deliberate apex spend (inherits top-level `model`). More than 2 omissions = under-routing.
45
+ 2. No wide/parallel role runs on Sol@high or inherits the apex profile; bulk roles sit at Luna/Terra regardless of the rest of the routing.
46
+ 3. Verifiers, judges, synthesis are at their floors even under budget pressure; `max_depth` prevents two roles editing the same files concurrently.
47
+ 4. Every downgraded trusted-adjacent stage has a stated escalation trigger, and an objective check (test/lint/typecheck via shell) gates before any LLM verifier where one exists.
48
+ 5. Every role's prompt states an objective + expected output shape + scope boundary vs sibling roles — Codex has no schema enforcement, so the prompt IS the contract.
49
+ 6. A stage is justified only if it accesses information the prior stage couldn't (new tool call, test run, independent read). A stage that reformats an upstream conclusion is overhead — collapse it into one higher-effort call.
50
+ 7. Profile/role names embed the tier (e.g. `verify-sol-high`), so a session log shows the routing without opening config.toml.
51
+
52
+ ## 6. Scope
53
+ This table binds any one-off `codex exec` dispatch too, not just multi-agent orchestrator runs: a lone bulk-reading call is still a Luna/Terra job; a lone terminal judgment still earns Sol@high or a deliberate apex inheritance. Absence of `[agents]`/`orchestrator` config does not relax the discipline — apply it by hand via `--profile` and `--config model_reasoning_effort=`.
54
+ # --- overcodex ultracode (end) ---
@@ -0,0 +1,42 @@
1
+ # overcodex hook wiring — TOML fragment. install.sh substitutes @HOOKS_DIR@
2
+ # with the resolved absolute $CODEX_HOME/hooks and appends the result between
3
+ # marker comments at the END of config.toml (a [hooks] table cannot be
4
+ # prepended: it would capture every top-level key that follows it).
5
+ #
6
+ # Shape provenance (codex-cli 0.144.5, reconstructed from the binary's
7
+ # embedded types — no public docs exist for this surface as of this writing):
8
+ # HookEventsToml: PascalCase event keys (PreToolUse, PermissionRequest,
9
+ # PostToolUse, PreCompact, PostCompact, SessionStart, UserPromptSubmit,
10
+ # SubagentStart, SubagentStop, Stop); each event holds an ARRAY of matcher
11
+ # groups (ConfiguredHookMatcherGroup: matcher?, hooks[]).
12
+ # Handlers: internally tagged HookHandlerConfig::Command with fields
13
+ # type / command / commandWindows / timeout / async / statusMessage
14
+ # (`timeout`, NOT the app-server wire name `timeoutSec`).
15
+ # `matcher` semantics for non-tool events are unconfirmed — omitted here;
16
+ # each script filters on its own payload fields instead.
17
+ # FLAG FOR LIVE VALIDATION: if codex warns on config load, check its
18
+ # configWarning output first — the shape above is inferred, not documented.
19
+
20
+ [[hooks.SessionStart]]
21
+ [[hooks.SessionStart.hooks]]
22
+ type = "command"
23
+ command = "bash @HOOKS_DIR@/overcodex-handoff-inject.sh"
24
+ timeout = 10
25
+
26
+ [[hooks.UserPromptSubmit]]
27
+ [[hooks.UserPromptSubmit.hooks]]
28
+ type = "command"
29
+ command = "bash @HOOKS_DIR@/overcodex-ctx-watch.sh"
30
+ timeout = 5
31
+
32
+ [[hooks.Stop]]
33
+ [[hooks.Stop.hooks]]
34
+ type = "command"
35
+ command = "bash @HOOKS_DIR@/overcodex-notify.sh"
36
+ timeout = 5
37
+
38
+ [[hooks.PreCompact]]
39
+ [[hooks.PreCompact.hooks]]
40
+ type = "command"
41
+ command = "bash @HOOKS_DIR@/overcodex-precompact-offer.sh"
42
+ timeout = 5
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env bash
2
+ # overcodex-ctx-lib.sh — shared helper, sourced by overcodex-ctx-watch.sh,
3
+ # overcodex-notify.sh and overcodex-precompact-offer.sh. Not a hook itself
4
+ # (no shebang execution expected; hooks.json never references this file).
5
+ #
6
+ # Codex hook stdin carries NO context-usage percentage (verified: none of the
7
+ # ten hook input JSON Schemas embedded in the codex-cli 0.144.5 binary —
8
+ # PreToolUse/PermissionRequest/PostToolUse/PreCompact/PostCompact/SessionStart/
9
+ # UserPromptSubmit/SubagentStart/SubagentStop/Stop — carry a token or context
10
+ # field). There is also no custom-command statusline relay available: Codex's
11
+ # `tui.status_line` only lists built-in item names (run state, ctx/limits
12
+ # meters) — unlike Claude Code's statusline, it is not an external command fed
13
+ # rich JSON, so there is nothing analogous to overclaude's
14
+ # statusline-command.sh relay-file trick to piggyback on.
15
+ #
16
+ # What IS available: every hook payload with a `transcript_path` field points
17
+ # at the session's rollout JSONL file
18
+ # ($CODEX_HOME/sessions/YYYY/MM/DD/rollout-<ts>-<thread-id>.jsonl, confirmed by
19
+ # direct inspection of a real rollout in this sandbox — never modified, only
20
+ # read). Codex periodically appends a line shaped like:
21
+ # {"timestamp":"...","type":"event_msg",
22
+ # "payload":{"type":"token_count",
23
+ # "info":{"total_token_usage":{"...","total_tokens":N},
24
+ # "last_token_usage":{...},
25
+ # "model_context_window":W},
26
+ # "rate_limits":{...}}}
27
+ # total_tokens / model_context_window * 100, taken from the LAST such line, is
28
+ # used as the context-usage percentage everywhere below. This is a real
29
+ # measurement (not a heuristic) but its recency depends on how often Codex
30
+ # emits token_count events — needs live-session validation (see hooks-test
31
+ # notes) to confirm the cadence is tight enough for the 60/75/85 thresholds to
32
+ # feel timely rather than lagging.
33
+ #
34
+ # Contract: ctx_pct_from_transcript prints an integer 0-100 on stdout and
35
+ # returns 0 on success; on ANY failure (no file, no jq, malformed, division by
36
+ # zero) it prints nothing and returns 1. Callers must treat a non-zero return
37
+ # as "context usage unknown right now" and silently skip, never error.
38
+ exec 2>/dev/null
39
+
40
+ # Only scan the last N bytes of the rollout for performance/safety on very
41
+ # long sessions (rollouts can grow to many MB; a full-file grep on every
42
+ # UserPromptSubmit would add up). Falls back to a full-file scan on the rare
43
+ # chance the last token_count line is further back than the tail window (e.g.
44
+ # immediately followed by a very large function_call_output).
45
+ OVERCODEX_CTX_TAIL_BYTES=${OVERCODEX_CTX_TAIL_BYTES:-2000000}
46
+
47
+ ctx_pct_from_transcript() {
48
+ tp="${1:-}"
49
+ [ -n "$tp" ] && [ "$tp" != "null" ] && [ -f "$tp" ] || return 1
50
+ command -v jq >/dev/null 2>&1 || return 1
51
+
52
+ line="$(tail -c "$OVERCODEX_CTX_TAIL_BYTES" "$tp" 2>/dev/null | grep -a '"type":"token_count"' | tail -n 1)"
53
+ if [ -z "$line" ]; then
54
+ line="$(grep -a '"type":"token_count"' "$tp" 2>/dev/null | tail -n 1)"
55
+ fi
56
+ [ -n "$line" ] || return 1
57
+
58
+ total="$(printf '%s' "$line" | jq -r '.payload.info.total_token_usage.total_tokens // empty' 2>/dev/null)"
59
+ window="$(printf '%s' "$line" | jq -r '.payload.info.model_context_window // empty' 2>/dev/null)"
60
+ [ -n "$total" ] && [ -n "$window" ] || return 1
61
+ case "$total" in ''|*[!0-9]*) return 1 ;; esac
62
+ case "$window" in ''|*[!0-9]*) return 1 ;; esac
63
+ [ "$window" -gt 0 ] || return 1
64
+
65
+ awk -v t="$total" -v w="$window" 'BEGIN { printf "%d", (t / w) * 100 }'
66
+ }
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env bash
2
+ # overcodex-ctx-watch.sh — Codex UserPromptSubmit hook.
3
+ # Ported from overclaude's hooks/ctx-watch.sh. Computes context usage from the
4
+ # session's rollout transcript (see overcodex-ctx-lib.sh for why: Codex hook
5
+ # stdin carries no context percentage and there is no statusline relay to
6
+ # piggyback on) and, when usage crosses a new threshold, injects a
7
+ # handoff-offer note into the turn via hookSpecificOutput.additionalContext.
8
+ # Contract: always exit 0, silent on every error path, defensive jq parsing,
9
+ # no state writes on the /handoff skip path.
10
+ exec 2>/dev/null
11
+ set -u
12
+
13
+ HOOKS_DIR="$(cd "$(dirname "$0")" 2>/dev/null && pwd)" || exit 0
14
+ # shellcheck source=./overcodex-ctx-lib.sh
15
+ . "$HOOKS_DIR/overcodex-ctx-lib.sh" 2>/dev/null || exit 0
16
+
17
+ # Thresholds (contract: variables at top).
18
+ T1=60
19
+ T2=75
20
+ T3=85
21
+
22
+ CODEX_HOME="${CODEX_HOME:-$HOME/.codex}"
23
+ CTX_DIR="$CODEX_HOME/overcodex/ctx"
24
+
25
+ INPUT="$(cat)" || INPUT=""
26
+ command -v jq >/dev/null 2>&1 || exit 0
27
+
28
+ # Skip path FIRST — before any state read/write. Matches the /prompts:handoff
29
+ # custom-prompt convention this kit's prompts/ builder uses (see repo
30
+ # README), plus /handoff and /swap as generic aliases in case those also end
31
+ # up wired.
32
+ prompt="$(printf '%s' "$INPUT" | jq -r '.prompt // empty' 2>/dev/null)"
33
+ case "$prompt" in
34
+ "/handoff"*|"/swap"*|"/prompts:handoff"*) exit 0 ;;
35
+ esac
36
+
37
+ session_id="$(printf '%s' "$INPUT" | jq -r '.session_id // empty' 2>/dev/null)"
38
+ [ -n "$session_id" ] || exit 0
39
+
40
+ transcript_path="$(printf '%s' "$INPUT" | jq -r '.transcript_path // empty' 2>/dev/null)"
41
+
42
+ # pct unavailable (missing transcript, no token_count line yet, no jq) ->
43
+ # silent exit. This is expected/common early in a session before the first
44
+ # token_count event has been written.
45
+ pct="$(ctx_pct_from_transcript "$transcript_path")" || exit 0
46
+ case "$pct" in ''|*[!0-9]*) exit 0 ;; esac
47
+
48
+ # State (missing/corrupt -> fired=0 bannered=0). Shared state file with
49
+ # overcodex-notify.sh: each script only actively manages its own field but
50
+ # preserves the other's across writes, exactly as overclaude's ctx-watch.sh /
51
+ # ctx-notify.sh pair do.
52
+ state="$CTX_DIR/$session_id.state"
53
+ fired=0
54
+ bannered=0
55
+ if [ -f "$state" ]; then
56
+ f="$(jq -r '.fired // 0' "$state" 2>/dev/null)"
57
+ b="$(jq -r '.bannered // 0' "$state" 2>/dev/null)"
58
+ case "$f" in 0|60|75|85) fired=$f ;; esac
59
+ case "$b" in 0|60|75|85) bannered=$b ;; esac
60
+ fi
61
+
62
+ changed=0
63
+
64
+ # Re-arm rule: if pct < fired-10 -> fired = highest threshold <= pct
65
+ # (else 0), and bannered = min(bannered, fired).
66
+ if [ "$pct" -lt $(( fired - 10 )) ]; then
67
+ new_fired=0
68
+ for t in "$T1" "$T2" "$T3"; do
69
+ [ "$pct" -ge "$t" ] && new_fired=$t
70
+ done
71
+ fired=$new_fired
72
+ [ "$bannered" -gt "$fired" ] && bannered=$fired
73
+ changed=1
74
+ fi
75
+
76
+ # Fire: highest threshold T with pct >= T and T > fired.
77
+ T=0
78
+ for t in "$T1" "$T2" "$T3"; do
79
+ [ "$pct" -ge "$t" ] && T=$t
80
+ done
81
+ if [ "$T" -gt "$fired" ]; then
82
+ jq -n --arg ctx "[context-watch] Context is at ${pct}%. After fully completing the user's current request, tell them context is filling up and OFFER /prompts:handoff to continue in a fresh session. Do NOT invoke the handoff flow yourself unless the user explicitly accepts in their own message — an offer you made is not acceptance. If they decline or ignore the offer, drop the subject; this notice will re-appear at the next threshold." \
83
+ '{hookSpecificOutput: {hookEventName: "UserPromptSubmit", additionalContext: $ctx}}'
84
+ fired=$T
85
+ changed=1
86
+ fi
87
+
88
+ # Persist only when something changed (atomic write).
89
+ if [ "$changed" -eq 1 ]; then
90
+ mkdir -p "$CTX_DIR" 2>/dev/null || exit 0
91
+ tmp="$state.tmp.$$"
92
+ printf '{"fired":%d,"bannered":%d}\n' "$fired" "$bannered" > "$tmp" 2>/dev/null && mv -f "$tmp" "$state" 2>/dev/null
93
+ fi
94
+ exit 0