strictcli v0.39.0 /python.strictcli
On this page

A strict, zero-dependency CLI framework for Python with mandatory help text, type-safe flags, groups, schema export, and PEP 561 type annotations.

#python.strictcli

#python.strictcli

A strict CLI framework for Python with mandatory help text, type-safe flags, groups, and schema export.

#RelativeToRoot

Opaque marker: a filesystem path relative to a declared infrastructure root.

Produced as RelativeToRoot(env_var, *parts) and accepted by a flag's default= and by App(config_path=...). env_var names the root (declared via App(infra_root={env_var: default})); parts are joined onto the resolved root path. Config-path markers resolve eagerly at construction; flag-default markers resolve when defaults are applied at parse time. A marker referencing an undeclared root is a registration-time hard error.

#_serialize_marker

python
def _serialize_marker(ref: RelativeToRoot) -> dict

Serialize a RelativeToRoot marker to a machine-stable JSON shape.

Emits only the declared env var and path parts -- never the resolved, machine-specific path. The shape is identical across the Python and Go implementations so the schema round-trips and cross-language byte-compares.

#_resolve_infra_root_path

python
def _resolve_infra_root_path(ref: RelativeToRoot, roots: dict[str, str]) -> str

Resolve a RelativeToRoot marker against a roots map (env var -> path).

Raises ValueError if the marker references an undeclared root.

#_validate_connection_binding

python
def _validate_connection_binding(f: 'Flag', connection_env_names) -> None

Enforce the connection-URL binding rules at registration time (mechanical enforcement, not review). A URL-class flag must bind to a declared connection env; the binding drives env resolution by reusing the per-flag env channel (connection_env is folded into env).

#_Source

Where a flag value came from.

#_SourcedEntry

A value paired with its provenance source.

#_SourcedStore

Map of flag-name to _SourcedEntry with source-filtered presence queries.

Replaces the plain cli_set: dict[str, object] in the validation pipeline, adding provenance tracking for each value.

#set

python
def set(self, name: str, value: object, source: str) -> None

#get

python
def get(self, name: str) -> tuple[object, bool]

Return (value, True) or (None, False).

#has

python
def has(self, name: str) -> bool

#get_value

python
def get_value(self, name: str) -> object

Return the value or raise KeyError.

#set_value

python
def set_value(self, name: str, value: object) -> None

Update the value of an existing entry, keeping its source.

#is_present_for_mutex

python
def is_present_for_mutex(self, name: str) -> bool

Present for mutex: only cli, env, config. NOT default or implied.

#is_present_for_deps

python
def is_present_for_deps(self, name: str) -> bool

Present for deps (CoRequired, Requires): everything except default.

#source_map

python
def source_map(self) -> dict[str, str]

Return a dict mapping flag names to source labels.

#from_dict

python
def from_dict(cls, d: dict[str, object], source: str) -> '_SourcedStore'

Build a store from a plain dict, marking all entries with source.

#_InfraAccess

A Context's view of infrastructure env vars: resolved root values (captured at construction), declared handshake env vars (read live), and declared connection env vars (read live, but suppressed under --hermetic).

#Context

Structured output context for command handlers.

Always injected as the first positional argument to every handler. Provides info/warn/debug/error methods that route to the correct stream, plus source/infra_value provenance accessors. To return structured data, a handler returns strictcli.outcome(data=...).

#dry_run

python
def dry_run(self) -> bool

True when the framework-owned --dry-run flag was passed.

#approve_consequential

python
def approve_consequential(self) -> bool

True when the framework-owned --approve-consequential flag was passed.

#quiet

python
def quiet(self) -> bool

True when the framework-owned --quiet flag was passed.

#verbose

python
def verbose(self) -> bool

True when the framework-owned --verbose flag was passed.

#effects

python
def effects(self) -> '_Effects'

The effects handle for this run (see the effects-regime contract).

#info

python
def info(self, msg: str) -> None

Write an informational message to stdout (hidden under --quiet).

#warn

python
def warn(self, msg: str) -> None

Write a warning message to stderr (never suppressed).

#debug

python
def debug(self, msg: str) -> None

Write a debug message to stdout (shown only under --verbose).

--quiet dominates --verbose: passing both hides debug output.

#error

python
def error(self, msg: str) -> None

Write an error message to stderr (never suppressed).

#source

python
def source(self, name: str) -> str

Return the provenance source label for a flag.

Returns one of: "cli", "env", "config", "default", "implied", "infra". ("infra" indicates the value came from a RelativeToRoot default resolved through a declared infrastructure root.) Raises KeyError if the flag name is not found.

#infra_value

python
def infra_value(self, env_var: str) -> tuple[str | None, bool]

Return the value of a declared infrastructure env var.

For a declared location root (infra_root), returns the value resolved eagerly at construction (env var if set, else the declared default) and True -- the resolved value is always available.

For a declared handshake var (handshake_env), reads the environment LIVE at call time (handshakes are set by the invoking process and carry no construction-time value), returning (value, is_set).

For a declared connection env (connection_env), reads the environment LIVE at call time and returns (value, is_set) -- EXCEPT under --hermetic, where it resolves as absent (None, False) so connection-dependent behavior skips visibly instead of connecting.

Raises KeyError if env_var is not a declared root, handshake, or connection var.

#connection_env_value

python
def connection_env_value(self, env_var: str) -> tuple[str | None, bool]

Return the value of a declared connection env (connection_env), read LIVE at call time -- EXCEPT under --hermetic, where it resolves as absent (None, False). Raises KeyError if env_var is not a declared connection env. This is the check-side and handler-side accessor for the connection-URL kind; see also infra_value, which resolves all three kinds.

#EffectFailed

A failed effect operation.

A failed operation is an error, not a value: a run whose child exits nonzero and an http whose status is outside 200-299 raise this, as does invalid UTF-8 on a captured stream. check=False opts a single call out.

#_DryRunTruncated

Raised when handler code extracts from or branches on an Unsettled value.

Derives from BaseException deliberately: a handler's except Exception must not be able to swallow the truncation and let the preview continue with a value the framework refuses to invent.

#Grant

A per-command, per-effect-kind authorization with a mandatory reason.

A grant is not permission to do something otherwise forbidden; it is a labelled reason that surfaces in the preview so a reviewer reading a dry run sees why a dangerous step is there.

#Completed

The result of a subprocess that ran to completion.

stdout/stderr are the child's output decoded as UTF-8 strictly, with a single trailing newline removed if present -- the form that can be forwarded straight into a later effect's argv.

#Response

The result of an HTTP request. Header names are lower-cased.

#Spawned

A handle for a started-but-not-awaited child process.

#wait

python
def wait(self, *, check: bool=True) -> Completed

Wait for the child and return its Completed result.

check mirrors run's opt-out: with the default True a nonzero exit raises :class:EffectFailed.

#Unsettled

A value standing in for a result that cannot exist because nothing ran.

Produced by every mutating effect recorded in dry mode and by every post-mutation observe. FORWARDING one into a later ctx.effects call is legal and renders its brand inline; EXTRACTING from it or BRANCHING on it truncates the preview with a precise error.

#_EffectRecord

One entry in the structured effect log (see the conformance surface).

#to_dict

python
def to_dict(self) -> dict

#render

python
def render(self) -> str

Render this record as a would-do log line (without the indent).

#_EffectLog

The ordered effect records produced by one dispatch.

TWO counters, deliberately. Would-do numbering is the numbering of the RENDERED lines: it feeds the log's <N>. prefix, the «step N output» brand and the truncation error's "ends at step N". CACHE_WRITEs are never rendered, so they must never consume one of those numbers -- otherwise a coverage-instrumented run would silently start its preview at 2.. They get their own sequence instead, so every record still carries a seq.

#append

python
def append(self, rec: _EffectRecord) -> None

#next_seq

python
def next_seq(self) -> int

The next would-do number. Pure: callers may ask without appending.

#next_cache_seq

python
def next_cache_seq(self) -> int

The next CACHE_WRITE number, on its own counter.

#render

python
def render(self) -> str

Render the would-do log. CACHE_WRITEs are never written to it.

#to_list

python
def to_list(self) -> list[dict]

#_msg_dry_run_truncated

python
def _msg_dry_run_truncated(step: int, cmd: str, brand: str) -> str

The truncation error. Carries its own error: prefix (it goes to stderr directly, not through the parse-error formatter).

#_msg_dry_run_aborted

python
def _msg_dry_run_aborted(step: int, cmd: str) -> str

The aborted-preview marker. Same shape and prefix as the truncation error above: both say the preview ended before the handler finished, and they differ only in why and in what the reader may conclude.

#_msg_confirm_prompt

python
def _msg_confirm_prompt(cmd_path: str) -> str

The confirm prompt. A prompt, not an error, but parity is still checked.

#_strip_confirm_line

python
def _strip_confirm_line(answer: str) -> str

Strip the confirm answer's line terminator: one \n, then one \r.

Exactly one of each, never more. The carriage return matters because a human at a Windows console types the same y as everyone else and their terminal terminates the line CRLF; a stdin stream that does not translate newlines hands us "y\r\n", and declining there would refuse an answer that was plainly given. Stripping only the terminator (rather than whitespace) keeps " y" a decline, which §8.2 requires.

#_msg_call_consequential_unconsented

python
def _msg_call_consequential_unconsented(cmd_path: str) -> str

The programmatic-path refusal (contract §8.5).

Requiring confirmation is a property of the COMMAND, so every channel has to honour it -- but a programmatic caller has no terminal to prompt. The refusal makes the caller state, in the call, that it is proceeding without a human, instead of the framework deciding that silently on its behalf.

#_consequential_grant_warning

python
def _consequential_grant_warning(cmd_path: str, grant: str, kind: str) -> str

The consequential-grant-agreement warning (contract §8.1, §11).

A grant exists so a reviewer reading a preview sees WHY a dangerous step is there (§6.1) -- the same judgement consequential makes. When the grant's kind is one that leaves this process (proc_mutate runs another program, net_mutate changes remote state), the two declarations should almost always agree. They can legitimately disagree, so this is a warning: making it an error would push consumers to declare consequential reflexively to clear a gate, which is exactly the reflex the declaration exists to end.

