strictcli v0.39.0 /Changelog
On this page

#Changelog

#0.39.0

Consequential commands now require explicit consent on the programmatic and MCP channels, tool descriptors publish their effects classification, and approve_consequential becomes a reserved parameter name.

Context

The confirmation requirement was a CLI-only property: a command marked consequential prompted (or refused) when it was reached through argv, but the same command was reachable with no consent at all through as_tools(), call(), acall(), Tool.execute and MCP tools/call. An agent driving the app over MCP therefore ran exactly the commands the requirement exists to stop for a human decision. This release makes consent a property of the command rather than of the channel: every call path refuses a consequential command unless the caller states consent, and the descriptors published to tool and MCP consumers carry effect and consequential so a caller can see the requirement before it calls.

Reserving approve_consequential follows from that. The name is now framework vocabulary in all three implementations, so a command declaring its own flag or positional arg of that name would shadow the consent parameter -- and in Python a positional arg of that name was already unreachable over call() while the same command stayed callable over MCP. Registration now rejects it outright.

Both changes are breaking, which under pre-1.0 versioning is a minor bump. The three implementations ship the same behavior in lockstep, verified by the conformance suite.

#Breaking

  • [strictcli] Tool export and programmatic calls honour the confirmation requirement. as_tools() descriptors and MCP tools/list now publish effect and consequential beside the argument schema, so a caller can see which tools require confirmation. call(), acall(), Tool.execute and MCP tools/call refuse a consequential command unless the call states consent (approve_consequential=True, or the approve_consequential param on an MCP tools/call).
  • [strictcli] **approve_consequential is a reserved parameter name.** Declaring a Flag or an Arg named approve_consequential is now a registration-time ValueError. The name is how a caller states consent to a consequential command through call(), acall(), Tool.execute and MCP tools/call; a positional arg of that name was unreachable over call() (the keyword-only consent parameter swallowed it) while the same command stayed callable over MCP.

#Features

  • [strictcli] The built-in config subcommands (path, show, set, edit, init) now explain what they actually do — precedence order, type coercion, comma-escaping and dry-run behavior — instead of one terse line.

#0.38.0

Receiver-aware effects-bypass lint: platform.system() is no longer a false positive, and aliased or from-imported receivers are now caught.

Context

The effects-bypass check matched bare leaf names, so any call named system was reported as an escaped effect -- including platform.system(), a pure in-process read whose suggested remediation could not be followed. The leaf is now a finding only through the os receiver (os.system, import os as o, from os import system).

Resolution runs through the module's own imports, which also closes the opposite gap: import requests as rq + rq.post(...) and from subprocess import run + run(...) were previously missed and are now caught. Builtin open is answered before import resolution so it keeps its own verdict.

Consumers pick the fix up on their next lock bump.

#Fixes

  • [strictcli] **effects-bypass no longer flags platform.system().** The system leaf is now a finding only through the os receiver (os.system, import os as o, from os import system), so a pure in-process read no longer produces a finding whose remediation could not be followed. Receivers are also resolved through the module's imports, so import requests as rq + rq.post(...) and from subprocess import run + run(...) are now caught.

#0.37.0

Adds the dry_run_supported=False command declaration -- a mutating command can now refuse --dry-run with a mandatory reason carried into help and the schema -- and corrects a Python README that documented a handler signature, a check API and a dependency claim the framework no longer has.

Context

--dry-run is a promise: the preview must describe what the real run would do. Some mutating commands cannot keep that promise, because their effects are not representable ahead of time. Until now such a command either rendered a preview that misrepresented it or hand-rolled a refusal inside the handler -- after parsing, invisible to --help and to --dump-schema. dry_run_supported moves the refusal to the registration site, where the framework can enforce it: the reason is mandatory, it is rendered in a dedicated Dry run: help section, it is emitted in the schema, and declaring it on a read_only command is a registration-time error (a command that changes nothing has nothing to preview).

The same release completes a documentation truth pass. Every registration example was re-checked against the shipped API, and the reserved flag quartet, the effects regime and the consequential confirm protocol -- previously explained nowhere -- are now documented. Examples are no longer trusted by inspection: the self-contained example programs are marked validate, and selfdoc check now assembles and RUNS each one against the checkout, so documentation that stops working fails the release rather than rotting quietly.

#Features

  • [strictcli] **dry_run_supported=False command declaration.** A mutating command whose effects a preview cannot honestly represent can now declare dry_run_supported=False with a mandatory dry_run_unsupported_reason. --dry-run is then refused at parse time with the reason instead of rendering a preview that would misrepresent the real run; the reason also appears in a new Dry run: help section and in the schema. Declaring it on a read_only command is a registration-time error.

#Fixes

  • [strictcli] The Python README now matches the shipped API. It documented the pre-ctx handler signature, omitted the mandatory effect= classification from every example, used flag names the framework reserves (verbose, quiet), claimed zero dependencies despite requiring tomlkit, claimed bool flags default to False when they are required without an explicit default, and described the removed @app.check/CheckResult API. It now also documents the effects regime, the reserved flag quartet, dry_run_supported and consequential.

#0.36.0

Confirmation keys on a declared consequential, not on mutating

Context

