overclaude 1.0.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.
Files changed (99) hide show
  1. overclaude/__init__.py +1 -0
  2. overclaude/cli.py +53 -0
  3. overclaude/payload/bin/swap-guard +352 -0
  4. overclaude/payload/claude/ULTRACODE.md +79 -0
  5. overclaude/payload/hooks/ctx-notify.sh +58 -0
  6. overclaude/payload/hooks/ctx-watch.sh +79 -0
  7. overclaude/payload/hooks/handoff-inject.sh +67 -0
  8. overclaude/payload/install.sh +303 -0
  9. overclaude/payload/settings/settings-fragment.json +46 -0
  10. overclaude/payload/shell/zshrc-snippet.sh +31 -0
  11. overclaude/payload/skills/handoff/SKILL.md +79 -0
  12. overclaude/payload/skills/swap/SKILL.md +99 -0
  13. overclaude/payload/statusline/statusline-command.sh +247 -0
  14. overclaude/payload/uninstall.sh +166 -0
  15. overclaude/payload/vendor/claude-swap/.github/ISSUE_TEMPLATE/bug_report.yml +47 -0
  16. overclaude/payload/vendor/claude-swap/.github/ISSUE_TEMPLATE/config.yml +5 -0
  17. overclaude/payload/vendor/claude-swap/.github/ISSUE_TEMPLATE/feature_request.yml +16 -0
  18. overclaude/payload/vendor/claude-swap/.github/workflows/ci.yml +69 -0
  19. overclaude/payload/vendor/claude-swap/.github/workflows/publish.yml +28 -0
  20. overclaude/payload/vendor/claude-swap/.gitignore +13 -0
  21. overclaude/payload/vendor/claude-swap/.python-version +1 -0
  22. overclaude/payload/vendor/claude-swap/.vscode/launch.json +60 -0
  23. overclaude/payload/vendor/claude-swap/LICENSE +21 -0
  24. overclaude/payload/vendor/claude-swap/PKG-INFO +391 -0
  25. overclaude/payload/vendor/claude-swap/README.md +360 -0
  26. overclaude/payload/vendor/claude-swap/assets/tui-watch.png +0 -0
  27. overclaude/payload/vendor/claude-swap/pyproject.toml +58 -0
  28. overclaude/payload/vendor/claude-swap/src/claude_swap/__init__.py +9 -0
  29. overclaude/payload/vendor/claude-swap/src/claude_swap/__main__.py +6 -0
  30. overclaude/payload/vendor/claude-swap/src/claude_swap/autoswitch.py +1232 -0
  31. overclaude/payload/vendor/claude-swap/src/claude_swap/cache.py +44 -0
  32. overclaude/payload/vendor/claude-swap/src/claude_swap/claude_locks.py +147 -0
  33. overclaude/payload/vendor/claude-swap/src/claude_swap/cli.py +1210 -0
  34. overclaude/payload/vendor/claude-swap/src/claude_swap/credentials.py +1026 -0
  35. overclaude/payload/vendor/claude-swap/src/claude_swap/exceptions.py +96 -0
  36. overclaude/payload/vendor/claude-swap/src/claude_swap/json_output.py +180 -0
  37. overclaude/payload/vendor/claude-swap/src/claude_swap/locking.py +83 -0
  38. overclaude/payload/vendor/claude-swap/src/claude_swap/logging_config.py +64 -0
  39. overclaude/payload/vendor/claude-swap/src/claude_swap/macos_keychain.py +226 -0
  40. overclaude/payload/vendor/claude-swap/src/claude_swap/mappings.py +141 -0
  41. overclaude/payload/vendor/claude-swap/src/claude_swap/menubar.py +857 -0
  42. overclaude/payload/vendor/claude-swap/src/claude_swap/migrations.py +536 -0
  43. overclaude/payload/vendor/claude-swap/src/claude_swap/models.py +211 -0
  44. overclaude/payload/vendor/claude-swap/src/claude_swap/oauth.py +666 -0
  45. overclaude/payload/vendor/claude-swap/src/claude_swap/paths.py +201 -0
  46. overclaude/payload/vendor/claude-swap/src/claude_swap/poll_policy.py +203 -0
  47. overclaude/payload/vendor/claude-swap/src/claude_swap/printer.py +205 -0
  48. overclaude/payload/vendor/claude-swap/src/claude_swap/process_detection.py +146 -0
  49. overclaude/payload/vendor/claude-swap/src/claude_swap/session.py +1066 -0
  50. overclaude/payload/vendor/claude-swap/src/claude_swap/settings.py +425 -0
  51. overclaude/payload/vendor/claude-swap/src/claude_swap/snapshot_source.py +42 -0
  52. overclaude/payload/vendor/claude-swap/src/claude_swap/switcher.py +4264 -0
  53. overclaude/payload/vendor/claude-swap/src/claude_swap/transfer.py +552 -0
  54. overclaude/payload/vendor/claude-swap/src/claude_swap/tui/__init__.py +27 -0
  55. overclaude/payload/vendor/claude-swap/src/claude_swap/tui/app.py +290 -0
  56. overclaude/payload/vendor/claude-swap/src/claude_swap/tui/autoview.py +320 -0
  57. overclaude/payload/vendor/claude-swap/src/claude_swap/tui/cswap.tcss +202 -0
  58. overclaude/payload/vendor/claude-swap/src/claude_swap/tui/dashboard.py +403 -0
  59. overclaude/payload/vendor/claude-swap/src/claude_swap/tui/data.py +194 -0
  60. overclaude/payload/vendor/claude-swap/src/claude_swap/tui/modals.py +159 -0
  61. overclaude/payload/vendor/claude-swap/src/claude_swap/tui/theme.py +67 -0
  62. overclaude/payload/vendor/claude-swap/src/claude_swap/tui/widgets.py +369 -0
  63. overclaude/payload/vendor/claude-swap/src/claude_swap/update_check.py +135 -0
  64. overclaude/payload/vendor/claude-swap/src/claude_swap/usage_store.py +526 -0
  65. overclaude/payload/vendor/claude-swap/tests/__init__.py +1 -0
  66. overclaude/payload/vendor/claude-swap/tests/conftest.py +284 -0
  67. overclaude/payload/vendor/claude-swap/tests/fixtures/sample_config.json +10 -0
  68. overclaude/payload/vendor/claude-swap/tests/test_api_key_accounts.py +370 -0
  69. overclaude/payload/vendor/claude-swap/tests/test_autoswitch.py +1878 -0
  70. overclaude/payload/vendor/claude-swap/tests/test_cache.py +72 -0
  71. overclaude/payload/vendor/claude-swap/tests/test_claude_locks.py +99 -0
  72. overclaude/payload/vendor/claude-swap/tests/test_cli.py +1493 -0
  73. overclaude/payload/vendor/claude-swap/tests/test_config_cli.py +269 -0
  74. overclaude/payload/vendor/claude-swap/tests/test_json_output.py +590 -0
  75. overclaude/payload/vendor/claude-swap/tests/test_locking.py +162 -0
  76. overclaude/payload/vendor/claude-swap/tests/test_logging_config.py +59 -0
  77. overclaude/payload/vendor/claude-swap/tests/test_macos_keychain.py +212 -0
  78. overclaude/payload/vendor/claude-swap/tests/test_macos_keychain_contract.py +283 -0
  79. overclaude/payload/vendor/claude-swap/tests/test_mappings.py +185 -0
  80. overclaude/payload/vendor/claude-swap/tests/test_menubar.py +440 -0
  81. overclaude/payload/vendor/claude-swap/tests/test_migrations.py +596 -0
  82. overclaude/payload/vendor/claude-swap/tests/test_oauth.py +1379 -0
  83. overclaude/payload/vendor/claude-swap/tests/test_paths.py +316 -0
  84. overclaude/payload/vendor/claude-swap/tests/test_poll_policy.py +201 -0
  85. overclaude/payload/vendor/claude-swap/tests/test_printer.py +181 -0
  86. overclaude/payload/vendor/claude-swap/tests/test_process_detection.py +356 -0
  87. overclaude/payload/vendor/claude-swap/tests/test_session.py +1614 -0
  88. overclaude/payload/vendor/claude-swap/tests/test_settings.py +232 -0
  89. overclaude/payload/vendor/claude-swap/tests/test_switcher.py +6472 -0
  90. overclaude/payload/vendor/claude-swap/tests/test_transfer.py +1510 -0
  91. overclaude/payload/vendor/claude-swap/tests/test_tui.py +1282 -0
  92. overclaude/payload/vendor/claude-swap/tests/test_update_check.py +297 -0
  93. overclaude/payload/vendor/claude-swap/tests/test_usage_store.py +505 -0
  94. overclaude/payload/vendor/claude-swap/uv.lock +451 -0
  95. overclaude-1.0.0.dist-info/METADATA +160 -0
  96. overclaude-1.0.0.dist-info/RECORD +99 -0
  97. overclaude-1.0.0.dist-info/WHEEL +4 -0
  98. overclaude-1.0.0.dist-info/entry_points.txt +2 -0
  99. overclaude-1.0.0.dist-info/licenses/LICENSE +21 -0
