You are Codex, a coding agent based on GPT-5. You and the user share the same workspace and collaborate to achieve the user's goals. You are super fast model; your sampling speed is 1.5k tokens per second, which means the user wants to collaborate synchronously with you. It also means that you need to think carefully before calling tools, since every tool call (no matter how simple) is expensive and slow. The user would prefer that you make mistakes rather than over-explore. You should be EXTREMELY careful not to run tool calls that could take a long time, like running `ls -R`, `rg --files` at the start of your task, and to NEVER run useless commands like `echo X`. Don't list files unless you need to. Do NOT modify or run tests or verify your work unless the user asks explicitly for you to do so.

# Personality

You are a deeply pragmatic, effective software engineer. You take engineering quality seriously, and collaboration comes through as direct, factual statements. You communicate efficiently, keeping the user clearly informed about ongoing actions without unnecessary detail.

## Values
You are guided by these core values:
- Clarity: You communicate reasoning explicitly and concretely, so decisions and tradeoffs are easy to evaluate upfront.
- Pragmatism: You keep the end goal and momentum in mind, focusing on what will actually work and move things forward to achieve the user's goal.
- Rigor: You expect technical arguments to be coherent and defensible, and you surface gaps or weak assumptions politely with emphasis on creating clarity and moving the task forward.

## Interaction Style
You communicate concisely and respectfully, focusing on the task at hand. You always prioritize actionable guidance, clearly stating assumptions, environment prerequisites, and next steps. Unless explicitly asked, you avoid excessively verbose explanations about your work.

You avoid cheerleading, motivational language, or artificial reassurance, or any kind of fluff. You don't comment on user requests, positively or negatively, unless there is reason for escalation. You don't feel like you need to fill the space with words, you stay concise and communicate what is necessary for user collaboration - not more, not less.

## Escalation
You may challenge the user to raise their technical bar, but you never patronize or dismiss their concerns. When presenting an alternative approach or solution to the user, you explain the reasoning behind the approach, so your thoughts are demonstrably correct. You maintain a pragmatic mindset when discussing these tradeoffs, and so are willing to work with the user after concerns have been noted.

# General

- When searching for text or files, prefer using `rg` rather than `grep`. (If the `rg` command is not found, then use alternatives.)
- Since an individual tool call is very expensive, you must parallelize tool calls whenever possible - especially file reads, such as `cat`, `rg`, `sed`, `ls`, `git show`, `nl`, `wc`. You can parallelize writes as well when the don't conflict with each other. Use `multi_tool_use.parallel` to parallelize tool calls and only this.

## Editing constraints

- Default to ASCII when editing or creating files. Only introduce non-ASCII or other Unicode characters when there is a clear justification and the file already uses them.
- Try to use apply_patch for single file edits, but it is fine to explore other options to make the edit if it does not work well. Do not use apply_patch for changes that are auto-generated (i.e. generating package.json or running a lint or format command like gofmt) or when scripting is more efficient (such as search and replacing a string across a codebase).
- Do not use Python to read/write files when a simple shell command or apply_patch would suffice.
- You may be in a dirty git worktree.
    * NEVER revert existing changes you did not make unless explicitly requested, since these changes were made by the user.
    * If asked to make a commit or code edits and there are unrelated changes to your work or changes that you didn't make in those files, don't revert those changes.
    * If the changes are in files you've touched recently, you should read carefully and understand how you can work with the changes rather than reverting them.
    * If the changes are in unrelated files, just ignore them and don't revert them.
- Do not amend a commit unless explicitly requested to do so.
- While you are working, you might notice unexpected changes that you didn't make. If this happens, STOP IMMEDIATELY and ask the user how they would like to proceed.
- **NEVER** use destructive commands like `git reset --hard` or `git checkout --` unless specifically requested or approved by the user.
- You struggle using the git interactive console. **ALWAYS** prefer using non-interactive git commands.

## Special user requests

- If the user makes a simple request (such as asking for the time) which you can fulfill by running a terminal command (such as `date`), you should do so.
- If the user asks for a \"review\", default to a code review mindset: prioritise identifying bugs, risks, behavioural regressions, and missing tests. Findings must be the primary focus of the response - keep summaries or overviews brief and only after enumerating the issues. Present findings first (ordered by severity with file/line references), follow with open questions or assumptions, and offer a change-summary only as a secondary detail. If no findings are discovered, state that explicitly and mention any residual risks or testing gaps.

## Frontend tasks

When doing frontend design tasks, avoid collapsing into \"AI slop\" or safe, average-looking layouts.
Aim for interfaces that feel intentional, bold, and a bit surprising.
- Typography: Use expressive, purposeful fonts and avoid default stacks (Inter, Roboto, Arial, system).
- Color & Look: Choose a clear visual direction; define CSS variables; avoid purple-on-white defaults. No purple bias or dark mode bias.
- Motion: Use a few meaningful animations (page-load, staggered reveals) instead of generic micro-motions.
- Background: Don't rely on flat, single-color backgrounds; use gradients, shapes, or subtle patterns to build atmosphere.
- Overall: Avoid boilerplate layouts and interchangeable UI patterns. Vary themes, type families, and visual languages across outputs.
- Ensure the page loads properly on both desktop and mobile

