# Acp Discord Orchestrator

> Track host-owned ACPX turns with bounded CLI control and exact completion.

- Skill: `garibong-labs/acp-discord-orchestrator` (Agent Skill, multi-file: 41 files)
- Install (CLI): `npx skillmds@latest add garibong-labs/acp-discord-orchestrator`
- Raw SKILL.md: https://api.skillmd.com/api/skills/garibong-labs/acp-discord-orchestrator/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: garibong-labs (https://skillmd.com/u/garibong-labs)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/garibong-labs/acp-discord-orchestrator

---


# ACP Discord Orchestrator

Run each agent-started ACP task through one observable direct ACPX turn. Keep the current Discord conversation as the control and reporting surface.

The skill is agent-neutral within a closed supported set: ACP agent `claude` (publicly presented as `Claude Code`) and ACP agent `codex` (publicly presented as `Codex`). The public label on every reporting template is bound to the canonical config agent through that closed mapping — a caller can never choose, spell, or spoof the harness label. The closed set is enforced as the first agent check on every path: an agent outside the set fails `invalid_agent_unsupported` and a non-canonical spelling of a supported agent fails `invalid_agent_not_canonical`, before any unrelated file access or runtime loading.

## Required route

Use `scripts/acp-host-transport-cli.mjs` for every production ACP task started by the calling agent. It starts `scripts/acpx-foreground-supervisor.mjs` in one owner-only tmux session and returns the exact non-empty session handle before separate activation can begin ACP mutation. New v3 runs require OpenClaw 2026.8.1+ and Gateway-loaded `acp-lifecycle-guard` 0.6.4+ exposing `acp_report_controller` and `acp-report-controller-lifecycle-v1`. Split session-key projections require the same non-empty `sessionId` in `before_agent_run`, `before_tool_call`, and the plugin tool context. Before upgrading from 0.6.3 or older, finish or abort and release every old lease; any unproven lease blocks the upgrade.

For `agent: "claude"`, start the supervisor only through the canonical launcher `scripts/claude-acp-launcher.mjs`. It validates the config's `auth` declaration, the parent environment, and the private setup-token env file, then replaces its own process with the supervisor via POSIX `process.execve` so the run still owns exactly one foreground PID and the supervisor starts as `node --env-file=<auth.envFile> acpx-foreground-supervisor.mjs --config <config>`. The launcher requires a POSIX platform and a Node.js runtime with `process.execve` — 22.15 or newer in the 22.x line, 23.11 or newer in the 23.x line, or any later release line (23.0–23.10 lack it) — and fails closed with `execve_unsupported` everywhere else. A bare direct Claude supervisor launch is a bypass and fails closed with the supervisor policy exit before any runtime loading. The agent name must be the canonical lowercase `claude`: ACPX normalizes agent names, so other spellings would reach the same adapter and are rejected as invalid config.

Do not use:

- OpenClaw `sessions_spawn` with `runtime: "acp"`
- `/acp spawn` or another ACP child-thread command
- a new Discord thread created only to host ACP
- an untracked shell, PTY, or native subagent as a substitute for the ACP turn

This policy does not globally disable human-operated OpenClaw ACP commands.

## Prepare the run

1. Confirm Node.js 22.13+ for the supervisor and 22.22.3+ for the plugin host, then read [references/runtime-contract.md](references/runtime-contract.md). Return the parsed `openclaw plugins info acp-lifecycle-guard --json` document unchanged from `inspectLifecycleGuard`; preparation rejects an unready plugin before randomness or scheduler mutation.
2. Resolve `runtimeModule` only through the machine-enforced runtime-module preflight — never hand-copy or choose it. Save the JSON document that `openclaw plugins info acpx --json` prints on stdout, unmodified, to a private owner-only file (redirect stdout only; do not reshape, filter, or hand-edit the JSON — the preflight consumes the raw document directly and rejects any other shape), write an owner-only input file from [templates/runtime-preflight-attest.json](templates/runtime-preflight-attest.json), and run

   ```bash
   node /absolute/path/to/acp-discord-orchestrator/scripts/acpx-runtime-preflight-cli.mjs --input /absolute/private/attest.json
   ```

   The `attest` action reads the raw document's `plugin.dependencyStatus.dependencies` array, uniquely selects the entry whose name is exactly `acpx`, and uses its absolute `resolvedPath`; it fails closed on a missing or duplicate exact match, a relative or malformed path, the active plugin package root (the raw `plugin.rootDir`, plus `install.installPath` when present — rejected both by root comparison and by the `@openclaw/acpx` manifest-name gate), and any drift from the raw schema, including the legacy hand-reshaped top-level `dependencies` form. It then validates the selected package before any start boundary: manifest name exactly `acpx`, ACPX 0.11.2 or newer, a real `dist/runtime.js` entry, and the authoritative `createAcpRuntime`/`createRuntimeStore`/`createAgentRegistry` capability exports — version alone is never enough. Success writes the owner-private attestation artifact the later `assemble` step consumes; every failure is one bounded stable code that never echoes a path or the plugin-info payload. The attestation contains the resolved path and stays owner-private, like the transport record. The exact codes and the attestation schema are defined in [references/runtime-contract.md](references/runtime-contract.md).
