# `carlos canary` — filling the reserved verb

**Date:** 2026-09-04
**Issue:** the reserved `canary` case in `main.go:131` exits `not implemented`.

## Why

CARLOS canaries instance apps today and has all year — `go.tito.io` follows
`canary/platform-adoption`, pinned as a live production channel in
`internal/release/laddercheck_test.go:12`. The machinery is all there:

- a route's channel lives on its instance record, per host, so two instances
  of one app on different channels stage different versions
  (`internal/host/agent.go:1549`);
- `canary/*` routes converge on the 2s fast loop, not the 5-minute tick
  (`internal/host/agent.go:1502`);
- promotion **into** a canary is unconditional on both the legacy ladder and
  an app-defined pipeline (`internal/release/ladder.go:84`,
  `internal/release/pipeline.go:328`);
- `VersionStage` only scans pipeline channels, so a build that has been on a
  canary still enters the pipeline as fresh — canarying costs no rung.

What is missing is *sugar*. Today a member types `instances create` +
`ship` + `promote` and works out the hostname themselves. A session in
September 2026 read `carlos canary`'s `not implemented` and README's
"canary hosts are minted for static apps only" and concluded the platform
could not canary an instance app at all. Both statements are true and
narrow; together they read as a blanket no. The verb closes that gap.

## Surface

```
carlos canary [deploy] --app <a> [--name <n>] [--version <sha>]
              [--kind binary|static] [--target <goos-goarch>] [--label <l>]
              [--timeout 120s] <files-or-site-dir>
carlos canary ls --app <a>
carlos canary rm --app <a> --name <n>
```

The bare form is `deploy`, the same way `carlos instances` has a bare form.
Sub-verbs are positional words that never reach `fs.Parse`, matching
`runInstances` (`cli.go:269`).

API mode only, like `carlos deploy`: an instance record is a
console-authorized write, and this verb must complete the job with no box
access (AGENTS.md, "every `carlos` command is designed for a user with no
access to the underlying infra").

## Name, channel and host

- `--name`, else the current git branch, slugified to a single channel
  segment (`internal/release`'s `channelSegRe`: `[a-zA-Z0-9._-]+`, ≤64).
- channel = `canary/<name>`.
- host = `<name>.<app>.<manifest domain>` — exactly the scheme README
  already documents: `<canary>.<app>.<account-sqid>.<apps-domain>`.

A branch that slugifies to nothing, or a `--name` that is not a valid
segment, is refused by name rather than silently repaired.

## `canary deploy`

1. Resolve session, account, app (deploy's rules; `requireDeployAPISession`).
2. Resolve the name, build the channel, refuse an invalid one.
3. `instancesEnable` — idempotent, and the only source of the domain suffix.
4. `instanceList`; then exactly one of:
   - no record on the canary host → `instanceCreate` with the canary channel;
   - record exists on a different channel → `instanceSetChannel`;
   - record already on the canary channel → no write at all.
5. `runShipAPI` (banner suppressed; this verb prints its own).
6. `promote` to `canary/<name>` — never refused, on any app, from any rung.
7. `watchVersion` against the canary host only, never the app's stable host.

Steps 3–4 are what make it sugar rather than a new subsystem: every call
already exists for `carlos instances` and `carlos deploy`.

## `canary ls`

Instance records whose channel starts `canary/`, joined to the channel
listing for the lease. Columns: name, host, channel, staged version, and
what is left of the seven-day lease (`release.CanaryTTL`, refreshed on every
re-promote). An expired canary is marked, because a canary that outlives its
reason is the exact hazard the TTL exists to surface.

## `canary rm`

Deletes the instance record for `canary/<name>`. It says plainly that the
channel pointer itself remains and expires on its own — there is no pointer
delete in the platform, and a command that implied otherwise would be
lying.

## Explicitly out of scope

**Traffic splitting.** This is a URL per canary, not a percentage of
production traffic on one hostname. That would be real work in the edge —
weighted routing, not sugar over existing calls — and nothing here pretends
otherwise.

## Tests

- name resolution: `--name` wins; git branch is slugified; an empty or
  invalid result is refused by name.
- host construction against a manifest domain.
- the create / re-channel / no-write table for step 4.
- lease arithmetic in `ls`, including an expired canary and a pointer that
  cannot be read.
- `rm` targets the record for the named canary and no other.
- the docsite gate: `### carlos canary` still exists, its fenced examples
  run real verbs, and every `/docs` link resolves.
