The tempa CLI
tempa (client/, published as @devleaps/tempa) runs on the developer's laptop. It connects the device to a control plane, keeps the profile in sync and answers the harness's hooks.
Commands
| Command | What it does |
|---|---|
tempa login [--endpoint URL] [--tenant ID] [--default] | Reads <endpoint>/.well-known/tempa.json, runs the device flow (prints the URL and code, opens the browser when it can: only an https page on the endpoint's own origin, TEMPA-237), stores the tokens and pins the endpoint's profile signing keys. The discovery document's own addresses (API, device and token endpoints, MCP) must be on the endpoint's origin, and the others https. Without --tenant the device is logged in to the tenant the sign-in belongs to, which becomes that endpoint's default, and your other tenants are listed; with it, to that tenant of yours (asked for before the sign-in: the browser page names it), next to the logins you have (Several tenants). --endpoint adds a login for that control plane; it becomes the default endpoint when the current default has no login yet, or with --default (Several endpoints) |
tempa status [--offline] | Endpoint and where it came from, user, tenant, default team, device id and login state of the default login, then every other tenant login, then every other endpoint with its tenants. Checks each login with GET /v1/me unless --offline. Exit 1 when one is not logged in, expired or revoked |
tempa sync [--repo PATH] [--endpoint URL] [--tenant ID] [--harness ID] | Fetches, verifies and caches the profile for the repo at PATH (see below), with the login of the repo's tenant. --tenant makes the repo use that tenant from now on, --endpoint that control plane (its default tenant, or --tenant there). --harness picks whose layout to render: claude-code (default) or copilot |
tempa mcp [--harness ID] | The local stdio MCP proxy the plugin starts (mcp-proxy.md) |
tempa otel-headers [--harness copilot] | Prints the OTLP auth headers for Claude Code's otelHeadersHelper (see Telemetry), or with --harness copilot for Copilot's telemetry.headersProvider (the repo from TEMPA_REPO_DIR) |
tempa otel-env --harness copilot [--pid ID] | Prints GitHub Copilot's OpenTelemetry environment for the repo in the current directory as export lines, for the copilot wrapper in ~/.tempa/copilot-otel.sh; with --pid (the wrapper's, which becomes Copilot's) it records the repo for Copilot's headers provider (TEMPA-199, telemetry.md) |
tempa otel-env --harness codex | Prints Codex's telemetry settings for the repo in the current directory (the token as OTEL_EXPORTER_OTLP_HEADERS, -c otel.* overrides), for the codex wrapper in ~/.tempa/codex-otel.sh (telemetry.md) |
tempa otel-env --harness gemini-cli | Prints the Gemini CLI's GEMINI_TELEMETRY_* settings for the repo in the current directory as export lines (the token in GEMINI_TELEMETRY_OTLP_HEADERS, prompts off unless the profile opts in), for the gemini wrapper in ~/.tempa/gemini-otel.sh (telemetry.md) |
tempa setup [--yes] [--harness ID] | Finds the coding harnesses on this machine (or only that one), shows what Tempa supports in each and prints the commands that install its plugin; --yes runs them, and adds Antigravity's user-level hook group where the App or the IDE is installed (tempa setup) |
tempa doctor [--repo PATH] | Checks the setup, per harness (see below); exit 1 when any check fails |
tempa copilot cloud-agent enable [--repo PATH] [--base BRANCH] [--branch NAME] [--cli PATH] [--no-push] | Opens a pull request that opts the repo in to Tempa's guardrails in the GitHub Copilot cloud agent: .github/hooks/tempa.json and an offline bundle in .github/tempa/, committed on a new branch without touching the working tree; a person merges it (harnesses.md). Exit 1 when the repo is not on GitHub, the CLI is not the bundled one, the profile has no policy module, or the control plane cannot sign bundles yet (TEMPA-226) |
tempa policy parse|check [--repo PATH] COMMAND | Local policy evaluation without running anything (policies.md): parse prints the tree the policies read (exit 1 when it does not parse, so no policy decides it); check decides the command from the cached profile's policy module as the hook would, or says why the hook would ask /v1/decide (exit 1) |
tempa work [start REF [--title T] | stop] [--repo PATH] | Shows or sets the issue or story you work on in this repo: a URL, a key like ABC-123, or #123 for the repo's own issue. Usually not needed: a reference in a prompt or the branch name is picked up on its own (telemetry.md) |
tempa work step STEP [--repo PATH] | Says which SDLC step the work in this repo is in (develop.test, review.pr-open, ...), as the MCP work_step tool does; sessions follow at their next hook. The demo replay uses it for the steps no tool call gives away |
tempa audit verify FILE [--strict] [--head HASH] | Check an audit log export (JSONL or CSV, from tempa-web's Audit log page or GET /v1/tenant/audit/export) offline: every row's hash and the links between rows; --strict also a complete chain from the start, --head the export's X-Tempa-Chain-Head. Needs no login; exit 0 intact, 1 broken (audit.md) |
tempa attest [--no-training | --withdraw] | --no-training records on this device that model training is off for your Claude account (date and CLI version, sent with the next heartbeat); --withdraw takes it back; without options it shows the kind of Claude account Tempa saw in the last session and the attestation (accounts.md) |
tempa lockdown copilot --tempa-path PATH [--tempa-path-windows PATH] [--out DIR] [--strict-marketplaces] [--only-tempa-mcp] [--otel-endpoint URL | --no-telemetry] | Administrators: writes GitHub Copilot's lock-down pack (managed-settings.json, policy.d/50-tempa.json, the macOS and Windows MDM forms) into DIR (default tempa-copilot-lockdown) to review and deploy; writes nothing outside DIR and refuses Copilot's own managed-settings folders (install.md) |
tempa lockdown codex --tempa-path PATH [--tempa-path-windows PATH] [--out DIR] [--strict-marketplaces] [--only-tempa-mcp] | Administrators: writes Codex's lock-down pack (requirements.toml and its macOS MDM payload) into DIR (default tempa-codex-lockdown); writes nothing outside DIR and refuses /etc/codex (install.md) |
tempa lockdown gemini --tempa-path PATH [--tempa-path-windows PATH] [--out DIR] [--only-tempa-mcp] [--strict-extensions] | Administrators: writes the Gemini CLI's lock-down pack (system settings.json with Tempa's hooks, policies/tempa-lockdown.toml admin policies, a Windows settings.json and install.sh for MDM) into DIR (default tempa-gemini-lockdown); writes nothing outside DIR and refuses Gemini's system folders and the one GEMINI_CLI_SYSTEM_SETTINGS_PATH names (install.md) |
tempa lockdown antigravity --tempa-path PATH [--tempa-path-windows PATH] [--out DIR] [--allow-mcp ID=URL,...] | Administrators: writes Antigravity's lock-down pack (the tempa-lockdown hook group, a permissions.deny fragment, the Gemini Enterprise MCP allowlist, and install.sh, install.ps1 and merge.mjs, the per-user merge an MDM runs) into DIR (default tempa-antigravity-lockdown); writes nothing outside DIR and refuses ~/.gemini, $GEMINI_HOME and tempa paths with spaces or quotes. Advisory: a developer owns the files and can move them with GEMINI_HOME (install.md) |
tempa config validate [DIR] [--endpoint URL] [--tenant ID] | Validates the config folder DIR (default: the current directory, with tenant.yaml) with tempa-api's validator and prints the issues; no draft is made (Config in CI) |
tempa config diff [DIR] [--endpoint URL] [--tenant ID] | The same check, printing what changes against the live version: each file's diff and the semantic summary |
tempa config push [DIR] [--draft] [--title T] [--endpoint URL] [--tenant ID] | Uploads the folder as a draft and publishes it; with --draft it stops at the draft for review in tempa-web |
tempa config token create --name NAME [--days N], token list, token revoke ID | Platform owners: config tokens for CI (the token is printed once, on stdout) |
tempa library publish DIR [--endpoint URL] [--visibility public|private] [--forbid a,b] | Operators only: uploads the library pack in DIR as a tar.gz with the Entra access token in TEMPA_OPERATOR_TOKEN (a device login is never an operator) and prints the validator's issues when it is refused (library.md) |
tempa library publish --partner DIR [--endpoint URL] [--tenant ID] [--visibility partner_clients|private] [--forbid a,b] | Partner admins (TEMPA-169): uploads the pack in DIR (publisher: the partner tenant's id) for the partner's clients with your own device login (tempa login; --tenant picks the partner tenant's login), no operator token; never public (library.md) |
tempa clean [--repo PATH] [--all] [--dry-run] | Takes back what Tempa rendered into the repo, for every harness, and its .git/info/exclude blocks; with --all every repo it remembers rendering into, plus its keys in ~/.claude/settings.json and settings.json.tempa-backup (Cleaning up) |
tempa logout [--endpoint URL] [--tenant ID] | Deletes the local tokens and cached profiles (with --tenant: only that tenant's login, its cached profiles and its repos; with --endpoint: only that control plane's logins, or that tenant there). The device stays registered until an admin revokes it |
A device login is scoped down: it never reads or changes the tenant's secrets, settings or domains, grants roles or invites people, or exports the tenant, whatever your roles. Do those in Tempa in your browser. Every command above keeps working with it (auth.md).
Which control plane
The first of these wins:
- The MDM-managed file:
/Library/Application Support/Tempa/config.json(macOS) or/etc/tempa/config.json(Linux),{"endpoint": "https://..."}. A--endpointflag that disagrees is ignored with a warning. tempa login --endpoint URL. A successful login with a flag saves it to~/.tempa/config.jsonwhen there is no login for the current default endpoint yet, or with--default; otherwise the login is added next to the others and the default stays (Several endpoints).~/.tempa/config.json{"endpoint": "https://..."}.https://tempa.devleaps.nl.
This is the default endpoint: every repo uses it unless it was pinned to another with tempa sync --endpoint. Plain http:// is accepted only for localhost and 127.0.0.1, for local development.
Files
| Path | What |
|---|---|
~/.tempa/ | Mode 0700 |
~/.tempa/config.json | User settings: endpoint, decide_timeout_ms |
~/.tempa/credentials.json | Mode 0600: one login per endpoint and tenant (endpoint, api and token URLs, access and refresh token, expiry, device id, user, tenant). The default endpoint's default login at the top level, its other tenants under tenants, other endpoints under endpoints (Several endpoints) |
~/.tempa/pinned-keys.json | Mode 0600: per endpoint, the profile signing keys pinned at tempa login; another key only with an endorsement by a pinned one (TEMPA-237, profiles.md) |
~/.tempa/repo-tenants.json | Which tenant (and, when pinned, which endpoint) each repo uses, resolved or chosen with tempa sync --tenant / --endpoint |
~/.tempa/credentials.lock | Exists only while one process refreshes the tokens |
~/.tempa/cache/discovery/<sha256(endpoint), 16 hex>.json | The last discovery document of each endpoint (older CLIs kept one, cache/discovery.json, which is still read) |
~/.tempa/cache/<sha256(repo key)>/ | profile.json (signed, as served) and meta.json per repo |
~/.tempa/run/evaluator-<version>.sock | Mode 0600 in a 0700 directory: the resident policy evaluator (tempa policy serve, started by the hook, exits when idle; policies.md) |
~/.tempa/cache/policies/<sha256>.wasm | Mode 0600: the policy modules the cached profiles name (TEMPA-123b), verified against the signed hash; the others are pruned |
~/.tempa/cache/cli-update.json | Per login, what the control plane last said about this CLI's version (X-Tempa-Cli-Update, cli-versions.md) |
~/.tempa/work/<sha256(repo key)>.json | The work item set with tempa work start or the MCP work_item tool, per repo |
File modes (TEMPA-225). When the CLI rewrites a file it keeps that file's mode: a ~/.claude/settings.json, ~/.gemini/settings.json or ~/.codex/config.toml you made 0600 (because it holds keys) stays 0600, and so does settings.json.tempa-backup. A file the CLI creates is 0600, except files in a repo (rendered assets, settings.local.json, .git/info/exclude) and lock-down packs, which get 0644 masked by your umask. Files with their own mode (the Codex hook script, 0755) get exactly that.
Sync and the profile cache
tempa sync [--repo PATH] resolves the repo of PATH (default: the current directory), fetches GET /v1/profile?harness=claude-code&repo=<raw remote>, verifies the Ed25519 signature against the discovery document's signing_keys and caches it. It exits 1 when it had to fall back to the cache.
- Repo: the URL of
origin, else of the first remote, sent as is; the server and the CLI normalise it the same way (git@github.com:Org/Repo.git→github.com/org/repo;client/src/repo.tsis a port ofapi/tempa_api/profile/repo.py, keep them in step). Outside git, or in a repo without remotes, norepois sent and the server answers with the default team profile. - Signature: a profile that does not verify is refused and not cached. A key id the CLI does not know makes it fetch the discovery document once more (key rotation) before refusing.
- Cache:
~/.tempa/cache/<sha256(repo key)>/profile.jsonandmeta.json(endpoint,fetched_at, node, matched by, etag). The repo key is the normalised remote, or@default. The discovery document is cached in~/.tempa/cache/discovery.json(refetched after 24 hours) so cached profiles can be verified offline. A cached profile is used only while it is not pastexpires_at, comes from the current endpoint and still verifies, so editing it on disk makes it unusable.tempa logoutclears the cache. - Offline: when the control plane cannot be reached, the cached profile is used until it expires.
- Speed: the SessionStart hook uses the cache without a request while it is younger than the profile's
sync_interval(default 15 minutes). A warm sync is onegitcall plus reading and verifying two files.
Rendering into Claude Code
After a sync inside a git working tree, tempa sync (and the SessionStart hook) writes the profile into the repo without anything reaching its history. This is the Claude Code renderer; how renderers and the shared apply step work for every harness is in harnesses.md.
| Profile part | File |
|---|---|
| Standards | .claude/rules/tempa-<name>.md |
| Skills | .claude/skills/tempa-<name>/SKILL.md, plus the skill's other files next to it |
| Commands | .claude/commands/tempa-<name>.md |
| Agents | .claude/agents/tempa-<name>.md |
| Permissions | permissions.ask and deny in .claude/settings.local.json; allow rules are not rendered (TEMPA-213: they would skip Claude Code's own prompt) and ones an older CLI wrote are removed |
- Every Markdown file carries `
. Where the asset has YAML frontmatter, the frontmatter stays first and the header follows it. Skills and agents getname: tempa-<name>` so Claude Code's name matches the folder or file (frontmatter is created when the asset has none). - Every rendered path is listed in
.git/info/excludebetween# >>> tempaand# <<< tempa. Tempa never edits.gitignore, and never writes a path git tracks (it reports it and skips it). A file at a Tempa path that Tempa did not write is not overwritten either. settings.local.json: the developer's own entries and other keys stay as they are. Tempa only adds the profile's rules that are missing, and later removes only the rules it added.- The manifest
.git/tempa-manifest.json(per worktree:git rev-parse --git-path tempa-manifest.json) records each rendered file with the sha256 of what was written, the permission rules Tempa owns, the excluded paths and the profile version (manifest version 2; a version 1 manifest from an older CLI is migrated on the next sync). A sync removes files from an older profile; one the developer edited since is left in place (and stays excluded).tempa doctorcompares the files with the manifest to report drift, and the next sync restores edited files. - Outside a git repo nothing is written into the directory.
Rendering into GitHub Copilot
tempa sync --harness copilot (and the Copilot plugin's sessionStart hook) renders the same profile where GitHub Copilot reads it (TEMPA-162, client/src/render/copilot.ts). Everything above holds the same way, with Copilot's own manifest (tempa-manifest-copilot.json next to Claude Code's) and its own block in .git/info/exclude (# >>> tempa:copilot), so the two renders never remove each other's files.
| Profile part | File |
|---|---|
| Standards | .github/instructions/tempa-<name>.instructions.md, with applyTo |
| Skills | .github/skills/tempa-<name>/SKILL.md, plus the skill's other files next to it |
| Commands | As skills (commands-as-skills): .github/skills/tempa-<name>/SKILL.md, invoked as /tempa-<name> |
| Agents | .github/agents/tempa-<name>.agent.md |
| Permissions | Not rendered: Copilot has no permission rules file Tempa can write; the hook applies the policies |
applyTo: a standard's ownapplyTostays; otherwise Claude Code'spaths(a string, an inline list or a block list) become a comma-separatedapplyTo, and a standard without either applies to every file (applyTo: "**").pathsis dropped. The asset's description is added when the frontmatter has none.- Commands: the Copilot CLI has no command files, and it invokes a skill as
/<name>, so a command becomes the skilltempa-<name>, whose description says to use it when the developer asks for the command. A skill with the same name wins; Claude Code-only keys (allowed-tools,model, ...) are dropped. - Agents: Claude Code's
modelandcolorare dropped (Copilot does not know Claude Code's model aliases);toolsstays, since Copilot maps Claude Code's tool names by alias (Read,Grep,Bash, ...). - A team's own committed
.github/...file is never overwritten; onlytempa-*paths are Tempa's. tempa doctorchecks Copilot's render as well (Rendered files (GitHub Copilot),Git exclude (GitHub Copilot)) once a repo has one.
Rendering into Codex
tempa sync --harness codex (and the Codex plugin's SessionStart hook) renders the profile where OpenAI Codex reads it (TEMPA-144, client/src/render/codex.ts), with its own manifest (tempa-manifest-codex.json) and its own block in .git/info/exclude (# >>> tempa:codex).
| Profile part | File |
|---|---|
| Standards | The repo-root AGENTS.md, packed within 24 KiB (standards-within-size-cap); summarised standards in full in .codex/tempa-standards/<name>.md. When the repo commits its own AGENTS.md: AGENTS.override.md instead |
| Skills | .agents/skills/tempa-<name>/SKILL.md, plus the skill's other files next to it |
| Commands | As skills (commands-as-skills): .agents/skills/tempa-<name>/SKILL.md |
| Agents | .codex/agents/tempa-<name>.toml, name = "tempa_<name>" (snake_case, as in Codex's examples) |
| Permissions | .codex/rules/tempa-permissions.rules (TEMPA-143): ask shell rules as execpolicy prompt rules, deny shell prefixes as forbidden. Allow rules and anything a prefix rule cannot express are left to the hook and reported |
| MCP server | User level: a # >>> tempa block with [mcp_servers.tempa] in ~/.codex/config.toml, the same for every repo (TEMPA-146), running ~/.tempa/codex-mcp.sh (mode 755), which starts the newest Codex plugin's launcher or this CLI with mcp --harness codex (TEMPA-195, mcp-proxy.md) |
| Telemetry | User level: the codex wrapper ~/.tempa/codex-otel.sh, which the developer loads from their shell profile (TEMPA-147, telemetry.md) |
| Hooks | User level, for the IDE extension: ~/.codex/hooks.json (Tempa's own file, marked in its description) running ~/.tempa/codex-hook.sh (mode 755), which runs this CLI's tempa hook --harness codex (TEMPA-145, hooks.md) |
- The size cap. Codex reads one instructions file per directory, from the project root down to the working directory, and stops adding files once they reach 32 KiB together (
project_doc_max_bytes). Tempa keeps the root file within 24 KiB, which leaves room for the repo's ownAGENTS.mdfiles in subdirectories. Locked standards come first; standards go in full until one no longer fits, and from there each is one line with a pointer to its full text. A file over the budget is reported, never cut. - A committed
AGENTS.md. Tempa never overwrites it, and Codex reads only one file per directory, preferringAGENTS.override.md. So Tempa writesAGENTS.override.md(git-excluded) with the repo's own text first and Tempa's standards after it, counted together against the budget. It follows the committed file at every sync; between syncs an edit toAGENTS.mdonly reaches Codex after the next one. AnAGENTS.mdthe developer wrote but did not commit (no Tempa marker) is left alone and reported. - Agents:
developer_instructionsis the agent's body; its description comes from the frontmatter or the asset. Claude Code's frontmatter (tools,model,color, ...) is dropped and reported: Codex's agent files take Codex models and no tool list. - Permission rules (rules): Codex reads
prefix_rule(pattern = [...], decision = ...)from.codex/rules/*.rulesonce the project is trusted, and the most restrictive matching rule wins.Bash(<prefix>:*)andBash(<prefix> *)are prefixes. A plainBash(<command>)ask becomes a prefix rule on its words, which asks a little more often, never less; as a deny it is left out, so Codex never forbids more than Claude Code denies. Rules with wildcards elsewhere, quoting or other tools (Read(...)) are left out. Allow rules are not rendered: Codex'sallowruns a command outside its sandbox. The hook uses the same rules (see hooks.md). - Other harnesses reading these files. GitHub Copilot also reads
AGENTS.mdand.agents/skills, so on a machine with both, a Copilot session sees Tempa's standards and skills twice (as Codex's and as its own). tempa doctorchecks Codex's render as well (Rendered files (Codex)) once a repo has one.
Rendering into Gemini CLI
tempa sync --harness gemini-cli (and the Gemini extension's SessionStart hook) renders the profile where the Gemini CLI reads it (TEMPA-154, client/src/render/gemini.ts), with its own manifest (tempa-manifest-gemini-cli.json) and its own block in .git/info/exclude (# >>> tempa:gemini-cli).
| Profile part | File |
|---|---|
| Standards | The repo-root context file, every standard in full, locked ones first: GEMINI.md, or the first name the project's (else the user's) context.fileName sets that the repo does not commit (AGENTS.md, for example). When the repo commits every such name: TEMPA.md, added to context.fileName in .gemini/settings.json |
| Skills | .gemini/skills/tempa-<name>/SKILL.md, plus the skill's other files next to it |
| Commands | .gemini/commands/tempa-<name>.toml (/tempa-<name>): description, and the body as prompt with $ARGUMENTS as {{args}} |
| Agents | .gemini/agents/tempa-<name>.md, name: tempa-<name>; Claude Code's tools as Gemini's tool names |
| Permissions | User level (TEMPA-153): ~/.gemini/policies/tempa-permissions.toml, ask shell rules as Policy Engine ask_user rules and deny shell prefixes as deny (priority 900 and 950 in Gemini's user tier), with a # Tempa profile: <node> <version> origin line. Allow rules and anything a prefix cannot express are left to the hook and reported |
| Telemetry | User level (TEMPA-156): the gemini wrapper ~/.tempa/gemini-otel.sh, which the developer loads from their shell profile, and telemetry.logPrompts: false in ~/.gemini/settings.json (telemetry.md) |
- Context files are read without ignore rules. Gemini finds its context files from the working directory up to the git root (
memoryDiscovery.ts) and does not apply.gitignoreor.git/info/excludethere, so the git-excluded file is read. It does load them only in a trusted folder. - No size cap. Gemini documents none, so every standard is in full (Codex's 32 KiB cap does not apply).
- A committed
GEMINI.mdis never overwritten. Gemini reads every name incontext.fileNamein each directory, so Tempa writesTEMPA.mdand adds it to the names in the project's.gemini/settings.json(keeping the names Gemini read before). When the repo commits.gemini/settings.jsontoo, that edit is skipped and reported, andTEMPA.mdis not read: name it in your owncontext.fileName. - Next to Codex. Gemini also reads
.agents/skills(and prefers it) and, throughcontext.fileName, maybeAGENTS.md. A skill or the standards Codex's render already wrote there are not rendered again for Gemini, and reported (codexFilesfrom Codex's manifest). - Commands: Gemini has
{{args}}for the whole argument text and no positional arguments, so a body with$1,$2keeps them as text after a note that says what they mean, and is reported.argument-hintbecomes a line in the prompt;allowed-toolsandmodelare dropped and reported. - Agents:
Read,Grep,Bashand the other Claude Code tools becomeread_file,grep_search,run_shell_command, ...; a tool without a Gemini name (an MCP tool) is left out and reported, and so aremodelandcolor. - Permission rules at the user level. Gemini's workspace policies (
.gemini/policies) are disabled in its source, so the rules go to the user's policies, which apply in every repo. The file holds the rules of the repo whose session (ortempa sync) rendered last, and its origin line says which profile that was. Gemini reads policies when it starts, so a change applies from the next session. The rules are a second line: they ask and deny even when a hook times out, and on a Gemini CLI that cannot ask from its hooks they are what asks (hooks.md).Bash(<prefix>:*)andBash(<prefix> *)becomecommandPrefix; a plainBash(<command>)ask becomes a prefix too (it asks a little more often), and as a deny it is left out. tempa doctorchecks the render as well (Rendered files (Gemini CLI)) once a repo has one.
Rendering into Antigravity
tempa sync --harness antigravity (and the Antigravity plugin's SessionStart hook) renders the profile where Google Antigravity (the agy CLI, the App and the IDE) reads it (TEMPA-180, client/src/render/antigravity.ts), with its own manifest (tempa-manifest-antigravity.json) and its own block in .git/info/exclude (# >>> tempa:antigravity). Antigravity's customization docs were the source (read 2026-11-05); what only a real agy can confirm is in TEMPA-185.
| Profile part | File |
|---|---|
| Standards | .agents/rules/tempa-<name>.md, one rule per standard, each with the frontmatter Antigravity needs (a rule without it is silently dropped): trigger: always_on for locked standards and those without paths, else trigger: glob with Claude Code's paths as globs (a comma-separated string); description from the standard |
| Skills | .agents/skills/tempa-<name>/SKILL.md, plus the skill's other files next to it; the CLI offers a skill as /tempa-<name> |
| Commands | As skills (commands-as-skills) in .agents/skills/tempa-<name>/: Antigravity has no commands (plugins carry none, IDE workflows retired on 2026-10-19) |
| Agents | .agents/agents/tempa-<name>.md, name: tempa-<name>, subagent: true, mainAgent: false; Claude Code's tools as Antigravity's tool names |
| Permissions and MCP | User level: permissions.ask, permissions.deny and mcp(tempa_tempa/*) in permissions.allow of ~/.gemini/antigravity-cli/settings.json (TEMPA-179, TEMPA-181), see Antigravity permission rules |
| Telemetry | Nothing to render: Antigravity exports no OpenTelemetry, so Tempa's hook reports (telemetry-from-hook) |
- 24,000 bytes per rule. A standard whose rule would be larger is summarised in its rule with a pointer to its full text in
.agents/tempa-standards/<name>.md(standards-within-size-cap), outside.agents/rulesso it is not a rule itself; the sync reports it. Antigravity gives always-on rules 20,000 tokens together and turns the rest into pointers the agent reads on demand. - Next to Codex. Codex renders the same
.agents/skills/tempa-<name>/and the repo-rootAGENTS.md, and Antigravity reads both. A skill (or command) whoseSKILL.mdCodex's render holds is not rendered again; a copy Antigravity's render wrote before is handed over to Codex and left in place (handedOverin the plan), never deleted. When Codex's render wroteAGENTS.md, Antigravity reads the standards there and writes no rules. Codex'sAGENTS.override.mdis not one of the files Antigravity reads, so its rules are written then. - Next to the Gemini CLI. Antigravity reads
GEMINI.mdtoo: when the Gemini CLI's render wrote it (orAGENTS.md), Antigravity writes no rules. With both Codex'sAGENTS.mdand Gemini'sGEMINI.md, Antigravity sees the standards twice (both are always on and belong to their harness); the sync says so, and setting Gemini'scontext.fileNametoAGENTS.mdleaves one.TEMPA.mdis not read by Antigravity. Gemini's.gemini/skills,.gemini/commandsand.gemini/agentsare not Antigravity folders. - Only files of those renders that are still on disk count, so a deleted
AGENTS.mdbrings Antigravity's rules back at the next sync. - Agents:
Read,Grep,Bashand the other Claude Code tools becomeview_file,grep_search,run_command, ...; a tool without an Antigravity name (an MCP tool,TodoWrite) is left out and reported, since an unknown tool name can hang the subagent. An agent left with no known tool getstools: [].model(Claude's aliases are not Antigravity'sinherit,flash,pro) andcolorare dropped and reported. - Session start no longer carries the standards' text inside a git repo: the summary points at the rules. Outside git nothing is rendered, and the summary carries the standards as for the other harnesses.
tempa doctorchecks the render as well (Rendered files (Antigravity)) once a repo has one.
Antigravity permission rules
The profile's shell ask and deny rules also go into the Antigravity CLI's user settings (TEMPA-179, client/src/harness/antigravity-rules.ts), as a second line next to the hook's force_ask and deny (hooks.md): a deny there still holds when the hook fails open.
| Profile part | File |
|---|---|
| Permissions | User level: permissions.ask and permissions.deny in ~/.gemini/antigravity-cli/settings.json, as command(<prefix>) entries, a JSON list merge. Allow rules and anything a prefix cannot express are left to the hook and reported |
| MCP | The same file: mcp(tempa_tempa/*) in permissions.allow (TEMPA-181), so Tempa's MCP server does not ask before every call (mcp-proxy.md) |
- A list merge Tempa owns. Tempa adds its entries to the lists and removes only those it added when the profile drops them; the developer's own entries, also one equal to Tempa's, and every other setting stay.
- User level, so the rules apply in every repo, and the file holds the rules of the repo whose session (or
tempa sync) rendered last. That makes other repos stricter, never looser: allow rules are not rendered, since an allow there would skip Antigravity's own confirmation for every repo on the machine. - What a prefix can say. Antigravity's
command(...)matches a command word by word from its start.Bash(<prefix>:*)andBash(<prefix> *)becomecommand(<prefix>); a plainBash(<command>)ask becomes a prefix too (it asks a little more often), and as a deny it is left out. Wildcards, quoting, shell operators and rules for other tools are left out and reported. Policies written in Rego cannot be rendered ahead of time; their asks follow the hook. - The CLI only. The App and the IDE keep their permission rules somewhere the docs do not name, so they get only the hook's answers (TEMPA-185).
- Written by Antigravity's renderer (TEMPA-180) at session start and on
tempa sync --harness antigravity; a settings file that is not valid JSON is left alone and reported.
Telemetry
Design and ingest rules: telemetry.md. What the CLI does:
- Claude Code's own telemetry. After every sync (
tempa syncand SessionStart) Tempa writes itsenvblock andotelHeadersHelperinto~/.claude/settings.json:CLAUDE_CODE_ENABLE_TELEMETRY=1, OTLP logs and metrics overhttp/protobufto the discovery document'sotel_endpoint, delta temporality andOTEL_RESOURCE_ATTRIBUTES=tempa.device=<device id>.OTEL_LOG_USER_PROMPTSandOTEL_LOG_TOOL_DETAILSare only set when the profile'stelemetry.log_prompts/log_tool_detailsare on; withtelemetry.enabledoff the block is removed. Every other key in the file stays as it was. The file is copied once tosettings.json.tempa-backupbefore Tempa first changes it, and~/.tempa/claude-settings.jsonrecords which keys Tempa owns.tempa logoutremoves exactly those. Claude Code reads the block at start, so a change applies from the next session. - Auth.
otelHeadersHelperrunstempa otel-headers(with absolute node and CLI paths when known), which prints theheadersofPOST /v1/telemetry/tokenfor the current repo, for example{"Authorization":"Bearer eyJ…"}. Tokens are cached in~/.tempa/telemetry/<sha256(repo)>.json(0600) and renewed when less than 30 minutes are left; a still-valid cached token is used when the control plane is down. - Tempa's own events are OTLP log records (
service.name=tempa-cli, the event name as body andevent.name,session.idwhen the event belongs to a session), spooled in~/.tempa/spool/and posted as OTLP/HTTP JSON to<otel_endpoint>/v1/logswith the repo's telemetry token:Event Attributes When profile.syncedprofile_version,changed,duration_msa profile was fetched from the control plane policy.decisiondecision,policy_ids(string array),bundle_version,tool_name,latency_msevery PreToolUse that is not the read-only fast path; local MCP denials have policy id tempa/mcp-allowlist, offline decisionstempa/offlinesession.offline_cachedcache_age_secondsfirst offline decision of a session with a cached profile session.offline_no_profilenone first PreToolUse of a session without a profile render.driftfiles,repoa sync restored rendered files that had been edited Records that cannot be sent stay in the spool; records older than a week are dropped, and so is everything when the token says telemetry is off.
tempa setup
tempa setup (TEMPA-138) is the first command on a new machine, or after installing another harness. It only looks: a harness counts as found when its binary is on the PATH or its config folder exists. It never reads or changes the harness's files beyond checking whether Tempa's plugin is installed.
| Harness | Binary | Config folder | Plugin installed when | Install commands |
|---|---|---|---|---|
| Claude Code | claude | $CLAUDE_CONFIG_DIR or ~/.claude | claude plugin list --json (or enabledPlugins) has tempa@devleaps enabled | claude plugin marketplace add Devleaps/tempa, claude plugin install tempa@devleaps |
| GitHub Copilot | copilot | $COPILOT_HOME or ~/.copilot | installed-plugins/devleaps/tempa/bin/tempa-launch exists there | copilot plugin marketplace add Devleaps/tempa, copilot plugin install tempa@devleaps |
| Codex | codex | $CODEX_HOME or ~/.codex | plugins/cache/devleaps/tempa/<version>/bin/tempa-launch exists there | codex plugin marketplace add Devleaps/tempa; then the developer installs Tempa from /plugins in Codex (Codex has no install command) |
| Gemini CLI | gemini | ~/.gemini | extensions/tempa/gemini-extension.json exists there (whether it is disabled is not checked) | gemini extensions install https://github.com/Devleaps/tempa --ref plugin-release-gemini-cli --consent |
| Antigravity | agy | $GEMINI_HOME/config or ~/.gemini/config | plugins/tempa/plugin.json exists there (whether it is disabled is not checked) | agy plugin install https://github.com/Devleaps/tempa/tree/plugin-release-antigravity; without agy (the App or the IDE alone), git clone --branch plugin-release-antigravity --depth 1 https://github.com/Devleaps/tempa.git ~/.gemini/config/plugins/tempa |
Per harness it prints whether it was found, what Tempa supports there (from the capability matrix: how many capabilities are native, which are degraded and by which rule, how its hooks fail; see harnesses.md) and, for a supported harness without the plugin, the exact commands. Without --yes nothing runs; with it, the commands run one by one (no shell), and the first failure stops that harness's install and makes the exit code 1. Where the harness has no command for a step (installing the Codex plugin), the report says what to do instead. --harness ID limits all of this to one harness.
For Antigravity it also lists the surfaces it found: the CLI (agy on the PATH), the App (/Applications or ~/Applications on macOS, %LOCALAPPDATA%\Programs\Antigravity on Windows, antigravity on the PATH on Linux; TEMPA_ANTIGRAVITY_APP names another place) and the IDE (its data folder, ~/.gemini/antigravity-ide). Where the App or the IDE is installed and the plugin is in place, --yes writes Tempa's tempa group into ~/.gemini/config/hooks.json (TEMPA-184, install.md): a JSON merge of that one key, written only when it differs from the installed plugin's hook lines. A file that is not a JSON object is left alone. tempa doctor reports the same (Antigravity surfaces, Antigravity user-level hooks, Antigravity hooks fired).
There is one login for all harnesses: credentials are stored per endpoint and tenant in ~/.tempa, and every plugin's hook runs the same tempa. tempa setup shows the login (without asking the control plane) and, when there is none, says to run tempa login once.
tempa doctor
Prints one line per check ([ ok ], [warn] or [FAIL]) and a Fix: line under anything that is not fine. It exits 1 when a check fails; warnings do not fail it. It first stores the node and tempa it runs with in ~/.tempa/node-path and ~/.tempa/cli-path for the plugin's launcher.
| Check | Fails when |
|---|---|
| Node.js | older than 20 |
| tempa on PATH | tempa is not on PATH |
| Harnesses | never; a warning when no coding harness is found (tempa setup detects them the same way) |
| Claude Code | claude is missing or older than 2.1.295 (older versions ignore onFailure, so guardrails fail open). When Claude Code is not installed but another supported harness is, it passes as "not installed" and Plugin, Minimal PATH launch and Telemetry are not checked for it |
| GitHub Copilot | only when Copilot is found: tempa@devleaps is not in ~/.copilot/installed-plugins/devleaps/tempa (or under $COPILOT_HOME); the fix is tempa setup --yes |
| GitHub Copilot launch | the Copilot plugin's launcher cannot start tempa with PATH=/usr/bin:/bin |
| Codex | only when Codex is found: no version of tempa@devleaps in ~/.codex/plugins/cache/devleaps/tempa/ (or under $CODEX_HOME); the fix is tempa setup --yes and installing Tempa from /plugins |
| Codex launch | the newest installed Codex plugin's launcher cannot start tempa with PATH=/usr/bin:/bin |
| Codex hook trust | Codex has no trust entry ([hooks.state."<key>"] trusted_hash in its config.toml) for any of Tempa's pre_tool_use hooks, so no guardrail runs; a warning names every other Tempa hook still to trust in /hooks (TEMPA-195) |
| Codex MCP server | the command of [mcp_servers.tempa] in ~/.codex/config.toml is missing; a warning when there is no such block, or when it is the /tempa:install-cli wrapper, which may run an older CLI (TEMPA-195) |
| Gemini CLI | only a warning when found: Tempa does not support it yet, so its tool calls are not guarded |
| Plugin | tempa@devleaps is not installed or not enabled (claude plugin list --json, enabledPlugins in ~/.claude/settings.json) |
| Minimal PATH launch | the installed plugin's launcher cannot start tempa with PATH=/usr/bin:/bin (what an IDE-started Claude Code may have); on Windows bin\tempa-launch.cmd through cmd.exe, with only Windows' own folders on the PATH (install.md) |
| Login | not logged in, expired, revoked, or logged in to another endpoint (a warning when the control plane cannot be reached to verify it) |
| Endpoint | the discovery document does not answer within 5 s |
| CLI version | the CLI is below the installation's or the repo's tenant's minimum (a warning when a newer release is available); a development build passes (cli-versions.md) |
| Profile | no cached profile for this repo, a signature that does not verify, or past expires_at (a warning when much older than the sync interval) |
| Rendered files | a rendered file is missing or changed since the last sync (a warning when nothing was rendered yet) |
| Git exclude | a rendered path is missing from the Tempa block in .git/info/exclude |
| Telemetry | the env block or otelHeadersHelper is missing from ~/.claude/settings.json, or no telemetry token can be had (passes when the profile turns telemetry off) |
Cleaning up
tempa clean [--repo PATH] [--all] [--dry-run] (TEMPA-241) takes back what Tempa wrote, for every harness that rendered into the repo (Claude Code, Codex, Gemini CLI, Copilot, Antigravity): it renders an empty profile, which removes exactly the files Tempa wrote and nobody changed since and Tempa's own entries in settings files such as .claude/settings.local.json, then drops the harness's block from .git/info/exclude and its manifest. A file you changed after Tempa wrote it is kept and listed (it shows up in git status once the exclude block is gone). Running it again is harmless.
- Without flags: the repo of the current directory (
--repopicks another). --all: every repo Tempa remembers rendering into (~/.tempa/rendered-repos.json, kept since TEMPA-241; a repo rendered by an older CLI is cleaned when you runtempa cleanin it), plus Tempa's keys in~/.claude/settings.jsonand thesettings.json.tempa-backupit made. Use it before uninstalling.--dry-run: lists what it would remove and changes nothing.
User-level files of Codex, Gemini CLI and Antigravity (~/.codex/config.toml's Tempa block, Gemini's and Antigravity's user hooks and policies) are not taken back by tempa clean; tempa doctor names them, and their install guides say how to remove them. tempa logout is separate: it forgets the tokens and cached profiles.
Several tenants
A consultant can belong to several tenants, for example Devleaps and a client (TEMPA-066, auth.md). A device token is for exactly one tenant, so the laptop keeps one login per tenant:
tempa login # the tenant your sign-in belongs to: the default; lists your other tenants
tempa login --tenant acme # a second login, for acme: the browser page names acme before you sign in
tempa status # every login
tempa logout --tenant acme # forget only that onetempa login --tenant asks for the tenant with the device code (TEMPA-113): the verification page says "Connecting <laptop> to Acme", and the server approves the device for acme only when you belong there (or hold an invite for a verified address, or may join through a verified domain). Not a member: the page says so and the CLI stops at once ("Login refused: You are not a member of tenant acme …"). The CLI also names the tenant when it collects the tokens, so a control plane from before TEMPA-113 still checks it there (auth.md).
Credentials file. ~/.tempa/credentials.json holds the default login at the top level, with the same fields a single login always had, and the logins for the other tenants under tenants, keyed by tenant id. CLI 0.1.x reads the default login from it unchanged; a file written by CLI 0.1.x (one login, no tenant) is read as the default login, its tenant taken from the access token's tid, and gets the new fields at the next write. If an older CLI rewrites the file (a refresh), it keeps the default login and drops the others: log in to them again. Logins of other control planes sit under endpoints (Several endpoints). Each login's tokens refresh on their own, under the same lock file.
Which tenant a repo uses. With one login there is nothing to decide. With several, tempa sync (and the SessionStart hook when its answer is older than a day) asks every logged-in tenant GET /v1/repos/resolve?remote=<git remote>, and caches the answer in ~/.tempa/repo-tenants.json:
| The repo is registered in | The repo uses |
|---|---|
| one of your tenants | that tenant |
| none (or outside git) | the default tenant |
| several | the tenant chosen with tempa sync --tenant <id> in the repo (kept until you log out of it); else the default tenant when it is one of them, else the first by id, with a warning in tempa sync and at SessionStart saying how to choose |
An answer is cached only when every tenant answered; a tenant that cannot be reached never moves a repo. A control plane from before TEMPA-066 answers 404, which counts as not registered. tempa login forgets the resolved answers (the new login may change them), not the choices.
Everything for a repo uses its tenant's login: the profile (GET /v1/profile, cached with the tenant in meta.json; a cached profile of another tenant is not used), the hooks' /v1/decide and heartbeats (one per tenant device), telemetry tokens, and the MCP proxy's /v1/mcp/session. Hooks never ask the network which tenant a repo is in: they read the cached answer, else use the default tenant.
Several endpoints
A laptop can hold logins for more than one control plane (TEMPA-113): a client's dedicated installation next to the shared one, or dev next to prd. Each login belongs to one endpoint and one tenant there:
tempa login # the default endpoint (see Which control plane)
tempa login --endpoint https://tempa.dev.example # a second endpoint, next to it; the default stays
tempa login --endpoint https://tempa.dev.example --tenant acme
tempa login --endpoint https://tempa.dev.example --default # make it the default endpoint
tempa status # every endpoint and its tenants
tempa sync --endpoint https://tempa.dev.example # this repo uses that endpoint from now on
tempa logout --endpoint https://tempa.dev.example # forget that endpoint's loginsCredentials file. Keyed by endpoint, then tenant, and readable by every older CLI:
{
"endpoint": "https://tempa.devleaps.nl", "tenant": "devleaps", "access_token": "…", "…": "…",
"tenants": { "acme": { "endpoint": "https://tempa.devleaps.nl", "tenant": "acme", "…": "…" } },
"endpoints": {
"https://tempa.dev.example": {
"default": "devleaps",
"tenants": { "devleaps": { "endpoint": "https://tempa.dev.example", "…": "…" } }
}
}
}- the top level is the default endpoint's default login, the fields a single login always had: CLI 0.1.4 reads it unchanged;
tenantsare the default endpoint's other tenants, as TEMPA-066 wrote them;endpointsholds every other endpoint: its default tenant and its logins by tenant.
Every login names its own endpoint, so reading does not depend on where a login sits: an old single-login file and a TEMPA-066 file are read as they are and rewritten in this layout at the next write (migrate on read), and when the default endpoint changes (--default, ~/.tempa/config.json, the managed file) the next write moves that endpoint's default login to the top level. An older CLI that rewrites the file keeps the top level (and, from TEMPA-066 on, tenants); it may drop endpoints: log in to those again.
Which endpoint a repo uses. The default endpoint, and its tenant as in Several tenants, unless the repo was pinned with tempa sync --endpoint URL [--tenant ID] (kept in ~/.tempa/repo-tenants.json until you log out of that login; then it falls back to the default endpoint). Repo → tenant resolution stays per endpoint: only the default endpoint's tenants are asked. The profile, its cache (checked against that endpoint's discovery keys), /v1/decide, heartbeats, telemetry tokens and the MCP proxy all use the repo's login. The Claude Code env block in ~/.claude/settings.json is global and follows the default endpoint.
Discovery is cached per endpoint (~/.tempa/cache/discovery/<sha256(endpoint), 16 hex>.json), so every endpoint's profiles verify offline.
Tokens
The access token lives for an hour. Every request refreshes it when it has less than a minute left, and once more when the server answers 401 for another reason than a revocation. Refresh tokens rotate and a reused one logs the device out, so a lock file makes sure only one tempa process (hooks run in parallel) refreshes at a time; the others pick up the new tokens. A revoked device gets the server's own message, for example "This device was revoked on 2026-10-17. Run 'tempa login' to connect it again."
Config in CI (TEMPA-083)
A tenant whose config lives in a Git repository Tempa does not sync from (any host, any CI) checks it on every merge request and publishes it on merge with tempa config validate, diff and push.
Why through the API. The validator is tempa-api's (tempa_config, Python), and the CLI is Node. Rather than a second validator that drifts, validate and diff pack the folder as a tar.gz (.git left out) and send it to POST /v1/config/uploads/check, which validates it exactly as a publish would and compares it with the live version, without making a draft. So they need the network and a credential. Offline, from a checkout of this repo: cd api && uv run python -m tempa_config validate <dir>.
The credential: a config token. A platform owner makes one per pipeline, with their own device login:
tempa config token create --name gitlab-ci --days 90 # prints the token once: store it as the CI secret
tempa config token list
tempa config token revoke <id>(or POST /v1/tenant/config-tokens {"name", "expires_in_days"}, GET /v1/tenant/config-tokens, POST /v1/tenant/config-tokens/{id}/revoke). A config token (tct_<tenant>.<secret>) works only in its tenant, may only validate, draft and publish the config (config.read, config.edit, config.publish: nothing else, not the MCP server), expires after at most a year and is refused at once when revoked. Tempa keeps only its SHA-256; making and revoking are audited (config_token.create, config_token.revoke), and what it does is audited as config-token:<name>. The CLI reads it from TEMPA_CONFIG_TOKEN (never a flag) and the control plane from --endpoint or TEMPA_ENDPOINT; without a config token it uses the device's login.
Exit codes
| Code | Meaning |
|---|---|
| 0 | valid; pushed (or the draft is made) |
| 1 | the config is not valid, or the push was refused (for example a git-mode tenant, whose repo Tempa syncs is the truth: publish is a pull request there) |
| 2 | wrong usage, or DIR is not a config folder |
| 3 | Tempa could not be reached, or refused the token |
GitLab CI
# .gitlab-ci.yml (TEMPA_CONFIG_TOKEN and TEMPA_ENDPOINT as masked, protected CI/CD variables)
tempa-config:
image: node:20
before_script:
- npm config set @devleaps:registry https://npm.pkg.github.com
- npm install -g @devleaps/tempa
script:
- tempa config validate .
- tempa config diff .
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
tempa-config-push:
image: node:20
before_script:
- npm config set @devleaps:registry https://npm.pkg.github.com
- npm install -g @devleaps/tempa
script:
- tempa config push . --title "$CI_COMMIT_SHORT_SHA $CI_COMMIT_TITLE"
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCHGitHub Actions
# .github/workflows/tempa-config.yml (secrets TEMPA_CONFIG_TOKEN, variable TEMPA_ENDPOINT)
name: tempa-config
on:
pull_request:
push:
branches: [main]
jobs:
config:
runs-on: ubuntu-latest
env:
TEMPA_CONFIG_TOKEN: ${{ secrets.TEMPA_CONFIG_TOKEN }}
TEMPA_ENDPOINT: ${{ vars.TEMPA_ENDPOINT }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20, registry-url: https://npm.pkg.github.com, scope: "@devleaps" }
- run: npm install -g @devleaps/tempa
env: { NODE_AUTH_TOKEN: "${{ secrets.GITHUB_TOKEN }}" }
- run: tempa config validate . && tempa config diff .
- if: github.event_name == 'push'
run: tempa config push . --title "${{ github.sha }}"Use --draft to leave every push as a draft that a person publishes in tempa-web.
Overrides for tests and development
| Variable | Effect |
|---|---|
TEMPA_HOME | Use this directory instead of ~/.tempa |
TEMPA_BINARY | The plugin's launcher runs this standalone binary instead of its own or Node (binaries.md) |
TEMPA_MANAGED_CONFIG | Use this file instead of the MDM-managed path |
TEMPA_NO_BROWSER | Do not open the browser on login |
TEMPA_SKIP_CLAUDE_AUTH_STATUS | Do not run claude auth status for the account check at SessionStart |
TEMPA_CLAUDE_MANAGED_SETTINGS | Read this file instead of Claude Code's managed settings file for the account check |
Releasing
Which versions stay supported, and how a version is deprecated, is in cli-versions.md. Bump version in client/package.json and in plugins/overlays/claude-code/.claude-plugin/plugin.json (the build refuses a mismatch), merge, then tag: git tag cli-v0.2.0 && git push origin cli-v0.2.0. .github/workflows/release-cli.yml tests, builds and publishes the package to GitHub Packages, builds every harness's plugin with the CLI inside (node client/build-plugins.mjs --cli client/dist/tempa.js), and commits each released plugin to its own branch (node client/build-plugins.mjs --released lists them): Claude Code's to plugin-release, which tempa@devleaps installs from, and GitHub Copilot's to plugin-release-copilot (Copilot's tempa@devleaps, since TEMPA-160). The other harnesses' plugins are built and checked on every pull request (the plugins CI job, artifact tempa-plugins) but not released until their epics ship. See harnesses.md.
The same tag also runs .github/workflows/build-binaries.yml. It builds the standalone binary for every platform, with Node inside, and keeps the binaries, their checksums and SBOMs as workflow artifacts only. It can also be run by hand (binaries.md).
To try a plugin change locally, from the repo root: node client/build-plugins.mjs (or pnpm --filter @devleaps/tempa build:plugins), then claude --plugin-dir dist/plugins/claude-code. Without a bundled CLI the launcher finds tempa through TEMPA_CLI or your PATH; add --cli client/dist/tempa.js after pnpm --filter @devleaps/tempa build to bundle it as the release does.