# sem-ai Agent-first CLI for [Semaphore CI/CD](https://semaphore.io). Structured JSON output, self-discovery, composable commands, and an embedded MCP server — built for AI agents to drive the full CI/CD loop without a browser. ## Why sem-ai - **JSON by default** — every command returns structured JSON. Agents parse it directly, humans use `--format table` - **Self-discovery** — `sem-ai discover` returns a full capability map. Every command supports `--examples` - **MCP server** — `sem-ai mcp` exposes all commands as native tools for Claude Code, Cursor, VS Code, and any MCP client - **Compound commands** — `diagnose` composes workflow → pipeline → failed jobs → logs → parsed test results into a single call - **Compatible** — shares `~/.sem.yaml` with the [legacy Semaphore CLI](https://github.com/semaphoreci/cli). Same tokens, same contexts **Full reference**: [docs.semaphore.io/reference/sem-ai-cli](https://docs.semaphore.io/reference/sem-ai-cli) ## Install ```sh curl -fsSL https://raw.githubusercontent.com/semaphoreio/sem-ai/main/install.sh | sh ``` Installs the latest release automatically. Supports macOS and Linux on amd64 and arm64. The binary is placed at `$HOME/.local/bin/sem-ai` when that directory is on `$PATH`, or at `$HOME/.semaphore-ai/bin/sem-ai` otherwise (a `$PATH` hint is printed to stderr in that case). Re-run the same command to update to the newest release (or to confirm you're already on the latest). Want to inspect what runs first? `curl -fsSL https://raw.githubusercontent.com/semaphoreio/sem-ai/main/install.sh | less` before piping to `sh`. ### Advanced / manual install For users who want to inspect-then-build, pin a specific commit, or work offline: ```sh git clone https://github.com/semaphoreio/sem-ai.git cd sem-ai make install ``` Requires Go 1.25+. ### Plugin install (Claude Code / Codex) sem-ai ships its skill bundle as a Claude Code / Codex plugin. **Claude Code** — install from the shell: ```sh claude plugin marketplace add semaphoreio/sem-ai claude plugin install sem-ai@semaphoreio ``` **Codex CLI** — install from the shell: ```sh codex plugin marketplace add semaphoreio/sem-ai codex plugin add sem-ai@semaphoreio ``` The plugin drops every sem-ai skill (debug-pipeline, deploy, fix-flaky, gha-to-semaphore, init, manage-infra, probe-agent-environment, project-health, sem-ai-bootstrap, semaphore-blocks, semaphore-ci, semaphore-preflight, semaphore-promotions, semaphore-test-results, semaphore-toolbox, test-intelligence, testbox, watch-after-push) into your host. Claude Code refreshes registered marketplaces at session start, picks up the new plugin version, and applies it automatically — there is no manifest flag to set for this. To force an immediate refresh: ```sh claude plugin marketplace update semaphoreio claude plugin update sem-ai@semaphoreio # restart running sessions to apply ``` Codex equivalent: `codex plugin marketplace upgrade semaphoreio`. Skills follow the [Agent Skills](https://agentskills.io) standard and give agents context on when and how to use each sem-ai command without reading documentation. #### Slash-command entry points User-invocable skills can be triggered directly with a namespaced slash command — useful when the activation keyword in a skill's description doesn't match your intent verbatim: | Slash command | What it does | |---|---| | `/sem-ai:init` | Initialize Semaphore CI/CD for the current repo — detects state (GHA present → translate; greenfield → from scratch; `.semaphore/` already → refine), applies defaults from the linked skills, validates, wires secrets, opens a PR | | `/sem-ai:gha-to-semaphore` | Translate only — same procedure as `init`'s translate path, scoped to converting `.github/workflows/*` | Other skills are loaded automatically when their description keywords match the user's prompt; the slash commands above are explicit triggers for the most common entry points. ### Skills-only install (`npx skills`, any agent) Prefer the full plugin above on Claude Code / Codex — it also wires the MCP server and the `SessionStart` hooks. For other agents (Cursor, OpenCode, …) or when you want just the skill instructions, use the cross-agent [`skills`](https://github.com/vercel-labs/skills) installer. It reads sem-ai's `.claude-plugin/marketplace.json` manifest, so it finds the bundle even though the skills live under `assets/plugin/skills/`: ```sh npx skills add semaphoreio/sem-ai --list # list available skills npx skills add semaphoreio/sem-ai --all # install all of them npx skills add semaphoreio/sem-ai --skill semaphore-ci --skill watch-after-push npx skills add semaphoreio/sem-ai --all -g # user-level (every repo) npx skills add semaphoreio/sem-ai --all --agent cursor opencode ``` Two caveats: - **Skills only.** `npx skills` installs `SKILL.md` files — not the MCP server and not the `SessionStart` hooks (release-update check, Semaphore-repo awareness). For those, use the plugin install above. - **The CLI is a prerequisite.** Every skill shells out to `sem-ai`; install the binary first (see [Install](#install)) and run `sem-ai connect`. `npx skills` installs instructions, not the CLI. ## Updates When running the sem-ai plugin in Claude Code or Codex, a `SessionStart` hook checks GitHub for new releases at most once every 6 hours and surfaces a one-line notice in chat when one is available: ``` sem-ai 0.4.1 is available (you have 0.3.0). Upgrade: curl -fsSL https://raw.githubusercontent.com/semaphoreio/sem-ai/main/install.sh | sh ``` To check from a shell at any time: ```shell sem-ai version --check ``` To opt out (honored by both the plugin hook and manual CLI): ```shell export SEM_AI_NO_UPDATE_CHECK=1 ``` Upgrade by re-running the install script — it fast-paths if you're already on latest: ```shell curl -fsSL https://raw.githubusercontent.com/semaphoreio/sem-ai/main/install.sh | sh ``` ## Quick start ```shell # Connect to your org (get token at https://me.semaphoreci.com/account) sem-ai connect myorg.semaphoreci.com YOUR_API_TOKEN # Check CI status sem-ai status --project my-app --branch main # Diagnose a failure — one command, full root cause sem-ai diagnose --project my-app --branch main ``` ## MCP server Run sem-ai as a persistent MCP server. Starts once, handles all tool calls through the in-memory command tree. ```shell sem-ai mcp ``` ### Claude Code Add to `.mcp.json` in your project: ```json { "mcpServers": { "semaphore": { "command": "sem-ai", "args": ["mcp"] } } } ``` All commands become native MCP tools (`project_list`, `diagnose`, `status`, `blast-radius`, etc). Long-running commands (`watch`, `promote-and-wait`) are excluded to prevent blocking. ## Commands ### Projects | Command | Description | |---------|-------------| | `project list` | List all projects | | `project show ` | Show project details | | `project update ` | Update project settings | | `project delete ` | Delete a project | ### Workflows | Command | Description | |---------|-------------| | `workflow list --project

