# Cloudflare-native Runtime (/docs/architecture/cloudflare-native-runtime)
This page describes `son-of-anton-review` on `feat/cloudflare-native`, the deployed-engine lineage from the handoff. It does not describe the **unmerged** `feat/greptile-operator-parity` API Worker. In particular, the native fetch handler does not serve `/operator/api/*`.
## Worker and bindings [#worker-and-bindings]
The main configuration is `cloudflare-native/wrangler.jsonc`. The key broker has a separate configuration, `cloudflare-native/wrangler.key-broker.jsonc`.
| Binding | Configured resource | Purpose |
| ------------------ | --------------------------------------------------------- | -------------------------------- |
| `OMP` | `OmpContainer`, `standard-4`, maximum 20 instances | Review orchestration container |
| `Sandbox` | `Sandbox`, `standard-3`, maximum 10 instances | Separate validation container |
| `REVIEW_QUEUE` | `gilf-review-intents` | Review intents |
| `DEAD_LETTER` | `gilf-review-dlq` | Consumer dead-letter destination |
| `DB` | D1 `son-of-anton-review-state` | Run state and traces |
| `DEDUPE` | KV | Webhook delivery-ID markers |
| `WEBHOOK_PAYLOADS` | R2 `son-of-anton-webhook-payloads` | Oversized webhook payloads |
| `CRON_ARTIFACTS` | R2 `son-of-anton-cron-artifacts` | Cron artifacts |
| `KEY_BROKER` | Service `son-of-anton-key-broker`, entrypoint `KeyBroker` | Token-minting RPC |
These are checked-in resource names and limits, not a probe of your account. Source: engine `cloudflare-native/wrangler.jsonc:17-105`.
## Public ingress [#public-ingress]
`GET /health` returns an ingress health response. `POST /github/webhooks` requires the GitHub delivery, event and signature headers, verifies HMAC against `GITHUB_WEBHOOK_SECRET`, parses JSON, then checks KV.
The delivery marker uses `DEDUPE_TTL_SECONDS`, default `604800` seconds. Accepted but non-actionable events are recorded as ignored. Actionable deliveries are recorded before enqueue. Record or enqueue failures attempt to delete the marker and return HTTP 500; deletion is best-effort. A 500 permits redelivery, but is not a guarantee that GitHub will redeliver automatically.
Payload offload begins above **96 × 1024 encoded bytes**, not characters, when `WEBHOOK_PAYLOADS` exists. Without that binding, the code attempts inline enqueue; an oversized queue message can then fail. The consumer resolves `payloadRef` before container dispatch.
Sources: engine `cloudflare-native/src/ingress.js:139-171,264-346`; `cloudflare-native/src/consumer.js:125-175,344-356`.
## Dispatch and container lifecycle [#dispatch-and-container-lifecycle]
The queue consumer configuration is batch size `1`, batch timeout `5` seconds and `max_retries: 8`. The consumer separately reads `MAX_ATTEMPTS`, default `8`, for its own error path. Do not treat these as the same counter.
The consumer claims a D1 dispatch lease, default `LEASE_MS = 1800000`, and renews it every `min(30000, floor(LEASE_MS / 3))` milliseconds, with a minimum of one millisecond. A competing delivery retries while another holder is running; a succeeded holder allows acknowledgement. The request to the container includes the lease holder and action.
`OmpContainer` listens on port `8080` and has `sleepAfter = '15m'`. Its internal server accepts `POST /review`; a concurrent request receives `503 container_busy`, and a stopping process returns `503 container_stopping`. `GET /health` reports the current phase. The process logs its active phase every 30 seconds. That logging is not proof of a platform-level keepalive guarantee.
A successful review response is sent only after the final state flush succeeds. On termination, the server aborts the job, attempts a checkpoint flush and schedules process exit after 10 seconds. Checkpoint failures are logged. This is best-effort recovery, not a guarantee that all in-flight state survives eviction.
Sources: engine `cloudflare-native/wrangler.jsonc:62-69`; `cloudflare-native/src/consumer.js:17,308-419,450-465`; `cloudflare-native/src/omp-container.js:14-46`; `cloudflare-native/container/entrypoint.mjs:391-486`.
## State and token transport [#state-and-token-transport]
The container does not receive D1 or key-broker service bindings. HTTP requests to `state.internal` and `broker.internal` are intercepted by the outbound Worker; `sandbox.internal` handles validation. The binding side performs D1 operations and broker RPC.
The store bridge hydrates an in-memory run, serializes flushes, and advances event cursors only after acknowledgement. Before a native publish claim it flushes the analysis state, then awaits the D1 publication operation. Trace rows ride the same HTTP flush request, but state and traces are written in separate steps. They are not one atomic transaction.
Recovery skips analysis only when the hydrated run already has `analysis.status = succeeded`. A saved phase label alone does not resume a model call or validation process from its interruption point.
Sources: engine `cloudflare-native/src/omp-container.js:62-84`; `cloudflare-native/src/state-transport.js:558-599,615-679`; `cloudflare-native/src/d1-store-adapter.js:263-354`; `cloudflare-native/container/entrypoint.mjs:49-68,95-148`.
## Effective configuration [#effective-configuration]
| Setting | Code fallback | Checked-in Worker value |
| ----------------------------------- | -------------------------------------------------------------------- | -------------------------------- |
| `GILF_PUBLISH_MODE` | `shadow` | `live` |
| `GILF_VALIDATION_EXECUTOR` | Container projection: `managed-cf-sandbox` | `managed-cf-sandbox` |
| Provider | `GILF_MODEL_PROVIDER`, then `GILF_CODEX_PROVIDER`, then `openrouter` | `GILF_CODEX_PROVIDER=openrouter` |
| `GILF_VALIDATION_SHADOW_EXECUTOR` | No selector forwarded when unset | `managed-e2b` |
| `GILF_VALIDATION_SHADOW_TIMEOUT_MS` | `600000` in validation code | `600000` |
| `GILF_E2B_BUDGET_USD` | `50` | `50` |
Only nonblank string values in `FORWARDED_KEYS` enter the container, plus the executor and `GILF_CF_SANDBOX_ENABLED=1`. The App private key and the V5 activation flags are absent. A Worker environment setting outside this projection does not configure the runner.
The E2B gate uses all recorded `validation.e2b` cost, not a monthly invoice. At or above the ceiling it removes only the E2B shadow selector. Missing D1 or a failed spend read leaves the lane enabled. Concurrent or unrecorded work can exceed the estimate ceiling. See [Test Lab](../operator/test-lab).
Sources: engine `cloudflare-native/src/container-env.js:15-141`; `cloudflare-native/src/publish-mode.js:26-35`; `cloudflare-native/container/entrypoint.mjs:319`; `src/validation-executor.js:378-386`; `cloudflare-native/wrangler.jsonc:132-161`.
## Cron schedules [#cron-schedules]
| UTC expression | Job |
| ------------------------------- | --------------------------------------------------- |
| `2-59/15 * * * *` | `pr-agent-loop` |
| `3-59/15 * * * *` | `watchdog` |
| `* * * * *` | `command-bridge` |
| `30 22 * * *` and `30 23 * * *` | `nightly-audit`, gated to its configured local time |
The checked-in configuration uses the paired UTC schedules for 23:30 Europe/London. `prime-watch` is explicitly unscheduled. The native watchdog is invoked with alert delivery disabled; recorded failures do not imply an external notification was sent.
Sources: engine `cloudflare-native/wrangler.jsonc:107-130`; `cloudflare-native/src/cron.js:53-88,175-176,260-263`.
Provisioning instructions: [Cloudflare quickstart](../getting-started/quickstart-cloudflare). Durability limits: [Queue and durability](./queue-and-durability).
# Key Broker (/docs/architecture/key-broker)
This page describes the broker in the deployed-engine lineage, `son-of-anton-review` on `feat/cloudflare-native`. It is not an endpoint in the **unmerged** `feat/greptile-operator-parity` operator API.
The native configuration keeps `GITHUB_APP_PRIVATE_KEY` in a separate Worker. The review container receives scoped installation tokens instead of the App private key. This removes the key from the container environment; it does **not** make a compromised container incapable of requesting broader tokens.
## Deployment boundary [#deployment-boundary]
`cloudflare-native/wrangler.key-broker.jsonc` selects `src/key-broker.js`, sets `workers_dev: false`, and defines no public route. Its default fetch handler returns 404. The main Worker binds service `son-of-anton-key-broker`, entrypoint `KeyBroker`, as `KEY_BROKER`.
The broker reads `GITHUB_APP_ID` and `GITHUB_APP_PRIVATE_KEY` from its own environment. The main container environment allowlist does not forward the private key. Keep that separation when provisioning; it is a property of this configuration, not a claim about every self-host deployment.
Sources: engine `cloudflare-native/wrangler.key-broker.jsonc:5-26`; `cloudflare-native/wrangler.jsonc:102-105`; `cloudflare-native/src/key-broker.js:13-33`; `cloudflare-native/src/container-env.js:15-85`.
## RPC contract [#rpc-contract]
The service method is `mintToken({ installationId, repositories, permissions })`. Its successful result contains `token` and `scope`.
| Input | Actual broker behavior |
| ------------------------------- | --------------------------------------------------------------------------------------- |
| `installationId` | Required to be non-null by `shapeTokenRequest` |
| `permissions` omitted or null | Defaults to `contents: read`, `pull_requests: write`, `checks: write`, `metadata: read` |
| `permissions` supplied | Passed through, not checked against a purpose allowlist |
| Nonempty `repositories` array | Passed through to scope the GitHub token request |
| Empty or omitted `repositories` | Omitted from the GitHub request, so the broker does not enforce repository restriction |
GitHub remains responsible for accepting or refusing the requested installation, repositories and permissions. The broker does not implement the design document's `publish`, `clone`, `reconcile`, or `audit` purpose authorization, nor an explicit denial of `contents:write` or `administration`.
Source: engine `cloudflare-native/src/key-broker-core.js:15-55`.
## Container transport and normal callers [#container-transport-and-normal-callers]
The container sends `POST http://broker.internal/mint`. Outbound interception invokes `handleBrokerRequest`, which forwards the caller's installation, repositories and permissions directly to `KEY_BROKER.mintToken`. This handler does not bind the requested scope to the active review lease.
The normal container client is stricter than the broker:
* `mintPublishToken` requires a broker, installation ID, repository scope and nonempty returned token.
* Clone setup requests `contents:read` and `metadata:read` for the target repository, including in shadow publish mode.
* Check publication requests `checks:write`; review/comment publication requests `pull_requests:write`, scoped to the repository.
* The broker-backed GitHub client mints for each request. Reads use up to three fetch attempts; writes use one attempt because a lost response is ambiguous.
These are caller conventions, not a security boundary against a caller that bypasses the helper. A compromised container with outbound broker access is not limited to the read token already present in its environment. The model-provider key is also present in the orchestration container.
Sources: engine `cloudflare-native/src/state-transport.js:583-599`; `cloudflare-native/src/broker-github-client.js:26-99`; `cloudflare-native/container/entrypoint.mjs:97-123`; `src/github-publisher.js:13-78`; `cloudflare-native/src/container-env.js:49-69`.
## Lifetime, caching and revocation [#lifetime-caching-and-revocation]
The broker does not set a custom token expiry. `GitHubAppClient` uses the expiry returned by GitHub. Its instance cache reuses a scope-matched token until 60 seconds before expiry.
However, **each broker RPC constructs a new `GitHubAppClient`**. The cache therefore does not persist across broker RPC calls. Do not use the client's cache implementation as evidence of broker-wide reuse.
Neither `key-broker.js` nor the internal broker HTTP handler exposes a revoke operation. There is no automatic post-use revocation in this path. Clearing the clone credential environment in the container's `finally` block restores local environment state; it does not invalidate the GitHub token.
Sources: engine `cloudflare-native/src/key-broker-core.js:41-55`; `src/github-app.js:75-111`; `cloudflare-native/src/key-broker.js:13-33`; `cloudflare-native/src/state-transport.js:583-599`; `cloudflare-native/container/entrypoint.mjs:178-183`.
## Node self-host differs [#node-self-host-differs]
`server.mjs` and `worker.mjs` can load `GITHUB_APP_PRIVATE_KEY` or read `GITHUB_APP_PRIVATE_KEY_PATH`, then construct `GitHubAppClient` in-process. The native broker isolation does not apply to those branches of the Node runtime.
`GILF_BROKER_URL`, `GILF_BROKER_TOKEN` and `GILF_BROKER_ALLOW_LOCAL_KEY` are design-only names, not configuration read by these entrypoints or the audited engine/broker modules. Do not set them expecting a broker cutover. The earlier unverified claims about these flags, purpose enforcement and revocation are replaced here with their implemented status.
Sources: engine `server.mjs:52-59`; `worker.mjs:35-40`; `cloudflare-native/src/key-broker-core.js:26-55`. A source search of engine `src/`, native `src/`, `server.mjs` and `worker.mjs` found no reads of those three variables or `/token/revoke`.
See [Security model](./security-model) and [Publish modes](../code-review/publish-modes). Token isolation is one control, not a certification or a complete hostile-code containment guarantee.
# Observability and Cost (/docs/architecture/observability-and-cost)
Tracing described here belongs to `son-of-anton-review` on `feat/cloudflare-native`, the deployed-engine lineage. The Cloudflare trace-reading API belongs to **unmerged** `feat/greptile-operator-parity`. The Node dashboard has a different trace contract. A working native engine does not imply that either dashboard API is deployed alongside it.
## Traces are observations, not a complete ledger of attempts [#traces-are-observations-not-a-complete-ledger-of-attempts]
`src/trace-emitter.js` creates an in-memory trace with a generated trace ID and spans. The general tracing switch is on unless `GILF_TRACING` is exactly `0` or `false`. The native container explicitly creates its injected tracer with `enabled: true`, and `GILF_TRACING` is not forwarded by the container allowlist. Setting that variable only on the Worker therefore does not disable native tracing.
Tracer methods guard their own errors and can disable further tracing. A flush is attempted once per tracer; a synchronous sink error is swallowed by the guard. This is best-effort telemetry, not proof that every accepted webhook or failed attempt produced a durable trace. A retry can create another trace ID for the same review key.
On the native path, the entrypoint owns the trace through publication. A later failure to flush the buffered trace to D1 can fail the review response even though the in-memory tracer itself does not throw.
Sources: engine `src/trace-emitter.js:9-10,60-82,88-97,261-277`; `cloudflare-native/container/entrypoint.mjs:126-176,378-385,421-450`; `cloudflare-native/src/container-env.js:15-85`.
## Phase spans [#phase-spans]
| Span | Meaning when emitted |
| -------------------------------------------------------- | ---------------------------------- |
| `agent.workflow` | Root workflow |
| `repo.prepare` | Repository preparation |
| `context.load` | Preflight context assembly |
| `context.graph` | Graph build within preflight |
| `chat.primary` | Primary semantic model call |
| `validation.cf-sandbox`, `validation.e2b` | Validation lanes named by provider |
| `publish` | Native service publication stage |
| `http.model-price` | OpenRouter price-check phase |
| `hypothesis.primary`, `hypothesis.shadow`, `chat.shadow` | Optional runner paths |
There is no fixed seven-span contract for every attempt. Early failures, policy skips, durable-analysis recovery and optional execution paths produce different span sets. A shadow-lane error does not necessarily make the trace's final status `error`: the entrypoint can explicitly end a successfully completed review as `ok` while an individual shadow span failed.
Spans support timing, status, error, model, token counts, cost, attributes, events and optional input/output. On trace failure, unfinished spans become errors and the cause is copied to trace metadata. Missing usage and cost remain null unless some span reports a value; totals sum only reported values.
Sources: engine `src/codex-review-runner.js:34-60,1112-1149`; `src/trace-emitter.js:18-39,111-173,186-229`; `cloudflare-native/container/entrypoint.mjs:95-155`.
## Storage and disclosure [#storage-and-disclosure]
Native traces map to D1 `review_traces` and `review_trace_spans`. The buffer replaces a snapshot with the same trace ID; the bridge restores drained snapshots when a flush fails. State is written before traces in the HTTP handler, so they are not an atomic snapshot across both operations.
The native row mapper can persist `input_json`, `output_json`, attributes, events and errors. It does not implement universal redaction. The parity API omits input/output fields from its returned DTO, and the parity UI has only Attributes and Events tabs. That omission is **not** proof that raw input/output can never exist in D1 or that arbitrary attributes are sanitized.
The Node trace store is selected through `GILF_DB_PATH`; without a trace store, the local trace-list API returns an empty list. It also returns an empty list on a caught trace read error, so empty results alone cannot establish absence of reviews.
Sources: engine `cloudflare-native/src/trace-buffer.js:32-54,62-118`; `cloudflare-native/src/d1-store-adapter.js:303-350`; `cloudflare-native/src/state-transport.js:558-564`; `server.mjs:27-35`; `src/operator-dashboard.js:291-313`; parity `cloudflare/src/review-trace-queries.js:14-64`; UI `src/components/TraceObservability.tsx:321-388`.
## Model cost [#model-cost]
`estimateProviderCostUsd` uses input and output token counts and USD-per-million-token rates. A model-specific `GILF_MODEL_PRICES` entry wins over the built-in provider table. OpenAI and Anthropic have static code tables; OpenRouter and Codex tables are empty. These are configured estimates, not live price verification or invoices.
The checked-in native configuration supplies this model-price entry:
```json
{"z-ai/glm-5.3-flash":{"input":0.075,"output":0.25}}
```
No usage or no resolved price returns null. With a price present, missing/non-numeric token fields or rate components are coerced to zero by the arithmetic. A displayed zero therefore deserves inspection of the inputs; it is not automatically proof of a free call.
Sources: engine `src/model-provider.js:44-93`; `cloudflare-native/wrangler.jsonc:154-161`.
## Validation cost [#validation-cost]
The code uses `costModel: estimate:wall-clock`, calculated as seconds multiplied by CPU, memory and disk rates. It does not query provider billing.
| Executor | CPU USD/vCPU-second | Memory USD/GiB-second | Disk USD/GB-second | Fallback shape |
| -------------------- | ------------------- | --------------------- | ------------------ | ----------------------------------- |
| `managed-cf-sandbox` | `0.000020` | `0.0000025` | `0.00000007` | 2 vCPU, 8192 MB, 16 GB disk |
| `managed-e2b` | `0.000014` | `0.0000045` | `0` | 2 vCPU, 512 MB, 0 disk contribution |
`GILF_VALIDATION_COST_RATES` merges partial executor rate overrides over the built-ins; malformed JSON falls back. Actual reported CPU/memory values can replace the fallback shape. The estimate excludes unrelated orchestration, Worker, database, storage and network charges.
Missing or invalid elapsed input normally prevents a rate estimate, but explicit null values undergo JavaScript numeric coercion. `validationSpanAttributes` can likewise convert explicit null `costUsd` or `seconds` to zero. Do not promise that all unknown values are perfectly represented as null.
Source: engine `src/validation-executor.js:146-245`.
## Reading averages and budgets [#reading-averages-and-budgets]
The **Node-only** `/operator/api/trace-stats` computes means over traces reporting each metric and excludes root spans from phase/type breakdowns. Nested and concurrent spans still overlap, so summed phase duration need not equal wall-clock duration.
The parity Worker instead exposes `/operator/api/traces` and `/operator/api/traces/:id`. Its dashboard computes PR averages from up to five recent trace details, not all historical runs.
The E2B budget gate sums all recorded `validation.e2b` costs, without date, repository or role filtering. Missing costs and concurrent work make it unsuitable as a hard billing cap. The parity Test Lab must read the same data and budget setting for its display to agree with the native gate.
Sources: engine `src/operator-dashboard.js:326-402`; parity `cloudflare/src/review-state-api.js:876-899`; UI `src/lib/api.ts:149-158`; engine `cloudflare-native/src/container-env.js:100-141`.
The implemented observability path is the custom tracer and stores. No Langfuse or OpenTelemetry exporter was found in the audited engine source and root package manifest. See [Cost and traces](../operator/cost-and-traces) for the operator workflow.
# Architecture Overview (/docs/architecture/overview)
## Branch and runtime boundary [#branch-and-runtime-boundary]
The deployed-engine lineage is `son-of-anton-review` on `feat/cloudflare-native`. The extended Cloudflare operator API is in `son-of-anton-operator-parity` on **unmerged** `feat/greptile-operator-parity`. The React parity dashboard is a third tree, `anton-ui-trace-parity`. These are not one deployable source tree.
The native Worker's public handler serves `GET /health` and `POST /github/webhooks`, not the parity `/operator/api/*` routes. The engine also contains a separate Node dashboard implementation in `src/operator-dashboard.js`; its GET-only API is not mounted by the native Worker.
Sources: engine `cloudflare-native/src/ingress.js:264-273`; parity `cloudflare/src/worker.js:93-157`; engine `src/operator-dashboard.js:609-690`. Deployment lineage is the supplied handoff, not a live deployment probe.
## Native review flow [#native-review-flow]
1. Ingress verifies the webhook signature, checks the **delivery ID** in KV, records the delivery and queues an intent. This is not review-key deduplication at ingress. KV is eventually consistent; duplicate enqueues remain possible.
2. The consumer hydrates oversized payloads from R2 and resolves a missing head SHA. Ordinary issue-comment chatter is acknowledged without a review dispatch.
3. A D1 compare-and-set lease selects a dispatcher for the review key and publish mode. The container is addressed by review key, not guaranteed to be a newly created process on every attempt.
4. The container hydrates run state, constructs `CodexReviewRunner` and `ReviewService`, and invokes analysis unless durable analysis already succeeded. The runner checks out the pinned head SHA.
5. `ReviewService` stores the analysis result and publishes through the selected publisher. Native publication claims go through an awaited D1 transport before GitHub writes. Model calls and publication are distinct failure stages.
Sources: engine `cloudflare-native/src/ingress.js:295-346`, `cloudflare-native/src/consumer.js:331-419`, `cloudflare-native/container/entrypoint.mjs:82-157,234-313,345-385`, `src/codex-review-runner.js:2843-2844`, `src/review-service.js:342-464`.
## Identity and publication [#identity-and-publication]
A normal review key is `owner/repo#prNumber@headSha`. A manual comment can add `:manual-` so a new command on an unchanged head is a distinct request. Redelivery of that same comment retains its identity. The dispatch action is `container-dispatch` for live publication and `container-dispatch:shadow` for shadow publication.
This reduces duplicate work; it is not a blanket exactly-once guarantee. A GitHub write and a D1 receipt cannot be committed atomically. An uncertain native publication is blocked pending reconciliation, rather than automatically retried after a timeout.
Check runs are created or patched when a check-run ID exists. PR reviews are **POSTed**, with an `anton-review` marker and the run's commit SHA; they are not updated in place. Receipt comments are a separate action. See [Queue and durability](./queue-and-durability).
Sources: engine `cloudflare-native/src/consumer.js:33-37,287-304`; `cloudflare-native/src/state-transport.js:490-524`; `src/github-publisher.js:22-79`; `src/review-service.js:181-191`.
## Configuration versus optional features [#configuration-versus-optional-features]
OpenRouter is the recommended provider. The native container selects `GILF_MODEL_PROVIDER`, then `GILF_CODEX_PROVIDER`, then `openrouter`. Its checked-in Worker configuration selects OpenRouter and a model through `GILF_MODEL`. OpenAI, Anthropic and Codex paths are alternatives, not evidence that each is provisioned in a deployment.
Hypothesis-primary, the LLM planner and model inversion are optional runner paths, not mandatory stages of every native review. The runner's V5 configuration defaults these paths off, and their activation variables are absent from `container-env.js`'s forwarding allowlist. Setting them only on the Worker does not enable them in the container. Shadow **validation** has separate configuration and is forwarded.
Sources: engine `cloudflare-native/container/entrypoint.mjs:314-324`; `cloudflare-native/wrangler.jsonc:132-161`; `src/codex-review-runner.js:2303-2312`; `cloudflare-native/src/container-env.js:15-85`.
## Node self-host is a separate deployment shape [#node-self-host-is-a-separate-deployment-shape]
`server.mjs` receives requests and `worker.mjs` drains jobs. They use the same review-service module, but have different runtime wiring from the native container.
Worker queue selection is, in order: both `GILF_QUEUE_URL` and `GILF_QUEUE_TOKEN`, then SQLite at `GILF_QUEUE_DB_PATH`, then SQLite at `GILF_DB_PATH`, then an in-memory queue. Omitting persistence does **not** default to durable SQLite. Node processes can load the App private key locally; the native broker separation does not automatically apply to them.
Sources: engine `src/worker-runner.js:29-39`; `server.mjs:27-59`; `worker.mjs:20-40`.
## Next pages [#next-pages]
* [Cloudflare-native runtime](./cloudflare-native-runtime): bindings, container transport and cron schedules.
* [Key broker](./key-broker): implemented token scoping and its remaining trust boundary.
* [Security model](./security-model): isolation controls, credentials and authorization limits.
* [Observability and cost](./observability-and-cost): traces, missing data and estimates.
* [Operator API](../operator/operator-api): the unmerged API contract, not native ingress routes.
# Queue, Idempotency and Durability (/docs/architecture/queue-and-durability)
This page describes the deployed-engine lineage, `son-of-anton-review` on `feat/cloudflare-native`. The Durable Object queue in `cloudflare/` is a different backend from the native Cloudflare Queue. Queue-repair APIs in **unmerged** `feat/greptile-operator-parity` must not be presented as native queue administration.
## Delivery is not exactly once [#delivery-is-not-exactly-once]
The native configuration consumes `gilf-review-intents` with `max_batch_size: 1`, `max_batch_timeout: 5` and `max_retries: 8`; the configured dead-letter queue is `gilf-review-dlq`.
Ingress uses KV key `delivery:`, with `DEDUPE_TTL_SECONDS` defaulting to seven days. KV is eventually consistent, so two deliveries can both pass the dedupe check. A D1 record is written before enqueue. A recording or enqueue failure attempts to remove the KV marker and returns 500. Those operations are not a cross-service transaction, and dedupe rollback itself is best-effort.
Do not describe this as exactly one review per head. Distinct manual commands intentionally have distinct identities, and remote publication can have an uncertain outcome.
Sources: engine `cloudflare-native/wrangler.jsonc:56-69`; `cloudflare-native/src/ingress.js:295-346`; `cloudflare-native/src/consumer.js:287-304`.
## Review keys and dispatch leases [#review-keys-and-dispatch-leases]
The ordinary key is `owner/repo#prNumber@headSha`. Manual comment requests can append `:manual-`. A supplied `reviewKey` takes precedence. Missing-head intents are resolved before the dispatch claim; this avoids leaving normal comment reviews permanently keyed at `@pending`.
The dispatch lease is in `publish_leases`, not `publish_ledger`. Its action is `container-dispatch` or `container-dispatch:shadow`. Claiming is a conditional update that refuses succeeded leases and runs already marked `superseded` or `abandoned`.
`LEASE_MS` defaults to 30 minutes. A running lease becomes claimable when its recorded start/renewal time is older than the cutoff. The active consumer renews ownership and aborts dispatch if renewal fails. Another delivery retries while a holder is running; only a succeeded holder lets the duplicate be acknowledged.
The fencing migration checks a matching **running action and holder** for snapshots carrying `dispatchLease`. It does not itself compare the timestamp to a wall-clock expiry. A replaced holder cannot write through that fence; simply passing the time cutoff is not the same event as replacement.
Sources: engine `cloudflare-native/src/consumer.js:17,33-105,287-304,344-419`; `cloudflare-native/migrations/003-runtime-lease-fencing.sql:5-29`.
## Publication claims do not expire into retries [#publication-claims-do-not-expire-into-retries]
Native publication uses an awaited transport backed by `publish_ledger`:
1. The bridge flushes run state before claiming a publication action.
2. A claim inserts a `running` action under the active dispatch identity.
3. A duplicate succeeded action returns its stored result without a new claim.
4. A duplicate non-succeeded action throws: remote receipt reconciliation is required.
5. A failed request records `uncertain`, not proof that GitHub did nothing.
6. Completion may record the remote result even after the dispatch lease changed, but only for the action's original claimant.
Unlike the dispatch lease, a native `running` or `uncertain` **publication claim has no expiry-based resend path**. Do not copy the older SQLite publication-lease behavior into the native runbook.
The GitHub client attempts a mutation once. Check creation carries `external_id: reviewKey`; PR reviews carry ``; receipt comments carry ``. These are evidence for reconciliation. They are not evidence that every ambiguous write is automatically reconciled by the normal retry path. Investigate the remote object and ledger before authorizing another mutation.
Sources: engine `cloudflare-native/src/d1-store-adapter.js:263-282`; `cloudflare-native/src/state-transport.js:490-524`; `cloudflare-native/src/broker-github-client.js:78-88`; `src/github-publisher.js:22-65`; `src/review-service.js:181-191`.
## What a checkpoint preserves [#what-a-checkpoint-preserves]
A container flush persists run state and buffered traces over the outbound Worker. The bridge restores drained traces after a failed flush and only advances its event cursor after acknowledgement. State and traces are separate writes within that request, so partial persistence remains possible.
The server waits for a successful final flush before returning success. During termination it attempts to record a phase marker and flush, with a 10-second exit timer. Failures are logged, not guaranteed recoverable.
On another attempt, durable `analysis.status = succeeded` lets the container skip the model and resume stored publication. A phase marker alone does not restore an interrupted model call or sandbox process. Work before a successful checkpoint can repeat.
Sources: engine `cloudflare-native/src/d1-store-adapter.js:303-354`; `cloudflare-native/src/state-transport.js:558-579`; `cloudflare-native/container/entrypoint.mjs:49-68,95-148,421-477`; `src/review-service.js:350-364`.
## Dead letters and watchdog counts [#dead-letters-and-watchdog-counts]
The consumer handles unroutable messages and errors reaching `MAX_ATTEMPTS` (default `8`) by calling its dead-letter helper and acknowledging the original message. `MAX_ATTEMPTS` uses the message's delivery-attempt count; it is separate from Wrangler's retry setting.
The helper first attempts the DLQ send, then records whether that send succeeded in `cron_dead_letters`. Both the send failure and a ledger-write failure are caught and logged. **The original message is still acknowledged.** A DLQ send is therefore not guaranteed, and a missing audit row can make the ledger undercount.
The watchdog projection reads unacknowledged ledger rows only when `CRON_DEAD_LETTER_LEDGER=enabled`; otherwise the count is unobservable, not zero. `acknowledged_at IS NULL` selects outstanding rows. No acknowledgement CLI was found in the audited engine `bin/`, `scripts/`, `src/` or native `src/`; do not invent a purge or acknowledgement command. Recovery must preserve evidence and distinguish ledger acknowledgement from physical queue removal.
Sources: engine `cloudflare-native/src/consumer.js:256-284,324-328,450-465`; `cloudflare-native/src/cron-queue.js:154-164`; `cloudflare-native/src/cron-state.js:98-101`; `cloudflare-native/src/cron-watchdog.js:112-117`.
## Node queue selection [#node-queue-selection]
`src/worker-runner.js` selects:
1. Remote Durable Object queue when both `GILF_QUEUE_URL` and `GILF_QUEUE_TOKEN` are set.
2. SQLite queue at `GILF_QUEUE_DB_PATH` when set.
3. SQLite queue at `GILF_DB_PATH` when set.
4. Otherwise, an in-memory queue.
The worker's lease derives from model and validation timeouts plus five minutes of slack unless a positive `GILF_QUEUE_LEASE_SECONDS` overrides it. This is not the native `LEASE_MS` setting. The parity `/operator/api/queue/*` repair routes forward to a Durable Object and then record an audit event; they do not operate on `gilf-review-intents` or provide a transaction across queue repair and audit storage.
Sources: engine `src/worker-runner.js:14-39`; parity `cloudflare/src/review-state-api.js:544-572`.
See [Cloudflare-native runtime](./cloudflare-native-runtime), [Publish modes](../code-review/publish-modes), and [Operator API](../operator/operator-api).
# Security Model (/docs/architecture/security-model)
Treat PR content, comments and repository automation as untrusted input. The controls below describe source behavior, not a certification, a penetration-test result or a guarantee against prompt injection.
The deployed-engine lineage is `son-of-anton-review` on `feat/cloudflare-native`. Extended operator authorization belongs to **unmerged** `feat/greptile-operator-parity`; dashboard session handling belongs to `anton-ui-trace-parity`. Do not infer that these are one deployed security boundary.
## Validation isolation [#validation-isolation]
The native container injects a Cloudflare Sandbox collector. The Worker receives an archive and validation shell, unpacks them into the sandbox workspace, executes there, and attempts sandbox destruction in `finally`. The orchestration container retains the model-provider key; the separate sandbox is where this validation path runs PR scripts.
If the Cloudflare Sandbox collector or binding is missing, validation refuses rather than falling back to local execution. Unknown executor selection also refuses. This does not mean validation always ran or passed: a refusal produces a missing-validation result.
`local` execution is explicitly available when `GILF_ALLOW_LOCAL_VALIDATION=1`. `self-host` requires `GILF_SELF_HOST_VALIDATION=1` plus an executor command. That command is responsible for isolation, networking and compute teardown. The launcher cannot prove that an operator-supplied command uses a secure container or VM.
The general executor resolver falls back to `managed-crabbox` after explicit and legacy configuration. Native environment projection instead supplies `managed-cf-sandbox`. These are different defaults at different layers.
Sources: engine `cloudflare-native/container/entrypoint.mjs:325-343`; `cloudflare-native/src/state-transport.js:615-679`; `src/validation-executor.js:61-97,524-579`; `cloudflare-native/src/container-env.js:72-85`.
## Child processes and repository configuration [#child-processes-and-repository-configuration]
The Codex child uses `buildCodexExecEnv` with `inheritEnv: false`. Its allowlist includes process basics such as `PATH`, `HOME`, locale and certificate variables, plus an explicit `CODEX_HOME`. Provider, App, webhook and queue keys are not copied by this helper.
This is environment reduction, not full filesystem or network isolation. The allowed home paths and configured agent credentials remain relevant. When execution is allowed, the Codex arguments include `--dangerously-bypass-approvals-and-sandbox`; the no-execution path instead uses `--disable shell_tool --sandbox read-only`. Do not generalize the no-execution setting into a guarantee for all reviews.
Crabbox refuses repository-supplied automation configuration unless `GILF_CRABBOX_ALLOW_REPO_CONFIG=1`. The runner also checks out the pinned `headSha` with `git checkout --force --detach`; it does not rely solely on a moving branch reference.
Sources: engine `src/codex-review-runner.js:1280-1284,1901-1904,2185-2221,2843-2844,3121-3125`.
## App key and token boundary [#app-key-and-token-boundary]
The native broker Worker is configured without public routes or a workers.dev domain, and its fetch handler always returns 404. The App private key is absent from the container forwarding allowlist. Normal clone calls request repository-scoped read permissions; publication requests narrower check or pull-request write permissions.
The remaining boundary matters: `handleBrokerRequest` forwards caller-supplied installation, repository and permission scope. The broker applies defaults but does not enforce a purpose allowlist or bind scope to a review lease. A compromised orchestration container is not limited to its initial clone token. See [Key broker](./key-broker).
Node self-host can load the App key in `server.mjs` and `worker.mjs`; do not claim the native key separation for that deployment.
Sources: engine `cloudflare-native/wrangler.key-broker.jsonc:16-26`; `cloudflare-native/src/key-broker.js:13-33`; `cloudflare-native/src/key-broker-core.js:26-55`; `cloudflare-native/src/state-transport.js:583-599`; `cloudflare-native/container/entrypoint.mjs:110-123`; `src/github-publisher.js:13-78`; `server.mjs:52-59`; `worker.mjs:35-40`.
## Webhooks, commands and publication [#webhooks-commands-and-publication]
Native ingress validates the webhook HMAC against `GITHUB_WEBHOOK_SECRET`. Its comparison checks length and XOR-accumulates the equal-length strings; this is separate from operator bearer-token handling. Signature verification authenticates the webhook delivery, not the commenter's permission to request a review.
The command authorization helper accepts explicitly allowed users or associations, defaulting to `OWNER`, `MEMBER` and `COLLABORATOR`. Ingress is a superset event filter, not the authoritative review-policy gate. Do not infer authorization merely from an event being queued.
Native publication defaults to shadow when unset, but the checked-in Wrangler configuration explicitly sets `GILF_PUBLISH_MODE=live`. Live publication requires a supplied authenticated client. Shadow publication still needs read-token minting for private repository preparation. Approval requires a pinned head SHA. Native publication claims protect against blind resends after ambiguous writes, not against all possible misuse of a compromised publisher.
Sources: engine `cloudflare-native/src/ingress.js:22-58,280-285`; `src/commands.js:29-39`; `cloudflare-native/src/publish-mode.js:26-35,44-67`; `cloudflare-native/wrangler.jsonc:133`; `cloudflare-native/container/entrypoint.mjs:97-123`; `src/github-publisher.js:50-65`.
## Operator API authorization is branch-specific [#operator-api-authorization-is-branch-specific]
The parity Worker uses `Authorization: Bearer `. Its resolver reads `SON_OF_ANTON_` before legacy `GILF_`. These aliases apply to that Worker, not all engine environment variables.
| Legacy token variable | Route scope |
| ----------------------------- | ---------------------------------------------------------------------------- |
| `GILF_OPERATOR_READ_TOKEN` | Ordinary operator GET routes, except key management |
| `GILF_OPERATOR_ADMIN_TOKEN` | Key listing/creation/revocation and designated configuration/queue mutations |
| `GILF_OPERATOR_COMMAND_TOKEN` | Command creation |
| `GILF_OPERATOR_BRIDGE_TOKEN` | Command updates and command reads |
Static-token comparison uses `timingSafeEqual` after a length check. Tokens are route-specific, not a role hierarchy: an admin token is not automatically accepted as a read or command token. Dynamic `anton_` keys have an explicit route allowlist and `read` / `memory:write` scopes; they cannot manage keys or create review commands.
For static-token calls, the Worker accepts `x-operator-actor`, defaulting to `service-admin`. That label is caller-supplied attribution by a trusted service-token holder, not independently authenticated human identity. The Node dashboard instead compares its bearer token using string equality and only serves GET requests.
Sources: parity `cloudflare/src/env.js:1-9`; `cloudflare/src/worker.js:31-81,115-135`; engine `src/operator-dashboard.js:601-620`.
## Dashboard sessions [#dashboard-sessions]
The parity BFF authenticates its opaque `__Host-anton-session` cookie, not a caller-supplied bearer. It uses `Secure`, `HttpOnly` and `SameSite=Lax`, with a 12-hour session lifetime. WorkOS validation pins the client-scoped issuer, verifies RS256 signatures through JWKS, requires subject and expiry, and checks `WORKOS_ALLOWED_USER_IDS`. It does not require an audience claim.
The BFF forwards server-configured backend tokens, and mutations require same-origin JSON requests. Static assets are served outside the API authentication gate; protected data requests are gated. This is source behavior, not confirmation of live WorkOS or backend configuration.
Sources: UI `worker/bff-sessions.js:4-6,24-25,69-75,294-297`; `worker/workos-auth.js:11-18,43-79`; `worker/index.js:24-73`; `worker/proxy-utils.js:95-100`.
## Data and egress limits [#data-and-egress-limits]
Memory file reads use pinned Git objects, reject non-regular blobs and enforce path/file/byte bounds. These checks are specific to memory ingestion, not a blanket statement about every repository read.
Egress enforcement must be verified per executor. The native sandbox transport shown here does not accept or enforce an allowed-domain list. Setting E2B network variables only on the Worker also has no effect when those variables are absent from the container forwarding allowlist. Do not describe the native runtime as globally default-deny.
Trace attributes, events, errors and optional input/output can be persisted. The parity API omitting input/output fields is not storage redaction. Treat trace and operator access as sensitive.
Sources: engine `src/review-memory-runtime.js:12-41`; `cloudflare-native/src/state-transport.js:625-679`; `cloudflare-native/src/container-env.js:15-70`; `cloudflare-native/src/trace-buffer.js:104-117`; parity `cloudflare/src/review-trace-queries.js:34-64`.
# Context Graph (/docs/code-review/context-graph)
In `son-of-anton-review` (`feat/cloudflare-native`), preflight builds an in-memory graph of the checked-out repository. It estimates affected files, potential test coverage and risk beyond the diff. It is a bounded regex-based graph, not a type-resolved or multi-repository index.
## What is built at preflight [#what-is-built-at-preflight]
Before any model call, the runner indexes the repo into a `ContextGraph` (`src/context-graph.js`). Per file it records:
| Fact | How it is extracted |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `imports` | `extractImports`: `import`, `export ... from`, dynamic `import()` and `require()`. Relative specifiers are resolved to repo paths; bare package names are dropped. |
| `symbols` | `extractSymbols`: regex matches for `function`, `class`, `const`, `let`, `var` declarations, including matches that are not exported or top-level |
| `references` | Every identifier in the file that is not one of its own symbols. |
| `tests`, `isTest` | Paths under `test/`, `tests/`, `__tests__/` or ending in `.test.*` / `.spec.*`. |
| `hash` | SHA-256 of content, used to report whether an indexed fact changed; extraction is still performed before that comparison |
Scope is JavaScript and TypeScript only (`.js .mjs .cjs .jsx .ts .mts .cts .tsx`). The walk caps at 1000 files and skips files over 512 KB. The graph is rebuilt per run from the checked-out head; it is not a long-lived index.
## Blast radius [#blast-radius]
`computeImpact(changedPaths)` does a breadth-first walk from the changed files:
1. Any file that imports a changed file is impacted (`imports`).
2. Any file containing a reference token matching a symbol recorded on an impacted file is impacted (`references_symbol`).
3. Repeat until no new files join.
The result carries `changed`, `impacted`, `productionFiles`, `tests`, `blastRadius` (count of impacted paths) and `reasons`, a per-path chain explaining why each file was pulled in.
`analyzeChange` layers intelligence on top:
* `testsToRun`: impacted test files plus tests inferred from direct imports or symbol references to impacted production files, capped at 20. This is context for the reviewer, not an instruction that automatically runs those individual files.
* `missingTests`: impacted production files with no inferred reaching test.
* `risk.score` (0 to 10) and `risk.level` (`low` under 4, `medium` under 7, `high` at 7 and above), weighted by changed count, blast radius, fan-in of changed files, missing tests and orphan imports. `risk.drivers` lists the reasons in plain text.
Preflight summary lines and the graph brief expose health, blast radius, risk and inferred tests. The hypothesis-primary result builder puts a non-zero blast radius into **Cross-Repo Impact**. The normal semantic result supplies its own section, and the deterministic docs-only result leaves that section empty. Do not treat the heading as proof of a second-repository traversal.
## Where it lives [#where-it-lives]
Optional persistence targets the **legacy queue Worker**, `cloudflare/src/worker.js`, through `cloudflare/src/context-graph-api.js`. It is not the native ingress API. The legacy configuration names D1 `gilf-context-graph` and R2 `gilf-context-graph-artifacts`.
| Endpoint | Method | Purpose |
| -------------------------- | ---------- | -------------------------------------------------------------- |
| `/context-graph/health` | GET | Reports whether the D1 and R2 bindings are present |
| `/context-graph/files` | POST / GET | Write or read per-file facts for a `repo` + `commitSha` |
| `/context-graph/snapshots` | POST / GET | Write or read the impact, intelligence and hypotheses snapshot |
| `/context-graph/artifacts` | POST / GET | Write or read review artifacts by `key` |
The client sends `authorization: Bearer `. The legacy Worker validates it against its `GILF_QUEUE_TOKEN`, including for `/context-graph/health`. The binding names below come from `cloudflare/wrangler.toml`; provisioning also requires the actual database identifier.
```toml
[[d1_databases]]
binding = "CONTEXT_GRAPH_DB"
database_name = "gilf-context-graph"
[[r2_buckets]]
binding = "CONTEXT_GRAPH_BUCKET"
bucket_name = "gilf-context-graph-artifacts"
```
The table schema is `cloudflare/schema/context-graph.sql`. The runner sends at most **50 file facts**, although graph analysis can scan up to 1000 files. Its snapshot includes impact, intelligence and hypotheses, not a complete persisted copy of every scanned file. A persistence exception emits `context_graph_persist_failed` and does not abort the review.
## Configuration [#configuration]
```bash
# Defaults to GILF_QUEUE_URL / GILF_QUEUE_TOKEN when unset.
GILF_CONTEXT_GRAPH_URL=https://gilf-pr-review-queue..workers.dev
GILF_CONTEXT_GRAPH_TOKEN=replace-with-context-graph-token
```
Both URL and token must be non-empty to construct the default client. The client token must match the legacy server's queue token unless a separately implemented server uses a different authorization contract.
**Native deployment limitation:** neither `GILF_CONTEXT_GRAPH_URL` / `GILF_CONTEXT_GRAPH_TOKEN` nor their `GILF_QUEUE_URL` / `GILF_QUEUE_TOKEN` fallbacks are forwarded by `cloudflare-native/src/container-env.js`. The native entrypoint does not inject a graph store. The graph is still built in-process, but Worker vars alone do not enable persistence. Native `src/main.js` routes public fetches to ingress, which does not expose these endpoints.
## Repo context files read at review time [#repo-context-files-read-at-review-time]
Semantic review passes the listed paths to `collectReviewCodeEvidence`, which reads pinned Git blobs rather than following working-tree files. It accepts regular blob modes, recursively lists a context directory and clips each excerpt to at most 4000 bytes, within a separate default 32 KiB/60-file guidance budget. Oversized, non-regular or unavailable evidence becomes a limitation.
The optional `buildReviewPrompt` fallback helper behaves differently when no pinned packet is passed: it reads working-tree files, includes only immediate regular children of directories, trims content and slices 4000 JavaScript string code units. Do not treat that fallback as the normal pinned semantic-review path.
| Path | Notes |
| ----------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| `AGENTS.md`, `CLAUDE.md` | Agent instruction files are read as review guidance |
| `README.md`, `ARCHITECTURE.md`, `docs/ARCHITECTURE.md` | Project overview |
| `CONTRIBUTING.md`, `SECURITY.md` | Contribution and security rules |
| `.github/CODEOWNERS`, `.github/copilot-instructions.md` | Ownership and existing assistant rules |
| `.cursor/rules` | Pinned evidence lists directory contents recursively, subject to the guidance budget |
| `pr-review-agent.yml` | Read as review guidance, not parsed here to apply runtime config |
| `system-map.yaml`, `critical-paths.yaml`, `contract-catalog.yaml` | Cross-repo relationships, critical paths, contracts |
The fallback helper skips empty/missing files. The pinned evidence collector can include empty regular-file excerpts and reports read/size limitations rather than silently claiming complete guidance.
## system-map.yaml shape [#system-mapyaml-shape]
`schemas/system-map.schema.json` defines the cross-repo map. Top level is `repos`, keyed by repo name:
```yaml
repos:
billing-api:
type: backend # frontend | backend | mobile | library | infra | docs | worker | service
owners: [payments-team]
domains: [payments]
deployTier: critical # critical | high | normal | low
depends_on: [shared-auth]
dependents: [web-app]
contracts:
- kind: api # api | event | schema | package | auth_boundary | db_model
name: /v1/charges
role: exposes # consumes | exposes | owns | shares
criticalPaths: [checkout]
```
The file is read as text into the review context. The schema is a contract for authors; nothing in the runner validates against it today.
## Honest scope [#honest-scope]
* **Cross-Repo Impact** is a result section, not evidence of a multi-repo graph. The deterministic hypothesis builder uses the local blast radius; semantic results may also reason from supplied text.
* The repo context files are text guidance. They do not turn the graph into a cross-repo index. See [Learning, memory and priors](/docs/code-review/learning-and-priors) for the separate memory system.
* Symbol references are identifier matches, not type-resolved. Expect some over-approximation on common names.
## Observability [#observability]
Graph construction emits `context_graph_started` / `context_graph_completed`, mapped to a `context.graph` retrieval span. The optional persistence events map to `context.graph.read` for preflight persistence and `context.graph.write` for hypothesis-artifact persistence, despite the former also writing data. These event names alone do not prove that a deployed graph store or dashboard view is available.
## Source evidence [#source-evidence]
* `son-of-anton-review/src/context-graph.js:3-7,15-55,58-212,306-351`.
* `son-of-anton-review/src/codex-review-runner.js:743-830,1485-1526,1618-1649,1758-1785,1795-1825,2517-2526`.
* `son-of-anton-review/src/review-code-evidence.js:3-7,122-160`; `src/codex-review-runner.js:1573-1596,1828-1853,2963-2969,3090-3093`.
* `son-of-anton-review/src/context-graph-storage.js:78-148`; `cloudflare/src/context-graph-api.js:57-160`; `cloudflare/src/worker.js:30-39,156-176`.
* `son-of-anton-review/cloudflare/wrangler.toml:9-16`; `schemas/system-map.schema.json:5-42`.
* `son-of-anton-review/cloudflare-native/src/container-env.js:15-85`; `cloudflare-native/src/main.js:26-29`; `cloudflare-native/container/entrypoint.mjs:345-374`.
# Docs-only Fast Path (/docs/code-review/docs-only-fast-path)
`son-of-anton-review` (`feat/cloudflare-native`) classifies paths before validation and model selection. Its “docs-only” label is a filename heuristic, not proof that a change cannot affect runtime behavior. Code, scripts or executable examples under a matching docs path can receive that label too.
## What happens on a docs-only PR [#what-happens-on-a-docs-only-pr]
1. Preflight lists the changed paths with `git diff --name-status origin/...HEAD`.
2. Every path is checked against the doc-like and generated-like matchers.
3. With `codeChangesOnly` enabled, docs-only or generated-only diffs skip sandbox validation. Execution policy can skip it independently.
4. A docs-only review satisfying the semantic-skip conditions below uses a deterministic body instead of the primary semantic model.
5. The context graph still builds and appears in preflight context. The deterministic docs-only result leaves `crossRepoImpact` empty.
The two classifications are independent, not exclusive. A path such as `docs/generated/README.md` can match both. When the validation diff-skip applies and both are true, the recorded reason is `docs_only`.
## Classification rules [#classification-rules]
Classification is deterministic and runs before any model call (`src/codex-review-runner.js`, `isDocLikePath` and `isGeneratedLikePath`).
| Class | Matches | Set when |
| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- |
| Doc-like | A `doc`, `docs`, `adr`, `rfc`, `rfcs`, `spec` or `specs` directory segment; basename `README`, `CHANGELOG`, `CONTRIBUTING`, `SECURITY`, `DESIGN` or `ARCHITECTURE` with no extension or one dot-free extension; any `.md`, `.mdx`, `.txt`, `.rst`, `.adoc` | `docsOnly` when every changed path matches |
| Generated-like | Any path under `dist/`, `build/`, `coverage/`, `generated/`, `vendor/`; any `.min.js`, `.min.css`, `.lock`, `.snap` | `generatedOnly` when every changed path matches |
An empty diff is neither. `shouldRunValidation` is `!docsOnly && !generatedOnly`. Both flags are emitted on `preflight_completed` and printed in the preflight summary as `Docs-only diff: yes|no` and `Generated-only diff: yes|no`.
The `.lock` suffix is not a universal lockfile rule. `yarn.lock`, `Cargo.lock` and `bun.lock` match; root `package-lock.json`, `pnpm-lock.yaml` and `bun.lockb` do not match the generated classifier just because they are lockfiles. Dependency-change classification is separate and recognizes those names. A mixed docs/generated diff is not skipped unless **all** paths satisfy one of the two classes.
## Validation skip [#validation-skip]
When `shouldRunValidation` is false and `codeChangesOnly` is on, the runner does not boot a sandbox:
| Event | `reason` | `missingValidations` entry |
| -------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `validation_skipped` | `docs_only` | Repo-native install/test/typecheck/lint/build steps were skipped because the diff is docs-only; review focused on contradictions between docs and live code/contracts. |
| `validation_skipped` | `generated_only` | Repo-native validation was skipped because only generated artifacts changed; inspect the source generator/input change before trusting the artifact output. |
The validation summary line in the review reads `SKIPPED repo-native validation: docs-only diff.` or `SKIPPED repo-native validation: generated-artifact-only diff.`
See [Validation and Evidence](../code-review/validation-and-evidence) for what runs when the skip does not apply.
## Semantic review skip [#semantic-review-skip]
When the conditions below hold, the primary semantic model is skipped and the runner emits `codex_skipped` with `reason: docs_only`. The base deterministic result has:
* Verdict `clear`, merge status `caution`, confidence 3.
* Overview: "Docs-only change for `#` at ``. Gilf skipped Codex semantic analysis for this automatic run because no runtime code changed."
* `missingValidations` gains: "Codex semantic analysis was skipped because this was an automatic docs-only review."
* Merge verdict: "Safe to merge if maintainers agree the documentation matches current runtime behavior."
Generated-only diffs that are not also docs-only skip validation by default but do not trigger this semantic shortcut.
The skip applies only when all of these hold (`src/codex-review-runner.js`, `skipCodexForDocsOnly`):
| Condition | Why |
| -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `GILF_CODEX_FOR_DOCS_ONLY` is not `1` | Setting `1` disables this shortcut |
| `preflight.docsOnly` | Classification above |
| No prepared review-memory text (`preflight.reviewMemory.text`) | Selected stored contexts or memory limitations prevent this shortcut; prior-review history by itself is not this predicate |
| No operator review guidance in the review policy | Custom guidance implies a real pass was requested |
| No manual command on the run | `run.command` prevents this docs-only shortcut |
| No competitor context | `run.competitorContext` prevents this docs-only shortcut |
A manual `@anton review` or `@anton rerun` prevents the **docs-only** semantic shortcut. It does not override admission, execution policy, a separately enabled validation-failure shortcut or provider failure. See [Triggers and commands](/docs/code-review/triggers-and-commands).
## When the model does run on docs [#when-the-model-does-run-on-docs]
When a semantic pass runs, the prompt instructs the model to check documentation against current code, tests, configuration, migrations and API behavior, and not invent runtime defects from unchanged code. This is a prompt instruction, not evidence that the model inspected every referenced contract.
Source: the docs-only branch of `buildReviewPrompt` in `src/codex-review-runner.js`, and `prompts/review-goal-template.md`.
## Configuration [#configuration]
| Setting | Default | Effect |
| ---------------------------------------- | ------------------------------- | ----------------------------------------------------------------------------------------------- |
| Repo config `validation.codeChangesOnly` | `true` (`src/config.js`) | Boolean `false` disables diff-based validation skipping when not overwritten by worker defaults |
| `GILF_VALIDATION_CODE_CHANGES_ONLY` | on (`src/validation-config.js`) | Exact string `false` sets the worker value to false; `0` does not |
| `GILF_CODEX_FOR_DOCS_ONLY` | unset | Exact string `1` disables this docs-only semantic shortcut |
`mergeValidationConfig` spreads worker defaults over run settings for top-level fields. Therefore the worker's `codeChangesOnly` wins when supplied. Only nested Crabbox/E2B options use run-specific precedence.
For a directly launched engine process:
```bash
export GILF_VALIDATION_CODE_CHANGES_ONLY=false
export GILF_CODEX_FOR_DOCS_ONLY=1
```
The first allows validation on these diffs but does not override execution policy or executor readiness. The second disables only the docs-only semantic shortcut.
**Native forwarding gap:** neither flag is forwarded by `cloudflare-native/src/container-env.js`. Adding either to Worker vars alone does not change the container. The entrypoint supplies `validationConfigFromEnv` defaults and does not override the docs-only semantic setting.
## Cost effect [#cost-effect]
When the shortcut applies with default-off optional workers and validation still skipped, it avoids sandbox execution and the semantic model call. Cloning, context/memory work, state and publication still occur, so this is not a zero-cost review. Generated-only diffs that are not also docs-only do not trigger this semantic shortcut.
The planner honors the semantic skip reason, but `#runHypothesisReview` is called before deterministic result selection even on this path. If an operator directly enables Prime or injects workers, those workers can still run. Do not promise zero model tokens after changing the default feature configuration.
Turn `codeChangesOnly` off only if your docs are executable (for example doctests) and the repo's validation scripts cover them.
## Related [#related]
* [Validation and Evidence](../code-review/validation-and-evidence)
* [Triggers and Commands](../code-review/triggers-and-commands)
* [Context Graph](../code-review/context-graph)
## Source evidence [#source-evidence]
* `son-of-anton-review/src/codex-review-runner.js:711-720,833-848,1529-1544,1618-1649,2303-2315,2528-2548,2620-2657,2756-2769,3342-3353`.
* `son-of-anton-review/src/validation-config.js:11-31`; `src/config.js:11-14`.
* `son-of-anton-review/cloudflare-native/src/container-env.js:15-85`; `cloudflare-native/container/entrypoint.mjs:310-313,345-354`.
# Finding Evolution Across Pushes (/docs/code-review/finding-evolution)
The engine in `son-of-anton-review` (`feat/cloudflare-native`) compares findings with the prior-review snapshots supplied to the run, not an unlimited history of every review. It dedupes findings and records `findingEvolution` on the run. The model finding schema has a file `path` but **no line numbers**; these identities do not represent inline GitHub comments.
## Pipeline [#pipeline]
## Stable fingerprint [#stable-fingerprint]
`stableFindingFingerprint(finding)` returns `finding:`.
1. If the finding carries an explicit id (`fingerprint`, `findingFingerprint`, `ruleId`, `rule_id` or `checkName`), that id is hashed.
2. Otherwise the hash covers normalized severity, category, path and title.
Normalization strips code spans, URLs, quotes and accents, lowercases, and drops `:line:col` suffixes from paths. `critical` maps to `blocker`, `warning` to `medium`.
`findingFallbackKey(finding)` is the looser identity: severity, path, and up to 18 sorted semantic tokens from title, body and the last two path segments. Tokens are stemmed and aliased (`authorization`, `permission`, `access` all become `auth`; `lacks`, `skips`, `without` become `missing`).
## Similarity match [#similarity-match]
`findingsAreSimilar(left, right)` is true when any of these hold:
| Test | Rule |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------ |
| Fingerprint | `stableFindingFingerprint` equal |
| Fallback key | `findingFallbackKey` equal |
| Fuzzy | same normalized path, severities within one rank of each other, and Jaccard token overlap of titles >= 0.35 or of title+body >= 0.28 |
## Severity order [#severity-order]
Ranking and severity compatibility use this order, most severe first:
| Rank | Severity |
| ---- | --------------------------- |
| 0 | `blocker` (also `critical`) |
| 1 | `high` |
| 2 | `medium` (also `warning`) |
| 3 | `low` |
| 4 | `nit` |
| 5 | `info` |
Unknown severities rank last. The review schema only accepts `blocker`, `high`, `medium`, `low` from the model (`src/codex-review-runner.js`, `REVIEW_SCHEMA`).
## Dedupe within one review [#dedupe-within-one-review]
`dedupeFindings(findings)` walks the list once. A finding that is similar to one already kept replaces it only if it is more severe, or equally severe with a longer body. `contextIds` from both are merged so memory provenance is never lost.
## Classification against the prior head [#classification-against-the-prior-head]
`classifyFindingEvolution(previous, next, { unresolvedFindings, unresolvedSources })` dedupes both sides, then for each new finding looks for an exact fingerprint match first and a similarity match second.
| Status | Meaning |
| ------------ | -------------------------------------------------------------------------------- |
| `new` | No prior finding matched |
| `persisting` | Matched and canonical text (severity, category, path, title, body) identical |
| `modified` | Matched but text changed |
| `resolved` | Prior finding not matched by anything in the new run |
| `unresolved` | Prior finding not matched, but flagged by same-head reconciliation as still open |
Each entry records `finding`, `previousFinding`, `fingerprint` and `fallbackKey`. `unresolved` entries also carry `sourceFindings` with the `reviewKey` and fingerprint of every original report.
The service builds `previous` from the latest supplied prior review plus same-head concerns recovered by reconciliation. Its outcome recorder separately selects the latest supplied review from a different head and writes one outcome per prior finding. A missing evolution status defaults to `resolved`, but an explicitly incomplete semantic analysis records `unknown` rather than `fixed`. That distinction matters: evolution status alone is not proof of a code fix.
## Model-reported dispositions [#model-reported-dispositions]
For same-head reconciliation, the model can explicitly disposition an old finding through `priorFindingDispositions`, a required array in the review schema. Across different heads, the classifier also calls unmatched prior findings `resolved`; it does not require a disposition for every cross-head omission.
```json
{
"priorReviewKey": "",
"fingerprint": "finding:...",
"disposition": "resolved | false_positive | no_longer_applicable",
"explanation": "why",
"evidence": [{ "headSha": "", "path": "src/x.js", "quote": "verbatim code" }]
}
```
The prompt tells the model that a missing finding on an unchanged head is not a fix, that omission is not a disposition, and to return `[]` when nothing is justified.
Two gates apply before a disposition counts:
1. The runner rejects any disposition whose evidence is not on the current `headSha`, has an absolute or `..` path, or whose `quote` is not found in `git show :`.
2. `reconcileSameHeadFindings` accepts it only if `priorReviewKey` and `fingerprint` name a known source occurrence, the disposition is one of the three allowed values, and the fingerprint is not re-reported in the current findings.
Accepted dispositions are stored as `priorFindingDispositions` on the review.
`validateMemoryFindingProvenance(findings, supplied)` rejects any `contextIds` entry not actually supplied to the model. See [Learning, memory and priors](/docs/code-review/learning-and-priors).
## Same-head reconciliation [#same-head-reconciliation]
`reconcileSameHeadFindings(run, findings, dispositions)` covers re-runs on the same commit. It collects findings and carried unresolved occurrences from supplied same-head snapshots, applies supported historical dispositions and tracks anything not covered by a current finding or accepted disposition. Each unresolved concern adds a limitation. The runner then forces `verdict: needs-attention`, caps `confidenceScore` at 3, sets `mergeStatus` to `caution` unless it is already `block`, and appends limitations to `missingValidations`. The service performs another continuity pass before publication.
## Feedback guard [#feedback-guard]
`guardFeedbackResolutions` reports omitted prior `blocker`, `high`, `critical`, security/auth/vulnerability-category concerns when supplied feedback has `reaction: down` linked by context ID or title. It does not reinsert findings or inspect code to decide whether the feedback was the only new signal. The runner turns held concerns into `missingValidations` and incomplete-context status.
This guard consumes the memory feedback snapshot, not a live reaction attached to every finding. The current publisher creates review bodies rather than inline comments, so do not infer a complete per-finding reaction-learning loop from the guard.
## Publishing across heads [#publishing-across-heads]
`GitHubPublisher` creates a check run with `external_id = reviewKey` and updates that check when its ID is known. PR reviews are `POST`ed with an `` marker and `commit_id = headSha`. There is no per-finding inline-comment creation or update.
Publication claims are scoped to a review key and action, not to a finding across all heads. A newer head or deliberate variant can publish another review containing a persisting concern. Native uncertain publication requires remote-receipt reconciliation before resending; the local SQLite ledger instead supports lease-based reclamation. Neither establishes a blanket “no duplicate comments across heads” guarantee. See [Publish modes](/docs/code-review/publish-modes).
## Known remaining work [#known-remaining-work]
The publisher does not edit individual prior findings according to their evolution status. Evolution and outcomes support run history and subsequent review context; omission, similarity matching and feedback are not line-level addressed detection.
## Source evidence [#source-evidence]
* `son-of-anton-review/src/finding-evolution.js:3-4,64-100,143-251,257-400`.
* `son-of-anton-review/src/codex-review-runner.js:210-248,2719-2783`; `src/review-service.js:397-462,478-515`.
* `son-of-anton-review/src/github-publisher.js:22-66`; `src/sqlite-store.js:415-462`; `cloudflare-native/src/state-transport.js:490-524`.
# Hypotheses and the Review Swarm (/docs/code-review/hypotheses-and-swarm)
## Status [#status]
The swarm is built but **off by default** in `son-of-anton-review` (`feat/cloudflare-native`). With no injected `hypothesisWorker` and no Prime opt-in, `#runHypothesisReview` returns `null`. An unset primary flag does not start shadow workers.
| Mode | Runtime configuration | Result selection |
| ------------- | ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| Off (default) | No injected worker; Prime disabled | Normal semantic review, subject to fast-path skips |
| Shadow | Injected worker or `GILF_PRIME_SHADOW=1`, primary flag off | Semantic review normally supplies the result; usable swarm output can become recovery output if semantic review fails |
| Primary | `GILF_HYPOTHESIS_PRIMARY=1` | Usable swarm output can replace semantic review when prepared review-memory text is empty and no semantic shortcut applies |
Primary mode also enables Prime unless `GILF_PRIME_SHADOW=0`. The usability check accepts a non-failed result with findings **or** at least one completed/succeeded worker. It does not require all workers to succeed.
**Cloudflare forwarding gap:** `cloudflare-native/src/container-env.js` does not forward the hypothesis, planner or Prime flags below, and the entrypoint does not inject a worker. Setting those flags only on the Worker cannot enable this pipeline. Enabling it in that deployment requires integration code changes, not just Worker vars. The examples below are environment settings for a directly launched engine process.
## Pipeline [#pipeline]
1. Preflight builds the [context graph](../code-review/context-graph) and classifies changed files into lenses.
2. `planHypothesisReview` turns those lenses (or planner output) into hypotheses and assigns one worker lens per hypothesis.
3. `runHypothesisWorkers` runs one worker per hypothesis with a concurrency cap from `GILF_HYPOTHESIS_CONCURRENCY` (runner default `4`). A worker that throws is recorded as `failed`; the run continues.
4. `synthesizeHypothesisReview` filters findings by evidence labels, dedupes them and reports `degraded` if any worker failed, otherwise `completed`. Its evolution helper accepts previous findings, but the runner does not pass prior findings into this swarm call; PR continuity is handled later by the runner/service.
## The lens floor [#the-lens-floor]
Eleven regex lenses classify changed paths. Each matching dimension gets a generic hypothesis and required evidence labels. Files matching no lens get Core Correctness.
| Lens | Dimension key | Required evidence |
| -------------------------- | --------------- | ----------------- |
| Payment Integrity | `payments` | diff, test |
| Auth Boundary | `auth` | diff, test |
| API Contract | `api` | diff, test |
| Data Persistence | `data` | diff, schema |
| Async Reliability | `jobs` | diff, test |
| Frontend Behavior | `frontend` | diff, runtime |
| Deploy Safety | `infra` | diff, config |
| Supply Chain | `dependencies` | diff, config |
| Observability | `observability` | diff |
| Test Signal | `tests` | diff |
| Documentation Truth | `docs` | diff |
| Core Correctness (default) | `core` | diff, test |
The planner appends the static floor after model hypotheses. This preserves the planned checks; it does not guarantee workers execute correctly or resist injected content.
## LLM planner [#llm-planner]
Set `GILF_HYPOTHESIS_PLANNER=1` in the engine process to request a planner pass. It reads the diff, graph summary and repository priors. The prompt asks for a file/symbol/behavior, concrete failure and `falsifiedBy` evidence. Prompt instructions are not proof that the generated hypothesis is correct. Priors add `SUPPRESS lens "..."` instructions and filter matching model-generated hypotheses; they do not remove static-floor hypotheses.
OpenRouter is the recommended planner provider. Choose a model available to your account and consistent with the free-model policy; no particular model ID is guaranteed by the engine.
| Flag | Default | Purpose |
| ----------------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `GILF_PLANNER_PROVIDER` | main review provider | Provider for the planner pass. OpenRouter recommended. The `codex` provider has no chat endpoint, so the planner is skipped under it. |
| `GILF_PLANNER_MODEL` | main review model | Planner model override |
| `GILF_PLANNER_TIMEOUT_MS` | `60000` | Abort the planner call. |
| `GILF_PLANNER_MAX_HYPOTHESES` | `8` | Cap on model hypotheses. The floor is added on top. |
| `GILF_PLANNER_MAX_DIFF_BYTES` | `60000` | Diff truncation before prompting. |
Timeouts, invalid output and provider errors fall back to static lenses. With the planner enabled, no worker yields `no_hypothesis_worker`; Codex yields `codex_provider_unsupported`. Docs-only and validation-failure **semantic skip decisions**, rather than all docs-only or failing-validation PRs, skip the planner. The OpenRouter free-model guard is shared with semantic review and defaults on.
## Evidence filter [#evidence-filter]
`filterFindingsByEvidence` keeps a finding only if:
1. Its `dimension` matches a planned hypothesis.
2. It states falsifiability (`falsifiable: true`, `falsification`, or `falsifiableBy`).
3. Its `evidence` entries cover every required evidence type for that dimension.
Rejected findings are kept in `rejectedFindings` and counted in the synthesis summary. Today the filter checks evidence type labels, not artifacts. See [validation and evidence](../code-review/validation-and-evidence) for artifact-backed evidence.
This is not independent execution verification. Worker normalization supplies missing falsifiability from the hypothesis and can expand one non-empty evidence string into all required type labels. Keep the distinction between accepted labels and an observed test/log artifact.
## Publication ownership [#publication-ownership]
Worker results return to the parent runner, whose `ReviewService` owns GitHub publication. Prime's prompt forbids publishing, messaging and production mutation. That prompt is an instruction, not an isolation guarantee. In particular, the label “shadow” does not prevent the parent from using swarm findings for semantic-failure recovery.
## Prime agent worker backend [#prime-agent-worker-backend]
Without an injected worker, the Prime adapter is selected by `GILF_PRIME_SHADOW=1`, or by primary mode unless explicitly disabled. It invokes `prime-agent --print --mode json --no-session`, normally with `--no-context-files`, plus provider/model/cwd arguments. The binary and its authentication must be provisioned separately. Source inspection cannot establish a deployed login state.
```bash
export GILF_PRIME_SHADOW=1
export GILF_PRIME_AGENT_BIN=/path/to/prime-agent
export GILF_PRIME_PROVIDER=codex
export GILF_PRIME_MODEL=your-provisioned-model-id
export GILF_PRIME_THINKING=low
export GILF_PRIME_NO_CONTEXT_FILES=1
export GILF_PRIME_TIMEOUT_MS=600000
```
When the repo policy forbids execution, the worker is limited to `read`, `grep` and `glob` tools.
## Turning it off [#turning-it-off]
If there is no injected worker and Prime is disabled, no swarm runs. Disable `GILF_HYPOTHESIS_PRIMARY` as well as `GILF_PRIME_SHADOW`, or explicitly set the latter to `0` when primary remains enabled.
`GILF_DISABLE_HYPOTHESIS_WORKERS=1` only changes the harness capability description in `src/harness-evolution.js`; it is not a runtime stop switch in `CodexReviewRunner`.
## Source evidence [#source-evidence]
* `son-of-anton-review/src/codex-review-runner.js:1682-1686,2298-2315,2351-2366,2620-2709,3248-3435`.
* `son-of-anton-review/src/review-hypotheses.js:1-214`; `src/hypothesis-orchestrator.js:30-62,89-172`.
* `son-of-anton-review/src/hypothesis-planner.js:189-250,377-391,454-463`; `src/prime-harness-adapter.js:11-52,212-223`.
* `son-of-anton-review/src/harness-evolution.js:56-59`.
* `son-of-anton-review/cloudflare-native/src/container-env.js:15-85`; `cloudflare-native/container/entrypoint.mjs:345-374`.
# Learning, Memory and Priors (/docs/code-review/learning-and-priors)
`son-of-anton-review` (`feat/cloudflare-native`) contains outcome aggregation, repository priors and a separate review-memory system. These are not a complete per-finding reaction/commit learning loop. The distinction is in the callers, not just the helper implementations.
## What the outcome recorder receives [#what-the-outcome-recorder-receives]
After analysis, `ReviewService.#recordPriorFindingOutcomes` selects the latest supplied prior review from a **different head**. It records each of that review's findings in `finding_outcomes`, keyed by `(review_key, finding_fingerprint)`.
The service passes `finding`, evolution status, available PR state and a timestamp to `computeFindingOutcome`. It does **not** pass `reactions`, `replies` or `followUpCommits`.
| Live recorder input/result | Meaning |
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------ |
| Explicitly incomplete semantic analysis | `unknown`, confidence `null`, with the incomplete-review reason |
| `resolved` evolution | `fixed`, confidence 0.85: the finding was not reported on the next head, not a verified hunk-level fix |
| Closed/merged metadata, no earlier outcome | `ignored`; confidence 0.8 for persisting/modified findings, otherwise 0.55 |
| No outcome signal | `unknown`, confidence 0 |
The service defaults an absent evolution status to `resolved`. The semantic-completeness guard is therefore important. Closed/merged outcomes depend on state actually being present in the run metadata; this recorder is not a merge-event feedback collector.
The D1 schema and existing-database migration are `cloudflare-native/schema.sql` and `cloudflare-native/migrations/004-finding-outcomes.sql`. Native state transport hydrates and flushes these rows across containers. See [Finding evolution](/docs/code-review/finding-evolution).
## Capabilities of the outcome helper, not full live wiring [#capabilities-of-the-outcome-helper-not-full-live-wiring]
`computeFindingOutcome` can also interpret the following **when a caller supplies them**:
1. Resolved evolution takes precedence; a matching follow-up commit raises confidence to 0.95.
2. A thumbs-down or dismissive reply produces `dismissed`.
3. A follow-up file/hunk change can produce `fixed`.
4. A thumbs-up or reply can produce `acknowledged`.
5. Closed/merged PRs without an earlier signal produce `ignored`; otherwise `unknown`.
Hunk matching accepts a finding line and uses a default five-line proximity. But the model finding schema has **no line numbers**, and the service does not supply follow-up commits. Do not describe this as deployed line-level addressed detection. Even the helper falls back to file-level matching when a line or patch is unavailable; it permits a candidate when either timestamp cannot be parsed.
The GitHub publisher posts a review body, not inline finding comments. The existence of reaction-handling code does not imply there is a published inline comment for each finding.
## Addressed rate and priors [#addressed-rate-and-priors]
`aggregateAddressedRate` groups by `repo`, `lens`, `severity` or ISO `week`. Its formula is `(fixed + acknowledged) / (total - unknown)`, or `null` if no decided outcomes exist. This metric is only as strong as its outcome inputs; it is not a measured precision score.
`buildRepoPriors` defaults to `minSamples: 5` and `suppressBelow: 0.2`. Each lens gets `samples`, `addressedRate`, `suppress` and a note. Suppression needs at least five decided samples and a rate strictly below 0.2.
`createStorePriorsProvider` reads up to 2000 repository outcomes. It is wired in **both** `worker.mjs` and `cloudflare-native/container/entrypoint.mjs`, not just the container. If the runner's provider throws, the planner records `hypothesis_priors_failed` and uses no priors.
The planner writes `SUPPRESS lens "..."` guidance and filters matching **model-generated** hypotheses. It still appends the static lens floor. Priors do not delete those baseline checks or directly change the monolithic review's ranking rules.
### Default-off and forwarding boundary [#default-off-and-forwarding-boundary]
Priors are consumed by the hypothesis planner, which requires `GILF_HYPOTHESIS_PLANNER=1`, a usable worker configuration and a supported planner provider. The planner is off by default. The native container allowlist does not forward that flag, worker-enabling hypothesis/Prime flags, or `GILF_PLANNER_*` settings. Wiring a priors provider does not make planner learning active in the native deployment. See [Hypotheses and swarm](/docs/code-review/hypotheses-and-swarm).
## Review-memory contexts [#review-memory-contexts]
Memory is a separate system with `rule` and `file` contexts. Status is `active`, `inactive`, `suggested` or `ignored`; source is `manual` or `learned`.
Selection includes active contexts with a matching repository scope and path glob. A `repo: null` scope matches any repo; `**` applies regardless of changed paths. When changed paths are not yet available, scope filtering can be broader until the runner performs path-aware selection.
Context mutations use revisions and audit records in the same D1 batch. Stale revisions return 409. Feedback suggestions are created as `source: learned`, `status: suggested`; changing them to `active` is an audited approval, not an automatic learning step.
A file context names `sourceRepo` and `sourcePath`. The local reader uses pinned Git objects, not working-tree content:
* Full 40- or 64-hex commit SHA required.
* Only regular blobs with mode `100644` or `100755` are accepted.
* Limits: 20 files, 32 KiB per file, 160 KiB total, 20000 tree paths.
* NUL bytes or lossy text decoding are rejected.
* Git reads disable system/global config, lazy fetch and terminal prompting.
Cross-repository sources require an explicit shared memory cluster and a source resolver; a cluster is permission to consider a source, not proof that it was retrieved. Unavailable sources and prompt-budget limits become review limitations. Knowledge documents are historical guidance, not proof about the new head; regeneration preserves human-edited content.
## Feedback ingestion and its limitations [#feedback-ingestion-and-its-limitations]
The native `syncReviewMemoryFeedback` transport fetches fresh GitHub responses. It scans the current PR plus recent published PRs, capped at five distinct PRs and 500 candidates. It accepts relevant user comments referencing a published review/finding, and explicit `remember:`, `review rule:` or `review guidance:` text. The captured suffix can become suggested guidance after permission checks.
| `memoryRuleCreation` | Permitted source user |
| ----------------------- | ------------------------------------------------------------ |
| `ADMINS_ONLY` (default) | GitHub permission `admin` |
| `MEMBERS` | `read`, `triage`, `write`, `maintain` or `admin` |
| `EVERYONE` | Those permissions, or a verified user on a public repository |
The reaction scan specifically enumerates inline review comments associated with successful published review IDs and reads their `+1`/`-1` reactions. The current publisher does not create those inline comments. Consequently, this code does not establish working per-finding thumbs-up/down attribution for current review-body publication. Explicit top-level feedback can still produce suggested guidance; it is separate from the outcome recorder described above.
`guardFeedbackResolutions` turns certain omitted high/security concerns linked to negative feedback into limitations. It does not make feedback code-based proof of resolution.
## Operator API branch [#operator-api-branch]
The memory-management API lives in the separate `son-of-anton-operator-parity` tree on `feat/greptile-operator-parity`. Do not assume the native engine Worker exposes it. Paths below are relative to `/operator/api/memory`:
| Path | Methods |
| ----------------------- | -------------------------------------------------------------------- |
| `/contexts` | `GET`, `POST` |
| `/contexts/:id` | `GET`, `PATCH`, `DELETE` |
| `/clusters` | `GET`, `POST` |
| `/clusters/:id` | `PATCH`, `DELETE` (no single-cluster GET handler) |
| `/knowledge` | `GET` |
| `/knowledge/document` | `GET` with `repo` and `path` query parameters; `PUT` with JSON input |
| `/integrations` | `GET` |
| `/integrations/preview` | `POST` |
The Worker token gates use `OPERATOR_READ_TOKEN` for reads and `OPERATOR_ADMIN_TOKEN` for memory mutations. The parity Worker also has scoped access-key authorization; these are not interchangeable with native container flags. A successful management request is not evidence that the separate native engine is using the same state.
## Source evidence [#source-evidence]
* `son-of-anton-review/src/review-service.js:478-515`; `src/addressed-rate.js:26-82,92-150,168-215`; `src/codex-review-runner.js:210-224,2231-2239,3342-3369`.
* `son-of-anton-review/worker.mjs:43-53`; `cloudflare-native/container/entrypoint.mjs:345-354`; `cloudflare-native/src/container-env.js:15-85`.
* `son-of-anton-review/src/hypothesis-planner.js:233-246,454-463`; `src/review-memory-store.js:137-149,159-249,308-341,389-398`.
* `son-of-anton-review/src/review-memory-runtime.js:11-118`; `src/review-memory-source.js:4-39`; `cloudflare-native/src/review-memory-transport.js:166-282`.
* `son-of-anton-review/cloudflare-native/src/state-transport.js:208-215,375-395`; `cloudflare-native/schema.sql:87-104`; `src/github-publisher.js:50-66`.
* `son-of-anton-operator-parity/cloudflare/src/review-memory-api.js:55-118`; `cloudflare/src/worker.js:69-80,115-139`.
# Model Inversion (/docs/code-review/model-inversion)
## Status [#status]
Built and **off by default** in `son-of-anton-review` (`feat/cloudflare-native`). `GILF_MODEL_INVERSION=1` enables the engine hook when supplied to the actual runner process. It can reroute the primary semantic review; planner and shadow-worker provider settings are separate.
**Native forwarding gap:** `cloudflare-native/src/container-env.js` does not forward `GILF_MODEL_INVERSION`, `GILF_MODEL_INVERSION_MIN_CONFIDENCE` or `GILF_MODEL_INVERSION_MAP`. The entrypoint does not pass an inversion override. Setting these only on the Worker does not enable inversion in the container. Code integration is needed before that deployment can use the settings.
| Setting | Default | Effect |
| ------------------------------------- | ------------ | --------------------------------------------------------------- |
| `GILF_MODEL_INVERSION` | off | Enable. Truthy values: `1`, `true`, `yes`, `on` |
| `GILF_MODEL_INVERSION_MIN_CONFIDENCE` | `0.5` | Skip inversion when detection confidence is below this (0 to 1) |
| `GILF_MODEL_INVERSION_MAP` | built-in map | JSON override of family to provider/model routing |
Source: `src/author-model.js`, `src/codex-review-runner.js` (`#chooseReviewProvider`).
## Why [#why]
Inversion is a routing heuristic: infer an authoring family from textual provenance and select a different family. The engine does not establish that this improves review accuracy, and inferred provenance is not proof of who authored the code.
## Detection [#detection]
`detectAuthoringModel` is a pure function. It reads no network, only the PR and its commits.
Inputs the runner passes:
| Input | Where it comes from |
| ----------------- | ----------------------------------------------------------------------------------------------------------------- |
| Commits | `git log origin/..HEAD`, falling back to the last 30 commits. Trailers, message body, author name and email |
| Branch | PR head ref, matched on prefixes like `claude/`, `codex/`, `copilot/`, `gemini/` |
| PR title and body | Explicit provenance phrases and product-name mentions |
Each hit becomes a weighted signal:
| Signal | Weight | Example |
| ----------------------------------------- | ------ | -------------------------------------------------------- |
| `Co-authored-by` trailer or commit author | 1.0 | `noreply@anthropic.com` |
| Strong marker in text | 1.0 | `Generated with Claude Code`, `codex[bot]` |
| Branch prefix | 0.8 | `codex/fix-retry` |
| Name mention in title or body | 0.4 | `ChatGPT`, `Gemini` |
| Robot emoji | 0.2 | Only reinforces a family already seen in the same source |
Output: `{ family, confidence, signals }`. Supported family labels are `anthropic`, `openai`, `google`, `human` and `unknown`. No matching signals returns `human` with confidence `1`; that means “no supported model signal found,” not verified human authorship. Otherwise confidence is signal strength (`1 - e^-score`) scaled by agreement and rounded to two decimals. Diagnostic match text is clipped to 120 characters.
## Routing [#routing]
`chooseReviewProvider` returns a routing reason. These are helper results; when inversion is disabled, the runner returns before detection and does not create an `analysis.modelInversion` record.
| Reason | Meaning |
| ----------------------- | --------------------------------------------------------------------------------- |
| `disabled` | Helper received an off flag; the runner normally short-circuits before calling it |
| `low_confidence` | Confidence below `GILF_MODEL_INVERSION_MIN_CONFIDENCE` |
| `no_target` | Family maps to `null` (default for `human` and `unknown`) |
| `target_not_configured` | Target provider has no credentials configured |
| `target_same_family` | Target would be the same family as the author |
| `inverted` | Review rerouted |
Default map routes through OpenRouter, the recommended provider:
```json
{
"anthropic": { "provider": "openrouter", "model": "openai/gpt-4.1" },
"openai": { "provider": "openrouter", "model": "anthropic/claude-sonnet-4-5" },
"google": { "provider": "openrouter", "model": "anthropic/claude-sonnet-4-5" },
"human": null,
"unknown": null
}
```
`GILF_MODEL_INVERSION_MAP` merges valid override entries onto the defaults. A `null` target disables a family. Invalid JSON retains the defaults; if routing reaches map resolution, the parse error is appended to `no_target` or `inverted`. Earlier `disabled`/`low_confidence` or target-configuration failures need not carry it.
For a directly launched engine process, this example enables inversion, raises the confidence threshold and opts into potentially paid OpenRouter targets:
```bash
export GILF_MODEL_INVERSION=1
export GILF_MODEL_INVERSION_MIN_CONFIDENCE=0.7
export GILF_MODEL_INVERSION_MAP='{"anthropic":{"provider":"openrouter","model":"openai/gpt-4.1"},"google":null}'
export GILF_OPENROUTER_REQUIRE_FREE=0
```
Direct `openai` and `anthropic` targets are supported and the inversion record marks them `paid: true`. This is a routing annotation, not a billing quote or a guarantee that credentials/models are usable.
## Where it plugs in [#where-it-plugs-in]
1. The runner reaches the normal semantic-review branch. A docs-only or validation-failure **shortcut** bypasses inversion. Hypothesis-primary mode bypasses it only when usable swarm output is actually selected and prepared review-memory text is empty.
2. `#chooseReviewProvider` runs detection and routing, then emits `model_inversion_evaluated` with the `describeInversion` string, for example `model-inversion: author=anthropic -> review via openrouter/openai/gpt-4.1`.
3. Evaluation is stored as `analysis.modelInversion` with `family`, `confidence`, `inverted`, `reason`, `provider`, `model`, commit count and the first eight signals. `paid` is only added on applicable routing branches.
4. If the inverted semantic provider throws, the runner emits `model_inversion_fallback`, records the fallback and retries the configured provider once. If that also fails, the existing recovery logic may use usable swarm output, or a deterministic recovery when `GILF_PUBLISH_ON_CODEX_FAILURE=1`; otherwise the review fails. That recovery flag is also absent from native container forwarding.
5. Any exception inside detection or routing is caught, logged as `model_inversion_failed`, and the review continues uninverted.
## Free-model guard [#free-model-guard]
`GILF_OPENROUTER_REQUIRE_FREE` defaults on; only the exact value `0` disables it. For an OpenRouter inversion target, the runner fetches model metadata and requires zero prompt and completion prices. A price-check error or refusal records `reason: openrouter_model_not_free` and leaves the original provider selected.
The built-in target IDs are routing defaults, not embedded price guarantees. With the default guard, rerouting only occurs if the provider's returned metadata passes the zero-price check. To permit non-zero prices for the review:
```bash
# allow paid OpenRouter models for the whole review
export GILF_OPENROUTER_REQUIRE_FREE=0
```
Alternatively, configure targets whose current metadata passes the free-model check. This guard is forwarded by the native container, but forwarding it does not enable the unforwarded inversion feature.
## Caveats [#caveats]
* The native image containing `src/author-model.js` is not enough: the flag still has to reach `CodexReviewRunner`. No deployed inversion behavior is claimed here.
* Detection is textual. A single product-name mention in PR title/body has weight 0.4 and confidence about 0.33, below the default threshold. Repeated or stronger provenance signals can increase confidence.
* The planner and shadow swarm retain their own settings. See [Hypotheses and swarm](/docs/code-review/hypotheses-and-swarm).
## Source evidence [#source-evidence]
* `son-of-anton-review/src/author-model.js:11-55,103-190,198-241`.
* `son-of-anton-review/src/codex-review-runner.js:2245-2270,2309-2314,2327-2329,2620-2709,3457-3509`.
* `son-of-anton-review/cloudflare-native/src/container-env.js:15-85`; `cloudflare-native/container/entrypoint.mjs:345-374`.
# Publish Modes and Rollout (/docs/code-review/publish-modes)
Publication behavior depends on the runtime. This page describes `son-of-anton-review` (`feat/cloudflare-native`) and distinguishes its native container from the standalone Node worker.
## Native publish mode [#native-publish-mode]
`cloudflare-native/src/publish-mode.js` resolves `GILF_PUBLISH_MODE`:
| Value | Result |
| ---------------------------------------- | ------------------------------------------------------ |
| Absent, empty or whitespace-only | `shadow` |
| `shadow` | In-memory `RecordingPublisher` |
| `live` | `GitHubPublisher`; an authenticated client is required |
| Case/whitespace variants, such as `Live` | Normalized with trim and lowercase, therefore `live` |
| Other values, such as `liv` or `on` | Throws `invalid GILF_PUBLISH_MODE` |
The native environment allowlist **does forward** `GILF_PUBLISH_MODE`. Configure it as a Worker var, for example in the existing `vars` object of `cloudflare-native/wrangler.jsonc`:
```json
{
"GILF_PUBLISH_MODE": "shadow"
}
```
This is a vars fragment, not a complete Wrangler configuration. Set `live` only when the native key-broker publication path is configured. The entrypoint logs `omp-container: publish mode ` and checks consistency with the dispatch lease. Shadow uses `container-dispatch:shadow`; live uses `container-dispatch`.
A local-development container using `GILF_DB_PATH` has no key-broker transport and cannot publish live. The native live publisher obtains a scoped client through the broker; the container does not receive the App private key.
### Standalone Node worker is different [#standalone-node-worker-is-different]
`worker.mjs` does **not** use `GILF_PUBLISH_MODE` to select its publisher. It starts with `RecordingPublisher` and selects `GitHubPublisher` when `GITHUB_APP_ID` and either `GITHUB_APP_PRIVATE_KEY` or `GITHUB_APP_PRIVATE_KEY_PATH` are configured. Do not use a native shadow-mode instruction as a safety switch for that runtime. This page does not require inspecting or printing any credential value.
## What shadow means [#what-shadow-means]
In the native container, review publication calls go to `RecordingPublisher`, which records `check_run`, `review`, `comment` and `pr_description` events and returns synthetic IDs for check runs. It does not issue those GitHub mutations. Cloning, model requests, sandbox validation and other read-side work are separate and remain conditional on their own gates; shadow publication is not a no-work mode.
Shadow publication ledger actions and check-run IDs are namespaced separately from live ones. A succeeded shadow action does not count as a succeeded live action for the same head. A stored analysis checkpoint may be reused, so a container does not necessarily analyze the PR again on every dispatch.
Do not confuse three features:
* **Publish shadow:** suppresses GitHub publication through the selected publisher.
* **Validation shadow:** comparison telemetry from another executor; not merged into findings.
* **Hypothesis shadow:** default off and normally secondary, but usable findings can become recovery output after semantic failure.
## Live publication policy [#live-publication-policy]
The `mode` and `publishing` fields in `DEFAULT_REPO_CONFIG` are not the gates consulted by `ReviewService.#publish` / `#publishCheck`. Those methods use normalized operator review policy.
| Policy key | Default | Behavior |
| --------------------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------- |
| `statusChecks.enabled` | `true` | Creates/updates the `Son of Anton / review` check. If a check already exists when disabled, it is completed neutrally. |
| `statusChecks.requiredConfidence` | `0` | Integer 0–5. Above zero, missing or insufficient confidence returns a failing conclusion. |
| `statusChecks.postStatusComments` | `false` | Enables the per-review-key status comment. |
| `updatePrDescription` | `false` | Allows the publisher's PR-description update after checking the current head. |
| `autoApprove.enabled` | `false` | Allows approval only when the additional confidence, coverage, risk and policy checks pass. |
Normal PR review events are `COMMENT`, or `APPROVE` when allowed. There is no `REQUEST_CHANGES` event in this publisher. Findings appear in the review body and check text, **not inline comments or check annotations**. The finding schema has no line positions.
The check is created with `external_id = reviewKey` and updated by ID using `PATCH`. The review is `POST`ed with `commit_id = headSha` and an `` body marker. A new head or explicit variant can create another review; the marker does not imply cross-head finding-comment updates.
### Check conclusions [#check-conclusions]
`reviewCheckConclusion` evaluates in this order:
1. If a positive required-confidence threshold is unmet, return `failure`.
2. If normalized merge status is not `clear`, return `neutral`.
3. With a positive threshold, incomplete/degraded/unpinned semantic coverage or an absent `missingValidations` array returns `neutral`.
4. Otherwise return `success`.
At threshold zero, merge status alone decides success versus neutral in this helper. A positive threshold does not prove validation passed: the code requires the gap array to exist, not to be empty. These conclusions are not a substitute for reading the review or configuring required checks in GitHub.
## Publication ledger: native and local differ [#publication-ledger-native-and-local-differ]
The service claims actions before publication and records success/failure afterward. Relevant actions include `pr_review`, `pr_description`, check-state actions, `review_status_comment` and `review_receipt_comment`. Manual help/status replies are direct publisher calls, so “every GitHub mutation uses this ledger” would be too broad.
| Storage path | Reclaim behavior |
| -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Native `nativePublishAction` in `cloudflare-native/src/state-transport.js` | Requires dispatch lease identity. A new claim inserts `running`; success stores `succeeded`; a reported failure becomes `uncertain`. Existing `running`/`uncertain` actions do not expire into another POST. Remote-receipt reconciliation is required first. |
| Standalone `SQLiteStore.claimPublishAction` | Refuses succeeded actions and running actions within their lease; expired running actions can be reclaimed. This is not the native uncertain-outcome contract. |
The native error requires reconciliation; it does not automatically find and reconcile a GitHub review merely because a marker exists. Before retrying an ambiguous mutation, compare GitHub state with the stored review key, marker, check `external_id` and receipts. A requeue alone is not proof that retrying publication is safe or that an uncertain action has been cleared.
## Separate shadow semantic model [#separate-shadow-semantic-model]
`CodexReviewRunner` accepts a secondary model via `shadowModel` or `GILF_CODEX_SHADOW_MODEL`, with `GILF_CODEX_SHADOW_PROVIDER`, `GILF_CODEX_SHADOW_REASONING_EFFORT` and `GILF_CODEX_SHADOW_TIMEOUT_MS` overrides. It is disabled when no shadow model is set. It runs after a successful normal semantic pass, not on every review shortcut, and its findings are stored separately rather than selected as the authoritative findings.
The provider utility recognizes `GILF_MODEL_SHADOW`, but this runner constructor does not use it to configure its secondary lane. The native entrypoint does not pass shadow-model options, and the allowlist forwards neither `GILF_MODEL_SHADOW` nor the `GILF_CODEX_SHADOW_*` settings. Worker vars alone cannot turn this lane on.
## Recommended rollout and rollback [#recommended-rollout-and-rollback]
This is an operating recommendation, not an automatically enforced promotion workflow:
1. Start native publication in shadow. Check the consolidated runtime evidence for the actual deployment before enabling writes.
2. Enable live comments with `requiredConfidence: 0` and auto-approval off. Keep the check optional in branch protection while assessing behavior.
3. Trial a positive confidence threshold while the check is still optional. Review both failures and neutral conclusions; a confidence number is not semantic coverage.
4. Only make the check required after deciding how those conclusions fit repository merge policy.
To roll back native publication, change the Worker publish mode to `shadow` and deploy that configuration through the normal release process. This is not cancellation of already-running live work. Drain or reconcile in-flight runs before starting another publisher. Cron mutation authority is a separate control; `CRON_AUTHORITY=shadow` is not a substitute for changing the container publish mode.
## Source evidence [#source-evidence]
* `son-of-anton-review/cloudflare-native/src/publish-mode.js:17-69`; `cloudflare-native/src/container-env.js:15-85`; `cloudflare-native/container/entrypoint.mjs:285-313`.
* `son-of-anton-review/worker.mjs:35-53`; `src/publisher.js`; `src/review-service.js:801-821,964-1096`.
* `son-of-anton-review/src/operator-review-policy.js:26-45`; `src/review-format.js:138-148`; `src/github-publisher.js:22-104`.
* `son-of-anton-review/cloudflare-native/src/state-transport.js:490-524`; `src/sqlite-store.js:415-462`.
* `son-of-anton-review/src/codex-review-runner.js:2324-2337,2650-2709,3198-3245`; `src/model-provider.js:349-355`; `cloudflare-native/src/cron-config.js`.
Related: [Validation and evidence](/docs/code-review/validation-and-evidence), [Context graph](/docs/code-review/context-graph), [Triggers and commands](/docs/code-review/triggers-and-commands).
# Triggers and Commands (/docs/code-review/triggers-and-commands)
This page describes `son-of-anton-review` (`feat/cloudflare-native`). Pull request webhooks, manual review comments and operator requeues can request reviews. Configured competitor-review events can also request audits. These paths share review processing, but do not all use the same queue-job or dedupe key.
## Automatic triggers [#automatic-triggers]
A `pull_request` webhook can request a review for `opened`, `reopened`, `ready_for_review` or `synchronize`. The native ingress filters events before enqueueing; configuration and authorization are checked downstream, not all before a Cloudflare Queue slot is used.
`edited` updates stored PR metadata when delivered directly to `ReviewService`. The native ingress instead records it as an ignored `unsupported_action` and does not dispatch it to that service.
The **standalone service webhook handler** applies `src/config.js` / `shouldAutoReview`:
| Config key | Default | Effect |
| ------------------ | ----------------- | -------------------------------- |
| `enabled` | `true` | Repo is reviewed at all |
| `review.auto` | `true` | Webhooks may queue reviews |
| `branches.include` | `["main", "dev"]` | Base branch must be in this list |
| `review.onDraft` | `false` | Draft PRs are skipped |
The native container instead seeds a run directly and applies `evaluateRuntimeAdmission`. It does not call `shouldAutoReview`, so the `main`/`dev`, `review.auto` and `review.onDraft` defaults above are not native admission gates. A configured repository uses its `enabled` value; without config, admission depends on prior review history or `autoEnableNewRepos` (policy default `false`).
The pinned review policy (`src/operator-review-policy.js`, `DEFAULT_REVIEW_POLICY`) applies:
| Policy key | Default | Effect |
| ---------------------- | ------- | ------------------------------------------------------------------------------ |
| `autoReviewNewCommits` | `true` | `false` skips `synchronize`; open, reopen and ready-for-review remain eligible |
| `reviewDrafts` | `false` | Draft PRs return `draft_review_disabled` |
| `fileChangeLimit` | `500` | PRs changing more files return `file_change_limit` |
| `pausedAuthors` | `[]` | Logins whose PRs are never reviewed, manual commands included |
| `filters` | `[]` | Rule list, see below |
## Standalone event filtering [#standalone-event-filtering]
`src/pr-update-engine.js` rejects unsupported actions, merge-commit events and no-op updates. Merge detection uses supplied metadata: an explicit merge flag, multiple head-commit parents, or a message beginning `Merge branch`, `Merge pull request` or `Merge remote-tracking branch`. It does not fetch the head commit to prove that it is a merge.
For `synchronize`, equal previous/current SHAs or a head already stored for the PR count as `no_op`. An explicit `noChanges` also skips the event. The service passes `skipBots: false` and `skipDraft: false` to this helper; draft admission is handled by repo config and review policy instead.
The transition helper defines a latest-waiting key, `{owner}/{repo}#{pr}:latest-waiting`, and can return `replace_waiting` when given an earlier waiting job. Do not equate that helper with removal of a message already in Cloudflare Queues.
## Dedupe keys [#dedupe-keys]
Two layers:
* Native ingress writes `delivery:{X-GitHub-Delivery}` into `DEDUPE` KV with `DEDUPE_TTL_SECONDS` (default 604800 seconds, seven days). A repeat answers `duplicate_delivery`. Recording/enqueue failures attempt to delete the key and return HTTP 500 so a redelivery can be accepted. KV is eventually consistent; downstream leases also matter. This setting is read in the Worker, not the container.
* Review key `{owner}/{repo}#{pr}@{headSha}` identifies the run. Manual commands append a variant, `manual-{commentId}`, so a fresh `@anton review` on an unchanged head is a new run rather than a `skipped_locked` hit.
## Comment commands [#comment-commands]
Post a top-level PR comment. The parser (`src/commands.js`) accepts:
```text
@anton review
@anton rerun
@anton status
@anton help
```
`@gilf` is an alias for every verb. Matching is case-insensitive, whitespace is collapsed, and the comment must start with the command:
```text
^(?:@anton|@gilf)\s+(review|rerun|status|help)(?:\s+(.*))?$
```
| Verb | What happens |
| -------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `review` | Queues a run for the stored PR head |
| `rerun` | Same review-request behavior as `review` |
| `status` | Replies with the latest stored PR run status and stored head abbreviation, no new run; the latest run is not filtered to that head |
| `help` | Replies with the command list, no run |
The standalone service only executes commands on `created` comments. Native ingress accepts `created` and `edited` issue comments and its consumer keeps only parsed `review`/`rerun` commands; native seeding does not repeat the standalone created-only check. The native review identity remains scoped to the comment ID, so editing one comment is not the same as posting a new rerun command.
Trailing text is parsed as `args`, but does not select another PR or head. The standalone service dedupes commands by `{owner}/{repo}#{pr}@{commentId}` and returns `duplicate_command` for an existing record. Native dispatch uses the review key and lease instead.
Not available: `@anton ignore`, `@anton resolve`, `@anton learn`. Those parse to `no_command` and are ignored.
Comment handling can be switched off per repo with `manual.enabled: false`.
## Who can run commands [#who-can-run-commands]
`isAuthorizedCommenter` checks the GitHub `author_association` on the comment against the repo config:
```json
{
"manual": {
"enabled": true,
"allowAssociations": ["OWNER", "MEMBER", "COLLABORATOR"],
"allowUsers": []
}
}
```
A login in `allowUsers` passes regardless of association. Anyone else is recorded as `unauthorized_commenter` and gets no reply. `pausedAuthors` still applies to manual runs.
The service requires a stored PR head even for `help` and `status`, otherwise it returns `unknown_pr_head`. Its reply strings still use the legacy Gilf name and help lists the `@gilf` aliases. The parser accepts both names.
Manual review requests bypass the policy's automatic draft, file-count and filter checks, but not `pausedAuthors` or separate repository admission/execution checks. Native seeding also checks `manual.enabled` and `isAuthorizedCommenter` against the authenticated webhook comment. Native `status`/`help` comments are acknowledged as `no_review_command` without dispatching a reply container.
## Policy filters [#policy-filters]
`filters` is a list of rules. Any matching rule satisfies this filter gate; within a rule every condition must match. An empty list imposes no filter restriction, but other admission gates still apply. Maximum 50 rules, 20 conditions each.
| Field | Operators | Values |
| -------------- | -------------------------- | ------------------------------------------------------- |
| `label` | `is`, `is_not` | Label names |
| `author` | `is`, `is_not` | Logins, lowercased |
| `repository` | `is`, `is_not` | `owner/repo` |
| `targetBranch` | `is`, `is_not` | Globs |
| `sourceBranch` | `is`, `is_not` | Globs |
| `path` | `is`, `is_not` | Globs against changed paths |
| `title` | `contains`, `not_contains` | Substrings |
| `keyword` | `contains`, `not_contains` | Substrings in title or body |
| `draft` | `is`, `is_not` | None |
| `filesChanged` | `at_most`, `more_than` | One nonnegative integer **string**, for example `"500"` |
```json
{
"filters": [
{ "conditions": [
{ "field": "targetBranch", "operator": "is", "values": ["main", "release/*"] },
{ "field": "path", "operator": "is_not", "values": ["docs/**"] }
] }
]
}
```
Globs are validated: no `..`, no partial `**` segments. A rule that needs metadata not yet available (for example changed paths) defers the decision until preflight fills it.
## Execution mode gate [#execution-mode-gate]
Sandbox validation is gated separately from review admission:
```json
{ "execution": { "mode": "always", "filters": [] } }
```
| Mode | Effect |
| --------- | ----------------------------------------------------------------------------- |
| `always` | Allows validation; docs/generated skips and executor availability still apply |
| `never` | Analysis only, summary shows `execution_disabled` |
| `filters` | Same rule engine as above, unmatched PRs show `execution_filters_not_matched` |
## Operator rerun and requeue [#operator-rerun-and-requeue]
From the host:
```bash
node bin/gilf-review.mjs requeue owner/repo#123
node bin/gilf-review.mjs requeue "owner/repo#123@" --variant manual-retry
```
Without `--variant`, the CLI appends `operator-{timestamp}` to the base review key. `--json` prints the result object, including `reviewKey`.
The separate `son-of-anton-operator-parity` tree (`feat/greptile-operator-parity`) implements the following durable command endpoint. It is not an endpoint on the native ingress Worker:
```http
POST /operator/api/commands/requeue
{ "repo": "owner/name", "number": 123, "variant": "manual-20260824", "reason": "operator_force_review" }
```
`POST` requires `OPERATOR_COMMAND_TOKEN`; `GET /operator/api/commands` and `/operator/api/commands/:commandId` accept `OPERATOR_READ_TOKEN` or `OPERATOR_BRIDGE_TOKEN`. Sibling commands: `retry-publish` and `replay-webhook`.
The CLI needs a stored PR head or exact stored review key and an installation ID. Run it from the engine checkout against the intended store/queue; it is not a stateless remote API client.
## Source evidence [#source-evidence]
* `son-of-anton-review/src/commands.js:9-39`; `src/config.js:1-45,86-97`.
* `son-of-anton-review/src/pr-update-engine.js:81-122,195-231`; `src/review-service.js:661-838`.
* `son-of-anton-review/src/operator-review-policy.js:26-78,138-180`.
* `son-of-anton-review/cloudflare-native/src/consumer.js:344-355`; `cloudflare-native/src/d1-store-adapter.js:146-170`; `src/review-policy-runtime.js:39-42`.
* `son-of-anton-review/cloudflare-native/src/ingress.js:59-132,295-345`.
* `son-of-anton-review/src/operator-cli.js:187-261,629-644`.
* `son-of-anton-operator-parity/cloudflare/src/worker.js:69-80`; `cloudflare/src/review-state-api.js:934-942`.
See [Context graph](/docs/code-review/context-graph) for preflight behavior and [Publish modes](/docs/code-review/publish-modes) for publication boundaries.
# Validation and Evidence (/docs/code-review/validation-and-evidence)
The engine in `son-of-anton-review` (`feat/cloudflare-native`) can run a repository's declared JS validation scripts and include their results in a review. Validation is conditional, not guaranteed on every PR, and a finding is not automatically backed by a persisted artifact.
## Executor selection and isolation [#executor-selection-and-isolation]
`runValidation` in `src/validation-executor.js` is the dispatch seam. `GILF_VALIDATION_EXECUTOR` takes precedence, then the merged `validation.provider`, then `GILF_VALIDATION_PROVIDER`, then `managed-crabbox`.
| Executor | Requirements and behavior |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `managed-crabbox` | Default for the generic engine. Requires a working `crabbox` binary; `GILF_CRABBOX_BIN` overrides its name/path. Repo-committed Crabbox automation is refused unless explicitly allowed. |
| `managed-e2b` | Requires `E2B_API_KEY` and the E2B SDK. |
| `managed-cf-sandbox` | Default selected by the native container environment. Requires an injected Cloudflare Sandbox collector; setting a flag alone cannot supply it. |
| `self-host` | Requires `GILF_SELF_HOST_VALIDATION=1` and `GILF_SELF_HOST_EXECUTOR_CMD` (legacy fallback `GILF_SELF_HOST_EXECUTOR`). The operator command must provide isolation. |
Aliases include `crabbox`, `e2b`, `cf-sandbox`, `cloudflare-sandbox`, `cfsandbox`, `selfhost` and `self_host`. Unknown executors or unavailable sandboxes do not silently fall back to executing PR scripts on the orchestration host. They produce a refusal or missing-validation result.
There is an explicit exception: legacy `local` executes on the worker when `GILF_ALLOW_LOCAL_VALIDATION=1`. It is refused otherwise. This opt-in does not verify that the repository is trusted. Do not use it for untrusted PRs. Legacy `shadow` means the older Crabbox/E2B comparison path, not the separate shadow-executor feature below.
For a directly launched engine process, an E2B selection is:
```bash
export GILF_VALIDATION_EXECUTOR=managed-e2b
```
Provision its API key separately. See [Validation executors](/docs/configuration/validation-executors).
## What runs [#what-runs]
The runner reads root `package.json`, detects the package manager and selects non-empty scripts named `test`, `typecheck`, `lint` and `build`, in that order. It does not discover arbitrary project validation commands.
| Step | Invocation or failure severity |
| ------------------ | ----------------------------------------------------------------------------------------------------------------- |
| Dependency install | `npm ci`, or `bun`, `pnpm`, `yarn` with `install --frozen-lockfile`; prerequisite, no finding severity of its own |
| `test` | `blocker` on an ordinary validation failure |
| `typecheck` | `blocker` |
| `lint` | `medium` |
| `build` | `high` |
No manifest or no standard scripts produces a gap rather than a pass. Some scripts can be classified as inconclusive before execution. An install failure stops the remote shell before subsequent scripts.
Remote shell steps emit `__GILF_VALIDATION_START__:` and `__GILF_VALIDATION_END__::` markers. The tag can include a per-run nonce. The parser uses the nonce it receives, but markers are an output-parsing mechanism, not proof that untrusted code cannot forge output or that every executor has identical nonce wiring.
| Summary prefix | Meaning |
| -------------- | ---------------------------------------------------------------------- |
| `PASS` | Matching completed step marker with exit 0 |
| `FAIL` | Reported non-zero step exit or overall runner failure |
| `INCONCLUSIVE` | Recognized infrastructure/toolchain limitation rather than a PR defect |
| `SKIPPED` | Step not reached, no standard scripts, or a diff-based skip |
| `REFUSED` | Executor not usable or host execution not allowed |
| `NOT RUN` | Execution policy disallowed validation |
A failed step with severity can produce a `validation` finding. Its path may be empty; the failure output is in the body. These findings have no structured line position and do not create inline GitHub comments.
## Skip conditions and precedence [#skip-conditions-and-precedence]
1. Execution policy runs first. `execution.mode: never` or unmatched execution filters disables validation. `always` permits execution but does not override diff classification or supply a working executor.
2. With `codeChangesOnly` enabled, docs-only and generated-only diffs skip repo-native validation. See [Docs-only fast path](/docs/code-review/docs-only-fast-path).
3. Otherwise the selected executor runs the available plan.
`validationConfigFromEnv` sets `codeChangesOnly` to true unless `GILF_VALIDATION_CODE_CHANGES_ONLY` is exactly `false`. `mergeValidationConfig` spreads the worker defaults **over** the run config at the top level, so a supplied worker default wins for `provider` and `codeChangesOnly`. Nested Crabbox and E2B settings merge in the opposite direction: run-specific values win.
`GILF_SKIP_CODEX_ON_VALIDATION_FAILURE=1` is a separate, default-off semantic-review shortcut. When prepared review-memory text is empty, any `blocker` or `high` validation finding enables the shortcut, even if it cannot be tied to the diff. For lower-severity findings, an extracted failure path must match a changed path. Prior-review history alone is not this memory-text gate. A validation failure does not normally disable the model when this flag is off.
## Shadow validation and cost [#shadow-validation-and-cost]
`GILF_VALIDATION_SHADOW_EXECUTOR` selects a second, supported, different executor when its collector is wired. Unset means no shadow lane. The lane starts alongside the primary dispatch and its result is reduced to telemetry, not merged into findings or validation gaps. The runner waits for it after result selection using the remaining `GILF_VALIDATION_SHADOW_TIMEOUT_MS` budget, default 600000 milliseconds. Exceptions and timeouts are recorded as error telemetry.
Costs are estimates, not bills: `estimate:wall-clock` multiplies elapsed seconds by a rate card. Built-in cards cover Cloudflare Sandbox and E2B; absent inputs yield `null`. Override cards with `GILF_VALIDATION_COST_RATES`, a JSON object. For example, in a shell:
```bash
export GILF_VALIDATION_COST_RATES='{"managed-e2b":{"vcpuUsdPerSec":0.000014,"memoryGibUsdPerSec":0.0000045}}'
```
Native dispatch uses `GILF_E2B_BUDGET_USD`, default 50, to withhold the exact `managed-e2b` shadow selector when recorded `validation.e2b` span cost reaches the ceiling. This is not a reservation or hard billing cap: in-flight spend is not reserved, and a D1 read failure logs an error and forwards the lane unchanged. The budget setting is consumed by the Worker, not forwarded into the container.
## Optional artifact evidence [#optional-artifact-evidence]
`GILF_EVIDENCE_ARTIFACTS=1` enables artifact construction and attachment. It is **off by default**. `GILF_EVIDENCE_REQUIRE_ARTIFACTS=1` requests an additional annotation step inside that feature; setting the require flag alone does not run it.
The artifact builder accepts validation steps/raw output and worker evidence. Kinds include `test-output`, `typecheck-output`, `lint-output`, `build-output`, `log`, `trace` and `screenshot`. If a collector returns only validation summaries, the builder can store a summary log instead of full step output. Do not promise a captured execution transcript for every executor.
Bodies have a default 256 KiB budget: text retains head and tail, binaries retain only the head. Keys follow `review-artifacts///evidence///`. A configured graph artifact store must accept the upload before new references attach to matching findings. Failed uploads are recorded, not treated as persisted evidence. See [Context graph](/docs/code-review/context-graph) for the storage boundary.
The require step marks execution-claim findings without a reference as `evidenceStatus: unverified` and adds `downgradedReason`. It does **not** remove them, lower their severity or independently verify their content. The predicate accepts an existing reference with a key, URL or ID; the normal attachment path filters failed uploads. Exceptions in the overall artifact hook record an error and return the original findings.
The review body renders artifact URLs or storage keys when present. A storage key is not necessarily a public download URL.
## Self-host command contract [#self-host-command-contract]
The command receives its configured arguments followed by ` `. It must start isolated compute, extract the archive, execute the shell and return output. The command string is split on whitespace, not evaluated as a shell program; avoid quoting-dependent command strings.
The child environment is built from a safe allowlist. The engine removes its packaging directory after execution, while the command must tear down its own compute. `GILF_SELF_HOST_NETWORK` only conveys network-policy intent; the executor must enforce egress. `GILF_SELF_HOST_TIMEOUT_MS` defaults to 2700000 milliseconds (45 minutes).
## Native container forwarding [#native-container-forwarding]
`cloudflare-native/src/container-env.js` sets `GILF_VALIDATION_EXECUTOR` to `managed-cf-sandbox` when absent and sets `GILF_CF_SANDBOX_ENABLED=1`. It forwards the E2B key, shadow selector/timeout, E2B timeout/CPU/memory/template and validation cost-rate override.
It does **not** forward `GILF_EVIDENCE_ARTIFACTS`, `GILF_EVIDENCE_REQUIRE_ARTIFACTS`, graph-storage URL/token settings, `GILF_VALIDATION_CODE_CHANGES_ONLY`, `GILF_SKIP_CODEX_ON_VALIDATION_FAILURE`, the self-host settings, `GILF_ALLOW_LOCAL_VALIDATION`, Crabbox overrides or E2B egress settings. Setting these only as Worker vars cannot configure the container paths. Native integration changes are required where those capabilities are needed.
## Source evidence [#source-evidence]
* `son-of-anton-review/src/validation-executor.js:35-135,146-163,251-257,304-362,378-581`.
* `son-of-anton-review/src/codex-review-runner.js:465-472,494-530,941-1097,1254-1385,1529-1544,2528-2627,2711-2714,3517-3570`.
* `son-of-anton-review/src/validation-config.js:11-31`; `src/evidence-artifacts.js:235-306,319-559`; `src/review-format.js:164-182`.
* `son-of-anton-review/cloudflare-native/src/container-env.js:15-85,100-141`; `cloudflare-native/container/entrypoint.mjs:325-354`.
# Dashboard Settings Reference (/docs/configuration/dashboard-settings)
Review policy is one stored JSON document for the workspace. This page describes the settings API on `son-of-anton-operator-parity`, branch `feat/greptile-operator-parity` (`P/` below), and the editor in `anton-ui-trace-parity`. That branch is not merged into the `son-of-anton-review` engine on `feat/cloudflare-native` (`R/`). The native engine can hydrate and pin policy from its own D1 snapshot; writing a different deployment's policy does not configure it.
## Where policy lives [#where-policy-lives]
* Storage: D1 table `review_policy_settings`, row `id = 'workspace'`, with a `revision` counter (`P/src/review-policy-store.js:59-89`).
* API: `/operator/api/settings` on `P/cloudflare/`. `GET` returns `{ ok, settings, revision, updatedAt }`. `PATCH` accepts `{ revision, settings }` and merges over the saved policy. Stale revisions return `409`; bodies over 256 KiB return `413` (`P/cloudflare/src/review-policy-api.js:5-63`).
* Audit: a successful policy write and its `review_policy_audit` entry share a D1 batch. `GET /operator/api/settings/audit?q=&limit=&offset=` returns policy audit plus memory/SCM audit where those tables exist. Limit 1 to 100, default 50 (`P/src/review-policy-store.js:69-110`).
* Usage: `GET /operator/api/settings/usage?from=YYYY-MM-DD&to=YYYY-MM-DD` reports model/chat telemetry. UTC date range defaults to month start through today; incomplete telemetry produces unknown values, not zero (`P/src/review-policy-store.js:26-38,112-168`).
* Defaults: `DEFAULT_REVIEW_POLICY` in `src/operator-review-policy.js`. Missing fields in a patch fall back to the stored value, then to the default.
```bash
# Read current policy and revision
curl -sS "$ANTON_OPERATOR/operator/api/settings" \
-H "authorization: Bearer $SON_OF_ANTON_OPERATOR_READ_TOKEN"
# Raise strictness. Revision must match the GET above.
curl -sS -X PATCH "$ANTON_OPERATOR/operator/api/settings" \
-H "authorization: Bearer $SON_OF_ANTON_OPERATOR_ADMIN_TOKEN" \
-H 'content-type: application/json' \
-d '{"revision": 3, "settings": {"strictness": "medium"}}'
```
`ANTON_OPERATOR` is your parity Worker origin in these examples, not an application env setting. Use the revision actually returned by GET, not the illustrative `3`. Canonical `SON_OF_ANTON_` tokens take precedence over `GILF_` fallbacks.
The handler requires `OPERATOR_ACTOR` supplied by the outer authenticated Worker. This is not an independently provisioned service binding: the Worker derives the actor and injects it. For legacy bearer callers it accepts `x-operator-actor` or defaults to `service-admin`; JSON cannot supply identity. Generated `anton_` machine keys cannot write policy or read the main settings document; their read scope permits settings usage only (`P/cloudflare/src/worker.js:65-80,115-146`).
The parity Settings view groups Repositories, Code providers, Code review, Summary, Checks & approval, Sandbox execution, Memory permissions, Models, Usage & limits, Audit log and API keys. Repositories also edits the workspace auto-enable policy and has separate per-repo writes. Models is a browse-only view, not a model selector that saves configuration (`anton-ui-trace-parity/src/views/Settings.tsx:16-27,209-218,247-264`). The original `anton-ui` does not contain this policy editor.
## Code review tab [#code-review-tab]
| Key | Default | Rule | Effect |
| ---------------------- | ------- | ----------------------- | ------------------------------------------------------------------------------------------------------------- |
| `autoEnableNewRepos` | `false` | boolean | Repos with no stored config are reviewed only if this is on or they were reviewed before (`src/scm-store.js`) |
| `autoReviewNewCommits` | `true` | boolean | `false` skips `synchronize` events |
| `reviewDrafts` | `false` | boolean | `false` returns `draft_review_disabled` for drafts |
| `fileChangeLimit` | `500` | integer 1 to 100000 | PRs over the limit return `file_change_limit` |
| `filters` | `[]` | rule list, see below | Empty list matches every PR |
| `customInstructions` | `""` | string, max 20000 chars | Passed to the reviewer as guidance (`src/review-policy-runtime.js`) |
| `strictness` | `low` | `low`, `medium`, `high` | Filters published findings, see below |
| `commentHeader` | `""` | string, max 500 chars | Prepended to the review comment |
| `updatePrDescription` | `false` | boolean | Writes the summary between `anton-review-summary` markers in the PR body |
| `promptToFix` | `false` | boolean | Appends a copyable "Prompt to Fix" block listing actionable findings |
| `featureTips` | `false` | boolean | Appends a footer listing `@anton` commands |
Strictness (`filterReviewFindings`):
* `low`: publish every finding.
* `medium`: drop medium-priority findings only when they carry a recognized confidence value below 0.8. Missing confidence is retained; the model finding schema does not require it.
* `high`: drop medium and low priority findings.
Findings ranked blocker, critical or high, or whose text matches security, injection, XSS, CSRF, SSRF, memory leak, infinite loop, null dereference or missing validation, are always kept. See [Triggers and Commands](../code-review/triggers-and-commands) for how these knobs gate a run.
## Summary tab [#summary-tab]
Five sections share one shape: `{ enabled, collapsible, defaultOpen }`, all booleans.
| Section | Default |
| --------------------- | ---------------------------- |
| `summary.summary` | enabled, not collapsible |
| `summary.confidence` | enabled, not collapsible |
| `summary.files` | enabled, not collapsible |
| `summary.diagram` | enabled, not collapsible |
| `summary.outsideDiff` | enabled, collapsible, closed |
A disabled section is omitted. A collapsible section renders as ``, opened when `defaultOpen` is true (`src/review-format.js` `appendSection`). Disabling `summary.confidence` also disables auto approval.
## Checks & approval tab [#checks--approval-tab]
| Key | Default | Rule | Effect |
| --------------------------------- | ------- | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `statusChecks.enabled` | `true` | boolean | Enable check-run publication, not a commit-status API. Disabling skips new checks but completes an existing check as `neutral` (`R/src/review-service.js:1038-1059`). |
| `statusChecks.requiredConfidence` | `0` | integer 0 to 5 | Above 0, missing/below-threshold confidence gives `failure`; adequate confidence with incomplete/unpinned semantic coverage gives `neutral`, not success |
| `statusChecks.postStatusComments` | `false` | boolean | Permit queued/failure status comments where the service posts them |
| `autoApprove.enabled` | `false` | boolean | Allow `APPROVE` reviews |
| `autoApprove.maxRisk` | `low` | `low`, `medium`, `high`, `critical` | Highest PR risk that may still be approved |
| `autoApprove.instructions` | `""` | string, max 20000 chars | Stored with the policy |
| `autoApprove.filters` | `[]` | rule list | PR must match these and the review filters |
Auto approval (`mayAutoApprove`) needs all of: confidence 5, complete semantic coverage with no failed or skipped workers, no missing validations, every finding at nit or info level with no protected match, a non-draft PR, and a head SHA that equals the SHA the coverage was computed on. The publisher refuses `APPROVE` without a pinned head commit (`src/github-publisher.js` `publishReview`). Everything else publishes as `COMMENT`.
## Sandbox execution tab [#sandbox-execution-tab]
| Key | Default | Rule |
| ------------------- | -------- | ------------------------------------------- |
| `execution.mode` | `always` | `always`, `never`, `filters` |
| `execution.filters` | `[]` | rule list, used only when mode is `filters` |
`shouldRunExecution` returns true, false, or the filter result. See [Validation and Evidence](../code-review/validation-and-evidence).
## Memory permissions tab [#memory-permissions-tab]
| Key | Default | Rule |
| -------------------- | ------------- | ------------------------------------ |
| `memoryRuleCreation` | `ADMINS_ONLY` | `ADMINS_ONLY`, `MEMBERS`, `EVERYONE` |
Stored and validated, but not an enforced runtime rule-creation authorization gate in the inspected engine. Do not rely on this enum to grant or restrict memory writes; the parity API's route authentication is separate.
## Usage & limits tab [#usage--limits-tab]
| Key | Default | Rule | Effect |
| ----------------------- | ------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `pausedAuthors` | `[]` | max 500 logins, lowercased, deduped | Paused authors are skipped even on manual `@anton review` |
| `monthlyModelBudgetUsd` | `null` | `null` or number 0 to 1e9 | At admission, refuses with `monthly_model_budget_reached` when current-month known model cost meets the cap; missing or stale usage refuses with `model_budget_usage_unavailable`. Not a reservation system or a strict concurrent-spend ceiling. |
## Filter rules [#filter-rules]
`filters`, `autoApprove.filters` and `execution.filters` share one grammar. A PR matches when any rule matches, and a rule matches when all its conditions match.
```json
{
"filters": [
{ "conditions": [
{ "field": "targetBranch", "operator": "is", "values": ["main", "release/*"] },
{ "field": "label", "operator": "is_not", "values": ["skip-review"] }
] }
]
}
```
| Field | Operators | Values |
| -------------------------------------- | -------------------------- | ------------------------------------------ |
| `label`, `author`, `repository` | `is`, `is_not` | strings; `repository` must be `owner/repo` |
| `targetBranch`, `sourceBranch`, `path` | `is`, `is_not` | safe globs |
| `title`, `keyword` | `contains`, `not_contains` | substrings, case-insensitive |
| `draft` | `is`, `is_not` | none |
| `filesChanged` | `at_most`, `more_than` | one integer string, 0 to 100000 |
If the PR metadata a condition needs is missing, the decision is deferred, not denied.
## Validation [#validation]
`PATCH` returns `400` with a message naming the path, for example `Unknown or invalid field: settings.foo`.
* Unknown fields are rejected at every level, including inside rules and conditions.
* Max 50 rules per list, 1 to 20 conditions per rule, max 50 values per condition, each value 1 to 500 chars.
* Globs allow `A-Z a-z 0-9 _ . @ space / ? * -`. Empty segments, `.`, `..` and `**` glued to other text are rejected.
* Control characters (U+0000 to U+001F except tab, newline and carriage return, plus U+007F) are rejected in every string.
* Integers must be safe integers within range. Booleans must be booleans.
## Models and API keys tabs [#models-and-api-keys-tabs]
The Models tab is read-only. It shows configured-key presence, selected provider and known model ids from the backend snapshot; it does not probe the actual review process or return provider credentials. OpenRouter is recommended; OpenAI, Anthropic and Codex are alternatives. See [Model providers](../configuration/model-providers) before selecting variables: Node reads the legacy runner names, and the native container only receives a restricted environment projection.
The API keys tab manages operator machine keys, not model credentials. `POST /operator/api/keys` with `{ name, scopes }` returns the token once; storage keeps its SHA-256 hash, 14-character prefix and metadata. Scopes are only `read` and `memory:write`; default scope is `read`. `POST /operator/api/keys/:id/revoke` revokes it. Creation/listing/revocation require the administrative bearer route, not another generated machine key (`P/src/operator-api-keys.js:1-3,65-109`; `P/cloudflare/src/worker.js:71,115-135`).
## Source evidence and runtime limits [#source-evidence-and-runtime-limits]
* Policy defaults, validation and filters: `R/src/operator-review-policy.js:25-108,110-180`; the corresponding `P/src/operator-review-policy.js` has the same definitions.
* Strictness and approval: `R/src/operator-review-policy.js:183-238`. Approval also checks risk ceiling and both filter lists. `R/src/github-publisher.js:50-65` pins approval to a head.
* Check conclusions and rendering: `R/src/review-format.js:127-159,186-245`. Coverage failure is not equivalent to a failed numeric confidence threshold.
* Runtime policy and budget admission: `R/src/review-policy-runtime.js:26-53,74-90`; `R/src/codex-review-runner.js:2376-2392`.
* Native policy snapshot: `R/cloudflare-native/src/d1-store-adapter.js:153-172`; settings routes remain on `P/cloudflare/src/worker.js:145-146`.
For conflicts, authorization failures or a missing endpoint, see [Troubleshooting](../reference/troubleshooting). No saved policy value can enable an unforwarded native V5 flag.
# Environment Reference (/docs/configuration/environment-reference)
Environment selects infrastructure, credentials and runner flags. Repository config and workspace review policy are also stored data, not environment-only configuration. Provider credentials are resolved from environment by the application; they are sent to the chosen provider for authentication.
Source prefixes below: `R/` is `son-of-anton-review` (`feat/cloudflare-native`); `P/` is `son-of-anton-operator-parity` (`feat/greptile-operator-parity`). These are separate, unmerged trees. A library default, a Node process default and a checked-in Cloudflare deployment value are not interchangeable.
## Runtime boundaries first [#runtime-boundaries-first]
* Keep the real `GILF_` names. There is no general `ANTON_` replacement in these entrypoints.
* Native Cloudflare only projects the keys listed in `R/cloudflare-native/src/container-env.js:15-86`. Setting an arbitrary Worker variable does not set it inside the review container.
* **Swarm, planner, Prime, inversion and artifact-evidence flags are not forwarded.** They cannot be enabled on this Cloudflare deployment by changing environment alone. Neither are role-specific model flags, legacy shadow-model flags, provider base URLs, prefixed model-key aliases or the docs-only override.
* `R/cloudflare-native/wrangler.jsonc:132-161` explicitly selects **live publication**, OpenRouter, `z-ai/glm-5.3-flash`, paid-model opt-in, Cloudflare Sandbox primary and E2B shadow. Those are checked-in deployment settings, not proof of the current remote deployment.
* Node `worker.mjs` does not call `resolveModelConfig`: use `GILF_CODEX_PROVIDER` and `GILF_CODEX_MODEL` for its actual runner. `GILF_MODEL_PROVIDER` can change dashboard status without changing that Node runner (`R/worker.mjs:43-54`; `R/src/codex-review-runner.js:2320-2326`).
## GitHub and Node state [#github-and-node-state]
| Variable | Default / requirement | Consumer and effect |
| ------------------------------------ | ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GITHUB_WEBHOOK_SECRET` | Required by Node server and native ingress | Node server exits with `Missing GITHUB_WEBHOOK_SECRET`; ingress verifies HMAC. `R/server.mjs:21-25`; `R/cloudflare-native/src/ingress.js:275-286` |
| `GITHUB_APP_ID` | Required to mint App tokens | Native broker or Node App client. `R/cloudflare-native/src/key-broker-core.js:41-55`; `R/server.mjs:52-62` |
| `GITHUB_APP_PRIVATE_KEY` | Node inline PEM, or native broker secret | Node prefers it to the path and expands escaped newlines. Native review containers do not receive it. |
| `GITHUB_APP_PRIVATE_KEY_PATH` | Node alternative to inline PEM | Read from disk by server and worker; not a native container setting. |
| `GILF_REPOS` | Empty list | Node server seeds repo configs at startup. **Not a deny-by-default allowlist.** See [Repo scoping](../configuration/repo-allowlist-and-scoping). `R/server.mjs:64-74` |
| `GILF_DB_PATH` | Unset: in-memory state | SQLite when set. Traces use the same database. `R/server.mjs:27-35` |
| `GILF_QUEUE_DB_PATH` | Falls back to `GILF_DB_PATH` | Shared local queue file for server and worker. Without either path or a remote queue, separate processes have separate in-memory queues. |
| `GILF_QUEUE_URL`, `GILF_QUEUE_TOKEN` | Both required for remote queue selection | Together override local queue selection. `R/server.mjs:36-50`; `R/src/worker-runner.js:29-39` |
| `PORT` | Node server `8787`; container `8080` | `R/server.mjs:108-112`; `R/cloudflare-native/container/entrypoint.mjs:391` |
Node automatically constructs `GitHubPublisher` when App id and private key are supplied (`R/server.mjs:52-62`; `R/worker.mjs:35-41`). **`GILF_PUBLISH_MODE=shadow` does not prevent Node publication.** That setting belongs to the native runtime.
## Models [#models]
OpenRouter is recommended. OpenAI direct, Anthropic direct and Codex CLI are alternatives. See [Model providers](../configuration/model-providers) for runnable, runtime-specific configuration.
| Variable | Code default / precedence |
| -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `GILF_MODEL_PROVIDER` | Generic resolver: then `GILF_CODEX_PROVIDER`, then `codex`. Native entrypoint: then `GILF_CODEX_PROVIDER`, then `openrouter`. Node runner does not read the current name. |
| `GILF_MODEL` | Generic resolver uses it after the role override. Native entrypoint passes it to the runner. Node runner does not read it. |
| `GILF_MODEL_PRIMARY`, `GILF_MODEL_SHADOW` | Generic resolver only: role value, `GILF_MODEL`, legacy shadow model for shadow, `GILF_CODEX_MODEL`, provider default. Neither entrypoint wires this resolver into its runner. |
| `GILF_CODEX_PROVIDER`, `GILF_CODEX_MODEL` | Node runner defaults: `codex`, `gpt-5.5`. Native accepts them as fallbacks, but only the provider is in `FORWARDED_KEYS`. Always supply native `GILF_MODEL`. |
| `GILF_CODEX_SHADOW_PROVIDER`, `GILF_CODEX_SHADOW_MODEL` | Runner defaults: primary provider, no shadow model. No model means no shadow call. Not forwarded on native Cloudflare. |
| `OPENROUTER_API_KEY`, `OPENAI_API_KEY`, `ANTHROPIC_API_KEY` | Provider-specific bare name, then respective `GILF_` alias in the provider abstraction. Native forwards bare names only. |
| `GILF_MODEL_PRICES` | Empty override map; JSON model-to-`input`/`output` USD per million tokens. Overrides built-in rates. Unknown model prices produce `null`, not free usage. Forwarded natively. |
| `GILF_OPENROUTER_REQUIRE_FREE` | On unless exactly `0`. Catalog lookup refuses nonzero or unknown prompt/completion prices. Native manifest explicitly sets `0`. |
| `GILF_OPENROUTER_MAX_TOKENS` | Unset. Despite the legacy name, the unified runner passes this cap to all HTTP providers (`R/src/codex-review-runner.js:2980-3024`). Not forwarded. |
| `GILF_OPENROUTER_BASE_URL` | `https://openrouter.ai/api/v1` |
| `GILF_OPENAI_BASE_URL` | `https://api.openai.com/v1` |
| `GILF_ANTHROPIC_BASE_URL` | `https://api.anthropic.com/v1` |
| `GILF_ANTHROPIC_VERSION`, `GILF_ANTHROPIC_MAX_TOKENS` | `2023-06-01`, `8192` |
| `GILF_OPENROUTER_REFERER`, `GILF_OPENROUTER_TITLE` | `https://agents.pelian.ai/gilf-pr-review/`, product name followed by ` PR Review System` |
| `GILF_CODEX_BIN`, `GILF_CODEX_HOME` | `codex`, home directory plus `.codex` |
| `GILF_CODEX_TIMEOUT_MS`, `GILF_CODEX_REASONING_EFFORT` | `2700000` ms, unset |
| `GILF_CODEX_SHADOW_TIMEOUT_MS`, `GILF_CODEX_SHADOW_REASONING_EFFORT` | Minimum of primary timeout and `120000` ms, unset |
The generic resolver's model defaults are `gpt-4.1` for OpenAI, `claude-sonnet-4-5` for Anthropic, `gpt-5.5` for Codex and `null` for OpenRouter. These are **not** the actual runner's per-provider defaults: the runner falls back to `gpt-5.5` regardless of provider. Explicitly select a model.
Sources: `R/src/model-provider.js:15-93,169-289,327-385`; `R/src/codex-review-runner.js:2022-2057,2215-2221,2320-2337`; `R/cloudflare-native/container/entrypoint.mjs:314-348`; `R/cloudflare-native/src/container-env.js:15-86`.
## Swarm, planner, inversion and evidence [#swarm-planner-inversion-and-evidence]
All rows here are runner-side settings. None are projected into native Cloudflare containers.
| Variable | Default / effect |
| --------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GILF_HYPOTHESIS_PRIMARY` | Off; exactly `1` permits usable hypothesis coverage to become primary. |
| `GILF_PRIME_SHADOW` | Off; exactly `1` enables a Prime worker. Also enabled by `GILF_HYPOTHESIS_PRIMARY=1` unless explicitly `0`. No worker means no swarm, not an automatic shadow swarm. |
| `GILF_HYPOTHESIS_CONCURRENCY` | `4` |
| `GILF_DISABLE_HYPOTHESIS_WORKERS` | Changes the harness manifest's `enabled` descriptor unless `1`; **not a runner kill switch**. |
| `GILF_HYPOTHESIS_PLANNER` | Off; exactly `1` enables planning, but no worker, a semantic skip or Codex planner provider skips the call. |
| `GILF_PLANNER_PROVIDER`, `GILF_PLANNER_MODEL` | Runner primary provider/model |
| `GILF_PLANNER_MAX_HYPOTHESES`, `GILF_PLANNER_MAX_DIFF_BYTES`, `GILF_PLANNER_TIMEOUT_MS` | `8`, `60000`, `60000` ms. Static hypotheses remain a floor. |
| `GILF_PRIME_PROVIDER`, `GILF_PRIME_MODEL`, `GILF_PRIME_THINKING` | Runner: `codex`; explicit runner model then `GILF_CODEX_MODEL` then `gpt-5.5`; `low`. |
| `GILF_PRIME_AGENT_BIN`, `GILF_PRIME_TIMEOUT_MS`, `GILF_PRIME_NO_CONTEXT_FILES` | `prime-agent`, `600000` ms, on unless exactly `0`. |
| `GILF_MODEL_INVERSION` | Off; accepts `1`, `true`, `yes`, `on`. |
| `GILF_MODEL_INVERSION_MAP` | JSON overrides the built-in author-family map; malformed JSON records an error and retains defaults. Human/unknown have no target. |
| `GILF_MODEL_INVERSION_MIN_CONFIDENCE` | `0.5` when unset or invalid; accepted range 0 to 1. |
| `GILF_EVIDENCE_ARTIFACTS` | Off; exactly `1` enables artifact processing. |
| `GILF_EVIDENCE_REQUIRE_ARTIFACTS` | Off; accepts `1`, `true`, `yes`, `on`; marks unsupported execution claims unverified when artifact processing runs. |
Sources: `R/src/codex-review-runner.js:2224-2226,2300-2314,2351-2366,3248-3267,3342-3353,3403-3405`; `R/src/prime-harness-adapter.js:153-163,212-222`; `R/src/author-model.js:15-16,161-231`; `R/src/harness-evolution.js:56-59`.
## Validation [#validation]
| Variable | Default / effect |
| --------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GILF_VALIDATION_EXECUTOR` | Explicit selection wins; otherwise repo `validation.provider`, then `GILF_VALIDATION_PROVIDER`, then `managed-crabbox`. Native projection supplies `managed-cf-sandbox` when unset. |
| `GILF_VALIDATION_PROVIDER` | Node env-derived repo config defaults to `crabbox`. |
| `GILF_VALIDATION_CODE_CHANGES_ONLY` | True unless exactly `false`; `0` does **not** disable this gate. |
| `GILF_INSTALL_TIMEOUT_MS`, `GILF_VALIDATION_TIMEOUT_MS` | Local collector defaults `900000` ms each. Not universal remote sandbox timeouts. |
| `GILF_GIT_TIMEOUT_MS` | `120000` ms |
| `GILF_VALIDATION_SHADOW_EXECUTOR` | Unset; must be a supported, different executor with a wired collector. |
| `GILF_VALIDATION_SHADOW_TIMEOUT_MS` | `600000` ms; clamps shadow lane config and bounds waiting. |
| `GILF_VALIDATION_COST_RATES` | JSON override of built-in CF/E2B rate cards, labeled `estimate:wall-clock`. Not a billing API. |
| `GILF_SKIP_CODEX_ON_VALIDATION_FAILURE` | Opt-in semantic skip; see source predicate and [Troubleshooting](../reference/troubleshooting). |
| `GILF_ALLOW_LOCAL_VALIDATION` | Host execution refused unless exactly `1`. Trusted repos only. |
| `GILF_CRABBOX_BIN`, `GILF_CRABBOX_ARGS` | `crabbox`, JSON array `[]` |
| `GILF_CRABBOX_CONFIG`, `GILF_CRABBOX_ALLOW_REPO_CONFIG` | No pinned path; repo executable config refused unless exactly `1`. Pinning alone does not bypass the refusal. |
| `GILF_CRABBOX_TIMEOUT_MS` | Collector default `2700000` ms |
| `E2B_API_KEY` | Required to execute E2B validation |
| `GILF_E2B_TIMEOUT_MS` | Collector fallback `600000` ms, but repo defaults can supply `2700000`. Native manifest explicitly supplies `600000`. |
| `GILF_E2B_CPU_COUNT`, `GILF_E2B_MEMORY_MB`, `GILF_E2B_TEMPLATE` | `2`, `512`, empty template |
| `GILF_E2B_ALLOW_INTERNET`, `GILF_E2B_ALLOWED_DOMAINS` | Repo default true, empty array. Internet disabled by `false` or `0`; domains are a JSON array. Not forwarded natively. |
| `GILF_E2B_BUDGET_USD` | Native dispatch default `50`; at the ceiling, withholds the exact `managed-e2b` shadow selector. D1 read errors log and leave the selector enabled. Not a primary-lane or monthly budget. |
| `GILF_CF_SANDBOX_ENABLED` | Native projection sets `1`; readiness hint, not a substitute for a wired collector. |
| `GILF_CF_SANDBOX_TIMEOUT_MS` | Collector default `1800000` ms; not forwarded. |
| `GILF_SELF_HOST_VALIDATION` | Exactly `1`, plus an executor command, required. |
| `GILF_SELF_HOST_EXECUTOR_CMD`, `GILF_SELF_HOST_EXECUTOR` | Command then legacy alias; unset otherwise. |
| `GILF_SELF_HOST_NETWORK`, `GILF_SELF_HOST_REPO_DIR` | Optional network intent; repo dir supplied by runner to the safe child environment, not an operator checkout override. |
| `GILF_SELF_HOST_TIMEOUT_MS` | `2700000` ms |
Sources: `R/src/validation-config.js:11-31`; `R/src/config.js:11-28`; `R/src/validation-executor.js:76-134,146-202,251-282,304-360,378-388,485-579`; `R/src/codex-review-runner.js:495-499,1152-1157,1254-1340,2335`; `R/cloudflare-native/src/cf-sandbox-validation.js:68-83`; `R/cloudflare-native/src/container-env.js:100-141`. See [Validation executors](../configuration/validation-executors) for merge precedence and isolation limits.
## Queue, context and worker lifecycle [#queue-context-and-worker-lifecycle]
These legacy queue settings configure `R/cloudflare/`, not the native Cloudflare Queue.
| Variable | Default / effect |
| ----------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GILF_QUEUE_LEASE_SECONDS` | Node server `300` seconds. Worker computes `3600` seconds with untouched timeout defaults; an explicit positive value overrides it. |
| `GILF_QUEUE_NAME`, `GILF_QUEUE_SHARDS` | `global`, `8` shards (maximum `256`) |
| `GILF_QUEUE_DEFAULT_LEASE_SECONDS`, `GILF_QUEUE_DEFAULT_MAX_ATTEMPTS` | `300`, `8` |
| `GILF_QUEUE_ACKED_TTL_SECONDS`, `GILF_QUEUE_DEAD_LETTER_TTL_SECONDS`, `GILF_QUEUE_CLEANUP_INTERVAL_SECONDS` | `86400`, `604800`, `3600` seconds |
| `GILF_CONTEXT_GRAPH_URL`, `GILF_CONTEXT_GRAPH_TOKEN` | Fall back respectively to queue URL/token; no client unless both resolve. |
| `GILF_WORKER_MAX_JOBS`, `GILF_WORKER_CONCURRENCY` | `1`, `1`; Node worker drains a bounded batch then exits. |
| `GILF_WORKER_ID` | Hostname, with process id appended by worker |
| `GILF_REVIEW_WORK_ROOT` | OS temp directory plus `gilf-pr-review-workspaces` |
| `GILF_REVIEW_KEEP_WORKSPACE` | Node keeps workspace only for exact `true` |
| `GILF_REVIEWER_MODE` | `codex`; other values skip constructing `CodexReviewRunner` |
| `GILF_BASE_BRANCH_FALLBACK` | `main` |
| `GILF_CODEX_FOR_DOCS_ONLY` | Off; exact `1` disables the model fast-path skip. Not forwarded natively. Memory, custom guidance, commands and competitor context also prevent this skip. |
The worker lease calculation uses a 10-minute model fallback while the runner model timeout is 45 minutes. Set explicit timeouts and a lease sized for the whole run; do not assume their defaults match.
Sources: `R/src/worker-runner.js:14-39,42-72`; `R/worker.mjs:43-66`; `R/src/codex-review-runner.js:1780-1785,2306,2317-2337,2620-2626`; `R/cloudflare/src/queue-routing.js:15-26`; `R/cloudflare/src/queue-do.js:37-49,138,202`.
## Publishing and observability [#publishing-and-observability]
| Variable | Default / effect |
| ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GILF_PUBLISH_MODE` | Native only: unset/blank becomes `shadow`; trimmed case-insensitive `shadow`/`live` accepted, other values throw. Manifest explicitly uses `live`. |
| `GILF_PUBLISH_ON_CODEX_FAILURE` | Off; exact `1` permits degraded fallback after semantic failure when usable hypothesis recovery is unavailable. Not forwarded natively. |
| `GILF_OPERATOR_UI_ENABLED`, `GILF_OPERATOR_UI_TOKEN` | Node dashboard disabled unless `1`; when enabled, missing/wrong token fails closed with `401 operator_auth_required` (`R/src/operator-dashboard.js:601-620`). |
| `GILF_TRACING`, `GILF_ENV` | Tracing on unless `0`/`false`; environment label `production`. Native entrypoint explicitly creates its tracer enabled. |
| `GILF_PRODUCT_NAME` | `Son of Anton` |
| `GILF_WATCHDOG_ALERT_CHANNEL`, `GILF_WATCHDOG_ALERT_ACCOUNT`, `GILF_WATCHDOG_ALERT_TARGET` | Node watchdog: `telegram`, `gilfoyle`, unset target |
| `GILF_BENCHMARK_RULES_PATH` | Current directory plus `pr-review-system/generated/benchmark-rules.md` |
`GILF_BROKER_URL`, `GILF_BROKER_TOKEN` and `GILF_BROKER_ALLOW_LOCAL_KEY` are not implemented configuration in these runtime entrypoints. Native uses the `KEY_BROKER` service binding and its outbound transport, not those design-era variables.
Sources: `R/cloudflare-native/src/publish-mode.js:26-69`; `R/cloudflare-native/container/entrypoint.mjs:273-302,378-385`; `R/src/codex-review-runner.js:2338,2706-2707`; `R/server.mjs:76-84`; `R/src/trace-emitter.js:9-10,54`; `R/src/branding.js:1`; `R/bin/gilf-pr-agent-watchdog.mjs:15-18`.
## Dashboard proxy and parity Worker [#dashboard-proxy-and-parity-worker]
`anton-ui/worker/proxy-utils.js:1-64` reads `SON_OF_ANTON_API_BASE` / `SON_OF_ANTON_API_TOKEN`, falling back to `GILF_OPERATOR_API_BASE` / `GILF_OPERATOR_UI_TOKEN`. Its interim `OPERATOR_REQUIRE_AUTH` gate is off unless truthy; when enabled, callers need `OPERATOR_ACCESS_TOKEN`. This is **not** the authentication design of `anton-ui-trace-parity`, whose Worker has session authentication. Do not copy the base UI's default-off gate into that deployment.
The parity UI requires session authentication (`anton-ui-trace-parity/worker/index.js:20-32`). Its `SON_OF_ANTON_ADMIN_TOKEN` supplies privileged backend requests; missing configuration gives `admin_backend_not_configured` (`:42-47`). WorkOS settings are implemented in this tree, not merely an unseen worktree: `WORKOS_CLIENT_ID`, `WORKOS_ISSUER` and comma-separated `WORKOS_ALLOWED_USER_IDS` are validated by `anton-ui-trace-parity/worker/workos-auth.js:11-18`. The issuer must be the client-scoped `https://api.workos.com/user_management/CLIENT_ID`, and the allowed-user list must be nonempty. These are not the complete session deployment prerequisites; they do not configure the original `anton-ui` bearer gate.
`P/cloudflare/src/env.js:1-10` resolves canonical `SON_OF_ANTON_` names before legacy `GILF_` names for suffixes its callers request. This is not a global alias system for Node or native container settings.
| Suffixes | Purpose |
| ------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------- |
| `QUEUE_ENQUEUE_TOKEN`, `QUEUE_WORKER_TOKEN`, `QUEUE_ADMIN_TOKEN` | Enqueue; claim/ack/fail/renew; queue inspection |
| `CONTEXT_GRAPH_TOKEN` | Context graph route authentication |
| `REVIEW_STATE_SYNC_TOKEN`, `REVIEW_STATE_READ_TOKEN` | State synchronization and health/read access |
| `OPERATOR_READ_TOKEN`, `OPERATOR_COMMAND_TOKEN`, `OPERATOR_BRIDGE_TOKEN`, `OPERATOR_ADMIN_TOKEN` | Route-specific operator read, command, bridge and administration access |
| `QUEUE`, `QUEUE_NAME` | Durable Object binding and routing name |
Sources: `P/cloudflare/src/worker.js:39-85,111-157`. Settings writes need the admin token; generated `anton_` API keys do not authorize policy writes. See [Dashboard settings](../configuration/dashboard-settings). Public source availability and license terms remain unestablished; these paths identify local source evidence, not public download promises.
# Model Providers (/docs/configuration/model-providers)
OpenRouter is recommended. OpenAI direct, Anthropic direct and Codex CLI are alternatives. The provider abstraction supports exactly `openrouter`, `openai`, `anthropic` and `codex`; Codex is a CLI marker rather than an HTTP `chat()` implementation (`son-of-anton-review/src/model-provider.js:15,169-325`).
All `R/` paths below mean `son-of-anton-review` on `feat/cloudflare-native`. Operator settings APIs on `son-of-anton-operator-parity` (`feat/greptile-operator-parity`) are a separate, unmerged tree. The two dashboard trees are also distinct.
## Configure the runtime you actually run [#configure-the-runtime-you-actually-run]
### Native Cloudflare [#native-cloudflare]
The container entrypoint reads `GILF_MODEL_PROVIDER`, then `GILF_CODEX_PROVIDER`, then defaults to `openrouter`. It passes `GILF_MODEL`, then `GILF_CODEX_MODEL`, to the runner. Only the former model name survives the Worker-to-container projection.
The checked-in manifest selects these **deployment values**, not library defaults:
```text
GILF_CODEX_PROVIDER=openrouter
GILF_MODEL=z-ai/glm-5.3-flash
GILF_OPENROUTER_REQUIRE_FREE=0
```
Provide the matching `OPENROUTER_API_KEY` Worker secret. HTTP OpenRouter, OpenAI and Anthropic do not need an interactive model CLI login. OpenRouter is not the only headless alternative.
Sources: `R/cloudflare-native/container/entrypoint.mjs:314-348`; `R/cloudflare-native/src/container-env.js:15-86`; `R/cloudflare-native/wrangler.jsonc:151-161`. Model slug availability and current provider prices are external facts; a checked-in slug is not a guarantee of continued availability.
### Node `worker.mjs` [#node-workermjs]
This entrypoint constructs `CodexReviewRunner` without a provider/model argument. The runner reads **legacy** provider/model names. Set both naming pairs if you also need dashboard status to agree:
```bash
export GILF_MODEL_PROVIDER=openrouter
export GILF_CODEX_PROVIDER=openrouter
export GILF_MODEL=z-ai/glm-5.3-flash
export GILF_CODEX_MODEL="$GILF_MODEL"
# Supply OPENROUTER_API_KEY securely in this process's environment.
export GILF_OPENROUTER_REQUIRE_FREE=0
```
The final line deliberately permits paid models. Omit it only when using a model whose catalog pricing passes the free-only guard. This config does not install credentials or start a worker.
`GILF_MODEL_PROVIDER` and `GILF_MODEL` alone do not configure this Node runner. Its fallback is `codex` / `gpt-5.5`, regardless of the generic resolver's provider-specific model defaults.
Sources: `R/worker.mjs:43-54`; `R/src/codex-review-runner.js:2320-2337`.
## Generic resolver versus active runner [#generic-resolver-versus-active-runner]
`resolveModelConfig` is an exported library helper, not the resolver used by either entrypoint above.
| Generic helper precedence | Value |
| ------------------------- | -------------------------------------------------------------------------------------------------- |
| Provider | `GILF_MODEL_PROVIDER`, then `GILF_CODEX_PROVIDER`, then `codex` |
| Model, primary | `GILF_MODEL_PRIMARY`, `GILF_MODEL`, `GILF_CODEX_MODEL`, provider default |
| Model, shadow | `GILF_MODEL_SHADOW`, `GILF_MODEL`, `GILF_CODEX_SHADOW_MODEL`, `GILF_CODEX_MODEL`, provider default |
| Provider defaults | OpenAI `gpt-4.1`; Anthropic `claude-sonnet-4-5`; Codex `gpt-5.5`; OpenRouter `null` |
Do not assume those role overrides or model defaults affect a deployed review. Explicit model selection avoids the actual runner's generic `gpt-5.5` fallback being sent to a different provider. Source: `R/src/model-provider.js:327-358`; compare the entrypoints cited above.
## Keys, endpoints and headers [#keys-endpoints-and-headers]
The application resolves each HTTP provider's nonblank bare key first, then its `GILF_` alias. These names are configuration inputs, not a promise that credentials never leave the process: HTTP clients send them in authentication headers.
| Provider | Key names | HTTP request |
| ---------- | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| OpenRouter | `OPENROUTER_API_KEY`, `GILF_OPENROUTER_API_KEY` | `POST https://openrouter.ai/api/v1/chat/completions` |
| OpenAI | `OPENAI_API_KEY`, `GILF_OPENAI_API_KEY` | `POST https://api.openai.com/v1/chat/completions` |
| Anthropic | `ANTHROPIC_API_KEY`, `GILF_ANTHROPIC_API_KEY` | `POST https://api.anthropic.com/v1/messages` |
| Codex | Local CLI authentication | Runner invokes `GILF_CODEX_BIN`, default `codex`; `CODEX_HOME` comes from `GILF_CODEX_HOME` or the user's `.codex` directory. |
The native projection forwards only bare provider keys. Its startup check asks whether **any** supported key is present; that does not prove the selected provider has a valid key. Codex's `configured: true` status likewise does not validate its installation or authentication.
| Variable | Default |
| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `GILF_OPENROUTER_BASE_URL` | `https://openrouter.ai/api/v1` |
| `GILF_OPENAI_BASE_URL` | `https://api.openai.com/v1` |
| `GILF_ANTHROPIC_BASE_URL` | `https://api.anthropic.com/v1` |
| `GILF_ANTHROPIC_VERSION` | `2023-06-01` |
| `GILF_ANTHROPIC_MAX_TOKENS` | `8192` |
| `GILF_OPENROUTER_MAX_TOKENS` | Unset; despite its name, the unified runner passes it to every HTTP provider (`R/src/codex-review-runner.js:2980-3024`) |
| `GILF_OPENROUTER_REFERER` | `https://agents.pelian.ai/gilf-pr-review/` |
| `GILF_OPENROUTER_TITLE` | Product name followed by ` PR Review System` |
OpenRouter sends `HTTP-Referer`, `X-Title` and `X-OpenRouter-Metadata: enabled`. These base URL, cap and attribution overrides are not forwarded by the native manifest's container projection.
Sources: `R/src/model-provider.js:24-37,169-307`; `R/src/codex-review-runner.js:2215-2221,2327-2332`; `R/cloudflare-native/container/entrypoint.mjs:319-324`; `R/cloudflare-native/src/container-env.js:15-70`.
## Free-only guard [#free-only-guard]
The runner enables the OpenRouter guard unless `GILF_OPENROUTER_REQUIRE_FREE` is exactly `0`. It fetches the configured base URL's `GET /models`, finds the exact model id and requires numeric zero prompt and completion prices. Missing models or missing/nonzero prices fail closed.
Representative source-defined errors include `OpenRouter model is required`, `OpenRouter model list returned invalid JSON`, and `OpenRouter model MODEL was not found`, where `MODEL` is substituted by the runner. A nonfree model error includes the actual prompt/completion prices.
Planner guard failure records `openrouter_model_not_free` and falls back to static planning; inversion guard failure retains the configured review provider. The guard does not obtain a billing quote and does not make a rate-card override authoritative. `GILF_MODEL_PRICES` cannot make a paid model pass it.
Sources: `R/src/codex-review-runner.js:2022-2057,2329,3385-3388,3493-3496`. See [Troubleshooting](../reference/troubleshooting).
## Shadow model and planner [#shadow-model-and-planner]
To enable a Node runner's shadow model, set `GILF_CODEX_SHADOW_MODEL` and optionally `GILF_CODEX_SHADOW_PROVIDER`. **`GILF_MODEL_SHADOW` alone does not start it.** The default shadow timeout is the smaller of the primary timeout and `120000` ms; override with `GILF_CODEX_SHADOW_TIMEOUT_MS`.
The shadow call runs after successful primary semantic analysis. It is skipped on deterministic fast paths or when no shadow model is selected. The result is stored as `shadowReview`; it is not a second published review. It still consumes provider time and tokens. None of these shadow model selectors are forwarded on native Cloudflare.
The planner uses `GILF_HYPOTHESIS_PLANNER=1`, with `GILF_PLANNER_PROVIDER` / `GILF_PLANNER_MODEL` defaulting to the actual runner's provider/model. It also requires a hypothesis worker; Codex cannot serve as its HTTP planner. Swarm and planner flags are not forwarded on native Cloudflare. See [Roadmap and flags](../reference/roadmap-and-flags).
Sources: `R/src/codex-review-runner.js:2324-2337,2361-2362,2650-2691,3198-3246,3342-3353`.
## Costs and dashboard status [#costs-and-dashboard-status]
Known token usage multiplied by a rate card produces `costUsd`. OpenAI and Anthropic have built-in tables; OpenRouter and Codex tables are empty. `GILF_MODEL_PRICES` overrides rates in USD per million input/output tokens. Unknown price or absent usage yields `null`. This is calculated telemetry, not an invoice; enter real applicable rates, not zero merely to remove an unknown-cost display.
`describeConfiguredProviders` returns `provider`, `selected`, `configured` and known/public model names, not key material. It reflects its own environment, not a probe of the review container. Both dashboard Models views let you browse these values without persisting model selection. On the parity branch policy `/operator/api/settings` is a policy document, not this provider-status response.
Sources: `R/src/model-provider.js:39-93,363-385`; `anton-ui/src/views/Settings.tsx:58-181`; `anton-ui-trace-parity/src/views/Settings.tsx:50-173`; `son-of-anton-operator-parity/cloudflare/src/review-policy-api.js:48-63`.
There are no separate Gemini, Bedrock, Vertex or Ollama provider implementations in the supported-provider switch. OpenRouter may route a model from another family when that model is offered by its catalog. An advertised model id in source is not evidence that a direct provider or external model is available today.
# Repo Allowlist and Scoping (/docs/configuration/repo-allowlist-and-scoping)
Admission differs between the Node service and the native Cloudflare runtime. Do not treat `GILF_REPOS` as a security allowlist or assume every declared repository setting is enforced by both paths.
`R/` means `son-of-anton-review` (`feat/cloudflare-native`). `P/` means `son-of-anton-operator-parity` (`feat/greptile-operator-parity`), a separate unmerged branch containing the operator Worker API.
## Node: `GILF_REPOS` seeds configuration [#node-gilf_repos-seeds-configuration]
```bash
export GILF_REPOS=owner/repo-a,owner/repo-b
```
`R/server.mjs:64-74` reads this comma-separated list at startup and calls `setRepoConfig` for each name with environment-derived validation settings. It also supplies the list to the Node dashboard. It does **not** reject signed deliveries for all other repositories.
`ReviewService` merges an absent repo record over defaults (`R/src/review-service.js:1295-1310`). Those defaults enable review, automatic review and the exact base branches `main` and `dev`. Limit GitHub App installation access deliberately; do not rely on absence from this seed list.
For automatic Node PR events, `shouldAutoReview` checks `enabled`, `review.auto`, the PR base branch and `review.onDraft`. A failed check is returned as `auto_review_disabled` (`R/src/config.js:86-97`; `R/src/review-service.js:716-717`).
## Native Cloudflare: stored enablement plus pinned policy [#native-cloudflare-stored-enablement-plus-pinned-policy]
Native ingress is a superset event filter, not the final authorization decision. The container hydrates a run from D1 and pins review policy. It does not read `GILF_REPOS`.
| Repository state at first policy snapshot | Native enabled state |
| ----------------------------------------- | ------------------------------------- |
| Stored repo config | Merged `config.enabled` |
| No config, previously reviewed | Enabled |
| No config, no previous review | `autoEnableNewRepos`, default `false` |
The reviewed-repo exception refers to analysis/publication statuses, not merely a PR head or a queued/skipped run. `evaluateRuntimeAdmission` refuses `repositoryEnabled === false` with `repository_disabled` before evaluating manual or automatic policy.
**Native branch-filter limitation:** the native entrypoint seeds and runs the review directly. Its hydration path reads repo enablement and manual authorization, but does not call Node's `shouldAutoReview`. Do not assume `branches.include`, `review.auto` or `review.onDraft` provide the same gate on this path. Use the pinned policy's branch filters, `autoReviewNewCommits` and `reviewDrafts` for native admission, and verify the resulting decision.
Sources: `R/cloudflare-native/src/ingress.js:54-77`; `R/cloudflare-native/src/d1-store-adapter.js:153-172`; `R/cloudflare-native/container/entrypoint.mjs:82-95,135-148`; `R/src/scm-store.js:24-28`; `R/src/review-policy-runtime.js:26-43`.
## Stored repository configuration [#stored-repository-configuration]
`DEFAULT_REPO_CONFIG` and `mergeRepoConfig` define this shape. The following are code defaults, not evidence of a particular workspace's saved policy:
```js
{
enabled: true,
mode: 'comment',
branches: { include: ['main', 'dev'] },
review: {
auto: true,
onDraft: false,
inlineComments: 'high_confidence_only',
lenses: ['security', 'correctness', 'tests', 'contracts'],
},
validation: { provider: 'crabbox', codeChangesOnly: true },
manual: {
enabled: true,
commands: ['@anton review', '@anton rerun', '@anton status', '@anton help',
'@gilf review', '@gilf rerun', '@gilf status', '@gilf help'],
allowAssociations: ['OWNER', 'MEMBER', 'COLLABORATOR'],
allowUsers: [],
},
competitor: { autoAudit: true, users: ['greptile-apps[bot]', 'greptile-apps'] },
publishing: { checkRun: true, review: true, stickyStatusComment: true },
}
```
Source: `R/src/config.js:1-97`. Nested Crabbox/E2B configuration is documented in [Validation executors](../configuration/validation-executors).
Important limits:
* Node `branches.include` uses exact equality, not globs. Its predicate does not read `branches.exclude`.
* `inlineComments` is declared configuration, not implemented per-line publication. The publisher sends review bodies, checks and issue comments, without inline positions (`R/src/github-publisher.js:22-79`).
* The parser recognizes its fixed four verbs; the declared `manual.commands` list is not a configurable parser grammar (`R/src/commands.js:1-26`).
* A stored key is not proof of a runtime consumer. In particular, the parity repo API accepts model fields and branch exclusions that must not be advertised as working native model/branch overrides.
## Workspace review policy [#workspace-review-policy]
For automatic runs, policy checks paused author, commit-trigger setting, draft setting, file count and filters. Missing metadata can defer a decision until hydration; it does not mean the PR matched.
| Setting | Default | Denial reason |
| ---------------------- | ------- | --------------------------------------- |
| `pausedAuthors` | `[]` | `author_paused` |
| `autoReviewNewCommits` | `true` | `new_commits_disabled` on `synchronize` |
| `reviewDrafts` | `false` | `draft_review_disabled` |
| `fileChangeLimit` | `500` | `file_change_limit` |
| `filters` | `[]` | `filters_not_matched` |
An authorized manual review bypasses the automatic checks **inside `evaluateReviewPolicy` after paused-author handling**. It does not bypass native repository disablement, authorization or the monthly model-budget gate. Unknown author metadata can still defer a manual request when authors are paused.
Sources: `R/src/operator-review-policy.js:154-180`; `R/src/review-policy-runtime.js:39-53`; `R/src/codex-review-runner.js:2376-2392`.
### Filter grammar [#filter-grammar]
A PR matches if any rule matches; each rule requires all its conditions. Empty rule lists match. Maximum 50 rules, 1 to 20 conditions per rule, up to 50 values per condition.
| Field | Operators | Value kind |
| -------------------------------------- | -------------------------- | ---------------------------------------- |
| `label`, `author`, `repository` | `is`, `is_not` | Strings; repository uses `owner/repo` |
| `targetBranch`, `sourceBranch`, `path` | `is`, `is_not` | Safe globs; `**` must be a whole segment |
| `title`, `keyword` | `contains`, `not_contains` | Case-insensitive substring |
| `draft` | `is`, `is_not` | Empty values list |
| `filesChanged` | `at_most`, `more_than` | One integer string from 0 to 100000 |
Example native target-branch policy:
```json
{
"filters": [{
"conditions": [{ "field": "targetBranch", "operator": "is", "values": ["main", "release/*"] }]
}]
}
```
This is a policy fragment, not the complete PATCH body. See [Dashboard settings](../configuration/dashboard-settings) for revision and authentication requirements. Source: `R/src/operator-review-policy.js:46-108,110-176`.
## Manual-command authority [#manual-command-authority]
`isAuthorizedCommenter` first permits a case-insensitive `manual.allowUsers` login match, otherwise checks the uppercased GitHub author association. Defaults permit `OWNER`, `MEMBER` and `COLLABORATOR`. `manual.enabled: false` disables manual commands.
The Node service also requires a known PR head, ignores non-command comments and deduplicates command ids. Native authorization uses the HMAC-authenticated comment in the delivered payload, not a caller-supplied actor string. Repeating an existing webhook is not the same as posting a new `@anton rerun` comment.
Sources: `R/src/commands.js:9-39`; `R/src/review-service.js:736-834`; `R/cloudflare-native/src/d1-store-adapter.js:162-172`; `R/cloudflare-native/src/ingress.js:80-114`.
## Editing repository config on the parity API [#editing-repository-config-on-the-parity-api]
This endpoint belongs to **`P/cloudflare/`**, not the native Worker's public ingress. Example for an operator who already has the admin token securely available:
```bash
curl -sS -X PATCH "$ANTON_OPERATOR/operator/api/repos/owner%2Frepo" \
-H "authorization: Bearer $SON_OF_ANTON_OPERATOR_ADMIN_TOKEN" \
-H 'content-type: application/json' \
-d '{"enabled":true,"branches":{"include":["main","dev"]},"reason":"enable main and dev"}'
```
`ANTON_OPERATOR` is an example shell variable holding your parity Worker origin, not product configuration. The Worker accepts canonical `SON_OF_ANTON_OPERATOR_ADMIN_TOKEN` or legacy `GILF_OPERATOR_ADMIN_TOKEN` configuration. Generated `anton_` machine keys do not grant repo-write authority.
Accepted patch fields: `enabled`; `branches.include` / `exclude`; `validation.provider` (`local`, `crabbox`, `e2b`, `shadow`) and `codeChangesOnly`; `model.provider`, `model.model`, `model.fallbackProvider`, `model.fallbackModel`, `model.reasoningEffort`; `alerts`; and `reason`. These are the patch validator's accepted fields, not proof every setting is consumed by the native engine. Writes record `repo-config-update`; enabling a repository whose synced GitHub access was removed returns `409`.
`GET /operator/api/repos` exposes repository rows. The original `anton-ui` Settings view is read-only; `anton-ui-trace-parity` adds actual per-repository toggles and disables enablement when access is removed.
Sources: `P/cloudflare/src/review-state-api.js:254-304,520-541`; `P/cloudflare/src/worker.js:65-80,115-135`; `anton-ui/src/views/Settings.tsx:190-305`; `anton-ui-trace-parity/src/views/Settings.tsx:209-218,253-264`.
## Token scope is caller-specific [#token-scope-is-caller-specific]
The native broker holds the App private key, while review clone and publish calls request scoped installation tokens. Publisher calls pass the target repository and narrow checks/pull-request permissions (`R/src/github-publisher.js:13-103`); clone requests ask for contents/metadata read access (`R/cloudflare-native/container/entrypoint.mjs:110-118`).
The broker helper itself does **not** require a nonempty repository list: `shapeTokenRequest` omits `repositories` when none are supplied. Do not describe it as a universal guarantee against installation-wide tokens (`R/cloudflare-native/src/key-broker-core.js:15-35`).
## Operator-side risk policy is separate [#operator-side-risk-policy-is-separate]
`loadReviewPolicy` reads `config/review-risk.yaml` and its scoring helper processes `risk_signals`, `negative_signals` and `hard_blocks`. This is distinct from the workspace admission policy above; a risk-policy design or file is not an extra native admission gate. Repository context files can contribute review guidance, not credentials or a replacement for operator authority. Sources: `R/src/review-policy.js:43-87`; `R/src/codex-review-runner.js:1499-1526`.
# Validation Executors (/docs/configuration/validation-executors)
The executor runs the PR's declared validation plan, which can include install, test, typecheck, lint and build steps. Selection and dispatch live in `src/validation-executor.js`. An unknown or unavailable sandbox is not permission to fall back to the worker host. The explicit `local` route is an exception: it runs on the host only after operator opt-in. A self-host command must enforce isolation itself.
Unless otherwise marked, source paths refer to `son-of-anton-review` on `feat/cloudflare-native`. The operator API on `son-of-anton-operator-parity` (`feat/greptile-operator-parity`) is separate and unmerged. See [Validation and Evidence](../code-review/validation-and-evidence).
## Selecting an executor [#selecting-an-executor]
Set `GILF_VALIDATION_EXECUTOR`. Resolution order (`resolveValidationExecutor`):
1. `GILF_VALIDATION_EXECUTOR` (explicit)
2. Per-repo `validation.provider`, then legacy `GILF_VALIDATION_PROVIDER`
3. Default: `managed-crabbox`
| Value | Aliases | Runs where | Ready when |
| -------------------- | ----------------------------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------ |
| `managed-crabbox` | `crabbox` | Compute selected by the operator's Crabbox configuration | `crabbox` binary available at run time, with trusted configuration |
| `managed-e2b` | `e2b` | Hosted E2B sandbox | `E2B_API_KEY` set |
| `managed-cf-sandbox` | `cf-sandbox`, `cloudflare-sandbox`, `cfsandbox` | Cloudflare Sandbox SDK, Cloudflare-native runtime only | `Sandbox` Durable Object binding present |
| `self-host` | `selfhost`, `self_host` | Your own container, VM or microVM | `GILF_SELF_HOST_VALIDATION=1` and `GILF_SELF_HOST_EXECUTOR_CMD` |
| `local` | | Worker host, trusted first-party repos only | `GILF_ALLOW_LOCAL_VALIDATION=1` |
| `shadow` | | Legacy combined Crabbox/E2B validation result | Both configured |
`shadow` and `local` are legacy routes, not in `SUPPORTED_EXECUTORS`, and cannot be a shadow lane (below).
Do not confuse legacy `GILF_VALIDATION_PROVIDER=shadow` with `GILF_VALIDATION_SHADOW_EXECUTOR`. The former combines both providers' summaries and missing validations, includes findings when both produced findings, and refuses host fallback when both are unavailable. It is not the telemetry-only shadow lane (`src/codex-review-runner.js:1451-1482`).
```bash
GILF_VALIDATION_EXECUTOR=managed-e2b
E2B_API_KEY=e2b_...
```
## managed-crabbox [#managed-crabbox]
| Variable | Default | Purpose |
| -------------------------------- | ------------------ | ----------------------------------------------------------------------------------------------- |
| `GILF_CRABBOX_BIN` | `crabbox` | Binary to invoke |
| `GILF_CRABBOX_ARGS` | `[]` | Extra args as a JSON array; choose flags supported by the installed Crabbox binary |
| `GILF_CRABBOX_CONFIG` | unset | Pinned config path, passed as `CRABBOX_CONFIG` so repo-local config in cwd is not the authority |
| `GILF_CRABBOX_TIMEOUT_MS` | `2700000` (45 min) | Per-run timeout |
| `GILF_CRABBOX_ALLOW_REPO_CONFIG` | unset | Set `1` to allow an in-repo Crabbox config |
Crabbox executes an in-repo `crabbox.yaml`, `crabbox.yml`, `.crabbox.yaml` or `.crabbox.yml` as project automation: it can pick a host-local provider or in-repo helper binaries. Reviews run on untrusted PRs, including forks, so the run is refused when such a file exists unless `GILF_CRABBOX_ALLOW_REPO_CONFIG=1`. Review and pin the config first.
## managed-e2b [#managed-e2b]
| Variable | Default | Purpose |
| -------------------------- | ----------------------------------------------------------------------- | --------------------------------------------------- |
| `E2B_API_KEY` | required | E2B credential |
| `GILF_E2B_TEMPLATE` | empty | Sandbox template |
| `GILF_E2B_CPU_COUNT` | `2` | vCPUs |
| `GILF_E2B_MEMORY_MB` | `512` | Memory |
| `GILF_E2B_TIMEOUT_MS` | Collector fallback `600000`; effective repo config can supply `2700000` | Per-run timeout |
| `GILF_E2B_ALLOW_INTERNET` | `true` | `false` or `0` cuts all egress |
| `GILF_E2B_ALLOWED_DOMAINS` | unset | JSON array allowlist, e.g. `["registry.npmjs.org"]` |
Do not treat the collector's 10-minute fallback as the effective timeout everywhere. `DEFAULT_REPO_CONFIG.validation.e2b.timeoutMs` is `2700000` (45 minutes), and `runSingleValidation` passes the merged repo timeout to the collector. The native manifest explicitly sets `GILF_E2B_TIMEOUT_MS=600000`. Choose an explicit timeout rather than relying on this mismatch (`src/config.js:18-28`, `src/validation-executor.js:508-517`, `src/codex-review-runner.js:1318-1328`, `cloudflare-native/wrangler.jsonc:143-150`).
Egress defaults open so registry installs work.
## managed-cf-sandbox [#managed-cf-sandbox]
The shipped native entrypoint wires this collector on its Cloudflare-backed store path, and `buildContainerEnv` supplies `GILF_VALIDATION_EXECUTOR=managed-cf-sandbox` by default plus `GILF_CF_SANDBOX_ENABLED=1`. Setting the readiness flag on Node does not wire the collector (`cloudflare-native/src/container-env.js:72-86`; `cloudflare-native/container/entrypoint.mjs:325-343`).
The lane needs the `Sandbox` Durable Object in `cloudflare-native/wrangler.jsonc`:
```jsonc
"containers": [
{ "class_name": "Sandbox", "image": "./sandbox.Dockerfile", "instance_type": "standard-3", "max_instances": 10 }
],
"durable_objects": { "bindings": [ { "name": "Sandbox", "class_name": "Sandbox" } ] },
"migrations": [ { "tag": "v2", "new_sqlite_classes": ["Sandbox"] } ]
```
`sandbox.Dockerfile:12-19` extends `docker.io/cloudflare/sandbox:0.12.9` and installs `git` and CA certificates. Validation commands run in that sandbox, not the model-key-holding review container. This describes the validation lane, not a guarantee about every possible model/worker tool.
Without the collector, the seam reports `has no Cloudflare Sandbox binding available (getSandbox collector not wired)`. Timeout: `GILF_CF_SANDBOX_TIMEOUT_MS`, collector default `1800000` (30 minutes). That timeout variable is **not forwarded** from the Worker to the container (`src/validation-executor.js:524-544`; `cloudflare-native/src/cf-sandbox-validation.js:68-83`; `cloudflare-native/src/container-env.js:15-70`).
## self-host [#self-host]
Runs the packaged repo and validation shell inside compute you control. Both settings are required; missing either refuses.
```bash
GILF_VALIDATION_EXECUTOR=self-host
GILF_SELF_HOST_VALIDATION=1
GILF_SELF_HOST_EXECUTOR_CMD=/path/to/run-in-sandbox
```
Your command is invoked as:
```text
```
It must start isolated compute, extract the archive, run the shell and return the `__GILF_VALIDATION_*__` markers on stdout. The runner waits `timeoutMs` plus 5 minutes. The command is split on whitespace, not parsed by a shell; use a wrapper executable rather than shell quoting or pipelines in this variable (`src/validation-executor.js:304-360`).
Isolation contract (`SELF_HOST_ISOLATION_CONTRACT`):
* **Secrets**: child env reduced to a safe allowlist. No GitHub App or token, webhook, queue or model keys reach the command.
* **Network**: egress is yours to enforce inside the container. Default-deny recommended.
* **Cleanup**: the packaging temp dir is removed after the run. Your command tears down its own compute.
| Variable | Default | Purpose |
| ----------------------------- | ------------------ | ------------------------------------------------- |
| `GILF_SELF_HOST_VALIDATION` | unset | Must be `1` |
| `GILF_SELF_HOST_EXECUTOR_CMD` | unset | Command to invoke |
| `GILF_SELF_HOST_NETWORK` | unset | Free-form egress intent, forwarded to the command |
| `GILF_SELF_HOST_TIMEOUT_MS` | `2700000` (45 min) | Timeout passed as the third argument |
| `GILF_SELF_HOST_REPO_DIR` | set by runner | Checkout path, exported to the command env |
## local [#local]
Refused by default: it runs PR code with the worker's secrets in reach. Trusted first-party repos only:
```bash
GILF_VALIDATION_EXECUTOR=local
GILF_ALLOW_LOCAL_VALIDATION=1
```
## Shadow lane [#shadow-lane]
A second executor runs the same PR concurrently for comparison. Its result becomes telemetry and is not merged into primary findings, missing validations or the published result. The wait is bounded, but the lane still consumes compute. The `validation-executor` Test Lab definition lives on the separate operator-parity branch.
| Variable | Default | Purpose |
| ----------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GILF_VALIDATION_SHADOW_EXECUTOR` | unset | One of `SUPPORTED_EXECUTORS`. Must differ from the primary or the lane does not run |
| `GILF_VALIDATION_SHADOW_TIMEOUT_MS` | `600000` | Caps lane timeout config and bounds the wait; a timeout is reported as shadow telemetry |
| `GILF_E2B_BUDGET_USD` | `50` | Native dispatch only: sums `validation.e2b` span costs and withholds the exact `managed-e2b` shadow selector at the ceiling. D1 read errors log `e2b_budget_read_failed` and leave it enabled. Not a primary or monthly budget. |
| `GILF_VALIDATION_COST_RATES` | unset | JSON rate-card override per executor for `estimate:wall-clock` costs |
Lane timeouts are clamped to the shadow budget. Rollback: remove `GILF_VALIDATION_SHADOW_EXECUTOR` and redeploy.
## Per-repo validation config [#per-repo-validation-config]
Per-repo config is stored as `repo_configs.config_json` in D1 or SQLite; the Node server seeds validation config from its environment. `DEFAULT_REPO_CONFIG` in `src/config.js:11-28` contains:
```json
{
"validation": {
"provider": "crabbox",
"codeChangesOnly": true,
"crabbox": { "args": [], "timeoutMs": 2700000 },
"e2b": {
"timeoutMs": 2700000,
"cpuCount": 2,
"memoryMB": 512,
"template": "",
"allowInternet": true,
"allowedDomains": []
}
}
}
```
`provider` feeds step 2 of executor resolution, but first the runner merges configuration. **Top-level default config wins over run config**, while nested Crabbox/E2B run fields win over their defaults. Thus environment-derived provider/code-changes-only defaults can override stored top-level choices. `codeChangesOnly: true` skips docs-only and generated-artifact-only diffs; the env parser disables it only for `GILF_VALIDATION_CODE_CHANGES_ONLY=false`, not `0` (`src/validation-config.js:11-31`; `src/codex-review-runner.js:1529-1543,2528-2561`).
Native forwards the E2B key, shape/time limits and shadow selector, but not the E2B egress variables, Crabbox configuration or self-host command/opt-in. Do not assume the presence of an executor selector means all of its configuration reaches the container. See [Environment reference](../configuration/environment-reference).
## Readiness view [#readiness-view]
`describeValidationExecutors(env)` reports selection and configuration hints without secrets. Crabbox reports configured before binary availability is checked; E2B checks key presence; Cloudflare checks the readiness flag; self-host reports opt-in and command presence. These are not live connectivity tests (`src/validation-executor.js:99-134`).
The parity `/operator/api/settings` handler returns review policy, not this executor descriptor (`son-of-anton-operator-parity/cloudflare/src/review-policy-api.js:48-63`). Refusal wording differs by route: the generic seam emits `REFUSED validation (...)`, local emits `REFUSED local validation`, and managed collectors supply their own summary before sandbox-unavailable normalization. Inspect `missingValidations` and `validation_refused`, not one universal string.
Source evidence: `src/validation-executor.js:30-96,251-282,287-360,378-430,485-579`; `src/codex-review-runner.js:1254-1340`; `cloudflare-native/src/container-env.js:100-141`. A refusal is an infrastructure gap, not a passed validation or proof of a PR regression.
# Agent Onboarding (/docs/getting-started/agent-onboarding)
There is no unattended public installation path. Start with an authorized `son-of-anton-review` checkout on `feat/cloudflare-native`. The handoff records the GitHub repository as private, and this checkout has no LICENSE. The owner must grant source access and clarify permitted use. Do not invent a public fork, license, credentials, installation ID or deployment target.
`bin/gilf-review.mjs` is an operator CLI, not an onboarding command. Its wrapper calls `executeCli` in `src/operator-cli.js`; it does not create an App, provision a host or configure Cloudflare.
## Connect with MCP v2 [#connect-with-mcp-v2]
Son of Anton MCP uses the stable TypeScript SDK v2 and supports the `2026-07-28` protocol, with the SDK's compatibility path for older clients. There are two intentionally separate access modes:
| Mode | Connection | Authority |
| ----------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| Hosted onboarding | `https://mcp.anton-son.com/mcp` over Streamable HTTP | Public docs, search, onboarding plans and connection guidance. No operator credentials or private repository access. |
| Local operator | `anton-mcp/bin/anton-mcp.mjs` over stdio from an authorized checkout | Calls the configured operator API with credentials supplied to the local process. Read-only by default. |
The hosted endpoint does not proxy private operations. It rejects `Authorization` headers: **do not send operator keys to it**. No OAuth sign-in or API key is needed for public onboarding.
### Hosted connection [#hosted-connection]
Add the URL above as a Streamable HTTP server in your MCP client. For clients that support URL entries in `mcpServers`, such as Cursor:
```json
{
"mcpServers": {
"anton-onboarding": {
"url": "https://mcp.anton-son.com/mcp"
}
}
}
```
Start with `anton_get_onboarding_plan` using `runtime: "node"` or `runtime: "cloudflare-native"`. A plan reports human gates as unknown; it does not inspect a checkout, authorize spending or provision anything.
Public tools are `anton_search_docs`, `anton_read_doc`, `anton_get_onboarding_plan` and `anton_connection_status`. Resources include `anton://docs/index`, `anton://docs/full`, `anton://docs/getting-started/agent-onboarding`, and `anton://onboarding/node` or `anton://onboarding/cloudflare-native`. Prompts are `anton_onboard`, `anton_review_pull_request` and `anton_triage_failure`.
### Local operator connection [#local-operator-connection]
Obtain owner-authorized access to the `anton-mcp` package; it is private, not a published npm package. Run `npm ci` in that directory. The standalone MCP requires Node 20.3 or newer; the **engine** runbook below separately requires Node 25.
Use an absolute path in your client's stdio configuration:
```json
{
"mcpServers": {
"anton": {
"command": "node",
"args": ["/absolute/path/to/authorized/workspace/anton-mcp/bin/anton-mcp.mjs"]
}
}
}
```
Configure the following through your MCP host's approved environment/secret mechanism, then restart the local server. Do not paste credentials into chat or commit them in shared client configuration.
| Environment variable | Meaning |
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ANTON_API_URL` | Operator API **origin** shown in the dashboard's API-key screen. HTTPS required except loopback for local development. Do not assume the dashboard or native engine URL serves this API. |
| `ANTON_API_KEY` | Revocable `anton_…` key with `read` scope, created by an authorized user in [the dashboard](https://app.anton-son.com). |
| `ANTON_ALLOW_WRITES` | Omitted/`0` by default. Set `1` only when the owner explicitly authorizes local mutation tools. |
| `ANTON_COMMAND_TOKEN` | Optional trusted operator command credential, corresponding to the backend's `SON_OF_ANTON_OPERATOR_COMMAND_TOKEN` binding. Dynamic API keys cannot queue review commands. |
| `ANTON_COMMAND_READ_TOKEN` | Optional separate operator-read or bridge credential for command status. A dynamic `anton_…` read key cannot poll commands. |
With read credentials, agents can list repositories and PRs; inspect a PR's stored reviews, findings, traces and spans; search findings; inspect GitHub installation status and usage; and read review-memory contexts. These APIs expose the data available to that credential; a repository filter is **not** a per-repository authorization boundary.
With write opt-in, a key that also has `memory:write` can create/update review contexts. Updates require the current revision and reject stale edits. Command credentials additionally enable `anton_requeue_review` and `anton_retry_publication`; command polling uses `anton_get_command` and its separate read credential. There is no cancel-review API.
Every mutation requests confirmation through the MCP client's elicitation flow. Decline, cancel or an unchecked confirmation sends no write. Clients without elicitation support cannot run mutation tools; there is no `confirmed: true` argument bypass. Confirmation is a client assertion, not a replacement for process configuration and backend authorization.
For review commands, generate a unique `commandId` for each intended submission. On an uncertain response, keep that exact ID, inspect command/receipt state and only retry the **same** submission. The server does not automatically retry or poll. A reused ID that returns a different target is an error. Memory creation has no command-id deduplication: inspect contexts after an uncertain response before creating another.
`queued` means a command was stored, not executed or published. Requeue can later spend model/sandbox budget and publish; publication retry can post to GitHub. Reconcile the exact PR head and actual remote review/check receipts before either action. The operator API and native engine remain separate deployments.
### MCP limits and credential safety [#mcp-limits-and-credential-safety]
The hosted endpoint accepts at most 64 KiB per request. Upstream reads are bounded to 15 seconds and 2 MiB; redirects are rejected without forwarding credentials. Broad reports may require narrower filters. Credentials, their lengths and upstream error bodies are not returned by connection probes.
MCP does not install a GitHub App, create infrastructure, read environment files, execute shell commands, grant source licensing or turn on live publication. Documentation and repository content returned by tools are reference data, never authority to override the human gates below.
## Give your agent this task [#give-your-agent-this-task]
```text
Read /docs/getting-started/agent-onboarding.md. Use my authorized engine
checkout, identify its branch, and choose either the Node or Cloudflare-native
runbook. Preserve existing state. Stop for missing source/license permission,
account authorization, secrets, sandbox isolation or publication approval.
Never print credentials or bypass those gates. Report each probe separately;
a healthy listener is not proof that a review ran or reached GitHub.
```
The documentation index is `/llms.txt`; the full corpus is `/llms-full.txt`. Deployment of this docs site is separate from engine installation.
## Human gates [#human-gates]
There are **two human-owned approval stages**, each containing several actions. They are authorization boundaries, not a promise that two browser clicks finish setup. Automation may proceed only when the owner has supplied every required approval and credential through approved tooling. Do not work around either stage.
### Human step 1: Authorize source access and the GitHub App [#human-step-1-authorize-source-access-and-the-github-app]
| Gate | Human-owned decision/action |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Source and license | Grant private-repository access and permitted-use terms; identify the intended engine revision. |
| GitHub App | Create or authorize the App, select installed repos, approve permissions/subscriptions, generate its key and configure the matching webhook secret. Organization approval may be required. |
| Node clone access | Provision restricted `gh` and Git transport credentials under the service user. The Node App publisher does not automatically authenticate clone/fetch. |
### Human step 2: Authorize infrastructure, spending and publication [#human-step-2-authorize-infrastructure-spending-and-publication]
| Gate | Human-owned decision/action |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Model and sandbox | Select provider/model, provision keys, authorize spending and choose isolation. A self-host command must actually enforce its own container/VM boundary. |
| Infrastructure | Approve the public HTTPS endpoint, host/storage ownership or Cloudflare account/resources. Cloudflare authentication, entitlement and quota cannot be inferred from source. |
| Authority cutover | Approve the canary repo and live publication. Stop/drain any competing publisher or scheduler before moving authority. |
Never set unsafe local validation merely to make a probe pass. Never place the App key in the native main Worker or container. Do not expose the older `anton-ui` proxy assuming it has the WorkOS session protection implemented in `anton-ui-trace-parity`.
## Machine steps: Node [#machine-steps-node]
### 1. Identify the checkout, without changing branches [#1-identify-the-checkout-without-changing-branches]
From the authorized checkout:
```bash
pwd -P
git rev-parse --show-toplevel
git branch --show-current
git rev-parse HEAD
node --version
git --version
gh --version
npm ci
```
Use Node 25, matching the shipped image and its `node:sqlite` requirement. Do not switch a shared worktree or overwrite existing config. In this workspace, `son-of-anton-operator-parity` and `anton-ui-trace-parity` have `.git` files pointing to worktree metadata, not standalone `.git` directories. They are not an engine/operator combined checkout.
### 2. Prepare the approved service environment [#2-prepare-the-approved-service-environment]
Follow [Self-host quickstart](/docs/getting-started/quickstart-self-host#2-create-a-dedicated-environment-file) to create `/etc/son-of-anton/engine.env` and service-owned storage. That example path is operator-chosen, not a runtime default. Required inputs for the documented E2B/OpenRouter path are:
* App ID, webhook secret and private-key path.
* Absolute `GILF_DB_PATH` and writable workspace directory.
* Explicit `GILF_CODEX_PROVIDER=openrouter`, `GILF_CODEX_MODEL`, provider key and paid-model decision. The current Node entrypoint does not wire the modern model-selection helper.
* Explicit E2B executor/key and timeout.
* Restricted clone/fetch authentication under the worker's OS user.
Have the human enter secrets outside chat. `GILF_REPOS` seeds config but does not exclude unlisted repos. Restrict the App installation itself. Leave remote queue overrides unset for the shared-SQLite setup. Both processes must see the same persistent state and queue.
### 3. Start ingress, then probe it [#3-start-ingress-then-probe-it]
In a dedicated terminal or supervisor, from the engine root:
```bash
node --env-file=/etc/son-of-anton/engine.env server.mjs
```
In a second terminal:
```bash
curl --fail-with-body --silent --show-error --max-time 10 http://localhost:8787/health
printf 'curl_exit=%s\n' "$?"
```
Expect JSON with `ok: true` and `service: "son-of-anton-pr-review"`. The legacy `validation.provider` field is not authoritative for `GILF_VALIDATION_EXECUTOR`. Do not echo the environment or key file.
### 4. Receive the canary event and drain [#4-receive-the-canary-event-and-drain]
After the human configures `/github/webhooks` on the public HTTPS endpoint, open an approved non-draft PR targeting `main` or `dev`, or receive a PR event for an existing PR. The Node handler needs that stored head before commands can work. An owner, member or collaborator then posts a **new** `@anton review` comment.
Run the one-shot worker from the engine root:
```bash
node --env-file=/etc/son-of-anton/engine.env worker.mjs
```
It exits after its batch; it is not a watcher. Schedule later invocations using [Run on a Node box](/docs/self-host/run-on-a-node-box). Node publishes when App credentials are configured; `GILF_PUBLISH_MODE=shadow` does not protect this path.
### 5. Probe durable state and the remote result [#5-probe-durable-state-and-the-remote-result]
With the optional `sqlite3` CLI, using the same database path as the service:
```bash
sqlite3 -readonly /var/lib/son-of-anton/anton.db 'SELECT review_key,status FROM review_runs ORDER BY rowid DESC LIMIT 10;'
```
Match the canary repository, PR and head, not merely any row. Read that run's analysis/publication details locally without dumping unrelated stored payloads into chat. Confirm its GitHub review/check receipts and validation gaps. `@anton status` is a Node command response, not a replacement for this check.
## State-probe truth table [#state-probe-truth-table]
Evaluate rows in order. An unexecuted or inaccessible probe is **unknown**, never false or successful by assumption.
| Source/auth gates | Health | Matching durable run | GitHub receipt | Interpretation and action |
| ----------------- | --------------------------------------------------- | ------------------------- | --------------------- | ----------------------------------------------------------------------------------------------------------------- |
| Missing/unknown | Any | Any | Any | Blocked. Complete the named human gate; do not attempt provisioning or publication. |
| Ready | Unreachable, non-2xx, invalid JSON or wrong service | Any | Any | Ingress not verified. Check the selected process, port and proxy. |
| Ready | Correct listener response | Probe fails/unavailable | Any | Durable state is unknown. Fix path/access/schema; do not create a replacement DB to get green. |
| Ready | Correct listener response | No matching run | None | No review proved. Inspect the webhook, stored head, policy and queue. Do not blindly resend. |
| Ready | Correct listener response | Queued/running | None | Work pending. On Node, schedule/drain the worker; investigate stale work before retrying. |
| Ready | Correct listener response | Skipped/failed/incomplete | None | Read the recorded reason. Policy refusal or unavailable validation is not a successful review. |
| Ready | Correct listener response | Reports publication | Missing/unknown | Publication unverified. Recording publishers can report successful local state; reconcile GitHub before retrying. |
| Ready | Correct listener response | Matching completed run | Matching live receipt | Canary publication proved. Report validation gaps separately; this is not an estate-wide health claim. |
### Exit codes are narrower than readiness [#exit-codes-are-narrower-than-readiness]
| Command/result | Meaning |
| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| Health curl exits 0 | HTTP request succeeded; inspect the JSON. It did not test the model, broker or sandbox. |
| Health curl exits 22 | With `--fail-with-body`, HTTP error response; inspect status/body. |
| Health curl exits 7 / 28 | Connection failed / timeout. |
| `server.mjs` exits 1 with `Missing GITHUB_WEBHOOK_SECRET` | Required secret absent from its environment. |
| `worker.mjs` exits 0 | Batch invocation ended. The drainer catches per-job failures, so inspect persisted state even on exit 0. |
| SQLite query succeeds with no rows | Query worked; no matching review was demonstrated. |
## Cloudflare-native alternative [#cloudflare-native-alternative]
Follow [Cloudflare quickstart](/docs/getting-started/quickstart-cloudflare) and the [full runbook](/docs/self-host/run-on-your-cloudflare-account), not the Node launch commands above:
1. Confirm account authorization and replace every deployment-specific binding ID and App/installation value.
2. Explicitly change the checked-in **live** publication and cron vars to **shadow**.
3. Provision resources; apply `schema.sql` **and** migration 002 on a fresh D1 database. Use guarded upgrades for existing state.
4. Enable one installed canary repo in D1. Default native policy disables an unseen repository; `GILF_REPOS` does not enable it.
5. Have the human supply main-Worker webhook/model secrets and the broker-only private key.
6. Deploy the broker and then the native main Worker. These commands do not deploy the parity operator API or dashboard.
7. Probe ingress, trigger a new `@anton review`, and inspect D1 run/trace state. Native `@anton status` and `@anton help` are ignored by the consumer.
8. Get live-publication approval, ensure no competing publisher remains, flip publication and verify an actual GitHub receipt. Cron authority is a separate decision.
Shadow suppresses review publication, not GitHub reads, model spending, validation or state writes. Report the exact runtime and mode with the canary evidence.
## Completion report [#completion-report]
Return the engine revision, deployment mode, configured resource names (not secrets), each human gate's status, probe outputs with sensitive payloads excluded, canary head/review/check identifiers, validation gaps and unresolved blockers. Do not claim a live deployment or licensing approval from source inspection alone.
# Anatomy of a Review (/docs/getting-started/anatomy-of-a-review)
This page describes the publisher and formatter in `son-of-anton-review` on `feat/cloudflare-native`. It applies when a run reaches live publication. Native shadow runs use a recording publisher; policy-skipped runs are not completed semantic reviews. The expanded dashboard policy API is in the separate `son-of-anton-operator-parity` tree, not the native main Worker.
## What gets posted [#what-gets-posted]
Publication is keyed by review run, not by PR lifetime. A new head or a new manual-comment variant can create another run. The publisher exposes these surfaces:
| Surface | Where | Behaviour |
| --------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| PR review | Reviews tab | POSTed with a `` marker and pinned commit. Event is `COMMENT`, or `APPROVE` when the full auto-approval policy passes. It is not edited in place across pushes. |
| Check run | Checks tab, named `Son of Anton / review` | Created when no check ID is stored, then PATCHed through run transitions. Enabled by default; policy can disable it. |
| Status comments | PR conversation | Off by default (`statusChecks.postStatusComments`). Receipt and completion comments are separate POSTs with their own markers, not one continuously edited sticky comment. |
There are no inline per-line comments or suggestion patches in this publisher. It also supports an optional PR-description update, described below. Publication claims and receipts reduce duplicate mutations; an ambiguous remote outcome still needs reconciliation before retrying.
## The header line [#the-header-line]
The check run summary is one line:
```text
[CLEAR] Safe to merge. Analyst confidence 4/5.
[CAUTION] Safe to merge with 2 high-risk follow-ups. Analyst confidence 3/5. Required confidence 4/5 was not met.
[BLOCK] Not safe to merge until 1 blocker is fixed. Analyst confidence 2/5.
```
* Merge status is `CLEAR`, `CAUTION`, or `BLOCK`. Blockers force `BLOCK`. High findings, a `needs-attention` verdict, incomplete validation, or an analyzer failure force at least `CAUTION`.
* `Analyst confidence N/5` is a recorded 1 to 5 score, not a probability or proof that validation passed. The deterministic docs-only path also supplies a score. When confidence display is enabled but no score exists, the summary says `Analyst confidence was not recorded.` Disabling `summary.confidence` hides that text.
* When `statusChecks.requiredConfidence` is set above 0 and the score falls short, the line ends with `Required confidence N/5 was not met.` and a `CLEAR` status is downgraded to `CAUTION`.
## Check run conclusion [#check-run-conclusion]
| Condition | Conclusion |
| ----------------------------------------------------------------------------------------------------- | ---------- |
| `requiredConfidence` > 0 and score missing or below it | `failure` |
| Merge status is `CAUTION` or `BLOCK` | `neutral` |
| `requiredConfidence` > 0 but semantic coverage is incomplete, degraded, or pinned to a different head | `neutral` |
| Otherwise | `success` |
Blockers therefore do not produce `success`. In this conclusion function, `failure` requires a configured confidence threshold; it does not mean that every blocking finding makes the GitHub check fail. A threshold can come from stored policy, not necessarily a dashboard.
## Review body sections [#review-body-sections]
Sections appear in this order. Empty sections are omitted.
| Section | Content |
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| Custom header | Optional `commentHeader` text from policy. |
| Son of Anton Summary | One-paragraph overview plus bullet highlights. |
| Merge Status | `CLEAR`, `CAUTION`, or `BLOCK` and the merge verdict sentence. Always shown. |
| Analyst Confidence: N/5 | The reasoning behind the score. |
| Important Files Changed | Table of `Filename` and `Overview`. |
| Findings | Findings not established as outside the diff. Missing changed-path metadata is unknown scope, not evidence that a finding is outside it. |
| Outside-Diff Findings | Findings on paths not in the PR. Collapsible by default. |
| Validation Status | Validation evidence lines and `MISSING:` items. Always shown when present. |
| Cross-Repo Impact | Supplied impact bullets. The graph is repository-scoped; the heading does not prove a multi-repository index or a verified effect in another repo. |
| Competitor Benchmark | Only when competitor review context was captured. Winner, benchmark confidence, agreements, misses, action items. |
| ASCII Flow | A fenced `text` block with plain-text arrows and boxes. The prompt forbids Mermaid syntax here. |
| Prompt to Fix | Off by default (`promptToFix`). A quoted prompt listing actionable findings, pinned to the head commit. |
| Footer | `Reviewed by Son of Anton for commit .` |
### Findings [#findings]
Each finding is one bullet:
```text
- **HIGH** Missing null check on session token in `src/auth/session.js`
- The body explains the defect and how to reproduce it.
- Evidence: log https://...
- Unverified: Finding claims execution-backed evidence but no evidence artifact ... was persisted for it.
```
* The model schema requires `severity`, `category`, `path`, `title`, `body` and `contextIds`. Model severities are `blocker`, `high`, `medium`, `low`; the renderer uppercases whatever severity it receives. There are no required line-number fields.
* The renderer prepends `[unverified]` only when `evidenceStatus` is explicitly `unverified`. It does not independently verify artifacts. Execution-artifact processing is gated by `GILF_EVIDENCE_ARTIFACTS`; enforcement additionally uses `GILF_EVIDENCE_REQUIRE_ARTIFACTS`. Both are off by default and neither is forwarded by the native container env allowlist.
* Policy `strictness=low` leaves findings unchanged. `medium` hides medium findings only when a declared confidence is below 0.8; unknown confidence remains visible. `high` hides medium and low findings except protected findings. Blocker/high findings and findings matching the safety-protection rules are retained.
### Validation Status [#validation-status]
Validation entries are strings supplied by the runner. These are illustrative formats, not output from a particular run:
```text
PASS test: `npm test` succeeded in 42s.
FAIL lint: `npm run lint` exited with status 1. Output: ...
SKIPPED validation: no standard validation scripts were declared.
INCONCLUSIVE build: `npm run build` was not run. validation process aborted in the sandbox.
MISSING: Codex semantic analysis was skipped because this was an automatic docs-only review.
```
`SKIPPED` means the named operation did not run. Automatic docs-only reviews can use a deterministic semantic fast path when there is no review memory, operator guidance, manual command or competitor context, and `GILF_CODEX_FOR_DOCS_ONLY` has not enabled the model pass. That fast path returns `CAUTION`, confidence 3/5, no findings and a missing-semantic-analysis entry. A manual `@anton review` bypasses this semantic skip, but does **not** necessarily force sandbox validation on a docs-only diff or bypass executor/policy failures.
## Finding evolution across pushes [#finding-evolution-across-pushes]
The service compares available prior findings with the current result and stores `findingEvolution`. Matching uses fingerprints and similarity:
| Label | Meaning |
| ------------ | ---------------------------------------------------------------------------------------------------------------------- |
| `new` | No prior finding matched by fingerprint or similarity. |
| `persisting` | Exact match with a prior finding. |
| `modified` | Similar to a prior finding but the text or location changed. |
| `resolved` | A previous finding is omitted and not retained in the unresolved set. This classification alone is not proof of a fix. |
| `unresolved` | An omitted prior concern remains unverified, with its source provenance retained. |
These labels are stored, not rendered as finding tags in the review body. Same-head continuity uses evidence-backed dispositions rather than silently dropping old concerns. The outcome ledger records `unknown` when the new semantic analysis is incomplete. Do not equate evolution labels with the addressed-rate methodology of the separate parity reports API.
## PR description block [#pr-description-block]
With `updatePrDescription` enabled (default off), the review markdown is also written into the PR description between owned markers:
```html
...review markdown...
```
With neither marker present, the block is appended. With one valid pair it is replaced, preserving the surrounding text. A lone marker, duplicates, reversed markers, reserved markers in the generated review, or an unreadable description causes refusal. The publisher also refuses if a fresh GitHub read shows that the PR head moved.
## What stored review policy can hide [#what-stored-review-policy-can-hide]
The `summary.*` settings control optional sections. Each has `enabled`, `collapsible`, and `defaultOpen`. These are their defaults:
```json
{
"summary": {
"summary": { "enabled": true, "collapsible": false, "defaultOpen": false },
"confidence": { "enabled": true, "collapsible": false, "defaultOpen": false },
"files": { "enabled": true, "collapsible": false, "defaultOpen": false },
"diagram": { "enabled": true, "collapsible": false, "defaultOpen": false },
"outsideDiff": { "enabled": true, "collapsible": true, "defaultOpen": false }
}
}
```
Merge Status and the footer always render. Main findings and validation/impact sections render when populated and are not controlled by these section toggles; finding filtering still applies. Outside-Diff Findings can be hidden, and hiding them also excludes those findings from Prompt to Fix. Optional `featureTips` can add a tip before the footer.
## Rerun a review [#rerun-a-review]
Authorized commenters (`OWNER`, `MEMBER`, `COLLABORATOR` by default) can comment:
```text
@anton rerun
```
`@anton review` and `@anton rerun` request a manual run; a new comment ID gives it a new variant. `@gilf` is a parser alias. Node resolves the target from its stored PR head, so it needs an earlier PR event; the native consumer resolves a missing head from GitHub. Node also replies to `status` and `help`, whereas the native consumer ignores those commands. See [Triggers and commands](/docs/code-review/triggers-and-commands).
# What is Son of Anton? (/docs/getting-started/introduction)
Son of Anton receives GitHub App webhooks, evaluates review policy, analyzes a pinned PR head, and publishes a review body and a check run when publication is enabled. Validation can run in a separately configured sandbox. Skipped or unavailable validation is reported, not counted as a successful check.
## Access and source layout [#access-and-source-layout]
These instructions require an authorized source checkout. The verified handoff records `justgetAI/son-of-anton-review` as private. The checked-out engine has no LICENSE file and its package is marked `private: true`; that package field alone does not establish GitHub visibility or grant a license. Do not assume a public clone or permission to redistribute. Ask the repository owner for access and licensing terms.
Source paths, repository names and branch references throughout these docs are citations for authorized users, not public source-download links. Publishing this documentation does not make the repositories public or grant source access.
The available trees are separate:
| Tree | Checked-out branch | Role |
| ------------------------------ | ------------------------------- | ---------------------------------------------------------------------------- |
| `son-of-anton-review` | `feat/cloudflare-native` | Review engine, Node entrypoints and Cloudflare-native runtime |
| `son-of-anton-operator-parity` | `feat/greptile-operator-parity` | Separate engine worktree containing the expanded Cloudflare operator API |
| `anton-ui` | `feat/hardening-observability` | Older dashboard with an optional bearer gate |
| `anton-ui-trace-parity` | `feat/trace-api-parity` | Separate UI worktree with WorkOS-backed sessions and expanded operator views |
The two parity directories are Git worktrees, not subdirectories of the engine deployment. There is no combined engine/operator deployment command. The native main Worker exposes ingress, not the parity `/operator/api/*` service. The Node server has its own smaller, read-only operator surface.
## Model and validation choices [#model-and-validation-choices]
**Model:** OpenRouter is the recommended setup. The engine also implements OpenAI direct, Anthropic direct and Codex CLI providers. Native deployment uses `GILF_MODEL_PROVIDER` and `GILF_MODEL`; the checked-in Node `worker.mjs` instead reaches constructor fallbacks `GILF_CODEX_PROVIDER` and `GILF_CODEX_MODEL`. Set the appropriate pair explicitly. OpenRouter's free-only guard is enabled unless `GILF_OPENROUTER_REQUIRE_FREE=0`. Select a currently available model and approve paid usage deliberately; checked-in model names and price estimates are not a current provider catalog.
**Validation:** `GILF_VALIDATION_EXECUTOR` selects `managed-crabbox`, `managed-e2b`, `managed-cf-sandbox` or `self-host`. With no explicit or legacy selection the resolver chooses `managed-crabbox`; the native deployment instead configures Cloudflare Sandbox. A self-host command must provide the isolation itself. Environment filtering is not a container boundary. The legacy `local` executor requires an explicit unsafe opt-in and is not a production recommendation.
Validation uses supported repo-native commands when available. It does not prove every finding by execution, and docs-only changes can skip validation. See [Validation executors](/docs/configuration/validation-executors).
**Observability:** persisted traces can include latency, tokens and estimated cost. Missing usage or unknown prices can leave cost null. A successful trace is evidence of the recorded operations, not a correctness guarantee or a billing invoice.
## Two engine deployment modes [#two-engine-deployment-modes]
| Mode | Processes and state | Start here |
| ----------------- | -------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
| Node | Long-running `server.mjs`, one-shot `worker.mjs`, shared SQLite state and queue | [Self-host quickstart](/docs/getting-started/quickstart-self-host) |
| Cloudflare-native | Main Worker, separate key-broker Worker, OMP and Sandbox container applications, Queue, D1, R2 and KV bindings | [Cloudflare quickstart](/docs/getting-started/quickstart-cloudflare) |
On Node, the server receives events and the separately scheduled worker drains jobs. In the native runtime, ingress verifies signatures and deduplicates delivery IDs; the consumer resolves the PR head and claims a dispatch lease before starting a container. The broker supplies scoped GitHub tokens without passing the App private key to the container.
Review identities include the repository, PR number and head SHA. Manual review comments create variants keyed by comment ID. Check runs are updated within a run; review bodies are POSTed, not edited in place across every push. This is not an exactly-one-review-per-PR promise.
## Optional features are not defaults [#optional-features-are-not-defaults]
| Feature | Engine state |
| ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Context graph | Builds repository/commit context, including impact beyond changed files; not a multi-repository index |
| Hypothesis swarm | No worker runs by default. `GILF_PRIME_SHADOW=1` selects the Prime worker; `GILF_HYPOTHESIS_PRIMARY=1` requests primary use and normally enables that worker unless explicitly disabled. The worker harness needs its own setup. |
| LLM hypothesis planner | `GILF_HYPOTHESIS_PLANNER=1`, also requires an available hypothesis worker |
| Model inversion | `GILF_MODEL_INVERSION`, default off |
| Execution-artifact processing | `GILF_EVIDENCE_ARTIFACTS=1`, default off; requiring artifacts is a separate gate |
| Auto-approval, PR-description updates, Prompt to Fix | Policy-controlled, default off |
The native container environment allowlist does **not** forward the swarm, planner, inversion or evidence-artifact flags. Adding them to Worker vars alone does not enable those features. That requires an engine change, not a documented deployment toggle.
## Reading the result [#reading-the-result]
The review separates merge status, analyst confidence, findings and missing validations. Findings have file paths but no required line-number fields. There are no inline review comments or GitHub suggestion patches in the publisher. Optional status comments and PR-description updates are separate surfaces.
See [Anatomy of a review](/docs/getting-started/anatomy-of-a-review). For an agent-assisted install with explicit human gates, use [Agent onboarding](/docs/getting-started/agent-onboarding).
## Names [#names]
Use the actual `GILF_` environment names. The parity operator Worker supports its own `SON_OF_ANTON_` token-name fallbacks; that is not a general `ANTON_` alias for engine configuration. The command parser accepts both `@anton` and `@gilf`, but supported replies differ by runtime.
# Quickstart: Cloudflare (/docs/getting-started/quickstart-cloudflare)
This deploys the **engine** from `son-of-anton-review` on `feat/cloudflare-native`. It does not deploy the expanded operator API from `son-of-anton-operator-parity` or either UI tree. Source access and licensing must be arranged with the owner: the handoff records a private repository and the checkout contains no LICENSE.
The native configuration runs a main Worker and a separate key-broker Worker, plus **two container applications**: OMP for review orchestration and Sandbox for validation. Commands below run from the authorized engine checkout root.
## 1. Prepare the account and checkout [#1-prepare-the-account-and-checkout]
Use Node 25, matching the OMP image, and install the locked dependencies with `npm ci`. The package already declares `@cloudflare/containers` and `@cloudflare/sandbox`. Wrangler is not a declared dependency; install an operator-approved version before using the commands below. Container image builds also need a working Docker build environment.
A human must authorize the Cloudflare account, its required Workers/Containers access, the GitHub App installation and spending. Run `wrangler login` if needed, then `wrangler whoami` and confirm the intended account. Local source cannot establish your account entitlements or available quota.
**Do not deploy the checked-in configuration unchanged.** It contains deployment-specific resource IDs, an App ID, `GILF_PUBLISH_MODE=live`, `CRON_AUTHORITY=live`, a paid-model selection and an E2B shadow lane.
Before deploying:
1. Set your account ID in both native Wrangler files, and your App ID in `cloudflare-native/wrangler.key-broker.jsonc`.
2. Set `GILF_PUBLISH_MODE` and `CRON_AUTHORITY` to `shadow` in `cloudflare-native/wrangler.jsonc`.
3. Select `GILF_MODEL_PROVIDER=openrouter` and your approved `GILF_MODEL`. Keep `GILF_OPENROUTER_REQUIRE_FREE=1` unless paid usage is authorized. Remove or replace the checked-in `GILF_MODEL_PRICES`; it is an estimate for one model, not a universal price table.
4. Keep `GILF_VALIDATION_EXECUTOR=managed-cf-sandbox` and both container/DO bindings. Remove `GILF_VALIDATION_SHADOW_EXECUTOR` unless you deliberately want E2B too.
5. Replace `CRON_AUDIT_INSTALLATIONS` with your own installation list and choose your own `PR_AGENT_LOOP_SINCE` reporting floor. Do not reuse the checked-in estate identifiers.
## 2. Provision a new deployment [#2-provision-a-new-deployment]
These create resources. For an existing deployment, inventory and reuse its bindings instead of creating duplicates.
```bash
wrangler d1 create son-of-anton-review-state
wrangler kv namespace create DEDUPE
wrangler r2 bucket create son-of-anton-webhook-payloads
wrangler r2 bucket create son-of-anton-cron-artifacts
wrangler queues create gilf-review-intents
wrangler queues create gilf-review-dlq
```
Put the returned D1 database ID and KV namespace ID in `cloudflare-native/wrangler.jsonc`. Ensure every configured resource belongs to your account. For a **fresh** database apply both files:
```bash
wrangler d1 execute son-of-anton-review-state --config cloudflare-native/wrangler.jsonc --remote --file cloudflare-native/schema.sql
wrangler d1 execute son-of-anton-review-state --config cloudflare-native/wrangler.jsonc --remote --file cloudflare-native/migrations/002-cron-parity.sql
```
The base schema does not contain the cron tables. Existing databases need the guarded procedure in [Upgrading](/docs/self-host/upgrading), not blind application of destructive migration 001.
## 3. Configure secrets and enable the canary repository [#3-configure-secrets-and-enable-the-canary-repository]
The human supplies secrets through approved tooling, never chat or committed config:
```bash
wrangler secret put GITHUB_WEBHOOK_SECRET --config cloudflare-native/wrangler.jsonc
wrangler secret put OPENROUTER_API_KEY --config cloudflare-native/wrangler.jsonc
wrangler secret put GITHUB_APP_PRIVATE_KEY --config cloudflare-native/wrangler.key-broker.jsonc
```
The App key belongs on the broker only. Configure the App permissions and event subscriptions from [Requirements](/docs/self-host/requirements).
`GILF_REPOS` is **not** a native allowlist. A new native repository without stored configuration or previous review history is disabled by the default `autoEnableNewRepos=false`. Explicitly enable one installed canary repo using the non-destructive D1 procedure in [Run on your Cloudflare account](/docs/self-host/run-on-your-cloudflare-account#enable-a-repository). Installation alone is insufficient, even for a manual review.
## 4. Deploy broker, then main Worker [#4-deploy-broker-then-main-worker]
```bash
wrangler deploy --config cloudflare-native/wrangler.key-broker.jsonc
wrangler deploy --config cloudflare-native/wrangler.jsonc
```
Keep the OMP entry's `image_build_context: ".."`: its Dockerfile copies engine-root files. Keep the `KEY_BROKER` binding aligned with the broker's configured Worker name.
Point the GitHub App webhook at your main Worker's `/github/webhooks` path with the matching webhook secret. Install it only on intended repositories.
```bash
# Set to the URL from your deployment, without a trailing slash.
export ANTON_ENGINE_URL='https://your-worker.example'
curl --fail-with-body --silent --show-error "$ANTON_ENGINE_URL/health"
```
Expected ingress body: `{"ok":true,"service":"son-of-anton-ingress"}`. This does not probe D1, the broker, model credentials or Sandbox.
## 5. Run a shadow canary, then choose publication [#5-run-a-shadow-canary-then-choose-publication]
An authorized repository owner, member or collaborator can post a **new** `@anton review` comment. The native consumer resolves the current head through GitHub, so an earlier PR webhook is not required for this command. `@anton rerun` also requests a review. Unlike the Node server, the native consumer ignores `@anton status` and `@anton help`; use D1 and logs instead.
```bash
wrangler d1 execute son-of-anton-review-state --config cloudflare-native/wrangler.jsonc --remote --command "SELECT review_key,status FROM review_runs ORDER BY rowid DESC LIMIT 5"
wrangler tail --config cloudflare-native/wrangler.jsonc
```
Inspect the matching run and trace, including policy admission, validation gaps and publication mode. A policy-skipped run or a row labeled published through a recording publisher is not a live GitHub review. Shadow still reads GitHub, uses the model/sandbox and persists state; it only suppresses review publication.
After a satisfactory canary and explicit human approval, set `GILF_PUBLISH_MODE=live`, redeploy, and post a new review comment. Confirm the exact head SHA and actual GitHub review/check receipts. Leave cron authority in shadow until its installation scope and any old schedulers have been reconciled separately. A mode change cannot stop an already-running container.
See the [full runbook](/docs/self-host/run-on-your-cloudflare-account) for state queries, schedules and rollback.
# Quickstart: Self-host on Node (/docs/getting-started/quickstart-self-host)
Use an authorized checkout of `son-of-anton-review` on `feat/cloudflare-native`. The repository is recorded as private in the handoff, and the checkout has no LICENSE. Obtain access and terms from the owner; this is not an anonymous public-clone workflow. The operator-parity engine worktree and the UI worktrees are separate deployments.
## 1. Prepare the host [#1-prepare-the-host]
Use **Node 25**, matching the native image. Node 20 is not sufficient: the entrypoints import stores backed by `node:sqlite` / `DatabaseSync`. Install `git`, GitHub CLI (`gh`) and the dependencies from the authorized checkout:
```bash
node --version
git --version
gh --version
npm ci
```
The worker clones with `gh repo clone`, then uses `git fetch` and checkout operations. Its OS user needs working GitHub CLI **and Git transport** authentication for every reviewed private repository. A human can provision a restricted credential or complete `gh auth login` and `gh auth setup-git` under that user. App publishing credentials alone do not establish this clone authentication on Node; the broker-based clone wiring is native-runtime-only.
Use a dedicated service account. Do not hand an agent a broad personal token or print credentials to prove authentication.
## 2. Create a dedicated environment file [#2-create-a-dedicated-environment-file]
Neither entrypoint automatically loads `.env`. Create an owner-readable environment file outside the checkout, for example `/etc/son-of-anton/engine.env`, using approved secret tooling. Substitute your real values for the descriptive placeholders below. Do not overwrite an existing environment file or source an unreviewed shell file.
```dotenv
GITHUB_WEBHOOK_SECRET=YOUR_WEBHOOK_SECRET
GITHUB_APP_ID=YOUR_NUMERIC_APP_ID
GITHUB_APP_PRIVATE_KEY_PATH=/etc/son-of-anton/app.private-key.pem
GILF_DB_PATH=/var/lib/son-of-anton/anton.db
GILF_REVIEW_WORK_ROOT=/var/lib/son-of-anton/workspaces
GILF_REPOS=YOUR_OWNER/YOUR_REPOSITORY
GILF_CODEX_PROVIDER=openrouter
GILF_CODEX_MODEL=YOUR_APPROVED_OPENROUTER_MODEL
OPENROUTER_API_KEY=YOUR_MODEL_KEY
GILF_OPENROUTER_REQUIRE_FREE=1
GILF_VALIDATION_EXECUTOR=managed-e2b
E2B_API_KEY=YOUR_SANDBOX_KEY
GILF_E2B_TIMEOUT_MS=600000
PORT=8787
```
Create the database parent directory and workspace directory with ownership assigned to the service user. Choose capacity for concurrent clones and persistent state; there is no source-backed universal CPU, RAM or disk minimum.
Important distinctions:
* Inline `GITHUB_APP_PRIVATE_KEY`, if present, takes precedence over `GITHUB_APP_PRIVATE_KEY_PATH`. Configure one, not both.
* The server exits 1 without `GITHUB_WEBHOOK_SECRET`. Without App ID/key configuration it uses a recording publisher, so `/health` can succeed without live publication.
* **`GILF_REPOS` seeds repository config, not a deny-by-default allowlist.** Unlisted repos use defaults too. Restrict the App installation to intended repos and use explicit repository policy for additional exclusions.
* **Use the legacy provider/model names for this Node entrypoint.** `worker.mjs` does not pass provider/model arguments, and the runner constructor reads `GILF_CODEX_PROVIDER` and `GILF_CODEX_MODEL`, falling back to `codex` / `gpt-5.5`. The modern `resolveModelConfig()` helper is not called on this path: `GILF_MODEL_PROVIDER`, `GILF_MODEL` and `GILF_MODEL_PRIMARY` alone do not select the Node runner's model. Set an explicit valid OpenRouter model and approve `GILF_OPENROUTER_REQUIRE_FREE=0` deliberately for paid usage. The native entrypoint wires the modern pair separately.
* E2B is the explicit choice here. Unset executor selection falls back through legacy validation config to Crabbox. Missing executor prerequisites are recorded as missing validation, not a host-execution fallback.
* The explicit E2B timeout avoids differing source defaults. See [Upgrading](/docs/self-host/upgrading#timeout-defaults).
OpenAI, Anthropic and Codex alternatives are documented under [Model providers](/docs/configuration/model-providers). Cloudflare Sandbox is not wired into the standalone Node worker.
## 3. Start the server [#3-start-the-server]
Run from the engine root, under the prepared service account:
```bash
node --env-file=/etc/son-of-anton/engine.env server.mjs
```
The server stays running. In another terminal:
```bash
curl --fail-with-body --silent --show-error http://localhost:8787/health
```
Expect `ok: true` and `service: "son-of-anton-pr-review"`. The `validation.provider` field comes from **legacy** `GILF_VALIDATION_PROVIDER` and can say `crabbox` even when `GILF_VALIDATION_EXECUTOR=managed-e2b` selects E2B. Health is a listener/config probe, not a model, queue-drain or sandbox readiness check.
## 4. Configure GitHub and receive a PR event [#4-configure-github-and-receive-a-pr-event]
A human creates/configures the App, installs it on selected repositories, and supplies its private key and webhook secret. See [Requirements](/docs/self-host/requirements) for permissions and subscriptions.
Expose the server through an approved HTTPS proxy or tunnel. Set the App webhook to your public `/github/webhooks` URL, using the same secret. Keep optional operator routes private.
Open a non-draft PR targeting `main` or `dev`, the default auto-review branches, or deliver a PR event for an existing PR. Confirm the delivery in GitHub. The Node command handler needs a stored PR head; installation alone does not populate one. An authorized owner, member or collaborator may then post a **new** comment:
```text
@anton review
```
`@anton rerun` requests another run. `@anton status` and `@anton help` can reply on Node once the head is stored. `@gilf` is a parser alias.
## 5. Drain and verify [#5-drain-and-verify]
In a separate terminal, with the same service user, environment and absolute database path:
```bash
node --env-file=/etc/son-of-anton/engine.env worker.mjs
```
The worker reconciles state, processes a bounded batch and **exits**. Defaults are one job and concurrency one; configure `GILF_WORKER_MAX_JOBS` and `GILF_WORKER_CONCURRENCY` for your workload. Re-run after another queued event, or schedule it as described in [Run on a Node box](/docs/self-host/run-on-a-node-box).
Do not equate exit 0 or `drained=0` with a successful review: per-job failures are collected by the drainer and can leave the process successful. With the optional `sqlite3` CLI, inspect state read-only:
```bash
sqlite3 -readonly /var/lib/son-of-anton/anton.db 'SELECT review_key,status FROM review_runs ORDER BY rowid DESC LIMIT 10;'
```
Match the run to the canary head, inspect its validation gaps, and confirm the actual GitHub review and check run. With App credentials present, Node publishes directly: **`GILF_PUBLISH_MODE=shadow` is not a Node safety switch.** Use an explicitly approved canary repository.
## State and dashboard [#state-and-dashboard]
For this quickstart, leave `GILF_QUEUE_URL` and `GILF_QUEUE_DB_PATH` unset so both processes use `GILF_DB_PATH` for queue and state. With no persistent paths they get separate in-memory stores/queues and cannot form a working two-process deployment. A remote queue does not replace the shared run store.
The optional Node dashboard needs both `GILF_OPERATOR_UI_ENABLED=1` and `GILF_OPERATOR_UI_TOKEN`. An enabled dashboard with no token rejects requests. It is read-only and is not the expanded parity API or WorkOS UI.
Next: [Agent onboarding](/docs/getting-started/agent-onboarding), [Secrets and keys](/docs/self-host/secrets-and-keys), [Environment reference](/docs/configuration/environment-reference).
# Cost and Traces (/docs/operator/cost-and-traces)
The trace producer is in `son-of-anton-review` on `feat/cloudflare-native`, the deployed-engine lineage. The API described below is in `son-of-anton-operator-parity` on **unmerged `feat/greptile-operator-parity`**, with the corresponding UI in `anton-ui-trace-parity`. Native ingress does not expose those API routes.
A trace is a recorded execution attempt, not proof that every webhook, retry or failed startup left complete telemetry. Multiple trace IDs can share a review key. [Observability and cost](../architecture/observability-and-cost) explains tracing and persistence limits.
## Open a trace in the parity dashboard [#open-a-trace-in-the-parity-dashboard]
1. Open **Analytics → Observability**. This view fetches the latest 50 trace summaries. Its list KPIs are trace count, failed runs, average duration and P95 duration, not cost or token totals.
2. Select a row. The client requests `/api/traces/:traceId`; the BFF maps it to `/operator/api/traces/:traceId`.
3. Inspect the header, waterfall and a selected span. The inspector has **Attributes** and **Events** tabs. There is no Input/Output tab in this UI.
The list's duration metrics exclude null, invalid and nonpositive durations. A failed-runs count is derived from trace status. The detail error badge instead counts spans with an error status or error message, including the root if it carries one. These are not interchangeable counts.
Sources: UI `src/views/Analytics.tsx:152-154`; `src/components/TraceObservability.tsx:30-105,107-165,177-219,321-388`; `src/lib/observability.ts:61-107,122-140`; `src/lib/api.ts:131-142`; `worker/proxy-utils.js:41-56`.
## API contract [#api-contract]
For a parity Worker base URL in `OPERATOR_URL`, list traces with the read token:
```bash
curl --fail-with-body -sS \
-H "Authorization: Bearer $GILF_OPERATOR_READ_TOKEN" \
"$OPERATOR_URL/operator/api/traces?repo=owner/name&prNumber=123&limit=5"
```
Then use a URL-encoded returned trace ID in `/operator/api/traces/:traceId`. The list response is `ok` plus `traces`. Detail is `ok`, `trace` and **sibling** `spans`, not spans nested inside `trace`. An unknown ID returns `404 not_found`.
Summary rows contain `traceId`, `rootSpanId`, review/PR identity, timing, status and attributes. They omit top-level token/cost totals and span counts. Detail spans use `spanId`, `parentSpanId`, `kind`, `inputTokens`, `outputTokens`, `costUsd`, `firstTokenLatencyMs`, attributes and events.
The API requires route-appropriate bearer authorization, or a dynamic key with the allowed `read` scope. The browser instead authenticates through the BFF's session cookie.
Sources: parity `cloudflare/src/review-state-api.js:876-899`; `cloudflare/src/review-trace-queries.js:14-64,73-132`; `cloudflare/src/worker.js:80,117-133`; UI `worker/bff-sessions.js:69-75`.
## Read phases without assuming a fixed span set [#read-phases-without-assuming-a-fixed-span-set]
Common phase names include `repo.prepare`, `context.load`, `context.graph`, `chat.primary`, `validation.cf-sandbox`, `validation.e2b` and native `publish`, beneath `agent.workflow`. Optional paths add hypothesis, shadow-chat and model-price spans.
Not every healthy attempt has every phase: a policy skip, early refusal, docs-only path or reuse of durable analysis changes what executes. A `publish` span records the service stage; its presence or `ok` status is not by itself proof of a new live GitHub review. Inspect its publication attributes, run state and ledger.
| Observation | Interpretation and next check |
| ------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| No trace | Could be no recorded attempt, tracing disabled on a non-native path, initialization failure or lost telemetry. Check run state and logs. |
| No `chat.primary` | Could be a skipped/early-failed path or reuse of stored analysis. Inspect the run before treating it as a tracing defect. |
| Validation `unavailable` with null `passed` | No usable verdict for that lane. Inspect the refusal/error and executor configuration. |
| Shadow validation error with timeout | Primary findings are not replaced by shadow output. Publication may still have waited for the remaining shadow timeout, and compute may still cost money. |
| `publish` error or uncertain ledger action | Do not blindly replay. Inspect remote publication evidence and native ledger state. |
Sources: engine `src/codex-review-runner.js:34-60,2534-2539,2711-2714`; `cloudflare-native/container/entrypoint.mjs:95-176`; `src/validation-executor.js:213-245,435-464`; `cloudflare-native/src/state-transport.js:490-524`.
## Cost and tokens [#cost-and-tokens]
The parity trace header sums non-null cost and token values from detail spans. If no value is reported, the total remains null and cost renders as a dash. If only one lane reports cost, the displayed total is partial; it is not the complete cost of the review.
Model cost uses configured `GILF_MODEL_PRICES` or built-in provider prices and reported usage. OpenRouter's built-in table is empty. Validation uses wall-clock estimates with CPU, memory and disk rates. Neither is an invoice. Worker, database, storage and other orchestration charges are not added by these formulas.
A numeric zero is not always a proven free operation: provider arithmetic treats missing numeric components as zero, and validation telemetry can coerce explicit null values to zero. Inspect the raw attributes and price configuration when a zero is surprising.
Sources: UI `src/lib/observability.ts:64-78,122-140,274-276`; engine `src/model-provider.js:44-93`; `src/validation-executor.js:146-245`.
## PR averages are a bounded sample [#pr-averages-are-a-bounded-sample]
The parity PR drawer loads **up to five recent trace details** for that PR. Its averages cover those loaded details, not all reviews in the database. Each optional metric averages only the runs reporting it. Phase/kind totals exclude roots and divide by the count of loaded details.
Nested phases and concurrent lanes overlap. Summing their durations is not a partition of wall-clock time. The minimum reported span first-token latency displayed in the header is likewise not necessarily elapsed time from the beginning of the whole review.
The separate Node API does have `/operator/api/trace-stats?repo=...&number=...`, and returns all traces supplied by its PR trace-store query. It exposes `runCount`, nullable averages, `phases[]` keyed by `phase` and `spanTypes[]` keyed by `type`. Do not call that endpoint on the parity Worker or describe the parity drawer as using it.
Sources: UI `src/lib/api.ts:149-158`; `src/lib/observability.ts:122-140,228-272`; engine `src/operator-dashboard.js:326-402,659-668`.
## Raw native storage [#raw-native-storage]
Native D1 tables are `review_traces` and `review_trace_spans`. These read-only SQL examples target the native schema; use your configured database tooling and authorization rather than assuming the parity API's binding points to the same database.
```sql
SELECT trace_id, review_key, status, duration_ms, cost_usd
FROM review_traces
ORDER BY started_at DESC
LIMIT 5;
-- Replace the bound parameter with the selected trace ID through your SQL client.
SELECT name, kind, status, duration_ms, input_tokens, output_tokens, cost_usd, error
FROM review_trace_spans
WHERE trace_id = ?1
ORDER BY seq;
SELECT COALESCE(SUM(cost_usd), 0) AS used
FROM review_trace_spans
WHERE name = 'validation.e2b';
```
The last query is the native E2B gate's all-time estimate. It does not filter by role, repository or date, and it ignores null costs. The Test Lab ceiling is not a billing cap.
Re-flushing a trace upserts its row and replaces its spans. No age-based trace pruning was found in the audited native trace path or Node trace store; this is not a retention SLA or confirmation of account-level policies. The native mapper can persist input/output JSON, attributes and events even though parity detail omits input/output. Protect the database accordingly.
Sources: engine `cloudflare-native/schema.sql:187-250`; `cloudflare-native/src/state-transport.js:417-451`; `cloudflare-native/src/trace-buffer.js:104-117`; `cloudflare-native/src/container-env.js:100-141`.
No live trace, database or dashboard was queried during this audit. Missing spans, deployment state and actual billed cost remain runtime questions for consolidated verification.
# Operator Dashboard (/docs/operator/dashboard)
## Choose the correct surface [#choose-the-correct-surface]
| Source tree | Surface | Status and scope |
| ---------------------------------------------------------------------------- | ---------------------------------- | ---------------------------------------------------------------------------------------- |
| `son-of-anton-review`, `feat/cloudflare-native` | Native engine Worker | Deployed-engine lineage; public health and webhook routes, not the extended operator API |
| `son-of-anton-review`, Node entrypoint | `/operator` HTML and GET-only JSON | Separate self-host runtime |
| `son-of-anton-operator-parity`, **unmerged** `feat/greptile-operator-parity` | Extended Cloudflare operator API | Reports, trace details, memory, settings, keys, SCM, commands and Test Lab |
| `anton-ui-trace-parity` | React dashboard and BFF | Client of the parity API; its source does not establish which build is live |
The dashboards are not interchangeable clients of one identical API. In particular, the Node trace-detail endpoint is `/operator/api/trace?id=...`; the parity endpoint is `/operator/api/traces/:id`. Do not point the parity dashboard at native ingress and expect operator routes to exist.
Sources: engine `cloudflare-native/src/ingress.js:264-273`, `src/operator-dashboard.js:609-690`; parity `cloudflare/src/worker.js:115-157`, `cloudflare/src/review-state-api.js:876-899`; UI `worker/proxy-utils.js:15-56`.
## Parity dashboard views [#parity-dashboard-views]
The parity router provides `/pull-requests`, `/analytics`, `/memory`, `/settings`, `/test-lab` and `/test-lab/:id`. Its default route redirects to Pull Requests.
* **Pull Requests:** the PR drawer loads detail and recent trace details. Its Re-run review control submits an operator command and reports command status. Command acceptance is not proof that a review finished.
* **Analytics:** PR Reviews and Bugs Caught consume the reports API, including filters and ZIP export. They are not the older placeholder charts. Observability separately loads recent traces without requiring the analytics endpoint.
* **Memory:** the parity view supports persisted contexts, approval/activation, editing, deletion, repository clusters, knowledge documents and source-provider workflows. Do not confuse it with the Node snapshot's sample memory rows.
* **Settings:** the parity view has policy editing, repository scheduling controls and specialized settings sections. It is not read-only.
* **Test Lab:** reads experiment summaries and per-experiment metrics; the page does not create or enable experiments.
Sources: UI `src/App.tsx:15-28`; `src/components/PrDrawer.tsx:94-108,129-131`; `src/lib/api.ts:149-180`; `src/views/Analytics.tsx:128-163`; `src/views/Memory.tsx:60-62,190-195,314-327`; `src/views/Settings.tsx:242-261`; `src/views/TestLab.tsx:69-73,239-243`.
## BFF configuration and sessions [#bff-configuration-and-sessions]
The browser calls `/api/*`. The BFF maps known paths to backend routes and attaches a server-configured token. It does not pass through a browser-supplied bearer token.
| Setting | Behavior |
| -------------------------- | -------------------------------------------------------------------------------- |
| `SON_OF_ANTON_API_BASE` | Backend base URL; fallback `GILF_OPERATOR_API_BASE` |
| `SON_OF_ANTON_API_TOKEN` | Ordinary backend token; fallback `GILF_OPERATOR_UI_TOKEN` |
| `SON_OF_ANTON_ADMIN_TOKEN` | Used for mutations and key listing |
| `WORKOS_CLIENT_ID` | WorkOS client identifier |
| `WORKOS_ISSUER` | Must equal the client-scoped `https://api.workos.com/user_management/` |
| `WORKOS_ALLOWED_USER_IDS` | Nonempty comma-separated allowlist |
| `ANTON_SESSIONS` | Session Durable Object binding |
Missing backend base returns `503 backend_not_configured`. A privileged call without the admin backend token returns `503 admin_backend_not_configured`. Authentication configuration failures are separate from backend failures.
The BFF accepts only its opaque `__Host-anton-session` cookie. The cookie is Secure, HttpOnly and SameSite=Lax; session lifetime is 12 hours. WorkOS access tokens are signature-, issuer-, expiry- and user-allowlist-checked. Auth routes are `GET /api/auth/login`, `GET /api/auth/session` and `POST /api/auth/logout`. Mutations require same-origin requests and JSON content type.
Backend operator tokens and WorkOS token responses are not forwarded as browser API credentials. Operator **API-key creation** is a distinct feature: its one-time `anton_` secret is deliberately returned in the creation response. Do not claim that no secret can ever reach the dashboard.
Sources: UI `worker/proxy-utils.js:1-5,15-56,95-100`; `worker/index.js:24-73`; `worker/workos-auth.js:11-18,43-79`; `worker/bff-sessions.js:4-6,24-25,69-75,117-156`; parity `src/operator-api-keys.js:65-78`.
## Known cross-tree token mismatch [#known-cross-tree-token-mismatch]
The BFF chooses `SON_OF_ANTON_ADMIN_TOKEN` for **all** allowed mutations, including `/api/commands/requeue` and `/api/commands/retry-publish`. The parity backend requires its separate `OPERATOR_COMMAND_TOKEN` for command creation. These sources do not automatically route the command credential correctly.
Likewise, `/api/queue` maps to `/queue/jobs`, but ordinary BFF reads use the ordinary API token while that backend route requires `QUEUE_ADMIN_TOKEN`.
A visible button or route mapping is not evidence that those operations work with distinct credentials. Integration must resolve the route-specific token selection; do not weaken token separation to conceal the mismatch. Other API requests can still work while these calls return 401.
Sources: UI `worker/index.js:7-16,42-46`; `worker/proxy-utils.js:19,36-37`; parity `cloudflare/src/worker.js:58-80`.
## Separate Node dashboard [#separate-node-dashboard]
For a Node server, set `GILF_OPERATOR_UI_ENABLED=1` and a nonempty `GILF_OPERATOR_UI_TOKEN` in the server environment. Only GET requests are served; other methods return 405. With no token, GET requests fail with `401 operator_auth_required`.
For a configured server URL in `REVIEW_URL`, a read request is:
```bash
curl --fail-with-body -sS \
-H "Authorization: Bearer $GILF_OPERATOR_UI_TOKEN" \
"$REVIEW_URL/operator/api/snapshot?view=pull-requests"
```
This is an API example, not a browser-login mechanism. The Node handler expects a bearer header on protected HTML, assets and data requests; the environment variables do not create a WorkOS session for it.
| Node route | Response |
| ----------------------------------------------- | ------------------------------------------------------------------------------ |
| `/operator/api/snapshot` | `schema`, `generatedAt`, `ttlSeconds`, `view`, `data`; no universal `ok` field |
| `/operator/api/pr?repo=...&number=...` | PR detail |
| `/operator/api/traces?limit=...` | `ok`, `traces`; default limit 50 |
| `/operator/api/trace?id=...` | `ok`, `trace`, with nested spans on success |
| `/operator/api/trace-stats?repo=...&number=...` | Per-PR averages and phase/type breakdowns |
The Node memory snapshot contains hardcoded sample entries. It is not authoritative evidence of active memory rules or configured integrations. Trace-list errors are caught and returned as empty lists, so an empty view can also mean a store problem.
Sources: engine `server.mjs:79-84`; `src/operator-dashboard.js:231-251,253-286,291-313,601-687`.
See [Operator API](./operator-api), [Cost and traces](./cost-and-traces), and [Dashboard settings](../configuration/dashboard-settings). Availability statements above are source-scoped; no live UI, session or backend was probed during this audit.
# Operator API (/docs/operator/operator-api)
## Branch boundary [#branch-boundary]
This reference describes `son-of-anton-operator-parity/cloudflare/` on **unmerged `feat/greptile-operator-parity`**. It is not the public API of the deployed-engine lineage, `son-of-anton-review` on `feat/cloudflare-native`. Native ingress serves health and GitHub webhooks, not these operator routes.
The engine's separate Node dashboard exposes GET-only snapshot, PR and trace routes. Its `/operator/api/trace?id=...` and `/operator/api/trace-stats` are **not** parity Worker routes. See [Dashboard](./dashboard) for the Node surface.
Sources: parity `cloudflare/src/worker.js:93-157`; engine `cloudflare-native/src/ingress.js:264-273`, `src/operator-dashboard.js:639-687`.
## Authentication [#authentication]
Send `Authorization: Bearer `. The parity Worker's token resolver prefers `SON_OF_ANTON_` over legacy `GILF_`, using nullish fallback. An empty canonical value does not fall through to the legacy value. This is not a general `ANTON_` alias for engine settings.
| Legacy variable | Accepted route class |
| ----------------------------- | ----------------------------------------------------------------------------------------------- |
| `GILF_OPERATOR_READ_TOKEN` | Ordinary operator GET routes, except `/operator/api/keys` and descendants |
| `GILF_OPERATOR_ADMIN_TOKEN` | Key management including GET; POST queue repair; PATCH repo config; non-GET memory/settings/SCM |
| `GILF_OPERATOR_COMMAND_TOKEN` | POST command creation |
| `GILF_OPERATOR_BRIDGE_TOKEN` | PATCH command updates; GET command list/detail |
| `GILF_QUEUE_ADMIN_TOKEN` | `/queue/jobs`, `/queue/stats` |
| `GILF_QUEUE_ENQUEUE_TOKEN` | `/queue/enqueue` |
| `GILF_QUEUE_WORKER_TOKEN` | `/queue/claim`, `/queue/ack`, `/queue/fail`, `/queue/renew` |
Missing or mismatched static tokens return `401` with `ok: false, error: unauthorized`. Token classes are not hierarchical: the admin token is not automatically valid for command creation or ordinary reads. Static service calls can set `x-operator-actor`; the default is `service-admin`. This is attribution supplied by the service-token holder, not separate human authentication.
Sources: parity `cloudflare/src/env.js:1-9`; `cloudflare/src/worker.js:27-81,115-135,159-167`.
### Scoped API keys [#scoped-api-keys]
Admin-only `POST /operator/api/keys` accepts `name` and optional `scopes`. Allowed scopes are `read` and `memory:write`; omitted scopes default to `read`. Creation returns HTTP 201 with `ok`, `key`, `token` and `apiBaseUrl`. The `anton_` token contains 64 hex characters after its prefix and is returned only on creation. Storage keeps its SHA-256 hash and metadata, including a 14-character display prefix.
A dynamic key with `read` may GET the snapshot, PR, repos, traces, reports, memory, SCM and Test Lab route families, plus exactly `/operator/api/settings/usage`. It cannot read general settings, commands, analytics, webhook history or key management through that scope. A `memory:write` key may make memory mutations except source-integration preview. Scope permission does not create nonexistent routes.
`GET /operator/api/keys?q=...` lists metadata. `POST /operator/api/keys/:id/revoke` revokes a key. A revoked or invalid key fails authentication; an authenticated key used outside its route scope receives 403.
Sources: parity `src/operator-api-keys.js:31-45,65-112`; `cloudflare/src/operator-api-keys-api.js:49-75`; `cloudflare/src/worker.js:117-127`.
## Read routes and envelopes [#read-routes-and-envelopes]
There is **no universal `ok` envelope**. Snapshot, PR detail and reports have their own schema envelopes; ZIP export is binary. JSON helpers generally set `cache-control: no-store`.
| Route | Parameters | Success shape |
| ---------------------------------------- | ------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| `GET /health` | None; unauthenticated | `ok`, `service: gilf-pr-review-queue` |
| `GET /operator/api/snapshot` | `view`, `q`, `repo`, `status` | `schema: son-of-anton.operator.v1`, `generatedAt`, `ttlSeconds: 15`, `view`, `data` |
| `GET /operator/api/pr` | `repo`, `number` | `schema`, `generatedAt`, `data` containing runs, findings, events and publication ledger |
| `GET /operator/api/repos` | None | `ok`, `repos` |
| `GET /operator/api/webhooks` | `status`, `limit` | `ok`, `deliveries` |
| `GET /operator/api/webhooks/:deliveryId` | Encoded ID | `ok`, `delivery` |
| `GET /operator/api/analytics` | None required | `ok`, `analytics`, including queue projection and at most 100 failure reasons |
| `GET /operator/api/traces` | `repo`, `prNumber` or `pr`, `reviewKey`, `runId`, `status`, `limit` | `ok`, `traces` |
| `GET /operator/api/traces/:traceId` | Encoded ID | `ok`, `trace`, `spans` as siblings |
| `GET /operator/api/commands` | `status`, `limit` | `ok`, `commands` |
| `GET /operator/api/commands/:id` | Encoded ID | `ok`, `command` |
Snapshot views are `pull-requests` (fallback), `analytics`, `memory` and `settings`. Pull-request snapshot items are capped at 100. Webhook, trace and command list limits default to 50 and are clamped to 1–100, with noninteger input falling back to 50. Command status filters accept `queued`, `running`, `succeeded` or `failed`.
Trace summaries expose identity, timing, status and attributes. They do not return top-level cost/token totals or span counts. Those values are available on detail spans, whose fields include `spanId`, `parentSpanId`, `kind`, `inputTokens`, `outputTokens` and `costUsd`. Detail does not expose input/output bodies. Missing trace, webhook or command IDs return 404.
Sources: parity `cloudflare/src/review-state-api.js:575-639,641-690,835-923`; `cloudflare/src/review-trace-queries.js:14-64,73-132`; `cloudflare/src/worker.js:97-99`.
## Reports [#reports]
`GET /operator/api/reports` accepts each of these parameters at most once. Unknown or duplicate parameters are rejected.
| Parameter | Values/defaults |
| --------------------- | -------------------------------------------------------------- |
| `repo` | `owner/name` |
| `author`, `team`, `q` | Text, at most 300 characters; `repo` has the same length limit |
| `from`, `to` | Valid `YYYY-MM-DD`; inclusive UTC; from must not follow to |
| `granularity` | `day` (default), `week`, `month` |
| `severity` | `P0`, `P1`, `P2`, `P3`, `info`, `unknown` |
| `status` | `open`, `addressed`, `not_reobserved`, `unknown` |
| `security` | `all` (default), `true`, `false`, `unknown` |
| `page` | 1–100000, default 1 |
| `pageSize` | 1–100, default 25 |
The response has `schema: son-of-anton.reports.v1`, filters/options, summary, comparison, series, rankings, coverage, notes and paginated findings, reviewed PRs and comments. A preceding-period comparison is generated only when both date bounds are explicit. Findings return nullable location fields; the API does not invent line numbers when the producer omitted them.
“Addressed” requires a resolution claim plus recorded evidence; a finding disappearing from a later review is not by itself a fix. Preserve the report's notes when interpreting metrics.
Source loading is bounded: 10000 runs, 10000 heads, 2000 configs, 30000 ledger rows, 30000 traces, 30000 findings, 1000 findings per run, 2000 per PR, and 20 MiB input. Exceeding a report bound returns 413 rather than a silently partial source report. Source history is primarily narrowed by repository; a shorter date interval does not necessarily avoid a source-history limit.
`GET /operator/api/reports/export` uses the report filters and returns a ZIP, with a 20 MiB export bound. Do not parse it as JSON. Report errors use `error` and `code` (`report_error` or `report_too_large`), not a universal `ok: false` shape.
Sources: parity `cloudflare/src/review-reports-api.js:4-6,42-132,145-179,309-354`.
## Command creation and updates [#command-creation-and-updates]
The API persists command events; it does not run reviews inside the request handler. A bridge/consumer must process them. A 202 response means **queued command**, not completed review.
All routes below have prefix `/operator/api`:
| Route | Accepted body fields |
| ------------------------------- | ------------------------------------------------------------------------------------------------ |
| `POST /commands/requeue` | Either `reviewKey` or `repo` plus `number`; optional `variant`, `reason`, `actorId`, `commandId` |
| `POST /commands/retry-publish` | `reviewKey`, optional `reason`, `actorId`, `commandId` |
| `POST /commands/replay-webhook` | `deliveryId`, optional `force`, `reason`, `actorId`, `commandId` |
| `PATCH /commands/:id` | `status`, optional `startedAt`, `completedAt`, `error`, `result` |
For a parity Worker URL in `BASE` and a configured command token:
```bash
curl --fail-with-body -sS "$BASE/operator/api/commands/requeue" \
-H "Authorization: Bearer $GILF_OPERATOR_COMMAND_TOKEN" \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: operator-review-request-123' \
--data '{"repo":"owner/name","number":123,"reason":"operator_force_review"}'
```
Use a new idempotency key for each intended new command. The header takes precedence over body `commandId`. The handler validates the request, then returns an existing ID as HTTP 200 with `created: false`; a new command returns 202 with `created: true`. It does not compare an existing command's payload with the new payload. Concurrent creation is not implemented as a single upsert.
Transitions allow the same status, `queued` to `running`, and `running` to `succeeded` or `failed`; other transitions return 409. A processed/succeeded webhook needs `force: true` to replay. `sync-repositories` commands cannot be updated through the generic PATCH route; they are owned by the native live cron bridge.
Sources: parity `cloudflare/src/review-state-api.js:328-387,423-445,934-940`.
## Repository configuration and queue repair [#repository-configuration-and-queue-repair]
`PATCH /operator/api/repos/:encodedRepo` accepts `enabled`, `branches`, `validation`, `model`, `alerts` and `reason`. Encode `owner/name` as one path segment, such as `owner%2Fname`.
* `branches`: `include`, `exclude` string lists.
* `validation`: `provider` in `local`, `crabbox`, `e2b`, `shadow`; `codeChangesOnly` boolean. This is the legacy config schema, not the native executor-name list.
* `model`: `provider`, `model`, `fallbackProvider`, `fallbackModel`, `reasoningEffort`.
* `alerts`: `telegram` boolean and `target` string.
Unknown fields are rejected. Acceptance into parity storage is not proof that an unmerged native deployment consumes the field.
`POST /operator/api/queue/requeue-dead` accepts `jobKey`, `resetAttempts`, `reason`, `actorId`. `POST /operator/api/queue/release-lease` accepts `jobKey` or `receiptId`, `force`, `reason`, `actorId`. These repair the parity **Durable Object queue**, not native Cloudflare Queue messages. The handler performs repair before recording its audit event; the two are not atomic.
Sources: parity `cloudflare/src/review-state-api.js:254-294,525-572`; `cloudflare/src/queue-do.js:282-318`.
## Memory, settings, SCM and Test Lab [#memory-settings-scm-and-test-lab]
| Family | Implemented routes under `/operator/api` |
| -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Memory | `GET /memory/integrations`; `POST /memory/integrations/preview`; `GET,POST /memory/contexts`; `GET,PATCH,DELETE /memory/contexts/:id`; `GET,POST /memory/clusters`; `PATCH,DELETE /memory/clusters/:id`; `GET /memory/knowledge`; `GET,PUT /memory/knowledge/document` |
| Settings | `GET,PATCH /settings`; `GET /settings/audit?q&limit&offset`; `GET /settings/usage?from&to` |
| Keys | `GET,POST /keys`; `POST /keys/:id/revoke` |
| SCM | `GET /scm`; `POST /scm/sync`; `DELETE /scm/alerts/:id` |
| Test Lab | `GET /test-lab`; `GET /test-lab/:id` |
Memory document GET uses `repo` and `path` query parameters; PUT passes its update in JSON. Memory context/cluster deletes and SCM alert dismissal require a body containing `revision`. SCM sync accepts an empty JSON object and returns 202 with a command. Test Lab details and limits are in [Test Lab](./test-lab).
Sources: parity `cloudflare/src/review-memory-api.js:48-118`; `cloudflare/src/review-policy-api.js:48-63`; `cloudflare/src/operator-api-keys-api.js:49-75`; `cloudflare/src/scm-api.js:23-41`; `cloudflare/src/test-lab-api.js:314-342`.
The BFF's current command and queue token routing has integration mismatches documented in [Dashboard](./dashboard). Use the route-specific backend contract above rather than assuming every UI action is already functional. No live API request was made during this source audit.
# Test Lab (/docs/operator/test-lab)
Test Lab is a read-only aggregation of recorded trace spans, not an experiment launcher. Its API is in `son-of-anton-operator-parity` on **unmerged `feat/greptile-operator-parity`**. Its UI is in `anton-ui-trace-parity`. The deployed-engine lineage, `son-of-anton-review` on `feat/cloudflare-native`, can emit the validation spans but does not expose the Test Lab routes through native ingress.
The API's `REVIEW_STATE_DB` and the engine's `DB` are separately configured bindings. Metrics only describe the data visible to the API; matching schema names do not establish that both services read the same deployment's database.
Sources: parity `cloudflare/src/worker.js:154-155`, `cloudflare/src/test-lab-api.js:314-342`; engine `cloudflare-native/src/ingress.js:264-273`, `cloudflare-native/wrangler.jsonc:73-80`.
## Experiment catalog [#experiment-catalog]
The API declares its catalog in `EXPERIMENTS`. `running` is a catalog label, not a live probe of enabled lanes or successful dispatches.
| ID | Catalog status | Declared variants |
| --------------------- | -------------- | -------------------------------------------------- |
| `validation-executor` | `running` | `managed-cf-sandbox` primary, `managed-e2b` shadow |
| `model-comparison` | `planned` | None |
| `hypothesis-shadow` | `planned` | None |
| `hypothesis-planner` | `planned` | None |
All entries declare `duration_ms` as their metric. Planned entries have no span prefix or variants and return zero runs. They are not proof that planner, model-comparison or hypothesis experiments are active. The engine's V5 activation flags are absent from native container environment forwarding.
Sources: parity `cloudflare/src/test-lab-api.js:12-55,269-282`; engine `cloudflare-native/src/container-env.js:15-70`.
## Sampling and lane interpretation [#sampling-and-lane-interpretation]
The API selects tool spans named under `validation.`. It first chooses recent trace IDs, then fetches all matching validation spans for those traces; the limit counts **runs/traces**, not spans. A second-lane row is not dropped just because the page boundary falls between two spans.
Each run contains identity fields, `prUrl`, nullable `agree`, and a `lanes` map keyed by executor. Executor and role come from attributes, with name fallbacks for `validation.cf-sandbox` and `validation.e2b` and a default role of `primary`. Multiple spans for one executor in one trace overwrite the same map entry; this is not a multi-sample-per-executor aggregation.
The engine's `passed` attribute means a plan or labeled steps existed, the lane was available, and its findings array was empty. It is not an independent replay of exit codes or a claim that all desired tests ran. An unavailable lane or no executed plan yields `passed: null`. Legacy spans without `passed` are treated as passed only when both stored findings and missing-validation counts are zero; otherwise their verdict is unknown.
Raw step output is not included by `validationSpanAttributes`; step metadata contains labels and exit codes. This narrower rule is not universal trace redaction.
Sources: parity `cloudflare/src/test-lab-api.js:84-152,171-215`; engine `src/validation-executor.js:205-245`.
## Metrics [#metrics]
| Field | Calculation |
| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `n` | Recorded lanes for that declared variant in the selected runs |
| `p50Ms`, `p95Ms` | Nearest-rank percentiles of non-null duration from available lanes |
| `unavailableRate` | Unavailable lanes divided by all lanes for the variant |
| `failRate` | Lanes with `passed === false` divided by lanes with a non-null verdict |
| `avgCostUsd`, `totalCostUsd` | Mean/sum of reported lane costs; null if none were reported |
| `agreeRate` | Runs where all declared variants are present, available and have matching non-null verdicts, divided by comparable runs |
Agreement says the stored verdicts match, not that either verdict is correct. Null agreement is not disagreement. Rates and percentiles are over the bounded selection, not all historical reviews.
Lane cost prefers the span's `cost_usd`, then the `costUsd` attribute. The engine's wall-clock estimates and numeric-coercion limits are described in [Observability and cost](../architecture/observability-and-cost); these are not invoice totals.
Sources: parity `cloudflare/src/test-lab-api.js:120-159,193-265`; engine `src/validation-executor.js:189-245`.
## Shadow validation configuration [#shadow-validation-configuration]
The checked-in native Worker configuration selects:
```text
GILF_VALIDATION_EXECUTOR=managed-cf-sandbox
GILF_VALIDATION_SHADOW_EXECUTOR=managed-e2b
GILF_VALIDATION_SHADOW_TIMEOUT_MS=600000
GILF_E2B_TIMEOUT_MS=600000
GILF_E2B_BUDGET_USD=50
```
These are repository configuration values, not a live status report. The forwarding allowlist also includes `E2B_API_KEY`, E2B shape/template settings and `GILF_VALIDATION_COST_RATES`. Secret provisioning belongs in [Validation executors](../configuration/validation-executors), not an unauthenticated Test Lab request.
The shadow selector must resolve to a supported, different executor with a wired collector. Legacy `local` and `shadow` routes are not valid comparison lanes. An unwired shadow collector does not run and emits no comparison sample.
Sources: engine `cloudflare-native/wrangler.jsonc:136-150`; `cloudflare-native/src/container-env.js:55-69`; `src/validation-executor.js:251-267,389-396`.
## Shadow affects timing and spend, not findings [#shadow-affects-timing-and-spend-not-findings]
`runValidation` starts primary and shadow collectors concurrently. The shadow result is converted to telemetry and is not merged into primary findings, missing validations or validation summary. Exceptions and timeout results become error-shaped lane attributes.
However, the runner can **wait for the shadow lane after its model call**, using the remaining timeout measured from validation start. The default is ten minutes total, not ten extra minutes after the model finishes. The wait can delay publication. A promise timeout does not itself cancel the underlying collector; collector timeouts and cleanup remain important.
Do not call shadow validation free, harmless or incapable of delaying a review. Its verdict is observational, but its latency and compute costs are real.
Sources: engine `src/validation-executor.js:378-473`; `src/codex-review-runner.js:1112-1149`.
## Budget is an estimate gate [#budget-is-an-estimate-gate]
The native dispatcher reads all-time `SUM(cost_usd)` for spans named `validation.e2b`. At or above `GILF_E2B_BUDGET_USD` (default 50), it withholds only the E2B shadow selector. A missing database binding or failed spend read leaves the selector unchanged.
The SQL has no repository, date or role filter. It includes any matching E2B span, not just the rows selected on screen. It does not reserve spend before dispatch, account for unflushed runs, or charge missing telemetry. Concurrent work and provider billing can exceed the displayed ceiling.
Test Lab's `usedUsd` uses the same query shape; `limitUsd` is read from the API Worker's own `GILF_E2B_BUDGET_USD`. Agreement with the native gate requires the same underlying data and limit configuration. Changing only the UI/API limit does not change the native dispatcher.
Sources: engine `cloudflare-native/src/container-env.js:100-141`; parity `cloudflare/src/test-lab-api.js:304-340`.
## API and UI [#api-and-ui]
| Route | Success shape |
| -------------------------------- | -------------------------------------- |
| `GET /operator/api/test-lab` | `ok`, `experiments` |
| `GET /operator/api/test-lab/:id` | `ok`, `experiment`, `variants`, `runs` |
Parameters: `days` defaults to 30, clamped to 1–365; `limit` defaults to 200, clamped to 1–1000; `repo` is an optional exact filter. Numeric inputs are truncated to integers; invalid numbers fall back. An authenticated request without `REVIEW_STATE_DB` returns `503 backend_not_configured`; an unknown experiment ID returns 404 after the data-loading step succeeds.
For a parity API base URL in `OPERATOR_URL`:
```bash
curl --fail-with-body -sS \
-H "Authorization: Bearer $GILF_OPERATOR_READ_TOKEN" \
"$OPERATOR_URL/operator/api/test-lab/validation-executor?days=14&limit=200&repo=owner/name"
```
The API requires the read token or an allowed `read` API key. The parity browser uses BFF session authentication instead, with `/api/test-lab` and `/api/test-lab/:id`; those client calls use a 30-second timeout. UI routes are `/test-lab` and `/test-lab/:id`.
Sources: parity `cloudflare/src/test-lab-api.js:63-68,314-342`, `cloudflare/src/worker.js:80,117-133`; UI `src/lib/test-lab-api.ts:82-102`, `src/App.tsx:24-25`.
No experiment or runtime was launched during this source audit. [Cost and traces](./cost-and-traces) explains how to inspect the underlying observations.
# Comparison with Hosted Reviewers (/docs/reference/comparison-with-hosted-reviewers)
Son of Anton offers operator-controlled model and validation choices. It does not establish feature parity or review-quality superiority over a hosted reviewer. This page compares evaluation requirements with the inspected implementation, not competitor pricing, benchmarks or current product claims.
`R/` is `son-of-anton-review`, branch `feat/cloudflare-native`. `P/` is `son-of-anton-operator-parity`, branch `feat/greptile-operator-parity`. The engine and parity API are not one merged deployable tree. `anton-ui` and `anton-ui-trace-parity` likewise have different capabilities.
## Decide by operating model [#decide-by-operating-model]
Use the operator-controlled approach when you need to choose the model provider, own the review infrastructure and inspect its run evidence, and can operate the required services. Evaluate a hosted alternative when managed onboarding, vendor support or capabilities missing below are requirements. Source access, support commitments, public availability and license rights must be established separately; local source is not a public distribution offer.
**Do not call this product open source yet.** The verified handoff records a private product repository and no established license. The local package still declares `private: true`, points at the old repository URL and contains no license field (`R/package.json:1-25`). Package privacy alone does not determine legal rights; neither a README statement nor access to this workspace grants an open-source license.
## Implemented paths and their limits [#implemented-paths-and-their-limits]
| Requirement | Inspected implementation | Evidence |
| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Choose the review model | OpenRouter recommended; OpenAI, Anthropic and Codex alternatives. Node and native selection variables differ. | `R/src/model-provider.js:15,169-325`; `R/worker.mjs:43-54`; `R/cloudflare-native/container/entrypoint.mjs:314-348` |
| Choose isolated validation | Managed Crabbox, E2B, Cloudflare Sandbox and a self-host command. Local host execution is a separate explicit opt-in, not a fallback. | `R/src/validation-executor.js:30-49,485-579` |
| Inspect review cost | Token-based model rate cards and wall-clock validation estimates. Unknown pricing is not zero; these are not invoices. | `R/src/model-provider.js:39-93`; `R/src/validation-executor.js:146-202` |
| Run without native Cloudflare containers | Node server plus a bounded queue-draining worker; shared SQLite or legacy Durable Object queue configuration is required across processes. | `R/server.mjs:27-50`; `R/src/worker-runner.js:29-72` |
| Separate the App private key from review containers | Native key-broker service; scoped clone/publish callers. The broker helper can also mint without a repository list, so scope depends on the caller. | `R/cloudflare-native/src/key-broker-core.js:15-55`; `R/cloudflare-native/container/entrypoint.mjs:110-118` |
| Dry-run publication | Native code defaults to shadow, but the checked-in manifest explicitly selects live. Node constructs a live publisher when App credentials are supplied. | `R/cloudflare-native/src/publish-mode.js:26-69`; `R/cloudflare-native/wrangler.jsonc:132-136`; `R/worker.mjs:35-41` |
| Configure workspace review policy | Defaults and admission/formatting helpers exist in the engine; settings API and editor are on parity trees. | `R/src/operator-review-policy.js:25-108,154-238`; `P/cloudflare/src/review-policy-api.js:48-63`; `anton-ui-trace-parity/src/views/Settings.tsx:182-268` |
| Explore graph impact | Context graph computes impacted paths, inferred tests and risk drivers. These are static relationships, not runtime proof. | `R/src/context-graph.js:90-145` |
| Track finding continuity | Fingerprints, comparison states and same-head disposition checks. Omission on an unchanged head is not automatically a fix. | `R/src/finding-evolution.js:201-211,267-375`; `R/src/review-service.js:397-413` |
| Derive feedback priors | Outcome ledger produces heuristic addressed rates and lens guidance. This is not complete GitHub reaction-based learning. | `R/src/addressed-rate.js:92-149,178-214`; `R/src/codex-review-runner.js:2232-2238` |
See [Model providers](../configuration/model-providers), [Validation executors](../configuration/validation-executors) and [Dashboard settings](../configuration/dashboard-settings) before configuring any of these paths.
## Review ergonomics [#review-ergonomics]
### Findings are not inline review comments [#findings-are-not-inline-review-comments]
The model finding schema contains a path but no line coordinates. The publisher posts a PR review body, check run and optional issue comments. It can patch an existing check id; it does not update an existing PR review body or post per-line review comments. The declared `inlineComments` config key is not an implemented inline publisher.
This matters when your workflow depends on commenting directly on a changed line, applying a patch in GitHub or gathering reactions to individual inline findings. A path and title are not sufficient to offer those workflows.
Evidence: `R/src/codex-review-runner.js:210-224`; `R/src/github-publisher.js:22-79`; `R/src/config.js:5-9`.
### Prompt to Fix is text, not an applyable patch [#prompt-to-fix-is-text-not-an-applyable-patch]
`promptToFix` defaults false. When enabled, the review includes a copyable instruction block naming the commit and actionable findings. It does not contain an automatically applyable GitHub suggestion or a verified fix. The coding assistant must inspect and verify the change independently.
Evidence: `R/src/operator-review-policy.js:41`; `R/src/review-format.js:235-242`.
### Repository context is not credential configuration [#repository-context-is-not-credential-configuration]
The runner can read context such as `AGENTS.md`, `CLAUDE.md`, `pr-review-agent.yml`, `.github/CODEOWNERS` and `.cursor/rules`. The prompt treats repository and operator guidance as untrusted review data. Policy storage and runtime environment remain separate authorities; see [Repo scoping](../configuration/repo-allowlist-and-scoping).
Evidence: `R/src/codex-review-runner.js:1499-1526,1840-1853`.
### Diagrams are ASCII [#diagrams-are-ascii]
The prompt requests plain-text arrows/boxes, explicitly not Mermaid. The review renderer uses a text fence for `sequenceDiagram`. Documentation pages may use Mermaid; that does not change posted review output.
Evidence: `R/src/codex-review-runner.js:1875`; `R/src/review-format.js:235`.
## Built but not default behavior [#built-but-not-default-behavior]
| Feature | Gate and actual default | Deployment limitation |
| ------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| Hypothesis primary review | `GILF_HYPOTHESIS_PRIMARY=1`, off by default | No automatic shadow swarm with flags unset; a worker must exist. |
| Prime worker | `GILF_PRIME_SHADOW=1`, or implied by primary flag unless explicitly `0` | The primary flag can make its output authoritative. Do not describe all Prime output as never published. |
| LLM planner | `GILF_HYPOTHESIS_PLANNER=1`, off | Also needs a worker and non-Codex planner provider; static hypotheses remain a floor. |
| Artifact evidence | `GILF_EVIDENCE_ARTIFACTS=1`, off | Needs artifact storage. Enforcement marks unsupported execution claims unverified rather than reducing severity. |
| Model inversion | `GILF_MODEL_INVERSION`, off | Heuristic author detection and configured alternate target; failures can fall back to the primary provider. |
| Auto-approve | `autoApprove.enabled: false` | Requires confidence 5, complete pinned coverage, no missing validation, allowed risk and filters, and no substantive findings. |
| PR description summary | `updatePrDescription: false` | Publisher checks head SHA and preserves text outside its owned markers. |
**The native projection forwards none of the swarm, Prime, planner, inversion or evidence flags.** Enabling them requires integration code changes, not just Worker variables. The policy toggles are different from process flags.
Evidence: `R/src/codex-review-runner.js:2300-2314,2657-2707,3248-3267,3342-3353,3517-3569`; `R/cloudflare-native/src/container-env.js:15-86`; `R/src/operator-review-policy.js:227-238`; `R/src/github-publisher.js:82-103`. See [Roadmap and flags](../reference/roadmap-and-flags).
## Gaps and branch-only capabilities [#gaps-and-branch-only-capabilities]
| Requirement | Current evidence and limit |
| ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| GitLab / Bitbucket | Inspected SCM integration reports `provider: 'github'`; no GitLab/Bitbucket adapter was found in the inspected engine or parity source trees. `R/src/scm-store.js:59-76`. |
| Developer CLI for a local diff | The shipped `gilf-review` CLI operates on stored runs and queues (`usage`, `harness`, `show`, `requeue`, reports), not an arbitrary local diff review. `R/src/operator-cli.js:574-644`; `R/package.json:9-12`. |
| IDE / MCP integration | The standalone private `anton-mcp` package now serves MCP SDK v2: public HTTP documentation/onboarding, plus local stdio operator reads and opt-in, confirmed memory/review commands over P's APIs. It does not merge P into the native engine, install an IDE extension or review arbitrary local diffs. See [MCP connection and permissions](../getting-started/agent-onboarding#connect-with-mcp-v2). |
| Ticket context | **Partial on parity:** explicit Linear issue snapshot preview/import exists, plus Confluence and Devin source previews. This is not automatic ticket discovery for every PR, and not a Jira integration. `P/src/memory-source-import.js:11-18,170-183,224-232`. |
| Built-in deterministic SAST / SCA | No dedicated Semgrep/Trivy/OSV/Snyk/Gitleaks integration was found in the inspected runtime source. A repository's own validation scripts may still run scanners. |
| Browser verification | A Frontend Behavior hypothesis exists; that is not a built-in browser-driving evidence collector. `R/src/review-hypotheses.js:54-61`; the validation dispatcher is `R/src/validation-executor.js:485-579`. |
| Historical quality benchmark | The benchmark CLI scores supplied fixture outputs. The inspected fixture directory contains `v5-smoke.json`; it is not a representative historical-PR quality corpus. `R/evals/v5-benchmark-cli.mjs:10-24,106-131`. |
| Unified dashboard and review engine | Expanded settings/reports/Test Lab APIs and UI are on parity trees. Native public fetch routes to ingress, not those APIs. `R/cloudflare-native/src/main.js:26-29`; `P/cloudflare/src/worker.js:142-157`; `anton-ui/worker/proxy-utils.js:15-23`; `anton-ui-trace-parity/worker/proxy-utils.js:15-38`. |
No numerical superiority, uptime, savings or addressed-rate claim follows from this table. Validate the exact deployed branch, provider, schema and run evidence before making an operational comparison.
# FAQ (/docs/reference/faq)
`R/` means `son-of-anton-review` (`feat/cloudflare-native`); `P/` means `son-of-anton-operator-parity` (`feat/greptile-operator-parity`). They are separate, unmerged trees. Answers describe inspected source, not a live deployment probe.
## Is Son of Anton open source? [#is-son-of-anton-open-source]
Public source availability and license terms have not been established. The verified handoff records a private repository with no license. The local package declares `private: true`, has no license field and still points to the old repository URL (`R/package.json:1-25`). Do not infer distribution rights from source access or a README claim.
## Do model keys ever leave the environment? [#do-model-keys-ever-leave-the-environment]
Yes, they must be sent to the selected provider to authenticate requests. The application resolves keys from environment and does not expose their values in the provider-status DTO or Models UI. That is narrower than claiming a key never leaves the process or can never be logged anywhere.
| Provider | Environment lookup |
| ---------- | ---------------------------------------------------- |
| OpenRouter | `OPENROUTER_API_KEY`, then `GILF_OPENROUTER_API_KEY` |
| OpenAI | `OPENAI_API_KEY`, then `GILF_OPENAI_API_KEY` |
| Anthropic | `ANTHROPIC_API_KEY`, then `GILF_ANTHROPIC_API_KEY` |
Native Cloudflare forwards bare names only. The native App private key remains on the separate broker; model keys do reach the review container. Do not paste keys into repository instructions, review bodies or diagnostic reports.
Sources: `R/src/model-provider.js:24-37,174-185,212-227,254-273,363-385`; `R/cloudflare-native/src/container-env.js:11-70`.
## Which provider should I use? [#which-provider-should-i-use]
OpenRouter is recommended. OpenAI direct and Anthropic direct are other HTTP options; Codex CLI uses its own local authentication. OpenRouter is not uniquely headless.
**Select variables for your runtime:** native entrypoint accepts `GILF_MODEL_PROVIDER` and `GILF_MODEL`; Node `worker.mjs` still needs `GILF_CODEX_PROVIDER` and `GILF_CODEX_MODEL`. Always select a model explicitly. The generic resolver's per-provider defaults are not wired into these entrypoints.
`GILF_OPENROUTER_REQUIRE_FREE` is on unless exactly `0`; nonzero or unknown catalog pricing is refused. The checked-in native manifest explicitly opts into paid models. A cost-rate override cannot bypass the catalog guard.
Sources: `R/worker.mjs:43-54`; `R/cloudflare-native/container/entrypoint.mjs:314-348`; `R/src/codex-review-runner.js:2022-2057,2320-2337`; `R/cloudflare-native/wrangler.jsonc:151-161`. See [Model providers](../configuration/model-providers).
## Why did validation refuse to run? [#why-did-validation-refuse-to-run]
An unavailable or unsafe executor is a missing validation, not a passed check or automatically a defect in the PR. The engine does not silently fall back to host-local execution. Summary text varies by route.
| Route | Common cause |
| ------------------ | --------------------------------------------------------- |
| Self-host | Missing `GILF_SELF_HOST_VALIDATION=1` or command |
| E2B | Missing `E2B_API_KEY`, SDK or sandbox failure |
| Cloudflare Sandbox | Collector/binding not wired or sandbox transport failure |
| Crabbox | Missing binary or unreviewed repo-local executable config |
| Local | `GILF_ALLOW_LOCAL_VALIDATION` is not exactly `1` |
Publication is a separate decision. A refusal can appear in a posted review if analysis and publishing succeed, but it does not guarantee publication. Do not enable host-local execution merely to clear the warning.
Sources: `R/src/validation-executor.js:287-296,485-579`; `R/src/codex-review-runner.js:1254-1340`; `R/cloudflare-native/src/cf-sandbox-validation.js:99-146`. See [Validation executors](../configuration/validation-executors).
## Why are there no inline comments? [#why-are-there-no-inline-comments]
The model finding schema contains a path, not line coordinates. The publisher posts a PR review body, check run and optional issue comments; it does not send inline comments or applyable suggestion patches. Check runs can be patched when an id is available. PR review bodies are posted, not updated in place across heads.
Sources: `R/src/codex-review-runner.js:210-224`; `R/src/github-publisher.js:22-79`. The declared `inlineComments` config key does not implement this missing capability. See [Review output schema](../reference/review-output-schema).
## Can a shadow lane affect the posted review? [#can-a-shadow-lane-affect-the-posted-review]
Distinguish three different mechanisms:
* **Shadow model:** actual runner selector is `GILF_CODEX_SHADOW_MODEL`, optionally `GILF_CODEX_SHADOW_PROVIDER`. The call follows successful primary semantic review and is stored as `shadowReview`, not published separately. `GILF_MODEL_SHADOW` alone does not activate it.
* **Shadow validation executor:** a supported, different and wired executor emits comparison telemetry. Its findings/missing validations are not merged into primary output. It still consumes compute and a bounded wait.
* **Hypothesis shadow:** not the same guarantee. Usable hypothesis results can recover failed monolithic analysis, and `GILF_HYPOTHESIS_PRIMARY=1` can promote them to the primary result.
With no enabling flags and no injected worker, the swarm returns `null`. Native Cloudflare does not forward the model-shadow or V5 worker/planner/evidence/inversion flags; changing those Worker vars alone has no effect.
Sources: `R/src/codex-review-runner.js:2324-2337,2657-2707,3198-3267`; `R/src/validation-executor.js:365-430`; `R/cloudflare-native/src/container-env.js:15-86`.
## Why is a successful native review not visible on GitHub? [#why-is-a-successful-native-review-not-visible-on-github]
Native code defaults to `GILF_PUBLISH_MODE=shadow`, which selects a recording publisher. Confirm the actual mode, policy and publication state, not just analysis success. The checked-in manifest explicitly sets live, so source defaults alone do not identify a deployment's mode.
The container logs `omp-container: publish mode` with the resolved value. Live mode requires broker transport and refuses a local SQLite dev-container path without it. On **Node**, the env publish-mode gate is not used: supplying App credentials constructs a live publisher.
Sources: `R/cloudflare-native/src/publish-mode.js:26-69`; `R/cloudflare-native/container/entrypoint.mjs:273-302`; `R/cloudflare-native/wrangler.jsonc:133`; `R/worker.mjs:35-41`.
## Can I run without Cloudflare? [#can-i-run-without-cloudflare]
Yes. The Node server can share SQLite state and queue files with the Node worker. Supply absolute paths in both processes, for example:
```bash
export GILF_DB_PATH=/absolute/path/to/data/anton.db
export GILF_QUEUE_DB_PATH=/absolute/path/to/data/anton-queue.db
```
Use your real writable paths, not these illustrative ones. The entrypoint commands are `node server.mjs` for the server and `node worker.mjs` for a bounded worker batch, run separately. The worker is **not** a persistent polling service: defaults are one job and one concurrent worker per invocation, so provide an appropriate supervisor/scheduler for continuous operation.
A configured `GILF_QUEUE_URL` plus `GILF_QUEUE_TOKEN` wins over SQLite. Without remote configuration or shared database paths, separate processes use separate in-memory queues and do not exchange jobs. The server requires `GITHUB_WEBHOOK_SECRET`; provider, validation and GitHub setup are additional prerequisites, not supplied by the two database variables.
Sources: `R/server.mjs:21-50,108-112`; `R/src/worker-runner.js:29-72`; `R/worker.mjs:64-66`. See [Run on a Node box](../self-host/run-on-a-node-box).
## Is `GILF_REPOS` a security allowlist? [#is-gilf_repos-a-security-allowlist]
No. It seeds Node repo configuration. An absent repo record is merged over defaults that enable automatic review on `main` and `dev`. Native Cloudflare instead derives enablement from stored config, previous reviewed state and `autoEnableNewRepos`. GitHub App installation access and authorization are separate controls.
Sources: `R/server.mjs:73-74`; `R/src/config.js:1-9,86-97`; `R/src/review-service.js:1295-1310`; `R/cloudflare-native/src/d1-store-adapter.js:153-172`. See [Repo scoping](../configuration/repo-allowlist-and-scoping).
## Why `GILF_` and not `ANTON_`? [#why-gilf_-and-not-anton_]
The runtime still reads `GILF_` names. Do not rename them based on product branding. The parity Worker has a narrower canonical alias mechanism: `SON_OF_ANTON_` names win over `GILF_` for the suffixes its callers request. This does not alias arbitrary Node or native container flags, and no future cutover date is promised here.
Sources: `R/cloudflare-native/src/container-env.js:15-70`; `P/cloudflare/src/env.js:1-10`.
## What does `@gilf` do? [#what-does-gilf-do]
It is a supported alias for `@anton` in the command parser. Both prefixes accept `review`, `rerun`, `status` and `help`, at the start of the comment, case-insensitively. Parsing a command is not authorization or proof every runtime handles every command the same way. Native container dispatch distinguishes review/rerun requests; Node also implements status/help responses.
Sources: `R/src/commands.js:1-39`; `R/src/review-service.js:764-834`; `R/cloudflare-native/src/ingress.js:80-114`. See [Triggers and commands](../code-review/triggers-and-commands).
## How do I see cost? [#how-do-i-see-cost]
Inspect persisted run/model telemetry and the appropriate dashboard trace surface. Known usage is multiplied by built-in OpenAI/Anthropic rates or `GILF_MODEL_PRICES`; OpenRouter has no built-in rate table. Unknown price or usage can produce `null`. Do not enter zero rates unless they are actually applicable.
Validation costs use separate wall-clock rate cards. Dashboard provider badges indicate its backend environment, not a successful review-container call. Expanded parity dashboard routes do not exist on native ingress.
Sources: `R/src/model-provider.js:39-93`; `R/src/validation-executor.js:146-202`; `anton-ui/worker/proxy-utils.js:15-23`; `anton-ui-trace-parity/worker/proxy-utils.js:15-38`. See [Observability and cost](../architecture/observability-and-cost).
## Does it review docs-only PRs? [#does-it-review-docs-only-prs]
Yes, potentially on a deterministic fast path. Validation is skipped when code-changes-only config is enabled and preflight classifies a docs-only diff. The semantic model skip additionally requires no supplied memory text, no rendered operator guidance, no command and no competitor context. Thus an automatic docs-only PR does not always skip the model.
`GILF_CODEX_FOR_DOCS_ONLY=1` disables the semantic fast-path skip in a process that actually receives it. It is not forwarded by the native container projection. Generated-artifact-only diffs also skip validation under code-changes-only config, but that alone is not the docs-only model skip.
Sources: `R/src/codex-review-runner.js:840-848,2528-2548,2620-2656`; `R/cloudflare-native/src/container-env.js:15-86`.
## Where do I start when a review is stuck? [#where-do-i-start-when-a-review-is-stuck]
[Troubleshooting](../reference/troubleshooting) separates ingress, admission, dispatch, model, validation, persistence and publication failures. Keep the delivery id, review key, head SHA, error and phase; omit secrets. An HTTP health response, configured badge or empty findings list is not evidence that the whole review succeeded.
# Glossary (/docs/reference/glossary)
`R/` means `son-of-anton-review` on `feat/cloudflare-native`. `P/` means `son-of-anton-operator-parity` on `feat/greptile-operator-parity`. These are separate, unmerged trees. `anton-ui` is the original dashboard; `anton-ui-trace-parity` adds the parity editor and session-authenticated proxy. A defined term does not mean that feature is enabled or deployed.
## Identity and deduplication [#identity-and-deduplication]
| Term | Meaning | Source |
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| Review key | Base identity `owner/repo#number@headSha`. Manual requests can add a command-specific variant. Not a guarantee that one head has only one review. | `R/src/pr-update-engine.js:35-40`; `R/cloudflare-native/src/ingress.js:80-114` |
| Head SHA | Commit to which review metadata and code evidence are pinned. Missing head is a container error, not permission to inspect an arbitrary branch tip. | `R/cloudflare-native/container/entrypoint.mjs:82-94`; `R/src/review-policy-runtime.js:56-71` |
| Delivery id | `X-GitHub-Delivery` header. Native ingress uses a KV dedupe marker and records the accepted payload in D1 `webhook_deliveries`. KV is eventually consistent; downstream leases handle duplicate enqueue. | `R/cloudflare-native/src/ingress.js:227-260,275-346` |
| Latest-waiting key | `owner/repo#number:latest-waiting`, used by the PR-update engine to track the latest waiting head. Do not confuse the legacy replace-latest queue with native Cloudflare Queues. | `R/src/pr-update-engine.js:39-40,149-153,195-198` |
## Runtime components [#runtime-components]
| Term | Meaning | Source |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------- |
| Ingress | Public webhook handler: verify signature, normalize intent, record delivery and enqueue. `/health` proves this handler responds, not that review dependencies work. | `R/cloudflare-native/src/ingress.js:264-346` |
| Consumer | Native Queue handler that resolves review identity, checks obsolescence and claims a mode-scoped dispatch lease before invoking a container. | `R/cloudflare-native/src/consumer.js:310-412` |
| OMP container | Ephemeral review process. The name does not imply it invokes OMP CLI: the default entrypoint selects OpenRouter HTTP. Validation uses a separate sandbox collector. | `R/cloudflare-native/container/entrypoint.mjs:314-374` |
| Key broker | Separate native Worker holding the App private key and minting installation tokens. Review callers request repo scope, but the core helper does not require a repository list. | `R/cloudflare-native/src/key-broker-core.js:15-55` |
| Command bridge | Scheduled handler that drains operator command events. Its schedule is once a minute; a queued command is not proof it completed. | `R/cloudflare-native/src/cron.js:27,45-59`; `R/cloudflare-native/src/cron-command-bridge.js` |
| Nightly audit / enforcer | Nightly audit handler with two UTC triggers gated to 23:30 Europe/London, not two nightly executions. | `R/cloudflare-native/src/cron.js:46-59`; `R/cloudflare-native/src/cron-nightly.js` |
| Watchdog | Scheduled health evaluation at minutes 03, 18, 33 and 48. Watches the loop, itself, command bridge and nightly audit; Prime-watch is excluded. | `R/cloudflare-native/src/cron.js:38-42,53-88` |
| Prime-watch | Handler exists but is intentionally absent from scheduled routes. Not the same as an enabled hypothesis worker. | `R/cloudflare-native/src/cron.js:61-73` |
## Review pipeline [#review-pipeline]
| Term | Meaning | Source |
| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| Preflight | Deterministic diff/context preparation. The event-to-span mapping calls it `context.load`; graph construction has its own `context.graph` span. | `R/src/codex-review-runner.js:35-41,840-894` |
| Context graph | Repository facts and derived impact used to identify related files and risks. A graph relation is not execution evidence. | `R/src/context-graph.js:90-145` |
| Blast radius | Count of impacted paths in the graph result, used in risk scoring. | `R/src/context-graph.js:113,130-141` |
| Review packet | Documentation shorthand for the context assembled into the model prompt; not an independently versioned transport API. | `R/src/codex-review-runner.js:1840-1875` |
| Lens | Review dimension such as Payment Integrity, Auth Boundary or API Contract. Frontend Behavior is a lens, not proof of an implemented browser runner. | `R/src/review-hypotheses.js:1-72` |
| Hypothesis | A candidate concern with files, required evidence types and falsification criteria. Static path rules and the optional LLM planner are different sources. | `R/src/review-hypotheses.js:160-182`; `R/src/hypothesis-planner.js` |
| Planner | Optional model-driven hypothesis generation. Off by default; no worker or a Codex planner provider prevents the call. | `R/src/codex-review-runner.js:2309,3342-3353` |
| Swarm | Bounded hypothesis workers. Runner concurrency defaults to 4, but **no worker means no swarm**, including no shadow swarm. | `R/src/codex-review-runner.js:2351-2357,3248-3267`; `R/src/hypothesis-orchestrator.js:103-137` |
| Synthesis | Filters worker findings for the hypothesis's evidence requirements, then deduplicates and compares them with prior findings. | `R/src/hypothesis-orchestrator.js:134-170` |
| Evidence filter | Requires a matching dimension, falsifiability and the required evidence-type labels. It is not itself a test runner or verification of the claimed evidence's truth. | `R/src/review-hypotheses.js:185-214` |
Swarm, planner, Prime, inversion and artifact-evidence flags are **not forwarded into native Cloudflare containers**. See [Roadmap and flags](../reference/roadmap-and-flags) and `R/cloudflare-native/src/container-env.js:15-86`.
## Validation and artifacts [#validation-and-artifacts]
| Term | Meaning | Source |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| Validation executor | Dispatcher for managed Crabbox, E2B, Cloudflare Sandbox or operator-supplied isolated compute. Legacy `local` requires explicit host-execution opt-in; unavailable sandboxes do not fall back to it. | `R/src/validation-executor.js:30-49,485-579` |
| Shadow validation lane | A different, supported and wired executor whose result is telemetry, not primary findings. Still consumes resources and has a bounded wait. | `R/src/validation-executor.js:251-282,365-430` |
| Missing validations | Review strings recording absent, skipped or incomplete checks. An empty finding list does not erase these gaps. | `R/src/validation-executor.js:287-296`; `R/src/review-format.js:215-218` |
| Evidence artifact | Persisted validation/worker output matched to findings by fingerprint or hypothesis id. A reference does not guarantee a public URL. | `R/src/evidence-artifacts.js:481-517` |
| Artifact enforcement | With both evidence flags enabled, an execution claim without a matching artifact is marked `unverified`; its severity is not automatically reduced. Module exceptions return the original findings. | `R/src/evidence-artifacts.js:520-558`; `R/src/codex-review-runner.js:3517-3569` |
## Publication and recovery [#publication-and-recovery]
| Term | Meaning | Source |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| Publish mode | Native `shadow` selects `RecordingPublisher`; native `live` requires an authenticated publisher. Code default is shadow; the checked-in manifest explicitly uses live. Node does not use this env gate. | `R/cloudflare-native/src/publish-mode.js:26-69`; `R/cloudflare-native/wrangler.jsonc:132-136`; `R/worker.mjs:35-41` |
| Publish ledger | Review-key/action records for publication state and receipts. Different from the native container-dispatch lease. | `R/cloudflare-native/schema.sql:62-75,111-126` |
| Lease | Ownership record used to avoid concurrent dispatch/publication. Losing a lease is a reason to stop, not to force another publish. | `R/cloudflare-native/src/consumer.js:369-412`; `R/worker.mjs:89-128` |
| Reconciler | Node recovery component invoked before draining the queue; recovery decisions depend on stored run state. | `R/worker.mjs:63-75`; `R/src/reconciler.js` |
| Dead letter | Native unroutable or exhausted message, sent to the dead-letter binding and recorded for cron observability when possible. Recording and sending are separate operations and can fail separately. | `R/cloudflare-native/src/consumer.js:256-284,325-328,455-461` |
## Findings and memory [#findings-and-memory]
| Term | Meaning | Source |
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| Finding | Model schema item with `severity`, `category`, `path`, `title`, `body`, `contextIds`. **No line number or per-finding confidence field is required.** | `R/src/codex-review-runner.js:210-224` |
| Fingerprint | Stable finding identity from explicit identity or normalized finding content. Does not create an inline code location. | `R/src/finding-evolution.js:201-211` |
| Evolution states | `new`, `persisting`, `modified`, `resolved`, and `unresolved`. Resolution is a comparison result, not universal proof of a fix. | `R/src/finding-evolution.js:267-310` |
| Disposition | Supported claim closing a prior same-head concern: `resolved`, `false_positive` or `no_longer_applicable`, with source review, fingerprint and current-head code evidence. | `R/src/codex-review-runner.js:226-247`; `R/src/review-service.js:397-413` |
| Outcome | Heuristic classification: `fixed`, `acknowledged`, `ignored`, `dismissed`, `unknown`. Not necessarily a direct author action receipt. | `R/src/addressed-rate.js:11,92-149` |
| Addressed rate | `(fixed + acknowledged) / (total - unknown)`; null without decided outcomes. This is the outcome-ledger metric, not a guarantee every dashboard KPI uses that formula. | `R/src/addressed-rate.js:178-197` |
| Priors | Lens-level guidance derived from a repo's outcomes. Defaults need 5 decided samples and suggest suppression below addressed rate 0.2. | `R/src/addressed-rate.js:204-214`; `R/src/codex-review-runner.js:2232-2238` |
| Memory context | Stored rule or file context with scope. Model `contextIds` may only cite supplied memory ids, not arbitrary paths, graph facts or feedback ids. | `R/cloudflare-native/schema.sql:265-281`; `R/src/codex-review-runner.js:1852` |
| Knowledge base | Stored repository knowledge documents, distinct from citable memory-context ids. | `R/cloudflare-native/schema.sql:292-305`; `R/src/codex-review-runner.js:1852,2511-2515` |
## Scales and operator terms [#scales-and-operator-terms]
| Term | Meaning | Source |
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| Severity | Model finding enum: `blocker`, `high`, `medium`, `low`. Other internal lanes/policy helpers accept additional priorities; they do not expand the model schema. | `R/src/codex-review-runner.js:215-222`; `R/src/operator-review-policy.js:183-203` |
| Confidence score | Integer 1 to 5 representing analyst confidence, not merge readiness. | `R/src/codex-review-runner.js:194,1844` |
| Verdict / merge verdict | `verdict` is `clear` or `needs-attention`; `mergeVerdict` is prose. Derived merge status also considers findings, validation and analyzer health. | `R/src/codex-review-runner.js:193-197`; `R/src/review-format.js:20-65` |
| Strictness | Low retains findings. Medium removes rank-2 findings only with known confidence below 0.8. High removes ranks 2/3. Protected safety findings remain. | `R/src/operator-review-policy.js:183-215` |
| Operator policy | Revisioned workspace document. Endpoint exists on parity Worker, not native public ingress. | `P/cloudflare/src/review-policy-api.js:48-63`; `R/cloudflare-native/src/main.js:26-29` |
| Filter rule | Conditions combined with AND; rules combined with OR; missing metadata can defer admission. | `R/src/operator-review-policy.js:46-78,110-176` |
| Execution mode | `always`, `never` or `filters`; filters can defer until metadata is available. | `R/src/operator-review-policy.js:178-180` |
| Test Lab experiment | Static experiment definition plus persisted-span analysis. A `running` label does not prove samples exist. | `P/cloudflare/src/test-lab-api.js:12-61,90-118` |
## Observability and legacy names [#observability-and-legacy-names]
| Term | Meaning | Source |
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| Trace / span | Run telemetry and nested operations. Native storage supports workflow, model, tool, function, http, queue, worker, agent, retrieval, chat, embeddings and stream kinds. | `R/cloudflare-native/schema.sql:187-222` |
| `estimate:wall-clock` | Validation rate-card cost estimate, not a billing receipt. Unknown executor rates return null. | `R/src/validation-executor.js:146-202` |
| Harness version | Stored harness identity/status used by operator reporting. The CLI's `harness` command also saves the reported deployed version when supported. | `R/cloudflare-native/schema.sql:79-84`; `R/src/operator-cli.js:619-623` |
| `GILF_`, `gilf-`, `@gilf` | Existing configuration/file names and supported command alias. Do not replace them with assumed `ANTON_` names. | `R/src/commands.js:1-26`; `R/cloudflare-native/src/container-env.js:15-70` |
| `codex_` events | Historical event naming still used for semantic-review skips and recovery, even with HTTP providers. | `R/src/codex-review-runner.js:2651-2707` |
| omarchy | Name used in native scheduler comments for the previous systemd authority host; not a required hostname for self-hosting. | `R/cloudflare-native/src/cron.js:1-5,33-52` |
# Review Output Schema (/docs/reference/review-output-schema)
Semantic review is requested as one JSON object. The runner applies a hand-written usability check to parsed output; it does **not** execute a complete JSON Schema validator. Unusable output is a semantic-analysis failure. Usable hypothesis coverage can recover it; otherwise the runner throws unless `GILF_PUBLISH_ON_CODEX_FAILURE=1` explicitly permits degraded fallback.
Source of truth: `REVIEW_SCHEMA` in `son-of-anton-review/src/codex-review-runner.js:170-281` (`R/` below), branch `feat/cloudflare-native`. The separate `son-of-anton-operator-parity` branch `feat/greptile-operator-parity` owns the parity Worker API. Neither branch's endpoints imply the other has been deployed.
## Top-level fields [#top-level-fields]
The requested schema requires all fourteen fields. Empty arrays are valid; `sequenceDiagram` can be empty. The runtime additionally requires nonblank `overview`, `summary` and `mergeVerdict`. Schema requirements and runtime checks differ as described below.
| Field | Type | Notes |
| -------------------------- | ---------------------------- | ------------------------------------------------------------- |
| `overview` | string | Non-empty. One paragraph on what the PR does. |
| `summary` | string | Non-empty. |
| `verdict` | `clear` \| `needs-attention` | Overall review verdict. |
| `confidenceScore` | integer 1-5 | Analyst confidence in the conclusions. Not merge readiness. |
| `bulletHighlights` | string\[] | Rendered as the summary bullet list. |
| `reasoning` | string | Why the confidence score is what it is. |
| `mergeVerdict` | string | Non-empty. The only place merge readiness is stated in prose. |
| `importantFiles` | `{ path, overview }[]` | Rendered as a table. |
| `findings` | finding\[] | See below. |
| `priorFindingDispositions` | disposition\[] | See below. `[]` when none apply. |
| `missingValidations` | string\[] | What was not run or could not be inspected. |
| `crossRepoImpact` | string\[] | Downstream repos or consumers affected. |
| `sequenceDiagram` | string | ASCII flow. Empty string allowed. |
| `competitorComparison` | object | See below. |
## `findings[]` [#findings]
```json
{
"severity": "blocker | high | medium | low",
"category": "string",
"path": "string",
"title": "string",
"body": "string",
"contextIds": ["string"]
}
```
The requested schema requires all six keys. There is **no line, column, end-line, per-finding confidence, suggestion or patch field**. `additionalProperties: false` forbids adding those to a schema-conforming model finding. `contextIds` must refer only to supplied memory context ids; use `[]` when none apply (`R/src/codex-review-runner.js:210-224,1852`). Downstream provenance checks remove unsupported citations; an arbitrary file path is not a memory id.
Blocker and high severities drive the merge status. See [Finding Evolution](../code-review/finding-evolution) for how findings are fingerprinted across heads.
## `priorFindingDispositions[]` [#priorfindingdispositions]
Used when the same PR head was reviewed before. A missing finding on an unchanged head is not a fix, so the model must either re-report the concern or close it with evidence.
```json
{
"priorReviewKey": "string",
"fingerprint": "string",
"disposition": "resolved | false_positive | no_longer_applicable",
"explanation": "string",
"evidence": [{ "headSha": "string", "path": "string", "quote": "string" }]
}
```
`priorReviewKey` identifies the source review, `fingerprint` identifies its finding, and evidence quotes current code at the current `headSha`. The runner verifies disposition sources and the service reconciles same-head history. Unsupported closure claims are not accepted as resolutions; unresolved concerns add missing-validation limitations and cap confidence at 3 (`R/src/codex-review-runner.js:2723-2768`; `R/src/finding-evolution.js:320-375`; `R/src/review-service.js:397-413`).
## `competitorComparison` [#competitorcomparison]
Present on every review. Ten keys, all required:
| Field | Type |
| -------------------------- | -------------------------------------------- |
| `available` | boolean |
| `winner` | `gilf` \| `greptile` \| `split` \| `unclear` |
| `summary` | string |
| `confidence` | integer 1-5 |
| `agreements` | string\[] |
| `greptileMisses` | string\[] |
| `gilfMisses` | string\[] |
| `strongerGreptileFindings` | string\[] |
| `strongerGilfFindings` | string\[] |
| `actionItems` | string\[] |
When nothing was recorded, `normalizeReview` substitutes this default (`src/review-format.js:83-94`):
```json
{
"available": false,
"winner": "unclear",
"summary": "No competitor comparison was recorded for this review run.",
"confidence": 1,
"agreements": [], "greptileMisses": [], "gilfMisses": [],
"strongerGreptileFindings": [], "strongerGilfFindings": [], "actionItems": []
}
```
The review body only renders a "Competitor Benchmark" section when `available` is `true`.
## Validation [#validation]
`isUsableCodexReviewOutput` (`R/src/codex-review-runner.js:1912-1939`) checks:
1. Payload is a non-null object, not an array.
2. `overview`, `summary`, `verdict`, `reasoning`, `mergeVerdict`, `sequenceDiagram` are strings; `overview`, `summary`, `mergeVerdict` must be nonblank.
3. `verdict` is `clear` or `needs-attention`; `confidenceScore` is an integer 1 to 5.
4. `bulletHighlights`, `importantFiles`, `findings`, `missingValidations`, `crossRepoImpact` are arrays.
5. `competitorComparison` is an object with the required boolean/string/enum/confidence values and six array fields.
It does **not** enforce nested finding/item shapes, `additionalProperties`, array element types or the presence of `priorFindingDispositions`. Emit the complete requested schema anyway; a payload passing this shallow gate is not necessarily a safe or useful review.
File and JSON-event readers return `null` for unreadable/unusable output (`R/src/codex-review-runner.js:1941-1981`). HTTP providers throw if no usable review can be extracted (`:3037-3038`). A failed Codex CLI can return usable partial output, but the runner records missing validation and degraded semantic coverage (`:3169-3175`). Failure recovery is conditional, not an automatic deterministic success (`:2692-2707`).
### Confidence is not merge readiness [#confidence-is-not-merge-readiness]
The prompt states it verbatim (`src/codex-review-runner.js:1844`):
> Treat confidenceScore strictly as analyst confidence in the review conclusions, not as merge readiness. Put merge readiness only in mergeVerdict and in the severity of findings.
Merge status is computed downstream from finding severity, `mergeVerdict`, validation state and analyzer health (`normalizeReview`, `src/review-format.js:20-101`). The confidence score only gates the check run when `statusChecks.requiredConfidence` is set in the review policy. See [Dashboard Settings](../configuration/dashboard-settings).
## Mapping to the posted review [#mapping-to-the-posted-review]
Rendering lives in `buildReviewMarkdown` and `buildCheckSummary` (`R/src/review-format.js:127-247`). The check name defaults to `Son of Anton / review`, subject to `GILF_PRODUCT_NAME`. `publishCheckRun` patches when a check id is supplied and creates otherwise. `publishReview` posts a new review body; it does not update an existing review in place (`R/src/github-publisher.js:22-66`).
| Schema field | Review body section | Check run field |
| -------------------------------------- | -------------------------------------------- | --------------------------------------------------- |
| `overview`, `bulletHighlights` | Summary | |
| derived `mergeStatus` + `mergeVerdict` | Merge Status: CLEAR / CAUTION / BLOCK | `output.summary` prefix `[CLEAR]` etc, `conclusion` |
| `confidenceScore`, `reasoning` | Analyst Confidence: n/5 | `output.summary` suffix "Analyst confidence n/5." |
| `importantFiles` | Important Files Changed (table) | |
| `findings` (inside changed paths) | Findings | `output.text`, one line per finding |
| `findings` (outside changed paths) | Outside-Diff Findings | |
| `missingValidations` | Validation Status, prefixed `MISSING:` | affects `conclusion` |
| `crossRepoImpact` | Cross-Repo Impact | |
| `competitorComparison` | Competitor Benchmark (only when `available`) | |
| `sequenceDiagram` | ASCII Flow (fenced text block) | |
| `findings` titles + paths | Prompt to Fix | |
`reviewCheckConclusion` returns `failure` for missing/below-threshold confidence when the threshold is positive. Non-clear merge status returns `neutral`. With a positive threshold, missing/incomplete/unpinned semantic coverage also returns `neutral`, not success. Otherwise it returns `success` (`R/src/review-format.js:138-149`). Section visibility follows pinned policy, but safety and validation remain visible. See [Publish modes](../code-review/publish-modes).
## Evidence artifact refs [#evidence-artifact-refs]
With `GILF_EVIDENCE_ARTIFACTS=1`, the runner attempts to persist validation/worker output and attach matching references (`R/src/codex-review-runner.js:3517-3569`; `R/src/evidence-artifacts.js:492-517`). This is a post-model extension, not a key requested from the model:
```json
"evidenceArtifacts": [{ "id": "…", "key": "…", "url": "…", "kind": "…" }]
```
The body renders `Evidence:` with artifact kind and URL, or storage key when no URL exists. `GILF_EVIDENCE_REQUIRE_ARTIFACTS=1` marks execution claims without artifacts using `evidenceStatus: "unverified"` and `downgradedReason`; it does not lower severity or remove them (`R/src/evidence-artifacts.js:520-558`; `R/src/review-format.js:164-181`).
The require flag has no effect if artifact processing is disabled. If artifact processing itself throws, the runner records `evidence_artifacts_failed` and returns the original findings. Neither evidence flag is forwarded into native Cloudflare containers, so Worker env changes alone cannot activate this behavior (`R/cloudflare-native/src/container-env.js:15-86`). See [Validation and Evidence](../code-review/validation-and-evidence).
## Storage and exposure [#storage-and-exposure]
* Accepted review data is stored as `review`, with `findings`, `verdict`, `summary` and competitor comparison also on the run (`R/src/review-service.js:419-426`). SQLite's `review_runs.record_json` stores the run record.
* The Node dashboard's `GET /operator/api/pr?repo=owner/name&number=1` returns summarized runs, flattened findings and events, not full raw run objects (`R/src/operator-dashboard.js:405-427`). Its normalized display shape may include a nullable `line`; that does not add line information to the model schema.
* The expanded parity Worker API is separate from this Node handler and native public ingress. See [Operator API](../operator/operator-api).
## Full example [#full-example]
The following is an illustrative payload, not a recorded review or a claim about this repository:
```json
{
"overview": "Adds retry with jitter to the webhook consumer and widens the dead-letter path.",
"summary": "Retry logic is correct. Dead-letter widening drops the original error.",
"verdict": "needs-attention",
"confidenceScore": 4,
"bulletHighlights": [
"Retry backoff capped at 5 attempts",
"Dead-letter payload no longer carries the original error"
],
"reasoning": "Diff and surrounding consumer code were fully available. Tests were not run.",
"mergeVerdict": "Safe to merge with 1 high-risk follow-up.",
"importantFiles": [
{ "path": "src/consumer.js", "overview": "Retry loop and dead-letter handoff." }
],
"findings": [
{
"severity": "high",
"category": "correctness",
"path": "src/consumer.js",
"title": "Dead-letter message discards original error",
"body": "toDeadLetter(msg) builds a new envelope without msg.error, so operators lose the failure cause.",
"contextIds": []
}
],
"priorFindingDispositions": [],
"missingValidations": ["Test suite not executed for this head."],
"crossRepoImpact": [],
"sequenceDiagram": "webhook -> consumer -> retry(5) -> dead-letter",
"competitorComparison": {
"available": false,
"winner": "unclear",
"summary": "No competitor comparison was recorded for this review run.",
"confidence": 1,
"agreements": [],
"greptileMisses": [],
"gilfMisses": [],
"strongerGreptileFindings": [],
"strongerGilfFindings": [],
"actionItems": []
}
}
```
No line location or artifact is invented in this example. A finding path alone is insufficient to post a GitHub inline review comment.
# Roadmap and Feature Flags (/docs/reference/roadmap-and-flags)
A built feature is not an enabled feature. With untouched runner flags, **the hypothesis swarm does not run**, even in shadow: no worker is constructed and the method returns `null`. Native publication has a separate shadow/live gate. Do not conflate that gate with shadow analysis.
`R/` means `son-of-anton-review` on `feat/cloudflare-native`. `P/` means `son-of-anton-operator-parity` on `feat/greptile-operator-parity`. These branches are unmerged. This page records implementation state, not a release schedule or a claim about the currently deployed environment.
## Flag table [#flag-table]
| Setting | Code default | Actual behavior |
| --------------------------------------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GILF_HYPOTHESIS_PRIMARY` | Off; exact `1` enables | Usable hypothesis output can replace monolithic semantic review. Also implicitly enables a Prime worker unless `GILF_PRIME_SHADOW=0`. |
| `GILF_PRIME_SHADOW` | Off; exact `1`, or implied by primary flag | Supplies a Prime hypothesis worker. Its output can become primary when primary is enabled; usable hypotheses can also recover a failed monolithic review. It is not an unconditional never-published guarantee. |
| `GILF_HYPOTHESIS_CONCURRENCY` | `4` in runner | Bounds worker execution only when a worker exists. |
| `GILF_HYPOTHESIS_PLANNER` | Off; exact `1` enables | Calls an HTTP planner only when a hypothesis worker exists and semantic review is not fast-path skipped. Codex planner selection skips. |
| `GILF_PLANNER_PROVIDER`, `GILF_PLANNER_MODEL` | Actual runner primary provider/model | Separate planner selection; OpenRouter recommended, direct HTTP alternatives supported. |
| `GILF_EVIDENCE_ARTIFACTS` | Off; exact `1` enables | Builds/persists artifacts and attempts matching to findings. Requires a storage client to persist. |
| `GILF_EVIDENCE_REQUIRE_ARTIFACTS` | Off; accepts `1`, `true`, `yes`, `on` | Inside enabled artifact processing, marks unsupported execution claims unverified. Does not lower severity, and module errors return original findings. |
| `GILF_MODEL_INVERSION` | Off; accepts `1`, `true`, `yes`, `on` | Heuristic author-family detection chooses another configured model family. Low confidence, missing target/key or free-model rejection leaves the configured primary. An inverted provider failure retries the primary once. |
| `GILF_VALIDATION_SHADOW_EXECUTOR` | Unset | Supported, different and wired executor runs for telemetry; output does not merge into primary findings. |
| `GILF_CODEX_SHADOW_MODEL` | Unset | Actual runner shadow-model selector. Runs only after successful primary semantic review, not every review. |
| `GILF_MODEL_SHADOW` | Unset | Read by the generic library resolver, not wired into the Node/native runner entrypoints. Not a working replacement for the legacy shadow selector. |
| `GILF_PUBLISH_MODE` | Native code: `shadow` | Native `live` requires authenticated publication. Checked-in native manifest explicitly sets `live`. Node does not consult this gate. |
Sources: `R/src/codex-review-runner.js:2224-2226,2300-2337,2351-2366,2650-2707,3198-3267,3342-3353,3517-3569`; `R/src/author-model.js:15-16,198-231`; `R/src/validation-executor.js:251-282,365-430`; `R/src/model-provider.js:348-358`; `R/cloudflare-native/src/publish-mode.js:26-69`; `R/worker.mjs:35-54`.
## Native Cloudflare is blocked at the environment boundary [#native-cloudflare-is-blocked-at-the-environment-boundary]
`R/cloudflare-native/src/container-env.js:15-86` forwards a deliberate allowlist. None of these families are included:
* `GILF_HYPOTHESIS_*` and `GILF_PLANNER_*`.
* `GILF_PRIME_*`.
* `GILF_MODEL_INVERSION` and its map/confidence controls.
* `GILF_EVIDENCE_ARTIFACTS` / `GILF_EVIDENCE_REQUIRE_ARTIFACTS`.
* Role-specific `GILF_MODEL_PRIMARY` / `GILF_MODEL_SHADOW` and legacy shadow-model variables.
Adding Worker vars or secrets with those names does not enable container behavior. A reviewed integration change must forward the intended controls, provide the required worker/provider/storage dependencies and verify the effective runtime. This page does not prescribe a production flag flip.
`GILF_DISABLE_HYPOTHESIS_WORKERS` is a harness-manifest descriptor control, not a runtime kill switch (`R/src/harness-evolution.js:56-59`). To stop a Node Prime-backed swarm, remove its actual enabling flags and account for the primary flag's implicit enablement.
The forwarding allowlist **does** carry current provider selection, `GILF_MODEL`, cost overrides, bare model keys, publication mode, paid-OpenRouter opt-in, E2B key/shape/timeout, validation shadow selection and validation rates. See [Environment reference](../configuration/environment-reference).
## Policy defaults are not environment flags [#policy-defaults-are-not-environment-flags]
| Policy capability | Default | Safety boundary |
| -------------------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Auto approval | `autoApprove.enabled: false` | Needs confidence 5, enabled confidence section, complete same-head coverage, no missing validation, allowed risk, matching filters and no substantive/protected findings. |
| PR description update | `updatePrDescription: false` | Reads latest PR, checks pinned head and changes only the owned summary section. |
| Prompt to Fix | `promptToFix: false` | Copyable text, not an applyable patch or verified repair. |
| Memory creation permission | `memoryRuleCreation: ADMINS_ONLY` | Stored/validated enum, not a runtime write-authorization gate in these trees. |
Sources: `R/src/operator-review-policy.js:25-45,227-238`; `R/src/github-publisher.js:82-103`; `R/src/review-format.js:237-242,249-266`. Policy editing endpoints are on `P/cloudflare/`; see [Dashboard settings](../configuration/dashboard-settings).
## Integration work, not promised releases [#integration-work-not-promised-releases]
The inspected source establishes these concrete remaining boundaries:
1. **Unify API and engine deployment deliberately.** `P/cloudflare/src/worker.js:142-157` dispatches settings, reports, SCM and Test Lab; `R/cloudflare-native/src/main.js:26-29` sends public fetch to ingress only. A dashboard proxy target change cannot merge those implementations.
2. **Wire and verify optional analysis dependencies.** An enabled planner with no hypothesis worker emits `no_hypothesis_worker`; a Codex planner emits `codex_provider_unsupported`. Forwarding flags alone cannot supply a worker or working provider (`R/src/codex-review-runner.js:3342-3353`).
3. **Establish artifact persistence before relying on evidence enforcement.** With no storage, processing records `no_artifact_storage`; generated artifacts are not automatically uploaded or linked (`R/src/codex-review-runner.js:3531-3546`).
4. **Add actual line-addressed findings before offering inline workflows.** Model output has no line fields and publisher review bodies have no inline comments (`R/src/codex-review-runner.js:210-224`; `R/src/github-publisher.js:50-66`).
5. **Establish representative evaluation evidence.** The benchmark CLI scores supplied fixtures, not live historical PR replay (`R/evals/v5-benchmark-cli.mjs:10-24,106-131`).
6. **Resolve distribution status separately.** Public source availability and license rights are not established. Local package metadata is private and still names the old repository (`R/package.json:1-25`).
These are source-backed integration needs, not dates, guarantees or an instruction to alter production. The older internal plan is not used here as evidence that an item is still missing: finding evolution and outcome priors already have implementations.
## Test Lab definitions [#test-lab-definitions]
The parity API defines the following experiment records:
| Experiment id | Definition status | What exists |
| --------------------- | ----------------- | ------------------------------------------------------------------- |
| `validation-executor` | `running` | CF Sandbox primary and E2B shadow variants; reads validation spans. |
| `model-comparison` | `planned` | No variants or span prefix. |
| `hypothesis-shadow` | `planned` | No variants or span prefix. |
| `hypothesis-planner` | `planned` | No variants or span prefix. |
A static `running` label does not prove observed samples or quality. Evidence: `P/cloudflare/src/test-lab-api.js:12-61,90-118`. Native manifest selects E2B shadow with a `600000` ms timeout and `50` USD ledger ceiling. The budget gate logs `e2b_budget_exhausted` and withholds the exact `managed-e2b` selector at the ceiling; a ledger read failure leaves it enabled (`R/cloudflare-native/wrangler.jsonc:143-150`; `R/cloudflare-native/src/container-env.js:100-141`).
## Benchmark CLI [#benchmark-cli]
From the review repository, this scores existing fixture outputs and writes a scorecard; it does not run the model on a local diff:
```bash
node evals/v5-benchmark-cli.mjs --fixture evals/benchmark/fixtures/v5-smoke.json --run-id smoke
```
The parser supports `--fixture`, `--out-dir`, `--run-id`, `--persist`, `--repo`, `--commit-sha` and `--help`. Default output root is `generated/benchmark-harness`; persistence is an explicit remote-write option requiring a configured context-graph store. Do not use fixture scorecards as public claims of review quality.
Source: `R/evals/v5-benchmark-cli.mjs:10-61,69-74,106-131`. This command is documented from source, not executed as part of this documentation update.
## Operational checks are runtime-specific [#operational-checks-are-runtime-specific]
Use the native ingress `/health` only as an ingress liveness probe. It does not expose the parity Worker's `/operator/api/*` or legacy `/queue/stats` routes. Node's server and worker also have different lifecycle and publication behavior. See [Troubleshooting](../reference/troubleshooting) before treating a green health response, configured-provider badge or unset feature flag as proof of an end-to-end review.
# Troubleshooting (/docs/reference/troubleshooting)
Start with the failing runtime and phase, not a blanket retry. `R/` below means `son-of-anton-review` (`feat/cloudflare-native`); `P/` means `son-of-anton-operator-parity` (`feat/greptile-operator-parity`). These are separate, unmerged trees. Commands below are operator procedures supported by source; they were not executed as part of this documentation update.
## Collect the minimum evidence [#collect-the-minimum-evidence]
Keep the delivery id, repository, PR number, head SHA, review key, phase, analysis status, publish status and exact error. Redact credentials and sensitive code from anything shared. A successful HTTP response, configured-provider badge or empty findings array does not establish a completed review.
| Surface | What it establishes | What it does not establish |
| ---------------------------- | ---------------------------------------------------------------------- | -------------------------------------------------------- |
| Native `GET /health` | Ingress handler responds | Broker, queue, D1, model, sandbox or publish health |
| Node `GET /health` | Node server responds with configured validation details | A worker is scheduled or shares the same queue |
| Provider `configured` badge | Key presence in the reporting process; Codex always reports configured | Key validity, CLI authentication or container forwarding |
| `analysis.status: succeeded` | Stored analysis completed | GitHub publication succeeded |
| Shadow validation span | A comparison lane was observed | Its result was used in the posted review |
Sources: `R/cloudflare-native/src/ingress.js:267-269`; `R/server.mjs:86-98`; `R/src/model-provider.js:363-385`; `R/src/review-service.js:419-443`; `R/src/validation-executor.js:365-377`.
### Native diagnostics [#native-diagnostics]
Set `ANTON_NATIVE` to your native ingress origin. It is an example shell variable, not product configuration:
```bash
curl -sS -i "$ANTON_NATIVE/health"
```
From the review repository, inspect the configured native Worker's logs and recent stored run states:
```bash
wrangler tail --config cloudflare-native/wrangler.jsonc
```
```bash
wrangler d1 execute son-of-anton-review-state \
--config cloudflare-native/wrangler.jsonc --remote \
--command 'SELECT review_key,status FROM review_runs ORDER BY rowid DESC LIMIT 5'
```
Use your deployment's actual D1 database name/configuration if different. The checked-in binding names that database at `R/cloudflare-native/wrangler.jsonc:73-81`; the diagnostic commands appear in `R/cloudflare-native/DEPLOY.md:529-531`. Treat logs as sensitive. Do not print environment values while debugging.
### Node diagnostics [#node-diagnostics]
Against the server's actual port, default `8787`:
```bash
curl -sS -i http://127.0.0.1:8787/health
```
The operator CLI can inspect the existing SQLite run store:
```bash
node bin/gilf-review.mjs runs --db "$GILF_DB_PATH" --limit 20 --json
node bin/gilf-review.mjs show "$REVIEW_KEY" --db "$GILF_DB_PATH" --json
node bin/gilf-review.mjs usage --db "$GILF_DB_PATH" --limit 100 --json
```
`REVIEW_KEY` is the actual stored key; `GILF_DB_PATH` must identify the existing database. A wrong path may create/open a different SQLite store. The CLI opens a normal application store, not a read-only forensic connection; use a preserved copy if you need strictly non-mutating inspection. Do not use `harness` as a read-only probe: it can save a harness version.
Sources: `R/src/operator-cli.js:70-89,553-644`; `R/bin/gilf-review.mjs:5-9`. Successful CLI dispatch returns exit code 0; thrown errors print to stderr and exit 1. A command's success exit code is not the reviewed PR's verdict.
## Webhook rejected or accepted but not queued [#webhook-rejected-or-accepted-but-not-queued]
Both Node and native expect `POST /github/webhooks`, with `x-github-delivery`, `x-github-event` and `x-hub-signature-256`. Do not test it with unsigned empty JSON and interpret the rejection as downtime.
| Response / log | Meaning | Recovery |
| ------------------------------------------------------ | ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `400 missing_headers` | A required GitHub header is absent | Correct the forwarding/delivery request; preserve GitHub's headers. |
| `401 invalid_signature` | Signature does not match the raw body and configured secret | Have the credential owner align webhook configuration; ensure intermediaries do not rewrite the body. Never bypass signature validation. |
| `400 invalid_json` | Signature passed, body was not valid JSON | Inspect delivery encoding and forwarding. |
| Native `duplicate_delivery` | KV marker already exists | Inspect the original run rather than replaying the same id as a new review. |
| Native `accepted: true, ignored: true` | Unsupported event/action or non-PR comment | Check event/action and intent reason. Acceptance does not mean a review was queued. |
| `delivery_record_failed`, log `delivery-record-failed` | Failed to persist a non-actionable delivery | Repair D1 binding/schema/access, then redeliver. |
| `enqueue_failed`, log `enqueue-failed` | D1 delivery persistence, R2 offload or Queue send failed | Read the associated error; repair that dependency before redelivery. |
Native ingress attempts to delete the KV marker after record/enqueue failure so a retry can proceed. Oversized payloads over 96 KiB are offloaded when `WEBHOOK_PAYLOADS` exists; without it, a payload exceeding the Queue message limit can keep failing. A stored `queued` delivery record alone does not prove the send succeeded.
Sources: `R/src/http-handler.js:19-61`; `R/cloudflare-native/src/ingress.js:63-132,139-170,227-259,275-346`.
## Review skipped by admission policy [#review-skipped-by-admission-policy]
| Reason | Check |
| ------------------------------------------------ | ------------------------------------------------------------------------ |
| `repository_disabled` | Native stored config, previous-review exception and `autoEnableNewRepos` |
| `auto_review_disabled` | Node repo enablement, exact base branch, auto and draft settings |
| `manual_disabled` / `unauthorized_commenter` | Manual opt-in and verified GitHub author association/login |
| `unknown_pr_head` | Node has no stored head for the requested PR |
| `author_paused` | Workspace paused-author list; manual review does not bypass it |
| `new_commits_disabled` / `draft_review_disabled` | Automatic commit/draft policy |
| `file_change_limit` / `filters_not_matched` | File count and filter conditions |
| `model_budget_usage_unavailable` | Budget enabled but current-month complete usage unavailable |
| `monthly_model_budget_reached` | Known current-month cost is at/over the configured cap |
Do not solve these with a forced publish. Inspect the saved policy and pinned metadata. Manual commands bypass only the automatic checks inside `evaluateReviewPolicy`, not repository disablement, command authorization or budget admission. Metadata-required reasons can defer rather than deny admission.
`GILF_REPOS` only seeds Node configuration; omission is not denial. Native does not enforce Node's `branches.include`/`review.auto` predicate in the same way. Use the runtime-specific controls in [Repo scoping](../configuration/repo-allowlist-and-scoping).
Sources: `R/src/config.js:86-97`; `R/src/review-service.js:716-717,768-834`; `R/cloudflare-native/src/d1-store-adapter.js:153-172`; `R/src/operator-review-policy.js:154-176`; `R/src/review-policy-runtime.js:39-53`.
## Queue or container appears stuck [#queue-or-container-appears-stuck]
### Native dispatch [#native-dispatch]
* `skipped_completed`: an already successful dispatch lease was reconciled and acknowledged.
* `skipped_locked`: another owner holds a noncompleted lease; the delivery is retried, not silently discarded.
* `skipped_obsolete`: run is `superseded` or `abandoned`.
* `ignored` with `no_review_command`: native issue-comment dispatch recognizes review/rerun, not status/help as review work.
* `container-dispatch-failed`: inspect the logged HTTP status and response body. The follow-on `container-dispatch-error` is not the root cause by itself.
* `lease-release-failed`: the source explicitly leaves the lease running until expiry and reclaim. Do not delete a possibly active lease to hurry recovery.
* `dead_lettered` with `unroutable` or `max_attempts`: repair the missing identity or underlying dispatch error before scheduling fresh work. `MAX_ATTEMPTS` defaults to 8.
The consumer retains the actual container error body rather than reporting only a generic 500. A missing head has a specific backstop: `review run ... has no headSha; the consumer must resolve the PR head before dispatch`. Inspect PR-head resolution and broker access; do not invent a head SHA.
Sources: `R/cloudflare-native/src/consumer.js:310-465`; `R/cloudflare-native/container/entrypoint.mjs:82-94`.
### Node worker [#node-worker]
The server and worker must use the same durable queue or SQLite path. Without shared paths, each has an independent in-memory queue. The worker drains a bounded batch then exits; default `GILF_WORKER_MAX_JOBS=1` and `GILF_WORKER_CONCURRENCY=1` do not create a daemon.
`lease_renew_failed` and `lease_lost_aborted` indicate ownership trouble. The worker stops rather than risk duplicate publication. Set explicit model/validation timeouts and a sufficiently long lease: the worker's lease formula assumes a 10-minute model timeout while the runner defaults to 45 minutes.
Sources: `R/server.mjs:27-50`; `R/worker.mjs:64-66,89-128`; `R/src/worker-runner.js:14-39,42-72`; `R/src/codex-review-runner.js:2335-2337`.
## Model missing, rejected, timed out or malformed [#model-missing-rejected-timed-out-or-malformed]
### Wrong provider despite dashboard selection [#wrong-provider-despite-dashboard-selection]
Node `worker.mjs` does not use the generic resolver. Set `GILF_CODEX_PROVIDER` / `GILF_CODEX_MODEL` for that runner, and the current naming pair if you also want the dashboard status to agree. Native uses current provider/model names, but only forwarded keys reach its container. `GILF_MODEL_SHADOW` alone does not activate either runner's shadow-model call.
Sources: `R/worker.mjs:43-54`; `R/src/codex-review-runner.js:2320-2337`; `R/cloudflare-native/container/entrypoint.mjs:314-348`; `R/cloudflare-native/src/container-env.js:15-86`.
### Missing key [#missing-key]
The container can throw `review provider "..." selected but no model API key in env (set OPENROUTER_API_KEY)`. Its guard checks for any supported key, not the selected provider's exact key; a later provider-specific missing-key error is still possible. Supply the **matching** bare secret to native Cloudflare. Prefixed aliases work in the provider abstraction but are not forwarded by native projection.
Source: `R/cloudflare-native/container/entrypoint.mjs:319-324`; `R/src/codex-review-runner.js:2952-2960`.
### OpenRouter catalog guard [#openrouter-catalog-guard]
Source-defined failures include:
* `OpenRouter model is required`.
* `OpenRouter model list failed (...)`, including response status.
* `OpenRouter model list returned invalid JSON`.
* `OpenRouter model ... was not found`.
* `OpenRouter model ... is not free (prompt=..., completion=...)`.
Check exact model id, configured base URL and current catalog pricing. Keep free-only mode when that is your spending policy; choose a valid zero-priced model. If paid inference is intended, explicitly set `GILF_OPENROUTER_REQUIRE_FREE=0`. Do not set fake zero prices in `GILF_MODEL_PRICES`: it changes accounting, not this guard.
Sources: `R/src/codex-review-runner.js:2022-2057,2982-2997`. OpenRouter is recommended; OpenAI/Anthropic HTTP or Codex CLI are alternatives, not automatic outage failovers.
### Timeout or unusable review [#timeout-or-unusable-review]
HTTP semantic calls can report `... chat completion timed out after ...ms` or `... succeeded but did not return a usable ... review JSON payload`. Check the actual timeout and provider response shape. `isUsableCodexReviewOutput` is a shallow gate, not full schema validation. A successful provider HTTP call alone does not mean a usable review was produced.
Do not enable `GILF_PUBLISH_ON_CODEX_FAILURE=1` merely to turn this into apparent success. It permits a degraded fallback only when hypothesis recovery is unavailable, and is not forwarded natively. Partial Codex CLI output can be retained with degraded coverage and a missing-validation warning.
Sources: `R/src/codex-review-runner.js:1912-1939,2692-2707,3037-3039,3074-3077,3169-3175`. See [Review output schema](../reference/review-output-schema).
## Validation unavailable or skipped [#validation-unavailable-or-skipped]
| Evidence | Recovery |
| -------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `SKIPPED E2B validation: E2B_API_KEY is unavailable.` | Provision the E2B key in the actual executing process. |
| `SKIPPED E2B validation: e2b SDK is unavailable.` | Restore the package's declared E2B dependency in the worker image/environment. |
| `SKIPPED Crabbox validation: ... is unavailable.` | Make the configured Crabbox binary available to the worker. |
| `REFUSED Crabbox validation: repo-committed ... is untrusted executable config.` | Review and pin config before deliberate opt-in; do not blindly enable repo execution. |
| `has no Cloudflare Sandbox binding available (getSandbox collector not wired)` | Restore the native Sandbox binding/collector and outbound transport. Setting the readiness flag alone is insufficient. |
| `self_host_not_configured` | Supply exact opt-in `GILF_SELF_HOST_VALIDATION=1` and a real isolated-executor command. |
| `local_execution_not_allowed` | Choose an isolated executor; do not run untrusted PR scripts on the host to suppress this refusal. |
| `unknown_executor` | Use a supported selector from [Validation executors](../configuration/validation-executors). |
| `No test/typecheck/lint/build scripts were declared in package.json.` | No standard JS validation plan exists. Add real project checks if appropriate; do not claim non-JS validation ran. |
Sources: `R/src/codex-review-runner.js:941-955,1268-1284,1337-1346`; `R/src/validation-executor.js:524-579`.
A docs-only or generated-only validation skip can be intentional. `GILF_VALIDATION_CODE_CHANGES_ONLY=false` disables that gate; `0` does not. Policy `execution.mode=never` produces `NOT RUN repo-native validation: execution_disabled.` rather than a sandbox failure (`R/src/validation-config.js:11-14`; `R/src/review-policy-runtime.js:74-81`).
**Timeout distinction:** E2B collector fallback is 10 minutes, repo defaults can supply 45 minutes, and the native manifest explicitly selects 10 minutes. Set an explicit timeout appropriate to the plan. Shadow lane config is capped by `GILF_VALIDATION_SHADOW_TIMEOUT_MS`. A shadow failure is not evidence that primary validation failed (`R/src/config.js:18-28`; `R/src/codex-review-runner.js:1321`; `R/src/validation-executor.js:269-282`).
## V5 enabled in Worker vars but nothing happens [#v5-enabled-in-worker-vars-but-nothing-happens]
This is an implementation boundary, not a provider outage. `FORWARDED_KEYS` excludes swarm, planner, Prime, inversion and evidence flags. No environment-only recovery exists on native Cloudflare; the integration owner must change the projection and supply the dependencies.
On Node, `hypothesis_planner_skipped` can name `no_hypothesis_worker` or `codex_provider_unsupported`. With no enabling worker flags, swarm returns `null`. `GILF_DISABLE_HYPOTHESIS_WORKERS` only changes a harness descriptor, not execution.
If artifact processing runs but cannot persist, `evidence_artifacts_persisted` can report `no_artifact_storage`; a processing exception emits `evidence_artifacts_failed` and returns original findings. `GILF_EVIDENCE_REQUIRE_ARTIFACTS` does not create storage or act when artifact processing is off.
Sources: `R/cloudflare-native/src/container-env.js:15-86`; `R/src/codex-review-runner.js:3248-3267,3342-3353,3517-3569`; `R/src/harness-evolution.js:56-59`.
## E2B shadow suddenly stops [#e2b-shadow-suddenly-stops]
Native dispatch sums `validation.e2b` cost rows and logs `e2b_budget_exhausted` at `GILF_E2B_BUDGET_USD`, default 50. It then withholds the exact `managed-e2b` shadow selector. Primary review and publication remain unchanged. `e2b_budget_read_failed` means the read failed and the selector was left enabled, not safely suspended.
Review the recorded costs and intended budget before increasing the ceiling. Do not delete cost rows or substitute zero prices to resume spend. This is a cumulative ledger gate, not a documented monthly reset or primary-E2B cap.
Source: `R/cloudflare-native/src/container-env.js:100-141`.
## Analysis succeeded, publication missing or unsafe to approve [#analysis-succeeded-publication-missing-or-unsafe-to-approve]
1. Inspect actual native log `omp-container: publish mode`. Code default is shadow; checked-in manifest is live. Node ignores this env gate and selects live publication when App credentials are provided.
2. Check `publish.status`, errors and receipts separately from `analysis.status`.
3. Live native startup requires broker transport. `Container publish mode does not match its dispatch lease` is a consistency failure, not a reason to bypass the lease.
4. Approval requires pinned head, confidence 5, complete coverage, no missing validation, allowed risk and matching filters. A high confidence score alone is not merge readiness.
5. A positive confidence threshold with incomplete coverage yields a neutral check, not a success. Missing/below-threshold confidence yields failure.
Sources: `R/cloudflare-native/container/entrypoint.mjs:273-302`; `R/worker.mjs:35-41`; `R/src/operator-review-policy.js:227-238`; `R/src/review-format.js:138-149`; `R/src/github-publisher.js:50-65`.
The native command bridge supports `retry-publish` only for an existing run with `analysis.status === 'succeeded'` and an installation id. Its actual errors include `Review run is not analyzed: ...` and `Cannot retry publish for ...: missing installation id`. It queues recovery work; it does not directly certify publication. Use [Operator API](../operator/operator-api) only on the branch/deployment that owns that command store. The native public ingress does not expose parity command endpoints.
Sources: `R/cloudflare-native/src/cron-command-bridge.js:225-247`; `R/cloudflare-native/src/main.js:26-29`.
## Dashboard 401, 403, 404 or settings conflict [#dashboard-401-403-404-or-settings-conflict]
There are three authentication surfaces, not one universal token:
* **Node operator handler:** `GILF_OPERATOR_UI_ENABLED=1` enables routing, but a missing or wrong `GILF_OPERATOR_UI_TOKEN` returns `401 operator_auth_required`. It is fail-closed.
* **Original `anton-ui` proxy:** its separate `OPERATOR_REQUIRE_AUTH` gate is off by default; that does not remove downstream backend auth requirements.
* **`anton-ui-trace-parity`:** `/api/*` authenticates a session before forwarding. It uses backend read configuration and `SON_OF_ANTON_ADMIN_TOKEN` for privileged operations. Setting the original UI's bearer gate is not equivalent to configuring this session system.
Sources: `R/src/operator-dashboard.js:601-620`; `anton-ui/worker/proxy-utils.js:46-64`; `anton-ui-trace-parity/worker/index.js:20-47`.
| Parity UI response | Meaning |
| ---------------------------------- | ------------------------------------------------------------- |
| `503 backend_not_configured` | Missing backend base URL |
| `503 admin_backend_not_configured` | Missing administrative backend token for privileged operation |
| `502 backend_unreachable` | Backend fetch threw |
| `403 cross_origin_mutation` | Request Origin did not match UI origin |
| `415 json_required` | Mutation is not JSON |
| `404 not_found` | No matching proxy route, or downstream branch lacks endpoint |
Sources: `anton-ui-trace-parity/worker/index.js:34-75`; `anton-ui-trace-parity/worker/proxy-utils.js:95-100`.
For direct **parity Worker** policy reads, with the origin and existing token securely configured:
```bash
curl -sS -i "$ANTON_OPERATOR/operator/api/settings" \
-H "authorization: Bearer $SON_OF_ANTON_OPERATOR_READ_TOKEN"
```
Settings PATCH uses the administrative token, a JSON body with the current `revision` and a `settings` patch. Generated `anton_` keys do not grant settings-write access. `409 Review policy revision conflict` means reload and compare before reapplying; do not overwrite a colleague's saved changes. The parity editor preserves edits and offers comparison/reapply/discard controls.
`503 Review policy schema is not initialized` means the workspace settings row is absent. `500 Review policy storage is unavailable` is the handler's generic storage failure. Confirm the correct D1 binding and reviewed schema initialization rather than treating either as a policy validation error. `413 Request body is too large` is the 256 KiB body limit. Usage can also return `413` for more than 50000 source rows; use a narrower date range.
Sources: `P/cloudflare/src/worker.js:65-80,115-146`; `P/cloudflare/src/review-policy-api.js:20-63`; `P/src/review-policy-store.js:40-42,69-89,143-146`; `anton-ui-trace-parity/src/views/Settings.tsx:235-245`.
## Empty traces or schema errors [#empty-traces-or-schema-errors]
First check whether tracing was disabled, whether Node has `GILF_DB_PATH`, and whether the UI targets the correct backend. Node trace-store initialization errors log `trace-store-init-failed`. A model with no known rate legitimately has null cost; that is not the same as missing traces.
Older native databases can have a narrow `review_trace_spans.kind` constraint and no `seq` column. Source records the failure `no such column: seq at offset 87: SQLITE_ERROR`. Re-running `CREATE TABLE IF NOT EXISTS` does not upgrade an existing table.
**Do not blindly apply `001-widen-trace-span-kind.sql`. It drops both trace tables.** Its written precondition requires both counts to be zero. Preserve/export existing data and use a reviewed copy-forward migration if either table contains rows. The relevant diagnostic query is read-only:
```bash
wrangler d1 execute son-of-anton-review-state \
--config cloudflare-native/wrangler.jsonc --remote \
--command 'SELECT (SELECT COUNT(*) FROM review_traces) AS traces, (SELECT COUNT(*) FROM review_trace_spans) AS spans'
```
If the tables themselves are absent, the query failing is also evidence; it is not authorization to destroy or recreate unrelated tables. See [Upgrading](../self-host/upgrading) for deployment-owned migration work.
Sources: `R/server.mjs:27-35`; `R/src/codex-review-runner.js:2297,2348-2350`; `R/cloudflare-native/schema.sql:159-177`; `R/cloudflare-native/migrations/001-widen-trace-span-kind.sql:1-32`.
## Replay or cron recovery does not complete [#replay-or-cron-recovery-does-not-complete]
The native command bridge operates on D1 `command_events`. `CRON_AUTHORITY=shadow` plans commands without claiming or executing them; `dryRun: true` and `planned` are not completion receipts. With live authority, commands transition queued to running to succeeded/failed. A transition conflict skips execution rather than racing another bridge.
Replay reads D1 `webhook_deliveries`, then an R2 delivery object fallback. An unknown-delivery error still contains stale wording saying ingress does not record deliveries; current ingress **does** record accepted deliveries. Investigate missing historical rows, the wrong database/bucket or unreplayable payload instead of assuming recording is unimplemented. Already processed/succeeded deliveries require explicit force; do not force blindly.
Sources: `R/cloudflare-native/src/cron-command-bridge.js:87-113,129-180,250-269,333-374`; `R/cloudflare-native/src/ingress.js:227-259,311-340`.
Watchdog messages such as `loop artifact generatedAt is missing or invalid`, `loop artifact is stale: ...`, or `...: no cron heartbeat recorded` identify missing/old evidence. Restore the loop artifact, schedule or dependency that failed; do not manufacture a current timestamp or hide historical dead letters. The native schedule watches loop, watchdog, command bridge and nightly audit; Prime-watch is deliberately unscheduled.
Sources: `R/cloudflare-native/src/cron-watchdog.js:49-94`; `R/cloudflare-native/src/cron.js:53-88`; `R/cloudflare-native/src/consumer.js:256-284`.
## Recovery completion criteria [#recovery-completion-criteria]
After an authorized recovery, inspect the new attempt's pinned head, actual phase result, missing validations and publication receipt. A requeue command only proves enqueue; a policy write only proves that revision was saved. Preserve the original failure evidence and record unresolved external prerequisites, including provider availability, deployment wiring and source/license access. Never turn a missing check into a claimed pass.
# Requirements (/docs/self-host/requirements)
These requirements describe the engine in `son-of-anton-review` on `feat/cloudflare-native`. The expanded operator API lives in `son-of-anton-operator-parity` on `feat/greptile-operator-parity`; WorkOS session support lives in `anton-ui-trace-parity` on `feat/trace-api-parity`, not the older `anton-ui` tree. Neither dashboard is required to run the engine.
## Source access and licensing [#source-access-and-licensing]
The verified handoff records the GitHub engine repository as private. No LICENSE is present in the checked-out engine; `package.json` is marked `private: true` and still names the legacy `gilf-pr-system` repository URL. Those fields do not grant permission to redistribute. Obtain an authorized checkout and permitted-use terms from the owner before installing. Do not assume an anonymous clone or a publishable fork.
The parity directories are linked Git worktrees. Check the actual branch and resolved path of an existing checkout rather than assuming the directory name means an integrated release. A native deploy does not deploy the parity API.
## Checklist [#checklist]
| Requirement | Node | Cloudflare-native |
| ----------------------------------------- | ------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| Authorized engine source and dependencies | Yes | Yes, on the deployment machine |
| Node runtime | Use Node 25, matching the shipped image | OMP Dockerfile pins `node:25-bookworm-slim` |
| Git and GitHub CLI | Required on the worker host | Installed in the OMP image |
| GitHub clone/fetch credentials | Provision for the service user separately from App publishing credentials | Broker-minted read token and `gh` credential helper are wired into the container |
| GitHub App ID, key and webhook secret | Yes | Yes, with the App key on the broker only |
| Model provider and authentication | Yes | Yes; OpenRouter is the headless default in the image |
| Validation executor | Explicit E2B, Crabbox or an isolated self-host command | Shipped config selects Cloudflare Sandbox |
| Durable state | Shared SQLite file(s), writable by both processes | D1, Queue, KV and R2 resources |
| Public HTTPS ingress | Proxy/tunnel to the Node server | Main Worker URL |
| Cloudflare deployment tooling | Only for optional remote services | Wrangler, account authorization and working container-image build tooling |
## Runtime and storage [#runtime-and-storage]
The entrypoints import `DatabaseSync` from `node:sqlite`; **Node 20 is not compatible**. The package has no `engines` check, so npm installation alone is not proof of runtime compatibility. Node 25 matches the checked-in image; this is a concrete runtime choice, not a claim that every other Node release was tested.
The Node runner uses `gh repo clone`, followed by Git fetch/checkout. A human must arrange restricted authentication for both tools under the worker's OS user. The Node App client authenticates publication, not those subprocesses.
Set an absolute `GILF_DB_PATH`, create its parent directory, and give both server and worker access. A separate `GILF_QUEUE_DB_PATH` is optional. With neither a persistent queue path nor a remote queue, each process has its own in-memory queue; they cannot exchange jobs. A remote queue does not replicate the SQLite run store.
Set `GILF_REVIEW_WORK_ROOT` to a service-owned workspace directory with enough space for the selected concurrency. Source defines no universal RAM, CPU, free-space or throughput minimum. Preserve durable policy, publication and review history in backups; it is not all reconstructible from GitHub.
## GitHub App and scope [#github-app-and-scope]
For review publication and source access, the implementation uses repository permissions `contents:read`, `pull_requests:write`, `checks:write` and metadata read access. The publisher narrows its token requests per operation and repository. The broker's default permission set is not a universal prohibition against other explicitly requested scopes.
Configure these event subscriptions if you want all event families handled by the engine:
* `pull_request`: PR lifecycle and head updates.
* `issue_comment`: manual commands on PR conversations.
* `pull_request_review` and `pull_request_review_comment`: additional review/competitor context events.
Not every delivery causes a review. Action filters, draft/branch settings, authorization and policy still apply. Node defaults automatic reviews to base branches `main` and `dev`, excluding drafts. Native ingress accepts a broader intent stream and the container applies native policy admission.
Install the App only on intended repositories. **`GILF_REPOS` is not a security allowlist:** Node uses it to seed stored config, while missing repo config still merges with enabled defaults. The native runtime does not read it. Native unseen repositories instead require explicit enablement under default `autoEnableNewRepos=false`; see [the native runbook](/docs/self-host/run-on-your-cloudflare-account#enable-a-repository).
Use `GITHUB_APP_PRIVATE_KEY_PATH` for an operator-protected Node key file, or inline `GITHUB_APP_PRIVATE_KEY`. Inline wins when both are present. Neither entrypoint automatically loads an env file; the quickstart uses Node's explicit `--env-file` option.
## Model [#model]
OpenRouter is recommended. Set `OPENROUTER_API_KEY` and an explicit provider/model pair: `GILF_CODEX_PROVIDER=openrouter` / `GILF_CODEX_MODEL` for the checked-in Node entrypoint, or `GILF_MODEL_PROVIDER=openrouter` / `GILF_MODEL` for native deployment. The Node runner does not call the modern `resolveModelConfig()` helper, so its per-provider defaults and role overrides do not describe Node execution. The free-only guard is on unless `GILF_OPENROUTER_REQUIRE_FREE=0`; paid usage needs deliberate approval. Source examples do not establish current model availability or price.
Alternatives are OpenAI direct, Anthropic direct and Codex CLI. Codex needs an installed, authenticated CLI on Node. The shipped native image points Codex at a failing placeholder, so switching its provider to `codex` is not sufficient. See [Model providers](/docs/configuration/model-providers).
## Validation compute [#validation-compute]
| Executor | Prerequisite and limitation |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `managed-crabbox` | Available `crabbox` binary, or `GILF_CRABBOX_BIN`, and a working service configuration. Resolver default when no explicit/legacy selection exists. |
| `managed-e2b` | E2B SDK dependency, `E2B_API_KEY` and usable sandbox capacity. Set the timeout explicitly. |
| `managed-cf-sandbox` | A wired native Sandbox collector/binding; setting `GILF_CF_SANDBOX_ENABLED=1` on Node cannot create it. |
| `self-host` | `GILF_SELF_HOST_VALIDATION=1` and `GILF_SELF_HOST_EXECUTOR_CMD`; the command must actually run code in isolated compute and clean it up. `GILF_SELF_HOST_NETWORK` declares intent, not enforcement. |
Missing/unknown executors refuse and report missing validation. Do not use `GILF_ALLOW_LOCAL_VALIDATION=1` to bypass this boundary for untrusted PRs. E2B network access defaults open in repository config; configure egress deliberately. See [Validation executors](/docs/configuration/validation-executors).
## Cloudflare account [#cloudflare-account]
The checked-in deployment runbook requires Workers Paid and Containers enabled. Account permissions, region capacity, current commercial requirements and quotas are external prerequisites: have the account owner verify them. No source inspection proves that a particular account can deploy.
Install an approved Wrangler version and confirm its target account. `npm ci` installs the declared Container/Sandbox SDKs but not Wrangler. Both native config files contain estate-specific IDs; replace those before deploying. The main config is checked in with publication and cron authority **live**, not shadow.
Provision the bindings in [Cloudflare quickstart](/docs/getting-started/quickstart-cloudflare). A fresh D1 database requires both `schema.sql` and migration 002 for cron tables. The OMP Dockerfile uses repo-root build context; the Sandbox application has its own Dockerfile.
## Network and human approvals [#network-and-human-approvals]
Node listens on `PORT`, default `8787`, and accepts GitHub webhooks at `POST /github/webhooks`. Use an approved HTTPS endpoint and keep operator routes protected. Outbound access is needed for GitHub API and repository transport, the chosen model provider and sandbox services; this is not an exhaustive domain allowlist for dependency installation.
A human must authorize repository access, App permissions/installation, credential provisioning, infrastructure changes and paid usage. Live publication and scheduler cutover are separate approvals. [Agent onboarding](/docs/getting-started/agent-onboarding) names those gates and distinguishes listener health from completed reviews.
# Run on a Node Box (/docs/self-host/run-on-a-node-box)
This is the production layout for `son-of-anton-review` on `feat/cloudflare-native`. Start with [Requirements](/docs/self-host/requirements) and the [Node quickstart](/docs/getting-started/quickstart-self-host). They cover private source access, missing license terms, Node 25, App configuration, separate Git clone authentication and an approved sandbox.
Do not substitute `son-of-anton-operator-parity` for this checkout: it is a separate Git worktree on `feat/greptile-operator-parity`, with a different Cloudflare queue/operator API. The UI trees are separate again.
## Process model [#process-model]
| Entrypoint | Lifecycle | Responsibility |
| ------------ | ------------ | --------------------------------------------------------------------- |
| `server.mjs` | Long-running | GitHub webhook ingress, health and optional read-only operator routes |
| `worker.mjs` | One-shot | Reconcile stored work, drain a bounded batch, then exit |
Run both as the same dedicated service user, with matching absolute state/queue paths and deliberate environment loading. Neither file automatically reads `.env`. With App credentials present they use a real GitHub publisher; `GILF_PUBLISH_MODE` is not a Node publication gate.
| Worker variable | Runtime default |
| ------------------------- | ---------------------------------------------------------------------------- |
| `GILF_WORKER_MAX_JOBS` | `1` |
| `GILF_WORKER_CONCURRENCY` | `1`, effective concurrency capped by the job limit |
| `GILF_WORKER_ID` | Hostname, with `:` appended to the lease owner |
| `GILF_REVIEWER_MODE` | `codex`, the real runner path, even when its selected provider is OpenRouter |
Do not change `GILF_REVIEWER_MODE` to the provider name: a value other than `codex` selects the entrypoint's empty-result fallback rather than the real reviewer. For this Node entrypoint use `GILF_CODEX_PROVIDER` and `GILF_CODEX_MODEL`. `worker.mjs` does not wire `resolveModelConfig()`, so the modern `GILF_MODEL_PROVIDER` / `GILF_MODEL` pair and role overrides do not select its runner. Native deployment explicitly wires the modern pair.
A worker invocation can exit 0 after individual jobs fail because the drainer collects failures. Monitor stored run state and receipts, not just the exit code or `drained=N`. Lease renewal and publication bookkeeping reduce races; they are not a blanket exactly-once guarantee across independent deployments.
## Queue and state [#queue-and-state]
Queue selection is:
1. Both `GILF_QUEUE_URL` and `GILF_QUEUE_TOKEN`: HTTP Durable Object queue.
2. Otherwise `GILF_QUEUE_DB_PATH`: SQLite queue at that path.
3. Otherwise `GILF_DB_PATH`: queue in the state database.
4. Otherwise a process-local in-memory queue.
The last choice cannot join separate server/worker processes. Setting only one remote-queue variable falls through, rather than reporting a complete remote configuration.
For a single box, use one absolute `GILF_DB_PATH`. SQLiteStore enables WAL and a 5000 ms busy timeout. Back up state and queue consistently using SQLite-aware backup tooling or a fully stopped deployment. Preserve policies, memory, publication receipts and pending work, not just review text. Copying only the live main DB file can miss WAL data.
A remote queue **does not share the run store**. Pointing two independent Node hosts at the same queue while each has its own SQLite state is not a complete multi-host deployment. Do not assume the parity API adds replication to these entrypoints.
### Optional remote queue from the engine tree [#optional-remote-queue-from-the-engine-tree]
`son-of-anton-review/cloudflare/` has a queue/context-graph Worker. Its `wrangler.toml` names `GILF_QUEUE`, a context-graph D1 database and an R2 bucket. Replace account/resource IDs and provision those bindings in your account before deploying.
Unlike the parity Worker, this engine-tree Worker uses **one `GILF_QUEUE_TOKEN`** for its queue and context-graph routes. The existing `cloudflare/deploy.sh` expects that token in the environment; it does not accept the parity split-token setup. A human may instead configure it explicitly from the engine root:
```bash
wrangler secret put GILF_QUEUE_TOKEN --config cloudflare/wrangler.toml
wrangler deploy --config cloudflare/wrangler.toml
```
Give both Node processes that URL/token through approved secret tooling. This deploys a queue Worker, not the native review runtime or expanded operator API. Do not overwrite an existing queue deployment from a different branch.
The Node context-graph client uses `GILF_CONTEXT_GRAPH_URL` / `GILF_CONTEXT_GRAPH_TOKEN`, falling back to the queue URL/token. If neither pair is usable, no remote graph client is constructed. Local graph analysis is separate from remote graph persistence.
`GILF_QUEUE_LEASE_SECONDS` overrides lease duration. Otherwise the worker calculates model timeout plus the longest configured validation timeout plus five minutes; its E2B and Crabbox lease-calculation fallbacks are 45 minutes each. The server's explicit queue lease default is 300 seconds. Set timeout/lease policy deliberately, especially if you copied the sample env's fixed 300-second lease.
## Supervision example: Linux systemd [#supervision-example-linux-systemd]
The following is an **example**, not a claim about installed units or a measured production timer interval. Adapt paths and the absolute Node binary. It assumes the service user's checkout is `%h/son-of-anton-review` and the private environment file is `/etc/son-of-anton/engine.env`.
Create `~/.config/systemd/user/son-of-anton-pr-review-webhook.service`:
```ini
[Unit]
Description=Son of Anton webhook server
[Service]
Type=simple
WorkingDirectory=%h/son-of-anton-review
EnvironmentFile=/etc/son-of-anton/engine.env
ExecStart=/usr/local/bin/node server.mjs
Restart=on-failure
[Install]
WantedBy=default.target
```
Create `~/.config/systemd/user/son-of-anton-pr-review-worker.service`:
```ini
[Unit]
Description=Son of Anton review batch
[Service]
Type=oneshot
WorkingDirectory=%h/son-of-anton-review
EnvironmentFile=/etc/son-of-anton/engine.env
ExecStart=/usr/local/bin/node worker.mjs
```
Create `~/.config/systemd/user/son-of-anton-pr-review-worker.timer`:
```ini
[Unit]
Description=Schedule Son of Anton review batches
[Timer]
OnBootSec=1min
OnUnitInactiveSec=1min
Unit=son-of-anton-pr-review-worker.service
[Install]
WantedBy=timers.target
```
After the service user has the required filesystem and GitHub access:
```bash
systemctl --user daemon-reload
systemctl --user enable --now son-of-anton-pr-review-webhook.service son-of-anton-pr-review-worker.timer
systemctl --user list-timers 'son-of-anton-*'
```
A user manager's boot/logout behavior is host policy; arrange persistence with the administrator. Ensure the supervised environment's `PATH` includes Git, `gh` and the selected executor. On non-systemd hosts use an equivalent long-running server supervisor and scheduled worker invocation. Never infer that a timer succeeded merely because it is enabled.
## Webhook and operator access [#webhook-and-operator-access]
Expose `POST /github/webhooks` through an approved HTTPS proxy/tunnel and set the same secret in GitHub and the server environment. `GET /health` verifies the listener, not end-to-end readiness. Default port is `8787`.
The optional Node dashboard is off by default. Enable it with `GILF_OPERATOR_UI_ENABLED=1` and a separately provisioned `GILF_OPERATOR_UI_TOKEN`. Requests need the exact `Authorization: Bearer` token; **missing token configuration fails closed**. Its views are `/operator/pull-requests`, `/operator/analytics`, `/operator/memory` and `/operator/settings`, with a smaller read-only snapshot/trace API. Keep the route private even when token-protected.
This is not the WorkOS session UI. `anton-ui` has a bearer gate defaulting off; `anton-ui-trace-parity` implements the authenticated session BFF. Neither is installed by starting `server.mjs`.
## Operator CLI [#operator-cli]
From the engine root, pass the actual database path:
```bash
node bin/gilf-review.mjs runs --db /var/lib/son-of-anton/anton.db --limit 20
node bin/gilf-review.mjs usage --db /var/lib/son-of-anton/anton.db --limit 100
node bin/gilf-review.mjs show 'OWNER/REPO#12@HEAD_SHA' --db /var/lib/son-of-anton/anton.db
```
Replace the quoted key with a real stored key. `show` includes the run payload; keep sensitive repository content out of public logs. `harness` is not purely read-only: it saves a deployed harness record. For strict read-only inspection use the SQLite CLI with `-readonly`; constructing the application store can initialize missing schema.
After proving a prior attempt is safe to retry, an operator may run:
```bash
node bin/gilf-review.mjs requeue 'OWNER/REPO#12@HEAD_SHA' --db /var/lib/son-of-anton/anton.db --variant retry-1
```
If queue and state are split, pass `--queue-db` for the real queue file, or load the configured remote queue environment. `--json` selects JSON output. Do not requeue to work around an unresolved publication receipt.
## Cold fallback and upgrades [#cold-fallback-and-upgrades]
Retain a fallback's code, state, environment and service definitions, but keep its mutation timers stopped while Cloudflare owns publication. Rollback requires draining the old authority, reconciling accepted/uncertain work, switching the webhook and only then resuming the replacement worker. Independent SQLite/D1 databases do not deduplicate each other automatically.
Follow [Upgrading](/docs/self-host/upgrading) and [the native rollback procedure](/docs/self-host/run-on-your-cloudflare-account#rollback). Verify one actual review/check receipt from the restored lane before calling rollback complete.
# Run on Your Cloudflare Account (/docs/self-host/run-on-your-cloudflare-account)
Run these commands from an authorized `son-of-anton-review` checkout on `feat/cloudflare-native`. The private-repository and licensing prerequisites in [Requirements](/docs/self-host/requirements) apply. The source runbook `cloudflare-native/DEPLOY.md` mixes provisioning instructions with historical deployment receipts; historical IDs and outcomes are not your installation's state.
**Engine and operator are separate trees.** This procedure deploys `cloudflare-native/src/main.js` and the key broker. The expanded API lives in `son-of-anton-operator-parity/cloudflare/` on `feat/greptile-operator-parity`; the session UI is in `anton-ui-trace-parity`. There is no combined command here, and the native ingress hostname does not serve their `/operator/api/*` routes.
## Account and configuration [#account-and-configuration]
1. Obtain human approval for the target Cloudflare account, spending and infrastructure changes. The checked-in runbook calls for Workers Paid with Containers enabled; have the owner confirm current entitlements and quota.
2. Install locked engine dependencies with `npm ci` and an approved Wrangler version separately. Provide a working Docker/container-image build environment.
3. Authenticate with `wrangler login` if needed, then confirm the intended account with `wrangler whoami`.
4. Replace the account IDs in **both** native config files, the App ID in `wrangler.key-broker.jsonc`, and the D1/KV IDs after provisioning. Resource names and the `KEY_BROKER` service name must match your deployment.
5. Change the checked-in `GILF_PUBLISH_MODE=live` and `CRON_AUTHORITY=live` to `shadow` **before** deploying. Replace `CRON_AUDIT_INSTALLATIONS` and `PR_AGENT_LOOP_SINCE`; do not operate on the checked-in estate.
Keep the OMP `image_build_context` set to `".."`. Its Dockerfile copies `src/`, `package.json`, `prompts/` and `config/` from the engine root. The configuration also declares a separate Sandbox container application and Durable Object binding. It is not a one-image deployment.
## Provision resources [#provision-resources]
For a new deployment only:
| Resource | Binding | Command |
| ----------------- | ------------------ | --------------------------------------------------------- |
| D1 | `DB` | `wrangler d1 create son-of-anton-review-state` |
| KV | `DEDUPE` | `wrangler kv namespace create DEDUPE` |
| R2 payload bucket | `WEBHOOK_PAYLOADS` | `wrangler r2 bucket create son-of-anton-webhook-payloads` |
| R2 cron artifacts | `CRON_ARTIFACTS` | `wrangler r2 bucket create son-of-anton-cron-artifacts` |
| Review queue | `REVIEW_QUEUE` | `wrangler queues create gilf-review-intents` |
| Dead-letter queue | `DEAD_LETTER` | `wrangler queues create gilf-review-dlq` |
Set the resulting IDs in `cloudflare-native/wrangler.jsonc`. For an existing deployment, inventory and reuse its state; do not create replacements merely because a probe fails.
Ingress deduplicates delivery IDs with KV. When a raw webhook exceeds 96 KiB and the R2 binding exists, it stores the payload there and queues a reference. The consumer hydrates that reference. R2 is also used for cron artifact bodies; D1 holds the indexes and durable run state.
### Fresh database [#fresh-database]
```bash
wrangler d1 execute son-of-anton-review-state --config cloudflare-native/wrangler.jsonc --remote --file cloudflare-native/schema.sql
wrangler d1 execute son-of-anton-review-state --config cloudflare-native/wrangler.jsonc --remote --file cloudflare-native/migrations/002-cron-parity.sql
```
Both are required: `schema.sql` contains the native lease fences, outcomes ledger and trace indexes, **but not the cron tables**. Migration 002 adds `cron_runs`, `cron_job_state`, `cron_artifacts` and `cron_dead_letters`.
Existing databases need [Upgrading](/docs/self-host/upgrading): migration 001 is destructive, and an older `publish_leases` needs a guarded `created_at` column addition. `CREATE TABLE IF NOT EXISTS` cannot upgrade an existing table's columns.
## Enable a repository [#enable-a-repository]
App installation and review enablement are different. The native adapter sets repository admission from stored `repo_configs`, previous review history, or `autoEnableNewRepos` (default false). `GILF_REPOS` is not read on this path. Even an authorized manual command remains blocked by explicit repository disablement.
After a human selects an installed canary repo, inspect its current setting. Replace `OWNER/REPO` with its exact full name; do not run the placeholder unchanged.
```bash
wrangler d1 execute son-of-anton-review-state --config cloudflare-native/wrangler.jsonc --remote --command "SELECT repo, json_extract(config_json, '$.enabled') AS enabled FROM repo_configs WHERE repo='OWNER/REPO';"
```
During setup, before reviewing that repo, this upsert enables it without replacing other stored config fields:
```bash
wrangler d1 execute son-of-anton-review-state --config cloudflare-native/wrangler.jsonc --remote --command "INSERT INTO repo_configs (repo, config_json) VALUES ('OWNER/REPO', json_object('enabled', json('true'))) ON CONFLICT(repo) DO UPDATE SET config_json=json_set(repo_configs.config_json, '$.enabled', json('true'));"
```
Repeat the readback and confirm `enabled=1`. This is a deliberate policy mutation, not discovery or installation. Do not race it with another policy writer. Existing run policy snapshots are pinned; use a new manual comment for a new attempt after changing settings.
## Secrets and model selection [#secrets-and-model-selection]
Have the authorized operator enter secret values, never commit or paste them into chat:
```bash
wrangler secret put GITHUB_WEBHOOK_SECRET --config cloudflare-native/wrangler.jsonc
wrangler secret put OPENROUTER_API_KEY --config cloudflare-native/wrangler.jsonc
wrangler secret put GITHUB_APP_PRIVATE_KEY --config cloudflare-native/wrangler.key-broker.jsonc
```
Only the broker holds the native App private key. The main Worker forwards selected model/validation secrets to OMP, which receives scoped broker tokens for GitHub access. Shadow still needs the broker for source access and PR metadata reads.
Set these **plain vars** in the main config:
| Variable | Setup choice |
| ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GILF_MODEL_PROVIDER` | `openrouter`, preferred over legacy `GILF_CODEX_PROVIDER` |
| `GILF_MODEL` | Your approved, currently available OpenRouter model |
| `GILF_OPENROUTER_REQUIRE_FREE` | `1` unless paid usage is explicitly approved; the committed value is `0` |
| `GILF_MODEL_PRICES` | Optional accurate JSON price map, USD per million input/output tokens; remove stale values. Unknown OpenRouter prices yield null estimates, not zero. |
| `GILF_VALIDATION_EXECUTOR` | `managed-cf-sandbox`, with both SDK class export and binding intact |
| `GILF_PUBLISH_MODE` | Explicit `shadow` for commissioning |
| `CRON_AUTHORITY` | Explicit `shadow` until scheduler cutover is approved |
Remove `GILF_VALIDATION_SHADOW_EXECUTOR` for a single-lane canary. If retaining `managed-e2b`, supply `E2B_API_KEY`; it is also required when E2B is primary. The committed E2B timeout is 600000 ms. The E2B budget gate only suppresses the **shadow selector**, and its ledger-read failure forwards the lane unchanged; it is not a hard account-wide spending limit.
Only `container-env.js` allowlisted vars reach OMP. Swarm/planner, inversion and artifact flags are not forwarded. The default image's Codex command is a failing placeholder, not an authenticated alternative ready to select.
## Deploy and connect GitHub [#deploy-and-connect-github]
```bash
wrangler deploy --config cloudflare-native/wrangler.key-broker.jsonc
wrangler deploy --config cloudflare-native/wrangler.jsonc
```
Deploy the broker first so the main service binding resolves. Confirm both container applications and their intended images, not only the Worker upload.
Configure the App webhook at your main Worker's `/github/webhooks` URL, using the matching secret, and install it on the selected repos. See [Requirements](/docs/self-host/requirements) for App permissions and event subscriptions.
```bash
# Shell variable for your deployment URL, not an engine configuration key.
export ANTON_ENGINE_URL='https://your-worker.example'
curl --fail-with-body --silent --show-error "$ANTON_ENGINE_URL/health"
```
Ingress returns `{"ok":true,"service":"son-of-anton-ingress"}`. That endpoint does not test resource bindings. The native fetch handler returns 404 on paths other than health and the webhook; it does not host the dashboard.
## Verify a review [#verify-a-review]
Post a new `@anton review` or `@anton rerun` comment as an authorized owner/member/collaborator. The consumer resolves a missing head from GitHub. It ignores `status` and `help` comments, although the Node server implements those replies.
Inspect the selected canary, not merely the newest unrelated row:
```bash
wrangler d1 execute son-of-anton-review-state --config cloudflare-native/wrangler.jsonc --remote --command "SELECT review_key,head_sha,status,json_extract(record_json,'$.skipReason') AS skip_reason,json_extract(record_json,'$.publish.mode') AS publish_mode FROM review_runs WHERE repo='OWNER/REPO' ORDER BY rowid DESC LIMIT 10;"
wrangler d1 execute son-of-anton-review-state --config cloudflare-native/wrangler.jsonc --remote --command "SELECT trace_id,review_key,status FROM review_traces WHERE repo='OWNER/REPO' ORDER BY started_at DESC LIMIT 10;"
```
Using the returned trace ID:
```bash
wrangler d1 execute son-of-anton-review-state --config cloudflare-native/wrangler.jsonc --remote --command "SELECT name,status,error FROM review_trace_spans WHERE trace_id='TRACE_ID' ORDER BY seq;"
wrangler tail --config cloudflare-native/wrangler.jsonc
```
Read policy decisions, semantic/validation gaps and applicable spans such as `context.graph` and `publish`. There is no invariant that every run has exactly seven successful spans: optional lanes, skips and errors change the result. Shadow suppresses GitHub publication, not model/sandbox execution or state writes. A stored publication status from a recording publisher is not a remote receipt.
For an approved live cutover, first drain competing publishers, then set `GILF_PUBLISH_MODE=live`, redeploy and trigger a new canary. Confirm actual GitHub review/check IDs at the expected head. Invalid publish-mode values throw; parsing trims and lowercases values, and an absent/blank value defaults to shadow. The shipped config explicitly overrides that default with live.
## Cron authority and cadence [#cron-authority-and-cadence]
`CRON_AUTHORITY` is separate from review publication. Shadow cron runs can still record observations and artifacts; do not treat shadow as a read-only database mode. Replace installation scope before scheduling audits and stop legacy mutation timers before enabling live cron authority.
| Cron expression | Job |
| ------------------------------- | --------------------------------------------- |
| `2-59/15 * * * *` | `pr-agent-loop` |
| `3-59/15 * * * *` | `watchdog` |
| `* * * * *` | `command-bridge` |
| `30 22 * * *` and `30 23 * * *` | `nightly-audit`, gated to 23:30 Europe/London |
The nightly handler checks local time so both UTC expressions do not run the audit twice. Prime-watch has a handler but is deliberately unscheduled. Verify actual persisted cron results; an enabled trigger or skipped DST-side event is not proof that the intended audit completed.
## Rollback [#rollback]
1. Set `CRON_AUTHORITY=shadow`, deploy, and let running cron mutations finish.
2. Repoint the App webhook to the retained Node endpoint and verify the saved URL. Keep replacement workers stopped while accepted native reviews finish.
3. Set native `GILF_PUBLISH_MODE=shadow`, deploy, and confirm no live native review remains. A config change cannot stop a running container.
4. Reconcile or redeliver accepted unfinished work before enabling the replacement publisher. Preserve uncertain receipts and dead-letter evidence; do not assume independent D1 and SQLite stores deduplicate each other.
5. Restore only the intended Node timers, then verify a real review and scheduled-job execution on that lane.
Retain fallback code, state, secrets and unit definitions until a separate health/rollback review approves removal. Historical retention dates in the source runbook do not prove today's fallback is safe to retire.
# Secrets and Keys (/docs/self-host/secrets-and-keys)
Keep secret values out of committed source, chat, logs and public reports. A private env file or protected PEM file is supported on Node; Worker secrets are used on Cloudflare. Secrets can legitimately be held by multiple authorized processes, so neither “never in a file” nor “exactly one process” describes the implementation.
These instructions require authorized access to the engine's private source and licensing terms from its owner. Engine code below refers to `son-of-anton-review` on `feat/cloudflare-native`; the expanded operator API and session UI are explicitly separate trees.
## Engine credential holders [#engine-credential-holders]
| Credential | Node | Cloudflare-native |
| --------------------------------------------------------- | --------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| `GITHUB_APP_ID` | Plain config for server/worker App clients | Plain broker var, not a secret |
| `GITHUB_APP_PRIVATE_KEY_PATH` or `GITHUB_APP_PRIVATE_KEY` | Server and worker read the key for publication. Inline PEM wins over a file path. | Only `GITHUB_APP_PRIVATE_KEY` on the key-broker Worker. No App key on main or OMP. |
| `GITHUB_WEBHOOK_SECRET` | Server requires it at boot; worker has an internal fallback, not an App setup mechanism | Main ingress verifies webhook HMAC |
| Provider key | Runner reads the selected provider key from env | Main Worker secret forwarded to OMP |
| `E2B_API_KEY` | Needed for the E2B executor | Forwarded to OMP when configured; needed for E2B primary or shadow validation |
| Git clone/fetch authentication | Separate restricted `gh` and Git authentication under the worker's OS user | A broker-minted read token is exposed temporarily to `gh`/Git through per-process credential environment |
| `GILF_OPERATOR_UI_TOKEN` | Required when the optional Node dashboard is enabled | Not a native ingress dashboard credential; the native main Worker does not expose that UI |
Node does not automatically load `.env`; use an approved service `EnvironmentFile` or explicit `node --env-file=...` as in [the quickstart](/docs/getting-started/quickstart-self-host). Protect both the env file and PEM with service-account-only access. Avoid defining both inline and file-based App keys because updating the file will not replace an inline key still in the environment.
The native broker defaults tokens to `contents:read`, `pull_requests:write`, `checks:write`, `metadata:read`, and accepts explicit permissions/repository scope from callers. Repository scoping is applied when supplied; the broker does not hard-require a nonempty repository list. Do not describe it as enforcing an unconditional single-repo maximum.
## Provider key precedence [#provider-key-precedence]
`src/model-provider.js` checks the bare key first, then the prefixed alternative:
| Provider | Key lookup order |
| ---------- | ---------------------------------------------------- |
| OpenRouter | `OPENROUTER_API_KEY`, then `GILF_OPENROUTER_API_KEY` |
| OpenAI | `OPENAI_API_KEY`, then `GILF_OPENAI_API_KEY` |
| Anthropic | `ANTHROPIC_API_KEY`, then `GILF_ANTHROPIC_API_KEY` |
Only the **bare** provider-key names are in the native container forwarding allowlist. Setting a prefixed key as a main-Worker secret is not sufficient for the container. Codex uses its own installed CLI and authentication; the provider status function reports it configured without checking that login. The native image ships a failing Codex placeholder, not that authenticated CLI.
Provider-status output contains names and configuration booleans, not key values. A configured boolean proves presence, not validity or provider entitlement.
Provider reporting and execution selection are different: the current Node worker uses the runner's `GILF_CODEX_PROVIDER` / `GILF_CODEX_MODEL` fallbacks, while the native entrypoint explicitly supplies `GILF_MODEL_PROVIDER` / `GILF_MODEL`. The exported modern config helper is not called by Node `worker.mjs`. A dashboard's selected-provider indicator is therefore not proof of which model that worker runs.
## Native container forwarding [#native-container-forwarding]
`cloudflare-native/src/container-env.js` forwards nonempty string values for exactly these names:
```text
GILF_MODEL_PROVIDER GILF_CODEX_PROVIDER GILF_MODEL GILF_MODEL_PRICES
GILF_PUBLISH_MODE GILF_OPENROUTER_REQUIRE_FREE
OPENROUTER_API_KEY OPENAI_API_KEY ANTHROPIC_API_KEY
E2B_API_KEY GILF_VALIDATION_SHADOW_EXECUTOR GILF_VALIDATION_SHADOW_TIMEOUT_MS
GILF_E2B_TIMEOUT_MS GILF_E2B_CPU_COUNT GILF_E2B_MEMORY_MB GILF_E2B_TEMPLATE
GILF_VALIDATION_COST_RATES
```
It also sets `GILF_VALIDATION_EXECUTOR` from the Worker or defaults it to `managed-cf-sandbox`, and always sets `GILF_CF_SANDBOX_ENABLED=1`. Neither setting alone creates the required Sandbox collector.
The App key, `GILF_REPOS`, prefixed provider keys, `GILF_MODEL_PRIMARY`, swarm/planner flags, model inversion and evidence-artifact flags are absent. Setting them on the Worker does not make them available to the runner. Adding support requires an engine change, not a secret rename.
The dispatcher can withhold `GILF_VALIDATION_SHADOW_EXECUTOR` at the configured E2B budget. Ledger-read failure forwards it unchanged. This is not a strict provider spending cap or a revocation mechanism for running containers.
## Validation boundary [#validation-boundary]
`buildSafeChildEnv` forwards a limited set of process environment names, including `PATH`, `HOME`, user/locale/terminal/temp settings and CA-certificate paths. It omits App, webhook, queue and provider secrets. Codex adds `CODEX_HOME` for its own authentication; the self-host command receives its repository/network settings.
Environment filtering is **not filesystem or network isolation**. A custom self-host command must enforce its own container/VM boundary and cleanup. `GILF_SELF_HOST_NETWORK` declares intent, not a firewall. The legacy `local` executor is refused unless `GILF_ALLOW_LOCAL_VALIDATION=1`; do not enable it for untrusted PRs. There is no implemented `GILF_BROKER_ALLOW_LOCAL_KEY` setup switch on this path: the Node entrypoints directly use configured App keys.
## Queue and operator tokens differ by tree [#queue-and-operator-tokens-differ-by-tree]
### Engine-tree queue [#engine-tree-queue]
`son-of-anton-review/cloudflare/src/worker.js` uses one `GILF_QUEUE_TOKEN` for its queue/context-graph HTTP routes. The Node caller selects the remote queue with `GILF_QUEUE_URL` and `GILF_QUEUE_TOKEN`. Its graph client can use `GILF_CONTEXT_GRAPH_URL` / `GILF_CONTEXT_GRAPH_TOKEN`, falling back to the queue pair; against this engine-tree Worker the supplied token must match its one configured token.
### Parity operator Worker [#parity-operator-worker]
`son-of-anton-operator-parity/cloudflare/src/worker.js` instead selects route-specific secrets. Its `workerEnv` resolves `SON_OF_ANTON_` before `GILF_` using nullish fallback. An empty canonical value therefore blocks the legacy fallback.
| Suffix | Route selection |
| ------------------------- | -------------------------------------------------------------------------------------------------------- |
| `QUEUE_ENQUEUE_TOKEN` | `/queue/enqueue` |
| `QUEUE_WORKER_TOKEN` | `/queue/claim`, `/queue/ack`, `/queue/fail`, `/queue/renew` |
| `QUEUE_ADMIN_TOKEN` | `/queue/jobs`, `/queue/stats` |
| `CONTEXT_GRAPH_TOKEN` | `/context-graph/*` |
| `REVIEW_STATE_READ_TOKEN` | `/review-state/health` and fallback review-state routes |
| `REVIEW_STATE_SYNC_TOKEN` | `/review-state/sync` |
| `OPERATOR_READ_TOKEN` | General operator reads; command GET accepts this or bridge token |
| `OPERATOR_ADMIN_TOKEN` | Selected queue/repo/memory/settings/SCM mutations and **all** `/operator/api/keys` access, including GET |
| `OPERATOR_COMMAND_TOKEN` | POST under `/operator/api/commands/` |
| `OPERATOR_BRIDGE_TOKEN` | PATCH under `/operator/api/commands/`; command reads also accept it |
Scoped `anton_` API keys follow a separate read/memory-write authorization path; they are not equivalent to admin service tokens. See [Operator API](/docs/operator/operator-api).
Do not give a Node worker only an enqueue token when its reconciler and drainer also need claim/ack/fail/renew. Those Node clients expose a single token setting; the parity split-token API is not a drop-in multi-token configuration for them. The engine queue and parity operator Worker are not interchangeable deployments.
The `SON_OF_ANTON_` lookup is specific to the parity Worker. It does not introduce `ANTON_` aliases for engine model or App variables, and rotating a credential does not implement missing aliases.
## Dashboard authentication is also branch-specific [#dashboard-authentication-is-also-branch-specific]
| Surface | Authentication |
| ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- |
| Engine Node dashboard | Off unless `GILF_OPERATOR_UI_ENABLED=1`; enabled with missing `GILF_OPERATOR_UI_TOKEN` rejects all requests |
| Older `anton-ui` Worker | `OPERATOR_REQUIRE_AUTH` defaults off. When enabled, requires bearer `OPERATOR_ACCESS_TOKEN`; do not expose it assuming SSO exists. |
| `anton-ui-trace-parity` Worker | WorkOS validation plus its own opaque session cookie and `ANTON_SESSIONS` Durable Object. Caller bearer headers do not authorize the BFF. |
The parity session code requires `WORKOS_CLIENT_ID`, `WORKOS_ISSUER` equal to the client-scoped WorkOS API issuer, and a nonempty comma-separated `WORKOS_ALLOWED_USER_IDS`. This public-client flow does not require a WorkOS API key. It sets `Secure; HttpOnly; SameSite=Lax` cookies; a WorkOS organization claim is carried as metadata, not a configured organization-allowlist gate here.
The UI proxy reads `SON_OF_ANTON_API_BASE` / `SON_OF_ANTON_API_TOKEN`, with legacy fallbacks, and uses `SON_OF_ANTON_ADMIN_TOKEN` for privileged requests. Configure these against the **actual backend route authorization** above. In particular, command POST routes select a command token, not the general admin token. There is no native-ingress `OPERATOR_ACCESS_TOKEN` shortcut that installs this backend or UI.
## Coordinated rotation, not a no-downtime promise [#coordinated-rotation-not-a-no-downtime-promise]
1. Identify every holder and caller of the old credential, including running workers/containers and overlapping canonical/legacy names. Keep values out of the inventory.
2. For provider/App keys, provision a replacement through the issuer while the old key remains valid when overlap is supported. Set it on the correct holder.
3. Treat a secret update as a deployment-affecting change. Do not assume it waits for a later explicit deploy or that it rewrites already-running containers. Verify the resulting deployment using the selected Wrangler workflow.
4. Restart/reload Node processes after changing their environment or key file; these entrypoints read App keys at startup.
5. Verify the relevant operation with fresh work and real receipts, then revoke the old key at the issuer. Health alone does not exercise an App/model key.
Webhook and shared bearer-token rotation needs coordination on both ends. The code checks one effective value per name; canonical/legacy names are precedence, not dual-value acceptance. Schedule a pause or controlled cutover rather than promising zero downtime.
For native secrets, the relevant commands remain:
```bash
wrangler secret put GITHUB_APP_PRIVATE_KEY --config cloudflare-native/wrangler.key-broker.jsonc
wrangler secret put OPENROUTER_API_KEY --config cloudflare-native/wrangler.jsonc
wrangler secret put GITHUB_WEBHOOK_SECRET --config cloudflare-native/wrangler.jsonc
```
Run from the engine root only after the human has approved the rotation and identified the intended account. See [Upgrading](/docs/self-host/upgrading) for draining publication authority and verifying a new image.
# Upgrading (/docs/self-host/upgrading)
This procedure covers `son-of-anton-review` on `feat/cloudflare-native`. Use an authorized source revision and resolve the private-source/license prerequisites before acquiring or redistributing an upgrade. Do not blindly pull into a shared worktree or deploy the operator-parity tree as an engine replacement.
`son-of-anton-operator-parity` and `anton-ui-trace-parity` are separate Git worktrees with separate deployment contracts. The native main Worker does not gain their operator API or WorkOS UI through an engine upgrade. Record and assess those versions independently.
## Before changing anything [#before-changing-anything]
1. Identify the active engine revision, Worker/container versions, database bindings, publication mode, cron authority and any retained Node fallback. Source files are not proof of the active deployment.
2. Review the target revision's config and schema changes. The checked-in native config contains estate-specific IDs and explicit **live** authority; preserve your own deployment settings.
3. Back up durable state and publication receipts. On Node, use a SQLite-aware backup or stop all writers before copying database files, including any outstanding WAL state. For D1/R2, use the account's approved backup/export process.
4. For publication, snapshot or lease-fencing changes, pause queue delivery while preserving accepted messages, then drain actual OMP instances and running leases. A Worker config change alone cannot stop existing containers. Coordinate cron writers separately.
5. Have the integration owner complete release verification before production deployment. No source inspection establishes that a migration has run or a new image is healthy.
## Schema inventory [#schema-inventory]
Run D1 commands from the engine root with the explicit native config. First inspect the target, rather than guessing its generation:
```bash
wrangler d1 execute son-of-anton-review-state --config cloudflare-native/wrangler.jsonc --remote --command "SELECT name,sql FROM sqlite_master WHERE name IN ('review_traces','review_trace_spans','publish_leases','finding_outcomes','cron_runs','cron_job_state','cron_artifacts','cron_dead_letters');"
```
| File | Effect and guard |
| ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `schema.sql` | Creates base tables, trace schema/indexes, native dispatch fences, outcomes and policy/memory/SCM tables. Re-running does not alter existing table definitions. **Does not create cron tables.** |
| `001-widen-trace-span-kind.sql` | Drops/recreates both trace tables to support wider kinds and the newer columns. Destructive; only for the legacy trace schema after explicit approval and both row counts verified zero. |
| `002-cron-parity.sql` | Creates `cron_runs`, `cron_job_state`, `cron_artifacts`, `cron_dead_letters` and their indexes. Idempotent table/index creation; does **not** add `publish_leases.created_at`. |
| `003-runtime-lease-fencing.sql` | Adds insert/update triggers rejecting snapshots without the matching running dispatch lease. Re-runnable, but publication-affecting. |
| `004-finding-outcomes.sql` | Adds the outcome ledger and repo/evaluation index. Re-runnable. |
| `005-trace-span-name-index.sql` | Adds the span-name index used by budget/report queries. Re-runnable. |
### Fresh database [#fresh-database]
Use both base and cron schema files:
```bash
wrangler d1 execute son-of-anton-review-state --config cloudflare-native/wrangler.jsonc --remote --file cloudflare-native/schema.sql
wrangler d1 execute son-of-anton-review-state --config cloudflare-native/wrangler.jsonc --remote --file cloudflare-native/migrations/002-cron-parity.sql
```
Do not run migration 001 on a fresh/current database. Migrations 003, 004 and 005 are already represented in the current base schema.
### Legacy trace tables [#legacy-trace-tables]
If schema inspection shows the old trace definition, drain writers and check both counts immediately before considering 001:
```bash
wrangler d1 execute son-of-anton-review-state --config cloudflare-native/wrangler.jsonc --remote --command "SELECT (SELECT COUNT(*) FROM review_traces) AS traces, (SELECT COUNT(*) FROM review_trace_spans) AS spans;"
```
**Stop if either table contains rows, the query fails, or the schema is uncertain.** Migration 001 drops the tables and carries no executable guard. A populated legacy database needs a reviewed copy-forward migration; exporting rows does not make the provided destructive migration preserve them.
Only for a confirmed empty legacy schema, with the destructive change approved:
```bash
wrangler d1 execute son-of-anton-review-state --config cloudflare-native/wrangler.jsonc --remote --file cloudflare-native/migrations/001-widen-trace-span-kind.sql
wrangler d1 execute son-of-anton-review-state --config cloudflare-native/wrangler.jsonc --remote --file cloudflare-native/schema.sql
wrangler d1 execute son-of-anton-review-state --config cloudflare-native/wrangler.jsonc --remote --file cloudflare-native/migrations/002-cron-parity.sql
```
The legacy span table lacks columns such as `seq`; applying the current schema first can fail at the index creation instead of upgrading it. `CREATE TABLE IF NOT EXISTS` is not a column migration.
### Guard `publish_leases.created_at` [#guard-publish_leasescreated_at]
The current base schema includes this column for newly created tables. An existing old table still needs a separate guarded change:
```bash
wrangler d1 execute son-of-anton-review-state --config cloudflare-native/wrangler.jsonc --remote --command "SELECT COUNT(*) AS has_created_at FROM pragma_table_info('publish_leases') WHERE name='created_at';"
```
Only if the table exists and this returns **0**, add the nullable column. If it returns **1**, skip the ALTER. Any other result/error is a stop condition.
```bash
wrangler d1 execute son-of-anton-review-state --config cloudflare-native/wrangler.jsonc --remote --command "ALTER TABLE publish_leases ADD COLUMN created_at TEXT;"
```
Backfill only timestamps already evidenced by the row, then repeat the column guard:
```bash
wrangler d1 execute son-of-anton-review-state --config cloudflare-native/wrangler.jsonc --remote --command "UPDATE publish_leases SET created_at=COALESCE(started_at,completed_at,failed_at) WHERE created_at IS NULL AND COALESCE(started_at,completed_at,failed_at) IS NOT NULL;"
wrangler d1 execute son-of-anton-review-state --config cloudflare-native/wrangler.jsonc --remote --command "SELECT COUNT(*) AS has_created_at FROM pragma_table_info('publish_leases') WHERE name='created_at';"
```
Require `has_created_at=1` before deploying the consumer: its lease INSERT names this column. Rows without any historical timestamp remain null. Do not invent a current timestamp to hide unknown age, and do not use SQLite's unsupported nonconstant-default ALTER from older instructions.
For an already-compatible base database, apply any missing 002 through 005 files before deploying code that needs them. Use the same explicit `--config`, `--remote` and `--file` command form. Inventory/read back the expected tables, column, triggers and indexes; command exit alone is not a schema inventory.
## Deploy and verify the actual image [#deploy-and-verify-the-actual-image]
After schema compatibility and draining are established, deploy the broker first if its code/config changed, then the main Worker:
```bash
wrangler deploy --config cloudflare-native/wrangler.key-broker.jsonc
wrangler deploy --config cloudflare-native/wrangler.jsonc
```
Preserve the OMP repo-root build context and both OMP/Sandbox bindings. Verify the intended container application/image versions as well as the Worker version before resuming delivery. Historical deployment receipts document rollout lag; they do not establish the current rollout state.
Trigger one approved canary with a new manual comment. Match its head SHA to D1 state and actual GitHub review/check receipts. Inspect the applicable trace spans and validation gaps using [the native runbook](/docs/self-host/run-on-your-cloudflare-account#verify-a-review). Do not require an invariant count of seven `ok` spans: optional lanes and policy/validation skips change the shape, and a successful operation trace does not prove the review found every defect.
The expanded `/operator/api/*` API is on the parity Worker, not native ingress. Use D1/logs unless a separately configured compatible operator service is available.
## Timeout defaults [#timeout-defaults]
There is no single universal E2B timeout across the source paths:
| Source path | Unset value or checked-in value |
| ------------------------------------------------ | --------------------------------------------------------------- |
| `collectE2bValidationEvidence` parameter default | 600000 ms (10 minutes) |
| `DEFAULT_REPO_CONFIG.validation.e2b.timeoutMs` | 2700000 ms (45 minutes) |
| `defaultReviewLeaseSeconds` E2B fallback | 2700000 ms, used to size a lease rather than execute validation |
| Native Wrangler vars | Explicit `GILF_E2B_TIMEOUT_MS=600000` |
The runner passes merged per-run/default validation settings into the E2B collector. A stored repo/run timeout can override the collector fallback and a current environment-derived default. Review both the environment and stored config; set the intended value explicitly rather than declaring every Node deployment “now ten minutes.”
There is a second mismatch worth checking: the runner's `GILF_CODEX_TIMEOUT_MS` fallback is 45 minutes, while the worker lease calculator's model-timeout fallback is 10 minutes. Choose explicit model/validation timeout and lease settings suitable for the deployment instead of treating their fallback calculation as a proof that no lease can expire mid-review.
## Model and flag compatibility [#model-and-flag-compatibility]
For the current Node entrypoint, set `GILF_CODEX_PROVIDER` and `GILF_CODEX_MODEL`. It does not call `resolveModelConfig()` and therefore does not consume the modern helper's role overrides or per-provider defaults. The native entrypoint explicitly supplies `GILF_MODEL_PROVIDER` / `GILF_MODEL` instead. Confirm the model recorded by a new run, not only a dashboard selection indicator.
The native container allowlist does not forward swarm/planner, inversion or artifact flags. Adding Worker vars cannot enable those features. Likewise, changing native provider to Codex without replacing the image's placeholder with a real authenticated CLI cannot work.
## Node upgrades [#node-upgrades]
1. Disable the worker schedule, let the current batch finish, and stop other writers before taking a consistent backup. Preserve the webhook-delivery plan during server downtime.
2. Prepare the approved revision in an operator-controlled release directory; do not overwrite another worker's edits with a blind `git pull`.
3. Use Node 25 and `npm ci` for that checkout. Preserve the service user's Git/`gh` authentication, protected App key and environment file, absolute DB/queue paths and workspace ownership.
4. Review schema compatibility. `SQLiteStore` creates missing tables/indexes on construction; it is not a universal arbitrary-schema migration framework. Do not run the Cloudflare D1 migration files against the Node database.
5. Start the intended server version, probe the listener, run one approved worker batch, inspect persisted failures and actual GitHub receipts, then restore the schedule. Worker exit 0 alone is insufficient.
See [Run on a Node box](/docs/self-host/run-on-a-node-box) for example units. No live publication safety comes from setting native-only `GILF_PUBLISH_MODE` on Node.
## Rollback and receipts [#rollback-and-receipts]
A source rollback must remain compatible with the migrated schema and durable publication state. Migrations 002 through 005 are additive in schema terms, but removing lease-fencing expectations can still change behavior; do not assume any older image is safe.
For a model-only rollback, restore the approved model and matching price settings for the correct runtime, then verify a new run. To disable only E2B shadow validation, remove `GILF_VALIDATION_SHADOW_EXECUTOR` from native vars and redeploy; do not restart a second publisher.
For a full native-to-Node authority rollback, follow [the ordered rollback procedure](/docs/self-host/run-on-your-cloudflare-account#rollback). Preserve uncertain publications and accepted work until reconciled.
Keep the source revision, migration/readback evidence, Worker and image versions, canary head, actual GitHub review/check IDs and trace ID in your approved release record. Runtime state, account entitlements, provider pricing and credentials cannot be verified from this source-only guide.