#_observe_allowlist_breadth_warning

python
def _observe_allowlist_breadth_warning(binary: str) -> str

The observe-allowlist-breadth warning (contract §6.2).

A one-token prefix is a near-blanket exemption for that binary: EVERY invocation of it becomes an observe, which means it really executes under --dry-run, is never written to the would-do log, and is legal inside a read_only command. That may be exactly what the app wants -- the allowlist is a declared, source-visible choice and it authorizes real execution in dry mode -- so this is a warning, not an error.

#_msg_effect_option_not_accepted

python
def _msg_effect_option_not_accepted(name: str, method: str, opt: str) -> str

An option the receiving method does not accept (contract §12.8).

Python reaches this through each method's **_options catch-all rather than through CPython's native unexpected keyword argument TypeError, so the rendered text is byte-identical to Go's and TypeScript's. <opt> is the canonical snake_case option name, which is what makes that identity hold.

#_reject_unaccepted_options

python
def _reject_unaccepted_options(name: str, method: str, options: dict) -> None

Raise on the first unaccepted option, in the caller's declaration order.

A TypeError, matching every other call-time argument guard on the handle (and what CPython itself raises for an unexpected keyword).

#_Effects

The effects handle reached as ctx.effects.

Exactly eight methods, and the set is CLOSED: there is no escape hatch that mints an unlisted effect, and CACHE_WRITE has no public method at all.

#_reject_carrier_params

python
def _reject_carrier_params(self, method: str, params: dict) -> None

Hard-error when a carrier reaches a parameter that cannot take one.

#_operand

python
def _operand(self, value: object, method: str, param: str) -> tuple

Resolve a carrier-accepting parameter.

Returns (runtime_value, rendered). runtime_value is None when the value is unsettled (nothing ran, so there is nothing to use); rendered is what the log line shows.

#_content_operand

python
def _content_operand(self, value: object) -> tuple

Resolve write's content. Returns (bytes_or_None, rendered).

The rendered form is the encoded byte count for a settled value, and the forwarded carrier's brand when the content is unsettled (there is no byte count to report -- nothing produced the bytes).

#_authorize

python
def _authorize(self, method: str, kind: str, grant: str | None) -> Grant | None

Read-only enforcement plus grant validation, at call time.

#_is_observe

python
def _is_observe(self, argv: list) -> bool

Element-wise argv-prefix matching by string equality. Nothing else.

#run

python
def run(self, argv: Sequence[str | Completed | Response], *, cwd=None, env=None, check=True, stream=False, resource=None, skip_if_current=None, grant=None, **_options) -> Completed

Run a subprocess to completion (PROC_MUTATE, or an observe).

#spawn

python
def spawn(self, argv: Sequence[str | Completed | Response], *, cwd=None, env=None, resource=None, skip_if_current=None, grant=None, **_options) -> Spawned

Start a subprocess without waiting (PROC_SPAWN).

Spawning is itself an effect: a dry run RECORDS the spawn instead of performing it, which is why no cross-process mode token exists.

#write

python
def write(self, path: str | os.PathLike[str] | Completed | Response, content: str | bytes | Completed | Response, *, resource=None, skip_if_current=None, grant=None, **_options) -> None

Write bytes to a path (FILE_WRITE).

#mkdir

python
def mkdir(self, path: str | os.PathLike[str] | Completed | Response, *, resource=None, skip_if_current=None, grant=None, **_options) -> None

Create a directory, parents included; an existing one is not an error.

#remove

python
def remove(self, path: str | os.PathLike[str] | Completed | Response, *, resource=None, skip_if_current=None, grant=None, **_options) -> None

Remove a file, symlink or directory tree; a missing path is not an error.

#rename

python
def rename(self, src: str | os.PathLike[str] | Completed | Response, dst: str | os.PathLike[str] | Completed | Response, *, resource=None, skip_if_current=None, grant=None, **_options) -> None

Move/rename a path (FILE_WRITE).

#chmod

python
def chmod(self, path: str | os.PathLike[str] | Completed | Response, mode, *, resource=None, skip_if_current=None, grant=None, **_options) -> None

Change a path's mode (FILE_WRITE).

#http

python
def http(self, method, url: str | os.PathLike[str] | Completed | Response, *, body=None, headers=None, check=True, resource=None, skip_if_current=None, grant=None, **_options) -> Response

Perform a network request (NET_MUTATE).

#_merged_env

python
def _merged_env(self, env)

env merges OVER the inherited environment, never replacing it.

#_decode_effect_output

python
def _decode_effect_output(data: bytes, cmd_path: str, method: str) -> str

Decode captured output as UTF-8 strictly, dropping one trailing newline.

#_remove_path

python
def _remove_path(path: str) -> None

Remove a file, a symlink or a directory tree. A missing path is fine.

#_validate_grants

python
def _validate_grants(cmd_name: str, grants) -> tuple

Validate a command's grant declarations at registration time.

#_BypassImports

What a module's imports say about the names it calls.

receivers maps a bound module name to the effect module it denotes (import os as o -> {"o": "os"}); calls maps a bound member name to the (module, member) pair it came from (from os import system as sh -> {"sh": ("os", "system")}). Both are used to normalize a call before the ban lists see it, so the lists stay written in terms of real module and member names rather than whatever the consumer spelled.

#_bypass_import_bindings

python
def _bypass_import_bindings(tree) -> _BypassImports

Names bound to effect modules and to their members, in one module.

Relative imports are skipped: from .os import system is the consumer's own module, not the stdlib one, and the analyser cannot resolve it.

#_call_target_name

python
def _call_target_name(node) -> tuple

Return (dotted_target, receiver) for a call's callee.

#_reaches_effects_handle

python
def _reaches_effects_handle(node, aliases=frozenset()) -> bool

True when a callee's receiver chain goes through .effects.

aliases are local names bound to the handle (e = ctx.effects), which is an ordinary way to write a handler and must not read as a bypass.

#_bypass_effects_aliases

python
def _bypass_effects_aliases(tree) -> frozenset

Names bound to the effects handle anywhere in the module.

e = ctx.effects then e.write(...) is the same call as ctx.effects.write(...); without this the lint would report the handle itself as a bypass.

#_function_opts_into_effects

python
def _function_opts_into_effects(fn) -> bool

True when a function body reaches for an .effects handle at all.

One of the two root conditions: a function that uses the effects handle must route ALL of its effects through it, or the preview it promises is a lie.

#_decorator_leaf

python
def _decorator_leaf(node) -> str | None

The last dotted component of a decorator expression, call or not.

#_bypass_handler_names

python
def _bypass_handler_names(tree) -> set

Function names passed as handler= anywhere in the module.

The second way a handler is registered: Passthrough(handler=_pt), app.command(..., handler=deploy). Name-based, because that is all a single-module AST can honestly resolve.

#_is_registered_handler

python
def _is_registered_handler(fn, handler_names: set) -> bool

True when this function is a registered command handler.

#_bypass_direct_call_names

python
def _bypass_direct_call_names(fn) -> set

Bare name(...) callees inside a function's subtree.

#_bypass_reachable_functions

python
def _bypass_reachable_functions(tree) -> set

The ids of every function REACHABLE FROM A REGISTERED COMMAND HANDLER.

§11's scope is reachability, not "a function whose own body mentions .effects" -- a handler that never touches the handle, and a bypass one helper-call away, are both trivial escapes from the narrower reading, and this lint is the sole stated mitigation for the accepted no-sandbox ceiling.

Roots are registered handlers (a .command / .passthrough decorator, or a name passed as handler=) plus, as before, any function that reaches for .effects itself. From each root the closure follows DIRECT calls to MODULE-LEVEL functions, transitively, within this one module -- the most a single-file AST can resolve without a symbol table.

#_bypass_resolve_call

python
def _bypass_resolve_call(leaf: str, receiver, imports: _BypassImports) -> tuple

The (leaf, receiver) a call really goes through, per its imports.

A bare name imported from an effect module answers with that module and the member's REAL name (from os import system as sh -> ("system", "os")), and an aliased module receiver answers with the module it denotes (import os as o -> os). Anything else is returned untouched, so an unresolvable receiver stays unresolvable rather than being guessed at.

#_bypass_call_is_banned

python
def _bypass_call_is_banned(node, target: str, receiver, imports: _BypassImports=_BYPASS_NO_IMPORTS) -> bool

True when one call is a direct effect the handle should have carried.

Two leaves are deliberately narrower than the rest: builtin open is a finding only in a writing mode, and system only through os -- platform.system() observes this process and starts nothing, and the effects handle has no method that could carry it.

Builtin open is answered BEFORE import resolution, because it is the one leaf whose meaning comes from being unqualified. Everything after it is resolved through the module's imports (see :func:_bypass_resolve_call), so the lists below are written in terms of real module and member names.

#_bypass_walk

python
def _bypass_walk(node, stack: list, reachable: set, findings: list, rel: str, aliases: frozenset, imports: _BypassImports) -> None

Walk one subtree, carrying the enclosing-function stack.

A banned call is reported once, at the INNERMOST enclosing function, when any enclosing function is reachable -- so a bypass inside a nested closure is one finding, not one per enclosing scope.

#_open_is_write_mode

python
def _open_is_write_mode(node) -> bool

True when a bare open(...) call requests a writing mode.

#_scan_effects_bypasses

python
def _scan_effects_bypasses(root: Path) -> list[tuple]

Find direct effect calls REACHABLE FROM a registered command handler.

Returns (relative_path, lineno, function_name, target) tuples, in file then line order. See :func:_bypass_reachable_functions for the scope rule.

#Outcome

A structured result returned by a command handler.

Built exclusively via the :func:outcome factory. Carries an exit code and optional structured data. When data is not None the framework JSON-prints it to stdout; test()/call() capture it as data.

#outcome

python
def outcome(exit_code: int=0, data: object=None) -> Outcome

Build an :class:Outcome for a command handler to return.

Args:

  • exit_code: process exit code (default 0).
  • data: optional structured data; when not None it is JSON-printed to

stdout and captured by test()/call().

#_interpret_handler_return