[--branch ] [--days ] [--limit ] [--full]` | List workflows (trimmed summaries, last 30 days by default; `--full` for the raw payload over the full history) | | `workflow show ` | Show workflow details | | `workflow run --project

` | Rerun the latest workflow | | `workflow rerun ` | Rerun a workflow | | `workflow stop ` | Stop a workflow | ### Pipelines | Command | Description | |---------|-------------| | `pipeline show ` | Show pipeline with blocks and jobs | | `pipeline list --project

[--branch ] [--days ] [--limit ] [--full]` | List pipelines (trimmed summaries, last 30 days by default; `--full` for the raw payload over the full history) | | `pipeline stop ` | Stop a pipeline | | `pipeline rebuild ` | Trigger partial rebuild (API call only) | | `pipeline promote ` | Trigger a promotion (deploy) | | `pipeline topology ` | Show block dependency graph | ### Jobs | Command | Description | |---------|-------------| | `job list --states RUNNING` | List jobs by state | | `job show ` | Show job details | | `job log ` | Fetch structured job logs | | `job stop ` | Stop a running job | ### Analytics | Command | Description | |---------|-------------| | `analytics summary --project

` | Pass rate, duration, failures, deploys — all in one | | `analytics duration --project

` | Duration trends (avg, p50, p95) with phase breakdown | | `analytics failures --project

