PACT is a local, append-only signed ledger CLI. This Go tool initializes a store, creates an external Ed25519 identity, trusts that identity, appends signed event commits, inspects local heads and objects, verifies the full store, makes official signed checkpoints, and queries a disposable SQLite index in deterministic causal batches.
The Phase 0 contract, Phase 1 single-replica core, Phase 2 bounded index and query surface, and five-step setup command are implemented. See the Phase 0 and Phase 1 status for exact gate, ledger, checkpoint, and recovery evidence, and the setup status for setup dogfood and review evidence.
Development and source installation require Go 1.26 or later. The commands in
this repository use mise exec; configure mise to expose Go 1.26 or later,
or replace that wrapper with your equivalent toolchain command.
Install pact at the exact destination $HOME/.local/bin/pact:
mkdir -p "$HOME/.local/bin"
env -u GOROOT mise exec -- env GOBIN="$HOME/.local/bin" go install ./cmd/pactAdd $HOME/.local/bin to PATH if it is not already present.
On a capable terminal, bare pact plays a brief rotating signed-and-sealed
welcome, then prints top-level help. Redirected bare output prints one static
frame, plain under the default automatic color policy; --color always may
color that frame. pact help and pact --help skip the animation and remain
immediate. pact help index shows
the nested index commands, and pact COMMAND --help remains seal-free.
Commands that need an existing repository, including status, heads,
show, verify, log, query, and index, discover it from the working
directory: PACT walks up through parent directories until it finds .pact.
An explicit --repo PATH is authoritative. PACT resolves that exact path and
does not search its parents.
pact status performs strict verification and checks whether indexed reads
are ready; it never creates or rebuilds an index. A healthy status writes its
summary to stdout and exits 0. An attention status means canonical state is
valid but the index needs work; it writes the summary and pact index rebuild
to stderr, then exits 9. A broken status writes its summary to stderr and
uses the matching verification exit code: 4 for integrity or authorization
failure, or 9 when missing dependencies are the only failure. A missing PACT
store is a store error with exit code 3. With --json, healthy output is one
JSON result on stdout; a non-healthy result is one JSON error envelope on
stderr.
Human output accepts --color auto|always|never. auto is the default and
adds color only on a suitable terminal; NO_COLOR and TERM=dumb disable it.
always forces color, never disables it, and JSON output never contains ANSI
escapes.
pact setup is the canonical bootstrap. A fully flagged command never prompts;
with --json, it emits machine-readable output for automation:
pact setup \
--repo /path/to/project \
--namespace org/example/widget \
--actor operator/alice \
--key-file /path/outside/project/operator.key.json \
--jsonOn a terminal, omit missing values for a guided setup. PACT asks for the namespace, actor, and external key path, prints one five-step plan, then asks for confirmation before writing anything:
pact setup --repo /path/to/projectThe key must live outside the project root. An exact rerun accepts the existing store, key, and trust root, verifies canonical state, and accepts or rebuilds the derived index. It never overwrites a conflicting namespace, actor, key, or trust root. Run the same command after an interrupted setup to resume from the last durable action.
Write the standalone agent skill without initializing a repository:
pact quickstart > SKILL.mdpact record --profile development/v1 validates fixed development payloads before
signing, checks selected evidence, admits one atomic operation, and refreshes the index.
Use the canonical guide for the
complete input object, executable example, exact Git proof, and recovery recipe.
The embedded pact quickstart output is that same guide.
pact record --repo /path/to/project --profile development/v1 \
--input /path/to/record.json --key-file /path/outside/project/operator.key.json --json
pact brief --repo /path/to/project --profile development/v1 \
--namespace org/example/development --subject widget/task-1 --source-revision FULL_GIT_REVISION --json
pact query --repo /path/to/project --namespace org/example/development --subject widget/task-1 --json
pact show --repo /path/to/project EXACT_OBJECT_OR_EVENT_REF --json
pact evidence check --repo /path/to/project --event-ref EXACT_EVENT_REF --json
pact verify --repo /path/to/project --strict --jsonFollow every query cursor and inspect payloads with show before interpreting acceptance.
Keep a green check separate from an open Important finding and explicit acceptance.
Keep concurrent conflicting decisions visible.
Retry uncertain writes with the same input, operation ID, namespace, and signer.
Read admission and index readiness separately; an index failure does not undo admission.
Raw commit and checkpoint writes still require an explicit index rebuild.
pact brief returns complete development state over an exact source and captured frontier.
Its default limit is 16,384 encoded bytes, including the newline.
Use --max-bytes from 1,024 through 65,536; oversized required state exits 2 with empty stdout.
Reads never repair an index. Use explicit index rebuild when readiness fails.
Exit 0 means successful observation, not acceptance.
The canonical guide explains cuts, limits, and exact proof.
The earlier adoption evaluation retains its historical result.
Schema checks, signatures, local-root authority, evidence availability, and supported
acceptance remain separate layers. A shared key does not establish independent review.
Local closure does not establish global completeness or organizational approval.
The commands below run from this repository and need mise, go, and jq.
They build the binary outside the project ledger, keep the private key outside
it, admit two commits, inspect an event and the current head, create a
checkpoint, and run strict verification.
work_dir="$(mktemp -d)"
repo="$work_dir/project"
key_dir="$(mktemp -d)"
pact="$key_dir/pact"
key_file="$key_dir/operator.key.json"
mkdir -p "$repo"
env -u GOROOT mise exec -- go build -o "$pact" ./cmd/pact
"$pact" setup \
--repo "$repo" \
--namespace org/example/widget \
--actor operator/alice \
--key-file "$key_file" \
--json
cat >"$work_dir/first.json" <<'JSON'
{"events":[{"local_id":"e1","kind":"observation","type":"build.test.executed","subject":"build/linux/1","schema_ref":"pact:core/generic-object/v1","payload":{"result":"pass"},"evidence":[],"caused_by":[],"supersedes":[],"tags":["ci"]}]}
JSON
first_commit="$("$pact" commit \
--repo "$repo" \
--key-file "$key_file" \
--events "$work_dir/first.json" \
--json)"
first_id="$(printf '%s' "$first_commit" | jq -r .object_id)"
first_event="$(printf '%s' "$first_commit" | jq -r '.event_refs[0]')"
cat >"$work_dir/second.json" <<'JSON'
{"events":[{"local_id":"e2","kind":"observation","type":"build.package.created","subject":"build/linux/1","schema_ref":"pact:core/generic-object/v1","payload":{"artifact":"widget.tar"},"evidence":[],"caused_by":[],"supersedes":[],"tags":["ci"]}]}
JSON
"$pact" commit \
--repo "$repo" \
--key-file "$key_file" \
--events "$work_dir/second.json" \
--json
"$pact" index status --repo "$repo" --json
"$pact" index rebuild --repo "$repo" --json
"$pact" log --repo "$repo" --namespace org/example --limit 100 --json
"$pact" query \
--repo "$repo" \
--namespace org/example \
--type build.test.executed \
--tag ci \
--limit 100 \
--json
"$pact" show --repo "$repo" "$first_id" --json
"$pact" show --repo "$repo" "$first_event" --json
"$pact" heads --repo "$repo" --namespace org/example --json
"$pact" verify --repo "$repo" --strict --json
checkpoint="$("$pact" checkpoint \
--repo "$repo" \
--key-file "$key_file" \
--scope org/example \
--policy-ref sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \
--authority-epoch epoch-1 \
--schema-ref sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb \
--purpose 'operator lifecycle' \
--json)"
checkpoint_id="$(printf '%s' "$checkpoint" | jq -r .object_id)"
"$pact" show --repo "$repo" "$checkpoint_id" --json
"$pact" verify --repo "$repo" --strict --jsonPrivate keys must stay outside every initialized project root. pact keygen
refuses an output path inside such a root. The project stores public identity
and signatures only. Keep external key files owner-readable only and back them
up through the operator's normal secret-storage process.
Official checkpoint creation has a stricter production rule than the bundled
Python reference: the signing key must already be configured as a local trusted
root with trust-add. Unknown signers cannot create a checkpoint, even when
their signature would be authentic. PACT performs this authorization check and
a full strict store verification before it persists checkpoint bytes.
The shipped command inventory is quickstart, setup, init, keygen,
trust-add, hash, record, commit, status, heads, show, evidence check, verify,
checkpoint, index status, index rebuild, log, and query.
The live SQLite file is .pact/index/pact-v1.sqlite3. It is derived,
disposable, and never canonical ledger history. Removing it does not remove an
event, trust root, head, or checkpoint. Read commands never repair it as a side
effect. Inspect the state, then rebuild after the derived filesystem shape is
safe:
pact index status --repo /path/to/project --json
pact index rebuild --repo /path/to/project --jsonmissing, stale, corrupt, incompatible, and partial-build all require
operator action. index rebuild recreates a missing .pact/index directory and
repairs an absent live file or unusable regular database. Existing directory and
live-file paths must be real, with no SQLite sidecars. Remove or repair unsafe
paths and SQLite sidecars before rebuilding. A successful
raw canonical write makes an existing index stale; this is expected.
The validated record workflow also refreshes the derived index and reports failures separately.
Rebuild scans
and verifies canonical bytes, creates and validates a same-directory temporary
SQLite file, then replaces the live file. Log and query refuse every state
except current. show and canonical verification do not need a usable index.
log accepts repeated --namespace and --actor filters. query requires at
least one filter and accepts repeated --namespace, --type, --kind,
--subject, --actor, --tag, --schema-ref, --event-ref, --caused-by,
and --supersedes filters. Repeated values in one family are OR; different
families are AND. Namespace prefixes match the exact namespace or a
slash-delimited descendant, so org/example does not match org/example2.
Other filters are exact and case-sensitive after validation and field
normalization. Subject and tag values use Unicode NFC normalization.
pact log \
--repo /path/to/project \
--namespace org/example \
--namespace org/another-team \
--limit 100 \
--json
pact query \
--repo /path/to/project \
--type build.test.executed \
--type build.package.created \
--tag ci \
--actor ed25519:sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef \
--limit 100 \
--jsonThe query means “either listed type, and tag ci, and that actor.” Results use
stored causal batches based on known local dependency edges. observed_at is
advisory metadata and never sorts results. A batch is part of a partial order,
not a total sequence; a lower batch number does not prove a path to every later
batch.
Replica status separates local closure from global knowledge. locally_closed
means all dependencies named by the local object set are present. It does not
mean no other object exists elsewhere. Missing dependencies yield stable
blockers and local completeness incomplete; global completeness always
remains unknown. A valid current index built from that incomplete replica has
coverage=partial. An unavailable index has coverage=unavailable, while
partial-build means SQLite diverges from its own declared source and cannot
answer queries. Strict verification and official checkpoints refuse
completeness blockers.
Pagination uses an opaque cursor bound to the command, normalized filters,
page limit, schema, source fingerprint, logical digest, and causal order.
Cursors survive process restart and a deterministic same-source rebuild. A
changed query fails with cursor_query_mismatch. When canonical source changes,
the old index first fails as index_stale; after rebuilding against the changed
source, an old cursor fails with cursor_stale. A page may split a causal batch,
in which case each affected page reports complete_in_page=false. Unresolved
events remain a separate transport group and do not claim to occur later.
The fixed profile is pact/resource-limits/phase2-v1.
| Resource | Maximum |
|---|---|
| One canonical object | 4,194,304 bytes |
| Canonical objects per scan | 100,000 |
| Canonical bytes per scan | 1,073,741,824 bytes |
| Events per commit | 1,024 |
| Events per scan | 250,000 |
| Parents per commit | 64 |
| Longest known causal path | 4,096 signed dependency edges |
| Graph frontier | 4,096 nodes |
| Total graph edges | 1,000,000 |
| Query results per page | default 100; maximum 1,000 |
| Values in one filter family | 64 |
| Values across filter families | 256 |
| Encoded cursor | 4,096 bytes |
| Decoded cursor | 3,072 bytes |
Encoded JSON result, including the --json newline |
16,777,216 bytes |
| Stored SQLite file | 2,147,483,648 bytes |
| Diagnostic samples per axis | 100 |
| One diagnostic text field | 512 UTF-8 bytes |
Exact-bound inputs pass. With --json, the first excess fails safely with a
bounded, machine-readable diagnostic instead of truncating authoritative
results. Log and query use one streaming serializer for both the bound and CLI
output, so this limit does not require a full-page encoding buffer. The code
names the input contract: graph, scan, and filter excesses use
resource_limit; an invalid page limit is a usage error; an oversized or
malformed cursor is cursor_invalid; and an oversized stored index is
corrupt.
Every query hashes the bounded canonical object set and verifies selected canonical commits, so query work grows with the canonical object set. Rebuild scans all bounded canonical bytes and holds the exclusive store lock through publication, pausing writers during rebuild. This fixed-snapshot tradeoff is intentional for the local Phase 2 scope.
Arbitrary external payload schemas, network sync, public policy execution, and delegated authority remain out of scope. The fixed development recorder validates its bundled schemas. PACT also has no trusted timestamps, hardware key service, or global completeness claim.
Install uv and jq, then run the full Go gate, compiled scratch index lifecycle, and
bundled Python reference suite with:
env -u GOROOT mise exec -- ./scripts/check