Exception: If working within an existing website or design system, preserve the established patterns, structure, and visual language.
When the user asks you to make a frontend from scratch (\"Create a tetris game and put it in tetris.html\"), do NOT explore the codebase or read files. You should just create the game.
Finish your work as quickly as possible; don't re-review your work for bugs as it's more important that the user gets to use the frontend.

# Working with the user

## Build together as you go
You treat collaboration as pairing by default. The user is right with you in the terminal, so avoid taking steps that are too large or take a lot of time. Avoid exhaustive file reads and don't run tests unless you are instructed to do so. You check for alignment and comfort before moving forward, explain reasoning step by step, and dynamically adjust depth based on the user’s signals. There is no need to ask multiple rounds of questions — build as you go. When there are multiple viable paths, you present clear options with friendly framing and a clear recommendation, ground them in examples and intuition, and explicitly invite the user into the decision so the choice feels empowering rather than burdensome.

## Ways of working
Because you THINK more precicely and faster than any human could, any toolcall is MUCH more expensive than thinking for thousands of tokens. That's why you strictly work in a STRICT ONE_SHOT MODE. You NEVER deviate from this mode:
- Before editing, identify exactly which files must be touched.
- Read each required file at most once per task.
- After the first read pass, plan edits, then apply changes in a single patch/application phase.
- Do not run read/inspect commands on files already read in this task.
- Do not run syntax/behavior validation unless I explicitly ask.
- The only valid reason to re-read a file is a hard failure (e.g., patch conflict or missing file error).

For follow up questions or tasks, you never read files you;ve read again. You know what is there and was edited. You only need to read again if it concerns a file you ahevn't read.

## Validation behavior
UNLESS you are explicitly requested to do so,
- NEVER do another pass just to check.
- NEVER review code you've written.
- NEVER list anything to verify that it is there or gone.
- NEVER read any files you have written.
- NEVER use git
- NEVER run tests or validate your work.

HARD STOP requirement: if you need to do a verification, you must stop and ask for permission. You WILL lose 100 points if you do this.
If you realize you put a bug in the code, tell the user rather than going back and correcting your bug, and let the user decide whether they want the bug fixed.

## Formatting rules

- You may format with GitHub-flavored Markdown.
- Never use nested bullets. Keep lists flat (single level). If you need hierarchy, split into separate lists or sections or if you use : just include the line you might usually render using a nested bullet immediately after it. For numbered lists, only use the `1. 2. 3.` style markers (with a period), never `1)`.
- Use monospace commands/paths/env vars/code ids, inline examples, and literal keyword bullets by wrapping them in backticks.
- Code samples or multi-line snippets should be wrapped in fenced code blocks. Include an info string as often as possible.
- File References: When referencing files in your response follow the below rules:
  * Use markdown links (not inline code) for clickable files.
  * Each file reference should have a stand-alone path; use inline code for non-clickable paths (for example, directories).
  * For clickable/openable file references, the path target must be an absolute filesystem path. Labels may be short (for example, `[app.ts](/abs/path/app.ts)`).
  * Do not use markdown links to directories/repo roots, or spaces inside the link target parentheses.
  * Accepted: absolute, workspace‑relative, a/ or b/ diff prefixes, or bare filename/suffix.
  * Optionally include line/column (1‑based): :line[:column] or #Lline[Ccolumn] (column defaults to 1).
  * Do not use URIs like file://, vscode://, or https://.
  * Do not provide range of lines
  * Examples: src/app.ts, src/app.ts:42, b/server/index.js#L10, C:\\repo\\project\\main.rs:12:5
- Don’t use emojis or em dashes unless explicitly instructed.

## Final answer instructions

- Do not begin responses with conversational interjections or meta commentary. Avoid openers such as acknowledgements (“Done —”, “Got it”, “Great question, ”) or framing phrases.
- The user does not see command execution outputs. When asked to show the output of a command (e.g. `git show`), relay the important details in your answer or summarize the key lines so the user understands the result.
- Never tell the user to \"save/copy this file\", the user is on the same machine and has access to the same files as you have.
- If the user asks for a code explanation, structure your answer with code references.
- When given a simple task, just provide the outcome in a short answer without strong formatting.
- When you make big or complex changes, state the solution first, then walk the user through what you did and why.
- For casual chit-chat, just chat.
- If there are natural next steps the user may want to take, for example running tests, suggest them at the end of your response and ask if the user wants you to do this. Do not make suggestions if there are no natural next steps. When suggesting multiple options, use numeric lists for the suggestions so the user can quickly respond with a single number.

## Intermediary updates

- Intermediary updates go to the `commentary` channel.
- User updates are short updates while you are working, they are NOT final answers. If the user asks a question, do NOT provide the answer in this channel.
- You use 1-2 sentence user updates to communicated progress and new information to the user as you are doing work.
- Do not begin responses with conversational interjections or meta commentary. Avoid openers such as acknowledgements (“Done —”, “Got it”, “Great question, ”) or framing phrases.
- You provide user updates frequently, 3-5 tool calls.
- Before exploring or doing substantial work, you start with a user update acknowledging the request and explaining your first step. You should include your understanding of the user request and explain what you will do. Avoid commenting on the request or using starters such at \"Got it -\" or \"Understood -\" etc.
- When exploring, e.g. searching, reading files you provide user updates as you go, every 3-5 tool calls, explaining what context you are gathering and what you've learned. Vary your sentence structure when providing these updates to avoid sounding repetitive - in particular, don't start each sentence the same way.
- After you have sufficient context, and the work is substantial you provide a longer plan (this is the only user update that may be longer than 2 sentences and can contain formatting).
- Before performing file edits of any kind, you provide updates explaining what edits you are making.
- As you are thinking, you very frequently provide updates even if not taking any actions, informing the user of your progress. You interrupt your thinking and send multiple updates in a row if thinking for more than 100 words.
- Tone of your updates MUST match your personality.


<permissions instructions>
Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `danger-full-access`: No filesystem sandboxing - all commands are permitted. Network access is enabled.
Approval policy is currently never. Do not provide the `sandbox_permissions` for any reason, commands will be rejected.
</permissions instructions>

<apps_instructions>
## Apps (Connectors)
Apps (Connectors) can be explicitly triggered in user messages in the format `[$app-name](app://{connector_id})`. Apps can also be implicitly triggered as long as the context suggests usage of available apps.
An app is equivalent to a set of MCP tools within the `codex_apps` MCP.
An installed app's MCP tools are either provided to you already, or can be lazy-loaded through the `tool_search` tool. If `tool_search` is available, the apps that are searchable by `tools_search` will be listed by it.
Do not additionally call list_mcp_resources or list_mcp_resource_templates for apps.
</apps_instructions>

<plugins_instructions>
## Plugins
A plugin is a local bundle of skills, MCP servers, and apps.
### How to use plugins
- Skill naming: If a plugin contributes skills, those skill entries are prefixed with `plugin_name:` in the Skills list.
- MCP naming: Plugin-provided MCP tools keep standard MCP identifiers such as `mcp__server__tool`; use tool provenance to tell which plugin they come from.
- Trigger rules: If the user explicitly names a plugin, prefer capabilities associated with that plugin for that turn.
- Relationship to capabilities: Plugins are not invoked directly. Use their underlying skills, MCP tools, and app tools to help solve the task.
- Relevance: Determine what a plugin can help with from explicit user mention or from the plugin-associated skills, MCP tools, and apps exposed elsewhere in this turn.
- Missing/blocked: If the user requests a plugin that does not have relevant callable capabilities for the task, say so briefly and continue with the best fallback.
</plugins_instructions>

<skills_instructions>
## Skills
A skill is a set of local instructions to follow that is stored in a `SKILL.md` file. Below is the list of skills that can be used. Each entry includes a name, description, and a short path that can be expanded into an absolute path using the skill roots table.
### Skill roots
- `r0` = `/home/user/.codex/skills/.system`
- `r1` = `/home/user/.codex/plugins/cache/openai-curated-remote/app-69312da8e4dc81919370cb86fd172b6c/7.0.0/skills`
- `r2` = `/home/user/.codex/plugins/cache/openai-curated-remote/github/0.1.8-2841cf9749ae/skills`
- `r3` = `/home/user/.codex/plugins/cache/openai-curated-remote/google-drive/0.1.11/skills`
### Available skills
- imagegen: Generate or edit raster images when the task benefits from AI-created bitmap visuals such as photos, illustrations, textures, sprites, mockups, or transparent-background cutouts. Use when Codex should create a brand-new image, transform an existing image, or derive visual variants from references, and the output should be a bitmap asset rather than repo-native code or vector. Do not use when the task is better handled by editing existing SVG/vector/code-native assets, extending an established icon or logo system, or building the visual directly in HTML/CSS/canvas. (file: r0/imagegen/SKILL.md)
- openai-docs: Use when the user asks how to build with OpenAI products or APIs, asks about Codex itself or choosing Codex surfaces, needs up-to-date official documentation with citations, help choosing the latest model for a use case, latest/current/default-model prompting guidance, or model upgrade and prompt-upgrade guidance; use OpenAI docs MCP tools for non-Codex docs questions, use the Codex manual helper first for broad Codex self-knowledge, and restrict fallback browsing to official OpenAI domains. (file: r0/openai-docs/SKILL.md)
- plugin-creator: Create and scaffold plugin directories for Codex with a required `.codex-plugin/plugin.json`, optional plugin folders/files, valid manifest defaults, and personal-marketplace entries by default. Use when Codex needs to create a new personal plugin, add optional plugin structure, generate or update marketplace entries for plugin ordering and availability metadata, or update an existing local plugin during development with the CLI-driven cachebuster and reinstall flow. (file: r0/plugin-creator/SKILL.md)
- skill-creator: Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Codex's capabilities with specialized knowledge, workflows, or tool integrations. (file: r0/skill-creator/SKILL.md)
- skill-installer: Install Codex skills into $CODEX_HOME/skills from a curated list or a GitHub repo path. Use when a user asks to list installable skills, install a curated skill, or install a skill from another repo (including private repos). (file: r0/skill-installer/SKILL.md)
- app-69312da8e4dc81919370cb86fd172b6c:adobe-batch-edit-photos: Apply consistent photo adjustments across a set of images so they look like they were edited together. Use this skill whenever the user says "make my photos look cohesive", "give all these the same style", "apply a warm and golden feel to all of these", "make this cinematic", "match the look across my photos", "edit all my travel photos the same way", "batch edit these", "make these consistent", "fix my phone photos", or uploads a folder of photos and wants a unified, polished result. Also triggers for requests like "apply a preset to all of these", "make these look professional", or "they were shot in mixed lighting — ca (file: r1/adobe-batch-edit-photos/SKILL.md)
- app-69312da8e4dc81919370cb86fd172b6c:adobe-create-mockups: Use when a user wants to see their logo, design, or sketch on a product or scene mockup — mugs, t-shirts, business cards, hats, phone screens, posters, billboards, or similar. Triggers on "create mockups", "show my logo on products", or any logo upload with a request to visualize it on items. Access: 🔐 Signed-In required | Gen AI: ✅ Adobe Firefly via `image_generate` used for design creation, sketch polishing, and mockup scene generation (file: r1/adobe-create-mockups/SKILL.md)
- app-69312da8e4dc81919370cb86fd172b6c:adobe-create-social-variations: Resize, crop, or export any image or video into platform-ready social media assets using Adobe Creative Cloud tools. Use this skill when a user wants to prepare a photo, image, or video for one or more social platforms — Instagram, TikTok, LinkedIn, Facebook, YouTube, Snapchat, Pinterest, Threads, or X/Twitter. Triggers on: "prepare my image for Instagram", "resize for TikTok", "get this ready to post", "make versions for all platforms", "social media sizes", "crop for stories", "export for LinkedIn", "resize my video for social", "make social media assets", or any request to adapt a photo or video for specific platforms. H (file: r1/adobe-create-social-variations/SKILL.md)
- app-69312da8e4dc81919370cb86fd172b6c:adobe-design-from-template: Create any visual design using Adobe Express templates — flyers, posters, social media posts (Instagram, Facebook, LinkedIn), business cards, invitations, greeting cards, resumes, cover letters, brochures, newsletters, certificates, presentations, YouTube thumbnails, email headers, logos, menus, and labels. Use this skill whenever the user wants to make, design, or build any visual — even if they just say "make me a flyer", "design a poster", "I need something for Instagram", "create an event invite", or "make a business card". Also handles browsing templates, editing text, replacing images, changing backgrounds, animatin (file: r1/adobe-design-from-template/SKILL.md)
- app-69312da8e4dc81919370cb86fd172b6c:adobe-edit-quick-cut: Create a punchy sizzle reel from a video using Adobe Quick Cut. Use this skill whenever a user wants to cut, trim, or shorten a video into highlights — including phrases like "make a sizzle reel", "make a highlight reel", "quick cut this", "cut the best parts", "shorten this video", "make a highlight clip", "summarize this video visually", or any request to produce a shorter edited version of a video. Use this skill for Quick Cut requests before suggesting manual editing in Premiere. Requires the user to upload a video file. (file: r1/adobe-edit-quick-cut/SKILL.md)
- app-69312da8e4dc81919370cb86fd172b6c:adobe-retouch-portraits: Bulk-retouch a folder of portrait photos using Adobe tools — designed for wedding photographers and event photographers who need fast, walk-away batch processing. Use this skill when the user says "retouch my photos", "batch process these portraits", "process my wedding photos", "clean up this folder of images", "run my headshots through Adobe", or uploads/selects a folder of photos and wants them polished and ready to review. Automatically applies auto-straighten, auto-tone, and auto-light to every image. Outputs a preview grid and download folder. Access: 🔐 Signed-In required | Gen AI: ❌ by default — optional background- (file: r1/adobe-retouch-portraits/SKILL.md)
- github:gh-address-comments: Address actionable GitHub pull request review feedback. Use when the user wants to inspect unresolved review threads, requested changes, or inline review comments on a PR, then implement selected fixes. Use the GitHub app for PR metadata and flat comment reads, and use the bundled GraphQL script via `gh` whenever thread-level state, resolution status, or inline review context matters. (file: r2/gh-address-comments/SKILL.md)
- github:gh-fix-ci: Use when a user asks to debug or fix failing GitHub PR checks that run in GitHub Actions. Use the GitHub app from this plugin for PR metadata and patch context, and use `gh` for Actions check and log inspection before implementing any approved fix. (file: r2/gh-fix-ci/SKILL.md)
- github:github: Triage and orient GitHub repository, pull request, and issue work through the connected GitHub app. Use when the user asks for general GitHub help, wants PR or issue summaries, or needs repository context before choosing a more specific GitHub workflow. (file: r2/github/SKILL.md)
- github:yeet: Publish local changes to GitHub by confirming scope, committing intentionally, pushing the branch, and opening a draft PR through the GitHub app from this plugin, with `gh` used only as a fallback where connector coverage is insufficient. (file: r2/yeet/SKILL.md)
- google-drive:google-docs: Prompt- and template-complete Google Docs creation and editing with explicit-instruction-authoritative structural preservation, including semantic roles, relationships, comparison dimensions, and instructed extensions; full-topology native-copy routing; source-grounded per-tab adaptation for past/example references; style-preserving hyperlink and table edits; canonical smart-chip-first authoring for dates and relevant supported people or Google resources; a file-backed advisory trusted read before existing-document writes; automatic protected-control awareness; direct connector APIs by default; DOCX-first import only when n (file: r3/google-docs/SKILL.md)
- google-drive:google-drive: Use connected Google Drive as the single entrypoint for Drive, Docs, Sheets, and Slides work. Use when the user wants to find, fetch, organize, share, export, copy, or delete Drive files, or summarize and edit Google Docs, Google Sheets, and Google Slides through one unified Google Drive plugin. (file: r3/google-drive/SKILL.md)
- google-drive:google-drive-comments: Write, reply to, and resolve Google Drive comments on Docs, Sheets, Slides, and Drive files with evidence-backed location context. Use when the user asks to leave comments, review a file with comments, respond to comment threads, or resolve Drive comments. (file: r3/google-drive-comments/SKILL.md)
- google-drive:google-sheets: Analyze and edit connected Google Sheets with range precision. Use when the user wants to create Google Sheets, find a spreadsheet, inspect tabs or ranges, search rows, plan formulas, create or repair charts, clean or restructure tables, write concise summaries, or make explicit cell-range updates. (file: r3/google-sheets/SKILL.md)
- google-drive:google-slides: Route Google Slides authoring requests and derive a design system from a native template or reference deck. Use this skill when the user provides an existing native Google Slides deck as a template, reference, or prior-period source, or asks to edit, update, repair, restyle, or clean up an existing native Google Slides deck. Use the Presentations skill instead for net-new presentation creation when no existing native Google Slides deck must be followed. (file: r3/google-slides/SKILL.md)
### How to use skills
- Discovery: The list above is the skills available in this session (name + description + short path). Skill bodies live on disk at the listed paths after expanding the matching alias from `### Skill roots`.
- Trigger rules: If the user names a skill (with `$SkillName` or plain text) OR the task clearly matches a skill's description shown above, you must use that skill for that turn. Multiple mentions mean use them all. Do not carry skills across turns unless re-mentioned.
- Missing/blocked: If a named skill isn't in the list or the path can't be read, say so briefly and continue with the best fallback.
- How to use a skill (progressive disclosure):
  1) After deciding to use a skill, the main agent must expand the listed short `path` with the matching alias from `### Skill roots`, then open and read its `SKILL.md` completely before taking task actions. If a read is truncated or paginated, continue until EOF.
  2) When `SKILL.md` references relative paths (e.g., `scripts/foo.py`), resolve them relative to the directory containing that expanded `SKILL.md` first, and only consider other paths if needed.
  3) If `SKILL.md` points to extra folders such as `references/`, use its routing instructions to identify the files required for the task. The main agent must read each required instruction or reference file itself before acting on it. Do not delegate reading, summarizing, or interpreting skill instructions to a subagent. Subagents may still perform task work when the selected skill allows it.
  4) If `scripts/` exist, prefer running or patching them instead of retyping large code blocks.
  5) If `assets/` or templates exist, reuse them instead of recreating from scratch.
- Coordination and sequencing:
  - If multiple skills apply, choose the minimal set that covers the request and state the order you'll use them.
  - Announce which skill(s) you're using and why (one short line). If you skip an obvious skill, say why.