` | Block-level failure rates and failure reasons | | `analytics queue --project

` | Queue wait time stats | | `analytics deploys --project

` | Deploy frequency (per day / per week) | | `analytics trend --project

` | Week-over-week trends for all key metrics | All analytics commands accept `--project` (auto-detected from git), `--days`, `--branch`, and `--limit`. `analytics trend` uses `--weeks` instead of `--days`. ### Test intelligence | Command | Description | |---------|-------------| | `test summary --pipeline ` | Parsed test results with file:line:message | | `test report --pipeline ` | Detailed test report | | `test flaky --project

` | Detect flaky tests across recent runs | ### Deployment targets | Command | Description | |---------|-------------| | `deploy targets --project

` | List targets | | `deploy show ` | Show target details | | `deploy history ` | Deployment history | | `deploy create ` | Create a target | | `deploy activate ` | Activate a target | | `deploy deactivate ` | Deactivate a target | | `deploy delete ` | Delete a target | ### Pre-flight checks Commands that run at pipeline initialization, before any block. One check per organization, one per project — `--project` selects the project check, omitting it selects the organization-wide one. | Command | Description | |---------|-------------| | `pfc show [--project

]` | Show the check (commands, secrets, agent) | | `pfc apply [--project

]` | Create or replace the check, from flags or `--from-file` | | `pfc delete [--project

]` | Remove the check | ### Secrets | Command | Description | |---------|-------------| | `secret list` | List secrets (org or project level) | | `secret show ` | Show secret details | | `secret create ` | Create a secret | | `secret update ` | Update a secret | | `secret delete ` | Delete a secret | ### Notifications, tasks, agents | Command | Description | |---------|-------------| | `notification list/show/create/delete` | Notification rules | | `task list/show/create/update/run/delete` | Scheduled tasks | | `agent types/show/list/delete` | Self-hosted agent management | ### Artifacts | Command | Description | |---------|-------------| | `artifact list --scope jobs --id ` | List artifacts | | `artifact get --scope jobs --id --path

