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
Confirm Node.js 22.13+ for the supervisor and 22.22.3+ for the plugin host, then read 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.
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, and run
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.
Copy 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.
Create a private prompt file and choose a new response-file path.
Set the config file and prompt file to owner-only permissions.
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.
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.
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 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
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.
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.
For agent: "claude", declare the required auth profile and keep the token in a private env file the config only points to:
"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; 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, 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: 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, 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.
{"schemaVersion":"acp-host-transport.v2","action":"prepare","configFile":"/absolute/private/run.json"}
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:
{"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 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)
1---2name: acp-discord-orchestrator3description: Track host-owned ACPX turns with bounded CLI control and exact completion.4---56# ACP Discord Orchestrator78Run each agent-started ACP task through one observable direct ACPX turn. Keep the current Discord conversation as the control and reporting surface.910The 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.1112## Required route1314Use `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.1516For `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.1718Do not use:1920- OpenClaw `sessions_spawn` with `runtime: "acp"`21- `/acp spawn` or another ACP child-thread command22- a new Discord thread created only to host ACP23- an untracked shell, PTY, or native subagent as a substitute for the ACP turn2425This policy does not globally disable human-operated OpenClaw ACP commands.2627## Prepare the run28291. 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.302. 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 run3132 ```bash33 node /absolute/path/to/acp-discord-orchestrator/scripts/acpx-runtime-preflight-cli.mjs --input /absolute/private/attest.json34 ```3536 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).373. 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.384. Create a private prompt file and choose a new response-file path.395. Set the config file and prompt file to owner-only permissions.406. 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.417. 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.428. 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:43 - 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.44 - 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.45 - 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 run4647 ```bash48 node /absolute/path/to/acp-discord-orchestrator/scripts/acp-start-message-cli.mjs --input /absolute/private/start-message.json49 ```5051 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.52 - 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.53 - 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.54 - 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.)55 - 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).569. 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.5710. For `agent: "claude"`, declare the required auth profile and keep the token in a private env file the config only points to:5859 ```json60 "auth": { "kind": "claude-setup-token-env-file", "envFile": "/absolute/private/claude-acp-oauth.env" }61 ```6263 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.6465 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.)6667 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.6811. 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.6912. 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`.7071 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.7273 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.7475Every CLI action reads one owner-only JSON file:7677The 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.7879Every 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`.8081```json82{"schemaVersion":"acp-host-transport.v2","action":"prepare","configFile":"/absolute/private/run.json"}83```8485```bash86CODEX_PATH=/absolute/path/to/codex node /absolute/path/to/acp-discord-orchestrator/scripts/acp-host-transport-cli.mjs --input /absolute/private/prepare.json87```8889`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:9091```json92{"schemaVersion":"acp-host-activation.v1","processHandle":"<exact-host-handle>"}93```9495Only 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.9697## Own the process without blocking the conversation9899Process 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.100101Cadence 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.102103Do not substitute:104105- a PID search or broad process monitoring106- transcript or direct log-file polling107- a long shell sleep108- a long blocking exec or write wait109- a second transport, wrapper, or nested runner around the same run110111A returned poll is a host-tool boundary. It is not activity evidence and never terminal evidence.112113Persist 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.114115If 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.116117## Stay responsive at each poll boundary118119Treat every returned poll as a servicing point. Before the next poll, read newly steered input from the current conversation and answer it there.120121Continue 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.122123When the message does explicitly cancel or replace the turn, follow the documented cancellation path for the retained handle instead of abandoning it.124125## Interpret events126127The 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.128129Treat `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.130131Read the private response file locally after the process reaches a terminal event. Do not publish that file automatically.132133Treat only the matching `terminal` event as terminal evidence. Preserve `completed`, `cancelled`, and `failed` as distinct states.134135Reporting 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.136137The 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.138139The 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.140141A 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.142143Map 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.144145After 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.146147Report 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.148149## Foreground policy150151The 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 `execut152153…(truncated)