python
def _interpret_handler_return(result: object) -> tuple[int, object]

Map a command handler's return value to (exit_code, data).

data is _MISSING when there is no structured payload to emit. The only permitted returns are int (exit code), None (exit 0), or an :class:Outcome built via :func:outcome. Anything else is a hard error.

#_config_path

python
def _config_path(app_name: str, *, override: str | None=None, config_format: str='json') -> str

Compute the config file path for an app.

If override is provided, expand ~ and return it directly. Otherwise compute from XDG_CONFIG_HOME + app_name.

#_ConfigLoadResult

Result of loading a config file.

#_compute_json_position

python
def _compute_json_position(text: str, offset: int) -> tuple[int, int]

Convert a byte offset to 1-based (line, column).

#_load_config

python
def _load_config(app_name: str, *, config_path_override: str | None=None, config_format: str='json', is_runtime_flag: bool=False) -> _ConfigLoadResult

Load the config file for an app.

Missing file with is_runtime_flag=True is a hard error (user explicitly passed --config). Missing file otherwise is soft (returns empty dict). Malformed file is always a hard error with position information.

#_toml_format_scalar

python
def _toml_format_scalar(value: object) -> str

Format a scalar value as a TOML literal.

#_load_toml_doc

python
def _load_toml_doc(path: str) -> 'tomlkit.TOMLDocument'

Load a TOML file as a comment/order-preserving tomlkit document.

Returns an empty document if the file does not exist yet.

#_toml_value_item

python
def _toml_value_item(value: object) -> object

Build a tomlkit item for value using canonical scalar formatting.

Scalars and lists are rendered through _toml_format_scalar (so floats keep their canonical spelling) and re-parsed into properly formatted tomlkit items. Dicts become section tables with keys sorted for deterministic output.

#_toml_set_nested

python
def _toml_set_nested(doc: 'tomlkit.TOMLDocument', dotted_key: str, value: object) -> None

Set a dot-separated key on a tomlkit document, preserving comments/order.

Only the target key is (re)written; intermediate tables are created as needed. Comments and ordering of all other keys are untouched.

#_toml_del_nested

python
def _toml_del_nested(doc: 'tomlkit.TOMLDocument', dotted_key: str) -> bool

Delete a dot-separated key from a tomlkit document.

Returns True if the key was found and removed. Prunes now-empty intermediate tables. Comments/order of untouched keys are preserved.

#_coerce_config_scalar

python
def _coerce_config_scalar(value: object, flag_type: type) -> object

Coerce a single JSON config value to the given type.

Returns the coerced value, or raises ValueError if coercion fails.

#_coerce_config_value

python
def _coerce_config_value(value: object, flag: 'Flag') -> object

Coerce a JSON config value to the flag's type.

Returns the coerced value, or raises ValueError if coercion fails. Handles scalar, array (repeatable), and object (dict) values.

#_resolve_flag_show_source

python
def _resolve_flag_show_source(f: 'Flag', config_data: dict) -> tuple[object, str]

Resolve the effective value and source for a flag in config show context.

Precedence: env > config > default. "cli" is structurally impossible in config show because the app's own flags were never passed on the command line.

#_format_config_value

python
def _format_config_value(value: object) -> str

Format a config value for display, matching Go's formatConfigValue.

#_nested_get

python
def _nested_get(data: dict, dotted_key: str) -> tuple[bool, object]

Look up a dot-separated key in a nested dict.

Returns (found, value). If any intermediate segment is missing or not a dict, returns (False, None).

#_nested_set

python
def _nested_set(data: dict, dotted_key: str, value: object) -> None

Set a dot-separated key in a nested dict, creating intermediate dicts.

#_nested_delete

python
def _nested_delete(data: dict, dotted_key: str) -> bool

Delete a dot-separated key from a nested dict.

Returns True if the key was found and deleted, False otherwise. Cleans up empty intermediate dicts.

#_collect_nested_keys

python
def _collect_nested_keys(data: dict, prefix: str='') -> list[str]

Collect all leaf keys from a nested dict as dot-separated paths.

Non-dict values are leaves. Dict values are recursed into.

#_check_config_field_type

python
def _check_config_field_type(cf: 'ConfigField', value: object) -> str | None

Validate that a config file value matches the config field's declared type.

Returns an error message, or None if the type matches.

#_config_set_field

python
def _config_set_field(effects: '_Effects', key: str, value: str | None, cf: 'ConfigField', existing: dict, path: str, config_format: str, kw: dict) -> int

Handle 'config set' for a config field (not a flag).

Returns an exit code (0 = success, 1 = error).

#_write_config_set

python
def _write_config_set(effects: '_Effects', data: dict, path: str, config_format: str, key: str, value: object) -> None

Set key = value in config and persist THROUGH ctx.effects.

For TOML, edits are comment/order-preserving: the existing file is loaded into a tomlkit document, only the changed key is written, and the document is dumped back. JSON is serialized from the in-memory data dict.

The write is a FILE_WRITE on the effects handle, not a bare open: config set is classified mutating, so under --dry-run the write must be RECORDED, never performed. A framework command that printed "DRY RUN -- no changes were made." while rewriting the user's config file would be the loudest possible counterexample to its own regime.

#_write_config_unset

python
def _write_config_unset(effects: '_Effects', data: dict, path: str, config_format: str, key: str) -> bool

Remove key from config and persist. Returns False if key was absent.

For TOML, the removal is comment/order-preserving (only the target key is dropped). JSON is serialized from the in-memory data dict. The write goes through ctx.effects for the reason spelled out above.

#_ensure_config_dir

python
def _ensure_config_dir(effects: '_Effects', path: str) -> None

Record/perform the config file's parent directory creation.

The existence probe is an ordinary filesystem READ (never an effect), and branching on it is branching on a real value, so the preview walks straight through it in both modes -- the §5.2 idiom. Probing keeps the preview honest: a mkdir line appears only when a directory would really be created.

#_generate_config_template_toml

python
def _generate_config_template_toml(flags: list['Flag'], config_fields: dict[str, 'ConfigField']) -> str

Generate a TOML config template with comments.

#_generate_config_template_json

python
def _generate_config_template_json(flags: list['Flag'], config_fields: dict[str, 'ConfigField']) -> str

Generate a JSON config template.

#_split_escaped

python
def _split_escaped(value: str, sep: str) -> list[str]

Split value on sep, treating backslash as escape character.

Escaped sep becomes literal sep. Escaped backslash becomes literal backslash. Trailing backslash with nothing to escape becomes literal backslash.

#_values_equal_for_conflict

python
def _values_equal_for_conflict(cli_val: object, config_val: object, flag: 'Flag') -> bool

Compare a CLI/env value and a config value for conflict-mode equality.

Equality semantics (pinned):

  • scalars: exact equality.
  • plain repeatable lists: order-sensitive exact equality.
  • Unique flags: order-insensitive multiset equality.

When the two values are equal, config+CLI/env co-presence is NOT a conflict (they agree), so error mode does not fire.

#_check_flag_configfield_default

python
def _check_flag_configfield_default(flag_name: str, flag_default: object, cf: 'ConfigField') -> None

Raise ValueError when a colliding flag and config field have conflicting explicit defaults.

A ConfigField whose name equals a flag's param name is a validation-only declaration -- it annotates the flag. Their defaults must agree. The matrix: both absent OK; equal OK; both present unequal = error; one absent OK (the flag's default wins for rendering). A flag default of None means "no default" (absent); a ConfigField default of _MISSING means absent.

#_find_duplicate

python
def _find_duplicate(values: list) -> object | None

Return the first duplicate value in the list, or None if all unique.

#_format_float_canonical

python
def _format_float_canonical(value: float) -> str

Format a float in strictcli canonical form (SCF).

Rules (must match the Go implementation for cross-language parity):

  1. Shortest decimal string that round-trips to the identical IEEE-754 double

