# Claude Code Lead Runtime Adapter

## Scope

This adapter maps the neutral Okstra lead operations to Claude Code host primitives. Read it only when the rendered launch prompt selects `leadRuntime=claude-code`.

## Capability declaration

| Field | Value |
|---|---|
| `runtime` | `claude-code` |
| `leadRoleLabel` | `Claude lead` |
| `userPromptMode` | `native-question` |
| `workerDispatchBackend` | `mixed` |
| `initialPromptDeliveryMode` | `lazy-path-reference` |
| `sessionAccounting` | `claude-jsonl` |
| `resumeMode` | `session-id` |
| `teardownMode` | `teammate-shutdown` |
| `leadEventSource` | `claude-jsonl+artifact` |

## Wizard interaction relay

Read this contract when `okstra preflight` returns this file as `runtimeReadiness.relayContract`. The JSON is the complete interaction mapping for this host. `semanticFunctions` is an allowlist: the skill may declare a function only when both this list and the live harness expose it.

```json
{
  "schemaVersion": 1,
  "runtime": "claude-code",
  "semanticFunctions": [
    "plain_text_input",
    "native_single_select",
    "native_multi_select",
    "native_question_group"
  ],
  "nativeLimits": {
    "minOptions": 2,
    "maxOptions": 4,
    "maxQuestions": 4
  },
  "interactions": {
    "native-single": {
      "function": "AskUserQuestion",
      "input": {
        "callCount": 1,
        "questions": "one",
        "question": "label-with-progress",
        "header": "Q1",
        "options": "all-in-original-order-as-label-description",
        "multiSelect": false
      },
      "response": {
        "envelope": "answers",
        "key": "rendered-question-text",
        "selection": "selected-label",
        "submit": "matching-option-value"
      }
    },
    "native-multi": {
      "function": "AskUserQuestion",
      "input": {
        "callCount": 1,
        "questions": "one",
        "question": "label-with-progress",
        "header": "Q1",
        "options": "all-in-original-order-as-label-description",
        "multiSelect": true
      },
      "response": {
        "envelope": "answers",
        "key": "rendered-question-text",
        "selection": "selected-labels-joined-comma-space",
        "submit": "csv-matching-option-values-in-option-order"
      }
    },
    "native-group": {
      "function": "AskUserQuestion",
      "input": {
        "callCount": 1,
        "questions": "all-in-original-order",
        "question": "label-with-progress",
        "header": "Q<one-based-question-position>",
        "options": "all-in-original-order-as-label-description",
        "multiSelect": "questions[].multi"
      },
      "response": {
        "envelope": "answers",
        "key": "rendered-question-text",
        "selection": "selected-label-or-labels-joined-comma-space",
        "multiValue": "csv-matching-option-values-in-option-order",
        "submit": "compact-step-json-values"
      }
    },
    "numbered-single": {
      "function": "host-text",
      "input": {
        "questions": "one",
        "question": "label-with-progress",
        "options": "all-in-original-order-as-numbered-markdown-label-and-description"
      },
      "response": { "source": "next-message", "submit": "raw" }
    },
    "numbered-multi": {
      "function": "host-text",
      "input": {
        "questions": "one",
        "question": "label-with-progress",
        "options": "all-in-original-order-as-numbered-markdown-label-and-description"
      },
      "response": { "source": "next-message", "submit": "raw" }
    },
    "sequential-group": {
      "function": "host-text",
      "input": {
        "questions": "all-one-at-a-time-in-original-order",
        "question": "label-with-progress",
        "options": "all-in-original-order-as-numbered-markdown-label-and-description"
      },
      "response": {
        "source": "next-message-by-question-position",
        "submit": "compact-step-json-raw"
      }
    },
    "plain-text": {
      "function": "host-text",
      "input": { "questions": "one", "question": "label-with-progress" },
      "response": { "source": "next-message", "submit": "raw" }
    }
  }
}
```

