# `carlos run`, Plan 4: CLI and Docs Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Give members `carlos run`: send a command and its files to the console, stream the run's output to the terminal about a second behind, exit with the command's own status (or the spec's codes for everything else), cancel on Ctrl-C, re-attach with `--follow`, and document both the command and what the app's binary sees.

**Architecture:** `run.go` at the repo root (package `main`) parses `carlos run`'s flags around a `--`, resolves the console session, account and app the way `carlos db` does, and talks to Plan 2's five routes through a small `runClient` interface whose HTTP half is `runAPI` over the existing `apiSession` (`doMultipart`, `getJSON`, `postJSON`). `followRun` is the loop: pages until the result, writes `o` chunks to stdout and `e` chunks to stderr in seq order, expires a run still queued 60 seconds after the request, acknowledges with `last_seq`, and maps the result to an exit code; Ctrl-C once cancels, twice stops waiting. `docs/site/cli.md` gains `### carlos run` and the run's contract under "What an instance is started with".

**Tech Stack:** Go 1.25, `flag`, `mime/multipart`, `os/signal`; the CLI's `apiSession`, `apiPath`, `apiErrorFromBody`, `resolveMode`, `resolvedAccount`, `resolvedApp`, `appFlag`, `accountFlags`, `consoleOption`, `flagUsage`, `multiFlag`; `internal/runs`, `internal/provision` (state names).

**Spec:** `docs/superpowers/specs/2026-10-08-carlos-run-design.md` rev 4: §3, §4 (the docs), §5.2 "No pickup" (the CLI half), §5.6, §5.7 (the CLI half), §6 (the argv warning), §8, §9 "CLI" and "On the dev box once built"; dispositions codex 5, R2-6. Index: `2026-10-08-carlos-run-0-index.md` (items 6 and 7: exit 130 for a cancel; `--follow` takes `--app` and `--instance`). Copy: `2026-10-08-carlos-run-copy.md`.

**Build target:** `main` with Plan 2 merged for the code; Plans 1 to 3 merged and rolled to the dev box for Task 6.

## Global Constraints

