ctl-core 0.6.26

Shared clap chassis for the *ctl CLIs
Documentation
# yaml-language-server: $schema=https://github.com/victor-software-house/qctl/releases/download/v0.5.9/tasks.schema.json
schema_version: 4
prefix: CTC

style:
  timezone: "-03:00"
  section_order: [queue, horizon, archive]
active: CTC-006

queue:
  - id: CTC-006
    title: Test fixture tree and per-suite temp dirs
    scope: ctl-core
    outcome: >-
      Integration tests write files through a small fixture API, not
      repeated fs::write + indoc. Each suite or scenario gets its own
      temp root.
    blocked_by: []
    acceptance:
      - A cfg(test) or ctl-core test feature exposes the helper (not a new public runtime crate unless we need one).
      - root.file(path, body) (or equivalent) is the write path. Nested paths create parents.
      - Suite::new(name) / suite.scenario(name) each own a TempDir that drops at the end of the test.
      - verctl (then qctl/forkctl) can drop the copy-pasted fs::write fixtures after pinning that ctl-core version.
    notes:
      - Triggered by verctl tests (prepare_stage, changelog, publish). Do not invent a second tempfile wrapper in each *ctl.

  - id: CTC-008
    title: Own the declared-input layer every ctl CLI parses config with
    scope: ctl-core
    outcome: >-
      One implementation of "read it, parse the shape, validate it once,
      complain in the repo's own words" — instead of one per CLI, drifting.
    blocked_by: []
    acceptance:
      - 'The shared validators live here: a path that must stay inside the repository, a single file name, a list that needs at least one entry, a value that cannot be empty.'
      - Each one is a garde `custom` rule generic over the context, so it works in a schema validated against nothing and in one validated against the whole document.
      - A complaint says what a person changes ("must declare at least one label"), never a stock validator's wording ("length is lower than 1").
      - 'One entry point loads a file: read, serde for shape, garde once at the boundary, nothing downstream re-checking.'
      - verctl drops its own src/schema.rs after pinning that ctl-core version, and no copy is left behind.
      - The declare-your-own-target template renderer is available to any ctl CLI, not only verctl.
      - qctl drops its own YAML parsing for config after pinning that ctl-core version.
      - The loader parses YAML with serde-saphyr, with strict booleans, and duplicate keys and merge keys refused; qctl, verctl, and forkctl read every YAML input through it.
      - A schema error names its file and line, placed through the yamled location index, and every error in a file is reported, not only the first.
      - The loader splits Markdown frontmatter, so verctl reads changeset fragments through it and drops gray_matter.
    notes:
      - "2026-08-18: verctl built this while shipping served files (VER-018, and VER-022 is the matching row there). It is already shared between two unrelated schemas in that repo — a TOML config file and a Jinja template's own exports — which is the evidence it is not verctl-shaped. Do this row before VER-022 so that one is a move rather than a fork. Related: qctl's schemars work and VER-021 want JSON Schema generated from these same types, so keep the types derive-friendly."
      - '2026-09-24: verctl parses YAML with yaml_serde 0.10.7 and qctl with serde_yml 0.0.12, so two ctl CLIs already disagree on the parser. Pick one crate here, by our own evaluation, and move both.'
      - "2026-09-24: verctl's config is YAML (.ctl/ver.yaml) since verctl 0.1.0. The TOML file in the 2026-08-18 note is gone."
      - '2026-09-28: YAML stack decided by evaluation on the build host. serde-saphyr 1.3.0 against yaml_serde 0.10.7: both keep dates as strings and reject 2026-02-31 through a JSON Schema date format; yaml_serde silently keeps the last duplicate key when read into serde_json::Value, turns .inf into null, and gives no line for a missing field; serde-saphyr reports a line for every error and rejects both once strict_booleans, merge_keys: Error, and with_snippet: false are set. Edits move to yamled (github.com/victor-software-house/yamled), a pure-Rust format-preserving editor on granit-parser spans, because yamlpatch requires yaml_serde. That drops yaml_serde, yamlpath, yamlpatch, tree-sitter, serde_yml, gray_matter, and yaml-rust2 from the family.'
      - "2026-09-29: first slice in ctl-core. The validate feature carries verctl's four garde rules; the input feature (Input::load, check, parse, frontmatter) reads YAML through serde-saphyr with strict booleans and duplicate and merge keys refused, and places every garde problem on its file and line through yamled 0.0.1 with default features off. Plan: openspec/changes/archive/2026-10-11-declared-input/proposal.md. Left on this row: verctl, qctl, and forkctl move onto input; verctl drops src/schema.rs and gray_matter; the template renderer moves here."

  - id: CTC-020
    title: Take template v0.0.5 and lock only the platforms in use
    scope: .copier-answers.yml at templates v0.0.5, mise.toml [settings], and every mise lock
    outcome: mise install on linux-x64 or macos-arm64 leaves every lock unchanged, because mise.toml declares lockfile_platforms and every lock records exactly those two platforms.
    blocked_by: []
    acceptance:
      - copier update to v0.0.5 leaves no conflict marker, and mise.toml sets lockfile_platforms = ["linux-x64", "macos-arm64"].
      - Every lock is regenerated whole with mise lock --platform linux-x64,macos-arm64, and its linux-x64 and macos-arm64 entries match main.
      - On a fresh linux-x64 machine, mise install leaves git status clean, and mise run verify passes.

  - id: CTC-021
    title: Read TOML config files through the declared-input loader
    scope: ctl-core input feature, the input-toml feature, and the declared-input spec
    outcome: 'A ctl CLI reads a TOML config file through the same Input as YAML: one parse, one garde validation, every problem on its file, line, and column.'
    blocked_by: []
    acceptance:
      - Input::open(path) picks the format from the extension (.toml, .yaml, .yml) and refuses any other extension with a problem; Input::new and Input::read stay YAML.
      - A TOML parse error, a type error, and each garde field problem (top-level key, table, inline table, array of tables) report a 1-based line and column.
      - A TOML text is held to max_reader_input_bytes, max_depth, and max_nodes from the same tiers as YAML, and a duplicate key reports on its line.
      - The input-toml feature compiles the toml crate and nothing else changes for a YAML-only consumer; check:features builds it alone and mise run verify passes.
    notes:
      - OpenSpec change toml-input in openspec/changes/toml-input.
      - 'Operator decision 2026-10-14: TOML is better for config files, so ctl-core reads it natively. forkctl, qctl, verctl, and state-sync keep reading YAML unchanged.'
    links:
      - https://github.com/victor-software-house/ctl-core/pull/120

  - id: CTC-022
    title: Bind every flag to an environment variable
    scope: ctl-core flags, help, and the env-flags spec
    outcome: A ctl CLI binds every long option to <PREFIX>_<LONG> with one root attribute, so an action or CI step sets env and runs the bare verb.
    blocked_by: []
    acceptance:
      - 'With #[command(env_prefix = "VERCTL")] on the root parser, every long option of the root and of every subcommand reads VERCTL_<LONG>, and a flag on the command line wins over it.'
      - An option that declares its own env, has no long name, is hidden, is a --no-* side, or is help or version stays unbound, and an empty variable counts as unset.
      - The command line wins over a variable when it sets the option, its --no-* side, or an option that conflicts with it, and two conflicting variables fail with an error that names both.
      - 'ctl-core help prints [env: NAME] beside each bound option.'
      - A command without the attribute binds nothing, and mise run verify passes.
    notes:
      - OpenSpec change env-bound-flags in openspec/changes/env-bound-flags.
      - 'Operator decision 2026-10-15: every ctl CLI is configurable through environment variables, with precedence flag, then variable, then config file, then default; actions set env and run the bare verb with no argument assembly in bash.'

  - id: CTC-023
    title: Crate docs on docs.rs
    scope: ctl-core
    outcome: docs.rs shows ctl-core as a landing page with a quick start, how output flows, and its Cargo features, lists modules and the prelude instead of every item twice, and every fallible public function documents its errors.
    blocked_by: []
    acceptance:
      - 'src/lib.rs carries the landing page as //! sections: a summary, Quick start as a compiled no_run doctest, How output flows, and Cargo features from document_features.'
      - 'Crate-root re-exports are #[doc(hidden)]; the prelude documents the flat surface, and its third-party re-exports are #[doc(no_inline)].'
      - 'No missing_errors_doc expect remains; every fallible public function has a # Errors section.'
      - package.metadata.docs.rs sets all-features and rustdoc-args --cap-lints warn, and the crate root enables doc_cfg under docsrs.
      - Module summaries live once, in each module's //! docs.
    notes:
      - Prototype from the docs.rs standard review on 2026-10-15; the standard is the rust-projects skill's Crate docs on docs.rs section.

  - id: CTC-025
    title: Read TOML input without the YAML and garde crates
    scope: src/input.rs, src/input/limits.rs, src/input/toml_text.rs, and the input and input-toml features in Cargo.toml
    outcome: A consumer that reads only TOML config compiles no YAML parser and no garde.
    blocked_by: []
    acceptance:
      - With default features off and only input-toml enabled, cargo tree for ctl-core lists none of serde-saphyr, yamled, or garde.
      - Input::check and Input::load exist only with the validate feature, and the YAML format, YAML limits, and REPO_FILE only with a YAML feature.
      - qctl, verctl, and forkctl build unchanged on the release that ships it, and cargo semver-checks reports the change at the version the changeset declares.
      - A TOML text is held to its own byte, depth, and node limits, with defaults ctl-core states; the call and process tiers apply without YAML, and the repository tier applies when a YAML feature reads REPO_FILE.
    notes:
      - Filed 2026-10-17 from infer-lab INF-074, which moves infer-machine onto ctl-core App and input-toml and reads only TOML. It takes serde-saphyr, yamled, and garde for now.
      - 'Why it is not a small split: Limits is defined as overrides of serde-saphyr Budget, the TOML path reads max_depth and max_nodes from that Budget, the repository limits file .ctl/input.yaml is parsed as YAML, and check and load are garde.'