For `AskUserQuestion`, map each wizard option to the tool's `{label, description}` input without removing, shortening, or reordering entries. Look up each answer by the exact rendered `question` string sent to the tool, including the progress suffix; the `answers` keys are question text, not positions or headers. A single-select answer is the selected label. A multi-select answer is one string whose selected labels are joined with `, `. Match those labels back to the complete original option list, emit their `value` fields in original option order, and join the values with `,` for the wizard. Native plans are emitted only when the prompt fits `nativeLimits` (unique option labels, two to four options, one to four questions); other prompts use the text mapping so no option is dropped. For a group, key the compact JSON object by the corresponding `questions[].step`; a multi-select question stores its ordered value CSV as one string, not a JSON array. Never use the display header as an answer key. The `Q1`…`Q4` headers only satisfy the host tool's short-header input and carry no domain meaning.

For a `host-text` mapping, render each numbered item as its option label followed by its description verbatim; preserve every item and its order. The next user message is the raw answer. Do not translate numbers, CSV members, labels, or values before `okstra wizard step`. A sequential group wraps each raw reply in one compact JSON object keyed by `questions[].step`; the wizard owns normalization.


## Semantic operation mapping

| Operation | Mapping |
|---|---|
| `read_artifacts` | Use the host file-read primitive and preserve the core contract's read order. |
| `write_artifact` | Use the host file-write primitive only for paths authorized by the active lifecycle phase. |
| `prompt_user` | Use `AskUserQuestion` for approvals and clarifications that fit `nativeLimits`. Do not print a numbered list in chat while that tool is available. Do not infer an answer from silence. |
| `dispatch_worker` | First verify the materialized invocation metadata. Dispatch `runner=native-session` through `Agent(name: "<role>", run_in_background: true)` without `team_name`, passing the verified final prompt and `hostModelValue`. Dispatch `runner=cli-wrapper` with the deterministic shell command `okstra worker-dispatch --project-root <root> --run-manifest <path> --workers <ids>`; never wrap that process in another `Agent(...)` call. **Not in a cmux run:** when the run manifest's `terminalBackend` is `cmux-pane`, `prompts/lead/adapters/cmux.md` overrides this row. |
| `await_workers` | Arm one background shell poll for the pending Result Paths; the spawn acknowledgement is not completion. |
| `redispatch_worker` | Materialize and verify a fresh invocation, then use a fresh native `Agent(...)` session or `okstra worker-dispatch` attempt according to the persisted runner. |
| `shutdown_workers` | For each confirmed-complete worker selected for cleanup, send `SendMessage(to: <name>, message: { type: "shutdown_request" })` to idle the roster member **and** call `TaskStop(task_id: "<name>")` to stop its background task. Both are required; neither subsumes the other. |
| `record_lead_event` | Append progress and activity records to the manifest-provided `leadEventsPath`, including activity-contract-v1 records. Emit the matching `PROGRESS:` line and, when an activity record is required, the immediately following `ACTIVITY:` line from the same structured fields. |
| `collect_usage` | Run `okstra token-usage` against the team-state; it reads the run-scoped `~/.claude/projects` session JSONL evidence. |

## Dispatch variants

- The session owns one implicit team. `TeamCreate` and `TeamDelete` are absent on current Claude Code builds; never probe for them and never pass `team_name`.
- Set `name` to the core-assigned functional role label so token attribution can match `agentName`.
- Map a `runner=native-session` Claude assignment to the real host execution definition for its function (`claude-worker`, `report-writer-worker`, or `translator-worker`). A CLI-wrapper assignment has no Claude agent definition; `worker-dispatch` starts its registered provider process directly.
- For `runner=native-session`, pass the persisted `hostModelValue` as the `model` argument. For `runner=cli-wrapper`, `worker-dispatch` passes the persisted `modelExecutionValue` to the provider process. Never interchange the two fields.
- Immediately before every native host primitive, run `okstra agent-prompt record-dispatch` with the project root, run manifest, verified metadata path, and `--enforcement-mode host-native-spec-link-gate`. After the Result Path exists, run `okstra agent-prompt link-result` with `--dispatch-id <invocationId>:attempt-1` and that path before accepting or parsing it. CLI-wrapper calls are recorded by `worker-dispatch` itself.
- A resumed lead can dispatch a fresh worker; resume is not a valid reason to omit a rostered role.

### Dispatch-time model enforcement

