Releasing Application Prompts
The bundle target writes a prompt bundle for every @prompts entry. prs release turns those
bundles into releases. It stores each bundle under its content hash, and it records which hash each
environment, such as staging or prod, serves. An application reads the label of its
environment, then the bundle file the label points at.
Read Application Prompts first. This page covers only the release flow.
Configuration
Releases are opt-in. List the environments in promotion order, first to last:
releases: environments: [staging, prod]Without releases, every prs release command fails with PS5001 and shows this snippet. See
releases for the name rules.
policies are read by every release command. A policy with an unknown kind fails the command, it is
never skipped. Policies parse the same way as in prs validate, against governance.roles,
releases.environments and the model catalog with models.profiles, so a role, environment or model
that validate refuses fails a release command too.
Files
All release files live in .promptscript/releases/ and belong in Git:
.promptscript/releases/ manifest.json bundle index of the latest build bundles/sha256-<hex>.json one immutable file per content hash staging.json label of the staging environment prod.json label of the prod environment- A bundle file has exactly the bytes the
bundletarget writes. A release never rewrites it and never deletes it. - The manifest is a bundle index of the latest build. Its
fileentries point intobundles/. - A label has one entry per prompt, sorted by id, and lists each id once. The
$schemaishttps://getpromptscript.dev/schema/prompt-release-label/v1.json.
{ "$schema": "https://getpromptscript.dev/schema/prompt-release-label/v1.json", "environment": "prod", "prompts": [ { "id": "acme-support/refund-reply", "contentHash": "sha256-<64 hex digits>" } ]}Release files must be regular files. A symbolic link in .promptscript, .promptscript/releases,
bundles/, or for a release file itself is refused. Names must match in case exactly: a
Prod.json or Manifest.json is refused, because a case-insensitive disk would read it as the
real file. A manifest or label that does not parse as one is never overwritten, the error tells
you to restore it from Git.
prs compile refuses any output inside .promptscript/releases/ (PS5008), and watch mode ignores
that directory. Compile cleanup never removes release files.
The flow
flowchart LR src[.prs source] -->|prs release build| bundles[bundles/ and manifest.json] bundles -->|promote --to staging| staging[staging.json] staging -->|promote --to prod| prod[prod.json]
Build
prs release buildBuild compiles the source in memory. It never reads the compiled prompts/ directory, and it works
whether or not bundle is in targets. It writes each new bundle file, checks the ones that exist,
and writes the manifest only when its bytes change. A second run writes nothing. A program without
prompts gives prompts: [].
A build always runs with these settings, so a release cannot ship what a normal compile would flag:
- Policies are on.
validation.skipPolicies: trueandvalidation.ignoreHashesin the YAML are ignored. Each policy keeps its ownseverity, so only a policy withseverity: errorfails the build. - The security rules PS005
blocked-patterns, PS010suspicious-urls, PS011authority-injection, PS012obfuscated-content, PS013path-traversaland PS014unicode-securityrun as errors, whatevervalidation.rulesandvalidation.disableRulessay. When the config switched one off, the error says so. validation.excludesdo not apply to these rules in a release, and imported text is scanned like local text. An import excluded from a rule inprs validateis still checked when it ships.validation.allowedPatternsdoes apply, like in every other command, so a pattern exempted there stays exempted.
An existing bundle file that does not verify fails with PS5004 and the advice to restore it from
Git. Use --dry-run to see what would be written.
Promote
prs release promote acme-support/refund-reply --to stagingprs release promote acme-support/refund-reply --to prodPromote points an environment at a bundle. The argument is <id> or <id>@<hash>. The hash is the
full sha256-<64 hex> or a prefix of at least 8 hex digits, with or without sha256-, matched
among the bundle files of that id. An ambiguous prefix lists the candidates.
The promotion order is fixed and has no escape:
- The first environment recompiles the source like build does and accepts only the fresh hash. A
hash you give must equal it. Without a hash, the manifest entry must equal it. Otherwise the
command fails with
PS5004and tells you to runprs release build. - A later environment accepts only the hash the previous environment holds now. Otherwise the
command fails with
PS5005and prints the exact promote command for the previous environment.
There is no --force, --skip-policies or --ignore-hashes. The bundle file must exist (PS5003)
and verify with that id (PS5004).
Promote warns, without failing, when the new bundle changes inputs compared with the bundle the environment holds now: a new required input, an input that became required, a removed input, or a changed type or options. Callers need an update before the release reaches them.
Before the label is written, inside the lock, the prompt version must pass the release gates. The label is written atomically. Promoting the hash an environment already holds is a no-op with exit code 0.
Roll back
prs release rollback prodRollback undoes the last committed change of a label. It reads Git history, there is no history field in the label.
- The label must have no uncommitted change, else
PS5006tells you to commit it or rungit restore. - It takes the label as it was before its last commit. At the first promotion nothing is before it,
and
PS5006says so. - In a shallow checkout the commit before may be missing.
PS5006then tells you to rungit fetch --unshallow. - The restored label must parse and every hash in it must verify.
- Every hash the rollback brings back must pass the release gates with the records committed now. An entry the rollback leaves as it is is not judged again.
A committed rollback is undone by a second rollback, like helm rollback. Rollback does not check
the promotion order.
To go further back, restore the label from an older commit with Git. Promote cannot do it: the
first environment takes only the fresh build and every later one only the hash of the environment
before it. Find the commit with prs release status --history or git log, restore the file, check
it, and commit it in a pull request like any other label change:
prs release status --history 10git restore --source=<commit> -- .promptscript/releases/prod.jsonprs release statusEvery rollback PS5006 message points to this.
Status
prs release statusprs release status --history 5Status lists every environment with the prompt id, the hash (12 hex digits), whether the hash is in the latest build (equals the manifest) and whether the bundle verifies. A missing label means an empty environment. A label file for an environment that is not configured gives a warning.
Every referenced bundle is verified, and a failure exits with code 1, so CI can run it.
--history <n> (1 to 1000) adds the commit, date and subject of the last n commits of each label.
It shows no author names.
With prompt policies, status also runs the release gates on every entry and adds
an approvals column: ok, missing: <roles>, or - when no required-approvals policy applies.
Plain status only reports: a gate that fails is a warning and the exit code does not change. The
check that fails on it is --base.
One pull request per promotion
Every promotion is a reviewed pull request, and Git history is the release log:
- Change the prompts and run
prs release build. - Commit the changed
.prssource,promptscript.yamland every config file it reaches throughextendstogether with the new bundle files and the manifest. Open a pull request and merge it. Promotion to the first environment recompiles the source with the config on the branch, so a build without them fails withPS5004. - Run
prs release promote <id> --to staging, commit the label, open a pull request. - When staging looks right, run
prs release promote <id> --to prodin a new pull request.
Roll back the same way: run prs release rollback prod, commit the label, open a pull request.
prs release status --base lets that pass, see below. Going back further than the last change is a
new promotion under the promote rules.
Promote, build and rollback take a project lock, .promptscript-release.lock. Another release
command that holds it fails with PS5007. A stale lock is broken after its process is gone or after
30 minutes. Dry runs take no lock. Add the lock file to .gitignore.
Approvals
An approval is a small JSON file that says one person approved one exact prompt version in one
role. prs approve writes it. The version is the content hash, so a record covers the messages,
the model and the governance values of that bundle, nothing else.
Approvals need releases and the roles under governance.roles, each with the teams that review
it:
releases: environments: [staging, prod]governance: roles: legal: ['@acme/legal'] security: ['@acme/appsec']Approve the full hash, for one environment or for all of them:
prs approve --list acme-support/refund-replyprs approve acme-support/refund-reply@sha256-<64 hex digits> --role legal --approver @jane --env prodprs approve acme-support/refund-reply@sha256-<64 hex digits> --role security --approver @bob --all-environments --comment "Checked in PR 123"--list prints the hash the checkout builds. The command checks, and fails without writing:
- the role is in
governance.roles(PS5101). A role named like a Windows device (con,nul,com1, …) is refused, since it names a directory. - the reference is
<id>@sha256-<64 hex>with a valid prompt id (PS5102). A prefix is not enough, and no id segment may end with a dot. --envis a configured environment (PS5002), or--all-environmentsis given. One of the two is required.--commenthas at most 2000 characters, no control characters except newlines and no invisible format characters such as bidi marks.--approveris a Git host user handle like@jane(PS5104). An email address or a team is refused, so no address ends up in a file name.- the hash is real (
PS5103): a bundle in.promptscript/releases/bundles/of that prompt that verifies, or what the source builds right now. A stored bundle that does not verify isPS5004.
Before it writes, the command prints what the hash holds to stderr: the description, governance
values, model, inputs with their defaults and options, output contract and messages. Invisible
characters, such as tag characters or bidi marks, show as <U+XXXX>. Read the preview. If it is not
what you meant to approve, delete the record file before you commit it. The command shows no diff
against an earlier version: the pull request diff is that diff. Link it in --comment or in the
pull request.
Record files
.promptscript/approvals/<role>/<prompt id>/<content hash>.<approver>.json.promptscript/approvals/legal/acme-support/refund-reply/sha256-3f9a...jane.jsonThe approver part is the handle without @, lowercased. There is one file per approver, role and
hash. The file holds exactly these bytes, with environment only when --env was given and
comment only when one was given:
{ "$schema": "https://getpromptscript.dev/schema/approval-record/v1.json", "prompt": "acme-support/refund-reply", "contentHash": "sha256-<64 hex digits>", "role": "legal", "approver": { "source": "asserted", "value": "@jane" }, "createdAt": "2026-10-22T09:14:03.120Z", "environment": "prod", "comment": "Checked in PR 123"}A record is never overwritten. When the file exists, the command fails with PS5105 and exit code 2. To withdraw an approval, delete the file in a commit. To change its environment or comment,
delete it in a commit and approve again.
A record counts only with these exact bytes. The first prs approve writes
.promptscript/approvals/.gitattributes with * -text, so a checkout with core.autocrlf never
changes its line endings. Commit that file too. When the file exists without that line, the command
warns and leaves it alone.
The path that counts is the one Git tracks, because CODEOWNERS matches that path, and the bytes
must equal the ones Git has for that path, because those are the reviewed ones. On a
case-insensitive disk, as on macOS and Windows, Legal/ and legal/ are one directory, so a record
committed as Legal/... shows up under legal/, and two such paths share one file on disk.
--list therefore compares the path in Git and the blob Git has for it, not only what the disk
shows.
The writer walks the record path from the project root. A symbolic link, a file where a directory
should be, or a name that differs only in case, like Legal/ next to legal, fails with PS5109.
It creates the record from a temporary file and links it into place, so an existing name is never
replaced. It takes the release lock .promptscript-release.lock, like build and promote.
Listing approvals
prs approve --listprs approve --list acme-support/refund-reply --format json--list compiles the source in memory, reads every record, asks Git with git ls-files -s which
paths it tracks and which blob each holds, and prints, per prompt, the hash the checkout builds and
each record with its role, approver, scope (prod or all environments) and state. When there are
record files, it needs Git and a repository, else PS5108.
| State | Meaning |
|---|---|
current | The record approves the hash the checkout builds |
labelled:<env> | Not the current hash, but the label of that environment holds it |
stale | Neither: the prompt changed since |
untracked | Not added to Git yet. A staged record already counts as tracked |
modified | The bytes on disk are not the ones Git has for the path |
misplaced | Git tracks the file at another path than its fields give |
unknown-role | The role is not in governance.roles |
Only current and labelled records can count. A file that is no valid record, for example one
with CRLF line endings, gets a PS5109 warning, and so do modified, misplaced and unknown-role
records. The file name is never trusted alone: the fields decide, and the path in Git must match
them. A record with a signature gets a warning, since signatures are not checked in this version.
Links are never followed. At most 10000 files and directories, files of up to 64 KiB each, and 8
directory levels below .promptscript/approvals/ are read. Git’s list of record paths may take up
to 10 MiB; a longer one fails with PS5108.
With --format json, stdout holds one document that matches
https://getpromptscript.dev/schema/approval-report/v1.json.
What an approval proves
In this version a record is a claim. The approver is whatever --approver said, and createdAt is
the clock of the machine that wrote it. The release gates count records, but only
committed ones at their own path.
The binding review is the one on the Git host: the pull request that adds the record, at the path
Git has for it. Give each role directory its own code owners, so a record under legal/ cannot
merge without a review from @acme/legal. prs governance codeowners writes these lines from
governance, see CODEOWNERS from Governance. By hand they look like this:
/promptscript.yaml @acme/prompt-platform/shared/base.yaml @acme/prompt-platform/.promptscript/releases @acme/prompt-platform/.github/CODEOWNERS @acme/prompt-platform/.promptscript/approvals @acme/prompt-platform/.promptscript/approvals/legal @acme/legal/.promptscript/approvals/security @acme/appsecThe admin lines cover what decides which review a record needs: the config and every file its
extends reaches (here shared/base.yaml), the release files and the CODEOWNERS file itself.
Turn on required code owner review for the branch. The host also stops a pull request author from
approving their own pull request; the CLI does not check that. For “who approved this for prod”,
prs audit log names the record and the commit that added it; read the review of
the pull request of that commit. A well-formed record there only has the right bytes at the
right path. It does not mean a gate accepts it, and the review by the role team lives in the
review log of the Git host, not in Git.
Release gates
The gates are the prompt policies, run on the prompt version a label is about to hold. They run
in prs release promote before the label is written, in prs release rollback for every hash it
brings back, in prs release status --base for every entry that changed, and in plain
prs release status for every entry, where they only warn. There is no flag or setting around
them: they call the policy evaluators themselves, so validation.disableRules,
validation.rules, custom rules and --skip-policies do not apply. validation.blockedPatterns
and validation.allowedPatterns do apply, like everywhere else.
Without a prompt policy the gates pass and nothing else runs. With one:
- The policies are parsed again. A policy that does not parse fails every gate.
- The source is built in memory, with the same settings as
prs release build. A source that does not build fails the gates, also for a rollback: fix the source, or change the policy in a reviewed pull request. - The policy risk is the highest of the bundle’s risk, the source’s risk and the floors of both.
matchsees the domain of either and the data classes of both. A bundle file only verifies against its own hash, so a bundle with lowered governance values still falls under the policies the source falls under. required-metadata,risk-floor,model-allowlistandcontent-rulesjudge the bundle that ships: its governance, model, inputs and messages.required-approvalscounts approval records for that prompt, hash and environment.required-evalsisunavailable, see Policy Engine.
A check of severity error that fails, or is unavailable, fails the command with a PS030 error
that names the policy and what to do. A check of severity warning is a warning. The skill policy
kinds are not part of the gates, they run as PS030 in the source build.
Which approvals count
The gates read the records committed at HEAD, with git ls-tree -r HEAD, never the disk or the
index. A record that is only staged, or edited and not committed, does not count. prs approve --list reads the index, so it shows a staged record as current while the gates do not count it
yet.
status --base on a pull request counts the records the pull request adds, and the gate cannot
tell who wrote them. A record binds only through review: each .promptscript/approvals/<role>/
needs that role’s team as code owners, with required code owner review on the branch (see
What an approval proves). prs governance codeowners writes those
lines, see CODEOWNERS from Governance. Without it, any author can add a record in
another person’s name and pass the gate.
A record counts for one approver when:
- its fields name the prompt, the hash and the role, and
environmentis the one promoted to, or absent; - its committed path is the one its fields give,
.promptscript/approvals/<role>/<prompt id>/.... CODEOWNERS protects records by that path, so a record anywhere else had the wrong reviewers; - the role is still in
governance.roles; - the bytes are a valid v1 record.
An approver counts once per role, compared lowercased. A record of the same prompt and role for
another hash shows as stale. Two committed paths under .promptscript/approvals/ that differ only
in case fail the gates with PS5109: a case-insensitive disk keeps one of them. A committed file
over 64 KiB is no record and does not count. When Git cannot list or read the records, for example
git is not on PATH or an object is missing, the gates fail with PS5108 and do not count zero
approvals.
prs release status --base <ref> takes the policies, governance and releases of <ref>, and the
records committed at HEAD.
Output
Gates for acme-support/refund-reply@sha256-3f9a1c2b4d5e to prod (policy risk: high) passed high-risk-models model-allowlist failed high-risk-signoff legal 0 of 1 (stale: @bob for sha256-91bc0d2e1a2b) unavailable prod-evals acme-support/refund-reply needs passing evals for prod, but release gates do not read eval results in this version of prsPromotion refused. Label prod is unchanged.With --format json, the report holds every result under gates, with the status, message and,
for approvals, the approvers found and the stale records per role.
In CI
set -eprs release buildtest -z "$(git status --porcelain -- .promptscript/releases)"prs release status --base refs/remotes/origin/mainBuild writes whatever the committed release files miss, so the test -z check fails when a pull
request changes prompts without the build. It also catches new bundle files, which
git diff --exit-code would not see because they are untracked. status fails when a label points
at a bundle that is missing or does not verify. All four commands accept
--format json, which prints one document on stdout that matches
https://getpromptscript.dev/schema/release-report/v1.json, and prints the human text on stderr.
Check label changes with --base
prs release status --base <ref> compares the label files in the checkout with the labels at
<ref>, usually the branch the pull request goes into. It looks at every label file: the
environments configured at <ref>, the ones configured in the checkout and every other *.json
file in .promptscript/releases/ except manifest.json. A file whose text did not change passes.
A changed file that does not read or parse as a label fails. Every entry that is new or changed
must pass the promotion rules of <ref>:
- The environment is in
releases.environmentsat<ref>. - The bundle file exists and verifies for that prompt id.
- The entry is a rollback: the label at
<ref>holds the entry, and its new hash is the one the label held before its last change at<ref>, the labelprs release rollbackrestores. This also covers agit revertof the last promotion of one environment. An entry that<ref>removed is not brought back this way; adding it again goes through the other rules. - Or, in the first environment, the hash is what the source in the checkout compiles to with the
policies,validationandmodels.supportedof<ref>and the forced security rules, the same compileprs release promoteruns. - Or, in a later environment, the hash is the one the environment before it holds in the checkout.
A rollback skips the promotion order and the compare with the fresh hash. That is safe only when
every commit on <ref> is the result of a pull request this check ran on, see the precondition
below: then the hash passed the rules when it was promoted. The release gates
still run for it, so the hash a rollback restores needs its approvals. A rollback can also restore a hash that the compile
rules at <ref> (validation, the forced security rules, models.supported) reject today; the
gates catch only what the prompt policies check, and review the rest. So when you take a prompt down after a rule
change, do it in two pull requests: remove the entry, then promote the fixed version. Deleting
the old bundle file does not help, a pull request can add it back from history. Reviewers should
look at a rollback like at any other label change. A removed entry ships nothing; it is listed with action: "removed" and not
checked.
Any failure exits with code 1. The config is read at <ref>: the first of promptscript.yaml,
promptscript.yml, .promptscriptrc.yaml and .promptscriptrc.yml that exists in the project
root, the same order the loader uses, with its extends chain read at the same commit. -c and
PROMPTSCRIPT_CONFIG are ignored with --base, with a warning. So a pull request cannot loosen the
rules it is checked with: a new policy, a removed policy, a wider models.supported or a new
environment takes effect after the merge. Add a new environment in a pull request of its own,
before the first label for it.
The rest of the config comes from the checkout: registry, registries, input.entry,
models.profiles (they describe the models and reach the bundle hash), and the lockfile and the
vendor directory. A pull request can change those, so review them with the labels.
<ref> must name one ref. A short name that matches a tag and a branch, for example a tag called
origin/main next to the remote branch, fails with PS5006; pass the full name such as
refs/remotes/origin/main.
In GitHub Actions, run it on pull_request events. Check out the history so the base exists, pass
the base branch through env, not inline in run, and run the CLI version the lockfile pins:
on: pull_request
jobs: releases: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - run: npm ci - run: npx prs release status --base "refs/remotes/origin/$BASE_REF" env: BASE_REF: ${{ github.base_ref }}What --base does not do:
- Entries that did not change are not checked again, even when the rules at
<ref>changed since they were promoted. - Going back further than the last change of a label is no rollback. It needs a new promotion under the promote rules, in the first environment with the source reverted.
- Config files and label files at
<ref>must be regular files. A config that is a link or a directory fails withPS5001; a label that is a link counts as empty, so every entry in the checkout is checked. - A config file that
extendsreaches outside the repository cannot be read at<ref>; the check fails withPS5006. - It trusts
<ref>. Point it at the protected branch the applications read from. - The rollback rule trusts the history of
<ref>: every commit on its first-parent chain must be the result of a pull request this check ran on. With rebase merging, or a fast-forward of a branch with several commits, an unchecked commit lands on that chain, and its label becomes a valid rollback target. Allow only squash or merge commits on that branch, see below. Label history from before the check was required counts as unchecked too: a label whose last change predates the check has an unchecked rollback target until its next change. - In a
pull_requestrun the pull request controls the workflow file,package.jsonand the lockfile, so it can swap the CLI that runs the check. Protect them as below, or run the check as a required workflow from a repository ruleset.
Read a release in your application
The @promptscript/runtime package loads the bundles this flow writes. It has no dependencies. It
is not published to npm yet and its API can still change, so it is in the repository only.
import { createPromptClient, toAnthropicMessages } from '@promptscript/runtime';import { directorySource } from '@promptscript/runtime/node';
const client = createPromptClient({ source: directorySource('.promptscript/releases'), label: 'prod',});
const prompt = await client.get('acme-support/refund-reply');const body = toAnthropicMessages(prompt.render({ message: 'Where is my refund?' }), { maxTokens: 1024,});Every load checks the schema, the structure and the contentHash, and verification cannot be turned
off. render() inserts input values as plain text. See the
runtime README for
the cache, the fail modes and the transport rules of httpSource.
What protects the labels
Run prs release status --base as a required check, and keep review on the release files:
- Add
.promptscript/releases/,promptscript.yaml, every config file it reaches throughextends,.github/workflows/,package.jsonand the lockfile toCODEOWNERS, and require the review.prs governance codeownerswrites these lines for the admins, see CODEOWNERS from Governance; add.github/workflows,package.jsonand the lockfile togovernance.codeowners.protect. - Protect the branch the applications read from with a ruleset: require the
prs release status --basecheck, allow only squash merges or merge commits (no rebase merging, no fast-forward of branches with several commits) and no direct pushes. The rollback rule relies on that. - An older label served from a cache, or a label on a rewritten branch, cannot be detected in this version.
Applications read the label file of their environment and the bundle file it names. Recompute the content hash before use, as described in Application Prompts.
Audit log
prs audit log reads the governance history of the project from Git: who changed a prompt file,
who approved which hash, who promoted, rolled back or removed a label entry, and who changed the
config or the generated CODEOWNERS section. Nothing is stored besides the repository, so the log is as good as
the history on the branch.
prs audit log --prompt acme-support/refund-reply --env prod --hash 850dce44head 4452d509b67930043f5155036a07f5e1f52d9183sha256-850dce44fbd1...2b73 2026-10-22T09:15:40Z promoted 4452d509b679 acme-support/refund-reply env=prod previous=- by Ann Lee <[email protected]>sha256-850dce44fbd1...2b73 2026-10-21T14:02:11Z approved 9ffb3347d0a8 acme-support/refund-reply role=legal approver=@jane (asserted) env=prod state=well-formed .promptscript/approvals/legal/acme-support/refund-reply/sha256-850dce44fbd1...2b73.jane.json by Jane Roe <[email protected]> comment: Refund wording checkedThe hashes are shortened here; the command prints them in full. That is the answer to “who approved
this hash for prod”: the approved line names the approver the record claims and the commit that
added it. The binding reviewer is in the review log of the Git host, for the pull request of that
commit.
Events
Events come newest commit first; inside one commit they follow the path order of Git. Each one
has type, commit, at (committer date), author, committer (name and email as Git has
them), path (relative to the repository top level) and mainline. mainline is true for a
commit on the first-parent chain of HEAD. An event with mainline: false is history of a branch:
its change may never have reached HEAD, or reached it only through a merge.
| Type | When | Extra fields |
|---|---|---|
changed | a .prs file of the project was added, modified or deleted | change (A, M, D) |
approved | an approval record was added | prompt, contentHash, role, environment, approver, approverSource, recordCreatedAt, comment, state |
approval-removed | an approval record was deleted, read from the parent commit | the same |
promoted | a label entry was added or changed | environment, prompt, contentHash, previousContentHash |
rolled-back | a label entry was set to a hash it held before in that label | the same |
label-removed | a label entry was removed, or the label file deleted | environment, prompt, previousContentHash |
merge-dropped | a merge did not keep a change of a parent after the first | parent, change, and the record fields, or environment, prompt, contentHash, droppedContentHash |
config-changed | a config file name of the project, the config or its extends changed | change |
generated | the CODEOWNERS section under this project’s marker changed | none |
A record file changed in place shows as approval-removed with the old content and approved
with the new. state comes from the record bytes and the path alone: well-formed at the path
its fields give, misplaced at another path, invalid when the file is no v1 record in the exact
form prs approve writes, is a link or a submodule, or is larger than 1 MiB (an invalid record
has no record fields). Well-formed does not mean a gate accepts the record: the gate also needs
the role in governance.roles and the scope, and the review by the role team lives in the review
log of the Git host. A misplaced or invalid record never counts in a gate. approverSource is
always asserted.
Commits of merged side branches are read too, each with its own events. A merge commit is read
against its first parent: its events are what it brought to the branch it was merged into, and
they carry viaMerge: true. So a promotion made on a side branch shows twice, once on the side
commit and once on the merge with viaMerge. A merge that takes an older version over from the
side, such as a revoked record or an older label hash, shows that as approved or rolled-back
on the merge. Whether a label entry is rolled-back is decided by the hashes the label held on
the first-parent chain, so a side branch never makes a later promotion look like a rollback.
Side branches a merge discarded, for example with -s ours, still show their own events.
The first-parent diff cannot show what a merge threw away from its other parents. So for every
merge and every parent after the first, the log compares that parent with the merge base and with
the merge result. When the parent changed an approval record, a label entry or the generated
CODEOWNERS section and the merge result does not have that change, the merge gets a
merge-dropped event naming the parent. A revoked record that a merge brings back, or a
promotion it resolves away, shows up this way even when the branch is fast-forwarded later, and
for octopus merges too. For a label the event names the hash the parent had
(droppedContentHash) and the hash the merge kept (contentHash); for a record, change says
what the parent did (A, M or D) and the record fields come from the parent’s version, or
from the merge’s when the parent deleted it.
After reading the history, the command replays the mainline events and compares the result with
the approval records and label entries at HEAD. Any difference fails with PS5108: the history
has a change the log cannot explain, and the log is not printed as if it were complete. Labels
that did not parse at some commit are left out of this check.
The log reads the whole project directory, keeping the .prs files, .promptscript/approvals/,
every .promptscript/releases/<environment>.json label, every config file name in the project
root, and outside the directory the config and its extends files inside the repository, the
files a --prompt prompt is built from and the four CODEOWNERS locations. A label of an
environment that is not in releases.environments is read too, with a warning, and its events
carry configured: false, so a look-alike file such as prodd.json stands apart from prod.
Filters
| Option | Keeps |
|---|---|
--prompt <id> | approval and release events of the prompt, and changed events of the files it is built from in the checkout |
--env <env> | release events of the environment, and approvals for it or for every environment |
--hash <prefix> | events whose contentHash (for label-removed, previousContentHash; for merge-dropped, either hash) starts with these hex digits (sha256- optional, at least 4 digits) |
--since <date> | events committed at or after the ISO date or date and time |
--until <date> | events committed at or before it; a date alone means the end of that day, UTC |
--limit <n> | the newest n events after the other filters, at most 100000 |
An event without the filtered field does not match: with --hash, only approval and release
events remain. --prompt finds the files of a prompt by resolving the checkout, so a prompt file
of an older layout is not matched; an invalid record matches when it lies in the prompt’s
directory under a role. Filters apply after the whole history is read, so rolled-back stays
right inside a time window.
Formats
--format text (the default) prints a head <commit> line, then one line per event with the
content hash first (- for events without one), (asserted) after the approver, and
[via merge], [side branch] or [environment not configured] where they apply.
--format json prints one document that matches
https://getpromptscript.dev/schema/audit-log/v1.json: $schema, version, success, head
(always there on success), filters, events, warnings and errors. C1 control, bidi and
zero-width characters in strings are written as \uXXXX escapes.
--format cef prints one ArcSight CEF line per event for a SIEM:
CEF:0|PromptScript|prs|<cli version>|prs-approved|Prompt approved|5|rt=1792591331000 act=approved [email protected] duser=@jane filePath=... cs1=acme-support/refund-reply cs1Label=prompt cs2=sha256-... cs2Label=contentHash cs3=legal cs3Label=role cs4=prod cs4Label=environment cs5=9ffb3347... cs5Label=commit outcome=well-formed msg=Refund wording checked flexString1=4452d509... flexString1Label=head flexString2=asserted flexString2Label=approverSource| Key | Value |
|---|---|
rt | commit time in milliseconds |
act | event type |
suser | author email |
duser | approver |
filePath | path |
cs1 to cs6 | prompt, contentHash, role, environment, commit, previous contentHash (the parent for merge-dropped) |
outcome | record state, or added, modified, deleted for file changes |
msg | record comment |
flexString1 | head, the commit the log was read from |
flexString2 | approverSource, always asserted, on approval events |
cn1 | 1 with cn1Label=viaMerge on events of a merge |
cn2 | 0 with cn2Label=configured on labels of unconfigured environments |
cn3 | 1 or 0 with cn3Label=mainline |
oldFileHash | droppedContentHash of a merge-dropped label entry |
Severities: changed and generated 3, approved 5, approval-removed, promoted and
config-changed 6, rolled-back, label-removed and merge-dropped 7. Header values escape \ and |; extension values escape \
and =, line breaks become \n, and other control characters are dropped. Text and CEF output
drop control, bidi and zero-width characters from Git and record fields.
Limits
- Author, committer and approver are what the commit and the record say. Git does not check them.
The committer date is set by the committer’s machine too, so
--sinceand--untilfilter on a claim. - A squash merge shows the person who merged, not the authors of the branch.
- The log reads the project directory where it is now. History from before the directory moved is not read: after a move the log starts at the move commit, and approvals, labels and prompt files from before it show up as added there, by the person who moved them.
- The review identity of the Git host is not in Git. Join on the commit to read it.
- Rewriting history changes the log. Block force pushes on the branch, and ship exports to a SIEM
on a schedule;
headin every export tells two exports apart. - The log reads the project directory as a whole. For a project at the repository root that is
every commit of the repository, and a very large monorepo can pass the 256 MiB the command
reads from
git log, which fails withPS5108. - A shallow clone, a missing
git, a timeout or a blob a partial clone would fetch fails withPS5108. In CI check out the full history, for exampleactions/checkoutwithfetch-depth: 0. - Git runs with replace refs and grafts off, so a
git replaceref or a.git/info/graftsfile cannot change what the log shows. - The output holds email addresses. Treat exports like other personal data.
Error codes
| Code | Meaning |
|---|---|
| PS5001 | releases is missing or invalid |
| PS5002 | The environment is not in releases.environments |
| PS5003 | A bundle or prompt is missing, or a hash prefix matches nothing |
| PS5004 | A release file does not verify, or the hash is not the fresh hash |
| PS5005 | The promotion skips the previous environment |
| PS5006 | Git cannot give rollback or --base the ref, commit or file |
| PS5007 | Another release command holds the lock |
| PS5008 | A compile output lies inside .promptscript/releases/ |
prs approve uses these codes:
| Code | Meaning |
|---|---|
| PS5101 | governance.roles is missing or invalid, or the role is not in it |
| PS5102 | The reference is not <id>@sha256-<64 hex> with a valid prompt id |
| PS5103 | The hash is neither what the checkout builds nor a bundle of that prompt |
| PS5104 | --approver is missing or not a user handle like @jane |
| PS5105 | The record exists (exit code 2) |
| PS5108 | --list cannot run Git, or the project is not in a repository |
| PS5109 | A record is invalid, modified, misplaced or has an unknown role, or the record path has a link |
prs audit log uses PS5102 for a --prompt or --hash value of the wrong shape and PS5108
when Git cannot give the history: no repository, no commit, a shallow clone, a timeout, a
missing blob, or a history whose mainline events do not lead to the records and labels at HEAD.