Schema: ConformanceProfile
File: conformance-profile.json
Used by:
conformance/run.py·conformance/profiles/
Purpose
A profile tells the conformance kit how to invoke a CLI and which invocations are safe to probe. The kit cannot guess a tool's subcommands or which of them delete data, so the author of the profile states it once and every run is reproducible.
Key decisions:
- Probes declare intent.
kindsays what a correct CLI does with the call: finish without side effects, exit2, refuse until confirmed, or stream JSONL lines to a terminal line - Confirmation flags never appear in a probe. Destructive probes run without confirmation and with their declared
dry_run_flag; the kit never deletes on purpose - Paths are relative to the profile. A committed profile works from any working directory
Values
| Field | Type | Required | Description |
|---|---|---|---|
schema_version |
"1.0" |
yes | Profile format version |
tool |
string | yes | Display name |
command |
string[] | yes | Invocation prefix; a first element containing / resolves against the profile directory, a bare name through PATH |
timeout_seconds |
number (0, 120] |
yes | Per-run limit; exceeding it counts as a hang |
manifest |
string[] | no | Arguments that print the manifest; omit to skip manifest_valid |
argument_order |
ArgumentOrder |
no | A read command and a global option to move around it; omit to skip argument_order |
probes |
Probe[] |
yes | Invocations to run |
Probe
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | yes | Unique label shown in evidence |
argv |
string[] | yes | Arguments after the prefix |
kind |
"read" | "destructive" | "invalid" | "stream" |
yes | Expected behavior class |
dry_run_flag |
string | when destructive | Flag that turns the probe into a preview; not allowed on a stream probe |
deadline_seconds |
number (0, 120] |
no | stream only: limit for the whole stream; defaults to timeout_seconds |
signal |
"INT" |
no | stream only: signal sent after after_lines lines; requires after_lines |
after_lines |
integer >= 1 |
no | stream only: stdout lines read before signal is sent; requires signal |
A stream probe runs once and skips the single-envelope checks. The kit reads its stdout line by line until the process exits, and kills the process group at the deadline (see conformance/README.md).
ArgumentOrder
The kit runs command_path with local_args, placing global_flag before the command path, between the path and the local option, and after the local option. Every placement must exit 0 with the same result; alternate_value must change stdout identically in every placement; the flag given twice with value and then alternate_value must exit 2. With positional, the kit also runs local_args after and before the positional: both must give the same data, and that data must differ from the positional alone, which proves the option was parsed rather than read as a second positional.
| Field | Type | Required | Description |
|---|---|---|---|
command_path |
string[] | yes | Path of a side-effect-free command |
local_args |
string[] | yes | A command-local option and its value |
global_flag |
string | yes | Long name of a global option, usually --format |
value |
string | yes | Value whose stdout is a ResponseEnvelope, usually json |
alternate_value |
string | yes | Another accepted value with different stdout, such as plain; must differ from value |
positional |
string | no | A positional command_path accepts; local_args must change the result when given after it |
Examples
Valid
{
"schema_version": "1.0",
"tool": "democli",
"command": ["../../benchmark/harness/cli/good/democli"],
"timeout_seconds": 5,
"manifest": ["manifest"],
"argument_order": { "command_path": ["deployments", "list"], "local_args": ["--cursor", "cGFnZTI="], "global_flag": "--format", "value": "json", "alternate_value": "plain", "positional": "staging" },
"probes": [
{ "name": "list deployments", "argv": ["deployments", "list"], "kind": "read" },
{ "name": "unknown flag", "argv": ["deployments", "list", "--no-such-flag"], "kind": "invalid" },
{ "name": "delete staging", "argv": ["deployments", "delete", "--filter", "env=staging"], "kind": "destructive", "dry_run_flag": "--dry-run" },
{ "name": "stream deployments", "argv": ["deployments", "list", "--stream"], "kind": "stream" },
{ "name": "interrupt the deployment stream", "argv": ["deployments", "list", "--stream"], "kind": "stream", "signal": "INT", "after_lines": 2 }
]
}
Invalid — destructive probe without a preview flag
{
"schema_version": "1.0",
"tool": "democli",
"command": ["democli"],
"timeout_seconds": 5,
"probes": [
{ "name": "delete staging", "argv": ["deployments", "delete", "--filter", "env=staging"], "kind": "destructive" }
]
}
Violation: dry_run_flag is required when kind is destructive.
Invalid — signal without a line count
{ "name": "interrupt", "argv": ["events", "watch"], "kind": "stream", "signal": "INT" }
Violation: signal and after_lines require each other, and both apply only to stream probes.
Common mistakes
- Putting
--yesor--forcein a destructive probe. The kit then executes the deletion for real; confirmation flags never belong in a profile - Marking a mutating command as
read. Read probes run several times (stdin closed, stdin open,NO_COLOR); anything that writes will write repeatedly - Pointing a profile at production credentials. Probes call the real tool; use a sandbox account or a mock
- Adding
--format jsonto probe argv. The envelope check exists to prove JSON activates in a non-TTY without flags (REQ-F-003);argument_orderis the one place a profile names the format flag - Choosing
local_argsthat do not change the result. Withpositional, the kit proves the option was parsed by comparing against the positional alone; an option with no visible effect fails that comparison - Choosing an
alternate_valuethat renders like the default. The overwrite check compares stdout; an alternate that prints the same bytes asvaluereads as an ignored option - Setting
after_linesbeyond what the stream writes. A stream that ends first never receives SIGINT, andstream_sigintfails; pick a count well inside a normal run - Pointing a
streamprobe at a stream that never ends without asignal. The kit waits for the terminal line and fails at the deadline; interrupt follow-mode streams withsignal
Agent interpretation
- Generate a profile from
tool manifest:danger_level: "safe"commands becomereadprobes,destructivecommands becomedestructiveprobes with the declared dry-run flag, andsafecommands withstreaming_default: trueor a--streamflag becomestreamprobes - Never add a probe for a command whose
danger_levelismutatingunless a sandbox is confirmed - Keep
timeout_secondsshort (5 or less); the hang checks rely on it
Coding agent notes
- Validate the profile against this schema before running anything;
conformance/run.pyexits2withINVALID_PROFILEotherwise - Commit profiles next to the CLI they describe and run the kit in that CLI's CI
- Tests: a profile with a destructive probe and no
dry_run_flagis rejected; duplicate probe names are rejected;signalwithoutafter_lines, or either on a non-streamprobe, is rejected
Implementation notes
Probe kinds are deliberately few. A mutating kind was left out because the kit cannot undo side effects, and a conformance check that damages state is worse than no check. Commands that mutate are covered by the destructive-refusal and dry-run checks when they declare a preview flag. A stream probe is side-effect free like read; REQ-O-004 refuses destructive streams, so the kit never interrupts one that deletes.