tokenmaxxing 1.6.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/DESIGN.md +5 -31
  2. package/LICENSE +21 -0
  3. package/README.md +1 -2
  4. package/agent-plugin/agents/tokenmaxxing-claude.md +43 -0
  5. package/agent-plugin/agents/tokenmaxxing-codex.md +40 -0
  6. package/agent-plugin/bin/tokenmaxxing-mcp +7 -0
  7. package/agent-plugin/hooks/cursor-relay.json +14 -0
  8. package/agent-plugin/mcp.json +10 -0
  9. package/agent-plugin/plugin.json +20 -0
  10. package/agent-plugin/skills/codex-pool/SKILL.md +23 -0
  11. package/agent-plugin/skills/codex-pool/references/codex.md +5 -0
  12. package/agent-plugin/skills/credentials-hygiene/SKILL.md +26 -0
  13. package/agent-plugin/skills/credentials-hygiene/references/credentials.md +6 -0
  14. package/agent-plugin/skills/doctor-diagnostics/SKILL.md +26 -0
  15. package/agent-plugin/skills/doctor-diagnostics/references/troubleshooting.md +5 -0
  16. package/agent-plugin/skills/pool-status/SKILL.md +27 -0
  17. package/agent-plugin/skills/pool-status/references/commands.md +8 -0
  18. package/agent-plugin/skills/relay-session/SKILL.md +118 -0
  19. package/agent-plugin/skills/relay-session/references/ipc.md +23 -0
  20. package/agent-plugin/skills/safe-contribution/SKILL.md +27 -0
  21. package/agent-plugin/skills/safe-contribution/references/ship.md +5 -0
  22. package/agent-plugin/skills/sdk-pairing/SKILL.md +33 -0
  23. package/agent-plugin/skills/sdk-pairing/references/sdk.md +6 -0
  24. package/agent-plugin/skills/switching-policy/SKILL.md +29 -0
  25. package/agent-plugin/skills/switching-policy/references/policy.md +7 -0
  26. package/package.json +3 -5
  27. package/src/cli/codexinit.ts +11 -2
  28. package/src/cli/init.ts +9 -3
  29. package/src/cli/relay.ts +323 -0
  30. package/src/entries/codexstophook.ts +10 -0
  31. package/src/entries/mcp.ts +288 -0
  32. package/src/entries/relaypermission.ts +105 -0
  33. package/src/entries/stophook.ts +11 -0
  34. package/src/lib/decide.ts +2 -4
  35. package/src/lib/install.ts +61 -7
  36. package/src/lib/lock.ts +3 -7
  37. package/src/lib/log.ts +8 -11
  38. package/src/lib/paths.ts +3 -9
  39. package/src/lib/relay/config.ts +84 -0
  40. package/src/lib/relay/decide.ts +75 -0
  41. package/src/lib/relay/gc.ts +80 -0
  42. package/src/lib/relay/install.ts +143 -0
  43. package/src/lib/relay/markers.ts +148 -0
  44. package/src/lib/relay/modes.ts +82 -0
  45. package/src/lib/relay/protocol.ts +61 -0
  46. package/src/lib/relay/registry.ts +175 -0
  47. package/src/lib/relay/tmux.ts +109 -0
  48. package/src/lib/relay/turn.ts +137 -0
  49. package/src/lib/relay/worker.ts +141 -0
  50. package/src/lib/usage.ts +6 -5
  51. package/src/main.ts +6 -6
  52. package/src/cli/serve.ts +0 -1790
  53. package/src/lib/slackbridge.ts +0 -1363
  54. package/src/lib/slackstate.ts +0 -352
  55. package/src/lib/slackstream.ts +0 -300
  56. package/src/serve-plugin/.claude-plugin/plugin.json +0 -4
  57. package/src/serve-plugin/skills/ask-the-user/SKILL.md +0 -41
  58. package/src/serve-plugin/skills/serve-session/SKILL.md +0 -50
package/DESIGN.md CHANGED
@@ -39,7 +39,7 @@ It is a process manager only - spawn with inherited stdio plus saved `stty -g` t
39
39
 
40
40
  - A periodic check job (`com.tokenmaxxing.check` launchd agent on macOS, `tokenmaxxing-check.timer` systemd user timer on Linux) running `tokenmaxxing check` every 180s: hooks alone miss long agentic turns, so the timer is the backstop that keeps switching engaged mid-turn.
41
41
 
42
- The switching path runs no long-lived daemon - the statusline pushes usage and hooks + supervisor react, with the periodic check job above as its only recurring process (`xx serve`, the opt-in Slack bridge, is a separate long-lived daemon). The `claude` binary and `~/.claude` layout are untouched.
42
+ The switching path runs no long-lived daemon. The statusline pushes usage. Hooks and the supervisor react. The periodic check job above is the only recurring process. The `claude` binary and `~/.claude` layout are untouched.
43
43
 
44
44
  ---
45
45
 
@@ -81,40 +81,12 @@ Each terminal ran the supervisor, so each has its own child `claude` and its own
81
81
  ## 5. Rotation policy
82
82
  The decision engages at `five_hour >= 50%` (policy.greedySessionFloor): from there it greedily converges on the usable account furthest behind its weekly pace, staying put whenever the current account wins or ties. The **Layer 1 screening bars** - `five_hour >= 95%` OR `seven_day >= 98%`, per org (`thresholds`) - force a switch onto a fresher account and also screen candidates. "Exhausted" is a **timestamped state** (`resets_at`), not a flag - an account is a candidate again after it resets. Optional projected threshold (`bar - policy.projectionMargin`, a fixed configured margin) so a single large turn is less likely to blow past 100% before the next Stop hook.
83
83
 
84
- **Two layers - pump the last drops.** The screening bars deliberately leave headroom, so when *every* account is over them Layer 1 alone would park the pool with 2-5% of each account's quota still unspent. **Layer 2 - the wall bars** (`hardThresholds`, default `100/100`, the server's own limit) - is the fallback reached only at that point: the session **holds its seat and squeezes** while it is under the wall, else swaps onto the best still-under-wall account (the same pace-pressure ranking as every other swap - squeeze the account whose weekly quota is most about to be forfeited first), and only parks (depleted-wait) once every account has truly walled. Recovery is then measured against the wall, not the screening bar, so an account whose 5h window drops below 100 is squeezable again even while its weekly window still sits above the Layer 1 bar. The wall reading is the statusLine's own `rate_limits` feed - the same server-side figure claude's `/rate-limit-options` renders - so when an account genuinely maxes out the tee shows 100 and Layer 2 moves on; a single-turn overshoot is caught one boundary later (the periodic `check` timer, or the next Stop hook) without needing to sniff assistant text. The serve/SDK path additionally stamps an account walled the instant an *errored* turn result reports a limit (`recordObservedLimit`, gated on `is_error`), because it has no statusLine tee. Set `hardThresholds` equal to `thresholds` to disable Layer 2. **Layer 2 is Claude-only:** a swap on Claude is a hot, in-place credential adoption every concurrent session follows automatically, whereas a running Codex refuses another account's credential (restart is the switch), so a last-drop-swap there would strand any sibling still on the walled account - Codex instead keeps riding its current account to the wall (its existing all-exhausted stay-put already squeezes it).
84
+ **Two layers - pump the last drops.** The screening bars deliberately leave headroom, so when *every* account is over them Layer 1 alone would park the pool with 2-5% of each account's quota still unspent. **Layer 2 - the wall bars** (`hardThresholds`, default `100/100`, the server's own limit) - is the fallback reached only at that point: the session **holds its seat and squeezes** while it is under the wall, else swaps onto the best still-under-wall account (the same pace-pressure ranking as every other swap - squeeze the account whose weekly quota is most about to be forfeited first), and only parks (depleted-wait) once every account has truly walled. Recovery is then measured against the wall, not the screening bar, so an account whose 5h window drops below 100 is squeezable again even while its weekly window still sits above the Layer 1 bar. The wall reading is the statusLine's own `rate_limits` feed - the same server-side figure claude's `/rate-limit-options` renders - so when an account genuinely maxes out the tee shows 100 and Layer 2 moves on; a single-turn overshoot is caught one boundary later (the periodic `check` timer, or the next Stop hook) without needing to sniff assistant text. Set `hardThresholds` equal to `thresholds` to disable Layer 2. **Layer 2 is Claude-only:** a swap on Claude is a hot, in-place credential adoption every concurrent session follows automatically, whereas a running Codex refuses another account's credential (restart is the switch), so a last-drop-swap there would strand any sibling still on the walled account - Codex instead keeps riding its current account to the wall (its existing all-exhausted stay-put already squeezes it).
85
85
 