` | Download an artifact | ### Utility | Command | Description | |---------|-------------| | `open` | Open workflow/pipeline in browser | | `watch ` | Poll workflow until done, streaming status | | `version` | Print version information | | `yaml validate --file ` | Validate pipeline YAML | | `troubleshoot workflow/pipeline/job ` | Server-side diagnostics | ## Compound commands These compose multiple API calls into a single operation. | Command | What it does | |---------|-------------| | `status` | CI status for a branch — pipeline state, block results | | `diagnose` | Full failure diagnosis — logs, test results, root cause | | `health` | Project health — pass rates, trends, deploy status, verdict | | `analytics summary` | All-in-one analytics overview for a project over a time window | | `analytics trend` | Week-over-week trends — pass rate, duration, queue, failures | | `critical-path ` | Longest dependency chain (bottleneck) | | `blast-radius ` | Root failures vs cascading cancellations | | `rerun-failed ` | Partial rebuild of failed blocks only | | `promote-and-wait ` | Promote and block until promoted pipeline finishes | ## Testbox Run commands in a real Semaphore CI environment before pushing. Creates a warm VM, syncs your local code, executes commands via SSH. ```shell # Start a testbox sem-ai testbox warmup --project my-app # Run tests in real CI env sem-ai testbox run --id "go test ./..." # Interactive SSH sem-ai testbox ssh --id # Stop when done sem-ai testbox stop --id ``` ## Output All commands output JSON by default: ```shell sem-ai status --project my-app # JSON sem-ai status --project my-app -f table # human-readable table sem-ai status --project my-app -f yaml # YAML ``` Errors are structured JSON on stderr: ```json {"error": true, "code": "not_found", "message": "project not found", "status": 404} ``` ## Configuration sem-ai uses `~/.sem.yaml` — the same config file as the [legacy Semaphore CLI](https://github.com/semaphoreci/cli). If you already have `sem` configured, sem-ai works immediately. ```shell sem-ai connect myorg.semaphoreci.com YOUR_TOKEN # add/update context sem-ai context list # list all orgs sem-ai context show # show active org ``` ### Pinning a context per invocation `context switch` writes the shared `active-context` key in `~/.sem.yaml`, so concurrent sessions (e.g. multiple agents on one machine) can flip each other's context mid-run. To pin a context without touching the file, pass `--context` on any command or set `SEM_CONTEXT`: ```shell sem-ai --context myorg_semaphoreci_com project list # this invocation only export SEM_CONTEXT=myorg_semaphoreci_com # every later call in this shell ``` Resolution precedence: 1. `--context` flag (named context, read-only) 2. `SEM_CONTEXT` env (named context, read-only) 3. `SEMAPHORE_HOST` / `SEMAPHORE_API_TOKEN` env (raw credentials, no config file needed) 4. `active-context` from `~/.sem.yaml` An explicit selector (1 or 2) fully shadows everything below it — no mixing of context credentials with env credentials. Without a selector, the raw env vars override the active context's values field by field. An unknown context name fails hard and lists the available contexts — except on the commands that create or report that state. `connect`, `signin`, `context switch`, and `context list` ignore both selectors: a pin naming a context that does not exist yet must not stop the command about to create it, and `context list`/`switch` report what `~/.sem.yaml` stores, so their active marking follows the file. `context show` reports what the current invocation resolves to, pin included, and each `context list` row carries `pinned` so a pinned caller can tell which one it is actually using. The selector is ignored on those commands, not applied to them: `connect` names the context it creates after its host (dots become underscores), so `SEM_CONTEXT=neworg sem-ai connect neworg.semaphoreci.com TOKEN` creates `neworg_semaphoreci_com` and leaves the pin pointing at a name that still does not exist. Onboarding then means updating the pin to the created name. Where `connect` is given both a `` and a selector that resolves to a *different* host, it refuses rather than picking one — it stores a token and moves `active-context`, so a two-organization invocation is a mistake worth failing. `signin` ignores the selector too, and says so on stderr. A context names an organization; `signin` authenticates an *account* against a deployment, and on Semaphore Cloud every organization's account lives on `me.semaphoreci.com` with the CLI-auth endpoints on `id.semaphoreci.com`. Taking signin's host from a pinned organization context would post the device flow to a host that does not serve it. Pass `[host]` for another deployment. Neither `connect` nor `signin` is exposed over MCP. `signin` runs a device flow that would hold the server's execution lock for the life of the grant and print its one-time code into a buffer the human only sees once the call returns; `connect` takes two positional arguments, which the tool-argument encoding cannot carry. Onboard on the command line, then point the MCP server at the context it created. This pins the *read* path: no selector changes `active-context` or any stored credential. (Any invocation still creates an empty `~/.sem.yaml` when none exists.) `context switch`, `connect`, and `signin` still write it, and `active-context` stays a shared key; pinning is how a session opts out of reading it. Writes replace the file through a temp file plus `rename(2)` (which needs a writable *directory*, so a config file deliberately made read-only is replaced rather than refused, and comes back `0600`), so a concurrent reader sees either the old config or the new one, never a half-written file. That is torn-read protection only: each write serializes the whole config as the writing process last read it, so two writes that overlap are last-writer-wins for the *entire file* — a context added by one can disappear when the other lands. Sequence commands that write, or let one session own the file. (The atomic path covers YAML; if viper picked up a non-YAML `~/.sem.json`, that write is still in-place.) ## Development ```shell # Build make build # Install locally make install # Run tests make test # Format and vet make fmt make vet ``` ### Project structure ``` cmd/ Command implementations (one file per resource) pkg/client/ HTTP client with retry, pagination, versioned API support pkg/config/ Configuration loading from ~/.sem.yaml pkg/output/ JSON/table/YAML output engine pkg/testparse/ Test result parsers (Go, pytest, rspec, minitest, jest, ExUnit, JUnit) pkg/gitutil/ Git remote/branch detection .agents/ Agent skill definitions ``` ### Adding a command 1. Create `cmd/.go` 2. Define a `cobra.Command` with `RunE`, `Short`, `Example` 3. Register it in `init()` with `rootCmd.AddCommand()` or as a subcommand 4. The command automatically appears in `discover`, MCP tools, and `--help`