# Engine Wrapper Contract

audience: AI coding agents first. Imperative, BLUF.

The uniform shell CLI **every** engine wrapper MUST implement so the harness dispatches any provider
(north, cursor/composer, codex, grok, …) through ONE call shape. The orchestrator node never knows
which engine it called — it resolves a seat→binding (see `PRESETS.md`), invokes the wrapper BY PATH,
reads the exit code. Canonical implementations: `skills/north-orchestrator/na.sh`,
`skills/cursor-orchestrator/ca.sh`. A new engine = a new wrapper that satisfies THIS contract + one
registry line. Zero orchestrator change.

## Invocation — fixed flag set

```
<wrapper> --workspace <dir> --trust <prompt> --task-slug <slug> [--model <id>] [--timeout <secs>] [--profile <slug>]
```

- `--workspace <dir>` — REQUIRED. Existing dir. cd into it before running the engine. Use a SHORT
  path (long absolute CWD triggers some engines' phantom-tree write bug — see `na.sh`).
- `--trust <prompt>` — REQUIRED. The full task prompt. Single arg (quote it).
- `--task-slug <slug>` — REQUIRED. Drives log filename + retry hygiene.
- `--model <id>` — pin the model. If the binding carries a model, pass it. NEVER rely on the engine's
  interactive default — an unpinned run falls through to whatever model the user last selected, and the
  log still claims the intended one (the shipped `ca.sh` regression, 2026-06-09..06-15). A wrapper for an
  engine that takes a model MUST require it resolved (binding or flag), never silently default.
- `--timeout <secs>` — optional. Default per wrapper (na.sh=360). Bound the engine with `timeout -k 5`.

## Behavior — MUST

1. **Foreground, bounded, un-hangable.** Run the engine in the FOREGROUND under `timeout -k 5 <secs>`,
   with `stdin </dev/null`. NEVER background a daemon-style engine or pipe it interactively — the
   `ccr start &` / foreground-pipe class hangs the whole run (see memory `workflow-daemon-dispatch-hang`).
2. **Env scoped to the subprocess.** Source any engine env (API keys, proxy vars, OpenRouter routing)
   inside the wrapper's OWN subprocess. Exports MUST NOT leak to the caller's shell. `na.sh` sources
   `NORTH.env` in-subprocess; copy that discipline.
3. **Fail-closed.** Engine/proxy/precondition unavailable → exit `3`, do NOT dispatch. An erroring
   wrapper MUST NOT read as success. Unknown/invalid args → exit `2`.
4. **Log raw to a file, not stdout.** Write the engine's raw stream/json to a per-run logfile; first log
   lines = the exact command + `%q`-quoted prompt (which tool + what it ran). Keep stdout for the status
   line only (below). Cost-mining tooling reads the logfile.
5. **Pin the model on BOTH the run line and the log line from ONE built array** so they cannot diverge
   (DRY — `ca.sh`'s `CURSOR_ARGS`).

## Exit codes — the status channel (HARD FLOOR — exact, do not renumber)

| code | meaning | orchestrator action |
|------|---------|---------------------|
| `0`  | completion — engine ran to completion | proceed to gate |
| `124`| non-completion — timeout / killed | retry ONCE, or 429-backoff; never treat as a defect |
| `2`  | usage error — bad/missing args | BLOCK — fix the call |
| `3`  | engine/proxy down — precondition failed, NO dispatch happened | BLOCK or try `fallback` binding |
| `75` | rate-limited — engine quota/429; NOT a defect, NOT engine-down | wait until reset, retry SAME binding; log wait + heartbeat so run reads alive |

Any other nonzero = engine's own error rc → treat as non-completion (retry once), then BLOCK.

Exit `75` status line SHOULD carry `resume_at` (ISO 8601) parsed from the engine's message:
`{"ok":false,"detail":"rate-limited","resume_at":"2026-07-06T18:29:00+07:00"}` — empty/absent → orchestrator default backoff.

## stdout — status line (optional, additive)

A wrapper MAY print ONE final JSON line to stdout for the caller to parse:

```json
{"ok": true, "detail": "...", "files": ["..."], "commitCount": 2}
```

`ok` MUST agree with the exit code (`ok:true` ⇔ exit 0). The orchestrator computes `baseSha`/`headSha`
itself (HEAD before/after dispatch) — a wrapper MUST NOT be trusted as the sole source of git state.

## Session continuity — additive, rc contract unchanged

If an engine exposes resumable conversation state, a wrapper MAY accept ONE provider-specific continuity
flag in addition to the fixed dispatch flags:

- `na.sh` style: `--session-id <id>`
- `ca.sh` style: `--resume <id>`
- `codex.sh` style: `--thread-id <id>`

Rules:

- This section is ADDITIVE. It does NOT change the fixed dispatch shape above, and it does NOT renumber
  exit codes.
- Continuity id present → wrapper MUST forward it on the provider run line. Do NOT silently drop it.
- Completion (`exit 0`) with a continuity-capable wrapper MUST surface the active continuity id on stdout:
  JSON wrappers SHOULD emit `session_id` or `thread_id` in the final status JSON; non-JSON wrappers MAY
  emit a deterministic status line carrying the same id.
- Continuity id absent → wrapper MAY create a fresh provider session/thread internally, then surface the
  resulting id on completion if the provider exposes one.
- Continuity MUST stay a dispatch concern only. Do NOT add gate/review/retry policy inside the wrapper.

## `--health` / `--list-models` — additive inspection modes

Wrappers MAY expose two non-dispatch inspection modes:

- `--health` — print provider/proxy health payload to stdout, then exit.
- `--list-models` — print provider model inventory payload to stdout, then exit.

Rules:

- These modes are ADDITIVE. They do NOT replace or alter the dispatch contract.
- They MUST NOT require `--workspace`, `--trust`, `--task-slug`, or `--model`.
- They MUST NOT dispatch a coding task.
- `0` = probe/list succeeded. `2` = bad args. `3` = engine/proxy/precondition unavailable. `124` only if
  the inspection itself times out or is killed.
- Output MAY be provider-native JSON or plain text; wrappers SHOULD proxy the underlying payload without
  inventing a second schema unless the provider has no structured output.

## NEVER

- NEVER let the wrapper hold a gate, review, or judge. A wrapper DISPATCHES a coding engine and reports
  rc. Review/gate is a separate seat with its own binding.
- NEVER inline the env block or the engine command at the call site. Agents invoke the wrapper BY PATH;
  re-deriving the rig = the re-derivation hazard the wrapper exists to kill.
- NEVER swallow a nonzero rc into 0. Fail-closed is the whole point.
