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 +4 -0
- overcodex/cli.py +53 -0
- overcodex/payload/bin/codex-swap +320 -0
- overcodex/payload/codex/AGENTS-ULTRACODE.md +54 -0
- overcodex/payload/config/hooks-block.toml.tpl +42 -0
- overcodex/payload/hooks/overcodex-ctx-lib.sh +66 -0
- overcodex/payload/hooks/overcodex-ctx-watch.sh +94 -0
- overcodex/payload/hooks/overcodex-handoff-inject.sh +94 -0
- overcodex/payload/hooks/overcodex-notify.sh +64 -0
- overcodex/payload/hooks/overcodex-precompact-offer.sh +40 -0
- overcodex/payload/install.sh +448 -0
- overcodex/payload/prompts/handoff-cancel.md +23 -0
- overcodex/payload/prompts/handoff-status.md +26 -0
- overcodex/payload/prompts/handoff.md +57 -0
- overcodex/payload/shell/zshrc-snippet.sh +24 -0
- overcodex/payload/uninstall.sh +196 -0
- overcodex-0.1.0.dist-info/METADATA +186 -0
- overcodex-0.1.0.dist-info/RECORD +21 -0
- overcodex-0.1.0.dist-info/WHEEL +4 -0
- overcodex-0.1.0.dist-info/entry_points.txt +2 -0
- overcodex-0.1.0.dist-info/licenses/LICENSE +21 -0
overcodex/__init__.py
ADDED
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
|