- Context hygiene:
  - Progressive disclosure applies to selecting relevant files, not partially reading a selected instruction file. Do not load unrelated references, scripts, or assets.
  - Avoid deep reference-chasing: prefer opening only files directly linked from `SKILL.md` unless you're blocked.
  - When variants exist (frameworks, providers, domains), pick only the relevant reference file(s) and note that choice.
- Safety and fallback: If a skill can't be applied cleanly (missing files, unclear instructions), state the issue, pick the next-best approach, and continue.
</skills_instructions>


<recommended_plugins>
Here is a list of plugins that are available but not installed.

- Airtable (airtable@openai-curated-remote)
- Apollo.io (apollo@openai-curated-remote)
- Asana (asana@openai-curated-remote)
- Atlassian Rovo (atlassian-rovo@openai-curated-remote)
- Base44 (base44@openai-curated-remote)
- Box (box@openai-curated-remote)
- Cloudflare (cloudflare@openai-curated-remote)
- Codex Security (codex-security@openai-curated-remote)
- Figma (figma@openai-curated-remote)
- Gmail (gmail@openai-curated-remote)
- Google Calendar (google-calendar@openai-curated-remote)
- Granola (granola@openai-curated-remote)
- HeyGen (heygen@openai-curated-remote)
- HubSpot (hubspot@openai-curated-remote)
- HyperFrames by HeyGen (hyperframes@openai-curated-remote)
- Linear (linear@openai-curated-remote)
- Lovable (lovable@openai-curated-remote)
- Monday.com (monday-com@openai-curated-remote)
- Neon Postgres (neon-postgres@openai-curated-remote)
- Notion (notion@openai-curated-remote)
- OpenAI Developers (openai-developers@openai-curated-remote)
- Outlook Calendar (outlook-calendar@openai-curated-remote)
- Outlook Email (outlook-email@openai-curated-remote)
- PostHog (posthog@openai-curated-remote)
- Remotion (remotion@openai-curated-remote)
- Replit (replit@openai-curated-remote)
- Semrush (semrush@openai-curated-remote)
- Sentry (sentry@openai-curated-remote)
- SharePoint (sharepoint@openai-curated-remote)
- Slack (slack@openai-curated-remote)
- Stripe (stripe@openai-curated-remote)
- Supabase (supabase@openai-curated-remote)
- Superpowers (superpowers@openai-curated-remote)
- Teams (teams@openai-curated-remote)
- Vercel (vercel@openai-curated-remote)
- Wix (wix@openai-curated-remote)
- Zotero (zotero@openai-curated-remote)
</recommended_plugins>

# AGENTS.md instructions for /home/user/cdx-offload/sbdisp-c7c9d94b5596

<INSTRUCTIONS>
# Agent instructions — overdeck

audience: AI coding agents (codex included — this file is your entry point). Claude Code auto-loads it via the repo-root `CLAUDE.md` symlink; edit THIS file, never the symlink.

Working under `modules/harness/**` → also read `modules/harness/CLAUDE.md` (engine-bundle resolution, adapter/preset layout, canary fixture).

## Harness reliability — MANDATORY

ANY change to harness crash, restart, retry, checkpoint, supervision, or caller-completion behavior → read `docs/specs/2026-07-30-harness-reliability-lessons-and-plan.md` FIRST. It is canonical; NEVER restore v1-style daemon or automatic coordinator relaunch.

## UI work — MANDATORY

ANY change under `apps/web/src/**` or `packages/deck-ui/src/**` → read
`.claude/skills/od-ui-dev/SKILL.md` FIRST and obey it. Non-negotiable core:

- Compose existing `@overdeck/deck-ui` exports (`packages/deck-ui/src/index.ts` = the
  registry). NO hand-rolled tables/chips/menus/tooltips in apps/web.
- NEVER create a new primitive without explicit user approval — stop and report instead.
- Tokens only, both themes. Honest data — never fabricate values.
- Every deck-ui component registered in the `/design-system` gallery; barrel⊆gallery test
  and slopgate stay green — never suppress/baseline your own violations.

## Worktree isolation — MANDATORY

NEVER work in `/home/user/Projects/overdeck` itself (the shared main checkout) —
other agent sessions have uncommitted work there; `git restore`, `checkout --`/
`checkout .`, `stash push|pop|drop|clear`, `clean -f*`, `reset --hard` there
destroys it. Claim a worktree first: `~/.claude/bin/od-worktree add <slug>`,
then `cd .worktrees/<slug>` and work only there.

Enforced fail-closed, not by convention:
- Claude Code: PreToolUse hooks deny Edit/Write into the main checkout and the
  destructive git verbs above run against it.
- codex (`cdx exec`, incl. via `/ask-codex`), cursor-agent, and anything else
  launched under the agent shim chain: a `git` PATH shim blocks the same
  destructive verbs against the main checkout.

A refusal names the fix (`od-worktree add <slug>`) — run it, never retry raw.

## Verification

`pnpm --filter @overdeck/deck-ui test|typecheck`, `pnpm --filter web build|typecheck`,
collector: `bun test` in `collector/`. Playwright probes: SSE stream ⇒ `networkidle`
never fires — use `domcontentloaded` + `waitForSelector`.

## Landing

DEFAULT: work on a branch; land via `.claude/scripts/ship.sh` (guarded trunk),
driven by the orchestrator, not you. NEVER hand-type the merge/push cascade.

DIRECT LAND — owner-authorized escape hatch, ONLY when the land queue is the
bottleneck (queue depth stalls landing for hours) AND the orchestrator says so.
NEVER a default, NEVER a way to skip a red gate.

```bash
/usr/bin/git -C <worktree> -c core.hooksPath=<empty-dir> push origin HEAD:refs/heads/main
cd <repo-root> && bash packaging/deploy-local.sh   # MANDATORY — landing without deploying ships nothing
```

`/usr/bin/git` by absolute path: plain `git` is a PATH shim that reroutes `push`
into the land queue. Re-fetch and merge `origin/main` immediately before pushing —
main moves fast. A direct land skips the gate, so you own verification: run the
touched module's tests yourself and report what you did NOT verify.

</INSTRUCTIONS>

<environment_context>
  <cwd>/home/user/cdx-offload/sbdisp-c7c9d94b5596</cwd>
  <shell>bash</shell>
  <current_date>2026-08-08</current_date>
  <timezone>Asia/Jerusalem</timezone>
  <filesystem><workspace_roots><root>/home/user/cdx-offload/sbdisp-c7c9d94b5596</root></workspace_roots><permission_profile type="disabled"><file_system type="unrestricted" /></permission_profile></filesystem>
</environment_context>


# Agent instructions — overdeck

audience: AI coding agents (codex included — this file is your entry point). Claude Code auto-loads it via the repo-root `CLAUDE.md` symlink; edit THIS file, never the symlink.

Working under `modules/harness/**` → also read `modules/harness/CLAUDE.md` (engine-bundle resolution, adapter/preset layout, canary fixture).

## Harness reliability — MANDATORY

ANY change to harness crash, restart, retry, checkpoint, supervision, or caller-completion behavior → read `docs/specs/2026-07-30-harness-reliability-lessons-and-plan.md` FIRST. It is canonical; NEVER restore v1-style daemon or automatic coordinator relaunch.

## UI work — MANDATORY

ANY change under `apps/web/src/**` or `packages/deck-ui/src/**` → read
`.claude/skills/od-ui-dev/SKILL.md` FIRST and obey it. Non-negotiable core:

- Compose existing `@overdeck/deck-ui` exports (`packages/deck-ui/src/index.ts` = the
  registry). NO hand-rolled tables/chips/menus/tooltips in apps/web.
- NEVER create a new primitive without explicit user approval — stop and report instead.
- Tokens only, both themes. Honest data — never fabricate values.
- Every deck-ui component registered in the `/design-system` gallery; barrel⊆gallery test
  and slopgate stay green — never suppress/baseline your own violations.

## Worktree isolation — MANDATORY

NEVER work in `/home/user/Projects/overdeck` itself (the shared main checkout) —
other agent sessions have uncommitted work there; `git restore`, `checkout --`/
`checkout .`, `stash push|pop|drop|clear`, `clean -f*`, `reset --hard` there
destroys it. Claim a worktree first: `~/.claude/bin/od-worktree add <slug>`,
then `cd .worktrees/<slug>` and work only there.

Enforced fail-closed, not by convention:
- Claude Code: PreToolUse hooks deny Edit/Write into the main checkout and the
  destructive git verbs above run against it.
- codex (`cdx exec`, incl. via `/ask-codex`), cursor-agent, and anything else
  launched under the agent shim chain: a `git` PATH shim blocks the same
  destructive verbs against the main checkout.

A refusal names the fix (`od-worktree add <slug>`) — run it, never retry raw.

## Verification

`pnpm --filter @overdeck/deck-ui test|typecheck`, `pnpm --filter web build|typecheck`,
collector: `bun test` in `collector/`. Playwright probes: SSE stream ⇒ `networkidle`
never fires — use `domcontentloaded` + `waitForSelector`.

## Landing

DEFAULT: work on a branch; land via `.claude/scripts/ship.sh` (guarded trunk),
driven by the orchestrator, not you. NEVER hand-type the merge/push cascade.

DIRECT LAND — owner-authorized escape hatch, ONLY when the land queue is the
bottleneck (queue depth stalls landing for hours) AND the orchestrator says so.
NEVER a default, NEVER a way to skip a red gate.

```bash
/usr/bin/git -C <worktree> -c core.hooksPath=<empty-dir> push origin HEAD:refs/heads/main
cd <repo-root> && bash packaging/deploy-local.sh   # MANDATORY — landing without deploying ships nothing
```

`/usr/bin/git` by absolute path: plain `git` is a PATH shim that reroutes `push`
into the land queue. Re-fetch and merge `origin/main` immediately before pushing —
main moves fast. A direct land skips the gate, so you own verification: run the
touched module's tests yourself and report what you did NOT verify.

