From 28fcffea58734dba46e459c459e3272f6325320f Mon Sep 17 00:00:00 2001 From: Rohit Ghumare Date: Fri, 24 Jul 2026 14:32:31 +0100 Subject: [PATCH 1/4] docs(harness): mechanics reference skill for turns, triggers, sub-agents --- harness/skills/reference.md | 211 ++++++++++++++++++++++++++++++++++++ 1 file changed, 211 insertions(+) create mode 100644 harness/skills/reference.md diff --git a/harness/skills/reference.md b/harness/skills/reference.md new file mode 100644 index 000000000..bc063b2f4 --- /dev/null +++ b/harness/skills/reference.md @@ -0,0 +1,211 @@ +--- +name: harness/reference +description: >- + The harness mechanics reference: how a turn runs from harness::send to its + terminal status, every public function and what the internal ones do, the + trigger types the harness publishes (turn-started, turn-completed, + message-queued, the five hooks), how a session registers triggers and what + fires back into it, and the full sub-agent lifecycle from spawn to + [child-failure]. The conduct playbooks (harness/orchestration, + harness/finishing) say what an agent SHOULD do; this says what the machine + DOES. +--- + +# harness/reference + +The harness is the durable turn loop. It wires three siblings into an agent: +`session-manager` (the transcript), `context-manager` (what fits in the +window), and `llm-router` (which model generates). A consumer stays thin: it +calls `harness::send`, renders the conversation from session events, and +reacts to turn boundaries. The assistant message streams into the session as +it generates — there is no separate render feed, and `harness::status` is a +point-in-time recovery read, not something to poll. + +## The turn lifecycle + +One turn, end to end: + +1. **`harness::send`** ensures the session exists, persists the incoming user + message, and kicks off a turn. It returns fast with + `{ accepted, session_id, turn_id }`. If a turn is already running in that + session, the message merges into it or queues behind it (a queued message + fires the `harness::message-queued` event; the console's edit path uses the + internal `harness::unqueue` / `harness::edit_queued` to modify a + still-parked entry). An `idempotency_key` dedupes redelivered webhooks: the + same key can only create one message (`e_idem_`). +2. **Context assembly.** The system prompt is composed as: optional operating + mode paragraph, then the identity — the router's operator override if set, + else the provider-declared prompt, else the harness's embedded default. + A caller `options.system_prompt` combines per + `options.system_prompt_strategy`: `override` replaces the identity + verbatim, `enrich` (default) appends to it. Then the `pre-generate` hooks + run and may mutate the assembled prompt (this is how fp injects its pipe + guidance). +3. **Generation.** The model streams; text lands in the session live. Each + `agent_trigger` the model emits goes through the internal + `harness::function::trigger`: the dispatch policy is checked + (`options.functions` is FAIL-CLOSED — absent means every call is denied + and the session is a plain chat loop), `pre-trigger` hooks may deny, hold, + or rewrite arguments, the function runs, `post-trigger` hooks may rewrite + the result, and the normalised result is appended to the transcript. +4. **Loop.** Steps repeat (each step is an internal `harness::turn` job on the + harness-turn queue, which is what makes the loop durable across restarts) + until the model produces a final reply, `options.max_turns` is reached, or + `harness::stop` cancels it. Transient provider errors resume the turn + (bounded by `max_transient_resumes`); output-contract violations retry + generation (bounded by `max_validation_retries`). +5. **Terminal status.** A turn ends `completed`, `cancelled`, or `failed` + (live states are `running` and `awaiting_functions`). The + `harness::turn-completed` event fires with the outcome. + +**Parking.** A pending call parks the turn without burning the loop: an +approval hold (approval-gate `pre-trigger` hook) or a hook hold. The internal +`harness::function::resolve` settles the pending call — delivering a result or +releasing the hold — and resumes the parked turn; a sweep expires stragglers +so an abandoned approval can never park a turn forever. Sub-agent spawns never +park: they are fire-and-forget (below). + +## Function surface + +Public, for consumers and agents: + +- `harness::send` — the entry point (message string sugar or a full + `AgentMessage`; `model` required; `provider`, `session_id`, + `idempotency_key`, `session` init metadata, and `options` optional). + Options: `system_prompt` + `system_prompt_strategy`, `mode`, `max_turns`, + `thinking_level`, `provider_options`, `output` contract, `functions` + allow/deny policy, `metadata` (tracing passthrough + `fs_scope`). +- `harness::spawn` — seed a child session fire-and-forget; returns + `{ child_session_id, child_turn_id }` immediately (details below). +- `harness::status` — recovery read for one session: `status`, `turn_id`, + `turn_count` / `max_turns`, `pending_function_calls`, `children`, `depth`, + `step`, `transient_resumes`, `validation_retries`, + `partial_result_available`, and the final `result` once terminal. +- `harness::stop` — request cancellation of the in-flight turn; cascades to + spawned children. +- `harness::react` — NEVER called directly (agents are denied); it exists as + a trigger TARGET that reshapes a subscribed event into a sub-agent spawn or + a function call (below). + +Internal (registered, visible in `engine::functions::info`, not for callers): +`harness::turn` (the queued loop step), `harness::function::trigger` / +`harness::function::resolve` (dispatch and pending-call settlement), +`harness::unqueue` / `harness::edit_queued` (console queued-message editing), +and `harness::filesystem::grant` / `grants` / `revoke` / `info` (per-session +filesystem roots). + +## Trigger types the harness publishes + +Session lifecycle events, bindable via `engine::register_trigger`: + +- `harness::turn-started` — a turn began executing its first loop step. +- `harness::turn-completed` — a turn reached a terminal status. Payload: + `{ session_id, turn_id, status, terminal, timestamp }` plus `result` (the + final text) when completed, `result_error` when the completion carries an + error, `reason`, and `parent` for sub-agent turns. **`terminal: false` + means the session still owns an armed wake** (a one-shot notify is + registered) — the run continues and a later turn carries the real outcome; + finalize only on `terminal: true`. +- `harness::message-queued` — a message was queued behind a running turn. + +Both turn-event types take the same binding config (unknown fields are +rejected): `{ session_id }` to follow ONE session, or `{ parent_session_id }` +to follow every child a session spawned. A join predecessor must filter on the +child's own `session_id` — a `parent_session_id` filter matches every sibling, +so the first completion would fill every join key. + +Hook points (operator-trusted policy siblings only — agents cannot bind +these): `harness::hook::pre-turn` (veto before any model spend), +`pre-generate` (extend the system prompt, append messages, or veto), +`post-generate` (observe), `pre-trigger` (deny / hold / rewrite arguments, +scoped by `functions` globs), `post-trigger` (rewrite results). Hooks run +synchronously in the turn's critical path, chained by ascending `priority`, +each under `timeout_ms` (default 5000) with `on_error` defaulting to +fail-closed for `pre-*` and fail-open for `post-*`. A worker that injects +prompt guidance binds `pre-generate` with `on_error: fail_open` so its +failure can never block generation. + +## How a session registers triggers, and what fires back + +`engine::register_trigger { trigger_type, function_id?, config, metadata?, +once? }` returns the subscription id (`engine::unregister_trigger { id }` +removes it). `once` defaults to one-shot for every type EXCEPT `cron`, which +is recurring — a cron used as a deadline needs `once: true` explicitly. Three +reaction forms, by what `function_id` and `metadata` carry: + +1. **Plain notify** — no `function_id`. The event is injected into the + REGISTERING session as a `[notification]` and wakes that agent's next + turn. This is the only way a session wakes ITSELF; anything that must wake + you (a turn-complete gate, a deadline) is a plain notify, never a react. +2. **React-spawn** — `function_id: harness::react` with + `metadata: { task, model?, session_id?, parent_session_id?, options?, + continue_on_error? }`. On fire, the harness spawns a sub-agent with your + `task` (the event JSON appended so it sees what fired). `session_id` + picks WHICH session the reaction runs in: omitted, it falls back to the + session that REGISTERED the trigger — the reaction delivers back into that + chat (right for a pipeline's final stage, wrong for any earlier stage). + Pin a fresh unique id for each stage that must run as its own child; a + reused id silently RESUMES the old session; never pin a fixed id on a + recurring trigger. `parent_session_id` only controls console nesting and + must be a REAL session id. `model` omitted runs the reaction on the + registering agent's model. Failed/cancelled turns and completions carrying + `result_error` skip the reaction unless `continue_on_error: true` (set it + on validator edges — an outcome checker must see failures too). +3. **React-call** — `metadata: { call: { function_id, payload, event_into? } }` + instead of `task`. The event is injected into the payload at `/event` + (or `event_into`) and the function dispatches directly: deterministic, + token-free, no session. The result is DISCARDED — a call can never wake + you. The target must be allowed by your policy, checked at registration. + +**Joins** (fan-in): register one `turn-completed` subscription per +predecessor, each filtered on that predecessor's own `session_id`, each +carrying the SAME `metadata.task` and the same +`join: { id, expect: [keys], key: }`. The harness accumulates each +predecessor's result durably and spawns the downstream exactly once — when +the last successful predecessor arrives, fed all results — then unregisters +the join's subscriptions. `join.rearm: true` keeps them registered for the +next complete set. A failed predecessor counts as arrived but blocks the +downstream unless its edge sets `continue_on_error: true`. + +Built-in loop breakers: a subscription never fires for the completion of the +sub-agent it itself spawned, reactive chains hard-cap at depth 8, and one +subscription is rate-limited to roughly 10 spawns per minute. + +## Sub-agent lifecycle + +`harness::spawn { task, session_id?, model?, options? }`: + +- **Fire-and-forget.** Returns `{ child_session_id, child_turn_id }` + immediately; the child's result is NEVER delivered back. It reaches the + world only through the state it writes and the `turn-completed` event its + finish fires — wire consumers BEFORE spawning. One automatic exception: a + terminally FAILED child delivers a `[child-failure]` message into the + parent session, so no failure listener is needed. +- **Naming.** Always pass `session_id`: a short readable slug plus a few + random characters (`fetch-headlines-b4k9`). Spawn creates the session if + absent — an id from an earlier run silently RESUMES that session, old + transcript included. +- **Policy.** An in-turn child inherits the parent's full dispatch policy; + `options.functions` may only NARROW it (spawns never escalate). If you + narrow, include everything the task must call — a child without + `state::set` finishes politely with its work stranded in its transcript. A + parentless spawn (direct call, CLI, trigger-fired react) has no parent to + mirror and starts from the read-only baseline; grant explicitly via + `options` (or react `metadata.options`). +- **Identity.** Children get the embedded sub-agent prompt, never the + router-served identity: one task, a state destination, nothing else. + `options.system_prompt` (+ `system_prompt_strategy`) is the escape hatch — + it is how a five-line special-purpose prompt from the prompt store becomes + a child's whole identity. +- **Shape.** A child is a LEAF by default; it inherited spawn/register + permission, so an explicit coordinator task MAY orchestrate its own + sub-tree, bounded by the spawn-depth limit. +- **Cancellation.** `harness::stop` on the parent cascades to children. + +## Reading this alongside the playbooks + +This doc is mechanics. For conduct — wire-before-spawn ordering, +self-contained child tasks, the `turn_complete` gate, validators, deadlines, +waves, and the armed-wake invariant — read `harness/orchestration` and +`harness/finishing`. For install and worker-authoring flows, read +`harness/building`. From b80ea934745865ff1723ea684bfbe6a7131ecf0c Mon Sep 17 00:00:00 2001 From: Rohit Ghumare Date: Fri, 24 Jul 2026 14:40:21 +0100 Subject: [PATCH 2/4] docs(harness): add dependency mesh and discovery sections to reference --- harness/skills/reference.md | 79 +++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/harness/skills/reference.md b/harness/skills/reference.md index bc063b2f4..3f8fd6a73 100644 --- a/harness/skills/reference.md +++ b/harness/skills/reference.md @@ -202,6 +202,85 @@ subscription is rate-limited to roughly 10 spawns per minute. sub-tree, bounded by the spawn-depth limit. - **Cancellation.** `harness::stop` on the parent cascades to children. +## The mesh around a turn: dependency workers + +The harness owns no storage and no model access; every capability is a +sibling worker reached over the bus. What it calls, per dependency: + +- **session-manager** — the transcript of record. `session::ensure` / + `session::create` on send, `session::append` for every message and + function result, `session::update-message` as the assistant message + streams, `session::set-status`, `session::messages` / `session::get` for + reads, and the `session::deleted` event (the harness cleans up turn state + when a session goes away). The console renders chats from session events, + not from the harness. +- **context-manager** — `context::assemble` fits the transcript to the + model's window before each generation (compaction lives here), and + `context::count-tokens` prices prompts and messages. +- **llm-router** — `router::chat` streams the generation over a channel; + `router::abort` cancels it; `router::models::get` / `list` / `supports` + resolve the model and its capabilities; `router::system_prompt::get` + serves the per-provider identity (operator override, then + provider-declared, then the harness falls back to its embedded default). + Behind the router sit the provider workers (`provider::::stream`), + which the harness never calls directly. +- **queue** — the harness defines a dedicated `harness-turn` queue at boot + (`queue::define`, required) and every loop step is a job on it; this is + what makes turns durable across harness restarts. +- **state** — `state::get` / `set` / `delete` / `list` hold the turn + records the loop resumes from and the `harness_idem` webhook-dedupe rows. + (Agents share the same state surface for their own run scopes.) +- **engine** — the bus itself: function dispatch, the trigger registry the + harness's event types and hook types register into, and the function + catalog that backs discovery. + +Optional policy siblings extend a turn without the harness knowing them by +name: **approval-gate** binds the `pre-trigger` hook to hold calls for human +approval; **fp**, **web**, and **memory** bind `pre-generate` to inject +usage guidance or recalled rules into the system prompt while they are +connected. Stop any of them and their contribution vanishes; the harness +keeps running. + +## Discovery and worker lifecycle: the surface an agent drives + +The harness executes calls, but finding and installing what to call is +engine + worker-manager + directory surface: + +- **Discovery (engine, always present):** `engine::functions::list` + (filter by `search` / `prefix` / `worker`) finds ids; + `engine::functions::info` returns the contract (request/response schema, + owning worker, bound triggers) — fetch it before a function's first use. + `engine::workers::list` / `engine::workers::info { name }` show who is + connected and what they register. `engine::triggers::list` / + `engine::triggers::info { id }` enumerate bindable trigger types and + their config schemas; `engine::registered-triggers::list` shows existing + bindings. +- **Worker lifecycle (worker-manager):** `worker::list` (installed + + running, including daemon-managed builtins — merge with + `engine::workers::list` by name to check liveness), `worker::add` + (install from `{ kind: "registry", name }` or an OCI source; new + function ids appear when the worker connects), `worker::start`, + `worker::update`, and `worker::stop` / `remove` / `clear` (the + destructive three require exactly `yes: true`, the boolean). +- **The directory worker** is the docs-and-catalog layer over all of it: + `directory::skills::index` / `list` / `get` serve the markdown skills + installed workers ship (this document included); + `directory::prompts::list` / `get` serve prompt templates; + `directory::registry::workers::list` / `info` proxy the public registry + so a capability can be judged BEFORE installing (`info` shows the same + api_reference shape you would get after install). With `auto_download` + on, the directory subscribes to the engine's worker-add event and pulls a + newly installed worker's skills automatically, and its + `directory::skills::on-change` / `directory::prompts::on-change` + triggers let other workers react when the on-disk doc set changes. + +The composed flow an agent actually runs: `engine::functions::list` finds +nothing fitting, `directory::registry::workers::list { search }` finds a +candidate, `registry::workers::info` judges it, `worker::add` installs it, +`engine::functions::list { prefix }` confirms the new ids, +`engine::functions::info` fetches the contract, and only then the call — +with the worker's skills already on disk for `directory::skills::get`. + ## Reading this alongside the playbooks This doc is mechanics. For conduct — wire-before-spawn ordering, From 3df38337c70e7570d44d74e374c5a0622990ceb2 Mon Sep 17 00:00:00 2001 From: Rohit Ghumare Date: Fri, 24 Jul 2026 15:01:19 +0100 Subject: [PATCH 3/4] docs(harness): semantics-only reference per no-duplication norm --- harness/skills/reference.md | 127 +++++++++--------------------------- 1 file changed, 30 insertions(+), 97 deletions(-) diff --git a/harness/skills/reference.md b/harness/skills/reference.md index 3f8fd6a73..2ba0fcff9 100644 --- a/harness/skills/reference.md +++ b/harness/skills/reference.md @@ -1,14 +1,13 @@ --- name: harness/reference description: >- - The harness mechanics reference: how a turn runs from harness::send to its - terminal status, every public function and what the internal ones do, the - trigger types the harness publishes (turn-started, turn-completed, - message-queued, the five hooks), how a session registers triggers and what - fires back into it, and the full sub-agent lifecycle from spawn to - [child-failure]. The conduct playbooks (harness/orchestration, - harness/finishing) say what an agent SHOULD do; this says what the machine - DOES. + What the API reference cannot tell you about the harness: how a turn runs + from send to terminal status, the trigger types it publishes and their + delivery semantics, the three reaction forms a session can register, and + the sub-agent lifecycle from fire-and-forget spawn to [child-failure]. + For the function surface read the API reference + (https://workers.iii.dev/workers/harness?tab=api). The conduct playbooks + say what an agent SHOULD do; this says what the machine DOES. --- # harness/reference @@ -65,34 +64,14 @@ releasing the hold — and resumes the parked turn; a sweep expires stragglers so an abandoned approval can never park a turn forever. Sub-agent spawns never park: they are fire-and-forget (below). -## Function surface +## The function surface lives in the API reference -Public, for consumers and agents: - -- `harness::send` — the entry point (message string sugar or a full - `AgentMessage`; `model` required; `provider`, `session_id`, - `idempotency_key`, `session` init metadata, and `options` optional). - Options: `system_prompt` + `system_prompt_strategy`, `mode`, `max_turns`, - `thinking_level`, `provider_options`, `output` contract, `functions` - allow/deny policy, `metadata` (tracing passthrough + `fs_scope`). -- `harness::spawn` — seed a child session fire-and-forget; returns - `{ child_session_id, child_turn_id }` immediately (details below). -- `harness::status` — recovery read for one session: `status`, `turn_id`, - `turn_count` / `max_turns`, `pending_function_calls`, `children`, `depth`, - `step`, `transient_resumes`, `validation_retries`, - `partial_result_available`, and the final `result` once terminal. -- `harness::stop` — request cancellation of the in-flight turn; cascades to - spawned children. -- `harness::react` — NEVER called directly (agents are denied); it exists as - a trigger TARGET that reshapes a subscribed event into a sub-agent spawn or - a function call (below). - -Internal (registered, visible in `engine::functions::info`, not for callers): -`harness::turn` (the queued loop step), `harness::function::trigger` / -`harness::function::resolve` (dispatch and pending-call settlement), -`harness::unqueue` / `harness::edit_queued` (console queued-message editing), -and `harness::filesystem::grant` / `grants` / `revoke` / `info` (per-session -filesystem roots). +Read contracts at https://workers.iii.dev/workers/harness?tab=api or live +with `engine::functions::info`. Two semantics the schemas cannot express: +`harness::react` is a trigger TARGET only (direct calls are denied - use +the id in `engine::register_trigger` without probing it first), and the +dispatch policy on `harness::send` `options.functions` is FAIL-CLOSED - +absent means every call is denied and the session is a plain chat loop. ## Trigger types the harness publishes @@ -207,29 +186,23 @@ subscription is rate-limited to roughly 10 spawns per minute. The harness owns no storage and no model access; every capability is a sibling worker reached over the bus. What it calls, per dependency: -- **session-manager** — the transcript of record. `session::ensure` / - `session::create` on send, `session::append` for every message and - function result, `session::update-message` as the assistant message - streams, `session::set-status`, `session::messages` / `session::get` for - reads, and the `session::deleted` event (the harness cleans up turn state - when a session goes away). The console renders chats from session events, - not from the harness. -- **context-manager** — `context::assemble` fits the transcript to the - model's window before each generation (compaction lives here), and - `context::count-tokens` prices prompts and messages. -- **llm-router** — `router::chat` streams the generation over a channel; - `router::abort` cancels it; `router::models::get` / `list` / `supports` - resolve the model and its capabilities; `router::system_prompt::get` - serves the per-provider identity (operator override, then - provider-declared, then the harness falls back to its embedded default). - Behind the router sit the provider workers (`provider::::stream`), - which the harness never calls directly. +- **session-manager** — the transcript of record: every message, function + result, and streaming assistant update lands there, and the harness cleans + up turn state when a session is deleted. The console renders chats from + session events, not from the harness. +- **context-manager** — fits the transcript to the model's window before + each generation (compaction lives here) and prices prompts and messages. +- **llm-router** — streams each generation over a channel, resolves models + and capabilities, and serves the per-provider identity prompt (operator + override, then provider-declared, then the harness falls back to its + embedded default). Provider workers sit behind the router and are never + called directly. See the llm-router skill for its own semantics. - **queue** — the harness defines a dedicated `harness-turn` queue at boot - (`queue::define`, required) and every loop step is a job on it; this is - what makes turns durable across harness restarts. -- **state** — `state::get` / `set` / `delete` / `list` hold the turn - records the loop resumes from and the `harness_idem` webhook-dedupe rows. - (Agents share the same state surface for their own run scopes.) + (required) and every loop step is a job on it; this is what makes turns + durable across harness restarts. +- **state** — holds the turn records the loop resumes from and the + webhook-dedupe rows. Agents share the same state surface for their own + run scopes. - **engine** — the bus itself: function dispatch, the trigger registry the harness's event types and hook types register into, and the function catalog that backs discovery. @@ -241,46 +214,6 @@ usage guidance or recalled rules into the system prompt while they are connected. Stop any of them and their contribution vanishes; the harness keeps running. -## Discovery and worker lifecycle: the surface an agent drives - -The harness executes calls, but finding and installing what to call is -engine + worker-manager + directory surface: - -- **Discovery (engine, always present):** `engine::functions::list` - (filter by `search` / `prefix` / `worker`) finds ids; - `engine::functions::info` returns the contract (request/response schema, - owning worker, bound triggers) — fetch it before a function's first use. - `engine::workers::list` / `engine::workers::info { name }` show who is - connected and what they register. `engine::triggers::list` / - `engine::triggers::info { id }` enumerate bindable trigger types and - their config schemas; `engine::registered-triggers::list` shows existing - bindings. -- **Worker lifecycle (worker-manager):** `worker::list` (installed + - running, including daemon-managed builtins — merge with - `engine::workers::list` by name to check liveness), `worker::add` - (install from `{ kind: "registry", name }` or an OCI source; new - function ids appear when the worker connects), `worker::start`, - `worker::update`, and `worker::stop` / `remove` / `clear` (the - destructive three require exactly `yes: true`, the boolean). -- **The directory worker** is the docs-and-catalog layer over all of it: - `directory::skills::index` / `list` / `get` serve the markdown skills - installed workers ship (this document included); - `directory::prompts::list` / `get` serve prompt templates; - `directory::registry::workers::list` / `info` proxy the public registry - so a capability can be judged BEFORE installing (`info` shows the same - api_reference shape you would get after install). With `auto_download` - on, the directory subscribes to the engine's worker-add event and pulls a - newly installed worker's skills automatically, and its - `directory::skills::on-change` / `directory::prompts::on-change` - triggers let other workers react when the on-disk doc set changes. - -The composed flow an agent actually runs: `engine::functions::list` finds -nothing fitting, `directory::registry::workers::list { search }` finds a -candidate, `registry::workers::info` judges it, `worker::add` installs it, -`engine::functions::list { prefix }` confirms the new ids, -`engine::functions::info` fetches the contract, and only then the call — -with the worker's skills already on disk for `directory::skills::get`. - ## Reading this alongside the playbooks This doc is mechanics. For conduct — wire-before-spawn ordering, From 62a6e3f5b1585b1e789e236f2ea30aa1b2ca107c Mon Sep 17 00:00:00 2001 From: Rohit Ghumare Date: Fri, 24 Jul 2026 16:01:47 +0100 Subject: [PATCH 4/4] docs(harness): point at the markdown API reference URL --- harness/skills/reference.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/harness/skills/reference.md b/harness/skills/reference.md index 2ba0fcff9..e000ce66e 100644 --- a/harness/skills/reference.md +++ b/harness/skills/reference.md @@ -6,7 +6,7 @@ description: >- delivery semantics, the three reaction forms a session can register, and the sub-agent lifecycle from fire-and-forget spawn to [child-failure]. For the function surface read the API reference - (https://workers.iii.dev/workers/harness?tab=api). The conduct playbooks + (https://workers.iii.dev/workers/harness.md). The conduct playbooks say what an agent SHOULD do; this says what the machine DOES. --- @@ -66,7 +66,7 @@ park: they are fire-and-forget (below). ## The function surface lives in the API reference -Read contracts at https://workers.iii.dev/workers/harness?tab=api or live +Read contracts at https://workers.iii.dev/workers/harness.md or live with `engine::functions::info`. Two semantics the schemas cannot express: `harness::react` is a trigger TARGET only (direct calls are denied - use the id in `engine::register_trigger` without probing it first), and the