The shipped regime INFERRED "mutating => must confirm". Adoption across six consumers falsified that inference by an order of magnitude: 391 of 624 commands (63%) classify mutating, so two thirds of every CLI in the fleet prompted -- including safegit commit, rlsbl changelog add, selfdoc gen, saferm delete (the safe, undoable deletion) and claudewheel launch, which had to be special-cased or every session would have opened with a Proceed? [y/N]. The genuinely dangerous commands are roughly 5-10% of that set: a ~1:10 signal-to-noise ratio, which guarantees the skip flag gets passed reflexively and leaves the guardrail dead while it still looks present.

The diagnosis: one field was answering two questions. effect correctly answers "should a dry run record rather than execute?" -- that is working and is unchanged. Consequence asks "are these effects worth interrupting someone for?" Almost everything has effects; very little is consequential. effect was only ever a proxy for the second question, and the proxy is off by 10x.

Commands now declare consequential=True and the framework prompts for those and no others. The declaration is deliberately named after a property of the COMMAND rather than after the framework's reaction, so other behaviours can hang off the same fact later. The skip flag --approve-consequential is deliberately unwieldy: a flag that cannot become muscle memory stays a decision. yes stays on the banned-names list so nobody reintroduces a private --yes meaning the same thing.

#Breaking

  • [strictcli] **Confirmation now keys on a declared consequential, not on mutating.** Commands opt in with consequential=True on app.command(...); a plain mutating command never prompts. The skip flag is --approve-consequential (--yes is gone, and the name yes stays banned for command and global flags); ctx.yes becomes ctx.approve_consequential. Declaring consequential on a read_only command is a registration-time error, and the new consequential-grant-agreement check warns when a command declares a process- or network-mutating grant without declaring itself consequential.

#0.35.4

Dry-run previews render on every exit path out of a handler

Context

A consumer migrating onto the effects regime found that a dry-run preview vanished whenever the handler left through anything other than a normal return. The would-do log was rendered only on the success path, so a handler that recorded its intended writes and then exited non-zero -- the ordinary shape of a validation command -- printed nothing at all. Safety was never at risk (dry mode still executed nothing), but the preview property was: silence is indistinguishable from 'this command would do nothing', and those runs are exactly the ones a reader most wants to see. The render is now owned by the single seam every dispatch already passes through in each language, so it cannot be missed by a path that forgets to call it. An unexpected crash still renders what was recorded, followed by a new stderr marker saying the preview may be incomplete; the exception itself is never swallowed. A handler that terminates the process itself (Go os.Exit, Node process.exit) remains outside the guarantee and is recorded as a ceiling in the effects contract.

#Fixes

  • [strictcli] Dry-run previews no longer vanish. A mutating handler that recorded effects and then exited through sys.exit or an uncaught exception printed nothing at all under --dry-run; the would-do log now renders on every exit path, and an unexpected crash marks the preview as possibly incomplete on stderr.

#0.35.3

Recognize the reserved flag quartet anywhere in argv

Context

The quartet (--dry-run, --yes, --quiet, --verbose) was extracted by a pre-scan that stopped at the first non-flag token, so it was only recognized BEFORE the command name -- while every documented invocation in this ecosystem writes these flags after it. myapp deploy --dry-run failed with unknown flag '--dry-run', and the first consumer to migrate onto the effects regime had to rewrite argv before handing it to the framework to make its own documented commands work.