- A native Claude execution definition declares `model: inherit`; the lead MUST override it with the verified assignment's `hostModelValue`.
- CLI-wrapper assignments never enter the Agent layer. `okstra worker-dispatch` validates the metadata and passes `modelExecutionValue` to the registered provider script.
- Missing or unsupported family-token mapping is a pre-dispatch contract failure. Never inherit the lead model, choose a nearby alias, or switch provider silently.
- Every analysis dispatch sets `name: "<workerId>-worker"`; convergence retries append `-reverify-r<N>`, implementation uses the functional `-executor` / `-verifier` suffix, and report writing uses `report-writer`. These values are retained as `agentName` in session JSONL for usage attribution.
- A CLI worker's role reaches the entrypoint from the invocation's duty, NOT from the prompt text. `WorkerJob.wrapper_role` resolves `role_for_duty(dutyId)` and passes the canonical role in the role positional; the entrypoint checks it against the same derivation in the invocation metadata and refuses a mismatch. Do not add, edit, or rely on a `**Pane role:**` line to change what a dispatch runs as — the prompt body carries that header for the initial analysis audiences only, and it selects nothing.
- The role decides the dispatch's idle budget, and `worker-dispatch` leaves the budget positional EMPTY so it can: an empty slot makes the entrypoint read the role's own budget (`domain/worker_role.role_spec` — `implementer` and `verifier` run silent build+test suites and get 1500s, every other role 600s). Passing an explicit `--idle-timeout-seconds` overrides that for the whole dispatch, so pass one only for a run-specific reason; filling the slot with a blanket default is what made the role budgets unreachable and reaped healthy workers mid-suite.
- The host may supply transport metadata for native calls, but acceptance records only the verified invocation specification link. Record `enforcementMode=host-native-spec-link-gate`, `promptPath`, and `metadataPath`; do not claim the host-delivered bytes were observed.
- A retry keeps the same Agent `name`. When logging a twice-failed CLI-wrapper attempt, reference both attempts' `bash_ids` and prompt-history paths.
- An internally detected contract violation without a specific worker uses `--agent "claude-lead"` in the error-log event.

### Reverify, critic, and report-writer assignments

- For convergence reverify, consume the persisted round plan exactly. This adapter may map and transport each returned batch, but it cannot change batch membership and does not classify findings or branch on task type, provider, or model identity.
- Reverify dispatch materializes `reverification-worker`, verifies its metadata, then uses a fresh one-shot native call named `<workerId>-worker-reverify-r<N>` or a fresh deterministic `worker-dispatch` attempt according to the persisted runner.
- Critic dispatch uses `name: "<provider>-worker-critic"`, `dispatchKind: "critic"`, and the exact mapped model from `config.critic.modelExecutionValue`. If that value cannot be mapped, record `critic-skipped: model-unresolved` and do not dispatch.
- Report-writer dispatch uses `name: "report-writer"` only for a native Claude assignment and passes `hostModelValue`. A CLI assignment goes through `worker-dispatch` with `modelExecutionValue`.
- Each variant persists its prompt path, Result Path, worker-results path, error paths, and `dispatchKind` before dispatch. Completion uses the shared background Result Path poll; an Agent acknowledgement never completes the variant.

## Completion, cleanup, and resume

- Follow the core Result Path + terminal-status completion contract. The Claude adapter's wake mechanism is one `Bash(run_in_background: true)` poll covering every pending Result Path, not foreground sleep or an idle-notification dependency. A spawn acknowledgement is never completion.
- The background poll uses a per-worker deadline of twice the expected duration: 20 minutes for `requirements-discovery`, 30 for `error-analysis`, 40 for `implementation-planning`, 40 for `implementation`, and 20 for `final-verification`. On timeout, record terminal status and apply the core's single shared retry budget.
- Each in-process worker heartbeat audit sidecar must update at least every five minutes while its result is pending. A missing or stale heartbeat consumes the same one-retry budget; after the second silent hang, record `timeout`. The result file remains the authoritative completion signal.
- **The background poll checks liveness, not only Result Paths.** Result Paths change once, at the very end, so polling them alone pays the full deadline for a worker that died at minute three. Each poll iteration MUST also run, in the same background shell, one `okstra worker-liveness` call covering every pending worker — one paired `--team-state <path> --worker <id>` per worker, in-process and CLI-wrapper alike. The probe reads that worker row's `livenessMode` to pick the artifact and its `startedAt` as the grace anchor; never pass an artifact path yourself and never infer the transport from provider or filename. It exits non-zero when a worker is `stalled` (heartbeat older than the cadence budget) or `did-not-launch`; either verdict ends the wait for that worker immediately and spends the core's one-retry budget, rather than waiting out the deadline. The command reports only — it never kills or re-dispatches. It shares its heartbeat budget with the Phase 7 audit (`okstra_ctl.worker_heartbeat`), so a worker the live probe passes cannot fail the post-hoc one for cadence. A `stalled` verdict is confirmed before it is returned — the probe waits half the stage's budget and re-reads, so a worker that is merely slow gets to prove it by appending its next heartbeat. Budget that confirmation window into the poll iteration; it is the price of not spending the one-retry budget on a live worker.
- The Claude Code harness blocks long foreground sleeps and shorter-sleep circumvention loops. Keep the result poll in a single background shell and let wrapper agents use their documented `BashOutput` loop.
- On approved cleanup, reconcile the current live session roster before sending shutdown requests. Never target the lead session.
- Collect usage before teardown. Resume through the recorded Claude session id and keep all run artifacts authoritative.

