From c5de8383e70d8e2c178c870e240c7ac661915633 Mon Sep 17 00:00:00 2001 From: Devvyn <22340871+opticon454@users.noreply.github.com> Date: Sat, 10 Oct 2026 14:06:04 +0800 Subject: [PATCH 01/18] feat(cli-registry): GitHub Copilot CLI as a run mode Copilot (`copilot`, npm @github/copilot) as a stock CLI entry with the same launch options as the other agent CLIs: model (--model), resume (--resume), continue (--continue) and allow-all (--yolo, clamped off for non-granted multi-user owners). Adds a resolver and GET /api/copilot/status probe, the agent-image package, Run-menu/mobile/logo entries, skill docs and tests. --- .changeset/cli-copilot.md | 5 + config/clis.stock.json | 28 +++ docker/agent.Dockerfile | 2 +- docs/cli-registry.md | 50 +++--- install.sh | 26 +-- plugins/codeman/skills/codeman/SKILL.md | 2 +- .../skills/codeman/reference/endpoints.md | 12 +- .../skills/codeman/reference/messaging.md | 4 +- .../skills/codeman/reference/recipes.md | 2 +- .../codeman/skills/codeman/reference/verbs.md | 6 +- skills/codeman/SKILL.md | 2 +- skills/codeman/reference/endpoints.md | 12 +- skills/codeman/reference/messaging.md | 4 +- skills/codeman/reference/recipes.md | 2 +- skills/codeman/reference/verbs.md | 6 +- src/config/cli-registry/stock.ts | 160 +++++++++++++++++- src/mux-interface.ts | 3 + src/session-cli-registry-bridge.ts | 2 + src/session.ts | 13 ++ src/tmux-manager.ts | 6 + src/types/session.ts | 35 +++- src/utils/cli-installed-probes.ts | 5 +- src/utils/copilot-cli-resolver.ts | 139 +++++++++++++++ src/utils/index.ts | 6 + src/web/public/app.js | 12 +- src/web/public/index.html | 4 +- src/web/public/mobile-overview.js | 1 + src/web/public/mobile.css | 20 +++ src/web/public/session-ui.js | 16 +- src/web/public/styles.css | 25 +++ src/web/public/terminal-ui.js | 1 + src/web/routes/session-routes.ts | 22 ++- src/web/routes/system-routes.ts | 14 ++ src/web/schemas.ts | 21 +++ src/web/server.ts | 1 + test/cli-capability-predicates.test.ts | 3 +- test/cli-registry-no-id-branching.test.ts | 2 + test/cli-registry-spawn-golden.test.ts | 47 +++++ test/copilot-cli-resolver.test.ts | 132 +++++++++++++++ test/copilot-custom-endpoint.test.ts | 25 +++ test/docker-agent-image-coverage.test.ts | 2 +- test/install-sh-detection-parity.test.ts | 8 + test/location-overlay-commands.test.ts | 2 + test/mobile-overview.test.ts | 1 + test/render-index-html.test.ts | 8 + test/resume-history-mode-fidelity.test.ts | 1 + test/routes/external-cli-bypass-clamp.test.ts | 19 +++ test/run-button-mode-colours.test.ts | 27 +++ 48 files changed, 867 insertions(+), 79 deletions(-) create mode 100644 .changeset/cli-copilot.md create mode 100644 src/utils/copilot-cli-resolver.ts create mode 100644 test/copilot-cli-resolver.test.ts create mode 100644 test/copilot-custom-endpoint.test.ts create mode 100644 test/run-button-mode-colours.test.ts diff --git a/.changeset/cli-copilot.md b/.changeset/cli-copilot.md new file mode 100644 index 00000000..d38ec7b2 --- /dev/null +++ b/.changeset/cli-copilot.md @@ -0,0 +1,5 @@ +--- +'aicodeman': minor +--- + +GitHub Copilot CLI is a run mode. It appears in the Run menu, the welcome screen and the cron agent list once `copilot` is installed, runs in tmux like the other agent CLIs, reports working and idle from its `Working` footer, shows the model it reads off its footer on the tab, and is listed in CLI management with an install command (`npm install -g @github/copilot`). It takes the same launch options as the other agent CLIs: a `copilotConfig` on the session create and quick-start bodies with `model` (`--model`), `resumeSessionId` (`--resume`), `continueSession` (`--continue`) and `allowAll` (`--yolo`, which the Run button sends and which is clamped off for non-granted multi-user owners), history resume, and a `GET /api/copilot/status` probe. diff --git a/config/clis.stock.json b/config/clis.stock.json index c213976b..d19f1808 100644 --- a/config/clis.stock.json +++ b/config/clis.stock.json @@ -277,5 +277,33 @@ "docsUrl": "https://omp.sh" } } + }, + { + "id": "copilot", + "label": "GitHub Copilot", + "shortBadge": "CP", + "enabled": true, + "order": 100, + "kind": "agent", + "discovery": { + "binaries": [ + "copilot" + ], + "searchDirs": [ + "~/.local/bin", + "/usr/local/bin", + "~/.bun/bin", + "~/.npm-global/bin", + "~/bin" + ], + "install": { + "command": { + "linux": "npm install -g @github/copilot", + "darwin": "npm install -g @github/copilot" + }, + "npmPackage": "@github/copilot", + "docsUrl": "https://github.com/github/copilot-cli" + } + } } ] diff --git a/docker/agent.Dockerfile b/docker/agent.Dockerfile index afc9c6e1..c70e828b 100644 --- a/docker/agent.Dockerfile +++ b/docker/agent.Dockerfile @@ -126,7 +126,7 @@ RUN set -eux; \ # ⚠️ The default is in REGISTRY order, byte-identical to what the generator emits. # A different order is a different RUN string, which is a different layer hash and # so a needless cache miss between a bare `docker build` and a scripted one. -ARG CLI_NPM_PACKAGES="@anthropic-ai/claude-code opencode-ai @openai/codex @google/gemini-cli" +ARG CLI_NPM_PACKAGES="@anthropic-ai/claude-code opencode-ai @openai/codex @google/gemini-cli @github/copilot" # uv/uvx: MCP servers are commonly launched with `uvx ` (e.g. the Nginx # Proxy Manager MCP), and Codex failed to enable them with "uvx not found". Copied # from the pinned upstream image into root-owned /usr/local/bin, never pip-installed. diff --git a/docs/cli-registry.md b/docs/cli-registry.md index 53343f05..4e7d454c 100644 --- a/docs/cli-registry.md +++ b/docs/cli-registry.md @@ -1,18 +1,18 @@ # The CLI registry -Every run mode Codeman can launch — Claude Code, Terminal/Shell, OpenCode, Codex, Gemini, Antigravity, Pi, Grok, DeepSeek Harness and OMP — is a `CliEntry`: a data record describing how to find the binary, how to build its command line, what environment it needs, and what it can do. Code that used to ask "which CLI is this?" asks the entry instead. +Every run mode Codeman can launch — Claude Code, Terminal/Shell, OpenCode, Codex, Gemini, Antigravity, Pi, Grok, DeepSeek Harness, OMP and GitHub Copilot CLI — is a `CliEntry`: a data record describing how to find the binary, how to build its command line, what environment it needs, and what it can do. Code that used to ask "which CLI is this?" asks the entry instead. ## Where it lives -| File | What it holds | -| ------------- | ------------------------------------------------------------------------------------------------- | -| `types.ts` | The `CliEntry` interface and everything under it. Read this first. | -| `stock.ts` | The shipped catalog. **The only file allowed to name a CLI id.** | -| `schema.ts` | Zod validation, including the cross-field checks that reject an incoherent entry at LOAD time. | -| `argv.ts` | The argv engine: the only code that turns typed tokens into a command string. | -| `patterns.ts` | The NAMED value patterns (`model`, `uuid`, `path-segment`, …) and the regex-compilation guard. | +| File | What it holds | +| ------------- | --------------------------------------------------------------------------------------------------- | +| `types.ts` | The `CliEntry` interface and everything under it. Read this first. | +| `stock.ts` | The shipped catalog. **The only file allowed to name a CLI id.** | +| `schema.ts` | Zod validation, including the cross-field checks that reject an incoherent entry at LOAD time. | +| `argv.ts` | The argv engine: the only code that turns typed tokens into a command string. | +| `patterns.ts` | The NAMED value patterns (`model`, `uuid`, `path-segment`, …) and the regex-compilation guard. | | `profiles.ts` | The names of behaviours that genuinely need code, kept import-free so `schema.ts` can validate one. | -| `registry.ts` | Loading, merging `~/.codeman/clis.json`, and the accessors (`getCli`, `enabledClis`). | +| `registry.ts` | Loading, merging `~/.codeman/clis.json`, and the accessors (`getCli`, `enabledClis`). | `src/session-cli-registry-bridge.ts` maps the legacy per-mode option bag onto the engine, and `src/utils/cli-resolver.ts` / `src/utils/cli-launcher.ts` do registry-driven binary resolution and launcher-profile dispatch. @@ -28,7 +28,7 @@ App Settings → Agents & CLIs → **CLI management** (`cliManagementEnabled`, d - installs a missing **stock** CLI by running its shipped install command, after a confirm that names the exact command. Only one install per CLI runs at a time, and the command runs without any `CODEMAN_*` variable in its environment. An `npm install -g` command is pointed at `~/.local` (which every resolver searches) when the npm global prefix is not writable by the server user, for example a system node under `/usr`; a writable prefix, a prefix that does not exist yet but could be created, and an explicit `NPM_CONFIG_PREFIX` are left alone. A custom entry's install command is never executed. - adds, edits and deletes **custom** entries (id, label, badge, binaries, launch argv). The server re-validates the whole assembled entry through `CliEntrySchema`, so the form cannot bypass the load-time rules. -These are the only writes to `clis.json`. They are serialized, and a file that does not parse or has unsafe permissions is refused rather than overwritten; fix it (or `chmod 600` it) and retry. The HTTP routes are listed in `docs/api-reference.md` under *CLI management*. +These are the only writes to `clis.json`. They are serialized, and a file that does not parse or has unsafe permissions is refused rather than overwritten; fix it (or `chmod 600` it) and retry. The HTTP routes are listed in `docs/api-reference.md` under _CLI management_. ## The shape of an entry @@ -67,7 +67,7 @@ Six capability fields carry a regular expression an override file can set: `disc `workingLine` is the one that matters most, because it is compiled once per session and then run against every accumulated PTY chunk and every pane capture. A nested quantifier there is a ReDoS against the event loop for the whole server, not just that session. The guard therefore runs in two places, and neither is redundant: `schema.ts` rejects the entry at LOAD time so a bad pattern never reaches a session, and `_workingLinePattern()` in `session.ts` compiles through the same helper so the runtime cannot end up with a pattern the schema would have refused. -`modelDetect.screenLine` names the model a session runs, for the tile grid's and the split pane's headers (`SessionState.displayModel`). It must have exactly ONE capture group, the model, which `schema.ts` checks at LOAD time, and it runs over the last `screenLines` (1 to 8, default 1) non-blank rows of the capture the idle/working probe already takes, joined with newlines so a pattern can anchor on the row above. Like `watchingLine`, the rows are pane text the agent writes most of, so a pattern must anchor on chrome only that CLI draws. The two stock ones, measured on live panes: dsh-TUI's status line on the row under its composer's rounded border (`╰─+╯\n ?()`, three rows), codex's ` · ` footer (its last row, or the row above codex 0.162's indented hint row, so a two-row window), and opencode's composer agent row (`┃ Build `, directly above the box's `╹` edge, eight rows because its home screen puts up to five rows of its own chrome below it). opencode's field is the model AND the provider, since only colour separates them on that row; the pattern takes the LAST such row in the window, so a composer-shaped row the agent prints higher up cannot stand in for it. A screen that does not match keeps the last model the session reported; a CLI without the field shows its launch model, if any. Claude needs none: its statusLine exporter reports `model.display_name` on every render. ⚠️ dsh-TUI's first field is the model only while its status bar's model field is on; switched off, it is the next field: the reasoning effort (` medium · `), the session mode, or the folder name. So a captured field is not taken when it is one of the CLI's declared `modelDetect.rejectWords` (single tokens, compared ignoring case; dsh lists every effort id its adapters offer and the shipped mode ids) or the session's own working-directory basename (the shared reader's rule, for every CLI). Anything else the pattern captures is the model, so the official `deepseek-chat` / `deepseek-reasoner` ids are read. +`modelDetect.screenLine` names the model a session runs, for the tile grid's and the split pane's headers (`SessionState.displayModel`). It must have exactly ONE capture group, the model, which `schema.ts` checks at LOAD time, and it runs over the last `screenLines` (1 to 8, default 1) non-blank rows of the capture the idle/working probe already takes, joined with newlines so a pattern can anchor on the row above. Like `watchingLine`, the rows are pane text the agent writes most of, so a pattern must anchor on chrome only that CLI draws. The two stock ones, measured on live panes: dsh-TUI's status line on the row under its composer's rounded border (`╰─+╯\n ?()`, three rows), codex's ` ·` footer (its last row, or the row above codex 0.162's indented hint row, so a two-row window), and opencode's composer agent row (`┃ Build `, directly above the box's `╹` edge, eight rows because its home screen puts up to five rows of its own chrome below it). opencode's field is the model AND the provider, since only colour separates them on that row; the pattern takes the LAST such row in the window, so a composer-shaped row the agent prints higher up cannot stand in for it. A screen that does not match keeps the last model the session reported; a CLI without the field shows its launch model, if any. Claude needs none: its statusLine exporter reports `model.display_name` on every render. ⚠️ dsh-TUI's first field is the model only while its status bar's model field is on; switched off, it is the next field: the reasoning effort (` medium · `), the session mode, or the folder name. So a captured field is not taken when it is one of the CLI's declared `modelDetect.rejectWords` (single tokens, compared ignoring case; dsh lists every effort id its adapters offer and the shipped mode ids) or the session's own working-directory basename (the shared reader's rule, for every CLI). Anything else the pattern captures is the model, so the official `deepseek-chat` / `deepseek-reasoner` ids are read. `modelDetect.configResolver` names a READER in `src/model-config-resolvers.ts` (a name, never code in config, like a launcher profile) that resolves the model the CLI's own config pins for one session, for while its screen names none (the `config` source of `displayModel`, ranked below any report from the running CLI). It runs at every pane start, attach and relaunch, with the session's own launch config and env, and must be read-only, bounded (probe before read, no synchronous filesystem call) and return the model id alone. The one stock reader, `deepseek-route` (`src/deepseek-route-config.ts`), resolves dsh-TUI's route the way dsh composes it for the session's profile under the session's `DSH_HOME`: the last of `profiles//cordis.patch.yml` and `$DSH_HOME/cordis.patch.yml` carrying `config` for the `dsh-tui` row counts, and only when it names both `provider` and `model`. Anything in doubt answers nothing: a half-pinned route, a profile without dsh-TUI, an unreadable, oversized or symlinked-out layer, a file beyond its narrow YAML subset. @@ -205,7 +205,7 @@ Measured 2026-10-11 against the installed builds or their source: every CLI in t The composed command line is interpolated into `bash -c "…"` inside tmux, which makes command construction a security boundary. Four independent layers keep config out of it: -1. **Config contains no shell text.** There is no `command: "..."` field anywhere in the schema. An entry declares a sequence of typed tokens; `argv.ts` is the only place that turns them into a string, and it owns every separator itself — one space between tokens, ` || ` between fallback variants. Neither can originate from config, because config has no field that could hold either. +1. **Config contains no shell text.** There is no `command: "..."` field anywhere in the schema. An entry declares a sequence of typed tokens; `argv.ts` is the only place that turns them into a string, and it owns every separator itself — one space between tokens, `||` between fallback variants. Neither can originate from config, because config has no field that could hold either. 2. **Every literal is validated at LOAD time** against a safe-word pattern (no space, quote, backtick, `$`, `;`, `&`, `|`, redirection, parens, braces, newline or backslash). A bad literal **rejects the whole entry** rather than being dropped, because a silently dropped flag would change security-relevant behaviour — losing `--no-approve` is not a cosmetic difference. 3. **Values resolve through NAMED patterns.** A value placeholder selects a `TokenPattern` (`model`, `uuid`, `slug`, `path-segment`, `tool-list`, …) from `patterns.ts`; config can never supply its own regex for a value, so a `clis.json` structurally cannot widen its own validation. A value that fails its pattern drops the whole argument, exactly as the hand-written builders did: an invalid `--model` omits `--model`, it never substitutes something else. 4. **Escaping is independent of validation.** `renderToken()` re-checks the resolved value before emitting it unquoted, and single-quotes anything else — so even a value that somehow bypassed validation is quoted, never concatenated raw. @@ -227,12 +227,12 @@ The names live in `profiles.ts`, which is kept free of imports so `schema.ts` ca DeepSeek is worth reading before assuming an entry looks like its siblings — the schema carries four extensions because of it. -| What it breaks | How the registry expresses it | -| ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ | -| `dsh` is a profile LAUNCHER, not the agent, so "installed" is not "runnable". | `discovery.launcherProfile` + `discovery.launcherTargetParam`. | +| What it breaks | How the registry expresses it | +| -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | +| `dsh` is a profile LAUNCHER, not the agent, so "installed" is not "runnable". | `discovery.launcherProfile` + `discovery.launcherTargetParam`. | | Its permission switch is the **`DSH_PERMISSION_MODE` env var**, not a flag — the harness has none. | `env.configSetenv` (so the ordinary `privilegedParams` clamp still reaches it) **and** `capabilities.privilegedEnvKeys`. | -| It is the only non-claude mode with real hook signals, and for it that is a per-SESSION question. | `capabilities.hooks: 'supervised'` — a third state, not a boolean. | -| Its transcript is zstd session files, one frame per write. | `capabilities.transcript: 'deepseek-zstd'`. | +| It is the only non-claude mode with real hook signals, and for it that is a per-SESSION question. | `capabilities.hooks: 'supervised'` — a third state, not a boolean. | +| Its transcript is zstd session files, one frame per write. | `capabilities.transcript: 'deepseek-zstd'`. | ## Identity probes @@ -272,10 +272,10 @@ Two things need the catalogue but cannot import TypeScript, so `npm run generate (`scripts/generate-cli-catalog.mts`) emits two artifacts from `stock.ts`. Both are committed, and `test/cli-catalog-sync.test.ts` fails if either drifts from a fresh generation. -| Artifact | Consumer | Why it exists | -| ------------------------------------ | ---------------------------------- | ---------------------------------------------------------------------------------- | -| `config/clis.stock.json` | `scripts/lib/cli-catalog.mjs` (Docker build args), tests | A `.mjs` cannot import the registry. | -| a marked block inside `install.sh` | the installer itself | It runs via `curl \| bash` before any checkout exists, so it can read neither. | +| Artifact | Consumer | Why it exists | +| ---------------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------ | +| `config/clis.stock.json` | `scripts/lib/cli-catalog.mjs` (Docker build args), tests | A `.mjs` cannot import the registry. | +| a marked block inside `install.sh` | the installer itself | It runs via `curl \| bash` before any checkout exists, so it can read neither. | Only `id`, `label`, `shortBadge`, `enabled`, `order`, `kind` and `discovery` are exported. `launch`, `env`, `capabilities` and `overlays` are spawn-time concerns the server alone @@ -336,7 +336,7 @@ A module-level const freezes at first import, and the failure is asymmetric: a C `capabilities.mcpConfig` (`{ path, format, relocation? }`, `path` relative to the home directory) names the file a CLI keeps its user-level MCP server list in and the dialect it is written in. `src/mcp-sync.ts` reads that list from every ENABLED CLI that declares one, and that is installed or already has the file (a CLI that is neither is reported `absent`, never created), and adds any server a CLI is missing from the others. It writes other tools' own config, so it is **opt-in**: `mcpSyncEnabled` (synced, default OFF) gates `GET`/`POST /api/mcp-sync` (403 while off) and the Settings → Agents & CLIs → MCP servers controls. Declared today for claude, gemini, codex, opencode and antigravity (plus GitHub Copilot CLI as a sync-only target, below); every format was checked against what the CLI's own `mcp add` writes, except opencode's (documented, not installed to check). A CLI with no entry (pi, grok, omp, deepseek) is not guessed at: it is listed as `unsupported` in the result when enabled. Adding one is a registry entry plus a small adapter in `mcp-sync.ts`, and a verified fixture in `test/mcp-sync.test.ts`. -**Tools that are not run modes.** GitHub Copilot CLI keeps an MCP list worth syncing but Codeman does not launch it, so it has no registry entry. `src/mcp-sync-targets.ts` declares such tools as plain data (`MCP_SYNC_ONLY_TOOLS`: id, label, config path, dialect, relocation var, the binary whose presence means "installed"). They join the registry CLIs as sync targets (listed after them, so a registry CLI's definition wins a same-name difference), under the same rules: installed or already configured, otherwise `absent`. Copilot's dialect is `copilot-json` (`~/.copilot/mcp-config.json`, relocated by `COPILOT_HOME`; checked against `copilot mcp add` 1.0.94): `mcpServers`, each entry with `tools` (`["*"]` = all), `type` `local` | `http` | `sse`, `command`/`args`/`env` or `url`/`headers`. `copilot mcp disable` does not mark the entry: it lists the name under `disabledMcpServers` in `settings.json` beside the config. Sync reads that list (never writes it) so a disabled server is not copied, and reports the target `unreadable` if `settings.json` is not valid JSON rather than guessing. +**Tools that are not run modes, or that declare no `mcpConfig`.** GitHub Copilot CLI keeps an MCP list worth syncing and is a run mode, but its registry entry declares no `mcpConfig`; its sync target is the plain-data entry below. `src/mcp-sync-targets.ts` declares such tools as plain data (`MCP_SYNC_ONLY_TOOLS`: id, label, config path, dialect, relocation var, the binary whose presence means "installed"). They join the registry CLIs as sync targets (listed after them, so a registry CLI's definition wins a same-name difference), under the same rules: installed or already configured, otherwise `absent`. Copilot's dialect is `copilot-json` (`~/.copilot/mcp-config.json`, relocated by `COPILOT_HOME`; checked against `copilot mcp add` 1.0.94): `mcpServers`, each entry with `tools` (`["*"]` = all), `type` `local` | `http` | `sse`, `command`/`args`/`env` or `url`/`headers`. `copilot mcp disable` does not mark the entry: it lists the name under `disabledMcpServers` in `settings.json` beside the config. Sync reads that list (never writes it) so a disabled server is not copied, and reports the target `unreadable` if `settings.json` is not valid JSON rather than guessing. `relocation` (`{ envVar, path }`) names the env var the CLI itself reads to move that file: claude `CLAUDE_CONFIG_DIR` (`.claude.json` under it), codex `CODEX_HOME` (`config.toml`), opencode `XDG_CONFIG_HOME` (`opencode/opencode.json`), gemini `GEMINI_CLI_HOME` (`.gemini/settings.json`) and, for the sync-only Copilot CLI, `COPILOT_HOME` (`mcp-config.json`); antigravity follows `$HOME` only, so it declares none. The var is read from the SERVER process env at call time, which is the env the CLIs Codeman spawns inherit. An absolute value moves the file to `/`, an empty one counts as unset (as it does for each CLI), and anything else reports the target `skipped` with the reason instead of writing a file the CLI never reads. A per-session relocation (a session's own `CLAUDE_CONFIG_DIR` in `envOverrides`) is not followed: the sync only knows the server's environment. @@ -347,3 +347,9 @@ The rules the module keeps and the tests pin: it only ADDS (a name already defin - [Agent CLIs](wiki/Agent-CLIs.md) — the user-facing per-CLI guide. - `docs/architecture-invariants.md` — the mechanics and the history behind the rules above. - `docs/deepseek-integration.md` — why DeepSeek is shaped the way it is. + +## GitHub Copilot CLI + +`copilot` (npm `@github/copilot`) is a stock entry. A bare launch is `copilot`, in its own Manual Approval mode. The launch parameters follow the grok/omp shape: `model` (`--model`), `resumeId` (`--resume `, an id or id prefix; names are refused by the id pattern) and `continueSession` (`--continue`, skipped when a resume id is set), plus `allowAll` (`--yolo`: allow every tool, path and URL). The legacy config object is `copilotConfig` (`{ model, allowAll, resumeSessionId, continueSession }`). The Run button sends `allowAll: true` like the other agent CLIs' bypass switches, and `allowAll` is declared in `privilegedParams`, so a non-granted multi-user owner has it forced back to `false` (an absent config is never materialized: a bare spawn is already safe). Session history resume goes through `copilotConfig.resumeSessionId` / `continueSession`. The tab names the model it reads from the footer (`modelDetect`). It declares no `mcpConfig`. + +Measured on 1.0.94 inside tmux: an alternate-screen TUI with mouse tracking on, whose composer is the `❯` between two rules. While a turn runs the footer reads `◉ Working esc edit prompt` (the dot alternates `◉`/`◎`), which is the `workDetect` working line. Keystrokes sent with `tmux send-keys` reach the composer, Shift+Enter inserts a newline, and a shell tool call completes inside tmux; github/copilot-cli#4180 (a driven PTY ignores input) and #4223 (a shell command never completes under tmux) report both failing on 1.0.70 to 1.0.74 and neither reproduces on 1.0.94. `COPILOT_HOME` and `COPILOT_ALLOW_ALL` are privileged env keys, so a non-granted multi-user owner cannot set them through `envOverrides`. diff --git a/install.sh b/install.sh index 71a8a4b3..446d64c4 100755 --- a/install.sh +++ b/install.sh @@ -168,19 +168,19 @@ export PUPPETEER_SKIP_DOWNLOAD="${PUPPETEER_SKIP_DOWNLOAD:-1}" # script will ever execute, and they arrive embedded in this file — same TLS fetch, same # commit as the script itself. Nothing fetched at install time is ever executed; there is # no network refresh of these arrays. See cli_catalog_select_platform below. -CLI_IDS=('claude' 'shell' 'opencode' 'codex' 'gemini' 'antigravity' 'pi' 'grok' 'deepseek' 'omp') -CLI_LABELS=('Claude Code' 'Shell' 'OpenCode' 'Codex' 'Gemini' 'Antigravity' 'Pi' 'Grok' 'DeepSeek' 'OMP') -CLI_ENABLED=(1 1 1 1 1 1 1 1 1 1) -CLI_LAUNCHER_ONLY=(0 0 0 0 0 0 0 0 1 0) -CLI_DOCS=('https://docs.claude.com/claude-code' '' 'https://opencode.ai/docs' 'https://developers.openai.com/codex/cli' 'https://github.com/google-gemini/gemini-cli' 'https://antigravity.google/cli' 'https://pi.dev' 'https://github.com/xai-org/grok-build' 'https://github.com/deepseek-ai/deepseek-harness' 'https://omp.sh') -CLI_CMD_LINUX=('curl -fsSL https://claude.ai/install.sh | bash' '' 'curl -fsSL https://opencode.ai/install | bash' 'npm install -g @openai/codex' 'npm install -g @google/gemini-cli' 'curl -fsSL https://antigravity.google/cli/install.sh | bash' 'npm install -g --ignore-scripts @earendil-works/pi-coding-agent' 'curl -fsSL https://x.ai/cli/install.sh | bash' '' 'curl -fsSL https://omp.sh/install | sh') -CLI_CMD_DARWIN=('curl -fsSL https://claude.ai/install.sh | bash' '' 'curl -fsSL https://opencode.ai/install | bash' 'npm install -g @openai/codex' 'npm install -g @google/gemini-cli' 'curl -fsSL https://antigravity.google/cli/install.sh | bash' 'npm install -g --ignore-scripts @earendil-works/pi-coding-agent' 'curl -fsSL https://x.ai/cli/install.sh | bash' '' 'brew install can1357/tap/omp') -CLI_ALL_BINS=('claude' 'opencode' 'codex' 'gemini' 'agy' 'pi' 'grok' 'dsh' 'omp') -CLI_BIN_OFF=(0 1 1 2 3 4 5 6 7 8) -CLI_BIN_LEN=(1 0 1 1 1 1 1 1 1 1) -CLI_ALL_PATHS=("$HOME/.local/bin/claude" "$HOME/.claude/local/claude" "/usr/local/bin/claude" "$HOME/.npm-global/bin/claude" "$HOME/bin/claude" "$HOME/.opencode/bin/opencode" "$HOME/.local/bin/opencode" "/usr/local/bin/opencode" "$HOME/go/bin/opencode" "$HOME/.bun/bin/opencode" "$HOME/.npm-global/bin/opencode" "$HOME/bin/opencode" "$HOME/.codex/bin/codex" "$HOME/.local/bin/codex" "/usr/local/bin/codex" "$HOME/.bun/bin/codex" "$HOME/.npm-global/bin/codex" "$HOME/bin/codex" "$HOME/.gemini/bin/gemini" "$HOME/.local/bin/gemini" "/usr/local/bin/gemini" "$HOME/.bun/bin/gemini" "$HOME/.npm-global/bin/gemini" "$HOME/bin/gemini" "$HOME/.local/bin/agy" "$HOME/.antigravity/bin/agy" "/usr/local/bin/agy" "$HOME/bin/agy" "$HOME/.local/bin/pi" "/usr/local/bin/pi" "$HOME/.bun/bin/pi" "$HOME/.npm-global/bin/pi" "$HOME/bin/pi" "$HOME/.grok/bin/grok" "$HOME/.local/bin/grok" "/usr/local/bin/grok" "$HOME/bin/grok" "$HOME/.local/bin/dsh" "/usr/local/bin/dsh" "$HOME/.npm-global/bin/dsh" "$HOME/bin/dsh" "$HOME/.local/bin/omp" "$HOME/.omp/bin/omp" "/usr/local/bin/omp" "$HOME/.bun/bin/omp" "$HOME/.npm-global/bin/omp" "$HOME/bin/omp") -CLI_PATH_OFF=(0 5 5 12 18 24 28 33 37 41) -CLI_PATH_LEN=(5 0 7 6 6 4 5 4 4 6) +CLI_IDS=('claude' 'shell' 'opencode' 'codex' 'gemini' 'antigravity' 'pi' 'grok' 'deepseek' 'omp' 'copilot') +CLI_LABELS=('Claude Code' 'Shell' 'OpenCode' 'Codex' 'Gemini' 'Antigravity' 'Pi' 'Grok' 'DeepSeek' 'OMP' 'GitHub Copilot') +CLI_ENABLED=(1 1 1 1 1 1 1 1 1 1 1) +CLI_LAUNCHER_ONLY=(0 0 0 0 0 0 0 0 1 0 0) +CLI_DOCS=('https://docs.claude.com/claude-code' '' 'https://opencode.ai/docs' 'https://developers.openai.com/codex/cli' 'https://github.com/google-gemini/gemini-cli' 'https://antigravity.google/cli' 'https://pi.dev' 'https://github.com/xai-org/grok-build' 'https://github.com/deepseek-ai/deepseek-harness' 'https://omp.sh' 'https://github.com/github/copilot-cli') +CLI_CMD_LINUX=('curl -fsSL https://claude.ai/install.sh | bash' '' 'curl -fsSL https://opencode.ai/install | bash' 'npm install -g @openai/codex' 'npm install -g @google/gemini-cli' 'curl -fsSL https://antigravity.google/cli/install.sh | bash' 'npm install -g --ignore-scripts @earendil-works/pi-coding-agent' 'curl -fsSL https://x.ai/cli/install.sh | bash' '' 'curl -fsSL https://omp.sh/install | sh' 'npm install -g @github/copilot') +CLI_CMD_DARWIN=('curl -fsSL https://claude.ai/install.sh | bash' '' 'curl -fsSL https://opencode.ai/install | bash' 'npm install -g @openai/codex' 'npm install -g @google/gemini-cli' 'curl -fsSL https://antigravity.google/cli/install.sh | bash' 'npm install -g --ignore-scripts @earendil-works/pi-coding-agent' 'curl -fsSL https://x.ai/cli/install.sh | bash' '' 'brew install can1357/tap/omp' 'npm install -g @github/copilot') +CLI_ALL_BINS=('claude' 'opencode' 'codex' 'gemini' 'agy' 'pi' 'grok' 'dsh' 'omp' 'copilot') +CLI_BIN_OFF=(0 1 1 2 3 4 5 6 7 8 9) +CLI_BIN_LEN=(1 0 1 1 1 1 1 1 1 1 1) +CLI_ALL_PATHS=("$HOME/.local/bin/claude" "$HOME/.claude/local/claude" "/usr/local/bin/claude" "$HOME/.npm-global/bin/claude" "$HOME/bin/claude" "$HOME/.opencode/bin/opencode" "$HOME/.local/bin/opencode" "/usr/local/bin/opencode" "$HOME/go/bin/opencode" "$HOME/.bun/bin/opencode" "$HOME/.npm-global/bin/opencode" "$HOME/bin/opencode" "$HOME/.codex/bin/codex" "$HOME/.local/bin/codex" "/usr/local/bin/codex" "$HOME/.bun/bin/codex" "$HOME/.npm-global/bin/codex" "$HOME/bin/codex" "$HOME/.gemini/bin/gemini" "$HOME/.local/bin/gemini" "/usr/local/bin/gemini" "$HOME/.bun/bin/gemini" "$HOME/.npm-global/bin/gemini" "$HOME/bin/gemini" "$HOME/.local/bin/agy" "$HOME/.antigravity/bin/agy" "/usr/local/bin/agy" "$HOME/bin/agy" "$HOME/.local/bin/pi" "/usr/local/bin/pi" "$HOME/.bun/bin/pi" "$HOME/.npm-global/bin/pi" "$HOME/bin/pi" "$HOME/.grok/bin/grok" "$HOME/.local/bin/grok" "/usr/local/bin/grok" "$HOME/bin/grok" "$HOME/.local/bin/dsh" "/usr/local/bin/dsh" "$HOME/.npm-global/bin/dsh" "$HOME/bin/dsh" "$HOME/.local/bin/omp" "$HOME/.omp/bin/omp" "/usr/local/bin/omp" "$HOME/.bun/bin/omp" "$HOME/.npm-global/bin/omp" "$HOME/bin/omp" "$HOME/.local/bin/copilot" "/usr/local/bin/copilot" "$HOME/.bun/bin/copilot" "$HOME/.npm-global/bin/copilot" "$HOME/bin/copilot") +CLI_PATH_OFF=(0 5 5 12 18 24 28 33 37 41 47) +CLI_PATH_LEN=(5 0 7 6 6 4 5 4 4 6 5) # <<< END GENERATED CLI CATALOGUE # ============================================================================ diff --git a/plugins/codeman/skills/codeman/SKILL.md b/plugins/codeman/skills/codeman/SKILL.md index f696a73d..a9772f77 100644 --- a/plugins/codeman/skills/codeman/SKILL.md +++ b/plugins/codeman/skills/codeman/SKILL.md @@ -553,7 +553,7 @@ Harness TUI reports `idle`/`working`/`blocked` to Codeman over the supervisor co implements, so dsh is the one external CLI with definitive `stop`/`blocked` signals instead of guessed-from-silence ones — and it writes a structured transcript, which is what `last-response` reads for it. `shell`, `opencode`, `gemini`, `antigravity`, `pi`, -`grok` and `omp` have neither, and `codex` has a transcript and a screen-read `blocked` but +`grok`, `omp` and `copilot` have neither, and `codex` has a transcript and a screen-read `blocked` but no `stop`, so all of them still need markers ([§5.5](reference/verbs.md#55-markers-for-hook-less-workers)). Three things to know before you spawn one: diff --git a/plugins/codeman/skills/codeman/reference/endpoints.md b/plugins/codeman/skills/codeman/reference/endpoints.md index 1bcdc70d..f2ef2fb2 100644 --- a/plugins/codeman/skills/codeman/reference/endpoints.md +++ b/plugins/codeman/skills/codeman/reference/endpoints.md @@ -237,7 +237,7 @@ minutes, never retry the credential. flushed slightly *after* the `stop` hook fires, so a read taken the instant the wait returns is too early (verified live: empty on the first call, full prose seconds later). It is also `""` before the worker's first completed turn, and permanently `""` for -`shell`, `opencode`, `gemini`, `antigravity`, `pi`, `grok` and `omp`, which write no transcript at +`shell`, `opencode`, `gemini`, `antigravity`, `pi`, `grok`, `omp` and `copilot`, which write no transcript at all. `deepseek` is NOT one of those — it is read from `$DSH_HOME/sessions/**` and lags for the same reason claude does (the harness finalizes the assistant message just after it reports `idle`), so poll it the same way. @@ -341,7 +341,7 @@ ESC=$(printf '\033') `POST /api/v1/quick-start` body (all optional): `{"caseName":"worker-1","mode":"claude","sessionName":"auth-worker","effort":"high"}` -, `mode` ∈ `claude|shell|opencode|codex|gemini|antigravity|pi|grok|deepseek|omp`; response is +, `mode` ∈ `claude|shell|opencode|codex|gemini|antigravity|pi|grok|deepseek|omp|copilot`; response is `.data.{sessionId, caseName, casePath}`. Creates the case directory (a real directory on the user's disk) if missing, do not retry it in a loop, and remember the name. @@ -356,9 +356,9 @@ a soft default the worker can change with `/advisor`. Remote and docker cases re `OPERATION_FAILED`; it never falls back to claude. Probe first whenever you did not pick the mode yourself: `GET /api/v1/claude/status`, `GET /api/v1/opencode/status`, `GET /api/v1/codex/status`, `GET /api/v1/gemini/status`, `GET /api/v1/antigravity/status`, `GET /api/v1/grok/status`, `GET /api/v1/deepseek/status`, -`GET /api/v1/pi/status` and `GET /api/v1/omp/status` each return `.data.{available, path}` (no session needed). -Pi's, grok's and OMP's also carry `.data.version`, because `pi` is a short generic name, -`grok` is a name with npm squatters, and `omp` is a similarly short name, so an unrelated +`GET /api/v1/pi/status`, `GET /api/v1/omp/status` and `GET /api/v1/copilot/status` each return `.data.{available, path}` (no session needed). +Pi's, grok's, OMP's and Copilot's also carry `.data.version`, because `pi` is a short generic name, +`grok` is a name with npm squatters, and `omp` is a similarly short name, and `copilot` is a generic one, so an unrelated binary on `$PATH` can shadow any of them: the resolver rejects one whose `--version` is not version-shaped, so `available:false` there can mean "a different program of the same name is in front" rather than "nothing is installed". `shell` has no CLI to probe. @@ -486,7 +486,7 @@ Quirks that will bite you: - ⚠️ **`active-tools` proves presence, never absence.** It is fed by the BashToolParser, which reads Claude's rendered `● Bash(…)` lines, and `_processExpensiveParsers` returns early for every external CLI mode (`session.ts:~2225`), so it is permanently - `[]` on `opencode`/`codex`/`gemini`/`antigravity`/`pi`/`grok`/`deepseek`/`omp`. ⚠️ **`shell` is NOT one of those** + `[]` on `opencode`/`codex`/`gemini`/`antigravity`/`pi`/`grok`/`deepseek`/`omp`/`copilot`. ⚠️ **`shell` is NOT one of those** (`isExternalCliMode`, `session.ts:176-187`, lists only those seven), so the parser does run on a shell worker, and `TEXT_COMMAND_PATTERN` (`bash-tool-parser.ts:89`) matches bare `tail|cat|head|less|grep|watch|multitail ` lines with no `● Bash(` wrapper: diff --git a/plugins/codeman/skills/codeman/reference/messaging.md b/plugins/codeman/skills/codeman/reference/messaging.md index 2d7d68d0..6de5a775 100644 --- a/plugins/codeman/skills/codeman/reference/messaging.md +++ b/plugins/codeman/skills/codeman/reference/messaging.md @@ -56,7 +56,7 @@ own head: the worker enforcing the cap is the one who has to be told about it. | synchronize on end of turn | HTTP `wait until=stop` (fires for message-initiated turns too, verified live) | | liveness / death check | HTTP `wait?until=exit` | | interrupt a running turn (break-glass) | HTTP input, a bare `\x1b` with no `\r` | -| non-claude modes (`shell`/`opencode`/`codex`/`gemini`/`antigravity`/`pi`/`grok`/`deepseek`/`omp`) | HTTP only (no other CLI has messaging) | +| non-claude modes (`shell`/`opencode`/`codex`/`gemini`/`antigravity`/`pi`/`grok`/`deepseek`/`omp`/`copilot`) | HTTP only (no other CLI has messaging) | | delete | HTTP, via SKILL.md's `delete_session` guard | ## Availability: probe, never assume @@ -352,7 +352,7 @@ Without a break-glass, a pair with a bad brief is a token bonfire with no off sw ### Mixed fleets: the pairing matrix -Non-claude workers (`shell`, `opencode`, `codex`, `gemini`, `antigravity`, `pi`, `grok`, `deepseek`, `omp`) cannot be peers +Non-claude workers (`shell`, `opencode`, `codex`, `gemini`, `antigravity`, `pi`, `grok`, `deepseek`, `omp`, `copilot`) cannot be peers at all; no other CLI has this feature. Their tasks route over HTTP, and you never mention messaging in their briefs. The claude half of the fleet can use messaging among itself, subject to the namespace rule: **messaging works between two sessions that share one diff --git a/plugins/codeman/skills/codeman/reference/recipes.md b/plugins/codeman/skills/codeman/reference/recipes.md index 39867a6d..be1db504 100644 --- a/plugins/codeman/skills/codeman/reference/recipes.md +++ b/plugins/codeman/skills/codeman/reference/recipes.md @@ -188,7 +188,7 @@ for _ in $(seq 1 10); do done printf '%s\n' "$TXT" # (.data is {text,timestamp}; text is also "" before the first completed turn and -# always "" for shell/opencode/gemini/antigravity/pi/grok/omp, which have no transcript, use +# always "" for shell/opencode/gemini/antigravity/pi/grok/omp/copilot, which have no transcript, use # the terminal tail there, and here only to diagnose an unsubmitted prompt.) # 6. clean up: exact id, own list only, through the fail-closed preamble helper diff --git a/plugins/codeman/skills/codeman/reference/verbs.md b/plugins/codeman/skills/codeman/reference/verbs.md index 690cdbf3..6ad55103 100644 --- a/plugins/codeman/skills/codeman/reference/verbs.md +++ b/plugins/codeman/skills/codeman/reference/verbs.md @@ -376,7 +376,7 @@ recovered by submitting it with `{"input":"\r"}`. only when the workspace actually has them, see [§5.1](#51-where-to-spawn)) **and for `deepseek`** — the one external CLI that reports its own lifecycle, so its `stop` is a real end-of-turn signal rather than a guess. On -`shell`/`opencode`/`codex`/`gemini`/`antigravity`/`pi`/`grok`/`omp`, requesting them explicitly is a +`shell`/`opencode`/`codex`/`gemini`/`antigravity`/`pi`/`grok`/`omp`/`copilot`, requesting them explicitly is a 400 (except `blocked` on `codex`, which Codeman reads off its screen when an approval, question or trust dialog is up; a wait for it answers at once if one already is), and lifecycle transitions there are coarse (a short shell command may emit **no** @@ -430,7 +430,7 @@ from the transcript file, which is flushed slightly *after* the `stop` hook fire single read taken the instant send-and-wait returns comes back `""` even though the turn finished (verified live: empty on the first call, full text seconds later). `text` is also `""` before the worker's first completed turn, and always `""` for modes with -no transcript (`shell`, `opencode`, `gemini`, `antigravity`, `pi`, `grok`, `omp`; the first four +no transcript (`shell`, `opencode`, `gemini`, `antigravity`, `pi`, `grok`, `omp`, `copilot`; the first four verified live, pi from the same source path), which is why the loop above is bounded rather than open-ended. A dsh worker lags too, for its own reason: the harness finalizes the assistant message just after it reports `idle`. Fall back to the terminal buffer @@ -516,7 +516,7 @@ turn), and both better than diffing terminal samples: ``` ⚠️ `active-tools` is parsed out of Claude's own output format, so it is **empty for -`opencode`/`codex`/`gemini`/`antigravity`/`pi`/`grok`/`deepseek`/`omp`** (those parsers are skipped wholesale) and +`opencode`/`codex`/`gemini`/`antigravity`/`pi`/`grok`/`deepseek`/`omp`/`copilot`** (those parsers are skipped wholesale) and in practice empty for `shell`. Source-verified, not measured live. Only if neither helps: sample `terminal?tail=` twice a few seconds apart. A changing diff --git a/skills/codeman/SKILL.md b/skills/codeman/SKILL.md index f696a73d..a9772f77 100644 --- a/skills/codeman/SKILL.md +++ b/skills/codeman/SKILL.md @@ -553,7 +553,7 @@ Harness TUI reports `idle`/`working`/`blocked` to Codeman over the supervisor co implements, so dsh is the one external CLI with definitive `stop`/`blocked` signals instead of guessed-from-silence ones — and it writes a structured transcript, which is what `last-response` reads for it. `shell`, `opencode`, `gemini`, `antigravity`, `pi`, -`grok` and `omp` have neither, and `codex` has a transcript and a screen-read `blocked` but +`grok`, `omp` and `copilot` have neither, and `codex` has a transcript and a screen-read `blocked` but no `stop`, so all of them still need markers ([§5.5](reference/verbs.md#55-markers-for-hook-less-workers)). Three things to know before you spawn one: diff --git a/skills/codeman/reference/endpoints.md b/skills/codeman/reference/endpoints.md index 1bcdc70d..f2ef2fb2 100644 --- a/skills/codeman/reference/endpoints.md +++ b/skills/codeman/reference/endpoints.md @@ -237,7 +237,7 @@ minutes, never retry the credential. flushed slightly *after* the `stop` hook fires, so a read taken the instant the wait returns is too early (verified live: empty on the first call, full prose seconds later). It is also `""` before the worker's first completed turn, and permanently `""` for -`shell`, `opencode`, `gemini`, `antigravity`, `pi`, `grok` and `omp`, which write no transcript at +`shell`, `opencode`, `gemini`, `antigravity`, `pi`, `grok`, `omp` and `copilot`, which write no transcript at all. `deepseek` is NOT one of those — it is read from `$DSH_HOME/sessions/**` and lags for the same reason claude does (the harness finalizes the assistant message just after it reports `idle`), so poll it the same way. @@ -341,7 +341,7 @@ ESC=$(printf '\033') `POST /api/v1/quick-start` body (all optional): `{"caseName":"worker-1","mode":"claude","sessionName":"auth-worker","effort":"high"}` -, `mode` ∈ `claude|shell|opencode|codex|gemini|antigravity|pi|grok|deepseek|omp`; response is +, `mode` ∈ `claude|shell|opencode|codex|gemini|antigravity|pi|grok|deepseek|omp|copilot`; response is `.data.{sessionId, caseName, casePath}`. Creates the case directory (a real directory on the user's disk) if missing, do not retry it in a loop, and remember the name. @@ -356,9 +356,9 @@ a soft default the worker can change with `/advisor`. Remote and docker cases re `OPERATION_FAILED`; it never falls back to claude. Probe first whenever you did not pick the mode yourself: `GET /api/v1/claude/status`, `GET /api/v1/opencode/status`, `GET /api/v1/codex/status`, `GET /api/v1/gemini/status`, `GET /api/v1/antigravity/status`, `GET /api/v1/grok/status`, `GET /api/v1/deepseek/status`, -`GET /api/v1/pi/status` and `GET /api/v1/omp/status` each return `.data.{available, path}` (no session needed). -Pi's, grok's and OMP's also carry `.data.version`, because `pi` is a short generic name, -`grok` is a name with npm squatters, and `omp` is a similarly short name, so an unrelated +`GET /api/v1/pi/status`, `GET /api/v1/omp/status` and `GET /api/v1/copilot/status` each return `.data.{available, path}` (no session needed). +Pi's, grok's, OMP's and Copilot's also carry `.data.version`, because `pi` is a short generic name, +`grok` is a name with npm squatters, and `omp` is a similarly short name, and `copilot` is a generic one, so an unrelated binary on `$PATH` can shadow any of them: the resolver rejects one whose `--version` is not version-shaped, so `available:false` there can mean "a different program of the same name is in front" rather than "nothing is installed". `shell` has no CLI to probe. @@ -486,7 +486,7 @@ Quirks that will bite you: - ⚠️ **`active-tools` proves presence, never absence.** It is fed by the BashToolParser, which reads Claude's rendered `● Bash(…)` lines, and `_processExpensiveParsers` returns early for every external CLI mode (`session.ts:~2225`), so it is permanently - `[]` on `opencode`/`codex`/`gemini`/`antigravity`/`pi`/`grok`/`deepseek`/`omp`. ⚠️ **`shell` is NOT one of those** + `[]` on `opencode`/`codex`/`gemini`/`antigravity`/`pi`/`grok`/`deepseek`/`omp`/`copilot`. ⚠️ **`shell` is NOT one of those** (`isExternalCliMode`, `session.ts:176-187`, lists only those seven), so the parser does run on a shell worker, and `TEXT_COMMAND_PATTERN` (`bash-tool-parser.ts:89`) matches bare `tail|cat|head|less|grep|watch|multitail ` lines with no `● Bash(` wrapper: diff --git a/skills/codeman/reference/messaging.md b/skills/codeman/reference/messaging.md index 2d7d68d0..6de5a775 100644 --- a/skills/codeman/reference/messaging.md +++ b/skills/codeman/reference/messaging.md @@ -56,7 +56,7 @@ own head: the worker enforcing the cap is the one who has to be told about it. | synchronize on end of turn | HTTP `wait until=stop` (fires for message-initiated turns too, verified live) | | liveness / death check | HTTP `wait?until=exit` | | interrupt a running turn (break-glass) | HTTP input, a bare `\x1b` with no `\r` | -| non-claude modes (`shell`/`opencode`/`codex`/`gemini`/`antigravity`/`pi`/`grok`/`deepseek`/`omp`) | HTTP only (no other CLI has messaging) | +| non-claude modes (`shell`/`opencode`/`codex`/`gemini`/`antigravity`/`pi`/`grok`/`deepseek`/`omp`/`copilot`) | HTTP only (no other CLI has messaging) | | delete | HTTP, via SKILL.md's `delete_session` guard | ## Availability: probe, never assume @@ -352,7 +352,7 @@ Without a break-glass, a pair with a bad brief is a token bonfire with no off sw ### Mixed fleets: the pairing matrix -Non-claude workers (`shell`, `opencode`, `codex`, `gemini`, `antigravity`, `pi`, `grok`, `deepseek`, `omp`) cannot be peers +Non-claude workers (`shell`, `opencode`, `codex`, `gemini`, `antigravity`, `pi`, `grok`, `deepseek`, `omp`, `copilot`) cannot be peers at all; no other CLI has this feature. Their tasks route over HTTP, and you never mention messaging in their briefs. The claude half of the fleet can use messaging among itself, subject to the namespace rule: **messaging works between two sessions that share one diff --git a/skills/codeman/reference/recipes.md b/skills/codeman/reference/recipes.md index 39867a6d..be1db504 100644 --- a/skills/codeman/reference/recipes.md +++ b/skills/codeman/reference/recipes.md @@ -188,7 +188,7 @@ for _ in $(seq 1 10); do done printf '%s\n' "$TXT" # (.data is {text,timestamp}; text is also "" before the first completed turn and -# always "" for shell/opencode/gemini/antigravity/pi/grok/omp, which have no transcript, use +# always "" for shell/opencode/gemini/antigravity/pi/grok/omp/copilot, which have no transcript, use # the terminal tail there, and here only to diagnose an unsubmitted prompt.) # 6. clean up: exact id, own list only, through the fail-closed preamble helper diff --git a/skills/codeman/reference/verbs.md b/skills/codeman/reference/verbs.md index 690cdbf3..6ad55103 100644 --- a/skills/codeman/reference/verbs.md +++ b/skills/codeman/reference/verbs.md @@ -376,7 +376,7 @@ recovered by submitting it with `{"input":"\r"}`. only when the workspace actually has them, see [§5.1](#51-where-to-spawn)) **and for `deepseek`** — the one external CLI that reports its own lifecycle, so its `stop` is a real end-of-turn signal rather than a guess. On -`shell`/`opencode`/`codex`/`gemini`/`antigravity`/`pi`/`grok`/`omp`, requesting them explicitly is a +`shell`/`opencode`/`codex`/`gemini`/`antigravity`/`pi`/`grok`/`omp`/`copilot`, requesting them explicitly is a 400 (except `blocked` on `codex`, which Codeman reads off its screen when an approval, question or trust dialog is up; a wait for it answers at once if one already is), and lifecycle transitions there are coarse (a short shell command may emit **no** @@ -430,7 +430,7 @@ from the transcript file, which is flushed slightly *after* the `stop` hook fire single read taken the instant send-and-wait returns comes back `""` even though the turn finished (verified live: empty on the first call, full text seconds later). `text` is also `""` before the worker's first completed turn, and always `""` for modes with -no transcript (`shell`, `opencode`, `gemini`, `antigravity`, `pi`, `grok`, `omp`; the first four +no transcript (`shell`, `opencode`, `gemini`, `antigravity`, `pi`, `grok`, `omp`, `copilot`; the first four verified live, pi from the same source path), which is why the loop above is bounded rather than open-ended. A dsh worker lags too, for its own reason: the harness finalizes the assistant message just after it reports `idle`. Fall back to the terminal buffer @@ -516,7 +516,7 @@ turn), and both better than diffing terminal samples: ``` ⚠️ `active-tools` is parsed out of Claude's own output format, so it is **empty for -`opencode`/`codex`/`gemini`/`antigravity`/`pi`/`grok`/`deepseek`/`omp`** (those parsers are skipped wholesale) and +`opencode`/`codex`/`gemini`/`antigravity`/`pi`/`grok`/`deepseek`/`omp`/`copilot`** (those parsers are skipped wholesale) and in practice empty for `shell`. Source-verified, not measured live. Only if neither helps: sample `terminal?tail=` twice a few seconds apart. A changing diff --git a/src/config/cli-registry/stock.ts b/src/config/cli-registry/stock.ts index fc874532..6122da56 100644 --- a/src/config/cli-registry/stock.ts +++ b/src/config/cli-registry/stock.ts @@ -1652,5 +1652,163 @@ const OMP: CliEntry = { }, }; +// GitHub Copilot CLI (`copilot`, github/copilot-cli, npm @github/copilot). Measured on a live +// 1.0.94 pane inside tmux (capture-pane through real turns, a shell tool call and /model, +// 2026-10-09). It is an ALTERNATE-SCREEN TUI with mouse tracking on (tmux `alternate_on=1`, +// `mouse_any_flag=1`, `history_size=0`), so it joins grok in the tmux-attach-time strip only. +// Keystrokes sent with `tmux send-keys` reach its composer and a shell tool call completes +// inside tmux, which github/copilot-cli#4180 (a driven PTY ignoring input) and #4223 (a shell +// command never reported done under tmux) say they do not on 1.0.70 to 1.0.74: neither +// reproduces on 1.0.94, and the register of `version` below is what would show a regression. +const COPILOT: CliEntry = { + id: 'copilot' as CliEntry['id'], + label: 'GitHub Copilot', + shortBadge: 'CP', + accent: '#a371f7', + enabled: true, + stock: true, + order: 100, + kind: 'agent', + discovery: { + binaries: ['copilot'], + searchDirs: [HOME_DIRS.local, HOME_DIRS.usrLocal, HOME_DIRS.bunBin, HOME_DIRS.npmGlobal, HOME_DIRS.homeBin], + // `copilot --version` prints `GitHub Copilot CLI 1.0.94.`; anchoring on the product name + // keeps an unrelated `copilot` binary on PATH from passing the probe. + version: { arg: '--version', regex: 'GitHub Copilot CLI (\\d+\\.\\d+\\.\\d+)', requireVersionMatch: true }, + install: { + command: { linux: 'npm install -g @github/copilot', darwin: 'npm install -g @github/copilot' }, + npmPackage: '@github/copilot', + docsUrl: 'https://github.com/github/copilot-cli', + }, + }, + // `--yolo` (allow every tool, path and URL) is opt-in through `allowAll`, which the Run button + // sends like the other agent CLIs' bypass switches; an absent config spawns a bare `copilot` + // in its own Manual Approval mode. `--model` and `--resume=` are Copilot's own flags + // (`copilot --help`, 1.0.94). `--resume` takes an id, id prefix or session NAME, so the id + // pattern below is what keeps a name or a path out of it. + launch: { + params: { + allowAll: { type: 'bool' }, + model: { type: 'token', pattern: 'model' }, + resumeId: { type: 'token', pattern: 'id-dotted' }, + continueSession: { type: 'bool' }, + // The tab's name, so the CLI's own session list reads the same as the tab. + sessionName: { type: 'engine', source: 'sessionName' }, + }, + variants: [ + { + id: 'default', + args: [ + { lit: 'copilot' }, + { flag: '--yolo', when: { param: 'allowAll', is: true } }, + // `--name` is refused next to `--resume` and `--continue` (copilot 1.0.95: "cannot be used + // with"), and a resumed session keeps the name it was created with. + { + flag: '--name', + valueFrom: 'sessionName', + quote: 'double', + when: { + allOf: [ + { param: 'sessionName', state: 'set' }, + { param: 'resumeId', state: 'unset' }, + { not: { param: 'continueSession', is: true } }, + ], + }, + }, + { flag: '--model', valueFrom: 'model', when: { param: 'model', state: 'set' } }, + { flag: '--resume', valueFrom: 'resumeId', when: { param: 'resumeId', state: 'set' } }, + { + lit: '--continue', + when: { + allOf: [ + { param: 'continueSession', is: true }, + { param: 'resumeId', state: 'unset' }, + ], + }, + }, + ], + }, + ], + legacyConfigAliases: { resumeId: 'resumeSessionId' }, + legacyConfigField: 'copilotConfig', + resumeAppend: { style: 'flag', flag: '--resume' }, + }, + env: { + exports: [{ name: 'COLORTERM', value: 'truecolor' }], + unset: ['NO_COLOR'], + // Sign-in is the CLI's own (`copilot login`, kept under ~/.copilot); a token in the + // environment (COPILOT_GITHUB_TOKEN, GH_TOKEN, GITHUB_TOKEN) is what a headless host uses. + tmuxSetenvKeys: [], + dockerExecEnvNames: [], + allowedPrefixes: ['COPILOT_'], + allowedKeys: ['GH_TOKEN', 'GITHUB_TOKEN'], + }, + capabilities: { + ...agentDefaults(), + altScreen: 'strip-mux-only', + echo: { policy: 'buffer', anchor: { kind: 'cursor' } }, + // The composer row is a `❯` between two rules, and the submitted prompt is echoed as + // ` ❯