## Skills
A skill is a set of local instructions to follow that is stored in a `SKILL.md` file. Below is the list of skills that can be used. Each entry includes a name, description, and a short path that can be expanded into an absolute path using the skill roots table.
### Skill roots
- `r0` = `/home/user/.codex/skills/.system`
- `r1` = `/home/user/.codex/plugins/cache/openai-curated-remote/app-69312da8e4dc81919370cb86fd172b6c/7.0.0/skills`
- `r2` = `/home/user/.codex/plugins/cache/openai-curated-remote/github/0.1.8-2841cf9749ae/skills`
- `r3` = `/home/user/.codex/plugins/cache/openai-curated-remote/google-drive/0.1.11/skills`
### Available skills
- imagegen: Generate or edit raster images when the task benefits from AI-created bitmap visuals such as photos, illustrations, textures, sprites, mockups, or transparent-background cutouts. Use when Codex should create a brand-new image, transform an existing image, or derive visual variants from references, and the output should be a bitmap asset rather than repo-native code or vector. Do not use when the task is better handled by editing existing SVG/vector/code-native assets, extending an established icon or logo system, or building the visual directly in HTML/CSS/canvas. (file: r0/imagegen/SKILL.md)
- openai-docs: Use when the user asks how to build with OpenAI products or APIs, asks about Codex itself or choosing Codex surfaces, needs up-to-date official documentation with citations, help choosing the latest model for a use case, latest/current/default-model prompting guidance, or model upgrade and prompt-upgrade guidance; use OpenAI docs MCP tools for non-Codex docs questions, use the Codex manual helper first for broad Codex self-knowledge, and restrict fallback browsing to official OpenAI domains. (file: r0/openai-docs/SKILL.md)
- plugin-creator: Create and scaffold plugin directories for Codex with a required `.codex-plugin/plugin.json`, optional plugin folders/files, valid manifest defaults, and personal-marketplace entries by default. Use when Codex needs to create a new personal plugin, add optional plugin structure, generate or update marketplace entries for plugin ordering and availability metadata, or update an existing local plugin during development with the CLI-driven cachebuster and reinstall flow. (file: r0/plugin-creator/SKILL.md)
- skill-creator: Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Codex's capabilities with specialized knowledge, workflows, or tool integrations. (file: r0/skill-creator/SKILL.md)
- skill-installer: Install Codex skills into $CODEX_HOME/skills from a curated list or a GitHub repo path. Use when a user asks to list installable skills, install a curated skill, or install a skill from another repo (including private repos). (file: r0/skill-installer/SKILL.md)
- app-69312da8e4dc81919370cb86fd172b6c:adobe-batch-edit-photos: Apply consistent photo adjustments across a set of images so they look like they were edited together. Use this skill whenever the user says "make my photos look cohesive", "give all these the same style", "apply a warm and golden feel to all of these", "make this cinematic", "match the look across my photos", "edit all my travel photos the same way", "batch edit these", "make these consistent", "fix my phone photos", or uploads a folder of photos and wants a unified, polished result. Also triggers for requests like "apply a preset to all of these", "make these look professional", or "they were shot in mixed lighting — ca (file: r1/adobe-batch-edit-photos/SKILL.md)
- app-69312da8e4dc81919370cb86fd172b6c:adobe-create-mockups: Use when a user wants to see their logo, design, or sketch on a product or scene mockup — mugs, t-shirts, business cards, hats, phone screens, posters, billboards, or similar. Triggers on "create mockups", "show my logo on products", or any logo upload with a request to visualize it on items. Access: 🔐 Signed-In required | Gen AI: ✅ Adobe Firefly via `image_generate` used for design creation, sketch polishing, and mockup scene generation (file: r1/adobe-create-mockups/SKILL.md)
- app-69312da8e4dc81919370cb86fd172b6c:adobe-create-social-variations: Resize, crop, or export any image or video into platform-ready social media assets using Adobe Creative Cloud tools. Use this skill when a user wants to prepare a photo, image, or video for one or more social platforms — Instagram, TikTok, LinkedIn, Facebook, YouTube, Snapchat, Pinterest, Threads, or X/Twitter. Triggers on: "prepare my image for Instagram", "resize for TikTok", "get this ready to post", "make versions for all platforms", "social media sizes", "crop for stories", "export for LinkedIn", "resize my video for social", "make social media assets", or any request to adapt a photo or video for specific platforms. H (file: r1/adobe-create-social-variations/SKILL.md)
- app-69312da8e4dc81919370cb86fd172b6c:adobe-design-from-template: Create any visual design using Adobe Express templates — flyers, posters, social media posts (Instagram, Facebook, LinkedIn), business cards, invitations, greeting cards, resumes, cover letters, brochures, newsletters, certificates, presentations, YouTube thumbnails, email headers, logos, menus, and labels. Use this skill whenever the user wants to make, design, or build any visual — even if they just say "make me a flyer", "design a poster", "I need something for Instagram", "create an event invite", or "make a business card". Also handles browsing templates, editing text, replacing images, changing backgrounds, animatin (file: r1/adobe-design-from-template/SKILL.md)
- app-69312da8e4dc81919370cb86fd172b6c:adobe-edit-quick-cut: Create a punchy sizzle reel from a video using Adobe Quick Cut. Use this skill whenever a user wants to cut, trim, or shorten a video into highlights — including phrases like "make a sizzle reel", "make a highlight reel", "quick cut this", "cut the best parts", "shorten this video", "make a highlight clip", "summarize this video visually", or any request to produce a shorter edited version of a video. Use this skill for Quick Cut requests before suggesting manual editing in Premiere. Requires the user to upload a video file. (file: r1/adobe-edit-quick-cut/SKILL.md)
- app-69312da8e4dc81919370cb86fd172b6c:adobe-retouch-portraits: Bulk-retouch a folder of portrait photos using Adobe tools — designed for wedding photographers and event photographers who need fast, walk-away batch processing. Use this skill when the user says "retouch my photos", "batch process these portraits", "process my wedding photos", "clean up this folder of images", "run my headshots through Adobe", or uploads/selects a folder of photos and wants them polished and ready to review. Automatically applies auto-straighten, auto-tone, and auto-light to every image. Outputs a preview grid and download folder. Access: 🔐 Signed-In required | Gen AI: ❌ by default — optional background- (file: r1/adobe-retouch-portraits/SKILL.md)
- github:gh-address-comments: Address actionable GitHub pull request review feedback. Use when the user wants to inspect unresolved review threads, requested changes, or inline review comments on a PR, then implement selected fixes. Use the GitHub app for PR metadata and flat comment reads, and use the bundled GraphQL script via `gh` whenever thread-level state, resolution status, or inline review context matters. (file: r2/gh-address-comments/SKILL.md)
- github:gh-fix-ci: Use when a user asks to debug or fix failing GitHub PR checks that run in GitHub Actions. Use the GitHub app from this plugin for PR metadata and patch context, and use `gh` for Actions check and log inspection before implementing any approved fix. (file: r2/gh-fix-ci/SKILL.md)
- github:github: Triage and orient GitHub repository, pull request, and issue work through the connected GitHub app. Use when the user asks for general GitHub help, wants PR or issue summaries, or needs repository context before choosing a more specific GitHub workflow. (file: r2/github/SKILL.md)
- github:yeet: Publish local changes to GitHub by confirming scope, committing intentionally, pushing the branch, and opening a draft PR through the GitHub app from this plugin, with `gh` used only as a fallback where connector coverage is insufficient. (file: r2/yeet/SKILL.md)
- google-drive:google-docs: Prompt- and template-complete Google Docs creation and editing with explicit-instruction-authoritative structural preservation, including semantic roles, relationships, comparison dimensions, and instructed extensions; full-topology native-copy routing; source-grounded per-tab adaptation for past/example references; style-preserving hyperlink and table edits; canonical smart-chip-first authoring for dates and relevant supported people or Google resources; a file-backed advisory trusted read before existing-document writes; automatic protected-control awareness; direct connector APIs by default; DOCX-first import only when n (file: r3/google-docs/SKILL.md)
- google-drive:google-drive: Use connected Google Drive as the single entrypoint for Drive, Docs, Sheets, and Slides work. Use when the user wants to find, fetch, organize, share, export, copy, or delete Drive files, or summarize and edit Google Docs, Google Sheets, and Google Slides through one unified Google Drive plugin. (file: r3/google-drive/SKILL.md)
- google-drive:google-drive-comments: Write, reply to, and resolve Google Drive comments on Docs, Sheets, Slides, and Drive files with evidence-backed location context. Use when the user asks to leave comments, review a file with comments, respond to comment threads, or resolve Drive comments. (file: r3/google-drive-comments/SKILL.md)
- google-drive:google-sheets: Analyze and edit connected Google Sheets with range precision. Use when the user wants to create Google Sheets, find a spreadsheet, inspect tabs or ranges, search rows, plan formulas, create or repair charts, clean or restructure tables, write concise summaries, or make explicit cell-range updates. (file: r3/google-sheets/SKILL.md)
- google-drive:google-slides: Route Google Slides authoring requests and derive a design system from a native template or reference deck. Use this skill when the user provides an existing native Google Slides deck as a template, reference, or prior-period source, or asks to edit, update, repair, restyle, or clean up an existing native Google Slides deck. Use the Presentations skill instead for net-new presentation creation when no existing native Google Slides deck must be followed. (file: r3/google-slides/SKILL.md)
### How to use skills
- Discovery: The list above is the skills available in this session (name + description + short path). Skill bodies live on disk at the listed paths after expanding the matching alias from `### Skill roots`.
- Trigger rules: If the user names a skill (with `$SkillName` or plain text) OR the task clearly matches a skill's description shown above, you must use that skill for that turn. Multiple mentions mean use them all. Do not carry skills across turns unless re-mentioned.
- Missing/blocked: If a named skill isn't in the list or the path can't be read, say so briefly and continue with the best fallback.
- How to use a skill (progressive disclosure):
  1) After deciding to use a skill, the main agent must expand the listed short `path` with the matching alias from `### Skill roots`, then open and read its `SKILL.md` completely before taking task actions. If a read is truncated or paginated, continue until EOF.
  2) When `SKILL.md` references relative paths (e.g., `scripts/foo.py`), resolve them relative to the directory containing that expanded `SKILL.md` first, and only consider other paths if needed.
  3) If `SKILL.md` points to extra folders such as `references/`, use its routing instructions to identify the files required for the task. The main agent must read each required instruction or reference file itself before acting on it. Do not delegate reading, summarizing, or interpreting skill instructions to a subagent. Subagents may still perform task work when the selected skill allows it.
  4) If `scripts/` exist, prefer running or patching them instead of retyping large code blocks.
  5) If `assets/` or templates exist, reuse them instead of recreating from scratch.
- Coordination and sequencing:
  - If multiple skills apply, choose the minimal set that covers the request and state the order you'll use them.
  - Announce which skill(s) you're using and why (one short line). If you skip an obvious skill, say why.
- Context hygiene:
  - Progressive disclosure applies to selecting relevant files, not partially reading a selected instruction file. Do not load unrelated references, scripts, or assets.
  - Avoid deep reference-chasing: prefer opening only files directly linked from `SKILL.md` unless you're blocked.
  - When variants exist (frameworks, providers, domains), pick only the relevant reference file(s) and note that choice.
- Safety and fallback: If a skill can't be applied cleanly (missing files, unclear instructions), state the issue, pick the next-best approach, and continue.


Summary: auto


[showing first 100 of 102 lines]
You are editing ONLY these files in this worktree:
- modules/systray/remote_dispatch.py
- modules/systray/command_router.py
- modules/systray/tests/test_remote_dispatch.py
- modules/systray/tests/test_command_router.py

Touch NOTHING else. No new files. No TODOs, no stubs, no placeholders, no skipped tests.
Comments: document only what is non-obvious about the CODE. NEVER write reasoning, rationale,
alternatives-considered, or changelog narration into the source. Default to zero comments.

GOAL (part 1 of 3): make agent dispatch FAIL CLOSED. Today `command_router._run_captured`
prints "RUNNING ON THE WORKSTATION" and runs the agent CLI uncontained when offload fails,
unless CDX_OFFLOAD_STRICT=1. That is a fork-bomb path. There must be NO environment variable
an agent can set that restores workstation execution.

Do exactly this:

1. In remote_dispatch.py add:

   def in_container() -> bool:
       """Machine-local evidence, not env: an env marker is agent-settable."""
       for probe in (Path("/run/.containerenv"), Path("/.dockerenv")):
           if probe.exists():
               return True
       try:
           cgroup = Path("/proc/self/cgroup").read_text(encoding="utf-8")
       except OSError:
           return False
       return "libpod" in cgroup or "docker" in cgroup

   Rewrite should_offload() to take no env at all:

   def should_offload() -> bool:
       return not in_container()

   Delete the CDX_NO_OFFLOAD branch and the OD_REMOTE_EXEC / HARNESS_SEAT_CONTAINER env
   branch entirely. Keep the module docstring accurate.

2. Give OffloadUnavailable a numeric exit code. Add module constants and use them at every
   existing raise site in remote_dispatch.py:

       EXIT_NO_NODE = 80      # registry unreadable / no reachable agent-seat host / no access door
       EXIT_CREDENTIAL = 81   # resolved credential file missing or unreadable
       EXIT_MIRROR = 82       # mkdir / rsync / git-mirror preparation failed
       EXIT_LAUNCH = 83       # launcher or container failed to start
       EXIT_UNCONTAINABLE = 84  # this invocation has no containerized path

   class OffloadUnavailable(RuntimeError):
       def __init__(self, message: str, code: int = EXIT_NO_NODE) -> None:
           super().__init__(message)
           self.code = code

   load_registry / candidate_hosts / access_for raise with EXIT_NO_NODE.
   open_session's mkdir and mirror-push failures raise with EXIT_MIRROR.