horizon:
  - id: CTC-024
    title: Remove the doc(hidden) semver-checks override
    scope: ctl-core
    outcome: Cargo.toml has no package.metadata.cargo-semver-checks.lints table.
    kind: deferred
    open: A ctl-core release with the hidden crate-root re-exports is on crates.io, so cargo-semver-checks compares against it.

archive:
  - id: CTC-019
    title: Take template v0.0.4 and its task layout
    scope: .copier-answers.yml at templates v0.0.4, mise.toml [task_config], mise-tasks/, lefthook.yml, and the AGENTS.md template and checks sections
    outcome: ctl-core runs the template's v0.0.4 hooks and keeps its own tasks in mise-tasks/, listed first in [task_config].includes
    completed: 2026-10-12T02:10:00
    evidence:
      - 'Merged in https://github.com/victor-software-house/ctl-core/pull/104 (0cb5549): on a fresh clone mise install --locked passed, a commit ran pre-commit and commit-msg, mise run verify (check:features as the file task mise-tasks/check/features) and pre-push passed; CI Verify and Template passed.'
    disposition: completed

  - id: CTC-018
    title: Take the shared Rust configuration from the Copier template
    scope: .copier-answers.yml at templates v0.0.3, the repository setup the template renders, the template mise env, the CI job that checks for a newer tag, and the retirement of the baseline generator
    outcome: ctl-core renders its repository setup and Rust configuration from the Copier template at a recorded tag, CI fails when the template has a newer tag, and the baseline generator is gone
    completed: 2026-10-12T01:30:00
    evidence:
      - 'Merged in ctl-core #101 (91a70d4) and released in 0.6.15 (tag v0.6.15) as a patch by operator decision; CI semver allowed only the baseline removal against 0.6.14, then the window was deleted'
      - 'On a fresh build runner: mise install armed the commit-msg, pre-commit, and pre-push hooks; MISE_ENV=ci installed none; mise run verify passed; the Template job passed through the composite action with the vsh-templates-reader App, and yamled #22 (a10bae3) uses the action at 91a70d4'
    disposition: completed

  - id: CTC-015
    title: Serve the Rust baseline from ctl-core
    scope: Embedded templates for the shared Rust config files and blocks, the .ctl/rust.yaml schema, render, and the self-check
    outcome: Each ctl repository renders its Rust configuration from the ctl-core release it links, and a drifted copy fails verify through the runner
    notes:
      - "Runner: verctl, as a post-0.4.0 patch (VER-045). Pending operator decision: join verctl's served-file block, or a separate ver baseline subcommand."
    completed: 2026-10-12T01:00:00
    evidence:
      - 'Retired by operator decision: the shared Copier template (project rust-crate) renders the same files, so ctl-core keeps one generator. CTC-018 removes the baseline feature, templates/baseline, tests/baseline.rs, .ctl/rust.yaml, and the served-baseline spec.'
    disposition: dropped

  - id: CTC-017
    title: Repository checks in the shared verify
    scope: tasks/rust.toml verify depends on a repository-defined check:repo; ctl-core defines its own; yamled replaces its verify override
    outcome: A repository adds its own checks to verify through check:repo, without copying the shared dependency list
    completed: 2026-10-11T23:20:00
    evidence:
      - 'Merged in ctl-core #98 (6b7eaa7) and released in 0.6.14 (tag v0.6.14); a repository without check:repo fails verify with task not found: check:repo'
      - 'yamled #21 (377bbb3) includes v0.6.14 and defines check:repo; mise run verify passed on a fresh build runner without system node and in CI'
    disposition: completed

  - id: CTC-016
    title: Serve the standard Rust tasks as a mise remote task include
    scope: 'tasks/rust.toml with check:toolchain, format, lint, test, test:doc, deny, deny:advisories, deps, semver, and verify; ctl-core reads it locally, other repositories by git:: include at a release tag'
    outcome: Every ctl repository runs the same Rust tasks from one file at a pinned ctl-core release, so a task fix reaches them by bumping the ref
    completed: 2026-10-11T22:50:00
    evidence:
      - 'Merged in ctl-core #95 (1f87bdb) and released in 0.6.13 (tag v0.6.13)'
      - 'yamled #19 (e9653ea) includes tasks/rust.toml at v0.6.13; mise run verify passed on a fresh build runner, in its pre-push hook, and in CI'
    disposition: completed

  - id: CTC-014
    title: Bring ctl-core to the pinned-stable Rust baseline
    scope: Toolchain pins, lints, cargo-deny policy, semver check, and verify tasks of the rust-projects baseline
    outcome: ctl-core builds, lints, and tests on pinned stable 1.99.0, formats on a dated nightly only, and its verify and CI match the shared baseline that later repos will render from ctl-core
    completed: 2026-10-11T22:17:00
    evidence:
      - 'Merged in ctl-core #90 (02941ff): pinned stable 1.99.0, nightly rustfmt only, rust-version checked by check:toolchain, test:doc, missing_debug_implementations, deny workspace-dependencies, semver in CI'
      - 'check:toolchain hardened in #94 (85c4e72): pipefail and jq error() name each drifting package'
      - mise run verify passed on build runners on every pushed commit; released in 0.6.12
    disposition: completed

  - id: CTC-013
    title: Let the environment override each style option
    scope: presentation
    outcome: An operator picks the record style, list style, row separation, and JSON layout per shell without a rebuild; the library defaults and overrides stay as they are.
    links:
      - https://github.com/victor-software-house/ctl-core/tree/main/openspec/changes/choose-visual-identity
    completed: 2026-09-23T20:37:11
    evidence:
      - https://github.com/victor-software-house/ctl-core/pull/65
    disposition: completed

  - id: CTC-010
    title: Own the unified ctl presentation kernel
    scope: ctl-core and all ctl consumers
    outcome: One typed Rust chassis in ctl-core renders pretty, colorless, JSON, help, notices, and errors through composable semantic primitives; consumer CLIs own domain data and import no terminal renderer.
    completed: 2026-09-23T20:19:00
    evidence:
      - https://github.com/victor-software-house/ctl-core/pull/63
    disposition: completed

  - id: CTC-003
    title: Optional per-user daemon sharing one session and view
    scope: ctl-core
    outcome: Multiple ctl clients could attach to one local per-user session and share one view.
    notes:
      - Same idea as QCTL-007. The one implementation now lives in the unixd repository.
    completed: 2026-09-23T16:06:51
    evidence:
      - 'Moved out of ctl-core: the daemon and cache now live in their own public repository, github.com/victor-software-house/unixd, as the unixd and unixd-cache crates. No ctl CLI needs them.'
    disposition: dropped

  - id: CTC-012
    title: Isolate Surface model dependencies
    scope: ctl-core
    outcome: Consumers can extract, validate, or serialize a Clap Surface without MiniJinja, while the existing surface feature remains a compatible aggregate for template rendering.
    completed: 2026-09-12T23:33:43
    evidence:
      - https://github.com/victor-software-house/ctl-core/pull/42
      - 'ctl-core 8e58d61: surface-model excludes MiniJinja, surface-serde serializes without it, surface-templates adds it, and surface remains compatible; focused tier tests, cargo metadata/tree proofs, 119-test full verify, ledger check, and CI pass.'
    disposition: completed

  - id: CTC-011
    title: Reserve a safe terminal-width margin in nested shells
    scope: ctl-core renderer
    outcome: Automatically detected terminal widths reserve one column before wrapping and table layout, so nested terminal shells do not clip or break the right edge; explicit render widths remain exact.
    completed: 2026-09-12T18:01:15
    evidence:
      - https://github.com/victor-software-house/ctl-core/pull/39
      - 'ctl-core 7fba205 on combined head 33a2e66: configurable width buffer and full-control overrides coexist with the short-option surface guard; range-diff is exact and mise run verify passed on the build host.'
    disposition: completed

  - id: CTC-009
    title: Prove this repo's release lane by shipping one release through it
    scope: ctl-core
    outcome: >-
      The next ctl-core version comes out of a Version PR, so the lane is
      known to work here rather than assumed to because it works elsewhere.
    notes:
      - '2026-08-18, checked rather than assumed. The lane is wired and green — Version PR ran twice on main today — but it has never carried a release: there is no changeset, so every run no-ops. v0.0.1 through v0.0.3 were cut by hand, which the history shows plainly, because CHANGELOG.md was last touched by a feature commit (f8cfdf6) and no "version packages" commit exists at all. `[prepare]` arrived after that in cf11ac2 (#6) and has therefore never run. This matters because ctl-core is about to become the home of the declared-input layer (CTC-008), and verctl will pin it — a release path nobody has walked is a bad thing to discover while migrating two other repos onto it. The pin here is still verctl 0.0.3, so this repo has neither served files nor pins-on-the-Version-PR yet.'
    completed: 2026-08-22T15:18:10
    evidence:
      - 'ctl-core#12 carried the changeset. #13 was the Version PR: 0.0.3 to 0.0.4, CHANGELOG written, Cargo.lock staged by cargo update --workspace.'
      - 'First Publish failed: origin/main missing on a shallow checkout. #14 added fetch-depth: 0. Dispatch then published.'
      - tag v0.0.4 is at 5b02305, the same SHA as GitHub Release target_commitish. Cargo.lock in that tree is 0.0.4. crates.io has 0.0.4.
    disposition: completed

  - id: CTC-007
    title: Version PR uses the released verctl tarball
    scope: ci
    completed: 2026-08-17T05:59:57
    disposition: completed
    outcome: >-
      Version PR execs github:verctl from PATH. No cargo install --git.
      mise envs match verctl: rust in dev, tarball in release.
    evidence:
      - github.com/victor-software-house/ctl-core/pull/3
      - MISE_ENV=release on Version PR; mise install --locked
      - MISE_ENV=dev,release on Publish

  - id: CTC-005
    title: ColorMode tables for command pretty output
    scope: ctl-core
    completed: 2026-08-17T02:21:47
    disposition: completed
    outcome: >-
      kv/grid take ColorMode. never emits no ANSI. View.show
      passes the view color. Consumers do not strip ANSI.
    evidence:
      - crates.io ctl-core 0.0.3
      - src/table.rs; tests use ColorMode::Never

  - id: CTC-002
    title: Publish ctl-core to crates.io
    scope: ctl-core
    completed: 2026-08-17T01:23:25
    disposition: completed
    outcome: >-
      Consumers pin a crates.io version instead of a git rev.
    evidence:
      - crates.io ctl-core 0.0.3
      - github.com/victor-software-house/ctl-core/releases/tag/v0.0.3
      - mise github:victor-software-house/verctl@0.0.1 for publish

  - id: CTC-004
    title: Harden ctl-core tests before any consumer migration
    scope: ctl-core
    completed: 2026-08-17T00:17:36
    disposition: completed
    outcome: >-
      Public API is covered harder than forkctl+qctl+verctl chassis
      tests. No extra clap/help copies land until this is green.
    evidence:
      - github.com/victor-software-house/ctl-core main fdf6e67
      - tests/{color_argv,format_tokens,flags_cli,flag_warnings,help_render,view,parse}
      - peek --color --no-color; first-party concat! still fails clippy
      - 'chassis last-wins + {bin}: warning: on clashes'
    notes:
      - Do not pad empty tests to out-count forkctl+qctl+verctl. Named public-path coverage is the bar.

  - id: CTC-001
    title: Shared clap chassis, views, and formatdoc
    scope: ctl-core
    completed: 2026-08-16T23:13:19
    disposition: completed
    outcome: >-
      ctl-core owns flatten flags, styled help, Envelope/View, and
      formatdoc. Not a command.
    evidence:
      - github.com/victor-software-house/ctl-core main 02c7e9f
      - prelude, Render, FormatArgs/ColorLong, cargo features
      - verctl already consumes it