Skip to main content

Pi Agent Workspace ABI

This ABI covers the one-shot practice runner's container workspace at /workspace/. The interactive mentor uses a separate workspace contract.

Layout

The workspace is partitioned by lifecycle, expressed as location (ADR 0020): read-only vs writable is decided by where a file lives, not by convention. Three top-level regions — inputs/ (read-only), work/ (agent + precompute scratch; never collected), and out/ (the only directory collected back into SQL).

/workspace/
├── inputs/ # READ-ONLY (enforced by directory ownership, not a path guard).
│ ├── manifest.json # Versioned source, capture-state, and artifact index.
│ ├── sources/ # Integration-namespaced source materialisations (ADR 0020).
│ │ └── scm/repo/ # Pinned, read-only SCM tree without Git metadata or history.
│ ├── context/ # System-written, AI-readable. Populated by the ContentSource SPI.
│ │ # Files: metadata.json, comments.json, diff.patch, diff_summary.md,
│ │ # diff_stat.txt, linked_work_items.json, review_threads.json,
│ │ # general_comments.json, project_inventory.json, context-map.md,
│ │ # issue_summary.md, conversation_thread.json, document.md,
│ │ # outline/index.json + outline/{collection}/{doc}.md — whichever
│ │ # of them apply to the artifact kind under review.
│ ├── history/ # What earlier reviews recorded and already said, about the
│ │ # person whose work this is: observations.json, feedback.json.
│ │ # ALWAYS present, empty on a first-ever review.
│ └── practices/ # Practice catalog (index.json, {slug}.md, all-criteria.md).
├── work/ # AGENT + PRECOMPUTE SCRATCH. Never collected back into SQL.
│ ├── precompute/ # Per-practice precompute scripts (TypeScript, run on Bun).
│ ├── precompute-out/ # Precompute runtime output (logs, structured hints).
│ └── analysis/practices/ # Per-practice analysis output (holds the .gitkeep marker).
├── out/ # THE ONLY collected directory — result.json, review-state.json,
│ # usage.json, runner-debug.json, watchdog-killed.json
├── task.json # TaskEnvelope<…> — runtime contract between handler and runner.
├── .pi/ # Pi SDK agent dir — $PI_CODING_AGENT_DIR resolves here.
│ ├── AGENTS.md # Orchestrator instructions
│ └── settings.json # Pi SDK config (provider, model, compaction)
├── pi-provider.json # Provider/model routing for the runner.
├── pi-provider.ts # Provider/model helper imported by both runners.
├── pi-error-text.ts # Shared error-message helper.
├── pi-observation-normalize.ts # Normalises sandbox observations before out/ is written.
├── pi-runner-usage.ts # Token-usage accounting sidecar.
├── pi-runner-timings.ts # Attempt-timing sidecar.
├── pi-runner-composition.ts # Feedback-composition contract and coherence rules.
├── feedback-composer.md # Composition-stage prompt; digested with the prompt scaffolding.
├── node_modules -> /opt/pi-sdk/… # Symlink created at container start, not staged.
└── .run-pi.ts # Runner script entry point.

The mentor runner stages pi-provider.ts, pi-error-text.ts and pi-mentor-protocol.ts instead. Everything here runs on Bun — the image ships no node and no npm, and its build fails if either appears; see ADR 0030.

The mentor interactive sandbox has its own context/target/, context/user/, and scratch/ layout and is not covered by this ABI.

Path conventions

  • inputs/ — the entire read-only region. Read-only-ness is a filesystem property, not a path guard: the server pre-creates only the work/ ancestors as the sandbox uid and lets the container runtime auto-create the inputs/ directories as root, so the unprivileged agent cannot write there. There is no runtime allowlist of inputs/. The one allowlist that does exist is server-side and narrower — it validates the paths an adapter may inject before the container exists, and covers inputs/context/ and the mentor's .sessions/ only.
  • inputs/context/ — system-written, AI-readable source materializations of the artifact under review. A source with nothing to report still writes its file, holding an empty list: "the record was read and held nothing" is a fact a review may reason from, and "the file is not there" is not.
  • inputs/history/ — the same, for what came before this event rather than for this event. Present on every review, because a review of one event is only a partial practice review and cannot be made whole by reading that event harder. Never reported COMPLETE: it is a bounded window over a growing record.
  • inputs/manifest.json — the source and artifact index, and the authoritative statement of what a run staged. Every source the source contract says applies to the artifact kind is captured and staged, on every review — practice bindings decide which practices may run, never which files the agent can see. A source listed as anything other than AVAILABLE says why in a typed reason code (NO_PROVIDER, GOVERNANCE_NOT_EFFECTIVE, COLLECTION_ERROR, …); none of those reasons is "no practice asked for it". It indexes context files but is not itself a source artifact.
  • inputs/sources/scm/repo/ — a read-only repository tree materialised at the manifest's immutable commit identity. It intentionally excludes .git, history, submodules, and symlink targets; inputs/manifest.json records whether the resulting source is complete or partial.
  • work/ — agent + precompute scratch. The Pi agent may write here; the server never collects it.
  • out/ — the only directory the sandbox collects after the run.
  • node_modules — a symlink to /opt/pi-sdk/node_modules, so the runner's bare-specifier import of @earendil-works/pi-coding-agent resolves. The name is only the directory convention Bun shares with Node: the agent image ships no node or npm binary, and its build fails if one appears. Everything in the sandbox — the runner, the precompute scripts, the SDK's own native bindings — executes on Bun.
  • task.jsonTaskEnvelope<T extends Task>. Schema version 1. The runner asserts schemaVersion and task.kind, exiting 42 on mismatch (envelope/image drift signal).
  • .pi* / inputs/practices/ / work/precompute* / work/analysis/ / out/ — internal control surfaces. AI-readable as configured by the orchestrator, but not part of the public agent ABI; subject to change without a schemaVersion bump.

Exit codes

CodeMeaning
0Success — out/result.json is complete, whether from the first attempt, the recovery retry, or reconstruction from persisted tool state
1No complete review output after the initial attempt and the recovery retry
2Fatal runner error — an unhandled rejection or uncaught exception. Debug and usage files are persisted first
3Watchdog hard-kill (budget + grace exceeded); writes out/watchdog-killed.json first
42Envelope mismatch (unsupported schemaVersion or unknown kind) — image/server drift

137 is a container SIGKILL from the runtime, not a runner code. The server treats 0 as success and 42 as drift; every other non-zero code is a generic failure.

Versioning policy

task.json.schemaVersion is per-envelope, not per-task variant. Add a new permitted Task subtype (a new kind discriminator) without bumping the version. Add a required field to an existing kind, and the version bumps. The Pi runner and the Java server ship in the same Docker-image rebuild, so version drift is bounded by deploy cadence; there is no cross-version contract.