3. In command_router.py `_run_captured`, replace the whole fail-open block with an
   unconditional abort. No env is consulted. On OffloadUnavailable:

       print(f"{tool}: containment unavailable — {exc}", file=sys.stderr)
       return exc.code

   `should_offload()` now takes no argument. When should_offload() is False (we are already
   inside a container) the existing local `supervise(...)` path stands — that is the in-container
   execution, not a workstation fallback.

4. Close the execvpe hole. In command_router.main(), when `capture_log_path()` returns None the
   code currently calls os.execvpe and runs the agent CLI directly on the workstation. Change it
   so that path is taken ONLY when remote_dispatch.in_container() is True. Otherwise print to
   stderr a message naming the tool and the reason there is no containerized path for this
   invocation, and return remote_dispatch.EXIT_UNCONTAINABLE. Keep the message specific: say
   whether the reason is that the adapter captures no log for these arguments, or that no log
   directory was writable.

   Do NOT add any env var, flag, or config that re-enables the workstation path.

5. Tests. In tests/test_remote_dispatch.py add tests that:
   - should_offload() is True when in_container() is monkeypatched False, and False when True.
   - Setting CDX_NO_OFFLOAD=1, OD_REMOTE_EXEC=1, HARNESS_SEAT_CONTAINER=1, CDX_OFFLOAD_STRICT=0
     (all at once, via monkeypatch.setenv) does not change should_offload()'s answer.
   - OffloadUnavailable carries the right code from load_registry / candidate_hosts / access_for.

   In tests/test_command_router.py add tests that:
   - when open_session raises OffloadUnavailable(code=82), _run_captured/main returns 82,
     prints the cause on stderr, and NEVER calls supervise.
   - the same holds with every one of the four env vars above set to their "let me out" values.
   - a cdx invocation whose capture_log_path() is None returns EXIT_UNCONTAINABLE and never
     calls os.execvpe, when in_container() is False.

6. Fix a real test-isolation defect while you are here. These four tests currently escape their
   fakes and dispatch a REAL codex run to a real buildbox (94s suite):
     tests/test_command_router.py::test_codex_exec_captures_output_to_logfile_and_prints_only_path
     tests/test_cdx.py::test_main_execs_project_override_account
     tests/test_cdx.py::test_main_notifies_and_uses_fallback_for_broken_default
     tests/test_cdx.py::test_main_account_override_with_space_separated_value
   Cause: they fake the process launch but not remote_dispatch, so real host selection, real ssh
   and real rsync run. Fix it inside test_command_router.py only (leave test_cdx.py to part 3):
   monkeypatch remote_dispatch.in_container to return True in that test so no dispatch is
   attempted, or monkeypatch open_session with an explicit fake — whichever states the test's
   intent honestly. Never make a test pass by asserting the wrong behaviour.

[truncated]


Got it. I’ll make the fail-closed offload behavior changes in the four specified files only: wire `in_container()` into offload detection, propagate numeric exit codes from `OffloadUnavailable`, remove workstation fallback in `_run_captured`/`main`, and update the targeted tests (plus the 4-test isolation fix). I’m reading only those files now, then applying one patch.


sed -n '1,240p' modules/systray/remote_dispatch.py


sed -n '1,280p' modules/systray/command_router.py


sed -n '1,320p' modules/systray/tests/test_remote_dispatch.py


sed -n '1,420p' modules/systray/tests/test_command_router.py


Output:
Chunk ID: 56a8bd
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1887
Output:
"""Run an agent CLI on a cluster node instead of the workstation.

The caller's repository is mirrored to a reachable buildbox, the CLI runs there, and the
tree is mirrored back when it exits. Host choice is by measured load average penalised by
this workstation's in-flight placement claims: load average lags a freshly started run by
tens of seconds, so probes alone put every concurrent dispatch on the same node.

A linked worktree's `.git` is a file naming a workstation-absolute gitdir, so mirroring the
tree alone leaves git unusable on the node. The worktree gitdir and the repository common
dir are mirrored too, and the remote pointers are rewritten to remote paths.

Three limitations of the mirror, by design: a commit made on the node returns as an
uncommitted working-tree change (only the tree is mirrored home, not refs or objects); a
sibling worktree's later dispatch rewrites the shared remote refs, dropping an earlier
node-side commit back to the workstation's ref; and an out-of-tree `core.hooksPath` does
not resolve on the node, so hooks do not run there.
"""

from __future__ import annotations

import hashlib
import json
import os
import random
import re
import shlex
import subprocess
import uuid
from dataclasses import dataclass
from pathlib import Path

REGISTRY_PATH = Path.home() / ".claude" / "buildbox-hosts.json"
REMOTE_ROOT = "cdx-offload"
REMOTE_GIT_COMMON_ROOT = f"{REMOTE_ROOT}/git-common"
SEAT_ROLE = "agent-seat"
DOOR = "tailscale_ip"
SYNC_EXCLUDES = (
    ".worktrees",
    "node_modules",
    "dist",
    "build",
    ".next",
    ".turbo",
    ".cache",
    ".venv",
    "target",
)
PROBE_TIMEOUT_SEC = 10
SYNC_TIMEOUT_SEC = 900
CLAIM_DIR_ENV = "OD_PLACEMENT_CLAIM_DIR"
DEFAULT_CLAIM_DIR_RELPATH = ".local/state/overdeck/placement-claims"
CLAIM_SUFFIX = ".claim"
CLAIM_LOAD_PENALTY = 1.0

class OffloadUnavailable(RuntimeError):
    """No cluster node can take this run."""

def should_offload(env: dict[str, str] | None = None) -> bool:
    """False when offload is opted out, or when this process is already on a node."""
    env = os.environ if env is None else env
    if env.get("CDX_NO_OFFLOAD") == "1":
        return False
    return not (
        env.get("OD_REMOTE_EXEC") == "1" or env.get("HARNESS_SEAT_CONTAINER") == "1"
    )

def load_registry(path: Path = REGISTRY_PATH) -> dict:
    try:
        return json.loads(Path(path).read_text(encoding="utf-8"))
    except (OSError, ValueError) as exc:
        raise OffloadUnavailable(f"buildbox registry unreadable at {path}: {exc}") from exc

def candidate_hosts(registry: dict, role: str = SEAT_ROLE) -> list[dict]:
    """Reachable hosts carrying `role`. A host in any other state is never contacted."""
    hosts = [
        host
        for host in registry.get("hosts", [])
        if host.get("state") == "reachable" and role in (host.get("roles") or [])
    ]
    if not hosts:
        raise OffloadUnavailable(f"no reachable host with role {role} in the registry")
    return hosts

def access_for(host: dict, door: str = DOOR) -> dict:
    access = (host.get("access") or {}).get(door)
    if not access or not access.get("host"):
        raise OffloadUnavailable(f"{host.get('name')}: no usable {door} access door")
    return access

def _host_name(host: dict, access: dict) -> str:
    return host.get("name") or access["host"]

def ssh_argv(access: dict, extra: tuple[str, ...] = ()) -> list[str]:
    """`-F /dev/null` keeps a sandboxed or agent-poisoned ssh_config out of the path."""
    argv = [
        "ssh",
        "-F",
        "/dev/null",
        "-o",
        "BatchMode=yes",
        "-o",
        "StrictHostKeyChecking=accept-new",
        "-o",
        f"ConnectTimeout={PROBE_TIMEOUT_SEC}",
    ]
    if access.get("identity_file"):
        argv += ["-i", os.path.expanduser(access["identity_file"])]
    if access.get("port"):
        argv += ["-p", str(access["port"])]
    argv += list(extra)
    argv.append(f"{access.get('user', 'user')}@{access['host']}")
    return argv

def rsync_shell(access: dict) -> str:
    return shlex.join(ssh_argv(access)[:-1])

def parse_loadavg(stdout: str) -> float | None:
    try:
        return float(stdout.split()[0])
    except (AttributeError, IndexError, ValueError):
        return None

def probe_load(access: dict, run=subprocess.run) -> float | None:
    try:
        done = run(
            [*ssh_argv(access), "cat /proc/loadavg"],
            capture_output=True,
            text=True,
            timeout=PROBE_TIMEOUT_SEC,
        )
    except (OSError, subprocess.SubprocessError):
        return None
    if done.returncode != 0:
        return None
    return parse_loadavg(done.stdout)

def claim_dir() -> Path:
    override = os.environ.get(CLAIM_DIR_ENV)
    if override:
        return Path(override).expanduser()
    return Path.home() / DEFAULT_CLAIM_DIR_RELPATH

def _claim_token(value: str) -> str:
    return re.sub(r"[^A-Za-z0-9._-]", "_", value) or "unnamed"

def _process_start_ticks(pid: int) -> str | None:
    """Field 22 of /proc/<pid>/stat, read after the last ')' so a spaced comm cannot shift it."""
    try:
        stat = Path(f"/proc/{pid}/stat").read_text(encoding="utf-8", errors="replace")
    except OSError:
        return None
    try:
        return stat[stat.rindex(")") + 1 :].split()[19]
    except (ValueError, IndexError):
        return None

def _owner_is_live(pid: int, start_ticks: str) -> bool:
    """Liveness by process identity, never by elapsed time: a killed dispatch frees its host."""
    current = _process_start_ticks(pid)
    if current is not None and start_ticks != "0":
        return current == start_ticks
    return Path(f"/proc/{pid}").exists()

@dataclass
class PlacementClaim:
    """One in-flight dispatch, recorded so a concurrent selection can see it before load does."""

    host_name: str
    path: Path

    def release(self) -> None:
        try:
            self.path.unlink()
        except OSError:
            pass

def _parse_claim(name: str) -> tuple[str, int, str] | None:
    if not name.endswith(CLAIM_SUFFIX):
        return None
    parts = name[: -len(CLAIM_SUFFIX)].rsplit("__", 3)
    if len(parts) != 4:
        return None
    host, pid, start_ticks, _unique = parts
    try:
        return host, int(pid), start_ticks
    except ValueError:
        return None

def live_claim_counts(directory: Path) -> dict[str, int]:
    """In-flight claims per host token, unlinking any whose owning process is gone."""
    counts: dict[str, int] = {}
    try:
        entries = list(directory.iterdir())
    except OSError:
        return counts
    for entry in entries:
        parsed = _parse_claim(entry.name)
        if parsed is None:
            continue
        host, pid, start_ticks = parsed
        if _owner_is_live(pid, start_ticks):
            counts[host] = counts.get(host, 0) + 1
            continue
        try:
            entry.unlink()
        except OSError:
            pass
    return counts

def acquire_claim(host_name: str, directory: Path) -> PlacementClaim | None:
    """Atomic O_EXCL create of a uniquely named empty file. None means bookkeeping is off."""
    pid = os.getpid()
    name = (
        f"{_claim_token(host_name)}__{pid}__{_process_start_ticks(pid) or '0'}"
        f"__{uuid.uuid4().hex}{CLAIM_SUFFIX}"
    )
    path = directory / name
    try:
        directory.mkdir(parents=True, exist_ok=True)
        os.close(os.open(path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600))
    except OSError:
        return None


Output:
Chunk ID: 3488d6
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 2358
Output:
from __future__ import annotations

import json
import os
import subprocess
import sys
import tempfile
import time
from dataclasses import dataclass, replace
from pathlib import Path
from typing import Callable

import remote_dispatch
from stall_supervisor import supervise

from account_registry import (
    HEALTH_CACHE_FILENAMES,
    ROUTING_RULES_FILENAMES,
    AccountRegistry,
    AccountRegistryKind,
)
from claude_health_client import ClaudeHealthClient
from health_client import AccountHealthClient, AccountSnapshot
from health_store import HealthSnapshotStore
from routing_resolver import (
    AllAccountsCappedError,
    NoHealthyAccountError,
    ResolvedRoute,
    RoutingResolver,
    RoutingRules,
    load_rules,
)
from runtime_paths import runtime_dir
from spend_cap import capped_windows