The quartet is now recognized anywhere in argv, matching the framework's own existing precedent for --help/-h. Two boundaries are preserved: a bare -- (everything after it is positional data) and a passthrough command's name (its args belong to the child process and are forwarded byte-for-byte, so a child's own --verbose is never eaten). --hermetic, --config, --dump-schema and --mcp stay pre-command-only. Effects contract §7.2 is amended accordingly (adoption ruling A1, §18.6).

#Fixes

  • [strictcli] The reserved flags now work after the command name. --dry-run, --yes, --quiet and --verbose are recognized anywhere in argv, exactly like --help -- myapp deploy --dry-run used to fail with unknown flag and now works, at any group nesting depth. A bare -- still ends recognition, and a passthrough command's args are still forwarded to the child untouched.

#0.35.2

Ship the 0.35.0 library content to PyPI, which neither 0.35.0 nor 0.35.1 reached.

Context

0.35.0 and 0.35.1 are both phantom versions: each has a git tag and a GitHub Release, and neither has an artifact on PyPI. This release ships that same library content -- unchanged since 0.35.0 -- to the registry.

0.35.0 failed because it was released as a three-releasable batch that pushed three separate candidate commits, and all three tags ended up on the final candidate. That commit changed nothing under python/, so the CI Router's paths filter skipped strictcli-ci, and the publish gate refused: a skipped check proves nothing about the commit. The batch defect is fixed upstream in rlsbl, which now commits every member and pushes exactly once.

0.35.1 failed differently. Its candidate was pushed and CI went green on it, but a commit from a concurrent session landed on the branch during the CI wait and the release aborted at its foreign-commit guard -- correctly. Resuming that release then tagged the branch tip instead of the CI-verified candidate it had recorded, so the tag pointed at a commit that had never produced any CI check runs at all, and the gate refused again. Both refusals were the gate working as designed; nothing was relaxed or bypassed.

This release is a clean single-releasable run from a quiet branch. Its version bump touches python/, so strictcli-ci runs on the tagged candidate and the gate can pass honestly.

There are no user-facing changes. 0.35.0's user-facing entries are already finalized into its own changelog, and the library code is byte-identical to what 0.35.0 and 0.35.1 would have shipped. This is infrastructure only: its sole purpose is getting that content to the registry.

#Infrastructure

  • Ship the 0.35.0 library content to PyPI, which neither 0.35.0 nor 0.35.1 reached.

#0.35.1

Republish 0.35.0's library content to PyPI after the 0.35.0 tag was published but never reached the registry.

Context

0.35.0 was tagged and got a GitHub Release, but its artifact never reached PyPI.

The 0.35.0 release ran as a three-releasable batch. That batch pushed three separate candidate commits, and all three tags ended up pointing at the final candidate. On that final commit the CI Router's per-project paths filter saw no change under python/, so the strictcli-ci / test job was skipped. The Publish Router's gate treats a skipped CI check as a hard failure -- correctly, since a skipped check proves nothing about the commit -- and refused to publish. The gate behaved exactly as designed; the defect was upstream, in the batch push.

The root cause is fixed in rlsbl: the batch path now commits every member and pushes exactly once, so the gated commit is the commit all tags point at and every member's CI actually triggers.

Retrying 0.35.0 is not possible: re-running CI on that SHA reproduces the same skip, because the SHA genuinely contains no python/ change. This release therefore ships the identical library content under a new version whose release commit does touch python/, so strictcli-ci runs on the tagged candidate and the gate can pass honestly.

There are no user-facing changes here: 0.35.0's user-facing entries are already finalized into its own changelog. This is an infrastructure release whose sole purpose is getting that content to the registry.

0.35.0 remains a phantom version -- a tag and a GitHub Release with no corresponding PyPI artifact. It was never installable and never will be.

#Infrastructure

  • Republish 0.35.0's library content to PyPI after the 0.35.0 tag was published but never reached the registry.

#0.35.0

The effects regime: mandatory command classification, framework-owned --dry-run/--yes/--quiet/--verbose, and side effects that flow through ctx.effects.

Context

A consumer command once accepted --dry-run into **kwargs, dropped it on the floor, and published a package to a public registry. Nothing in the framework could have caught that: honoring a dry run was a convention, and a convention is something a handler can forget. This release makes it structural.

Every command now declares effect="read_only" or effect="mutating" at registration -- no default, no inference from names or tags, a hard error if you omit it. The four reserved flags stop being something each app spells for itself; the framework owns --dry-run, --yes, --quiet and --verbose, so they mean the same thing in every strictcli program, and declaring a flag by one of those names is now a registration error. Side effects ride ctx.effects: run, spawn, write, mkdir, remove, rename, chmod, http. In a dry run the handle records instead of performing and the framework prints the would-do log; in a real run the same handler code executes for real. Handlers never branch on a dry_run kwarg again, because the mode lives in the handle rather than in an argument somebody has to remember to read.

The hard part of previewing is what a recorded mutation returns. Rather than guess, the handle returns an Unsettled carrier. Forward it into a later effect and the preview continues with the provenance rendered inline -- step-output and stale-value brands appear in the log line itself. Try to read it or branch on it and you get a hard error and a truncated preview that says exactly where the preview stopped and why. A short honest preview beats a long invented one, and no amount of inference would have made the guess trustworthy.

Two shipped checks keep the seam visible: effects-bypass follows the call graph reachable from every registered handler and reports direct ambient effects, and observe-allowlist-breadth names any single-token observe prefix broad enough to exempt a whole binary.

This is a breaking release by design: every consumer's commands are unclassified today and will fail registration at their lock bump. That is deliberate -- a silent grace period would have left exactly the bug class this regime exists to kill. The fleet migrates in a dedicated wave right after this release, and the same regime lands in the Go and TypeScript implementations in this same coordinated release, so the three stay in lockstep.

#Breaking

  • [strictcli] Breaking: the effects regime. Every command must now declare effect="read_only" or effect="mutating" at registration. --dry-run, --yes, --quiet and --verbose are reserved framework flag names, delivered on the Context (ctx.dry_run, ctx.yes, ctx.quiet, ctx.verbose) and gating ctx.info/ctx.debug. ctx.effects mints the eight recorded operations (run, spawn, write, mkdir, remove, rename, chmod, http); under --dry-run they are recorded, not executed, and rendered as a would-do log, with Unsettled carriers that forward into later effects and truncate honestly when extracted from. Mutating commands prompt for confirmation unless --yes is passed. A **kwargs handler must declare forwarding=Forwarding(reason=...). A built-in effects-bypass check fails on direct process, filesystem or network calls inside effects-using handlers.

#Features

  • [strictcli] **New built-in observe-allowlist-breadth check (severity warn).** A single-token proc_observe_allowlist prefix such as ["git"] exempts an entire binary: every matching invocation executes for real under --dry-run, is never logged, and is legal inside a read_only command. The check names each one; --ignore-warnings clears it.

#Fixes

  • [strictcli] **config set, config init and config edit now honour --dry-run.** The framework's own mutating commands routed their writes (and config edit's $EDITOR launch) around ctx.effects, so a dry run printed "no changes were made" while rewriting your config file and opening your editor. Every mutation now rides the handle and is recorded, not performed.
  • [strictcli] Three effects-regime correctness fixes. (1) The dry-run preview no longer skips numbers: framework-blessed cache writes (the schema dump and test-coverage shards) consumed would-do sequence numbers they never rendered, so a coverage-instrumented dry run began its preview at 2. and every «step N output» brand and truncation "ends at step N" shifted with it — cache writes now carry their own counter. (2) ctx.effects.run's check and stream and ctx.effects.http's check now reject a forwarded carrier instead of silently ignoring it in dry mode. (3) Writing an attribute on an Unsettled carrier truncates the preview: __setattr__ was unpoisoned, so a forged _brand could mint a preview line describing nothing and a forged _forwardable could make a void carrier forwardable.
  • [strictcli] The confirm prompt accepts a CRLF-terminated answer. A y typed at a console whose terminal ends lines with CRLF was read as y\r and declined. The answer's line terminator is now stripped as exactly one newline then exactly one carriage return; y still declines.
  • [strictcli] **The effects-bypass check now analyses everything reachable from a registered command handler.** It previously only looked inside functions whose own body mentioned ctx.effects, so two shapes escaped completely: a handler that never mentions the handle, and a bypass one helper-call away. Roots are now registered handlers (a .command/.passthrough decorator, or a name passed as handler=) plus, as before, any function that uses the handle; the closure follows direct calls to module-level functions transitively. Expect this check to surface findings it previously missed.