### CLI process polling

- Start `okstra worker-dispatch` with `Bash(run_in_background: true)` and poll `BashOutput(bash_id)` back-to-back until terminal completion. The deterministic dispatcher starts the registered provider script after metadata verification. Never add a foreground sleep.
- Return accumulated stdout on success. On a non-zero `exit_code`, record the real code and observed duration.
- At the 1800-second cap, inspect the live log mtime once. Recent output grants one extension to 2100 seconds; otherwise call `KillShell(shell_id)`, record exit code 124, and return the wrapper timeout sentinel.
- Keep the background process handle until the provider process reaches terminal state.

### Session accounting

- At the start of Phase 7, run `okstra token-usage /abs/path/to/run/state/team-state-<task-type>-<seq>.json --write --summary` with the literal team-state path.
- Read the lead and Claude-side wrapper evidence from `~/.claude/projects/<encoded-cwd>/<sessionId>.jsonl`; attribute workers by the dispatch `agentName` recorded above.
- Resolve `teamName` from `state.teamName` or `state.team.teamName` and use the full manifest-provided value as the team needle. If it is missing, the collector's short-form fallback cannot match worker JSONLs and records those workers as `source: "unavailable"`.
- Keep every underlying provider CLI's usage separate from the Claude wrapper-session usage. Persist `leadUsage`, per-worker usage, and `usageSummary` before report substitution and cleanup.

## Run-scoped resource lifecycle

