schema_version: 3
prefix: VER
style:
timezone: "-03:00"
section_order: [queue, horizon, archive]
active: VER-025
queue:
- id: VER-025
title: Declarations live in .ctl/ver.yaml
scope: verctl
outcome: >-
A repo has one `.ctl` directory that every ctl CLI shares, and verctl's
declarations are one YAML file inside it. `verctl.toml` is gone, not
tolerated.
blocked_by: []
acceptance:
- "`.ctl/ver.yaml` holds every section verctl.toml holds today: drivers, packages, publishers, runners, assets, ci, prepare, templates, patterns, pins."
- "Served-file templates move to `.ctl/templates/`, shared rather than namespaced, and each still declares its own target."
- "Cross-section rules keep working, so the files are merged into one document before garde runs: `every_pattern_is_listed` still checks patterns against pins, and a job or asset target naming an undeclared runner still fails."
- YAML, not TOML, so every file a repo writes for a ctl CLI is the same language as tasks.yaml.
- "`verctl.toml` is not read at all: no fallback, no deprecation window, no both-are-accepted state."
- Every known consumer moves in the same slice — this repo, qctl, and ctl-core — and the old file is deleted from each.
- Loading is still one boundary, so a complaint names its file and field the way it does today.
notes: >-
2026-08-18, decided by Victor and recorded late — this row exists
because the decision lived in a conversation instead of the repo, which
is the failure the cold-start rule is about. His words, twice: first
"keep the damn config in there, .verctl/config.toml etc etc … anything
more than that, then we should use a damn directory already", then, on
seeing this written up as an open question, "we should just fucking drop
fucking verctl.toml and keep shit under .verctl/{config, etc etc}.yaml".
The second supersedes the first on two points: the format is YAML, and
`verctl.toml` is dropped rather than still recognized. A per-concern
split, YAML only, with both consumers migrated, is what was agreed at
the time.
This is a break to a contract we own, so the rule applies: inventory
every producer and consumer, migrate them in the same slice, remove the
old path. VER-021 declares this its blocker rather than saying so in
prose, because a schema written against verctl.toml is written twice.
VER-024 is the smaller version of the same problem and can ride along.
2026-08-19, from qctl QCTL-019: this slice picks a name two tools have to
live with, so pick it on purpose. qctl will need project config of its own
— a registry naming the ledgers a repo reads — and if this lands `.verctl/`
while that invents `.qctl/`, two tools that ship together have two
conventions and nothing written down about why.
Decided by Victor the same day, and it supersedes this row's original
shape: one `.ctl/` directory, one file per tool, and the file name drops
the ctl — `.ctl/ver.yaml`, `.ctl/q.yaml`, `.ctl/fork.yaml`. The names match
the tasks a consumer already runs (`mise run ver`, `mise run q`,
`mise run fork`), so a file is named after the verb the operator types
rather than after the crate that reads it, and ctl-core has one place to
look when it becomes the shared layer. `.verctl/` is not the target.
What is superseded, explicitly: one YAML file per concern. The sections
are already named inside the document, so a directory of ten files split
what a reader opens without splitting what they have to understand. If it
ever does grow, the loader can accept a directory beside the file without
a second migration — that is a follow-up, not this slice. Templates keep a
directory of their own at `.ctl/templates/`, shared rather than
namespaced, because a template already declares its own target and nothing
in one is verctl's except who renders it.
One constraint from that side, so the shape is not chosen blind: a
committed file can only carry what is true on every machine. qctl's
registry splits accordingly — prefixes in the repo, paths on the machine —
and any `.ctl/` convention should expect the same split rather than assume
everything a tool declares is committable.
- id: VER-011
title: Finish the Changesets-shaped release loop
scope: verctl
outcome: >-
Fragment in, Version PR, exact-SHA publish, per-package CHANGELOG,
lockfile, already-published, optional natives. No hand-edited
versions.
blocked_by: []
acceptance:
- prepare writes versions + per-package CHANGELOG and consumes fragments.
- Version PR is the only version-authority path (CI / --pr).
- "`[prepare].after` plus stage globs; unexpected dirty files fail."
- publish is overridable recipes; cargo and bun are stock examples.
- Lockfile is committed; cargo publish --locked works after bump.
- Pretty is ctl-core ColorMode tables; tests use --color never.
- Native assets only when [assets] is set; PR CI stays one runner.
- Asset jobs are named by target id; cargo/bun inherit stdio.
notes: >-
Original product goal. Do not name the private release-skill in
repo docs. Rust and bun are stock examples, not the architecture.
Adopt ctl-core fixture trees (CTC-006) instead of more fs::write
+ indoc copies. 2026-08-17: status/check/prepare are ColorMode
tables; publish proves changelog headings + default-branch
history (no commit-subject sniff); GitHub notes come from each
package CHANGELOG. Hand-edit guard shipped as VER-012 / PR #9.
Still open for archive: lockfile-after-bump (VER-013).
- id: VER-015
title: Asset runners are configuration, not a hardcoded map
scope: verctl
outcome: >-
Each [assets] target names the runner that builds it. The plan
emits what the repo declared, so a public repo stays on free
hosted minutes and a private one can point at its own labels.
blocked_by: []
acceptance:
- A machine is declared once as [runners.NAME] with labels; [ci.NAME] names several, [assets.NAME] exactly one.
- verctl ci and verctl assets print what resolved and the matrix carries it.
- A public repo needs no config to keep macos-latest / ubuntu-latest, and no [ci] to keep one verify job.
- A declared self-hosted or nscloud label set reaches runs-on unchanged, in order.
- An undeclared machine name fails listing the declared ones; a label is never accepted as a name.
- Tests cover default, override, multi-label, fan-out naming, empty list, and a partial undeclared target.
notes: >-
2026-08-17: found while adopting the lane in qctl (qctl#3). Two
claims first written here were wrong and are corrected: a per-target
`runner` override already existed, and a job can be told its runner
by an earlier job. The real defects are that `runner` is one string
where GitHub takes a label set, that CI has no configuration surface
at all, and that an unknown id with a partial record is accepted and
reaches `cargo build --target ""`. Shape settled after several wrong
turns, recorded so they are not retried: anonymous list rows needing
an `id` field (`[[ci.jobs]]`, `runners = [{ labels = [...] }]`) were
rejected as ceremony — names are table headers instead; a `runners`
list on an asset target was rejected because two machines would
overwrite one tarball, which the shape now makes unsayable; and one
identical shape per phase was abandoned once it put `os`/`arch` on a
job while its labels sat a level below. A name→labels registry is
deliberately not the alias table rejected earlier: an alias made an
invented word look like a resolved label, whereas a machine name
resolves against `[runners]` or fails and never reaches GitHub.
M1 (generating ci.yml) and M3 (a repo variable) were considered
and rejected for M2, the plan-job bootstrap the publish lane already
uses; M3 would move the value out of verctl.toml entirely.
Asked why `plan` is not configurable, the inventory settled the law:
verctl decides how many jobs there are, the workflow file decides
where a fixed job runs. Unary jobs (`plan`, `crate`, `pin`,
`prepare`) all have literal machines; only `verify` and `assets`,
whose count the repo decides, read config. A static YAML file cannot
declare N jobs without knowing N — that fan-out is what config buys,
and the machine rides along on it. So `[release]` is dropped, not
deferred: publish and pin are inherently unary, nothing decides their
count, and a self-hosted-only repo is already served by editing three
literals in workflows it owns. `verctl check --ci` was rejected with
it — verifying those literals needs a YAML dependency this crate does
not have, to check a value the law puts there on purpose.
- id: VER-020
title: check validates served files, and one file has one owner
scope: verctl
outcome: >-
A served file edited by hand, or claimed by both a template and a
pin, fails on the PR that does it instead of at release time.
blocked_by: []
acceptance:
- "`check` runs the same validation prepare does: every pattern still matches at its declared arity."
- "`check` compares every served file to what its template renders at the versions on HEAD."
- A file that a template renders and a [[pins]] entry also names fails before anything is written.
- The failure names the file and which two declarations claim it.
notes: >-
2026-08-18: docs/served-files.md §6 describes check as designed, not
implemented — only prepare and its dry run validate today, so drift
introduced by hand survives until a release. The double-owner gap is
real and silent: prepare_report rewrites pins first and renders
templates second, so a template would clobber the rewrite without
saying so.
- id: VER-021
title: Every config file we own has a JSON Schema, at a URL an IDE can fetch
scope: verctl
outcome: >-
Editing a repo's declarations, tasks.yaml, or a changeset fragment in an
IDE completes the keys and marks a wrong one, in every ctl repo, with
no editor-local setup.
blocked_by: [VER-025]
acceptance:
- The schemas are generated from the Rust types, so a key that exists in code exists in the schema and no other.
- "Every declaration file a repo writes carries `# yaml-language-server: $schema=<url>`, and so do tasks.yaml and changeset fragments."
- The URL is versioned, and a repo's own schema line is kept current by a [patterns] pin, not by hand.
- "A generated schema that drifts from the types fails a check, the same way a served file does."
- qctl and ctl-core carry the same lines for the same files.
notes: >-
2026-08-18, decided with Victor: free and already versioned wins over
a prettier host, so this follows the pattern that already exists
rather than inventing one. Evidence: qctl ships schema/tasks.schema.json
as a release asset, ctl-core's tasks.yaml already points $schema at
`releases/download/v0.0.1/tasks.schema.json`, and qctl's own ledger
already decided the URL is a pinned tag and never /latest — a schema
that changes under an unchanged file is worse than an old one. What is
missing is the rest: verctl.toml and changeset fragments have no
schema at all, qctl's is hand-written JSON rather than generated from
the types, and every pinned URL is bumped by hand. The staleness
objection is what VER-018 just answered: a schema URL is a version
site like any other, so it becomes a [patterns] entry and prepare
keeps it current on the Version PR. mise ships its own schema and we
only reference it; template declarations are not a file an editor
validates by URL.
2026-08-18, blocker found by running it: every qctl mutation deletes
the schema line it is supposed to protect. `qctl start` on a copy of
this file rewrote it through serde_yaml and dropped every comment,
line 1 included, so `# yaml-language-server: $schema=…` survives only
until the next `q start` or `q archive`. tasks.yaml cannot carry a
schema pointer until qctl writes comments back. The same applies to
every comment here: none of them survive a verb, which is why an
archived blocker is recorded in notes rather than beside the key.
- id: VER-022
title: Move the reusable schema layer into ctl-core
scope: verctl
outcome: >-
Every ctl CLI parses and validates declared input the same way,
from one implementation, instead of three that drift.
blocked_by: [VER-025]
acceptance:
- "The shared validators (`inside_the_repo`, `one_file_name`, `at_least_one`, `cannot_be_empty`) live in ctl-core and verctl uses them from there."
- "Config loading — read, parse with serde, validate once with garde against the whole document — is one ctl-core entry point."
- "The template renderer's declare-your-own-target mechanism is available to any ctl CLI, not just verctl."
- No verctl-specific vocabulary leaks into ctl-core, and no copy of the layer is left behind here.
notes: >-
2026-08-18: src/schema.rs is already shared between verctl's TOML
config and its Jinja template declarations, which is the proof it is
not verctl-shaped. ctl-core carries the matching row, CTC-008,
merged 2026-08-18 as ctl-core#7, so the destination is agreed
before anything moves.
- id: VER-024
title: Delete the retired [assets].targets shim
scope: verctl
outcome: >-
A config key that no longer exists is not still half-parsed. The
migration message has done its job and goes with it.
blocked_by: []
acceptance:
- "`Assets::retired_targets` and the bail in assets.rs are gone, along with the test that asserts the message."
- Both consumers are checked first — qctl and ctl-core name no `[assets].targets` and their verctl pin is at or past the release that replaced it.
- "A repo that still writes the old key gets serde's unknown-field error, which is the correct answer once nothing migrates."
notes: >-
2026-08-18: written down because it was not. The shim exists only so a
repo still on `targets = [...]` reads the migration instead of a serde
type error, and its own doc comment says "remove once qctl and ctl-core
have bumped their verctl pin" — a removal condition with no row to
enforce it, which is how compatibility code becomes permanent. Verify
the condition rather than assume it: grep both consumers for the key
and read their pins.
- id: VER-026
title: An untracked template in the source tree stops the release
scope: verctl
outcome: >-
A template a repo committed to nothing renders nothing and says so.
Adding a served file and getting silence is not a state a release
passes through.
blocked_by: []
acceptance:
- "A `*.jinja` inside `[templates].source` that git does not track fails the run, naming the file: commit it or delete it."
- "`only_a_tracked_template_in_the_source_tree_is_rendered` keeps its second half — a template outside the source tree is still ignored without a word — and its first half asserts the error instead of an empty plan."
- "§5's claim that exactly one thing is silently skipped reads true again: an unbumped `[[pins]]` package, and nothing else."
- "§1's \"an untracked template is scratch work and renders nowhere\" is gone, replaced by what it does instead: fail the run, naming the file. The clause beside it — a template outside the source tree is not verctl's to render, which is what keeps this crate's changelog templates out — is already true and stays."
notes: >-
2026-08-19, found in qctl while adopting served files: `prepare
--dry-run` listed the one `[[pins]]` file and neither of the two
templates, with no diagnostic, because they were written but not yet
staged. Diagnosed by reading `templates::sources`, which filters
`git::tracked` — and the current test pins that silence as intended,
asserting an empty plan and an unchanged served file.
Refusing to render is right: `prepare` auto-stages rendered outputs, so
a template git does not carry would put a served file on the tag whose
source is not in the tree, and `check` would have nothing to compare on
a fresh clone. What is wrong is what happens next — the release
continues and serves the stale hand-authored file, which is the exact
failure served files exist to end. It fails open, once per repo, at the
moment somebody is learning the feature.
- id: VER-027
title: The served task runs the verctl its own pin names
scope: verctl
outcome: >-
`mise run ver` runs the version `tasks/ver/ver` pins, not whichever
verctl the machine happens to hold.
blocked_by: []
acceptance:
- "`tasks/ver/ver` becomes a template under `.verctl/templates/` and execs `verctl` in the same change — the pin has to be rendered before anything trusts it. Today the file says 0.0.1 while the crate is at 0.0.4, and only `mise where` is hiding that, so removing it alone would freeze `mise run ver` on 0.0.1."
- "Proven the way qctl proved it: one machine with two versions installed, one task pinned to the older, and the task runs the older."
- "No release ships one half: a tag whose `tasks/ver/ver` execs `verctl` also carries the version that tag names."
notes: >-
2026-08-19, measured in qctl: `mise where <tool>` resolves a version
from the surrounding config, so a task-scoped `#MISE tools` pin
guarantees the install and never the selection. With 0.0.1, 0.1.0 and
0.1.1 installed and one task pinned to 0.0.1, `exec qctl "$@"` ran
0.0.1 and `exec "$(mise where …)/qctl" "$@"` ran 0.1.1 — mise already
puts the task's pinned tool first on PATH. Found by running `mise run q
-- status` in this repo, whose include is pinned to qctl v0.0.1 and
which still got the schema-3 complaint that only 0.1.1 can produce.
`tasks/fork/fork` in forkctl carries the same line and the same bug.
That repo has no ledger, so this row is where it is written down until
it does.
- id: VER-028
title: Decide who owns a template in the shared .ctl/templates
scope: verctl
outcome: Two ctl CLIs can both render served files from one repo without either rendering the other's templates.
blocked_by: []
acceptance:
- The rule is decided and written where the loader enforces it, not in a PR comment.
- A template belonging to another ctl CLI is either skipped or never in verctl's source tree, and a test proves which.
- docs/served-files.md §1 says the rule, and VER-025's shared-not-namespaced line is either upheld or superseded in writing.
notes: >-
2026-08-19, raised on PR #44 and deliberately not acted on there.
`templates.source` defaults to `.ctl/templates`, verctl renders every
tracked `*.jinja` under it, and `.ctl/` is shared — so once a second ctl
CLI serves files, verctl would render its templates too: an unknown name
fails the whole Version PR, and one that only reads `versions[…]` gets
written to the other tool's target. Nothing collides today, because no
other ctl CLI renders anything, which is why this is a row and not a fix.
Scoping the directory to `.ctl/templates/ver/` is the obvious answer and
is exactly what VER-025 ruled out ("shared rather than namespaced,
because a template already declares its own target"), so it is Victor's
to reverse, not this row's to assume. The other shape is a template
declaring its owning tool, which keeps the directory shared and pays for
it with one more export.
horizon:
- id: VER-019
title: Triage the review backlog from the closed PRs
scope: verctl
kind: evaluation
outcome: >-
Every review finding on already-merged code is either a named
defect with a fix, or written off with the reason.
open: >-
2026-08-18: nine findings were raised on PRs #4, #6, #9, #12, #13,
#14 and never triaged, because those PRs were closed with the
comments unread. Listed so they stop being invisible; none is
confirmed except where noted. Security-shaped first.
(1) #12: CARGO_REGISTRY_TOKEN is exported into the same step that
compiles this repo's source, so a build script sees the crates.io
token. Same trust boundary as publishing at all, but publishing
from the released binary instead of a fresh build would remove it —
see VER-016, which wants that step off the compiler anyway.
(2) #12: `${{ matrix.id }}` and `${{ needs.crate.outputs.tag }}`
are interpolated straight into `run:` bodies. The values come from
verctl.toml on the default branch, not from a fork, so this needs
write access to exploit; passing them through `env:` costs nothing.
(3) #9: the version guard runs on a shallow checkout in some lanes,
where `check --versions` cannot reach the merge base.
(4) #6: `consume_fragments` runs only in the `--pr` branch, so a
local `prepare` leaves the fragments it already applied.
(5) #6: publish forces `--access public` for bun packages.
(6) #6: no npm auth is set up for the bun publisher.
(7) #6: `tag_for` falls back to the first package's version, which
is wrong for a repo whose packages version independently.
(8) #4: HEAD is not restored after the Version PR branch write, and
`add_all(["*"])` stages more than the plan named.
(9) #14: `recurse_untracked_dirs(true)` walks into large ignored
trees. Retired 2026-08-18: #13's invalid `runner` context in a
workflow-level `env` is gone — no workflow or example uses the
`runner` context, and actionlint 1.7.12 passes every file.
- id: VER-016
title: The plan job should not install a compiler to parse TOML
scope: verctl
kind: evaluation
outcome: >-
The ci.yml bootstrap costs what reading verctl.toml costs, and
verify stops waiting on a toolchain it does not share.
open: >-
Measured on run 32078158505: plan took 33s, of which 21s was mise
plus rust-cache, and verify did not start until 37s in. It needs
verctl and verctl.toml, not cargo. The tarball is the fix and it
is gated, not undecided: `git show v0.0.2:src/ci.rs` does not
resolve, so the published binary has no `ci` verb and
MISE_ENV=release cannot plan until a release carries VER-015.
Flip the plan job then and delete its toolchain steps. Rejected
meanwhile: a stable-rust plan job, which trades a second
toolchain and a cold cache for ~13s; and one rust-cache key
across plan and verify, since rust-cache saves only when the key
is absent, so plan's dev-binary target dir would land first and
leave verify a thinner cache than its own. Efficiency only — the
runner contract itself is settled (VER-015).
- id: VER-013
title: Stock lockfile refresh after cargo bumps
scope: verctl
kind: evaluation
outcome: >-
After a cargo version write, Cargo.lock is regenerated so
publish --locked does not fail.
open: >-
Decide whether this is default after for the cargo driver or a
[prepare].after the repo opts into. 2026-08-18: qctl 0.0.2 failed
on exactly this and is evidence for the default. The cargo driver
already names `cargo generate-lockfile`, but `apply_plan` returns
it as a follow-up that only prints in the `next` column, so every
cargo consumer has to restate it as `[prepare].after` plus a
`Cargo.lock` stage glob — and a repo that does not know to is one
Version PR away from a dirty-tree `cargo package --locked`.
Running the driver's own after is the fix; the opt-in escape is
then a `[prepare]` that overrides it. The command itself is settled
and no longer part of the question: measured on a scratch copy with
the manifest bumped to 0.0.9, `cargo generate-lockfile` printed
`Locking 138 packages` and `Adding serde_yml v0.0.12` for a 3-line
diff, while `cargo update --workspace` printed `Locking 1 package`
for a 1-line diff. The driver, this repo, qctl, and ctl-core all
name the narrow one now.
- id: VER-014
title: First-party composite Action packages
scope: verctl
kind: evaluation
outcome: >-
action.yml packages get floating major tags without npm.
open: Need a first-party Action that is not this repo's own actions/.
- id: VER-010
title: Stock JS publish for npm, pnpm, and yarn
scope: verctl
kind: evaluation
outcome: >-
package.json still publishes through one table of argv. Bun stays
the default. npm/pnpm/yarn are extra rows, not new verbs or
Action commands.
open: >-
Need a consumer that is not Bun. Infer from packageManager /
lockfile the same way detect.rs already does for install. Action
stays `verctl publish`.
- id: VER-008
title: Generic regex version writer
scope: verctl
kind: evaluation
outcome: Packages that are not Cargo or package.json can still bump via a declared pattern.
open: Need a real third manifest (not a hypothetical) before adding a regex writer.
- id: VER-006
title: Conventional Commits synthesize the same fragments
scope: verctl
kind: deferred
outcome: An opt-in mode writes .changeset/*.md from commit types.
open: Default stays off. Fragments remain the human input.
- id: VER-007
title: OIDC publish backends for cargo and bun
scope: verctl
kind: deferred
outcome: Public npm uses trusted publishing, not NPM_TOKEN.
open: bun publish --tolerate-republish already ships. OIDC is the remaining auth path.
- id: VER-023
title: Is this how a modern Rust CLI is built?
scope: verctl
kind: evaluation
outcome: >-
The crate layout, error handling, test structure, and fixture
approach are what current Rust practice recommends — or we know
exactly where we differ and why we keep it.
open: >-
2026-08-18, raised by Victor: nothing here is unmanageable, but the
pattern was never checked against the field. Questions worth
researching rather than guessing: flat `src/*.rs` modules versus
grouped ones as the crate keeps growing; anyhow everywhere versus a
typed error at the library boundary with anyhow only in main; unit
tests in-file versus `tests/` (both are used here, with no stated
rule); hand-written table tests versus a fixture or snapshot crate;
and how much of the answer belongs in ctl-core so all three CLIs
share it (VER-022). Deliverable is a short written standard in
ctl-core plus a list of what to change here, not a rewrite.
archive:
- id: VER-018
title: A repo declares the files it serves instead of scripting them
scope: verctl
completed: "2026-08-18T19:50:03"
disposition: completed
outcome: >-
Anything a release leaves stale is rewritten by declaration,
whatever file it lives in, without verctl learning that file's
format.
evidence:
- "github.com/victor-software-house/verctl/pull/31"
- "37a59ed on main"
- "docs/served-files.md is the contract; AGENTS.md carries the schema standard"
- "templates declare path / name / executable as top-level Jinja exports, discovered through the git index"
- "patterns are named once as [patterns.<id>] and listed by every file that carries one, with a declared arity"
- "examples/verctl.toml exercises both mechanisms and is parsed by tests/examples.rs"
- "mutation runs: 8 of 10 pin, 8 of 9 config-schema, 2 of 2 git — the uncaught ones do not compile"
- "left to VER-020: check does not run the same validation, and no rule refuses a file two declarations both own"
- id: VER-017
title: Pins ride the Version PR, and publish pushes only the tag
scope: verctl
completed: "2026-08-18T19:50:03"
disposition: completed
outcome: >-
The released tree carries its own pins, so a consumer that fetches
by tag or ?ref= gets the version it asked for.
evidence:
- "github.com/victor-software-house/verctl/pull/28"
- "git show v0.0.4:examples/mise.toml names 0.0.4 in both the [tools] pin and the ?ref="
- "the Version PR lane runs MISE_ENV=dev, so 0.0.4 was prepared by the build that ships the mechanism"
- "publish pushed only the tag; v0.0.4 carries the two native tarballs"
- "mise.release.toml stays hand-bumped, as the bootstrap exception requires"
- id: VER-012
title: Block hand-edited versions on the default branch
scope: verctl
completed: "2026-08-17T05:59:57"
disposition: completed
outcome: >-
A pre-push or check fails when a declared manifest version
differs from origin/main outside version-packages.
evidence:
- "github.com/victor-software-house/verctl/pull/9"
- "474cace on main"
- "verctl check --versions plus mise run versions on pre-push"
- "merge-base of HEAD vs origin/HEAD, GITHUB_BASE_REF, main, master"
- "only version-packages exempt; CI does not skip; tests inject Skip"
- id: VER-005
title: "Publish native verctl so mise github: can install it"
scope: release
completed: "2026-08-17T02:21:38"
disposition: completed
outcome: >-
crates.io plus GitHub Release assets for darwin-arm64 and linux-x64.
evidence:
- "crates.io verctl 0.0.1"
- "github.com/victor-software-house/verctl/releases/tag/v0.0.1"
- "verctl_0.0.1_linux_x64.tar.gz and verctl_0.0.1_macos_arm64.tar.gz"
- "mise x github:victor-software-house/verctl@0.0.1 -- verctl --version"
- id: VER-009
title: Keep verctl on ctl-core after the chassis hardens
scope: verctl
completed: "2026-08-17T01:29:52"
disposition: completed
outcome: >-
verctl stays on ctl-core prelude/View/formatdoc. No local help
or color copy comes back.
evidence:
- "ctl-core = \"0.0.3\" on crates.io"
- "ColorMode tables; no path = \"../ctl-core\""
- id: VER-004
title: Open or update the Version PR
scope: verctl
completed: "2026-08-17T01:29:52"
disposition: completed
outcome: >-
One prepare-release PR consumes fragments, commits version files
and CHANGELOG, and deletes the consumed .changeset files.
evidence:
- "github.com/victor-software-house/verctl/pull/7"
- "crates.io verctl 0.0.1"
- "actions/version-pr and actions/publish"
- id: VER-003
title: Write Cargo.toml and package.json versions
scope: verctl
completed: "2026-08-16T20:11:59"
disposition: completed
outcome: >-
prepare --no-pr applies the max bump per declared package to
Cargo.toml and package.json.
evidence:
- verctl prepare --no-pr
- cargo/npm are stock Path drivers (format + keys + after)
- custom drivers via format+keys or read/write shell
- tests/bump.rs (including declared keys and shell driver)
- id: VER-002
title: Render changelogs with minijinja
scope: verctl
completed: "2026-08-16T19:48:01"
disposition: completed
outcome: Defaults are Jinja2 templates rendered by minijinja 2.x.
evidence:
- templates/changelog.jinja and dependency-changelog.jinja
- tests/changelog.rs
- id: VER-001
title: Parse Changesets YAML fragments (quoted and unquoted)
scope: verctl
completed: "2026-08-16T19:48:01"
disposition: completed
outcome: YAML fragments via gray_matter. status and check fail closed.
evidence:
- src/fragment.rs
- tests/fragment.rs and tests/status.rs