(Python's repr already yields shortest round-trip digits).

  1. Integer-valued floats in fixed notation always carry a trailing .0.
  2. -0.0 is preserved as -0.0.
  3. Fixed notation for |x| in [1e-6, 1e21); scientific outside. Zero

(0.0 / -0.0) is always rendered fixed.

  1. Scientific spelling: lowercase e, explicit sign, no zero-padding on

the exponent (e.g. 1e+21, 1e-7, 1.5e+300).

  1. The .0 rule applies only in the fixed branch, never scientific.

#_format_dict_for_display

python
def _format_dict_for_display(value: dict) -> str

Render a dict flag value as canonical key=value pairs.

Keys are sorted for deterministic output, matching Go's formatDictForDisplay. Values are rendered via _format_value_for_error.

#_format_default_for_help

python
def _format_default_for_help(value: object) -> str

Format a default value for help text.

Floats use the canonical form (SCF); dict values render as sorted key=value pairs (matching Go). Every other type is rendered as str.

#_format_value_for_error

python
def _format_value_for_error(value: object) -> str

Format a value for inclusion in error messages (without quotes).

Floats use the canonical form (SCF). Bools are lowercase. Dict values render as sorted key=value pairs (matching Go). Strings are returned as-is.

#_config_typename

python
def _config_typename(value: object) -> str

Return a type name for config values, matching Go's typeName.

#_parse_checks_toml

python
def _parse_checks_toml(data: bytes) -> tuple[str, dict[str, _CheckDef]]

Parse and validate checks TOML data, returning (app_name, check_defs).

Raises ValueError on any schema violation or invalid TOML.

#_load_checks_toml

python
def _load_checks_toml(path: str | Path) -> tuple[str, dict[str, _CheckDef]]

Read and parse a checks.toml file, returning (app_name, check_defs).

Raises ValueError on any file error, schema violation, or invalid TOML.

#_HelpRequested

Raised when --help or -h is encountered.

#_VersionRequested

Raised when --version or -v is encountered.

#_DumpSchemaRequested

Raised when --dump-schema is encountered.

#_McpRequested

Raised when --mcp is encountered.

#_ParseError

Raised for user-facing parse errors.

#InvokeError

Raised by app.call() for invocation errors (unknown command, missing flags, etc.).

#_strict_bool

python
def _strict_bool(s: str) -> bool

Parse a boolean string strictly.

Accepts: 1, true, yes (case-insensitive) -> True Accepts: 0, false, no (case-insensitive) -> False Everything else raises ValueError.

#_strict_int

python
def _strict_int(s: str) -> int

Parse an integer string strictly -- no leading/trailing whitespace allowed.

Python's int() silently strips whitespace; Go's strconv.Atoi does not. This matches Go's stricter behavior. Additionally, the result is range-checked to fit in a signed 64-bit integer, matching Go's int/int64.

All errors raise ValueError with the same message format as Go's parseIntStrict: "expected integer, got ''".

#_strict_float

python
def _strict_float(s: str) -> float

Parse a float string strictly -- no leading/trailing whitespace allowed.

Rejects nan, inf, and -inf (case-insensitive) since these are valid Python floats but not useful CLI values.

#_float_parse_error

python
def _float_parse_error(flag_name: str, raw: str, exc: ValueError, *, env: str | None=None) -> '_ParseError'

Build a _ParseError for a failed float parse.

If the ValueError is a NaN/Inf rejection, use its message directly. Otherwise, produce the generic "expected float, got ..." message.

#_coerce_arg_value

python
def _coerce_arg_value(a: 'Arg', raw: str) -> object

Coerce a raw positional arg string to the declared type.

Uses the same strict parsing functions as flags: _strict_int, _strict_float, _strict_bool. Error messages follow the same pattern as flag type errors, with "argument ''" instead of "--".

#_resolve_at_prefix

python
def _resolve_at_prefix(flag_name: str, raw: str, stdin_consumed_by: str | None) -> tuple[str, str | None]

Resolve @-prefix for string flag values.

Returns (resolved_value, updated_stdin_consumed_by).

#_parse_dict_value

python
def _parse_dict_value(flag_name: str, raw: str, value_type: type) -> tuple[str, object] | dict[str, object]

Parse a dict flag value from CLI.

Two formats:

  • key=value: splits on first '=', coerces value to value_type
  • JSON string starting with '{': parsed as JSON dict

For key=value format, returns a (key, coerced_value) tuple. For JSON format, returns a dict of {key: coerced_value}.

#_coerce_dict_json_value

python
def _coerce_dict_json_value(flag_name: str, key: str, value: object, value_type: type) -> object

Coerce a JSON-parsed value to the dict's value type.

#_store_dict_flag

python
def _store_dict_flag(f: 'Flag', raw: str, cli_set: dict) -> None

Parse and store a dict flag value from a raw CLI string.

Handles both key=value and JSON formats. For JSON, may add multiple entries at once. For key=value, adds one entry.

#_raise_flag_name_reserved_by_framework

python
def _raise_flag_name_reserved_by_framework(name: str)

Message template: a flag name collides with the reserved quartet.

#_raise_flag_name_yes_banned

python
def _raise_flag_name_yes_banned()

Message template: a flag named yes is banned outright.

yes owns no framework flag any more, but a private --yes would restate --approve-consequential in a spelling that IS muscle memory -- which is exactly what the rename removed.

python
def _raise_flag_name_consent_reserved()

Message template: a flag name collides with the consent parameter.

python
def _raise_arg_name_consent_reserved()

Message template: an arg name collides with the consent parameter.

#_parse_compound_type

python
def _parse_compound_type(raw_type: type, context: str) -> tuple[str, type | None, type | None]

Parse a type annotation into (kind, item_type, value_type).

Returns:

  • ("scalar", None, None) for str/bool/int/float
  • ("list", item_type, None) for list[T]
  • ("dict", None, value_type) for dict[str, T]

Raises ValueError for invalid compound types.

#_validate_element_type

python
def _validate_element_type(flag_name: str, expected_type: type, value: object, context: str) -> None

Validate that a value matches the expected scalar type.

#Flag

Represents a --flag declaration.

#Arg

Represents a positional argument.

#FlagSet

A reusable bundle of flags.

#MutexGroup

A group of mutually exclusive flags.

#CoRequired

Flags that must all appear together or none.

#Requires

Flag that depends on another flag being present.

#Implies

When a trigger flag is provided, automatically set a target flag to a value.

#Passthrough

Marks a command as passthrough -- all tokens after the command name are forwarded to the handler as a raw list, bypassing flag/arg parsing.

#Forwarding

Declares that a handler deliberately accepts and forwards **kwargs.

Guard v2 refuses a var-keyword handler unless the command declares forwarding. The reason is mandatory, non-empty, and emitted in the schema so a consumer's audit gate can review every forwarding site.

#DeprecatedCommand

A declaration-only deprecated command: prints message to stderr and exits 1.

#_raise_command_read_only_consequential

python
def _raise_command_read_only_consequential(name: str)

A read_only command cannot be consequential (contract §8.1).

Classification answers "should a dry run record rather than execute?"; consequential answers "are these effects worth interrupting someone for?". A command that changes nothing has no effects to weigh, so the two declarations cannot both hold.

#_raise_command_read_only_dry_run_unsupported

python
def _raise_command_read_only_dry_run_unsupported(name: str)

A read_only command cannot declare dry_run_supported=False.

Mirrors the read_only + consequential prohibition: a command that changes nothing records nothing, so a preview of it can never be dishonest and there is no reason to refuse one.

#_validate_dry_run_declaration

python
def _validate_dry_run_declaration(name: str, effect: str, dry_run_supported: bool, dry_run_unsupported_reason: str | None) -> None

The three registration-time guards on the dry-run declaration.

Shared by :class:Command.__post_init__ and :func:_build_and_validate_command so both registration surfaces reject the same shapes with the same messages.

#Command

A leaf command with a handler.

#Group

A container for nested commands and subgroups (arbitrary depth).

#group

python
def group(self, name: str, *, help: str, tags: set[str] | None=None, hidden: bool=False) -> Group

Create and register a child subgroup.

#deprecate

python
def deprecate(self, name: str, *, message: str, effect: str | None=None) -> None

Register a deprecated subcommand in this group.

Deprecated entries are classification-EXEMPT: they have no handler and execute nothing, so passing effect= is a registration-time error.

#command

python
def command(self, name: str, *, help: str, effect: str | None=None, consequential: bool=False, dry_run_supported: bool=True, dry_run_unsupported_reason: str | None=None, args: list[Arg] | None=None, flag_sets: list[FlagSet] | None=None, mutex: list[MutexGroup] | None=None, dependencies: list[CoRequired | Requires | Implies] | None=None, passthrough: Passthrough | None=None, grants: list[Grant] | None=None, forwarding: Forwarding | None=None, tags: set[str] | None=None, hidden: bool=False, interactive: bool=False, config_fields: list[str] | None=None) -> Callable[[F], F]

Decorator to register a command within this group.

#ConfigField

Declares a typed config file field.

Fields with no default are required — the config system will error if they are missing from the config file. Fields with a default are optional.

#Result

Returned by app.test().

#Tool

A tool descriptor for exposing CLI commands to tool-using LLM agents.

#_CheckProblem

A single minted finding: text plus severity ("error" or "warn").

Module-private -- problems are minted only via reporter methods.

#_CheckOutcome

The ceiling-typed result of a check implementation.

Module-private with a construction guard: a valid outcome is obtained ONLY through reporter methods (passed/skipped/found) or the runner's internal skip mint, both of which pass _MINT_TOKEN. Direct construction raises.

#status

python
def status(self) -> str

Derived verdict label ("pass"/"fail"/"warn"/"skip").

#_ordered_problems

python
def _ordered_problems(self) -> tuple[_CheckProblem, ...]

Problems grouped by severity: all error problems, then all warns.

#_mint_skip

python
def _mint_skip(message: str) -> _CheckOutcome

Runner-internal mint for cascade/scope skip outcomes.

#_derive_status

python
def _derive_status(outcome: _CheckOutcome) -> str

Map a minted outcome to its verdict label.

passed => pass; skipped => skip; found with an error problem => fail; found with only warns => warn.

#_ReporterCore

Shared problem accumulator and minting surface for both reporters.

Holds warn()/passed()/skipped()/found(). Error-minting lives ONLY on ErrorReporter, so WarnReporter structurally lacks it (accessing .error on a WarnReporter is an AttributeError at runtime and a type error under mypy).

#note

python
def note(self, text: str) -> None

Record an informational note. Non-empty text required.

Notes are allowed on EVERY outcome, including a pass -- they never trigger the problems-present errors that passed()/skipped() enforce. Notes are verdict-inert: they surface only under --verbose and in JSON.

#warn

python
def warn(self, text: str) -> None

Mint a warn-severity problem. Non-empty text required.

#passed

python
def passed(self, message: str) -> _CheckOutcome

Finalize a terminal PASS. Errors if any problems were reported.

#skipped

python
def skipped(self, reason: str) -> _CheckOutcome

Finalize a terminal SKIP. Errors if any problems were reported.

#found

python
def found(self, message: str) -> _CheckOutcome

Finalize an outcome carrying the accumulated problems.

Errors when nothing was reported -- nothing found means pass, so say so explicitly with passed().

#WarnReporter

Reporter handed to warn-severity check impls.

Can mint warn-severity problems and terminal outcomes but structurally LACKS error-minting: there is no error method, so a warn check cannot produce an error-severity problem and can never cascade.

#ErrorReporter

Reporter handed to error-severity check impls.

Everything WarnReporter has PLUS error (mints an error-severity problem).

#error

python
def error(self, text: str) -> None

Mint an error-severity problem. Non-empty text required.

#SkipCheck

Directive a scope adapter returns to skip a check with a reason.

The adapter can no longer mint arbitrary outcomes -- it either returns a replacement context (context projection) or this skip directive.

#CheckRunResult

A named check outcome returned by App.run_checks().

The verdict is derived from the minted outcome; the runner's exit/cascade logic and the formatters all consume these same accessors (one source of truth).

#status

python
def status(self) -> str

Derived label: "pass", "fail", "warn", or "skip".

#message

python
def message(self) -> str

The outcome's human-readable message.

#problems

python
def problems(self) -> tuple[_CheckProblem, ...]

The minted problems (error and warn severity) from this check run.

#notes

python
def notes(self) -> tuple[str, ...]

Informational notes recorded during the check run (verdict-inert).

#gated

python
def gated(self) -> bool

Whether the outcome carries an error-severity problem (derived FAIL).

#warned

python
def warned(self) -> bool

Whether the outcome carries only warn-severity problems (derived WARN).

#CheckContext

Minimal interface that tool-specific check contexts must satisfy.

#ConnectionEnvReader

OPTIONAL capability a check context may expose: the value of a declared connection env (connection_env), read live -- EXCEPT under --hermetic, where it resolves as absent (None, False) so a check can skip visibly instead of connecting. The check command wraps the tool-supplied check context in a value that satisfies this protocol, backed by the app's declared connection envs and the invocation's hermetic state. Checks that need a connection URL call ctx.connection_env_value("DATABASE_URL").

is_hermetic() reports whether the invocation ran under --hermetic. It exists so a check can DISTINGUISH the two cases that connection_env_value's present=False otherwise conflates: "--hermetic suppressed the connection env" vs "the env var is simply unset". A check that layers config fallbacks below the env must honor hermetic even when the env is unset -- otherwise it falls through to a config URL and connects, violating the hermetic guarantee::

dsn, present = ctx.connection_env_value("DATABASE_URL") if not present: if ctx.is_hermetic(): return reporter.skipped("hermetic: connection suppressed") # env unset but not hermetic -- config fallback is allowed here

#connection_env_value

python
def connection_env_value(self, env_var: str) -> 'tuple[str | None, bool]'

#is_hermetic

python
def is_hermetic(self) -> bool

#_CheckContextWithConn

Wraps a tool-supplied check context, delegating attribute access while adding connection-env access (hermetic-suppressed) so check functions can read declared connection envs without the tool implementing anything beyond project_root.

#connection_env_value

python
def connection_env_value(self, env_var: str) -> 'tuple[str | None, bool]'

#is_hermetic

python
def is_hermetic(self) -> bool

Report whether the invocation ran under --hermetic. Mirrors the hermetic flag captured when the wrapper was built.

#_CheckDef

Internal definition of a single check loaded from TOML.

#CheckSpec

A fully-formed, ceiling-typed check produced by a check provider.

Opaque by construction: build one only via :func:error_check_spec or :func:warn_check_spec, which bind the reporter form to the declared severity so the impl cannot mint a problem its severity forbids. Providers return lists of these (see :meth:App.register_check_provider).

#error_check_spec

python
def error_check_spec(*, name: str, tags: list[str], fast: bool, pure: bool, needs_network: bool, depends_on: list[str], impl: Callable, severity: str='error', scope: str='') -> CheckSpec

Build an error-severity check spec for a provider.

impl receives (ctx, reporter) where reporter is an :class:ErrorReporter (can mint both error- and warn-severity problems). severity must be "error" -- a mismatch is a hard error at materialization (the provider analog of the TOML/register severity check).

#warn_check_spec

python
def warn_check_spec(*, name: str, tags: list[str], fast: bool, pure: bool, needs_network: bool, depends_on: list[str], impl: Callable, severity: str='warn', scope: str='') -> CheckSpec

Build a warn-severity check spec for a provider.

impl receives (ctx, reporter) where reporter is a :class:WarnReporter, which structurally lacks error-minting: a warn check cannot cascade. severity must be "warn".

#App

The root CLI application.

#_validate_flag_infra_marker

python
def _validate_flag_infra_marker(self, f: Flag) -> None

Panic if a flag's default is a RelativeToRoot marker referencing an undeclared root. Called at registration for construction-time errors.

#_infra_access

python
def _infra_access(self, hermetic: bool=False) -> '_InfraAccess | None'

Snapshot infra data for a Context: resolved roots + declared handshake env var names + declared connection env var names. Connection envs are suppressed when hermetic is True. Returns None when nothing is declared.

#_record_coverage

python
def _record_coverage(self, cmd_path: str) -> None

Append a coverage record for the resolved command path.

Each test() or call() invocation appends one JSONL line to the process's shard file (named ".jsonl"). Uniqueness across concurrent writers comes from the PID and O_APPEND; one shard per process is sufficient, so there is no per-write shard counter.

#_collect_all_command_paths

python
def _collect_all_command_paths(self) -> set[str]

Enumerate all non-deprecated leaf command paths as dotted strings.

#_collect_all_commands

python
def _collect_all_commands(self) -> list[tuple[str, 'Command']]

Enumerate (dotted path, Command) pairs in registration order.

#_test_coverage_provider

python
def _test_coverage_provider(self) -> list[CheckSpec]

Built-in check provider for cli-test-coverage.

Registered automatically when test_coverage=True. The verdict is derived from committed state: the covered set is the union of the committed manifest (.strictcli/test-coverage.json) and any per-process shard files merged from .strictcli/coverage/. Every live registered command path (minus the injected check command) must be present in that union to pass; otherwise the check fails naming each uncovered command.

Because the verdict reads the committed manifest, it is deterministic on every machine -- a machine that never ran the suite (no local shards) still gets a stable verdict from the committed manifest alone. Both the coverage dir and the manifest path are anchored to the App's construction-time cwd, so the check evaluated from a foreign cwd reads the app's own repo state.

The manifest is rewritten as the monotonic union of its prior contents and the freshly merged shards, but ONLY when that content actually changes -- a pure check must not dirty a byte-identical file. Accepted staleness: deleting a test leaves its command covered in the manifest until the manifest is deliberately regenerated (e.g. by removing it and re-running the suite), because the union never removes a command.

#_effects_bypass_provider

python
def _effects_bypass_provider(self) -> list[CheckSpec]

Built-in check provider for the three effects-regime lints.

Registered whenever the check system turns on, so a consumer that adopts checks at all gets all three without a TOML declaration:

  • effects-bypass (error) fails on any direct process,

filesystem-mutation or network call REACHABLE FROM A REGISTERED COMMAND HANDLER. Its remediation is always "route it through ctx.effects", so a leaf the handle could not carry must never be a finding: the handle's closed method set has no in-process-observe method, which is why platform.system() is exempt while os.system(...) is not (see :data:_BYPASS_PROCESS_OS_ONLY);

  • observe-allowlist-breadth (warn) surfaces short

proc_observe_allowlist prefixes, which authorize real execution under --dry-run;

  • consequential-grant-agreement (warn) surfaces commands that

declare a process- or network-mutating grant but do not declare themselves consequential.

#config_file_path

python
def config_file_path(self) -> str

Return the resolved config file path for this app.

#dump_schema_dict

python
def dump_schema_dict(self) -> dict

Return the app's full schema as a dict, excluding project_id.

This is the public, CWD-free accessor for the schema. Unlike the --dump-schema flag (which writes .strictcli/schema.json and derives project_id from pyproject.toml in the current working directory), this method reads only the in-memory App and performs no filesystem or CWD access. The returned dict is byte-identical to the written schema file with the project_id field removed.

#config_field

python
def config_field(self, name: str, type: type, help: str, default: object=_MISSING) -> ConfigField

Declare a typed config file field.

Args:

  • name: Field name. Dots allowed for TOML sections (e.g. "serve.port").

Names starting with underscore are reserved for framework fields.

  • type: Field type — str, bool, int, or float.
  • help: Help text describing the field.
  • default: Default value. If omitted, the field is required.

Returns:

  • The registered ConfigField.

Raises:

  • ValueError: If the name is invalid, duplicated, reserved, or

the default doesn't match the declared type.

#_register_framework_field

python
def _register_framework_field(self, name: str, type: type, help: str) -> ConfigField

Register a framework-owned config field (e.g. _schema_version).

Framework fields must start with underscore. They are declared by the framework, not the user, and cannot conflict with user fields.

#error_check

python
def error_check(self, name: str) -> Callable[[F], F]

Decorator registering an error-severity check implementation.

The decorated function takes (ctx, reporter) where reporter is an :class:ErrorReporter (annotate it as such for mypy binding). It must return a :class:_CheckOutcome obtained from that reporter. The check must be declared severity = "error" in checks.toml.

#warn_check

python
def warn_check(self, name: str) -> Callable[[F], F]

Decorator registering a warn-severity check implementation.

The decorated function takes (ctx, reporter) where reporter is a :class:WarnReporter (which structurally lacks error, so a warn check cannot cascade). The check must be declared severity = "warn".

#_make_check_decorator

python
def _make_check_decorator(self, name: str, form: str) -> Callable[[F], F]

Build the shared registration decorator for error/warn checks.

Enforces the double-entry contract (declared vs registered) and cross-checks the registration FORM against the TOML-declared severity so that @app.error_check on a severity="warn" definition is a hard error.

#_validate_check_registrations

python
def _validate_check_registrations(self) -> str | None

Validate that all declared checks have registered implementations.

Returns an error message if any are missing, or None if all OK.

#tag_contract

python
def tag_contract(self, tag: str, *, requires_flag: str) -> None

Declare that any command with the given tag must have the named flag.

#_validate_tag_contracts

python
def _validate_tag_contracts(self) -> str | None

Check that all tag contracts are satisfied.

Returns an error message if any command violates a contract, or None.

#_resolve_config_data

python
def _resolve_config_data(self, runtime_path_override: str | None=None, hermetic: bool=False, is_runtime_flag: bool=False) -> _ConfigLoadResult

Single entry point for all config loading.

is_runtime_flag indicates the path came from --config (hard error on missing).

#_validate_config_fields

python
def _validate_config_fields(self, cmd: Command, config_data: dict) -> str | None

Validate config file contents against the command's bound config fields.

Checks:

  1. Each bound required config field exists in config with the correct type.
  2. Each key in config matches a registered flag, config field, or framework

field. Unknown keys are hard errors.

Returns an error message string, or None if all OK.

#set_check_context

python
def set_check_context(self, factory: Callable) -> None

Set the factory function that creates CheckContext for check runs.

The factory is called with no arguments and must return a CheckContext.

#_wrap_check_context

python
def _wrap_check_context(self, base)

Augment a tool-supplied check context with connection-env access (hermetic-suppressed). When no connection envs are declared, the base context is returned unchanged so the common case is unaffected.

#set_scope_adapter

python
def set_scope_adapter(self, adapter: Callable) -> None

Set the scope adapter callback for scoped checks.

The adapter is called as adapter(context, scope_string) and must return one of:

  • a replacement context object -- used as the check's context (context

projection), or

  • a :class:SkipCheck directive -- skips the check with the given