- Every user-facing string (CLI output, errors, console messages, docs prose) is copy-reviewed by Paul before it is written into code. The deck is `docs/superpowers/plans/2026-10-08-carlos-run-copy.md` and its gate is `approved_run_copy_test.go`. Implementation tasks use only strings from the deck; a task that writes one adds its id to `runCopyBuilt` in the same commit. Copy voice: no em dashes; full stops and new sentences instead.
- A deck string appears in Go as one string literal, or one `fmt.Sprintf`/`fmt.Errorf`/`errors.New` format, equal to the row with `%s`/`%d` where the row has a `<placeholder>`, and with no trailing newline inside the literal: print it with `fmt.Fprintln`. The top-level usage line (RUN-C83) is the one fragment.
- CI cross-compiles darwin and windows: linux-only syscalls/units code needs `//go:build linux` plus a `!linux` stub. (`run.go` uses only `os.Interrupt` and `syscall.SIGTERM`, which every target defines.)
- Local test commands: run targeted tests (`go test ./internal/<pkg>/ -run TestX`), not the full suite in a loop. Inside the sandbox the default Go build cache may be read-only; if so use `GOCACHE=$TMPDIR/gocache` re-exported per command. Socket tests need a short TMPDIR (`TMPDIR=/tmp/g`). The local gate may need `GOFLAGS=-buildvcs=false`.
- Heavy loops run under `nice`; no -race full suite until the end of a plan.
- Commits end with `Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>`.
- A test that shells out to the carlos binary builds its env through `carlosTestEnv` and sets `cmd.Dir` to a scratch dir (AGENTS.md).
- A new `carlos` command is not done until `docs/site/cli.md` names it (`internal/docsite`'s `TestCLICommandsAreDocumented`).
- Exit codes (spec §5.6, index item 6): the command's own status; 128 + n for signal n; 124 timed out; 125 interrupted (an instance stop, an agent restart, a lost box, a failed start); 130 cancelled, before or after launch, and the second Ctrl-C; 3 refused by the box (flat, claim, wake, incarnation, import) or not picked up in 60 s; 2 refused by the console (any 4xx) or a usage error; 1 anything else (a network failure).
- The CLI polls once a second, writes `o` chunks to stdout and `e` chunks to stderr in seq order, reads pages until the result is in hand and the page is not cut short, then acknowledges with the result's `last_seq`.
- Gate before the plan's last commit: `go build ./...`, `go vet ./...`, `gofmt -l .` clean, `make test`, and `GOOS=darwin go build ./...`, `GOOS=windows go build ./...`, `GOOS=linux GOARCH=arm64 go build ./...`.

## Review Focus

1. A run whose result arrives while the follower is several pages behind must have every page printed before the CLI exits and acknowledges; the ack carries the result's `last_seq`, not the last seq the CLI happened to see. Test in Task 2 (`TestFollowRunPagesUntilTheResult`).
2. Ctrl-C pressed while a page request is in flight must still be seen (the signal channel is buffered), and a second Ctrl-C must exit 130 at once without waiting for the box. Test in Task 3 (`TestFollowRunCtrlCOnceCancelsTwiceExits`).
3. The 60-second expiry losing to a pickup (the console answers 409) must not end the CLI: it keeps following and exits with the command's status. Test in Task 2 (`TestFollowRunKeepsFollowingWhenExpiryLoses`).
4. `carlos run --app a --instance h import --dry-run` with no `--` must be a usage error, never a run of `import` with `--dry-run` swallowed as a carlos flag. Test in Task 1 (`TestParseRunArgs`).
5. A `--file` that does not exist, or a directory, must fail locally (exit 2, RUN-C53) or at the upload with a message, and never send a request whose meta and parts disagree. Test in Task 1 (`TestRunMetaForStatsEveryFile`).

---

## File structure

| path | responsibility |
|---|---|
| `run.go` | flags, `runAPI` (the five routes), `followRun`, `runExit`, `startRun`, `runRun` |
| `run_test.go` | parsing, wire shapes against an httptest console, the follow loop with a scripted client |
| `main.go` | `case "run"` and the usage line |
| `docs/site/cli.md` | `### carlos run`; the run's contract under "What an instance is started with" |
| `approved_run_copy_test.go` | the closing check that every deck row is built |

---

### Task 1: Flags, the wire, and the dispatch

**Files:**
- Create: `run.go`, `run_test.go`
- Modify: `main.go:74` (dispatch, after `case "db":`) and the usage text at `main.go:204-205` (after the `db` lines)

**Interfaces:**
- Consumes: `appFlag(fs, usage) *string` (`flags.go:63`), `accountFlags(fs) (raw, rawID *string, resolve func() string)` (`cli.go:2022`), `consoleOption(fs, usage) *string` (`flags.go:39`), `flagUsage(fs, header) func()` (`flags.go:30`), `multiFlag` (`logs.go:151`), `apiPath(a apiAcct, app, tail string) string` (`apimode.go:446`), `(*apiSession).getJSON`, `.postJSON`, `(*consoleClient).doMultipart(ctx, path, write) (int, []byte, error)` (`auth.go:487`), `apiErrorFromBody(status int, body []byte) error` (`apimode.go:791`), `newTestSession(base) *apiSession` (`apimode_test.go:212`), `runs.Meta`, `runs.Created`, `runs.Page`, `runs.Ack`, `runs.StateAnswer`, `runs.CheckTimeout`, `runs.DefaultTimeout`.
- Produces:
  - `const runUsageRun`, `const runUsageFollow` (RUN-C47, RUN-C48), `func runUsageText() string`.
  - `type runOpts struct { app, instance, follow, console string; acctRaw, acctIDRaw *string; files []string; timeout time.Duration; argv []string }`; `func parseRunArgs(args []string, stderr io.Writer) (runOpts, error)`; `var errRunUsage`.
  - `func runMetaFor(o runOpts) (runs.Meta, error)`.
  - `type runClient interface { create(ctx, meta runs.Meta, paths []string) (runs.Created, error); page(ctx, id string, after int64) (runs.Page, error); cancel(ctx, id string) (string, error); expire(ctx, id string) (string, error); ack(ctx, id string, lastSeq int64) error }`.
  - `type runConsoleError struct { status int; msg string }` (a 4xx answer; exit 2).
  - `type runAPI struct { s *apiSession; a apiAcct; app, host string }` implementing `runClient`.
  - `func runRun(args []string)` (the dispatch target; Task 2 completes its body).

- [ ] **Step 1: Write the failing tests**

`run_test.go`:

```go
package main

import (
	"context"
	"encoding/json"
	"errors"
	"io"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"github.com/carlosframework/platform/internal/runs"
)

func TestParseRunArgs(t *testing.T) {
	ok, err := parseRunArgs([]string{"--app", "titogo", "--instance", "h.example.com", "--file", "a.csv", "--file", "b.csv", "--timeout", "2m", "--", "import", "--dry-run"}, io.Discard)
	if err != nil {
		t.Fatal(err)
	}
	if ok.app != "titogo" || ok.instance != "h.example.com" || len(ok.files) != 2 || ok.timeout != 2*time.Minute || strings.Join(ok.argv, " ") != "import --dry-run" {
		t.Fatalf("opts %+v", ok)
	}
	def, err := parseRunArgs([]string{"--instance", "h.example.com", "--", "x"}, io.Discard)
	if err != nil || def.timeout != 30*time.Minute {
		t.Fatalf("default timeout %s, %v", def.timeout, err)
	}
	for args, want := range map[string]string{
		"--app titogo --instance h.example.com import --dry-run": "",
		"--app titogo -- import":                                  "--app and --instance name the instance to run against",
		"--instance h.example.com --timeout 7h -- x":              "--timeout must be between 1s and 6h",
		"--instance h.example.com --timeout 0s -- x":              "--timeout must be between 1s and 6h",
		"--instance h.example.com --follow run_x -- x":            "--follow takes a run id and no command after --",
		"--instance h.example.com --follow run_x --file a":        "--follow takes a run id and no command after --",
	} {
		_, err := parseRunArgs(strings.Fields(args), io.Discard)
		if err == nil {
			t.Errorf("%q accepted", args)
			continue
		}
		if want == "" && !errors.Is(err, errRunUsage) {
			t.Errorf("%q: %v, want the usage error", args, err)
		}
		if want != "" && err.Error() != want {
			t.Errorf("%q: %q, want %q", args, err, want)
		}
	}
}

func TestRunMetaForStatsEveryFile(t *testing.T) {
	dir := t.TempDir()
	p := filepath.Join(dir, "checkins.csv")
	if err := os.WriteFile(p, []byte("a,b\n"), 0o600); err != nil {
		t.Fatal(err)
	}
	m, err := runMetaFor(runOpts{argv: []string{"import"}, timeout: 90 * time.Second, files: []string{p}})
	if err != nil || m.TimeoutS != 90 || len(m.Files) != 1 || m.Files[0].Name != "checkins.csv" || m.Files[0].Size != 4 {
		t.Fatalf("meta %+v, %v", m, err)
	}
	missing := filepath.Join(dir, "nope.csv")
	if _, err := runMetaFor(runOpts{files: []string{missing}}); err == nil || !strings.HasPrefix(err.Error(), "could not read "+missing+": ") {
		t.Fatalf("missing file: %v", err)
	}
}

func TestRunAPIWireShapes(t *testing.T) {
	type seen struct{ method, path, auth, body string }
	var got []seen
	var metaJSON, fileName, fileBody string
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		s := seen{method: r.Method, path: r.URL.RequestURI(), auth: r.Header.Get("Authorization")}
		if strings.HasPrefix(r.Header.Get("Content-Type"), "multipart/") {
			mr, err := r.MultipartReader()
			if err != nil {
				t.Fatal(err)
			}
			for {
				part, err := mr.NextPart()
				if err != nil {
					break
				}
				raw, _ := io.ReadAll(part)
				if part.FormName() == "meta" {
					metaJSON = string(raw)
				} else {
					fileName, fileBody = part.FileName(), string(raw)
				}
			}
			got = append(got, s)
			w.WriteHeader(http.StatusAccepted)
			json.NewEncoder(w).Encode(runs.Created{ID: "run_a", Host: "h.example.com", State: "queued"})
			return
		}
		raw, _ := io.ReadAll(r.Body)
		s.body = string(raw)
		got = append(got, s)
		switch {
		case strings.Contains(s.path, "/expire"):
			w.WriteHeader(http.StatusConflict)
			json.NewEncoder(w).Encode(map[string]string{"error": "run run_a has already started"})
		case r.Method == http.MethodGet:
			json.NewEncoder(w).Encode(runs.Page{ID: "run_a", State: "picked"})
		default:
			json.NewEncoder(w).Encode(runs.StateAnswer{State: "cancel"})
		}
	}))
	defer ts.Close()
	dir := t.TempDir()
	p := filepath.Join(dir, "x.csv")
	os.WriteFile(p, []byte("1,2"), 0o600)
	c := runAPI{s: newTestSession(ts.URL), a: apiAcct{Seg: "tito"}, app: "titogo", host: "h.example.com"}
	ctx := context.Background()
	created, err := c.create(ctx, runs.Meta{Argv: []string{"import"}, TimeoutS: 60, Files: []runs.MetaFile{{Name: "x.csv", Size: 3}}}, []string{p})
	if err != nil || created.ID != "run_a" {
		t.Fatalf("create %+v, %v", created, err)
	}
	if !strings.Contains(metaJSON, `"argv":["import"]`) || fileName != "x.csv" || fileBody != "1,2" {
		t.Fatalf("meta %s file %s %q", metaJSON, fileName, fileBody)
	}
	if _, err := c.page(ctx, "run_a", 3); err != nil {
		t.Fatal(err)
	}
	if st, err := c.cancel(ctx, "run_a"); err != nil || st != "cancel" {
		t.Fatalf("cancel %q, %v", st, err)
	}
	_, err = c.expire(ctx, "run_a")
	var ce *runConsoleError
	if !errors.As(err, &ce) || ce.status != http.StatusConflict || ce.msg != "run run_a has already started" {
		t.Fatalf("expire %v", err)
	}
	if err := c.ack(ctx, "run_a", 7); err != nil {
		t.Fatal(err)
	}
	base := "/api/cli/apps/tito/titogo/instances/h.example.com/runs"
	want := []seen{
		{http.MethodPost, base, "Bearer cli-token", ""},
		{http.MethodGet, base + "/run_a?after=3", "Bearer cli-token", ""},
		{http.MethodPost, base + "/run_a/cancel", "Bearer cli-token", "{}"},
		{http.MethodPost, base + "/run_a/expire", "Bearer cli-token", "{}"},
		{http.MethodPost, base + "/run_a/ack", "Bearer cli-token", `{"last_seq":7}`},
	}
	if len(got) != len(want) {
		t.Fatalf("requests %+v", got)
	}
	for i := range want {
		if got[i] != want[i] {
			t.Errorf("request %d = %+v, want %+v", i, got[i], want[i])
		}
	}
}
```

- [ ] **Step 2: Run to verify they fail**

Run: `nice -n 10 go test . -run 'TestParseRunArgs|TestRunMetaFor|TestRunAPIWireShapes' -count=1`
Expected: FAIL, `undefined: parseRunArgs`.

- [ ] **Step 3: Implement**

`run.go` (this task's half; Task 2 appends the follow loop):

```go
package main

import (
	"context"
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"io"
	"mime/multipart"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"slices"
	"strings"
	"time"

	"github.com/carlosframework/platform/internal/runs"
)

const runUsageRun = "usage: carlos run --app <app> --instance <host> [--file <path>]... [--timeout 30m] -- <arg>..."

const runUsageFollow = "carlos run --app <app> --instance <host> --follow <id>"

func runUsageText() string { return runUsageRun + "\n       " + runUsageFollow + "\n" }

const runDirectModeMsg = "carlos run works through a console, and this shell is in direct bucket mode. unset CARLOS_DEPLOYMENT_BUCKET and CARLOS_DEPLOYMENT_DIR, then run carlos auth login --console <url>"

// errRunUsage is a command line that does not parse: the usage text says
// what was wrong.
var errRunUsage = errors.New("carlos run: usage")

// runOpts is `carlos run` parsed.
type runOpts struct {
	app, instance, follow, console string
	acctRaw, acctIDRaw             *string
	files                          []string
	timeout                        time.Duration
	argv                           []string
}

// parseRunArgs splits at the first "--": carlos's flags before it, the
// command after it. Nothing may sit between the flags and the "--", so a
// forgotten "--" is a usage error rather than a command whose flags carlos
// swallowed.
func parseRunArgs(args []string, stderr io.Writer) (runOpts, error) {
	flagArgs, argv := args, []string(nil)
	if i := slices.Index(args, "--"); i >= 0 {
		flagArgs, argv = args[:i], args[i+1:]
	}
	fs := flag.NewFlagSet("run", flag.ContinueOnError)
	fs.SetOutput(stderr)
	app := appFlag(fs, "the app whose command to run")
	instance := fs.String("instance", "", "the instance to run it against")
	var files multiFlag
	fs.Var(&files, "file", "a file to upload with the run (repeatable; 64 MB in total)")
	timeout := fs.Duration("timeout", runs.DefaultTimeout, "how long the command may run (1s to 6h)")
	follow := fs.String("follow", "", "attach to a run already started, by its id")
	acctRaw, acctIDRaw, _ := accountFlags(fs)
	console := consoleOption(fs, "console to run through (default: the only logged-in one)")
	fs.Usage = flagUsage(fs, runUsageText())
	if err := fs.Parse(flagArgs); err != nil {
		return runOpts{}, err
	}
	if fs.NArg() > 0 {
		return runOpts{}, errRunUsage
	}
	o := runOpts{app: strings.TrimSpace(*app), instance: strings.TrimSpace(*instance), follow: strings.TrimSpace(*follow),
		console: *console, acctRaw: acctRaw, acctIDRaw: acctIDRaw, files: files, timeout: *timeout, argv: argv}
	switch {
	case o.instance == "":
		return o, errors.New("--app and --instance name the instance to run against")
	case o.follow != "" && (len(o.argv) > 0 || len(o.files) > 0):
		return o, errors.New("--follow takes a run id and no command after --")
	case runs.CheckTimeout(o.timeout) != nil:
		return o, errors.New("--timeout must be between 1s and 6h")
	}
	return o, nil
}

// runMetaFor is the create request's meta: argv, the timeout, and each
// file's base name and size. The console checks all of it again.
func runMetaFor(o runOpts) (runs.Meta, error) {
	m := runs.Meta{Argv: o.argv, TimeoutS: int64(o.timeout / time.Second)}
	for _, p := range o.files {
		st, err := os.Stat(p)
		if err != nil {
			return m, fmt.Errorf("could not read %s: %w", p, err)
		}
		m.Files = append(m.Files, runs.MetaFile{Name: filepath.Base(p), Size: st.Size()})
	}
	return m, nil
}

// runClient is the console's five run routes (run spec §5.1, §5.6, §5.7).
type runClient interface {
	create(ctx context.Context, meta runs.Meta, paths []string) (runs.Created, error)
	page(ctx context.Context, id string, after int64) (runs.Page, error)
	cancel(ctx context.Context, id string) (string, error)
	expire(ctx context.Context, id string) (string, error)
	ack(ctx context.Context, id string, lastSeq int64) error
}

// runConsoleError is a 4xx the console answered, with its message: a
// refusal by the console, exit 2.
type runConsoleError struct {
	status int
	msg    string
}

func (e *runConsoleError) Error() string { return e.msg }

// runAnswer decodes a want-status answer into out, or turns any other
// answer into an error: a 4xx into a runConsoleError.
func runAnswer(status int, body []byte, err error, want int, out any) error {
	if err != nil {
		return err
	}
	if status != want {
		e := apiErrorFromBody(status, body)
		if status >= 400 && status < 500 {
			return &runConsoleError{status: status, msg: e.Error()}
		}
		return e
	}
	if out == nil {
		return nil
	}
	return json.Unmarshal(body, out)
}

// runAPI is runClient over the signed-in console.
type runAPI struct {
	s    *apiSession
	a    apiAcct
	app  string
	host string
}

func (c runAPI) path(tail string) string {
	return apiPath(c.a, c.app, "/instances/"+url.PathEscape(c.host)+"/runs"+tail)
}

func (c runAPI) create(ctx context.Context, meta runs.Meta, paths []string) (runs.Created, error) {
	status, body, err := c.s.c.doMultipart(ctx, c.path(""), func(mw *multipart.Writer) error {
		mp, err := mw.CreateFormField("meta")
		if err != nil {
			return err
		}
		if err := json.NewEncoder(mp).Encode(meta); err != nil {
			return err
		}
		for _, p := range paths {
			f, err := os.Open(p)
			if err != nil {
				return err
			}
			fp, err := mw.CreateFormFile("file", filepath.Base(p))
			if err == nil {
				_, err = io.Copy(fp, f)
			}
			f.Close()
			if err != nil {
				return err
			}
		}
		return nil
	})
	var out runs.Created
	return out, runAnswer(status, body, err, http.StatusAccepted, &out)
}

func (c runAPI) page(ctx context.Context, id string, after int64) (runs.Page, error) {
	status, body, err := c.s.getJSON(ctx, c.path("/"+url.PathEscape(id)+fmt.Sprintf("?after=%d", after)))
	var out runs.Page
	return out, runAnswer(status, body, err, http.StatusOK, &out)
}

func (c runAPI) cancel(ctx context.Context, id string) (string, error) {
	status, body, err := c.s.postJSON(ctx, c.path("/"+url.PathEscape(id)+"/cancel"), struct{}{})
	var out runs.StateAnswer
	return out.State, runAnswer(status, body, err, http.StatusOK, &out)
}

func (c runAPI) expire(ctx context.Context, id string) (string, error) {
	status, body, err := c.s.postJSON(ctx, c.path("/"+url.PathEscape(id)+"/expire"), struct{}{})
	var out runs.StateAnswer
	return out.State, runAnswer(status, body, err, http.StatusOK, &out)
}

func (c runAPI) ack(ctx context.Context, id string, lastSeq int64) error {
	status, body, err := c.s.postJSON(ctx, c.path("/"+url.PathEscape(id)+"/ack"), runs.Ack{LastSeq: lastSeq})
	return runAnswer(status, body, err, http.StatusOK, nil)
}
```

`runRun` comes in Task 2 with the loop. For this task's build, add the dispatch with a body that parses and stops:

```go
// runRun is `carlos run` (run spec §3).
func runRun(args []string) {
	o, err := parseRunArgs(args, os.Stderr)
	if err != nil {
		if errors.Is(err, flag.ErrHelp) {
			os.Exit(0)
		}
		if !errors.Is(err, errRunUsage) {
			fmt.Fprintln(os.Stderr, err.Error())
		}
		fmt.Fprint(os.Stderr, runUsageText())
		os.Exit(2)
	}
	_ = o
}
```

`main.go`, after `case "db":`:

```go
	case "run":
		runRun(args)
```

and in the usage text after the two `db` lines (RUN-C83):

```
  run             run one of your app's commands against one instance, as that instance
```

`runCopyBuilt` gains `"RUN-C47", "RUN-C48", "RUN-C49", "RUN-C50", "RUN-C51", "RUN-C52", "RUN-C53", "RUN-C83"`.

- [ ] **Step 4: Run to verify they pass**

Run: `nice -n 10 go test . -run 'TestParseRunArgs|TestRunMetaFor|TestRunAPIWireShapes|TestRunBuilt' -count=1`
Expected: PASS. (`internal/docsite`'s `TestCLICommandsAreDocumented` now fails: `run` is in the dispatch with no heading yet. Task 4 adds it; do not merge this plan before Task 4.)

- [ ] **Step 5: Commit**

```bash
git add run.go run_test.go main.go approved_run_copy_test.go
git commit -m "cli: carlos run's flags and the five console routes (run spec §3, §5.1)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```

---

### Task 2: Follow, expire, ack, and the exit codes

**Files:**
- Modify: `run.go` (append; replace `runRun`'s body)
- Test: `run_test.go` (append)

**Interfaces:**
- Consumes: Task 1's `runClient`, `runConsoleError`, `runOpts`, `runMetaFor`, `runAPI`; `resolveMode(consoleFlag) (*apiSession, error)`, `printUsing(w, sess)`, `resolvedAccount`, `resolvedApp`; `runs.Result`, `runs.Page`, the `Refused*` constants, `runs.PickupWindow`; `provision.RunQueued`, `RunExpired`, `RunCancelled`.
- Produces:
  - `type runTerm struct { stdout, stderr io.Writer; interrupts <-chan os.Signal; after func(time.Duration) <-chan time.Time; now func() time.Time }`.
  - `func followRun(ctx context.Context, c runClient, t runTerm, host, id string, expireAt time.Time) int`: the exit code. `expireAt` zero means never expire (`--follow`).
  - `func runExit(host string, r runs.Result) (int, []string)`: the exit code and the lines for stderr.
  - `func startRun(ctx context.Context, c runClient, t runTerm, o runOpts, app string) int`.

- [ ] **Step 1: Write the failing tests**

Append to `run_test.go`:

```go
// scriptedRun is a runClient that answers pages from a script.
type scriptedRun struct {
	pages    []runs.Page
	expireTo string
	expireErr error
	acked    []int64
	cancels  int
	expires  int
	created  runs.Meta
}

func (s *scriptedRun) create(_ context.Context, m runs.Meta, _ []string) (runs.Created, error) {
	s.created = m
	return runs.Created{ID: "run_a", State: "queued"}, nil
}
func (s *scriptedRun) page(context.Context, string, int64) (runs.Page, error) {
	p := s.pages[0]
	if len(s.pages) > 1 {
		s.pages = s.pages[1:]
	}
	return p, nil
}
func (s *scriptedRun) cancel(context.Context, string) (string, error) { s.cancels++; return "cancel", nil }
func (s *scriptedRun) expire(context.Context, string) (string, error) {
	s.expires++
	return s.expireTo, s.expireErr
}
func (s *scriptedRun) ack(_ context.Context, _ string, last int64) error {
	s.acked = append(s.acked, last)
	return nil
}

type termFx struct {
	out, err strings.Builder
	sig      chan os.Signal
	now      time.Time
}

func newTermFx() *termFx { return &termFx{sig: make(chan os.Signal, 2), now: time.Unix(1_700_000_000, 0)} }

func (f *termFx) term() runTerm {
	return runTerm{stdout: &f.out, stderr: &f.err, interrupts: f.sig,
		after: func(d time.Duration) <-chan time.Time {
			f.now = f.now.Add(d)
			c := make(chan time.Time, 1)
			c <- f.now
			return c
		},
		now: func() time.Time { return f.now }}
}

func chunk(seq int64, stream, data string) runs.Chunk {
	return runs.Chunk{Seq: seq, Stream: stream, Data: []byte(data)}
}

func TestFollowRunInterleavesAndAcks(t *testing.T) {
	f := newTermFx()
	s := &scriptedRun{pages: []runs.Page{
		{State: "picked", Chunks: []runs.Chunk{chunk(1, "o", "one\n"), chunk(2, "e", "warn\n")}},
		{State: "picked", Chunks: []runs.Chunk{chunk(3, "o", "two\n")}, Result: &runs.Result{Exit: 0, LastSeq: 3}},
	}}
	if code := followRun(context.Background(), s, f.term(), "h.example.com", "run_a", time.Time{}); code != 0 {
		t.Fatalf("exit %d", code)
	}
	if f.out.String() != "one\ntwo\n" || f.err.String() != "warn\n" {
		t.Fatalf("stdout %q stderr %q", f.out.String(), f.err.String())
	}
	if len(s.acked) != 1 || s.acked[0] != 3 {
		t.Fatalf("acked %v", s.acked)
	}
}

func TestFollowRunPagesUntilTheResult(t *testing.T) {
	f := newTermFx()
	res := &runs.Result{Exit: 0, LastSeq: 3}
	s := &scriptedRun{pages: []runs.Page{
		{State: "done", Chunks: []runs.Chunk{chunk(1, "o", "a")}, Result: res, More: true},
		{State: "done", Chunks: []runs.Chunk{chunk(2, "o", "b")}, Result: res, More: true},
		{State: "done", Chunks: []runs.Chunk{chunk(3, "o", "c")}, Result: res},
	}}
	followRun(context.Background(), s, f.term(), "h.example.com", "run_a", time.Time{})
	if f.out.String() != "abc" || len(s.acked) != 1 || s.acked[0] != 3 {
		t.Fatalf("stdout %q acked %v (codex 5: every page before the ack)", f.out.String(), s.acked)
	}
}

func TestFollowRunExitCodes(t *testing.T) {
	started := time.Unix(1_700_000_000, 0)
	for _, c := range []struct {
		res  runs.Result
		code int
		says string
	}{
		{runs.Result{Exit: 7}, 7, ""},
		{runs.Result{Signal: "9"}, 137, ""},
		{runs.Result{TimedOut: true, Started: started, Ended: started.Add(30 * time.Minute)}, 124, "h.example.com: the command timed out after 30m0s"},
		{runs.Result{Interrupted: "the instance was restarted"}, 125, "h.example.com: the run was interrupted: the instance was restarted. it may have done part of its work"},
		{runs.Result{Cancelled: true}, 130, "h.example.com: the command was cancelled"},
		{runs.Result{Refused: runs.RefusedFlat}, 3, "h.example.com: the box is not on the nested layout yet, so carlos run cannot run there"},
		{runs.Result{Refused: runs.RefusedClaim}, 3, "h.example.com: the box has host claims armed. carlos run cannot run there yet"},
		{runs.Result{Refused: runs.RefusedWake, Detail: "wake failed: restore"}, 3, "h.example.com: the instance did not wake. wake failed: restore"},
		{runs.Result{Refused: runs.RefusedIncarnation}, 3, "h.example.com was deleted and created again after this run was requested. the run did not start"},
		{runs.Result{Refused: runs.RefusedImport}, 3, "h.example.com is an import that is not committed yet. the run did not start"},
		{runs.Result{Exit: 0, Truncated: true}, 0, "h.example.com: the output passed 64 MB and was cut off there. the command ran on to its end"},
	} {
		code, lines := runExit("h.example.com", c.res)
		if code != c.code || (c.says != "" && !slices.Contains(lines, c.says)) || (c.says == "" && len(lines) != 0) {
			t.Errorf("%+v: %d %q, want %d %q", c.res, code, lines, c.code, c.says)
		}
	}
}

func TestFollowRunExpiresAQueuedRunAfterSixtySeconds(t *testing.T) {
	f := newTermFx()
	s := &scriptedRun{pages: []runs.Page{{State: "queued"}}, expireTo: "expired"}
	code := followRun(context.Background(), s, f.term(), "h.example.com", "run_a", f.now.Add(60*time.Second))
	if code != 3 || s.expires != 1 || !strings.Contains(f.err.String(), "h.example.com: not picked up within 60 seconds. the run did not start. try again") {
		t.Fatalf("exit %d expires %d stderr %q", code, s.expires, f.err.String())
	}
	if f.now.Sub(time.Unix(1_700_000_000, 0)) < 60*time.Second {
		t.Fatal("expired before 60 seconds")
	}
}

func TestFollowRunKeepsFollowingWhenExpiryLoses(t *testing.T) {
	f := newTermFx()
	queued := runs.Page{State: "queued"}
	pages := make([]runs.Page, 0, 62)
	for range 61 {
		pages = append(pages, queued)
	}
	pages = append(pages, runs.Page{State: "done", Result: &runs.Result{Exit: 4}})
	s := &scriptedRun{pages: pages, expireErr: &runConsoleError{status: http.StatusConflict, msg: "run run_a has already started"}}
	if code := followRun(context.Background(), s, f.term(), "h.example.com", "run_a", f.now.Add(60*time.Second)); code != 4 || s.expires != 1 {
		t.Fatalf("exit %d expires %d (a late pickup wins over the expiry)", code, s.expires)
	}
}

func TestFollowRunStatesWithoutAResult(t *testing.T) {
	for state, want := range map[string]struct {
		code int
		says string
	}{
		"expired":   {3, "h.example.com: not picked up within 60 seconds. the run did not start. try again"},
		"cancelled": {130, "h.example.com: run run_a was cancelled before it started"},
	} {
		f := newTermFx()
		s := &scriptedRun{pages: []runs.Page{{State: state}}}
		if code := followRun(context.Background(), s, f.term(), "h.example.com", "run_a", time.Time{}); code != want.code || !strings.Contains(f.err.String(), want.says) {
			t.Errorf("%s: exit %d stderr %q", state, code, f.err.String())
		}
	}
}

func TestStartRunPrintsTheFollowCommand(t *testing.T) {
	f := newTermFx()
	s := &scriptedRun{pages: []runs.Page{{State: "done", Result: &runs.Result{Exit: 0}}}}
	o := runOpts{instance: "h.example.com", argv: []string{"import"}, timeout: time.Minute}
	if code := startRun(context.Background(), s, f.term(), o, "titogo"); code != 0 {
		t.Fatalf("exit %d", code)
	}
	if !strings.Contains(f.err.String(), "run run_a on h.example.com. to follow it again: carlos run --app titogo --instance h.example.com --follow run_a") {
		t.Fatalf("stderr %q", f.err.String())
	}
	if s.created.TimeoutS != 60 {
		t.Fatalf("meta %+v", s.created)
	}
}
```

Add `"slices"` to `run_test.go`'s imports.

- [ ] **Step 2: Run to verify they fail**

Run: `nice -n 10 go test . -run 'TestFollowRun|TestStartRun' -count=1`
Expected: FAIL, `undefined: followRun`.

- [ ] **Step 3: Implement**

Append to `run.go` (add `log`, `os/signal`, `strconv`, `syscall` and `internal/provision` to its imports):

```go
// runTerm is how the follow loop meets the terminal and the clock; tests
// replace all of it.
type runTerm struct {
	stdout, stderr io.Writer
	interrupts     <-chan os.Signal
	after          func(time.Duration) <-chan time.Time
	now            func() time.Time
}

func (t runTerm) say(s string) { fmt.Fprintln(t.stderr, s) }

// followRun prints a run's output until its result, acknowledges it, and
// returns the exit code (run spec §5.6). It polls once a second; a page
// cut short is followed at once; the result is in hand only when a page
// carries it and is not cut short, because the console reads the result
// before it lists chunks. A run still queued at expireAt is expired (§5.2);
// zero never expires. The first Ctrl-C cancels the run on the box and keeps
// following; the second stops waiting (§5.7).
func followRun(ctx context.Context, c runClient, t runTerm, host, id string, expireAt time.Time) int {
	var after int64
	cancelled := false
	for {
		p, err := c.page(ctx, id, after)
		if err != nil {
			var ce *runConsoleError
			if errors.As(err, &ce) && ce.status == http.StatusNotFound {
				t.say(fmt.Sprintf("%s has no run %s, or this console has no carlos run yet", host, id))
				return 2
			}
			t.say(fmt.Sprintf("carlos run: %v", err))
			return 1
		}
		for _, ch := range p.Chunks {
			if ch.Seq <= after {
				continue
			}
			w := t.stdout
			if ch.Stream == runs.StreamErr {
				w = t.stderr
			}
			w.Write(ch.Data)
			after = ch.Seq
		}
		if p.More {
			continue
		}
		if p.Result != nil {
			_ = c.ack(ctx, id, p.Result.LastSeq)
			code, lines := runExit(host, *p.Result)
			for _, l := range lines {
				t.say(l)
			}
			return code
		}
		switch p.State {
		case provision.RunExpired:
			t.say(fmt.Sprintf("%s: not picked up within 60 seconds. the run did not start. try again", host))
			return 3
		case provision.RunCancelled:
			t.say(fmt.Sprintf("%s: run %s was cancelled before it started", host, id))
			return 130
		case provision.RunQueued:
			if !expireAt.IsZero() && !t.now().Before(expireAt) {
				expireAt = time.Time{} // ask once; a pickup that won is followed
				if state, err := c.expire(ctx, id); err == nil && state == provision.RunExpired {
					t.say(fmt.Sprintf("%s: not picked up within 60 seconds. the run did not start. try again", host))
					return 3
				}
			}
		}
		select {
		case <-t.after(time.Second):
		case <-t.interrupts:
			if cancelled {
				return 130
			}
			cancelled = true
			t.say(fmt.Sprintf("cancelling run %s. press Ctrl-C again to stop waiting", id))
			if _, err := c.cancel(ctx, id); err != nil {
				t.say(fmt.Sprintf("carlos run: %v", err))
			}
		}
	}
}

// runExit is a result's exit code and what stderr says about it (run spec
// §5.6, §8; index item 6).
func runExit(host string, r runs.Result) (int, []string) {
	var lines []string
	if r.Truncated {
		lines = append(lines, fmt.Sprintf("%s: the output passed 64 MB and was cut off there. the command ran on to its end", host))
	}
	switch {
	case r.Refused == runs.RefusedFlat:
		return 3, append(lines, fmt.Sprintf("%s: the box is not on the nested layout yet, so carlos run cannot run there", host))
	case r.Refused == runs.RefusedClaim:
		return 3, append(lines, fmt.Sprintf("%s: the box has host claims armed. carlos run cannot run there yet", host))
	case r.Refused == runs.RefusedWake:
		return 3, append(lines, fmt.Sprintf("%s: the instance did not wake. %s", host, r.Detail))
	case r.Refused == runs.RefusedIncarnation:
		return 3, append(lines, fmt.Sprintf("%s was deleted and created again after this run was requested. the run did not start", host))
	case r.Refused == runs.RefusedImport:
		return 3, append(lines, fmt.Sprintf("%s is an import that is not committed yet. the run did not start", host))
	case r.Refused != "":
		return 3, append(lines, fmt.Sprintf("carlos run: %s", r.Refused))
	case r.Cancelled:
		return 130, append(lines, fmt.Sprintf("%s: the command was cancelled", host))
	case r.TimedOut:
		return 124, append(lines, fmt.Sprintf("%s: the command timed out after %s", host, r.Ended.Sub(r.Started).Round(time.Second)))
	case r.Interrupted != "":
		return 125, append(lines, fmt.Sprintf("%s: the run was interrupted: %s. it may have done part of its work", host, r.Interrupted))
	case r.Signal != "":
		n, _ := strconv.Atoi(r.Signal)
		return 128 + n, lines
	}
	return r.Exit, lines
}

// startRun sends the run and follows it. A console refusal is exit 2.
func startRun(ctx context.Context, c runClient, t runTerm, o runOpts, app string) int {
	meta, err := runMetaFor(o)
	if err != nil {
		t.say(err.Error())
		return 2
	}
	created, err := c.create(ctx, meta, o.files)
	if err != nil {
		t.say(fmt.Sprintf("carlos run: %v", err))
		var ce *runConsoleError
		if errors.As(err, &ce) {
			return 2
		}
		return 1
	}
	t.say(fmt.Sprintf("run %s on %s. to follow it again: carlos run --app %s --instance %s --follow %s", created.ID, o.instance, app, o.instance, created.ID))
	return followRun(ctx, c, t, o.instance, created.ID, t.now().Add(runs.PickupWindow))
}
```

Replace `runRun`'s body:

```go
// runRun is `carlos run` (run spec §3).
func runRun(args []string) {
	o, err := parseRunArgs(args, os.Stderr)
	if err != nil {
		if errors.Is(err, flag.ErrHelp) {
			os.Exit(0)
		}
		if !errors.Is(err, errRunUsage) {
			fmt.Fprintln(os.Stderr, err.Error())
		}
		fmt.Fprint(os.Stderr, runUsageText())
		os.Exit(2)
	}
	sess, err := resolveMode(o.console)
	if err != nil {
		log.Fatal(err)
	}
	if sess == nil {
		log.Fatal(runDirectModeMsg)
	}
	printUsing(os.Stderr, sess)
	acct := resolvedAccount(sess, o.acctRaw, o.acctIDRaw)
	app := resolvedApp(sess, acct, o.app, runUsageText())
	interrupts := make(chan os.Signal, 2)
	signal.Notify(interrupts, os.Interrupt, syscall.SIGTERM)
	t := runTerm{stdout: os.Stdout, stderr: os.Stderr, interrupts: interrupts, after: time.After, now: time.Now}
	c := runAPI{s: sess, a: acct, app: app, host: o.instance}
	ctx := context.Background()
	if o.follow != "" {
		os.Exit(followRun(ctx, c, t, o.instance, o.follow, time.Time{}))
	}
	os.Exit(startRun(ctx, c, t, o, app))
}
```

`runCopyBuilt` gains `"RUN-C22", "RUN-C54", "RUN-C55", "RUN-C56", "RUN-C57", "RUN-C58", "RUN-C59", "RUN-C60", "RUN-C61", "RUN-C62", "RUN-C63", "RUN-C64", "RUN-C65", "RUN-C66", "RUN-C67"`.

- [ ] **Step 4: Run to verify they pass**

Run: `nice -n 10 go test . -run 'TestFollowRun|TestStartRun|TestParseRunArgs|TestRunAPI|TestRunBuilt' -count=1 && GOOS=windows go build . && GOOS=darwin go build .`
Expected: PASS; both cross builds succeed.

- [ ] **Step 5: Commit**

```bash
git add run.go run_test.go approved_run_copy_test.go
git commit -m "cli: carlos run follows, expires, acks and exits with the spec's codes (run spec §5.2, §5.6, codex 5, R2-6)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```

---

### Task 3: Ctrl-C once and twice

**Files:**
- Test: `run_test.go` (append)

**Interfaces:**
- Consumes: `followRun`, `runTerm.interrupts`. The behaviour is Task 2's; this task pins it, including a signal that arrives while a page request is in flight.

- [ ] **Step 1: Write the tests**

```go
func TestFollowRunCtrlCOnceCancelsTwiceExits(t *testing.T) {
	f := newTermFx()
	s := &scriptedRun{pages: []runs.Page{{State: "picked"}}}
	f.sig <- os.Interrupt // arrives while the first page is being read
	done := make(chan int, 1)
	go func() { done <- followRun(context.Background(), s, f.term(), "h.example.com", "run_a", time.Time{}) }()
	// The first interrupt cancels and keeps following.
	deadline := time.Now().Add(2 * time.Second)
	for s.cancels == 0 && time.Now().Before(deadline) {
		time.Sleep(5 * time.Millisecond)
	}
	if s.cancels != 1 {
		t.Fatal("the first Ctrl-C did not cancel the run")
	}
	f.sig <- os.Interrupt
	select {
	case code := <-done:
		if code != 130 {
			t.Fatalf("exit %d, want 130", code)
		}
	case <-time.After(2 * time.Second):
		t.Fatal("the second Ctrl-C did not stop waiting")
	}
	if !strings.Contains(f.err.String(), "cancelling run run_a. press Ctrl-C again to stop waiting") {
		t.Fatalf("stderr %q", f.err.String())
	}
}

func TestFollowRunCtrlCOnceWaitsForTheResult(t *testing.T) {
	f := newTermFx()
	s := &scriptedRun{pages: []runs.Page{
		{State: "picked"},
		{State: "done", Result: &runs.Result{Cancelled: true, Exit: 143}},
	}}
	f.sig <- os.Interrupt
	if code := followRun(context.Background(), s, f.term(), "h.example.com", "run_a", time.Time{}); code != 130 || s.cancels != 1 {
		t.Fatalf("exit %d cancels %d", code, s.cancels)
	}
	if !strings.Contains(f.err.String(), "h.example.com: the command was cancelled") {
		t.Fatalf("stderr %q", f.err.String())
	}
}
```

The test's `after` hands back an already-fired channel, so `select` may pick either ready case; the first test's polling loop and the buffered `sig` keep it deterministic in outcome: the interrupt is consumed within a few iterations. `s.cancels` is read without a lock while `followRun` runs: run this file's tests under `-race` once in Task 5's gate, and if the detector flags it, give `scriptedRun` a mutex around `cancels`.

- [ ] **Step 2: Run them**

Run: `nice -n 10 go test . -run 'TestFollowRunCtrlC' -count=1`
Expected: PASS (Task 2's loop already does this).

- [ ] **Step 3: Commit**

```bash
git add run_test.go
git commit -m "cli: pin Ctrl-C once and twice for carlos run (run spec §5.7)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```

---

### Task 4: The docs

**Files:**
- Modify: `docs/site/cli.md`: a new `### carlos run` after `### carlos db`'s section (before `### carlos schedule` at `:472`); the contract after `:699-700` ("A seed … and no control socket.")

**Interfaces:**
- Consumes: `internal/docsite` (`TestCLICommandsAreDocumented`, `TestFencedCarlosCommandsAreReal`, `TestFenceLanguagesAreDeclared`).
- Produces: the page `carlos run`'s section and the run contract, each sentence an approved deck row.

- [ ] **Step 1: Run the docs test to see it fail**

Run: `nice -n 10 go test ./internal/docsite/ -count=1`
Expected: FAIL, `TestCLICommandsAreDocumented` names `run` (dispatched since Task 1, no heading).

- [ ] **Step 2: Write the section**

Insert before `### carlos schedule` (each paragraph is a deck row; apply Paul's approved text, which may differ from these drafts). Keep each row's text on one line: the gate matches docs raw, so a wrapped sentence fails it.

````markdown
### carlos run

Runs one command of your app against one instance, as that instance: the same build, user, directories, database and environment it serves with. Output streams back to your terminal about a second behind, and carlos exits with the command's own status.

```sh
carlos run --app titogo --instance acme.go.tito.io --file checkins.csv -- import checkins.csv
```

Any person who is a member of the account can run a command. A service credential cannot.

Each --file is uploaded with the request and placed, under its own name, in the directory CARLOS_RUN_DIR names. Together they can come to 64 MB. The directory is deleted when the run ends.

The command runs for at most 30 minutes unless you pass --timeout, up to 6 hours. Ctrl-C cancels it on the box. A second Ctrl-C stops waiting, and the cancel still happens. If your terminal goes away the run carries on, and carlos run --follow picks its output up again. Output is kept until carlos has read all of it, and for 24 hours at most.

A hibernating instance is woken first. A live one keeps serving, and the command opens the same database beside it. One run at a time per instance. carlos run needs the instance's box on the nested layout, and it is refused while the instance is moving.

carlos run exits with the command's own status, 128 plus the signal number when a signal ended it, 124 when it timed out, 125 when an instance stop or an agent restart interrupted it, 130 when you cancelled it, 3 when the box refused it or did not pick it up within 60 seconds, and 2 when the console refused it.

Arguments are kept in the account's audit log, with who ran them. Never put a secret in an argument. Pass it in a --file, or keep it in the instance's environment.
````

After `A seed (the --seed command of carlos instances enable) runs with the same environment, CARLOS_HOST included, and no control socket.` (`:699-700`):

```markdown
A command started with carlos run gets the same environment, without CARLOS_CONTROL_SOCKET or CARLOS_PURGE, and three variables of its own:

| Variable | Value |
|---|---|
| `CARLOS_DB` | the instance's database, the path --db names when the instance starts |
| `CARLOS_RUN_ID` | this run's id |
| `CARLOS_RUN_DIR` | a directory holding the files passed with --file, under their own names. Always set, even with no files. Deleted after the run |

Its arguments are exactly what followed --, after your binary. The platform adds nothing. It runs in the instance's data directory. On a cancel, a timeout or an instance stop it gets SIGTERM, then SIGKILL 20 seconds later.
```

`runCopyBuilt` gains `"RUN-C68"` to `"RUN-C81"` (fourteen ids).

- [ ] **Step 3: Run the docs and copy tests**

Run: `nice -n 10 go test ./internal/docsite/ -count=1 && nice -n 10 go test . -run 'TestRunBuilt|TestRunCopy' -count=1`
Expected: PASS.

- [ ] **Step 4: Commit**

```bash
git add docs/site/cli.md approved_run_copy_test.go
git commit -m "docs: carlos run, and what a run's command is started with (run spec §4, §6)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```

---

### Task 5: Every deck row built, and the gate

**Files:**
- Modify: `approved_run_copy_test.go` (one test)

**Interfaces:**
- Produces: `TestRunCopyIsFullyBuilt`: `runCopyBuilt` names every id in `runCopyIDs`. From here on a deck row that nothing writes fails the gate, as an unapproved one does.

- [ ] **Step 1: Write the test**

```go
// The series is done only when every deck row is in the code or the docs.
func TestRunCopyIsFullyBuilt(t *testing.T) {
	built := map[string]bool{}
	for _, id := range runCopyBuilt {
		built[id] = true
	}
	for _, id := range runCopyIDs {
		if !built[id] {
			t.Errorf("%s is in the deck but no task built it", id)
		}
	}
}
```

- [ ] **Step 2: Run it**

Run: `nice -n 10 go test . -run 'TestRunCopy|TestRunBuilt' -count=1`
Expected: PASS. A failure names a row a task missed; find its task in Plans 1 to 4 and add the id there, or, if Paul struck the row in review, remove it from the deck and `runCopyIDs`.

- [ ] **Step 3: Run the gate**

```bash
export GOFLAGS=-buildvcs=false
go build ./... && go vet ./... && test -z "$(gofmt -l .)"
TMPDIR=/tmp/g nice -n 10 make test
GOOS=darwin go build ./... && GOOS=windows go build ./... && GOOS=linux GOARCH=arm64 go build ./...
nice -n 10 go test -race . -run 'TestFollowRun|TestStartRun|TestRunAPI' -count=1
```

Expected: every command exits 0 (prefix `GOCACHE=$TMPDIR/gocache` if the cache is read-only).

- [ ] **Step 4: Commit**

```bash
git add approved_run_copy_test.go
git commit -m "run: every deck row is built

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```

---

### Task 6: On the dev box (needs Paul's go)

Rolling a build to the dev box is a rollout step; ask Paul before Step 1. The steps use the platform roll recipe the operator already runs (build arm64, copy, `update-platform-binary.sh`, `roll-console.sh`); this task adds only the checks.

**Files:** none.

- [ ] **Step 1: Roll Plans 1 to 4 to the dev box and its console**, with Paul's go-ahead, by the roll recipe in the README ("Releasing the platform").

- [ ] **Step 2: A real run, including a write the instance then serves**

On a nested instance of a test app whose binary has a command that writes a row (for example `seed-row <text>`) and a page that lists rows:

```bash
carlos run --app <test-app> --instance <host> -- seed-row hello-from-run
echo "exit $?"
curl -s https://<host>/rows | grep hello-from-run
```

Expected: the command's output on the terminal, `exit 0`, the row served by the instance; one `run-start` and one `run-end` row in the app's activity.

- [ ] **Step 3: A run across a hibernation timer**

Run a command that sleeps past the deployment's idle window (`carlos run … -- sleep-then-write 20m` on a box whose idle is 15m). Expected: the instance stays awake for the run (the hold), the write lands, `exit 0`; the instance hibernates one idle window after the run.

- [ ] **Step 4: A run interrupted by `carlos restart`**

Start a long command, then from another terminal `carlos restart --app <test-app> --host <host>`. Expected: the first terminal prints `<host>: the run was interrupted: the instance was restarted. it may have done part of its work` and exits 125; a write the command committed before the restart is present after the instance comes back (it reached the replica before replication closed).

- [ ] **Step 5: Refusals**

On a flat box's instance: exit 3 with the flat sentence. Ctrl-C once during a long run: the cancel sentence, then the cancelled sentence, exit 130. `carlos run --follow <id>` from a second terminal during a run: the same output.

- [ ] **Step 6: Record the results** on amadan carlos/platform #259 with the commands and exit codes seen.

---

## Self-review notes

- §3: the synopsis (Task 1, with `--app`/`--instance` on `--follow`, index item 7), the release the instance follows (the box execs `/opt/carlos/live/<host>`; nothing here names a binary), streaming about a second behind (Task 2's one-second poll), the command's own status (Task 2).
- §3 defaults: `--timeout` 30m default, 6h max (Task 1), `--file` repeatable with 64 MB (the console enforces; the CLI states sizes), stdin none (nothing reads it), Ctrl-C (Tasks 2 and 3).
- §5.2 "No pickup": 60 s after the request, expire; success exits 3, a lost race keeps following (Task 2).
- §5.6: pages, interleaving, `last_seq` ack, the exit table (Task 2). §5.7: Ctrl-C once and twice (Tasks 2, 3).
- §4 and §6's docs (Task 4); §8's CLI messages all from the deck; §9 CLI and dev-box items (Tasks 1 to 3, 6).