#0.34.0

Add is_hermetic() to the check-side ConnectionEnvReader

#Features

  • [strictcli] Hermetic detection in checks. ConnectionEnvReader now exposes is_hermetic(), letting a check distinguish --hermetic suppression from an unset connection env (both surface as connection_env_value present=False) and honor hermetic even when the env is absent.

#0.33.0

Connection env vars: a hermetic-suppressed, app-level env primitive for connection URLs

Context

Adds a third infra-env kind alongside infra roots and handshake vars. A connection env (e.g. a database DSN) is declared once at app level, read lazily with no default, and suppressed under --hermetic so connection-dependent behavior (including checks) skips visibly. Flags bind to it by reference and check functions can read it through the check context.

#Features

  • [strictcli] Connection env vars. Declare a hermetic-suppressed connection URL (e.g. a database DSN) at app level with App(connection_env=...); bind flags to it with connection_url/connection_env, read it from handlers via ctx.connection_env_value(), and from checks via the wrapped check context. Under --hermetic it resolves absent so connection-dependent behavior skips visibly.

#0.32.2

Strict parsing: reject underscore separators in ints and overflow-to-infinity floats

#Fixes

  • [strictcli] Fix. Integer values with underscore separators (1_000) are now rejected, matching the strict-parsing rules of the other implementations.
  • [strictcli] Fix. Float values that overflow to infinity (1e999) are now rejected, matching the no-Inf strict-parsing rule of the other implementations.

#0.32.1

cli-test-coverage skips instead of failing when run outside the app's own dev tree

Context

An installed app that runs its checks from a foreign project's directory anchored cli-test-coverage to that foreign cwd, which has no coverage manifest or shard files, so the check failed listing the app's entire command surface as uncovered. The check now applies subject-matter gating: when the anchored coverage root contains neither a manifest nor any shard files, it reports a visible skip naming the anchored path. When either exists, behavior is unchanged.