reason (no cascade, no exit-code change).

The adapter can no longer mint arbitrary outcomes: it either projects the context or skips. (This is the Python-only scope hook; Go has no scope adapter -- see the note in the Go check.go.)

#register_check_provider

python
def register_check_provider(self, provider: Callable[[], list[CheckSpec]]) -> None

Register a provider that supplies check specs at materialization time.

Three check-system hooks (do not confuse them):

  1. Check provider (this method) -- REGISTRY POPULATION. A provider

returns a list of fully-formed check specs (metadata + a ceiling-typed impl). Providers are the TOML-less way to add checks: they run lazily at the first registry read (materialization) and their specs go through the same single add-path as TOML-declared checks, so a name colliding with a TOML check or another provider's check is the usual hard error. Registering a provider ENABLES the check system (a TOML-less app with a provider gets a working check command).

  1. Check-context factory (:meth:set_check_context) -- PROJECT

CONSTRUCTION. Called once per run to build the CheckContext handed to every check impl. Answers "what project are we checking?".

  1. Scope adapter (:meth:set_scope_adapter, Python-only) -- PER-CHECK

CONTEXT PROJECTION. Called per scoped check to project the context or skip the check.

A provider decides WHICH checks exist; the context factory decides WHAT project they see; the scope adapter decides HOW an individual check sees that project.

