diff --git a/.claude-plugin/registry.json b/.claude-plugin/registry.json index a63eb227..aa70cb20 100644 --- a/.claude-plugin/registry.json +++ b/.claude-plugin/registry.json @@ -205,24 +205,24 @@ { "name": "sc-launch-term", "version": "0.12.0", - "description": "Launch Claude, Codex, and Gemini sessions in supported terminals with platform-aware terminal autodetect and optional tmux session management.\n", + "description": "Launch Claude (including Fable), Codex (including Sol, Terra, and Luna), and Gemini sessions in supported terminals with platform-aware autodetect, cmux workspace tabs, and optional tmux session management.\n", "author": "randlee", "license": "MIT", "keywords": [], "category": "tools", "artifacts": { - "commands": 0, + "commands": 9, "skills": 0, "agents": 0, "scripts": 3, "schemas": 0 }, - "lastUpdated": "2026-04-29T04:44:47.191851+00:00" + "lastUpdated": "2026-08-09T14:54:36.245664" }, { "name": "sc-launchpad", "version": "0.12.0", - "description": "Launch Claude, Codex, or Gemini as a separate background sub-agent runtime, with explicit ATM teammate-mode normalization and roster registration.\n", + "description": "Launch Claude (including Fable), Codex (including Sol, Terra, and Luna), or Gemini as a separate background sub-agent runtime, with explicit ATM teammate-mode normalization and roster registration.\n", "author": "synaptic-canvas", "license": "MIT", "keywords": [], @@ -234,7 +234,7 @@ "scripts": 2, "schemas": 0 }, - "lastUpdated": "2026-04-29T04:44:47.192473+00:00" + "lastUpdated": "2026-08-09T14:54:36.395961" }, { "name": "sc-manage", @@ -323,13 +323,13 @@ } ], "metadata": { - "totalPackages": 17, - "totalCommands": 10, - "totalSkills": 20, + "totalPackages": 18, + "totalCommands": 19, + "totalSkills": 21, "totalAgents": 45, "totalScripts": 50, "totalSchemas": 2 }, - "generated": "2026-05-15T21:50:55.867734+00:00", - "lastUpdated": "2026-05-15T21:50:55.867734+00:00" + "generated": "2026-08-09T14:54:36.395998", + "lastUpdated": "2026-08-09T14:54:36.396000" } diff --git a/packages/sc-docling-pdf/CHANGELOG.md b/packages/sc-docling-pdf/CHANGELOG.md new file mode 100644 index 00000000..777baf1c --- /dev/null +++ b/packages/sc-docling-pdf/CHANGELOG.md @@ -0,0 +1,15 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.1.0] - 2026-04-22 + +### Added + +- Initial local-only `sc-docling-pdf` package release. +- `docling-pdf-extraction` skill for profile-aware PDF conversion. +- References for text, scan/OCR, rich, VLM, and code-oriented conversion profiles. +- Markdown, image, table, and JSON output guidance. diff --git a/packages/sc-launch-term/.claude-plugin/plugin.json b/packages/sc-launch-term/.claude-plugin/plugin.json index 02df3b05..12c3e32c 100644 --- a/packages/sc-launch-term/.claude-plugin/plugin.json +++ b/packages/sc-launch-term/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "sc-launch-term", - "description": "Launch Claude, Codex, and Gemini sessions in supported terminals with platform-aware terminal autodetect and optional tmux session management.", + "description": "Launch Claude (including Fable), Codex (including Sol, Terra, and Luna), and Gemini sessions in supported terminals with platform-aware autodetect, cmux workspace tabs, and optional tmux session management.", "version": "0.12.0", "author": { "name": "randlee" @@ -12,8 +12,13 @@ "macos", "windows", "tmux", + "cmux", "claude", + "fable", "codex", + "sol", + "terra", + "luna", "gemini" ], "scripts": [ @@ -25,6 +30,10 @@ "./commands/sc/sonnet.md", "./commands/sc/haiku.md", "./commands/sc/opus.md", + "./commands/sc/fable.md", + "./commands/sc/sol.md", + "./commands/sc/terra.md", + "./commands/sc/luna.md", "./commands/sc/codex.md", "./commands/sc/gemini.md" ] diff --git a/packages/sc-launch-term/CHANGELOG.md b/packages/sc-launch-term/CHANGELOG.md index f648b540..297b65a0 100644 --- a/packages/sc-launch-term/CHANGELOG.md +++ b/packages/sc-launch-term/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## [Unreleased] +### Added +- Added the `/sc:fable` Claude model command. +- Added cmux terminal/workspace-tab launching. +- Added shared `/sc:sol`, `/sc:terra`, and `/sc:luna` Codex model launchers. +- Deprecated `/sc:codex` now routes to the Terra model and identity pool. +- Added sci-fi/cyberpunk identity pools for the Sol, Terra, and Luna aliases. + +### Fixed +- Use Codex's supported `--yolo` automation flag and `--enable hooks` feature flag for model launches. + ## [0.10.0] - 2026-04-25 ### Added - Initial `sc-launch-term` release with `/sc:sonnet`, `/sc:haiku`, `/sc:opus`, `/sc:codex`, and `/sc:gemini`. diff --git a/packages/sc-launch-term/README.md b/packages/sc-launch-term/README.md index cf64eb2e..f453e48d 100644 --- a/packages/sc-launch-term/README.md +++ b/packages/sc-launch-term/README.md @@ -1,13 +1,18 @@ # sc-launch-term Launch Claude, Codex, and Gemini sessions in supported terminals with a shared -autodetect launcher. +autodetect launcher. Codex exposes Sol, Terra, and Luna model aliases through +one shared model-launch implementation. ## Commands - `/sc:sonnet` - `/sc:haiku` - `/sc:opus` +- `/sc:fable` +- `/sc:sol` +- `/sc:terra` +- `/sc:luna` - `/sc:codex` - `/sc:gemini` @@ -17,32 +22,36 @@ Each command accepts: - `--terminal ` to force a specific terminal backend - `--tab` to request a new tab when the backend supports it - `--tmux` to create or attach to a named tmux session -- `--identity ` required when `ATM_TEAM` is set; exports both `ATM_TEAM` and `ATM_IDENTITY` +- `--identity ` optional; if omitted, the launcher generates a model-specific identity and exports both `ATM_TEAM` and `ATM_IDENTITY` - `-- ` to forward extra CLI args to the launched tool ## Terminal Backends Autodetect order: -- macOS: `iterm2`, `ghostty`, `wezterm`, `warp`, `terminal` +- macOS: `iterm2`, `ghostty`, `wezterm`, `warp`, `cmux`, `terminal` - Windows: `wt`, `warp` Supported explicit values: -- macOS: `auto`, `iterm2`, `ghostty`, `wezterm`, `warp`, `terminal` +- macOS: `auto`, `iterm2`, `ghostty`, `wezterm`, `warp`, `cmux`, `terminal` - Windows: `auto`, `wt`, `windows-terminal`, `warp` Notes: - Warp automation uses Warp launch configurations and opens a new window. -- `/sc:sonnet`, `/sc:haiku`, and `/sc:opus` launch `claude` directly with +- cmux automation creates and focuses a new cmux workspace, which is cmux's tab primitive. +- `/sc:sonnet`, `/sc:haiku`, `/sc:opus`, and `/sc:fable` launch `claude` directly with `--model --dangerously-skip-permissions`, so they no longer depend on shell aliases or functions. -- When `--tmux` is used with those Claude model commands, the launcher also +- `/sc:sol`, `/sc:terra`, and `/sc:luna` launch Codex with + `--model gpt-5.6-sol`, `--model gpt-5.6-terra`, and `--model gpt-5.6-luna` + respectively, using the shared Codex launcher and model-specific identity pools. +- `/sc:codex` is a deprecated compatibility alias for `/sc:terra`. +- When `--tmux` is used with Claude model commands, the launcher also adds `--teammate-mode tmux` to match the current local wrappers. -- If `ATM_TEAM` is set in the current environment, every launch requires - `--identity ` and exports both `ATM_TEAM` and `ATM_IDENTITY=` - into the launched session. +- If `ATM_TEAM` is set in the current environment, every launch exports both + `ATM_TEAM` and an explicit or generated `ATM_IDENTITY=` into the launched session. - When `ATM_TEAM` is set, the launcher also runs `atm teams add-member --model --cwd ` before starting the AI tool. Pane-specific `--pane-id` wiring is intentionally left @@ -62,9 +71,11 @@ This package is intended for global command installation under `~/.claude/`. ## Security and Runtime Notes - The commands launch locally installed CLIs only. They do not download code or open network connections by themselves. -- `/sc:sonnet`, `/sc:haiku`, and `/sc:opus` invoke the local `claude` CLI with `--dangerously-skip-permissions`, so use this package only in environments where that launch model is acceptable. -- `/sc:codex` and `/sc:gemini` forward to the local `codex` and `gemini` executables on `PATH`. -- When `ATM_TEAM` is present, the launcher requires `--identity ` and exports both `ATM_TEAM` and `ATM_IDENTITY` into the child session. +- `/sc:sonnet`, `/sc:haiku`, `/sc:opus`, and `/sc:fable` invoke the local `claude` CLI with `--dangerously-skip-permissions`, so use this package only in environments where that launch model is acceptable. +- `/sc:sol`, `/sc:terra`, `/sc:luna`, and deprecated `/sc:codex` forward to the local `codex` executable on `PATH`. +- `/sc:gemini` forwards to the local `gemini` executable on `PATH`. +- cmux requires the local `cmux` CLI on `PATH`; cmux launches always target a new workspace/tab. +- When `ATM_TEAM` is present, the launcher exports both `ATM_TEAM` and an explicit or generated `ATM_IDENTITY` into the child session. - In ATM teammate mode, the launcher also runs `atm teams add-member --model --cwd ` before starting the tool. - `--tmux` creates or reuses a local tmux session and therefore depends on a trusted local `tmux` installation. - The installed wrapper is `.claude/scripts/sc-term-launch.sh`, which resolves Python through the local machine in this order: `python3`, `py -3`, then `python`. @@ -75,6 +86,10 @@ This package is intended for global command installation under `~/.claude/`. /sc:sonnet ~/projects/foo /sc:haiku ~/projects/foo --terminal ghostty /sc:opus ~/projects/foo --tab +/sc:fable ~/projects/foo --terminal cmux --tab +/sc:sol ~/projects/foo +/sc:terra ~/projects/foo --terminal cmux --tab +/sc:luna ~/projects/foo /sc:codex ~/projects/foo --identity alice /sc:sonnet ~/projects/foo -- --continue /sc:codex ~/projects/foo --tmux diff --git a/packages/sc-launch-term/commands/sc/codex.md b/packages/sc-launch-term/commands/sc/codex.md index 0e3ec9c4..471606d2 100644 --- a/packages/sc-launch-term/commands/sc/codex.md +++ b/packages/sc-launch-term/commands/sc/codex.md @@ -1,115 +1,21 @@ --- -description: Launch Codex CLI in a supported terminal at a specified directory -argument-hint: "[dir] [--terminal ] [--tab] [--tmux] [--identity ]" +description: Deprecated compatibility alias for launching Codex Terra in a supported terminal +argument-hint: "[dir] [--terminal ] [--tab] [--tmux] [--identity ] [-- ]" allowed-tools: Bash, AskUserQuestion --- -Launch Codex CLI in a supported terminal. +`/sc:codex` is retained as a deprecated compatibility alias. It routes to the `terra` model and Terra identity pool, launching Codex as `gpt-5.6-terra`. -## Arguments +Use `/sc:terra` for new invocations. The shared launcher handles argument parsing, terminal selection, cmux workspace/tab creation, tmux session collision handling, identity generation, and Codex passthrough arguments. -Parse from `$ARGUMENTS`: -- `dir` optional positional target directory. Default: `$PWD`. -- `--terminal ` optional explicit backend. Default: `auto`. -- `--tab` optional request for a new tab instead of a new window when supported. -- `--tmux` optional tmux session flow using the session name `"{folder} - codex"`. -- `--identity ` optional. If omitted, a random name is generated. Exports both `ATM_TEAM` and `ATM_IDENTITY=` into the launched session. +For this compatibility alias, resolve the optional directory and arguments using the same contract as the model commands, then invoke: -Supported backends: -- macOS: `iterm2`, `ghostty`, `wezterm`, `warp`, `terminal` -- Windows: `wt`, `warp` - -Backend notes: -- Warp opens a new window only; reject `--tab` if `warp` is selected. -- `--tmux` requires `tmux` on `PATH`. On Windows, only use it if your shell already supports `tmux`. -- If `ATM_TEAM` is set and `--identity` is supplied, the launcher registers the member first with `atm teams add-member --model codex --cwd `. - -The launched command is: - -```text -codex --yolo -c features.codex_hooks=true -``` - -## Steps - -**Step 0** — If `$ARGUMENTS` contains `--help`, print the following and stop: - -``` -Usage: /sc:codex [dir] [--terminal ] [--tab] [--tmux] [--identity ] - -Launch Codex CLI in a supported terminal. - -Examples: - /sc:codex - /sc:codex ~/projects/foo - /sc:codex ~/projects/foo --terminal wt - /sc:codex ~/projects/foo --tab - /sc:codex ~/projects/foo --tmux - /sc:codex ~/projects/foo --identity alice -``` - -**Step 1** — Parse `$ARGUMENTS`. Extract `dir`, `--terminal`, `--tab`, `--tmux`, and `--identity`. - -**Step 2** — Resolve `dir` to an absolute path. - -**Step 3** — If `warp` is selected with `--tab`, stop and explain that scripted Warp launches open a new window only. - -**Step 4** — Build `TERM_ARG`: `--terminal ` if supplied, otherwise omit. - -**Step 5** — Resolve identity: use `--identity ` if supplied, otherwise generate a random name. Build `IDENTITY_ARG` from the resolved value. - -### Without `--tmux` - -Run and stop: - -```bash -.claude/scripts/sc-term-launch.sh launch "codex --yolo -c features.codex_hooks=true" "" --member-model codex -``` - -### With `--tmux` - -**Step 6** — Build `SESSION_NAME = "{basename(dir)} - codex"`. - -**Step 7** — Check whether tmux session naming is available: - -```bash -.claude/scripts/sc-term-launch.sh check-session "" -``` - -Parse JSON: `{"available": bool, "exists": bool, "next_name": str}`. - -If `available` is false, stop and tell the user that local tmux support is unavailable. - -If `exists` is false, run and stop: - -```bash -.claude/scripts/sc-term-launch.sh launch "codex --yolo -c features.codex_hooks=true" "" --member-model codex --tmux "" -``` - -If `exists` is true, use `AskUserQuestion` exactly once with: -- `Connect to existing session (no Codex)` -- `Connect to existing session + Codex in new pane` -- `New session: ` -- `Enter custom session name` - -Handle responses: - -**Connect (no Codex)**: ```bash -.claude/scripts/sc-term-launch.sh attach "" +.claude/scripts/sc-term-launch.sh launch-codex-model codex "" ``` -**Connect + Codex pane**: -```bash -.claude/scripts/sc-term-launch.sh attach-pane "" "codex --yolo -c features.codex_hooks=true" --cwd "" --member-model codex -``` +The deprecated `codex` model alias is normalized by the launcher to: -**New session (`next_name`)**: -```bash -.claude/scripts/sc-term-launch.sh launch "codex --yolo -c features.codex_hooks=true" "" --member-model codex --tmux "" -``` - -**Enter custom name** — ask for the session name in a follow-up message, then: -```bash -.claude/scripts/sc-term-launch.sh launch "codex --yolo -c features.codex_hooks=true" "" --member-model codex --tmux "" +```text +codex --model gpt-5.6-terra --yolo --enable hooks ``` diff --git a/packages/sc-launch-term/commands/sc/fable.md b/packages/sc-launch-term/commands/sc/fable.md new file mode 100644 index 00000000..c8db71f4 --- /dev/null +++ b/packages/sc-launch-term/commands/sc/fable.md @@ -0,0 +1,119 @@ +--- +description: Launch Claude Code (fable) in a supported terminal at a specified directory +argument-hint: "[dir] [--terminal ] [--tab] [--tmux] [--identity ] [-- ]" +allowed-tools: Bash, AskUserQuestion +--- + +Launch Claude Code with `claude --model fable --dangerously-skip-permissions` in a supported terminal. + +## Arguments + +Parse from `$ARGUMENTS`: +- `dir` optional positional target directory. Default: `$PWD`. +- `--terminal ` optional explicit backend. Default: `auto`. +- `--tab` optional request for a new tab when the backend supports it. +- `--tmux` optional tmux session flow using the session name `"{folder} - fable"`. +- `--identity ` optional. If omitted, a random name is generated. Exports both `ATM_TEAM` and `ATM_IDENTITY=` into the launched session. +- `-- ` optional passthrough Claude CLI arguments. Everything after `--` is forwarded to `claude`. + +Supported backends: +- macOS: `iterm2`, `ghostty`, `wezterm`, `warp`, `cmux`, `terminal` +- Windows: `wt`, `warp` + +Backend notes: +- Warp opens a new window only; reject `--tab` if `warp` is selected. +- cmux creates and focuses a new cmux workspace, which is cmux's tab primitive. +- `--tmux` requires `tmux` on `PATH`. On Windows, only use it if your shell already supports `tmux`. +- When `--tmux` is used, the launcher also adds `--teammate-mode tmux` to match the local Claude wrapper behavior. +- If `ATM_TEAM` is set and `--identity` is supplied, the launcher registers the member first with `atm teams add-member --model fable --cwd `. + +The launched command is: + +```text +claude --model fable --dangerously-skip-permissions +``` + +## Steps + +**Step 0** — If `$ARGUMENTS` contains `--help`, print the following and stop: + +``` +Usage: /sc:fable [dir] [--terminal ] [--tab] [--tmux] [--identity ] [-- ] + +Launch Claude Code (fable) in a supported terminal. + +Examples: + /sc:fable + /sc:fable ~/projects/foo + /sc:fable ~/projects/foo --terminal cmux --tab + /sc:fable ~/projects/foo --tab + /sc:fable ~/projects/foo --tmux + /sc:fable ~/projects/foo --identity alice + /sc:fable ~/projects/foo -- --continue +``` + +**Step 1** — Parse `$ARGUMENTS`. Extract `dir`, `--terminal`, `--tab`, and `--tmux`. If `--` is present, collect everything after it into `CLAUDE_ARG_TAIL`; otherwise `CLAUDE_ARG_TAIL` is empty. Extract `--identity` when supplied. + +**Step 2** — Resolve `dir` to an absolute path. + +**Step 3** — If `warp` is selected with `--tab`, stop and explain that scripted Warp launches open a new window only. + +**Step 4** — Build `TERM_ARG`: `--terminal ` if supplied, otherwise omit. + +**Step 5** — Resolve identity: use `--identity ` if supplied, otherwise generate a random name. Build `IDENTITY_ARG` from the resolved value. + +### Without `--tmux` + +Run and stop: + +```bash +.claude/scripts/sc-term-launch.sh launch-claude-model fable "" +``` + +### With `--tmux` + +**Step 6** — Build `SESSION_NAME = "{basename(dir)} - fable"`. + +**Step 7** — Check whether tmux session naming is available: + +```bash +.claude/scripts/sc-term-launch.sh check-session "" +``` + +Parse JSON: `{"available": bool, "exists": bool, "next_name": str}`. + +If `available` is false, stop and tell the user that local tmux support is unavailable. + +If `exists` is false, run and stop: + +```bash +.claude/scripts/sc-term-launch.sh launch-claude-model fable "" --tmux "" +``` + +If `exists` is true, use `AskUserQuestion` exactly once with: +- `Connect to existing session (no Claude)` +- `Connect to existing session + Claude in new pane` +- `New session: ` +- `Enter custom session name` + +Handle responses: + +**Connect (no Claude)**: +```bash +.claude/scripts/sc-term-launch.sh attach "" +``` + +**Connect + Claude pane**: +```bash +.claude/scripts/sc-term-launch.sh attach-pane-claude-model "" fable --cwd "" +``` + +**New session (`next_name`)**: +```bash +.claude/scripts/sc-term-launch.sh launch-claude-model fable "" --tmux "" +``` + +**Enter custom name** — ask for the session name in a follow-up message, then: +```bash +.claude/scripts/sc-term-launch.sh launch-claude-model fable "" --tmux "" +``` diff --git a/packages/sc-launch-term/commands/sc/gemini.md b/packages/sc-launch-term/commands/sc/gemini.md index 44fc9261..283dd924 100644 --- a/packages/sc-launch-term/commands/sc/gemini.md +++ b/packages/sc-launch-term/commands/sc/gemini.md @@ -16,11 +16,12 @@ Parse from `$ARGUMENTS`: - `--identity ` optional. If omitted, a random name is generated. Exports both `ATM_TEAM` and `ATM_IDENTITY=` into the launched session. Supported backends: -- macOS: `iterm2`, `ghostty`, `wezterm`, `warp`, `terminal` +- macOS: `iterm2`, `ghostty`, `wezterm`, `warp`, `cmux`, `terminal` - Windows: `wt`, `warp` Backend notes: - Warp opens a new window only; reject `--tab` if `warp` is selected. +- cmux creates and focuses a new workspace, which is cmux's tab primitive. - `--tmux` requires `tmux` on `PATH`. On Windows, only use it if your shell already supports `tmux`. - If `ATM_TEAM` is set and `--identity` is supplied, the launcher registers the member first with `atm teams add-member --model gemini --cwd `. diff --git a/packages/sc-launch-term/commands/sc/haiku.md b/packages/sc-launch-term/commands/sc/haiku.md index 01b065de..5a9e2ef0 100644 --- a/packages/sc-launch-term/commands/sc/haiku.md +++ b/packages/sc-launch-term/commands/sc/haiku.md @@ -17,11 +17,12 @@ Parse from `$ARGUMENTS`: - `-- ` optional passthrough Claude CLI arguments. Everything after `--` is forwarded to `claude`. Supported backends: -- macOS: `iterm2`, `ghostty`, `wezterm`, `warp`, `terminal` +- macOS: `iterm2`, `ghostty`, `wezterm`, `warp`, `cmux`, `terminal` - Windows: `wt`, `warp` Backend notes: - Warp opens a new window only; reject `--tab` if `warp` is selected. +- cmux creates and focuses a new workspace, which is cmux's tab primitive. - `--tmux` requires `tmux` on `PATH`. On Windows, only use it if your shell already supports `tmux`. - When `--tmux` is used, the launcher also adds `--teammate-mode tmux` to match the current local `haiku` wrapper behavior. - If `ATM_TEAM` is set and `--identity` is supplied, the launcher registers the member first with `atm teams add-member --model haiku --cwd `. diff --git a/packages/sc-launch-term/commands/sc/luna.md b/packages/sc-launch-term/commands/sc/luna.md new file mode 100644 index 00000000..e93b4bbe --- /dev/null +++ b/packages/sc-launch-term/commands/sc/luna.md @@ -0,0 +1,15 @@ +--- +description: Launch Codex with the GPT-5.6 Luna model in a supported terminal +argument-hint: "[dir] [--terminal ] [--tab] [--tmux] [--identity ] [-- ]" +allowed-tools: Bash, AskUserQuestion +--- + +Launch Codex with model alias `luna` using the shared Codex model-launch flow. + +Use the same argument contract and tmux collision handling as `/sc:codex`, but invoke: + +```bash +.claude/scripts/sc-term-launch.sh launch-codex-model luna "" +``` + +The shared launcher resolves a missing identity from the Luna sci-fi/cyberpunk name pool and runs the model as `gpt-5.6-luna`. diff --git a/packages/sc-launch-term/commands/sc/opus.md b/packages/sc-launch-term/commands/sc/opus.md index 277d597c..b5cf2a62 100644 --- a/packages/sc-launch-term/commands/sc/opus.md +++ b/packages/sc-launch-term/commands/sc/opus.md @@ -17,11 +17,12 @@ Parse from `$ARGUMENTS`: - `-- ` optional passthrough Claude CLI arguments. Everything after `--` is forwarded to `claude`. Supported backends: -- macOS: `iterm2`, `ghostty`, `wezterm`, `warp`, `terminal` +- macOS: `iterm2`, `ghostty`, `wezterm`, `warp`, `cmux`, `terminal` - Windows: `wt`, `warp` Backend notes: - Warp opens a new window only; reject `--tab` if `warp` is selected. +- cmux creates and focuses a new workspace, which is cmux's tab primitive. - `--tmux` requires `tmux` on `PATH`. On Windows, only use it if your shell already supports `tmux`. - When `--tmux` is used, the launcher also adds `--teammate-mode tmux` to match the current local `opus` wrapper behavior. - If `ATM_TEAM` is set and `--identity` is supplied, the launcher registers the member first with `atm teams add-member --model opus --cwd `. diff --git a/packages/sc-launch-term/commands/sc/sol.md b/packages/sc-launch-term/commands/sc/sol.md new file mode 100644 index 00000000..20481a97 --- /dev/null +++ b/packages/sc-launch-term/commands/sc/sol.md @@ -0,0 +1,15 @@ +--- +description: Launch Codex with the GPT-5.6 Sol model in a supported terminal +argument-hint: "[dir] [--terminal ] [--tab] [--tmux] [--identity ] [-- ]" +allowed-tools: Bash, AskUserQuestion +--- + +Launch Codex with model alias `sol` using the shared Codex model-launch flow. + +Use the same argument contract and tmux collision handling as `/sc:codex`, but invoke: + +```bash +.claude/scripts/sc-term-launch.sh launch-codex-model sol "" +``` + +The shared launcher resolves a missing identity from the Sol sci-fi/cyberpunk name pool and runs the model as `gpt-5.6-sol`. diff --git a/packages/sc-launch-term/commands/sc/sonnet.md b/packages/sc-launch-term/commands/sc/sonnet.md index 82c683f6..a2a6afde 100644 --- a/packages/sc-launch-term/commands/sc/sonnet.md +++ b/packages/sc-launch-term/commands/sc/sonnet.md @@ -17,11 +17,12 @@ Parse from `$ARGUMENTS`: - `-- ` optional passthrough Claude CLI arguments. Everything after `--` is forwarded to `claude`. Supported backends: -- macOS: `iterm2`, `ghostty`, `wezterm`, `warp`, `terminal` +- macOS: `iterm2`, `ghostty`, `wezterm`, `warp`, `cmux`, `terminal` - Windows: `wt`, `warp` Backend notes: - Warp opens a new window only; reject `--tab` if `warp` is selected. +- cmux creates and focuses a new workspace, which is cmux's tab primitive. - `--tmux` requires `tmux` on `PATH`. On Windows, only use it if your shell already supports `tmux`. - When `--tmux` is used, the launcher also adds `--teammate-mode tmux` to match the current local `sonnet` wrapper behavior. - If `ATM_TEAM` is set and `--identity` is supplied, the launcher registers the member first with `atm teams add-member --model sonnet --cwd `. diff --git a/packages/sc-launch-term/commands/sc/terra.md b/packages/sc-launch-term/commands/sc/terra.md new file mode 100644 index 00000000..98192a87 --- /dev/null +++ b/packages/sc-launch-term/commands/sc/terra.md @@ -0,0 +1,15 @@ +--- +description: Launch Codex with the GPT-5.6 Terra model in a supported terminal +argument-hint: "[dir] [--terminal ] [--tab] [--tmux] [--identity ] [-- ]" +allowed-tools: Bash, AskUserQuestion +--- + +Launch Codex with model alias `terra` using the shared Codex model-launch flow. + +Use the same argument contract and tmux collision handling as `/sc:codex`, but invoke: + +```bash +.claude/scripts/sc-term-launch.sh launch-codex-model terra "" +``` + +The shared launcher resolves a missing identity from the Terra sci-fi/cyberpunk name pool and runs the model as `gpt-5.6-terra`. diff --git a/packages/sc-launch-term/manifest.yaml b/packages/sc-launch-term/manifest.yaml index a24c5c66..4587b765 100644 --- a/packages/sc-launch-term/manifest.yaml +++ b/packages/sc-launch-term/manifest.yaml @@ -1,8 +1,9 @@ name: sc-launch-term version: 0.12.0 description: > - Launch Claude, Codex, and Gemini sessions in supported terminals with - platform-aware terminal autodetect and optional tmux session management. + Launch Claude (including Fable), Codex (including Sol, Terra, and Luna), and + Gemini sessions in supported terminals with platform-aware autodetect, cmux + workspace tabs, and optional tmux session management. author: randlee license: MIT tags: @@ -11,8 +12,13 @@ tags: - macos - windows - tmux + - cmux - claude + - fable - codex + - sol + - terra + - luna - gemini artifacts: @@ -20,6 +26,10 @@ artifacts: - commands/sc/sonnet.md - commands/sc/haiku.md - commands/sc/opus.md + - commands/sc/fable.md + - commands/sc/sol.md + - commands/sc/terra.md + - commands/sc/luna.md - commands/sc/codex.md - commands/sc/gemini.md scripts: diff --git a/packages/sc-launch-term/scripts/launch_term_shared.py b/packages/sc-launch-term/scripts/launch_term_shared.py index 43ac56f6..809b1e78 100644 --- a/packages/sc-launch-term/scripts/launch_term_shared.py +++ b/packages/sc-launch-term/scripts/launch_term_shared.py @@ -7,7 +7,6 @@ import os import secrets import shlex -import sys from pathlib import Path @@ -42,26 +41,125 @@ def resolve_team() -> str | None: # Per-tool identity name pools. _IDENTITY_NAMES: dict[str, list[str]] = { - "gemini": ["Gamma", "Galaxian", "Grimaldi", "Ganymede", "Grogu", - "Grievous", "Greedo", "Glitch", "Gauss", "Glimmer"], - "codex": ["Cipher", "Crypto", "Cryptex", "Cassian", "Crimson", - "Cloud", "Citan", "Caesar", "Cascade", "Chewbacca"], - "haiku": ["Homer", "Helix", "Helios", "Horus", "Halo", - "Hal", "Hex", "Hive", "Heretic", "Hydra"], - "sonnet": ["Sinatra", "Santana", "Slash", "Spock", "Skynet", - "Striker", "Siren", "Sentinel", "Sting", "Synth"], - "opus": ["Orion", "Oracle", "Omega", "Orwell", "Obiwan", - "Optimus", "Oblivion", "Onyx", "Odyssey", "Octane"], + "gemini": [ + "Gamma", + "Galaxian", + "Grimaldi", + "Ganymede", + "Grogu", + "Grievous", + "Greedo", + "Glitch", + "Gauss", + "Glimmer", + ], + "haiku": [ + "Homer", + "Helix", + "Helios", + "Horus", + "Halo", + "Hal", + "Hex", + "Hive", + "Heretic", + "Hydra", + ], + "sonnet": [ + "Sinatra", + "Santana", + "Slash", + "Spock", + "Skynet", + "Striker", + "Siren", + "Sentinel", + "Sting", + "Synth", + ], + "opus": [ + "Orion", + "Oracle", + "Omega", + "Orwell", + "Obiwan", + "Optimus", + "Oblivion", + "Onyx", + "Odyssey", + "Octane", + ], + "fable": [ + "Fable", + "Folklore", + "Fiction", + "Fairytale", + "Fantasy", + "Fathom", + "Fresco", + "Fableton", + "Figment", + "Fablewood", + ], + # Codex model identities use the model alias initial and a sci-fi/cyberpunk + # vocabulary. The deprecated `codex` alias is normalized to `terra` below. + "sol": [ + "Smith", + "Switch", + "Spoon", + "Saito", + "Sark", + "Seraph", + "Sombra", + "Synth", + "Specter", + "Sentinel", + ], + "terra": [ + "Trinity", + "Tank", + "Turing", + "Tyrell", + "Tessier", + "Talon", + "Tracer", + "Titan", + "Terminal", + "Turin", + ], + "luna": [ + "Lamb", + "Link", + "Lucy", + "Lyra", + "Lambda", + "Lazarus", + "Lumen", + "Lotus", + "Lucid", + "Legion", + ], } +_IDENTITY_ALIASES = {"codex": "terra"} + _FALLBACK_NAMES: list[str] = [ - "Amber", "Bold", "Calm", "Deft", "Eager", - "Fleet", "Gold", "Hale", "Iron", "Jade", + "Amber", + "Bold", + "Calm", + "Deft", + "Eager", + "Fleet", + "Gold", + "Hale", + "Iron", + "Jade", ] def generate_identity(tool: str | None = None) -> str: - pool = _IDENTITY_NAMES.get(tool or "", _FALLBACK_NAMES) + pool_key = _IDENTITY_ALIASES.get(tool or "", tool or "") + pool = _IDENTITY_NAMES.get(pool_key, _FALLBACK_NAMES) name = secrets.choice(pool) suffix = secrets.token_hex(2) return f"{name}-{suffix}" @@ -104,11 +202,15 @@ def ulid_timestamp(launch_id: str) -> datetime: def session_filename_from_launch_id(launch_id: str) -> str: timestamp = ulid_timestamp(launch_id) - prefix = timestamp.strftime("%Y%m%d%H%M%S") + f"{int(timestamp.microsecond / 1000):03d}" + prefix = ( + timestamp.strftime("%Y%m%d%H%M%S") + f"{int(timestamp.microsecond / 1000):03d}" + ) return f"{prefix}-{launch_id}.json" -def build_session_record_path(project_dir: str | Path, tool: str, launch_id: str) -> Path: +def build_session_record_path( + project_dir: str | Path, tool: str, launch_id: str +) -> Path: root = Path(project_dir).expanduser().resolve() return root / ".sc" / "sessions" / tool / session_filename_from_launch_id(launch_id) diff --git a/packages/sc-launch-term/scripts/sc-term-launch.py b/packages/sc-launch-term/scripts/sc-term-launch.py index e685ec22..61afc1e3 100644 --- a/packages/sc-launch-term/scripts/sc-term-launch.py +++ b/packages/sc-launch-term/scripts/sc-term-launch.py @@ -17,7 +17,7 @@ sys.path.insert(0, str(Path(__file__).resolve().parent)) from launch_term_shared import ( - _IDENTITY_NAMES, + _IDENTITY_NAMES, # noqa: F401 - retained as a module-level identity-pool view build_claude_session_record_path, build_codex_session_record_path, generate_ulid, @@ -30,10 +30,16 @@ ) -MAC_TERMINALS = ("iterm2", "ghostty", "wezterm", "warp", "terminal") +MAC_TERMINALS = ("iterm2", "ghostty", "wezterm", "warp", "cmux", "terminal") WINDOWS_TERMINALS = ("wt", "warp") -CLAUDE_MODELS = ("sonnet", "haiku", "opus") -TEAM_MEMBER_MODELS = CLAUDE_MODELS + ("codex", "gemini") +CLAUDE_MODELS = ("sonnet", "haiku", "opus", "fable") +CODEX_MODELS = ("sol", "terra", "luna", "codex") +CODEX_MODEL_IDS = { + "sol": "gpt-5.6-sol", + "terra": "gpt-5.6-terra", + "luna": "gpt-5.6-luna", +} +TEAM_MEMBER_MODELS = CLAUDE_MODELS + CODEX_MODELS + ("gemini",) MACOS_SHELL_SETTLE_DELAY_SECONDS = 0.8 @@ -73,7 +79,9 @@ def warp_launch_config_dir() -> Path: if os.name == "nt": appdata = os.environ.get("APPDATA") if not appdata: - fail("APPDATA is not set; cannot locate Warp launch configuration directory") + fail( + "APPDATA is not set; cannot locate Warp launch configuration directory" + ) return Path(appdata) / "warp" / "Warp" / "data" / "launch_configurations" fail("Warp launch configuration automation is only supported on macOS and Windows") @@ -89,6 +97,8 @@ def available_terminals() -> list[str]: available.append("wezterm") if detect_macos_app("Warp"): available.append("warp") + if command_exists("cmux"): + available.append("cmux") if detect_macos_app("Terminal"): available.append("terminal") return available @@ -118,7 +128,9 @@ def resolve_terminal(requested: str | None) -> str: "windows-terminal": "wt", "terminal.app": "terminal", } - requested_value = aliases.get((requested or "auto").lower(), (requested or "auto").lower()) + requested_value = aliases.get( + (requested or "auto").lower(), (requested or "auto").lower() + ) supported = platform_supported_terminals() available = available_terminals() @@ -295,7 +307,9 @@ def launch_wezterm(shell_command: str, dir_path: str, use_tab: bool) -> None: start_cmd.extend(["--cwd", dir_path]) subprocess.run(start_cmd, check=True) time.sleep(0.35) - subprocess.run(["wezterm", "cli", "send-text", "--no-paste", shell_command], check=True) + subprocess.run( + ["wezterm", "cli", "send-text", "--no-paste", shell_command], check=True + ) subprocess.run(["wezterm", "cli", "send-text", "--no-paste", "\n"], check=True) @@ -323,7 +337,9 @@ def write_warp_launch_config(command: str, dir_path: str, title: str) -> Path: def launch_warp(shell_command: str, dir_path: str, use_tab: bool, title: str) -> None: if use_tab: - fail("Warp automation currently supports new-window launches only; omit --tab or choose another terminal") + fail( + "Warp automation currently supports new-window launches only; omit --tab or choose another terminal" + ) config_path = write_warp_launch_config(shell_command, dir_path, title) uri = f"warp://launch/{urllib.parse.quote(str(config_path), safe='')}" @@ -336,6 +352,26 @@ def launch_warp(shell_command: str, dir_path: str, use_tab: bool, title: str) -> fail("Warp automation is only supported on macOS and Windows") +def launch_cmux(shell_command: str, dir_path: str, use_tab: bool, title: str) -> None: + """Launch a command in a cmux workspace, which is cmux's tab primitive.""" + del use_tab # cmux workspaces are always tab-like targets. + subprocess.run( + [ + "cmux", + "new-workspace", + "--cwd", + dir_path, + "--name", + title, + "--command", + shell_command, + "--focus", + "true", + ], + check=True, + ) + + def preferred_windows_shell() -> list[str]: if command_exists("pwsh"): return ["pwsh", "-NoExit", "-Command"] @@ -353,7 +389,9 @@ def launch_windows_terminal(shell_command: str, dir_path: str, use_tab: bool) -> subprocess.run(command, check=True) -def run_launch(terminal: str, shell_command: str, dir_path: str, use_tab: bool, title: str) -> None: +def run_launch( + terminal: str, shell_command: str, dir_path: str, use_tab: bool, title: str +) -> None: if terminal == "iterm2": launch_iterm2(shell_command, use_tab) return @@ -369,6 +407,9 @@ def run_launch(terminal: str, shell_command: str, dir_path: str, use_tab: bool, if terminal == "warp": launch_warp(shell_command, dir_path, use_tab, title) return + if terminal == "cmux": + launch_cmux(shell_command, dir_path, use_tab, title) + return if terminal == "wt": launch_windows_terminal(shell_command, dir_path, use_tab) return @@ -416,7 +457,9 @@ def render_command_argv(command_argv: list[str], terminal: str) -> str: return shlex.join(command_argv) -def build_claude_argv(model: str, extra_args: list[str], teammate_mode: bool) -> list[str]: +def build_claude_argv( + model: str, extra_args: list[str], teammate_mode: bool +) -> list[str]: command = ["claude", "--model", model, "--dangerously-skip-permissions"] if teammate_mode: command.extend(["--teammate-mode", "tmux"]) @@ -424,6 +467,28 @@ def build_claude_argv(model: str, extra_args: list[str], teammate_mode: bool) -> return command +def canonical_codex_model(model: str) -> str: + """Normalize the deprecated Codex alias to the Terra model identity.""" + return "terra" if model == "codex" else model + + +def codex_model_id(model: str) -> str: + return CODEX_MODEL_IDS[canonical_codex_model(model)] + + +def build_codex_argv(model: str, extra_args: list[str]) -> list[str]: + command = [ + "codex", + "--model", + codex_model_id(model), + "--yolo", + "--enable", + "hooks", + ] + command.extend(extra_args) + return command + + def apply_env_prefix( command: str, terminal: str, @@ -458,7 +523,9 @@ def apply_atm_env_prefix( return apply_env_prefix(command, terminal, env_vars) -def wait_for_path(path: Path, timeout_seconds: float = 5.0, interval_seconds: float = 0.25) -> bool: +def wait_for_path( + path: Path, timeout_seconds: float = 5.0, interval_seconds: float = 0.25 +) -> bool: deadline = time.monotonic() + timeout_seconds while time.monotonic() < deadline: if path.exists(): @@ -511,21 +578,140 @@ def session_tracking_for_member_model( member_model: str | None, dir_path: str, ) -> tuple[str, Path] | tuple[None, None]: - if member_model == "codex": + if member_model in CODEX_MODELS: launch_id = generate_ulid() return launch_id, build_codex_session_record_path(dir_path, launch_id) return None, None +def model_session_record_path(tool: str, dir_path: str, launch_id: str) -> Path: + if tool == "claude": + return build_claude_session_record_path(dir_path, launch_id) + if tool == "codex": + return build_codex_session_record_path(dir_path, launch_id) + fail(f"Unsupported model-launch tool: {tool}") + + +def build_model_argv( + tool: str, + model: str, + extra_args: list[str], + teammate_mode: bool, +) -> list[str]: + if tool == "claude": + return build_claude_argv(model, extra_args, teammate_mode) + if tool == "codex": + return build_codex_argv(model, extra_args) + fail(f"Unsupported model-launch tool: {tool}") + + +def canonical_model_label(tool: str, model: str) -> str: + if tool == "codex": + return canonical_codex_model(model) + return model + + +def handle_launch_model(args: argparse.Namespace, tool: str) -> None: + terminal = resolve_terminal(args.terminal) + if args.tmux and not tmux_available(): + fail("tmux is not available on PATH; omit --tmux or install tmux") + + model = canonical_model_label(tool, args.model) + team = resolve_team() + identity = resolve_identity(args.identity, model) + register_team_member(team, identity, model, args.dir) + launch_id = generate_ulid() + session_record = model_session_record_path(tool, args.dir, launch_id) + extra_args = normalize_passthrough_args(getattr(args, "model_args", [])) + command = render_command_argv( + build_model_argv(tool, model, extra_args, teammate_mode=bool(args.tmux)), + terminal, + ) + env_vars = { + "SC_LAUNCH_ID": launch_id, + "SC_SESSION_RECORD": str(session_record), + } + if team: + env_vars["ATM_TEAM"] = team + if identity: + env_vars["ATM_IDENTITY"] = identity + command = apply_env_prefix(command, terminal, env_vars) + shell_command = prepare_shell_command(terminal, command, args.dir, args.tmux) + run_launch(terminal, shell_command, args.dir, args.tab, title_from_label(model)) + result = { + "ok": True, + "tool": tool, + "model": model, + "launch_id": launch_id, + "session_record": str(session_record), + "session_record_found": wait_for_path(session_record), + } + if tool == "codex": + result["model_id"] = codex_model_id(model) + emit_json(result) + + +def handle_attach_pane_model(args: argparse.Namespace, tool: str) -> None: + terminal = resolve_terminal(args.terminal) + if not tmux_available(): + fail("tmux is not available on PATH; cannot attach to a session") + + model = canonical_model_label(tool, args.model) + team = resolve_team() + identity = resolve_identity(args.identity, model) + register_team_member(team, identity, model, args.cwd) + launch_id = generate_ulid() + session_record = model_session_record_path(tool, args.cwd, launch_id) + extra_args = normalize_passthrough_args(getattr(args, "model_args", [])) + command = render_command_argv( + build_model_argv(tool, model, extra_args, teammate_mode=True), + terminal, + ) + env_vars = { + "SC_LAUNCH_ID": launch_id, + "SC_SESSION_RECORD": str(session_record), + } + if team: + env_vars["ATM_TEAM"] = team + if identity: + env_vars["ATM_IDENTITY"] = identity + command = apply_env_prefix(command, terminal, env_vars) + run_launch( + terminal, + build_tmux_attach_pane(args.session, command), + args.cwd, + args.tab, + f"sc-launch-term pane {model}", + ) + result = { + "ok": True, + "tool": tool, + "model": model, + "launch_id": launch_id, + "session_record": str(session_record), + "session_record_found": wait_for_path(session_record), + } + if tool == "codex": + result["model_id"] = codex_model_id(model) + emit_json(result) + + def handle_launch(args: argparse.Namespace) -> None: terminal = resolve_terminal(args.terminal) if args.tmux and not tmux_available(): fail("tmux is not available on PATH; omit --tmux or install tmux") team = resolve_team() - identity = resolve_identity(args.identity, args.member_model) - register_team_member(team, identity, args.member_model, args.dir) - launch_id, session_record = session_tracking_for_member_model(args.member_model, args.dir) + member_model = ( + canonical_model_label("codex", args.member_model) + if args.member_model in CODEX_MODELS + else args.member_model + ) + identity = resolve_identity(args.identity, member_model) + register_team_member(team, identity, member_model, args.dir) + launch_id, session_record = session_tracking_for_member_model( + member_model, args.dir + ) env_vars: dict[str, str] = {} if launch_id and session_record: env_vars["SC_LAUNCH_ID"] = launch_id @@ -536,12 +722,14 @@ def handle_launch(args: argparse.Namespace) -> None: env_vars["ATM_IDENTITY"] = identity command = apply_env_prefix(args.command, terminal, env_vars) shell_command = prepare_shell_command(terminal, command, args.dir, args.tmux) - run_launch(terminal, shell_command, args.dir, args.tab, title_from_command(args.command)) + run_launch( + terminal, shell_command, args.dir, args.tab, title_from_command(args.command) + ) if launch_id and session_record: emit_json( { "ok": True, - "tool": args.member_model, + "tool": member_model, "launch_id": launch_id, "session_record": str(session_record), "session_record_found": wait_for_path(session_record), @@ -569,7 +757,9 @@ def handle_attach_pane(args: argparse.Namespace) -> None: team = resolve_team() identity = resolve_identity(args.identity, args.member_model) register_team_member(team, identity, args.member_model, args.cwd) - launch_id, session_record = session_tracking_for_member_model(args.member_model, args.cwd) + launch_id, session_record = session_tracking_for_member_model( + args.member_model, args.cwd + ) env_vars: dict[str, str] = {} if launch_id and session_record: env_vars["SC_LAUNCH_ID"] = launch_id @@ -599,84 +789,19 @@ def handle_attach_pane(args: argparse.Namespace) -> None: def handle_launch_claude_model(args: argparse.Namespace) -> None: - terminal = resolve_terminal(args.terminal) - if args.tmux and not tmux_available(): - fail("tmux is not available on PATH; omit --tmux or install tmux") - - team = resolve_team() - identity = resolve_identity(args.identity, args.model) - register_team_member(team, identity, args.model, args.dir) - launch_id = generate_ulid() - session_record = build_claude_session_record_path(args.dir, launch_id) - extra_args = normalize_passthrough_args(getattr(args, "claude_args", [])) - command = render_command_argv( - build_claude_argv(args.model, extra_args, teammate_mode=bool(args.tmux)), - terminal, - ) - env_vars = { - "SC_LAUNCH_ID": launch_id, - "SC_SESSION_RECORD": str(session_record), - } - if team: - env_vars["ATM_TEAM"] = team - if identity: - env_vars["ATM_IDENTITY"] = identity - command = apply_env_prefix(command, terminal, env_vars) - shell_command = prepare_shell_command(terminal, command, args.dir, args.tmux) - run_launch(terminal, shell_command, args.dir, args.tab, title_from_label(args.model)) - emit_json( - { - "ok": True, - "tool": "claude", - "model": args.model, - "launch_id": launch_id, - "session_record": str(session_record), - "session_record_found": wait_for_path(session_record), - } - ) + handle_launch_model(args, "claude") def handle_attach_pane_claude_model(args: argparse.Namespace) -> None: - terminal = resolve_terminal(args.terminal) - if not tmux_available(): - fail("tmux is not available on PATH; cannot attach a pane") + handle_attach_pane_model(args, "claude") - team = resolve_team() - identity = resolve_identity(args.identity, args.model) - register_team_member(team, identity, args.model, args.cwd) - launch_id = generate_ulid() - session_record = build_claude_session_record_path(args.cwd, launch_id) - extra_args = normalize_passthrough_args(getattr(args, "claude_args", [])) - command = render_command_argv( - build_claude_argv(args.model, extra_args, teammate_mode=True), - terminal, - ) - env_vars = { - "SC_LAUNCH_ID": launch_id, - "SC_SESSION_RECORD": str(session_record), - } - if team: - env_vars["ATM_TEAM"] = team - if identity: - env_vars["ATM_IDENTITY"] = identity - command = apply_env_prefix(command, terminal, env_vars) - run_launch( - terminal, - build_tmux_attach_pane(args.session, command), - os.getcwd(), - args.tab, - f"sc-launch-term pane {args.session}", - ) - emit_json( - { - "ok": True, - "tool": "claude", - "model": args.model, - "launch_id": launch_id, - "session_record": str(session_record), - "session_record_found": wait_for_path(session_record), - } - ) + +def handle_launch_codex_model(args: argparse.Namespace) -> None: + handle_launch_model(args, "codex") + + +def handle_attach_pane_codex_model(args: argparse.Namespace) -> None: + handle_attach_pane_model(args, "codex") def split_passthrough_argv(argv: list[str]) -> tuple[list[str], list[str]]: @@ -715,6 +840,17 @@ def build_parser() -> argparse.ArgumentParser: launch_claude_model.add_argument("--tmux") launch_claude_model.add_argument("--identity") + launch_codex_model = subparsers.add_parser( + "launch-codex-model", + help="Launch a Codex model with the standard wrapper flags", + ) + launch_codex_model.add_argument("model", choices=CODEX_MODELS) + launch_codex_model.add_argument("dir") + launch_codex_model.add_argument("--terminal") + launch_codex_model.add_argument("--tab", action="store_true") + launch_codex_model.add_argument("--tmux") + launch_codex_model.add_argument("--identity") + attach = subparsers.add_parser("attach", help="Attach to an existing tmux session") attach.add_argument("session") attach.add_argument("--terminal") @@ -742,6 +878,17 @@ def build_parser() -> argparse.ArgumentParser: attach_pane_claude_model.add_argument("--tab", action="store_true") attach_pane_claude_model.add_argument("--identity") + attach_pane_codex_model = subparsers.add_parser( + "attach-pane-codex-model", + help="Split a tmux session and launch a Codex model in the new pane", + ) + attach_pane_codex_model.add_argument("session") + attach_pane_codex_model.add_argument("model", choices=CODEX_MODELS) + attach_pane_codex_model.add_argument("--cwd", default=os.getcwd()) + attach_pane_codex_model.add_argument("--terminal") + attach_pane_codex_model.add_argument("--tab", action="store_true") + attach_pane_codex_model.add_argument("--identity") + return parser @@ -749,10 +896,17 @@ def main() -> None: parser = build_parser() argv, passthrough_args = split_passthrough_argv(sys.argv[1:]) args = parser.parse_args(argv) - if args.subcommand in {"launch-claude-model", "attach-pane-claude-model"}: - args.claude_args = passthrough_args + if args.subcommand in { + "launch-claude-model", + "attach-pane-claude-model", + "launch-codex-model", + "attach-pane-codex-model", + }: + args.model_args = passthrough_args elif passthrough_args: - parser.error("passthrough args after -- are only supported for Claude model launches") + parser.error( + "passthrough args after -- are only supported for Claude or Codex model launches" + ) if args.subcommand == "detect": detect_command() @@ -766,6 +920,9 @@ def main() -> None: if args.subcommand == "launch-claude-model": handle_launch_claude_model(args) return + if args.subcommand == "launch-codex-model": + handle_launch_codex_model(args) + return if args.subcommand == "attach": handle_attach(args) return @@ -775,6 +932,9 @@ def main() -> None: if args.subcommand == "attach-pane-claude-model": handle_attach_pane_claude_model(args) return + if args.subcommand == "attach-pane-codex-model": + handle_attach_pane_codex_model(args) + return parser.error(f"unknown subcommand: {args.subcommand}") diff --git a/packages/sc-launchpad/.claude-plugin/plugin.json b/packages/sc-launchpad/.claude-plugin/plugin.json index e036bcd9..58883e76 100644 --- a/packages/sc-launchpad/.claude-plugin/plugin.json +++ b/packages/sc-launchpad/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "sc-launchpad", - "description": "Launch Claude, Codex, or Gemini through a separate background sub-agent runtime with explicit ATM teammate-mode handling.", + "description": "Launch Claude (including Fable), Codex (including Sol, Terra, and Luna), or Gemini as a separate background sub-agent runtime with explicit ATM teammate-mode normalization and roster registration.", "version": "0.12.0", "author": { "name": "synaptic-canvas" @@ -10,7 +10,11 @@ "background-agents", "claude", "codex", + "sol", + "terra", + "luna", "gemini", + "fable", "atm" ], "commands": [], diff --git a/packages/sc-launchpad/CHANGELOG.md b/packages/sc-launchpad/CHANGELOG.md index 916cfa5d..19eed879 100644 --- a/packages/sc-launchpad/CHANGELOG.md +++ b/packages/sc-launchpad/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## [Unreleased] +### Added +- Added Claude Fable model aliases to the background launch runtime. +- Added Sol, Terra, and Luna Codex model aliases with Terra routing for deprecated `codex`. + +### Fixed +- Use the installed Codex CLI's supported `--yolo` automation flag for background launches. + ## [0.10.0] - 2026-04-25 ### Added - Initial `sc-launchpad` release with a background-launch skill and thin forwarding agent. diff --git a/packages/sc-launchpad/README.md b/packages/sc-launchpad/README.md index 2ac0502f..7d089cc6 100644 --- a/packages/sc-launchpad/README.md +++ b/packages/sc-launchpad/README.md @@ -1,6 +1,6 @@ # sc-launchpad -Launch Claude, Codex, or Gemini as a background sub-agent runtime without +Launch Claude (including the `fable` model), Codex (including `sol`, `terra`, and `luna`), or Gemini as a background sub-agent runtime without opening a visible terminal window. ## Included Artifacts @@ -51,6 +51,14 @@ Otherwise, the runtime clears both ATM variables before spawning the child. - The runtime expects absolute working-directory paths in launch payloads. - This package depends on local Python 3 plus whichever AI CLI you are launching. +For Claude launches, `model` accepts `sonnet`, `haiku`, `opus`, or `fable`, +plus the corresponding supported full model names. + +For Codex launches, `model` accepts `sol`, `terra`, or `luna`. The deprecated +`codex` alias routes to `gpt-5.6-terra`; the runtime also accepts the known +legacy Codex aliases for compatibility. The canonical model IDs are +`gpt-5.6-sol`, `gpt-5.6-terra`, and `gpt-5.6-luna`. + ## Installation ```bash @@ -67,8 +75,9 @@ surface where orchestrator skills or agents need background launch support. "description": "Run a background code review", "prompt": "Review the changed files for risks and missing tests.", "tool": "codex", + "model": "terra", "cwd": "/abs/path/to/repo", "atm_identity": "reviewer-1", - "extra_args": ["--model", "gpt-5.5"] + "extra_args": ["--skip-git-repo-check"] } ``` diff --git a/packages/sc-launchpad/manifest.yaml b/packages/sc-launchpad/manifest.yaml index b1b5aff3..b061ad4a 100644 --- a/packages/sc-launchpad/manifest.yaml +++ b/packages/sc-launchpad/manifest.yaml @@ -1,15 +1,21 @@ name: sc-launchpad version: 0.12.0 description: > - Launch Claude, Codex, or Gemini as a separate background sub-agent runtime, - with explicit ATM teammate-mode normalization and roster registration. + Launch Claude (including Fable), Codex (including Sol, Terra, and Luna), or + Gemini as a separate background + sub-agent runtime, with explicit ATM teammate-mode normalization and roster + registration. author: synaptic-canvas license: MIT tags: - background-agents - claude - codex + - sol + - terra + - luna - gemini + - fable - atm artifacts: diff --git a/packages/sc-launchpad/scripts/sc_launchpad_task.py b/packages/sc-launchpad/scripts/sc_launchpad_task.py index 4d0506b5..4a570148 100644 --- a/packages/sc-launchpad/scripts/sc_launchpad_task.py +++ b/packages/sc-launchpad/scripts/sc_launchpad_task.py @@ -20,9 +20,16 @@ ToolName = Literal["claude", "codex", "gemini"] -ClaudeModel = Literal["sonnet", "haiku", "opus"] +ClaudeModel = Literal["sonnet", "haiku", "opus", "fable"] CodexModel = Literal[ "codex", + "sol", + "terra", + "luna", + "gpt-5.6-sol", + "gpt-5.6-terra", + "gpt-5.6-luna", + "gpt-5.6", "codex-max", "max", "codex-mini", @@ -35,7 +42,16 @@ ] CODEX_MODEL_MAP = { - "codex": "gpt-5.2-codex", + # `codex` remains a deprecated compatibility alias for Terra. + "codex": "gpt-5.6-terra", + "sol": "gpt-5.6-sol", + "terra": "gpt-5.6-terra", + "luna": "gpt-5.6-luna", + "gpt-5.6-sol": "gpt-5.6-sol", + "gpt-5.6-terra": "gpt-5.6-terra", + "gpt-5.6-luna": "gpt-5.6-luna", + "gpt-5.6": "gpt-5.6-sol", + # Preserve explicit legacy model requests without using them as defaults. "gpt-5.2-codex": "gpt-5.2-codex", "codex-max": "gpt-5.1-codex-max", "max": "gpt-5.1-codex-max", @@ -47,13 +63,22 @@ "gpt-5.2": "gpt-5.2", } +CODEX_ROSTER_MODEL_MAP = { + "gpt-5.6-sol": "sol", + "gpt-5.6-terra": "terra", + "gpt-5.6-luna": "luna", +} + CLAUDE_MODEL_MAP = { "sonnet": "sonnet", "haiku": "haiku", "opus": "opus", + "fable": "fable", "claude-sonnet-4-6": "sonnet", "claude-haiku-4-5": "haiku", "claude-opus-4-1": "opus", + "claude-fable-5": "fable", + "claude-fable-5[1m]": "fable", } @@ -107,7 +132,9 @@ def error_response(error: LaunchpadError, data: dict | None = None) -> dict: def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--json", action="store_true", help="Treat remaining args as JSON payload") + parser.add_argument( + "--json", action="store_true", help="Treat remaining args as JSON payload" + ) parser.add_argument("args", nargs="*", help="JSON payload") return parser.parse_args() @@ -156,13 +183,13 @@ def normalize_tool_model(payload: LaunchpadInput) -> str | None: return normalized if payload.tool == "codex": if payload.model is None: - return "gpt-5.2-codex" + return "gpt-5.6-terra" normalized = CODEX_MODEL_MAP.get(payload.model) if normalized is None: raise LaunchpadError( "VALIDATION.INVALID_MODEL", f"Unsupported Codex model: {payload.model}", - suggested_action="Use codex, max, mini, gpt-5, or a known Codex model alias.", + suggested_action="Use sol, terra, luna, codex (deprecated), or a known Codex model alias.", ) return normalized return payload.model @@ -171,6 +198,10 @@ def normalize_tool_model(payload: LaunchpadInput) -> str | None: def roster_model(payload: LaunchpadInput, normalized_model: str | None) -> str: if payload.tool == "claude": return normalized_model or "sonnet" + if payload.tool == "codex": + return CODEX_ROSTER_MODEL_MAP.get( + normalized_model or "", normalized_model or "terra" + ) return payload.tool @@ -209,7 +240,11 @@ def maybe_add_member( # TODO: When pane-aware launchpad mode exists, append `--pane-id ` here. result = subprocess.run(command, text=True, capture_output=True, env=parent_env) if result.returncode != 0: - message = result.stderr.strip() or result.stdout.strip() or "atm teams add-member failed" + message = ( + result.stderr.strip() + or result.stdout.strip() + or "atm teams add-member failed" + ) raise LaunchpadError( "ATM.ADD_MEMBER_FAILED", message, @@ -227,7 +262,7 @@ def build_command(payload: LaunchpadInput, normalized_model: str | None) -> list command.append(payload.prompt) return command if payload.tool == "codex": - command = ["codex", "exec", "--full-auto"] + command = ["codex", "exec", "--yolo"] if normalized_model: command.extend(["--model", normalized_model]) command.extend(extra) @@ -244,8 +279,12 @@ def run_payload(payload: LaunchpadInput, parent_env: dict[str, str]) -> dict: cwd = resolve_cwd(payload.cwd) ensure_command(payload.tool) normalized_model = normalize_tool_model(payload) - teammate_mode, team, identity = resolve_teammate_mode(parent_env, payload.atm_identity) - maybe_add_member(parent_env, teammate_mode, team, identity, payload, normalized_model, cwd) + teammate_mode, team, identity = resolve_teammate_mode( + parent_env, payload.atm_identity + ) + maybe_add_member( + parent_env, teammate_mode, team, identity, payload, normalized_model, cwd + ) child_env = build_child_env(parent_env, teammate_mode, team, identity) command = build_command(payload, normalized_model) result = subprocess.run( diff --git a/packages/sc-launchpad/skills/launchpad/SKILL.md b/packages/sc-launchpad/skills/launchpad/SKILL.md index d6f21ec5..30b53876 100644 --- a/packages/sc-launchpad/skills/launchpad/SKILL.md +++ b/packages/sc-launchpad/skills/launchpad/SKILL.md @@ -47,6 +47,8 @@ Optional fields: - `atm_identity` - `extra_args` +For Claude launches, `model` accepts `sonnet`, `haiku`, `opus`, or `fable`. + ## ATM Rules Teammate mode is enabled only when both conditions are true: diff --git a/scripts/validate-script-references.py b/scripts/validate-script-references.py index d1b90d09..2037e860 100755 --- a/scripts/validate-script-references.py +++ b/scripts/validate-script-references.py @@ -130,8 +130,8 @@ def extract_frontmatter(file_path: str) -> Result[Dict[str, Any], ScriptReferenc match = re.match(pattern, content, re.DOTALL) if not match: - # No frontmatter is okay for some files - return Success(value={}, warnings=["No frontmatter found"]) + # No frontmatter is valid for files without hooks or metadata. + return Success(value={}, warnings=[]) raw_frontmatter = match.group(1) diff --git a/tests/scripts/test_sc_launchpad_task.py b/tests/scripts/test_sc_launchpad_task.py index 9856ade1..0247811c 100644 --- a/tests/scripts/test_sc_launchpad_task.py +++ b/tests/scripts/test_sc_launchpad_task.py @@ -3,6 +3,8 @@ from pathlib import Path import importlib.util +import pytest + spec = importlib.util.spec_from_file_location( "sc_launchpad_task", @@ -86,6 +88,18 @@ def test_build_command_for_claude(): ] +def test_normalize_fable_model_aliases(): + for model in ("fable", "claude-fable-5", "claude-fable-5[1m]"): + payload = sc_launchpad_task.LaunchpadInput( + description="Launch Fable", + prompt="Review the diff", + tool="claude", + model=model, + cwd="/tmp", + ) + assert sc_launchpad_task.normalize_tool_model(payload) == "fable" + + def test_build_command_for_codex(): payload = sc_launchpad_task.LaunchpadInput( description="Launch Codex", @@ -99,7 +113,7 @@ def test_build_command_for_codex(): assert command == [ "codex", "exec", - "--full-auto", + "--yolo", "--model", "gpt-5.1-codex-max", "--skip-git-repo-check", @@ -107,6 +121,55 @@ def test_build_command_for_codex(): ] +@pytest.mark.parametrize( + ("alias", "model_id"), + [ + ("sol", "gpt-5.6-sol"), + ("terra", "gpt-5.6-terra"), + ("luna", "gpt-5.6-luna"), + ("codex", "gpt-5.6-terra"), + ], +) +def test_normalize_codex_model_aliases(alias, model_id): + payload = sc_launchpad_task.LaunchpadInput( + description="Launch Codex", + prompt="Implement the fix", + tool="codex", + model=alias, + cwd="/tmp", + ) + assert sc_launchpad_task.normalize_tool_model(payload) == model_id + + +def test_build_command_for_terra_codex(): + payload = sc_launchpad_task.LaunchpadInput( + description="Launch Terra", + prompt="Implement the fix", + tool="codex", + model="terra", + cwd="/tmp", + ) + command = sc_launchpad_task.build_command(payload, "gpt-5.6-terra") + assert command == [ + "codex", + "exec", + "--yolo", + "--model", + "gpt-5.6-terra", + "Implement the fix", + ] + + +def test_codex_defaults_to_terra(): + payload = sc_launchpad_task.LaunchpadInput( + description="Launch Codex", + prompt="Implement the fix", + tool="codex", + cwd="/tmp", + ) + assert sc_launchpad_task.normalize_tool_model(payload) == "gpt-5.6-terra" + + def test_build_command_for_gemini(): payload = sc_launchpad_task.LaunchpadInput( description="Launch Gemini", @@ -129,11 +192,11 @@ def test_build_command_for_gemini(): ] -def test_roster_model_uses_tool_name_for_codex(): +def test_roster_model_uses_canonical_alias_for_codex(): payload = sc_launchpad_task.LaunchpadInput( description="Launch Codex", prompt="Implement the fix", tool="codex", cwd="/tmp", ) - assert sc_launchpad_task.roster_model(payload, "gpt-5.2-codex") == "codex" + assert sc_launchpad_task.roster_model(payload, "gpt-5.6-terra") == "terra" diff --git a/tests/scripts/test_sc_term_launch.py b/tests/scripts/test_sc_term_launch.py index a72a0484..78b68848 100644 --- a/tests/scripts/test_sc_term_launch.py +++ b/tests/scripts/test_sc_term_launch.py @@ -19,7 +19,9 @@ def test_build_claude_argv_without_tmux(): - command = sc_term_launch.build_claude_argv("sonnet", ["--continue"], teammate_mode=False) + command = sc_term_launch.build_claude_argv( + "sonnet", ["--continue"], teammate_mode=False + ) assert command == [ "claude", "--model", @@ -47,6 +49,38 @@ def test_build_claude_argv_with_tmux(): ] +def test_build_claude_argv_for_fable(): + command = sc_term_launch.build_claude_argv("fable", [], teammate_mode=False) + assert command == [ + "claude", + "--model", + "fable", + "--dangerously-skip-permissions", + ] + + +@pytest.mark.parametrize( + ("model", "model_id"), + [ + ("sol", "gpt-5.6-sol"), + ("terra", "gpt-5.6-terra"), + ("luna", "gpt-5.6-luna"), + ("codex", "gpt-5.6-terra"), + ], +) +def test_build_codex_argv_routes_models(model, model_id): + command = sc_term_launch.build_codex_argv(model, ["--search"]) + assert command == [ + "codex", + "--model", + model_id, + "--yolo", + "--enable", + "hooks", + "--search", + ] + + def test_render_command_argv_posix_quotes_spaces(): command = sc_term_launch.render_command_argv( ["claude", "--model", "opus", "--resume", "session with spaces"], @@ -86,6 +120,27 @@ def test_parser_collects_passthrough_claude_args(): assert passthrough == ["--continue", "--resume", "abc123"] +def test_parser_collects_passthrough_codex_args(): + argv, passthrough = sc_term_launch.split_passthrough_argv( + [ + "launch-codex-model", + "sol", + "/tmp/project", + "--terminal", + "cmux", + "--", + "--search", + ] + ) + parser = sc_term_launch.build_parser() + args = parser.parse_args(argv) + assert args.subcommand == "launch-codex-model" + assert args.model == "sol" + assert args.dir == "/tmp/project" + assert args.terminal == "cmux" + assert passthrough == ["--search"] + + def test_apply_atm_env_prefix_posix(): command = sc_term_launch.apply_atm_env_prefix( "claude --model sonnet", @@ -132,6 +187,16 @@ def test_resolve_identity_uses_tool_pool(monkeypatch): assert len(suffix) == 4 +@pytest.mark.parametrize("model", ["sol", "terra", "luna", "codex"]) +def test_resolve_identity_uses_codex_model_pool(monkeypatch, model): + monkeypatch.setenv("ATM_TEAM", "atm-core") + identity = sc_term_launch.resolve_identity(None, model) + name, suffix = identity.rsplit("-", 1) + pool_model = "terra" if model == "codex" else model + assert name in sc_term_launch._IDENTITY_NAMES[pool_model] + assert len(suffix) == 4 + + def test_generate_ulid_and_session_path(): launch_id = sc_term_launch.generate_ulid() path = sc_term_launch.build_claude_session_record_path("/tmp/project", launch_id) @@ -148,19 +213,38 @@ def test_generate_codex_session_path(): def test_session_tracking_for_codex_member_model(): - launch_id, session_record = sc_term_launch.session_tracking_for_member_model("codex", "/tmp/project") + launch_id, session_record = sc_term_launch.session_tracking_for_member_model( + "codex", "/tmp/project" + ) assert launch_id is not None assert session_record is not None - assert session_record.parent == Path("/tmp/project").resolve() / ".sc" / "sessions" / "codex" + assert ( + session_record.parent + == Path("/tmp/project").resolve() / ".sc" / "sessions" / "codex" + ) assert session_record.stem.endswith(launch_id) def test_session_tracking_for_non_codex_member_model(): - launch_id, session_record = sc_term_launch.session_tracking_for_member_model("gemini", "/tmp/project") + launch_id, session_record = sc_term_launch.session_tracking_for_member_model( + "gemini", "/tmp/project" + ) assert launch_id is None assert session_record is None +def test_session_tracking_for_codex_model_alias(): + launch_id, session_record = sc_term_launch.session_tracking_for_member_model( + "sol", "/tmp/project" + ) + assert launch_id is not None + assert session_record is not None + assert ( + session_record.parent + == Path("/tmp/project").resolve() / ".sc" / "sessions" / "codex" + ) + + def test_apply_env_prefix_with_session_tracking_posix(): command = sc_term_launch.apply_env_prefix( "claude --model haiku", @@ -174,3 +258,31 @@ def test_apply_env_prefix_with_session_tracking_posix(): command == "export SC_LAUNCH_ID=01JVY7YVYH57FHE2S2P0S8F3XW && export SC_SESSION_RECORD=/tmp/project/.sc/sessions/claude/session.json && claude --model haiku" ) + + +def test_launch_cmux_creates_focused_workspace(monkeypatch): + calls = [] + + def fake_run(command, *, check): + calls.append((command, check)) + + monkeypatch.setattr(sc_term_launch.subprocess, "run", fake_run) + sc_term_launch.launch_cmux("claude --model fable", "/tmp/project", True, "fable") + + assert calls == [ + ( + [ + "cmux", + "new-workspace", + "--cwd", + "/tmp/project", + "--name", + "fable", + "--command", + "claude --model fable", + "--focus", + "true", + ], + True, + ) + ] diff --git a/tests/scripts/test_validate_script_references.py b/tests/scripts/test_validate_script_references.py index 721a756e..e981f991 100644 --- a/tests/scripts/test_validate_script_references.py +++ b/tests/scripts/test_validate_script_references.py @@ -141,7 +141,7 @@ def test_extract_frontmatter_no_frontmatter(temp_dir): result = extract_frontmatter(str(file_path)) assert isinstance(result, Success) assert result.value == {} - assert "No frontmatter found" in result.warnings[0] + assert result.warnings == [] def test_extract_frontmatter_missing_file(temp_dir):