dont-harness-cli Specification
Purpose
Defines the invocation contract between dont and its callers — human operators, CI
pipelines, and LLM harnesses. Covers project directory resolution (DONT_DIR), config
loading, the --help / after_help surface, and the subprocess protocol that external
tools use when spawning dont as a child process.
Requirements
Requirement: Project directory resolution
The system SHALL resolve the active project directory through a deterministic two-step
lookup. First it MUST check the DONT_DIR environment variable; if set to a non-empty
value, that path is used as the project directory without walking the filesystem. If
DONT_DIR is absent or empty, the system MUST walk upward from the current working
directory until it finds a .dont/ directory with a config.toml inside it, or exhaust
the filesystem hierarchy. A walk that finds no .dont/ directory MUST produce a
config-missing error with exit code 3.
Scenario: DONT_DIR overrides filesystem walk
- WHEN
DONT_DIRis set to a non-empty path beforedontis invoked - THEN the system uses that path as the project directory without walking the current working directory upward
Scenario: filesystem walk finds nearest ancestor
- WHEN
DONT_DIRis unset and the current working directory is a subdirectory of a project - THEN the system walks upward and uses the nearest ancestor that contains
.dont/config.toml
Scenario: no project directory produces config-missing error
- WHEN
DONT_DIRis unset and the filesystem walk reaches the root without finding.dont/config.toml - THEN the process exits with code
3and the envelope hascode: "config-missing"with a remediation entry suggestingdont init
Scenario: DONT_DIR path that lacks config.toml is rejected
- WHEN
DONT_DIRpoints to a directory that exists but does not containconfig.toml - THEN the process exits with code
3and the envelope hascode: "config-missing"
Requirement: Config validation on every invocation
The system SHALL validate config.toml before executing any command payload. Validation
MUST parse the TOML file, check all known fields for type correctness and valid values,
and reject the invocation with a structured error when any field is invalid. The error
envelope MUST carry code: "config-invalid" and a message identifying the offending
field. Invalid configuration MUST be surfaced before any store mutation is attempted.
Scenario: invalid config field aborts invocation
- WHEN
config.tomlcontains a field with an invalid type or out-of-range value - THEN the process exits with code
3before any store mutation - AND the envelope has
code: "config-invalid"andmessagenames the offending field
Scenario: valid config proceeds to command execution
- WHEN
config.tomlparses and validates successfully - THEN the system proceeds to execute the requested command
Requirement: Author resolution order
The system SHALL resolve the author string for each invocation through a fixed priority
chain: the --author / -a flag (highest priority), then the $DONT_AUTHOR environment
variable, then the $USER environment variable. If none of these yields a value the
author is omitted from the event record for that invocation. The resolved author is stored
only on events produced during that invocation and does not persist to config.toml.
Scenario: explicit flag takes highest priority
- WHEN
--author agent:ci-botis supplied andDONT_AUTHOR=human:aliceis set - THEN the author recorded on the resulting event is
agent:ci-bot
Scenario: DONT_AUTHOR overrides USER
- WHEN
DONT_AUTHOR=agent:ci-botis set andUSER=aliceis also set and no--authorflag is provided - THEN the author recorded on the resulting event is
agent:ci-bot
Scenario: USER is the last fallback
- WHEN neither
--authornorDONT_AUTHORprovides a value andUSER=aliceis set - THEN the author recorded on the resulting event is
alice
Requirement: Help surface contract
The system SHALL expose two equivalent surfaces for per-command help: dont help <cmd>
and dont <cmd> --help. Both MUST produce the same output. The help output for each
command MUST include the command description, all flags with their types and defaults,
and an after_help examples block showing the canonical invocation patterns for that
command. The examples block MUST use the exact form dont <cmd> <typical-args> and MUST
cover at least the most common successful invocation and one invocation showing a
non-default flag. The global --help output (bare dont --help or dont help) MUST
list every subcommand with a one-line description and conclude with an after_help
block listing representative session-start examples.
Scenario: subcommand help is reachable two ways
- WHEN the caller runs
dont lock --help - THEN the output is identical to
dont help lock
Scenario: every command has an examples block
- WHEN the caller runs
dont <cmd> --helpfor any implemented subcommand - THEN the output contains an examples section showing at least one canonical invocation
Scenario: global help lists all subcommands
- WHEN the caller runs
dont --helpordont help - THEN the output lists every subcommand with a one-line description
- AND the output ends with a representative session-start examples block
Scenario: after_help examples use canonical flag form
- WHEN the examples block of a subcommand shows a flag
- THEN it uses the long form (e.g.
--status,--evidence) rather than the short form - AND each example is a complete, runnable
dontinvocation
Requirement: Subprocess invocation protocol
The system SHALL define a stable contract for external tools (CI pipelines, LLM harnesses,
shell wrappers) that invoke dont as a subprocess. A conformant subprocess caller MUST
always pass --json to get machine-readable output on stdout, MUST treat the process exit
code as the primary routing signal before parsing the envelope body, and MUST read the
full stdout before inspecting the exit code to avoid SIGPIPE on large outputs. On exit
code 0 the caller SHOULD read data for payload and warnings[] for non-blocking
notices. On exit code 1 the caller MUST read data.remediation[0].command and act on
it rather than retrying the same invocation. On exit codes 3 and 4 the caller MUST
stop LLM-driven retry and escalate to operator intervention. Exit code 2 indicates a
usage error in the caller’s own invocation and MUST be treated as a caller bug.
Scenario: subprocess caller routes on exit code before parsing body
- WHEN a subprocess caller receives a non-zero exit from
dont - THEN it branches first on the exit code (0/1/2/3/4) before inspecting the JSON body
Scenario: exit code 1 directs caller to remediation
- WHEN a subprocess caller receives exit code
1 - THEN it reads
data.remediation[0].commandand issues that command as the next action - AND it does not retry the refused invocation unchanged
Scenario: exit codes 3 and 4 halt LLM retry
- WHEN a subprocess caller receives exit code
3or4 - THEN it does not attempt further LLM-driven retries
- AND it surfaces the error envelope to the operator for manual intervention
Scenario: exit code 2 is treated as caller error
- WHEN a subprocess caller receives exit code
2 - THEN it treats this as a bug in its own argument construction
- AND reads
data.messageto understand the malformed argument
Scenario: subprocess caller reads full stdout before exit code
- WHEN a subprocess caller runs
dontwith--json - THEN it reads all of stdout before checking the process exit code to prevent SIGPIPE on the dont side
Requirement: Subprocess output routing
The system SHALL route all JSON envelope output to stdout and all human-readable
diagnostic text (warnings, errors in non-json mode) to stderr. When --json is active,
stdout MUST contain only the JSON envelope line and nothing else; log noise or debug text
MUST go to stderr. A subprocess caller that captures only stdout while discarding stderr
MUST receive a clean, parseable JSON line.
Scenario: JSON mode writes only the envelope to stdout
- WHEN
dont <cmd> --jsonis invoked - THEN stdout contains exactly one JSON object per invocation (or one per stdin line in NDJSON mode)
- AND no human-readable prose appears on stdout
Scenario: stderr carries warnings and diagnostic text
- WHEN
dont <cmd>is invoked in human mode and warnings are produced - THEN the warning text appears on stderr, not stdout
Scenario: subprocess can discard stderr safely
- WHEN a subprocess caller captures only stdout with
--json - THEN it receives a complete, parseable JSON envelope regardless of warning or debug output
Requirement: Environment variable reference
The system SHALL honour the following environment variables. DONT_DIR (string): path
to the project directory; overrides the filesystem walk. DONT_AUTHOR (string): default
author string used when --author is not supplied; may be in any format but <kind>:<id>
is recommended. NO_COLOR (any non-empty string): disables ANSI colour codes in human
output; see dont-cli-surface for full colour-resolution rules. CLICOLOR_FORCE=1:
forces ANSI colour even when stdout is redirected; see dont-cli-surface. No other
DONT_ variables are part of the stable contract; callers MUST NOT rely on
undocumented DONT_ variables between releases.
Scenario: DONT_DIR is the documented project-root override
- WHEN a caller sets
DONT_DIR=/tmp/myproject/.dont - THEN
dontuses that directory as the project root without walking the filesystem - AND this behaviour is stable across releases
Scenario: DONT_AUTHOR sets the default event author
- WHEN a CI pipeline sets
DONT_AUTHOR=agent:ci-pipeline - THEN all events recorded during that session carry
agent:ci-pipelineas the author unless--authoroverrides it
Scenario: undocumented DONT_ variables are not relied upon
- WHEN a caller script sets an undocumented variable such as
DONT_VERIFY_EVIDENCE_MOCK - THEN that variable is not guaranteed to exist or behave consistently in future releases
- AND the caller MUST NOT use it in production integrations
Requirement: DONT_DIR isolation for test environments
The system SHALL support test isolation through DONT_DIR. A test harness MUST be able
to point multiple concurrent dont invocations at distinct temporary directories using
DONT_DIR without those invocations interfering with each other or with the host
project. Each isolated DONT_DIR path MUST be independently initialised with dont init
before any other command is issued against it.
Scenario: two concurrent test invocations with distinct DONT_DIR values
- WHEN two
dontprocesses run concurrently withDONT_DIRpointing at different temporary directories - THEN each process operates on its own isolated project state with no cross-contamination
Scenario: test isolation does not require a parent .dont directory
- WHEN
DONT_DIRpoints at a path that is not named.dont(e.g./tmp/test-abc/) - THEN
dontaccepts that path as the project directory without requiring the name to be.dont