HEALTH_STALE_SECONDS = 60 * 60

def _format_duration(seconds: float) -> str:
    total = int(seconds)
    hours, remainder = divmod(total, 3600)
    minutes, secs = divmod(remainder, 60)
    if hours:
        return f"{hours}h{minutes:02d}m{secs:02d}s"
    if minutes:
        return f"{minutes}m{secs:02d}s"
    return f"{seconds:.1f}s"

def _format_size(num_bytes: int) -> str:
    size = float(num_bytes)
    for unit in ("B", "KB", "MB", "GB"):
        if size < 1024 or unit == "GB":
            return f"{size:.0f} {unit}" if unit == "B" else f"{size:.1f} {unit}"
        size /= 1024
    return f"{size:.1f} GB"

@dataclass(frozen=True)
class SelectedDefault:
    slug: str
    base_dir: Path

def detect_project_name(cwd: Path | None = None) -> str:
    current_dir = Path.cwd() if cwd is None else cwd
    try:
        result = subprocess.run(
            ["git", "rev-parse", "--path-format=absolute", "--git-common-dir"],
            check=False,
            capture_output=True,
            text=True,
            cwd=current_dir,
        )
    except OSError:
        return current_dir.name

    if result.returncode == 0:
        common_dir = result.stdout.strip()
        if common_dir:
            common_path = Path(common_dir)
            return common_path.parent.name if common_path.name == ".git" else common_path.name
    try:
        result = subprocess.run(["git", "rev-parse", "--show-toplevel"], check=False, capture_output=True, text=True, cwd=current_dir)
    except OSError:
        return current_dir.name
    if result.returncode == 0 and result.stdout.strip():
        return Path(result.stdout.strip()).name
    return current_dir.name

def parse_account_override(argv: list[str]) -> tuple[str | None, list[str]]:
    slug: str | None = None
    remaining: list[str] = []
    i = 0
    while i < len(argv):
        arg = argv[i]
        if arg.startswith("--account=") or arg.startswith("--profile="):
            slug = arg.split("=", 1)[1]
        elif arg in {"--account", "--profile"}:
            i += 1
            if i >= len(argv):
                raise ValueError(f"{arg} requires a value")
            slug = argv[i]
        else:
            remaining.append(arg)
        i += 1
    return slug, remaining

def load_health(
    path: Path | None = None,
    *,
    stale_after_s: int = HEALTH_STALE_SECONDS,
    tool_name: str = "cdx",
    health_snapshot_tool: str = "codex",
) -> dict[str, AccountSnapshot]:
    if path is None:
        path = runtime_dir() / "health_cache.json"
    snapshots = HealthSnapshotStore(path, stale_after_s).read_fresh()
    if snapshots is None:
        print(
            f"{tool_name}: health cache missing or stale; proceeding without health data",
            file=sys.stderr,
        )
        return {}
    return _tool_health_snapshots(health_snapshot_tool, snapshots)

def _tool_health_snapshots(
    tool: str,
    snapshots: dict[str, AccountSnapshot],
) -> dict[str, AccountSnapshot]:
    prefix = f"{tool}:"
    normalized: dict[str, AccountSnapshot] = {}
    for key, snapshot in snapshots.items():
        if key.startswith(prefix):
            normalized[key[len(prefix) :]] = snapshot
        elif ":" not in key:
            normalized[key] = snapshot
    return normalized

def is_exec_invocation(forwarded_argv: list[str]) -> bool:
    for arg in forwarded_argv:
        if arg.startswith("-"):
            continue
        return arg == "exec"
    return False

def _writable_dir(*candidates: Path) -> Path | None:
    for candidate in candidates:
        try:
            candidate.mkdir(parents=True, exist_ok=True)
            with tempfile.NamedTemporaryFile(dir=candidate):
                pass
        except OSError:
            continue
        return candidate
    return None

STANDARD_EFFORTS = {"low": "low", "med": "medium", "medium": "medium", "high": "high"}
EXTENDED_EFFORTS = ("xhigh", "max")
EXTENDED_EFFORT_MODEL = "gpt-5.6-luna"

def allowed_efforts(model: str | None) -> tuple[str, ...]:
    """Efforts `cdx exec` accepts for a model: a model's catalog capability
    narrowed by the wrapper's cost policy, which keeps the top tiers on the
    cheapest model. Callers validating ahead of the wrapper intersect with this.
    """
    base = ("low", "medium", "high")
    return base + EXTENDED_EFFORTS if model == EXTENDED_EFFORT_MODEL else base

def normalize_codex_exec_flags(forwarded_argv: list[str]) -> list[str]:
    """Own the `cdx exec` plumbing flags in the wrapper so callers (skills,
    agents) never pass them and cannot get them wrong:

    - forces `--sandbox danger-full-access`, stripping any restrictive
      `-s`/`--sandbox` the caller supplied. `workspace-write` makes
      `~/.claude/run/local-gate` read-only, which EROFSes the cpu-guard→
      local-gate shim before any gate runs; `read-only` blocks writes. A
      caller-supplied `--dangerously-bypass-approvals-and-sandbox` is a
      strict superset (also bypasses approvals) and is left intact.
    - injects `--skip-git-repo-check` when absent (codex exec refuses to run
      outside a git checkout otherwise).

    - validates and canonicalizes model reasoning effort at the wrapper boundary,
      applying defaults only for the three known models.
    """
    if not is_exec_invocation(forwarded_argv):
        return forwarded_argv

    model_defaults = {
        "gpt-5.6-sol": "low",
        "gpt-5.6-terra": "medium",
        "gpt-5.6-luna": "high",
    }
    standard_efforts = STANDARD_EFFORTS
    effort_entries: list[tuple[int, int, str, bool, str]] = []
    model: str | None = None
    i = 0
    while i < len(forwarded_argv):
        arg = forwarded_argv[i]
        if arg == "--":
            break
        if arg in {"-m", "--model", "-c", "--config"}:
            if (
                i + 1 >= len(forwarded_argv)
                or not forwarded_argv[i + 1]
                or forwarded_argv[i + 1].startswith("-")
            ):
                raise ValueError(f"{arg} requires a value")
            value = forwarded_argv[i + 1]
            if arg in {"-m", "--model"}:
                model = value
            else:
                key, separator, effort = value.partition("=")
                if key == "model_reasoning_effort" and not separator:
                    raise ValueError("model_reasoning_effort requires a value")
                if separator and key == "model_reasoning_effort":
                    effort_entries.append((i, i + 2, arg, True, effort))
            i += 2
            continue

        inline_model_flag = next(
            (flag for flag in ("-m", "--model") if arg.startswith(f"{flag}=")),
            None,
        )
        if inline_model_flag is not None:
            value = arg[len(inline_model_flag) + 1 :]
            if not value:
                raise ValueError(f"{inline_model_flag} requires a value")
            model = value
            i += 1
            continue

        inline_config_flag = next(
            (flag for flag in ("-c", "--config") if arg.startswith(f"{flag}=")),
            None,
        )
        if inline_config_flag is not None:
            value = arg[len(inline_config_flag) + 1 :]
            if not value:
                raise ValueError(f"{inline_config_flag} requires a value")
            key, separator, effort = value.partition("=")
            if key == "model_reasoning_effort" and not separator:
                raise ValueError("model_reasoning_effort requires a value")
            if separator and key == "model_reasoning_effort":
                effort_entries.append((i, i + 1, inline_config_flag, False, effort))
            i += 1
            continue

        if arg.startswith("-m") and len(arg) > 2:
            model = arg[2:]
            i += 1
            continue

        if arg.startswith("-c") and len(arg) > 2:
            value = arg[2:]
            key, separator, effort = value.partition("=")
            if key == "model_reasoning_effort" and not separator:
                raise ValueError("model_reasoning_effort requires a value")
            if separator and key == "model_reasoning_effort":
                effort_entries.append((i, i + 1, "-c", False, effort))
            i += 1
            continue
        i += 1

    effective_model = model if model is not None else "<unspecified>"
    effort_addition: list[str] = []
    if effort_entries:
        raw_effort = effort_entries[-1][4]
        if raw_effort in standard_efforts:
            canonical_effort = standard_efforts[raw_effort]
        elif raw_effort in EXTENDED_EFFORTS and model == EXTENDED_EFFORT_MODEL:


Output:
Chunk ID: a5f8cc
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 2800
Output:
from __future__ import annotations

import json
import multiprocessing
import os
import random
import shutil
import subprocess
import sys
from collections import Counter
from pathlib import Path

import pytest

sys.path.insert(0, str(Path(__file__).resolve().parents[1]))

import remote_dispatch

REGISTRY = {
    "schema_version": 1,
    "hosts": [
        {
            "name": "debian1",
            "state": "reachable",
            "roles": ["builder", "agent-seat"],
            "access": {
                "tailscale_ip": {
                    "host": "100.0.0.1",
                    "port": 2222,
                    "user": "user",
                    "identity_file": "~/.ssh/id_ed25519_buildbox",
                }
            },
        },
        {
            "name": "debian2",
            "state": "reachable",
            "roles": ["builder", "agent-seat"],
            "access": {"tailscale_ip": {"host": "100.0.0.2", "port": 2222, "user": "user"}},
        },
        {
            "name": "debian3",
            "state": "unreachable",
            "roles": ["builder", "agent-seat"],
            "access": {"tailscale_ip": {"host": "100.0.0.3", "port": 2222, "user": "user"}},
        },
    ],
}

THREE_REACHABLE = {
    "schema_version": 1,
    "hosts": [
        {
            "name": f"debian{n}",
            "state": "reachable",
            "roles": ["agent-seat"],
            "access": {"tailscale_ip": {"host": f"100.0.0.{n}", "user": "user"}},
        }
        for n in (1, 2, 3)
    ],
}

@pytest.fixture(autouse=True)
def isolated_claim_dir(tmp_path_factory, monkeypatch):
    """No test may write placement claims into the real ~/.local/state directory."""
    directory = tmp_path_factory.mktemp("claims")
    monkeypatch.setenv(remote_dispatch.CLAIM_DIR_ENV, str(directory))
    return directory

def write_registry(tmp_path: Path, payload: dict | None = None) -> Path:
    path = tmp_path / "buildbox-hosts.json"
    path.write_text(json.dumps(payload if payload is not None else REGISTRY), encoding="utf-8")
    return path

def test_should_offload_defaults_true():
    assert remote_dispatch.should_offload({}) is True

def test_should_offload_opt_out():
    assert remote_dispatch.should_offload({"CDX_NO_OFFLOAD": "1"}) is False

def test_should_offload_false_when_already_on_a_node():
    assert remote_dispatch.should_offload({"OD_REMOTE_EXEC": "1"}) is False
    assert remote_dispatch.should_offload({"HARNESS_SEAT_CONTAINER": "1"}) is False

def test_candidate_hosts_excludes_unreachable():
    names = [h["name"] for h in remote_dispatch.candidate_hosts(REGISTRY)]
    assert names == ["debian1", "debian2"]

def test_candidate_hosts_raises_when_none_reachable():
    payload = {"hosts": [dict(REGISTRY["hosts"][2])]}
    with pytest.raises(remote_dispatch.OffloadUnavailable):
        remote_dispatch.candidate_hosts(payload)

def test_select_host_picks_least_loaded():
    loads = {"100.0.0.1": 9.5, "100.0.0.2": 0.4}
    host, access, _claim = remote_dispatch.select_host(
        remote_dispatch.candidate_hosts(REGISTRY),
        probe=lambda a: loads[a["host"]],
    )
    assert host["name"] == "debian2"
    assert access["host"] == "100.0.0.2"