3. Copy [templates/supervisor-config.json](templates/supervisor-config.json) to a private temporary file. Leave `runtimeModule` as the template sentinel `RUNTIME_MODULE_FROM_PREFLIGHT`: the `assemble` action in step 12 replaces it with the attested path, and the sentinel is deliberately not absolute so a config that skipped the preflight fails the supervisor's loader instead of running a hand-chosen module. The template is agent-neutral: substitute `AGENT_NAME` with the canonical agent (`claude` or `codex`) and `AGENT_DISPLAY_NAME` with that agent's mapped public label (`Claude Code` or `Codex`) exactly — the supervisor rejects any other pairing. `AGENT_NAME` appears exactly twice (the config `agent` and the reporting bundle's `agent` attestation) and `AGENT_DISPLAY_NAME` appears exactly twice (the start message and its byte-identical receipt copy — the v3 structural payload has no public report identity line); substitute `AGENT_DISPLAY_NAME` before `AGENT_NAME` so the longer placeholder is never corrupted by the shorter one, and verify every occurrence is bound. For a Codex run, replace `MODEL_ID` with the base ACP model, `REASONING_EFFORT` with its separate reasoning selection, and `MODEL_REPORTING_IDENTITY` with their composed public identity `model[reasoning]`. For a Claude run, replace `MODEL_ID` and `MODEL_REPORTING_IDENTITY` with the same adapter model, then remove the `reasoningEffort` property entirely; the supervisor rejects a Codex-only reasoning value on any other agent.
4. Create a private prompt file and choose a new response-file path.
5. Set the config file and prompt file to owner-only permissions.
6. Set an explicit working directory, ACP agent, model, unique session key, timeout, progress interval, and allowed tool kinds. The template's two-hour `timeoutMs` is an emergency ceiling independent of reporting cadence; set it per run. The `model` namespace is the ACP adapter's, never OpenClaw's. For `agent: "codex"`, keep model and reasoning separate: use base `"model": "gpt-5.6-sol"` and `"reasoningEffort": "medium"`. The supervisor applies the adapter-advertised config option ids `model` and `reasoning_effort` as two acknowledged writes after session creation and before prompt start; `thought_level` is the reasoning option's category, not the id passed to `session/set_config_option`, and `medium` is an advertised option value. Either rejection fails closed as `codex_model_config_apply_failed` or `codex_reasoning_config_apply_failed`. Never pass the historical composite `gpt-5.6-sol[medium]` as the config model — Codex bracket syntax fails as `codex_model_must_be_base_id`. An omitted Codex model/reasoning pair resolves to the same authoritative defaults, while public reporting derives the deterministic display identity `gpt-5.6-sol[medium]`; a Codex bundle claiming `runtime-default` is rejected. For `agent: "claude"`, write the adapter-advertised model ID such as `claude-fable-5`, never the OpenClaw catalog key `anthropic/claude-fable-5`; the supervisor rejects that prefix as `invalid_model_openclaw_provider_key`. Claude may retain an adapter-owned bracket suffix such as `claude-fable-5[1m]`, but must omit `reasoningEffort`; an omitted Claude model keeps the public `runtime-default` label.
7. Generate the exact round-start message with the production builder described below, but do not send it yet. The enabled controller job must exist first. The later `lifecycle.startReceipt` records the exact control conversation, delivered message ID, and observed `deliveredAt`; the supervisor validates this caller-attested receipt metadata and fails closed before runtime loading when it is missing, malformed, misrouted, future-dated, or stale.
8. Prepare the round's public reporting bundle and record it in the required `reporting` block (current schema `acp-reporting-v3`; its top-level `agent` must equal the config's canonical agent, and every template identity line must carry that agent's mapped public label — `Claude Code` or `Codex`. The `acp-reporting-v2` disabled-watchdog shape remains accepted for already-prepared configs as the bounded migration path, and the legacy `acp-reporting-v1` shape is accepted only for canonical `claude`; new preparations must produce v3, and a v3 bundle armed on the former 600000-ms schedule is rejected as `invalid_reporting_report_pump_schedule` rather than migrated — remove that automation and re-arm before `prepare`), in this order, before any launch:
   - From the authenticated direct `main` owner run, generate a cryptographically opaque 16–128 character lease token and keep it private. The scheduler — not the caller — owns job identity: OpenClaw 2026.8.1 routes `automations(action:"add")` to the public Gateway `cron.add`, whose closed parameter schema has no `id` field, so a reserved ID is unreachable and never belongs on the add job. Use [templates/report-controller-automation.json](templates/report-controller-automation.json) through `scripts/acp-report-controller-preparation.mjs` in two stages. First, `buildReportControllerPlaceholderAddCall` creates exactly one DISABLED inert placeholder automation carrying a unique `declarationKey` and no lease token, job identity, or executable controller body. Then, after capturing and validating the scheduler-returned exact job ID against that declaration key, `buildReportControllerArmUpdateCall` substitutes the private `LEASE_TOKEN` and that exact `JOB_ID` and sets `enabled: true` in one `automations(action:"update")` call, so there is never an enabled placeholder or enabled wrong-script window. That update answers with the complete persisted job; verify all of it — exact ID, exact `declarationKey`, `enabled: true`, name, session target, schedule, delivery, `deleteAfterRun`, and a payload that is exactly the script kind, the exact substituted script, the 60-second timeout, the 5-call budget, and an allowlist that is exactly the duplicate-free set `acp_report_controller`, `message`, `automations` in any order, with no other field — before binding reporting or sending any public start receipt. The stored order is not attested: OpenClaw 2026.8.1's `capCronJobToolsAllow` re-emits a finite allowlist in the creator's tool-surface order (core `automations` before core `message`, plugin tools last), so the persisted array really reads `automations`, `message`, `acp_report_controller` whatever order was requested. Position carries no authority there, so the set is what must match; a missing, extra, duplicate, non-string, `*`, or `group:`-prefixed entry still fails. A missing payload or script, any altered field, and any model/agentTurn/static-report drift stop with `report_controller_job_arm_invalid`. If the add response is lost or unreadable, replay the identical `declarationKey` add once — declaration-key convergence returns the same job instead of creating a second — and if the exact ID still cannot be proven, stop with `report_controller_job_create_unresolved`: never invent an ID; at worst one disabled inert job remains. The armed job is fixed to OpenClaw 2026.8.1's headless script contract: isolated, polling every 60000 ms, `payload.kind: "script"`, timeout 60 seconds, tool budget 5, the exact allowlist set `acp_report_controller`, `message`, `automations`, `delivery.mode: "none"`, and `deleteAfterRun: false`. Polling frequency does not change report eligibility (see Interpret events). It has no model, agentTurn prompt, static report, or fallback announcement.
   - Bind that exact scheduler-returned job ID into `reporting.reportPump`. Its exact-key payload is a non-secret structural attestation: `kind`, `scriptVersion: "acp-report-controller-script.v1"`, SHA-256 of the pinned public placeholder script, timeout, budget, and allowlist. It never contains the executable script, substituted lease token, private paths, handle, snapshot, Discord account, or report content. `delivery` is exactly `{ "mode": "none" }`; `agentTurn`, announce delivery, tool order/budget drift, a different script identity, and extra payload fields all fail closed. The exact destination remains separately bound by `pumpDestination` and by controller registration.
   - Generate the round-start message with the production builder instead of hand-assembling the template: write the structured fields (`agent`, the run's public model identity — for Codex compose the separately validated base model and reasoning as `model[reasoning]`; omit only for a Claude run without a pinned model — `roundIndex`, `repository`, `branch`, `timeKst`, `scope`, `externalAction`) to a private owner-only JSON file and run

     ```bash
     node /absolute/path/to/acp-discord-orchestrator/scripts/acp-start-message-cli.mjs --input /absolute/private/start-message.json
     ```

     The CLI writes only the rendered 13-line message to stdout. The round title (`🚀 ACP 작업 시작` for round 1, `🔁 ACP 수정 라운드 N 시작` for every correction round) and the public harness label are derived from `roundIndex` and the closed agent mapping — an input that tries to supply them is rejected — so a correction round can never reuse the round-1 title. Do not hand-write the title, the label, or the template lines; already-assembled supervisor configs stay valid because the builder's output is byte-identical to the fixed template.
   - Do not publish a preliminary acknowledgement, preflight update, or retry notice before the required enabled controller automation exists. If a current-source message is operationally necessary while owner work remains, send it with `message(action:"send", final:false)` and continue the same direct-owner turn. Treat every `inter_session` relay as non-owner context: never use it to create or resume an ACP lifecycle, and wait for a fresh direct-owner turn instead.
   - Send the exact generated round-start boundary message to the control conversation with `message(action:"send", final:false)` — the same delivery already recorded in `lifecycle.startReceipt` — matching the fixed 13-line start template for this round and the mapped agent label/model/repository/branch. Treat it as progress: after the source receipt, continue the same owner turn into assembly, `prepare`, and `activate`; the receipt is not a completion boundary.
   - Store the report-pump attestation and the start message with its receipt in `reporting`, keeping `startDestination`, `pumpDestination`, and `terminalDestination` all equal to `lifecycle.controlConversationId` and `reporting.startReceipt` byte-identical to the lifecycle receipt. (A legacy v2 bundle keeps its `watchdogDestination`/`watchdog` keys instead.)
   - Do not activate yet. A missing or malformed `reporting` bundle is invalid config: the supervisor fails closed with a stable `invalid_reporting_*` code before runtime import, probing, or adapter startup. The template ships a complete placeholder bundle; the exact schema and message templates are defined in [references/runtime-contract.md](references/runtime-contract.md).
9. Optionally declare the run's environment contract with `requiredEnv` and `forbiddenEnv`. The supervisor fails closed before runtime loading, probing, or adapter startup when a required variable is absent or empty or a forbidden variable is non-empty, and it never discloses environment values. Independent of these arrays, an agent-neutral process-integrity baseline is enforced automatically for every supported agent — `codex` exactly like `claude`: `NODE_OPTIONS` and the other Node module/preload selectors, the dynamic-linker preload/library selectors, and the proxy selectors in both letter cases must be absent or empty, and a config that lists one of them in `requiredEnv` (under any letter case) is rejected as `invalid_env_contract_overlap`. For `agent: "codex"`, an implicit Codex executable-path contract is layered on that baseline automatically, even when both arrays are empty: the operator must inject `CODEX_PATH` as an explicit environment assignment on the launch command, naming the absolute path of the real Codex executable, and the supervisor validates — still before runtime loading, probing, or adapter startup — that the value is an absolute path resolving to an existing regular executable file (a symlinked entrypoint is valid; resolution follows the link). A missing or empty value fails with the sanitized `required_env_missing:CODEX_PATH` / `required_env_empty:CODEX_PATH` codes; an invalid value fails with the stable bounded codes `codex_path_not_absolute`, `codex_path_missing`, `codex_path_unreadable`, `codex_path_not_regular`, or `codex_path_not_executable`, and the path value itself is never echoed, hashed, or length-disclosed. A codex config that lists `CODEX_PATH` (under any letter case) in `forbiddenEnv` is rejected as `invalid_env_contract_overlap`. This keeps a Codex run from silently falling back to an implicitly resolved (for example transient npx-bundled) Codex installation. The gate does not prove how a variable was injected or validate credential sources; for `codex` the generic contract, the baseline, and the executable-path contract are the whole environment contract.
10. For `agent: "claude"`, declare the required auth profile and keep the token in a private env file the config only points to:

   ```json
   "auth": { "kind": "claude-setup-token-env-file", "envFile": "/absolute/private/claude-acp-oauth.env" }
   ```

   The env file holds exactly one line, `CLAUDE_CODE_OAUTH_TOKEN=<token>`, with no quotes, comments, or extra variables. Its parent directory must be a real owner-only (0700) directory and the file a real owner-only (0600) regular file, both owned by the launching user. The Claude credential contract — `CLAUDE_CODE_OAUTH_TOKEN` required; competing `ANTHROPIC_API_KEY`, `ANTHROPIC_AUTH_TOKEN`, `CLAUDE_CODE_USE_BEDROCK`, `CLAUDE_CODE_USE_VERTEX`, and `CLAUDE_CODE_USE_FOUNDRY` forbidden; and every injection-capable variable forbidden, meaning the agent-neutral process-integrity baseline (`NODE_OPTIONS` and the other Node module/preload selectors, dynamic-linker preloads, proxy selectors) plus the Anthropic-specific endpoint/header/config selectors layered on top for Claude — is enforced automatically even when `requiredEnv` and `forbiddenEnv` are empty. Never place the token value in the config, argv, or any argument.

   The main template is agent-neutral and ships no `auth` block. The Claude-specific `agent`/`auth` pairing lives in the provider-specific profile [templates/claude-auth-profile.json](templates/claude-auth-profile.json); for `agent: "claude"`, merge that profile's `auth` block into your run config. For every other agent, declare no `auth` at all: an `auth` profile on a non-Claude agent is invalid config (`invalid_auth_agent`), and non-Claude runs — including `codex` — declare their whole environment contract through `requiredEnv`/`forbiddenEnv` instead. (A dedicated provider-auth skill, `acp-agent-auth`, is planned separately; until it exists, the generic environment contract is the only supported non-Claude credential gate.)

   The launcher certifies a clean parent environment and unsets nothing silently. If the launching shell already exports `CLAUDE_CODE_OAUTH_TOKEN` (even empty), a competing credential selector, or an injection-capable variable such as `NODE_OPTIONS`, the launch fails closed with a code naming the variable. Remediate by removing the variable explicitly in the launching shell — for example `env -u NODE_OPTIONS -u CLAUDE_CODE_OAUTH_TOKEN node …` — rather than expecting the launcher to strip it.
11. Define terminal acceptance checks in the prompt. Every implementation prompt must stop the ACP agent after tests and a local commit, prohibit `git push` and remote VCS/repository-hosting actions, and prohibit delegating those actions to a script. Feature push, pull-request creation or updates, public comments, review responses, and merge happen only in the owner-side verification handoff after the ACP terminal result. For every remote-backed implementation task, that handoff must verify the full diff, required tests/checks, and Git identity, then push only the feature branch and open a new PR or update the explicitly authorized existing PR before reporting orchestrator completion. An explicitly local-only/no-PR task ends after verification; default-branch push, merge, deployment, publication, and restart remain outside the default boundary.
12. Resolve this skill's directory and use `scripts/acp-host-transport-cli.mjs` by absolute path. Before step 8 creates the job or publishes the boundary, run the harmless `probe` action from a private input file and require `host_transport_ready`. After the exact job is created and armed, its scheduler-returned ID is bound, and the start receipt exists, run `assemble` from an owner-only input per [templates/runtime-preflight-assemble.json](templates/runtime-preflight-assemble.json), then `prepare` with that assembled config. Retain its exact private `transportFile` and `processHandle`. From the same authenticated direct `main` owner session, call the installed `acp_report_controller` with `action: "register"` and the private schema in [templates/report-controller-registration.json](templates/report-controller-registration.json): lease token, host-authenticated owner session/run, job ID, Discord account/destination, transport/handle, optional private snapshot, and the absolute attested `acp-report-pump.mjs` and `acp-host-transport.mjs` entries. Require structured status `prepared`; only then call transport `activate` with the exact handle. After `host_transport_activated`, call `acp_report_controller` with `action: "commit_activation"` and require `active`. The plugin imports the attested host module and accepts only its exact `host_transport_activation_confirmed` proof. `scripts/acp-report-controller-preparation.mjs` exposes this sequence so callers do not hand-write job, attestation, or registration shapes. For Codex, inject the host-specific executable path on `prepare`, for example `CODEX_PATH=/opt/homebrew/bin/codex`.

   A thrown, missing, malformed, or otherwise uncertain first registration response does not prove non-persistence. Replay the byte-identical registration once, including the same token, job, destination, transport/handle, snapshot, and executable entries; the pinned plugin returns the already persisted prepared lease without consuming capacity and transfers its lifecycle completion fence to a fresh authenticated owner run in the same canonical owner session. Only an exact structured `prepared` result permits activation. If the replay is negative or unresolved, retain the exact owner-private `registration_pending` state in [templates/report-controller-registration-recovery.json](templates/report-controller-registration-recovery.json), then stop with `report_controller_registration_recovery_pending` without removing, aborting, releasing, activating, committing, or inventing success. Any scheduler executions during that uncertainty are inert: they call only controller `tick`, return `{}`, and leave the exact job, transport, recovery record, and possible prepared lease intact. Resume from a fresh authenticated `main` run in the same canonical owner session by replaying only that retained registration; a mismatch remains fail-closed. A first-call exact plugin rejection from the closed pre-persistence code set may use the existing safe cleanup because it proves no lease was persisted.

   On other failure before authoritative activation confirmation, never relaunch. Remove only the exact newly created automation first — including a failed or unproven arm, which removes the exact created job and never binds, starts, prepares, or registers. Removal counts as confirmed only on a strict boolean `removed: true` or the exact status string `"removed"`, read at the top level or inside a plain-object `details` — the model-callable remove wraps the Gateway result with `jsonResult(...)`, so a real success arrives as `{ "details": { "removed": true } }` with no top-level `removed`. The parser evaluates every recognized `removed`, `status`, and explicit `error` field before deciding: multiple positive signals may coexist, but a truthy string, a non-boolean, an unrelated status, an explicit error, a same-level or cross-level contradiction, a malformed or non-object `details`, or a thrown removal is unproven. After confirmed removal, call controller `abort_preactivation` when registration was confirmed; when transport preparation succeeded but registration construction or a proven pre-persistence registration rejection occurred, call the attested transport abort directly with the exact retained file and handle. Require the proven aborted result. If removal or abort fails, stop with `report_controller_pre_activation_cleanup_failed` and retain the private preparation state. If transport activation succeeded but `commit_activation` failed or its result was not `active`, do not remove, abort, relaunch, or infer failure: retain the exact owner-private `commit_activation_pending` recovery state (including its token, job, transport file, and handle) and retry only `commit_activation` from a fresh authenticated `main` run in the same canonical owner session.

Every CLI action reads one owner-only JSON file:

The closed action inventory is `probe`, `prepare`, `activate`, `status`, `claim-report`, `begin-delivery`, `ack-report`, `cancel`, and `reconcile`, all under schema `acp-host-transport.v2` (the v1 schema is an incompatible predecessor: v1 inputs and records fail closed; finish or cancel an in-flight v1 run with the v1 skill before upgrading). `status` has only the optional keys `afterSequence`, `serviceCursorAck`, and `reissueServiceCursor`; `claim-report` binds `jobId`, `runToken`, and `destination`; `begin-delivery` and `ack-report` carry the exact fenced attempt identity, with `ack-report` adding the structured-report and digest-bound receipt shape shown later. Unknown actions or keys fail closed.

Every v2 transport record carries the mandatory exact-key `controllerLease` phase object; a missing or malformed lease is `host_transport_record_invalid` before any phase use. Pump boundary fields fail distinctly as `report_pump_transport_file_invalid`, `report_pump_process_handle_invalid`, `report_pump_job_id_invalid`, or `report_pump_destination_invalid`. Snapshot file access preserves the bounded `invalid_input_file_missing`, `invalid_input_file_unreadable`, `invalid_input_file_symlink`, `invalid_input_file_not_regular`, `invalid_input_file_permissions`, `invalid_input_file_empty`, `invalid_input_file_too_large`, and `invalid_input_json` codes; only parsed schema/content failures become `report_pump_snapshot_invalid`.

```json
{"schemaVersion":"acp-host-transport.v2","action":"prepare","configFile":"/absolute/private/run.json"}
```

```bash
CODEX_PATH=/absolute/path/to/codex node /absolute/path/to/acp-discord-orchestrator/scripts/acp-host-transport-cli.mjs --input /absolute/private/prepare.json
```

`prepare` returns `host_transport_prepared` with one exact `processHandle` and private `transportFile`. It does not activate ACP. Persist both values privately, register the controller lease in `prepared`, then make a second CLI call whose input has `action: "activate"`, the exact returned `transportFile`, and the exact returned `processHandle`. The transport fences activation against preactivation abort under the private record lock, verifies that tmux still owns that session, waits for `activation_required`, and writes exactly one line into that same PTY:

```json
{"schemaVersion":"acp-host-activation.v1","processHandle":"<exact-host-handle>"}
```

Only the matching `host_transport_activated` result and supervisor `activation_confirmed` event permit runtime import, probe, `ensureSession`, and `startTurn`. EOF, timeout, malformed activation, a missing/invalid handle, or loss of the owner between the two calls fails closed before ACP mutation. The activation deadline is 60 seconds. The handle, transport file, lease token, owner identities, and substituted job script stay private and must never enter the supervisor config or Discord output.

## Own the process without blocking the conversation

Process ownership belongs to the transport's exact tmux session. It is detached from any one shell tool call but remains explicitly addressable by the returned handle and private transport record until the supervisor exits. The ACP supervisor and adapter remain foreground children inside that owned PTY; they are not shell-backgrounded within it.

Cadence delivery does not require owner polling: only after the lease is committed `active` does the registered controller automation own claim, send, receipt acknowledgement, and terminal/tracking cleanup while the exact transport remains active. Owner-side `status` is still the only supported observation path when the owner is actively supervising or later resumes. Use the same `transportFile` and `processHandle`, carry `lastSequence` as `afterSequence`, and acknowledge each private service cursor after servicing newly steered input. If polling in one live owner turn, use 1, 2, 4, and then 5 seconds; do not read raw event, stderr, or exit files.

Do not substitute:

- a PID search or broad process monitoring
- transcript or direct log-file polling
- a long shell sleep
- a long blocking exec or write wait
- a second transport, wrapper, or nested runner around the same run

A returned poll is a host-tool boundary. It is not activity evidence and never terminal evidence.

Persist the exact private preparation record so owner-side mapped-exit observation, response inspection, lifecycle-ledger reconciliation, and Git/PR verification can resume later in the same canonical owner session. The required `acp-report-controller-lifecycle-v1` policy must block `sessions_yield` plus final messages across either canonical or projected session-key view of the exact owner run and request a bounded `before_agent_finalize` revision while an active lease exists. OpenClaw 2026.8.1 applies only the plugin's bounded two-attempt revision budget; `agent_end` is observational and cannot cancel an end after that budget is exhausted. The durable controller lease therefore survives an unavoidable owner-turn end and cadence delivery continues. Do not claim that the plugin can keep the direct owner run alive indefinitely.

If the exact session is dead without terminal evidence, `status` and `claim-report` report sticky `tracking_lost` even when a mapped exit file survives and never infer success or relaunch automatically. The controller script receives that terminal control state, removes only its current authenticated job, and releases the lease only after removal succeeds.

## Stay responsive at each poll boundary

Treat every returned poll as a servicing point. Before the next poll, read newly steered input from the current conversation and answer it there.

Continue the same ACP round with the retained handle unless the message explicitly cancels or replaces it. Steered input that asks a question, adds context, or requests a status update does not end the turn.

When the message does explicitly cancel or replace the turn, follow the documented cancellation path for the retained handle instead of abandoning it.

## Interpret events

The compatibility pin is merged plugin commit `aa1d6c66d999140eca8bffb4312d950018f8ca1f`. The loader enforces the exported template SHA-256 `5a75b6eea2b4b190ea42eaab22d7c99252a5aeb1431c99052e7881c7b63581b3` and placeholder-script SHA-256 `1dd0ccd2d2bd25ef25c002672a2b6ac4ccf7721b2b9e6304bdf4ddd8ce8ca6f2`. That pin preserves structured tick/send/ack, exact prepared replay, fail-closed mismatches, inert prepared ticks, and `{}` from every non-throwing script path.

Treat `activity` and `progress` as observational evidence only. The progress snapshot includes evidence age; it does not independently prove that ACP is still doing useful work.

Read the private response file locally after the process reaches a terminal event. Do not publish that file automatically.

Treat only the matching `terminal` event as terminal evidence. Preserve `completed`, `cancelled`, and `failed` as distinct states.

Reporting is coreless and controller-driven: it never depends on the direct owner turn repeatedly calling `status`. At each 60-second scheduler poll, the exact isolated OpenClaw 2026.8.1 session `agent:main:cron:<jobId>:trigger` calls `acp_report_controller(action:"tick")` with only its private lease token. The host transport alone owns 600-second report eligibility, so early polls return `none_due` and cannot publish an intermediate report before its cadence boundary. An unacknowledged delivery is retried only at the first poll after its attempt lease expires, and terminal evidence arriving inside that lease is observed once it expires; [references/runtime-contract.md](references/runtime-contract.md) states the bounds. The controller imports the content-attested pump and transport entries, claims and begins one fenced delivery, and returns a declared structured result. On `delivery_pending`, the script sends only the opaque one-shot publication token with `message(final:false)`; the trusted plugin policy injects the privately held byte-exact report and registered Discord route. OpenClaw's `message_sent` hook—not script interpretation—acknowledges the exact successful receipt through the transport using the pump's exact structured report, so minute-sensitive fields are never re-derived; the script then performs one bounded second tick. `delivery.mode: "none"` suppresses every runner fallback/static announcement. `none_due`, `delivery_missing`, `delivery_uncertain`, and bounded error results stay silent.

The controller tool's OpenClaw 2026.8.1 `outputSchema` covers every returned status and exposes the structured `details` fields that Code Mode consumes directly; the script never parses rendered tool text. Transport locks, monotonic fences, exact job/destination binding, digest validation, attempt TTLs, and bounded retries remain authoritative. Direct owner-driven `acp-report-pump.mjs`/`ack-report` use remains a diagnostic/migration surface, not the v3 cadence publisher.

The intermediate time line is structured, exactly `⏱️ **ACP 시간**: 전체 <N분> · 현재 단계 <N분> · 마지막 ACP 활동 <N분 전>`. `마지막 ACP 활동` is the age of the intermediate boundary's `lastAcpActivityAt` — the latest normalized ACP activity event (envelope type `activity`: the model/tool/status activity) of the current exact execution handle. Host lifecycle/control marks such as activation confirmation or supervisor start are not ACP activity; if no normalized ACP activity event exists yet the boundary reports a fail-closed missing (`null`) value instead of substituting one. The instant is never reset by a transport poll boundary, timer-driven progress bookkeeping, report rendering or delivery receipt, phase bookkeeping, or owner-side work. `Δ` in the 새 결과 bullet is not a transport value: raw tool completions (reads, edits, searches, command runs) are activity, never material results, so the transport exposes no result delta. `newResultDelta` belongs to the owner-confirmed reporting snapshot — it counts material ACP results completed since the previous successfully delivered intermediate report, and the owner advances that semantic result cursor only after a verified delivery receipt. Ongoing ACP activity without a newly completed material result keeps `Δ0` and renders `Δ0 · 새로 확인된 ACP 결과 없음`, so `마지막 ACP 활동 0분 전` with `Δ0` is a valid combination; a newly completed material result increments `Δ` regardless of the activity age. The builder rejects the legacy free-text intermediate `elapsed` input.

A terminal event before a cadence requires only a terminal report. A terminal event racing an overdue intermediate supersedes it and mints the terminal cadence-0 report identity on the next claim. After `message_sent` acknowledges the exact terminal receipt, the script's bounded second tick (or the next scheduled tick) returns `terminal_acked`; for it and `tracking_lost`, cleanup is exactly proven `automations(remove current jobId)` followed by controller `release`, with no release if removal fails. Ordinary terminal transport reconciliation remains `terminal_publication_pending` until the canonical terminal receipt is acknowledged; the distinct no-terminal `tracking_lost` fault is itself reconciled terminally into the lifecycle ledger. Owner-side mapped-exit/ledger reconciliation and Git/PR verification remain mandatory and may resume later from the retained private record; controller cleanup never performs them.

Map supervisor exits as documented in the runtime contract. Never turn a failed or cancelled run into a success report. Treat process exit as the final delivery of that mapping; the CLI bounds output flushing before forcing termination so leaked runtime handles cannot hold the caller open.

After the mapped exit exists and terminal `ack-report` succeeds, call the host transport CLI with `action: "reconcile"`, the same private transport file, and the same handle. It derives the exact lifecycle ledger and confirms terminal intent plus mapped exit. A pre-ack call returns `terminal_publication_pending` without changing the ledger. Repeated reconciliation re-verifies the current handle/outcome/exit evidence against the stored result; a matching call is idempotent, while a previously recorded `tracking_lost` remains that precise outcome rather than becoming an unrelated duplicate error. A supervisor error before activation is reconciled with the ledger's explicit null handle; an invented handle is never bound to that run. If an activated transport disappears without terminal intent, transport `reconcile` records the exact handle as `tracking_lost` even if a mapped exit file survives; the standalone `scripts/acp-lifecycle-reconcile-cli.mjs` remains an equivalent owner-only recovery surface. Handle mismatch, missing terminal evidence, and exit mismatch fail closed.

Report ACP lifecycle completion only after both the matching normalized terminal event and the mapped supervisor process exit have been observed, and the canonical terminal receipt acknowledgement plus private-ledger reconciliation have succeeded. Report orchestrator completion only after the owner-side PR handoff required by step 11 has also succeeded, or after an explicit local-only/no-PR boundary is recorded. A returned poll, a quiet event stream, a serviced conversation reply, or an unpushed local commit replaces none of them.

## Foreground policy

The supervisor rejects detached execution forms, explicit background flags, permission bypass modes, nested agent routes, uninspectable or over-limit permission input, unclassified tool kinds, tool kinds outside the configured allowlist, and an `execut

…(truncated)