A provider that returns an empty list is honest-empty (no checks for this context) and a valid no-op. A provider that raises is a hard error in every mode.

Reentrancy: a provider must not trigger check execution during materialization (e.g. by calling :meth:run_checks or the check command). Doing so re-enters materialization while it is in progress -- behavior is undefined (unbounded recursion). A provider's job is to return specs, nothing else.

#reset_check_provider_cache

python
def reset_check_provider_cache(self) -> None

Drop provider-sourced definitions and clear the materialization memo.

The next registry read re-runs all providers. Intended for tests and long-lived singletons. Does NOT unregister the providers themselves.

#_materialize_check_providers

python
def _materialize_check_providers(self) -> None

Run providers and insert their specs, memoized on the cwd.

Single chokepoint called at the start of every registry read (the check command handler and :meth:run_checks). A repeat call in the same cwd is a cheap no-op; a cwd change re-runs the providers (dropping the previous provider-sourced defs first).

#run_checks

python
def run_checks(self, context: CheckContext, *, tag_expr: str | None=None, name_glob: str | None=None, run_all: bool=False, ignore_warnings: bool=False, pure_only: bool=False) -> tuple[list[CheckRunResult], list[str], int]

Run checks programmatically with filtering and dependency resolution.

Returns (results, impure_listed, exit_code):

  • results: the executed checks as a list of CheckRunResult.
  • impure_listed: the ordered names of checks NOT executed because of the

purity partition (empty unless pure_only is set). Listed checks contribute nothing to the exit code -- a consumer renders them as e.g. "would run: <name> (impure)".

  • exit_code: 0 if all executed checks pass (or all warn with

ignore_warnings), else 1.

With pure_only set, only checks that are declared pure AND do not need network access execute; every other selected check (including a pure check that depends on a listed one) is listed instead. The default (pure_only False) is byte-identical to the previous behavior.

#_enable_checks

python
def _enable_checks(self) -> None

Turn on the check system exactly once.

Flips _checks_enabled, initializes the check registry if it is not already present, and registers the auto-generated check command a single time. Idempotent: calling it again is a no-op. Callers (currently the TOML-loading branches) route through this so that future check sources share the same enablement path.

#_add_check_def

python
def _add_check_def(self, cdef: _CheckDef) -> None

Single internal insertion point for check definitions.

Rejects duplicate names as a hard error and inserts the definition into the registry. TOML loading routes through here; this is also the future insertion point for provider-sourced definitions.

#_register_check_command

python
def _register_check_command(self) -> None

Register the auto-generated 'check' command when checks.toml exists.

#command

python
def command(self, name: str, *, help: str, effect: str | None=None, consequential: bool=False, dry_run_supported: bool=True, dry_run_unsupported_reason: str | None=None, args: list[Arg] | None=None, flag_sets: list[FlagSet] | None=None, mutex: list[MutexGroup] | None=None, dependencies: list[CoRequired | Requires | Implies] | None=None, passthrough: Passthrough | None=None, grants: list[Grant] | None=None, forwarding: Forwarding | None=None, tags: set[str] | None=None, hidden: bool=False, interactive: bool=False, config_fields: list[str] | None=None) -> Callable[[F], F]

Decorator to register a top-level command.

#group

python
def group(self, name: str, *, help: str, tags: set[str] | None=None, hidden: bool=False) -> Group

Create and register a command group.

#deprecate

python
def deprecate(self, name: str, *, message: str, effect: str | None=None) -> None

Register a deprecated top-level command.

Deprecated entries are classification-EXEMPT: they have no handler and execute nothing, so passing effect= is a registration-time error.

#_collect_all_flags

python
def _collect_all_flags(self) -> list[Flag]

Collect all flags (global + all commands in all groups), for config show.

#_colliding_config_fields

python
def _colliding_config_fields(self) -> dict[str, ConfigField]

Return {flag_param_name: ConfigField} for config fields whose name equals a flag's param name.