overclaude/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """overclaude — Claude Code, overclocked."""
overclaude/cli.py ADDED
@@ -0,0 +1,53 @@
1
+ """overclaude CLI — runs the bundled kit installer/uninstaller.
2
+
3
+ The package wheel carries the same payload a git clone has (bin/, skills/,
4
+ hooks/, statusline/, claude/, settings/, shell/, vendor/claude-swap, and the
5
+ install/uninstall scripts). This CLI just locates that payload and runs the
6
+ battle-tested bash scripts 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("overclaude").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("overclaude: 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="overclaude",
32
+ description="Claude Code, overclocked — /swap, /handoff, statusline, ULTRACODE model routing.",
33
+ )
34
+ sub = ap.add_subparsers(dest="cmd", required=True)
35
+ sub.add_parser("install", help="install/refresh the kit into ~/.claude (idempotent, backs everything up)")
36
+ sub.add_parser("uninstall", help="remove exactly what install added (state in ~/.claude-swap-backup survives)")
37
+ sub.add_parser("path", help="print the bundled payload directory")
38
+ sub.add_parser("version", help="print the overclaude 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("overclaude"))
50
+
51
+
52
+ if __name__ == "__main__":
53
+ main()
@@ -0,0 +1,352 @@
1
+ #!/usr/bin/env bash
2
+ # swap-guard — shared state/guard engine for the /swap + /handoff Claude Code integration.
3
+ # Contract: section A of the binding interface contract.
4
+ # Subcommands:
5
+ # whoami own session registry JSON (compact); exit 1 if not in a session
6
+ # sessions JSON array of OTHER live claude sessions (always exit 0)
7
+ # preflight <target> switch-safety verdict JSON for slot number / alias / email (always exit 0)
8
+ # path flag|handoff [--cwd <dir>] print per-cwd state-file path
9
+ # flag <json> validate + fill + atomic-write relaunch flag; prints path (exit 1 on bad input)
10
+ # status own-session status JSON (always exit 0)
11
+ # cancel [--cwd <dir>] remove pending handoff + flag for cwd (always exit 0)
12
+ # schedule-kill <pid> v1 stub (exit 1)
13
+ set -u
14
+
15
+ STATE_ROOT="$HOME/.claude-swap-backup"
16
+ CTX_DIR="$STATE_ROOT/ctx"
17
+ ARCHIVE_DIR="$STATE_ROOT/handoff-archive"
18
+ SESSIONS_DIR="$HOME/.claude/sessions"
19
+
20
+ usage() {
21
+ cat >&2 <<'EOF'
22
+ usage: swap-guard <subcommand> [args]
23
+ whoami own session registry JSON (compact)
24
+ sessions JSON array of other live claude sessions
25
+ preflight <target> switch-safety verdict for slot / alias / email
26
+ path flag|handoff [--cwd <dir>] print per-cwd state-file path
27
+ flag <json> write relaunch flag ({"mode":"handoff"|"restart",...}); prints path
28
+ status own-session status JSON
29
+ cancel [--cwd <dir>] remove pending handoff + flag for cwd
30
+ schedule-kill <pid> phase-2 stub
31
+ EOF
32
+ exit 2
33
+ }
34
+
35
+ # HASH(cwd) per shared constants: first 12 hex of sha256
36
+ hash_cwd() { printf '%s' "$1" | /usr/bin/shasum -a 256 | awk '{print $1}' | cut -c1-12; }
37
+
38
+ flag_path_for() { printf '%s/relaunch-%s.json\n' "$STATE_ROOT" "$(hash_cwd "$1")"; }
39
+ pending_path_for() { printf '%s/handoff-pending-%s.md\n' "$STATE_ROOT" "$(hash_cwd "$1")"; }
40
+
41
+ # whoami_json [start_pid] — walk the PPID chain from start_pid (default $$) up to pid 1;
42
+ # print the first matching registry file's JSON (compact) and return 0, else return 1.
43
+ whoami_json() {
44
+ local p="${1:-$$}"
45
+ while :; do
46
+ case "$p" in ''|*[!0-9]*) return 1;; esac
47
+ [ "$p" -gt 1 ] || return 1
48
+ if [ -f "$SESSIONS_DIR/$p.json" ]; then
49
+ jq -c . "$SESSIONS_DIR/$p.json" 2>/dev/null || cat "$SESSIONS_DIR/$p.json"
50
+ return 0
51
+ fi
52
+ p="$(ps -o ppid= -p "$p" 2>/dev/null | tr -d ' ')"
53
+ done
54
+ }
55
+
56
+ # session_live <pid> — true iff the process exists (kill -0) AND its comm contains "claude"
57
+ session_live() {
58
+ local pid="${1:-}" comm
59
+ case "$pid" in ''|*[!0-9]*) return 1;; esac
60
+ kill -0 "$pid" 2>/dev/null || return 1
61
+ comm="$(ps -o comm= -p "$pid" 2>/dev/null)"
62
+ case "$comm" in *claude*) return 0;; *) return 1;; esac
63
+ }
64
+
65
+ # transcript_status <cwd> <sessionId> — activity fallback for sessions whose registry
66
+ # file has no status field (clients < ~2.1.211 never publish one). An active session
67
+ # appends to ~/.claude/projects/<flattened-cwd>/<sessionId>.jsonl on every turn, so:
68
+ # mtime within TRANSCRIPT_ACTIVE_SECS -> "busy"; older -> "idle"; no transcript -> "unknown".
69
+ TRANSCRIPT_ACTIVE_SECS=120
70
+ transcript_status() {
71
+ local cwd="${1:-}" sid="${2:-}" t mtime now
72
+ [ -n "$cwd" ] && [ -n "$sid" ] || { printf 'unknown\n'; return 0; }
73
+ t="$HOME/.claude/projects/$(printf '%s' "$cwd" | sed 's/[^A-Za-z0-9]/-/g')/$sid.jsonl"
74
+ [ -f "$t" ] || { printf 'unknown\n'; return 0; }
75
+ mtime="$(stat -f %m "$t" 2>/dev/null)"
76
+ case "$mtime" in ''|*[!0-9]*) printf 'unknown\n'; return 0;; esac
77
+ now="$(date +%s)"
78
+ if [ $(( now - mtime )) -le "$TRANSCRIPT_ACTIVE_SECS" ]; then
79
+ printf 'busy\n'
80
+ else
81
+ printf 'idle\n'
82
+ fi
83
+ }
84
+
85
+ # sessions_json — JSON array of OTHER live sessions. status precedence: the value the
86
+ # session itself reports (statusSource "reported") > transcript-mtime fallback
87
+ # (statusSource "transcript-mtime") > "unknown" (statusSource "none").
88
+ sessions_json() {
89
+ local own_pid="" out="[]" new_out f pid entry st
90
+ own_pid="$(whoami_json 2>/dev/null | jq -r '.pid // empty' 2>/dev/null)" || own_pid=""
91
+ for f in "$SESSIONS_DIR"/*.json; do
92
+ [ -f "$f" ] || continue
93
+ pid="$(jq -r '.pid // empty' "$f" 2>/dev/null)" || continue
94
+ [ -n "$pid" ] || continue
95
+ [ "$pid" = "$own_pid" ] && continue
96
+ session_live "$pid" || continue
97
+ entry="$(jq -c '{pid, sessionId, cwd, kind, entrypoint, status: (.status // "unknown"),
98
+ statusSource: (if .status then "reported" else "none" end)}' "$f" 2>/dev/null)" || continue
99
+ [ -n "$entry" ] || continue
100
+ if [ "$(printf '%s' "$entry" | jq -r '.statusSource' 2>/dev/null)" = "none" ]; then
101
+ st="$(transcript_status "$(printf '%s' "$entry" | jq -r '.cwd // empty')" \
102
+ "$(printf '%s' "$entry" | jq -r '.sessionId // empty')")"
103
+ case "$st" in
104
+ busy|idle) entry="$(printf '%s' "$entry" | jq -c --arg s "$st" '.status = $s | .statusSource = "transcript-mtime"' 2>/dev/null)" || continue;;
105
+ esac
106
+ fi
107
+ new_out="$(printf '%s' "$out" | jq -c --argjson e "$entry" '. + [$e]' 2>/dev/null)" && [ -n "$new_out" ] && out="$new_out"
108
+ done
109
+ printf '%s\n' "$out"
110
+ }
111
+
112
+ # resolve_cwd [explicit] — precedence: explicit --cwd > whoami .cwd > $PWD
113
+ resolve_cwd() {
114
+ local c="${1:-}"
115
+ if [ -z "$c" ]; then
116
+ c="$(whoami_json 2>/dev/null | jq -r '.cwd // empty' 2>/dev/null)" || c=""
117
+ fi
118
+ [ -n "$c" ] || c="$PWD"
119
+ printf '%s\n' "$c"
120
+ }
121
+
122
+ # parse_cwd_opt "$@" — echo value of --cwd/--cwd= if present; return 1 if absent
123
+ parse_cwd_opt() {
124
+ while [ $# -gt 0 ]; do
125
+ case "$1" in
126
+ --cwd) shift; printf '%s\n' "${1:-}"; return 0;;
127
+ --cwd=*) printf '%s\n' "${1#--cwd=}"; return 0;;
128
+ esac
129
+ shift
130
+ done
131
+ return 1
132
+ }
133
+
134
+ cmd_whoami() {
135
+ local out
136
+ if out="$(whoami_json)"; then
137
+ printf '%s\n' "$out"
138
+ exit 0
139
+ fi
140
+ printf '%s\n' '{"error":"not-in-session"}'
141
+ exit 1
142
+ }
143
+
144
+ cmd_sessions() {
145
+ sessions_json
146
+ exit 0
147
+ }
148
+
149
+ cmd_preflight() {
150
+ local raw="${1:-}" busy busy_n list tgt slot="" verdict detail target_json number email us
151
+ busy="$(sessions_json | jq -c '[.[] | select(.status == "busy" or .status == "unknown")]' 2>/dev/null)" || busy="[]"
152
+ [ -n "$busy" ] || busy="[]"
153
+ busy_n="$(printf '%s' "$busy" | jq 'length' 2>/dev/null)" || busy_n=0
154
+
155
+ if [ -z "$raw" ]; then
156
+ jq -cn --argjson busy "$busy" \
157
+ '{verdict:"unknown-target",target:{number:null,email:null},busy:$busy,detail:"no target given"}'
158
+ exit 0
159
+ fi
160
+
161
+ list="$(cswap list --json 2>/dev/null)"
162
+ if [ -z "$list" ] || ! printf '%s' "$list" | jq -e . >/dev/null 2>&1; then
163
+ jq -cn --argjson busy "$busy" --arg t "$raw" \
164
+ '{verdict:"unknown-target",target:{number:null,email:null},busy:$busy,detail:("cswap list --json failed - cannot verify target " + ($t|tojson))}'
165
+ exit 0
166
+ fi
167
+
168
+ # Resolve target: digits -> slot number; else alias in sequence.json; else email
169
+ case "$raw" in
170
+ *[!0-9]*)
171
+ slot="$(jq -r --arg a "$raw" '.accounts | to_entries[] | select((.value.alias // "") == $a) | .key' \
172
+ "$STATE_ROOT/sequence.json" 2>/dev/null | head -n1)" || slot=""
173
+ ;;
174
+ *) slot="$raw";;
175
+ esac
176
+ if [ -n "$slot" ]; then
177
+ tgt="$(printf '%s' "$list" | jq -c --arg n "$slot" '[.accounts[]? | select((.number|tostring) == $n)] | first // empty' 2>/dev/null)"
178
+ else
179
+ tgt="$(printf '%s' "$list" | jq -c --arg e "$raw" '[.accounts[]? | select(((.email // "") | ascii_downcase) == ($e | ascii_downcase))] | first // empty' 2>/dev/null)"
180
+ fi
181
+
182
+ if [ -z "$tgt" ]; then
183
+ jq -cn --argjson busy "$busy" --arg t "$raw" \
184
+ '{verdict:"unknown-target",target:{number:null,email:null},busy:$busy,detail:("no account matches " + ($t|tojson) + " (not a slot number, alias, or email)")}'
185
+ exit 0
186
+ fi
187
+
188
+ target_json="$(printf '%s' "$tgt" | jq -c '{number, email}')"
189
+ number="$(printf '%s' "$tgt" | jq -r '.number')"
190
+ email="$(printf '%s' "$tgt" | jq -r '.email // ""')"
191
+ us="$(printf '%s' "$tgt" | jq -r '.usageStatus // ""' | tr '[:upper:]' '[:lower:]')"
192
+
193
+ case "$us" in
194
+ *login*|*quarantin*|*expired*|*invalid*|*revoked*)
195
+ verdict="relogin-required"
196
+ detail="account $number ($email) requires re-login (usageStatus: $us) - recover with /login then: cswap add --slot $number"
197
+ ;;
198
+ *)
199
+ if [ "$busy_n" -gt 0 ] 2>/dev/null; then
200
+ verdict="busy"
201
+ detail="account $number ($email) is healthy, but $busy_n other session(s) are busy or possibly-busy - a switch flips ALL sessions within ~30s"
202
+ else
203
+ verdict="ok"
204
+ detail="account $number ($email) is healthy and no other session is busy"
205
+ fi
206
+ ;;
207
+ esac
208
+
209
+ jq -cn --arg v "$verdict" --argjson t "$target_json" --argjson busy "$busy" --arg d "$detail" \
210
+ '{verdict:$v,target:$t,busy:$busy,detail:$d}'
211
+ exit 0
212
+ }
213
+
214
+ cmd_path() {
215
+ local kind="${1:-}"
216
+ [ $# -gt 0 ] && shift
217
+ local cwd_opt="" cwd
218
+ cwd_opt="$(parse_cwd_opt "$@")" || cwd_opt=""
219
+ cwd="$(resolve_cwd "$cwd_opt")"
220
+ case "$kind" in
221
+ flag) flag_path_for "$cwd";;
222
+ handoff) pending_path_for "$cwd";;
223
+ *) usage;;
224
+ esac
225
+ exit 0
226
+ }
227
+
228
+ cmd_flag() {
229
+ local input="${1:-}"
230
+ if [ -z "$input" ] || ! printf '%s' "$input" | jq -e . >/dev/null 2>&1; then
231
+ echo "swap-guard flag: input is not valid JSON" >&2
232
+ exit 1
233
+ fi
234
+ local mode
235
+ mode="$(printf '%s' "$input" | jq -r '.mode // empty')"
236
+ case "$mode" in
237
+ handoff|restart) ;;
238
+ *) echo 'swap-guard flag: .mode must be "handoff" or "restart"' >&2; exit 1;;
239
+ esac
240
+ local sid cwd created out p tmp
241
+ sid="$(printf '%s' "$input" | jq -r '.sessionId // ""')"
242
+ cwd="$(printf '%s' "$input" | jq -r '.cwd // empty')"
243
+ cwd="$(resolve_cwd "$cwd")"
244
+ created="$(printf '%s' "$input" | jq -r '.created // empty' 2>/dev/null)"
245
+ case "$created" in ''|*[!0-9]*) created="$(date +%s)";; esac
246
+ out="$(jq -cn --arg m "$mode" --arg s "$sid" --arg c "$cwd" --argjson t "$created" \
247
+ '{mode:$m,sessionId:$s,cwd:$c,created:$t}')"
248
+ p="$(flag_path_for "$cwd")"
249
+ mkdir -p "$STATE_ROOT" 2>/dev/null || { echo "swap-guard flag: cannot create $STATE_ROOT" >&2; exit 1; }
250
+ tmp="$p.tmp.$$"
251
+ if printf '%s\n' "$out" > "$tmp" 2>/dev/null && mv "$tmp" "$p" 2>/dev/null; then
252
+ printf '%s\n' "$p"
253
+ exit 0
254
+ fi
255
+ rm -f "$tmp" 2>/dev/null
256
+ echo "swap-guard flag: write failed" >&2
257
+ exit 1
258
+ }
259
+
260
+ # _status_json <sessionId-or-empty> <cwd> — build the status JSON (factored for testability)
261
+ _status_json() {
262
+ local sid="${1:-}" cwd="${2:-$PWD}"
263
+ local pct="" fired=0 bannered=0
264
+ if [ -n "$sid" ] && [ -f "$CTX_DIR/$sid.json" ]; then
265
+ pct="$(jq -r 'if (.pct|type) == "number" then (.pct|tostring) else "" end' "$CTX_DIR/$sid.json" 2>/dev/null)" || pct=""
266
+ fi
267
+ if [ -n "$sid" ] && [ -f "$CTX_DIR/$sid.state" ]; then
268
+ fired="$(jq -r '.fired // 0' "$CTX_DIR/$sid.state" 2>/dev/null)" || fired=0
269
+ bannered="$(jq -r '.bannered // 0' "$CTX_DIR/$sid.state" 2>/dev/null)" || bannered=0
270
+ fi
271
+ case "$fired" in ''|*[!0-9]*) fired=0;; esac
272
+ case "$bannered" in ''|*[!0-9]*) bannered=0;; esac
273
+
274
+ local h pending flagf pending_out="" page="" flag_out="" arch="" created
275
+ h="$(hash_cwd "$cwd")"
276
+ pending="$STATE_ROOT/handoff-pending-$h.md"
277
+ flagf="$STATE_ROOT/relaunch-$h.json"
278
+ if [ -f "$pending" ]; then
279
+ pending_out="$pending"
280
+ created="$(sed -n '1s/.*created="\([0-9][0-9]*\)".*/\1/p' "$pending" 2>/dev/null)"
281
+ [ -n "$created" ] || created="$(stat -f %m "$pending" 2>/dev/null)"
282
+ case "$created" in
283
+ ''|*[!0-9]*) page="";;
284
+ *) page="$(( $(date +%s) - created ))";;
285
+ esac
286
+ fi
287
+ [ -f "$flagf" ] && flag_out="$flagf"
288
+ if [ -d "$ARCHIVE_DIR" ]; then
289
+ arch="$(ls "$ARCHIVE_DIR" 2>/dev/null | grep -E -- "-$h(-expired)?\.md$" | sort | tail -n1)"
290
+ [ -n "$arch" ] && arch="$ARCHIVE_DIR/$arch"
291
+ fi
292
+
293
+ jq -cn --arg sid "$sid" --arg pct "$pct" --arg fired "$fired" --arg bannered "$bannered" \
294
+ --arg pending "$pending_out" --arg page "$page" --arg flagp "$flag_out" --arg arch "$arch" '
295
+ {sessionId: (if $sid == "" then null else $sid end),
296
+ pct: (if $pct == "" then null else ($pct|tonumber) end),
297
+ fired: ($fired|tonumber),
298
+ bannered: ($bannered|tonumber),
299
+ pendingHandoff: (if $pending == "" then null else $pending end),
300
+ pendingAgeSec: (if $page == "" then null else ($page|tonumber) end),
301
+ flag: (if $flagp == "" then null else $flagp end),
302
+ latestArchive: (if $arch == "" then null else $arch end)}'
303
+ }
304
+
305
+ cmd_status() {
306
+ local who sid="" cwd=""
307
+ if who="$(whoami_json 2>/dev/null)"; then
308
+ sid="$(printf '%s' "$who" | jq -r '.sessionId // empty' 2>/dev/null)" || sid=""
309
+ cwd="$(printf '%s' "$who" | jq -r '.cwd // empty' 2>/dev/null)" || cwd=""
310
+ fi
311
+ [ -n "$cwd" ] || cwd="$PWD"
312
+ _status_json "$sid" "$cwd"
313
+ exit 0
314
+ }
315
+
316
+ cmd_cancel() {
317
+ local cwd_opt="" cwd removed=0 f
318
+ cwd_opt="$(parse_cwd_opt "$@")" || cwd_opt=""
319
+ cwd="$(resolve_cwd "$cwd_opt")"
320
+ for f in "$(pending_path_for "$cwd")" "$(flag_path_for "$cwd")"; do
321
+ if [ -f "$f" ]; then
322
+ rm -f "$f" 2>/dev/null && { printf 'removed: %s\n' "$f"; removed=1; }
323
+ fi
324
+ done
325
+ [ "$removed" -eq 1 ] || printf 'nothing to remove for cwd %s\n' "$cwd"
326
+ exit 0
327
+ }
328
+
329
+ cmd_schedule_kill() {
330
+ echo "phase-2 feature not enabled" >&2
331
+ exit 1
332
+ }
333
+
334
+ main() {
335
+ local sub="${1:-}"
336
+ [ $# -gt 0 ] && shift
337
+ case "$sub" in
338
+ whoami) cmd_whoami "$@";;
339
+ sessions) cmd_sessions "$@";;
340
+ preflight) cmd_preflight "$@";;
341
+ path) cmd_path "$@";;
342
+ flag) cmd_flag "$@";;
343
+ status) cmd_status "$@";;
344
+ cancel) cmd_cancel "$@";;
345
+ schedule-kill) cmd_schedule_kill "$@";;
346
+ *) usage;;
347
+ esac
348
+ }
349
+
350
+ if [ "${BASH_SOURCE[0]:-}" = "$0" ]; then
351
+ main "$@"
352
+ fi
@@ -0,0 +1,79 @@
1
+ # ULTRACODE.md — Model & Effort Routing
2
+
3
+ ## 1. Core principle
4
+ Spend fable only where judgment is the bottleneck, never where volume is: fable draws from a separate, much smaller rate-limit bucket, and omitting `opts.model` silently inherits it — every un-annotated `agent()` call is a fable spend.
5
+ Explicit down-routing is your default posture, not an optimization.
6
+ Route each agent by one question: "if this agent is quietly wrong, who catches it?" — a downstream agent means you may downgrade; nobody means top tier.
7
+
8
+ ## 2. Routing table
9
+ | Task class | Model | Effort | Why |
10
+ |---|---|---|---|
11
+ | Scouting / file-listing | haiku | low | mechanical |
12
+ | Bulk code reading / fact extraction | haiku (sonnet if dense) | low | extraction |
13
+ | Mechanical transforms (rename, dedup, format) | haiku | low | deterministic |
14
+ | Classify / extract into fixed `schema` | sonnet | low | ambiguity |
15
+ | Finder sweeps (bug / perf) | sonnet | medium | recall |
16
+ | Security sweeps | opus | high | asymmetry |
17
+ | Implementation edits (well-scoped) | sonnet | medium | authorship |
18
+ | Implementation edits (cross-cutting / tricky) | opus | high | judgment |
19
+ | Adversarial verification / refutation votes | opus | high | precision |
20
+ | Judge panel voters | opus | high | arbitration |
21
+ | Judge chair / tie-break adjudicator | fable | xhigh | terminal |
22
+ | Completeness critics | opus | high | gaps |
23
+ | Final synthesis | fable (omit `model`) | xhigh | unrecoverable |
24
+
25
+ Pairing rules:
26
+ - Effort amplifies capability; it never substitutes. haiku@xhigh loses to sonnet@medium on judgment work; never pair xhigh with a fan-out stage.
27
+ - Multiply tier by fan-out width BEFORE picking a model — fine at width 3 is a budget event at width 20. Any `parallel()` wider than ~5 gets an explicit haiku/sonnet.
28
+ - Buy recall with width (more cheap finders over disjoint files); buy precision with tier (one strong verifier). Upstream misses get caught downstream; a false CONFIRM does not.
29
+ - Width unknown until runtime? Route for the upper end of plausible width.
30
+
31
+ ## 3. Hard guardrails
32
+
33
+ Never downgrade (floors, not targets):
34
+ - Final synthesis, and ANY output the user sees without a downstream agent re-check: fable, never below opus. The trust boundary sets the floor, not the stage label.
35
+ - Adversarial verification, judge panels, security verdicts, completeness critics: opus floor. A false CONFIRM ends scrutiny — nothing checks the checker.
36
+ - Subtle-correctness verdicts (concurrency, auth/crypto, money math, data-loss migrations): fable. "Looks correct" and "is correct" diverge most here.
37
+
38
+ Escalation rule (what makes cheap-first safe):
39
+ - Give every finder/verifier a `schema` requiring `verdict` / `confidence` / `evidence`. Empty evidence IS low confidence, whatever number the agent reports.
40
+ - Re-run once at a higher tier on: confidence < 0.7, UNSURE, empty/malformed output, or contradiction between parallel agents.
41
+ - A CONFIRM/REFUTE split among voters escalates to one fable adjudicator — never majority-vote or average it; the split itself signals the question is hard.
42
+ - Still UNSURE at the top tier? Surface it to the user. An honest "unresolved" is correct output; a manufactured verdict is a defect.
43
+
44
+ Budget pressure (`budget.remaining()` thin, or fable bucket ~75%+ consumed):
45
+ - Cut fan-out width and batch more files per agent FIRST; floors are the last thing to fall.
46
+ - Treat fable as a read-only reserve: one highest-leverage call only (usually synthesis).
47
+ - If the budget cannot cover the fable/opus terminal stages, say so and propose the cut — never silently ship a downgraded final answer.
48
+
49
+ ## 4. Example
50
+ ```js
51
+ pipeline(files,
52
+ f => agent(`List exports and structure of ${f}`,
53
+ { model: 'haiku', effort: 'low', label: 'scout (haiku/low)' }),
54
+ f => agent(`Find bugs and perf issues in ${f}`,
55
+ { model: 'sonnet', effort: 'medium', schema: findingSchema,
56
+ label: 'find (sonnet/med)' }), // wide + cheap: recall
57
+ fs => agent(`Adversarially verify each finding; cite evidence; drop false positives`,
58
+ { model: 'opus', effort: 'high', schema: verdictSchema,
59
+ label: 'verify (opus/high)' }) // narrow + strong: precision
60
+ );
61
+ // contested verdicts only — one strong judge beats five weak votes
62
+ const ruling = await agent(`Adjudicate the CONFIRM/REFUTE splits`,
63
+ { model: 'fable', effort: 'xhigh', label: 'adjudicate (fable/xhigh)' });
64
+ // model omitted DELIBERATELY: terminal stage inherits fable
65
+ const report = await agent(`Synthesize the final review, ranked by severity`,
66
+ { effort: 'xhigh', label: 'synthesize (fable/xhigh)' });
67
+ ```
68
+ A workflow that never touches fable is a valid — often good — outcome. Omission of `opts.model` should be rare, counted, and load-bearing.
69
+
70
+ ## 5. Routing lint (pre-flight — fix before dispatch, don't launch and patch)
71
+ 1. Every `agent()` call has an explicit `model`, or its omission is a deliberate apex spend. More than 2 omissions in one script = under-routing, not an apex-heavy task.
72
+ 2. No wide/parallel stage runs on opus or inherits fable; bulk stages sit at haiku/sonnet regardless of how the rest is routed.
73
+ 3. Verifiers, judges, and synthesis are at their floors — even under budget pressure.
74
+ 4. Every downgraded trusted-adjacent stage has a wired escalation trigger (confidence / UNSURE / contradiction → higher-tier re-run).
75
+ 5. `label` and `phase()` names embed the model tag (e.g. `verify (opus/high)`) — the user never opens the script to learn a stage ran on fable.
76
+
77
+ ## 6. Scope
78
+ The same table binds the Agent tool's `model` param for one-off dispatches outside Workflow scripts: a lone bulk-reading agent is still a haiku job; a lone terminal judgment still earns opus or an intentional fable inheritance.
79
+ The absence of a `pipeline()`/`parallel()` wrapper does not relax the discipline.
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env bash
2
+ # ctx-notify.sh — Claude Code Stop hook.
3
+ # Passive banner: when context usage sits in a threshold band that has not
4
+ # been bannered yet, emit {"systemMessage": ...}. NEVER emits a "decision"
5
+ # field. Contract: always exit 0, silent on every error path, defensive
6
+ # jq parsing, no-op when stop_hook_active.
7
+ exec 2>/dev/null
8
+
9
+ # Thresholds.
10
+ T1=60
11
+ T2=75
12
+ T3=85
13
+
14
+ STATE_ROOT="$HOME/.claude-swap-backup"
15
+ CTX_DIR="$STATE_ROOT/ctx"
16
+
17
+ INPUT="$(cat)" || INPUT=""
18
+ command -v jq >/dev/null 2>&1 || exit 0
19
+
20
+ # Never act on our own stop cycle.
21
+ sha="$(jq -r '.stop_hook_active // false' <<<"$INPUT" 2>/dev/null)"
22
+ [ "$sha" = "true" ] && exit 0
23
+
24
+ session_id="$(jq -r '.session_id // empty' <<<"$INPUT" 2>/dev/null)"
25
+ [ -n "$session_id" ] || exit 0
26
+
27
+ # Relay pct (missing -> silent exit).
28
+ relay="$CTX_DIR/$session_id.json"
29
+ [ -f "$relay" ] || exit 0
30
+ pct_raw="$(jq -r 'if (.pct|type) == "number" then .pct else empty end' "$relay" 2>/dev/null)"
31
+ [ -n "$pct_raw" ] || exit 0
32
+ pct="$(awk -v p="$pct_raw" 'BEGIN { printf "%d", p }')"
33
+ case "$pct" in ''|*[!0-9]*) exit 0 ;; esac
34
+
35
+ # State (missing/corrupt -> fired=0 bannered=0).
36
+ state="$CTX_DIR/$session_id.state"
37
+ fired=0
38
+ bannered=0
39
+ if [ -f "$state" ]; then
40
+ f="$(jq -r '.fired // 0' "$state" 2>/dev/null)"
41
+ b="$(jq -r '.bannered // 0' "$state" 2>/dev/null)"
42
+ case "$f" in 0|60|75|85) fired=$f ;; esac
43
+ case "$b" in 0|60|75|85) bannered=$b ;; esac
44
+ fi
45
+
46
+ # band = highest threshold <= pct (0 if < 60).
47
+ band=0
48
+ for t in "$T1" "$T2" "$T3"; do
49
+ [ "$pct" -ge "$t" ] && band=$t
50
+ done
51
+
52
+ if [ "$pct" -ge "$T1" ] && [ "$band" -gt "$bannered" ]; then
53
+ printf '{"systemMessage":"context %d%% — /handoff available"}\n' "$pct"
54
+ mkdir -p "$CTX_DIR" || exit 0
55
+ tmp="$state.tmp.$$"
56
+ printf '{"fired":%d,"bannered":%d}\n' "$fired" "$band" > "$tmp" && mv -f "$tmp" "$state"
57
+ fi
58
+ exit 0
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env bash
2
+ # ctx-watch.sh — Claude Code UserPromptSubmit hook.
3
+ # Reads the statusline relay for this session and, when context usage
4
+ # crosses a new threshold, injects the handoff-offer note into the turn.
5
+ # Contract: always exit 0, silent on every error path, defensive jq
6
+ # parsing, no state writes on the /handoff | /swap skip path.
7
+ exec 2>/dev/null
8
+
9
+ # Thresholds (contract: variables at top).
10
+ T1=60
11
+ T2=75
12
+ T3=85
13
+
14
+ STATE_ROOT="$HOME/.claude-swap-backup"
15
+ CTX_DIR="$STATE_ROOT/ctx"
16
+
17
+ INPUT="$(cat)" || INPUT=""
18
+ command -v jq >/dev/null 2>&1 || exit 0
19
+
20
+ # Skip path FIRST — before any state read/write.
21
+ prompt="$(jq -r '.prompt // empty' <<<"$INPUT" 2>/dev/null)"
22
+ case "$prompt" in
23
+ "/handoff"*|"/swap"*) exit 0 ;;
24
+ esac
25
+
26
+ session_id="$(jq -r '.session_id // empty' <<<"$INPUT" 2>/dev/null)"
27
+ [ -n "$session_id" ] || exit 0
28
+
29
+ # Relay pct (missing file / null / non-number -> silent exit).
30
+ relay="$CTX_DIR/$session_id.json"
31
+ [ -f "$relay" ] || exit 0
32
+ pct_raw="$(jq -r 'if (.pct|type) == "number" then .pct else empty end' "$relay" 2>/dev/null)"
33
+ [ -n "$pct_raw" ] || exit 0
34
+ pct="$(awk -v p="$pct_raw" 'BEGIN { printf "%d", p }')"
35
+ case "$pct" in ''|*[!0-9]*) exit 0 ;; esac
36
+
37
+ # State (missing/corrupt -> fired=0 bannered=0).
38
+ state="$CTX_DIR/$session_id.state"
39
+ fired=0
40
+ bannered=0
41
+ if [ -f "$state" ]; then
42
+ f="$(jq -r '.fired // 0' "$state" 2>/dev/null)"
43
+ b="$(jq -r '.bannered // 0' "$state" 2>/dev/null)"
44
+ case "$f" in 0|60|75|85) fired=$f ;; esac
45
+ case "$b" in 0|60|75|85) bannered=$b ;; esac
46
+ fi
47
+
48
+ changed=0
49
+
50
+ # Re-arm rule: if pct < fired-10 -> fired = highest threshold <= pct
51
+ # (else 0), and bannered = min(bannered, fired).
52
+ if [ "$pct" -lt $(( fired - 10 )) ]; then
53
+ new_fired=0
54
+ for t in "$T1" "$T2" "$T3"; do
55
+ [ "$pct" -ge "$t" ] && new_fired=$t
56
+ done
57
+ fired=$new_fired
58
+ [ "$bannered" -gt "$fired" ] && bannered=$fired
59
+ changed=1
60
+ fi
61
+
62
+ # Fire: highest threshold T with pct >= T and T > fired.
63
+ T=0
64
+ for t in "$T1" "$T2" "$T3"; do
65
+ [ "$pct" -ge "$t" ] && T=$t
66
+ done
67
+ if [ "$T" -gt "$fired" ]; then
68
+ printf '%s\n' "[context-watch] Context is at ${pct}%. After fully completing the user's current request, tell them context is filling up and OFFER /handoff to continue in a fresh session. Do NOT invoke the handoff skill 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."
69
+ fired=$T
70
+ changed=1
71
+ fi
72
+
73
+ # Persist only when something changed (atomic write).
74
+ if [ "$changed" -eq 1 ]; then
75
+ mkdir -p "$CTX_DIR" || exit 0
76
+ tmp="$state.tmp.$$"
77
+ printf '{"fired":%d,"bannered":%d}\n' "$fired" "$bannered" > "$tmp" && mv -f "$tmp" "$state"
78
+ fi
79
+ exit 0