86
86
  **Model-aware trigger.** Claude subscriptions also enforce **per-model weekly caps** - currently only for Sonnet and Fable (there is no Opus-only quota), and Fable's tighter limit binds *before* the aggregate (e.g. 80% week-Fable at only 50% week-all-models). This cap isn't in statusLine stdin, so when the active model is in `policy.switchModels` we read it from `claude -p '/usage'` (free, 0 tokens, TTL-cached) and add `week(<activeModel>) >= threshold` to the trigger. A Fable session switches on the Fable cap; a Sonnet session rides the aggregate. Both layers apply the per-model gate: a burnt Fable cap screens an account out of a Layer 1 switch, and a Fable cap at the wall screens it out of the Layer 2 squeeze too.
87
87
 
88
88
  ---
89
89
 
90
- ## 5b. The Slack bridge (`xx serve`, 0.18.0)
91
-
92
- A local Socket Mode daemon (no public URL) that turns Slack threads into Claude Code sessions on the pooled accounts. Stack (user decision 2026-07-18): Vercel's Chat SDK (`chat` + `@chat-adapter/slack`) for the Slack side; the Claude Agent SDK driven through `src/sdk.ts`'s pooled surface for the claude side - `xx serve` is that surface's first in-repo consumer. EVE (Vercel's agent framework) was researched and explicitly dropped: it owns its own model loop via AI Gateway, so it would replace Claude Code rather than drive it.
93
-
94
- - **Config**: `slack.json` (0600 - it holds the xoxb-/xapp- tokens) with per-channel links `{channel, repo, permissionMode, model?}`. `serve setup` prints the app manifest (bot scopes: app_mentions:read, assistant:write, channels:history, groups:history, chat:write, files:write, im:history, users:read; agent_view enabled; events incl. app_mention, message.channels/groups/im, app_home_opened, app_context_changed; socket mode) and prompts for the tokens; `serve link <channel-id> <repo>` manages links (channel IDs only - names drift, ids don't).
95
- - **Thread = session**: a bot mention in a linked channel subscribes the thread and opens the session in the linked repo checkout (normal mode, user decision 2026-07-18 superseding the same-day worktree-per-thread default; a later same-day decision: the agent cuts its own worktree FIRST for any mutating task - read-only turns stay parallel in the shared checkout - taught by the serve-session skill, since the recorded cwd can never move), and records `{threadId, cwd, sessionId}` under `slack-threads/`. Resume is cwd-keyed in claude, so the cwd stays byte-stable for the thread's life; records from the worktree era pin their old `slack-worktrees/<threadKey>` cwd and keep working (those worktrees are never auto-deleted - they hold the thread's work).
96
- - **Thread close-out (finish_thread)**: when the user says the work is finished, the model calls the in-process MCP tool `finish_thread` (a per-turn `createSdkMcpServer` in relayThread, `alwaysLoad: true`, granted via `allowedTools` since nobody can answer a permission prompt over Slack). The handler runs in the daemon but only flags the turn outcome; after the turn ends (claude subprocess gone, segments posted) serve.ts runs `cleanupThread`: delete the `slack-threads/` record, `thread.unsubscribe()`, and post one confirmation line. Threads run in the shared repo checkout, so there is nothing on disk to collect and the checkout is never touched; a fresh mention after close-out starts a new session.
97
- - **Turn = spawn**: each thread message runs ONE `query()` with `resume: sessionId` (the SDK subprocess reads credentials at spawn, so per-turn spawns are what let `ensureBestAccount()` land each turn on the freshest account, and the daemon can restart without losing threads). `stopHookCheck` rides along as the SDK Stop hook. Streamed `text_delta`s feed `thread.post(AsyncIterable)` (the adapter debounces edits); tool-only turns post the final result text.
98
- - **Steering (2026-07-27, owner decision: steering is the default for a mid-turn reply)**: the per-turn query takes STREAMING input (`prompt` as an AsyncIterable of stream-json user messages) instead of a one-shot string, so a relayable reply landing while the turn runs is written onto the live child's stdin. The CLI folds it into the running turn at the next tool boundary (the same queued-message injection interactive Claude Code uses; a message that misses the last fold window runs as its own turn in the same child before exit, so nothing drops - both verified against claude 2.1.220). serve-side: `relayThread` exposes a per-attempt `steer()` via `onSteer`, refused once the attempt's result arrives (stdin ends then, and the SDK silently drops writes to an ended stdin); `buildServeRuntime` keeps one acceptor per live thread and tries it per arriving message - refusals (idle thread, parked pool, a non-empty inbox or waiting turn for ordering, an out-of-order late arrival, drain) fall back to the inbox path. Steered text appends to the durable `activeTurn.prompt` (kill-replays and limit-resumes carry it) and folds into retry prompts; steered message ids ride the marker (`steeredMessageIds`) wearing the same hourglass-to-terminal reaction lifecycle; an inbound takeover of a deferred turn adopts the held turn's unsettled ids so no hourglass is orphaned, and a steer answers a resumed turn's pending ask (attention cleared) like a queued turn would. SUCCESS IS STICKY per attempt: a post-fold-window steer's own drained turn can emit a second, errored result after the primary answer already succeeded, and marking the turn failed then would re-run completed work through the retry machinery - the steer is announced lost in-thread instead, `outcome.steerLost` settles the steered messages' reactions as failed (a lost instruction must never read green; attribution is per-turn, a documented tradeoff on TurnOutcomeSchema), a limit classification stays sticky across a child's errored results (a drained turn's generic death must not declassify the primary turn's recoverable limit), and each turn's result-only answer is flushed AT its result so a trailing turn's notice or result can neither suppress nor clobber a tool-only answer (all adversarial-review catches). Two CLI gotchas are load-bearing: steered messages carry NO uuid (the CLI dedupes by uuid and silently swallows a reuse) and NO priority (default "next" folds politely; "now" is a hard interrupt).
99
- - **Arrival dispatch (2026-07-27, replacing the chat queue strategy)**: Chat runs `concurrency: {strategy: "concurrent"}` - every message reaches `onMessage` the moment Slack delivers it. The queue strategy's 30s dispatch-lock lease made mid-turn messages invisible until the turn ended and could REORDER them (a reply enqueued behind a live lease was released only when a later message took the expired lock, newest dispatched first - adversarial-review catch on the steering redesign, present at any TTL). The daemon owns everything the queue provided: per-thread turn ordering via the serialized chain (`serializedTurn` counts only TURN-producing work, so reaction notes and nudges can never gate steering - a second review catch); mid-turn folding via steering; and batching via a per-thread INBOX - un-steered messages accumulate and drain as ONE folded turn, sorted by Slack ts (ids are timestamps), so an upstream arrival race cannot reorder the prompt and a burst behind a long turn costs one metered spawn, not N. Per-thread arrival decisions are themselves chained (steer-or-inbox for one message completes before the next begins), and chat's message-id dedupe runs before its concurrency branch, so the app_mention + message.channels double-delivery stays deduped.
100
- - **Safety posture**: per-link `permissionMode`, default `acceptEdits`; `--yolo` (alias `--dangerous`) opts a link into `bypassPermissions`, and relayThread pairs it with the SDK's mandatory `allowDangerouslySkipPermissions: true` opt-in. `AskUserQuestion` is disallowed (unanswerable over Slack; the model asks in prose instead). Turn failures post a trimmed message-only diagnostic (never a raw error body). Outsiders must not drive sessions (harvested from Slaude at its 2026-07-18 shutdown): `isOutsideAuthor` fail-closed rejects any message whose team-origin fields disagree with the home workspace's `workspaceTeamId` (captured via auth.test at setup and re-captured at every daemon start, so the reference can never go stale against a rotated token), so Slack Connect externals and cross-workspace guests are silently ignored and can never open a session.
101
- - **Serve skills** (`src/serve-plugin/`, ships in the package via `files: ["src"]`): a Claude Code plugin loaded per turn (`plugins: [{type: "local", path}]`; discovered skills are enabled by default, so no `skills` option). `tokenmaxxing:ask-the-user` teaches the decision protocol - when input is needed, tag the requester with the raw Slack mention token `<@U...>`, ask compactly, END the turn (the thread reply is the next turn); `tokenmaxxing:serve-session` documents how the session runs (shared repo checkout, resume across turns, worktree-by-default for mutating tasks, handoff). The one dynamic fact skills cannot carry - who asked - rides in per turn via a `UserPromptSubmit` hook whose `additionalContext` ("Slack relay context: ...", built by `serveTurnContext`) names the triggering message author's mention token. The mention survives the pipeline because streamed `markdown_text` deltas pass verbatim and the post-and-edit fallback's `finalize` only linkifies bare `@U...`, never escaping an already-formed `<@U...>`. Skills are independent of the system prompt choice (probe-verified 2026-07-18 under the custom-string `SLACK_SYSTEM_PROMPT`: the init message lists the plugin + both skills + the Skill tool; the skill listing arrives as a conversation system-reminder, and CLAUDE.md loads via settingSources).
102
- - **Socket lifecycle**: `bot.initialize()` starts the persistent auto-reconnecting SocketModeClient; the daemon then just stays alive. The leased `startSocketModeListener` API must never be looped: it returns instantly without `waitUntil` and the loop starves the event loop (live incident 2026-07-18 - connected but silent).
103
- - **Id mapping**: Chat SDK ids are adapter-prefixed (`thread.channelId` = `slack:C0123`, `thread.id` = `slack:C0123:<threadTs>`) while links store bare Slack ids - lookups strip the prefix via `bareChannelId`. Subscriptions live in the daemon's memory state, so every mention re-subscribes its thread; per-thread turn serialization is owned by the daemon itself (a promise chain per thread id), and message batching by the daemon's inbox (see the arrival-dispatch bullet; the chat queue strategy and its skipped/TTL machinery were retired with the 2026-07-27 steering redesign, though `context.skipped` is still merged defensively when present); unlinked-channel traffic logs `serve.unlinked_channel` and stays silent in Slack.
104
- - **Restart resilience** (0.19.0, from the 2026-07-18 dead-thread incident: a deploy restart cut a turn mid-answer and left the thread deaf to follow-ups): startup re-subscribes every `slack-threads/` record straight on the state adapter (`state.subscribe(threadId)`; message routing checks `stateAdapter.isSubscribed`, verified in chat 4.34.0), so open threads survive restarts without needing a fresh mention. SIGTERM/SIGINT drains instead of dying: new turns are dropped loudly (`serve.drain_dropped` plus a tracked in-thread notice - a log-only drop reads as the bot thinking), tracked in-flight turns get up to 300s to finish (a re-snapshotting wait, so late-added notices still flush), then `Chat.shutdown()`; a second signal forces exit; the drain also aborts any depleted-pool park so a countdown never delays a restart. The claude child spawns detached in its own process group (`detachedClaudeSpawn` via the SDK's `spawnClaudeCodeProcess` hook), because a terminal Ctrl-C signals the whole foreground group and a non-detached child died with the daemon before the drain could save the turn; the signal handlers register before `bot.initialize()` so no turn can start while the process still has default signal disposition, and a rejecting `Chat.shutdown()` is caught so the drain always reaches its exit. When a `thread.post` rejects mid-turn (e.g. Slack finalizes an idle stream after an undocumented window: `message_not_in_streaming_state`), relayThread salvages the dead segment in TEXT space: proven-delivered text is a mirror StreamingMarkdownRenderer's committable prefix over the confirmed chunks (each pull proves the previous append landed; the adapter's renderer holds back the trailing unterminated line until its post-iteration forced flush, so chunk-granular salvage would miss a final reply with no trailing newline - the live 2026-07-21 incident), and the remainder re-posts as a fresh message split at line boundaries, plus unconfirmed cards, bounded by a futility budget that refills on delivery progress; the textLost diagnostic remains only for exhausted salvage, and ambiguous failures duplicate a tail line rather than lose it.
105
- - **Interrupted-turn recovery** (from the second 2026-07-18 restart incident: a redeploy killed a ship turn 8 minutes in with zero notice - drain cannot save a long turn, and a group-wide kill can take the claude child even detached): the thread record persists the claude session id the moment the init message assigns it, and every turn is wrapped in a durable `activeTurn` marker (original prompt, start time, resume count) written before the spawn and cleared when the turn returns - except that a turn failing DURING a drain keeps its marker, since that failure is presumed to be the shutdown signal killing the child. On startup, a surviving marker means a restart killed that turn: the daemon posts a notice into the thread (the chat-sdk's documented proactive handle, rebuilt with the thread's newest human message as streaming recipient context so the resumed turn keeps its native task cards, that message's author becoming the turn's requester) and auto-resumes the work - resuming the recorded session with a continuation prompt, or replaying the original prompt fresh when the kill landed before the session opened; resumed turns settle like inbound ones (outcome log + finish_thread GC). Retries cap at 3 (each spends real quota) with a loud give-up notice posted before its marker clears; a per-thread turn lock keeps a startup resume from ever racing an inbound message turn in the same cwd, every recovery branch recomputes its decision from a fresh record reload under that lock (so a resume superseded by a faster inbound turn no-ops instead of double-running), and a blocking singleton flock makes a new daemon generation wait for the previous one - drain included - to fully exit before touching any thread record. Uncatchable deaths cannot leak a working child either: SIGHUP drains like SIGTERM (its default disposition skips the exit hook that kills the detached group), and the marker records the child's group pid plus its C-locale ps start-time token at spawn, so recovery kills only an exactly-identified SIGKILL-orphaned claude (never a recycled pid) before resuming its turn.
106
- - **Depleted-pool recovery** (harvested from Slaude, reshaped around the pool): relayThread consumes the spawn-boundary switch decision instead of discarding it - a depleted pool with a known recovery inside the message's one 14min parking deadline (shared across chained parks, so the thread's queue slot is never held longer in total) posts a park notice and retries at the reset. A mid-turn limit - detected ONLY on errored results, since `is_error` can ride subtype `"success"` (`Claude AI usage limit reached|<epoch>`) and a successful answer discussing limits must never be re-run - is persisted into usage.json first (`recordObservedLimit`: the serve process has no statusLine tee, and the snapshot TTL would otherwise feed the retry the stale pre-limit state), then retried silently after a short beat, so a pool swap makes the hiccup invisible. Bounded recoveries; every drop the relay performs is announced in-thread.
107
- - **Usage-limit auto-resume** (2026-07-21, superseding the drop-beats-false-promise rule for the known-wake case): a turn that ends at a usage limit with a KNOWN recovery time defers instead of dropping. relayThread reports `deferUntil`, the thread's durable activeTurn marker keeps a `resumeAt`, and a per-thread daemon timer (cancelled on drain, re-armed from the record at startup: future wakes schedule, past wakes recover immediately) fires the same interrupted-turn recovery path - serialized chain, fresh-reload supersede check, pool-recovery notice, `resumeDecision` capping total resumes. The defer triggers: known recovery past the parking deadline or recovery budget, retries exhausted while a post-burn pool probe reports a wake, and an unclassifiable child failure (no limit phrase in the error) converted by a pool probe reporting exhaustion - the pool state is the evidence the error text did not carry. Only an unknown recovery time still posts the honest re-send drop.
108
- - **Slack-native output hygiene**: relayed turns run a small standalone `systemPrompt` telling the model replies render as Slack markdown, never HTML (a live turn once answered with a literal `<br>`; the SDK's default system prompt is minimal since 0.1.0, so the string replaces nothing).
109
- - **Slash commands** (2026-07-18): a mention-stripped thread message that starts with `/` runs as a claude slash command - the SDK delivers a string prompt as one stream-json user message and the CLI routes a leading-slash prompt through its command table, on fresh and resumed sessions alike (live-verified with free `/usage` and `/context` reads). Local command output (`num_turns` 0) arrives as a non-streamed assistant message plus `result.result`, which relayThread's no-text fallback posts; `slackstream` also maps the documented `system/local_command_output` wire subtype in case a claude update flips the emitter. `/goal` (built-in since 2.1.139) works headless and its active goal is restored on resume, so it survives the per-message resume pattern. Slack gotcha: the composer eats ANY message whose first character is `/` client-side (channels and thread replies; registered app commands cannot even dispatch from threads), so the supported forms are `@bot /usage` (mention-first) and ` /usage` (leading space, Slack's own documented workaround) - both normalize to a position-0 command via `stripLeadingMention`.
110
- - **Agent representation** (`src/lib/slackstream.ts`): turns stream natively (`chat.startStream`, which works in channel threads regardless of the assistant:write scope): thinking and tool calls as task cards ("Thinking"/tool name/"Turn", input summary + truncated output) that Slack groups into ONE collapsible plan block per turn (the serve edge wraps every posted segment in a `StreamingPlan` with `groupTasks: "plan"`, Slack's `task_display_mode: "plan"`; user ask 2026-07-20 "squash them into one dropdown", superseding the 2026-07-18 separate-messages shape), and reply text as native markdown with rendered code fences - so one turn streams as a single Slack message: the reply plus one dropdown of its activity. Reply text also splits at `SEGMENT_TEXT_MAX` (10,000 chars): Slack rejects an over-long message with `msg_too_long` (the streamed message accumulates server-side toward the documented 12,000-char `markdown_text` envelope) and nothing in the chat stack bounds or splits text, so one over-cap append used to reject the whole post and lose the reply (live incident 2026-07-20); a break forced inside a code fence closes and reopens the fence, fence parity is tracked over the segment's accumulated text (SDK deltas do not respect markdown token boundaries), and a cut never slices through a backtick run. A later main text block (typically post-tool prose) opens on a `\n\n` paragraph break (`textStreamed`), restoring the separation the per-tool message split used to provide; whitespace-only deltas do not arm it. Notices still post as their own messages, and a rejected append (Slack's message cap, a finalized idle stream) carries its undelivered chunks into a follow-on message via the dead-segment salvage.
111
- - **One Slack app per daemon** (live incident 2026-07-20): Slack Socket Mode delivers each event envelope to exactly one of an app's open connections, so two serve daemons sharing one Slack app steal each other's events - mentions usually survive (Slack emits `app_mention` plus `message.channels` for one mention and the Chat SDK dedupes them per-process), but a non-mention thread follow-up rides a single envelope and silently dies when the wrong daemon receives it. No code can route Slack's load-balancing (the hello frame's `num_connections` is discarded by `@slack/socket-mode` before any event fires), so the constraint is documented (docs limitations page) and diagnosed: `serve.unlinked_channel` logs once per channel per run with the shared-app explanation. Every host needs its own Slack app with its own tokens.
112
- - **Todo checklist card**: TodoWrite is bookkeeping, not a real tool run, so it never opens a generic card. Instead each stream gets one stable-id "Todos" card (id `todos`, subagents `todos-<parent_tool_use_id>`) that updates in place on every TodoWrite: `✅ content` for completed, `🔄 activeForm` for the in-progress item (live narration), `⬜ content` for pending (the Chat SDK Plan object's own iconography); card status goes complete only when every item is completed. The "Todos have been modified successfully" tool_result is suppressed, but a FAILED TodoWrite flips the card to error (the optimistic checklist must not claim a state that never took effect). Card ids do not carry across messages, so a TodoWrite after a dead-segment recovery starts a fresh card in the follow-on message: accepted, the latest state is always in the newest message. claude >= 2.1.142 defaults to the structured Task tools and never emits TodoWrite, so relayThread sets `CLAUDE_CODE_ENABLE_TASKS: "0"` in the spawn env (the documented opt-out; without it the checklist card is inert).
113
- - **Terminal echo** (2026-07-18): the daemon registers a `log()` echo (`setLogEcho` in log.ts, off by default so hooks and the statusline keep their stdout protocols clean), so every event while it runs - serve.* plus the in-process swap/decision events from `ensureBestAccount()`/`stopHookCheck` - also prints one colored line in the foreground terminal (`formatLogLine`: dim HH:MM:SS, event painted red/yellow/cyan by structural endsWith severity, redacted fields). `serve.turn_done`/`serve.turn_failed` (with seconds) close every relayed turn, so a foreground `xx serve` is observable without tailing `tokenmaxxing.log`.
114
- - **Live-verified end-to-end** (2026-07-18, in a live workspace channel): mention opens a session (worktree-per-thread at the time), replies stream, thread follow-ups resume with context, cards + fenced code render, segmentation and queue folding behave. Plus the hermetic suite: schemas/links, stream mapping, fail-fast paths.
115
-
116
- ---
117
-
118
90
  ## 6. Honest papercuts
119
91
  - **Respawn hiccup (depleted pause only).** Plain swaps never restart the session. When the whole pool is depleted you see `claude` stop, a countdown, and a resume; anything typed in the split second before the SIGTERM is lost, and the supervisor resets terminal mode so nothing is left garbled.
120
92
  - **Adoption lag.** macOS reads the keychain through a raw 30s cache, so at most the first turn after a swap can still meter the old account. The bars' headroom absorbs it.
@@ -139,7 +111,9 @@ A local Socket Mode daemon (no public URL) that turns Slack threads into Claude
139
111
 
140
112
  **Shipped since (0.13.0):** Codex as a second pool, parallel state (`codex-accounts.json`, `codex-creds/`, own flock), same pace-pressure policy. Codex differences that shaped it: restart IS the switch (a running codex refuses another account's credential), usage is a free direct GET with epoch resets and a duration-classified window set (the weekly window is primary on current plans), the refresh token rotates with reuse punished (harvest-by-true-owner, persist every rotation), and codex's new Stop-hook system drives the auto-swap through a codex supervisor shim that respawns `codex resume <session-id>` (hooks must be trusted once via `/hooks`). Sibling sessions left on ANY non-live account are reconciled cross-session (owner decisions 2026-07-20; a non-live session cannot refresh cross-account and would wedge at token expiry, healthy or not): the deciding actor drops a reconcile marker addressed to the sibling's supervisor, and the sibling's own Stop hook promotes it into a respawn onto the live account at its next turn boundary - the only safe respawn point - while a blocked live seat never receives a signal.
141
113
 
142
- **Non-goals:** an API/MITM proxy; reimplementing OAuth beyond the single refresh-grant call in the swap.
114
+ **Shipped since (relay companion):** `tokenmaxxing relay` hosts durable tmux workers for cheap host subagents. Claude `--permission-mode` vocabulary (default `auto`), Codex sandbox mapping, per-session flock + registry under `relay/`, additive turn-done markers (never `respawn/`), and `permission-needed` stdout pings decided by main. Host finish hooks still run on every return including pings. Skill + agents live under `agent-plugin/` (`relay-session`, `tokenmaxxing-claude`, `tokenmaxxing-codex`). Docs: `docs/content/docs/commands.mdx`, `state-directory.mdx`, `agent-plugin.mdx`.
115
+
116
+ **Non-goals:** an API/MITM proxy; reimplementing OAuth beyond the single refresh-grant call in the swap; Slack serve as the relay bus; soft concurrency caps; Cloud Agents without local tmux.
143
117
 
144
118
  ---
145
119
 
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 anaclumos
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -73,7 +73,6 @@ claude # use claude as always
73
73
  | `tokenmaxxing status --force` | additionally ping every account (one tiny haiku request each) so all 5h session timers start now, then sample fresh |
74
74
  | `tokenmaxxing watch [seconds]` | live status: re-render every N seconds (default 120, floor 30; never pings) |
75
75
  | `tokenmaxxing config` | effective config with sources; `get`/`set`/`unset` dotted keys, `tidy` prunes unknown keys |
76
- | `tokenmaxxing serve` | Slack bridge daemon (Socket Mode, no public URL): `setup` prints the app manifest and stores the two tokens, `link <channel-id> <repo>` ties a channel to a repo (`--yolo` for full-autonomy bypassPermissions sessions), then mentioning the bot in that channel opens a Claude Code session per thread in the repo checkout (the session cuts its own git worktree only when a task needs isolation) and thread messages relay in and out |
77
76
  | `tokenmaxxing doctor` | verify the supervisor + settings entries survived |
78
77
  | `tokenmaxxing rename [--codex] <sel> <label>` / `rm [--codex] <sel>` | manage the pool (`--codex` targets the codex pool: one email can hold both a claude and a codex account) |
79
78
  | `tokenmaxxing uninstall` | remove supervisor + settings entries (accounts/credentials kept) |
@@ -159,7 +158,7 @@ Two codex-specific facts worth knowing: codex does not run hooks it has not been
159
158
 
160
159
  ## How it's built
161
160
 
162
- TypeScript on Bun: one multi-call entry (`src/main.ts`) serves the CLI, the `claude` supervisor, and the hook/statusLine shims, and runs directly under bun. [Zod](https://zod.dev) validates every external-boundary payload (credential blobs, hook/statusLine stdin, OAuth responses, config), [es-toolkit](https://es-toolkit.dev) for utilities. The supervisor is process/terminal-only - it never proxies API traffic or touches tokens in flight. Cross-process coordination uses `flock(2)` via `bun:ffi` (macOS has no `flock(1)`; one codepath serves both platforms). Credential I/O goes through one platform-selected store: `security(1)` generic-passwords on macOS, atomic 0600 file writes on Linux.
161
+ TypeScript on Bun. One multi-call entry (`src/main.ts`) runs the CLI, the `claude` supervisor, and the hook/statusLine shims. [Zod](https://zod.dev) validates every external-boundary payload (credential blobs, hook/statusLine stdin, OAuth responses, config). [es-toolkit](https://es-toolkit.dev) for utilities. The supervisor is process/terminal-only. It never proxies API traffic or touches tokens in flight. Cross-process coordination uses `flock(2)` via `bun:ffi` (macOS has no `flock(1)`; one codepath for both platforms). Credential I/O goes through one platform-selected store: `security(1)` generic-passwords on macOS, atomic 0600 file writes on Linux.
163
162
 
164
163
  ## License
165
164
 
@@ -0,0 +1,43 @@
1
+ ---
2
+ name: tokenmaxxing-claude
3
+ description: Cheap shell-only relay to a durable Claude Code worker via tokenmaxxing relay. Use for long-running Claude sessions with permission pings back to main.
4
+ model: inherit
5
+ readonly: false
6
+ is_background: false
7
+ ---
8
+
9
+ # Tokenmaxxing Claude relay
10
+
11
+ You are a thin shell. Do not edit files yourself. Do not spawn nested Task/Agent children. Only call `tokenmaxxing relay` (alias `xx relay`).
12
+
13
+ ## Commands
14
+
15
+ ```bash
16
+ tokenmaxxing relay turn --worker claude --permission-mode auto --cwd <dir> --session <uuid-or-omit> "<prompt>"
17
+ tokenmaxxing relay decide --session <uuid> --approve
18
+ tokenmaxxing relay decide --session <uuid> --deny
19
+ tokenmaxxing relay set-permission-mode --session <uuid> --permission-mode <mode>
20
+ tokenmaxxing relay status --session <uuid>
21
+ tokenmaxxing relay destroy --session <uuid>
22
+ ```
23
+
24
+ Default permission mode is `auto`. Modes: `default` (alias `manual`), `acceptEdits`, `plan`, `auto`, `dontAsk`, `bypassPermissions`.
25
+
26
+ ## Stdout protocol
27
+
28
+ ```
29
+ session: <id>
30
+ permission-mode: <mode>
31
+ # turn output OR:
32
+ permission-needed: <requestId>
33
+ summary: <one line>
34
+ detail: <flagged action>
35
+ session: <id>
36
+ permission-mode: auto
37
+ ```
38
+
39
+ On `permission-needed`, return that block to main and stop. Main decides (or auto-approves when Cursor Run Mode is Run Everything, or when Claude main is `bypassPermissions`). Then you run `relay decide` and wait.
40
+
41
+ ## Finish hooks
42
+
43
+ Your host finish hook (`subagentStop` / `SubagentStop`) must run on every return, including permission-needed. A permission park is not a worker Stop.
@@ -0,0 +1,40 @@
1
+ ---
2
+ name: tokenmaxxing-codex
3
+ description: Cheap shell-only relay to a durable Codex worker via tokenmaxxing relay. Claude permission-mode names map to Codex sandbox flags.
4
+ model: inherit
5
+ readonly: false
6
+ is_background: false
7
+ ---
8
+
9
+ # Tokenmaxxing Codex relay
10
+
11
+ You are a thin shell. Do not edit files yourself. Do not spawn nested Task/Agent children. Only call `tokenmaxxing relay` (alias `xx relay`).
12
+
13
+ ## Commands
14
+
15
+ ```bash
16
+ tokenmaxxing relay turn --worker codex --permission-mode auto --cwd <dir> --session <uuid-or-omit> "<prompt>"
17
+ tokenmaxxing relay decide --session <uuid> --approve
18
+ tokenmaxxing relay decide --session <uuid> --deny
19
+ tokenmaxxing relay set-permission-mode --session <uuid> --permission-mode <mode>
20
+ tokenmaxxing relay status --session <uuid>
21
+ tokenmaxxing relay destroy --session <uuid>
22
+ ```
23
+
24
+ CLI still speaks Claude permission-mode names. Codex mapping:
25
+
26
+ | Mode | Codex flags |
27
+ |---|---|
28
+ | `plan` / `default` | `--sandbox read-only` + ask-for-approval on |
29
+ | `acceptEdits` | `--sandbox workspace-write` |
30
+ | `auto` | workspace-write + ask-for-approval |
31
+ | `dontAsk` | read-only + approval never |
32
+ | `bypassPermissions` | `--sandbox danger-full-access` + approval off |
33
+
34
+ ## Stdout protocol
35
+
36
+ Same as the Claude relay agent. On `permission-needed`, return the block to main; then `relay decide`.
37
+
38
+ ## Finish hooks
39
+
40
+ Host finish hooks run on every return including permission-needed. Worker Stop still runs only on real turn finish.
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env bun
2
+ import { main } from "../../src/entries/mcp.ts";
3
+
4
+ main().catch((e: unknown) => {
5
+ console.error(e instanceof Error ? e.message : String(e));
6
+ process.exit(1);
7
+ });
@@ -0,0 +1,14 @@
1
+ {
2
+ "version": 1,
3
+ "hooks": {
4
+ "tokenmaxxingRelay": {
5
+ "description": "tokenmaxxing-owned Cursor hook group for relay finish behavior. Host finish hooks must still run on every subagent return including permission-needed.",
6
+ "subagentStop": [
7
+ {
8
+ "type": "command",
9
+ "command": "/usr/bin/true"
10
+ }
11
+ ]
12
+ }
13
+ }
14
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json",
3
+ "mcpServers": {
4
+ "tokenmaxxing": {
5
+ "type": "stdio",
6
+ "command": "./bin/tokenmaxxing-mcp",
7
+ "cwd": "${PLUGIN_ROOT}"
8
+ }
9
+ }
10
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
+ "name": "tokenmaxxing",
4
+ "version": "1.8.0",
5
+ "description": "Pool Claude Code and Codex logins, switch on pace pressure, and expose safe ops tools to agent clients.",
6
+ "author": {
7
+ "name": "anaclumos",
8
+ "url": "https://github.com/anaclumos"
9
+ },
10
+ "homepage": "https://github.com/anaclumos/tokenmaxxing",
11
+ "repository": "https://github.com/anaclumos/tokenmaxxing",
12
+ "license": "MIT",
13
+ "keywords": [
14
+ "claude",
15
+ "codex",
16
+ "quota",
17
+ "account-switching",
18
+ "mcp"
19
+ ]
20
+ }
@@ -0,0 +1,23 @@
1
+ ---
2
+ name: codex-pool
3
+ description: Operate the Codex side of the pool (restart-is-switch, no Layer 2, never target a running sibling). Use for codex init/switch/status questions or MCP pool_switch with codex=true.
4
+ ---
5
+
6
+ # Codex pool
7
+
8
+ ## Mechanics
9
+
10
+ - No hot-swap: restart IS the switch (`codex resume <sid>`). A credential write takes effect on the next codex start.
11
+ - Auto-switching needs trusted hooks (`/hooks`). Do not clobber the user's `notify` key in `config.toml`.
12
+ - Refresh-token reuse is punished; a superseded token can kill the grant family. Persist every rotation immediately.
13
+ - An account running in another supervised session is never a swap target and never sampler-refreshed. Parked does not mean idle.
14
+ - Classify windows by DURATION, never by position. Some plans have no 5h window.
15
+ - Layer 2 wall squeeze is Claude-only. Do not extend it to Codex.
16
+
17
+ ## Agent actions
18
+
19
+ - Read: `pool_ls` / `pool_status` (Codex section included when present).
20
+ - Mutate: MCP `pool_switch` with `codex=true`, `confirm=true`, and `TOKENMAXXING_AGENT_MUTATIONS=1` after user approval.
21
+ - Interactive `init --codex` / `add --codex` / reauth stay human-driven.
22
+
23
+ See [references/codex.md](references/codex.md).
@@ -0,0 +1,5 @@
1
+ # Codex references
2
+
3
+ - `docs/content/docs/codex.mdx`
4
+ - `src/lib/codexdecide.ts`, `src/lib/codexpick.ts`, `src/lib/codexswap.ts`
5
+ - `.memory/cc-codex-auth-mechanics.md`
@@ -0,0 +1,26 @@
1
+ ---
2
+ name: credentials-hygiene
3
+ description: Credential and identity rules for tokenmaxxing (fetchTokenOrg identity, flock, no blob compare, never print secrets). Use before any auth, swap, or credential-touching change.
4
+ ---
5
+
6
+ # Credentials hygiene
7
+
8
+ ## Identity
9
+
10
+ - Identity is whatever `fetchTokenOrg` reports, never a stored label and never a blob byte compare. Two rotations of one account differ byte-for-byte.
11
+ - Park a credential under its token's real owner. Commit the active label in the same critical section as credential writes.
12
+ - Every live-store write goes through `withClaudeRefreshLock` (Claude). Near-expiry sessions can rotate into the live store at any moment.
13
+
14
+ ## Isolation
15
+
16
+ - Refuse ambient `CLAUDE_CONFIG_DIR` / `CLAUDE_SECURESTORAGE_CONFIG_DIR` in CLI, SDK, and MCP.
17
+ - A missing namespaced keychain item never falls back to the live one.
18
+ - Same accounts pooled on several hosts race on refresh (documented limitation).
19
+
20
+ ## Agent hard stops
21
+
22
+ - No tool may return keychain blobs, `.credentials.json`, `auth.json`, or OAuth access/refresh tokens.
23
+ - Do not bypass Claude `/usage` with direct `GET /api/oauth/usage` (owner-rejected; 429s on the active session).
24
+ - Prefer MCP read tools; never `cat` credential paths into the transcript.
25
+
26
+ See [references/credentials.md](references/credentials.md).
@@ -0,0 +1,6 @@
1
+ # Credential references
2
+
3
+ - `docs/content/docs/credentials.mdx`
4
+ - `DESIGN.md` sections on install and swap
5
+ - `.memory/cc-codex-auth-mechanics.md`
6
+ - `src/lib/credstore.ts`, `src/lib/oauth.ts`
@@ -0,0 +1,26 @@
1
+ ---
2
+ name: doctor-diagnostics
3
+ description: Diagnose a broken or incomplete tokenmaxxing install (supervisor, hooks, timer, credential identity). Use when doctor fails, PATH looks wrong, or identity drift is suspected. Prefer the MCP doctor tool.
4
+ ---
5
+
6
+ # Doctor and diagnostics
7
+
8
+ ## Prefer MCP
9
+
10
+ Call `doctor`. It checks the supervisor wrapper, settings hooks, periodic timer, live/parked credential presence, and org identity match. Output is pass/fail plus labels.
11
+
12
+ Also useful: `config_get` (no key) for effective config sources, and `pool_ls` for needs-reauth.
13
+
14
+ ## Hard stops
15
+
16
+ - Never dump keychain items, `.credentials.json`, `auth.json`, or OAuth tokens into chat.
17
+ - Doctor never refreshes tokens. Expired access tokens show as unverifiable identity, not as a prompt to paste secrets.
18
+ - Do not run `init` / `add` / `auth` from an agent session without the user present for interactive login.
19
+
20
+ ## Common repairs (ask before mutating)
21
+
22
+ - Missing wrapper or hooks: user runs `tokenmaxxing init` (or `init --codex`).
23
+ - needs-reauth: user runs `tokenmaxxing auth <label>` (or `--all`).
24
+ - PATH: `binDir` must precede the real claude.
25
+
26
+ See [references/troubleshooting.md](references/troubleshooting.md).
@@ -0,0 +1,5 @@
1
+ # Troubleshooting pointers
2
+
3
+ - `docs/content/docs/troubleshooting.mdx`
4
+ - `docs/content/docs/state-directory.mdx` for where state lives under `~/.config/tokenmaxxing` (or `TOKENMAXXING_HOME`)
5
+ - Ambient `CLAUDE_CONFIG_DIR` / `CLAUDE_SECURESTORAGE_CONFIG_DIR` must be unset for CLI, SDK, and this MCP server
@@ -0,0 +1,27 @@
1
+ ---
2
+ name: pool-status
3
+ description: Read the Claude and Codex account pool safely (list, usage bars, watch). Use when checking quota, which account is active, needs-reauth, or before any switch. Prefer MCP pool_ls and pool_status over raw shell.
4
+ ---
5
+
6
+ # Pool status (safe reads)
7
+
8
+ ## Prefer MCP
9
+
10
+ - `pool_ls` for labels, active marker, needs-reauth
11
+ - `pool_status` for 5h / weekly / per-model bars (free `/usage` path)
12
+
13
+ If MCP is unavailable, run `tokenmaxxing ls` or `tokenmaxxing status` (alias `xx`). Never add `--force`.
14
+
15
+ ## Hard stops
16
+
17
+ - Do not run `status --force` or bare `xx --force`. That pings every account with a real request and opens each 5h window.
18
+ - Do not print credential files, keychain blobs, or OAuth tokens. Labels and status only.
19
+ - Do not kill sessions or supervisors to "free" an account.
20
+
21
+ ## Notes
22
+
23
+ - Active Claude usage often comes from the statusLine tee; parked accounts are probed in isolation.
24
+ - `watch` re-renders status on an interval and never force-pings.
25
+ - Hermetic agents: set `TOKENMAXXING_HOME` to a throwaway directory.
26
+
27
+ See [references/commands.md](references/commands.md).
@@ -0,0 +1,8 @@
1
+ # Status and list commands
2
+
3
+ - `tokenmaxxing` / `tokenmaxxing status`: sample and show bars; persists cached usage for the picker.
4
+ - `tokenmaxxing status --force`: DENIED for agents without explicit user approval. Meters every account.
5
+ - `tokenmaxxing ls`: compact list.
6
+ - `tokenmaxxing watch [seconds]`: live re-render (default 120), never `--force`.
7
+
8
+ Docs: `docs/content/docs/commands.mdx`.
@@ -0,0 +1,118 @@
1
+ ---
2
+ name: relay-session
3
+ description: Run durable Claude Code or Codex workers through tokenmaxxing relay (tmux). Use when a host agent needs a long-lived pooled worker, permission pings to main, or tokenmaxxing-claude / tokenmaxxing-codex agents.
4
+ ---
5
+
6
+ # Relay session
7
+
8
+ Host agents stay cheap. They shell into `tokenmaxxing relay`, which owns a durable tmux worker (Claude Code or Codex via tokenmaxxing). High-churn concurrent sessions are expected.
9
+
10
+ Install templates: `tokenmaxxing relay install --target cursor|claude|all`.
11
+
12
+ ## Claude worker permission modes
13
+
14
+ CLI: `claude --permission-mode <mode>` / `relay set-permission-mode --permission-mode <mode>`.
15
+
16
+ | Mode | What runs without asking |
17
+ |---|---|
18
+ | `default` (alias `manual`) | Reads only |
19
+ | `acceptEdits` | Reads, file edits, common filesystem cmds in cwd |
20
+ | `plan` | Reads (+ classifier-approved cmds when auto available) |
21
+ | `auto` | Everything with background safety classifier (**relay default**) |
22
+ | `dontAsk` | Only pre-approved tools; else deny |
23
+ | `bypassPermissions` | Everything (needs allow-dangerously flags) |
24
+
25
+ Claude `auto` is classifier-assisted autonomy, not "defer to main". Relay pings main only when the worker would still prompt.
26
+
27
+ ## Cursor Run Modes (main only)
28
+
29
+ | Run Mode | Role for relay |
30
+ |---|---|
31
+ | Auto-review | Main decides each ping or escalates to the user |
32
+ | Allowlist | Main decides / escalates |
33
+ | Run Everything | Main auto-`relay decide --approve` on worker pings |
34
+
35
+ Do not probe undocumented APIs for main's Run Mode. Main states or infers it.
36
+
37
+ If Claude main is itself in `bypassPermissions`, auto-approve worker pings the same way.
38
+
39
+ ## Codex mapping
40
+
41
+ | Claude mode | Codex flags |
42
+ |---|---|
43
+ | `plan` / `default` | `--sandbox read-only` + ask-for-approval on |
44
+ | `acceptEdits` | `--sandbox workspace-write` |
45
+ | `auto` | workspace-write + ask-for-approval |
46
+ | `dontAsk` | read-only + approval never |
47
+ | `bypassPermissions` | `--sandbox danger-full-access` + approval off |
48
+
49
+ ## Shared stdout protocol
50
+
51
+ ```
52
+ session: <id>
53
+ permission-mode: <mode>
54
+ # turn output OR:
55
+ permission-needed: <requestId>
56
+ summary: <one line>
57
+ detail: <flagged action>
58
+ session: <id>
59
+ permission-mode: auto
60
+ ```
61
+
62
+ ## Commands
63
+
64
+ | Command | Job |
65
+ |---|---|
66
+ | `relay turn` | Ensure session, send prompt, wait until turn-done or permission-needed |
67
+ | `relay decide` | Approve/deny pending ping; resume worker |
68
+ | `relay set-permission-mode` | Change live worker mode |
69
+ | `relay status` / `destroy` / `gc` | Inspect, tear down, reap |
70
+ | `relay install` / `relay config` | Host templates + relay.json |
71
+
72
+ Flags: `--worker claude|codex`, `--session`, `--cwd`, `--permission-mode`, prompt via argv/stdin.
73
+
74
+ ## Finish hooks
75
+
76
+ 1. Cheap relay host finish hooks (`subagentStop` / `SubagentStop`) fire on every return, including `permission-needed`.
77
+ 2. Worker Stop / Codex Stop still run on real turn finish (pool logic unchanged).
78
+ 3. Relay turn-done markers are additive under `$TOKENMAXXING_HOME/relay/turn-done/`. Permission park is not a fake worker Stop. Never write into `respawn/`.
79
+
80
+ ## Cursor IPC matrix
81
+
82
+ | Contract | Relay support |
83
+ |---|---|
84
+ | Task / custom agent prompt | Primary in |
85
+ | Task final message | Primary out (delta or permission-needed) |
86
+ | Task resume / interrupt | Continue with decide / set-permission-mode / next turn |
87
+ | Foreground / background + completion notify | Prefer foreground under modes that still prompt |
88
+ | Frontmatter model / readonly / is_background / tools | Shell-only templates; configurable cheap model |
89
+ | `/name` delegate | Same agents |
90
+ | subagentStart / subagentStop (+ followup_message) | Optional hooks; finish hooks always run |
91
+ | Task preToolUse / postToolUse | Optional |
92
+ | UpdateCurrentStep | UI phases only, not permission channel |
93
+ | Nesting | Relay must not spawn Task children |
94
+
95
+ ## Claude IPC matrix
96
+
97
+ | Contract | Relay support |
98
+ |---|---|
99
+ | Agent tool prompt / final result | Primary |
100
+ | SendMessage resume / mid-run | Primary continue path |
101
+ | Background + completion notify | Supported |
102
+ | Fork / `/subtask` | Supported if host uses it |
103
+ | Permission UI bubble | Orthogonal; do not suppress |
104
+ | PermissionRequest / Notification | Optional hooks; worker pings stay stdout protocol |
105
+ | SubagentStart / SubagentStop | Finish hooks always run on every return including pings |
106
+ | Pre/PostToolUse (incl. on Agent) | Optional |
107
+ | Agent teams mailbox / task list | Same stdout contract over team delivery |
108
+ | Cross-session SendMessage | Supported; peer messages never carry user authority |
109
+ | subagentStatusLine | UI only |
110
+
111
+ ## Hard stops
112
+
113
+ - Never pattern-kill tmux. Destroy by exact session name only.
114
+ - Never print credentials or OAuth tokens.
115
+ - Never spawn nested Task/Agent from the cheap relay agent.
116
+ - Prefer `tokenmaxxing-claude` / `tokenmaxxing-codex` agents over ad-hoc shells.
117
+
118
+ See [references/ipc.md](references/ipc.md).
@@ -0,0 +1,23 @@
1
+ # Relay IPC checklist
2
+
3
+ ## Cursor
4
+
5
+ - In: Task prompt, `/tokenmaxxing-claude`, `/tokenmaxxing-codex`
6
+ - Out: final message with stdout protocol
7
+ - Continue: Task resume after `relay decide` or `relay set-permission-mode`
8
+ - Finish: `subagentStop` always (including permission-needed returns)
9
+ - Optional: `subagentStart`, Task `preToolUse` / `postToolUse`
10
+ - Not a permission channel: `UpdateCurrentStep`, transcripts
11
+
12
+ ## Claude Code
13
+
14
+ - In: Agent tool prompt
15
+ - Out: Agent final result with stdout protocol
16
+ - Continue: SendMessage / next Agent turn after decide
17
+ - Finish: `SubagentStop` always (including permission-needed returns)
18
+ - Optional: `PermissionRequest` / `Notification` on main; worker pings still use relay stdout
19
+ - UI only: `subagentStatusLine`
20
+
21
+ ## Finish-hook rule
22
+
23
+ Permission park returns to main without faking a worker Stop. Worker Stop hooks still run when the worker actually finishes a turn and write additive `relay/turn-done/<session>` markers only.
@@ -0,0 +1,27 @@
1
+ ---
2
+ name: safe-contribution
3
+ description: Safely change and ship tokenmaxxing (tests, PR window, Mac vs Linux skew, public-repo hygiene). Use when editing this repo, opening PRs, or verifying decision-path changes.
4
+ ---
5
+
6
+ # Safe contribution
7
+
8
+ ## Verify
9
+
10
+ - Default suite: `bun test`.
11
+ - After decision-path changes, also run `bun test/e2e/swap-concurrency.ts` by hand (not part of `bun test`).
12
+ - Hermetic CLI: `TOKENMAXXING_HOME=/tmp/xx-test bun run src/main.ts ...`.
13
+ - Ask before any run that meters real quota (`status --force`, live-pool inference). Free `/usage` / plain status is fine.
14
+
15
+ ## Ship
16
+
17
+ - Work on a branch; bump `package.json` in the same PR; open PR; wait the full review window; handle every review; merge; `gh release create v<version>`; verify npm publish.
18
+ - Never push directly to main. npm trusted publishing is bound to workflow filename `ci.yml`.
19
+
20
+ ## Machine gotchas
21
+
22
+ - The owner's Mac may run this working tree live. Do not kill sessions or supervisors without asking.
23
+ - Linux boxes often run an older npm global: compare installed version before "works on Mac not Linux" debugging.
24
+ - Repo is PUBLIC: no Slack IDs, device info, or secrets in commits, PRs, docs, or `.memory`.
25
+ - Do not reintroduce banned patterns listed in `AGENTS.md` (Stop text-sniff failsafe, rejected statusline formats, old-state shims).
26
+
27
+ See [references/ship.md](references/ship.md).
@@ -0,0 +1,5 @@
1
+ # Ship and testing references
2
+
3
+ - `AGENTS.md` (Release and CI, Testing, Do not reintroduce)
4
+ - `.memory/shipping-pr-based.md`
5
+ - `.memory/review-window-is-mandatory-full-length.md`