Project-configuration reference
This is the complete local path, artifact, override, and Viewer-preference contract.
Use one explicitly named ptc-project.json to remember stable local paths and
development preferences:
ptc run ptc-project.json
ptc doctor ptc-project.json
ptc doctor ptc-project.json --connect
ptc repl --project ptc-project.json
ptc repl --project ptc-project.json --mission review
ptc viewer ptc-project.jsonptc init DIRECTORY generates this file alongside ptc.json, main.clj, and
an AGENTS.md routing card for coding agents. PtcRunner never searches parent
directories or guesses a project from its filename; the "kind": "ptc-project"
discriminator identifies the document.
The kernel tutorial ships one project document per runnable example. A credential-free run and its Viewer need only the same JSON path:
ptc init kernel-tutorial --example kernel-tutorial
ptc run kernel-tutorial/01-orders.ptc-project.json
ptc viewer kernel-tutorial/01-orders.ptc-project.jsonProvider-backed Examples 02 through 04 additionally reference the shared host
document and kernel-tutorial/.env from their project files. Example 06 uses a
dedicated host document so its deliberate cost ceiling cannot affect the
successful examples. After creating the explicitly named environment file,
their run commands have the same single-argument shape:
ptc run kernel-tutorial/04-multi-turn-agent.ptc-project.jsonKeep the three files separate
| File | What it holds |
|---|---|
ptc.json | workflow, components, input, provider selections, and narrower limits |
ptc-host.json | installed providers, credential references, commands, endpoints, and outer limits |
ptc-project.json | paths to those files, local artifact policy, and Viewer preferences |
Credential values and provider declarations do not belong in the project file. Project choices do not become part of application content identity.
Document shape
{
"$schema": "https://ptc-runner.dev/schemas/ptc-project-config.schema.json",
"kind": "ptc-project",
"version": 1,
"application": {"path": "ptc.json"},
"host": {
"path": "ptc-host.json",
"env_file": {"path": ".env"}
},
"artifacts": {
"root": ".ptc",
"trace": true,
"inspection": false,
"result": false
},
"viewer": {
"port": 0,
"open": true,
"repl": true,
"private": false
}
}kind, version, and application are required. host, artifacts, and
viewer are optional. A document that declares "kind": "ptc-project" and
then fails this schema reports project/project_schema_invalid. The diagnostic
names a bounded schema rule and the deepest project-schema-authorized JSON
Pointer; it does not retain the rejected value, an unknown property name, or
the filesystem path. If bounded schema validation times out or exceeds its
resource bound, the retryable project/schema_validation_unavailable
diagnostic reports the unavailable validation instead of claiming that the
document is invalid. A syntactically valid run, validate, doctor, or
models invocation still publishes a requested --envelope before any project
reference or provider is opened. Malformed command syntax remains an argument
rejection and publishes no envelope.
Every object rejects unknown and duplicate keys. Paths are portable relative
paths resolved beneath the project document's directory; absolute paths and
.. traversal are rejected. The generated schema is served as ptc docs schema-project
(priv/schemas/ptc-project-config.schema.json
in the repository).
Inspection requires traces because private records must correlate with a
matching run. viewer.private is a separate explicit local grant: creating a
private artifact does not automatically expose it to Viewer. viewer.repl
independently enables the browser REPL, including when viewer.private is
true. REPL evaluations remain fixed to the public run-analysis-v1 profile
and its immutable normal-trace snapshot; they cannot query the private evidence
displayed elsewhere in the Viewer.
Artifact layout
For run, enabled project artifacts derive from the command run reference:
artifacts.envelope defaults to true; every project run attempts to write
its public command envelope at .ptc/envelopes/<run-ref>.json. Set it to
false only when the caller deliberately owns durable envelope retention.
.ptc/traces/<run-ref>.jsonl
.ptc/traces/<run-ref>.private.jsonl
.ptc/inspection/<run-ref>.ptcins
.ptc/results/<run-ref>.json
.ptc/results/<run-ref>.private.json
.ptc/envelopes/<run-ref>.jsonThe root and its fixed child directories are owner-only. The first project run
creates the complete layout atomically; an existing incomplete, permissive, or
symlinked layout is refused. When a pre-existing directory fails the owner-only
(0700) check, a run that publishes an envelope names the path and the
chmod 700 remedy rather than reporting a bare publication failure; the
conditions for that are in the table below. Artifact files retain the normal
no-replace and privacy rules. Private directories (0700) and regular files
(0600) must both be owned by the runtime user; root ownership is accepted
only when the runtime itself runs as root. Temporary private directories
receive mode 0700 at creation, before any artifacts are written.
Shared path resolution follows at most 16 symlinks, including links in parent
directories. This bound also applies to the MCP stdio launcher: a path through
16 links is accepted; 17 or more is refused with
mcp_stdio_launcher_unavailable. There is no per-caller override.
artifacts.result controls both the standalone result artifact and result-value
retention in command envelopes. When it is false and no --output or
--private-output override requests a result artifact, persisted project-ledger
and --envelope documents keep result_class but omit result.value. Normal
results still print to stdout. An explicit result destination makes the artifact
state written and retains the value in the envelope.
The run creates the root and its four children, and nothing above them. A
directory above the root is yours, so its mode and ownership are not the
command's to choose; artifacts.root may name a path several levels deep, but
every level above the root must already exist.
Ancestors are held to a weaker rule than the root itself: each one must be
owned by you or by root, and must not be writable by group or other unless it
carries the sticky bit. A sticky world-writable directory such as /tmp is
therefore accepted, while an ordinary 0775 directory is not. The root and its
four children remain owner-only (0700).
The refusals below name the directory that earned them; anything else falls back to the generic publication failure:
| Cause | Reported as | Remedy |
|---|---|---|
| an ancestor of the root does not exist | envelope/destination_parent_unavailable | mkdir -p the root's parent, or point artifacts.root at an existing directory |
| an ancestor is group- or world-writable and not sticky | envelope/destination_parent_unsafe | chmod go-w PATH |
| an ancestor is owned by another user | envelope/destination_parent_unsafe | point artifacts.root under a directory you own |
| the root exists without its four children, at any mode | envelope/publication_failed | remove the root and let ptc recreate it |
| the complete root or a child exists at a wider mode | envelope/publication_failed | chmod 700 PATH |
| the root's parent is not writable and searchable by its owner | envelope/publication_failed | chmod u+wx PATH, or point artifacts.root elsewhere |
| the filesystem refuses creation despite writable mode bits, such as on a read-only mount | envelope/publication_failed | point artifacts.root at a writable filesystem |
| any other refusal | envelope/publication_failed | point artifacts.root elsewhere |
The missing-ancestor message names the shallowest missing directory and offers
mkdir -p on the resolved parent, so one command creates every level rather
than only the first. Where a symlink stands in the ancestry the two differ: the
message names the link's target, because creating the link's own name would
fail on a path that already exists.
Every path is printed quoted and escaped, since a symlink target is filesystem
content rather than something you typed, and a suggested command is offered
only when its path holds no control characters. If no envelope destination
succeeds, these arrive on stderr with exit 74 because the envelope that would
normally carry a diagnostic is the artifact that could not be written. A
separately writable --envelope convenience copy is still attempted; when it
succeeds, the command retains its original exit status and reports the failed
project-ledger destination on stderr.
The table above describes a run with the project envelope ledger enabled, the
ptc init default, where these refusals arrive at exit 74. With
artifacts.envelope set to false the same preflight still stops the run
before it executes and its sentences still name the path, rule, and remedy;
they are reported through the ordinary destination phase as
destination/invalid_destination at exit 7. A requested --envelope copy
carries that diagnostic when its independent destination is writable.
ptc viewer reports the named sentences as viewer/artifact_root_unusable.
Before ptc run bootstraps its runtime, automatic cleanup considers hidden
.ptc-private-<12 lowercase hex digits> artifact staging directories only under
your configured artifacts.root and its fixed envelopes, inspection,
results, and traces children. New staging directories record their owning
process in an owner-only owner file. Cleanup uses the same-host reservation
policy: only a proven dead owner permits deletion; live owners, permission
failures, malformed or unreadable markers, and unknown liveness are preserved.
Markerless directories are eligible only when their modification time is more
than 60 seconds old. Ownership across hosts on a network filesystem is outside
this policy.
The optional launcher companion (0.2.1 or later) supplies the bounded
filesystem operations. When it is unavailable, admission skips the staging
sweep without enumeration or deletion; destination-driven reservation recovery
continues independently. Each admission with the helper inspects at most 256
directory entries across those five locations, including unrelated entries, and considers at most 16 staging candidates. The
launcher replays the consumed prefix using only readdir; that skip opens,
stats, reads markers from, and deletes none of those entries and is outside
the inspection and deletion budget. Each
candidate reads at most five additional entries and deletes at most the two
fixed regular files (owner and artifact) and their directory. Extra or
nested contents are preserved; cleanup never recursively scans them. Excess
candidates remain for later admissions. The budgets reserve work for every
location, and each parent keeps a bounded owner-only
.ptc-artifact-staging.cursor progress file under its admission lock. Later
admissions resume past preserved candidates and unrelated entries rather than
repeatedly starting at the first entry. Deletions can shift the consumed prefix;
the cursor wraps at directory exhaustion to revisit entries shifted behind it.
Symlinks, changed identities,
published artifacts, unrelated entries, and outside-root output parents are
preserved. Reservation directories remain destination-driven and are never
part of this sweep. Cleanup is best effort and does not create an artifact
root when all project artifact defaults are disabled.
The trace filenames are also the canonical directory-discovery contract. Each file contains exactly the run ID named by its stem and one trace identity; arbitrary aggregate filenames and split histories remain supported only when a caller explicitly selects one file, not when Viewer or run analysis discovers a directory. Stable damaged components are isolated without hiding unrelated valid runs.
Overrides and lazy environment loading
An explicit command value wins over the corresponding project default for host, environment, and trace/inspection/result destinations:
ptc run ptc-project.json --host-config deployment/staging-host.json
ptc run ptc-project.json --trace-dir tmp/one-off-traces
ptc repl --project ptc-project.json --env-file deployment/staging.env
ptc repl --project ptc-project.json --mission review
ptc viewer ptc-project.json --env-file deployment/staging.env--envelope FILE is different: it adds a convenience copy for the invocation
and does not suppress the project ledger under .ptc/envelopes/ when
artifacts.envelope is enabled. The ledger and convenience copy publish
independently, so a completed run retains its ledger record even when the copy
cannot be written. Trace, inspection, and result overrides still replace their
project defaults.
The command reserves an explicit --envelope destination before bootstrap. A
syntactically valid path that the filesystem cannot reserve is reported as
destination/envelope_destination_unavailable at exit 7, with the resolved
path and a bounded reason such as eacces, enospc, edquot, or erofs.
Invalid path syntax remains an argument error at exit 2.
Mission selection, input, and component-override switches remain
invocation-only. Mission names stay in the application manifest rather than
being duplicated as project defaults. An environment file explicitly selected
by the project or --env-file is loaded only when inert preparation proves that
a selected mission provider or its dependency uses an environment-backed
credential. Unrelated providers do not cause environment-backed credentials to
be read. Runs without providers, passive doctor, Viewer startup, and file- or
literal-backed credentials do not read it.
Viewer-started workflows and missions read the selected file lazily through
their ordinary command preparation. When the file is loaded, assigned keys
override process values, including with empty values, while omitted keys retain
their process values.
Direct manifest invocation remains the low-level form for automation:
ptc run ptc.json \
--host-config ptc-host.json \
--trace-dir build/traces \
--envelope build/command.jsonViewer
ptc viewer ptc-project.json uses the project's trace root, port,
browser-opening preference, REPL setting, and private-data authorization. Trace
and correlated inspection directories are captured before the listener starts;
HTTP requests select only a run ID and never a filesystem path. Revoking
viewer.private takes effect on the next request and drops the Viewer's held
private evidence. This grant controls the Viewer only; it does not restrict
ptc transcript or ptc repl --profile private-run-analysis-v2, which can
still read the inspection artifact with their own explicit private authorization.
To stop future inspection capture, set artifacts.inspection: false; to remove
existing inspection evidence, delete the .ptcins files under
artifacts.root/inspection/ and any copies or exported private transcripts.
This does not remove other retained data: review .ptc/envelopes/, result
artifacts, captured stdout, and backups separately.
Turning the grant on, and discovering runs that finished after startup, require Refresh on the Runs list. Other project fields stay boot-read. Browser opening is a bounded convenience, and additionally requires an attached terminal: missing or failing platform openers do not stop Viewer.
The Viewer port defaults to 0, which asks the operating system for a free
port; startup prints the selected address before opening a browser. Set a fixed
port only when another process needs a stable address. If that port is occupied,
the command probes loopback and names the project when another PTC Viewer owns
it. The Live project header exposes the exact project
document path, so a working page cannot silently look like the project whose
startup just failed.
The REPL and private-data settings are orthogonal. Enabling both presents the public-trace REPL alongside the private evidence panels without adding private inspection authority to the evaluation session.
Because artifacts.inspection and viewer.private must both hold, granting the
Viewer access to an existing inspection artifact needs no re-run. The artifact
on disk is already usable once the Viewer refreshes after the grant. Revoking
the grant takes effect on the next request without Refresh. For headless access
to the same reconstructed conversation, use
ptc repl --profile private-run-analysis-v2; run ptc docs repl for the
complete invocation.
Viewer-started workflows and missions use the project's host.env_file when
one is declared. ptc viewer ptc-project.json --env-file FILE supplies an
invocation-time override instead. PtcRunner never searches implicitly for
.env; without either form, credentials must already be present in the Viewer
process environment or use another trusted host binding.
The listener binds 127.0.0.1. The project document deliberately cannot change
that: exposure is an invocation-time decision made with --listen 0.0.0.0,
where it stays visible in the command line rather than stored in a file. See
the Viewer exposure reference.
The Viewer ships in the standalone release and container image and is absent from the published package.