Such config fields are validation-only: they annotate the colliding flag rather than rendering as a separate config key. Callers use this to render the key once (on the flag line, with the config field's help as a trailing annotation).

#_confirm_consequential

python
def _confirm_consequential(self, cmd: 'Command', cmd_path: str) -> None

The framework-owned confirm protocol.

Fires before dispatching a command that DECLARES ITSELF consequential, on the real CLI path, when neither --dry-run nor --approve-consequential was passed. A plain mutating command never prompts: classification answers "should a dry run record rather than execute?", which is a different question from "are these effects worth interrupting someone for?". Never fires on the programmatic paths (test/call/_invoke/MCP), which have no TTY contract and would hang.

A consequential PASSTHROUGH is not exempt: the framework knows LESS about what is about to happen, not more.

#_arm_effects

python
def _arm_effects(self, cmd: 'Command', cmd_path: str, *, dry_run: bool) -> '_Effects'

Arm the effects handle for one dispatch (the runtime seal).

Called at EVERY ctx-construction site that dispatches a handler, so there is no path on which ctx.effects is missing or a carrier escapes unpoisoned. The log itself is reset by :meth:_begin_dispatch, which runs earlier so pre-handler CACHE_WRITEs (coverage shards) land in the same dispatch's log.

#_begin_dispatch

python
def _begin_dispatch(self) -> None

Start a new dispatch: reset the structured effect log.

#_render_dry_log

python
def _render_dry_log(self, cmd_path: str, *, aborted: bool) -> None

Write the would-do log for a dry run. No-op outside dry mode.

Called on every exit path out of a dispatch, so a handler that leaves through sys.exit or an exception still shows the preview it was asked for. The log always goes to stdout and is never suppressed by --quiet: it is dry mode's primary output.

aborted marks a dispatch that did not finish. The log is still written -- the recorded effects are owed either way -- and the marker that follows it on stderr says the reader cannot assume the list is the whole preview. The truncation path (which ends the preview for its own pinned reason) renders itself and never comes through here.

#_record_cache_write

python
def _record_cache_write(self, path: str) -> None

Record a framework-blessed CACHE_WRITE.

The closed list of sites is exactly three: the schema dump, the test-coverage shards, and the test-coverage manifest. CACHE_WRITEs have no public method, never appear in the would-do log, never trip read-only enforcement, and EXECUTE even in dry mode -- which is why they always carry recorded: false.

#effect_log

python
def effect_log(self) -> list[dict]

Return the structured effect records of the most recent dispatch.

Test-only surface, beside test() and _last_sources.

#_build_framework_command

python
def _build_framework_command(self, name: str, *, help: str, effect: str, handler: Callable, args: list[Arg] | None=None, mutex: list[MutexGroup] | None=None, extra_flags: list[Flag] | None=None, interactive: bool=False) -> Command

Build one of strictcli's own auto-registered commands.

Framework-internal commands (check and the five config subcommands) go through the same single validated registration path as every consumer command -- there is no direct-Command-construction bypass left. Their handlers absorb the app's app-defined global flag values through **kwargs, which is legal only because they declare forwarding, and the private _framework_internal marker (unreachable from any public factory) makes the framework verify that the handler is actually defined in this module.

#_register_config_group

python
def _register_config_group(self) -> None

Register the auto-generated 'config' command group.

#_pre_scan_reserved_flags

python
def _pre_scan_reserved_flags(self, argv: list[str]) -> dict

Pre-scan for the framework-owned reserved flags.

Handles --dump-schema, --mcp, --config, --hermetic and the effects-regime quartet --dry-run/--approve-consequential/--quiet/--verbose.

Two regions, two rulesets (contract §7.2, amended):

  • The pre-command region (before the first non-flag token, before

--) recognizes every reserved flag. Known global flags and their values are skipped so that a global-flag value matching a command name does not terminate the region early.

  • The command region recognizes ONLY the quartet, anywhere, exactly

like --help/-h. --hermetic/--config/--dump-schema/--mcp stay pre-command-only. See _scan_command_region_quartet.

Returns a dict with keys: dump_schema, serve_mcp, hermetic, config_path, dry_run, approve_consequential, quiet, verbose, err, cleaned_argv.

#_scan_command_region_quartet

python
def _scan_command_region_quartet(self, argv: list[str], start: int, result: dict, exclude_indices: set[int]) -> None

Recognize the reserved quartet in the command region of argv.

Contract §7.2 (amended 2026-08-04): the quartet's four tokens are recognized ANYWHERE in argv, exactly like --help/-h, because their applicability is per-command -- requiring them before the command name was backwards. Only the quartet is recognized here; --hermetic, --config, --dump-schema and --mcp remain pre-command-only.

The scan stops for good at two boundaries:

  • a bare --, after which every token is positional data;
  • a passthrough command's name, after which every token belongs to

the child process and is forwarded byte-for-byte. Eating a child's own --verbose would silently change what the child does.

Routing tokens are walked through the group/command tree so a quartet token may sit anywhere among them. Nothing here raises: routing errors are the real parse's job.

Both boundaries are visible in the dry_run_supported=False refusal, which reads the flag this scan resolved. app cmd -- --dry-run and app passthrough --dry-run are NOT refused, because in neither case did the operator ask this app for a dry run: after -- the token is the command's own data, and after a passthrough's name it is the child process's flag. app --dry-run passthrough IS refused -- there the token is unambiguously addressed to this app.

#_parse

python
def _parse(self, argv: list[str]) -> tuple[Command, dict[str, object] | list[str], dict[str, str]]

Parse argv (without program name) into a resolved Command and kwargs.

For normal commands, returns (Command, kwargs_dict, sources). For passthrough commands, returns (Command, raw_args_list, {}). Callers disambiguate by checking cmd.passthrough.

After parsing, self._last_global_values holds the parsed global flag values (used by passthrough command handlers).

#_resolve_command

python
def _resolve_command(self, path_segments: list[str]) -> tuple[Command, list[str], list[str]]

Traverse groups/commands tree to resolve a command from path segments.

Takes the remaining argv tokens after global flag parsing (group names, command name, and command arguments). Consumes group and command tokens from the front, returning the resolved Command, the unconsumed tokens (command arguments), and the list of group names traversed.

Raises _HelpRequested for group-level help and _ParseError for deprecated or unknown commands.

#_parse_global_flags

python
def _parse_global_flags(self, argv: list[str], *, hermetic: bool=False) -> tuple[dict[str, object], dict[str, str], list[str]]

Parse global flags from argv, returning (global_values, global_sources, remaining_tokens).

Scans tokens from left to right. Global flags are consumed; the first non-global-flag token (the command name) and everything after it are returned as remaining tokens. A bare -- stops global flag parsing and is included in the remaining tokens.

When hermetic is True, env var and config resolution are skipped entirely.

#_find_command_prefix

python
def _find_command_prefix(self, cmd: Command) -> str

Find the group prefix for a command (for help formatting).

Traverses the group tree recursively to find the full path.

#run

python
def run(self) -> None

Run the CLI application, reading from sys.argv.

#test

python
def test(self, argv: list[str]) -> Result

Run the CLI with given argv, capturing output and exit code.

#_invoke

python
def _invoke(self, command_path: str, kwargs: dict[str, object], *, approve_consequential: bool=False) -> object

Invoke a command programmatically with pre-typed kwargs.

This is the internal pipeline for programmatic invocation. It bypasses CLI parsing, env var resolution, config file loading, and stdin handling. The caller provides fully-typed values directly.

Args:

  • command_path: dot-separated path to the command

(e.g. "deploy" or "config.set").

  • kwargs: handler keyword arguments. Flag names use underscores

(e.g. dry_run). Positional args use their declared name. For passthrough commands, pass a single key "_args" with a list of raw string arguments.

  • approve_consequential: the caller's explicit consent. A command

that declares itself consequential is refused without it.

Returns:

  • The handler's return value (structured data, int, or None).

Raises:

  • _ParseError: if validation fails (missing required flags,

mutex violations, dependency errors, etc.), or if the command is consequential and no consent was supplied.

  • _HelpRequested: if the command path resolves to a group

with no subcommand.

#call

python
def call(self, command_path: str, *, approve_consequential: bool=False, **kwargs: object) -> object

Invoke a command programmatically and return its result.

Unlike _invoke(), this is the public API. It converts internal _ParseError exceptions to InvokeError so callers don't need to depend on private types.

Args:

  • command_path: dot-separated path to the command

(e.g. "deploy" or "config.set").

  • approve_consequential: the caller's explicit consent, the

programmatic counterpart of --approve-consequential. Keyword-only, and never a handler kwarg: the name is framework-reserved, so no command can declare a parameter that collides with it. A command that declares itself consequential is refused without it. Read-only and plain mutating commands ignore it.

  • **kwargs: handler keyword arguments. Flag names use underscores

(e.g. dry_run). Positional args use their declared name. For passthrough commands, pass _args=[...] for raw arguments.

Returns:

  • The handler's return value (structured data, int, or None).

Raises:

  • InvokeError: if validation fails (unknown command, missing

required flags, mutex violations, dependency errors, etc.), or if the command is consequential and no consent was given.

#_call_with_kwargs

python
def _call_with_kwargs(self, command_path: str, kwargs: dict[str, object], *, approve_consequential: bool) -> object

call() with the handler kwargs as a dict instead of a splat.

The MCP server routes through here rather than call(**arguments): an approve_consequential key inside a tools/call arguments object is a parameter of the command's own namespace -- no command can declare that reserved name, so it must surface as the usual unknown-parameter error, exactly as it does in the siblings whose kwargs are a map. Splatting it would silently promote it to consent.

#acall

python
async def acall(self, command_path: str, *, approve_consequential: bool=False, **kwargs: object) -> object

Async version of call(). Runs the handler in a thread.

Args:

  • command_path: dot-separated path to the command.
  • approve_consequential: the caller's explicit consent (same as

call()).

  • **kwargs: handler keyword arguments (same as call()).

Returns:

  • The handler's return value (structured data, int, or None).

Raises:

  • InvokeError: if validation fails, or if the command is

consequential and no consent was given.

#json_schema

python
def json_schema(self, command_path: str) -> dict

Produce a JSON Schema parameters object for a command's flags and args.

Args:

  • command_path: dot-separated path to the command (e.g. "deploy"

or "config.show").

Returns:

  • A JSON Schema object with "type": "object", "properties",
  • "required", and "additionalProperties": false.

Raises:

  • InvokeError: if the command path is invalid or resolves to a group.

#as_tools

python
def as_tools(self) -> list[Tool]

Export non-hidden, non-interactive leaf commands as Tool descriptors.

Returns a list of Tool objects, one per eligible command plus a router tool. Each tool's execute function wraps acall().

#_collect_tools_from_group

python
def _collect_tools_from_group(self, group: Group, path: list[str], tools: list[Tool], command_paths: list[str]) -> None

Recursively collect non-hidden, non-interactive commands from a group.

#_make_tool

python
def _make_tool(self, command_path: str, cmd: Command) -> Tool

Build a Tool for a single command.

#_make_router_tool

python
def _make_router_tool(self, command_paths: list[str]) -> Tool

Build the router tool that dispatches to per-command tools.

#serve_mcp

python
def serve_mcp(self, *, input: io.TextIOBase | None=None, output: io.TextIOBase | None=None) -> None

Run a JSON-RPC 2.0 MCP server on stdin/stdout.

Reads one JSON object per line from input (default: sys.stdin), writes one JSON object per line to output (default: sys.stdout). Handles initialize, tools/list, tools/call, and notifications.

The server runs until input is exhausted (EOF).

#_build_json_schema

python
def _build_json_schema(cmd: Command) -> dict

Build a JSON Schema parameters object for a command's flags and args.

#_tokens_contain_help

python
def _tokens_contain_help(tokens: list[str]) -> bool

Check if --help or -h appears in tokens before any -- separator.

#_validate_choices

python
def _validate_choices(name: str, val: object, repeatable: bool, choices: list | None, *, is_arg: bool=False) -> None

Validate a resolved flag or arg value against its choices list.

Raises _ParseError on an invalid value. is_arg selects the message prefix ("argument 'name':" instead of "--name:"); the two f-strings are kept as full literals so conformance/check_error_parity.py can extract them. A None value is exempt from validation: None only arises when the flag or arg was not passed (an unset mutex flag, or default=None on an arg) -- a CLI-supplied value is never None.

#_validate_and_build_kwargs

python
def _validate_and_build_kwargs(cmd: Command, store: _SourcedStore, positionals: list[str], global_flag_names: set[str], infra_roots: dict[str, str] | None=None) -> tuple[Command, dict[str, object], dict[str, object], dict[str, str]]

Validate parsed values and build the kwargs dict for the command handler.

This is the second half of command parsing: mutex enforcement, implies resolution, dependency checks, defaults, choices validation, custom validation, positional arg resolution, and kwargs building. It operates on sourced values in the store and doesn't care how they were produced.

Returns (cmd, kwargs, global_cli_set, sources) where sources maps flag param names to source labels (cli/env/config/default/implied).

#_parse_command

python
def _parse_command(cmd: Command, tokens: list[str], global_flags: list[Flag] | None=None, config_data: dict | None=None, stdin_consumed_by: list[str | None] | None=None, conflict_mode: str='cli-wins', hermetic: bool=False, infra_roots: dict[str, str] | None=None) -> tuple[Command, dict[str, object], dict[str, object], dict[str, str]]

Parse tokens against a resolved command's flags and args.

Returns (cmd, kwargs, global_cli_set, sources) where global_cli_set contains any global flag values parsed from tokens appearing after the command name.

stdin_consumed_by is a mutable single-element list tracking which flag has already consumed stdin via @-. Updated in-place.

When hermetic is True, env var and config resolution are skipped entirely.

#_flag_param_name

python
def _flag_param_name(flag_name: str) -> str

Convert a flag name like '--dry-run' to a Python parameter name 'dry_run'.

If the result is a Python keyword (e.g. 'global', 'class'), appends '_' per PEP 8 convention (e.g. 'global_', 'class_').

#_build_and_validate_command

python
def _build_and_validate_command(name: str, *, help: str, effect: str | None, consequential: bool=False, dry_run_supported: bool=True, dry_run_unsupported_reason: str | None=None, handler: Callable, args: list[Arg] | None, flag_sets: list[FlagSet] | None, mutex: list[MutexGroup] | None, dependencies: list[CoRequired | Requires | Implies] | None=None, env_prefix: str | None, global_flags: list[Flag] | None=None, passthrough: Passthrough | None=None, grants: list[Grant] | None=None, forwarding: Forwarding | None=None, framework_internal: bool=False, extra_flags: list[Flag] | None=None, tags: set[str] | None=None, inherited_tags: frozenset[str] | None=None, hidden: bool=False, interactive: bool=False, config_fields: list[str] | None=None, config_fields_ref: dict[str, ConfigField] | None=None, infra_root_names: frozenset[str] | None=None, connection_env_names: frozenset[str] | None=None) -> Command

