Skip to main content
A harness is a tiny program Archal spawns as a child process. Its job: read the task text out of one environment variable, invoke your real agent runtime, and print the final answer to stdout. That’s it. Put it at ./.archal/harness.ts for new integrations. npx archal init generates one for you; this page is the reference for writing or extending one by hand.

Template

./.archal/harness.ts
// 1. Preflight short-circuit.
//    archal runs the harness with ARCHAL_PREFLIGHT=1 before it provisions
//    twins, just to confirm the entrypoint exists and can exit cleanly.
//    Return early so preflight never does real work or burns model credits.
if (process.env.ARCHAL_PREFLIGHT === '1') {
  console.log('OK');
  process.exit(0);
}

// 2. Read the task.
const task = process.env.ARCHAL_ENGINE_TASK;
if (!task) {
  console.error('Missing ARCHAL_ENGINE_TASK');
  process.exit(1);
}

// 3. Twin endpoints.
//    Every twin in the scenario gets ARCHAL_<TWIN>_URL (MCP) and
//    ARCHAL_<TWIN>_BASE_URL (REST) injected into this process's env.
//    Example: ARCHAL_GITHUB_URL, ARCHAL_GITHUB_BASE_URL.
const githubBase = process.env.ARCHAL_GITHUB_BASE_URL;

// 4. Call your real agent runtime.
//    Avoid booting the full app shell (UI, servers, auth flows). The harness
//    is a direct callsite into your core agent code.
const result = await runMyAgent({ task, githubBase });

// 5. Print the answer to stdout.
//    Send logs to stderr — the stdout-extractor prefers structured output
//    and will get confused by interleaved log lines.
console.log(typeof result === 'string' ? result : JSON.stringify(result));

Stdout contract

Archal extracts the final answer from stdout with this precedence (cli/src/runner/execution/response-extraction.ts:200):
  1. Whole stdout parses as a single JSON object. Archal reads the payloads[] or text field. Recommended shape for structured output:
    { "text": "The final answer." }
    
  2. JSON object embedded in mixed stdout. Archal scans for any outermost {...} block containing payloads or text. Works when your agent prints logs and then a final JSON payload, but is fragile — prefer option 1.
  3. NDJSON (one JSON object per line). Archal parses each line and picks the object with a usable payloads / text field. Useful when your harness streams intermediate events.
  4. Plain text fallback. If nothing above matches, Archal joins trimmed non-empty stdout lines outside JSON blocks. Any log lines you wrote to stdout end up in the “answer”. Keep logs on stderr if you rely on this path.
For most harnesses a single console.log(JSON.stringify({ text: result })) is the right default.

Environment variables Archal injects

Archal sets these on the harness process before it spawns:
VariableDescription
ARCHAL_ENGINE_TASKThe full task text (scenario prompt or --task "...").
ARCHAL_PREFLIGHTSet to 1 during the boot preflight. Short-circuit and exit 0.
ARCHAL_<TWIN>_URLPer-twin MCP endpoint (e.g. ARCHAL_GITHUB_URL).
ARCHAL_<TWIN>_BASE_URLPer-twin REST base URL (e.g. ARCHAL_GITHUB_BASE_URL).
ARCHAL_TWIN_NAMESComma-separated list of twins in this run.
ARCHAL_MCP_CONFIGPath to an MCP server config JSON, if your runtime wants to mount MCP servers directly.
ARCHAL_TOKENBearer token, if you need to call hosted twin APIs directly.
HTTPS_PROXYPresent when the TLS proxy is on.
NODE_EXTRA_CA_CERTSPath to the proxy’s short-lived CA cert (present when the proxy is on).
Your own model API keys (OPENAI_API_KEY, ANTHROPIC_API_KEY, etc.) are not injected by Archal. Put them in .env or your CI secret store and read them in the harness.

Exit-code contract

CodeMeaning
0Run succeeded, final answer printed to stdout. Evaluator scores the trace.
non-zeroRuntime error. The run is marked failed; [P] criteria are not evaluated.
The exit code only decides whether the evaluator runs. [D] criteria about twin state are still checked either way as long as the run produced a trace.

How to route traffic to the twins

Two options, both supported side-by-side:
  1. Direct HTTP to the injected endpoints. Read ARCHAL_<TWIN>_BASE_URL / ARCHAL_<TWIN>_URL and pass them to your SDK (new Octokit({ baseUrl: process.env.ARCHAL_GITHUB_BASE_URL })). This is the preferred path — explicit, no TLS hijacking.
  2. TLS proxy (route mode). When your runtime has hardcoded service domains you can’t override (for example SDKs calling oauth2.googleapis.com directly), Archal starts a TLS proxy and sets HTTPS_PROXY / NODE_EXTRA_CA_CERTS on the harness process. Calls to known twin domains get intercepted transparently. The proxy is on by default for local harness runs — pass --no-proxy to disable it. See Route-mode trust and safety.

Sanity-check the harness in isolation

Reproduce the preflight without running a scenario:
ARCHAL_PREFLIGHT=1 ARCHAL_ENGINE_TASK="noop" npx tsx ./.archal/harness.ts
That should print OK and exit 0. If it hangs or crashes, archal run will too — fix the harness first.