Changelog
Every change that alters a canonical schema, a requirement's acceptance criteria, or the failure mode taxonomy is recorded here.
Versioning
- Spec version (
MAJOR.MINOR.PATCH, git tagvX.Y.Z) versions the corpus as a whole.MINORreleases add or change failure modes, requirements, or schemas.PATCHreleases fix prose, examples, and tooling without changing any contract.MAJORis reserved for restructuring the taxonomy or the tier model - Contract version (
MAJOR.MINOR, per canonical schema, listed inschemas/index.md) versions each wire contract independently. A change that can reject a previously valid instance, or change what a field means, increments the contract'sMAJOR. Additive optional fields incrementMINOR - A contract
MAJORincrement always ships in a specMINORrelease with a migration section in this file meta.schema_versioninside a response is neither: it versions one command's output shape (REQ-F-022)
Unreleased
1.16.0 — 2026-10-09
A confirmation only a person gives: requires_person
- REQ-C-036 Person-Only Commands Declare
requires_person(P1): a command whose confirmation only a person may give, such as approving a change an agent proposed, declaresrequires_person: truewithinteractive: trueandmcp: false. It asks the person to type back an expected string, only when stdin and stdout are terminals and--non-interactiveis absent, and no flag answers it,--yesincluded. Off a terminal it exits4witherror.code: "PERSON_REQUIRED",retryable: false, and a suggestion that names no flag; a wrong answer exits4withATTESTATION_MISMATCH; neither runs anything. The requirement states its non-goal: a speed bump and an honest record, not a security boundary, since an agent running as the same OS user can fake a terminal - REQ-C-005: its first criterion gains the one exception, a command with
requires_person: trueignores--yesfor its attestation - ManifestResponse 3.21:
CommandEntry.requires_person(trueonly, absent otherwise); the schema requiresinteractive: trueandmcp: falsebeside it. A producer that declares it emitsschema_version3.21; earlier manifests stay valid.manifest-response.mdgains a valid and an invalid example, common mistakes, and the agent interpretation;tests/test_manifest_schema.pypins the behaviour response-envelope.mdgains the agent interpretation ofPERSON_REQUIREDandATTESTATION_MISMATCH: hand the command to a person and never retry it- §10 and §23 point person-only confirmations at REQ-C-036 in their solutions, §10's agent workaround says to hand a
PERSON_REQUIREDcommand to a person, andtriage.mdrow 1 routesPERSON_REQUIRED. No new failure mode
Why: REQ-C-005 made --yes answer every confirmation, so a conformant CLI could not have a step only a person takes; an agent approved its own proposal with decisions approve <id> --yes (#86).
1.15.0 — 2026-10-09
§79 Work Outlives the Caller's Budget: bounded calls that keep their work
- New failure mode §79 (Part II, Critical): a command whose run length depends on its input meets an agent's fixed per-call budget, and the call is killed with its work; a read-only
TIMEOUTmarked retryable repeats on every identical rerun. The rule: a synchronous call is bounded, and work is never lost to a budget - REQ-F-080 Sync Call Budget (P1): a command declaring
interruptionreturns within the budget in non-interactive mode, with its result orINCOMPLETE (14). The budget resolves from--budget, thenAGENT_CALL_BUDGET_MS(set by the harness, no tool prefix), then30000ms;--budget 0turns it off.meta.budget_msrecords it - REQ-F-081 Detached Job Runtime (P1): a
detachcommand's work continues in a background job when the budget runs out. Observable guarantees: the caller's descriptors are closed, the job runs in its own session, its state lives in a per-user job directory, the identical invocation attaches to a running job, a dead worker is reported asJOB_LOST, a job without progress foridle_timeout_msends asTIMEOUTwitherror.code: "STALLED" - REQ-F-082 Incomplete-Work Response (P1): exit
14witherror.code: "INCOMPLETE"anddata.state(runningorpaused),job_id,progress,continue_command, andcancel_command;retryableandside_effectscome from the command's declaredINCOMPLETEentry. A writing command's pausedcontinue_commandcarries--idempotency-key - REQ-C-033 Commands Declare Interruption (P1):
interruption: {detach, resume, idle_timeout_ms?, max_lifetime_ms?}with at least one ofdetachandresumetrue, anINCOMPLETE (14)exit entry, and noasync, streaming, orstdout: "protocol";resumeon a writing command needs--idempotency-key. A command that declares nothing keeps today's behavior - REQ-C-034 Long-Running Commands Report Progress (P1):
ctx.progress(done, total?, unit?)at least every 10 s;donenever decreases. Progress, not a timer, tells working from hung - REQ-C-035 Resumable Commands Checkpoint at Safe Points (P2):
ctx.restore(fingerprint)andctx.checkpoint(state)at least every 10 s, written by rename, keyed like jobs so the identical invocation resumes; stop requests take effect only at a checkpoint; a changed input discards the checkpoint with aCHECKPOINT_DISCARDEDwarning - ExitCode 1.1:
14INCOMPLETEjoins the framework-reserved codes (0–14) in a newcontinuationgroup; framework extensions are now15–63. REQ-F-001's table,exit-code.md,exit-code-entry.md, and the conformance kit's allowed codes follow - ResponseEnvelope 2.4:
ResponseMetagains optionalbudget_ms, andtimeout_ms, which REQ-F-011 already required but the schema did not list; an incomplete-work example and the agent interpretation ofINCOMPLETE - ManifestResponse 3.20:
CommandEntry.interruption; the schema requires anexit_codesentry for"14"beside it and excludesasync: true,streaming_default: true, andstdout. A producer that declaresinterruptionemitsschema_version3.20; earlier manifests stay valid.tests/test_manifest_schema.pypins the behaviour - REQ-F-011: a command declaring
interruptionis bounded per call by the budget and over its whole work by--timeoutorinterruption.max_lifetime_ms, with an idle limit for background jobs; it never needs--timeout 0to survive a large input - REQ-F-053: the sentence mandating heartbeats moves out; heartbeats stay opt-in (REQ-O-012, REQ-O-038), and a Foundation requirement no longer requires an opt-in one
- REQ-C-022 and §49:
job statusexits14for a running job, not3, which REQ-F-001 assigns toPARTIAL_FAILURE; §49's table also gives a timed-out job10, not7(PERMISSION_DENIED), and so does §11's framework-design list - §11 and §49 point input-dependent work at §79;
triage.mdroutesINCOMPLETEin row 1 and adds §79 to rows 3 and 14; the checklist gains three reliability items;IMPLEMENTING.mdgains a long-running commands section with a reference detach pattern
Why: the spec's only answer to "this is taking long" was a wall-clock timeout that kills the work. It cannot tell a long run from a hung one, it discards the progress made, and for input-dependent read-only work its retryable TIMEOUT sent the agent into the same timeout again (#82, #83). Merging REQ-O-012 and REQ-O-038 into one heartbeat and an expected_duration manifest hint, both decided in #83, are left to follow-up changes.
1.14.0 — 2026-10-08
A read-only TIMEOUT is not retryable by default
- REQ-C-014: the
TIMEOUTacceptance criterion no longer saysside_effects: "none"makes a timeoutretryable: true. ATIMEOUTerror'sretryableequals the command's declaredTIMEOUTentry;side_effects: "partial"forcesfalse, andside_effects: "none"permitstruewithout implying it - REQ-C-014: new acceptance criterion: a command whose run length depends on its input declares
TIMEOUTwithretryable: false, side_effects: "none", and its error carrieserror.fix_requirednaming a larger--timeout. The wire format and example gain that case beside the retryable timeout - REQ-F-011: a command whose run length depends on its input declares a per-command default timeout sized for its expected inputs; no new mechanism
- §19: the retry taxonomy no longer lists
TIMEOUTasretryable: true, and the exit code alignment saysside_effects: "none"is necessary for a retryable timeout, not sufficient exit-code-entry.mdandexit-code.mdgain the input-dependent read-only timeout as a validretryable: false, side_effects: "none"example and name declaring it retryable as a common mistake; theTIMEOUT (10)agent action inexit-code.mdfollows the entry'sretryableandfix_required. No JSON schema changes
Why: REQ-C-014 defines retryable as "the identical invocation, unchanged, may succeed", yet its criterion made every side-effect-free timeout retryable, though a read-only command whose run length depends on its input times out again on the identical re-run (#82).
1.13.0 — 2026-10-06
ExitCodeEntry 1.1: error_codes names the error.code values under each exit
ExitCodeEntrygains optionalerror_codes: a unique array of theerror.codevalues the command emits under that exit, each matching^[A-Z][A-Z0-9_]+$, the patternResponseEnvelopeputs onerror.code. Absent means the command does not declare them, never "none"; an empty array means the exit carries noerror.code- REQ-C-001: each entry SHOULD list its
error.codevalues inerror_codes. New acceptance criterion (a response under a declared list carries a listed code, or none for[]), and theCONFLICT (6)entry in the wire format and example listsALREADY_DEPLOYED - REQ-C-028: the
CONFLICT (6)entry SHOULD listALREADY_EXISTSinerror_codes. New acceptance criterion and anexit_codestable in the wire format exit-code-entry.mdgains the field row, a valid and an invalid example, common mistakes, the agent interpretation (branch on the receivederror.code; the list says in advance which values to plan for), and generated assertions;manifest-response.mdgains a3.19example and the agent interpretation;tests/test_manifest_schema.pypins the schema behaviourManifestResponsebecomes 3.19, since itsexit_codesmaps carryExitCodeEntry. A producer that listserror_codesemitsschema_version3.19; earlier manifests stay valid, and on them an exit'serror.codevalues can appear only in itsdescription
Why: an ExitCodeEntry named the exit code (6 CONFLICT) but not the error.code an agent branches on, and its closed schema left a REQ-C-028 command no way to publish ALREADY_EXISTS in its manifest (#78).
1.12.0 — 2026-10-05
tool doctor exits 4 when a check fails
- REQ-O-026: the Description now says
tool doctorexits4(PRECONDITION) witherror.code: "DOCTOR_CHECKS_FAILED"when any check fails, as its acceptance criteria and wire format already did; it no longer says exit1
Why: the Description and the acceptance criteria named different exit codes for a failed check, so an agent branching on doctor's exit code could not tell which to expect (#71).
ResponseEnvelope 2.3: numbered stream lines
- REQ-O-004: a stream MAY number its item lines with the reserved key
_seq,1on the first and one more on each next line. A numbered stream numbers every item line, never a heartbeat or terminal line, puts"_count": N(the number of item lines) on its summary line, and ends a failure on an error envelope withmeta.items_emitted(the last_seq,0before the first item). A line's_seqis never item data: an item type with its own_seqfield does not number its stream, and a records consumer removes_seqbefore validating a record. New Description paragraph, three acceptance criteria, and numbered wire-format lines ResponseMeta.items_emitted(optional, non-negative integer) makesResponseEnvelope2.3;response-envelope.mdgains the field row, a numbered-stream failure example, and the agent interpretation. Earlier instances stay valid, and a stream without_seqkeeps its meaning- §76 gains numbered lines as an optional enhancement against silent truncation; the streaming guide gains a "Numbering Stream Lines" section
- Conformance kit:
stream_contractchecks a stream whose first item line carries_seq:_seqon every item line counting up from1, none on heartbeat or terminal lines,_countequal to the number of item lines, andmeta.items_emittedequal to the last_seqon an error terminal envelope; a later_seqin an unnumbered stream fails. The good democli mock numbers itsdeployments list --streamlines, includingmeta.items_emittedon its SIGINT envelope, andstreamcligains a passing numbered stream and failure, plus eight failing cases
Why: a REQ-O-004 stream gave an agent no way to tell a complete short stream from one that lost lines in a pipe, a log, or a truncating runtime, nor which item a failed stream stopped after (#67).
A cancelled or partial run flags data.partial and names its signal in error.context
- REQ-F-013 and REQ-F-069: the Description and Example now put the partial flag at
data.partial: trueand REQ-F-069's signal name aterror.context.signal, as both wire formats already did. A top-levelpartialorerror.signalfailsResponseEnvelope, which admits no extra properties at either level - §11, §13, and §16 examples move
partialand the other partial-result fields (completed_steps,resume_from,resume_token,results,summary) intodataand become complete envelopes; §13's agent workaround reads them fromdata.comparison-matrix.md's REQ-F-013 sentence says the same - Conformance kit:
stream_sigintalso requires the terminalCANCELLEDenvelope to carrydata.partial: true. The good democli mock's--streamcancellation now emits it, andstreamcligains a failingint-no-partialcase. The process-leak test looks only for sleeps from its own run, so concurrent runs in other checkouts no longer fail it - No contract changes; generated reports under
evaluations/are records of past runs and stay as they were
Why: REQ-F-013's and REQ-F-069's prose and examples put partial at the top level and the signal on ErrorDetail, which the envelope schema rejects, while their own wire formats used data.partial and error.context.signal, so an implementer had two answers and only one of them validated (#68).
A declared dependency is needed at any version
- REQ-O-031: a
DependencyEntry.min_versionvalue is a minimum version or"*", meaning any version, presence only.tool doctorruns the dependency'scheck_commandand passes when it exits0, skippingversion_regexand the version comparison; a dependency with no--versionflag uses a presence command such ascommand -v bean-format. New acceptance criterion, and a"*"dependency in the wire format and example - REQ-O-026: the
required_tools"*"rule and the declared dependency rule read as one:"*"is a presence check, byPATHlookup for arequired_toolsentry and bycheck_commandfor a dependency, reported asmin_version: "*"with nofound_versionindata.dependencies. New acceptance criterion manifest-response.mdand themin_versiondescription inmanifest-response.jsonsay the same. The field stays a required string, soManifestResponsestays 3.18 with a description change only
Why: min_version was a required "minimum supported version" with no way to declare a dependency needed at any version, so a program with no --version flag could not be declared without inventing a version doctor could never check; required_tools gained "*" for the same reason in #58 (#63).
required_tools declares a program needed at any version
- REQ-C-018: a
required_toolsvalue is a minimum version or"*", meaning any version; the program need only resolve onPATHand is never run to read a version. New acceptance criterion and a"*"entry in the wire format and example - REQ-O-026:
tool doctorchecks a"*"entry byPATHlookup only, never runs the program, and reportsrequired: "*"with noversion. New acceptance criterion and a passing"*"check in the wire format manifest-response.mdand therequired_toolsdescription inmanifest-response.jsonsay the same. The value type stays a string, soManifestResponsestays 3.16 with a description change only
Why: required_tools mapped each program to a minimum version with no way to declare one needed at any version, so a program with no --version flag, such as bean-format, could not be declared without inventing a version doctor could never check (#58).
ManifestResponse 3.18: integer enum_values
- REQ-C-015: a flag or positional whose valid values are a few integers declares
type: "integer"and lists them inenum_valuesas integers ([0, 1, 2]), with the same membership rule as atype: "enum"entry: every value a caller may pass, and nothing else. A value outside the list exits2withARG_ERRORbefore any side effect FlagEntry.enum_valuesandPositionalEntry.enum_valuesare string arrays ontype: "enum"entries, as before, and integer arrays ontype: "integer"entries. The schema rejects a string list on an integer entry and an integer list on an enum entry; themedia_typesrule still requirestype: "enum"- REQ-C-015's wire format and example gain an integer flag with
enum_values, and a new acceptance criterion;manifest-response.mdgains the field rows, a valid and an invalid example, common mistakes, the agent interpretation (agents and shell completion read the integers as data), and a generated assertion;tests/test_manifest_schema.pypins the schema behaviour - A producer that lists integer
enum_valuesemitsschema_version3.18. A 3.17 manifest whosetype: "integer"entry carried stringenum_valuespassed the schema though the prose forbade it; the schema now rejects it. The bump stays minor because it enforces a rule the prose already stated, and no corpus example is affected. On a pre-3.18 manifest an integer entry's allowed values can appear only in itsdescription
Why: an integer flag such as --sig-type 0|1|2 either lost its values to the description, where agents and shell completion cannot read them, or listed them as strings on a type: "enum" entry while the JSON routes take numbers (#59).
ManifestResponse 3.17: commands kept off the MCP server
- REQ-C-032: a command the tool's own MCP server must never offer as a tool (an approval a person gives, a project-creating
init, a watch loop that never returns) declaresCommandEntry.mcp: false. The field is present only whenfalse; absent means the tool's MCP server may offer the command, and the schema rejectsmcp: true - A server built from the manifest never lists a command marked
mcp: false, and a call naming one is refused inside the protocol; an agent runs such a command through the CLI or hands it to a person - REQ-O-035:
tool mcp-validatenever reports a command markedmcp: falseasmissing_from_mcp manifest-response.mdgains the field row, a valid and an invalid example, the agent interpretation, and a generated assertion;tests/test_manifest_schema.pypins the schema behaviour- A producer that sets
mcpemitsschema_version3.17; a pre-3.17 manifest stays valid, and on it an absentmcpcannot mark a command kept off the server
Why: a tool can serve its commands as MCP tools, yet CommandEntry took no extra properties and had no field to keep a command off that server, so a manifest could say so only in the description (#55).
Conformance kit: stream probes check a streaming command's JSONL contract
ConformanceProfileadds probe kindstream, with optionaldeadline_seconds(defaults totimeout_seconds) and optionalsignal: "INT"plusafter_lines, which require each other. The schema andconformance/run.pyreject either field on a non-streamprobe, anddry_run_flagon astreamprobe. Existing profiles stay valid- A
streamprobe runs once, outside the single-envelope checks. The kit reads stdout line by line until the process exits; at the deadline it kills the probe's process group and fails the run instead of hanging - New check
stream_contract(level 3, REQ-O-004, §5, §76): every line is one JSON object, the stream ends on exactly one terminal line (the"_summary": trueline or an errorResponseEnvelopewithok: false), a summary line exits0, an error envelope'smeta.exit_codematches the exit code, and the process exits before the deadline - New check
stream_sigint(level 3, REQ-O-004, §16): withsignal, the kit sends SIGINT afterafter_lineslines and requires exit130and a terminal error envelope witherror.codeCANCELLED(REQ-F-069). It is skipped on Windows - A profile without a
streamprobe now reports both checks asskip, so itslevel_3verdict isincompleteinstead ofpass - The good democli mock's
deployments listaccepts--stream, anddemocli-good.jsonadds twostreamprobes, so it still passes every check. New fixturestreamcliwith a passing and a failing profile
Why: the kit's probes expected one envelope per command, so nothing checked that a streaming command's lines parse, that it ends on one terminal line matching its exit code, or that SIGINT cancels it with exit 130 (#60).
1.11.0 — 2026-10-03
External content in error.context is tagged and masked
- REQ-F-035: when
error.contextholds external content (a wrapped program's stderr, a remote log tail, an upstream error body), it carries_source: "external"and_trusted: falseat its top level, and its external string values get REQ-F-058's high-entropy masking. A free-text value keeps its text: only each high-entropy substring is replaced. Keys an agent branches on (exit_code,line,code,retryable) are never masked - A framework helper's own subprocess-failure context marks the child's stderr external by default, without command author action
- Outside text an author wants tagged and masked goes in
error.context(REQ-C-013);detailkeeps its meaning, andresponse-envelope.mdtells an agent to treatmessage,detail, andcauseas untrusted text always - REQ-O-004: the
UPSTREAM_FAILEDcontext is external; the upstream's strings are masked, butline,upstream_exit_code,upstream.code, andupstream.retryableare not. REQ-C-031: a passthrough envelope copies no delegated output, so it carries no tags - REQ-O-023 and REQ-O-037 cover the context too:
--no-injection-protectiondrops its tags,--unmaskreturns its raw values - New acceptance criteria and an error-envelope example in REQ-F-035;
response-envelope.mdtells an agent never to follow instructions found in a taggeddataorerror.context.ErrorDetail.contextalready allows extra keys, soResponseEnvelopestays 2.1 with a description change only
Why: a failure's error.context routinely carries a wrapped program's stderr, yet REQ-F-035 tagged and masked external content only in data, so injected instructions and raw tokens reached an agent through the error path untagged (#38).
ResponseEnvelope 2.2, AuditLogEntry 1.1: mutating streams
- REQ-O-004: a streaming command declares
danger_levelsafeormutating; the framework refuses to register a streamingdestructivecommand, since a stream cannot ask confirmation per action (REQ-C-002) - REQ-O-004, REQ-C-003: on a mutating stream, every item line carries its own
effect, and the summary line carrieseffects, the number of events per effect value ({"created": 2, "noop": 1}). A buffered answer puts the events indataand the counts inmeta.effects - REQ-O-004:
--dry-runcovers the whole stream: every event reports awould_*effect (would_noopfor an unchanged one), the summary line carries"dry_run": true, and a failed dry-run stream's error envelope carriesmeta.dry_run: true - REQ-O-004: a mutating stream that fails after a live effect other than
noopends on an error envelope withretryable: false - REQ-C-007: a streaming mutating command MUST NOT accept
--idempotency-key, and the framework refuses to register one that does; every non-streaming mutating or destructive command still must accept it. REQ-C-002 names the exemption - REQ-O-030: a stream is one invocation and gets one audit entry, whose new optional
effectsholds the summary line's counts, or the counts of the events emitted before a failure ResponseMeta.effects(optional) makesResponseEnvelope2.2, andAuditLogEntry.effects(optional) makesAuditLogEntry1.1; earlier instances stay valid- §12 and the streaming guide describe the exemption and the per-event effect
Why: REQ-C-003's effect, REQ-C-007's replay, dry-run, and the audit entry all assumed one result per run, so a bulk import or sync that streams its changes had no contract for reporting, previewing, retrying, or auditing them (#34).
ManifestResponse 3.10: an output side-effect kind for a command's product
FilesystemSideEffect.typegains"output": a path the command writes as its product (a generated report, a rendered dashboard, collected evidence). The schema rejectsttl_secondsandclearable_withon anoutputentry- REQ-C-011:
tool status --show-side-effectslists anoutputpath;tool cleanupnever removes it, under any--scopeincludingall(REQ-O-027, REQ-O-028) - REQ-C-011 and REQ-C-002:
cache,log,temp, andoutputwrites are not state changes, so a command whose only writes are of these kinds staysdanger_level: "safe";credentialandconfigwrites make it at leastmutating - REQ-C-011 and REQ-O-001:
type: "output"declares a location the command chooses itself; a path the caller names per call with--outputis declared byoutput_fileand is not repeated as anoutputentry. A default location used when--outputis absent is anoutputentry - A producer that declares an
outputside effect emitsschema_version3.10; the other five kinds are unchanged, so earlier manifests stay valid
Why: a command that writes its product to a path it chooses had no fitting kind: declaring it cache let cleanup delete what the user asked for, leaving it undeclared broke REQ-C-011, and mutating over-declared a read-only command (#39).
ManifestResponse 3.11: child log on stderr
CommandEntry.stderr(optional,"child_log"; absent means the framework's own diagnostics only) marks a command that streams a wrapped program's output (ansible-playbook,terraform apply) to stderr as plain text, line by line, regardless of--formatand verbosity- REQ-F-038 names the exception: auto-quiet,
--verbose, and--debugleave a declared child log streaming,--quiet(REQ-O-008) still silences it, and stdout still carries only the envelope. A command without the declaration is unchanged - The schema and REQ-C-031 reject
child_logon a passthrough command, whose stdout belongs to the delegated tool and whose envelope is the last line of stderr - An agent may discard stderr for such a command and never reads stderr text as a failure signal; the exit code and the envelope stay authoritative
- A producer that sets
stderremitsschema_version3.11; earlier manifests stay valid
Why: a command that wraps a tool whose log a person reads later had to break auto-quiet silently or hide the log, and the closed CommandEntry left only its description to warn an agent that stderr would be busy (#35).
ManifestResponse 3.12: media types of format values
- REQ-O-001 fixes a media type for each spec format value:
json→application/json,jsonl→application/x-ndjson,tsv→text/tab-separated-values,plain,table, andid→text/plain FlagEntry.media_types(optional, rootformatflag only): a map from format value to lowercasetype/subtypemedia type. Required for every value outside the spec's table; a spec value listed there maps to the table's media type, and every key is one ofenum_values. The schema rejects it on any other flag and on a non-enumflagCommandEntry.output_media_types(optional) is the same map beside REQ-O-049'soutput_formats, which keeps its type; it is required for a command-specific value that neither the table nor the root map covers, and overrides the root map for that command- An agent parses only output whose media type is
application/json,application/x-ndjson, or a+jsontype, and treats every other format's output as an opaque artifact - A producer that sets either map emits
schema_version3.12; earlier manifests stay valid
Why: a tool can register its own --format values beside the spec's (html for a page a person reads), and an agent had no way to learn from the manifest that such a value is not JSON, short of parsing the flag's description (#36).
ManifestResponse 3.13: every environment variable has a home
- Root
secret_env_vars(optional, a string array of names, never a value or default) lists the secrets every command reads, such as a tool-wide API key. A name there appears in no command'ssecret_env_varsand no flag'senv_vars; a command's secrets are the root list plus its own - REQ-F-073 names four homes in order: an auth command's
token_env_vars, asecret_env_vars(root or command), a flag'senv_vars, and rootenv_vars. When a variable fits two, the first wins: a token an auth command accepts is not repeated in that command'ssecret_env_vars, a secret never backs a flag'senv_vars, and a flag-backed variable never sits in rootenv_vars. This replaces the "exactly one of three" wording - Root
env_varsfollows theFlagEntry.env_varsrule: an entry without the tool prefix, such as an ecosystem'sLEDGER_FILE, is allowed only right after the entry for the same setting's prefixed name, which the tool reads first. The criterion that every root entry carries the prefix is replaced; new acceptance criteria cover the order and the root secret list - The universal exceptions gain
PWD,COLUMNS,ALL_PROXY, the lowercasehttp_proxy,https_proxy,no_proxy, andall_proxy,REQUESTS_CA_BUNDLE,SSL_CERT_FILE,GITHUB_ACTIONS, andJENKINS_URL; the naming guide's table lists them - A producer that sets root
secret_env_varsemitsschema_version3.13; a pre-3.13 manifest stays valid, and on it an absent rootsecret_env_varsmeans unknown, not none
Why: implementing ManifestResponse 3.5 left a tool-wide secret with no home, forbade a root setting's established unprefixed name that a flag could declare, left token_env_vars out of the "three homes", and made tools that read COLUMNS or a CA bundle variable non-conforming (#37).
ManifestResponse 3.14: a command's own flag confirms execution
CommandEntry.confirm_flag(optional string) names a boolean flag of the command or root, such asyes. Without that flag the command previews under the dry-run contract (would_*effect,meta.dry_run: true, exit0, no side effects) and prompts for nothing; with it the command runs, withmeta.dry_run: falseandmeta.confirmed: true(REQ-O-048)--dry-runwins:--dry-run --yespreviews, andtool exec --dry-runpreviews a dispatchedconfirm_flagcommand even when its line passes the flag (REQ-O-050)- The framework refuses
confirm_flagat registration on asafecommand, next tosafe_default: true, on a passthrough command (REQ-C-031), on a command without a dry-run path, and when it names no declared boolean flag. The schema rejects the first three - With REQ-O-021 enabled, a destructive command's
confirm_flagisconfirm-destructive, and without it the command previews and exits0instead of exiting2withCONFIRMATION_REQUIRED. REQ-C-005's--yesstays a no-op on commands that never prompt, except whereconfirm_flagisyes - REQ-O-048 is retitled "High-Stakes Commands Default to Dry-Run Mode" and covers both
safe_defaultandconfirm_flag;safe_defaultis unchanged - A producer that sets
confirm_flagemitsschema_version3.14; a pre-3.14 manifest stays valid, and on it an absentconfirm_flagmeans unknown, not "runs without a flag"
Why: a mutating command that previews unless its own --yes is given could state that only in the flag's description, because safe_default is a boolean, destructive-only, and names --live. An agent reading the manifest expected a bare call to run (#40).
ManifestResponse 3.15: idempotent commands
CommandEntry.idempotent(optional boolean, absent meansfalse): a repeat with the same arguments converges on the same state, whatever a previous attempt left behind. A delete of a named resource qualifies as much as a file rewrite; on asafecommand the field is redundant and accepted without warning (REQ-C-002)- The
ExitCodeEntryinvariant is unchanged: a partial-failure exit of an idempotent command staysretryable: false,side_effects: "partial", becauseretryable: truestill promises that nothing was written - New agent rule: on a non-retryable exit whose entry declares
side_effects: "partial"from a command declaredidempotent: true, rerun the identical command once without inspecting state, and stop if it fails with the sameerror.code. The rule never coversARG_ERROR (2)or an error carryingfix_requiredorfix_command.guides/recoverable-errors.mdgains a convergent rung, andexit-code-entry.md,exit-code.md,manifest-response.md, §12, andchallenges/triage.md's default retry policy state the rule - REQ-C-002 and REQ-C-007 separate
idempotentfrom--idempotency-key, which deduplicates one request and returnseffect: "noop"; a command declaredidempotentstill accepts the key - A producer that sets
idempotentemitsschema_version3.15; a pre-3.15 manifest stays valid
Why: a mutating command that is safe to rerun after a partial failure (a snapshot writer that rewrites one file per server) had nowhere to say so, and its only honest exit declaration, retryable: false with side_effects: "partial", sent agents to inspect state before every rerun (#41).
A boolean flag given as false is not present
- REQ-C-026: a boolean flag is present only when its value is true. An explicit false (
--no-exact, or false through any other explicit input channel) is the same as leaving the flag out, forif_present,default_when_absent,any_of, andone_ofalike if_valuecompares values, soif_value: falsestill matches an explicit false- New acceptance criterion: with
one_of: ["exact", "fuzzy"],--no-exact --fuzzypasses Phase 1, and--no-exactalone exits 2 ConditionalRuledescriptions inmanifest-response.jsonandmanifest-response.mdstate the definition;ManifestResponsestays 3.15 with a description change only
Why: REQ-C-026 defined a flag as present when the caller supplies it, so --no-exact could be read as choosing exact in a one_of(exact, fuzzy) group, and two conforming frameworks could accept and reject the same call (#50).
ManifestResponse 3.16: protocol servers on stdout
- New REQ-C-032: a command that serves a long-lived protocol over stdio (
tool mcp serve, a language server) declaresCommandEntry.stdout: "protocol"and names the protocol inCommandEntry.protocol, a lowercase kebab-case string with documented valuesmcp-stdio,lsp, anddap. Each field requires the other; absentstdoutmeans stdout carries envelopes, as before - Stdout is the protocol channel from the first byte and never carries an envelope. A failure before serving begins writes nothing to stdout: it exits with a declared code, and in JSON mode the envelope is the last line of stderr. An invalid flag exits
2and the server never starts - Exit
0is a clean shutdown (stdin end-of-file or the protocol's own shutdown sequence) and writes no envelope; any other exit ends stderr with the envelope in JSON mode, and a signal exits128 + N. A malformed request is answered inside the protocol, never by exit2 - A protocol server is a foreground process, not a background process (REQ-C-010) or an async job (REQ-C-022). The wall-clock timeout covers only the time before serving begins; the client ends the session by closing stdin or sending a signal
- The schema and the framework reject
stdout: "protocol"next toarguments: "passthrough",stderr,stdin,interactive: true,streaming_default: true,output_file,output_schema,output_formats,output_media_types,async: true, the REQ-C-010 fields,confirm_flag, orsafe_default: true. A protocol command takes no--idempotency-key(REQ-C-007) and, even whendestructive, no--dry-run(REQ-C-004) - An agent that sees
stdout: "protocol"starts the command only as a client of the named protocol and never parses its stdout as an envelope - A producer that sets
stdoutemitsschema_version3.16; a pre-3.16 manifest stays valid, and on it an absentstdoutcannot mark a protocol command
Why: a tool can ship its own protocol server as a command, such as an MCP server over stdio, yet CommandEntry had no way to say that its stdout is not an envelope, so an agent could try to parse one from MCP messages (#51).
1.10.0 — 2026-10-01
ManifestResponse 3.2: group rules in requires
ConditionalRulegains two shapes:{ "any_of": [...] }(at least one listed flag is present) and{ "one_of": [...] }(exactly one listed flag is present). Each lists at least two distinct flag names (REQ-C-026)- REQ-C-026 defines "present": the caller supplies the flag on the command line or through another explicit input channel the framework treats as supplied; a declared default does not count. New acceptance criteria: an
any_ofgroup with no flag present and aone_ofgroup with two or more present exit 2 before any I/O, aone_ofgroup with exactly one present passes, and theARG_ERRORmessage and details name every flag in the group - A
one_ofgroup replaces pairwiseprohibitedrules between its members - A producer that emits a group rule emits
schema_version3.2; the three existing shapes are unchanged, so a3.0or3.1manifest stays valid
Why: a command that takes one identifier from several flags (--isin, --figi, --symbol) could not declare that one is needed, so --schema did not reveal every required flag and REQ-C-026's own criterion failed for it (#14).
Cursor errors and audit-log paging are defined
- REQ-O-003: a
--cursorthe framework cannot honor (malformed or expired) exits2(ARG_ERROR) with error codeINVALID_CURSOR,retryable: false, and afix_requiredto rerun without--cursor, the same on every list command. New wire example and acceptance criteria - REQ-O-003: a token binds the query that produced it (filters and sort order); reusing it with a different query fails as
INVALID_CURSOR.--limitis not part of the query and may change between pages - REQ-O-030: the
audit-logcursor anchors on the last returned entry'stimestampplusrequest_id, never a file offset or rotated-file index. After pruning, the next page returns the older matches that remain, without error, andtotalmay shrink; entries appended after the first page never appear on later pages. The invalid-cursor criterion now names exit2andINVALID_CURSOR, and new criteria cover a token reused with other filters, pruning between pages, and an entry appended between pages
Why: both requirements said an invalid cursor "fails with a structured error" without an exit or error code, and left undefined what a token means under different filters or after rotation removes entries between pages, so frameworks would diverge and an agent could not branch on the failure (#15).
--output writes a binary result's raw bytes
- REQ-O-001: when a command's result is a single binary value (REQ-F-017) and
--output <path>is given, the file gets the raw bytes through the atomic write (REQ-F-070), and--formatselects only the representation of the response on stdout - The envelope's
datadescribes the write:{path, bytes, content_type, sha256};content_typeis present only when the command declares one --output -on a binary result exits2and writes nothing; without--output, the payload stays base64-encoded in the envelope as before- Commands whose result is not binary are unchanged:
--formatstill selects the file's representation
Why: a downloader, exporter, or renderer's result is a file. Writing it "in the --format representation" produced a JSON or plain wrapper around base64, so such commands rolled their own path flag and file write and lost the atomic write, the no-file-on-failure guarantee, and a uniform envelope (#19).
ManifestResponse 3.3: file output is marked
CommandEntry.output_file(optional,"formatted"or"binary") is present on every command that registers--output <path>, so an agent knows before the call whether the file gets the--formatrepresentation or the raw bytes (REQ-O-001)- A producer that sets
output_fileemitsschema_version3.3; a3.2manifest stays valid
ManifestResponse 3.4: a flag declares the environment variables it reads
FlagEntry.env_vars(optional, an array of{name, deprecated?}) lists the variables a flag reads when it is not passed, in precedence order: the first one set wins, and a passed flag beats them all. Rootflagsuse it too, so--formatlistsTOOL_FORMAT(REQ-O-042)- Agents set the first non-deprecated name instead of parsing the flag's
description; on a pre-3.4 manifest an absentenv_varsmeans unknown, not none secret_env_varskeeps its meaning: a secret is never a flag value (REQ-C-016), so its variable never appears inenv_vars- A producer that sets
env_varsemitsschema_version3.4; a3.3manifest stays valid
Declared environment variables without the tool prefix
- REQ-F-073: a flag may read a variable outside the tool prefix and the universal exceptions, such as a service's established name or one shared across a tool family (
CLOUDFALL_PROJECT), only when the manifest declares it in that flag'senv_varsand the tool-prefixed name is listed first. The framework rejects a registration that reads an undeclared one - REQ-F-073's promise that the manifest lists every recognized variable now points at
env_varsandsecret_env_vars; its wire format no longer shows anenvironmentfield the schema never had
Why: frameworks wrote "(read from $A or $B when not passed)" into flag descriptions because FlagEntry allowed no other place, and REQ-F-073 forbade the borrowed names outright. Contamination comes from variables a tool reads without saying so; a declared name is visible to the agent (#20).
ManifestResponse 3.5: variables that back no flag are declared
- Root
env_vars(optional, an array of{name, deprecated?, description}) lists every variable the tool reads that backs no flag and supplies no secret, such as<TOOLNAME>_DEBUG,<PREFIX>AUDIT_LOG, and<PREFIX>SESSION_ID. Each entry requiresdescription; universal names such asNO_COLORandHOMEare not listed EnvVarEntrygains an optionaldescription, which stays optional in a flag'senv_vars- REQ-F-073: every recognized variable appears in exactly one of root
env_vars, a flag'senv_vars, orsecret_env_vars. New acceptance criteria; the wire example shows rootenv_vars - REQ-F-051 and REQ-O-030 declare their variables in root
env_varswhen no flag backs them, with new acceptance criteria - A producer that sets root
env_varsemitsschema_version3.5; a3.4manifest stays valid, and on it an absent rootenv_varsmeans unknown, not none
Why: REQ-F-073 promises that tool manifest lists every variable the tool recognizes, but after 3.4 the manifest had homes only for flag-backed variables and secrets, and its root rejects unknown fields, so <TOOLNAME>_DEBUG and the audit log's variables could not appear anywhere (#23).
ManifestResponse 3.6: handler-written and envelope --output files
CommandEntry.output_filegains two values:"handler"(the command handler writes the file; the command's own documentation describes its contents and--formatdoes not select it) and"envelope"(the framework writes the finalResponseEnvelopeas JSON to the file whatever--formatselects;--formatshapes only stdout) (REQ-O-001)- The key stays on every command that registers
--output <path>, so absence still means the command has no--output. New acceptance criterion: an"envelope"command given--format plainstill writes a JSON envelope to the file - A producer that emits either value emits
schema_version3.6; earlier manifests stay valid
Why: "formatted" and "binary" both assume the framework renders the result into the file, so a command whose handler owns the path, or whose stdout carries something other than an envelope and saves the envelope to --output, fit neither and had to mislabel itself or omit the key, which reads as "no --output" (#27).
ManifestResponse 3.7: object flags and the base of a relative --output
FlagEntry.typegains"object": the value is one argv token of JSON text, as--raw-payloadtakes it (REQ-O-032). The newFlagEntry.schemaholds the draft-07 schema of one value: the object for anobjectflag (required there), one item for anarrayflag of objects; it appears on no other type (REQ-C-015)- REQ-C-015: a JSON-object flag should declare
type: "object"rather thantype: "string"with the shape in prose (astringdeclaration stays conforming); for anobjectflag the framework validates the value againstschemain Phase 1 and exits2(ARG_ERROR) on text that is not JSON or does not match. New acceptance criteria cover both CommandEntry.output_file_base(optional,"cwd","project_root", or"resource") names the directory a relative--outputpath resolves against; it appears only withoutput_file, and absence meanscwd. An absolute path is used as given. A command whose base is notcwdmust declare it (REQ-O-001)- A producer that sets either field emits
schema_version3.7; earlier manifests stay valid
Why: a flag that takes a JSON object had to declare itself a string and describe the shape in prose, and an agent writing to a project-relative --output from a subdirectory looked for the file under its working directory (#24).
ManifestResponse 3.8: line-mode stdin and records consumers
- REQ-F-054: the 65536-byte cap (
TOOL_MAX_STDIN_BYTES, exit2STDIN_TOO_LARGE) now names buffered stdin, and is unchanged there. A command that declares line mode (stdin_input: "lines"or"records") reads nothing before the handler runs and gets one line at a time, with no total cap and a per-line cap (default 1048576 bytes). A longer line exits1withLINE_TOO_LARGEandcontext.line:1, not2, because the handler has already started (REQ-F-002) - REQ-F-054 and §61: line mode avoids the pipe deadlock only when a separate process writes stdin, as in a shell pipeline; a caller that writes stdin and reads stdout from one thread still uses
--input-file - REQ-O-039:
--input-fileis registered on line-mode commands too and reads the file as lines with the same per-line cap;--input-file -reads stdin as lines - REQ-O-004: a stream ends with one terminal line, the
_summaryline on success or the errorResponseEnvelope(ok: false) when the command fails after its first line - REQ-O-004: a records consumer skips blank and heartbeat lines (REQ-O-038), validates each other line against its record type, and stops at the
_summaryline. An upstream error envelope exits1withUPSTREAM_FAILED(context.line,upstream,upstream_exit_code), end of stdin without a terminal line exits1withUPSTREAM_INCOMPLETE(context.line,records), and an invalid line exits1withRECORD_INVALID(context.line,field); all carryretryable: false. Read through--input-file <path>, end of file ends the input without a_summaryline CommandEntry.stdin(optional,{mode, max_bytes?, max_line_bytes?, record_schema?}) declares how a command reads stdin:bufferedwithmax_bytes,lineswithmax_line_bytes, orrecordswithmax_line_bytesand a requiredrecord_schema. An agent picks a pipe or--input-fileand matches a producer's items torecord_schemabefore the call- REQ-F-065 links to REQ-O-004 for pipelines the caller's shell runs, and §56 names the records consumer errors as a framework defense
- A producer that sets
stdinemitsschema_version3.8; older manifests stay valid
Why: REQ-F-054 capped every stdin read at 64 KiB, so an NDJSON pipeline of 300 records of 320 bytes failed with STDIN_TOO_LARGE, and a consumer of another command's stream could not tell a failed or truncated upstream run from short valid input (#26).
ManifestResponse 3.9: passthrough commands
CommandEntry.arguments(optional,"declared"or"passthrough"; absent means"declared") marks a command that hands every token after its path to another tool's parser. The schema requiresoption_placement: "strict", emptyflags, and nopositionalson a passthrough command, and rejectsdanger_level: "destructive"on itCommandEntry.help_argv(optional, passthrough only) is the argv forwarded in place of a lone--helpor-hafter the command path- New REQ-C-031 collects the passthrough rules. Framework options go before the command path. In JSON mode the final envelope is the last line of stderr, and the
--outputfile when given; stdout belongs to the delegated tool. The process exits with the tool's own code, and a non-zero one giveserror.code: "DELEGATED_EXIT",data.exit_code, andretryable: false - REQ-F-001, REQ-F-002, REQ-F-004, REQ-F-006, REQ-C-001, REQ-C-003, REQ-C-006, and REQ-C-015 each name their exemption for passthrough commands only; declared commands are unchanged. A passthrough command MUST NOT be destructive, and the framework refuses that registration: it cannot preview what the delegated tool would change, so it cannot offer the dry run or confirmation preview a destructive command owes. The timeout, signal handling, session deduplication, and the audit log still apply
- REQ-O-030 and
AuditLogEntry.args: a passthrough command records its forwarded argv as"argv": "[OMITTED]". Descriptions only, soAuditLogEntrystays 1.0 - A delegated
2does not promise "no side effects":challenges/triage.mdrow 6, §14's workaround,exit-code.md, andresponse-envelope.mdtell an agent to decide fromerror.codeandretryable, not from the process exit code - A producer that sets
argumentsorhelp_argvemitsschema_version3.9; earlier manifests stay valid, and an absentargumentsreads as"declared"
Why: a command wrapping another tool's parser (beangulp's ingest) cannot put the envelope on stdout or map the tool's exit codes onto the framework table without breaking the tool's own scripts and documentation, and the manifest had no way to say so beyond option_placement: "strict" and a sentence in description (#25).
1.9.0 — 2026-09-30
Audit log and logger rotation defaults are recommendations
- REQ-O-030: configuring the maximum size, rotated file count, and maximum age stays MUST; the default values (10 MB, 5 files, 30 days) become SHOULD. A framework may ship smaller defaults, but its default size bound
max_size × (max_rotated_files + 1)should not exceed 60 MB. New acceptance criterion: the application can set all three bounds - REQ-F-042: the same wording for the framework logger's defaults (100 MB, 5 files, 30 days); it states the derived bound and that its defaults do not apply to the audit log
Why: both requirements put the defaults in parentheses inside a MUST sentence and no criterion tested them, so a framework could not tell whether smaller defaults conformed. Frameworks also read REQ-F-042's defaults (600 MB bound) as the audit log's (#7).
ManifestResponse 3.1: built-in commands are marked
CommandEntry.builtin(optional boolean, defaultfalse) istruefor commands the framework registers itself, such asmanifest,doctor, andaudit-log, and for their subcommands; an application command that replaces a built-in's name isfalse(REQ-O-041)- Agents filter on
builtinwhen building a task list or skill set instead of matching a hard-coded list of built-in names - A producer that sets
builtinemitsschema_version3.1; a3.0manifest stays valid and reads as having no marked built-ins (#9)
Audit log accepts framework conventions
- REQ-O-030: an entry's
commandequalsmeta.commandexactly, space-separated (config set) or dot-separated (config.set) as the framework spells it consistently;audit-log --commandaccepts both spellings and keeps the whole-word prefix rule in each. New acceptance criteria cover the dot form andcommandmatchingmeta.command - REQ-O-030:
audit-logis a list command. Its default buffered answer carriesdata.entriesandmeta.pagination(REQ-F-018);totalis the matched count ornull. It accepts--cursor: when--limitleaves out older matching entries,truncatedandhas_morearetrueandnext_cursorreturns the next-older page, oldest first within it; the last page hasnext_cursor: null. A framework may make it streaming-default under REQ-O-004, and--no-streamthen returns the buffered envelope; streamed output carries the pagination on its final summary line - REQ-O-030:
session_idcomes from the framework's one session variable, a prefixed variable it already reads for the agent session id (for example for REQ-C-007 idempotency keys), else<PREFIX>SESSION_ID; the framework documents its name AuditLogEntrychanges descriptions only (command,session_id): no instance valid before becomes invalid, so its contract version is unchanged
Why: a framework implementing REQ-O-030 (treaty) already spells meta.command as a dot path, streams list output, and reads a session id for idempotency keys. REQ-F-024 never pins meta.command's separator, so requiring the space form forced two spellings of one command in one CLI, and a second session variable would record a different value from the one the framework already trusts (#12).
1.8.0 — 2026-09-30
Breaking: --format selects output representation
- REQ-O-001 makes
--format <format>the canonical representation flag;--outputand-omust not select a format --output <path>is reserved for a destination file; a command that registers it must reject a bare format name (--output json) with exit2and a suggestion naming--format json- With
--output <path>,--formatselects the file's representation and stdout carries theResponseEnvelope - REQ-O-004 (
--format jsonl) and REQ-O-005 (--format id) follow the rename; REQ-O-042 reads<TOOLNAME>_FORMATinstead of<TOOLNAME>_OUTPUT - Every example, check, and agent workaround in the corpus uses
--format; references to real tools (aws --output json,kubectl -o json) are unchanged
Why: --output is a format in cloud CLIs (aws, kubectl, az) and a file path in build and transfer tools (gcc, curl, sort, pandoc). An agent that passes --output json to a path-typed flag gets exit 0, empty stdout, and a file named json. --format has one meaning wherever it appears.
Migration: rename the framework's global --output flag to --format; rename <TOOLNAME>_OUTPUT to <TOOLNAME>_FORMAT; rename any file-destination flag to --output <path> and add the format-name guard.
Breaking: ManifestResponse 3.0
- The root
flagsmap lists global options: flags every command accepts, before or after the command path (REQ-F-079) CommandEntry.flagsnow means command-local flags only; a global option never appears in it, and no local flag reuses a global name or short alias- A consumer that reads only
CommandEntry.flagsno longer sees--format,--quiet, or any other global option; the accepted set for a command is rootflagsplus its ownflags CommandEntry.positionalslists positional arguments in call order asPositionalEntryobjects (name,type,required,description,enum_values,variadic); before 3.0 a manifest had no place for them, so O-041's "construct any call from the manifest alone" could not hold for a command with positionals (REQ-C-015)schema_versionmust be3.x, so a consumer can tell a 3.0 manifest from an older one before readingflags; every example and the good democli mock emit"3.0". Earlier examples emitted"1.0"under the 2.x contract, so consumers treat any value other than3.xas pre-3.0 rather than looking for a2.prefix
Why: the field keeps its shape but changes meaning, which the versioning rules above treat as a MAJOR change. A 2.x consumer that builds calls from CommandEntry.flags alone would conclude that --format does not exist.
Migration: producers emit "schema_version": "3.0", move framework and application-wide flags from every CommandEntry.flags into the root flags map, and declare each command's positional arguments in positionals, in call order, instead of describing them in description. Consumers look a flag up in root flags first, then in the command's flags, place root flags before the command path, and give positionals in array order after the local options.
Breaking: the audit log is opt-in (REQ-F-026 retired)
- REQ-F-026 (append-only audit log) is merged into REQ-O-030 and its ID is retired; the corpus has 158 requirements (78 REQ-F)
- As a Framework-Automatic requirement, the log made every CLI append to the user's home directory on every invocation, unbounded and without the author knowing. REQ-O-030 keeps it off by default, enabled by the application or by the operator through
<PREFIX>AUDIT_LOG
Migration: stop writing the audit log unconditionally; write it only when the application calls enable_audit_log() or the operator sets <PREFIX>AUDIT_LOG.
New failure mode: §78 Output Flag Meaning Collision
- §78 covers an agent passing
--output jsonor-o jsonto a tool whose--outputtakes a path: exit0, empty stdout, and a stray file namedjson - Triage row 16 routes the signal (exit
0, no JSON, a file named after a format value) to §78; the catch-all row becomes 17 - REQ-O-001 lists §78 as a source; its format-name guard on path-typed
--outputis the framework fix
Argument order and global options
- New REQ-F-079 (Global Option Scope): global options are listed once in the manifest root
flags, accepted in any position on every command path, and never overwritten by a subcommand default; a command-local flag that reuses a global option's long name or short alias fails registration - REQ-F-067 adds two acceptance criteria:
--ends option parsing, and a scalar option repeated with different values exits2. Its framework examples are corrected: argparse and Click already accept options after positionals; their real gap is root options after the subcommand, whichparse_intermixed_args()does not fix - REQ-C-027 gives
strictone meaning: every option, global or local, precedes the first positional and may follow the command path. The criterion that a strict command rejects later options with exit2is removed; those tokens are forwarded to the child, which is whatstrictdeclares - §69 is rewritten around four modes (global option after the command path, local option before it, option read as a positional, value overwritten or duplicated). The Agent Workaround moves from "front-load every flag", which breaks local flags, to the canonical order
tool <global> <command path> <local> [--] <positionals>, and from Tier A to Tier B - The conformance kit adds
argument_order(level 3, REQ-F-067, REQ-F-079): a profile's optionalargument_ordernames a read command and a global option; the kit moves the option around the command path, detects a value overwritten by a subcommand default, and expects exit2for a conflicting repeat. An optionalpositionalalso proves a local option after a positional is parsed, not read as a second positional (§69 Mode 3). The good democli mock parses--format json|plainglobally, lists it in its manifest rootflags, anddeployments listtakes an optional environment positional cli-agent-diagnoseroutes to §69: a flag error followed by the same tokens succeeding in another order is §69, not §52; exit2after a single-value option (--format,--limit,--timeout, and a few more) repeated with different values is §69;runner.preflightflags that repeat before the call. Repeatable options such as--header,--env, and curl's--outputnever count
Why: "front-load all flags" traded Mode 1 for Mode 2, and nothing in the manifest told an agent which flags were global. The argparse default-overwrite case exits 0 with the wrong format.
Migration: move framework flags from each CommandEntry.flags into the root flags; register global options with parent parsers using SUPPRESS defaults (argparse) or persistent flags (Cobra); rename any local flag that shadows a global name or short alias.
REQ-O-030 audit log: gaps closed, AuditLogEntry 1.0, ResponseEnvelope 2.1
<PREFIX>AUDIT_LOGaccepts only1,0, or an absolute path; any other value exits2withINVALID_AUDIT_LOG_SETTING.1keeps the application's path and falls back to the default; an absolute path overrides bothtool audit-logis registered on every CLI; with the log disabled it exits4withAUDIT_LOG_DISABLEDinstead of returning an empty list- Every invocation that resolves to a command is logged, including argument errors after resolution,
--validate-only,--dry-run, and refused destructive commands; unresolved commands are not operatorbecomessession_id, read verbatim from<PREFIX>SESSION_ID;trace_idis present only whenTOOL_TRACE_IDis set; a newwarningsfield records warning codes, so REQ-O-023 and REQ-O-047 events are queryable- Entries are capped at 16 KiB (oversized
argsvalues become[TRUNCATED], withtruncated: true); the maximum age applies to the active file as well as rotated ones;tool cleanupnever removes the log - The log file is created
0600and its directory0700; existing modes are left unchanged AUDIT_LOG_UNAVAILABLEgoes to stderr when the output has no envelope; the entry is written before the response is emittedaudit-logreturns entries oldest first,--limit nkeeps the newestn,--sinceaccepts<n>s|m|h|dor an ISO 8601 datetime, and--commandmatches a space-separated path or a whole-word prefix- New canonical schema
AuditLogEntry1.0 (schemas/audit-log-entry.json);ResponseEnvelope2.1 adds the optionalmeta.audit_log_path - REQ-O-023 emits an
INJECTION_PROTECTION_DISABLEDwarning whenever--no-injection-protectionis used; it andAUDIT_LOG_UNAVAILABLEjoin the standard warning codes
Why: two conforming frameworks could disagree on every point above, the size and age bounds did not hold for large arguments or rarely used tools, and the log was readable by every local user under a default umask.
ResponseMeta declares its REQ-F fields
tool_version,update_available(REQ-F-023),trace_id,command, andtimestamp(REQ-F-024) are declared as optionalResponseMetafields; examples using them are now type-checked. Part ofResponseEnvelope2.1
Validation
- The example validator and the conformance kit enforce draft-07
formatkeywords (date-timeviarfc3339-validator) and fail when the checker is unavailable, instead of passing any string - The example validator checks meta-only envelope fragments, standalone
ErrorDetailobjects, failure mode index entries, and exit-code entries that lack required fields; field sets for classification are read from the schema files - Corpus counters in report templates, the website, mkdocs, and the requirements index footer are checked by
validate_links.py - Skill reference bundles sync by checksum, not size and mtime
Tooling
cli-agent-diagnosedetects the §78 template echo:--format <value>exits0and every stdout line is that literal value- The preflight hook splits compound commands without surrounding spaces, reads
#as a comment only at the start of a word, strips trailing comments and backslash-newline continuations, and is checked against bash word splitting by a differential test cli-agent-readinessscores a pre-3.0 manifest as an older contract, not as schema-invalidManifestResponse3.0 adds an optional rootexit_codestable that every command inherits;CommandEntry.exit_codesthen holds only additions and overrides- Benchmark harness: scenarios S6 to S8 with graders,
--cli-dirand free-form--modefor builds outside the repo, and fixed S2 and S5 graders that counted argument errors and--helpas live calls
Comparison matrix
- §75–78 rows and rationale notes added; Part 2 score tables are recomputed from all 75 Part 1 cells
- Part 3 has one analysis section per matrix row, including §69–78
1.7.0 — 2026-09-15
Breaking: ResponseEnvelope 2.0
meta.exit_codeis required and must equal the process exit code;okis derived from it and the schema enforces the relationshipwarningsitems areWarningDetailobjects (code,message, optionalcontext) instead of stringsmeta.pagination(total,returned,truncated,has_more,next_cursor) replaces the top-levelpaginationobject andmeta.cursortool execoutput lines carry_cmdand_lineinmeta, not at the top levelErrorDetaildeclarescause,context, anddocs_url; structured facts move fromdetailobjects tocontexterror.codeis a domain code that may reuse anExitCodename; the exit code travels inmeta.exit_codedataon failure isnullunless the command declares a failure payload (REQ-C-009, REQ-C-028, REQ-O-026)
Migration: set meta.exit_code in the envelope factory; wrap each warning string as { "code": "...", "message": "<old string>" }; move pagination and cursor under meta.pagination; move _cmd/_line into meta; rename object-valued error.detail to error.context.
Breaking: ManifestResponse 2.0
CommandEntry.required_scopesis required (the doc already said so; the JSON did not)CommandEntryandFlagEntrydeclare every field the Command Contract and Opt-In requirements add; unknown fields remain rejectedexit_codeskeys must be integer strings
Migration: emit required_scopes: [] for commands without auth; rename any undeclared extension fields to the names in schemas/manifest-response.md.
Schema mechanics
- Every schema
$idequals its filename, so$refby filename resolves in ajv and jsonschema DispatchRequest._optsvalues useanyOf; integer overrides validate- New tooling schemas:
FailureModeIndex,ConformanceProfile,ConformanceResult
Contract fixes
- Framework signal handlers may exit
130(SIGINT) and143(SIGTERM); commands still may not use126–255 exit-code.mdno longer calls126–255safe to retry; signal exits require state inspection- On any disagreement between envelope and process exit code, failure wins (resolves a contradiction with triage row 2)
- REQ-F-045 rejects hallucinated input with exit
2, not3 - REQ-C-028 uses
CONFLICT (6)witherror.code: "ALREADY_EXISTS" - REQ-O-041 example moves
TIMEOUTto code10withretryable: false - REQ-F-022
meta.schema_versionisMAJOR.MINOR - IMPLEMENTING.md Path A no longer calls timeouts retryable
- README exit code example names the right codes
Added
requirements/levels.md: conformance levels 1 (12 requirements), 2 (allP0), 3 (all)conformance/run.py: deterministic conformance kit with eleven checks and level verdictschallenges/index.json: generated machine-readable failure mode taxonomy- CI gate: link, section, counter, snippet, level, schema, and example validation; skill bundle drift check; tests; ajv compile
- Benchmark harness v2: trials per cell, tool-log grading, per-trial state isolation,
--regrade, rendered results cli-agent-diagnose: classifier readschallenges/index.jsonand a rule table (signal_rules.py) covering 31 failure modes, one or more rules per triage row; trace capture and analysis scripts (filter.py,analyze.py,stats.py,traj.py); OpenRouter models for--llm
Changed
cli-agent-diagnoseclassifies a shellcommand not foundas §20 (missing dependency) per triage row 4, not §52
Known gaps
- §70 (single-argument arity) has no requirement
1.6.0
Baseline before this changelog: 74 failure modes, 158 requirements, triage and recovery layer (Signature, Tier, Fallback lines; fix_command; extract_envelope).