Agent docs

Local development and debugging

> Evidence: 0 file:line witnesses as of 2026-08-18.

Evidence: 0 file:line witnesses as of 2026-08-18. Confidence: derived, written from the seams and the estate's failures rather than from citations; treat each rule as a design call and argue with it. What changes it: the first module built against this doc (../PLAN.md forge-1) and its instance breakdown.

Running a module on a laptop with nothing provisioned, running it inside an app's process, and answering "it works on my computer but not on prod" without guessing. The owner named this as a large, recurring source of friction; the direct-API and agent path is "the debugging / building side", and it is a contract concern, not a convenience.

Audience: anyone running a module outside prod, or wiring one into an app in-process.

What the instances do#

versable-runnerspeedwaywalmart-mvp
in-process modeRUNNER_DISPATCH=local: in-process semaphore, no queueCloud Tasks vars unset means setImmediate in-processstorage_backend=local, stub_feed_submit=True fakes the vendor round trip
zero-env importyes: every config var has a default (overrides/lib/config/__init__.py)ADC against real Firestore/GCS; expired ADC makes every page 500.env + pydantic settings
local storageGCS stillGCS + Firestore still (shared dev project)local filesystem
local authpassword empty means IAM-locked modereal sessions against dev dataJWT against local Postgres
build canarynone/build-info reports "dev" when unstamped/api/build-info

versable-runner is closest: it imports with zero env and can dispatch in-process. It still needs a bucket. speedway's local mode talks to a real shared dev project, which is why expired ADC 500s every page and why its CLAUDE.md says to check auth before debugging anything.

The rule#

Every module runs in-process with zero environment, against local storage, with a local verifier, and every environment answers the same /health/deep so two environments can be diffed.

In-process is a first-class adapter, not a dev hack#

The queue port has an in-process adapter (04-dispatch-and-workers.md), the storage port has a local-directory adapter (05-storage-and-persistence.md), the verifier has local (02-identity-and-tenancy.md), tracing is a no-op without keys, and usage events go to a local ledger file. Selecting all of them is what env: local means, and it is the default when nothing is set, so python -m module (or the language's equivalent) serves the full surface on a port with no cloud at all. The owner's draft names this directly: a stage "can be a python module, a shell script, even a separate service ... we only care about the input / output"; in-process is how a module is a python module.

Same code path, same handler, same outcome shapes as prod. The only things that differ are adapters and the env value.

Embedded in an app#

An app that wants a capability without a network hop (the console in local development, a test suite, a batch script) imports the module and calls the same runner with the in-process adapters, submitting the same job envelope and reading the same outcomes. State lives wherever the app's adapters put it. Nothing about the payload knows the difference; that is the seam doing its job (01-runner-and-payload.md).

Local auth that cannot leak#

The local verifier accepts one static token from config and refuses to start if the module believes it is on a public URL (a configured service URL that is not localhost, or a platform env var that says Cloud Run or Render). Zero-env means "runs on a laptop", never "runs unauthenticated on the internet".

Diffing environments#

/health/deep returns the effective non-secret config, the adapters in use, the reference-data versions, and the verifier, in the same shape in every environment. "Works locally, not on prod" starts with two curls and a diff, and the answer is usually in the diff: a different reference snapshot, a different variant default, a queue ceiling, a missing secret reported as absent. Secrets are never in the echo, but their presence or absence is.

Re-running one item is a call, not a shell session#

The debug path for a failed item in prod is a single-item child job with parent_job_id and log_level: debug, read back through GET /jobs/{job_id}/outcomes/{item_id}/log (07-observability.md). No shell, no bucket browsing, callable from an app, from curl, or from an agent reading the /guide prose.

Local against dev cloud, when needed#

Sometimes a payload needs the real vendor or the real reference bucket. The module then runs with env: dev credentials for exactly those adapters and local for the rest, and /health/deep says so. speedway's shape (everything against a shared dev project) is the thing to avoid as the default, because one expired credential breaks all of local.

Do-nots#

  • Do not require a bucket, a queue, a database, or a credential to run a module on a laptop.
  • Do not let the in-process mode take a different code path from prod. Same handler, different adapters.
  • Do not let the local verifier start on a public URL.
  • Do not make "debug one prod item" require shell access.
  • Do not point local at a shared cloud project by default. (speedway)
  • Do not answer /health/deep differently in shape across environments.
@versable-git/ui · reference, canon, and method, read in place