Build a Command from a decorated handler, validate everything.

This is the single registration path: every command in every app -- including strictcli's own framework-internal check and config commands -- is built here, so classification, signature validation and flag validation are unbypassable.

#flag

python
def flag(name: str, *, short: str | None=None, type: type=str, default: object=_MISSING, help: str, env: str | None=None, env_separator: str | None=None, prefixed: bool=True, negatable: object=_MISSING, choices: list | None=None, validate: Callable | None=None, repeatable: bool=False, unique: object=_MISSING, conflict_mode: object=_MISSING, connection_url: bool=False, connection_env: str | None=None) -> Callable[[F], F]

Module-level decorator to attach a Flag to a command handler.

#arg

python
def arg(name: str, *, help: str, required: bool=True, default: object=_MISSING, variadic: bool=False, type: type=str, choices: list | None=None) -> Callable[[F], F]

Module-level decorator to attach an Arg to a command handler.

#_format_version

python
def _format_version(app: App) -> str

Format version string: '{name} {version}'.

#_format_app_help

python
def _format_app_help(app: App) -> str

Format app-level help shown when the user runs 'myapp --help'.

#_format_group_help

python
def _format_group_help(app: App, group: Group, path: list[str] | None=None) -> str

Format group-level help shown when the user runs 'myapp group --help'.

path is the list of group names leading to this group (e.g. ['dns', 'zone']). When None, the path is computed by searching the app's group tree.

#_find_group_path

python
def _find_group_path(app: App, target: Group) -> list[str]

Find the full path (list of group names) from app root to the target group.

#_build_flag_spec

python
def _build_flag_spec(f: Flag) -> str

Build the left-column spec string for a flag (e.g. '--target, -t ').

#_build_flag_meta

python
def _build_flag_meta(f: Flag) -> str

Build the bracketed metadata suffix for a flag.

#_format_dry_run_section

python
def _format_dry_run_section(cmd: Command) -> list[str]

The Dry run: section of command help, or nothing.

Rendered only for a command that declares dry_run_supported=False: the baseline (dry run works) needs no announcement, and a section on every command would be noise. Byte-identical across implementations.

#_format_command_help

python
def _format_command_help(app: App, cmd: Command, prefix: str='') -> str

Format command-level help shown when the user runs 'myapp cmd --help'.

#_tagdsl_tokenize

python
def _tagdsl_tokenize(expr: str) -> list[tuple[str, str, int]]

Tokenize a tag expression into (type, value, position) tuples.

#_tagdsl_parse

python
def _tagdsl_parse(tokens: list[tuple[str, str, int]]) -> tuple

Parse tag expression tokens into an AST using recursive descent.

Precedence (tightest first): NOT, AND, XOR, OR, DIFF.

#_tagdsl_evaluate

python
def _tagdsl_evaluate(ast: tuple, tags: set[str]) -> bool

Evaluate a tag DSL AST against a set of tags.

#_match_tag_expr

python
def _match_tag_expr(expr: str, tags: set[str]) -> bool

Evaluate a tag expression against a set of tags. Returns bool.

#_filter_checks

python
def _filter_checks(check_defs: dict[str, _CheckDef], tag_expr: str | None, name_glob: str | None, run_all: bool) -> set[str]

Filter checks by tag expression and/or name glob.

Returns the set of selected check names.

#_resolve_check_order

python
def _resolve_check_order(check_defs: dict[str, _CheckDef], selected: set[str]) -> list[str]

Resolve execution order via topological sort, pulling in dependencies.

If a selected check depends on an unselected check, the dependency is pulled into the execution set. Raises ValueError on cycles.

#_find_cycle

python
def _find_cycle(check_defs: dict[str, _CheckDef], nodes: set[str]) -> str

Find and format a cycle among the given nodes for error reporting.

#_check_is_pure

python
def _check_is_pure(cdef: _CheckDef) -> bool

Whether a check is executable under the purity partition: declared pure AND not requiring network access. Everything else is "impure".

#_run_checks

python
def _run_checks(check_defs: dict, check_names: list[str], context: CheckContext, ignore_warnings: bool, scope_adapter: object | None=None, pure_only: bool=False) -> tuple[list[tuple[str, _CheckOutcome, int]], list[str], int]

Execute checks in order, skipping dependents of gated (FAIL) checks.

Returns (results_list, impure_listed, exit_code). Each results_list entry is (name, outcome, duration_ms) where duration_ms is the wall-clock time in integer milliseconds spent inside the impl (0 for non-executed checks). impure_listed holds the ordered names of checks left unexecuted by the purity partition (empty unless pure_only=True); listed checks contribute nothing to the exit code. exit_code is 0 if all executed checks pass (or all warn with ignore_warnings=True), 1 otherwise.

Purity partition (pure_only): only pure, non-network checks execute; every other check is listed. A check also joins the listing if any dependency was listed (its precondition cannot be verified). The failed-dependency cascade takes precedence over the listing.

#_check_list_mode

python
def _check_list_mode(check_defs: dict[str, _CheckDef], json_mode: bool) -> None

Print check listing in human or JSON format.

#_check_dry_run_mode

python
def _check_dry_run_mode(check_defs: dict[str, _CheckDef], order: list[str]) -> None

Print execution plan without running checks.

#format_check_results

python
def format_check_results(results: list[CheckRunResult], verbose: bool=False) -> str

Format check results as a human-readable aligned string.

Shows the derived status label, name, and message, with minted problems listed under the check row grouped by severity (error problems first, then warn problems), each tagged with its severity. Problems appear for fail/warn/skip outcomes or when verbose is True.

#format_check_results_json

python
def format_check_results_json(results: list[CheckRunResult]) -> str

Format check results as a JSON string.

Each entry carries the derived status plus the minted problems (each with its severity and text). Problems serialize as [] when empty.

#_serialize_flag

python
def _serialize_flag(f: Flag) -> dict

Serialize a Flag to a JSON-serializable dict.

Identity fields (name, type, help) are always included. Other fields are omitted when they match the schema defaults.

#_serialize_arg

python
def _serialize_arg(a: Arg) -> dict

Serialize an Arg to a JSON-serializable dict.

Identity fields (name, help) are always included. Other fields are omitted when they match the schema defaults.

#_serialize_command

python
def _serialize_command(cmd: Command) -> dict

Serialize a Command to a JSON-serializable dict.

Identity fields (name, help) are always included. Other fields are omitted when they match the schema defaults.

#_serialize_group

python
def _serialize_group(group: Group) -> dict

Serialize a Group to a JSON-serializable dict (recursive).

Identity fields (name, help) are always included. Other fields are omitted when they match the schema defaults.

#_build_schema_defaults

python
def _build_schema_defaults() -> dict

Return the defaults object documenting what 'missing' means in the schema.

#_read_project_id

python
def _read_project_id() -> str

Read project name from pyproject.toml in the current working directory.

#_collect_config_field_bindings

python
def _collect_config_field_bindings(commands: dict[str, Command], bindings: dict[str, list[str]], path: list[str]) -> None

Walk commands and record which commands bind each config field.

#_collect_config_field_bindings_from_group

python
def _collect_config_field_bindings_from_group(group: Group, bindings: dict[str, list[str]], path: list[str]) -> None

Recursively walk groups to collect config field bindings.

#_dump_schema_core

python
def _dump_schema_core(app: App) -> dict

Build the full schema dict, excluding project_id.

This is the CWD-free, filesystem-free core of schema production. It reads only the in-memory App (name, version, help, flags, commands, groups, etc.). project_id is added later by the file-writer path, since it is the only field that requires reading pyproject.toml from the CWD.

Fields whose values match the schema defaults are omitted. The top-level defaults key documents what each missing field means.

#_dump_schema

python
def _dump_schema(app: App) -> dict

Produce the full schema dict including project_id (reads the CWD).

Delegates the bulk of the work to :func:_dump_schema_core and inserts project_id immediately after defaults so the on-disk layout is stable and byte-identical to the core dict once project_id is removed.

#_check_schema_project_id

python
def _check_schema_project_id(file_path: str, new_project_id: str) -> None

Verify that an existing schema file belongs to the same project.

Raises RuntimeError on mismatch. Silently passes on: missing file, unreadable file, JSON without project_id field, or matching project_id.

#_write_schema

python
def _write_schema(app: App) -> str

Write the schema to .strictcli/schema.json and return the path.

#_mcp_collect_commands

python
def _mcp_collect_commands(app: App) -> dict[str, tuple[Command, str]]

Collect non-hidden, non-interactive leaf commands as {dotted_path: (cmd, help)}.

Returns a dict mapping dotted command paths to (Command, help_text) tuples.

#_mcp_jsonrpc_error

python
def _mcp_jsonrpc_error(req_id: object, code: int, message: str) -> dict

Build a JSON-RPC 2.0 error response.

#_mcp_handle_initialize

python
def _mcp_handle_initialize(app: App, req_id: object) -> dict

Handle the MCP 'initialize' request.

#_mcp_handle_tools_list

python
def _mcp_handle_tools_list(app: App, commands: dict[str, tuple[Command, str]], req_id: object) -> dict

Handle the MCP 'tools/list' request.

#_mcp_handle_tools_call

python
def _mcp_handle_tools_call(app: App, req_id: object, params: dict) -> dict

Handle the MCP 'tools/call' request.

#_run_mcp_server

python
def _run_mcp_server(app: App, *, input: io.TextIOBase | None=None, output: io.TextIOBase | None=None) -> None

Run the MCP JSON-RPC 2.0 server loop.

Reads one JSON object per line from input, writes responses to output. Notifications (no 'id' field) get no response.

#PROC_MUTATE

python
PROC_MUTATE = 'proc_mutate'

#PROC_SPAWN

python
PROC_SPAWN = 'proc_spawn'

#FILE_WRITE

python
FILE_WRITE = 'file_write'

#NET_MUTATE

python
NET_MUTATE = 'net_mutate'
Search