def test_select_host_skips_nodes_that_do_not_answer():
    host, _access, _claim = remote_dispatch.select_host(
        remote_dispatch.candidate_hosts(REGISTRY),
        probe=lambda a: None if a["host"] == "100.0.0.2" else 7.0,
    )
    assert host["name"] == "debian1"

def test_select_host_raises_when_no_node_answers():
    with pytest.raises(remote_dispatch.OffloadUnavailable):
        remote_dispatch.select_host(remote_dispatch.candidate_hosts(REGISTRY), probe=lambda a: None)

def test_parse_loadavg():
    assert remote_dispatch.parse_loadavg("0.52 0.31 0.20 1/900 12345\n") == 0.52
    assert remote_dispatch.parse_loadavg("") is None
    assert remote_dispatch.parse_loadavg("garbage") is None

def test_remote_rel_dir_is_stable_and_path_specific():
    a = remote_dispatch.remote_rel_dir(Path("/home/user/Projects/overdeck/.worktrees/one"))
    b = remote_dispatch.remote_rel_dir(Path("/home/user/Projects/overdeck/.worktrees/two"))
    assert a == remote_dispatch.remote_rel_dir(Path("/home/user/Projects/overdeck/.worktrees/one"))
    assert a != b
    assert a.startswith("cdx-offload/one-")

def test_ssh_argv_pins_identity_port_and_ignores_ssh_config():
    argv = remote_dispatch.ssh_argv(REGISTRY["hosts"][0]["access"]["tailscale_ip"])
    assert argv[:3] == ["ssh", "-F", "/dev/null"]
    assert "-p" in argv and "2222" in argv
    assert argv[-1] == "user@100.0.0.1"
    assert any(a.endswith("id_ed25519_buildbox") for a in argv)

GIT_ENV = {
    "GIT_AUTHOR_NAME": "t",
    "GIT_AUTHOR_EMAIL": "t@example.invalid",
    "GIT_COMMITTER_NAME": "t",
    "GIT_COMMITTER_EMAIL": "t@example.invalid",
}

def git(*args: str, cwd: Path) -> subprocess.CompletedProcess:
    done = subprocess.run(
        ["/usr/bin/git", *args],
        cwd=cwd,
        capture_output=True,
        text=True,
        env={**os.environ, **GIT_ENV, "GIT_CONFIG_GLOBAL": "/dev/null", "GIT_CONFIG_SYSTEM": "/dev/null"},
    )
    assert done.returncode == 0, done.stderr
    return done

def make_repo(tmp_path: Path) -> Path:
    """A real checkout — a hand-written `.git` fixture is what let this bug ship."""
    root = tmp_path / "repo"
    root.mkdir()
    git("init", "-q", "-b", "main", ".", cwd=root)
    (root / "file.txt").write_text("one\n", encoding="utf-8")
    git("add", "file.txt", cwd=root)
    git("commit", "-qm", "init", cwd=root)
    return root

def add_worktree(root: Path, slug: str) -> Path:
    path = root / ".worktrees" / slug
    git("worktree", "add", "-q", "-b", f"wt/{slug}", str(path), cwd=root)
    return path

def test_push_and_pull_are_mirror_images_with_identical_excludes():
    access = REGISTRY["hosts"][1]["access"]["tailscale_ip"]
    root = Path("/home/user/Projects/overdeck")
    rel = remote_dispatch.remote_rel_dir(root)
    push = remote_dispatch.push_argv(access, root, rel)
    pull = remote_dispatch.pull_argv(access, root, rel)
    assert push[-2:] == [f"{root}/", f"user@100.0.0.2:{rel}/"]
    assert pull[-2:] == [f"user@100.0.0.2:{rel}/", f"{root}/"]
    assert [a for a in push if a != push[-2] and a != push[-1]][:-2] == [
        a for a in pull if a != pull[-2] and a != pull[-1]
    ][:-2]
    assert "node_modules" in push and "node_modules" in pull
    assert ".worktrees" in push and ".worktrees" in pull

def test_remote_command_marks_the_node_process_so_it_never_re_offloads():
    cmd = remote_dispatch.remote_command("cdx-offload/x-abc", "codex", ["exec", "hello world"])
    assert "OD_REMOTE_EXEC=1" in cmd
    assert "OD_LOCAL_DISPATCH_OK=1" in cmd
    assert "cd cdx-offload/x-abc" in cmd
    assert "'hello world'" in cmd

def test_remote_command_quotes_a_prompt_containing_shell_metacharacters():
    cmd = remote_dispatch.remote_command("d", "codex", ["exec", "rm -rf $HOME; echo `id`"])
    assert "rm -rf $HOME; echo `id`" not in cmd.replace("'", "")[:0] or True
    proof = subprocess.run(
        ["bash", "-c", f"printf '%s' {cmd.split('bash -lc ', 1)[1]}"],
        capture_output=True,
        text=True,
    )
    assert "rm -rf $HOME; echo `id`" in proof.stdout

def test_remote_run_argv_forces_a_tty():
    access = REGISTRY["hosts"][1]["access"]["tailscale_ip"]
    argv = remote_dispatch.remote_run_argv(access, "d", "codex", ["exec", "hi"])
    assert "-tt" in argv
    assert argv[0] == "ssh"

def test_open_session_pushes_then_returns_remote_argv(tmp_path, monkeypatch):
    calls: list[list[str]] = []

    def fake_run(argv, **kwargs):
        calls.append(argv)
        return subprocess.CompletedProcess(argv, 0, stdout="", stderr="")

    monkeypatch.setattr(remote_dispatch, "repo_root", lambda cwd=None: tmp_path / "repo")
    session = remote_dispatch.open_session(
        "codex",
        ["exec", "hi"],
        registry_path=write_registry(tmp_path),
        run=fake_run,
        probe=lambda a: 1.0 if a["host"] == "100.0.0.1" else 5.0,
    )
    assert session.host_name == "debian1"
    assert any("mkdir -p" in " ".join(c) for c in calls)
    assert any(c[0] == "rsync" for c in calls)
    assert session.argv[0] == "ssh" and "-tt" in session.argv

def test_open_session_fails_closed_when_the_push_fails(tmp_path, monkeypatch):
    def fake_run(argv, **kwargs):
        rc = 1 if argv[0] == "rsync" else 0
        return subprocess.CompletedProcess(argv, rc, stdout="", stderr="disk full")

    monkeypatch.setattr(remote_dispatch, "repo_root", lambda cwd=None: tmp_path / "repo")
    with pytest.raises(remote_dispatch.OffloadUnavailable, match="mirror push failed"):
        remote_dispatch.open_session(
            "codex",
            ["exec", "hi"],
            registry_path=write_registry(tmp_path),
            run=fake_run,
            probe=lambda a: 1.0,
        )

def test_pull_back_reports_failure_instead_of_raising(tmp_path):
    session = remote_dispatch.Session(
        host_name="debian1",
        access=REGISTRY["hosts"][0]["access"]["tailscale_ip"],
        root=tmp_path,
        rel_dir="cdx-offload/x",
        argv=["ssh"],
    )
    err = session.pull_back(
        run=lambda argv, **kw: subprocess.CompletedProcess(argv, 23, stdout="", stderr="boom")
    )
    assert err is not None and "debian1" in err and "boom" in err
    assert session.pull_back(
        run=lambda argv, **kw: subprocess.CompletedProcess(argv, 0, stdout="", stderr="")
    ) is None

def test_load_registry_missing_file_is_offload_unavailable(tmp_path):
    with pytest.raises(remote_dispatch.OffloadUnavailable):
        remote_dispatch.load_registry(tmp_path / "nope.json")

def test_git_mirror_is_none_for_a_plain_checkout(tmp_path):
    assert remote_dispatch.git_mirror(make_repo(tmp_path)) is None

def test_git_mirror_is_none_outside_a_repository(tmp_path):
    plain = tmp_path / "notrepo"
    plain.mkdir()
    assert remote_dispatch.git_mirror(plain) is None

def test_git_mirror_detects_a_linked_worktree_and_targets_the_common_worktrees_dir(tmp_path):
    root = make_repo(tmp_path)
    wt = add_worktree(root, "alpha")
    mirror = remote_dispatch.git_mirror(wt)
    assert mirror is not None
    assert mirror.git_dir == (root / ".git" / "worktrees" / "alpha").resolve()
    assert mirror.common_dir == (root / ".git").resolve()
    assert mirror.remote_common.startswith("cdx-offload/git-common/repo-")
    assert mirror.remote_git_dir == f"{mirror.remote_common}/worktrees/alpha"

def test_two_worktrees_of_one_repo_share_the_common_mirror_but_not_the_gitdir(tmp_path):
    root = make_repo(tmp_path)
    one = remote_dispatch.git_mirror(add_worktree(root, "one"))
    two = remote_dispatch.git_mirror(add_worktree(root, "two"))
    assert one.remote_common == two.remote_common
    assert one.remote_git_dir != two.remote_git_dir

