tp maturity

task-plus.yml declares the project's level on the manual's two maturity ladders: code flow 0–2 and deployment 0–4. maturity.code alone picks how tp release works (ADR 0006):

Code level Name A release is
0 Trunk a tag on the base branch, committed and pushed there
1 Batch stories, each carrying a changes/<n>.<kind>.md fragment. One story ships by its own pull request into the base branch, a lone release; two or more go through release, opened by tp release batch or by the second story's run, and ship by one release pull request (ADR 0007). The version follows from the fragments
2 Gated a batch, with CI on every pull request (#51)

A batch only holds if the base branch refuses direct pushes on the forge. tp maturity is the write side of the code ladder (#84): it declares the level and makes the forge agree.

Command Does Prints
tp maturity reads the declared levels and asks the forge about the base branch, the merge style and what a release in flight has left declared: code N, deploy M, then main on origin: protected or not protected, then merge commits on origin: the default or not the default, then at rest or in flight: …
tp maturity code N declares code level N (0, 1 or 2) in task-plus.yml once the repository is at rest; from 1, first protects the base branch on the release remote and makes merge commits its default merge style the level set, the branch's protection, the merge style, the token used
tp maturity code N -dry-run the same, writing nothing (dry-run) Would ... for the file and the API call

tp --init writes maturity: {code: 0} (#109), so a new project starts on trunk; a missing maturity.code is 0 too. At level 1 and up tp check errors on a hand-edited CHANGELOG [Unreleased] or a .tp-release-comment, which a batch does not read, and warns on the obsolete fragments: key at any level.

The base branch is the release remote's default branch (main, or master when there is no main). Protection means no direct pushes: on Forgejo a branch protection rule named for the branch with pushes disabled; on GitLab push access level 0 and merge access level 40 (maintainers). Pull requests still merge.

code N Release remote Token Base branch Then
1 or 2 Forgejo or GitLab yes not protected protected on the forge, confirmed, then the level written: main on origin: protected now
1 or 2 Forgejo or GitLab yes protected level written; already protected
1 or 2 Forgejo or GitLab yes either, merge commits not the default merge commits allowed and made the default (Forgejo allow_merge_commits, default_merge_style: merge; GitLab merge_method: merge), then the level written: merge commits on origin: the default now (#129)
1 or 2 Forgejo or GitLab yes merge style refused (403) error naming the right to grant; nothing written
1 or 2 Forgejo or GitLab yes forge answers 403 error naming the right to grant (repository admin); nothing written
1 or 2 Forgejo or GitLab none — error: set token_env on the release remote; nothing written
1 or 2 GitHub or none — — error: a batch needs a Forgejo or GitLab release remote; nothing written
0 any — protected level written; still protected; tp release refuses a trunk release until the protection is removed on the forge
0 any — not protected level written
same as declared — — — maturity.code: N (unchanged); the protection is still checked and made
3 or more, negative — — — error: the ladder is 0, 1 or 2

At rest

The level alone picks the release process, so it changes only when no release is in flight (#181, story 029): what the old process left behind would otherwise be stranded, and tp has no migration from one process to the other. tp maturity code N with N different from the declared level reads the in-flight state first and refuses with one error listing every item and its remedy; nothing is written and the forge is not touched. The same level is a repair, not a change, and runs with a release in flight. -dry-run gives the same refusal under the (dry-run) prefix.

In flight Listed as Clear it by
release on the release remote release exists on origin finishing the batch (tp release on main) or deleting the branch
.tp-release.yml in the checkout .tp-release.yml exists finishing the batch or deleting the file
an open pull request into release (a story's) pull request #12 (story/12-x → release) is open merging or closing it
an open pull request from release (the release's) pull request #13 (release → main) is open merging it and tagging, or closing it
an open pull request from a story/<n>-<slug> branch into main pull request #14 (story/14-y → main) is open merging or closing it

Any other open pull request does not count. Without a forge API or token (allowed when lowering to 0) the branch and the file are still read through git; the pull requests are not, and the output says pull requests on origin were not checked (no API). tp maturity prints the same items without their remedies, in flight: release on origin; pull request #12 (story/12-x → release), or at rest.

Merge commits are needed because a batch's stories are all cut from the same base: once the first is merged into release, the rest are no longer descendants of it, so a forge that only fast-forwards refuses them, and a rebase would rewrite the commits whose ancestry tp tracks. Fast-forward stays allowed. At level 1 and up tp check warns when merge commits are not the forge's default, naming tp maturity code N.

The protection is made before the level is written, so a forge that refuses leaves task-plus.yml as it was and the two never disagree. The token is the release remote's token_env (through the secrets provider when one is configured), or the default chain tp release uses. Deployment levels are declared by hand for now; tp check reporting each ladder is #50, and code level 2's CI check is #51.