- At run start, record `teamName` as the audit label in team-state and populate `lead.sessionId`; the session transcript lives under `~/.claude/projects/<encoded-cwd>/<sessionId>.jsonl`. You do NOT write `teamCreate`: `okstra team dispatch` records the implicit-team marker (`{ attempted: false, status: "implicit" }`) itself, on every dispatch path, because v2.1.178 made that value a constant rather than a judgment. The one marker that IS yours is the concurrent-run decision — a concurrent run records `teamCreate: { attempted: false, status: "skipped", reason: "concurrent-run" }` **before** the first dispatch, and dispatch then leaves it alone.
- Collect and persist token usage before any live-roster cleanup, including cleanup between batches and the run-end shutdown sequence.
- Before each new worker batch (and before the next phase's render-bundle), close the panes of the dispatches that finished in the prior round, in two passes. First count: `okstra team reclaim --project-root "<PROJECT_ROOT>" --run-manifest "<RUN_MANIFEST_PATH>" --dry-run` closes nothing and prints one `<paneId>\t<kind>` line per pane it would close — count those lines as `<n>`. Then run the same command **without** `--dry-run` to close them, and emit the neutral contract's `PROGRESS: phase-batch-cleanup panes=<n>` checkpoint with that count. Call both passes after collecting that round's results and token usage and before the next dispatch. The command reads each dispatch's recorded status, so an in-progress worker keeps its pane whichever moment you call it — you do not scope the pass by hand. It closes only the panes okstra opened and recorded; a pane the harness opened for itself carries no recorded id and is not okstra's to close. A `cli-wrapper` run holds no pane at all, so `<n>` is `0` — still emit the checkpoint.
- Reclaiming a pane does not stop the worker's background task. Every `dispatch_worker` Agent runs with `run_in_background: true`, so a worker whose result is already collected stays a live background task for the rest of the session — that residue is what fills the harness's exit-time `Background work is running` list. At the same batch boundary, right after the pane reclaim, call `TaskStop(task_id: "<name>")` once per worker of the completed batch, passing the exact `name` used at dispatch (`<workerId>-worker`, `<workerId>-worker-reverify-r<N>`, `<provider>-worker-critic`, `report-writer`). Stop only workers whose results were already collected — never an in-flight worker, never the lead, and keep `report-writer` while it is in flight, matching the pane pass's `--keep report-writer-worker`. `TaskStop` on an already-finished task is a no-op; treat a failure as benign, record nothing, and continue the boundary. This runs in a non-tmux session too, where the pane passes no-op but the background tasks still exist.
- Before any `prompt_user`/`AskUserQuestion` that follows worker dispatch — an approval, clarification, or decision gate — run the same two passes used at a round boundary: `okstra team reclaim … --dry-run` to count `<n>`, then the same command without `--dry-run` to close, and emit `PROGRESS: phase-gate-cleanup panes=<n>`. Then `TaskStop(task_id: "<name>")` each completed worker, exactly as at a batch boundary. A bare `TaskStop` idles the roster task and closes no pane, so it is never cleanup on its own. This keeps the user from being shown a gate while finished worker panes are still open. After that cleanup, follow the lead contract "User confirmation before an approval blocker": read cited plan items, worker findings, and files before asking, and ask in the user's language with each option's outcome.
- After batch cleanup, record the current live session generation with `okstra token-usage "<TEAM_STATE_PATH>" --record-observed-session --project-root "<PROJECT_ROOT>"`. This protects usage accounting when Claude Code re-issues the session id after resume or compaction.
- Claude Code cannot delete the implicit team or surgically remove an idle roster entry. Explain that teammates may remain visible until session end and, when needed, give the manual action `Delete team <teamName> in Teams/FleetView`.
- The `SessionEnd` hook runs `$HOME/.okstra/bin/okstra-team-reconcile.sh --session-end` as the safety net for the current live session.

### Run-end cleanup sequence (BLOCKING)

1. Call `collect_usage` through this adapter and persist usage before inspecting or changing any residual resource.
2. Run `okstra team teardown --project-root "<PROJECT_ROOT>" --run-manifest "<RUN_MANIFEST_PATH>" --dry-run` exactly once and inspect the current live roster without mutating it. Unlike the round boundary, this lists every pane the run recorded, finished or not — the run is ending, so none of them is expected to continue.
3. If no residual pane and no current live roster exists, skip the question and perform no cleanup operation.
4. Otherwise call `prompt_user` exactly once with this binary prompt. This one answer controls the complete residual pane and teammate set; never ask a second teammate-only question.
   > This phase is ending. The following Okstra panes and worker teammates remain — close and clean them up?
   > <quoted `--list` output>
   > (Yes) Close everything and clean up teammates / (No) Keep everything
5. On `keep`, preserve every residual resource and show `okstra team teardown --project-root "<PROJECT_ROOT>" --run-manifest "<RUN_MANIFEST_PATH>"` plus the manual Teams/FleetView action. Tell the user that `keep` holds only until the next boundary: if this session goes on to another phase/batch, that transition's round-boundary cleanup reclaims the kept **completed** panes unattended (in-flight resources and the lead pane are never touched).
6. On approved `clean`, emit the teardown checkpoint, run `okstra team teardown --project-root "<PROJECT_ROOT>" --run-manifest "<RUN_MANIFEST_PATH>"`, then run `$HOME/.okstra/bin/okstra-team-reconcile.sh --project-root "<PROJECT_ROOT>" --fallback-team "session-<lead.sessionId-prefix>"` exactly once. The resolver reads the current live session's `~/.claude/teams/session-<live>/config.json`, falling back to the snapshot directory only when the live directory is absent, and prints `dismissible-member: <name>` records.
7. Send `SendMessage(to: <name>, message: { type: "shutdown_request" })` to each printed, confirmed-complete non-lead member. The `message` MUST be the object literal shown, NEVER a JSON string in a text field. Never target the lead.
8. Call `TaskStop(task_id: "<name>")` for every worker this run dispatched, reusing the step-7 names plus any batch worker already reclaimed earlier. `shutdown_request` only idles the roster member and the step-6 pane reclaim only closes the pane — neither ends the background task, so this step is the one that empties the harness's exit-time `Background work is running` list. Never target the lead; a `TaskStop` on an already-finished task is a benign no-op.