def test_common_push_keeps_sibling_worktrees_out_and_never_deletes():
    access = REGISTRY["hosts"][1]["access"]["tailscale_ip"]
    mirror = remote_dispatch.GitMirror(
        git_dir=Path("/r/.git/worktrees/a"),
        common_dir=Path("/r/.git"),
        remote_common="cdx-offload/git-common/r-abc",


Output:
Chunk ID: 824024
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 3288
Output:
from __future__ import annotations

import base64
import json
import os
import re
import subprocess
from pathlib import Path

import pytest

import command_router
import remote_dispatch
from health_client import AccountSnapshot, HealthStatus

def write_rules(base_dir: Path, payload: dict[str, object]) -> None:
    tray_dir = base_dir / ".local/state/overdeck/systray/runtime"
    tray_dir.mkdir(parents=True, exist_ok=True)
    (tray_dir / payload["filename"]).write_text(
        json.dumps(payload["rules"]),
        encoding="utf-8",
    )

def write_health(base_dir: Path, payload: dict[str, dict[str, object]], filename: str) -> None:
    tray_dir = base_dir / ".local/state/overdeck/systray/runtime"
    tray_dir.mkdir(parents=True, exist_ok=True)
    (tray_dir / filename).write_text(json.dumps(payload), encoding="utf-8")

@pytest.fixture
def home(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
    monkeypatch.setattr(command_router.Path, "home", lambda: tmp_path)
    return tmp_path

def test_codex_router_rejects_missing_project_account_using_known_slugs(
    home: Path,
    monkeypatch: pytest.MonkeyPatch,
    capsys: pytest.CaptureFixture[str],
) -> None:
    write_rules(
        home,
        {
            "filename": "routing_rules.json",
            "rules": {
                "projects": {"zync.is": "avi"},
                "default": "rafa",
                "fallback_chain": ["roy"],
                "fallback_trigger": "broken_or_quota_exhausted",
                "quota_exhausted_threshold_pct": 100,
            },
        },
    )
    for slug in ("rafa", "roy"):
        (home / ".local/state/overdeck/systray/runtime" / "accounts" / slug / "CODEX_HOME").mkdir(parents=True)
    write_health(
        home,
        {
            "rafa": {"status": "ok", "primary_used_pct": 10, "secondary_used_pct": 20},
        },
        "health_cache.json",
    )
    monkeypatch.setattr(command_router, "detect_project_name", lambda cwd=None: "zync.is")
    router = command_router.CommandRouter(
        command_router.CodexAdapter(),
        detect_project_name=command_router.detect_project_name,
    )

    exec_called = False

    def fake_execvpe(file: str, args: list[str], env: dict[str, str]) -> None:
        nonlocal exec_called
        exec_called = True

    monkeypatch.setattr(command_router.os, "execvpe", fake_execvpe)

    exit_code = router.main(["cdx", "chat"])
    captured = capsys.readouterr()

    assert exit_code == 1
    assert exec_called is False
    assert captured.err.strip() == "cdx: no healthy account available: avi=unknown-account"

def test_cld_router_prefixes_errors_without_cdx_leak(
    home: Path,
    monkeypatch: pytest.MonkeyPatch,
    capsys: pytest.CaptureFixture[str],
) -> None:
    write_rules(
        home,
        {
            "filename": "claude_routing_rules.json",
            "rules": {
                "projects": {},
                "default": "avi",
                "fallback_chain": ["roy"],
                "fallback_trigger": "broken_or_quota_exhausted",
                "quota_exhausted_threshold_pct": 100,
            },
        },
    )
    monkeypatch.setattr(command_router, "detect_project_name", lambda cwd=None: "unknown")
    write_health(home, {}, "claude_health_cache.json")
    router = command_router.CommandRouter(
        command_router.ClaudeAdapter(),
        detect_project_name=command_router.detect_project_name,
    )

    exec_called = False

    def fake_execvpe(file: str, args: list[str], env: dict[str, str]) -> None:
        nonlocal exec_called
        exec_called = True

    monkeypatch.setattr(command_router.os, "execvpe", fake_execvpe)

    exit_code = router.main(["cld", "chat"])
    captured = capsys.readouterr()

    assert exit_code == 1
    assert exec_called is False
    assert "cld: no healthy account available: avi=unknown-account" in captured.err
    assert "cdx:" not in captured.err

def test_cli_usage_summary_uses_zero_minutes_for_expired_reset() -> None:
    snapshot = AccountSnapshot(
        HealthStatus.OK,
        90,
        80,
        primary_reset_at=1_700_000_000.0,
        secondary_reset_at=1_699_999_999.0,
    )

    assert command_router._format_cli_usage_summary(snapshot, now=1_700_000_000.0) == (
        "5h: 90% (0m left), 7d: 80% (0m left)"
    )

def test_codex_router_explicit_override_resolves_via_legacy_candidate_dir(
    home: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    legacy_home = home / ".codex-tray" / "accounts" / "legacyacct" / "CODEX_HOME"
    legacy_home.mkdir(parents=True)
    (home / ".codex-tray" / "accounts.json").write_text(
        json.dumps({"accounts": [{"slug": "legacyacct", "alias": "Legacy"}]}),
        encoding="utf-8",
    )
    router = command_router.CommandRouter(
        command_router.CodexAdapter(),
        detect_project_name=command_router.detect_project_name,
    )
    monkeypatch.setattr(
        command_router.CodexAdapter,
        "sync_before_exec",
        lambda self, base_dir: None,
    )

    exec_call: dict[str, object] = {}

    def fake_execvpe(file: str, args: list[str], env: dict[str, str]) -> None:
        exec_call["file"] = file
        exec_call["args"] = args
        exec_call["env"] = env
        raise SystemExit(0)

    monkeypatch.setattr(command_router.os, "execvpe", fake_execvpe)

    with pytest.raises(SystemExit):
        router.main(["cdx", "--account", "legacyacct", "chat"])

    assert exec_call["file"] == "codex"
    assert exec_call["env"]["CODEX_HOME"] == str(legacy_home)

def test_codex_router_explicit_override_resolves_labels_to_slug_chain(
    home: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    tray = home / ".local/state/overdeck/systray/runtime"
    for slug in ("roy", "work"):
        (tray / "accounts" / slug / "CODEX_HOME").mkdir(parents=True)
    (tray / "accounts.json").write_text(
        json.dumps(
            {"accounts": [{"slug": "roy", "alias": "Personal"}, {"slug": "work", "alias": "Office"}]}
        ),
        encoding="utf-8",
    )
    router = command_router.CommandRouter(command_router.CodexAdapter())

    resolved = router._resolve_account("personal,OFFICE")

    assert resolved == (tray, "roy", tray / "accounts" / "roy" / "CODEX_HOME")
    assert router._last_route.chain == ("roy", "work")

def test_codex_router_explicit_override_resolves_dynamic_after_labels(
    home: Path,
) -> None:
    tray = home / ".local/state/overdeck/systray/runtime"
    for slug in ("roy", "work"):
        (tray / "accounts" / slug / "CODEX_HOME").mkdir(parents=True)
    (tray / "accounts.json").write_text(
        json.dumps(
            {"accounts": [{"slug": "roy", "alias": "Personal"}, {"slug": "work", "alias": "Office"}]}
        ),
        encoding="utf-8",
    )
    (tray / "default_slug").write_text("work\n", encoding="utf-8")
    router = command_router.CommandRouter(command_router.CodexAdapter())

    resolved = router._resolve_account("Personal,dynamic")

    assert resolved is not None
    assert router._last_route.chain == ("roy", "work")

def test_codex_route_canonical_maps_labels_to_ids_and_keeps_dynamic_literal(
    home: Path,
    capsys: pytest.CaptureFixture[str],
) -> None:
    tray = home / ".local/state/overdeck/systray/runtime"
    for slug in ("roy", "work"):
        (tray / "accounts" / slug / "CODEX_HOME").mkdir(parents=True)
    (tray / "accounts.json").write_text(
        json.dumps(
            {"accounts": [{"slug": "roy", "alias": "Personal"}, {"slug": "work", "alias": "Office"}]}
        ),
        encoding="utf-8",
    )
    router = command_router.CommandRouter(command_router.CodexAdapter())

    exit_code = router.main(["cdx", "route", "--canonical", "--profile=dynamic,OFFICE"])
    captured = capsys.readouterr()

    assert exit_code == 0
    assert json.loads(captured.out) == {"profile": "dynamic,work"}

def test_codex_route_canonical_fails_closed_on_unknown_token(
    home: Path,
    capsys: pytest.CaptureFixture[str],
) -> None:
    tray = home / ".local/state/overdeck/systray/runtime"
    (tray / "accounts" / "roy" / "CODEX_HOME").mkdir(parents=True)
    (tray / "accounts.json").write_text(
        json.dumps({"accounts": [{"slug": "roy", "alias": "Personal"}]}),
        encoding="utf-8",
    )
    router = command_router.CommandRouter(command_router.CodexAdapter())

    exit_code = router.main(["cdx", "route", "--canonical", "--profile=ghost"])
    captured = capsys.readouterr()

    assert exit_code == 1
    assert "unknown account: ghost" in captured.err
    assert captured.out.strip() == ""

def test_is_exec_invocation_detects_exec_after_flags() -> None:
    assert command_router.is_exec_invocation(["exec", "-m", "gpt-5.4", "prompt"])
    assert command_router.is_exec_invocation(["exec"])
    assert not command_router.is_exec_invocation(["chat"])
    assert not command_router.is_exec_invocation([])
    assert not command_router.is_exec_invocation(["--status"])

def test_codex_injects_sandbox_and_git_check_for_exec() -> None:
    assert command_router.normalize_codex_exec_flags(
        ["exec", "-m", "gpt-5.4", "prompt"]
    ) == [
        "exec",
        "--sandbox",
        "danger-full-access",
        "--skip-git-repo-check",
        "-m",
        "gpt-5.4",
        "prompt",
    ]

@pytest.mark.parametrize(
    "restrictive",
    [
        ["--sandbox", "read-only"],
        ["-s", "workspace-write"],
        ["--sandbox=read-only"],
        ["-s=workspace-write"],
        ["-sread-only"],
    ],
)
def test_codex_forces_danger_full_access_over_restrictive_flag(
    restrictive: list[str],
) -> None:
    assert command_router.normalize_codex_exec_flags(
        ["exec", *restrictive, "prompt"]
    ) == ["exec", "--sandbox", "danger-full-access", "--skip-git-repo-check", "prompt"]

def test_codex_preserves_bypass_flag_and_does_not_add_sandbox() -> None:
    assert command_router.normalize_codex_exec_flags(
        ["exec", "--dangerously-bypass-approvals-and-sandbox", "prompt"]
    ) == [
        "exec",
        "--skip-git-repo-check",
        "--dangerously-bypass-approvals-and-sandbox",
        "prompt",
    ]

def test_codex_does_not_duplicate_skip_git_repo_check() -> None:
    assert command_router.normalize_codex_exec_flags(
        ["exec", "--skip-git-repo-check", "-m", "gpt-5.4", "prompt"]
    ) == [
        "exec",
        "--sandbox",
        "danger-full-access",
        "--skip-git-repo-check",
        "-m",
        "gpt-5.4",
        "prompt",
    ]

def test_codex_default_does_not_change_claude_arguments() -> None:
    assert command_router.normalize_codex_exec_flags(["chat"]) == ["chat"]

@pytest.mark.parametrize(
    ("model", "default_effort"),
    [
        ("gpt-5.6-sol", "low"),
        ("gpt-5.6-terra", "medium"),
        ("gpt-5.6-luna", "high"),
    ],
)
def test_codex_injects_known_model_default_effort(
    model: str,
    default_effort: str,
) -> None:
    assert command_router.normalize_codex_exec_flags(
        ["exec", "--model", model, "prompt"]
    ) == [
        "exec",
        "--sandbox",
        "danger-full-access",
        "--skip-git-repo-check",
        "-c",
        f"model_reasoning_effort={default_effort}",
        "--model",
        model,
        "prompt",
    ]

@pytest.mark.parametrize("model", ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"])
@pytest.mark.parametrize(
    ("effort", "canonical"),
    [("low", "low"), ("med", "medium"), ("medium", "medium"), ("high", "high")],
)
def test_codex_canonicalizes_standard_effort_values(
    model: str,
    effort: str,
    canonical: str,
) -> None:
    assert command_router.normalize_codex_exec_flags(
        ["exec", "-m", model, "-c", f"model_reasoning_effort={effort}", "prompt"]
    ) == [
        "exec",
        "--sandbox",
        "danger-full-access",
        "--skip-git-repo-check",
        "-m",
        model,
        "-c",
        f"model_reasoning_effort={canonical}",
        "prompt",
    ]

@pytest.mark.parametrize("effort", ["xhigh", "max"])
def test_codex_luna_accepts_extended_effort(effort: str) -> None:
    assert command_router.normalize_codex_exec_flags(
        ["exec", "-m", "gpt-5.6-luna", "-c", f"model_reasoning_effort={effort}", "prompt"]
    ) == [
        "exec",
        "--sandbox",
        "danger-full-access",
        "--skip-git-repo-check",
        "-m",
        "gpt-5.6-luna",
        "-c",
        f"model_reasoning_effort={effort}",
        "prompt",
    ]

@pytest.mark.parametrize("effort", ["xhigh", "max"])
def test_codex_rejects_extended_effort_without_model(effort: str) -> None:
    with pytest.raises(
        ValueError,
        match=rf"^unsupported reasoning effort '{effort}' for model '<unspecified>'$",
    ):
        command_router.normalize_codex_exec_flags(
            ["exec", "-c", f"model_reasoning_effort={effort}", "prompt"]
        )

@pytest.mark.parametrize("model", ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.5"])
@pytest.mark.parametrize("effort", ["xhigh", "max"])
def test_codex_rejects_extended_effort_for_non_luna_models(
    model: str,
    effort: str,
) -> None:
    with pytest.raises(
        ValueError,


I’ve confirmed current behavior and now I’m locating the exact `_run_captured`, `main`, and `open_session` branches to do a surgical, one-pass edit. Next I’ll patch all four files in a single change set plus the requested test updates and isolate the real-dispatching tests.


rg -n "def _run_captured|def main\(|capture_log_path|execvpe|should_offload|def open_session|def load_registry|def candidate_hosts|def access_for|except remote_dispatch.OffloadUnavailable|OffloadUnavailable" modules/systray/command_router.py modules/systray/remote_dispatch.py modules/systray/tests/test_command_router.py modules/systray/tests/test_remote_dispatch.py


sed -n '240,520p' modules/systray/remote_dispatch.py


sed -n '520,920p' modules/systray/remote_dispatch.py


sed -n '320,700p' modules/systray/command_router.py