#Fixes

  • [strictcli] The cli-test-coverage check no longer fails when an installed app runs its checks outside its own development tree (e.g. from a consumer project's directory); it now reports a visible skip when no coverage manifest or shard files exist at the anchored root.

#0.32.0

Deterministic cli-test-coverage verdict from the committed manifest; chdir-safe coverage recording

Context

The cli-test-coverage check previously derived its verdict solely from local per-process shard files, so any machine that had not run the suite failed with 'no coverage data' regardless of repo state. The check now derives its verdict from the committed .strictcli/test-coverage.json manifest (union with any local shards), making it deterministic across machines. Coverage recording and the check are also anchored to the app's construction-time directory, so tests that chdir still record into the repo and a check evaluated from a foreign cwd reads the app's own state.

#Fixes

  • [strictcli] Deterministic cli-test-coverage verdict. The cli-test-coverage check now derives its verdict from the committed .strictcli/test-coverage.json manifest (union with any local shards), so it produces the same result on every machine instead of failing with "no coverage data" where the suite has not been run locally. Coverage recording and the check are anchored to the app's construction-time directory, so tests that change directory still record into the repo and the check reads the app's own state when run from elsewhere.

#0.31.0

npm wrapper discontinued and MCP error-message parity

Context

The npm distribution of the Python implementation is discontinued. Starting at 0.31.0, the npm name is being reassigned to a native TypeScript implementation. Python users should install strictcli from PyPI.

#Breaking

  • [strictcli] npm wrapper discontinued. strictcli on npm becomes the native TypeScript implementation from 0.31.0; Python users install from PyPI.

#Fixes

  • [strictcli] MCP error-message parity. tools/call parameter-validation errors now match across implementations, and unknown tools are reported as tool-result errors instead of protocol errors.

#0.30.1

Fix coverage shard directory creation in test contexts

#Fixes

  • [strictcli] Bug fix. Coverage shard directory is now created on-demand in _record_coverage, fixing crashes when app.test() runs in a different working directory than where the App was constructed.

#0.30.0

CLI test-coverage instrumentation, notes channel

#Features

  • [strictcli] New feature. App(test_coverage=True) enables CLI test-coverage instrumentation. Every test() and call() invocation records the resolved command path. A built-in cli-test-coverage check merges per-process shard files and hard-FAILs listing uncovered commands.

#0.29.0

#Breaking

  • [strictcli] Handler contract redesign. Command and passthrough handlers now always receive ctx as their first argument (no annotation required), and must return int (exit code), None (exit 0), or strictcli.outcome(exit_code, data). Context.emit is removed and any other return value is a hard error. strictcli.outcome(...) is the new way to return structured data.
  • [strictcli] **Breaking: version is now required on App.** The metadata auto-detect fallback is gone. Migrate with version=importlib.metadata.version("<dist>").

#Features

  • [strictcli] Check reporters gain a note channel and --verbose now shows notes, durations, and a count summary. Check implementations can record informational notes via reporter.note (allowed on any outcome, including a pass) that never affect status or exit codes. Under --verbose, results now render per-check notes, per-check durations, and a trailing pass/fail/warn/skip count summary; JSON output always includes notes and duration_ms fields.

#Fixes

  • [strictcli] Canonical float formatting. Floats now render consistently in help defaults, config show, TOML config set writes, and choices/validation error messages: shortest round-trip decimal, integer-valued floats keep a trailing .0, -0.0 is preserved, fixed notation for magnitudes in [1e-6, 1e21) and scientific (1e+21, 1e-7) outside. Fixes malformed output like 1e+16.0 in error messages.
  • [strictcli] Compact JSON data output. Handler outcome(data=...) now prints one compact JSON line (no spaces after ,/:), matching the Go implementation byte-for-byte.
  • [strictcli] **config set/config set --default preserve comments and key order in TOML config files.** Edits now touch only the changed key instead of rewriting the whole file.
  • [strictcli] Python-Go parity fixes. @-prefix file/stdin trimming now trims only Go's cutset (space, tab, CR, LF), preserving other whitespace; tag/check/config-field name validation rejects trailing newlines; config edit prints error: editor failed and exits 1 when the editor fails or is missing; MCP JSON-RPC errors match Go (Parse error, Method not found) and non-object JSON is reported as a parse error; dict flag values render with deterministically sorted keys in help defaults and error messages.

#0.28.0

Check outcome model (sealed reporters, per-problem severity, purity partition), check providers, InfraEnv location roots, global-flag conflict fix, conformance parity.

Context

Breaking changes (minor bump in 0.x):

  • CheckResult deleted. Check handlers now receive a ceiling-typed reporter

(ErrorReporter for error-severity checks, WarnReporter for warn-severity) and return outcomes via reporter.passed / reporter.found / reporter.skipped.

  • @app.check replaced by @app.error_check / @app.warn_check decorators that

enforce the severity-form contract at registration time.

  • Check implementations change from fn(ctx) -> CheckResult to

fn(ctx, reporter) -> outcome.

  • Scope adapters return SkipCheck(reason=...) instead of CheckResult("skip", ...).

#Breaking

  • [strictcli] Breaking: sealed reporter outcome model. CheckResult removed; check handlers now receive a ceiling-typed reporter (ErrorReporter / WarnReporter) and return outcomes via reporter.passed / reporter.found / reporter.skipped. Registration via @app.error_check / @app.warn_check replaces @app.check.

#Features

  • [strictcli] InfraEnv primitive. New location-root concept with RelativeToRoot flag modifier, handshake env vars, and InfraEnv protocol for tools that manage infrastructure directories.
  • [strictcli] Check providers. register_check_provider() hook for scoped, lazy per-cwd check materialization. CheckSpec / error_check_spec / warn_check_spec public constructors for provider-supplied check definitions.
  • [strictcli] Purity partition. Check runner now splits checks into pure (no subprocess, no network) and impure sets; --dry-run runs pure checks only. Scope-adapter replacement context validated at registration time.

#Fixes

  • [strictcli] Fix: global-flag config-conflict now checked in post-command position. Flags placed after the command token are now conflict-checked against config values, matching the pre-command behavior.
  • [strictcli] Fix: --dump-schema serializes RelativeToRoot flag defaults machine-stably. Schema output is now deterministic across platforms.

#0.27.0

Public schema-dict accessor, divergence-aware config conflict mode with per-flag override, and validation-only ConfigField/flag coexistence

Context

Phase 2 additions, all backward compatible:

  • dump_schema_dict() exposes the CLI schema as a dict with no filesystem or

CWD access (the --dump-schema writer path adds project_id on top).

  • config_conflict_mode="error" now only errors when the config and CLI/env

values actually diverge; identical values agree. A per-flag conflict_mode kwarg overrides the app default for a single flag.

  • A config field whose name equals a flag's param name is a validation-only

annotation of that flag: it renders once (in config show/init) and its default must agree with the flag's default (registration error otherwise).

#Features

  • [strictcli] **dump_schema_dict().** New public App method returning the CLI schema as a dict (with version, without project_id), computed with zero filesystem or working-directory access.
  • [strictcli] **Divergence-aware config conflict mode + per-flag conflict_mode.** In config_conflict_mode="error", a value set in both the config file and the CLI/env is now only an error when the two values differ; identical values agree and are accepted. A new per-flag conflict_mode kwarg overrides the app-level mode for a single flag.
  • [strictcli] Config fields that share a flag's name are now validation-only. A config_field whose name matches a flag renders once (on the flag, with its help as a trailing annotation) in config show and config init instead of appearing twice. Declaring such a field with a default that disagrees with the flag's default is now a registration error.

#0.26.0

PEP 561 py.typed marker and typed decorator return annotations for consumer type checking.

Context

Adds py.typed marker file for PEP 561 compliance and fixes decorator return type annotations so consumer type checkers (mypy, pyright) see proper types for flag(), arg(), command(), and check() decorators instead of generic Callable.

#Features

  • [strictcli] New feature. PEP 561 py.typed marker and typed decorator return annotations -- consumer type checkers now see proper types for flag(), arg(), command(), and check() decorators.

#0.25.0

Config lifecycle overhaul: parse-time loading, --config flag, --hermetic, conflict mode, hard-error loading, ctx.Source provenance API.

Context

Major rework of the config system. Config files are now loaded at parse time rather than lazily, giving deterministic precedence (CLI > env > config > default). The new --config flag lets callers specify an explicit config path. --hermetic disables all config/env resolution for reproducible runs. Conflict mode makes a value set both in the config file and on the CLI (or env) a hard error instead of silently letting the CLI win. Hard-error config loading fails loudly on malformed config files instead of silently ignoring them. The ctx.Source provenance API lets handlers inspect where each flag value came from (cli, env, config, default). Reserved global flag enforcement prevents user code from shadowing built-in flags.

#Breaking

  • [strictcli] Reserved global flag names enforced. Global flags named help, version, dump-schema, config, hermetic, or mcp are now rejected at registration time.

#Features

  • [strictcli] Parse-time config loading. Config files are now loaded at parse time (not construction time), ensuring late-written config files are honored.
  • [strictcli] **--config flag and no_default_config_path option.** --config <path> selects a config file explicitly; no_default_config_path requires explicit --config instead of searching the default path.
  • [strictcli] Hard-error config loading and config conflict mode. Malformed TOML and JSON config files produce hard errors with line/column position information. config_conflict_mode="error" (an App argument) makes a value set both in the config file and on the CLI (or env) a hard error instead of silently letting the CLI win; the default stays cli-wins.
  • [strictcli] **--hermetic flag.** Ignores env vars and config, using only CLI flags and defaults. Mutually exclusive with --config and config subcommands.
  • [strictcli] **ctx.Source provenance API.** ctx.source(flag) returns the origin of each flag value: cli, env, config, default, or implied.

#0.24.2

  • No user-facing changes.

#0.24.1

Recovered full release history and added MIT license

Context

53 pre-releasable versioned changelog files were recovered from saferm archive, restoring complete release history in CHANGELOG.md. Project now includes MIT license.

#Features

  • [strictcli] New feature. Project now includes MIT license.

#0.24.0

Optional values skip choices validation; WARN check results satisfy depends_on; arg default-type validation.

Context

Choices validation previously rejected optional flags/args that were not passed (None flowed into the choices check, producing "invalid value 'None'"). Validation is now skipped for absent optional values across flags, args, and globals via one shared helper. Check dependency semantics changed: a check that WARNs satisfies its dependents (WARN means passed-with-notes, not failed); only FAIL cascade-skips. Arg default-type validation gains str and list cases.

#Features

  • [strictcli] Warn satisfies check dependencies. A check that returns warn no longer causes its dependents to be cascade-skipped; dependents are skipped only when a dependency fails (or was itself skipped). Warn still makes the check run exit non-zero unless --ignore-warnings.

#Fixes

  • [strictcli] Fixed. Unset mutex flags with choices and optional args with default=None + choices no longer fail with "invalid value 'None'" when not passed. Custom validators are no longer invoked with None for not-passed flags.
  • [strictcli] Fixed. Arg defaults are now fully validated at registration: a non-string default on a str arg is rejected, and list arg defaults are validated as non-empty lists of the item type (previously a valid list default on a list[int]/list[float] arg was wrongly rejected with a scalar-type error). Int elements in float list defaults are coerced to float.

#0.23.0

Registration-time ban on bare --force flag name and --no-* prefix

Context

Flag names 'force' (exact) and names starting with 'no-' are now rejected at registration time. Bare --force encourages agents to bypass guardrails without thinking; qualified names like --force-overwrite make the intent explicit. The no- prefix is reserved for strictcli's auto-generated negation system (--no-flag for negatable bools).

#Features

  • [strictcli] New. Registration-time ban on bare --force flag name and --no-* prefix. Flag names starting with no- are reserved for the negation system.

#0.22.0

Required booleans, Context type, schema project_id guard, tag contract global flags fix

Context

Breaking change: Bool flags no longer auto-default to false. BoolFlag without explicit Default(false) is now required. New Context type for structured handler communication. Schema dump now validates project_id. TagContract now checks global flags.

#Breaking

  • [strictcli] Breaking. Bool flags no longer auto-default to false. BoolFlag without explicit Default(false) is now required — user must pass --flag or --no-flag.

#Features

  • [strictcli] New. Schema dump validates project_id before writing — hard error if existing schema belongs to a different project.
  • [strictcli] New. Context type with output methods (Info, Warn, Debug, Error, Emit) for structured handler communication.
  • [strictcli] New. Comprehensive sub-project READMEs replacing severely stale documentation.

#Fixes

  • [strictcli] Fix. TagContract now checks global flags, not just command-level flags.

#0.21.0

Command declaration framework: structured handler returns, programmatic invocation, typed args, config fields, compound types, tool export, MCP projection, and scope-based check filtering.

Context

This release transforms strictcli from a CLI framework into a command declaration framework with multiple output projections. Commands declared once can be invoked via CLI (existing), programmatic API (app.call/acall), tool export (as_tools with JSON Schema), and MCP server (--mcp flag).

Key additions: handlers can return structured data (backward compatible -- int returns still work), positional args gain type and choices support, first-class config fields with per-command binding and startup validation, compound types (list[T] and dict[str,T]), command visibility (hidden/interactive), schema enrichment with versioning and constraint serialization, and declarative scope field on check definitions with set_scope_adapter() for context-dependent pre-check filtering.

#Features

  • [strictcli] New feature. Programmatic invocation via app.call() and app.acall() with InvokeError exception for structured error handling.
  • [strictcli] New feature. Schema versioning with schema_version field, constraint serialization (mutex, CoRequired, Requires, Implies), tag contracts, and arg defaults in --dump-schema output.
  • [strictcli] New feature. Typed positional args with type (str/int/float/bool) and choices support, matching the flag type system.
  • [strictcli] New feature. Command visibility: hidden and interactive declarations with help text filtering and schema serialization.
  • [strictcli] New feature. First-class config fields with config_field() declarations, per-command binding, startup validation, config init template generation, and unknown-key rejection.
  • [strictcli] New feature. Compound types list[T] and dict[str, T] on flags and args with projection-specific CLI parsing.
  • [strictcli] New feature. Tool export: json_schema(), as_tools(), Tool dataclass, and router tool for AI agent integration.
  • [strictcli] New feature. MCP projection via serve_mcp() and --mcp flag for Model Context Protocol tool serving.
  • [strictcli] New feature. Optional scope field on check definitions for declarative pre-check context filtering, and set_scope_adapter() method on App for registering scope transformation callbacks.

#Fixes

  • [strictcli] Fix. Validate unknown kwargs and enforce required globals in passthrough invoke path.

#0.20.1

Fix keyword collision in flag parameter names

Context

Flags named after Python keywords (--global, --class, --import, etc.) now produce valid parameter names with a trailing underscore per PEP 8 convention.

#Fixes

  • [strictcli] Fix. Flags named after Python keywords (--global, --class, --import) now produce valid parameter names with a trailing underscore (e.g., global_).

#0.20.0

Frozen Command, command tags, tag contracts

Context

Breaking: Command dataclass is now frozen=True with tuple fields (was mutable list). New: command tags (string labels with group inheritance) and tag contracts (registration-time validation that tagged commands have required flags).

#Breaking

  • [strictcli] Breaking. Command dataclass is now frozen=True with tuple fields instead of list.

#Features

  • [strictcli] New. Command tags: string labels on commands and groups with group-to-command inheritance.
  • [strictcli] New. Tag contracts: registration-time validation that tagged commands have required flags.

#0.19.0

#Breaking

  • [strictcli] Breaking. Renamed Tag to FlagSet and tags= parameter to flag_sets= across all API surfaces.

#0.18.0

Public API for running checks programmatically

Context

Adds App.run_checks(), format_check_results(), format_check_results_json(), and CheckRunResult — a stable public API replacing the private _filter_checks/_resolve_check_order/_run_checks/_check_format_human functions that consumers like rlsbl were forced to use.

#Features

  • [strictcli] New. Public API for running checks programmatically: App.run_checks(), format_check_results(), format_check_results_json(), CheckRunResult.

#0.17.0

Repeatable flag default validation and help text display

#Breaking

  • [strictcli] Breaking. Repeatable flag defaults are now validated at registration: must be a list, default=[] is rejected as redundant, and element types must match the flag type.

#Features

  • [strictcli] New. Help text now displays non-empty repeatable flag defaults as [default: a, b, c].

#0.16.1

Error message fixes and help text improvements

#Fixes

  • [strictcli] Fix. config set mutex error messages now include the config set: prefix for consistency.
  • [strictcli] Fix. Config set help text now mentions backslash escaping for values containing commas in repeatable flags.

#0.16.0

unique and env_separator flag fields, config array coercion, config set repeatable support

Context

Breaking: config show now requires --plain or --json. New features: unique field enforces no duplicate values on repeatable flags, env_separator controls how env vars are split into arrays, config arrays are coerced to declared types, config set supports repeatable flags with --clear and --default.

#Breaking

  • [strictcli] Config show. config show now requires explicit --plain or --json flag instead of defaulting to plain text.

#Features

  • [strictcli] New flag field. unique field on repeatable flags enforces no duplicate values at CLI parse time and in config arrays.
  • [strictcli] New flag field. env_separator on repeatable flags controls how environment variable values are split into arrays.
  • [strictcli] Config arrays. Config values for repeatable flags are now coerced from strings to the declared type.
  • [strictcli] Config set. Repeatable flags can now be managed with config set, including --clear to reset and --default to restore default values.

#Fixes

  • [strictcli] Fix. Float NaN/Inf env var error messages now include the environment variable suffix.
  • [strictcli] Fix. Config error messages now use consistent type names and array formatting.
  • [strictcli] Fix. Flag collection no longer includes config group pseudo-flags, preventing incorrect behavior in commands that enumerate all flags.

#0.15.1

Config show bool parity and negative values in config set

Context

config show now outputs lowercase true/false matching Go. config set accepts negative numeric values.

#Breaking

  • [strictcli] Breaking. Config set now accepts negative numeric values. Unknown single-dash tokens produce 'unexpected argument' instead of 'unknown flag'.

#Fixes

  • [strictcli] Fix. Config show now outputs lowercase true/false for bool values, matching Go implementation.

#0.15.0

Config set type coercion and key validation

Context

config set now validates keys against registered flags and coerces string values to the flag's declared type before writing. This is a breaking change: unknown keys are rejected.

#Breaking

  • [strictcli] Breaking. config set now validates keys against registered flags and rejects unknown keys.

#Features

  • [strictcli] New feature. config set coerces values to the flag's declared type (int, bool, float) before writing to config.

#0.14.0

checks_embed for inline TOML data

Context

Adds checks_embed parameter as an alternative to checks_path, allowing TOML bytes to be passed directly without requiring a file on disk.

#Features

  • [strictcli] New feature. checks_embed parameter accepts raw TOML bytes, enabling inline checks configuration without a file on disk.

#0.13.0

#Features

  • [strictcli] New feature. Schema dump (--dump-schema) now includes a project_id field read from pyproject.toml, providing provenance for schema validation.

#0.12.0

#Breaking

  • [strictcli] Breaking. CWD auto-discovery of checks.toml removed; checks must be explicitly enabled via checks_path=. checks.toml now requires a top-level app field matching the app name.

#0.11.0

#Features

  • [strictcli] New feature. checks_path parameter on App for explicit checks.toml location, replacing CWD-based discovery.

#0.10.0

#Features

  • [strictcli] New feature. --dump-schema omits fields matching defaults and includes a top-level defaults object documenting what missing fields mean.
  • [strictcli] New feature. @-prefix for string flag values: @path reads from file, @- reads from stdin, @@ escapes. 1 MB size limit, trailing whitespace stripped.

#0.9.1

#Features

  • [strictcli] New feature. Public config_file_path property on App.

#Fixes

  • [strictcli] Fix. Config format validation errors now have parity between Python and Go.

#0.9.0

#Features

  • [strictcli] New feature. config_path and config_format options for TOML-based configuration file support.

#0.8.7

#Features

  • [strictcli] New feature. Go implementation now includes subcommand name in parse error help suggestions, matching Python.

#Fixes

  • [strictcli] Fix. Unknown subcommand errors in groups now suggest group-level help (e.g., try 'myapp config --help').

#0.8.6

#Features

  • [strictcli] New feature. Parse error messages now suggest the correct subcommand help (e.g., try 'app stream --help' instead of try 'app --help').

#0.8.5

#Features

  • [strictcli] New feature. App-level and command-level help output now includes a 'Global flags' section.

#Fixes

  • [strictcli] Fix. Auto-registered check command no longer collides with app-level global flags (e.g., --dry-run).

#0.8.4

#Fixes

  • [strictcli] Fix. Allow empty tags = [] in .strictcli/checks.toml. Checks without tags are valid -- they are addressable by --name and included in --all.

#0.8.3

  • No user-facing changes.

#0.8.2

  • No user-facing changes.

#0.8.1

  • No user-facing changes.

#0.8.0

#Features

  • [strictcli] New feature. Check system -- a first-class, security-hardened check/validation framework. Register checks in .strictcli/checks.toml (source of truth) with metadata (tags, severity, dependencies), implement them via @app.check() decorator, and run them with the auto-registered check command. Includes: tag-based filtering with a set-operation DSL (&, |, ^, -, !), DAG-based dependency ordering, human/JSON output, --list/--dry-run/--verbose modes, --ignore-warnings, and --dump-schema integration.

#0.7.1

#Features

  • [strictcli] New feature. Handlers with **kwargs signatures are accepted without strict parameter validation.

#0.7.0

#Features

  • [strictcli] New feature. --dump-schema flag auto-generates .strictcli/schema.json describing the full CLI structure.
  • [strictcli] New feature. Opt-in JSON config file support with App(config=True). Reads ~/.config/{name}/config.json with precedence: CLI > env > config > default. Auto-registers config show/set/path/edit subcommands.
  • [strictcli] New feature. Recursive group nesting to arbitrary depth. Groups can contain subgroups via group.group(name, help=...).
  • [strictcli] New feature. type=float flag support. Rejects NaN and Inf.
  • [strictcli] New feature. --help and -h recognized anywhere in the argument list, not just at token boundaries.
  • [strictcli] New feature. App(version=None) auto-detects version from importlib.metadata.

#0.6.1

#Fixes

  • [strictcli] Fix. Harmonize Implies and deprecated command error messages with Go for exact parity.

#0.6.0

#Features

  • [strictcli] New feature. Deprecated commands: declare a command name + message that prints the deprecation notice and exits when invoked, with a Deprecated: section in help output.

#0.5.0

#Features

  • [strictcli] New feature. Implies flag dependency type: when a trigger flag is set, automatically set a target bool flag to a specified value. Explicit contradictions are parse errors.

#0.4.1

#Fixes

  • [strictcli] CI publish fix. Fixed publish workflow for GitHub Actions compatibility.

#0.4.0

#Breaking

  • [strictcli] Breaking: mutex groups always required. Removed the required parameter from MutexGroup. All mutex groups now require exactly one flag to be provided.

#Features

  • [strictcli] Flag dependencies. New CoRequired and Requires types for declaring flags that must appear together.

#Fixes

  • [strictcli] Choices on global flags. Global flags now validate choices correctly.
  • [strictcli] Repeatable global flags. Global flags with repeatable=True now work correctly.
  • [strictcli] Strict int parsing. Integer flags now enforce 64-bit signed range to match Go behavior.
  • [strictcli] Strict int env parsing. Integer environment variables reject leading/trailing whitespace.
Search