Audience: Everyone Source of truth: each tool's
--describeoutput; generatedtools/CATALOGUE.mdStatus: Live forengine.py,validate_report.py,report.py,render_report.py,facts.py,recipe_lint.py(all nine rules, including the generated catalogue)build_skills.py,progress.py(all commands),check_drift.py, andcompare_reports.py.
| Item | Description | Status |
|---|---|---|
engine.py |
Runs the deterministic engine from any directory | Live |
validate_report.py |
Validates a report against schema and semantic rules | Live |
report.py |
Builds and updates the audit report; the only writer | Live |
render_report.py |
Renders a report to Markdown | Live |
facts.py |
Emits inventory, stack, instruction-file and component facts, no verdicts | Live |
recipe_lint.py |
Checks recipe Markdown for broken links, bad SKILL.md frontmatter, oversize files, missing tools, unknown check, requirement or playbook IDs, and a stale tool catalogue | Live |
build_skills.py |
Builds the agentic-readiness-audit and agentic-readiness-implement skill bundles under dist/skills/ |
Live |
progress.py |
Creates, shows and renders the implement recipe's per-repository state; the only tool that writes inside a target | Live |
check_drift.py |
Lists the audit findings whose cited files changed between the audited commit and HEAD |
Live |
compare_reports.py |
Compares two audit reports: which checks and findings moved, and which moves the readiness state explains | Live |
Every tool follows the tool contract. It prints one JSON envelope on
stdout, uses the exit codes below and answers --describe. Its output is bounded, it uses no network, and it checks
for Python 3.11 first. It writes nothing inside the target; progress.py and the target's two state files are the
one exception. The explanation is in Tool Contract.
| Exit code | Meaning |
|---|---|
0 |
OK |
1 |
Usage or input error, including a Python older than 3.11 |
2 |
The input was read and failed validation |
3 |
Internal error |
Run a tool as python3 audit/tools/<tool>.py: the path depends on where you run it, the tool does not. Below Python 3.11 a tool prints how to run itself with uv run --python 3.12.
Runs the deterministic engine from any directory, writes its JSON report under a name that carries the repository's name, and never replaces a file. The decision to import the engine is ADR 0001; the source is engine.py.
Synopsis
python3 audit/tools/engine.py <target> [--out-dir DIR] [--repo-label LABEL] [--phase N]
[--exclude GLOB]... [--max-files N]
[--run-gates --allow-probe VERB... [--timeout SECONDS]]
[--emit-baseline] [--no-user-config]
python3 audit/tools/engine.py --list-checks
python3 audit/tools/engine.py --describe
Inputs
| Argument | Meaning |
|---|---|
<target> |
The repository to scan. It must exist and be a directory. The engine only reads it. |
--out-dir DIR |
Where the report goes. It is created if it does not exist, and it must be outside the target. Default: the current directory, which is refused when it is inside the target. |
--repo-label LABEL |
The label in the file name: up to 32 of a to z, 0 to 9, dot, underscore and hyphen, starting with a letter or digit. Default: derived from the target's directory name, as in Output files. |
--phase N |
The phase recorded in the report's provenance. The engine implements phase 1 only, so a higher value changes nothing else; the engine's default is 1. |
--exclude GLOB |
An extra path pattern to leave out of the audit, matched against the whole path from the target's root, so vendor-samples/* covers every file under that directory. It applies to the checks that read git's tracked files as well as to the walk. May be repeated. |
--max-files N |
A hard cap on the files the engine inspects. |
--run-gates, --allow-probe VERB, --timeout SECONDS |
Let the engine run the target's own commands. Nothing runs unless --run-gates is given and every verb is named in an --allow-probe. --timeout is the limit per command. |
--emit-baseline |
Forwarded to the engine's own --emit-baseline (D51): also writes do-not-do.draft.md beside the report, the CON-04 remediation draft. |
--no-user-config |
Forwarded to the engine: read no user-level configuration ($HOME, XDG), so the IDX verdicts do not depend on the machine. The report's provenance.user_config says what was read. |
--list-checks |
Print the engine's checks and exit. It takes no other argument. |
The engine's --force, --out, --format, --fail-on and --baseline are not offered, and abbreviations of the options above are refused.
Outputs
A scan writes one file, <label>.engine-report.<stamp>.json, in the output directory, and, with --emit-baseline, do-not-do.draft.md beside it. Both are the engine's own output, untouched. The envelope's data is:
| Key | Meaning |
|---|---|
report |
The path of the file written. |
label, stamp |
The two parts of the name. The stamp is the UTC launch time as YYYYMMDDTHHMMSSZ. |
target |
The target's resolved path. |
baseline_draft |
The draft's path, when --emit-baseline was given; null otherwise. |
engine |
version and ruleset_revision of the engine that ran. |
--list-checks writes nothing. Its data holds engine (name, version, ruleset revision and hash), checks (the engine's list, one entry per check) and omitted, the number of checks left out to stay within the output budget, which is 0 today.
Exit codes
| Code | When |
|---|---|
0 |
The report was written, or the checks were listed. |
1 |
A bad argument, a missing target, an output directory inside the target, a bad label, or a refusal by the engine, whose reason is in data.error. |
3 |
The engine exited with any other code, raised, or reported success without writing the report or (with --emit-baseline) the draft. |
Errors
| Message | Fix |
|---|---|
refusing to write inside the target |
Pass --out-dir with a directory outside the repository, or run from outside it. |
the engine refused the run: --run-gates requires at least one --allow-probe VERB |
Name each verb the engine may run with --allow-probe. |
the engine refused the run: ... exists; nothing was replaced, run the tool again |
Another run took the report's name in the same second, or do-not-do.draft.md already exists in --out-dir from an earlier --emit-baseline run. Run the tool again, or point --out-dir elsewhere. |
repo_label ... is not allowed |
Use a label that matches the pattern above. |
--list-checks takes no other argument |
Run --list-checks on its own. |
Checks an audit report against its schema and against the rules a schema cannot express, which are numbered in SCHEMA.md. It reads the report and writes nothing. The source is validate_report.py, and the schema checker it uses is _jsonschema_subset.py.
Synopsis
python3 audit/tools/validate_report.py <report> [--target <repo>]
python3 audit/tools/validate_report.py --describe
Inputs
| Argument | Meaning |
|---|---|
<report> |
The report to check: a JSON file of at most 32 MiB. |
--target <repo> |
The audited repository. With it the tool also looks for each cited file path and warns about the ones that are missing (rule 12). It asks git whether the commit in provenance.target.git_sha holds the path, or looks in the working tree when no commit is recorded. It never writes to the target. |
Outputs
A valid report gives data with valid (true), report (the path as given), schema_version, status, the number of checks and findings, and target_checked. Warnings are in the envelope's warnings, bounded like every list; when some were left out, truncated is true and data.warnings_omitted says how many. Warnings come only once a report has no errors.
An invalid report gives data.error, which counts the errors and names the stage, and data.errors, one string for each error:
rule 6: /checks/6: TOOL-03 overrides the engine's PARTIAL with PASS and gives no override_reason
schema: /checks/1/verdict: expected one of ["PASS", "PARTIAL", "FAIL", "UNKNOWN", "NOT_APPLICABLE"]
The tool checks in three stages. The first reads schema_version. A report with another version than the schema's gets one plain error that names both versions and what to do: run a new audit (an older report), or update the framework (a newer one). Then it applies the schema, and a report that fails it is returned with its schema errors alone, up to 500. The second applies the rules, only to a report that passed the first, because the rules read fields the schema guarantees. Errors come in rule order, then in the order of the document. An error names the place with a JSON Pointer and never repeats the value it found, so a credential in a quote does not travel into the list.
Exit codes
| Code | When |
|---|---|
0 |
The report is valid. |
1 |
A bad argument, a report that is missing, not a file or over the size limit, or a target that does not exist. |
2 |
The report was read and is not valid: it fails the schema or a rule, or the file is not JSON, repeats a key, holds NaN or Infinity, is not UTF-8, or is nested too deeply. |
3 |
Internal error. |
Errors
| Message | Fix |
|---|---|
rule 1: ... to rule 12: ... |
The rule is in Rules beyond the schema. The message says which item to change. |
rule 10: /summary/...: the report says X and its checks and findings give Y |
The summary is stale. It is never edited by hand: the report builder, report.py, recomputes it on every change, so the report was changed outside the builder. Y is the correct number. |
rule 11: ... it looks like a credential |
Shorten the quote or the output tail, or leave it out. The patterns are the engine's, so a line such as token: abc is flagged even when it is not a secret. |
the report has schema 'X' (made by framework vY); these tools read schema 'Z'. ... |
The report and these tools come from releases with different report schemas. The message says what to do; there is no conversion. |
schema: /path: ... |
The pointer names the field. The schema is audit-report.schema.json. |
the file is not a report: ... |
The file is not one JSON document that this tool accepts. |
Builds the audit report. It is the only writer of the draft: the agent calls a subcommand and never edits the JSON by hand. The draft's path may be a link: the draft it points to is changed, and the link stays a link. Every file it writes is validated first with the checks of validate_report.py, and the readiness summary is computed by the tool, never taken from a caller. The source is report.py.
The subcommands are init, import-engine, add-check, add-finding, set-disposition, set-stack-profile, status and finalise.
Every subcommand but init and status changes the draft it is given, in place. A change is one step. The tool
reads and validates the draft, makes the change, recomputes the summary and validates the result. Only then does it
write the draft, atomically, so a crash or a refusal leaves the previous draft as it was. Commands that change
drafts in one folder take turns, through a lock on the folder that the system releases when the process ends. Run
them one at a time anyway: a command that cannot get the lock in 30 seconds exits with 1. A final report is never
changed.
Synopsis
python3 audit/tools/report.py init <intake>
python3 audit/tools/report.py import-engine <draft> <engine report>
python3 audit/tools/report.py add-check <draft> --from FILE|-
python3 audit/tools/report.py add-finding <draft> --from FILE|-
python3 audit/tools/report.py set-disposition <draft> --via VIA
(--finding ID --disposition DISPOSITION [--note TEXT] | --from FILE|-)
python3 audit/tools/report.py set-stack-profile <draft> <id> [--derived] [--confirmed]
python3 audit/tools/report.py status <draft>
python3 audit/tools/report.py finalise <draft>
python3 audit/tools/report.py --describe
Starts a draft from an intake file. It reads the intake (the answers to the audit's Stage 0 questions, audit-intake.schema.json), captures the target's provenance, and creates <label>.audit-draft.<stamp>.json in the intake's output_dir. A second run on the same repository starts a second draft with a new stamp; the earlier draft stays as it was, and a name that is taken gets -2, -3. The names are defined in Output files.
Inputs
| Argument | Meaning |
|---|---|
<intake> |
The intake file, a JSON file of at most 32 MiB. Relative paths inside it, target_path and output_dir, are relative to the working directory. |
What it records
| Field | Source |
|---|---|
provenance.target.path |
The intake's target_path, resolved to an absolute path. |
provenance.target.git_sha, branch, dirty |
Read from git with read-only commands. The commit id is recorded as its first 12 hex digits, because a full id is what secret scanners read as an access token. Each is null when it cannot be known: the target is not a repository, the repository has no commit, HEAD is detached (no branch), or git is missing. A target that is a subfolder of a repository records nothing and gives a warning, because a commit id says nothing about a subfolder. |
provenance.target.label |
The intake's repo_label, or a label derived from the target directory's name. |
provenance.framework |
The release (FRAMEWORK_RELEASE in report.py), the framework checkout's commit (12 hex digits, or null outside a checkout) and the install: checkout, bundle or copy. |
provenance.catalogue_version |
The version of catalogue.json. |
provenance.agent |
The intake's harness and model. |
provenance.started_at |
The UTC start time. Its YYYYMMDDTHHMMSSZ form is the stamp in the file name. |
run_context |
The intake's eval_mode, confirmation_policy, provider_exposure_acknowledged, index (without allow_install), probes (default: nothing allowed, 120 seconds), excluded and known_components. allow_install and output_dir are instructions to the run and are not in the report. |
The draft starts with no checks, no findings and a summary computed from them. The scan facts (stack, components) and the engine's checks arrive with import-engine.
Outputs
data holds draft (the path written), label, target (path, git_sha, branch, dirty) and status (draft). Warnings are in the envelope.
Exit codes
| Code | When |
|---|---|
0 |
The draft was written. |
1 |
A bad argument, an intake that is missing or too large, a target that is missing or not a directory, or an output_dir inside the target, including through a symlink. Nothing is written. |
2 |
The intake fails its schema or its rules, or is not valid JSON. data.errors lists every error. Nothing is written. |
3 |
Internal error. |
Errors
| Message | Fix |
|---|---|
schema: /pointer: ... |
The pointer names the field of the intake that the schema refuses. |
intake rule: /confirmation_policy: 'accept-all' is allowed only with eval_mode set to true |
accept-all confirms every finding without a person, so it is for evaluation runs. Set eval_mode or choose another policy. |
intake rule: /confirmation_policy: an eval-mode run cannot be 'interactive' |
An evaluation run has no person to ask. Use defer-all or accept-all. |
intake rule: /index/name: ... the index needs a name |
Name the index tool, or set index.kind to none. |
target not found: ... |
Fix target_path. |
refusing to write inside the target: ... |
Choose an output_dir outside the repository. |
Brings the engine's report into a draft: its checks and findings, the stack and components it detected, its file counts, the probes it ran and its unattested items. The report is the JSON that engine.py wrote. The rules of the conversion are in Engine schema v2 mapping. The conversion is convert, a pure function of the engine's report, and apply_import applies its result to the draft in one validated step, as every other change is applied.
python3 audit/tools/report.py import-engine <draft> <engine report>
It runs once for a draft. A draft that already holds an engine report is refused, because a second import would replace the first one's checks and the decisions made on them: start a new draft with init to audit again. It also runs for the draft's own repository only: the engine report's target must be the draft's target, and its commit the draft's commit when both are known, otherwise the report is refused. The draft's own target facts are kept, because init read them from the directory.
Inputs
| Argument | Meaning |
|---|---|
<draft> |
The draft to change. A final report is refused. |
<engine report> |
The engine's JSON report, schema v2 and phase 1, as engine.py wrote it: a file of at most 32 MiB. It is only read. |
What it records
| In the draft | From the engine's report |
|---|---|
provenance.engine |
The engine's name, version, ruleset revision and hash, and the scan time. |
checks[] |
Each engine check with method: automated and the engine's verdict in both verdict and engine_verdict, after any check the agent has already recorded. A check the engine left out as not applicable has the verdict NOT_APPLICABLE. |
checks[].evidence |
The evidence of the check's findings, gathered, then the probes the engine ran for it, as command evidence. |
findings[] |
Each engine finding, with a key (the engine's id, and :2, :3 after a repeat) and an id one past the highest in use. It is pending, or confirmed by policy under accept-all. |
unattested[] |
The engine's five fixed items as the requirements they stand for, in the engine's words, and each requirement of a check the engine marks ATTEST. |
stack, components, coverage.inventory |
The engine's own. A path that looks like a component and that nothing declares is a component with declared: false. |
An engine evidence path is not always a path. A placeholder ((no example file), <repository>) names no file and is dropped. A path outside the repository (the index store that IDX-02 cites) is dropped with a warning. package.json:lint becomes the file package.json with the note lint, and the note does not say what it means, because the engine writes that form for a key that is set and for one that is not. locate holds the rules.
Outputs
data holds these keys:
draft, andengine_report(the path read);engine: what was recorded inprovenance.engine;checks_imported, andby_verdict: a count for each verdict, includingNOT_APPLICABLE;findings_imported,unattested_listedandprobes_carried;evidence_not_carried: the evidence items dropped with a warning;checks_recorded,checks_expected,findingsandfindings_pending: the counts of the draft as it now is.
When the warnings are too many for the envelope, warnings_omitted says how many were left out, and truncated is
true.
Nothing the engine said is dropped without a warning, except a placeholder and a probe the engine refused to run (the check's own summary says why).
| Warning | Meaning |
|---|---|
<ID>: the evidence path 'P' is not carried: the path is absolute, so it is outside the repository |
The engine cited a place outside the repository. The finding's statement says where the engine looked. |
<ID>: the <field> is N characters and the report allows M, so it was cut |
A text over the report schema's limit was cut, and ends in …. |
<ID>: N evidence items, and the report allows M, so the rest are left out |
A list over its limit kept its first items. |
probe <verb>: ... |
A probe that ran has no known check, no command, or output over the limit. A probe whose check the report does not hold says probe: <ID> ran it, and the report holds no such check. |
the engine's scan was cut short, so what it did not reach is not in the report |
The engine's inventory.truncated is true. |
the engine's phase-1 score is X and the report's is Y; ... |
The score the engine computed and the score the report computes from its checks differ. The report's is kept. |
only the engine knows the target's commit, so the draft's cannot be compared |
One side recorded no commit, so the two could not be compared. The message says which side has one. |
the engine lists the unattested item 'X', which maps to no requirement, so it is not carried |
The engine lists an item the adapter has no requirement for. |
/checks/N/evidence/M: the file P is not in the target at C |
The validator's rule 12, for the imported checks, when the target is on this machine. |
Exit codes
| Code | When |
|---|---|
0 |
The engine's report was imported. |
1 |
A bad argument; the draft or the engine report is missing, a directory or too large; the draft is a final report; the draft's folder is inside the target; the lock could not be taken; the file could not be written. |
2 |
The engine report is not valid JSON, is not schema v2 for phase 1, lacks a field the mapping reads, holds a check the catalogue lacks or only an agent can judge, or is of another repository or commit than the draft's; the draft already holds an engine report; the draft was already invalid; or the change would leave it invalid (data.errors has the rule). Nothing is written. |
3 |
Internal error. |
Errors
| Message | Fix |
|---|---|
the engine report has schema_version X, and the adapter reads only '2' |
Scan again with engine.py. |
the engine report is for phase N, and an audit report covers phase 1 |
Scan again without --phase. |
the engine report has the wrong shape: N errors |
Each error starts engine report: /pointer: and names the field. The file is not an engine report, or was edited. |
the engine report cannot be imported: N problems |
A check is not in catalogue.json, or only an agent can judge it, or appears twice, or a finding names a check the report does not hold. The engine and the catalogue are from different revisions of the framework. |
the engine report is not of this draft's target |
data.errors names both directories, or both commits. Scan the draft's own repository at its commit, or start a new draft with init. |
this draft already holds an engine report (ruleset H) |
Start a new draft with init. |
the draft already records X |
The draft holds a check the engine reports. Start a new draft. |
the change would leave the draft invalid, so nothing was written |
data.errors names the rule and the place, for example an engine finding more severe than its check (rule 7). |
add-check and the subcommands after it take a fragment: a JSON object with the fields the agent decides, from a file (--from FILE) or from stdin (--from -, for example with a here-document). The tool fills in everything it can compute, so a fragment holds nothing it could get wrong.
- A field that the tool computes is refused as an unknown field, with its place:
fragment: /: unexpected property: 'title'. - Evidence is the agent's. Leave out
captured_by,toolandtool_version; the tool writescaptured_by: agent. A fragment that claims a tool captured something is refused, because the report must never show a deterministic source for text a model wrote. - A fragment fails as a whole: every problem is listed in
data.errors, each starting withfragment:and a JSON Pointer into the fragment.
A fragment that is valid on its own can still leave the draft invalid, for example by citing a path that leaves the target. The tool then refuses it with the rule that fails (rule 3: ...) and writes nothing.
Records the result for one check.
python3 audit/tools/report.py add-check <draft> --from -
{
"id": "AGT-11",
"verdict": "PASS",
"confidence": "high",
"summary": "AGENTS.md states a release rule that the code does not reveal.",
"evidence": [{ "kind": "file", "path": "AGENTS.md", "lines": "14-15", "quote": "Never run npm publish by hand." }],
"searches": []
}| Fragment field | Meaning |
|---|---|
id, verdict, summary |
Required. The check's ID in catalogue.json, the verdict, and one short sentence. |
confidence, evidence, searches |
Optional. As in the report schema. |
override_reason |
Only when disputing a verdict the engine gave; see below. |
The tool adds the check's title, phase, severity, scored and method from the catalogue, so the agent cannot misstate them. The check is added at the end of checks; recording a check again replaces its verdict, summary, confidence, evidence and searches in place.
A check only the engine can run. add-check cannot add an automated check, whose verdict comes from the engine (the checks arrive with import-engine). It can dispute one the engine already ran: the engine's verdict stays in engine_verdict, with the engine's evidence and data, and the agent's verdict, summary, override_reason and agent-captured evidence are recorded beside them. A dispute needs an override_reason and at least one evidence item (rule 6). Disputing the same check again replaces the agent's earlier evidence. A fragment whose verdict equals the engine's clears an earlier dispute (and has no override_reason). A check whose finding is still pending cannot be disputed to PASS: reject the finding first (rule 7).
Outputs. data holds draft, check, action (added, replaced, overridden or agreed), verdict, engine_verdict (for an engine check), checks_recorded and checks_expected (the phase-1 checks of the catalogue). Warnings list each cited file that is not in the target at the recorded commit, for this check only (rule 12).
Exit codes
| Code | When |
|---|---|
0 |
The check was recorded. |
1 |
A bad argument; the draft or the fragment file is missing or too large; the draft is a final report; the draft's folder is inside the target; the lock could not be taken; the file could not be written. |
2 |
The fragment is not valid JSON or fails its schema; the check is unknown, or automated and not yet in the draft; the draft was already invalid; or the change would leave it invalid (data.errors has the rule). Nothing is written. |
3 |
Internal error. |
Errors
| Message | Fix |
|---|---|
fragment: /pointer: ... |
Fix the field the pointer names. |
fragment: /evidence/N: the agent cannot record tool-captured evidence ... |
Leave out captured_by, tool and tool_version. |
<ID> is an automated check, so its verdict comes from the engine ... |
Import the engine's report first; then dispute the verdict if the evidence says it is wrong. |
the draft is not valid before the change |
The draft was edited by hand or by another tool. Fix it, or start a new draft with init. |
the change would leave the draft invalid, so nothing was written |
data.errors names the rule and the place. Change the fragment. |
rule 5: ... is an agent check with the verdict PASS and has no evidence |
Cite what shows the check passes, or record FAIL or UNKNOWN and say what was missing. |
another report command has held the lock on ... |
Another report.py command is still running on a draft in the same folder. Wait for it, and run commands one at a time. |
Records an issue that a check raised.
python3 audit/tools/report.py add-finding <draft> --from -
{
"check": "SEC-02",
"scope": "src/config/constants.ts",
"statement": "An agent cannot tell a credential from a placeholder because a live-looking key sits in constants.ts.",
"evidence": [{ "kind": "file", "path": "src/config/constants.ts", "lines": "12" }],
"remediation": { "summary": "Move the key to the environment and rotate it.", "playbook": "env-inventory-and-secrets", "effort": "S" }
}| Fragment field | Meaning |
|---|---|
check, statement, remediation |
Required. The check the finding belongs to, which must already be in the draft; the statement, always "An agent cannot X because Y."; and the remediation (summary, and optionally playbook, effort, depends_on and touches). A playbook must be one the catalogue names. |
scope |
Optional. A path or component that tells two findings of one check apart. |
severity |
Optional. The check's severity by default. It may be lower, never higher. |
evidence, searches |
Optional. As for add-check. |
The tool computes what the agent must not write:
keyis the check ID, then:and the scope when there is one:SEC-02:src/config/constants.ts. A re-audit matches findings by key, so the tool writes the scope in one spelling: backslashes become/,./and//and..segments are resolved, and a scope of.means the whole check. A scope that is absolute, climbs out of the target, is empty or holds whitespace is refused.idis the nextF-nnnafter the highest in the draft. It is for display, and it is never reused.dispositionispendinguntil a person decides. Under theaccept-allpolicy (evaluation runs only) it isconfirmed, withconfirmed_via: policy.
Recording a finding again with the same key replaces it in place and keeps its id, as long as it is pending. A finding that has a decision is not the agent's to rewrite: the command exits 2 and names it. A finding on a check whose verdict is PASS is refused, because a passing check has no issue to raise (rule 7).
Outputs. data holds draft, finding (the id), key, action (added or replaced), check, severity, disposition, findings and findings_pending. Warnings list each cited file that is not in the target at the recorded commit, for this finding only.
Exit codes are those of add-check. A check that is not in the draft, a scope that is not usable and a finding that already has a decision are exit 2.
Errors
| Message | Fix |
|---|---|
<ID> is not a check in this draft |
Record the check first with add-check, or import the engine's report. |
fragment: /scope: ... |
Use a path inside the target, or a component name without whitespace; or leave scope out. |
<F-id> (<key>) already has a decision, so the agent cannot rewrite it |
A person decided. Set the finding back to pending with set-disposition to record it again, or give the new finding another scope. |
rule 7: ... is higher than <ID>'s <severity> |
A finding is never more severe than its check. |
rule 7: ... is PASS, so its finding must be rejected |
The check passes, so it raises no finding. Record the check as failing first, if it fails. |
rule 2: ... the catalogue names no playbook ... |
Use a playbook named in catalogue.json, or leave playbook out. |
rule 1: ... is not a finding in this report |
A depends_on entry must be the id of a finding already recorded. |
Records what a person decided about findings, and how the decision was made. The agent asks the person and then calls this command; the tool never decides. It changes one finding or a list in a single write: if any decision in a list is refused, none is applied.
python3 audit/tools/report.py set-disposition <draft> --via interactive \
--finding F-003 --disposition rejected --note "Placeholders in a unit test."
python3 audit/tools/report.py set-disposition <draft> --via batch --from -
A list is a JSON array, each item with finding and disposition and, optionally, a note:
[
{ "finding": "F-004", "disposition": "confirmed" },
{ "finding": "F-005", "disposition": "rejected", "note": "Not an issue here." }
]| Argument | Meaning |
|---|---|
--via |
How the decision was made: interactive (the person answered for this finding), batch (the person answered for a group, which cannot include a BLOCKER) or policy (the run's accept-all policy decided; evaluation runs only). It is recorded as confirmed_via. Not needed to take a decision back. |
--finding, --disposition, --note |
One decision: the finding's id, pending, confirmed, rejected, deferred or edited, and the person's reason. |
--from FILE, or --from - for stdin |
A list of decisions, instead of --finding, --disposition and --note. |
pending takes a decision back: the finding loses its confirmed_via and its note. Setting any other disposition replaces the old note with the new one, or with none. The tool does not edit a finding's text. To change what a decided finding says, set it back to pending, record it again with add-finding, and ask the person again.
The tool refuses a decision that would claim something false about who made it:
| Refused | Because |
|---|---|
--via batch for a finding of severity BLOCKER |
A person decides on a blocker alone; a final report needs confirmed_via: interactive for each (rule 8). |
--via interactive or batch in an eval-mode run |
An evaluation run has no person. |
--via policy unless the policy is accept-all |
Only that policy decides anything. Under defer-all every finding stays pending until a person decides later, with --via interactive. |
A decision on a finding of a passing check that is not rejected |
A passing check has no standing finding (rule 7). |
Outputs. data holds draft, updated (the number of decisions), by_disposition (a count for each), findings, findings_pending and blockers_pending.
Exit codes
| Code | When |
|---|---|
0 |
The decisions were recorded. |
1 |
A bad argument: no decision given, both --from and --finding, a missing --via, a value outside the choices; or the draft or file is missing, the draft is final, or the file could not be written. |
2 |
A list that fails its schema; a finding that is not in the draft or appears twice; a refused decision; a draft that was invalid or would be. Nothing is written. |
3 |
Internal error. |
Errors
| Message | Fix |
|---|---|
--via is required when a finding gets a decision |
Say how the decision was made. |
<F-id> is a BLOCKER, so a person decides on it alone: use --via interactive |
Ask the person about the blocker, and call the command for it on its own. |
this is an eval-mode run, which has no person to decide: only --via policy is allowed |
An evaluation run's findings are decided by the policy, or stay pending. |
--via policy needs the 'accept-all' confirmation policy ... |
Use --via interactive or batch, for a person's decision. |
<F-id> is not a finding in this draft |
Use an id from status. |
fragment: /N/... |
Fix the item the pointer names. |
Records run_context.stack_profile in a draft: which stack profile the audit used, whether it shipped or was
derived, and whether the user confirmed the choice (stacks/README.md).
Audit Stage 1 calls it once the user has agreed the stack. A second call replaces the first. The eval scorer
(tools/eval/score.py) reports the field as stack_profile beside its metrics, informational and never scored; an
eval-mode run records it the same way.
| Argument | Meaning |
|---|---|
<id> |
A shipped profile id, one of java, python, typescript-node (the files in stacks/; a test holds the list and the folder together). With --derived, any id of lower-case letters, digits and hyphens, up to 41 characters. |
--derived |
No profile ships for this stack, so guidance was derived from the check files. Refused for a shipped id. |
--confirmed |
The user confirmed the choice. Without it confirmed_by_user is false. |
Outputs. data holds the recorded id, source (shipped or derived) and confirmed_by_user, and the draft's path.
| Error | What to do |
|---|---|
no shipped stack profile 'X'; the shipped ones are ... |
Use a shipped id, or pass --derived to name a stack with no profile. |
X is a shipped profile; drop --derived |
A shipped profile is never "derived". |
Says where an audit stands: which families of checks are done, which are not, and which findings still wait for a decision. It is what to run first when a session resumes after a crash or after the agent's context ran out, because the draft holds everything and the tool reads it. It reads the draft and writes nothing, and it does not wait for the lock.
python3 audit/tools/report.py status <draft>
The draft may be final: status then reports it and says it is already final. A draft that fails validation is exit 2, with the errors.
Outputs. The envelope is at most 40 lines on stdout, so a report of 70 checks costs a few hundred tokens and not a page. data holds:
| Key | Meaning |
|---|---|
draft, status |
The path as given, and draft or final. |
eval_mode, confirmation_policy |
The run's answers, which say who may decide. |
checks_recorded, checks_expected |
The checks in the draft, and the phase-1 checks of the catalogue. |
families_done |
The families whose checks are all recorded, in the order the audit works through them (the catalogue's). |
families_remaining |
For each family with a check not yet recorded, the IDs of those checks. |
findings, findings_pending, blockers_pending |
The findings, those waiting for a decision, and the BLOCKER ones among them. |
ready_to_finalise, blocking |
Whether finalise has nothing to stop it, and if not, why in a sentence: an eval-mode run, checks not recorded, findings pending, a BLOCKER not decided interactively, or a report that is already final. finalise has the last word: it validates the final report. |
pending |
The findings waiting for a decision, one line each as <id> <severity> <key>, blockers first, as many as fit in the line budget (at most 8). |
pending_omitted |
How many pending findings are not listed, when some are left out. The envelope then has truncated: true. |
Exit codes
| Code | When |
|---|---|
0 |
The state was reported. |
1 |
A bad argument, or the draft is missing or too large. |
2 |
The draft is not valid JSON, or fails its schema or a rule: it was edited by hand or by another tool. |
3 |
Internal error. |
Writes the final report from a finished draft. It checks that nothing stands in the way, validates the result, renders its Markdown with render_report.py, and creates <label>.audit-report.<stamp>.json and <label>.audit-report.<stamp>.md beside the draft. The two files share one name apart from the extension: if either name is taken, both take -2 after the stamp. The draft is left exactly as it was, and no earlier file is touched: a second finalise writes a second pair.
python3 audit/tools/report.py finalise <draft>
The final report is the draft with status: final and provenance.finalised_at, and nothing else. The stamp in the name is the finalised_at time. The Markdown is the rendering of that report, made before anything is written, so a failure leaves no file of either kind. A final report is never changed afterwards: every other subcommand refuses it.
A draft can be finalised only when all of this holds, and each problem that does not is one line in data.errors:
| Condition | Why |
|---|---|
| The run is not an eval-mode run. | An evaluation run has no person to decide, so its report is never final. |
| Every phase-1 check of the catalogue is recorded, in every family. | A final report that leaves checks out would show a score over fewer checks than the audit has, with nothing to say so. A check that could not be evaluated is recorded as UNKNOWN with a summary that says why. |
No finding is pending. |
Each finding has been confirmed, rejected, deferred or edited by a person. |
Every BLOCKER finding was decided with --via interactive. |
A person decides on a blocker alone (rule 8). |
The final report passes validate_report.py. |
The same schema and rules, applied to the file before it is written. |
status reports the same conditions as ready_to_finalise and blocking, without writing anything.
Outputs. data holds report (the path of the new JSON file), markdown (the path of the Markdown file beside it), draft, status (final), finalised_at, checks, findings and summary (the readiness summary: score, scored checks, blockers remaining and the two qualifiers). Warnings list each cited file that is not in the target at the recorded commit, for the whole report, bounded.
Exit codes
| Code | When |
|---|---|
0 |
The final report and its Markdown were written. |
1 |
A bad argument; the draft is missing or too large, or is already a final report; its folder is inside the target; the lock could not be taken; the file could not be written. Nothing is written, and no partial file of either kind is left. |
2 |
The draft cannot be finalised (data.errors lists every problem), the draft was already invalid, or the final report would be. Nothing is written. |
3 |
Internal error. |
Errors
| Message | Fix |
|---|---|
the draft cannot be finalised: N problems |
Read data.errors: record the checks it lists, ask the person about the findings it lists, then run status and finalise again. |
checks not recorded in <FAMILY>: ... |
Record each check: add-check for an agent check, import-engine for the engine's. |
N findings still pending: F-001 ... |
Ask the person, and record the answers with set-disposition. The list shows the first 20. |
BLOCKER findings not decided interactively: ... |
Ask the person about each blocker, and record it with --via interactive. |
this is an eval-mode run, and its report is never final |
An evaluation run writes drafts only. |
the final report would not be valid, so nothing was written |
data.errors names the rule. This is a defect in the draft's state that status could not see, or a bug in the tool: report it. |
Renders an audit report to Markdown. The Markdown is a pure function of the JSON report: one report gives the same
bytes every time, whatever order its keys come in. Nothing in the Markdown is absent from the JSON, apart from the
words that explain a verdict, a severity or a decision, and the fixed order of the first actions. The readiness
summary is computed with _summary.py, the code that the validator and the report
builder use, and never taken from the file. The source is render_report.py.
report.py finalise calls it to write the Markdown beside the final report, and it is a command of its
own for a draft, or for a report written earlier.
Synopsis
python3 audit/tools/render_report.py <report> --out <md>
python3 audit/tools/render_report.py --describe
Inputs
| Argument | Meaning |
|---|---|
<report> |
An audit report, draft or final: a JSON file of at most 32 MiB. It must be valid. The tool applies the checks of validate_report.py and renders nothing when one fails, because it reads fields that only a valid report guarantees. |
--out <md> |
The Markdown file to create. It must not exist, its folder must, and the folder must be outside the audited target. The tool never replaces a file. |
What the Markdown holds
| Section | What it shows |
|---|---|
| Title and banners | The repository's name. A warning banner for an evaluation run, a note banner for a draft that counts the findings awaiting a decision, and a note that the audit reports and does not gate. |
| Bottom line | The answer first, after the title and banners. The blockers remaining, from the readiness summary. The checks that are UNKNOWN (up to five IDs, then a count) and the number of unattested requirements. Then the first three actions, in a table: open findings (not rejected or deferred) ranked by severity, then a finding that waits on an open depends_on finding after one that does not, then effort (S, M, L, none), then the check and finding IDs. Each action gives the check, the statement and the fix, and says when it awaits a decision. |
| Provenance | A field and value table, straight after the bottom line: the target, the commit (a bold flag for a dirty working tree), the status, when the audit started and finished, the recipe and catalogue, the engine and ruleset, the framework revision, the agent (self-reported) and the probes. |
| Readiness summary | The headline of spec/readiness-summary.md and no other number: the phase-1 score to two places, the blockers remaining by state, and the unattested requirements, then the two qualifiers. |
| Checks by verdict and severity | How many checks have each verdict and what each earns, and for each severity its weight, its scored checks and how many are not met. |
| Checks that need attention | A table for each severity, with the columns Check, Verdict, What we found, Evidence, Fix and Decision. A row is a finding, or a check that has none. A check is listed when it is not met, was overridden, was judged by the agent, or raised a finding. |
| Overrides of the engine's verdicts | Each check whose verdict differs from the engine's, with both verdicts and the reason. |
| Evidence in full | Each quote and each command's output, in a fenced block, under the check that cites it. Left out when nothing is quoted. |
| Coverage, Stack and components, Run context | What the audit examined and skipped, what was detected, and the intake answers the run used. |
| Appendix — all checks | Every check, grouped by family: its severity, method, whether it is scored, its verdict, the engine's verdict and its summary. |
| Terms | What each verdict, each severity and each decision on a finding means, in the words the sections above use. |
In the tables of checks that need attention, evidence is grouped by who captured it, so each item says whether a
tool or the agent recorded it. A tool is named only when it is not the engine that the provenance names. A cell
shows at most eight items for each group and counts the rest, which are in the JSON report. A quote or output is not
in a cell: the cell says where it is. A check that is FAIL or UNKNOWN and shows no evidence and no search, on
the check or on any of its findings, has "We did not find any evidence." in its Evidence cell. A later finding of a
check whose fix is the same as an earlier one's says "Same as" and the finding, instead of printing it again. A
passing check that a tool settled is in the appendix and nowhere else, and the engine's structured payloads are in
the JSON report.
The words of a report come from the audited repository, through a model, so the tool treats them as untrusted. Free text has every Markdown special character escaped, so that it cannot become a link, an image, a tag or a heading. A path or a command is in a code span, and a quote or an output tail in a fenced block, each with a delimiter longer than any run of backticks inside it. A control character, and a character that reorders or hides text, is written out as {U+XXXX}. A | cannot add a column to a table.
Outputs
data holds markdown (the path written), report (the path as given), status (draft or final), lines and bytes. Warnings are the validator's, bounded like every list.
Exit codes
| Code | When |
|---|---|
0 |
The Markdown was written. |
1 |
A bad argument, a report that is missing, not a file or over the size limit, an --out that exists or has no folder, a folder inside the target, or a file that could not be written. Nothing is written, and no partial file is left. |
2 |
The report was read and is not valid: it fails the schema or a rule, or the file is not JSON. data.errors lists every error. Nothing is written. |
3 |
Internal error. |
Errors
| Message | Fix |
|---|---|
the report is not valid, so it was not rendered: N errors in the S stage |
Run validate_report.py on the report and fix what it names. A report that report.py wrote is valid, so this means it was edited by hand. |
<path> already exists, and a tool never replaces a file |
Choose another --out. To refresh a golden file on purpose, delete it first and read the diff. |
the folder for --out does not exist: <folder> |
Create the folder, or choose one that exists. The tool creates files and never folders. |
refusing to write inside the target: <folder> |
Choose a folder outside the audited repository. The tools never write inside it. |
cannot write <path>: <reason> |
The system's reason, such as a full disk or a missing permission. No partial file is left. |
Describes a repository with counts and names and no verdicts, in a file the audit recipe reads before its assessment
stage. The file holds the inventory, the stack, the instruction files, the components and a size class. The tool
takes them with the engine's own scan, stack, components, target and rules/variants modules (ADR
0001). It gives no verdict, and nothing in it says
that a file is good, that a stack is right or that a repository is ready. The source is
facts.py; the format is facts.schema.json,
described in Facts file.
Synopsis
python3 audit/tools/facts.py <target> --out FILE [--repo-label LABEL] [--exclude GLOB]... [--max-files N]
python3 audit/tools/facts.py --describe
Inputs
| Argument | Meaning |
|---|---|
<target> |
The repository to describe. It must exist and be a directory. The tool only reads it. |
--out FILE |
The file to create. It must not exist, its folder must, and the folder must be outside the target. The tool never replaces a file. |
--repo-label LABEL |
The label in the file: up to 32 of a to z, 0 to 9, dot, underscore and hyphen, starting with a letter or digit. Default: derived from the target's directory name, as in Output files. |
--exclude GLOB |
An extra path pattern to leave out of the inventory, spelled as engine.py's is, so the facts describe the inventory the engine sees. Up to 10 are named in the file; the rest still apply. May be repeated. |
--max-files N |
A cap on the files the walk inspects. Default: the engine's own cap. A walk the cap stops is truncated, which pushes the size class to huge whatever the count. |
Outputs
A run writes one file, the one --out names, in facts.schema.json's format: facts_version, label, git (sha, dirty), inventory, stack, instruction_files, components, size and caps, every section required. The file holds no time, no absolute path and no machine or user name, so one repository state gives one file. data holds file (the path written), bytes and facts_version. A walk --max-files cut short gives a warning, and every count in the file is then a lower bound.
Exit codes
| Code | When |
|---|---|
0 |
The file was written. |
1 |
A bad argument, a missing or non-directory target, a filesystem root, a bad --out or label, or a write the system refused. |
3 |
The facts do not fit facts.schema.json, or are over the 30 KB bound: a bug in this tool. Nothing is written. |
Errors
| Message | Fix |
|---|---|
target not found: ..., target is not a directory: ..., refusing to scan a filesystem root |
Point <target> at an existing repository directory. |
the folder for --out does not exist: ... |
Create the folder, or choose one that exists. |
refusing to write inside the target: ... |
Choose a folder outside the audited repository. |
<path> already exists, and a tool never replaces a file |
Choose another --out. To refresh a golden file on purpose, delete it first and read the diff. |
cannot write <path>: <reason> |
The system's reason, such as a full disk or a missing permission. |
repo_label ... is not allowed |
Use a label that matches the pattern above. |
--max-files must be at least 1, not ... |
Pass a whole number of at least 1. |
the facts do not fit facts.schema.json, which is a bug in the tool |
Report it; data.errors names each place. Nothing written. |
Checks the Markdown under audit/, implement/ and docs/, so that a recipe does not silently rot:
- every relative link and anchor resolves;
- a
SKILL.md's frontmatter is valid; - each recipe's
SKILL.mdlinks the shared promptaudit/prompts/talking-to-the-user.md; - a
SKILL.mdor a check-family file (audit/checks/NN-*.md) stays inside its 500-line budget (R-10); - every
audit/tools/<name>.pyorimplement/tools/<name>.pythe Markdown names exists and answers--describe; - every check, requirement and playbook ID named is one that
catalogue.jsonorspec/readiness-requirements.mdhas; - the generated
tools/CATALOGUE.mdmatches what every tool's own--describewould produce today.
The link rule also covers the framework pages outside those folders: the root README.md, spec/ and
greenfield/. The framework pages under docs/framework/ are already recipe Markdown. No other rule reads those
pages. The greenfield/templates/ links are examples relative to the destination, and are skipped. The tool only
reads, apart from running each tool's own --describe, which the contract guarantees does no work. It writes
nothing unless --write-catalogue is given. The source is recipe_lint.py.
Synopsis
python3 dev/recipe_lint.py <root>
python3 dev/recipe_lint.py <root> --write-catalogue
Inputs
| Argument | Meaning |
|---|---|
<root> |
The repository that holds audit/ (and, beside it, implement/ and docs/). It must exist and be a directory. A link may resolve anywhere under it, even outside those folders (a check file citing docs/framework/phased-approach.md does); a link that resolves outside <root> is an error. |
--write-catalogue |
Regenerate audit/tools/CATALOGUE.md from every tool's own --describe output and exit; none of the other rules run in this mode. |
Outputs
Nothing is written by a plain recipe_lint.py <root>; a clean run's data holds root and files, the count of Markdown files checked. --write-catalogue atomically replaces audit/tools/CATALOGUE.md (the second documented exception to "a tool never replaces a file", alongside the draft report; tools/README.md rule 7) and its data holds file, bytes and tools, the count of tools described.
Exit codes
| Code | When |
|---|---|
0 |
Every file is clean, or (with --write-catalogue) the catalogue was written. |
1 |
A bad argument, or <root> has no audit/ directory. |
2 |
A link does not resolve, an anchor matches nothing, a SKILL.md's frontmatter is invalid, a file is over its line budget, a named tool is missing or fails --describe, a named check, requirement or playbook ID does not exist, or tools/CATALOGUE.md is missing, stale or could not be generated. data.errors lists every one; the markdown rules give path:line: message. |
Errors
| Message | Fix |
|---|---|
<root> has no audit/ directory |
Point <root> at the repository root, not at audit/ itself. |
the link '...' does not resolve: no such file or directory |
Fix the path, or remove the link. A placeholder link under docs/templates/ is exempt. |
the link '...' leaves <root> |
A relative link climbed out of the repository; point it at a path inside <root>. |
the anchor #... in '...' matches no heading |
The target file's heading changed, or the anchor was typed wrong; GitHub's slug is lower case with one hyphen per removed space. |
'...': the anchor #... is out of range |
A #L<n>-L<n> line-range anchor outgrew the file it points at; update the range. |
a SKILL.md must open with '---' frontmatter and a closing '---' |
Add the frontmatter block. |
name '...' must be lowercase letters, digits and hyphens, at most 64 characters |
Fix name to match the pattern. |
name '...' must match its folder, '...' |
Rename name or the folder so they agree. |
description is N characters; at most 1024 are allowed |
Shorten description. |
<file> has N lines; the budget is 500 |
Split the file, or move content on demand into a file it links to. |
the tool audit/tools/<name>.py does not exist |
Fix the name, or write the tool; mark the line TBD: if it is only planned. |
audit/tools/<name>.py --describe exited N: ... |
--describe must not run any work; fix the tool so it only prints its manifest. |
audit/tools/<name>.py --describe did not finish within Ns |
The tool hung or is slow; fix it, or raise the budget in recipe_lint.py. |
audit/tools/<name>.py --describe printed no valid JSON envelope on stdout |
The tool wrote something other than the envelope to stdout. |
audit/tools/<name>.py --describe printed ok: False |
--describe must always succeed; fix the tool. |
the check '...' is not in catalogue.json |
Fix the ID, or add the check to catalogue.json. Never checked inside audit/engine/, which has its own registry. |
the requirement '...' is not in spec/readiness-requirements.md |
Fix the ID, or add the requirement to the spec. |
the catalogue names no playbook '...' |
Fix the name, or add it to a check's playbooks in catalogue.json; the file under implement/playbooks/ may not exist yet (schema/SCHEMA.md, rule 2). |
audit/tools/CATALOGUE.md is missing; regenerate it with 'recipe_lint.py <root> --write-catalogue' |
Run --write-catalogue and commit the result. |
audit/tools/CATALOGUE.md is stale; regenerate it with 'recipe_lint.py <root> --write-catalogue' |
A tool's manifest changed since the file was last generated; run --write-catalogue and commit the result. |
N tool(s) could not be described (with --write-catalogue) |
Fix the tool named in each error so --describe succeeds, then try again; nothing is written while any tool fails. |
Builds the two skill bundles under dev/dist/skills/ (gitignored), self-contained copies that a harness installs and
runs with no checkout of this repository. The source is build_skills.py. Each
bundle is replaced whole on every run, so a stale file from an earlier build can never survive. Each has three parts:
-
SKILL.md: the recipe's own file, with its frontmatternamerewritten from the source folder's name to the bundle's own name; -
references/: read by the model; -
scripts/: run by it. -
agentic-readiness-audit:references/holds the check files, the stack profiles and the schema docs. It also holds the shared prompt on how to talk to the user and the sections of this page for the tools it carries.scripts/holds the tools the recipe calls. Beside them are the schema files and thecatalogue.jsonthey read at a fixed path. The engine'sauditpackage sits one folder level up, asengine.pyexpects.audit.pyzis the engine's single-file build. Its Markdown files are rewritten the same way as the implement bundle's, below. -
agentic-readiness-implement:references/holds the state contract, the playbooks, the kickoff prompt and the shared prompt on how to talk to the user. It also holds the check files, the stack profiles, ADR 0003,spec/command-surface.mdand the spec decisions, the state schema and its docs, and the sections of this page for the tools it carries.scripts/holdsprogress.py,check_drift.py,compare_reports.pyandvalidate_report.pywith their private modules, the schema files,catalogue.json, and anengine.pyshim. The engine is not copied. The shim loadsengine.pyfrom theagentic-readiness-auditskill installed in the same skills folder, so both skills run one engine. A tool that needs the engine without it exits 1 and names the missing skill. In both bundles, every carried Markdown file is rewritten in three ways:- a
python3 audit/tools/<tool>.pycommand becomes the path from the skill's own folder (python3 scripts/tools/<tool>.py; in the implement bundle, the audit skill's path forengine.py); - a link to a carried file points at its place in the bundle;
- any other relative link becomes plain text that names the framework path. No link leaves the bundle.
- a
make validate-skills builds both bundles and validates each with
skills-ref, the Agent Skills specification's own reference
validator. It is the PyPI package skills-ref, with the CLI entry point agentskills. It is a dev-only tool, run
through uvx with a pinned version, and never imported by the tools or the engine. An install smoke-run (S4.5.3)
acts on the audit bundle.
Synopsis
python3 dev/build_skills.py [--out-dir DIR]
Inputs
| Argument | Meaning |
|---|---|
--out-dir |
Where to build skills/ itself (default: dev/dist/skills). The bundles always land at <out-dir>/agentic-readiness-audit/ and <out-dir>/agentic-readiness-implement/. |
Outputs
data.bundles lists both bundles, each with name, bundle (its path) and files (how many files it has).
data.bundle and data.files repeat the audit bundle's path and count.
Exit codes
| Code | When |
|---|---|
0 |
The bundle was built. |
1 |
The engine's own audit/engine/build.py failed to produce audit.pyz (the message is audit/engine/build.py failed to build audit.pyz) (data.errors carries its stderr), or a source file lacks the frontmatter name line or a Tools CLI section the builder rewrites. |
progress.py and check_drift.py import their sibling modules (_common, validate_report, _jsonschema_subset),
so run them as python3 audit/tools/<tool>.py, without python3 -I (isolated mode drops the script's
folder from the import path and the import of _common fails).
Creates the implement recipe's state for a target from a final audit report. It records the SHA-256 of the report
file in source_reports[].sha256. It summarises the state, renders its Markdown view, and applies the validated
changes to it: status transitions, skip and defer, preferences and theme progress. It is the one tool that writes
inside a target, and only the two state files, by ADR
0002. The source is
progress.py; the format is
readiness-state.schema.json, described in Readiness
state; the rules are the state contract.
Synopsis
python3 implement/tools/progress.py init --report REPORT --target REPO [--default-branch BRANCH] [--phase PHASE] [--date YYYY-MM-DD] [--framework-rev SHA]
python3 implement/tools/progress.py show --target REPO [--label LABEL] [--json]
python3 implement/tools/progress.py render --target REPO [--label LABEL]
python3 implement/tools/progress.py set-status FINDING STATUS --target REPO --phase PHASE [--commit SHA] [--verification-command CMD --verification-result TEXT] [--debt what=TEXT,count=N[,baseline=FILE]] [--clear-debt] [--reason TEXT] [--note TEXT] [--by owner|agent] [--date YYYY-MM-DD] [--label LABEL] [--render]
python3 implement/tools/progress.py skip|defer FINDING --reason TEXT --by owner --target REPO --phase PHASE [--render]
python3 implement/tools/progress.py unskip|undefer FINDING --by owner --target REPO --phase PHASE [--reason TEXT] [--render]
python3 implement/tools/progress.py pref set KEY VALUE --reason TEXT --by owner --target REPO --phase PHASE [--render]
python3 implement/tools/progress.py pref unset KEY --reason TEXT --by owner --target REPO --phase PHASE [--render]
python3 implement/tools/progress.py theme THEME --status branch_open|pr_open|merged [--branch B] [--pr N] [--merged-sha SHA] --target REPO --phase PHASE [--render]
python3 implement/tools/progress.py --describe
Inputs
| Argument | Meaning |
|---|---|
--report |
init: the final audit report (JSON). It must be inside the target, so the state can point at it, and validate_report.py must accept it. |
--target |
The repository. The state is docs/agentic-readiness/<label>-agentic-readiness-state.json in it, and its view <label>-agentic-readiness-STATE.md. |
--default-branch |
init: the branch the theme PRs go into. Default: the report's provenance.target.branch, else main. |
--phase |
init: the step recorded in the first log entry. Default: init. |
--date |
init: the date recorded on the items and the log, YYYY-MM-DD. Default: today, UTC. |
--framework-rev |
init: the framework commit to record, cut to 12 digits. Default: the framework checkout's HEAD, or null outside one. |
--label |
Every command but init: which state to use when the folder holds more than one. |
FINDING STATUS |
set-status: the item and the status to move it to. The move must be in the state diagram of the state contract. skipped, deferred and the moves back out of them have their own commands. |
--commit |
set-status: a commit SHA to add to the item (repeatable), 7 to 40 digits, stored cut to 12. |
--verification-command, --verification-result |
set-status: the verification and what it gave; given together. done needs one. |
--debt, --clear-debt |
set-status: record debt (repeatable; same what replaces) or drop it. partial needs debt; done needs none. |
--reason |
The reason. Required by failed, blocked, skip, defer and pref. |
KEY VALUE |
pref set: a lower-case key (letters, digits, dot, underscore, hyphen; at most 64) and its value. The value none records that the owner was asked and has no preference. |
--note |
set-status: text appended to the item's notes. When the item already has the given status, this is the only effect (the note and any --commit are added and the log says "note added"); without --note that case is refused. |
--by |
Who decided, owner or agent (default agent). skip, unskip, defer, undefer and pref refuse anything but owner: the agent relays the owner's answer; the tool cannot see who is typing. |
--phase |
Every write: the step recorded in the log entry and on the item or preference. |
--render |
Every write: also replace the Markdown view. Without it the tool warns that the view is out of date. |
--status, --branch, --pr, --merged-sha |
theme: the theme's new status (it only moves forward) and the data that status needs: a branch, then a PR, then the merge SHA (stored cut to 12 digits). |
--json |
show: print the state itself instead of the summary. Long lists are cut to fit the output and truncated is set; the log keeps its newest entries. |
Outputs
init writes the JSON and the Markdown. Its data holds their paths, the label, the item count, the count per
status and the not_queued count, with versions as check_drift.py gives it. show puts the
summary in data:
- the counts per status and per theme (
items, andopen: neitherdonenorskipped); - the skipped finding ids, and the number of preferences, debt entries and log entries;
versions: the framework that created the state (created), the one that last wrote it (last) and these tools (tools).
show --json puts the state under data.state, with the same versions. render writes the Markdown and puts its
path and size in data. Every other command checks the changed state whole and appends one log entry. It appends
two when the framework changed since the last write (see last_framework). Then it replaces the JSON and puts the
path and the count per status in data. A refused command changes nothing, and a refused move says which states
the item may go to. The Markdown view keeps prose lines under 110 columns, so it passes MD013: the item details, and
the full text of a cut preference cell. A preference's value or reason longer than 100 characters is cut in its
table row, and given in full in a list below the table. Each write is atomic. A temporary file that a killed write
left behind is removed, with a warning.
Exit codes
| Code | When |
|---|---|
0 |
Done. |
1 |
A bad argument, a missing report or target, a report outside the target, an existing state for init, no state for show or render, a state folder that leaves the target, or a state file that is a symlink. |
2 |
The report is invalid, not final, from a newer framework release, or has a finding on a check with no theme; or the state fails the schema or its rules. data.errors lists what to fix. |
3 |
Internal error, including a write the system refused. The old file is left whole. |
Errors
| Message | Fix |
|---|---|
the report is a draft, not final |
Finalise it with report.py finalise and pass the final report. |
the report was made by framework vX; these tools are vY. Update the framework ... |
The report is newer than these tools. Install release vX or later of both skills, or run a new audit with vY. |
the report must be inside the target, so the state can point at it |
Commit the report folder to the target and pass that path. |
a state already exists: ...; init never replaces one |
Use show, render and the transition commands. |
F-nnn is done and done is final |
A regression is a new finding in the next audit. |
F-nnn is skipped and only the owner can bring it back |
Ask the owner, then unskip --by owner. |
F-nnn is queued and must be in_progress before it can be ... |
Move it to in_progress first. |
F-nnn: ... has its own command |
Use skip, defer, unskip or undefer with --by owner. |
... is an owner decision |
Ask the owner and pass --by owner. |
F-nnn cannot be done without a verification / ... still has N debt entries / ... cannot be partial without debt |
Give the verification, use partial, or record the debt. |
theme Tn is ... and a theme only moves forward |
Themes go not_started, branch_open, pr_open, merged. |
the report has findings on checks with no theme: ... |
Add the checks to THEMES in progress.py. |
no state in ... |
Run init first. |
N states in ...; pick one with --label |
Pass --label. |
the state is not valid: N errors |
Fix each entry in data.errors; schema: errors come from the schema, rule: errors from the rules above. |
refusing a state file that is a symlink |
Replace the link with the real file. |
Tells which audit findings the target has moved under since the audit. It reads the report's
provenance.target.git_sha, compares it with HEAD of the target using read-only git, and lists the paths that
changed and the findings whose cited file evidence or remediation.touches intersect them. A changed path
matches a cited path when they are equal or the changed path is below a cited directory. It judges nothing: a
listed finding is one to re-check before acting on the report's text. It writes nothing. The source is
check_drift.py.
Synopsis
python3 implement/tools/check_drift.py REPORT REPO [--json]
python3 implement/tools/check_drift.py --describe
--json is accepted for symmetry; the output is always the JSON envelope.
Output
data.status is one of:
| Status | Meaning |
|---|---|
current |
HEAD is the audited commit. |
drifted |
HEAD differs. changed_count, changed_paths and findings (each with id, check, changed_paths) are set, and drifted is true. |
sha_missing |
The audited commit is not in the target's history (rewritten or not fetched). Nothing is compared; a warning says to re-check every finding. |
no_sha |
The report recorded no git_sha. A warning says to re-check every finding. |
audited_sha and head_sha are always present. audited_sha is the id as the report records it, usually 12
digits; head_sha is the full id. The status compares the full id the short one resolves to. Two more fields never
change the status or the exit code:
-
dirtyistruewhen the target's working tree has uncommitted or untracked changes (git status --porcelainis not empty),falsewhen it is clean,nullwhen git could not say. A warning and a line in the text output say so when it istrue, so the recipe does not ask the agent to rungit statusby hand. -
report_changedcompares the report file's SHA-256 withsource_reports[].sha256in the target's state (progress.py initrecords it).truemeans the report was edited since the state was created: a warning says not to trust its text until the owner confirms the change.falsemeans it matches.nullmeans there is nothing to compare: the target has no state, or the state does not list this report. -
versionscompares the framework release and commit that made the report with these tools'. It holdsreportandtools, each withreleaseandrevision(12 digits, ornulloutside a checkout), andrelation:same,olderornewer.oldergives a warning that verdicts can differ between releases, and a note in the text output. With the same release at two known commits, a warning names both. Anewerreport stops the tool with exit 2: these tools may not know its checks.
When the audited commit is not an ancestor of HEAD, the diff is
between the two trees and a warning says so. Long lists are bounded: changed_paths_omitted and
findings_omitted count what was left out, and truncated is true.
Exit codes
| Code | When |
|---|---|
0 |
Done, whatever the status. Read data.status. |
1 |
A bad argument, a missing report or target, or a target that is not a git repository. |
2 |
The report is not valid, or it comes from a newer framework release than these tools; data.error says what to do. |
3 |
Internal error. |
Compares two audit reports of one repository, normally the report the implementation started from and the
re-audit after it. It writes nothing. Both reports must validate (exit 2 otherwise). The source is
compare_reports.py.
Synopsis
python3 implement/tools/compare_reports.py BEFORE AFTER [--state STATE] [--json]
python3 implement/tools/compare_reports.py --describe
How it compares
- A check is matched by id.
improvedandregressedfollow FAIL < PARTIAL < PASS. A move to or fromUNKNOWNorNOT_APPLICABLEischanged, because no order exists. A check in one report only isaddedorremoved. - A finding is matched by its
key. A key only inBEFOREisclosed, only inAFTERisnew, in both ismovedwhen the severity differs andunchangedotherwise. Findings with dispositionrejectedwere never confirmed gaps and are left out on both sides; their number issummary.rejected_ignored. - With
--state, every move gets alabel. The state's items name findings of theBEFOREreport; an item whose check disagrees with that report is ignored with a warning. A closed finding, or an improved check, isexplained:doneorexplained:partialwhen an item reached that status. Everything else isunexplained(a closed finding adds the item's status, for exampleunexplained:in_progress, or none when no item exists). A regression and a new finding are alwaysunexplained. Without--statethe label is-. - A real run to read: the worked example holds a comparison this tool wrote for the Server (draft, annotated by hand).
- It does not know which audit-engine defects distorted a verdict. The caller annotates a result that an open
audit-engineissue explains.
Output
By default data holds summary and markdown. The Markdown is the deterministic report: summary lines, checks
that moved, findings that moved, and findings still open. With --json it holds summary, checks and findings
as rows instead. summary has the two reports' file names, commits and scores, the counts per move, rejected_ignored,
and unexplained. before and after also hold each report's framework (release, revision) and
catalogue_version. A warning is raised when the reports name different repositories. version_warnings holds one
when they come from different releases, from the same release at two commits, or from different catalogues: a
verdict can move for that reason alone. Each is also a warning and a line in the Markdown. Long lists are bounded:
truncated is true, and the Markdown (or data.omitted) says how many rows were left out.
Exit codes
| Code | When |
|---|---|
0 |
Done. A comparison with moves is not an error. |
1 |
A bad argument, or a missing report or state file. |
2 |
A report or the state is not valid; data.errors lists what to fix. |
3 |
Internal error. |